conn.Read常读不全因TCP是流式协议,只保证最多读len(buf)字节;需用io.ReadFull读固定头、bufio.Scanner处理换行但须设缓冲上限;TLV等复杂协议才需自维护per-connection缓冲区。

conn.Read 为什么经常读不全?
因为 conn.Read 是底层 TCP 流读取,它只承诺“最多读 len(buf) 字节”,不保证一次把一个逻辑消息读完。你发了 "HELLO\n",它可能第一次返回 "HELLO"(n=5),第二次才返回 "\n"(n=1);也可能把三次发送拼成一次返回。这不是 bug,是 TCP 协议本身的流式特性。
- 永远用
buffer[:n]取有效数据,别直接用整个buffer - 遇到
err == io.EOF表示对方关闭连接,不是错误,该退出循环 - 如果只读固定长度头(比如 4 字节包长),必须用
io.ReadFull,否则可能只读到 2 字节就返回,导致后续解析崩溃
用 bufio.Scanner 处理换行分隔协议够用吗?
够用,但有隐含限制:它默认按 \n 切分,且内部缓冲区无上限。若客户端发超长行(比如 10MB 日志),scanner.Scan() 会卡住或 OOM。
- 务必调用
scanner.Buffer(make([]byte, 4096), 1 设定初始和最大缓冲区大小 - 若协议用
\r\n或自定义分隔符(如"###"),需用scanner.Split()自定义分割函数 - 返回的
scanner.Text()包含换行符前的内容,不含\n;但scanner.Bytes()返回的是带换行符的原始切片,注意清理
如何安全解析带长度前缀的二进制包?
这是生产环境最推荐的方式,但手写容易漏掉关键校验点。核心是“先读头、再读体”,两步都必须用 io.ReadFull,不能用普通 Read。
- 头长度建议用
uint32(4 字节),大端序(binary.BigEndian),避免跨平台字节序混乱 - 读到的 body 长度必须做范围检查,比如限制
if length > 10*1024*1024就直接断连,防内存耗尽 - 不要在每次调用
readPacket前都make([]byte, length),高频场景建议用sync.Pool复用 body 缓冲区
func readPacket(conn net.Conn) ([]byte, error) {
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil {
return nil, err
}
bodyLen := binary.BigEndian.Uint32(header[:])
if bodyLen > 10*1024*1024 {
return nil, errors.New("packet too large")
}
body := make([]byte, bodyLen)
if _, err := io.ReadFull(conn, body); err != nil {
return nil, err
}
return body, nil
}
粘包问题到底要不要自己维护缓冲区?
要,但只在协议无法用 bufio.Scanner 或固定长度头覆盖时才需要。比如 TLV 协议(Type-Length-Value)、变长字段嵌套、或头部本身也变长(如首字节表示头长)。
立即学习“go语言免费学习笔记(深入)”;
- 别用全局
bytes.Buffer,每个连接配一个独立实例,避免 goroutine 竞态 - 每次
conn.Read后把数据WriteTo或Write进缓冲区,再循环尝试“从缓冲区提取完整包” - 提取成功后,用
buffer.Next(n)截走已处理部分,剩余字节自动保留在缓冲区等待下次读取
readPacket 函数更消耗调试时间。










