Go处理TCP粘包拆包的核心是识别消息边界,必须用4字节大端序长度头协议:先读4字节得长度n,再用io.ReadFull读满n字节,循环解析缓冲区,每连接独享buffer,严格校验长度防OOM。
go 中处理 tcp 粘包与拆包,核心不是“防”,而是“识别边界”——因为 net.conn.read 返回的永远是字节流,不保证一次读到一个业务消息。必须在应用层定义协议,并严格按该协议解析缓冲区。
为什么 bufio.Scanner 和 ReadBytes('\n') 容易出错
它们看似能切分消息,但本质是“找分隔符”,一旦业务数据里含 '\n'(比如 JSON 字段值、用户输入、日志内容),就会提前截断。这不是 bug,是设计使然——bufio.Scanner 默认用 ScanLines,根本不管你的数据是否合法。
- 测试时用
"hello\nworld\n"没问题,上线后用户发一条带换行的评论,立刻粘错 -
ReadBytes遇到 EOF 会返回部分数据 +io.ErrUnexpectedEOF,但很多代码忽略这个错误,直接拿部分数据去解析 - 二进制协议完全不能用分隔符,因为
0x00、0x0A可能是有效 payload
用 4 字节大端长度头是最稳的通用方案
每条消息前加 uint32 表示 body 长度,接收端先读 4 字节 → 解出长度 n → 再读满 n 字节。它天然兼容粘包、拆包、多包合并等所有情况。
- 头部统一用
binary.BigEndian.PutUint32,跨语言通信(如 Go ↔ Java/Python)不踩字节序坑 - 不要用
int32:Go 的int32有符号,而消息长度不可能为负;必须用uint32 - 发送时别分开
Write头和 body:要确保原子性,推荐用io.WriteFull(conn, header)+io.WriteFull(conn, body) - 接收端必须用
io.ReadFull读头,否则可能只读到 2 字节就返回,导致后续解析全乱
解包逻辑必须循环处理缓冲区
一次 conn.Read 可能带回:0.5 个包 + 2 个完整包 + 0.3 个包。不能读一次就 parse 一次,必须累积到 bytes.Buffer 或切片,然后 while 循环切分。
- 每次从缓冲区开头尝试解一个包:若不够 4 字节头,break 等下次数据;若头够但 body 不够,
buf.UnreadByte()把头推回去,再等 - 切出来的每个完整包,要从缓冲区里
Next(n+4)或bytes.TrimPrefix掉,否则重复解析 - 别用全局共享 buffer:每个连接必须独享自己的缓冲区,否则并发读写会 panic
- 记得检查
msgLen是否过大(比如 > 10MB),防止恶意客户端耗尽内存
别碰 “Sleep 等包齐” 和 “SetReadDeadline” 这类权宜之计
靠 time.Sleep(10 * time.Millisecond) 或 conn.SetReadDeadline 去“猜”包什么时候来完,等于把 TCP 流当 UDP 用——网络一抖就丢数据,延迟一高就超时,而且根本没法压测。
- 这类做法在本地 loopback 跑得飞快,一上公网或容器网络立刻暴露
-
SetReadDeadline是给整个 Read 调用设超时,不是给“等下一个包”设,容易误杀正常长连接 - 真正要控时延,应该在协议层加 request-id + timeout 字段,由业务逻辑决定是否重试,而不是让 I/O 层瞎等
最易被忽略的一点:解包循环里对 bytes.Buffer 的 Read 和 UnreadByte 调用,必须成对出现且位置精准——少一次 UnreadByte,下一轮就读偏;多一次,就卡死在头部。这地方没日志、难调试,上线后只能靠抓包定位。











