conn.read不能直接按“一条消息”读取,因为net.conn是字节流接口,read只返回已就绪字节,不保证业务完整包,易出现半包或粘包;必须用固定长度大端序(如4字节)长度头协议,并配合缓冲区循环解析。

为什么 conn.Read 不能直接按“一条消息”读取
因为 net.Conn 是字节流接口,Read 只保证返回「已就绪的可用字节」,不保证对应业务逻辑上的完整包。一次调用可能只读到半个包(半包),也可能读到两个以上完整包加一个残缺包(连包/粘包)。常见现象包括:Read 返回 12 字节,但你的协议要求先读 4 字节头;或返回 1024 字节里实际含 3 个完整消息 + 半个新消息。
必须用长度头协议,且头部必须固定长度
推荐用 binary.BigEndian.PutUint32 写 4 字节大端序长度头——不是为了“兼容旧系统”,而是避免跨语言、跨平台时因字节序不一致导致解包失败。小端序在 x86 上自然,但嵌入式设备、Java、Python 默认都是大端(网络字节序)。长度头字段本身必须固定(如 4 字节),否则无法确定该读多少字节来解析头。
- 不要用
int或uint:它们长度随平台变(32/64 位),接收方无法预知头长 - 不要用分隔符(如
\n):业务数据里出现该字符就会切错,除非你全程控制并转义所有 payload,等价于重造协议 - 长度字段建议上限设为 1MB 左右:
if length > 1024*1024直接拒绝,防内存耗尽
缓冲区必须循环解析,不能“读一次就处理一次”
典型错误是每次 conn.Read(buf) 后立即尝试解包,结果只处理了第一个包,剩下数据留在 buf 末尾被下次覆盖。正确做法是用 bytes.Buffer 或自定义 ring buffer 累积数据,并在每次收到新字节后反复尝试提取完整包:
- 先检查缓冲区是否 ≥ 4 字节:不够则等待下一轮
Read - 读出头,解析出
msgLen;若msgLen > buf.Len(),说明 body 不全,return nil, io.ErrUnexpectedEOF - 成功取出一个包后,**不要清空缓冲区**,而是用
buf.Next(int(msgLen) + 4)跳过已处理部分,继续解析剩余内容 - 每个连接应独享缓冲区实例,不可多个 goroutine 共用同一
bytes.Buffer
io.ReadFull 是关键,但只管“读够”,不管“读完”
io.ReadFull(conn, header) 确保拿到完整的 4 字节头,但它不负责后续 body 的完整性校验。你仍需手动检查 buf.Len() >= msgLen,并在不足时保留状态等待更多数据。别依赖 bufio.Reader.ReadString 或 ReadBytes,它们内部也是找分隔符,对二进制协议完全失效,且会把半包误判为“无分隔符”而阻塞。
最易忽略的一点:解包逻辑必须和连接生命周期绑定。缓冲区不能是全局变量,也不能在 handler 函数退出时被 GC 掉——它得活在连接 goroutine 内部,直到 conn.Close()。











