HelloCoder HelloCoder
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
  • 《k8s学习心得》
随笔
关于作者
  • 《从0到1学习Java多线程》

  • 《从0到1搭建服务器》

  • 源码学习

  • 可观测和监控

  • 玩转IDEA

  • AI学习

  • 03-RPC

    • rmi-远程方法调用
    • SPI机制
    • RPC下线窗口期
    • 目标函数调用
    • 粘包问题
  • 05-《Java日志框架》

  • k8s

  • 专栏
  • 03-RPC
#粘包问题
HaC
2026-06-28
目录

粘包问题

不管是HTTP,还是RPC,它们都是建立在TCP的层面上。

在 TCP 这种“面向字节流”的传输协议中,操作系统只管把数据当成一汪泉水一样连绵不断地发送,它根本不知道什么是**“一个完整的业务请求”。这就是拆包与粘包**问题。

# 一、HTTP 三种报文分界机制

# 方案 1:Content-Length 定长长度(最常用)

响应头携带 Content-Length: N,代表响应体精确字节数。

流程:

  1. 客户端读取响应头,解析出数字 N;

  2. 持续从 TCP 缓冲区读数据,凑够 N 字节为止,就是完整 body;

  3. 多余未读字节属于下一条 HTTP 请求 / 响应,留到下次处理。

    直接从长度分割,彻底杜绝粘包。

# 方案 2:Transfer-Encoding: chunked 分块编码(动态长度)

不知道 body 总大小(流式输出、大文件实时转发)时使用:

  1. 数据分成多个块,每块格式:块长度\r\n块内容\r\n

  2. 最后以 0\r\n\r\n 标识结束;

    客户端按分块标记逐段读取,识别结束符,分割报文。

# 方案 3:短连接(HTTP/1.0 默认 Connection: close)

服务器发完整条响应后,直接关闭 TCP 连接。

客户端读到 TCP 流关闭,判定本条 HTTP 数据接收完成。

缺点:每次请求新建 TCP,性能差,HTTP1.1 默认长连接 Keep-Alive。

# 二、Dubbo是如何解决的?

Dubbo 解决这个问题的核心逻辑非常简单粗暴但有效:既然字节流是无界的,那我就给每个请求加一个固定格式的“请求头(Header)”,在头里明确写上这个请求的“数据体(Body)到底有多少个字节”。

# 1. Dubbo 协议的定长报文头(Header)

Dubbo 每次发请求,都会在真正的业务数据(Body)前面,加上一个 16 字节(128位) 的固定长度请求头。

这 16 个字节的排布紧凑而精妙:

  • Magic Number (2字节): 魔数(固定为 0xdabb)。服务端收到数据先看前两个字节是不是它,用来判断这到底是不是 Dubbo 协议的数据,不是就直接断开,防止误读。
  • Flag (1字节): 标志位。记录是请求还是响应、是双向调用还是单向通知、序列化类型(比如是 Hessian 还是 Fastjson)等。
  • Status (1字节): 状态位。如果是响应,记录成功还是失败。
  • Invoke ID (8字节): 请求的唯一 ID。异步通信时,用来把响应和请求匹配起来。
  • Data Length (4字节): 核心就在这里! 这 4 个字节变成无符号整型,记录了后面紧跟的 Body(业务数据)的绝对长度(字节数)。

所以Dubbo也是使用了和HTTP Content-Length 的“定长长度”方案。但是HTTP是不固定大小的纯文本,所以必须借助 \r\n\r\n 隔离,然后再去读 Content-Length。

而 Dubbo 是一个纯二进制协议,它的报文头(Header)本身就是死规定、雷打不动的 16 个字节。

它的执行逻辑是:

无论这次调用发了什么数据,Dubbo 都是在客户端把整个业务数据全部序列化完,计算出总字节数(比如 500 字节),把 500 写入 Header 的最后 4 字节,然后把这 16 + 500 = 516 个字节一并扔给 TCP 发送。

服务端接收后,看到末尾的 4 个字节是 500,就知道,读完 500 字节,本条消息结束,剩下水管里的数据属于下一盒消息,留着下次解析。

这在结构上,和 HTTP 携带 Content-Length 的原理是完全一致的。

用个生活化的比喻来解释一下:

客户端 = 工厂打包工人

Dubbo 消息包 = 标准快递件

  • 固定 16 字节 Header = 统一硬纸板快递外包装盒
  • Header 末尾 4 字节数字 = 盒子侧面印的「内部货物总容量」
  • 序列化业务数据 Body = 盒里的商品
  • 单件完整包裹总大小 = 16 包装盒字节 + 商品字节

