客户端进程被杀、WiFi 切 4G、NAT 超时,这些场景下 TCP 连接不会立刻断开,服务器要靠心跳判定连接死活。心跳间隔太短浪费流量,太长则死连接占用资源,玩家换网络后还可能收到幽灵状态,网关层的心跳保活是长连接游戏的标配。
local HEARTBEAT_INTERVAL = 15
local HEARTBEAT_TIMEOUT = 45
function Gateway.onHeartbeat(conn, clientTime)
conn.lastAlive = os.time()
conn.rtt = os.time() - clientTime
QF_Send(conn, "heartbeat_ack", { t = os.time() })
end
客户端每 15 秒发一次心跳,服务器回 ack 并顺带做时间对齐。超时线设在 45 秒,容忍连续两次丢失,移动网络的单次抖动不至于误杀。
function Gateway.sweep()
local now = os.time()
for _, conn in pairs(connections) do
if now - conn.lastAlive > HEARTBEAT_TIMEOUT then
Gateway.close(conn, "heartbeat_timeout")
end
end
end
清理扫描每 15 秒跑一轮,死连接关闭时触发玩家离线流程,清理挂机占用的地图资源与组队状态。心跳还顺带承载往返延迟采样,rtt 数据进入分地区质量报表,运营商网络问题据此定位。NAT 环境下 15 秒间隔能维持大多数运营商的映射表项,实测 30 秒就会出现单向假活,这也是间隔不能放大的实证。
心跳超时导致的断线占比按网络类型统计,正常应低于总断线的两成。
最早的心跳由业务层处理,某个业务 bug 吞掉心跳包后全服玩家被批量误踢,心跳下沉到网关层与业务严格隔离后才稳定。重连风暴场景也优化过:大版本更新后十万连接同时重连,心跳扫描的全表遍历造成主循环尖峰,改成时间轮分桶扫描,单轮耗时从 40 毫秒压到 2 毫秒。
心跳包的载荷设计留了扩展位,早期只有时间戳,后来顺带携带客户端版本号与网络类型,网关层的质量报表因此能按版本与网络细分。心跳超时的处置做了分级:第一次超时发探测包主动询问,第二次超时才断开连接,两次探测的设计把弱网玩家的误踢率压到千分之一以下。重连后的心跳节奏自动降频,后台挂机场景的心跳间隔放宽到六十秒,流量敏感的玩家感知友好。
本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
实战应用:用在哪里 脚本的规范靠人盯盯不过来:命名风格漂移、全局变量乱用、缩进混着来——风格检查器把这些规则的执行自动化。提…
实战应用:用在哪里 拍卖行的出价是一场与时间的博弈:出价的按钮、倒计时的紧迫、截拍瞬间的悬念——拍卖界面的要点是出价的流程、…
实战应用:用在哪里 强化连败七次是什么体验?幸运值机制给失败的玩家一个隐性的承诺:每次失败累加幸运值,幸运值越高下一次的成功…
实战应用:用在哪里 审计日志的价值在于不可抵赖:被改过的日志比没有日志更危险——纠纷的追溯、内部的追责都建立在「日志没被动过…
实战应用:用在哪里 活动结束给 3 万玩家发奖励:一次性群发让邮件表瞬间多 3 万行、投递的队列堵塞正常的通信。批量邮件的分…
实战应用:用在哪里 面对面的交易是传奇最经典的交互:两人面对面、各自放入物品与金币、双方确认后成交。交易的协议要点是会话的建…