当前位置: 首页 > news >正文

Netty LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder 继承自 ByteToMessageDecoder,因此它天然就拥有了父类提供的处理半包、粘包问题的能力。它的核心任务是解决一个比 FixedLengthFrameDecoder 更普遍的问题:消息的长度不是固定的,而是由消息头中的一个“长度字段”来动态指定的

ByteToMessageDecoder分析见:Netty ByteToMessageDecoder解码机制解析

LengthFieldBasedFrameDecoder 的设计哲学是:通过一组配置参数,就能适配绝大多数“头部包含长度”的二进制协议。这些参数共同定义了如何从字节流中找到长度字段,并根据其值计算出整个消息帧的长度。

我们来看一下它的核心字段,这些字段直接对应其构造函数的参数:

// ... existing code ...
public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {private final ByteOrder byteOrder;private final int maxFrameLength;private final int lengthFieldOffset;private final int lengthFieldLength;private final int lengthFieldEndOffset; // 内部计算得出private final int lengthAdjustment;private final int initialBytesToStrip;private final boolean failFast;// ... 状态字段 ...
// ... existing code ...

这些参数就是解码协议的“五要素”:

  • maxFrameLength最大帧长度。用于防止客户端发送超大报文耗尽内存,是对服务器的保护。如果计算出的帧长度超过此值,会抛出 TooLongFrameException
  • lengthFieldOffset长度字段偏移量。指长度字段相对于整个报文起始位置的偏移(从0开始)。
  • lengthFieldLength长度字段本身的字节数。例如,如果长度用一个 int 表示,这里就是4;用 short 表示就是2。
  • lengthAdjustment长度调节值。这是最关键也最灵活的参数。它的作用是在“从长度字段中读出的值”的基础上进行微调,以计算出真正的内容长度
    • 一个常见的场景是:长度字段的值包含了头部自身的长度。例如,报文总长14字节,头部2字节,内容12字节,但长度字段的值是14(总长)。此时就需要一个负的调节值 -2 来减去头部的长度,得到真正的内容长度12。
    • 另一个场景是:长度字段后面还有其他头部字段。例如 [长度(4字节)][版本号(2字节)][内容(...)]。长度字段的值只代表了内容的长度,但要读取整个帧,还需要跳过版本号。此时就需要一个正的调节值 2
  • initialBytesToStrip解码后要剥离的字节数。解码成功后,从帧的头部开始,要去掉多少字节。通常用来去掉协议头(包括长度字段本身),只将纯粹的业务数据(payload)传递给下一个 Handler

这五个参数的组合几乎可以描述所有基于长度的协议格式,这也是为什么该类的注释中给出了大量图文并茂的例子。

 ASCII 图直观解释

 LengthFieldBasedFrameDecoder 的核心参数是如何与一个二进制消息帧(Frame)的各个部分对应的。这会比单纯看文字定义清晰得多。

我们将基于一个通用的、复杂的协议帧结构来进行说明。假设我们的消息帧结构如下:

+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
|  (Junk Bytes)    |      (LEN)     | (More Junk)    |     (Payload)    |
+------------------+----------------+----------------+------------------+

现在,我们来逐一解释 LengthFieldBasedFrameDecoder 的五个关键参数分别对应这个图的哪一部分。

1. lengthFieldOffset

含义:长度字段(Length Field)的起始位置,相对于整个帧的起始点的偏移量。

图解

<lengthFieldOffset>
+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
+------------------+----------------+----------------+------------------+
^
|
Frame Start (offset = 0)

在这个例子中,lengthFieldOffset 就是 Other Header A 的长度。如果消息帧开头就是长度字段,那么 lengthFieldOffset 就是 0。

2. lengthFieldLength

含义:长度字段本身占用的字节数。

图解

                   <lengthFieldLength>
+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
+------------------+----------------+----------------+------------------+

这个参数告诉解码器应该读取多少个字节来解析出长度值。例如,如果长度是用一个 Java 的 int 类型表示,那么 lengthFieldLength 就是 4。

3. lengthAdjustment

含义:长度调节值。这是最灵活也最关键的参数,用于对从 Length Field 中读取到的原始值进行修正,以得到我们真正想要的数据块的长度。

LengthFieldBasedFrameDecoder 内部采用了一个统一的、无分支的计算公式来确定帧的总长度:

最终帧总长度 = 长度字段的值 + lengthAdjustment + lengthFieldEndOffset

这个公式非常优雅。解码器不需要写 if/else 去判断协议属于哪种情况,它只需要把配置的参数代入这个固定的公式即可。

实际Content 的长度 = 从 Length Field 读取的值 + lengthAdjustment

我们分两种情况讨论:

情况一:Length Field 的值代表 Actual Content 的长度

+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
|                  |    (value=L)   |                |   (length=L)     |
+------------------+----------------+----------------+------------------+<---------------------------------->Frame part to be read<lengthAdjustment>

在这种情况下,解码器读出长度 L 后,需要知道从哪里开始读取这 L 个字节。它需要跳过 Other Header B。所以,lengthAdjustment 就等于 Other Header B 的长度。

情况二:Length Field 的值代表整个消息帧的长度

<------------------------- Length Field value is L ------------------------->
+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
|                  |    (value=L)   |                |                  |
+------------------+----------------+----------------+------------------+

在这种情况下,Length Field 的值 L 是整个帧的长度。但是,解码器需要知道 Actual Content 的长度。 Content 的长度 = L - (Other Header A 的长度 + Length Field 的长度 + Other Header B 的长度)

所以,lengthAdjustment 应该是一个负数,其值为: lengthAdjustment = - (Other Header A 的长度 + Length Field 的长度 + Other Header B 的长度)

总结 lengthAdjustment

  • 它弥补了 Length Field 的结束点 和 Actual Content 的起始点 之间的差距。
  • 如果 Content 在 Length Field 之后,lengthAdjustment 就是它们之间 Other Header B 的长度。
  • 如果 Length Field 的值包含了头部信息,lengthAdjustment 就是一个负数,用来减去这些头部的长度。

4. initialBytesToStrip

含义:在成功解码出一个完整的帧之后,从这个帧的头部开始,要丢弃(剥离)多少个字节。剩下的部分才会作为最终结果传递给下一个 ChannelHandler

图解

假设我们只想把 Actual Content 传递给后续业务逻辑,需要剥离掉 Other Header ALength Field 和 Other Header B

<-------------- initialBytesToStrip ----------------->
+------------------+----------------+----------------+------------------+
|  Other Header A  |  Length Field  | Other Header B |  Actual Content  |
+------------------+----------------+----------------+------------------+|V+------------------+|  Actual Content  |  (This is the final output)+------------------+

在这个例子中,initialBytesToStrip 就等于 Other Header A 的长度 + Length Field 的长度 + Other Header B 的长度。

综合示例

让我们用 LengthFieldBasedFrameDecoder 类注释中的一个复杂例子来串联所有参数:

 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)* +------+--------+------+----------------+      +------+----------------+* | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |* | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |* +------+--------+------+----------------+      +------+----------------+

