心跳三要素须满足超时阈值>检测间隔×2且>心跳周期×2,例如心跳25秒、检测1秒,则超时至少65秒;客户端须发送可见标识如"ping"或json心跳包,服务端收到后仅更新lastmessagetime,避免误判断连。

Workerman主动推送信息时,若心跳检测设置不合理,连接会在防火墙或NAT设备超时前被静默断开,导致推送消息无法触达客户端;必须让心跳频率、检测间隔、超时阈值三者形成安全冗余,不能简单设为相同数值。
先明确心跳三要素的数学关系
心跳发送周期(客户端发PING)、心跳检测间隔(服务端每秒扫一次)、连接超时阈值(最后活动时间距今超过多少秒就关)——这三者必须满足:【超时阈值 > 检测间隔 × 2,且超时阈值 > 心跳周期 × 2】。例如心跳每30秒发一次,检测每1秒跑一次,那超时至少设61秒;设60秒就存在刚好卡在两次检测之间断连却没被及时发现的风险。
不满足这个关系,就会出现“连接已断但服务端还当它活着”——推送消息发出去石沉大海。
WebSocket场景下标准配置写法
定义常量:
define('HEARTBEAT_INTERVAL', 25); // 客户端心跳发送间隔,比30秒少5秒留缓冲
define('HEARTBEAT_CHECK_INTERVAL', 1); // 服务端每秒检查一次
define('HEARTBEAT_TIMEOUT', 65); // 超时阈值,必须大于25×2且大于1×2,取65最稳妥
在Worker启动时注册心跳检测逻辑:
$worker->onWorkerStart = function ($worker) {
Timer::add(HEARTBEAT_CHECK_INTERVAL, function () use ($worker) {
$now = time();
foreach ($worker->connections as $connection) {
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $now;
continue;
}
if ($now - $connection->lastMessageTime > HEARTBEAT_TIMEOUT) {
$connection->close("heartbeat timeout");
}
});
};
客户端心跳发送必须带可识别标识
方法一:前端JavaScript用字符串"PING"发送
ws.onopen = () => { setInterval(() => ws.send("PING"), HEARTBEAT_INTERVAL * 1000); };
方法二:更健壮的做法是发送JSON结构体,便于后端区分业务消息和心跳包
ws.onopen = () => { setInterval(() => ws.send(JSON.stringify({ type: "heartbeat", ts: Date.now() })), HEARTBEAT_INTERVAL * 1000); };
⚠️注意:如果客户端只发空字符串或不可见字符(如\u0000),某些代理或WAF会拦截或丢弃,导致服务端收不到任何心跳信号——【务必使用可见、非控制字符的明确心跳标识】。
服务端接收心跳并更新时间戳
第一步:在onMessage回调中捕获心跳包
第二步:判断是否为心跳类型,是则仅更新时间戳,不走业务逻辑
$worker->onMessage = function ($connection, $data) {
try {
$msg = json_decode($data, true);
if (is_array($msg) && isset($msg['type']) && $msg['type'] === 'heartbeat') {
$connection->lastMessageTime = time();
return; // 阻止后续业务处理
}
} catch (\Exception $e) {
// 非JSON数据当作原始心跳字符串处理
if (trim($data) === 'PING' || trim($data) === 'HEARTBEAT') {
$connection->lastMessageTime = time();
return;
}
}
// 此处放真实业务消息处理逻辑
};











