├── 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 | [](https://packagist.org/packages/yiiplus/yii2-websocket)
6 | [](https://packagist.org/packages/yiiplus/yii2-websocket)
7 | [](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 |
--------------------------------------------------------------------------------