From 1a176394eb10ebc686366e8bd24cf1c5f1bd2ae8 Mon Sep 17 00:00:00 2001 From: Sergey Poltaranin Date: Fri, 18 Nov 2016 00:49:14 +0700 Subject: [PATCH] Release 1.0.0 --- LICENSE.txt | 21 ++ README.md | 405 ++++++++++++++++++++++++++++++++ WebSocketServer.php | 194 +++++++++++++++ composer.json | 22 ++ events/ExceptionEvent.php | 12 + events/WSClientCommandEvent.php | 15 ++ events/WSClientErrorEvent.php | 10 + events/WSClientEvent.php | 12 + events/WSClientMessageEvent.php | 10 + 9 files changed, 701 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 WebSocketServer.php create mode 100644 composer.json create mode 100644 events/ExceptionEvent.php create mode 100644 events/WSClientCommandEvent.php create mode 100644 events/WSClientErrorEvent.php create mode 100644 events/WSClientEvent.php create mode 100644 events/WSClientMessageEvent.php diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b0bd292 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Sergey Poltaranin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49d493c --- /dev/null +++ b/README.md @@ -0,0 +1,405 @@ +# Yii2 [WebSocketServer](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/WebsocketController.php) + +Used [Ratchet](http://socketo.me/) + +## Installation + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +composer require consik/yii2-websocket +``` + +or add + +```json +"consik/yii2-websocket": "^1.0" +``` + +## WebSocketServer class description + +### Properties + +1. ``` int $port = 8080``` - Port number for websocket server +2. ``` bool $closeConnectionOnError = true``` - Close connection or not when error occurs with it +3. ``` bool $runClientCommands = true``` - Check client's messages for commands or not +4. ``` null|IoServer $server = null``` - IOServer object +5. ``` null|\SplObjectStorage $clients = null``` - Storage of connected clients + +### Methods + +### Events + +* EVENT_WEBSOCKET_OPEN + +> **Class** yii\base\Event - +Triggered when binding is successfully completed + +* EVENT_WEBSOCKET_CLOSE + +> **Class** yii\base\Event - +Triggered when socket listening is closed + +* EVENT_WEBSOCKET_OPEN_ERROR + +> **Class** [events\ExceptionEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/ExceptionEvent.php) - +Triggered when throwed Exception on binding socket + +* EVENT_CLIENT_CONNECTED + +> **Class** [events\WSClientEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientEvent.php) - +Triggered when client connected to the server + +* EVENT_CLIENT_DISCONNECTED + +> **Class** [events\WSClientEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientEvent.php) - +Triggered when client close connection with server + +* EVENT_CLIENT_ERROR + +> **Class** [events\WSClientErrorEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientErrorEvent.php) - +Triggered when an error occurs on a Connection + +* EVENT_CLIENT_MESSAGE + +> **Class** [events\WSClientMessageEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientMessageEvent.php) - +Triggered when message recieved from client + +* EVENT_CLIENT_RUN_COMMAND + +> **Class** [events\WSClientCommandEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientCommandEvent.php) - +Triggered when controller starts user's command + +* EVENT_CLIENT_END_COMMAND + +> **Class** [events\WSClientCommandEvent](https://gitlab.com/consigliere-kz/yii2-websocket/blob/master/events/WSClientCommandEvent.php) - +Triggered when controller finished user's command + +## Examples + +### Simple echo server + +Create your server class based on WebSocketServer. For example ```daemons\EchoServer.php```: + +```php +on(self::EVENT_CLIENT_MESSAGE, function (WSClientMessageEvent $e) { + $e->client->send( $e->message ); + }); + } + +} +``` + +Create yii2 console controller for starting server: + +```php +port = $port; + } + $server->start(); + } +} +``` + +Start your server using console: + +> php yii server/start + +Now let's check our server via js connection: + +```javascript +var conn = new WebSocket('ws://localhost:8080'); + conn.onmessage = function(e) { + console.log('Response:' + e.data); + }; + conn.onopen = function(e) { + console.log("Connection established!"); + console.log('Hey!'); + conn.send('Hey!'); + }; +``` + +Console result must be: + +> Connection established! + +> Hey! + +> Response:Hey! + +### Handle server starting success and error events + +Now we try handle socket binding error and open it on other port, when error occurs; + +Create yii2 console controller for starting server: + +```php +port = 80; //This port must be busy by WebServer and we handle an error + + $server->on(WebSocketServer::EVENT_WEBSOCKET_OPEN_ERROR, function($e) use($server) { + echo "Error opening port " . $server->port . "\n"; + $server->port += 1; //Try next port to open + $server->start(); + }); + + $server->on(WebSocketServer::EVENT_WEBSOCKET_OPEN, function($e) use($server) { + echo "Server started at port " . $server->port; + }); + + $server->start(); + } +} +``` + +Start your server using console command: + +> php yii server/start + +Server console result must be: + +> Error opening port 80 + +> Server started at port 81 + +### Recieving client commands + +You can implement methods that will be runned after some of user messages automatically; + +Server class ```daemons\CommandsServer.php```: + +```php +send('Pong'); + } + +} +``` + +Run the server like in examples above + +Check connection and command working by js script: + +```javascript + var conn = new WebSocket('ws://localhost:8080'); + conn.onmessage = function(e) { + console.log('Response:' + e.data); + }; + conn.onopen = function(e) { + console.log('ping'); + conn.send('ping'); + }; +``` + +Console result must be: + +> ping + +> Response:Pong + +### Chat example + +In the end let's make simple chat with sending messages and function to change username; + +Code without comments, try to understand it by youself ;) + +* Server class ```daemons\ChatServer.php```: + +```php +on(self::EVENT_CLIENT_CONNECTED, function(WSClientEvent $e) { + $e->client->name = null; + }); + } + + + protected function getCommand(ConnectionInterface $from, $msg) + { + $request = json_decode($msg, true); + return !empty($request['action']) ? $request['action'] : parent::getCommand($from, $msg); + } + + public function commandChat(ConnectionInterface $client, $msg) + { + $request = json_decode($msg, true); + $result = ['message' => '']; + + if (!$client->name) { + $result['message'] = 'Set your name'; + } elseif (!empty($request['message']) && $message = trim($request['message']) ) { + foreach ($this->clients as $chatClient) { + $chatClient->send( json_encode([ + 'type' => 'chat', + 'from' => $client->name, + 'message' => $message + ]) ); + } + } else { + $result['message'] = 'Enter message'; + } + + $client->send( json_encode($result) ); + } + + public function commandSetName(ConnectionInterface $client, $msg) + { + $request = json_decode($msg, true); + $result = ['message' => 'Username updated']; + + if (!empty($request['name']) && $name = trim($request['name'])) { + $usernameFree = true; + foreach ($this->clients as $chatClient) { + if ($chatClient != $client && $chatClient->name == $name) { + $result['message'] = 'This name is used by other user'; + $usernameFree = false; + break; + } + } + + if ($usernameFree) { + $client->name = $name; + } + } else { + $result['message'] = 'Invalid username'; + } + + $client->send( json_encode($result) ); + } + +} +``` + +* Simple html form ```chat.html```: + +```html +Username:
+ + +
+ +Message:
+ +
+``` + +* JS code for chat with [jQuery](https://jquery.com/): + +```javascript + + +``` + +Enjoy ;) + +## Other + +### Starting yii2 console application as daemon using [nohup](https://en.wikipedia.org/wiki/Nohup) + +```nohup php yii _ControllerName_/_ActionName_ &``` \ No newline at end of file diff --git a/WebSocketServer.php b/WebSocketServer.php new file mode 100644 index 0000000..96ab9c3 --- /dev/null +++ b/WebSocketServer.php @@ -0,0 +1,194 @@ +server = IoServer::factory( + new HttpServer( + new WsServer( + $this + ) + ), + $this->port + ); + $this->trigger(self::EVENT_WEBSOCKET_OPEN); + $this->clients = new \SplObjectStorage(); + $this->server->run(); + + return true; + + } catch (\Exception $e) { + $errorEvent = new ExceptionEvent([ + 'exception' => $e + ]); + $this->trigger(self::EVENT_WEBSOCKET_OPEN_ERROR, $errorEvent); + return false; + } + } + + /** + * @return void + */ + public function stop() + { + $this->server->socket->shutdown(); + $this->trigger(self::EVENT_WEBSOCKET_CLOSE); + } + + /** + * @param ConnectionInterface $conn + * + * @event WSClientEvent EVENT_CLIENT_CONNECTED + */ + function onOpen(ConnectionInterface $conn) + { + $this->trigger(self::EVENT_CLIENT_CONNECTED, new WSClientEvent([ + 'client' => $conn + ])); + + $this->clients->attach($conn); + } + + /** + * @param ConnectionInterface $conn + * + * @event WSClientEvent EVENT_CLIENT_DISCONNECTED + */ + function onClose(ConnectionInterface $conn) + { + $this->trigger(self::EVENT_CLIENT_DISCONNECTED, new WSClientEvent([ + 'client' => $conn + ])); + + $this->clients->detach($conn); + } + + /** + * @param ConnectionInterface $conn + * @param \Exception $e + * + * @event WSClientErrorEvent EVENT_CLIENT_ERROR + */ + function onError(ConnectionInterface $conn, \Exception $e) + { + $this->trigger(self::EVENT_CLIENT_ERROR, new WSClientErrorEvent([ + 'client' => $conn, + 'exception' => $e + ])); + + if ($this->closeConnectionOnError) { + $conn->close(); + } + } + + /** + * @param ConnectionInterface $from + * @param string $msg + * + * @event WSClientMessageEvent EVENT_CLIENT_MESSAGE + * @event WSClientCommandEvent EVENT_CLIENT_RUN_COMMAND + * @event WSClientCommandEvent EVENT_CLIENT_END_COMMAND + */ + function onMessage(ConnectionInterface $from, $msg) + { + $this->trigger(self::EVENT_CLIENT_MESSAGE, new WSClientMessageEvent([ + 'client' => $from, + 'message' => $msg + ])); + + if ($this->runClientCommands) { + $command = $this->getCommand($from, $msg); + + if ($command && method_exists($this, 'command' . ucfirst($command))) { + $this->trigger(self::EVENT_CLIENT_RUN_COMMAND, new WSClientCommandEvent([ + 'client' => $from, + 'command' => $command + ])); + + $result = call_user_func([$this, 'command' . ucfirst($command)], $from, $msg); + + $this->trigger(self::EVENT_CLIENT_END_COMMAND, new WSClientCommandEvent([ + 'client' => $from, + 'command' => $command, + 'result' => $result + ])); + } + } + } + + /** + * @param ConnectionInterface $from + * @param $msg + * @return null|string - _NAME_ of command that implemented in class method command_NAME_() + */ + protected function getCommand(ConnectionInterface $from, $msg) + { + /** + * There u can parse message, check client rights, etc... + * + * Example: + * Client sends json message like {"action":"chat", "text":"test message"} + * if ($json = json_decode($msg)) { return $json['action'] } + * If runMessageCommands == true && class method commandChat implemented, + * Than will run $this->commandChat($client, $msg); $msg - source message string, $client - ConnectionInterface + */ + return null; + } +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7afa616 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "consik/yii2-websocket", + "description": "Yii2 websocket server component", + "type": "yii2-extension", + "keywords": ["yii2", "websocket", "component", "ratchet"], + "license": "MIT", + "authors": [ + { + "name": "Sergey Poltaranin (Consik)", + "email": "consigliere.kz@gmail.com" + } + ], + "require": { + "yiisoft/yii2": "*", + "cboden/ratchet": "0.3.*" + }, + "autoload": { + "psr-4": { + "consik\\yii2websocket\\": "" + } + } +} \ No newline at end of file diff --git a/events/ExceptionEvent.php b/events/ExceptionEvent.php new file mode 100644 index 0000000..74ae1f7 --- /dev/null +++ b/events/ExceptionEvent.php @@ -0,0 +1,12 @@ +