├── docs ├── .DS_Store └── guide │ ├── README.md │ ├── console.md │ ├── dirver-swoole.md │ ├── dirver-workerman.md │ └── usage.md ├── examples ├── models │ └── LiveRoom.php ├── channels │ ├── ExitLiveRoomChannel.php │ ├── PushChannel.php │ └── EnterLiveRoomChannel.php ├── controllers │ └── LiveRoomController.php └── ws.html ├── composer.json ├── src ├── WebSocket.php ├── ChannelInterface.php ├── cli │ ├── WebSocket.php │ └── Command.php └── drivers │ ├── workerman │ ├── WebSocket.php │ └── Command.php │ └── swoole │ ├── Command.php │ └── WebSocket.php ├── README.md └── LICENSE /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiiplus/yii2-websocket/HEAD/docs/.DS_Store -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Yii2 WebSocket 扩展 2 | 3 | 在 yii2 下运行 WebSocket 服务。 4 | 5 | ## 介绍 6 | - [基本使用](usage.md) 7 | - [控制台执行](console.md) 8 | 9 | ## WebSocket 驱动 10 | - [swoole](dirver-swoole.md) 11 | - [workerman](dirver-workerman.md) -------------------------------------------------------------------------------- /docs/guide/console.md: -------------------------------------------------------------------------------- 1 | # 控制台执行 2 | 3 | 控制台用于启动 WebSocket 守护进程和查看 channel 4 | 5 | ## 启动 WebSocket 守护进程 6 | 7 | ```bash 8 | yii websocket/start 9 | ``` 10 | 11 | `start` 命令用于启动一个守护进程,它可以处理各个 channel 的任务。 12 | 13 | `start` 命令参数: 14 | 15 | - --host, -h: 指定 WebSocket Server 的 host 16 | - --port, -p: 指定 WebSocket Server 的端口号 17 | 18 | ## 查看 channel 列表 19 | 20 | ```bash 21 | yii websocket/list 22 | ``` -------------------------------------------------------------------------------- /docs/guide/dirver-swoole.md: -------------------------------------------------------------------------------- 1 | # Swoole 驱动 2 | 3 | 驱动程序使用 Swoole 的 WebSocket。 4 | 5 | 您需要安装 [Swoole](https://www.swoole.com/) 扩展到你的php中。 6 | 7 | ## 配置实例 8 | 9 | ```php 10 | return [ 11 | 'bootstrap' => [ 12 | 'websocket', 13 | ], 14 | 'compoents' => [ 15 | 'websocket' => [ 16 | 'class' => '\yiiplus\websocket\swoole\WebSocket', 17 | 'host' => '127.0.0.1', 18 | 'port' => 9501, 19 | 'channels' => [ 20 | ... 21 | ], 22 | ], 23 | ], 24 | ]; 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/models/LiveRoom.php: -------------------------------------------------------------------------------- 1 | $fd]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/controllers/LiveRoomController.php: -------------------------------------------------------------------------------- 1 | websocket->send([ 17 | 'channel' => 'push', 18 | 'room_id' => Yii::$app->request->get('room_id', 1), 19 | 'message' => Yii::$app->request->get('message', '用户 Gunn 送给主播 象拔河 一架飞机!') 20 | ]); 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiiplus/yii2-websocket", 3 | "description": "使用yii2封装 websocket 扩展", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension","websocket","swoole"], 6 | "license": "Apache-2.0", 7 | "authors": [ 8 | { 9 | "name": "gengxiankun", 10 | "email": "gengxiankun@126.com" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "~2.0.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "yiiplus\\websocket\\": "src", 19 | "yiiplus\\websocket\\swoole\\": "src/drivers/swoole", 20 | "yiiplus\\websocket\\workerman\\": "src/drivers/workerman" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/channels/PushChannel.php: -------------------------------------------------------------------------------- 1 | where(['room_id' => $data->room_id])->column('id'); 21 | 22 | return [ 23 | $liveRoomIds, 24 | $data->message 25 | ]; 26 | } 27 | 28 | public function close($fd) 29 | { 30 | return; 31 | } 32 | } -------------------------------------------------------------------------------- /docs/guide/dirver-workerman.md: -------------------------------------------------------------------------------- 1 | # Workerman 驱动 2 | 3 | 驱动程序使用 Workerman 的 WebSocket。 4 | 5 | 您需要安装 [Workerman](https://www.workerman.net/) 扩展到你的php中。 6 | 7 | ## 配置实例 8 | 9 | ```php 10 | return [ 11 | 'bootstrap' => [ 12 | 'websocket', 13 | ], 14 | 'compoents' => [ 15 | 'websocket' => [ 16 | 'class' => '\yiiplus\websocket\workerman\WebSocket', 17 | 'host' => '127.0.0.1', 18 | 'port' => 9501, 19 | 'channels' => [ 20 | ... 21 | ], 22 | ], 23 | ], 24 | ]; 25 | ``` 26 | 27 | ## 控制台启动 WebSocket 守护进程 28 | 29 | ```bash 30 | yii websocket/start 31 | ``` 32 | 33 | `start` 命令用于启动一个守护进程,它可以处理各个 channel 的任务。 34 | 35 | `start` 命令参数: 36 | 37 | - --host, -h: 指定 WebSocket Server 的 host 38 | - --port, -p: 指定 WebSocket Server 的端口号 39 | - --worker_num, -w: 指定 worker 进程数 40 | -------------------------------------------------------------------------------- /src/WebSocket.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0.0 22 | */ 23 | abstract class WebSocket extends Component 24 | { 25 | /** 26 | * 发送数据到WebSocket 27 | * 28 | * @param mixed $data 发送的数据 29 | * @param string $type 数据类型 30 | * @param bool $masked 是否设置掩码 31 | */ 32 | abstract public function send($data, $type = 'text', $masked = true); 33 | } 34 | -------------------------------------------------------------------------------- /examples/channels/EnterLiveRoomChannel.php: -------------------------------------------------------------------------------- 1 | id = $fd; 24 | $liveRoomModel->room_id = $data->room_id; 25 | $liveRoomModel->uid = $data->uid; 26 | 27 | $liveRoomModel->save(); 28 | } 29 | 30 | public function close($fd) 31 | { 32 | return; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ChannelInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket 6 | 7 | 8 |

