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为:
'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文件,写入以下代码
namespace App\WebSocket;use EasySwoole\Socket\AbstractInterface\ParserInterface;use EasySwoole\Socket\Client\WebSocket;use EasySwoole\Socket\Bean\Caller;use EasySwoole\Socket\Bean\Response;/*** Class WebSocketParser** 此类是自定义的 websocket 消息解析器* 此处使用的设计是使用 json string 作为消息格式* 当客户端消息到达服务端时,会调用 decode 方法进行消息解析* 会将 websocket 消息 转成具体的 Class -> Action 调用 并且将参数注入** @package App\WebSocket*/class WebSocketParser implements ParserInterface{/*** decode* @param string $raw 客户端原始消息* @param WebSocket $client WebSocket Client 对象* @return Caller Socket 调用对象*/public function decode($raw, $client) : ? Caller{// 解析 客户端原始消息$data = json_decode($raw, true);if (!is_array($data)) {echo "decode message error! \n";return null;}// new 调用者对象$caller = new Caller();/*** 设置被调用的类 这里会将ws消息中的 class 参数解析为具体想访问的控制器* 如果更喜欢 event 方式 可以自定义 event 和具体的类的 map 即可* 注 目前 easyswoole 3.0.4 版本及以下 不支持直接传递 class string 可以通过这种方式*/$class = '\\App\\WebSocket\\'. ucfirst($data['class'] ?? 'Index');$caller->setControllerClass($class);// 提供一个事件风格的写法// $eventMap = [// 'index' => Index::class// ];// $caller->setControllerClass($eventMap[$data['class']] ?? Index::class);// 设置被调用的方法$caller->setAction($data['action'] ?? 'index');// 检查是否存在argsif (!empty($data['content'])) {// content 无法解析为array 时 返回 content => string 格式$args = is_array($data['content']) ? $data['content'] : ['content' => $data['content']];}// 设置被调用的Args$caller->setArgs($args ?? []);return $caller;}/*** encode* @param Response $response Socket Response 对象* @param WebSocket $client WebSocket Client 对象* @return string 发送给客户端的消息*/public function encode(Response $response, $client) : ? string{/*** 这里返回响应给客户端的信息* 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理*/return $response->getMessage();}}
::: warning 注意,请按照你实际的规则实现,本测试代码与前端代码对应。 :::
注册服务
新人提示
::: warning 如果你尚未明白easyswoole运行机制,那么这里你简单理解为,当easyswoole运行到一定时刻,会执行以下方法。 :::
::: warning 这里是指注册你上面实现的解析器。 :::
在根目录下EasySwooleEvent.php文件mainServerCreate方法下加入以下代码
//注意:在此文件引入以下命名空间use EasySwoole\Socket\Dispatcher;use App\WebSocket\WebSocketParser;public static function mainServerCreate(EventRegister $register): void{/*** **************** websocket控制器 ***********************/// 创建一个 Dispatcher 配置$conf = new \EasySwoole\Socket\Config();// 设置 Dispatcher 为 WebSocket 模式$conf->setType(\EasySwoole\Socket\Config::WEB_SOCKET);// 设置解析器对象$conf->setParser(new WebSocketParser());// 创建 Dispatcher 对象 并注入 config 对象$dispatch = new Dispatcher($conf);// 给server 注册相关事件 在 WebSocket 模式下 on message 事件必须注册 并且交给 Dispatcher 对象处理$register->set(EventRegister::onMessage, function (\swoole_websocket_server $server, \swoole_websocket_frame $frame) use ($dispatch) {$dispatch->dispatch($server, $frame->data, $frame);});}
::: warning 在EasySwooleEvent中注册该服务。 :::
前端测试
友情提示
::: warning easyswoole 提供了强大的WebSocket调试工具:WEBSOCKET CLIEN; :::
WebSocket 控制器
新人提示
::: warning WebSocket控制器必须继承EasySwoole\Socket\AbstractInterface\Controller; :::
::: warning actionNotFound方法提供了当找不到该方法时的返回信息,默认会传入本次请求的actionName。 :::
创建App/WebSocket/Index.php文件,写入以下内容
<?php/*** Created by PhpStorm.* User: Apple* Date: 2018/11/1 0001* Time: 14:42*/namespace App\WebSocket;use EasySwoole\EasySwoole\ServerManager;use EasySwoole\EasySwoole\Task\TaskManager;use EasySwoole\Socket\AbstractInterface\Controller;/*** Class Index** 此类是默认的 websocket 消息解析后访问的 控制器** @package App\WebSocket*/class Index extends Controller{function hello(){$this->response()->setMessage('call hello with arg:'. json_encode($this->caller()->getArgs()));}public function who(){$this->response()->setMessage('your fd is '. $this->caller()->getClient()->getFd());}function delay(){$this->response()->setMessage('this is delay action');$client = $this->caller()->getClient();// 异步推送, 这里直接 use fd也是可以的TaskManager::getInstance()->async(function () use ($client){$server = ServerManager::getInstance()->getSwooleServer();$i = 0;while ($i < 5) {sleep(1);$server->push($client->getFd(),'push in http at '. date('H:i:s'));$i++;}});}}
::: warning 该控制器使用了task组件:https://www.easyswoole.com/Cn/Components/task.html :::
::: warning composer require easyswoole/task :::
测试
如果你按照本文配置,那么你的文件结构应该是以下形式
App├── HttpController│ ├── websocket.html│ └── WebSocket.php├── Websocket│ └── Index.php└── └── WebSocketParser.php
首先在根目录运行easyswoole
php easyswoole start
如果没有错误此时已经启动了easyswoole服务;
访问 127.0.0.1:9501/WebSocket/index 可以看到之前写的测试html文件;
::: warning
新人提示:这种访问方式会请求HttpController控制器下Index.php中的index方法
:::
扩展
自定义解析器
在上文的 WebSocketParser.php 中,已经实现了一个简单解析器; 我们可以通过自定义解析器,实现自己需要的场景。
/*** decode* @param string $raw 客户端原始消息* @param WebSocket $client WebSocket Client 对象* @return Caller Socket 调用对象*/public function decode($raw, $client) : ? Caller{// 解析 客户端原始消息$data = json_decode($raw, true);if (!is_array($data)) {echo "decode message error! \n";return null;}// new 调用者对象$caller = new Caller();/*** 设置被调用的类 这里会将ws消息中的 class 参数解析为具体想访问的控制器* 如果更喜欢 event 方式 可以自定义 event 和具体的类的 map 即可* 注 目前 easyswoole 3.0.4 版本及以下 不支持直接传递 class string 可以通过这种方式*/$class = '\\App\\WebSocket\\'. ucfirst($data['class'] ?? 'Index');$caller->setControllerClass($class);// 提供一个事件风格的写法// $eventMap = [// 'index' => Index::class// ];// $caller->setControllerClass($eventMap[$data['class']] ?? Index::class);// 设置被调用的方法$caller->setAction($data['action'] ?? 'index');// 检查是否存在argsif (!empty($data['content'])) {// content 无法解析为array 时 返回 content => string 格式$args = is_array($data['content']) ? $data['content'] : ['content' => $data['content']];}// 设置被调用的Args$caller->setArgs($args ?? []);return $caller;}/*** encode* @param Response $response Socket Response 对象* @param WebSocket $client WebSocket Client 对象* @return string 发送给客户端的消息*/public function encode(Response $response, $client) : ? string{/*** 这里返回响应给客户端的信息* 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理*/return $response->getMessage();}
::: warning
例如{“class”:”Index”,”action”:”hello”}
:::
::: warning 则会访问App/WebSocket/WebSocket/Index.php 并执行hello方法 :::
::: tip 当然这里是举例,你可以根据自己的业务场景进行设计 :::
