[TOC] #### 1. Swoole 介绍 --- Swoole 是一个使用 C/C++ 编写的 PHP 扩展,使 PHP 开发人员可以编写高性能高并发的 TCP、UDP、Unix Socket、HTTP、 WebSocket 等服务,让 PHP 不再局限于 Web 领域 想要了解 Swoole 更多内容,可以参考 Swoole 官方文档:<https://wiki.swoole.com> 本文内容主要讲述了当前最新的 `think-swoole` 扩展的使用,目前仅支持 Linux/MacOS 环境下运行 + 由于 Swoole 不支持 Windows 环境,所以我们使用虚拟机环境测试(Ubuntu 24.04 Server 操作系统) 本文主要讲述的是 ThinkPHP8.1 中 think-swoole4.1 扩展包的用法,使用的开发环境: + PHP 8.2.28 + Composer 2.9.8 + ThinkPHP 8.1.4 + think-swoole 4.1.2 #### 2. 环境准备 --- 本文使用的操作系统及软件介绍: | 名称 | 描述 | 文章 | | ------------ | ------------ | ------------ | | Windows 10 专业版 | 在 Windows 系统上使用虚拟机软件 | | | Oracle VirtualBox | 虚拟机软件(Windows 版本) | [VirtualBox 介绍及安装](https://www.itqaq.com/index/627.html) | | ubuntu-24.04.3-live-server-amd64.iso | Ubuntu24.04 LTS 服务器版镜像文件 | [Ubuntu 镜像文件下载地址](https://ubuntu.com/download/server) | | Oh My Zsh | Zsh 终端配置管理工具 | [Oh My Zsh 介绍及安装](https://www.itqaq.com/index/362.html) | | zsh-autosuggestions | 根据历史命令自动提示并补全命令 | [Oh My Zsh 第三方插件](https://www.itqaq.com/index/568.html) | | zsh-syntax-highlighting | 检测命令是否存在,命令高亮显示 | [Oh My Zsh 第三方插件](https://www.itqaq.com/index/568.html) | | 宝塔面板 | 安全高效的服务器运维面板 | [宝塔面板官方网站](https://www.bt.cn) | | Swoole | PHP 协程框架 | [Swoole 官方网站](https://www.swoole.com) | Ubuntu24.04 LTS 操作系统的安装过程在此不做过多描述,网络修改为 “桥接模式”,运行以下命令: ```bash # 安装 openssh-server 服务,使 Ubuntu 系统支持 SSH 连接 sudo apt update sudo apt install openssh-server -y ``` 切换终端 Shell 类型,然后安装 Oh My Zsh ```bash # 将终端切换为 zsh sudo apt install zsh -y chsh -s $(which zsh) # 重新打开终端,安装 Oh My Zsh sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)" ``` #### 3. 安装软件 --- ##### 安装宝塔面板 ```bash # 适用于 Ubuntu/Deepin 的安装脚本 wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh && sudo bash install_panel.sh ed8484bec ``` 宝塔面板安装完成后可以看到访问地址,因为我们用的是虚拟机,所以我们使用 “内网面板地址”: ```plaintext 【云服务器】请在安全组放行 20744 端口 外网ipv4面板地址: https://116.30.139.108:20744/ef249a2d 内网面板地址: https://192.168.1.43:20744/ef249a2d username: 9thfsbe4 password: b33618f4 ``` ##### 安装 PHP 8.2 访问宝塔面板内网地址,然后登录宝塔账号,安装 `PHP8.2`(因为 ThinkPHP8 要求 PHP版本 >= 8.0+) + 在宝塔面板的 “软件商店” 中选择 `PHP8.2` 点击安装即可(极速安装) + 安装 PHP8.2 之前,宝塔面板会先自动安装其所需的依赖库(必要环境库) ##### 安装 ThinkPHP 需要先安装 `Composer` 包管理器,因为我们需要使用 Composer 来安装 ThinkPHP 框架 ```bash # 安装 Composer curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer ``` 进入宝塔面板的站点目录,然后运行 ThinkPHP 框架安装命令: ```bash cd /www/wwwroot && sudo composer create-project topthink/think tp ``` 安装过程中,你可能会遇到以下问题,这是因为需要开启 PHP 的 `fileinfo` 扩展 + 解决方案:在宝塔面板 “软件商店” 中的 PHP8.2 的 “设置” 中,安装扩展: `fileinfo` ```plaintext $ sudo composer create-project topthink/think tp ... Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires topthink/think-filesystem ^2.0|^3.0 -> satisfiable by topthink/think-filesystem[v2.0.0, v2.0.1, v2.0.2, v2.0.3, v3.0.0]. - league/flysystem[1.1.4, ..., 1.1.10] require ext-fileinfo * -> it is missing from your system. Install or enable PHP's fileinfo extension. .... To enable extensions, verify that they are enabled in your .ini files: - /www/server/php/82/etc/php-cli.ini You can also run `php --ini` in a terminal to see which files are used by PHP in CLI mode. Alternatively, you can run Composer with `--ignore-platform-req=ext-fileinfo` to temporarily ignore these required extensions. ``` 重新运行 ThinkPHP 框架安装命令,依次出现以下报错: + 解决方案:在宝塔面板 “软件商店” 中的 PHP8.2 的 “设置” 中,删除禁用函数 `putenv`、`proc_open` ```plaintext Uncaught Error: Call to undefined function Composer\XdebugHandler\putenv() in ... The Process class relies on proc_open, which is not available on your PHP installation. ``` 再次运行 ThinkPHP 框架安装命令,就可以安装成功了 ```plaintext $ sudo composer create-project topthink/think tp Creating a "topthink/think" project at "./tp" Installing topthink/think (v8.1.3) - Installing topthink/think (v8.1.3): Extracting archive ... Generating autoload files > @php think service:discover Succeed! > @php think vendor:publish File /www/wwwroot/tp/config/trace.php exist! Succeed! 4 packages you are using are looking for funding. Use the `composer fund` command to find out more! No security vulnerability advisories found. ``` 测试运行,运行以下命令: + 需要删除禁用函数:`passthru`,删除后可以启动服务了,但是浏览器还是无法访问 + 还需要在宝塔面板中的 “安全” 中,添加端口规则放行 8000 端口,放行后就可以在浏览器访问了 + 访问地址:`http://192.168.1.43:8000`(192.168.1.43 是局域网 IP) ```plaintext $ cd tp $ php think run ThinkPHP Development server is started On <http://0.0.0.0:8000/> You can exit with `CTRL-C` Document root is: /www/wwwroot/tp/public [Error] Call to undefined function think\console\command\passthru() ``` ##### 安装 think-swoole 先安装 `Swoole` 扩展,然后再安装 `think-swoole` 依赖包 ```bash # 在宝塔面板软件商店中,找到对应 PHP版本,点击管理,可以一键安装 Swoole 扩展 # ... 此时省略安装过程(安装 Swoole4) # 验证安装 php -m | grep swoole # 安装 ThinkPHP 官方的 Swoole 依赖包 sudo composer require topthink/think-swoole ``` 启动 `Swoole` 服务,测试运行: ```bash # 默认只启动 HTTP 服务,而 Websocket 服务默认是没有启用的 php think swoole ``` 启动完成后,默认会启动一个 HTTP Server,可以直接访问当前应用,相关配置可以在 `config/swoole.php` 修改 + 默认访问地址:`http://192.168.1.43:8080`(需要在 “宝塔面板-安全” 放开 8080 端口才能访问) ```php return [ 'http' => [], 'websocket' => [], 'hot_update' => [], // ... ]; ``` #### 4. 热更新(方便调试) --- Swoole 服务运行过程中,PHP 文件是常驻内存运行的,可以避免重复读取磁盘、重复解释编译 PHP,以便达到最高性能 + 所以更改业务代码后必须手动 `reload` 或者 `restart` 才能生效 `think-swoole` 扩展提供了热更新功能,在检测到相关目录的文件有更新后会自动 `reload`,方便开发调试 如果开启了调试模式,默认是开启热更新的,通过 `config/swoole.php` 可以看到热更新默认配置: + 生产环境不建议开启热更新,一方面有性能损耗,另一方面是对文件所做的任何修改都需要确认无误才能进行更新部署 ```php return [ 'hot_update' => [ // 是否开启热更新 'enable' => env('APP_DEBUG', false), // 监控那些类型的文件变动 'name' => ['*.php'], // 监控那些路径下的文件变动 'include' => [app_path()], // 排除目录 'exclude' => [], ], ]; ``` #### 5. Websocket 路由调度 --- 前面我们已经使用过 `think-swoole` 的 HTTP 服务,现在我们来如何使用 Websocket 服务 在 `config/swoole.php` 中,http、websocket 相关配置参数默认值如下所示: + enable:是否开启 Websocket 服务,默认未启用 + route:是否使用路由调度方式,默认是使用的(这是个坑,还需要手动创建控制器注册路由才能使用) + websocket 的端口和 http 端口一致 ```php 'http' => [ 'enable' => true, 'host' => '0.0.0.0', 'port' => 8080, 'worker_num' => swoole_cpu_num(), 'options' => [], ], 'websocket' => [ 'enable' => false, 'route' => true, 'handler' => \think\swoole\websocket\Handler::class, // ... ], ``` 将`websocket.enable` 改为 `true`,然后运行以下命令: ```bash php think swoole ``` 现在已经启用 Websocket 服务,可以在客户端连接 Websocket 了,但是你会发现无法连接成功 + 此时你应该看到报错信息:`WebSocket connection to 'ws://192.168.1.43:8080/' failed:` + 这是因为默认开启了路由调度,但是我们并没有配置路由文件,导致握手失败 ```javascript const ws = new WebSocket('ws://192.168.1.43:8080'); ws.onopen = () => { console.log('✅ 恭喜!WebSocket 已成功建立连接!'); }; ws.onclose = (event) => { console.log('⚠️ 连接已关闭。错误代码:', event.code, '原因:', event.reason); }; ws.onerror = (error) => { console.error('❌ 发生连接错误:', error); }; ``` 解决方案:在 [think-swoole 项目仓库](https://github.com/top-think/think-swoole) 的 README.md 文件中有关于 “路由调度” 的说明,需要注册路由使用 创建控制器类文件: ```bash sudo php think make:controller Controller --plain ``` 文件内容: ```php declare(strict_types=1); namespace app\controller; use think\swoole\Websocket; use think\swoole\websocket\Event; use Swoole\WebSocket\Frame; use think\swoole\websocket\Room; class Controller { public function action1() { //不可以在这里注入websocket对象 return \think\swoole\helper\websocket() ->onOpen(function () {}) ->onMessage(function (Websocket $websocket, Frame $frame) { //只可在事件响应这里注入websocket对象 //... $websocket->join('room_key'); //将当前连接加入到某个room,后续可以向该room发送消息 这个room里的都可以收到 //比如room_key可以直接使用这个用户的id,然后其他地方需要给某个用户发送消息,直接向这个room发送消息即可 //... $websocket->push('message'); //给当前连接发送消息 //... $websocket->emit('event_name', 'message'); //给当前连接发送事件 //... $websocket->to('room_key')->push('message'); //给指定room的所有连接发送消息 在http请求的控制器中也可以注入Websocket对象这样发消息 //... }) ->onClose(function () {}); } public function action2() { return \think\swoole\helper\websocket() ->onOpen(function () {}) ->onMessage(function (Websocket $websocket, Frame $frame) { //... }) ->onClose(function () {}); } } ``` 路由定义(修改 `route/app.php`): ```php Route::get('path1','controller/action1'); Route::get('path2','controller/action2'); ``` 然后在 WebSocket 连接地址后面加上定义的路由即可连接成功 ```javascript const ws = new WebSocket('ws://192.168.1.43:8080/path1'); ``` #### 6. Websocket 消息处理器 --- 如果不想使用路由调度,还可以通过事件监听的方式,首先要关闭路由调度,也就是将 `route` 改为 `false` + 当关闭路由调度时,Swoole 会将收到的消息完全交给 `handler` 指向的类(\think\swoole\websocket\Handler) + 消息处理类文件所在位置:`vendor/topthink/think-swoole/src/websocket/Handler.php` ```php return [ 'websocket' => [ 'enable' => true, // 关闭路由调度 'route' => false, // 消息处理器 'handler' => \think\swoole\websocket\Handler::class, // ... ], ]; ``` 这个类接管了 WebSocket 连接最基础的三个生命周期阶段:建立连接、接收消息、断开连接 + onOpen:当客户端握手成功时触发。它不做任何复杂逻辑,只是抛出一个 `swoole.websocket.Open` 全局事件 + onClose:当客户端断开连接时触发。同样只负责抛出一个全局事件,方便你在监听器里做资源清理或用户下线记录 ```php public function onOpen(Request $request) { $this->event->trigger('swoole.websocket.Open', $request); } public function onMessage(Frame $frame) { $this->event->trigger('swoole.websocket.Message', $frame); $event = $this->decode($frame->data); if ($event) { $this->event->trigger('swoole.websocket.Event', $event); } } public function onClose() { $this->event->trigger('swoole.websocket.Close'); } ``` 因为消息处理器抛出了事件,所以我们可以 “注册监听器”,告诉 ThinkPHP,当事件被触发时,应该由哪个类来处理 ```php // config/swoole.php return [ 'websocket' => [ 'enable' => true, 'route' => false, 'handler' => \think\swoole\websocket\Handler::class, // ... // 👇 在这里注册事件监听器 'listen' => [ 'Open' => \app\listener\WsOpen::class, // 监听连接建立 'Message' => \app\listener\WsMessage::class, // 监听接收消息 'Close' => \app\listener\WsClose::class, // 监听连接断开 ], 'subscribe' => [], ], ]; ``` 创建并编写监听器类,可以通过命令快速生成: ```bash php think make:listener WsOpen php think make:listener WsClose ``` 生成的监听器类文件示例: ```php declare (strict_types = 1); namespace app\listener; class WsOpen { /** * 事件监听处理 * * @return mixed */ public function handle($event) { // } } ``` 远程编辑代码 因为代码是存放在虚拟机中的,修改代码不太方便,那么我们可以在代码编辑器中安装插件,实现远程修改代码 + [VSCode 安装 Remote SSH 插件,通过 SSH 连接虚拟机远程修改代码](https://www.itqaq.com/index/351.html) 在 VSCode 中修改远程文件后保存报错,提示没有权限,这是因为: + 因为我们使用的是 Ubuntu 系统,框架目录中的文件普通用户无法修改,可以将目录权限修改为普通用户 ```bash sudo chown -R $USER:$USER /www/wwwroot/tp ``` #### 7. think-swoole 连接事件 --- 打开生成的 `app/listener/WsOpen.php` 文件,可以编写具体的业务逻辑 + `handle` 方法可以通过依赖注入获取到 `think\swoole\Websocket` 对象,从而对连接进行操作 + `Request` 对象里包含了前端发起 Websocket 握手时的 GET 参数、Header 等信息,非常适合用来提取用户信息 ```php declare(strict_types=1); namespace app\listener; use think\Request; use think\swoole\Websocket; class WsOpen { /** * 事件监听处理 * * @return mixed */ public function handle($event, Websocket $websocket, Request $request) { // 1. 获取当前连接的客户端唯一标识 (fd) $fd = $websocket->getSender(); // 2. 获取握手时的请求参数(例如前端连接时带的 ?token=xxx&userId=1001) $userId = $request->get('userId'); // 3. 执行你的业务逻辑 // 比如:把 fd 和 userId 存入 Redis,标记该用户已上线 // cache('user_fd_' . $userId, $fd); // 4. 让当前连接加入一个专属房间(方便后续定向推送) if ($userId) { $websocket->join('user_' . $userId); } echo "客户端 {$fd} 已成功建立 WebSocket 连接!\n"; } } ``` #### 8. 客户端和服务端的关闭事件 --- 当客户端断开连接时(关闭窗口/主动调用断开连接方法),会触发服务端的关闭事件 当服务端主动关闭 `think-swoole` 服务时,同样也会触发客户端的 `ws.onclose` 事件 打开 `app/listener/WsOpen.php`,这是服务器端的关闭事件监听器,打印下默认的 `$event` 发现值是 NULL + 说明这个 `$event` 是没有用的,我们可以直接给它删除。然后依赖注入:`think\swoole\Websocket` ```php class WsClose { /** * 事件监听处理 * * @return mixed */ public function handle($event) { echo 'WebSocket 连接已关闭!' . PHP_EOL; var_dump($event); // NULL } } ``` 调整后的关闭事件监听器 ```php declare(strict_types=1); namespace app\listener; use think\swoole\Websocket; class WsClose { /** * 事件监听处理 * * @return mixed */ public function handle(Websocket $websocket) { echo 'WebSocket 连接已关闭!' . PHP_EOL; // 获取即将断开连接的客户端 fd(临时桌号) $fd = $websocket->getSender(); // 核心业务逻辑:清理该连接留下的“痕迹” // 1. 从 Redis 中删除该用户与 fd 的绑定关系 // 2. 更新数据库或缓存中的“在线状态”为离线 // 3. 广播给其他用户:“某某某下线了” echo "客户端 {$fd} 已断开连接,资源清理完毕。\n"; } } ``` #### 9. 客服端给服务端发送消息 --- 通过查看消息处理类 `think\swoole\websocket\Handler` 源码,可以看到以下方法: + 触发一个事件 `swoole.websocket.Message`,并将 Swoole 传进来的最原始的 `$frame` 传入到事件监听器 + 然后调用 `decode` 方法对前端发来的 `$frame->data` 进行解析 + 它会尝试提取 type 和 data 并封装为框架认识的 WsEvent 对象 + 如果上一步拿到了 `WsEvent` 对象,就再次触发一个名为 `swoole.websocket.Event` 的事件 ```php public function onMessage(Frame $frame) { $this->event->trigger('swoole.websocket.Message', $frame); $event = $this->decode($frame->data); if ($event) { $this->event->trigger('swoole.websocket.Event', $event); } } protected function decode($payload) { $data = json_decode($payload, true); if (!empty($data['type'])) { return new WsEvent($data['type'], $data['data'] ?? null); } return null; } ``` 修改 `config/swoole.php` 文件,添加 `Message` 事件的监听器 ```php 'listen' => [ 'Message' => \app\listener\WsMessage::class, // 监听接收消息 ], ``` 创建监听器类文件,运行以下命令: ```bash php think make:listener WsMessage ``` 修改监听器类文件: ```php declare(strict_types=1); namespace app\listener; class WsMessage { /** * 事件监听处理 * * @return mixed */ public function handle(\Swoole\WebSocket\Frame $frame) { echo '服务端收到消息:' . $frame->data . PHP_EOL; } } ``` 服务端已经准备好接收消息,接下来编写客户端代码: ```html <h1>WebSocket 功能</h1> <input id="msg" type="text"> <button onclick="sendMessage()">发送消息</button> ``` ```javascript const ws = new WebSocket('ws://192.168.1.43:8080'); ws.onopen = () => { console.log('✅ 恭喜!WebSocket 已成功建立连接!'); }; // 监听接收到服务器消息 ws.onmessage = (event) => { console.log('📩 收到服务器发来的消息:', event.data); }; // 监听连接关闭 ws.onclose = (event) => { console.log('⚠️ 连接已关闭。错误代码:', event.code, '原因:', event.reason); }; // 监听发生错误 ws.onerror = (error) => { console.error('❌ 发生连接错误:', error); }; // 发送消息 function sendMessage() { console.log('🔼 准备发送消息...'); const message = document.getElementById('msg').value; if (!message) { console.error('❌ 消息内容不能为空!'); return; } ws.send(message); console.log('✅ 消息已发送:', message); } ``` 客户端发送最简单的文本内容(如:“你好啊”),服务器终端输出结果: ```plaintext $ php think swoole Starting swoole server... You can exit with `CTRL-C` 客户端 1.1 已成功建立 WebSocket 连接! 服务端收到消息:你好啊 ``` #### 10. think-swoole 的自定义事件 --- 认真分析 `think\swoole\websocket\Handler` 中的 `onMessage` 方法,可以得出两个结论: + 接收到前端发送的消息会进行解析,如果是普通字符串,就只触发 `swoole.websocket.Message` 事件 + 如果是 JSON 字符串且含有 `type`,就封装为 `WsEvent` 对象,然后再触发 `swoole.websocket.Event` 事件 ```php public function onMessage(Frame $frame) { $this->event->trigger('swoole.websocket.Message', $frame); // 这里需要特别注意,意思是: // 如果拿到 WsEvent 对象,就触发 swoole.websocket.Event 事件 $event = $this->decode($frame->data); if ($event) { $this->event->trigger('swoole.websocket.Event', $event); } } ``` 我们将前端发送的消息格式修改为以下形式: ```javascript ws.send(JSON.stringify({ type: 'Chat', data: message })) ``` 修改 `config/swoole.php` 文件,添加 `Event` 事件的监听器 ```php 'listen' => [ 'Event' => \app\listener\WsEvent::class, // 全局事件监听 ], ``` 创建监听器类文件,运行以下命令: ```bash php think make:listener WsEvent ``` 修改监听器类文件: ```php declare(strict_types=1); namespace app\listener; use think\swoole\websocket\Event; class WsEvent { public function handle(Event $event) { echo "捕获到事件: {$event->type},携带的数据:{$event->data}" . PHP_EOL; } } ``` 网上有些视频可能会教你这样监听自定义事件(`think-swolle` 老版本的写法,本文使用的版本不支持): 修改 `config/swoole.php` 文件,添加自定义事件 `Chat` 的监听器 ```php 'listen' => [ 'Chat' => \app\listener\WsChat::class, // 自定义事件 ], ``` 创建监听器类文件: ```bash php think make:listener WsChat ``` 如果你这样写,你会发现发送 `Chat` 类型的事件是无法触发这个监听器的,具体原因需要看: + 其实 `think\swoole\websocket\Handler` 中的 `onMessage` 方法已经写的很清楚了 + 如果发现消息属于自定义事件,它是直接触发 `swoole.websocket.Event` 事件的,并没有自动映射监听器 ```php public function onMessage(Frame $frame) { $this->event->trigger('swoole.websocket.Message', $frame); $event = $this->decode($frame->data); if ($event) { $this->event->trigger('swoole.websocket.Event', $event); } } ``` 但是,如果你现在就是想实现自动映射,需要修改以下内容: + 不建议直接修改 `think\swoole\websocket\Handler` 文件,因为它处于 `vendor` 目录 + 真想要修改可以将其拷贝一份放在 `app` 目录下,然后修改 `websocket.handler` 对应的消息处理类 ```php // 将以下代码 $this->event->trigger('swoole.websocket.Event', $event); // 修改为(其实就是将固定触发 Event 事件,改为自动映射事件) $this->event->trigger('swoole.websocket.' . $event->type, $event); ``` #### 11. 服务端给客户端发送消息 --- 在 `think-swoole` 中,服务端给客户端发送消息非常简单,核心都是通过注入的 `Websocket` 对象来操作 根据项目需求(是发给当前发消息的人、特定的人,还是所有人),主要有以下几种常用的发送方式: + 回复给 “当前” 发消息的客户端 + 发给 “指定” 的客户端或房间 + 广播给 “所有” 在线客户端 如果你想在收到消息后,直接回复给刚刚给你发消息的那个客户端,可以使用 `push()` 或 `emit()` + `push($data)`:直接发送原始数据(字符串或数据) + `emit($event, $data)`:配合前端 Socket.io 等库使用,发送带有事件名的数据包 ```php namespace app\listener; use think\swoole\Websocket; use Swoole\WebSocket\Frame; class WsMessage { /** * OnMessage 事件监听处理 * * @param \think\swoole\Websocket $websocket * @param \Swoole\WebSocket\Frame $frame */ public function handle(Websocket $websocket, Frame $frame) { // 假设前端发送的消息是:{"type":"Chat","data":"hello"} // 方式一:直接推送字符串或数组 // 客户端会收到:服务端收到消息:{"type":"Chat","data":"hello"} $websocket->push('服务端收到消息' . $frame->data); // 方式二:带事件名推送(推荐前端用 socket.io 时使用) // 客户端会收到:{"type":"chat_reply","data":[{"status":"success","data":{"id":1}}]} $websocket->emit('chat_reply', ['status' => 'success', 'data' => ['id' => 1]]); } } ``` 如果你想主动给某个特定用户或某个聊天室发消息,可以使用 `to($fd_or_room)->push()` + `$fd` 是 Swoole 分配给每个连接的唯一数字标识 ```php // 发送给 fd 为 5 的特定客户端 $websocket->to(5)->push('这是专门发给你的定向消息'); // 发送给名为 'chat_room_1' 的房间里的所有在线用户 $websocket->to('chat_room_1')->emit('new_message', '房间广播消息'); // 同时发送给多个指定的 fd(传入数组) $websocket->to([5, 8, 10])->push('群发给这几个人的消息'); ``` 使用示例: ```html <style> .item { margin-bottom: 15px; } .item label { display: inline-block; width: 130px; } .item input { height: 30px; padding: 0 12px; } button { width: 300px; border: none; height: 38px; background: #409eff; color: #fff; cursor: pointer; border-radius: 4px; } </style> <h1>WebSocket 功能</h1> <div class="item"> <label for="">消息内容:</label> <input id="msg" type="text" placeholder="请输入要发送的消息"> </div> <div class="item"> <label for="">发送给指定用户:</label> <input id="fd" type="text" placeholder="请输入客户端 fd"> </div> <div class="item"> <button onclick="sendMessage()">发送消息</button> </div> ``` ```javascript const ws = new WebSocket('ws://192.168.1.43:8080'); ws.onopen = () => { console.log('✅ 恭喜!WebSocket 已成功建立连接!'); }; ws.onmessage = (event) => { console.log('📩 收到服务器发来的消息:', event.data); }; ws.onclose = (event) => { console.log('⚠️ 连接已关闭。错误代码:', event.code, '原因:', event.reason); }; // 发送消息 function sendMessage() { console.log('🔼 准备发送消息...'); const fd = document.getElementById('fd').value; const message = document.getElementById('msg').value; const data = { type: 'Chat', data: { fd, message, } } ws.send(JSON.stringify(data)) console.log('✅ 消息已发送:', data); } ``` 服务端的事件监听器: ```php class WsOpen { public function handle(\think\swoole\Websocket $websocket) { // 获取当前连接的客户端唯一标识 $fd = $websocket->getSender(); // 给客户端法发送消息,告知 fd 的值 $websocket->push("连接成功(fd:{$fd})"); // 将内容输出到终端 echo "客户端 {$fd} 已成功建立 WebSocket 连接!\n"; } } class WsMessage { public function handle(\think\swoole\Websocket $websocket, \Swoole\WebSocket\Frame $frame) { // $frame->data 是客户端发送的原始数据 echo '服务端收到消息:' . $frame->data . PHP_EOL; $message = json_decode($frame->data); // 使用 to($fd) 可以向指定客户端发送消息 $websocket->to($message->data->fd)->push($message->data->message); } } ``` 在 ThinkPHP8.1 + `think-swoole 4.1` 中,发送广播(群发)消息最优雅且推荐的方式是使用房间(Room)机制 + think-swoole 4.x 版本废弃了旧版的 `broadcast` 方法,采用了更灵活的链式调用 ```php class WsOpen { public function handle(\think\swoole\Websocket $websocket) { $fd = $websocket->getSender(); $websocket->push("连接成功(fd:{$fd})"); // 【关键步骤】将所有新连接的用户加入一个全局公共房间(例如:all_users) // 这样只要后续向 all_users 发消息,就等于实现了全员广播 $websocket->join('all_users'); echo "客户端 {$fd} 已成功建立 WebSocket 连接!\n"; } } class WsMessage { public function handle(\think\swoole\Websocket $websocket, \Swoole\WebSocket\Frame $frame) { $data = $frame->data; // 向所有在线用户广播这条消息 $websocket->to('all_users')->push("【广播消息】: 网站即将维护"); } } ``` 指定房间广播(如:聊天室) 如果你的业务是多个聊天室,可以在用户进入时让其加入特定的房间 ID,然后只向该房间推送 ```php // 用户加入特定房间 $websocket->join('room_1001'); // 仅向 room_1001 里的所有用户广播 $websocket->to('room_1001')->push('房间内的广播消息'); ``` 如果想在普通的控制器里(比如:后台管理系统)主动给所有的 WebSocket 在线用户发通知: + 方式一:通过依赖注入 Websocket 实例 + 方式二:使用助手函数 app() 获取 Websocket 实例 其实两种方式只是获取 Websocket 实例方法不同。本质上都是先拿到实例,然后向 `all_users` 房间发送广播消息 ```php // 这是一个普通的控制器方法,依赖注入 Websocket 实例 public function hello(\think\swoole\Websocket $websocket) { // 直接向之前定义好的 'all_users' 房间推送系统公告 $websocket->to('all_users')->push('这是一条来自后台的系统公告!'); } // 使用助手函数 app() 获取实例,然后调用 app('think\swoole\Websocket')->to('all_users')->push('这是一条来自后台的系统公告!'); ``` 服务端回复客户端消息的方法总结: ```php // 获取当前客户端的连接ID $fd = $ws->getSender(); // 向当前客户端发送消息(原始消息) $ws->push($messageData); // 向当前客户端发送消息(带有事件名的数据包) $ws->emit('chat', $messageData); // 向特定客户端发送消息 $ws->to($fd)->emit('chat', $data); // 将当前客户端加入房间 $ws->join('room_1'); // 向某个房间内所有人广播 $ws->to('room_1')->emit('broadcast', $data); ``` #### 12. 客户端加入/离开房间事件 --- 核心方法: ```php // 加入房间 $websocket->join($roomName); // 离开房间 $websocket->leave($roomName); // 查看房间内所有在线成员(这是助手函数形式,也可以通过依赖注入获取实例调用) app('think\swoole\websocket\Room')->getClients($roomName); // 给房间内所有成员发送消息(原始消息) $websocket->to($roomName)->push('...'); // 给房间内所有成员发送消息(带有事件名的数据包) $websocket->to($roomName)->emit('事件名', '消息内容'); ``` 聊天室示例代码(实现功能:加入房间/离开房间/给房间成员发消息): ```css .container { width: 320px; padding: 20px 20px; margin: 30px auto 0; border-radius: 4px; border: 1px solid #ccc; } .item label { width: 130px; display: inline-block; } .item input { height: 30px; padding: 0 12px; } button { border: none; cursor: pointer; border-radius: 4px; } .room button { width: 135px; height: 32px; color: #fff; margin-left: 8px; margin-bottom: 14px; background-color: #f47e1e; } .room button.leave { background-color: #808080; } .send button { width: 300px; height: 38px; color: #fff; background: #409eff; } ``` ```html <div class="container"> <h1>WebSocket 聊天室</h1> <div class="item" style="margin-bottom: 15px;"> <label for="">消息内容:</label> <input id="msg" type="text" placeholder="请输入要发送的消息"> </div> <div class="item room" style="margin-bottom: 5px;"> <button class="join" type="button" onclick="joinRoom('room_1')">加入1号聊天室</button> <button class="join" type="button" onclick="joinRoom('room_2')">加入2号聊天室</button> <button class="leave" type="button" onclick="leaveRoom('room_1')">离开1号聊天室</button> <button class="leave" type="button" onclick="leaveRoom('room_2')">离开2号聊天室</button> </div> <div class="item send"> <button onclick="sendMessage()">发送消息</button> </div> </div> ``` ```javascript const ws = new WebSocket('ws://192.168.1.43:8080'); ws.onopen = () => { console.log('✅ 恭喜!WebSocket 已成功建立连接!'); }; ws.onmessage = (event) => { console.log('📩 收到服务器发来的消息:', event.data); }; ws.onclose = (event) => { console.log('⚠️ 连接已关闭。错误代码:', event.code, '原因:', event.reason); }; let currentRoom = '' function leaveRoom(roomName) { currentRoom = '' ws.send(JSON.stringify({ type: 'leaveRoom', data: { room: roomName } })) } function joinRoom(roomName) { currentRoom = roomName ws.send(JSON.stringify({ type: 'joinRoom', data: { room: roomName } })) } function sendMessage() { console.log('🔼 准备发送消息,当前房间:' + currentRoom); const message = document.getElementById('msg').value; const data = { type: 'chat', data: { room: currentRoom, message } } ws.send(JSON.stringify(data)) console.log('✅ 消息已发送:', data); } ``` ```php class WsOpen { public function handle(\think\swoole\Websocket $websocket) { $fd = $websocket->getSender(); echo "客户端 {$fd} 已成功建立 WebSocket 连接!\n"; $websocket->push("客户端({$fd})"); } } use think\swoole\Websocket; use think\swoole\websocket\Room; class WsEvent { // 事件监听处理 public function handle($event, Websocket $websocket, Room $room) { $fd = $websocket->getSender(); $roomName = $event->data['room']; if ($event->type === 'joinRoom') { $websocket->join($roomName); echo "客户端:{$fd} 加入房间 {$roomName}" . PHP_EOL; $this->showOnlineUser($room, $roomName); $websocket->to($roomName)->push("客户端:{$fd} 加入房间 {$roomName}"); } else if ($event->type === 'leaveRoom') { $websocket->leave($roomName); echo "客户端:{$fd} 离开房间 {$roomName}" . PHP_EOL; $this->showOnlineUser($room, $roomName); $websocket->to($roomName)->push("客户端:{$fd} 离开房间 {$roomName}"); } else if ($event->type === 'chat') { $message = $event->data['message']; $websocket->to($roomName)->emit('say', "{$fd}: {$message}"); } } // 查看房间在线成员 public function showOnlineUser(Room $room, $roomName) { $clients = $room->getClients($roomName); echo "{$roomName} 房间在线成员:" . implode(',', $clients) . PHP_EOL; } } ``` #### 13. 事件订阅(事件集中到一个类) --- 简单来说,监听(Listener)是一个事件对应一个类,而订阅(Subscribe)是将多个相关的事件集中写在同一个类里 + 这样可以让代码更集中,管理起来更方便 创建订阅类: ```bash php think make:subscribe WebSocketEvent ``` 编写订阅类处理逻辑,核心要点: + 方法命名规则:方法名必须是 `on` + 事件标识(首字母大写驼峰) + 依赖注入:可以直接在方法中声明需要的依赖(如 Websocket 类),容器会自动注入 ```php namespace app\subscribe; use think\swoole\Websocket; use Swoole\WebSocket\Frame; class WebSocketEvent { public function onOpen(Websocket $websocket) { $fd = $websocket->getSender(); $websocket->push("连接成功(fd: {$fd})"); echo "[订阅] 客户端 {$fd} 已成功 WebSocket 连接!\n"; } public function onMessage(Websocket $websocket, Frame $frame) { $fd = $websocket->getSender(); echo "[订阅] 服务端收到 {$fd} 发送的消息: {$frame->data}\n"; } public function onClose(Websocket $websocket) { $fd = $websocket->getSender(); echo "[订阅] 客户端 {$fd} 已断开连接。\n"; } } ``` #### 14. 完整示例代码,快速上手 --- 修改 `config/swoole.php` 文件,添加事件监听器: ```php 'listen' => [ 'Open' => \app\listener\WsOpen::class, // 监听连接建立 'Message' => \app\listener\WsMessage::class, // 监听接收消息 'Close' => \app\listener\WsClose::class, // 监听连接断开 'Event' => \app\listener\WsEvent::class, // 监听事件消息 ], ``` 运行以下命令,创建监听器类文件: ```bash php think make:listener WsOpen php think make:listener WsEvent php think make:listener WsClose php think make:listener WsEvent php think make:listener WsMessage ``` ```php namespace app\listener; use think\Request; use think\swoole\Websocket; class WsOpen { public function handle(Websocket $websocket, Request $request) { $fd = $websocket->getSender(); $websocket->push("连接成功(fd: {$fd})"); echo "客户端 {$fd} 已成功建立 WebSocket 连接!\n"; } } ``` ```php namespace app\listener; use think\swoole\Websocket; class WsClose { public function handle(Websocket $websocket) { $fd = $websocket->getSender(); // 核心业务逻辑:清理该连接留下的“痕迹” // 1. 从 Redis 中删除该用户与 fd 的绑定关系 // 2. 更新数据库或缓存中的“在线状态”为离线 // 3. 广播给其他用户:“某某某下线了” echo "客户端 {$fd} 已断开连接,资源清理完毕。\n"; } } ``` ```php namespace app\listener; use think\swoole\Websocket; use Swoole\WebSocket\Frame; class WsMessage { public function handle(Websocket $websocket, Frame $frame) { $fd = $websocket->getSender(); echo "服务端收到 {$fd} 发送的消息: {$frame->data}\n"; } } ``` ```php namespace app\listener; use think\swoole\Websocket; use think\swoole\websocket\Room; use think\swoole\websocket\Event; class WsEvent { public function handle(Websocket $websocket, Event $event, Room $room) { $fd = $websocket->getSender(); $data = json_encode($event->data, JSON_UNESCAPED_UNICODE); echo "服务端收到 {$fd} 发送的 {$event->type} 事件: {$data}\n"; } } ```