TCPTransmission Control Protocol是一个 面向数据流 的协议它只保证数据按顺序、可靠地到达但不保留应用层消息之间的边界。发送端每次调用 send() 发送的数据可能会在传输过程中被拆分成多个小包分包也可能会合并成一个大包粘包然后一次性交付给接收端。具体行为取决于Nagle 算法合并小包以提高效率操作系统缓冲区大小网络延迟和 MTU最大传输单元这里举一个例子。客户端分两次发送1234 和 5678 服务端可能一次性收到 12345678粘包也可能先收到 12过一会儿再收到 345678拆包。这会导致接收方无法分辨哪部分是第一条消息、哪部分是第二条消息从而出现消息边界丢失的问题。这个造成的影响有游戏逻辑错误例如将两条指令解析成一条无效指令数据错位后续所有消息解析都错误严重时导致连接断开或程序崩溃解决粘包的方法为了在 TCP 流中正确分离消息通常在应用层设计一种消息边界协议。以下是三种最常用的数据切分方法。固定长度法发送方和接收方约定每个消息的长度固定例如 64 字节。如果实际数据不足用特殊字符如\0填充。接收方每次读取固定长度的字节然后解析。--消息1: 1234 60字节填充 --消息2: 5678 60字节填充。 -- 固定长度 local MSG_LEN 64 -- 将原始消息打包成固定长度的字符串 function pack_fixed(msg) if #msg MSG_LEN then -- 如果消息过长可以截断或报错这里简单截断 return msg:sub(1, MSG_LEN) else -- 不足部分用 \0 填充 local padding string.rep(\0, MSG_LEN - #msg) return msg .. padding end end -- 模拟从 TCP 接收数据可能一次收到多个消息也可能只收到部分 function on_tcp_receive(data) buffer buffer .. data -- 只要缓冲区长度 MSG_LEN就切出一条消息 while #buffer MSG_LEN do local one_msg buffer:sub(1, MSG_LEN) buffer buffer:sub(MSG_LEN 1) -- 去除填充的 \0得到原始消息 local raw_msg one_msg:match(^([^%z]*)) -- 截取到第一个 \0 之前 print(收到消息:, raw_msg) -- 这里可以交给上层业务逻辑处理 end end接收方每次读 64 字节得到完整的两条消息。优点实现简单无需解析边界处理速度快缺点浪费带宽填充数据多不适合长度变化很大的消息例如聊天消息 vs 大文件块游戏场景适用于指令集固定、消息长度统一的场景如简单的按键操作每个操作固定4字节。特殊分隔符法在每个消息末尾添加一个唯一的分隔符例如\n、\r\n或自定义\x00\xFF接收方不断读取数据直到遇到分隔符时认为一条消息结束。--发送: 1234\n 和 5678\n --接收: 可能收到 1234\n5678\n按 \n 切分即可。 -- 在每条消息末尾添加换行符 function pack_line(msg) return msg .. \n end local buffer -- 处理收到的 TCP 数据 function on_tcp_receive(data) buffer buffer .. data -- 尝试按换行符切分 while true do local pos buffer:find(\n) if not pos then break -- 没有完整消息等待更多数据 end -- 提取一条消息不含末尾的 \n local one_msg buffer:sub(1, pos - 1) buffer buffer:sub(pos 1) -- 移除消息和分隔符 print(收到完整消息:, one_msg) -- 这里可以交给上层业务逻辑处理 end end需要注意数据本身不能包含分隔符否则需要转义如\n转成\\n接收方需要维护一个缓冲区判断分隔符位置优点简单直观节省带宽无填充适合文本协议如 HTTP、Redis RESP缺点需要转义处理增加复杂度扫描数据寻找分隔符有一定 CPU 开销游戏场景常用于基于文本的通信协议如早期 MUD 游戏、部分手游的后台调试协议。长度前缀法最常用在每条消息的开头固定位置写入一个长度字段通常 2 或 4 字节指明后续数据的长度。接收方先读取长度字段再根据长度读取相应字节数的数据。--4 字节长度大端序 --消息1: [0,0,0,4] 1234 --消息2: [0,0,0,4] 5678 -- 打包消息返回 4 字节长度大端 消息内容 function pack_len(msg) local len #msg -- 使用 string.pack 将长度编码为 4 字节大端无符号整数 local prefix string.pack(I4, len) return prefix .. msg end local buffer function on_tcp_receive(data) buffer buffer .. data while #buffer 4 do -- 至少够读一个长度字段 -- 读取前 4 字节作为长度大端 local len string.unpack(I4, buffer) local total_needed 4 len -- 一条完整消息需要的总字节数 if #buffer total_needed then -- 提取完整消息不包含长度前缀 local msg buffer:sub(5, total_needed) buffer buffer:sub(total_needed 1) print(收到消息:, msg) -- 这里可以交给上层业务逻辑 else -- 数据不够等待更多数据 break end end end接收方读取 4 字节得到长度 4再读取 4 字节得到 1234继续读取下一个长度。优点无数据填充无转义带宽利用率高可处理任意长度的消息解析效率高只需一次长度读取缺点实现稍复杂需要处理长度字段可能跨包的情况需要确保发送端正确填写长度游戏场景绝大多数现代游戏网络模块都采用这种方式例如《王者荣耀》、《英雄联盟》等使用自定义二进制协议消息头包含消息 ID 和长度。总结TCP 只提供可靠字节流应用层必须自己定义消息边界。选择合适的方法能让游戏网络模块稳定、高效地运行。