TCP 是字节流不是消息流:两条消息可能粘成一段、一条消息也可能被拆成两段——粘包与拆包是 TCP 编程的第一课。长度前缀的协议设计让接收端知道每条消息的边界,粘包的解析有了确定的规则。
---发送端:长度前缀加消息体
function Pkt.send(conn, msgId, body)
local bodyLen = #body
local head = string.pack('>I2I4', msgId, bodyLen)
conn:send(head .. body)
end
---接收端:缓冲区的解析循环
function Pkt.parse(conn)
local buf = conn.buffer
while true do
-- 头部不足 6 字节:等待更多数据
if #buf < 6 then break end
local msgId, bodyLen = string.unpack('>I2I4', buf)
local total = 6 + bodyLen
-- 消息体不足:等待更多数据
if #buf < total then break end
local body = buf:sub(7, total)
buf = buf:sub(total + 1)
MsgRoute.dispatch(conn, msgId, body)
end
conn.buffer = buf
end
协议的格式是定长头加变长体:头部 6 字节包含协议号与体的长度,接收端先读头再按长度读体。解析的循环处理三种状态:缓冲不足一个头、头齐体不足、完整的包——三种状态的分支覆盖了粘包与拆包的全部形态。缓冲区的管理用移除式消费:解析掉的字节从缓冲头部移除,残留的不完整数据留在缓冲等待后续的到达。
---异常的防护:长度字段的合法性校验
local MAX_BODY = 64 * 1024
function Pkt.safeParse(conn)
local buf = conn.buffer
while #buf >= 6 do
local msgId, bodyLen = string.unpack('>I2I4', buf)
-- 长度超限:连接异常,直接断开
if bodyLen > MAX_BODY then
conn:close('非法长度')
return
end
if #buf < 6 + bodyLen then break end
MsgRoute.dispatch(conn, msgId, buf:sub(7, 6 + bodyLen))
buf = buf:sub(7 + bodyLen)
end
conn.buffer = buf
end
健壮的解析带长度的上限校验:长度字段的合法范围 0 到 64KB,超限的包直接断开连接——恶意构造的超长长度是拒绝服务攻击的常见手法,上限的校验在解析的入口拦住。协议的版本兼容:头部的第一个字节保留为版本号,新旧协议的共存靠版本的路由。解析的性能:缓冲的字符串操作避免频繁的拼接,收到即追加、解析时一次性扫描的批量模式让单连接的解析开销在微秒级。
解析器的单元测试覆盖全部形态:粘包、拆包、半包、空包、非法长度逐个用例;解析的异常计数监控,非法长度的包触发连接级别的告警与样本留存。
长度的字段曾经按本机字节序写入,跨端的解析读出乱码,网络字节序的大端约定统一。缓冲的截断曾经用了错误的起始位置,解析后的残留数据错位,偏移的计算用测试用例锁死。异常的连接曾经没有断开,畸形包的连接长期占着资源,非法长度即断开的策略兜底。
协议的长度前缀格式进协议文档:头部的布局、字节序、上限三要素写明。解析器的实现与协议文档同源评审,两端的理解偏差在评审期消除。异常连接的样本留存用于攻击的分析,防护的策略随样本迭代。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
实战应用:用在哪里 悬赏捕杀把玩家之间的恩怨变成可托付的系统行为:仇家难以亲自复仇时,花费金条发布悬赏,全服玩家接单击杀目标…
实战应用:用在哪里 策划改表是日常动作,漏填字段、引用了不存在的编号、数值越界这三类问题占了配表故障的绝大多数。等玩家报错再…
实战应用:用在哪里 行会擂台给中小行会提供了固定的对抗出口:挑战方击败守擂方后接管擂台,连胜次数决定每日结算奖励档位。接口层…
实战应用:用在哪里 世界首领战里玩家最关心的是伤害前十名:榜单要求随时插入、随时能按序取出,每来一条伤害记录都重新排序一次太…
实战应用:用在哪里 百人同屏的攻城战里,把每条移动、施法消息广播给全地图是最常见的性能失误。九宫格视野的做法是把地图切成等大…
实战应用:用在哪里 留言板、行会招募语、邮件标题这类字段会被玩家自由填写,并写入角色变量或转发给持久层。拼接式的写法一旦遇到…