swoole服务端主动断开“正常”连接是因heartbeat_idle_time超时机制设计所致,非bug;默认60秒无数据即关闭,定时30秒扫描,低频业务需调大该值,协程客户端须手动tick发合规心跳包。

为什么 Swoole 服务端会主动断掉看似“正常”的连接
因为服务端根本不管客户端是否“活着”,只认 heartbeat_idle_time 这个时间戳。只要连接在该时间内没收发任何数据,Swoole 就直接 close() 掉——不是 bug,是设计如此。
常见现象:客户端 connect() 成功,但 60 秒后无业务通信,onClose 就被触发了;你没发错包,也没 crash,就是被“礼貌清退”了。
-
heartbeat_idle_time默认 60 秒,heartbeat_check_interval默认 30 秒:意味着服务端每 30 秒扫一次,发现空闲 ≥60 秒的连接就干掉 - 这个机制完全不依赖客户端发
"ping",纯靠服务端记时 + 定时扫描 - 如果你的业务本身就是低频上报(比如 IoT 设备每 5 分钟传一次数据),必须调大
heartbeat_idle_time,否则必断
协程客户端没法用 set(['heartbeat' => 45]) 开心跳
这个坑文档没写清楚:set(['heartbeat' => x]) 只对传统 swoole_client(同步阻塞)生效,对 Co\Socket、Co\Http\Client、Co\WebSocket\Client 全无效——设了等于白设。
真正能用的只有手写 tick + send():
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
$client->tick(45000, function() use ($client) {
if ($client->isConnected()) {
$client->send("\x00\x00\x00\x01\x01"); // 匹配服务端 open_length_check 格式
}
});
- 心跳内容不能随便发
"ping":如果服务端开了open_length_check或自定义包头,纯字符串会被丢弃或解析失败 - 每次
send()后务必检查$client->isConnected():返回false不代表刚断,可能是上次 send 就已失效 - 别在
onConnect里只发一次:必须持续 tick,且间隔要 heartbeat_idle_time(建议 ≤ 45 秒)
WebSocket 客户端如何判断“真断线”而不是网络抖动
浏览器原生 WebSocket 没有内置健康探测,onclose 触发太晚(往往等超时或发包失败才响应),所以必须自己加心跳 + 超时计数。
- 心跳包发出去后,启动一个
setTimeout等pong回复,超时(如 5s)就计 1 次失败 - 连续 3 次没收到
pong,才判定为真断线,触发重连;单次超时大概率是抖动 - 收到任何业务消息也要重置心跳计时器:说明连接还通,不用急着重连
- 别依赖
ws.readyState === WebSocket.OPEN:这个值可能还是OPEN,但中间 NAT 已经把连接踢了
重连时最容易忽略的资源竞争和状态残留
重连不是 new 一个新 client 就完事。旧连接的定时器、未处理的回调、未释放的资源,全还在内存里跑。
- 每次重连前,必须手动
clearTimeout/clearInterval所有心跳和重试定时器 - 旧
Co\WebSocket\Client实例要显式close(),否则 fd 泄漏,撑爆连接数限制 - 重连成功后,要重新绑定
onMessage、onClose等回调,不能指望复用旧实例的监听 - 如果用了全局状态(比如登录 token、seq id),重连后要刷新,否则服务端可能拒收重复序列的消息
最麻烦的是并发重连:多个定时器同时触发 connect(),服务端看到一堆相同 IP 的建连请求,可能限流或误判为攻击。加个 lockReconnect 标志位是底线操作。










