├── LICENSE ├── README.md ├── WebSocketServer.php ├── composer.json └── events ├── ExceptionEvent.php ├── WSClientCommandEvent.php ├── WSClientErrorEvent.php ├── WSClientEvent.php └── WSClientMessageEvent.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergey Poltaranin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 [WebSocketServer](/WebSocketServer.php) 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/consik/yii2-websocket/v/stable)](https://packagist.org/packages/consik/yii2-websocket) 4 | [![Total Downloads](https://poser.pugx.org/consik/yii2-websocket/downloads)](https://packagist.org/packages/consik/yii2-websocket) 5 | [![License](https://poser.pugx.org/consik/yii2-websocket/license)](https://packagist.org/packages/consik/yii2-websocket) 6 | 7 | Used [Ratchet](http://socketo.me/) 8 | 9 | ## Installation 10 | 11 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 12 | 13 | Either run 14 | 15 | ``` 16 | composer require consik/yii2-websocket 17 | ``` 18 | 19 | or add 20 | 21 | ```json 22 | "consik/yii2-websocket": "^1.0" 23 | ``` 24 | 25 | ## WebSocketServer class description 26 | 27 | ### Properties 28 | 29 | 1. ``` int $port = 8080``` - Port number for websocket server 30 | 2. ``` bool $closeConnectionOnError = true``` - Close connection or not when error occurs with it 31 | 3. ``` bool $runClientCommands = true``` - Check client's messages for commands or not 32 | 4. ``` null|IoServer $server = null``` - IOServer object 33 | 5. ``` null|\SplObjectStorage $clients = null``` - Storage of connected clients 34 | 35 | ### Methods 36 | 37 | ### Events 38 | 39 | * EVENT_WEBSOCKET_OPEN 40 | 41 | > **Class** yii\base\Event - 42 | Triggered when binding is successfully completed 43 | 44 | * EVENT_WEBSOCKET_CLOSE 45 | 46 | > **Class** yii\base\Event - 47 | Triggered when socket listening is closed 48 | 49 | * EVENT_WEBSOCKET_OPEN_ERROR 50 | 51 | > **Class** [events\ExceptionEvent](/events/ExceptionEvent.php) - 52 | Triggered when throwed Exception on binding socket 53 | 54 | * EVENT_CLIENT_CONNECTED 55 | 56 | > **Class** [events\WSClientEvent](/events/WSClientEvent.php) - 57 | Triggered when client connected to the server 58 | 59 | * EVENT_CLIENT_DISCONNECTED 60 | 61 | > **Class** [events\WSClientEvent](/events/WSClientEvent.php) - 62 | Triggered when client close connection with server 63 | 64 | * EVENT_CLIENT_ERROR 65 | 66 | > **Class** [events\WSClientErrorEvent](/events/WSClientErrorEvent.php) - 67 | Triggered when an error occurs on a Connection 68 | 69 | * EVENT_CLIENT_MESSAGE 70 | 71 | > **Class** [events\WSClientMessageEvent](/events/WSClientMessageEvent.php) - 72 | Triggered when message recieved from client 73 | 74 | * EVENT_CLIENT_RUN_COMMAND 75 | 76 | > **Class** [events\WSClientCommandEvent](/events/WSClientCommandEvent.php) - 77 | Triggered when controller starts user's command 78 | 79 | * EVENT_CLIENT_END_COMMAND 80 | 81 | > **Class** [events\WSClientCommandEvent](/events/WSClientCommandEvent.php) - 82 | Triggered when controller finished user's command 83 | 84 | ## Examples 85 | 86 | ### Simple echo server 87 | 88 | Create your server class based on WebSocketServer. For example ```daemons\EchoServer.php```: 89 | 90 | ```php 91 | on(self::EVENT_CLIENT_MESSAGE, function (WSClientMessageEvent $e) { 105 | $e->client->send( $e->message ); 106 | }); 107 | } 108 | 109 | } 110 | ``` 111 | 112 | Create yii2 console controller for starting server: 113 | 114 | ```php 115 | port = $port; 128 | } 129 | $server->start(); 130 | } 131 | } 132 | ``` 133 | 134 | Start your server using console: 135 | 136 | > php yii server/start 137 | 138 | Now let's check our server via js connection: 139 | 140 | ```javascript 141 | var conn = new WebSocket('ws://localhost:8080'); 142 | conn.onmessage = function(e) { 143 | console.log('Response:' + e.data); 144 | }; 145 | conn.onopen = function(e) { 146 | console.log("Connection established!"); 147 | console.log('Hey!'); 148 | conn.send('Hey!'); 149 | }; 150 | ``` 151 | 152 | Console result must be: 153 | 154 | > Connection established! 155 | 156 | > Hey! 157 | 158 | > Response:Hey! 159 | 160 | ### Handle server starting success and error events 161 | 162 | Now we try handle socket binding error and open it on other port, when error occurs; 163 | 164 | Create yii2 console controller for starting server: 165 | 166 | ```php 167 | port = 80; //This port must be busy by WebServer and we handle an error 179 | 180 | $server->on(WebSocketServer::EVENT_WEBSOCKET_OPEN_ERROR, function($e) use($server) { 181 | echo "Error opening port " . $server->port . "\n"; 182 | $server->port += 1; //Try next port to open 183 | $server->start(); 184 | }); 185 | 186 | $server->on(WebSocketServer::EVENT_WEBSOCKET_OPEN, function($e) use($server) { 187 | echo "Server started at port " . $server->port; 188 | }); 189 | 190 | $server->start(); 191 | } 192 | } 193 | ``` 194 | 195 | Start your server using console command: 196 | 197 | > php yii server/start 198 | 199 | Server console result must be: 200 | 201 | > Error opening port 80 202 | 203 | > Server started at port 81 204 | 205 | ### Recieving client commands 206 | 207 | You can implement methods that will be runned after some of user messages automatically; 208 | 209 | Server class ```daemons\CommandsServer.php```: 210 | 211 | ```php 212 | send('Pong'); 239 | } 240 | 241 | } 242 | ``` 243 | 244 | Run the server like in examples above 245 | 246 | Check connection and command working by js script: 247 | 248 | ```javascript 249 | var conn = new WebSocket('ws://localhost:8080'); 250 | conn.onmessage = function(e) { 251 | console.log('Response:' + e.data); 252 | }; 253 | conn.onopen = function(e) { 254 | console.log('ping'); 255 | conn.send('ping'); 256 | }; 257 | ``` 258 | 259 | Console result must be: 260 | 261 | > ping 262 | 263 | > Response:Pong 264 | 265 | ### Chat example 266 | 267 | In the end let's make simple chat with sending messages and function to change username; 268 | 269 | Code without comments, try to understand it by youself ;) 270 | 271 | * Server class ```daemons\ChatServer.php```: 272 | 273 | ```php 274 | on(self::EVENT_CLIENT_CONNECTED, function(WSClientEvent $e) { 289 | $e->client->name = null; 290 | }); 291 | } 292 | 293 | 294 | protected function getCommand(ConnectionInterface $from, $msg) 295 | { 296 | $request = json_decode($msg, true); 297 | return !empty($request['action']) ? $request['action'] : parent::getCommand($from, $msg); 298 | } 299 | 300 | public function commandChat(ConnectionInterface $client, $msg) 301 | { 302 | $request = json_decode($msg, true); 303 | $result = ['message' => '']; 304 | 305 | if (!$client->name) { 306 | $result['message'] = 'Set your name'; 307 | } elseif (!empty($request['message']) && $message = trim($request['message']) ) { 308 | foreach ($this->clients as $chatClient) { 309 | $chatClient->send( json_encode([ 310 | 'type' => 'chat', 311 | 'from' => $client->name, 312 | 'message' => $message 313 | ]) ); 314 | } 315 | } else { 316 | $result['message'] = 'Enter message'; 317 | } 318 | 319 | $client->send( json_encode($result) ); 320 | } 321 | 322 | public function commandSetName(ConnectionInterface $client, $msg) 323 | { 324 | $request = json_decode($msg, true); 325 | $result = ['message' => 'Username updated']; 326 | 327 | if (!empty($request['name']) && $name = trim($request['name'])) { 328 | $usernameFree = true; 329 | foreach ($this->clients as $chatClient) { 330 | if ($chatClient != $client && $chatClient->name == $name) { 331 | $result['message'] = 'This name is used by other user'; 332 | $usernameFree = false; 333 | break; 334 | } 335 | } 336 | 337 | if ($usernameFree) { 338 | $client->name = $name; 339 | } 340 | } else { 341 | $result['message'] = 'Invalid username'; 342 | } 343 | 344 | $client->send( json_encode($result) ); 345 | } 346 | 347 | } 348 | ``` 349 | 350 | * Simple html form ```chat.html```: 351 | 352 | ```html 353 | Username:
354 | 355 | 356 |
357 | 358 | Message:
359 | 360 |
361 | ``` 362 | 363 | * JS code for chat with [jQuery](https://jquery.com/): 364 | 365 | ```javascript 366 | 367 | 401 | ``` 402 | 403 | Enjoy ;) 404 | 405 | ## Other 406 | 407 | ### Starting yii2 console application as daemon using [nohup](https://en.wikipedia.org/wiki/Nohup) 408 | 409 | ```nohup php yii _ControllerName_/_ActionName_ &``` 410 | -------------------------------------------------------------------------------- /WebSocketServer.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2016 9 | */ 10 | 11 | namespace consik\yii2websocket; 12 | 13 | use consik\yii2websocket\events\ExceptionEvent; 14 | use consik\yii2websocket\events\WSClientCommandEvent; 15 | use consik\yii2websocket\events\WSClientErrorEvent; 16 | use consik\yii2websocket\events\WSClientEvent; 17 | use consik\yii2websocket\events\WSClientMessageEvent; 18 | use Ratchet\ConnectionInterface; 19 | use Ratchet\Http\HttpServer; 20 | use Ratchet\MessageComponentInterface; 21 | use Ratchet\Server\IoServer; 22 | use Ratchet\WebSocket\WsServer; 23 | use Symfony\Component\Console\Event\ConsoleEvent; 24 | use Symfony\Component\Console\Event\ConsoleExceptionEvent; 25 | use yii\base\Component; 26 | 27 | class WebSocketServer extends Component implements MessageComponentInterface 28 | { 29 | /** 30 | * @event yii\base\Event Triggered when binding is successfully completed 31 | */ 32 | const EVENT_WEBSOCKET_OPEN = 'ws_open'; 33 | /** 34 | * @event yii\base\Event Triggered when socket listening is closed 35 | */ 36 | const EVENT_WEBSOCKET_CLOSE = 'ws_close'; 37 | /** 38 | * @event ExceptionEvent Triggered when throwed Exception on binding socket 39 | */ 40 | const EVENT_WEBSOCKET_OPEN_ERROR = 'ws_open_error'; 41 | 42 | /** 43 | * @event WSClientEvent Triggered when client connected to the server 44 | */ 45 | const EVENT_CLIENT_CONNECTED = 'ws_client_connected'; 46 | /** 47 | * @event WSClientErrorEvent Triggered when an error occurs on a Connection 48 | */ 49 | const EVENT_CLIENT_ERROR = 'ws_client_error'; 50 | /** 51 | * @event WSClientEvent Triggered when client close connection with server 52 | */ 53 | const EVENT_CLIENT_DISCONNECTED = 'ws_client_disconnected'; 54 | /** 55 | * @event WSClientMessageEvent Triggered when message recieved from client 56 | */ 57 | const EVENT_CLIENT_MESSAGE = 'ws_client_message'; 58 | /** 59 | * @event WSClientCommandEvent Triggered when controller starts user's command 60 | */ 61 | const EVENT_CLIENT_RUN_COMMAND = 'ws_client_run_command'; 62 | /** 63 | * @event WSClientCommandEvent Triggered when controller finished user's command 64 | */ 65 | const EVENT_CLIENT_END_COMMAND = 'ws_client_end_command'; 66 | 67 | 68 | /** 69 | * @var int $port 70 | */ 71 | public $port = 8080; 72 | 73 | /** 74 | * @var bool $closeConnectionOnError 75 | */ 76 | protected $closeConnectionOnError = true; 77 | 78 | /** 79 | * @var bool $runMessageCommands 80 | */ 81 | protected $runClientCommands = true; 82 | 83 | /** 84 | * @var IoServer|null $server 85 | */ 86 | protected $server = null; 87 | 88 | /** 89 | * @var null|\SplObjectStorage $clients 90 | */ 91 | protected $clients = null; 92 | 93 | /** 94 | * @return bool 95 | * 96 | * @event yii\base\Event EVENT_WEBSOCKET_OPEN 97 | * @event ExceptionEvent EVENT_WEBSOCKET_OPEN_ERROR 98 | */ 99 | public function start() 100 | { 101 | try { 102 | $this->server = IoServer::factory( 103 | new HttpServer( 104 | new WsServer( 105 | $this 106 | ) 107 | ), 108 | $this->port 109 | ); 110 | $this->trigger(self::EVENT_WEBSOCKET_OPEN); 111 | $this->clients = new \SplObjectStorage(); 112 | $this->server->run(); 113 | 114 | return true; 115 | 116 | } catch (\Exception $e) { 117 | $errorEvent = new ExceptionEvent([ 118 | 'exception' => $e 119 | ]); 120 | $this->trigger(self::EVENT_WEBSOCKET_OPEN_ERROR, $errorEvent); 121 | return false; 122 | } 123 | } 124 | 125 | /** 126 | * @return void 127 | * 128 | * @event yii\base\Event EVENT_WEBSOCKET_CLOSE 129 | */ 130 | public function stop() 131 | { 132 | $this->server->socket->shutdown(); 133 | $this->trigger(self::EVENT_WEBSOCKET_CLOSE); 134 | } 135 | 136 | /** 137 | * @param ConnectionInterface $conn 138 | * 139 | * @event WSClientEvent EVENT_CLIENT_CONNECTED 140 | */ 141 | function onOpen(ConnectionInterface $conn) 142 | { 143 | $this->trigger(self::EVENT_CLIENT_CONNECTED, new WSClientEvent([ 144 | 'client' => $conn 145 | ])); 146 | 147 | $this->clients->attach($conn); 148 | } 149 | 150 | /** 151 | * @param ConnectionInterface $conn 152 | * 153 | * @event WSClientEvent EVENT_CLIENT_DISCONNECTED 154 | */ 155 | function onClose(ConnectionInterface $conn) 156 | { 157 | $this->trigger(self::EVENT_CLIENT_DISCONNECTED, new WSClientEvent([ 158 | 'client' => $conn 159 | ])); 160 | 161 | $this->clients->detach($conn); 162 | } 163 | 164 | /** 165 | * @param ConnectionInterface $conn 166 | * @param \Exception $e 167 | * 168 | * @event WSClientErrorEvent EVENT_CLIENT_ERROR 169 | */ 170 | function onError(ConnectionInterface $conn, \Exception $e) 171 | { 172 | $this->trigger(self::EVENT_CLIENT_ERROR, new WSClientErrorEvent([ 173 | 'client' => $conn, 174 | 'exception' => $e 175 | ])); 176 | 177 | if ($this->closeConnectionOnError) { 178 | $conn->close(); 179 | } 180 | } 181 | 182 | /** 183 | * @param ConnectionInterface $from 184 | * @param string $msg 185 | * 186 | * @event WSClientMessageEvent EVENT_CLIENT_MESSAGE 187 | * @event WSClientCommandEvent EVENT_CLIENT_RUN_COMMAND 188 | * @event WSClientCommandEvent EVENT_CLIENT_END_COMMAND 189 | */ 190 | function onMessage(ConnectionInterface $from, $msg) 191 | { 192 | $this->trigger(self::EVENT_CLIENT_MESSAGE, new WSClientMessageEvent([ 193 | 'client' => $from, 194 | 'message' => $msg 195 | ])); 196 | 197 | if ($this->runClientCommands) { 198 | $command = $this->getCommand($from, $msg); 199 | 200 | if ($command && method_exists($this, 'command' . ucfirst($command))) { 201 | $this->trigger(self::EVENT_CLIENT_RUN_COMMAND, new WSClientCommandEvent([ 202 | 'client' => $from, 203 | 'command' => $command 204 | ])); 205 | 206 | $result = call_user_func([$this, 'command' . ucfirst($command)], $from, $msg); 207 | 208 | $this->trigger(self::EVENT_CLIENT_END_COMMAND, new WSClientCommandEvent([ 209 | 'client' => $from, 210 | 'command' => $command, 211 | 'result' => $result 212 | ])); 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * @param ConnectionInterface $from 219 | * @param $msg 220 | * @return null|string - _NAME_ of command that implemented in class method command_NAME_() 221 | */ 222 | protected function getCommand(ConnectionInterface $from, $msg) 223 | { 224 | return null; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consik/yii2-websocket", 3 | "description": "Yii2 websocket server component", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2", "websocket", "component", "ratchet"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Sergey Poltaranin (Consik)", 10 | "email": "consigliere.kz@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "*", 15 | "cboden/ratchet": "0.3.*" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "consik\\yii2websocket\\": "" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /events/ExceptionEvent.php: -------------------------------------------------------------------------------- 1 |