Go处理TCP粘包的核心是应用层识别消息边界,因TCP本身无消息概念;bufio.Reader.Read出现粘包是正常行为,因其按字节流读取而非按消息;主流方案中长度前缀法最推荐,需用io.ReadFull循环读取header和body并校验长度。

Go 处理 TCP 粘包问题,核心不是“防”,而是“识别边界”——因为 TCP 本身不提供消息边界,你必须在应用层自己定义、编码、解析。
为什么 bufio.Reader.Read 会读出粘包?
很多人用 bufio.NewReader(conn).Read(buf[:]) 循环读取,结果发现多条消息挤在一次 n 返回里(比如 "HelloWorld"),误以为是“bug”。其实这是完全符合预期的行为:TCP 是字节流,Read 只管从内核缓冲区尽可能多地搬数据,不关心你的业务逻辑怎么切。
-
Read的语义是“读到多少给多少”,不是“读一条消息给一条” - 哪怕客户端调用了 10 次
conn.Write([]byte("msg")),服务端一次Read也可能拿到全部 10 条拼起来的字节 - 反过来,一个大消息也可能被拆成多次
Read返回(即“拆包”)
三种主流方案选哪个?看场景和约束
没有银弹。选择取决于你对性能、兼容性、协议扩展性的要求:
-
分隔符法(如
\n):适合文本协议(日志推送、简单命令)、消息内容可严格规避分隔符的场景;bufio.Scanner开箱即用,但遇到二进制数据或无法控制内容时容易误切 -
固定长度法:实现最简单,
io.ReadFull直接读够 N 字节;但带宽浪费严重,只适用于消息长度高度可控(如传感器采样点) -
长度前缀法(推荐):通用性强、无内容限制、性能好;需约定头部长度(2/4/8 字节)、字节序(
binary.BigEndian最常用);几乎所有自研 RPC、IM 协议都用它
长度前缀法实操:封装一个可靠的 readMessage
关键点:不能假设一次 Read 就能读完 header 或 body,必须循环直到读满。
立即学习“go语言免费学习笔记(深入)”;
func readMessage(conn net.Conn) ([]byte, error) {
// 1. 先读 4 字节 header(uint32,大端)
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil {
return nil, err
}
msgLen := binary.BigEndian.Uint32(header[:])
// 2. 再读 msgLen 字节 body
data := make([]byte, msgLen)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, err
}
return data, nil}
// 使用示例
for {
msg, err := readMessage(conn)
if err != nil {
// 处理断连、超时等
break
}
process(msg)
}
⚠️ 容易踩的坑:
- 没用
io.ReadFull,而用Read—— 可能只读到 header 的前 2 字节就返回,后续解析全乱 - header 长度和实际序列化方式不一致(比如写用
PutUint16,读却用Uint32) - 没做长度校验(如
msgLen > 10*1024*1024),可能被恶意构造大长度耗尽内存
要不要自己写封包/解包逻辑?
小项目直接手写没问题;中大型系统建议封装成 DataPack 接口(类似 zinx 框架的思路),把 Pack/Unpack 抽离,方便统一加 CRC、压缩、加密。
真正复杂的地方不在“怎么读”,而在“读错怎么办”:连接中断时缓存未读完的半个包、并发读写冲突、长连接保活期间的粘包累积……这些才是压测和线上真正暴露的问题。










