workerman纯tcp长连接自动断开主因是中间设备空闲超时静默关闭,onclose不触发因断开发生在tcp层以下;需自行实现双向心跳(非websocket参数),客户端≤25秒发ping、服务端45秒检测+60秒清理,并禁用nagle算法。

Workerman TCP长连接自动断开,绝大多数情况不是代码崩溃或逻辑错误,而是连接在应用层“看似活着”,实则已被中间网络设备静默关闭——典型表现是客户端发包无响应、服务端onClose没触发、日志里也查不到异常。
为什么onClose不触发,连接却断了?
因为断开发生在TCP层以下:NAT网关、运营商防火墙、云SLB等设备设置了空闲超时(常见30–180秒),一旦连接上没有数据流动,它们会直接丢弃后续包或发送RST,但不会通知Workerman。Workerman的socket仍处于ESTABLISHED状态,onClose自然不会触发。
- 用
ss -i src :端口可看到连接的retrans(重传)次数持续上升,说明SYN/ACK或数据包被中间设备拦截 -
tcpdump -i any port 端口抓包会发现客户端发了数据,但服务端收不到回包 - Workerman自身不感知这种“假死”,必须靠心跳主动探测
$worker->pingInterval和$worker->pingNotResponseClose怎么配?
这两个参数只对WebSocket协议生效,**对纯TCP连接完全无效**。如果你用的是tcp://0.0.0.0:5678,设置它们毫无作用——这是最常被误用的点。
- 纯TCP场景下,必须自己实现双向心跳:客户端定时发
{"type":"ping"},服务端收到后更新$connection->lastMessageTime = time() - 服务端另起一个定时器(如每45秒执行一次),遍历所有连接,对
time() - $connection->lastMessageTime > 60的连接调用$connection->close() - 别依赖
onMessage时间戳做判断——如果客户端只发一次就沉默,这个时间戳永远不会更新
客户端不回心跳,服务端怎么及时清理?
不能只等客户端发心跳再更新时间,要区分“有业务数据”和“纯心跳”。否则客户端发完业务消息就断网,服务端会误判为活跃连接。
- 在
onMessage里,先判断消息是否为心跳(如json_decode($data, true)['type'] === 'ping'),是则只回pong,不更新lastMessageTime - 只有非心跳消息才更新
lastMessageTime,这样能真实反映业务活跃度 - 客户端必须实现超时重连:发
ping后等待pong,若3秒内没收到,立即断开并重连——否则服务端清理了,客户端还傻等
公网部署时心跳间隔为什么不能设成60秒?
因为主流NAT设备的空闲超时集中在60–90秒,设成60秒等于踩在刀尖上:网络稍有延迟,一次心跳就可能错过,触发连续断连。
- 推荐客户端心跳间隔≤25秒(留足3倍余量),服务端检测阈值设为60秒
- 避免使用WebSocket原生
ping/pong帧——某些DPI设备会直接过滤,改用自定义JSON心跳更稳妥 - 禁用Nagle算法:
$connection->setProtocol(\Workerman\Protocols\Tcp::class); $connection->nodelay = true;,防止小包合并导致心跳延迟
真正难处理的不是心跳逻辑本身,而是客户端不可控:手机休眠、浏览器标签页冻结、小程序后台限制都会让心跳停摆。所以服务端清理策略必须激进(比如45秒未活动就踢),而客户端重连必须无感且带退避(指数级延迟重试)。这两边不同步,长连接就永远不稳定。