Echo Test

9 | 10 | 11 |
12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/guide/usage.md: -------------------------------------------------------------------------------- 1 | # 基本使用 2 | 3 | ## 配置 4 | 5 | ```php 6 | return [ 7 | 'bootstrap' => [ 8 | 'websocket', 9 | ], 10 | 'compoents' => [ 11 | 'websocket' => [ 12 | 'class' => '\yiiplus\websocket\\WebSocket', 13 | 'host' => '127.0.0.1', 14 | 'port' => 9501, 15 | 'channels' => [ 16 | 'push-message' => '\xxx\channels\PushMessageChannel', // 配置 channel 对应的执行类 17 | ], 18 | ], 19 | ], 20 | ]; 21 | ``` 22 | 23 | ## 定义 channel 执行类 24 | 25 | 每个 channel 的功能都需要定义一个单独的类,WebSocket Server 会通过客户端传来的 channel 参数解析。 26 | 27 | 例如,如果你需要为所有客户端推送一条消息,则该类可能如下所示: 28 | 29 | ```php 30 | namespace xxx\channels; 31 | 32 | class PushMessageChannel extends BaseObject implements \yiiplus\websocket\ChannelInterface 33 | { 34 | public function execute($fd, $data) 35 | { 36 | return [ 37 | $fd, // 第一个参数返回客户端ID,多个以数组形式返回 38 | $data // 第二个参数返回需要返回给客户端的消息 39 | ]; 40 | } 41 | 42 | public function close($fd) 43 | { 44 | return; 45 | } 46 | } 47 | ``` 48 | 49 | > 定义好的执行类需要注册到 compoents 配置中的 [channel](#配置) 下。 50 | 51 | 当客户端断开连接时会触发所有 channels 下的 `close` 方法,用于清理客户端在服务器上与业务的绑定关系。 52 | 53 | ## 客户端发送 channel 消息,触发任务 54 | 55 | ```php 56 | Yii::$app->websocket->send(['channel' => 'push-message', 'message' => '用户 xxx 送了一台飞机!']); 57 | ``` 58 | 59 | ## 控制台执行 60 | 61 | 执行任务的确切方式取决于使用的驱动程序。 大多数驱动程序可以使用控制台命令运行,组件需要在应用程序中注册。 62 | 63 | 此命令启动一个守护进程,该守护进程维护一个 WebSocket Server,根据客户端发来的数据,处理相关 channel 的任务: 64 | 65 | ```bash 66 | yii websocket/start 67 | ``` 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-websocket 2 | 3 | 在 yii2 下运行 WebSocket 服务。 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/yiiplus/yii2-websocket/v/stable)](https://packagist.org/packages/yiiplus/yii2-websocket) 6 | [![Total Downloads](https://poser.pugx.org/yiiplus/yii2-websocket/downloads)](https://packagist.org/packages/yiiplus/yii2-websocket) 7 | [![License](https://poser.pugx.org/yiiplus/yii2-websocket/license)](https://packagist.org/packages/yiiplus/yii2-websocket) 8 | 9 | ## 驱动支持 10 | - [swoole](docs/guide/dirver-swoole.md) 11 | - ~~[workerman](docs/guide/dirver-workerman.md)~~ 12 | 13 | ## 安装 14 | 15 | 安装此扩展程序的首选方法是通过 [composer](http://getcomposer.org/download/). 16 | 17 | 编辑运行 18 | 19 | ```bash 20 | php composer.phar require --prefer-dist yiiplus/yii2-websocket "^1.0.0" 21 | ``` 22 | 23 | 或添加配置到项目目录下的`composer.json`文件的 require 部分 24 | 25 | ``` 26 | "yiiplus/yii2-websocket": "^1.0.0" 27 | ``` 28 | 29 | ## 基本使用 30 | 31 | 每个 channel 的功能都需要定义一个单独的类。例如,如果你需要为指定客户端推送一条消息,则该类可能如下所示: 32 | 33 | ```php 34 | namespace xxx\channels; 35 | 36 | class PushMessageChannel extends BaseObject implements \yiiplus\websocket\ChannelInterface 37 | { 38 | public function execute($fd, $data) 39 | { 40 | return [ 41 | $fd, // 第一个参数返回客户端ID,多个以数组形式返回 42 | $data // 第二个参数返回需要返回给客户端的消息 43 | ]; 44 | } 45 | 46 | public function close($fd) 47 | { 48 | return; 49 | } 50 | } 51 | ``` 52 | 53 | 以下是从客户端发送消息的方法: 54 | 55 | ```php 56 | Yii::$app->websocket->send(['channel' => 'push-message', 'message' => '用户 xxx 送了一台飞机!']); 57 | ``` 58 | 59 | 执行任务的确切方式取决于使用的驱动程序。 大多数驱动程序可以使用控制台命令运行,组件需要在应用程序中注册。 60 | 61 | 此命令启动一个守护进程,该守护进程维护一个 WebSocket Server,根据客户端发来的数据,处理相关 channel 的任务: 62 | 63 | ```bash 64 | yii websocket/start 65 | ``` 66 | 67 | 有关驱动程序特定控制台命令及其选项的更多详细信息,请参阅 [文档](docs/guide/)。 68 | -------------------------------------------------------------------------------- /src/cli/WebSocket.php: -------------------------------------------------------------------------------- 1 | getComponents(false) as $id => $component) { 59 | if ($component === $this) { 60 | return Inflector::camel2id($id); 61 | } 62 | } 63 | throw new InvalidConfigException('WebSocket must be an application component.'); 64 | } 65 | 66 | /** 67 | * ConsoleApp 引导程序 68 | */ 69 | public function bootstrap($app) 70 | { 71 | if ($app instanceof ConsoleApp) { 72 | $app->controllerMap[$this->getCommandId()] = [ 73 | 'class' => $this->commandClass, 74 | 'websocket' => $this, 75 | ] + $this->commandOptions; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/drivers/workerman/WebSocket.php: -------------------------------------------------------------------------------- 1 | [ 26 | * ... 27 | * 'websocket' => [ 28 | * 'class' => 'yiiplus\websocket\workerman\WebSocket', 29 | * 'host' => '127.0.0.1', 30 | * 'port' => '9501', 31 | * ], 32 | * ... 33 | * ], 34 | * ``` 35 | * 36 | * 然后通过components的方式调用: 37 | * 38 | * ```php 39 | * $websocketClient = \Yii::$app->websocket; 40 | * $websocketClient->send(['channel' => 'yiiplus', 'message' => 'hello websocket!']); 41 | * ``` 42 | * 43 | * @property string $host WebSocket服务端HOST,此参数必须在components配置中设置 44 | * @property integer $port WebSocket服务端端口号,此参数必须在components配置中设置 45 | * @property string $path WebSocket Request-URI,默认为'/',可通过components配置设置此参数 46 | * @property string $origin string Header Origin,默认为null,可通过components配置设置此参数 47 | * @property mixed $returnData 返回数据 48 | * @property mixed $_key Websocket Sec-WebSocket-Key 49 | * @property swoole_client $_socket WebSocket客户端 50 | * @property mixed $_buffer 用于对`recv`方法获取服务器接受到的数据进行缓存 51 | * @property mixed $_connected 链接的状态 52 | * 53 | * @author gengxiankun 54 | * @since 1.0.0 55 | */ 56 | class WebSocket extends CliWebSocket 57 | { 58 | /** 59 | * @var string WebSocket 服务端HOST 60 | */ 61 | public $host; 62 | 63 | /** 64 | * @var integer WebSocket 服务端端口号 65 | */ 66 | public $port; 67 | 68 | /** 69 | * @var string WebSocket Request-URI 70 | */ 71 | public $path = '/'; 72 | 73 | /** 74 | * @var string Header Origin 75 | */ 76 | public $origin = null; 77 | 78 | /** 79 | * @var array 客户端组件类配置,因为 workerman 不支持 php-fpm 运行环境下的同步客户端,所以 workerman 的使用 swoole 驱动的客户端 80 | */ 81 | public $client = [ 82 | 'class' => 'yiiplus\websocket\swoole\WebSocket' 83 | ]; 84 | 85 | /** 86 | * @var string command class name 87 | */ 88 | public $commandClass = Command::class; 89 | 90 | /** 91 | * 对象初始化 92 | */ 93 | public function init() 94 | { 95 | parent::init(); 96 | 97 | if (!isset($this->host)) { 98 | throw new InvalidParamException('Host parameter does not exist.'); 99 | } 100 | 101 | if (!isset($this->port)) { 102 | throw new InvalidParamException('Port parameter does not exist.'); 103 | } 104 | } 105 | 106 | /** 107 | * 向服务器发送数据 108 | * 109 | * @param string $data 发送的数据 110 | * @param string $type 发送的类型 111 | * @param bool $masked 是否设置掩码 112 | * 113 | * @return bool 是否发送成功的状态 114 | */ 115 | public function send($data, $type = 'text', $masked = true) 116 | { 117 | // 创建 swoole WebSocket 客户端发送数据 118 | return Yii::createObject([ 119 | 'class' => $this->client['class'], 120 | 'host' => $this->host, 121 | 'port' => $this->port, 122 | 'path' => $this->path, 123 | 'origin' => $this->origin, 124 | ])->send($data, $type, $masked); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/drivers/workerman/Command.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 1.0.0 25 | */ 26 | class Command extends CliCommand 27 | { 28 | /** 29 | * @var Worker WebSocket Server 30 | */ 31 | protected $_server; 32 | 33 | /** 34 | * @var integer 设置 worker 进程数 35 | */ 36 | public $worker_num = 4; 37 | 38 | /** 39 | * 指定命令行参数,增加 worker_num 参数选项 40 | * 41 | * @param string actionID 42 | * 43 | * @return array 返回指定的参数 44 | */ 45 | public function options($actionID) 46 | { 47 | return array_merge(parent::options($actionID), [ 48 | 'worker_num' 49 | ]); 50 | } 51 | 52 | /** 53 | * 为命令行的参数设置别名 54 | * 55 | * @return array 参数别名键值对 56 | */ 57 | public function optionAliases() 58 | { 59 | return array_merge(parent::optionAliases(), [ 60 | 'w' => 'worker_num' 61 | ]); 62 | } 63 | 64 | /** 65 | * 启动 WebSocket Server 66 | * 67 | * @return null 68 | */ 69 | public function actionStart() 70 | { 71 | $this->_server = new Worker('websocket://' . $this->host . ':' . $this->port); 72 | 73 | $this->_server->count = $this->worker_num; 74 | 75 | $this->_server->onConnect = [$this, 'connect']; 76 | 77 | $this->_server->onMessage = [$this, 'message']; 78 | 79 | $this->_server->onClose = [$this, 'close']; 80 | 81 | echo '[info] websocket service has started, host is ' . $this->host . ' port is ' . $this->port . PHP_EOL; 82 | 83 | // 设置启动参数 84 | global $argv; 85 | $argv[1] = 'start'; 86 | 87 | Worker::runAll(); 88 | } 89 | 90 | /** 91 | * 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数 92 | * 93 | * @param object $connection 客户端连接对象 94 | * 95 | * @return null 96 | */ 97 | public function connect($connection) 98 | { 99 | echo '[info] new connection, fd' . $connection->id . PHP_EOL; 100 | } 101 | 102 | /** 103 | * 当服务器收到来自客户端的数据帧时会回调此函数 104 | * 105 | * @param object $connection 客户端连接对象 106 | * @param string $data 客户端发送的数据 107 | * 108 | * @return bool/null 109 | */ 110 | public function message($connection, $data) 111 | { 112 | $result = $this->triggerMessage($connection->fd, $data); 113 | 114 | if (!$result) { 115 | return false; 116 | } 117 | 118 | list($fds, $data) = $result; 119 | 120 | foreach ($fds as $fd) { 121 | if (!$this->_server->connections[$fd]->send($data)) { 122 | echo '[error] client_id ' . $fd . ' send failure.' . PHP_EOL; 123 | return false; 124 | } 125 | 126 | echo '[success] client_id ' . $fd . ' send success.' . PHP_EOL; 127 | } 128 | } 129 | 130 | /** 131 | * 客户端断开连接 132 | * 133 | * @param object $connection 客户端连接对象 134 | * 135 | * @return null 136 | */ 137 | public function close($connection) 138 | { 139 | $this->triggerClose($connection->id); 140 | 141 | echo '[closed] client '. $connection->id . ' closed' . PHP_EOL; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/drivers/swoole/Command.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 1.0.0 25 | */ 26 | class Command extends CliCommand 27 | { 28 | /** 29 | * @var \Swoole\WebSocket\Server 30 | */ 31 | protected $_server; 32 | 33 | /** 34 | * 启动 WebSocket Server 35 | * 36 | * @return null 37 | */ 38 | public function actionStart() 39 | { 40 | $this->_server = new \Swoole\WebSocket\Server($this->host, $this->port); 41 | 42 | $this->_server->on('handshake', [$this, 'user_handshake']); 43 | 44 | $this->_server->on('open', [$this, 'open']); 45 | 46 | $this->_server->on('message', [$this, 'message']); 47 | 48 | $this->_server->on('close', [$this, 'close']); 49 | 50 | echo '[info] websocket service has started, host is ' . $this->host . ' port is ' . $this->port . PHP_EOL; 51 | 52 | $this->_server->start(); 53 | } 54 | 55 | /** 56 | * WebSocket建立连接后进行握手,通过onHandShake事件回调 57 | * 58 | * @param swoole_http_request $request Websocket请求 59 | * @param swoole_http_response $response Websocket响应 60 | * 61 | * @return bool 握手状态 62 | */ 63 | public function user_handshake(\swoole_http_request $request, \swoole_http_response $response) 64 | { 65 | $sec_websocket_key = $request->header['sec-websocket-key'] ?? null; 66 | 67 | //自定定握手规则,没有设置则用系统内置的(只支持version:13的) 68 | if (!isset($sec_websocket_key)) 69 | { 70 | //'Bad protocol implementation: it is not RFC6455.' 71 | $response->end(); 72 | return false; 73 | } 74 | if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $sec_websocket_key) 75 | || 16 !== strlen(base64_decode($sec_websocket_key)) 76 | ) 77 | { 78 | //Header Sec-WebSocket-Key is illegal; 79 | $response->end(); 80 | return false; 81 | } 82 | 83 | $key = base64_encode(sha1($sec_websocket_key 84 | . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 85 | true)); 86 | $headers = array( 87 | 'Upgrade' => 'websocket', 88 | 'Connection' => 'Upgrade', 89 | 'Sec-WebSocket-Accept' => $key, 90 | 'Sec-WebSocket-Version' => '13', 91 | 'KeepAlive' => 'off', 92 | ); 93 | foreach ($headers as $key => $val) 94 | { 95 | $response->header($key, $val); 96 | } 97 | $response->status(101); 98 | $response->end(); 99 | 100 | echo '[info] handshake success with fd ' . $request->fd . PHP_EOL; 101 | return true; 102 | } 103 | 104 | /** 105 | * 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数;设置onHandShake回调函数后不会再触发onOpen事件,需要应用代码自行处理 106 | * 107 | * @param swoole_websocket_server $server WebSocket Server 108 | * @param swoole_http_response @request Websocket响应 109 | * 110 | * @return null 111 | */ 112 | public function open(\swoole_websocket_server $server, \swoole_http_response $request) 113 | { 114 | echo '[info] new connection, fd' . $request->fd . PHP_EOL; 115 | } 116 | 117 | /** 118 | * 当服务器收到来自客户端的数据帧时会回调此函数 119 | * 120 | * @param object $server WebSocket Server 121 | * @param object $frame frame对象,包含了客户端发来的数据帧信息 122 | * 123 | * @return null/bool 124 | */ 125 | public function message($server, $frame) 126 | { 127 | $result = $this->triggerMessage($frame->fd, $frame->data); 128 | 129 | if (!$result) { 130 | return false; 131 | } 132 | 133 | list($fds, $data) = $result; 134 | 135 | foreach ($fds as $fd) { 136 | if (!$server->push($fd, $data)) { 137 | echo '[error] client_id ' . $fd . ' send failure.' . PHP_EOL; 138 | return false; 139 | } 140 | 141 | echo '[success] client_id ' . $fd . ' send success.' . PHP_EOL; 142 | } 143 | } 144 | 145 | /** 146 | * WebSocket客户端关闭后,在worker进程中回调此函数 147 | * 148 | * @param swoole_websocket_server $server WebSocket Server 149 | * @param integer $fd 连接的文件描述符 150 | * 151 | * @return null 152 | */ 153 | public function close(\Swoole\WebSocket\Server $server, $fd) 154 | { 155 | $this->triggerClose($fd); 156 | 157 | echo '[closed] client '. $fd . ' closed' . PHP_EOL; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/cli/Command.php: -------------------------------------------------------------------------------- 1 | 26 | * @since 1.0.0 27 | */ 28 | abstract class Command extends Controller 29 | { 30 | /** 31 | * @var object WebSocket compoent 32 | */ 33 | public $websocket; 34 | 35 | /** 36 | * @var string WebSocket host 37 | */ 38 | public $host = '0.0.0.0'; 39 | 40 | /** 41 | * @var integer WebSocket 端口号 42 | */ 43 | public $port = 9501; 44 | 45 | /** 46 | * @var string 默认方法 47 | */ 48 | public $defaultAction = 'start'; 49 | 50 | /** 51 | * 指定命令行参数 52 | * 53 | * @param string actionID 54 | * 55 | * @return array 返回指定的参数 56 | */ 57 | public function options($actionID) 58 | { 59 | return [ 60 | 'host', 61 | 'port' 62 | ]; 63 | } 64 | 65 | /** 66 | * 为命令行的参数设置别名 67 | * 68 | * @return array 参数别名键值对 69 | */ 70 | public function optionAliases() 71 | { 72 | return [ 73 | 'h' => 'host', 74 | 'p' => 'port', 75 | ]; 76 | } 77 | 78 | /** 79 | * 启动 WebSocket Server 80 | * 81 | * @return null 82 | */ 83 | abstract public function actionStart(); 84 | 85 | /** 86 | * 获取 WebSocket channel list 87 | * 88 | * @return null 89 | */ 90 | public function actionList() 91 | { 92 | echo 'channels:' . PHP_EOL; 93 | 94 | foreach ($this->websocket->channels as $key => $channel) { 95 | echo ' - ' . $key . PHP_EOL; 96 | } 97 | } 98 | 99 | /** 100 | * 触发指定 channel 下的执行方法 101 | * 102 | * @param interge $fd 客户端连接描述符 103 | * @param miexd $data 传输的数据 104 | * 105 | * @return array 106 | */ 107 | protected function triggerMessage($fd, $data) 108 | { 109 | $class = $this->channelResolve($data); 110 | 111 | if (!$class) { 112 | return false; 113 | } 114 | 115 | $result = call_user_func([$class, 'execute'], $fd, json_decode($data)); 116 | 117 | if (!$result) { 118 | return false; 119 | } 120 | 121 | list($fds, $data) = $result; 122 | 123 | if (!is_array($fds)) { 124 | $fds = [$fds]; 125 | } 126 | 127 | return [$fds, $data]; 128 | } 129 | 130 | /** 131 | * 触发所有 channels 下的 cloos hook 132 | * 133 | * @param integer $fd 客户端文件描述符 134 | * 135 | * @return null 136 | */ 137 | protected function triggerClose($fd) 138 | { 139 | $classNames = $this->websocket->channels; 140 | 141 | foreach ($classNames as $className) { 142 | $class = $this->getClass($className); 143 | call_user_func([$class, 'close'], $fd); 144 | } 145 | } 146 | 147 | /** 148 | * channel 解析 149 | * 150 | * @param json $data 客户端传来的数据 151 | * 152 | * @return object channel 执行类对象 153 | */ 154 | protected function channelResolve($data) 155 | { 156 | // 获取 channel 157 | $data = json_decode($data); 158 | if (!is_object($data) || !property_exists($data, 'channel')) { 159 | echo '[error] missing client data.' . PHP_EOL; 160 | return false; 161 | } 162 | if (!array_key_exists($data->channel, $this->websocket->channels)) { 163 | echo '[error] channel parameter parsing failed.' . PHP_EOL; 164 | return false; 165 | } 166 | 167 | return $this->getClass($this->websocket->channels[$data->channel]); 168 | } 169 | 170 | /** 171 | * 解析类对象 172 | * 173 | * @param string $className 类名,包含命名空间 174 | * 175 | * @return bool|object 返回类对象 176 | * 177 | * @throws \ReflectionException $className Must be a ChannelInterface instance instead. 178 | */ 179 | protected function getClass($className) 180 | { 181 | // 判断 channel 绑定的类是否存在 182 | if (!class_exists($className)) { 183 | echo '[error] ' . $className . ' class not found.' . PHP_EOL; 184 | return false; 185 | } 186 | 187 | // 验证 channel 类是否规范 188 | $reflectionClass = new \ReflectionClass($className); 189 | $class = $reflectionClass->newInstance(); 190 | if (!($class instanceof \yiiplus\websocket\ChannelInterface)) { 191 | echo '[error] ' . $class. ' must be a ChannelInterface instance instead.' . PHP_EOL; 192 | return false; 193 | } 194 | 195 | return $class; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/drivers/swoole/WebSocket.php: -------------------------------------------------------------------------------- 1 | [ 23 | * ... 24 | * 'webslocket' => [ 25 | * 'class' => 'yiiplus\websocket\swoole\WebSocket', 26 | * 'host' => '127.0.0.1', 27 | * 'port' => '9501', 28 | * 'path' => '/', 29 | * 'origin' => null, 30 | * 'channels' => [], 31 | * ], 32 | * ... 33 | * ], 34 | * ``` 35 | * 36 | * @property string $host WebSocket服务端HOST,此参数必须在components配置中设置 37 | * @property integer $port WebSocket服务端端口号,此参数必须在components配置中设置 38 | * @property string $path WebSocket Request-URI,默认为'/',可通过components配置设置此参数 39 | * @property string $origin string Header Origin,默认为null,可通过components配置设置此参数 40 | * @property mixed $returnData 返回数据 41 | * @property mixed $_key Websocket Sec-WebSocket-Key 42 | * @property swoole_client $_socket WebSocket客户端 43 | * @property mixed $_buffer 用于对`recv`方法获取服务器接受到的数据进行缓存 44 | * @property mixed $_connected 链接的状态 45 | * 46 | * @author gengxiankun 47 | * @since 1.0.0 48 | */ 49 | class WebSocket extends CliWebSocket 50 | { 51 | /** 52 | * @const string PHPWebSocket客户端版本号 53 | */ 54 | const VERSION = '0.1.4'; 55 | 56 | /** 57 | * @const integer 生成TOKEN的长度 58 | */ 59 | const TOKEN_LENGHT = 16; 60 | 61 | /** 62 | * @var string WebSocket服务端HOST 63 | */ 64 | public $host; 65 | 66 | /** 67 | * @var integer WebSocket服务端端口号 68 | */ 69 | public $port; 70 | 71 | /** 72 | * @var string WebSocket Request-URI 73 | */ 74 | public $path = '/'; 75 | 76 | /** 77 | * @var string Header Origin 78 | */ 79 | public $origin = null; 80 | 81 | /** 82 | * @var mixed 返回数据 83 | */ 84 | public $returnData = false; 85 | 86 | /** 87 | * @var string command class name 88 | */ 89 | public $commandClass = Command::class; 90 | 91 | /** 92 | * @var string Websocket Sec-WebSocket-Key 93 | * Sec-WebSocket-Key是客户端也就是浏览器或者其他终端随机生成一组16位的随机base64编码 94 | */ 95 | private $_key; 96 | 97 | /** 98 | * @var swoole_client Swoole客户端 99 | */ 100 | private $_socket; 101 | 102 | /** 103 | * @var mixed 用于对recv获取服务器接受到的数据进行缓存 104 | */ 105 | private $_buffer = ''; 106 | 107 | /** 108 | * @var bool 是否链接 109 | */ 110 | private $_connected = false; 111 | 112 | /** 113 | * 对象初始化 114 | */ 115 | public function init() 116 | { 117 | parent::init(); 118 | 119 | if (!isset($this->host)) { 120 | throw new InvalidParamException('Host parameter does not exist.'); 121 | } 122 | 123 | if (!isset($this->port)) { 124 | throw new InvalidParamException('Port parameter does not exist.'); 125 | } 126 | 127 | $this->_key = $this->generateToken(self::TOKEN_LENGHT); 128 | } 129 | 130 | /** 131 | * 将客户端连接到服务器 132 | * 133 | * @return $this 134 | */ 135 | protected function connect() 136 | { 137 | // 建立连接 138 | $this->_socket = new \swoole_client(SWOOLE_SOCK_TCP); 139 | if (!$this->_socket->connect($this->host, $this->port)) { 140 | return false; 141 | } 142 | // 握手确认 143 | $this->_socket->send($this->createHeader()); 144 | return $this->recv(); 145 | } 146 | 147 | /** 148 | * 向服务器发送数据 149 | * 150 | * @param string $data 发送的数据 151 | * @param string $type 发送的类型 152 | * @param bool $masked 是否设置掩码 153 | * 154 | * @return bool 是否发送成功的状态 155 | */ 156 | public function send($data, $type = 'text', $masked = true) 157 | { 158 | $this->connect(); 159 | 160 | switch($type) 161 | { 162 | case 'text': 163 | $_type = WEBSOCKET_OPCODE_TEXT; 164 | break; 165 | case 'binary': 166 | case 'bin': 167 | $_type = WEBSOCKET_OPCODE_BINARY; 168 | break; 169 | case 'ping': 170 | $_type = WEBSOCKET_OPCODE_PING; 171 | break; 172 | default: 173 | return false; 174 | } 175 | 176 | // 将WebSocket消息打包并发送 177 | return $this->_socket->send(\swoole_websocket_server::pack(json_encode($data), $_type, true, $masked)); 178 | } 179 | 180 | /** 181 | * 为WebSocket客户端创建Header 182 | * 183 | * @return string 184 | */ 185 | private function createHeader() 186 | { 187 | $host = $this->host; 188 | if ($host === '127.0.0.1' || $host === '0.0.0.0') 189 | { 190 | $host = 'localhost'; 191 | } 192 | 193 | return "GET {$this->path} HTTP/1.1" . "\r\n" . 194 | "Origin: {$this->origin}" . "\r\n" . 195 | "Host: {$host}:{$this->port}" . "\r\n" . 196 | "Sec-WebSocket-Key: {$this->_key}" . "\r\n" . 197 | "User-Agent: PHPWebSocketClient/" . self::VERSION . "\r\n" . 198 | "Upgrade: websocket" . "\r\n" . 199 | "Connection: Upgrade" . "\r\n" . 200 | "Sec-WebSocket-Protocol: wamp" . "\r\n" . 201 | "Sec-WebSocket-Version: 13" . "\r\n" . "\r\n"; 202 | } 203 | 204 | /** 205 | * 从服务器端接收数据 206 | * 207 | * @return mixed 208 | */ 209 | public function recv() 210 | { 211 | $data = $this->_socket->recv(); 212 | if ($data === false) 213 | { 214 | echo "Error: {$this->_socket->errMsg}"; 215 | return false; 216 | } 217 | $this->_buffer .= $data; 218 | $recv_data = $this->parseData($this->_buffer); 219 | if ($recv_data) 220 | { 221 | $this->_buffer = ''; 222 | return $recv_data; 223 | } 224 | else 225 | { 226 | return false; 227 | } 228 | } 229 | 230 | /** 231 | * 解析收到的数据 232 | * 233 | * @param $response 相应数据 234 | * 235 | * @return 236 | */ 237 | private function parseData($response) 238 | { 239 | if (!$this->_connected) 240 | { 241 | // 确认请求来自WebSocket 242 | $response = $this->parseIncomingRaw($response); 243 | if (isset($response['Sec-Websocket-Accept']) 244 | && base64_encode(pack('H*', sha1($this->_key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))) === $response['Sec-Websocket-Accept'] 245 | ) 246 | { 247 | $this->_connected = true; 248 | return true; 249 | } 250 | else 251 | { 252 | throw new \Exception("error response key."); 253 | } 254 | } 255 | 256 | // 解析WebSocket数据帧,@link: https://wiki.swoole.com/wiki/page/798.html 257 | $frame = \swoole_websocket_server::unpack($response); 258 | if ($frame) 259 | { 260 | return $this->returnData ? $frame->data : $frame; 261 | } 262 | else 263 | { 264 | throw new \Exception("swoole_websocket_server::unpack failed."); 265 | } 266 | } 267 | 268 | /** 269 | * 解析传入数据 270 | * 271 | * @param $response 相应数据 272 | * 273 | * @return array 返回解析的数据 274 | */ 275 | private function parseIncomingRaw($response) 276 | { 277 | $retval = array(); 278 | $content = ""; 279 | $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $response)); 280 | foreach ($fields as $field) 281 | { 282 | if (preg_match('/([^:]+): (.+)/m', $field, $match)) 283 | { 284 | $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', 285 | function ($matches) 286 | { 287 | return strtoupper($matches[0]); 288 | }, 289 | strtolower(trim($match[1]))); 290 | if (isset($retval[$match[1]])) 291 | { 292 | $retval[$match[1]] = array($retval[$match[1]], $match[2]); 293 | } 294 | else 295 | { 296 | $retval[$match[1]] = trim($match[2]); 297 | } 298 | } 299 | else 300 | { 301 | if (preg_match('!HTTP/1\.\d (\d)* .!', $field)) 302 | { 303 | $retval["status"] = $field; 304 | } 305 | else 306 | { 307 | $content .= $field . "\r\n"; 308 | } 309 | } 310 | } 311 | $retval['content'] = $content; 312 | return $retval; 313 | } 314 | 315 | /** 316 | * 生成Token 317 | * 318 | * @param int $length 生成Token的长度,默认16位 319 | * 320 | * @return string 返回生成的Token值 321 | */ 322 | private function generateToken($length) 323 | { 324 | $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'; 325 | $useChars = array(); 326 | // 生成一些随机字符: 327 | for ($i = 0; $i < $length; $i++) 328 | { 329 | $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; 330 | } 331 | // 添加数字 332 | array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); 333 | shuffle($useChars); 334 | $randomString = trim(implode('', $useChars)); 335 | $randomString = substr($randomString, 0, self::TOKEN_LENGHT); 336 | return base64_encode($randomString); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [gengxiankun] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------