Workerman心跳检测必须用time()而非microtime(true),因其秒级精度足够且无浮点误差、内存小、速度快;误用microtime会导致超时不触发,且NTP/虚拟化环境易引发时钟倒退误判。

Workerman 心跳检测为什么必须用 time() 而不是 microtime(true)
因为心跳超时判断依赖的是「秒级时间差」,而 time() 返回整型、无精度误差、内存占用小、比较快;用 microtime(true) 反而引入浮点数比较风险(如 1716345020.999999 和 1716345021.000001 在 PHP 浮点精度下可能被判定为不等),且每次调用开销略高。所有官方示例和生产环境实践都只用 time() 记录 lastMessageTime。
实际踩坑点:
- 若误用
microtime(true)存入$connection->lastMessageTime,后续做time() - $connection->lastMessageTime > 60时,PHP 会隐式转成 float,可能因精度丢失导致超时不触发 -
time()是系统时钟,受 NTP 同步影响极小;而microtime(true)在某些容器或虚拟化环境中可能因时钟漂移出现倒退,引发误判 - Workerman 定时器回调里也应统一用
time()做判断,避免混用造成逻辑错位
如何在 onConnect 中安全初始化 lastMessageTime
不能直接写 $connection->lastMessageTime = time(); 就完事——有些连接可能在建立后立刻断开(如 TLS 握手失败、协议不匹配),还没走到 onMessage 就 close,但定时器仍可能访问该属性,触发 Notice。
推荐做法是结合属性存在性检查 + 延迟赋值:
立即学习“PHP免费学习笔记(深入)”;
- 在
onConnect中先不设值,或设为null:$connection->lastMessageTime = null; - 在
onMessage中首次收到数据时才赋值:if ($connection->lastMessageTime === null) { $connection->lastMessageTime = time(); } - 定时器扫描逻辑里加 guard:
if ($connection->lastMessageTime !== null && time() - $connection->lastMessageTime > 60) { $connection->close(); }
这样可避免对未真正“活起来”的连接做无效心跳判断,也防止 undefined property 报错。
Timer::add() 放在 onWorkerStart 还是每个连接单独设?
必须放在 onWorkerStart 做全局扫描,而不是为每个连接 new 一个 Timer::add()。后者在万级连接时会导致定时器数量爆炸,Workerman 底层最小堆管理成本陡增,CPU 占用飙升,且极易内存泄漏。
正确姿势是单一定时器轮询所有连接:
- 用
Timer::add(1, function() use ($worker) { ... })每秒扫一次,比高频 tick 更稳 - 遍历
$worker->connections时注意:连接可能正在关闭中,isset($connection->lastMessageTime)比!empty()更可靠 - 别在循环里调用
$connection->close()后继续操作该对象,应先收集需关闭的$fd,再统一处理 - 若业务要求更精细超时控制(如不同用户心跳阈值不同),可在
$connection上挂载自定义字段,如$connection->heartbeatTimeout = 45;,而非起一堆定时器
客户端心跳间隔为什么必须严格小于服务端超时阈值
不是“建议”,而是硬约束。例如服务端设 lastMessageTime 超过 60 秒即断连,客户端却每 65 秒发一次心跳,中间有 5 秒空窗——Nginx 的 proxy_read_timeout 60 就会在这期间切断连接,你根本等不到 Workerman 自己清理。
真实部署中要拉通三层时间:
- 客户端发送间隔(如 30s)
- 服务端超时阈值(如 60s),必须 ≥ 客户端间隔 × 1.5,留出网络抖动余量
- Nginx / CDN 的
proxy_read_timeout(如 120s),必须 ≥ 服务端阈值 × 2,否则代理先动手
漏掉任意一层,心跳就形同虚设。很多线上“偶发断连”问题,最后都定位到 CDN 的 WebSocket 超时配置没同步改。



















