go net/rpc 默认不处理粘包,需应用层加长度前缀(如4字节大端整数),或实现自定义servercodec/clientcodec;grpc基于http/2天然解决粘包但更重;手写协议推荐binary.write+io.readfull,避免bufio.reader破坏帧边界。

Go net/rpc 默认不处理粘包,必须自己加帧头
Go 标准库 net/rpc 基于 gob 编码,它本身**不定义传输层协议格式**,也不做分包/粘包处理。底层走的是裸 net.Conn,数据以字节流方式收发。如果客户端连续两次 Call(),服务端可能一次 Read() 就收到两个请求拼在一起(粘包),也可能一个请求被拆成两次读到(半包)。
常见错误现象:gob: unknown type id or corrupted data 或 EOF 出现在非预期位置,本质是 gob.Decoder 试图解码不完整的或混杂的数据块。
- 必须在应用层添加长度前缀(如 4 字节大端整数表示后续 payload 长度)
- 客户端写入前先写长度,再写
gob编码后的字节;服务端先读 4 字节,再按该长度读满 payload,再交给gob.Decoder - 不要复用
gob.Encoder/Decoder跨多个逻辑消息——每个 RPC 消息应独占一对编/解码器,或确保它们严格按“写长度→写 gob→读长度→读 gob”节奏配对
使用 grpc-go 可绕过粘包问题,但代价是协议栈变重
grpc-go 底层基于 HTTP/2,其帧(frame)机制天然解决粘包:每个 RPC 请求/响应被切成 HEADERS + DATA 帧,由 HTTP/2 连接层保证顺序和边界。你不需要手动加长度头,gRPC 自动处理。
但要注意:
- HTTP/2 的多路复用、头部压缩、流控等带来额外 CPU 和内存开销,对极低延迟、高吞吐的内部微服务间通信可能不如自定义二进制协议轻量
- 若已有
net/rpc服务想升级,不能直接替换——gRPC要求定义.proto接口、生成 stub,不是 drop-in 替换 - 调试时看到
http2: server: error reading preface from client,往往是因为客户端没发 HTTP/2 preface(PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n),比如用 telnet 直连或错用了 HTTP/1.1 客户端
自定义高性能协议建议用 binary.Write + io.ReadFull 组合
若追求极致控制与性能(如游戏服、高频交易网关),推荐手写帧协议:固定 4 字节长度头 + binary.BigEndian.PutUint32() 写入,接收端用 io.ReadFull(conn, header[:]) 确保读满头,再分配 buffer 并用 io.ReadFull(conn, buf) 读满 body。
关键细节:
- 避免用
bufio.Reader包裹net.Conn后调Read()——它会内部缓存,破坏帧边界判断;如需缓冲,只对 body 解码阶段用(例如把读到的完整 payload 丢给bytes.NewReader()再喂给gob.Decoder) - 长度字段建议限制最大值(如 ≤ 16MB),防止恶意客户端申请超大内存导致 OOM
- 服务端每连接建议启动单独 goroutine 处理读循环,用
select+context控制超时和取消,别让一个卡住的读阻塞整条连接
net/rpc 的 codec 接口是粘包处理的合法入口点
Go net/rpc 允许你实现 rpc.ServerCodec 和 rpc.ClientCodec 接口,把帧处理逻辑封装进去。这是最干净的解法——既保留 net/rpc 的服务注册、方法路由能力,又把粘包逻辑隔离在 codec 层。
典型结构:
-
WriteRequest():先binary.Write(enc, length),再enc.Encode(req) -
ReadResponseHeader():先io.ReadFull()读长度,再io.ReadFull()读 body 到临时 buffer,最后dec.Decode()解 header - 注意
ReadResponseBody()也要同样读 body 长度并解码,否则 response 也会粘包 - 官方
gobrpc示例里有个server.go的gobServerCodec实现,可作起点,但记得补上帧头逻辑
真正难的不是写通,而是压测时发现某个连接在高并发下偶发解码失败——大概率是 codec 实现里共享了 gob.Encoder/Decoder 或没正确处理 partial read。每个连接实例必须持有独立的编解码器状态。