对应的参数设置是:

  • lengthFieldOffset = 1 (HDR1 的长度)
  • lengthFieldLength = 2 (Length 字段的长度)
  • lengthAdjustment = 1 (HDR2 的长度)
  • initialBytesToStrip = 3 (HDR1 的长度 + Length 字段的长度)

解码过程

  1. 解码器跳过 1 个字节 (lengthFieldOffset),找到 Length 字段。
  2. 读取 2 个字节 (lengthFieldLength),得到值 0x000C (十进制 12)。
  3. 解码器知道 Length 字段后面还有 lengthAdjustment = 1 个字节的 HDR2
  4. 所以,Actual Content 的长度就是 12
  5. 整个帧的长度 = Length 字段结束的位置 + lengthAdjustment + Actual Content 的长度 = (1+2) + 1 + 12 = 16 字节。
  6. 解码器成功从缓冲区读取一个 16 字节的完整帧。
  7. 根据 initialBytesToStrip = 3,解码器从这个 16 字节的帧中丢弃前 3 个字节(HDR1 和 Length)。
  8. 最终,一个包含 HDR2 和 Actual Content 的 13 字节的 ByteBuf 被传递给下一个 Handler


构造函数

LengthFieldBasedFrameDecoder 提供了多个重载的构造函数,最终都调用了参数最全的那个。

