不加心跳的workerman长连接在真实网络中通常60秒内被中间件静默断开;需客户端每25秒发ping、服务端同步回pong,并调大nginx proxy_read_timeout至86400,配合定时器扫描清理假连接。

不加心跳的 Workerman 长连接,在真实网络环境下基本活不过 60 秒——不是代码写错了,而是中间件直接把你“静默踢出”。
为什么 Nginx / ALB / 运营商网关会主动断连
TCP 连接本身可以空闲数小时不中断,但你根本等不到那一刻。现实链路中堆着一堆只认“空闲时间”的中间设备:
- Nginx 默认
proxy_read_timeout 60s,第 61 秒无数据就发 FIN,不通知、不重试 - AWS ALB 固定 60 秒超时,不可调
- 国内三大运营商 NAT 网关普遍 30–90 秒空闲回收
- Android 后台进程休眠后,系统可能直接回收 Socket 文件描述符
它们不关心你的业务逻辑是否“正在等待用户输入”,只看 recv() 是否持续有数据流。一旦超时,连接在 TCP 层已断,Workerman 的 $connection->isConnected() 在下一次读写前仍返回 true,造成“假存活”。
onMessage 不是心跳,别指望它自动保活
Workerman 的 onMessage 只响应真实业务消息,它不会因为你没发数据就帮你“刷线”。常见错误包括:
- 把聊天场景当心跳:用户两分钟没打字,连接已被 Nginx 断开,
onMessage再也收不到任何东西 - 误以为
Worker::$maxConnection限制的是“活跃连接”:实际它只计数accept()成功的 socket,大量假连接占满配额,新用户连不上,监控却显示“连接数正常” - 在
onMessage里处理心跳时用了协程或异步 Redis 查询:导致pong延迟 >25 秒,客户端判定超时并主动 close
正确做法是:客户端每 25 秒发一次 {"type":"ping","ts":1725225061};服务端在 onMessage 中识别 type === "ping",立刻同步回 {"type":"pong"},不走任何队列或 IO 等待。
心跳检测必须配定时器 + 连接状态双校验
光回 pong 不够,你还得主动清理“已断但未感知”的连接。Workerman 没有内置心跳清理,必须自己用 Timer::add() 实现:
- 检查周期建议设为心跳间隔的 1.5–2 倍(如客户端 25s 心跳,服务端每 40s 扫一次)
- 每次扫描必须先调用
$connection->isConnected(),再判断time() - $connection->last_message_time > 60,否则对已断连接重复close()会触发 PHP warning - 别给每个连接单独起一个
Timer::add():成千上万连接会撑爆最小堆。应统一用单个定时器遍历全局$worker->connections
底层 Timer 基于最小堆,添加/执行复杂度是 O(log N),但高频创建定时器(比如每连接每秒一个)会导致堆频繁重排,CPU 升高且精度下降。
Nginx proxy_read_timeout 不改,心跳再勤也没用
Workerman 侧的心跳只是应用层努力,若前置代理卡着默认 60 秒,你客户端每 25 秒发 ping、服务端秒回 pong,到第 61 秒 Nginx 仍会单方面断开 TCP 连接。此时客户端收到的不是 pong,而是 WebSocket is already in CLOSING or CLOSED state 或直接触发 onerror。
必须同步修改 Nginx 配置:
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400; # 关键:设为 24 小时
}
这个值要大于你服务端心跳超时阈值(如 60 秒)和客户端最大可能延迟之和,否则所有心跳努力都在对抗一个根本没改的网关。
最易被忽略的一点:心跳包内容必须轻量、格式固定、解析零依赖。别用 json_decode($data, true) 全量解析再判断 type——万一客户端发了个非法 JSON,onMessage 就会因解析失败而跳过心跳逻辑,连接在下次扫描时被误杀。











