scanner遇粘包停摆因未定义拆分规则,需用bufio.splitfunc自定义协议边界;它仅在缓冲区找边界,不读数据,须正确处理长度头截断、payload跨读、缓冲区扩容及ateof语义。

Scanner遇到粘包就停摆?bufio.SplitFunc才是协议解析的开关
Go 的 Scanner 默认按行切分,遇到自定义二进制协议(比如头4字节是长度、后面是 payload)直接失效。它不是不能拆,而是没告诉它“怎么算一个完整包”——bufio.SplitFunc 就是这个裁刀的定制接口。
关键点在于:它不负责读数据,只负责在已有缓冲区里找边界;返回的 advance 是下次从哪开始看,token 是这次切出来的字节切片,err 决定是否终止扫描。
- 必须返回
advance ,否则 <code>Scanner.Scan()会 panic - 如果还没凑够一个包(比如只读到一半长度字段),返回
0, nil,Scanner会继续读取填充缓冲区 - 不要在
SplitFunc里做解码或业务逻辑,只做切分判断——否则阻塞 Scanner 线程
写一个带长度头的 SplitFunc:先读4字节长度,再等足 payload
常见私有协议格式:[uint32_be][payload]。难点不在读长度,而在于“长度字段可能被截断”(比如缓冲区只拿到前2字节)和“payload 可能跨多次 Read”。
示例函数要处理三种状态:长度未读满 → 返回 0, nil;长度已读但 payload 不足 → 同样返回 0, nil;长度+payload 都齐了 → 返回 advance 和完整 token:
立即学习“go语言免费学习笔记(深入)”;
func lengthPrefixedSplit(maxPayload int) bufio.SplitFunc {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) == 0 {
return 0, nil, nil
}
// 1. 检查长度字段是否完整(4字节)
if len(data) < 4 {
if atEOF {
return 0, nil, io.ErrUnexpectedEOF
}
return 0, nil, nil // 还不够,等更多数据
}
// 2. 解析长度(大端)
payloadLen := int(binary.BigEndian.Uint32(data[:4]))
if payloadLen < 0 || payloadLen > maxPayload {
return 0, nil, fmt.Errorf("invalid payload length: %d", payloadLen)
}
totalLen := 4 + payloadLen
// 3. 检查整个包是否就绪
if len(data) < totalLen {
if atEOF {
return 0, nil, io.ErrUnexpectedEOF
}
return 0, nil, nil // 继续等
}
return totalLen, data[:totalLen], nil
}
}
为什么 Scanner 有时卡住不动?缓冲区大小和 atEOF 判断是关键
默认 Scanner 缓冲区只有 64KB,如果单个包超限(比如 1MB 图片传输),SplitFunc 永远收不到足够字节,一直返回 0, nil,看起来就像卡死。
- 用
scanner.Buffer(make([]byte, 0, 2 手动扩容缓冲区(第二个参数是最大容量) -
atEOF为true时,不代表“数据结束了”,只代表本次Read没读到新内容——可能是连接关闭,也可能是暂时无数据;必须结合协议逻辑判断是否该报错 - TCP 连接未关闭但长时间空闲时,
atEOF不会变true,Scanner会阻塞在下一次Read——这不是SplitFunc的问题,得靠连接层心跳或读超时控制
别把 SplitFunc 当万能解析器:它和 io.ReadCloser 的生命周期强绑定
SplitFunc 没有独立状态,所有中间状态(比如“已读2字节长度”)必须存在闭包变量里。一旦 Scanner 被复用或重置,这些变量就失效。
- 多个并发
Scanner不能共享同一个SplitFunc实例(除非内部用sync.Pool管理状态) - 如果协议需要跳过非法包继续解析(比如某次校验失败),不能靠
continue循环跳过——Scanner已经把这部分数据吞掉了;得自己实现带回退的 Reader 包装器 -
SplitFunc返回的token是data的子切片,如果后续还要异步处理,必须copy出来,否则原缓冲区被复用后内容就变了
真正麻烦的从来不是怎么写 SplitFunc,而是想清楚:这个包到底有没有明确边界、网络层会不会丢字节、出错后要不要重同步。这些事,bufio.Scanner 一律不管。










