不能直接用 bufio.Scanner 解析自定义 TCP 协议,因为它默认按行切分,不支持长度前缀或固定包头,无法处理粘包/半包,易卡住或丢数据;正确做法是用 io.ReadFull 手动解析包头再读 payload,并维护状态缓冲区安全处理边界情况。

为什么不能直接用 bufio.Scanner 解析自定义 TCP 协议
因为 bufio.Scanner 默认按行(\n)切分,而大多数自定义 TCP 协议用的是长度前缀(如 4 字节大端表示 body 长度)或固定包头+变长 payload,它既不识别长度字段,也无法处理粘包/半包。一旦网络抖动或写入未满缓冲区,Scan() 就会卡住或丢数据。
真正可用的起点是 io.ReadFull + 手动解析包头,再按需读取 payload:
- 先用
io.ReadFull(conn, headerBuf)读满包头(比如 6 字节:2 字节 magic + 2 字节 version + 2 字节 length) - 校验
headerBuf[0:2]是否等于预期 magic(如[0x12, 0x34]),否则直接断连 - 用
binary.BigEndian.Uint16(headerBuf[4:6])解出 payload 长度plen - 再用
io.ReadFull(conn, payloadBuf[:plen])读满 body —— 这步失败说明连接异常中断
如何安全处理粘包和半包而不阻塞
核心是别让单次读操作跨逻辑包边界。必须把连接包装成带状态的 reader,维护一个未消费的字节缓冲([]byte),每次从缓冲里尝试提取完整包,不够就继续 conn.Read() 补充。
关键点:
立即学习“go语言免费学习笔记(深入)”;
- 不要用
conn.SetReadDeadline()做超时 —— 它对多次小读无效;应在每次Read()前单独设置,且超时后必须重置连接状态 - 缓冲区扩容要预估最大包长(比如 64KB),避免频繁
append导致内存抖动 - 解析失败(如 magic 不匹配、length 超限)时,必须跳过当前疑似错误字节(至少跳 1 字节),否则会永远卡在坏数据上
- 示例中常见错误:用
bytes.Index在缓冲里找 magic —— 这忽略包头位置固定性,且无法区分 magic 是 payload 还是真实包头
encoding/binary 读取多字段时字节序和对齐怎么搞
Go 的 binary.Read 默认要求结构体字段严格按二进制布局顺序排列,且不能有 padding。比如协议定义:4 字节 length(big-endian)、1 字节 cmd、2 字节 seq —— 对应结构体必须写成:
type Header struct {
Length uint32
Cmd byte
Seq uint16
}然后用 binary.Read(r, binary.BigEndian, &h)。如果字段顺序错(比如把 Cmd 放最前),或用了 uint8 以外类型存单字节,就会读歪。
容易踩的坑:
-
binary.Read内部调用io.ReadFull,所以传入的io.Reader必须能保证数据连续 —— 不能直接传conn,得传你自己的缓冲 reader - 如果协议里有字符串字段(比如 16 字节固定长度 name),别用
string字段,用[16]byte,解析后手动转:string(h.Name[:bytes.IndexByte(h.Name[:], 0)]) - 字段太多时,手写
binary.Read易错,可改用unsafe.Slice+binary.BigEndian.PutUint32等逐字段写,更可控
怎么让解析模块支持并发读但不乱序
TCP 流本身是有序的,但如果你开了多个 goroutine 同时从同一个 net.Conn 读,结果一定是数据错乱 —— Go 不保证并发 Read() 的原子性。唯一安全的方式是:一个 goroutine 负责从 conn 持续读、拆包、发到 channel;其他 goroutine 从 channel 消费。
典型结构:
- 启动一个
readLoopgoroutine,循环调用你的解析函数,每成功解出一包就ch - channel 类型建议用
chan Packet(非缓冲或缓冲大小=1),避免消费者慢导致内存堆积 - 如果需要按连接 ID 分发,别在解析层做路由 —— 先统一收包,再由上层根据
packet.ConnID分发,否则解析模块耦合业务逻辑 - 注意:
conn.Close()后,readLoop必须能及时退出,推荐用sync.Once触发 cleanup,而不是靠 channel close 判断
最常被忽略的是错误传播 —— 解析失败(如长度非法)不能只 log,必须通过 channel 或回调通知上层,否则连接可能挂着不动。


















