• WebSocket控制器
    • 新人帮助
    • 实现命令解析
    • 注册服务
    • 测试前端代码
    • 测试用HttpController 视图控制器
    • WebSocket 控制器
    • 测试
    • 扩展
      • 自定义解析器
      • 自定义握手
      • 自定义关闭事件
      • 支持Wss

    WebSocket控制器

    参考Demo: WebSocketController

    EasySwoole 3.x支持以控制器模式来开发你的代码。

    首先,修改项目根目录下配置文件dev.php,修改SERVER_TYPE为:

    1. 'SERVER_TYPE' => EASYSWOOLE_WEB_SOCKET_SERVER,

    并且引入 easyswoole/socket composer 包:

    composer require easyswoole/socket警告:请保证你安装的 easyswoole/socket 版本大 >= 1.0.7 否则会导致ws消息发送客户端无法解析的问题

    新人帮助

    • 本文遵循PSR-4自动加载类规范,如果你还不了解这个规范,请先学习相关规则。
    • 本节基础命名空间App 默认指项目根目录下App文件夹,如果你的App指向不同,请自行替换。
    • 只要遵循PSR-4规范,无论你怎么组织文件结构都没问题,本节只做简单示例。

    实现命令解析

    新人提示

    这里的命令解析,其意思为根据请求信息解析为具体的执行命令;

    在easyswoole中,可以让WebSocket像传统框架那样按照控制器->方法这样去解析请求;

    需要实现EasySwoole\Socket\AbstractInterface\ParserInterface;接口中的decode 和encode方法;

    创建App/WebSocket/WebSocketParser.php文件,写入以下代码

    1. namespace App\WebSocket;
    2. use EasySwoole\Socket\AbstractInterface\ParserInterface;
    3. use EasySwoole\Socket\Client\WebSocket;
    4. use EasySwoole\Socket\Bean\Caller;
    5. use EasySwoole\Socket\Bean\Response;
    6. /**
    7. * Class WebSocketParser
    8. *
    9. * 此类是自定义的 websocket 消息解析器
    10. * 此处使用的设计是使用 json string 作为消息格式
    11. * 当客户端消息到达服务端时,会调用 decode 方法进行消息解析
    12. * 会将 websocket 消息 转成具体的 Class -> Action 调用 并且将参数注入
    13. *
    14. * @package App\WebSocket
    15. */
    16. class WebSocketParser implements ParserInterface
    17. {
    18. /**
    19. * decode
    20. * @param string $raw 客户端原始消息
    21. * @param WebSocket $client WebSocket Client 对象
    22. * @return Caller Socket 调用对象
    23. */
    24. public function decode($raw, $client) : ? Caller
    25. {
    26. // 解析 客户端原始消息
    27. $data = json_decode($raw, true);
    28. if (!is_array($data)) {
    29. echo "decode message error! \n";
    30. return null;
    31. }
    32. // new 调用者对象
    33. $caller = new Caller();
    34. /**
    35. * 设置被调用的类 这里会将ws消息中的 class 参数解析为具体想访问的控制器
    36. * 如果更喜欢 event 方式 可以自定义 event 和具体的类的 map 即可
    37. * 注 目前 easyswoole 3.0.4 版本及以下 不支持直接传递 class string 可以通过这种方式
    38. */
    39. $class = '\\App\\WebSocket\\'. ucfirst($data['class'] ?? 'Index');
    40. $caller->setControllerClass($class);
    41. // 提供一个事件风格的写法
    42. // $eventMap = [
    43. // 'index' => Index::class
    44. // ];
    45. // $caller->setControllerClass($eventMap[$data['class']] ?? Index::class);
    46. // 设置被调用的方法
    47. $caller->setAction($data['action'] ?? 'index');
    48. // 检查是否存在args
    49. if (isset($data['content']) && is_array($data['content'])) {
    50. $args = $data['content'];
    51. }
    52. // 设置被调用的Args
    53. $caller->setArgs($args ?? []);
    54. return $caller;
    55. }
    56. /**
    57. * encode
    58. * @param Response $response Socket Response 对象
    59. * @param WebSocket $client WebSocket Client 对象
    60. * @return string 发送给客户端的消息
    61. */
    62. public function encode(Response $response, $client) : ? string
    63. {
    64. /**
    65. * 这里返回响应给客户端的信息
    66. * 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理
    67. */
    68. return $response->getMessage();
    69. }
    70. }

    注意,请按照你实际的规则实现,本测试代码与前端代码对应。

    注册服务

    新人提示

    如果你尚未明白easyswoole运行机制,那么这里你简单理解为,当easyswoole运行到一定时刻,会执行以下方法。

    这里是指注册你上面实现的解析器。

    在根目录下EasySwooleEvent.php文件mainServerCreate方法下加入以下代码

    1. //注意:在此文件引入以下命名空间
    2. use EasySwoole\Socket\Dispatcher;
    3. use App\WebSocket\WebSocketParser;
    4. public static function mainServerCreate(EventRegister $register): void
    5. {
    6. /**
    7. * **************** websocket控制器 **********************
    8. */
    9. // 创建一个 Dispatcher 配置
    10. $conf = new \EasySwoole\Socket\Config();
    11. // 设置 Dispatcher 为 WebSocket 模式
    12. $conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);
    13. // 设置解析器对象
    14. $conf->setParser(new WebSocketParser());
    15. // 创建 Dispatcher 对象 并注入 config 对象
    16. $dispatch = new Dispatcher($conf);
    17. // 给server 注册相关事件 在 WebSocket 模式下 on message 事件必须注册 并且交给 Dispatcher 对象处理
    18. $register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) use ($dispatch) {
    19. $dispatch->dispatch($server, $frame->data, $frame);
    20. });
    21. }

    在EasySwooleEvent中注册该服务。

    测试前端代码

    友情提示

    easyswoole 提供了更强大的WebSocket调试工具,[foo]: http://www.evalor.cn/websocket.html ‘WEBSOCKET CLIENT’;

    创建App/HttpController/websocket.html文件,写入以下代码

    1. <html>
    2. <head>
    3. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    4. </head>
    5. <body>
    6. <div>
    7. <div>
    8. <p>info below</p>
    9. <ul id="line">
    10. </ul>
    11. </div>
    12. <div>
    13. <select id="action">
    14. <option value="who">who</option>
    15. <option value="hello">hello</option>
    16. <option value="delay">delay</option>
    17. <option value="404">404</option>
    18. </select>
    19. <input type="text" id="says">
    20. <button onclick="say()">发送</button>
    21. </div>
    22. </div>
    23. </body>
    24. <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    25. <script>
    26. var wsServer = 'ws://127.0.0.1:9501';
    27. var websocket = new WebSocket(wsServer);
    28. window.onload = function () {
    29. websocket.onopen = function (evt) {
    30. addLine("Connected to WebSocket server.");
    31. };
    32. websocket.onclose = function (evt) {
    33. addLine("Disconnected");
    34. };
    35. websocket.onmessage = function (evt) {
    36. addLine('Retrieved data from server: ' + evt.data);
    37. };
    38. websocket.onerror = function (evt, e) {
    39. addLine('Error occured: ' + evt.data);
    40. };
    41. };
    42. function addLine(data) {
    43. $("#line").append("<li>"+data+"</li>");
    44. }
    45. function say() {
    46. var content = $("#says").val();
    47. var action = $("#action").val();
    48. $("#says").val('');
    49. websocket.send(JSON.stringify({
    50. action:action,
    51. content:content
    52. }));
    53. }
    54. </script>
    55. </html>

    测试用HttpController 视图控制器

    新人提示

    这里仅提供了前端基本的示例代码,更多需求根据自己业务逻辑设计

    创建App/HttpController/WebSocket.php文件,写入以下代码

    1. namespace App\HttpController;
    2. use EasySwoole\Http\AbstractInterface\Controller;
    3. use EasySwoole\EasySwoole\ServerManager;
    4. /**
    5. * Class WebSocket
    6. *
    7. * 此类是通过 http 请求来调用具体的事件
    8. * 实际生产中需要自行管理 fd -> user 的关系映射,这里不做详细解释
    9. *
    10. * @package App\HttpController
    11. */
    12. class WebSocket extends Controller
    13. {
    14. /**
    15. * 默认的 websocket 测试页
    16. */
    17. public function index()
    18. {
    19. $content = file_get_contents(__DIR__ . '/websocket.html');
    20. $this->response()->write($content);
    21. $this->response()->end();
    22. }
    23. }

    本控制器主要为方便你获得前端页面和从HTTP请求中对websocket 做推送。

    WebSocket 控制器

    新人提示

    WebSocket控制器必须继承EasySwoole\Socket\AbstractInterface\Controller;

    actionNotFound方法提供了当找不到该方法时的返回信息,默认会传入本次请求的actionName。

    创建App/WebSocket/Index.php文件,写入以下内容

    1. namespace App\WebSocket;
    2. use EasySwoole\EasySwoole\ServerManager;
    3. use EasySwoole\EasySwoole\Swoole\Task\TaskManager;
    4. use EasySwoole\Socket\AbstractInterface\Controller;
    5. /**
    6. * Class Index
    7. *
    8. * 此类是默认的 websocket 消息解析后访问的 控制器
    9. *
    10. * @package App\WebSocket
    11. */
    12. class Index extends Controller
    13. {
    14. function hello()
    15. {
    16. $this->response()->setMessage('call hello with arg:'. json_encode($this->caller()->getArgs()));
    17. }
    18. public function who(){
    19. $this->response()->setMessage('your fd is '. $this->caller()->getClient()->getFd());
    20. }
    21. function delay()
    22. {
    23. $this->response()->setMessage('this is delay action');
    24. $client = $this->caller()->getClient();
    25. // 异步推送, 这里直接 use fd也是可以的
    26. TaskManager::async(function () use ($client){
    27. $server = ServerManager::getInstance()->getSwooleServer();
    28. $i = 0;
    29. while ($i < 5) {
    30. sleep(1);
    31. $server->push($client->getFd(),'push in http at '. date('H:i:s'));
    32. $i++;
    33. }
    34. });
    35. }
    36. }

    测试

    如果你按照本文配置,那么你的文件结构应该是以下形式

    App
    ├── HttpController
    │ ├── websocket.html
    │ └── WebSocket.php
    ├── Websocket
    │ └── Index.php
    └── └── WebSocketParser.php

    首先在根目录运行easyswoole

    php easyswoole start

    如果没有错误此时已经启动了easyswoole服务;访问 127.0.0.1:9501/WebSocket/index 可以看到之前写的测试html文件;新人提示:这种访问方式会请求HttpController控制器下Index.php中的index方法

    扩展

    自定义解析器

    在上文的 WebSocketParser.php 中,已经实现了一个简单解析器;
    我们可以通过自定义解析器,实现自己需要的场景。

    1. /**
    2. * decode
    3. * @param string $raw 客户端原始消息
    4. * @param WebSocket $client WebSocket Client 对象
    5. * @return Caller Socket 调用对象
    6. */
    7. public function decode($raw, $client) : ? Caller
    8. {
    9. // 解析 客户端原始消息
    10. $data = json_decode($raw, true);
    11. if (!is_array($data)) {
    12. echo "decode message error! \n";
    13. return null;
    14. }
    15. // new 调用者对象
    16. $caller = new Caller();
    17. /**
    18. * 设置被调用的类 这里会将ws消息中的 class 参数解析为具体想访问的控制器
    19. * 如果更喜欢 event 方式 可以自定义 event 和具体的类的 map 即可
    20. * 注 目前 easyswoole 3.0.4 版本及以下 不支持直接传递 class string 可以通过这种方式
    21. */
    22. $class = '\\App\\WebSocket\\'. ucfirst($data['class'] ?? 'Index');
    23. $caller->setControllerClass($class);
    24. // 提供一个事件风格的写法
    25. // $eventMap = [
    26. // 'index' => Index::class
    27. // ];
    28. // $caller->setControllerClass($eventMap[$data['class']] ?? Index::class);
    29. // 设置被调用的方法
    30. $caller->setAction($data['action'] ?? 'index');
    31. // 检查是否存在args
    32. if (isset($data['content']) && is_array($data['content'])) {
    33. $args = $data['content'];
    34. }
    35. // 设置被调用的Args
    36. $caller->setArgs($args ?? []);
    37. return $caller;
    38. }
    39. /**
    40. * encode
    41. * @param Response $response Socket Response 对象
    42. * @param WebSocket $client WebSocket Client 对象
    43. * @return string 发送给客户端的消息
    44. */
    45. public function encode(Response $response, $client) : ? string
    46. {
    47. /**
    48. * 这里返回响应给客户端的信息
    49. * 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理
    50. */
    51. return $response->getMessage();
    52. }

    例如{“class”:”Index”,”action”:”hello”}则会访问App/WebSocket/WebSocket/Index.php 并执行hello方法

    当然这里是举例,你可以根据自己的业务场景进行设计

    自定义握手

    在常见业务场景中,我们通常需要验证客户端的身份,所以可以通过自定义WebSocket握手规则来完成。

    创建App/WebSocket/WebSocketEvent.php文件,写入以下内容

    1. namespace App\WebSocket;
    2. /**
    3. * Class WebSocketEvent
    4. *
    5. * 此类是 WebSocet 中一些非强制的自定义事件处理
    6. *
    7. * @package App\WebSocket
    8. */
    9. class WebSocketEvent
    10. {
    11. /**
    12. * 握手事件
    13. *
    14. * @param \swoole_http_request $request
    15. * @param \swoole_http_response $response
    16. * @return bool
    17. */
    18. public function onHandShake(\swoole_http_request $request, \swoole_http_response $response)
    19. {
    20. /** 此处自定义握手规则 返回 false 时中止握手 */
    21. if (!$this->customHandShake($request, $response)) {
    22. $response->end();
    23. return false;
    24. }
    25. /** 此处是 RFC规范中的WebSocket握手验证过程 必须执行 否则无法正确握手 */
    26. if ($this->secWebsocketAccept($request, $response)) {
    27. $response->end();
    28. return true;
    29. }
    30. $response->end();
    31. return false;
    32. }
    33. /**
    34. * 自定义握手事件
    35. *
    36. * @param \swoole_http_request $request
    37. * @param \swoole_http_response $response
    38. * @return bool
    39. */
    40. protected function customHandShake(\swoole_http_request $request, \swoole_http_response $response): bool
    41. {
    42. /**
    43. * 这里可以通过 http request 获取到相应的数据
    44. * 进行自定义验证后即可
    45. * (注) 浏览器中 JavaScript 并不支持自定义握手请求头 只能选择别的方式 如get参数
    46. */
    47. $headers = $request->header;
    48. $cookie = $request->cookie;
    49. // if (如果不满足我某些自定义的需求条件,返回false,握手失败) {
    50. // return false;
    51. // }
    52. return true;
    53. }
    54. /**
    55. * RFC规范中的WebSocket握手验证过程
    56. * 以下内容必须强制使用
    57. *
    58. * @param \swoole_http_request $request
    59. * @param \swoole_http_response $response
    60. * @return bool
    61. */
    62. protected function secWebsocketAccept(\swoole_http_request $request, \swoole_http_response $response): bool
    63. {
    64. // ws rfc 规范中约定的验证过程
    65. if (!isset($request->header['sec-websocket-key'])) {
    66. // 需要 Sec-WebSocket-Key 如果没有拒绝握手
    67. var_dump('shake fai1 3');
    68. return false;
    69. }
    70. if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $request->header['sec-websocket-key'])
    71. || 16 !== strlen(base64_decode($request->header['sec-websocket-key']))
    72. ) {
    73. //不接受握手
    74. var_dump('shake fai1 4');
    75. return false;
    76. }
    77. $key = base64_encode(sha1($request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
    78. $headers = array(
    79. 'Upgrade' => 'websocket',
    80. 'Connection' => 'Upgrade',
    81. 'Sec-WebSocket-Accept' => $key,
    82. 'Sec-WebSocket-Version' => '13',
    83. 'KeepAlive' => 'off',
    84. );
    85. if (isset($request->header['sec-websocket-protocol'])) {
    86. $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
    87. }
    88. // 发送验证后的header
    89. foreach ($headers as $key => $val) {
    90. $response->header($key, $val);
    91. }
    92. // 接受握手 还需要101状态码以切换状态
    93. $response->status(101);
    94. var_dump('shake success at fd :' . $request->fd);
    95. return true;
    96. }
    97. }

    在根目录下EasySwooleEvent.php文件mainServerCreate方法下加入以下代码

    1. //注意:在此文件引入以下命名空间
    2. use EasySwoole\Socket\Dispatcher;
    3. use App\WebSocket\WebSocketParser;
    4. use App\WebSocket\WebSocketEvent;
    5. public static function mainServerCreate(EventRegister $register): void
    6. {
    7. /**
    8. * **************** websocket控制器 **********************
    9. */
    10. // 创建一个 Dispatcher 配置
    11. $conf = new \EasySwoole\Socket\Config();
    12. // 设置 Dispatcher 为 WebSocket 模式
    13. $conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);
    14. // 设置解析器对象
    15. $conf->setParser(new WebSocketParser());
    16. // 创建 Dispatcher 对象 并注入 config 对象
    17. $dispatch = new Dispatcher($conf);
    18. // 给server 注册相关事件 在 WebSocket 模式下 on message 事件必须注册 并且交给 Dispatcher 对象处理
    19. $register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) use ($dispatch) {
    20. $dispatch->dispatch($server, $frame->data, $frame);
    21. });
    22. //自定义握手事件
    23. $websocketEvent = new WebSocketEvent();
    24. $register->set(EventRegister::onHandShake, function (\swoole_http_request $request, \swoole_http_response $response) use ($websocketEvent) {
    25. $websocketEvent->onHandShake($request, $response);
    26. });
    27. }

    自定义关闭事件

    在常见业务场景中,我们通常需要在用户断开或者服务器主动断开连接时设置回调事件。

    创建App/WebSocket/WebSocketEvent.php文件,增加以下内容

    1. /**
    2. * 关闭事件
    3. *
    4. * @param \swoole_server $server
    5. * @param int $fd
    6. * @param int $reactorId
    7. */
    8. public function onClose(\swoole_server $server, int $fd, int $reactorId)
    9. {
    10. /** @var array $info */
    11. $info = $server->getClientInfo($fd);
    12. /**
    13. * 判断此fd 是否是一个有效的 websocket 连接
    14. * 参见 https://wiki.swoole.com/wiki/page/490.html
    15. */
    16. if ($info && $info['websocket_status'] === WEBSOCKET_STATUS_FRAME) {
    17. /**
    18. * 判断连接是否是 server 主动关闭
    19. * 参见 https://wiki.swoole.com/wiki/page/p-event/onClose.html
    20. */
    21. if ($reactorId < 0) {
    22. echo "server close \n";
    23. }
    24. }
    25. }

    在根目录下EasySwooleEvent.php文件mainServerCreate方法下加入以下代码

    1. /**
    2. * **************** websocket控制器 **********************
    3. */
    4. // 创建一个 Dispatcher 配置
    5. $conf = new \EasySwoole\Socket\Config();
    6. // 设置 Dispatcher 为 WebSocket 模式
    7. $conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);
    8. // 设置解析器对象
    9. $conf->setParser(new WebSocketParser());
    10. // 创建 Dispatcher 对象 并注入 config 对象
    11. $dispatch = new Dispatcher($conf);
    12. // 给server 注册相关事件 在 WebSocket 模式下 on message 事件必须注册 并且交给 Dispatcher 对象处理
    13. $register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) use ($dispatch) {
    14. $dispatch->dispatch($server, $frame->data, $frame);
    15. });
    16. //自定义握手事件
    17. $websocketEvent = new WebSocketEvent();
    18. $register->set(EventRegister::onHandShake, function (\swoole_http_request $request, \swoole_http_response $response) use ($websocketEvent) {
    19. $websocketEvent->onHandShake($request, $response);
    20. });
    21. //自定义关闭事件
    22. $register->set(EventRegister::onClose, function (\swoole_server $server, int $fd, int $reactorId) use ($websocketEvent) {
    23. $websocketEvent->onClose($server, $fd, $reactorId);
    24. });

    支持Wss

    这里推荐使用Nginx反向代理解决wss问题。

    即客户端通过wss协议连接 Nginx 然后 Nginx 通过ws协议和server通讯。
    也就是说Nginx负责通讯加解密,Nginx到server是明文的,swoole不用开启ssl,而且还能隐藏服务器端口和负载均衡(何乐不为)。

    1. server {
    2. # 下面这个部分和你https的配置没有什么区别,如果你是 宝塔 或者是 oneinstack 这里用生成的也是没有任何问题的
    3. listen 443;
    4. server_name 这里是你申请的域名;
    5. ssl on;
    6. # 这里是你申请域名对应的证书(一定要注意路径的问题,建议绝对路径)
    7. ssl_certificate 你的证书.crt;
    8. ssl_certificate_key 你的密匙.key;
    9. ssl_session_timeout 5m;
    10. ssl_session_cache shared:SSL:10m;
    11. ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv2 SSLv3;
    12. ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    13. ssl_prefer_server_ciphers on;
    14. ssl_verify_client off;
    15. # 下面这个部分其实就是反向代理 如果你是 宝塔 或者是 oneinstack 请把你后续检查.php相关的 和重写index.php的部分删除
    16. location / {
    17. proxy_redirect off;
    18. proxy_pass http://127.0.0.1:9501; # 转发到你本地的9501端口 这里要根据你的业务情况填写 谢谢
    19. proxy_set_header Host $host;
    20. proxy_set_header X-Real_IP $remote_addr;
    21. proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
    22. proxy_http_version 1.1;
    23. proxy_set_header Upgrade $http_upgrade; # 升级协议头
    24. proxy_set_header Connection upgrade;
    25. }
    26. }

    重启nginx 如果没有错误
    点我打开ws调试工具;

    服务地址输入wss://你上面的域名不加端口号谢谢

    点击开启连接 恭喜你 wss成了