onmessage第一行必须无条件更新lastmessagetime,无论消息是否合法;匿名连接也需初始化该字段;服务端超时阈值≤90秒,检查周期40~60秒;心跳须二进制标识并同步响应;前端重连需分层退避。

onMessage第一行必须无条件更新lastMessageTime
很多人只在成功解析业务数据后才写$connection->lastMessageTime = time(),结果客户端发的{"type":"ping"}因JSON解析失败被return跳过,时间戳根本没更新。30秒后服务端就误判离线。
不管消息是二进制、乱码、空字符串还是非法JSON,只要进了onMessage,第一行就得强制刷新时间戳:
$worker->onMessage = function($connection, $data) {
$connection->lastMessageTime = time(); // 必须放第一行,不加任何if
// 后续再做JSON decode、type判断、业务分发...
};
匿名连接(未调$connection->bindUid())也得初始化这个字段,否则定时器遍历时会触发PHP Notice。
服务端超时检查必须严控在NAT防火墙最短超时内
Linux内核tcp_keepalive_time=7200在公网完全无效。运营商NAT/4G基站普遍60~120秒静默回收空闲连接。设成90秒已是极限,再高就必丢连接。
检查逻辑不能依赖“每秒扫一次”这种暴力轮询:
- 心跳包由客户端发出,间隔建议≤45秒(如25秒或30秒)
- 服务端检查周期设为40~60秒(取心跳间隔的1.5~2倍)
- 超时阈值必须≤90秒,且要预留一次心跳丢包容忍(即允许客户端漏发一次)
示例中用Timer::add(45, ...)每45秒检查一次,但判断条件是time() - $conn->lastMessageTime > 90,这才是关键。
别把心跳响应写成异步或协程回调
收到ping必须立即同步回pong,不能丢进队列、不能await、不能sleep。延迟超过100ms就可能让客户端超时重连。
识别心跳的推荐方式是二进制前缀(如\x01)或WebSocket subprotocol,而不是依赖JSON结构——避免因json_decode()失败导致心跳被忽略。
同步响应写法示例:
if ($data[0] === "\x01") { // 二进制心跳标识
$connection->send("\x02"); // 立即回pong
return;
}
前端重连不能固定间隔,必须按网络生命周期退避
固定3秒重连在4G模组上是灾难:SIM卡附着可能耗时8秒,AT指令+CGATT: 1和+CREG: 1,1没返回前建WS全是空转。
真实可用的退避策略要分层:
- 首次断开:立刻重试(
new WebSocket()) - 连续失败2次:3~5秒 + ±1秒随机抖动
- 连续失败5次:升至15~30秒,并检查
navigator.onLine和信号强度
最难的不是写定时器,而是把lastMessageTime塞进每个数据入口点,以及让前端重连逻辑真正理解模组的注册周期——这一步漏掉,心跳再准也没用。











