直接读 conn.Read() 会遇到黏包和半包,因 TCP 是字节流协议,Read() 不保证按业务包边界读取;需结合长度前缀协议、环形缓冲区与带 conn/context/error 的回调机制处理。

为什么直接读 conn.Read() 会遇到黏包和半包
因为 TCP 是字节流协议,conn.Read() 只保证最多读取指定长度的字节,不保证一次调用就拿到一个完整业务包。可能一次读到多个包(黏包),也可能只读到半个包(半包),甚至空读(没数据但连接还活着)。回调机制本身不解决这个问题——它只是把“读到数据后怎么处理”这个逻辑解耦出来,真正要靠协议设计+缓冲区管理来应对。
用回调 + 环形缓冲区(ring buffer)做基础粘包剥离
别一上来就写状态机或复杂解析,先确保数据能按边界正确切分。推荐用 bytes.Buffer 或更轻量的 golang.org/x/exp/slices 配合自定义协议头(如 4 字节长度前缀)做初步剥离:
- 每次
conn.Read()后把数据追加到缓冲区(buf.Write()) - 循环检查:若
buf.Len() >= 4,读出前 4 字节解析为payloadLen - 若
buf.Len() >= 4 + payloadLen,则切出完整包,触发回调onMessage([]byte) - 切完后从缓冲区移除已处理字节(
buf.Next(n)),留下的继续等待下一轮
注意:不要用 bufio.Scanner,它默认按行切割,不适用于二进制协议;也别在回调里直接阻塞处理耗时逻辑,否则会卡住后续包解析。
回调函数签名设计要支持上下文与错误传递
常见错误是定义成 func([]byte) 这种裸函数,导致无法区分来源连接、没法返回解析失败原因、也无法控制重试或丢弃。建议统一用:
立即学习“go语言免费学习笔记(深入)”;
type MessageHandler func(conn net.Conn, data []byte) error
这样可以在回调里做:
- 通过
conn.RemoteAddr()区分客户端,做连接级限流 - 返回非 nil error 触发连接关闭或日志记录(比如解码失败)
- 配合
context.WithTimeout()控制单次回调执行上限,防雪崩
如果回调里需要异步处理,务必复制 data 内容(copy(dst, data)),因为原始切片底层数组可能被下一次 Read() 覆盖。
真实场景中容易漏掉的三个点
一是超长包没限制:没对 payloadLen 做校验,攻击者发个 2GB 的长度头会让服务端 OOM。必须加硬限制,比如 if payloadLen > 1024*1024 { return ErrPacketTooLarge }。
二是连接断开时缓冲区残留:io.EOF 出现时,缓冲区里可能还有未凑够长度的半包,得判断是否丢弃或告警,不能静默忽略。
三是并发读写冲突:同一个 conn 多 goroutine 调用 Read() 会导致数据错乱,必须确保只有一个 goroutine 负责读,再通过 channel 或回调通知业务层。


















