Skip to content

Commit

Permalink
Merge pull request #145 from mcsky/feature/on-disconnect
Browse files Browse the repository at this point in the history
Add on client disconnect event to MessageHandlerInterface
  • Loading branch information
Nek- authored Oct 4, 2018
2 parents 45b50c7 + 118450e commit f7bb89a
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).


## [Unreleased]
### Added
- `onDisconnect` method could be implemented in a MessageHandler. This method is called when the connection between client and server is resume
### Changed
- Allow nekland/tools in 2.0 version (still works with 1.0)

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class YourMessageHandler extends TextMessageHandler
// Sending back the received data
$connection->write($data);
}

public function onDisconnect(AbstractConnection $connection)
{
// Doing something when the connection between client/server is disconnecting
// Optionnal
}
}
```

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "nekland/woketo",
"license": "MIT",
"type": "library",
"description": "A WebSocket library for PHP",
"autoload": {
"psr-4": {
"Nekland\\Woketo\\": "src/"
Expand Down
6 changes: 4 additions & 2 deletions src/Core/AbstractConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,12 @@ protected function getHandler() : MessageHandlerInterface

/**
* Close the connection with normal close.
* @param int $status
* @param string|null $reason
*/
public function close()
public function close(int $status = Frame::CLOSE_NORMAL, string $reason = null)
{
$this->messageProcessor->close($this->stream);
$this->messageProcessor->close($this->stream, $status, $reason);
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/Message/MessageHandlerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/**
* Interface MessageHandlerInterface
*
* If there is only one message handler object (that *you* instanciate) you can guess what is the current client using the spl hash of the connection.
* If there is only one message handler object (that *you* instantiate) you can guess what is the current client using the spl hash of the connection.
*/
interface MessageHandlerInterface
{
Expand Down Expand Up @@ -47,6 +47,14 @@ public function onBinary(string $data, AbstractConnection $connection);
* This callback is call when there is an error on the websocket protocol communication.
*
* @param WebsocketException $e
* @param AbstractConnection $connection
*/
public function onError(WebsocketException $e, AbstractConnection $connection);

/**
* Is called when the connection is closed by the client
*
* @param AbstractConnection $connection
*/
public function onDisconnect(AbstractConnection $connection);
}
5 changes: 5 additions & 0 deletions src/Message/SimpleMessageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public function onError(WebsocketException $e, AbstractConnection $connection)
{
echo 'An error occurred : ' . $e->getMessage();
}

public function onDisconnect(AbstractConnection $connection)
{
// Doing nothing
}
}
6 changes: 4 additions & 2 deletions src/Rfc6455/MessageProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,12 @@ public function timeout(ConnectionInterface $socket)

/**
* @param ConnectionInterface $socket
* @param int $status
* @param string|null $reason
*/
public function close(ConnectionInterface $socket)
public function close(ConnectionInterface $socket, int $status = Frame::CLOSE_NORMAL, string $reason = null)
{
$this->write($this->frameFactory->createCloseFrame(), $socket);
$this->write($this->frameFactory->createCloseFrame($status, $reason), $socket);
$socket->end();
}
}
5 changes: 4 additions & 1 deletion src/Server/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ private function initListeners()
$this->stream->on('data', function ($data) {
$this->processData($data);
});
$this->stream->once('end', function() {
$this->getHandler()->onDisconnect($this);
});
$this->stream->on('error', function ($data) {
$this->error($data);
});
Expand All @@ -64,7 +67,7 @@ private function processData($data)
$this->getHandler()->onError($e, $this);
} catch (NoHandlerException $e) {
$this->getLogger()->info(sprintf('No handler found for uri %s. Connection closed.', $this->uri));
$this->close();
$this->close(Frame::CLOSE_WRONG_DATA);
}
}

Expand Down
42 changes: 38 additions & 4 deletions src/Server/WebSocketServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class WebSocketServer
private $messageHandlers;

/**
* @var array
* @var Connection[]
*/
private $connections;

Expand Down Expand Up @@ -82,8 +82,8 @@ class WebSocketServer
private $logger;

/**
* @param int $port The number of the port to bind
* @param string $host The host to listen on (by default 127.0.0.1)
* @param int $port The number of the port to bind
* @param string $host The host to listen on (by default 127.0.0.1)
* @param array $config
*/
public function __construct($port, $host = '127.0.0.1', $config = [])
Expand Down Expand Up @@ -144,7 +144,7 @@ public function start()
$this->getLogger()->info('Enabled ssl');
}

$this->server->on('connection', function ($socketStream) {
$this->server->on('connection', function (ConnectionInterface $socketStream) {
$this->onNewConnection($socketStream);
});

Expand All @@ -162,10 +162,44 @@ private function onNewConnection(ConnectionInterface $socketStream)
return $this->getMessageHandler($uri, $connection);
}, $this->loop, $this->messageProcessor);

$socketStream->on('end', function () use($connection) {
$this->onDisconnect($connection);
});

$connection->setLogger($this->getLogger());
$connection->getLogger()->info(sprintf('Ip "%s" establish connection', $connection->getIp()));
$this->connections[] = $connection;
}

/**
*
* @param Connection $connection
*/
private function onDisconnect(Connection $connection)
{
$this->removeConnection($connection);
$connection->getLogger()->info(sprintf('Ip "%s" left connection', $connection->getIp()));
}

/**
* Remove a Connection instance by his object id
* @param Connection $connection
* @throws RuntimeException This method throw an exception if the $connection instance object isn't findable in websocket server's connections
*/
private function removeConnection(Connection $connection)
{
$connectionId = spl_object_hash($connection);
foreach ($this->connections as $index => $connectionItem) {
if ($connectionId === spl_object_hash($connectionItem)) {
unset($this->connections[$index]);
return;
}
}

$this->logger->critical('No connection found in the server connection list, impossible to delete the given connection id. Something wrong happened');
throw new RuntimeException('No connection found in the server connection list, impossible to delete the given connection id. Something wrong happened');
}

/**
* @param string $uri
* @param Connection $connection
Expand Down
10 changes: 10 additions & 0 deletions tests/Woketo/Message/SimpleMessageHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ public function testItEchosOnError()

$this->assertContains('foobar', $out);
}

public function testItDoNothingOnDisconnection()
{
\ob_start();
$res = $this->instance->onDisconnect($this->prophesize(Connection::class)->reveal());
$out = \ob_get_clean();

$this->assertNull($res);
$this->assertEquals('', $out);
}
}

class SimpleMessageHandlerImplementation extends SimpleMessageHandler
Expand Down
45 changes: 22 additions & 23 deletions tests/Woketo/Server/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Test\Woketo\Server;

use Evenement\EventEmitterTrait;
use Nekland\Woketo\Message\MessageHandlerInterface;
use Nekland\Woketo\Rfc6455\Frame;
use Nekland\Woketo\Rfc6455\Handshake\ServerHandshake;
Expand Down Expand Up @@ -88,6 +89,26 @@ public function testItSupportsBinaryMessage()
$reactMock->emit('data', [$binaryFrame]);
}

public function testItCallOnDisconnectOnHandlerWhenDisconnect()
{
// Mocks
$reactMock = new ReactConnectionMock();
$handler = $this->prophesize(MessageHandlerInterface::class);
$loop = $this->prophesize(LoopInterface::class);
/** @var MessageProcessor $processor */
$processor = $this->prophesize(MessageProcessor::class);
$handshakeProcessor = $this->prophesize(ServerHandshake::class);

// Init
$connection = new Connection($reactMock, function () use ($handler) {return $handler->reveal();}, $loop->reveal(), $processor->reveal(), $handshakeProcessor->reveal());
$server = new ReactConnectionMock();
$server->emit('connection', [$connection]);


$handler->onDisconnect(Argument::type(Connection::class))->shouldBeCalled();
$reactMock->emit('end');
}

private function getHandshake()
{
return "GET /foo HTTP/1.1\r\n"
Expand All @@ -109,32 +130,10 @@ private function getHandshake()

class ReactConnectionMock implements ConnectionInterface
{
public function __construct()
{
}

private $on = [];

public function on($event, callable $listener)
{
$this->on[$event] = $listener;
}

public function emit($event, array $arguments = [])
{
call_user_func_array($this->on[$event], $arguments);
}
use EventEmitterTrait;

public function getRemoteAddress() {}

public function once($event, callable $listener) {}

public function removeListener($event, callable $listener) {}

public function removeAllListeners($event = null) {}

public function listeners($event = null) {}

public function isReadable(){}

public function pause() {}
Expand Down
50 changes: 27 additions & 23 deletions tests/Woketo/Server/WebSocketServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
namespace Test\Woketo\Server;


use Evenement\EventEmitterTrait;
use Nekland\Woketo\Core\AbstractConnection;
use Nekland\Woketo\Exception\ConfigException;
use Nekland\Woketo\Exception\RuntimeException;
Expand Down Expand Up @@ -83,6 +84,31 @@ public function onConnection(AbstractConnection $connection)
$this->assertTrue($handler->called);
}

public function testItCallTheDisconnectionMethodOfHandler()
{
$handler = new class extends TextMessageHandler {
public $called = false;
public function onMessage(string $data, AbstractConnection $connection) {}
public function onConnection(AbstractConnection $connection) {}

public function onDisconnect(AbstractConnection $connection)
{
$this->called = true;
}
};

$server = new WebSocketServer(1000, '127.0.0.1', ['prod' => false]);
$server->setMessageHandler($handler);
$server->setLoop($this->prophesize(LoopInterface::class)->reveal());
$server->setSocketServer($socket = new FakeSocketServerForTestMethodHandlerConnection());
$server->setLogger(new NullLogger());
$server->start();
$socket->callCb($co = new ServerReactConnectionMock());
$co->emit('data', [self::getHandshake()]);
$co->emit('end');
$this->assertTrue($handler->called);
}

/**
* @dataProvider getMessageHandlerTestData
*/
Expand Down Expand Up @@ -275,32 +301,10 @@ public function close() {}

class ServerReactConnectionMock implements ConnectionInterface
{
public function __construct()
{
}

private $on = [];

public function on($event, callable $listener)
{
$this->on[$event] = $listener;
}

public function emit($event, array $arguments = [])
{
call_user_func_array($this->on[$event], $arguments);
}
use EventEmitterTrait;

public function getRemoteAddress() {}

public function once($event, callable $listener) {}

public function removeListener($event, callable $listener) {}

public function removeAllListeners($event = null) {}

public function listeners($event = null) {}

public function isReadable(){}

public function pause() {}
Expand Down
12 changes: 10 additions & 2 deletions tests/browser_testing/echo_server/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ <h1>Test woketo echo server <small>with browser</small></h1>
<button id="hello" class="btn btn-primary">Say Hello</button>
<button id="yolo" class="btn btn-primary">Say Yolo</button>
<button id="swagg" class="btn btn-primary">Say Swagg</button>
<button id="disconnect" class="btn btn-primary btn-warning">Disconnect</button>

<pre id="result">
</pre>
Expand All @@ -24,10 +25,13 @@ <h1>Test woketo echo server <small>with browser</small></h1>
var ws = new WebSocket("ws://127.0.0.1:1337/foo");
var pre = document.getElementById('result');
ws.onopen = function () {
result.innerHTML += '\nConnection opened.';
pre.innerHTML += '\nConnection opened.';
};
ws.onmessage = function (e) {
result.innerHTML += '\nGot message: ' + e.data;
pre.innerHTML += '\nGot message: ' + e.data;
};
ws.onclose = function (event) {
pre.innerHTML += '\nConnection closed';
};

document.getElementById('hello').onclick = function () {
Expand All @@ -39,6 +43,10 @@ <h1>Test woketo echo server <small>with browser</small></h1>
document.getElementById('swagg').onclick = function () {
ws.send('OMG THIS IZ SO SWAGG11!11!1!11');
};
document.getElementById('disconnect').onclick = function () {
pre.innerHTML += '\nYou\'re closing connection';
ws.close();
};
</script>
</body>
</html>
Loading

0 comments on commit f7bb89a

Please sign in to comment.