完整课程入口:996 全套课程体系 | Lua 学习路径 | 幂尔框架 mirs.cn
TCP 是流式传输,它只保证字节按序到达,不保证"一次发送对应一次接收"。你 send 了三条消息,对方可能一次收到两条半;发一条 8KB 的大消息,也可能被切成三段到达。这就是粘包与拆包。它们不是 bug,是 TCP 的本性,所以应用层必须自己定义消息边界——所有"偶现的解析错乱"十有八九是边界没处理干净。
游戏里通行的做法是"长度前缀协议":每条消息前面加 2 或 4 字节的长度字段。接收方维护一个缓冲区,收到数据就追加进去,然后循环判断:缓冲区够不够读出一个长度头?够,读出长度 N;缓冲区剩余够不够 N 字节?够,取出完整消息、移除,继续循环;不够,等待下次数据到达。这段"拆包状态机"是网络层的地基,务必独立成函数并反复测试。
-- 拆包骨架(示意)
while true do
if #buf < 4 then break end
local len = string.unpack(">I4", buf) -- 4 字节大端长度头
if #buf < 4 + len then break end -- 半包,等下一段
local msg = buf:sub(5, 4 + len)
buf = buf:sub(5 + len)
handleMessage(pb.decode("game.Msg", msg))
end
消息体序列化,Protobuf 是成熟选择:字段用编号编码,体积比 JSON 小一大截,加字段不破坏老客户端的兼容性(新增字段老代码会跳过未知编号)。工作流是先写 .proto 描述文件,用工具生成各端代码;Lua 侧常用 pb 库直接 pb.encode/decode。协议演进纪律只有一条:老字段只废弃不删除、新字段只加在末尾,编号永远不复用,否则新老版本混跑期间必然出脏数据。
心跳:客户端每 15~30 秒发一个空心跳包,服务器两倍周期没收到就标记掉线。它同时承担"探测半开连接"和"防止中间设备回收空闲连接"两个职责。
断线重连:重连后不能裸连,先走"校验 token + 同步版本号 + 补发离线期间的关键变更"三步。给每条关键消息带自增序号,重连时把序号报给服务器要增量数据,可以大幅简化状态同步。
封包校验:长度头里留两位做魔数校验,解析前先核对,能把脏数据在入口处拦下来。网络层的问题永远要"早失败、带上下文失败",把缓冲区长度、预期长度记进日志,联机问题才能定位到具体一包。