// ... existing code ...public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip, boolean failFast) {this.byteOrder = checkNotNull(byteOrder, "byteOrder");checkPositive(maxFrameLength, "maxFrameLength");checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset");checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip");if (lengthFieldOffset > maxFrameLength - lengthFieldLength) {throw new IllegalArgumentException("maxFrameLength (" + maxFrameLength + ") " +"must be equal to or greater than " +"lengthFieldOffset (" + lengthFieldOffset + ") + " +"lengthFieldLength (" + lengthFieldLength + ").");}this.maxFrameLength = maxFrameLength;this.lengthFieldOffset = lengthFieldOffset;this.lengthFieldLength = lengthFieldLength;this.lengthAdjustment = lengthAdjustment;this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;this.initialBytesToStrip = initialBytesToStrip;this.failFast = failFast;}
// ... existing code ...

构造函数主要做了两件事:

  1. 参数校验:对传入的参数进行严格的合法性检查,例如 maxFrameLength 必须为正数,lengthFieldOffset 不能为负数等。还有一个关键检查是 lengthFieldOffset + lengthFieldLength 不能超过 maxFrameLength,确保长度字段本身就在最大帧的范围内。
  2. 字段初始化:将校验后的参数赋值给 final 字段。同时,它会预先计算一个内部字段 lengthFieldEndOffset,即 lengthFieldOffset + lengthFieldLength,代表长度字段的结束位置。这个预计算可以简化后续 decode 方法中的逻辑。

核心解码逻辑 decode

decode 方法是整个解码器的灵魂所在,我们分步来看它的执行流程。

// ... existing code ...@Overrideprotected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {Object decoded = decode(ctx, in);if (decoded != null) {out.add(decoded);}}protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {long frameLength = 0;if (frameLengthInt == -1) { // new frame// 1. 处理正在丢弃的超长帧 (如果上一帧是超长帧)if (discardingTooLongFrame) {discardingTooLongFrame(in);}// 2. 检查头部是否完整if (in.readableBytes() < lengthFieldEndOffset) {return null; // 连长度字段都读不到,半包,等待更多数据}// 3. 读取并计算帧长度int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);if (frameLength < 0) {failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);}// 关键计算:应用 lengthAdjustmentframeLength += lengthAdjustment + lengthFieldEndOffset;// 4. 各种合法性校验if (frameLength < lengthFieldEndOffset) {failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);}if (frameLength > maxFrameLength) {exceededFrameLength(in, frameLength);return null;}// never overflows because it's less than maxFrameLengthframeLengthInt = (int) frameLength;}// 5. 检查完整帧是否到达if (in.readableBytes() < frameLengthInt) { // frameLengthInt exist , just check bufreturn null; // 数据不够一个完整帧,半包,等待更多数据}// 6. 剥离头部并提取帧if (initialBytesToStrip > frameLengthInt) {failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);}in.skipBytes(initialBytesToStrip);// extract frameint readerIndex = in.readerIndex();int actualFrameLength = frameLengthInt - initialBytesToStrip;ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);in.readerIndex(readerIndex + actualFrameLength);// 7. 重置状态,准备解码下一帧frameLengthInt = -1; // start processing the next framereturn frame;}
// ... existing code ...

解码步骤详解

  1. 处理丢弃帧discardingTooLongFrame 是一个状态标志。如果上一个解码的帧因为超长而被标记为需要丢弃,这里会先跳过那些属于上一个超长帧的字节,直到丢弃完毕。
  2. 检查头部if (in.readableBytes() < lengthFieldEndOffset) 这是第一道半包检查。如果当前缓冲区里的数据连长度字段的结束位置都覆盖不到,说明连长度值本身都读不出来,直接返回 null 等待更多数据。
  3. 读取并计算帧长度
    • getUnadjustedFrameLength(...):这个辅助方法会根据 lengthFieldLength 的值(1, 2, 3, 4, 8),从 in 缓冲区的指定位置(actualLengthFieldOffset)读取相应字节数,并转换成一个 long 类型的长度值。注意,这个读取操作不会移动 in 的 readerIndex
    • frameLength += lengthAdjustment + lengthFieldEndOffset;:这是整个解码器最核心的计算公式。它将原始长度值、调节值和整个头部的长度(lengthFieldEndOffset)加在一起,得到从报文起始点开始计算的、完整的帧的总长度
  4. 合法性校验:对计算出的 frameLength 进行一系列检查,比如不能小于头部长度、不能超过 maxFrameLength 等。如果超过最大长度,会进入丢弃模式(见第4部分)。
  5. 检查完整帧if (in.readableBytes() < frameLengthInt) 这是第二道半包检查。此时已经知道了需要一个多长的完整帧 (frameLengthInt),如果当前缓冲区可读字节数还不够,就返回 null 等待。
  6. 剥离并提取
    • in.skipBytes(initialBytesToStrip):根据配置,跳过帧头不需要的部分。
    • extractFrame(...):提取出最终的数据帧。这个方法默认就是 in.retainedSlice(...),返回一个共享内存但独立指针的 ByteBuf
    • in.readerIndex(...):手动移动 in 的 readerIndex,越过刚刚提取的帧,为解码下一个粘包中的消息做准备。
  7. 重置状态:将 frameLengthInt 重置为-1,表示当前帧处理完毕,下一个循环将开始解码一个全新的帧。

