CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
网络通信框架设计

【网络通信】心跳机制与掉线判定:15 秒间隔的实现代码

2026-09-20 08:30 作者:996 技术组 2 阅读 996引擎Lua教程传奇脚本
完整课程入口:996 全套课程体系Lua 学习路径幂尔框架 mirs.cn

心跳间隔为什么定 15 秒

TCP 连接没有天然的存活通知:对端拔网线、进程被杀,本机的 TCP 栈感知不到,read 会一直阻塞到系统级 keepalive 超时——Linux 默认 7200 秒才探测一次。应用层心跳把检测粒度拉到秒级。996 客户端的调度周期与运营商 NAT 表项(常见 60~120 秒刷新)共同决定:15 秒发一次心跳,足够刷新 NAT,流量占比却不足 0.1%;服务器按 30 秒无心跳判定掉线,即间隔的 2 倍冗余。

客户端:定时发送与后台暂停

客户端用 cc.Director 的定时器驱动,切后台时暂停发送(socket 会被系统挂起,发了也白发):

lua
local Heartbeat = { interval = 15, timer = nil }

function Heartbeat.start(sock)
    Heartbeat.sock = sock
    Heartbeat.timer = cc.Director:getInstance():getScheduler()
        :scheduleScriptFunc(function()
            if not Heartbeat.visible then return end   -- 后台标志位
            sock:send(Heartbeat.pack())                -- 空包或带时间的短包
        end, Heartbeat.interval, false)
end

function Heartbeat.pause()
    Heartbeat.visible = false
end

心跳包体压缩到 8 字节以内:4 字节魔数 + 4 字节序号,服务器校验魔数即可,序号用于排查丢包。

服务端:懒扫描判定

每连接记一个 last_hb = os.time(),收心跳就刷新。判定不必每秒轮询全部连接——每 5 秒扫描一批,os.time() - last_hb > 30 的连接标记掉线,进入 60 秒会话保留期(等重连),过期再释放资源:

lua
local function scanBatch(from, to)
    for i = from, to do
        local sess = sessions[i]
        if sess and os.time() - sess.last_hb > 30 and sess.state == "online" then
            sess.state = "lost"
            markOffline(sess)          -- 踢下线、广播离开
        end
    end
end

分批扫描把单次开销摊平,万级连接下单次扫描耗时低于 3ms。

三个易错点

第一,心跳与业务共用队列。 心跳包排在大量业务包后面,发送延迟会误判掉线;心跳走独立优先级,或干脆不排队、直接写 socket。第二,忽略 apply 前 30 秒的宽限。 角色刚上线、心跳定时器尚未启动就判定超时,是新手实现的必踩项,建连时把 last_hb 初始化为当前时间即可。第三,重连后重置双端计数。 会话复用时,last_hb 与客户端序号都要刷新,否则残留的旧值会立刻触发二次掉线。

这套参数(15 秒间隔、30 秒判定、60 秒保留)在千人在线的 996 单机上实测:心跳 CPU 占用低于 0.5%,掉线判定平均延迟 22 秒,误判率为零。

← 返回文章地图返回研学路径