tcp keepalive 是内核级探测,仅检测底层连接通断,无法感知应用层状态;必须与应用层心跳(如每30秒ping+服务端60秒超时扫描)配合使用,才能可靠维持长连接。

TCP Keepalive 是内核级探测,不带业务语义
Linux 内核的 tcp_keepalive_time 默认是 7200 秒(2 小时),意味着空闲连接要等 2 小时才发第一个探测包。这在长连接场景下完全不可用——防火墙、NAT 设备通常 5–30 分钟就回收空闲连接,SO_KEEPALIVE 根本来不及触发。
它只回答一个问题:“底层 TCP 连接是否还通?” 不管应用层有没有数据、有没有登录态、有没有未确认消息。哪怕客户端进程已崩溃,只要 TCP 状态没变,Keepalive 仍可能认为“在线”。
-
setsockopt($fd, SOL_SOCKET, SO_KEEPALIVE, [1])只是开启开关,参数仍由系统全局控制 - 想调小探测间隔,必须改系统参数(如
net.ipv4.tcp_keepalive_time),影响所有 TCP 连接,生产环境通常不允许 - 探测失败后,内核直接关闭 socket,PHP 层收不到明确通知,
onClose可能延迟数秒甚至不触发
心跳检测是应用层逻辑,必须自己实现
Swoole 没有内置“心跳自动响应”机制。所谓“心跳”,就是你定义一个约定:比如客户端每 30 秒发 "ping",服务端收到后更新 $connection->last_heartbeat = time(),再配一个定时器每 10 秒扫一遍,发现超 60 秒没更新就 $server->close($fd)。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 心跳包内容可自定义:
"ping"、{"type":"hb","ts":1716459023},甚至携带 token 续期信息 - 服务端可区分处理:收到
"ping"不一定立刻回"pong",但必须重置超时计时器 - 客户端发心跳失败(
send返回 false 或触发onError)应立即重连,不能等 Keepalive 发现
二者必须共存,不能只靠其中一个
只开 SO_KEEPALIVE:业务层无法感知“假在线”,连接在中间设备断了却还在内存里挂着,资源泄漏;只做应用心跳:遇到内核级异常(如网卡硬故障、SYN flood 后连接卡死),心跳包根本发不出去,服务端等到超时才关,延迟高且被动。
- 推荐组合:客户端启用
SO_KEEPALIVE(保底),同时发 30s 一次应用心跳;服务端用 60s 超时 + 定时扫描清理 - Swoole Server 启动时可统一设置:
$server->set(['tcp_keepalive' => true]),但这只是打开 socket 选项,不改变内核默认值 - 真正起效的心跳逻辑全在 PHP 层:
swoole_timer_tick()+$server->connections遍历 +$connection->last_time手动维护
容易被忽略的坑:心跳包不是发了就完事
很多团队加了 swoole_timer_tick(30000, fn() => $client->send("ping")) 就以为搞定了,结果压测时大量连接堆积不释放。问题出在服务端没做“接收确认”或“时间戳校验”。
- 客户端发
"ping"后,服务端onReceive里必须显式更新时间,不能依赖last_time(它只记录最后收包时间,但可能收的是业务数据) - 如果业务协议是二进制或分包的,
"ping"可能被粘包,需先解析完整帧再判断是否心跳 - 心跳超时阈值(如 60s)必须大于心跳间隔(30s)+ 网络抖动余量,否则弱网下误杀活跃连接
最麻烦的其实是连接上下文管理——谁来存 last_heartbeat?存在 $connection->ext 里?还是用独立数组映射?一不留神就内存泄漏或并发冲突。这个细节,90% 的 Swoole 长连接项目都踩过。










