必须同时配置应用层心跳协议与tcp层keep-alive;仅启用其一将导致60~300秒内出现半开连接。tcp keep-alive需在onconnect中调用socket_set_option显式开启并调优参数,应用层心跳则通过lastmessagetime+定时器或时间轮实现连接活性检测。

要在 Workerman 4.0.10 中真正让长连接不被 NAT 或防火墙静默断开,必须同时配置应用层心跳协议与 TCP 层 Keep-Alive,缺一不可;只做一边会导致连接在 60~300 秒内无预警失效,客户端发包成功但服务端收不到,形成“半开连接”。
启用 TCP 层 Keep-Alive(必须在 onConnect 中操作)
Workerman 4.0.10 默认完全不启用 TCP Keep-Alive,底层 socket 继承系统默认值(Linux 通常为 7200 秒),远超游戏或 IM 场景容忍阈值。
第一步:在 onConnect 回调中获取原始 socket 资源,调用 socket_set_option 显式开启并调优参数:
$connection->getSocket() → socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
第二步:设置探测起始时间(TCP_KEEPIDLE)、重试间隔(TCP_KEEPINTVL)和失败次数(TCP_KEEPCNT)。Linux 下需使用 socket_set_option 的 SOL_TCP 域:
socket_set_option($socket, IPPROTO_TCP, TCP_KEEPIDLE, 45); // 首次探测前空闲 45 秒socket_set_option($socket, IPPROTO_TCP, TCP_KEEPINTVL, 15); // 每隔 15 秒重探socket_set_option($socket, IPPROTO_TCP, TCP_KEEPCNT, 3); // 连续 3 次无响应则断连
【必须在 onConnect 中执行,不能放在 onWorkerStart 或全局初始化里;否则 socket 资源未创建,会报 Warning: socket_set_option(): unable to set socket option】
注意:Windows 不支持 TCP_KEEPIDLE/TCP_KEEPINTVL,仅能设 SO_KEEPALIVE 开关;若需跨平台兼容,建议封装判断逻辑。
实现应用层心跳协议(PING/PONG)
TCP Keep-Alive 只能穿透中间设备,无法感知业务层“假活”——比如客户端进程卡死但 TCP 连接仍 ESTABLISHED。此时必须靠应用层心跳识别真实活跃状态。
方法一:基于 Connection 自定义属性 + 全局定时器检测
在 onConnect 中为每个连接初始化最后通信时间戳:$connection->lastMessageTime = time();
在 onMessage 中更新该时间戳:$connection->lastMessageTime = time();
在 onWorkerStart 中启动全局检测定时器(例如每 55 秒执行一次):
Timer::add(55, function () {<br> foreach ($worker->connections as $connection) {<br> if (time() - $connection->lastMessageTime > 60) {<br> $connection->close();<br> }<br> }<br>});
方法二:为每个连接单独绑定超时定时器(更精准,但内存占用略高)
在 onConnect 后立即为该连接添加专属心跳检测定时任务:
$connection->heartbeat_timer_id = Timer::add(60, function () use ($connection) {<br> if (time() - $connection->lastMessageTime > 60) {<br> $connection->close();<br> }<br>});
当连接关闭时(onClose),务必清除该定时器:
if (isset($connection->heartbeat_timer_id)) {<br> Timer::del($connection->heartbeat_timer_id);<br>}
处理客户端心跳请求(服务端响应 PONG)
客户端发送 {"type":"ping"} 或原始字节 0x01 时,服务端必须回传对应 {"type":"pong"} 或 0x02,否则客户端无法确认服务端存活。
在 onMessage 中加入判断逻辑:
if ($data === "\x01" || $data === '{"type":"ping"}') {<br> $connection->send('{"type":"pong"}');<br> $connection->lastMessageTime = time();<br> return;<br>}
这一步必须放在消息解析之前,避免被业务逻辑拦截或丢弃;【如果客户端发 ping 但服务端没响应 pong,客户端超时后会主动断连,此时服务端连接对象仍存在但已失效】
无需额外记录 ping 时间戳——只要收到 ping 就说明连接活跃,直接刷新 lastMessageTime 即可。
避免定时器性能瓶颈(时间轮优化)
当连接数超过 1 万时,全局遍历所有连接的定时器(方法一)会产生明显 CPU 压力;最小堆定时器管理虽高效,但大量短周期任务会频繁触发堆重排。
推荐改用时间轮(Timing Wheel)结构替代默认最小堆:引入第三方扩展 workerman/timing-wheel 或自行实现单层时间轮,将超时检查从 O(N) 降为 O(1) 平摊复杂度。
初始化时间轮(精度 1 秒,总槽位 3600):
$wheel = new TimingWheel(3600, 1);<br>$wheel->start();
为每个连接注册超时任务:
$wheel->add($connection->id, 60, function () use ($connection) {<br> if (time() - $connection->lastMessageTime > 60) {<br> $connection->close();<br> }<br>});
连接关闭时移除任务:$wheel->remove($connection->id);











