[TOC] #### 前言 --- [ThinkPHP 5.1 Workerman 快速上手指南](https://www.kancloud.cn/thinkphp/think-worker/content) 是 ThinkPHP 开发团队的领导者及核心开发者 “流年” 编写的文档,本文是基于这个文档以及实际开发中遇到的问题和经验做的总结,方便后续再次使用时可以快速上手,更快的完成开发 在 [ThinkPHP 5.1 官方开发文档](https://www.kancloud.cn/manual/thinkphp5_1/content) 中,可以看到框架对 PHP 的版本要求是大于等于 5.6,我推荐使用 PHP 7.4 本文使用的开发环境:ThinkPHP 5.1、命令行的PHP版本 7.4、Composer 2.9 请先检查您的开发环境,是否和上述版本一致,有微小版本差异也是没问题的,比如 PHP 版本为 7.3 ```bash php -v composer -V php think version ``` #### 安装依赖包 --- `Workerman` 是一款纯 PHP 开发的 socket 服务器框架,`think-worker` 是 TP 官方发布的一个 `workerman` 扩展 ```bash composer require workerman/workerman ``` 运行以下命令安装 `think-worker` 扩展,会自动安装 `workerman` 依赖包,无需再另外手动安装 `workerman` 所以我们不需要执行上面安装 `workerman` 的命令,只需要安装 `think-worker` 即可 ```bash composer require topthink/think-worker='2.0.*' ``` 安装 `think-worker` 后,在项目根目录下的 config 目录中会生成三个文件 + gateway_worker.php + worker_server.php + worker.php 安装依赖如果报错或依赖安装成功后,workman 服务启动失败,可以尝试更新 composer 版本,再看还有没有问题 ```bash composer self-update ``` #### workman http 服务 --- 运行以下命令,启动 workman http 服务,其对应的配置文件为 `config/worker.php` ```bash php think worker ``` 运行之后可以看到在 `0.0.0.0:2346` 启动了 HTTP Server 服务端,通过这个地址可以直接访问当前应用 ```bash http://localhost:2346 ``` 以守护进程模式运行(只支持 linux 环境) ```bash php think worker -d ``` 停止服务 ```bash php think worker stop ``` 另外还可以使用命令 ```bash # 重启服务 php think worker restart # 平滑重启服务 php think worker reload ``` #### workman server 服务 --- ##### 基础用法 通过简单的配置快速启动一个 workman 服务,包括 `WebSocket/Http/Socket` 服务。 ```bash php think worker:server ``` `WebSocket` 协议不支持通过 `HTTP` 访问,可以通过 JS 代码测试,该协议常用于实现聊天功能、即时通讯 ```javascript const ws = new WebSocket("ws://127.0.0.1:2345"); ws.onopen = function () { console.log("连接成功"); ws.send('hello,thinkphp'); console.log("给服务端发送一个字符串:hello,thinkphp"); }; ws.onmessage = function (e) { console.log("收到服务端的消息:" + e.data); }; ``` 如果需要使用守护进程的方式运行,可以使用 ```bash php think worker:server -d ``` 其他命令 ```bash # 停止服务 php think worker:server stop # 重启服务 php think worker:server restart # 平滑重启服务 php think worker:server reload ``` 如果需要自定义参数,可以在配置文件 `config/worker_server.php` 中修改。配置较多,列举部分: ```php return [ // 扩展自身需要的配置 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text 'host' => '0.0.0.0', // 监听地址 'port' => 2345, // 监听端口 'socket' => '', // 完整监听地址 'context' => [], // socket 上下文选项 'worker_class' => '', // 自定义Workerman服务类名 支持数组定义多个服务 // ... 'onMessage' => function ($connection, $data) { $connection->send('receive success'); }, // ... ]; ``` 自定义服务类 一般情况下,项目需求都不是简单收发消息就结束了,而需要灵活性更高的处理方式,我们可以自定义事件回调 创建一个自定义服务类,`application/http/Worker.php` 服务类必须继承 `think\worker\Server` 类,支持 `workerman` 所有的回调方法定义(必须是 public 类型) [ThinkPHP 5.1 Workerman 快速上手指南](https://www.kancloud.cn/thinkphp/think-worker/content) 中的自定义服务类示例是有错误的,下面示例是我优化过的 + 问题1: 通过查看父类,可以发现属性名应该是 `$options`,而不是 `$option` + 问题2: 某些版本不支持在类的属性定义中使用表达式语法(pidFile 的赋值) + 问题3: 默认端口应该使用 2345,而不是 2346,应该和默认的配置文件中的端口一致 ```php <?php namespace app\http; use think\facade\Env; use think\worker\Server; class Worker extends Server { protected $protocol = 'websocket'; protected $host = '127.0.0.1'; protected $port = 2345; protected $options = [ 'count' => 4, 'pidFile' => '', 'name' => 'think' ]; protected function init() { $this->options['pidFile'] = Env::get('runtime_path') . 'worker.pid'; } public function onMessage($connection, $data) { $connection->send('receive success'); } } ``` 在 `worker_server.php` 文件中修改 `worker_class` 参数,指定我们创建的 Workerman 服务类 定义该参数后,`worker_server.php` 中的其他配置参数都不在生效,后续只需关注实现自定义的服务类 ```php 'worker_class' => \app\http\Worker::class, // 自定义Workerman服务类名 支持数组定义多个服务 ``` 启动服务,此时就会加载自定义服务类中的配置及方法 ```bash php think worker:server ``` 当客户端成功连接 WebSocket 时,服务端会收到一个数字 0 的消息。通过以下代码测试可以证实 ```javascript const ws = new WebSocket("ws://127.0.0.1:2345"); ws.onopen = function () { console.log("WebSocket 连接成功"); }; ws.onmessage = function (e) { console.log("收到服务端的消息:" + e.data); }; ``` ```php public function onMessage($connection, $data) { $string = json_encode(['data' => $data, 'dataType' => gettype($data)]); $connection->send('receive success ' . $string); } ``` ##### 事件回调 自定义服务类需要继承 `think\worker\Server`,可以查看父类中的事件属性定义得知有哪些事件回调可用 ```php protected $event = [ 'onWorkerStart', 'onConnect', 'onMessage', 'onClose', 'onError', 'onBufferFull', 'onBufferDrain', 'onWorkerReload', 'onWebSocketConnect' ]; ``` onWorkerStart:当工作进程启动时触发。也就是运行以下命令启动服务时会触发该方法 ```bash php think worker:server ``` 在开发聊天功能时,功能测试可以正常使用,但是第二天客户反馈聊天功能无法正常使用。 通过查看守护进程日志发现报错 `MySQL server has gone away`,原因:聊天中有数据库操作,程序连接池中的连接长时间未使用,超过了 mysql 的 `wait_timeout` 设置,导致空间连接被服务器主动关闭。 解决方案:每隔一段时间,执行一次数据库操作,保持数据库连接活跃。示例代码如下: ```php use think\Db; use Workerman\Lib\Timer; // 当工作进程启动时触发 public function onWorkerStart() { // 每隔10分钟,执行一次数据库操作,保持连接活跃,且删除旧数据,避免数据表爆满 Timer::add(600, function () { Db::table('qs_worker')->insert(['data' => time(), 'time' => date('Y-m-d H:i:s')]); Db::table('qs_worker')->where('data', '<', time() - 12000)->delete(); }); } ``` onConnect:当有新的连接时触发。方法中直接输出内容,会显示在命令窗口中,可以看到连接的 id ```php // 当有新的连接时触发 public function onConnect($connection) { echo "新的连接: {$connection->id}\n"; } ``` onMessage:当服务端收到消息时触发。下面是 onMessage 方法的示例代码,提供一个思路 ```php // 当接收到数据消息时触发 public function onMessage($connection, $data) { // WebSocket 连接成功时,会收到一个数字 0 的消息,不做处理 if (is_integer($data)) return; // 和前端约定好数据格式,只传 json 字符串格式的数据 $message = json_decode($data, true); // 数据中应有一个 type,区分消息类型,比如:聊天消息、心跳检测 if (!isset($message['type'])) { $connection->send(json_encode(['error' => '消息格式错误'])); return false; } // 判断消息类型,执行不同逻辑 switch ($message['type']) { case 'chat': // 聊天消息 $this->handleChat($connection, $message); break; case 'heartbeat': // 心跳检测 $this->heartbeat($connection, $message); break; default: $connection->send(json_encode(['error' => '未知消息类型'])); } } ``` onClose:当连接关闭时触发。比如:客户端主动断开连接 ```php public function onClose($connection) { echo "关闭连接: {$connection->id}\n"; } ``` 客户端主动断开连接、直接刷新页面、手动停止 WebSocket 服务,都会触发 onClose 回调,测试代码如下: ```javascript const ws = new WebSocket("ws://127.0.0.1:2345"); ws.onopen = function () { console.log("WebSocket 连接成功"); }; setTimeout(() => { console.log('客户端主动关闭连接'); ws.close() }, 3000) ```