diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a1cfa..98b72f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index ee11da4..80985f2 100644 --- a/README.md +++ b/README.md @@ -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 + } } ``` diff --git a/composer.json b/composer.json index 23394d7..759fa8f 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,7 @@ "name": "nekland/woketo", "license": "MIT", "type": "library", + "description": "A WebSocket library for PHP", "autoload": { "psr-4": { "Nekland\\Woketo\\": "src/" diff --git a/src/Core/AbstractConnection.php b/src/Core/AbstractConnection.php index 2722f9b..a10ae18 100644 --- a/src/Core/AbstractConnection.php +++ b/src/Core/AbstractConnection.php @@ -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); } /** diff --git a/src/Message/MessageHandlerInterface.php b/src/Message/MessageHandlerInterface.php index 741fcc5..024a487 100644 --- a/src/Message/MessageHandlerInterface.php +++ b/src/Message/MessageHandlerInterface.php @@ -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 { @@ -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); } diff --git a/src/Message/SimpleMessageHandler.php b/src/Message/SimpleMessageHandler.php index 917489a..d1e13f3 100644 --- a/src/Message/SimpleMessageHandler.php +++ b/src/Message/SimpleMessageHandler.php @@ -25,4 +25,9 @@ public function onError(WebsocketException $e, AbstractConnection $connection) { echo 'An error occurred : ' . $e->getMessage(); } + + public function onDisconnect(AbstractConnection $connection) + { + // Doing nothing + } } diff --git a/src/Rfc6455/MessageProcessor.php b/src/Rfc6455/MessageProcessor.php index 32f79b3..3546986 100644 --- a/src/Rfc6455/MessageProcessor.php +++ b/src/Rfc6455/MessageProcessor.php @@ -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(); } } diff --git a/src/Server/Connection.php b/src/Server/Connection.php index cca00e1..7b97948 100644 --- a/src/Server/Connection.php +++ b/src/Server/Connection.php @@ -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); }); @@ -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); } } diff --git a/src/Server/WebSocketServer.php b/src/Server/WebSocketServer.php index 75b1323..8595231 100644 --- a/src/Server/WebSocketServer.php +++ b/src/Server/WebSocketServer.php @@ -52,7 +52,7 @@ class WebSocketServer private $messageHandlers; /** - * @var array + * @var Connection[] */ private $connections; @@ -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 = []) @@ -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); }); @@ -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 diff --git a/tests/Woketo/Message/SimpleMessageHandlerTest.php b/tests/Woketo/Message/SimpleMessageHandlerTest.php index 221540d..f83f3dd 100644 --- a/tests/Woketo/Message/SimpleMessageHandlerTest.php +++ b/tests/Woketo/Message/SimpleMessageHandlerTest.php @@ -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 diff --git a/tests/Woketo/Server/ConnectionTest.php b/tests/Woketo/Server/ConnectionTest.php index 86ac0f3..062cd64 100644 --- a/tests/Woketo/Server/ConnectionTest.php +++ b/tests/Woketo/Server/ConnectionTest.php @@ -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; @@ -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" @@ -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() {} diff --git a/tests/Woketo/Server/WebSocketServerTest.php b/tests/Woketo/Server/WebSocketServerTest.php index fbc44f3..690bbfa 100644 --- a/tests/Woketo/Server/WebSocketServerTest.php +++ b/tests/Woketo/Server/WebSocketServerTest.php @@ -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; @@ -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 */ @@ -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() {} diff --git a/tests/browser_testing/echo_server/index.html b/tests/browser_testing/echo_server/index.html index e6f7c32..52094a0 100644 --- a/tests/browser_testing/echo_server/index.html +++ b/tests/browser_testing/echo_server/index.html @@ -13,6 +13,7 @@

Test woketo echo server with browser

+
                 
@@ -24,10 +25,13 @@

Test woketo echo server with browser

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 () { @@ -39,6 +43,10 @@

Test woketo echo server with browser

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(); + }; diff --git a/tests/browser_testing/echo_server/server.php b/tests/browser_testing/echo_server/server.php index ba2426e..26b0eb5 100644 --- a/tests/browser_testing/echo_server/server.php +++ b/tests/browser_testing/echo_server/server.php @@ -5,19 +5,26 @@ use Nekland\Woketo\Server\Connection; use Nekland\Woketo\Message\TextMessageHandler; use Nekland\Woketo\Server\WebSocketServer; +use Nekland\Woketo\Core\AbstractConnection; class EchoMessageHandler extends TextMessageHandler { - public function onConnection(Connection $connection) + public function onConnection(AbstractConnection $connection) { echo "New client connected !\n"; } - public function onMessage(string $data, Connection $connection) + public function onMessage(string $data, AbstractConnection $connection) { // Sending back the received data $connection->write($data); } + + + public function onDisconnect(AbstractConnection $connection) + { + echo "Client disconnected !\n"; + } } $server = new WebSocketServer(1337, '127.0.0.1', [ diff --git a/tests/echo-server.php b/tests/echo-server.php index 1f24987..c41febf 100644 --- a/tests/echo-server.php +++ b/tests/echo-server.php @@ -31,6 +31,10 @@ public function onError(\Nekland\Woketo\Exception\WebsocketException $e, \Neklan { echo '(' . get_class($e) . ') ' . $e->getMessage() . "\n"; } + + public function onDisconnect(\Nekland\Woketo\Core\AbstractConnection $connection) + { + } } $foo->setMessageHandler(new EchoServer()); diff --git a/tests/javascript/js_server/client.php b/tests/javascript/js_server/client.php index 73c0010..9f00adb 100644 --- a/tests/javascript/js_server/client.php +++ b/tests/javascript/js_server/client.php @@ -28,4 +28,9 @@ public function onError(\Nekland\Woketo\Exception\WebsocketException $e, Abstrac var_dump($e->getMessage()); echo $e->getTraceAsString(); } + + public function onDisconnect(AbstractConnection $connection) + { + $connection->write('see you soon'); + } });