Golang如何接收TCP数据_Golang TCP数据接收与处理技巧

9次阅读

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

Golang如何接收TCP数据_Golang TCP数据接收与处理技巧

conn.Read 为什么经常读不全?

因为 conn.Read 是底层 TCP 流读取,它只承诺“最多读 len(buf) 字节”,不保证一次把一个逻辑消息读完。你发了 "HELLOn",它可能第一次返回 "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 设定初始和最大缓冲区大小
  • 若协议用 rn 或自定义分隔符(如 "###"),需用 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 后把数据 WriteToWrite 进缓冲区,再循环尝试“从缓冲区提取完整包”
  • 提取成功后,用 buffer.Next(n) 截走已处理部分,剩余字节自动保留在缓冲区等待下次读取

真正麻烦的从来不是怎么读,而是读到一半连接断了、对方发了非法长度、或者缓冲区累积了几天没清的脏数据——这些边界情况,比写对一个 readPacket 函数更消耗调试时间。

text=ZqhQzanResources