Skip to content

Commit

Permalink
Merge pull request #86 from clue-labs/limit
Browse files Browse the repository at this point in the history
Add LimitingServer to limit and keep track of open connections
  • Loading branch information
WyriHaximus authored Apr 4, 2017
2 parents 84d4981 + fee5f36 commit 390c71b
Show file tree
Hide file tree
Showing 4 changed files with 482 additions and 11 deletions.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ and [`Stream`](https://github.com/reactphp/stream) components.
* [close()](#close)
* [Server](#server)
* [SecureServer](#secureserver)
* [LimitingServer](#limitingserver)
* [getConnections()](#getconnections)
* [ConnectionInterface](#connectioninterface)
* [getRemoteAddress()](#getremoteaddress)
* [getLocalAddress()](#getlocaladdress)
Expand Down Expand Up @@ -378,6 +380,84 @@ If you use a custom `ServerInterface` and its `connection` event does not
meet this requirement, the `SecureServer` will emit an `error` event and
then close the underlying connection.

### LimitingServer

The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible
for limiting and keeping track of open connections to this server instance.

Whenever the underlying server emits a `connection` event, it will check its
limits and then either
- keep track of this connection by adding it to the list of
open connections and then forward the `connection` event
- or reject (close) the connection when its limits are exceeded and will
forward an `error` event instead.

Whenever a connection closes, it will remove this connection from the list of
open connections.

```php
$server = new LimitingServer($server, 100);
$server->on('connection', function (ConnectionInterface $connection) {
$connection->write('hello there!' . PHP_EOL);
});
```

See also the [second example](examples) for more details.

You have to pass a maximum number of open connections to ensure
the server will automatically reject (close) connections once this limit
is exceeded. In this case, it will emit an `error` event to inform about
this and no `connection` event will be emitted.

```php
$server = new LimitingServer($server, 100);
$server->on('connection', function (ConnectionInterface $connection) {
$connection->write('hello there!' . PHP_EOL);
});
```

You MAY pass a `null` limit in order to put no limit on the number of
open connections and keep accepting new connection until you run out of
operating system resources (such as open file handles). This may be
useful it you do not want to take care of applying a limit but still want
to use the `getConnections()` method.

You can optionally configure the server to pause accepting new
connections once the connection limit is reached. In this case, it will
pause the underlying server and no longer process any new connections at
all, thus also no longer closing any excessive connections.
The underlying operating system is responsible for keeping a backlog of
pending connections until its limit is reached, at which point it will
start rejecting further connections.
Once the server is below the connection limit, it will continue consuming
connections from the backlog and will process any outstanding data on
each connection.
This mode may be useful for some protocols that are designed to wait for
a response message (such as HTTP), but may be less useful for other
protocols that demand immediate responses (such as a "welcome" message in
an interactive chat).

```php
$server = new LimitingServer($server, 100, true);
$server->on('connection', function (ConnectionInterface $connection) {
$connection->write('hello there!' . PHP_EOL);
});
```

#### getConnections()

The `getConnections(): ConnectionInterface[]` method can be used to
return an array with all currently active connections.

```php
foreach ($server->getConnection() as $connection) {
$connection->write('Hi!');
}
```

### ConnectionInterface

The `ConnectionInterface` is used to represent any incoming connection.
Expand Down
17 changes: 6 additions & 11 deletions examples/02-chat-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use React\Socket\Server;
use React\Socket\ConnectionInterface;
use React\Socket\SecureServer;
use React\Socket\LimitingServer;

require __DIR__ . '/../vendor/autoload.php';

Expand All @@ -29,17 +30,11 @@
));
}

$clients = array();

$server->on('connection', function (ConnectionInterface $client) use (&$clients) {
// keep a list of all connected clients
$clients []= $client;
$client->on('close', function() use ($client, &$clients) {
unset($clients[array_search($client, $clients)]);
});
$server = new LimitingServer($server, null);

$server->on('connection', function (ConnectionInterface $client) use ($server) {
// whenever a new message comes in
$client->on('data', function ($data) use ($client, &$clients) {
$client->on('data', function ($data) use ($client, $server) {
// remove any non-word characters (just for the demo)
$data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data));

Expand All @@ -50,8 +45,8 @@

// prefix with client IP and broadcast to all connected clients
$data = $client->getRemoteAddress() . ': ' . $data . PHP_EOL;
foreach ($clients as $client) {
$client->write($data);
foreach ($server->getConnections() as $connection) {
$connection->write($data);
}
});
});
Expand Down
201 changes: 201 additions & 0 deletions src/LimitingServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

namespace React\Socket;

use Evenement\EventEmitter;

/**
* The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible
* for limiting and keeping track of open connections to this server instance.
*
* Whenever the underlying server emits a `connection` event, it will check its
* limits and then either
* - keep track of this connection by adding it to the list of
* open connections and then forward the `connection` event
* - or reject (close) the connection when its limits are exceeded and will
* forward an `error` event instead.
*
* Whenever a connection closes, it will remove this connection from the list of
* open connections.
*
* ```php
* $server = new LimitingServer($server, 100);
* $server->on('connection', function (ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* See also the `ServerInterface` for more details.
*
* @see ServerInterface
* @see ConnectionInterface
*/
class LimitingServer extends EventEmitter implements ServerInterface
{
private $connections = array();
private $server;
private $limit;

private $pauseOnLimit = false;
private $autoPaused = false;
private $manuPaused = false;

/**
* Instantiates a new LimitingServer.
*
* You have to pass a maximum number of open connections to ensure
* the server will automatically reject (close) connections once this limit
* is exceeded. In this case, it will emit an `error` event to inform about
* this and no `connection` event will be emitted.
*
* ```php
* $server = new LimitingServer($server, 100);
* $server->on('connection', function (ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* You MAY pass a `null` limit in order to put no limit on the number of
* open connections and keep accepting new connection until you run out of
* operating system resources (such as open file handles). This may be
* useful it you do not want to take care of applying a limit but still want
* to use the `getConnections()` method.
*
* You can optionally configure the server to pause accepting new
* connections once the connection limit is reached. In this case, it will
* pause the underlying server and no longer process any new connections at
* all, thus also no longer closing any excessive connections.
* The underlying operating system is responsible for keeping a backlog of
* pending connections until its limit is reached, at which point it will
* start rejecting further connections.
* Once the server is below the connection limit, it will continue consuming
* connections from the backlog and will process any outstanding data on
* each connection.
* This mode may be useful for some protocols that are designed to wait for
* a response message (such as HTTP), but may be less useful for other
* protocols that demand immediate responses (such as a "welcome" message in
* an interactive chat).
*
* ```php
* $server = new LimitingServer($server, 100, true);
* $server->on('connection', function (ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* @param ServerInterface $server
* @param int|null $connectionLimit
* @param bool $pauseOnLimit
*/
public function __construct(ServerInterface $server, $connectionLimit, $pauseOnLimit = false)
{
$this->server = $server;
$this->limit = $connectionLimit;
if ($connectionLimit !== null) {
$this->pauseOnLimit = $pauseOnLimit;
}

$this->server->on('connection', array($this, 'handleConnection'));
$this->server->on('error', array($this, 'handleError'));
}

/**
* Returns an array with all currently active connections
*
* ```php
* foreach ($server->getConnection() as $connection) {
* $connection->write('Hi!');
* }
* ```
*
* @return ConnectionInterface[]
*/
public function getConnections()
{
return $this->connections;
}

public function getAddress()
{
return $this->server->getAddress();
}

public function pause()
{
if (!$this->manuPaused) {
$this->manuPaused = true;

if (!$this->autoPaused) {
$this->server->pause();
}
}
}

public function resume()
{
if ($this->manuPaused) {
$this->manuPaused = false;

if (!$this->autoPaused) {
$this->server->resume();
}
}
}

public function close()
{
$this->server->close();
}

/** @internal */
public function handleConnection(ConnectionInterface $connection)
{
// close connection if limit exceeded
if ($this->limit !== null && count($this->connections) >= $this->limit) {
$this->handleError(new \OverflowException('Connection closed because server reached connection limit'));
$connection->close();
return;
}

$this->connections[] = $connection;
$that = $this;
$connection->on('close', function () use ($that, $connection) {
$that->handleDisconnection($connection);
});

// pause accepting new connections if limit exceeded
if ($this->pauseOnLimit && !$this->autoPaused && count($this->connections) >= $this->limit) {
$this->autoPaused = true;

if (!$this->manuPaused) {
$this->server->pause();
}
}

$this->emit('connection', array($connection));
}

/** @internal */
public function handleDisconnection(ConnectionInterface $connection)
{
unset($this->connections[array_search($connection, $this->connections)]);

// continue accepting new connection if below limit
if ($this->autoPaused && count($this->connections) < $this->limit) {
$this->autoPaused = false;

if (!$this->manuPaused) {
$this->server->resume();
}
}
}

/** @internal */
public function handleError(\Exception $error)
{
$this->emit('error', array($error));
}
}
Loading

0 comments on commit 390c71b

Please sign in to comment.