websocket长连接不断开的关键在于服务端及时响应ping帧、反向代理超时配置合理、客户端心跳可控三者协同。swoole需手动注册onping回调以自动回pong帧;workerman须在onmessage开头解析opcode并发送\x8a\x00;nginx的proxy_read_timeout须大于心跳间隔×2;客户端须校验readystate并设超时重连机制。

PHP 实现 WebSocket 长连接不断开,核心不在“连上”,而在“持续被中间层和对端认为还活着”。光设个定时器发消息没用——Nginx 会静默关掉空闲连接,Swoole 自己可能先踢人,浏览器根本不会自动发 ping,而 Workerman 压根不识别 ping 帧。必须服务端响应及时、反向代理配置对齐、客户端行为可控,三者缺一不可。
为什么 Swoole 的 onPing 必须手动注册
Swoole 不会自动回复 WebSocket 协议规定的 ping 帧(opcode 0x9),即使你启用了 websocket.heartbeat_idle_time。不注册 onPing 回调,收到 ping 就丢弃,Nginx 在 proxy_read_timeout(默认 60 秒)后直接断连,服务端日志里甚至看不到错误。
- 必须在 Server 启动前注册:
$server->on('ping', function ($server, $fd) { }); - 回调里不用做任何事,Swoole 会自动回最小 pong 帧(
\x8a\x00),但不注册就等于没这回事 - 若同时用了自定义心跳消息(如
{type:"heartbeat"}),onPing和业务逻辑要分开处理,别混在一起
Workerman 怎么正确响应 ping 帧
Workerman 没有 onPing 事件,得在 onMessage 里手动解析帧头判断 opcode。浏览器或某些客户端发的 ping 是二进制帧,不是 JSON 字符串,直接 json_decode($data) 会失败,还会漏掉关键心跳信号。
- 先检查
$data是否为二进制:用ord($data[0]) & 0x0f提取低 4 位 opcode - 如果是 0x9(ping),立刻发最小 pong 帧:
$connection->send("\x8a\x00"); - 不要尝试解析 ping 内容——标准 ping 帧 payload 可为空,解析反而出错
- 注意:此逻辑必须放在
onMessage开头,避免被后续业务逻辑拦截或超时阻塞
Nginx 的 proxy_read_timeout 必须大于客户端心跳间隔 ×2
这个值决定 Nginx 保持后端连接多久不收数据就断开。它和 Swoole 的 heartbeat_idle_time 完全无关——后者只管内存里连接是否“空闲”,不管 Nginx 已经把连接关了。
- 假设前端每 30 秒发一次心跳,Nginx 至少设为
proxy_read_timeout 60;,建议设 120 或 180 更稳妥 - 还要同步调整
proxy_send_timeout,防止大消息发送卡住触发超时 - SSL + CDN 场景下,Cloudflare 要打开「WebSockets」开关;部分免费 CDN 强制 120 秒断连,换方案比调参数更有效
客户端不能只靠 ws.send() 发心跳
浏览器 WebSocket API 不自动发 ping,且 ws.send() 在连接已关闭时会抛异常或静默失败。不检查状态就发,心跳实际没出去,服务端却还在等,最后双双断连。
- 发心跳前必加判断:
if (ws.readyState === WebSocket.OPEN) { ws.send(...); } - 心跳消息推荐用轻量自定义格式(如
"{t:'h'}"),比 JSON.parse 更快,也比标准 ping 帧更容易和服务端业务逻辑对齐 - 前端要设超时兜底:比如 60 秒没收到服务端确认(哪怕只是 echo),就主动
ws.close()并重连 - 别依赖单次超时就断连——弱网下可能连续丢 2–3 次,应记录最近 3 次成功时间,仅当「最后成功心跳距今 > 3 × 间隔」才判定失联
最容易被忽略的是:心跳机制的有效性,取决于服务端响应延迟、Nginx 超时、CDN 行为、客户端发送时机这四者的最小值。任何一个环节卡住或配错,长连接就会在你看不见的地方悄然断开。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











