Go处理TCP粘包的核心是应用层识别消息边界,因TCP本身无消息概念;bufio.Reader.Read出现粘包是正常行为,因其按字节流读取而非按消息;主流方案中长度前缀法最推荐,需用io.ReadFull循环读取header和body并校验长度。
Go 处理 TCP 粘包问题,核心不是“防”,而是“识别边界”——因为 TCP 本身不提供消息边界,你必须在应用层自己定义、编码、解析。
bufio.Reader.Read 会读出粘包?很多人用 bufio.N 循环读取,结果发现多条消息挤在一次
ewReader(conn).Read(buf[:])n 返回里(比如 "HelloWorld"),误以为是“bug”。其实这是完全符合预期的行为:TCP 是字节流,Read 只管从内核缓冲区尽可能多地搬数据,不关心你的业务逻辑怎么切。
Read 的语义是“读到多少给多少”,不是“读一条消息给一条”conn.Write([]byte("msg")),服务端一次 Read 也可能拿到全部 10 条拼起来的字节Read 返回(即“拆包”)没有银弹。选择取决于你对性能、兼容性、协议扩展性的要求:
\n):适合文本协议(日志推送、简单命令)、消息内容可严格规避分隔符的场景;bufio.Scanner 开箱即用,但遇到二进制数据或无法控制内容时容易误切io.ReadFull 直接读够 N 字节;但带宽浪费严重,只适用于消息长度高度可控(如传感器采样点)binary.BigEndian 最常用);几乎所有自研 RPC、IM 协议都用它readMessage
关键点:不能假设一次 Read 就能读完 header 或 body,必须循环直到读满。
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 字节就返回,后续解析全乱PutUint16,读却用 Uint32)msgLen > 10*1024*1024),可能被恶意构造大长度耗尽内存小项目直接手写没问题;中大型系统建议封装成 DataPack 接口(类似 zinx 框架的思路),把 Pack/Unpack 抽离,方便统一加 CRC、压缩、加密。
真正复杂的地方不在“怎么读”,而在“读错怎么办”:连接中断时缓存未读完的半个包、并发读写冲突、长连接保活期间的粘包累积……这些才是压测和线上真正暴露的问题。