io.ReadFull仅保证单次读满指定字节数,不处理半包累积与滚动解析;必须配合bytes.Buffer等应用层缓冲区循环检查头长、截取完整包并保留残余半包。
为什么 io.ReadFull 不能直接解决半包问题
很多人以为调用 io.readfull(conn, header) 就能“自动等够数据”,其实它只保证本次调用阻塞到填满 header 或出错;但若连接已关闭、对方崩溃或网络中断,它会立即返回 io.eof 或 io.errunexpectedeof。更关键的是:它不感知上层协议状态——比如你刚读完一个包的 body,缓冲区里还剩 3 字节,下一次 readfull 仍会从这 3 字节开始读,而不是跳过它们去等新数据。
必须自己维护接收缓冲区并循环解析
真实 TCP 流中,一次 conn.Read(buf) 可能含:0 个完整包 + 半个包、1 个完整包 + 半个包、3 个完整包 + 半个包。不维护缓冲区就无法判断哪部分是“已就绪可消费”,哪部分是“待续”。
- 用
bytes.Buffer或 ring buffer 累积所有收到的原始字节 - 每次收到新数据后,进入解析循环:
for buf.Len() >= 4 { ... } - 先尝试读头(4 字节),若不够就
return等下次数据 - 读到头后检查
msgLen,若buf.Len() ,说明 body 不全,break出循环等待 - 否则切出完整包,
buf.Next(4 + int(msgLen))移除已处理数据
bufio.Reader 的 ReadBytes 和 ReadString 为什么危险
它们内部也是边读边找分隔符,但前提是“分隔符不会出现在业务数据中”。一旦用户发一条含 \n 的 JSON、带换行的日志、或任意二进制 payload,就会提前截断或永远阻塞。
-
r.ReadBytes('\n')可能返回"{\"msg\":\"hello\nworld\"}\n"—— 实际是 1 个完整 JSON,却被切成 2 次返回 -
r.ReadString('\n')在没遇到\n时会一直阻塞,而真实协议往往无换行 - 即使加了超时,也解决不了“粘包”本身,只是让错误暴露得更快
长度头协议中容易忽略的三个细节
用 4 字节大端长度头最稳妥,但实操时这三个点常被跳过:
- 发送端写入必须原子:不能
conn.Write(header)成功后,conn.Write(body)失败——应合并为一次conn.Write(append(header, body...))或用io.MultiWriter - 长度校验必须做上限:
if msgLen > 1024*1024 { return nil, fmt.Errorf("too large") },否则恶意构造 4GB 头会导致 OOM - 接收端不能假设
conn.Read一次返回全部数据:即使你传入 4096 字节的 buf,也可能只读到 127 字节,必须循环调用直到满足协议要求长度
缓冲区管理不是“锦上添花”,而是粘包场景下唯一能区分“已收齐”和“还在路上”的手段;漏掉循环解析逻辑,或把 bufio.Reader 当成协议解析器,上线后遇到混合流量立刻出问题。











