应通过二进制前缀或subprotocol在协议层隔离心跳,而非依赖json字段;onmessage首行必须无条件更新lastmessagetime;心跳响应需同步零延迟;检查阈值须严控在nat超时内(如25秒间隔/75秒超时)。

用二进制前缀或 subprotocol 区分,别依赖 JSON 字段
靠 {"type":"ping"} 这类 JSON 结构识别心跳,本质是把协议语义和数据格式耦合了——一旦客户端发错格式(比如少个逗号、字段名拼错),json_decode() 失败,onMessage 里连 type 都取不到,更别说更新时间戳或响应。结果就是连接被误杀。
真正可靠的做法是:在协议层就做隔离。
- WebSocket 场景下,直接协商
subprotocol,比如客户端连时指定new WebSocket(url, ['heartbeat-v1']),服务端在onConnect里检查$connection->websocketSubProtocol,匹配则走心跳专用逻辑 - TCP 或自定义协议场景,约定一个轻量二进制前缀,比如首字节为
\x01表示心跳,\x02表示业务数据;服务端收包后先读头字节,不解析全文,直接分流 - 如果必须用 JSON,至少加一层原始校验:所有进入
onMessage的$msg,第一行强制$connection->lastMessageTime = time(),第二行再尝试json_decode($msg, true),失败就return,不干扰心跳计时
onMessage 里必须无条件更新 lastMessageTime
很多人只在成功解析出 type 后才写 $connection->lastMessageTime,这是最大隐患。哪怕客户端只发了个裸字符串 "ping" 或乱码二进制,只要数据抵达 Workerman 并触发了 onMessage,就说明链路通,时间戳就得刷。
否则,一次解析失败 → 时间戳不更新 → 下次检查超时 → 连接被关 —— 完全不是网络问题,而是协议处理逻辑漏掉了“存活信号”。
- 正确写法:
$connection->lastMessageTime = time();必须是onMessage函数体第一行,不加任何if - 匿名连接(没调
bindUid())也得初始化,否则定时器遍历$worker->connections时会触发 PHP Notice - 这个时间戳只代表“最后收到任意数据”,不是“最后收到有效业务消息”,二者语义不能混用
心跳响应必须同步、零延迟,禁止丢进协程或异步队列
客户端发完 ping 就开始倒计时等 pong,如果服务端响应被塞进协程调度、数据库查询回调或消息队列,哪怕只延迟 200ms,在弱网 + 高频心跳(如 25 秒间隔)下,客户端极易判定超时并主动断连。
- 识别到心跳包后,立刻
$connection->send('{"type":"pong"}'),不查数据库、不调外部 API、不进业务逻辑分支 - 不要用
Gateway::sendToClient($client_id, ...)代替直连$connection->send(),前者多一层路由开销 - 心跳路径上禁用任何中间件、日志埋点、性能监控 hook,确保从收包到回包控制在 1ms 内
服务端检查阈值必须严控在 NAT 最短超时内
Linux 内核 tcp_keepalive_time=7200 对长连接毫无意义——中间的运营商 NAT、4G 基站、企业防火墙,普遍在 60~120 秒静默回收空闲连接。你设成 120 秒检查,等于默认允许连接被中间设备砍掉一次。
真实可行的窗口是:心跳间隔 ≤ 45 秒,服务端超时阈值 ≤ 90 秒(容忍单次丢包)。农业传感器等低功耗设备可放宽至 90/150,但 4G 远程终端类必须压到 25/75。
- 绝对禁止设成 5 秒心跳:多数 4G 模组 AT 固件不支持,反而导致模组卡死或流量暴增
- 前端重连也不能固定秒数,得按失败次数指数退避,且首次断线后要立即重试,不是等 3 秒
- 真正难的不是写定时器,而是让每个数据入口点(
onMessage、onClose、甚至onError)都对时间戳有明确行为定义











