完整课程入口: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 + 同步版本号 + 补发离线期间的关键变更"三步。给每条关键消息带自增序号,重连时把序号报给服务器要增量数据,可以大幅简化状态同步。
封包校验:长度头里留两位做魔数校验,解析前先核对,能把脏数据在入口处拦下来。网络层的问题永远要"早失败、带上下文失败",把缓冲区长度、预期长度记进日志,联机问题才能定位到具体一包。
sendluamsg(actor, msgid, param1, param2, param3, sMsg, sendScope, targetMapId)
发送消息,出站统一加长度头,粘包问题一半在发的时候就已经定了
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象(必填参数) |
| msgid | integer | 消息ID(必填参数) |
| param1 | integer | 参数1 |
| param2 | integer | 参数2 |
| param3 | integer | 参数3 |
| sMsg | string | 消息内容 |
sendluamsg(actor, 996, 0, 2, 3, "发送给自己的网络消息",0)
sendluamsg(actor, 996, 1, 1, 1, "发送给全服的网络消息",1)
handlerequest(actor, msgid, param1, param2, param3, sMsg)
监听消息,入站统一入口,做拆包与 msgid 校验
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象(必填参数) |
| msgid | integer | 消息ID(必填参数) |
| param1 | integer | 参数1(必填参数) |
| param2 | integer | 参数2(必填参数) |
| param3 | integer | 参数3(必填参数) |
| sMsg | string | 消息内容(必填参数) |
tbl2jsonex(str)
表格转换成字符串,二进制协议之外的轻量方案,字段顺序要固定
| 参数 | 类型 | 说明 |
|---|---|---|
| str | table | 需要转表的table |
json2tbl(str, reslut)
字符串转换成表格,解析后必须先判结构与长度再进业务
| 参数 | 类型 | 说明 |
|---|---|---|
| str | string | 需要转表的json |
tbl2jsonex(str)
表格转换成字符串,嵌套结构序列化时注意数字 key 会被转成字符串
| 参数 | 类型 | 说明 |
|---|---|---|
| str | table | 需要转表的table |
sendrefluamsg(actor, msgid, param1, param2, param3, sMsg)
发送视野内广播消息,广播类协议最容易在拆包上出问题
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象(必填参数) |
| msgid | integer | 消息ID(必填参数) |
| param1 | integer | 参数1 |
| param2 | integer | 参数2 |
| param3 | integer | 参数3 |
| sMsg | string | 消息内容 |
sendcustommsg(actor, type, msg, FColor, BColor, X, Y)
屏幕任意坐标发送公告信息,带坐标的公告类消息属于可变长字段,需单独测
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象(必填参数) |
| type | integer | 发送对象(必填参数)0-所有人1-自己2-组队3-行会4-当前地图 |
| msg | string | 消息内容(必填参数) |
| FColor | integer | 前景色(0-255色)(必填参数) |
| BColor | integer | 背景色(0-255色)(必填参数) |
| X | integer | X轴坐标(必填参数) |
md5str(str)
MD5加密,拆包组装完成后做一次完整性检查
| 参数 | 类型 | 说明 |
|---|---|---|
| str | string | “需要加密的文本 |