think-swoole-websocket
在 ThinkPHP 6 (TP6) 中使用 think-swoole (通常对应 v3.x 版本插件) 实现 WebSocket,核心逻辑是基于事件(Event)驱动。
以下是针对 TP6 开发环境的实战配置、多房群聊(房间)、在线状态追踪以及 Nginx 反向代理的完整落地方案。
1. 核心文件结构
在 TP6 中,建议将 WebSocket 相关的事件监听类统一归类。结构如下:
text
app/
├── listener/
│ ├── WsConnect.php # 监听连接
│ ├── WsClose.php # 监听断开
│ └── WsMessage.php # 监听自定义业务消息
config/
└── swoole.php # swoole 配置文件
请谨慎使用此类代码。
2. 精准事件配置 (config/swoole.php)
TP6 中,think-swoole 内置了类似 socket.io 的分发机制。你需要将底层事件映射到你的全局监听类中:
php
return [
'server' => [
'host' => '0.0.0.0',
'port' => 9501, // 建议避开常规端口
],
'websocket' => [
'enable' => true,
'handler' => \think\swoole\websocket\Handler::class,
'listen' => [
// 基础连接事件
'connect' => \app\listener\WsConnect::class,
'close' => \app\listener\WsClose::class,
// 自定义业务事件(客户端发送不同的事件名,路由到对应的类)
'join' => \app\listener\WsMessage::class, // 加入房间
'chat' => \app\listener\WsMessage::class, // 聊天
],
],
// 必须开启 RPC 或常驻内存相关的沙箱,确保多线程下容器正常工作
'sandbox' => [
'child_status' => true,
],
];
请谨慎使用此类代码。
3. 事件处理器编写 (基础与业务)
think-swoole 在事件触发时会依赖依赖注入。你可以直接在 handle 方法中注入 think\swoole\Websocket 对象和当前的数据。
A. 连接管理与用户绑定 (app/listener/WsConnect.php)
php
namespace app\listener;
use think\swoole\Websocket;
class WsConnect
{
public function handle(Websocket $ws)
{
// 获取当前连接的客户端文件描述符 (fd)
$fd = $ws->getSender();
// 建议:此处可以通过握手参数(如:ws://url?token=xxx)获取用户信息
// 并将 uid 与 fd 的映射关系存入 Redis,用于之后的精准推送
$ws->emit('connected', ['fd' => $fd, 'msg' => '连接成功']);
}
}
请谨慎使用此类代码。
B. 核心业务:房间与聊天 (app/listener/WsMessage.php)
think-swoole 提供了非常方便的 join, leave, to(房间隔离)方法:
php
namespace app\listener;
use think\swoole\Websocket;
class WsMessage
{
protected $ws;
// 构造函数注入 Websocket 实例
public function __construct(Websocket $ws)
{
$this->ws = $ws;
}
public function handle($data)
{
// $data 是客户端发送过来的载荷数据
// 配合 Swoole 的沙箱,这里可以直接调用系统方法
// 伪代码:通过当前事件名称执行不同逻辑
// 注:TP6 事件分发时,如果多个事件指向同一个类,可以通过辅助手段或拆分不同类处理。
// 推荐:为不同的业务事件建立独立的 Listener 类(如 WsChat, WsJoin)
}
/**
* 场景一:加入房间逻辑(可放于单独的 WsJoin 监听器中)
*/
public function joinRoom($data)
{
$roomName = $data['room'] ?? 'default_room';
$currentFd = $this->ws->getSender();
// 将当前连接加入指定房间
$this->ws->join($roomName);
// 通知当前房间内的所有人(不含自己用 to($room)->broadcast(),含自己用 to($room))
$this->ws->to($roomName)->emit('sys_msg', "用户 {$currentFd} 加入了房间");
}
/**
* 场景二:群发聊天逻辑(可放于单独的 WsChat 监听器中)
*/
public function sendChat($data)
{
$roomName = $data['room'] ?? 'default_room';
$message = $data['content'] ?? '';
// 推送给房间内的所有人
$this->ws->to($roomName)->emit('new_chat', [
'from' => $this->ws->getSender(),
'content' => $message,
'time' => date('H:i:s')
]);
}
}
请谨慎使用此类代码。
4. 前端 JS 调用规范
think-swoole 底层对原生 WebSocket 数据包做了封装。前端发送的数据格式必须是 JSON 数组,格式为 ["事件名", 数据对象]:
javascript
const ws = new WebSocket('ws://127.0.0.1:9501');
ws.onopen = () => {
console.log('WS 连接成功');
// 1. 发送加入房间事件
ws.send(JSON.stringify([
'join',
{ room: 'room_101' }
]));
};
ws.onmessage = (res) => {
// 服务端返回的数据也是标准 JSON
const [event, data] = JSON.parse(res.data);
if (event === 'sys_msg') {
console.log('系统通知:', data);
}
if (event === 'new_chat') {
console.log('收到消息:', data.from + ': ' + data.content);
}
};
请谨慎使用此类代码。
5. 生产环境配置:Nginx 反向代理 (支持 HTTPS/WSS)
在生产环境中,严禁直接暴露 9501 端口。应使用 Nginx 作为前置代理,转发 WebSocket 连接并配置 SSL:
nginx
server {
listen 80;
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /path/to/ssl.crt;
ssl_certificate_key /path/to/ssl.key;
# 正常的 HTTP 业务走 TP6
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if (!-e $request_filename) {
proxy_pass http://127.0.0.1:9501;
}
}
# 特定的 /ws 路径走 WebSocket 转发
location /ws {
proxy_pass http://127.0.0.1:9501; # 指向 TP6 Swoole 端口
proxy_http_version 1.1;
# 核心:升级协议为 WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 延长超时时间,防止 Nginx 自动断开长连接(根据业务调整,单位:秒)
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
请谨慎使用此类代码。
前端连接地址顺应改为:wss://://yourdomain.com
如果想继续深入开发,可以告诉我:
需要引入 Redis 来持久化存储用户在线状态(Fd 与 UserID 绑定)的代码吗?
业务中是否需要实现**点对点私聊(指定 uid 发送)**而不仅仅是房间群聊?
是否遇到了代码修改后,Swoole 热重载(Hot Reload)失效的问题?