Workerman 5.2.3 中自定义协议的心跳检测需显式嵌入协议解析与连接生命周期:一、input/decode 必须无损识别心跳包(如首字节\x01或JSON标记);二、onMessage 首行强制更新 $connection->lastMessageTime;三、服务端超时检查设为30秒周期、90秒阈值;四、避免延迟响应、错误清理及Gateway进程误用。

Workerman 5.2.3 中自定义协议的心跳检测不能依赖框架自动处理,必须在协议解析流程和连接生命周期中显式嵌入。关键在于:心跳包要能被 input → decode 链路无损识别,且 onMessage 入口必须第一时间更新活跃时间戳——哪怕消息格式非法或不属于业务类型。
一、在自定义协议类里支持心跳识别
假设你已实现 MyProtocol 类,含 input()、decode()、encode() 三个静态方法。心跳包应设计为轻量、易识别的格式,推荐二进制前缀或固定 JSON 结构:
- 用二进制标识(更健壮):首字节为
\x01表示心跳包,\x02表示业务包;input()可直接读首字节判断是否完整(如固定 1 字节头 + 4 字节长度,则返回 5) - 用 JSON 标识(需容错):允许
{"type":"ping"}或{"t":"p"},但decode()不应因json_decode($data) === null就抛异常,而应返回一个带标记的结构,例如:return ['type' => 'heartbeat', 'raw' => $data]; -
input()必须能准确截断心跳包:不能因为心跳包太短(如仅 12 字节)就误判为不完整,否则会堆积缓冲区、阻塞后续数据
二、onMessage 中无条件更新 lastMessageTime
这是最容易出错的一环。无论 $msg 是解码成功还是失败,只要进入 onMessage,就必须立刻记录时间:
- 第一行强制写:
$connection->lastMessageTime = time(); - 再判断是否为心跳:
if (isset($package['type']) && $package['type'] === 'heartbeat') { $connection->send(['type'=>'pong']); return; } - 匿名连接(未 bindUid)也必须初始化该字段,否则定时器遍历时触发 PHP Notice
三、服务端超时检查要适配 NAT 实际限制
Workerman 5.2.3 的 Timer::add() 仍基于最小堆,性能可靠,但逻辑需严守网络现实:
- 客户端心跳间隔 ≤45 秒(避开主流运营商 NAT 60 秒静默回收)
- 服务端检查周期设为 30 秒,超时阈值设为 90 秒(容忍一次心跳丢失)
- 检查逻辑必须同步执行,禁止在回调里启动协程或异步任务:
if (time() - ($connection->lastMessageTime ?? 0) > 90) { $connection->close(); }
四、避免常见陷阱
这些细节在 5.2.3 版本中依然生效,且影响线上稳定性:
- 别把心跳响应写成
Timer::add(0.1, fn() => $connection->send(...))—— 延迟 pong 会导致客户端主动断连 - 不要在
onClose里才清理lastMessageTime,它只读不写;真正清理应在连接销毁后由 GC 处理 - 若使用 GatewayWorker,心跳逻辑应放在
Events.php的onMessage,而非 Gateway 进程 —— 后者不直接持有连接对象 - 前端重连不能与心跳间隔强绑定:比如心跳 45 秒,重连初始间隔建议 1~3 秒,失败后指数退避,而非固定 45 秒

















