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成了