title: 用php基于swoole实现websocket通讯的聊天室 meta:

  • name: description content: 用php基于swoole实现websocket通讯的聊天室
  • name: keywords content: swoole|swoole 拓展|swoole 框架|EasySwoole Socket|swoole socket|swoole websocket|swoole tcp|swoole udp|php websocket

前言

大多人都习惯用PHP做WEB编程,很少有人用php实现websocket通讯,因为在PHP中,从socket的连接、建立、绑定、监听等都需要开发者自己去操作完成,对于初学者来说,难度方面也挺大的,因此我们用Easyswoole这个框架来实现聊天室的建立。

1、socket协议的简介

2、介绍client与server之间的连接原理

3、PHP中建立socket的过程讲解

4、用一个聊天室作为实例详细讲解在PHP中如何使用socket

WebSocket控制器

::: warning 参考Demo: WebSocketController :::

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

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

  1. 'SERVER_TYPE' => EASYSWOOLE_WEB_SOCKET_SERVER,

并且引入 easyswoole/socket composer 包:

::: warning composer require easyswoole/socket :::

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

新人帮助

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

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

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

::: danger 请先阅读TCP控制器实现章节,将以简明的文字讲述原理,以下代码较多,主要提供示例。 :::

::: warning 解析器需要实现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 (!empty($data['content'])) {
  50. // content 无法解析为array 时 返回 content => string 格式
  51. $args = is_array($data['content']) ? $data['content'] : ['content' => $data['content']];
  52. }
  53. // 设置被调用的Args
  54. $caller->setArgs($args ?? []);
  55. return $caller;
  56. }
  57. /**
  58. * encode
  59. * @param Response $response Socket Response 对象
  60. * @param WebSocket $client WebSocket Client 对象
  61. * @return string 发送给客户端的消息
  62. */
  63. public function encode(Response $response, $client) : ? string
  64. {
  65. /**
  66. * 这里返回响应给客户端的信息
  67. * 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理
  68. */
  69. return $response->getMessage();
  70. }
  71. }

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

注册服务

新人提示

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

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

在根目录下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. }

::: warning 在EasySwooleEvent中注册该服务。 :::

前端测试

友情提示

::: warning easyswoole 提供了强大的WebSocket调试工具:WEBSOCKET CLIEN; :::

WebSocket 控制器

新人提示

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

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

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

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

::: warning 该控制器使用了task组件:https://www.easyswoole.com/Cn/Components/task.html :::

::: warning composer require easyswoole/task :::

测试

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

  1. App
  2. ├── HttpController
  3. ├── websocket.html
  4. └── WebSocket.php
  5. ├── Websocket
  6. └── Index.php
  7. └── └── WebSocketParser.php

首先在根目录运行easyswoole

  1. php easyswoole start

如果没有错误此时已经启动了easyswoole服务;
访问 127.0.0.1:9501/WebSocket/index 可以看到之前写的测试html文件;

::: warning 新人提示:这种访问方式会请求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 (!empty($data['content'])) {
  33. // content 无法解析为array 时 返回 content => string 格式
  34. $args = is_array($data['content']) ? $data['content'] : ['content' => $data['content']];
  35. }
  36. // 设置被调用的Args
  37. $caller->setArgs($args ?? []);
  38. return $caller;
  39. }
  40. /**
  41. * encode
  42. * @param Response $response Socket Response 对象
  43. * @param WebSocket $client WebSocket Client 对象
  44. * @return string 发送给客户端的消息
  45. */
  46. public function encode(Response $response, $client) : ? string
  47. {
  48. /**
  49. * 这里返回响应给客户端的信息
  50. * 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理
  51. */
  52. return $response->getMessage();
  53. }

::: warning 例如{“class”:”Index”,”action”:”hello”}
:::

::: warning 则会访问App/WebSocket/WebSocket/Index.php 并执行hello方法 :::

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