TCP 管道 = 连通工厂(客户端)和仓库(服务端)的一根无隔断传送带

服务端接收缓冲区 = 仓库临时收货托盘

Netty 解码器 = 仓库分拣员(负责拆分快递,处理粘包 / 拆包)

# 2. 服务端如何用它解决粘包/拆包?

Dubbo 底层使用了高性能的网络框架 Netty。在 Netty 中,Dubbo 实现了一个核心的解码器叫做 InternalDecoder(继承自 ByteToMessageDecoder)。

当服务端收到一堆凌乱的字节流时,解码器的处理逻辑场景如下

# 场景 A:数据还没攒够(拆包)

  1. 解码器检查当前接收缓冲区里的字节数,发现居然连 16 个字节都不到(连 Header 都不全)。
  2. 解码器直接 return(不对数据做任何处理),Netty 会继续等待网络数据,直到攒够了再唤醒它。

工厂工人(客户端)打包好一件快递丢上传送带,传送带分段送货。

仓库托盘(服务端)上现在只有一小截纸板,凑不齐完整 16 格外包装盒。

分拣员(Netty接码器,拆包)一看盒子残缺,读不出里面商品的容量,啥也不干,原地等待传送带继续送纸板碎片,凑齐完整盒子再开工。

# 场景 B:知道了长度,等 Body(拆包)

  1. 缓冲区够 16 字节了,解码器先读取前 16 字节。
  2. 解析出最后的 Data Length(假设是 500 字节)。
  3. 此时解码器计算:整个完整包需要 16 + 500 = 516 字节。
  4. 检查缓冲区,发现目前只有 300 字节。说明发生了拆包,数据还没发完。
  5. 解码器回滚读指针,继续 return 等待。

托盘攒齐完整 16 格外包装盒,分拣员看到盒上标注:内部商品 500 单位。

算完整件需要 16 盒 + 500 商品 = 516 单位,可托盘全部物料加起来只有 300 单位,商品缺大半。

分拣员把刚拆开的盒子放回托盘(回滚读指针),暂停处理,等待传送带补全剩余商品物料。

# 场景 C:刚好一个完整的包

  1. 缓冲区积攒的数据 = 516 字节。
  2. 解码器直接在内存中精准地切出这 516 个字节。
  3. 把前 16 字节丢掉(或者提取出 ID 和序列化类型),根据header头要求的接码器,把剩下的 500 字节的 Body 扔给 Hessian2 或 Protobuf 反序列化器。
  4. 这就完成了一次完整、不多不少的调用。

托盘里物料总量 = 516 单位,刚好装满一整套快递。

分拣员精准切出 516 单位完整包裹,撕掉外包装盒(提取调用信息),只把内部 500 单位商品交给解析工具反序列化,一次 RPC 调用解析完成。

# 场景 D:多个包粘在一起了(粘包)

  1. 缓冲区里一口气来了 1200 个字节(包含了 A 请求和 B 请求)。
  2. 解码器看第一个包需要 516 字节,直接在内存中把前 516 字节“切走”送去处理。
  3. 关键点: 此时缓冲区还剩 1200 - 516 = 684 字节。Netty 的解码器是个循环(while 循环),它不会停,会立刻对剩下的 684 字节重复上述步骤,继续解析下一个请求。

工厂连续打包两件快递,传送带一次性送了合计 1200 单位物料到托盘。

  1. 分拣员先切走前 516 单位第一件完整快递,拿去解析;
  2. 托盘剩余:1200 - 516 = 684 单位物料;
  3. 分拣员不会停止,循环重复分拣逻辑,立刻处理托盘剩下的 684 单位,继续拆分第二件快递;
  4. 直到托盘剩余物料凑不齐一整件快递,才停下等待后续数据。

# 3. 总结

Dubbo(以及绝大多数 RPC 框架,如 gRPC)解决粘包的底层逻辑可以总结为八个字:定长头 + 长度字段(LengthFieldBasedFrameDecoder 思想)。

  • 如何知道发送完毕没有? 不靠特殊的结束符,而是靠 Header 里的 Data Length 计数器。
  • 数据缺失了怎么办? 如果缓冲区字节数 Header 里的长度,说明没发完,服务端就死等,直到 TCP 把后续碎片拼齐。
#粘包问题
上次更新: 2026-06-28 03:58:08
最近更新
01
悲观锁和乐观锁
06-28
02
MySQL-like是否可以使用索引
06-28
03
MySQL大表索引重建
06-28
更多文章>
Theme by Vdoing | Copyright © 2020-2026 HaC
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式