异常处理

LengthFieldBasedFrameDecoder 对异常情况,特别是超长帧,有周全的处理。

  • failFast 模式:
    • 如果 failFast 为 true(默认),一旦通过长度字段计算出 frameLength > maxFrameLength,会立即抛出 TooLongFrameException,并进入 discardingTooLongFrame 模式,开始丢弃后续属于这个超长帧的数据。
    • 如果为 false,解码器会默默地接收并丢弃所有属于这个超长帧的数据,直到整个超长帧都接收完毕,然后才抛出异常。
  • discardingTooLongFrame 状态:这是一个内部状态机。一旦进入此状态,decode 方法的首要任务就是不断调用 in.skipBytes() 来消耗缓冲区,直到累计丢弃的字节数达到 tooLongFrameLength 为止。这确保了即使面对恶意攻击,解码器也不会因为一个超大帧而卡死,并且能正确地从下一个合法帧开始继续工作。

总结

LengthFieldBasedFrameDecoder 是一个高度工程化的解码器典范。它完美地诠释了 Netty 的设计哲学:

  • 继承与模板方法:继承 ByteToMessageDecoder 来解决通用的半包/粘包问题。
  • 配置优于编码:通过一组设计精良的参数,将复杂的协议解析逻辑转化为简单的配置问题,极大地提高了通用性和易用性。
  • 健壮性:内置了详细的参数校验和强大的异常处理机制(特别是针对超长帧的丢弃策略),保证了在生产环境中的稳定运行。
  • 高性能:通过 retainedSlice、预计算字段等方式,在保证功能强大的同时,也兼顾了性能。

理解了 LengthFieldBasedFrameDecoder,基本上就能应对绝大多数基于TCP的自定义二进制协议的解码需求。

http://www.dtcms.com/a/394070.html

相关文章:

  • 后端_HTTP 接口签名防篡改实战指南
  • 区块链论文速读 CCF A--WWW 2025(5)
  • 机器学习周报十四
  • 如何解决stun服务无法打洞建立p2p连接的问题
  • 解决项目实践中 java.lang.NoSuchMethodError:的问题
  • JavaSE-多线程(5.2)- ReentrantLock (源码解析,公平模式)
  • 2025华为杯A题B题C题D题E题F题选题建议思路数学建模研研究生数学建模思路代码文章成品
  • 【记录】Docker|Docker中git克隆私有库的安全方法
  • Web之防XSS(跨站脚本攻击)
  • 使用 AI 对 QT应用程序进行翻译
  • Windows下游戏闪退?软件崩溃?游戏环境缺失?软件运行缺少依赖?这个免费工具一键帮您自动修复(DLL文件/DirectX/运行库等问题一键搞定)
  • 【从入门到精通Spring Cloud】统一服务入口Spring Cloud Gateway
  • setfacl 命令
  • Photoshop - Photoshop 分享作品和设计
  • 【Agent 设计模式与工程化】如何做出好一个可持续发展的agent需要考虑的架构
  • 【Camera开发】疑难杂症记录
  • 如何提高自己的Java并发编程能力?
  • Polkadot - ELVES Protocol详解
  • springBoot图片本地存储
  • 蝉镜-AI数字人视频创作平台
  • Linux入门(五)
  • MySqL-day4_03(索引)
  • Vue 深度选择器(:deep)完全指北:从“能用”到“用好”
  • [Nodejs+LangChain+Ollama] 1.第一个案例
  • 设计模式2.【备忘录模式】
  • Spring Boot 入门:快速构建现代 Java 应用的利器
  • Redis 实例 CPU 飙高到 90%,如何排查和解决?
  • 中国女篮备战全运会,宫鲁鸣重点培养年轻核心
  • 【Qt】常用控件1——QWidget
  • 9.21关于大模型推理未来的思考