首页 / 技术文章地图 / 正文

【网络通信】TCP 粘包拆包与 Protobuf 序列化实战

发布:2026-09-20 07:41 | 作者:996 技术组 | 5 阅读
完整课程入口:996 全套课程体系Lua 学习路径幂尔框架 mirs.cn

为什么会粘包

TCP 是流式传输,它只保证字节按序到达,不保证"一次发送对应一次接收"。你 send 了三条消息,对方可能一次收到两条半;发一条 8KB 的大消息,也可能被切成三段到达。这就是粘包与拆包。它们不是 bug,是 TCP 的本性,所以应用层必须自己定义消息边界——所有"偶现的解析错乱"十有八九是边界没处理干净。

游戏里通行的做法是"长度前缀协议":每条消息前面加 2 或 4 字节的长度字段。接收方维护一个缓冲区,收到数据就追加进去,然后循环判断:缓冲区够不够读出一个长度头?够,读出长度 N;缓冲区剩余够不够 N 字节?够,取出完整消息、移除,继续循环;不够,等待下次数据到达。这段"拆包状态机"是网络层的地基,务必独立成函数并反复测试。

lua
-- 拆包骨架(示意)
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 在协议层的价值

消息体序列化,Protobuf 是成熟选择:字段用编号编码,体积比 JSON 小一大截,加字段不破坏老客户端的兼容性(新增字段老代码会跳过未知编号)。工作流是先写 .proto 描述文件,用工具生成各端代码;Lua 侧常用 pb 库直接 pb.encode/decode。协议演进纪律只有一条:老字段只废弃不删除、新字段只加在末尾,编号永远不复用,否则新老版本混跑期间必然出脏数据。

配套三件套

心跳:客户端每 15~30 秒发一个空心跳包,服务器两倍周期没收到就标记掉线。它同时承担"探测半开连接"和"防止中间设备回收空闲连接"两个职责。

断线重连:重连后不能裸连,先走"校验 token + 同步版本号 + 补发离线期间的关键变更"三步。给每条关键消息带自增序号,重连时把序号报给服务器要增量数据,可以大幅简化状态同步。

封包校验:长度头里留两位做魔数校验,解析前先核对,能把脏数据在入口处拦下来。网络层的问题永远要"早失败、带上下文失败",把缓冲区长度、预期长度记进日志,联机问题才能定位到具体一包。

消息收发

sendluamsg(actor, msgid, param1, param2, param3, sMsg, sendScope, targetMapId)

发送消息,出站统一加长度头,粘包问题一半在发的时候就已经定了

参数类型说明
actorobject玩家对象(必填参数)
msgidinteger消息ID(必填参数)
param1integer参数1
param2integer参数2
param3integer参数3
sMsgstring消息内容
lua
sendluamsg(actor, 996, 0, 2, 3, "发送给自己的网络消息",0)
sendluamsg(actor, 996, 1, 1, 1, "发送给全服的网络消息",1)

handlerequest(actor, msgid, param1, param2, param3, sMsg)

监听消息,入站统一入口,做拆包与 msgid 校验

参数类型说明
actorobject玩家对象(必填参数)
msgidinteger消息ID(必填参数)
param1integer参数1(必填参数)
param2integer参数2(必填参数)
param3integer参数3(必填参数)
sMsgstring消息内容(必填参数)

序列化

tbl2jsonex(str)

表格转换成字符串,二进制协议之外的轻量方案,字段顺序要固定

参数类型说明
strtable需要转表的table

json2tbl(str, reslut)

字符串转换成表格,解析后必须先判结构与长度再进业务

参数类型说明
strstring需要转表的json

tbl2jsonex(str)

表格转换成字符串,嵌套结构序列化时注意数字 key 会被转成字符串

参数类型说明
strtable需要转表的table

广播

sendrefluamsg(actor, msgid, param1, param2, param3, sMsg)

发送视野内广播消息,广播类协议最容易在拆包上出问题

参数类型说明
actorobject玩家对象(必填参数)
msgidinteger消息ID(必填参数)
param1integer参数1
param2integer参数2
param3integer参数3
sMsgstring消息内容

sendcustommsg(actor, type, msg, FColor, BColor, X, Y)

屏幕任意坐标发送公告信息,带坐标的公告类消息属于可变长字段,需单独测

参数类型说明
actorobject玩家对象(必填参数)
typeinteger发送对象(必填参数)0-所有人1-自己2-组队3-行会4-当前地图
msgstring消息内容(必填参数)
FColorinteger前景色(0-255色)(必填参数)
BColorinteger背景色(0-255色)(必填参数)
XintegerX轴坐标(必填参数)

校验

md5str(str)

MD5加密,拆包组装完成后做一次完整性检查

参数类型说明
strstring“需要加密的文本
作者履历与出处
本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理,讲解体系出自多年商业端开发生产一线。作者团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。
© 威海旷世互娱 · 返回文章地图 · 课程体系 · 幂尔框架