根本原因是心跳检测未嵌入每个数据入口点,需在onmessage首行无条件更新lastmessagetime,并用二进制前缀或子协议可靠识别心跳包,避免json解析失败导致时间戳不更新。

Workerman服务在4G模组、农业传感器或远程终端等真实物联网场景中频繁掉线,根本原因不是代码写错,而是心跳检测没嵌进每个数据入口点——客户端发的{"type":"ping"}因JSON解析失败被return,lastMessageTime没更新,30秒后连接就被误判离线。
onMessage里无条件更新时间戳
第一步:打开你的Worker类文件,在onMessage回调函数第一行插入$connection->lastMessageTime = time();
这一步必须放在最开头,哪怕后续要json_decode($msg, true)失败也得先执行。很多人把这行代码塞在if判断后面,结果心跳包一解析出错就跳过,时间戳永远不更新。
第二步:删除所有对$connection->lastMessageTime的条件赋值逻辑,包括“仅当业务消息解析成功才更新”这类写法。
【所有进入onMessage的原始$msg,无论是否可解析、是否含type字段、是否为二进制,第一行就必须是$connection->lastMessageTime = time();】
安全初始化lastMessageTime属性
方法一:在onConnect中设为null
$connection->lastMessageTime = null;
方法二:在onMessage中首次收到数据时延迟赋值
if ($connection->lastMessageTime === null) { $connection->lastMessageTime = time(); }
这个做法能避开TLS握手失败、协议不匹配等连接建立后立刻断开的场景——那些连接根本没真正“活起来”,但定时器仍会遍历它们,直接赋值time()会触发PHP Notice。
设置全局心跳扫描定时器
第一步:在onWorkerStart回调中添加单一定时器
Timer::add(1, function() use ($worker) { });
第二步:在定时器回调内遍历所有连接
foreach ($worker->connections as $connection) { }
第三步:加guard防止未初始化连接报错
if ($connection->lastMessageTime !== null && time() - $connection->lastMessageTime > 90) { $connection->close(); }
注意:阈值必须严控在NAT防火墙最短超时内,运营商级NAT普遍60~120秒,所以设90秒是安全上限;农业传感器等低功耗设备可放宽到150秒,但不能超过模组AT固件支持的最大空闲时间。
客户端心跳间隔与服务端阈值匹配
客户端发ping间隔必须严格小于服务端超时阈值,形成冗余空间。
远程控制终端类设备:客户端每25秒发一次ping → 服务端检查阈值设为75秒
农业传感器:客户端每90秒发一次ping → 服务端检查阈值设为150秒
【绝对禁止客户端设5秒心跳——多数4G模组AT固件不支持高频心跳,会导致模块卡死或流量暴增】
区分心跳与业务消息的可靠方式
方法一:用二进制前缀标识
客户端发送\x01{"type":"ping"},服务端用substr($msg, 0, 1) === "\x01"快速识别,避免JSON解析失败导致心跳失效
方法二:启用WebSocket子协议
new WebSocket('ws://xxx', ['heartbeat.v1']);
服务端在onConnect中通过$connection->subProtocol获取协议名,再决定是否走心跳逻辑
这两种方式都比依赖{"type":"ping"}结构更健壮,尤其在弱网下JSON包体可能被截断或乱码时。











