onmessage第一行必须无条件更新$connection->lastmessagetime,无论消息是否可解析;匿名连接也需初始化该字段;超时阈值须严控在nat防火墙最短超时内(如≤90秒),并区分设备类型设定心跳间隔与容忍阈值。

onMessage第一行必须更新lastMessageTime
客户端离线检测失效,八成是因为$connection->lastMessageTime没在入口处无条件更新。很多人只在成功解析业务消息后才写这句,结果客户端发了个原始{"type":"ping"},JSON decode失败直接return,时间戳根本没动——30秒后就被误判下线。
正确做法是:不管消息内容、格式、是否可解析,只要进onMessage,第一行就是$connection->lastMessageTime = time()。
- 匿名连接(未调
$connection->bindUid())也得初始化该字段,否则定时器遍历时会触发PHP Notice - 若需区分心跳和业务消息,建议用二进制前缀(如
\x01)或WebSocket subprotocol,别依赖JSON结构判断 - 不要在
onMessage里做耗时操作(如同步HTTP请求),否则会阻塞时间戳更新,导致误判
服务端超时阈值必须严控在NAT防火墙最短超时内
Linux内核的tcp_keepalive_time=7200在公网几乎无效。运营商级NAT/4G基站防火墙普遍在60~120秒内静默回收空闲连接。设超时为120秒,等于默认允许一次心跳丢失;设180秒,大概率已被中间设备断连,服务端还傻等。
实际建议:
- 通用场景:心跳间隔≤45秒,服务端检查阈值≤90秒
- 低功耗设备(如农业传感器):心跳90秒,服务端容忍150秒(允许丢两次)
- 高可靠终端(如远程控制面板):心跳25秒,服务端阈值75秒,并配合客户端三次探测+指数退避
- 绝对禁止设5秒心跳——多数4G模组AT固件不支持,反而卡死或流量暴增
定时器里检查连接存活必须带防错兜底
Workerman没有内置心跳机制,得自己用Timer::add()轮询。但很多人只写if (time() - $conn->lastMessageTime > 90) { $conn->close(); },漏掉关键边界。
真实代码里必须:
- 先判断
$conn->lastMessageTime是否存在,避免未初始化连接报Notice - 检查
$conn->isClosed()再操作,防止对已关闭连接调send()或close()引发警告 - 在
onClose里清理自定义属性(如unset($conn->lastMessageTime)),否则下次复用连接时残留旧时间戳 - 不要在定时器回调里做阻塞操作(如
file_get_contents),否则整个事件循环卡住,所有连接都无法检测
客户端断网但服务端没触发onClose怎么办
这是最隐蔽的问题:客户端物理断网(拔网线、断电),服务端既不收数据也不发FIN,onClose永远不会触发,lastMessageTime停在断连前一刻,靠定时器还能发现;但若客户端只是“假死”(应用崩溃但TCP连接未释放),服务端可能永远等不到超时。
此时要靠php start.php connections命令辅助判断:
- 查
send_queue_size > 0且last_send_time长时间未更新 → 客户端已不可达,但连接没关 - 大量连接处于
close_wait状态 → 很可能是onClose里做了同步IO(如日志写入、HTTP上报),阻塞了连接释放流程 - 某worker的
connections长期为0但total_request不动 → 该worker的onMessage或onClose里有未捕获异常,被Workerman静默吞掉后不再处理新连接
真正难的不是写定时器,而是把心跳逻辑嵌进每个数据入口点,并让onClose能应对各种异常退出路径——比如客户端进程被kill -9,服务端得靠超时兜底,不能指望它主动发FIN。











