不能。onWorkerStart中设全局定时器无法绑定具体连接,保活需为每个活跃连接单独设置心跳定时器,绑定$fd并配合onClose清理,实现双向检测与动态超时。

onWorkerStart 里加定时器真能保活长连接?
不能。在 onWorkerStart(常见于 Swoole Worker 进程启动钩子)中直接 new 一个全局定时器,对长连接保活基本无效——它只在当前 Worker 进程内生效,而客户端连接可能被调度到任意 Worker,且定时器无法感知具体连接状态。
真正要保活的是「每个活跃连接」,不是整个进程。心跳必须绑定到具体 connection 或其上下文(如 $fd),否则发出去的包根本不知道该问谁、该等谁回。
- 定时器必须在连接建立时(
onConnect)或首次数据到达后启动,且与该连接生命周期强绑定 - 不能用
Swoole\Timer::tick()全局轮询所有$fd,高并发下 CPU 和系统调用开销爆炸 - 若用
onWorkerStart启动一个“扫描线程”,还要自己维护连接 Map、加锁、处理超时,等于重复造轮子且极易出错
心跳发送该放在 onReceive 还是独立 channel?
都不合适。靠 onReceive 触发心跳是被动响应,客户端不发数据你就永远不会发心跳,连接早被 NAT 或中间设备静默断开了;而另起 goroutine / 协程轮询,又破坏了 Swoole 的事件驱动模型,容易引发协程混乱或 fd 被回收后误操作。
正确做法是:在连接建立后,用 $server->after() 或 $server->tick() 为该连接单独设置一个「连接级心跳定时器」,并把 $fd 作为闭包变量捕获。
$server->tick(30000, function() use ($fd, $server) { if ($server->isEstablished($fd)) { $server->send($fd, json_encode(['type'=>'ping'])); } });- 必须配合
onClose中调用$server->clearTimer($timerId)(需提前保存 ID),否则内存泄漏+定时器继续向已关闭 fd 写数据,触发 warning - 心跳包内容建议轻量,比如纯字符串
"ping"或二进制空包,避免序列化开销和协议解析负担
服务端怎么判断心跳失效?别只看「没收到 ping」
只等客户端发 ping 再判超时,太被动。真正的健壮逻辑是双向检测:服务端发 ping,等 pong;同时监听客户端发来的 ping,立刻回 pong —— 这样既能发现网络中断,也能发现客户端进程卡死或崩溃。
- 客户端不回
pong达 2 个周期(如 60s),才主动close($fd),避免误杀弱网用户 - 服务端收到
ping必须立即回pong(不要走业务队列),否则延迟累积会导致客户端误判 - 注意:Swoole 默认关闭
heartbeat_check_interval,即使开了,也只做基础 TCP 层探测,无法替代应用层心跳语义
心跳间隔设成 5 秒还是 30 秒?看你的部署环境
没有通用最优值。5 秒在局域网很稳,但在移动网络下会显著增加电量和流量消耗;30 秒对大多数公网服务够用,但遇上运营商 NAT 映射 60–120 秒超时,就容易掉连。
推荐策略:初始设 25 秒,同时在客户端埋点统计「从发 ping 到收 pong 的 P95 延迟」,动态上调至延迟 × 3;服务端同步记录每个连接的最近一次 pong 时间戳,超时阈值 = 心跳间隔 × 2.5。
- Nacos 默认心跳间隔
DEFAULT_HEART_BEAT_INTERVAL = 5000,超时DEFAULT_HEART_BEAT_TIMEOUT = 15000,这是注册中心场景,对长连接 IM/推送不适用 - WebSocket 场景下,浏览器限制
setInterval最小约 4ms,但实际网络抖动会让 10s 以下心跳变得不可靠 - 千万别把心跳间隔硬编码进配置文件还标“永久有效”——上线后得靠监控曲线反推真实水位
最常被忽略的一点:心跳不是发完就完事,你得确保每次 send 都成功返回,失败时要立即 close fd 并清理资源。很多线上问题,不是心跳没发,而是 send 返回 false 后被静默吞掉了。










