必须用独立常驻进程运行WebSocket服务,不可在HTTP控制器中启动;ThinkPHP的短生命周期会销毁连接,导致断连、丢消息;正确做法是通过think-swoole扩展注册监听器,在onMessage中遍历$server->connections并校验isEstablished()后广播。

直接用 Swoole\WebSocket\Server 做广播,不走 ThinkPHP HTTP 生命周期,否则会卡住请求、丢连接、无法维持长连接——这是最常被忽略的前提。
为什么不能在普通控制器里 new WebSocket Server
ThinkPHP 的 HTTP 请求是短生命周期的:一次请求 → 执行完所有逻辑 → 关闭连接。而 WebSocket 必须保持一个长期运行的进程监听端口、管理连接池、响应心跳。如果在 index/index 这类 HTTP 控制器里启动 Swoole\WebSocket\Server,它会在响应结束时被销毁,客户端立刻断连。
- 错误现象:
WebSocket connection to 'ws://...' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED - 正确做法:把 WebSocket 服务抽成独立命令行脚本,用
php think swoole:server或php start_websocket.php启动 - ThinkPHP 官方
think-swoole扩展只支持 HTTP/WS 混合模式,但WebSocket路由必须显式注册,且不能和onOpen中的业务逻辑混写在同一个类里
如何用 think-swoole 正确注册 WebSocket 广播事件
安装 topthink/think-swoole 后,在 app/listener/ 下建一个 WebSocketListener 类,监听 SwooleEvent::ON_MESSAGE 事件,而不是在控制器里写 onMessage。
-
config/swoole.php中必须开启'type' => 'websocket',并配置'handler' => \app\listener\WebSocketListener::class - 广播不能用
$server->task()直接发,要先用Gateway::sendToAll()(需配合 GatewayWorker)或自己维护$server->connections数组遍历send() - 注意:Swoole 4.8+ 默认关闭
enable_coroutine,若在onMessage里调用 DB 查询,必须用协程 MySQL 客户端,否则阻塞整个 Worker 进程
广播前必须处理的三个连接状态问题
真实场景中,90% 的“消息没收到”不是代码错,而是连接状态没管好。
立即学习“PHP免费学习笔记(深入)”;
- 客户端未发
ping或服务端未回pong:Swoole 默认 60 秒超时断连,需在onOpen后立即设置$server->set(['heartbeat_idle_time'=>300, 'heartbeat_check_interval'=>60]) - 用户重复登录导致 fd 冲突:不能仅靠
$fd标识用户,要用 Redis 存user_id → fd_list映射,每次登录先publish下线通知,再存新 fd - 广播时跳过已断开的 fd:遍历
$server->connections前,必须用$server->isEstablished($fd)判断,否则触发 warning 导致进程 crash
上线前必须改掉的默认配置
本地跑通不等于能上生产。Swoole 默认配置在高并发下极易出问题。
-
worker_num不要设为 1:至少设为 CPU 核数 × 2,否则单个 Worker 卡住就全站 WebSocket 失效 -
max_conn必须显式设大(如 10000),否则默认值 1024,几百人同时在线就拒绝新连接 -
log_file要指向可写的 runtime 目录,否则onClose报错日志根本看不到,排查无从下手 - Nginx 反向代理 WebSocket 时,
proxy_read_timeout必须 ≥heartbeat_idle_time,否则 Nginx 主动断连
真正难的不是写几行 send(),而是让成百上千个 fd 在不同机器、不同网络状况下始终处于「可触达」状态——连接保活、异常剔除、用户维度聚合,这些才是广播可靠性的底层支点。



















