workerman默认不启用tcp keep-alive,需在onconnect中用socket_set_option显式配置so_keepalive及tcp_keepidle、tcp_keepintvl、tcp_keepcnt参数,否则nat/防火墙易导致连接假死;应用层心跳与tcp保活须协同使用。

Workerman本身不自动启用TCP层Keep-Alive,游戏客户端常因NAT/防火墙静默断连,不配就容易出现“连接还活着但发不出包”的假死状态。
Workerman默认不开启TCP Keep-Alive
Workerman的TcpConnection对象在建立后,底层socket默认继承系统全局设置(Linux通常为2小时才探测),远超游戏场景容忍范围。它不会自动调用setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)),更不会调整TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT这三个关键参数。
这意味着:即使你设置了$connection->keepalive = true(这是Workerman应用层长连接开关,仅影响是否主动关闭空闲连接),TCP协议栈仍按系统默认策略保活——对游戏来说太迟钝、太不可控。
-
$connection->keepalive = true只是让Workerman不主动关掉空闲连接,和TCP保活机制无关 - 真正起作用的是操作系统级socket选项,必须显式配置
- Workerman未封装跨平台的Keep-Alive参数设置接口,需手动
stream_set_option()或socket_set_option()
游戏场景下NAT和防火墙是主要断连元凶
移动端或家庭宽带环境中的NAT设备(如家用路由器)、运营商级NAT、企业防火墙,普遍会在60–300秒内清理“无数据交互”的TCP连接。它们只看IP+端口五元组是否有报文流动,不理解HTTP头或自定义心跳包语义。
此时TCP Keep-Alive探测包(空ACK)是唯一能穿透这些中间件并维持连接映射表的合法流量。
- 客户端发完一个技能包后静默120秒 → NAT已删映射 → 下次发移动包被丢弃,但
send()仍返回成功(因为内核缓冲区没满) - 服务器收不到后续数据,也收不到FIN/RST,连接卡在ESTABLISHED状态,变成“半开”
- 只有TCP Keep-Alive在
TCP_KEEPIDLE(如45秒)后开始探测,才能在NAT超时前触发重置或确认
如何在Workerman中正确配置TCP Keep-Alive
必须在onConnect回调里对原始socket资源操作,不能只依赖Workerman的keepalive属性。
示例(Linux环境,使用socket_set_option):
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
$worker = new Worker('tcp://0.0.0.0:2345');
$worker->onConnect = function (TcpConnection $connection) {
// 获取底层socket资源
$socket = $connection->getSocket();
if ($socket !== null && is_resource($socket)) {
// 启用TCP Keep-Alive
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
// 首次探测延迟:45秒(避免刚连上就发包)
socket_set_option($socket, IPPROTO_TCP, TCP_KEEPIDLE, 45);
// 探测间隔:10秒
socket_set_option($socket, IPPROTO_TCP, TCP_KEEPINTVL, 10);
// 最大失败次数:3次(即30秒无响应后断连)
socket_set_option($socket, IPPROTO_TCP, TCP_KEEPCNT, 3);
}
};
- Windows不支持
TCP_KEEPIDLE/TCP_KEEPINTVL/TCP_KEEPCNT,只能设SO_KEEPALIVE,依赖系统默认(通常2小时) - PHP
stream_set_option()在部分版本中对Keep-Alive参数支持不稳定,优先用socket_set_option() - 务必在
onConnect里做,不能在onMessage或定时器里补 —— socket资源可能已失效
Keep-Alive和应用层心跳不是二选一,而是分层协作
TCP Keep-Alive解决“物理链路是否断开”,应用层心跳解决“对端进程是否存活+业务逻辑是否就绪”。游戏后端必须两者共存。
- TCP Keep-Alive防止NAT撕毁连接,但它不保证你的PHP进程还在处理逻辑(比如进程OOM被kill,但TCP连接未关闭)
- 应用层心跳(如每30秒
{"type":"ping"})能触发onMessage,更新$connection->lastMessageTime,配合定时器清理僵死连接 - 若只靠TCP Keep-Alive,服务端无法感知客户端逻辑崩溃;若只靠心跳,NAT早把连接回收了,心跳包根本发不出去
最容易被忽略的是:TCP Keep-Alive参数必须匹配你的网络部署环境。云服务器(如AWS/Azure)的负载均衡器、K8s Service、Ingress Controller都有各自空闲超时设置,它们往往比NAT更激进。比如Azure Load Balancer默认空闲超时是4分钟,那你的TCP_KEEPIDLE就得设成≤200秒,否则探测永远赶不上LB断连。











