Go处理TCP粘包拆包必须用4字节大端序长度头协议:先io.ReadFull读4字节得长度n,再io.ReadFull读满n字节;需bytes.Buffer滚动解析缓冲区,每连接独享buffer,严格校验长度防OOM。

Go 的 net.Conn.Read 从不返回“一条消息”,只返回“当前可用字节”——粘包和半包不是异常,是 TCP 字节流的默认行为。必须用长度前缀协议 + 应用层缓冲区滚动解析,否则上线后必现静默丢包、json.Unmarshal 报 invalid character、或 goroutine 泄漏。
为什么不能靠单次 conn.Read + io.ReadFull 解包
io.ReadFull 只保证「本次调用填满目标切片」,不感知上层协议状态。它不会跳过缓冲区里已有的残余字节,也不会等待后续数据补全半包。
- 你调
io.ReadFull(conn, header),如果连接刚断开,它立刻返回io.EOF;如果只收到 2 字节,它返回io.ErrUnexpectedEOF—— 这两种都不是“等一等再试”,而是直接失败 - 更常见的是:上一次解包后 buffer 剩下 3 字节,下一次
ReadFull仍从这 3 字节开始读,结果头被截半,长度解析错成几百万 -
conn.Read(buf)返回 1024 字节,可能含 3 个完整包 + 半个新包;只处理第一个就退出,后面 900+ 字节永远卡在 buffer 里
必须用 bytes.Buffer 滚动累积和消费
每个连接独享一个 *bytes.Buffer,所有读 goroutine 收到数据后立即 buffer.Write(),解包逻辑循环检查、提取、移除,不丢不阻塞。
- 每次
conn.Read(buf)后,无条件执行buffer.Write(buf[:n]),别清空 buffer - 解包循环写成:
for buffer.Len() >= 4 {→ 读头 →if buffer.Len() → 截出完整包 → <code>buffer.Next(4 + int(msgLen)) - 不要在
buffer上调Read()或ReadBytes(),它们会移动内部 offset,破坏后续长度判断 - buffer 不是全局变量,必须绑定到连接生命周期内;handler 函数退出时不能让它被 GC 掉
长度头协议的三个实操雷区
用 binary.BigEndian.PutUint32 写 4 字节头最稳妥,但以下三点线上高频出事:
立即学习“go语言免费学习笔记(深入)”;
- 发送端没原子写:先
conn.Write(header)成功,再conn.Write(body)失败 → 对方收到一个非法头,后续全乱。应改用conn.Write(append(header, body...))或io.MultiWriter - 长度校验无上限:
if msgLen > 1024*1024不做检查,恶意客户端发0xFFFFFFFF,服务端直接make([]byte, 4GB)OOM - 接收端误信
conn.Read返回值:传入 4096 字节 buf,只读到 127 字节就停手,以为“没数据了”。必须循环调用Read直到满足协议要求(比如凑够 4 字节头),或超时断连
为什么 bufio.Scanner 和 ReadString 在二进制协议里危险
它们本质是分隔符驱动,只适合纯文本且你能控制 payload 内容的场景。一旦业务数据含 \n(比如用户输入 JSON、日志带换行、表情符号),就必然切错。
-
r.ReadString('\n')遇不到\n就永远阻塞,而真实二进制 payload 根本不含该字符 -
r.ReadBytes('\n')可能返回{"msg":"hello\nworld"}\n—— 实际是 1 条完整 JSON,却被切成两段 - 即使加了
conn.SetReadDeadline,也只是让错误暴露更快,没解决粘包本身 - 它们内部反复扩容 buffer,性能差,且无法表达空消息(长度为 0)
最容易被忽略的其实是“剩余字节”的归属问题:buffer 里那几个没被取走的字节,既不属于上一个包,也不属于下一个包,而是下一次解析的起点。漏掉这个意识,等于放弃对 TCP 流的控制权。



















