go处理tcp粘包需手动控制字节流边界,因tcp是无消息边界的字节流;必须用binary.bigendian统一收发两端字节序;bufio.reader不解决定界问题,须用状态机+io.readfull+长度校验实现安全解析。

Go 处理 TCP 粘包不是加个 bufio.Reader 就能完事的问题,核心在于你是否亲手控制过字节流的边界识别——否则线上一压测,json.Unmarshal 就 panic,invalid character 报错满天飞。
为什么 conn.Read 会返回半条消息?
TCP 是字节流协议,不保证“一次 conn.Read 返回一个完整应用层消息”。它只按内核接收缓冲区当前可用数据量返回,常见情况包括:
- 两个小包被内核合并,
conn.Read一次性返回 2048 字节(含两条完整消息) - 一个 4KB 消息被 TCP 分段,第一次
conn.Read只拿到前 1500 字节(MSS 限制) - 对端刚发完头 4 字节就断网,你读到 4 字节但后续
bodyLen解析出错,再往后conn.Read就永远卡住
这不是 Go 的问题,是所有基于 TCP 的服务都必须面对的底层事实。依赖“等数据来齐了再处理”这种想法,等于把协议设计权交给了网络抖动。
binary.BigEndian.Uint32 必须用大端,不是习惯问题
网络字节序就是大端序(Big Endian),这是 RFC 1700 定死的。如果你用 binary.LittleEndian.Uint32 去解析发送方按标准写入的 4 字节长度头,结果必然错乱:
- 正确长度 1024 → 十六进制为
0x00000400→BigEndian解析得 1024 - 同组字节用
LittleEndian解析 → 得到0x00040000= 262144 → 后续io.ReadFull会死等 262KB,直接触发超时或 OOM
发送方也必须用 binary.BigEndian.PutUint32 写头,两端字节序必须严格一致。别信“本地测试能通就行”,跨平台、跨语言通信时这就是硬伤。
bufio.Reader.ReadString('\n') 不适用于二进制协议
bufio.Reader 只是带缓冲的封装,不具备消息定界能力。用 ReadString('\n') 或 ReadBytes('\n') 属于典型误用:
- 二进制 payload 里可能含任意字节,
\n可能是有效数据的一部分,导致错切 - 没有长度校验,攻击者发一个超长无
\n的 payload,缓冲区会无限增长 -
ReadString在没遇到分隔符时会一直阻塞,连接异常时无法及时感知
真正安全的做法是自己管理缓冲区:用状态机判断头部是否收全 → 解出 body 长度 → 调用 io.ReadFull 等待完整 body → 校验长度防溢出 → 循环处理剩余字节。每连接独享 buffer,不能复用。
粘包处理中最容易被忽略的三个点
很多实现能跑通简单 case,但一上生产就崩,往往栽在这三处:
- 没做
bodyLen上限检查:如果对端恶意构造头为0xffffffff,你会试图分配 4GB 内存,直接 OOM - 没处理
conn.Read返回0, nil(连接静默关闭):循环里只判err != nil,会陷入空转 - 共享缓冲区未重置:多个 goroutine 共用同一
[]byte,前一条消息残留数据污染后一条的解析
边界校验和状态隔离不是锦上添花,是保命线。哪怕协议再简单,也要在读头、读体、解析三步之间插入显式校验逻辑,而不是靠“应该不会出问题”来赌。











