黏包是tcp字节流不保留消息边界导致的多个消息粘连或单个消息拆分的现象;go的net.conn直接暴露tcp流,read()返回字节数不确定,需自行通过固定长度头、bufio.reader分隔符或gob/json流式解码等方案拆包。

什么是黏包,为什么 Go 的 net.Conn 会遇到它
TCP 是字节流协议,不保留应用层消息边界。Go 的 net.Conn 直接暴露底层 TCP 流,Read() 返回的字节数完全取决于内核缓冲区状态和网络状况——可能一次读到多个“包”,也可能一个“包”分多次读完。这不是 Go 的 bug,而是 TCP 本质;但新手常误以为 Write() 发一个结构体,对方 Read() 就能原样收一个。
常见错误现象:
- 客户端发了两条 JSON 消息,服务端
Read()一次性拿到拼在一起的字符串,json.Unmarshal()报invalid character - 用固定长度头(如 4 字节)解析消息体,但
Read()只读到 2 字节就返回,后续再读又混入下一条数据
关键点:黏包不是 Go 特有,但 Go 不像 Python 的 socket.recv() 那样默认带“按行/按长度”语义,你得自己负责拆包。
用 bufio.Reader + 自定义分隔符解决简单场景
适合日志推送、命令行协议等以换行符或特殊字符结尾的消息。比裸 Read() 稳定,且避免反复小内存分配。
立即学习“go语言免费学习笔记(深入)”;
使用场景:
- MQTT 的 CONNECT/CONNACK(纯文本控制报文)
- 内部微服务间基于行的轻量通信(如
GET /health\n)
实操建议:
-
bufio.NewReader(conn)包一层,再调用ReadString('\n')或ReadBytes('\n') - 注意:如果数据本身含
\n(比如 JSON 含换行),必须换分隔符或改方案 -
ReadString()会包含结尾符;ReadBytes()也是,记得bytes.TrimSuffix(buf, []byte{'\n'}) - 超时控制要设在
conn.SetReadDeadline()上,bufio.Reader不自动继承
用固定长度头 + io.ReadFull() 处理二进制协议
最常用、最可控的解法,适用于自定义 RPC、游戏协议、序列化数据(如 Protobuf、gob)。
酷纬企业网站管理系统Kuwebs是酷纬信息开发的为企业网站提供解决方案而开发的营销型网站系统。在线留言模块、常见问题模块、友情链接模块。前台采用DIV+CSS,遵循SEO标准。 1.支持中文、英文两种版本,后台可以在不同的环境下编辑中英文。 3.程序和界面分离,提供通用的PHP标准语法字段供前台调用,可以为不同的页面设置不同的风格。 5.支持google地图生成、自定义标题、自定义关键词、自定义描
参数差异:
- 头部通常 2 或 4 字节(uint16/uint32),表示后续 payload 长度
-
io.ReadFull()保证读满指定字节数,否则返回io.ErrUnexpectedEOF,比手动循环Read()更可靠
容易踩的坑:
- 头部字节序没统一(服务端用
binary.BigEndian,客户端用了LittleEndian,结果长度解析错成几 MB) - 忘记对头部做范围检查,恶意客户端发个
0xFFFFFFFF,直接make([]byte, 4GB)OOM -
io.ReadFull()第二个参数是切片,不是指针;传buf[:2]而不是&buf[0]
示例片段:
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil {
return err
}
length := binary.BigEndian.Uint32(header[:])
if length > 1024*1024 { // 防爆内存
return errors.New("payload too large")
}
payload := make([]byte, length)
if _, err := io.ReadFull(conn, payload); err != nil {
return err
}用 gob 或 encoding/json 自带流式编码绕过黏包
不是“解决”黏包,而是让编码器自己处理边界。适合 Go 服务间通信,双方都可控。
性能与兼容性影响:
-
gob快、紧凑,但仅限 Go 生态;跨语言必须换 -
json通用,但每个消息都要加长度头(标准库不提供流式 JSON 解析),否则仍会黏——所以实际要用json.Decoder配合bufio.Reader,它内部会按 token 边界读取
实操建议:
- 用
json.NewDecoder(bufio.NewReader(conn)),然后反复.Decode(&v) - 每次
Decode()会自动跳过空白、定位到下一个完整 JSON 值(对象/数组/字符串等),天然抗黏包 - 但要求每条消息是合法独立 JSON,不能是
{"a":1}{"b":2}连写(这算两个 JSON,但中间无分隔符,Decode()会卡在第二个{处报错)
复杂点在于:所有方案都依赖「双方约定」。没有银弹——如果你对接的是 C++ 写的老协议,就得老老实实手撸长度头;如果只是两个 Go 服务,gob 加 io.ReadFull() 最省心。别指望 runtime 自动猜出你的消息边界。










