Netty:LengthFieldBasedFrameDecoder 五个参数与 lengthAdjustment 计算原理
一、先理解长度字段的三个概念
使用 LengthFieldBasedFrameDecoder
时,最容易混淆的是:长度字段本身占多少字节,以及长度字段里面存储的数值是多少。
例如协议:
[Magic 2字节][BodyLength 2字节][Body 10字节]因此,BodyLength 字段本身占 2 字节,但里面存储的数值是
10。这是两个不同的概念。
字段名称不一定能说明它存储的内容。协议可以定义 BodyLength
只记录正文长度,也可以定义 TotalLength
记录整条消息的总长度,必须以协议约定为准。
二、五个参数分别做什么?
new LengthFieldBasedFrameDecoder(
maxFrameLength,
lengthFieldOffset,
lengthFieldLength,
lengthAdjustment,
initialBytesToStrip
);1. maxFrameLength
它限制的是完整帧的最大长度,包含消息头和正文,不只是正文。
例如:
new LengthFieldBasedFrameDecoder(
1024, // 完整帧最大 1024 字节
2,
2,
0,
4
);如果消息头是 4 字节,那么正文最大可用长度还要扣除消息头所占空间。
2. lengthFieldOffset
它表示长度字段从帧的哪个字节开始,索引从 0 开始。
[Magic 2字节][BodyLength 2字节][Body]
0 1 2 3
↑
offset = 2所以 lengthFieldOffset = 2。offset
只是定位长度字段,不代表跳过这些字节。是否跳过消息头由initialBytesToStrip 决定。
3. lengthFieldLength
它表示长度字段自身占用多少字节。
例如 [Magic 2字节][BodyLength 2字节][Body],那么lengthFieldLength = 2。
不要把 lengthFieldLength 和 lengthFieldValue 混淆:
lengthFieldLength = 2:字段自身占 2 字节。lengthFieldValue = 10:字段里面存储的数值是 10。
三、lengthAdjustment:如何计算完整帧长度?
这是五个参数中最容易混淆的一个。
Netty 使用的计算公式:
frameLength = lengthFieldEndOffset
+ lengthFieldValue
+ lengthAdjustment其中:
lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength合并后:
\(FrameLength=Offset+FieldLength+LengthValue+Adjustment\)
也可以反过来计算:
\(Adjustment=实际完整帧长度-Offset-FieldLength-LengthValue\)
理解 adjustment
的关键:先看长度字段里面的数值统计了哪些字节,再看公式还需要补充或扣除哪些字节。
情况一:长度字段只记录正文长度,后面没有其他字段
协议:
[Magic 2字节][BodyLength 2字节][Body 10字节]约定 BodyLength 只记录正文长度,所以字段里面的数值是 10。
完整帧长度:
2 + 2 + 10 = 14 字节代入公式:
frameLength = 2 + 2 + 10 + 0
= 14所以 lengthAdjustment = 0。公式已经计算了长度字段之前的 2
字节、长度字段自身的 2 字节,再加上正文长度 10,刚好得到完整帧长度。
情况二:长度字段只记录正文长度,但长度字段后面还有额外字段
协议:
[Magic 2字节][BodyLength 2字节][Version 1字节][Body 10字节]约定 BodyLength 仍然只记录 Body,所以字段里面的数值是 10。
但完整帧还包含一个没有被 BodyLength 统计的 Version 字段。
完整帧长度:
2 + 2 + 1 + 10 = 15 字节代入公式:
frameLength = 2 + 2 + 10 + 1
= 15所以 lengthAdjustment = 1。这里的 +1
是补上长度字段之后、但没有被长度值统计的 Version 字段。
情况三:长度字段记录 Version + Body
协议仍然是:
[Magic 2字节][Length 2字节][Version 1字节][Body 10字节]但这次 Length 存储的是:
Version 1字节 + Body 10字节 = 11完整帧长度:
2 + 2 + 11 = 15 字节代入公式:
frameLength = 2 + 2 + 11 + 0
= 15所以 lengthAdjustment = 0。因为长度值 11 已经包含了 Version 和
Body,不需要再额外补 1 字节。
情况四:长度字段记录整条消息的总长度
协议:
[Magic 2字节][TotalLength 2字节][Body 10字节]约定 TotalLength 存储整条消息的总长度,因此字段里面的数值是 14。
注意:这是另一种协议设计。它与前面 BodyLength = 10
的协议结构相同,但长度字段存储的含义不同。
Netty 公式中的前两项已经计算了 Magic 2 字节和 TotalLength 字段自身 2
字节。而 TotalLength = 14 已经包含这 4
字节。如果直接相加,就重复计算了消息头。
所以需要减去 4:
frameLength = 2 + 2 + 14 - 4
= 14因此 lengthAdjustment = -4。
-4 的原因:Length 字段存储的 14 已经包含前面的 4
字节消息头,而公式又计算了一次,所以要减去 4。
四种情况对比
不要死记“正文长度就一定是
0”或“总长度就一定是负数”。应使用反推公式,根据实际协议字段计算:
adjustment = 实际完整帧长度
- lengthFieldOffset
- lengthFieldLength
- lengthFieldValue四、initialBytesToStrip:完整帧提取后跳过多少字节?
这个参数与 lengthAdjustment 不同。
lengthAdjustment:参与计算完整帧长度。initialBytesToStrip:完整帧提取出来后,决定从帧开头跳过多少字节,再交给下一个
Handler。
它不参与帧长度计算,也不会改变消息边界。
继续使用协议:
[Magic 2字节][BodyLength 2字节][Version 1字节][Body 10字节]完整消息头共 5 字节。
如果下一个 Handler 还需要解析 Magic、长度和 Version,就设置为
0;如果只需要正文,就设置为 5。
例如:
new LengthFieldBasedFrameDecoder(
1024,
2,
2,
0,
5
);这表示:按长度字段完成组帧后,跳过前 5 字节消息头,只把 Body 交给下一个
Handler。
五、根据协议配置参数:完整示例
示例一:Length 只记录 Body 长度
协议:
[Magic 2][BodyLength 2][Body N]配置:
pipeline.addLast(
new LengthFieldBasedFrameDecoder(
1024, // 最大完整帧长度
2, // 长度字段从索引 2 开始
2, // 长度字段占 2 字节
0, // BodyLength 只记录正文,后面没有额外字段
4 // 跳过 Magic 和 BodyLength,只保留 Body
)
);示例二:Length 记录整条消息总长度
协议:
[Magic 2][TotalLength 2][Body N]假设 TotalLength 包含整个消息头和正文。
配置:
pipeline.addLast(
new LengthFieldBasedFrameDecoder(
1024,
2,
2,
-4, // TotalLength 已包含前面 4 字节,扣除重复计算
4 // 跳过 4 字节消息头,只保留 Body
)
);两种协议的物理结构相同,但长度字段的定义不同,因此 lengthAdjustment
不同。
六、常见问题与答案
1. BodyLength 是字段长度吗?
BodyLength 是字段名称。字段自身占多少字节由 lengthFieldLength
表示;字段里面存储的数值由 lengthFieldValue 表示。例如字段占 2
字节,里面存储的正文长度是 10。
2. lengthFieldOffset 是不是跳过消息头的长度?
不是。它只表示长度字段从帧起始位置的哪个索引开始。真正决定输出时跳过多少字节的是initialBytesToStrip。
3. 为什么 BodyLength = 10 时 adjustment 可以是 0?
例如 [Magic 2][BodyLength 2][Body 10],公式中的 2 + 2 + 10
已经得到完整帧长度 14,不需要调整。
4. 为什么 TotalLength = 14 时 adjustment 是 -4?
因为 TotalLength 的 14 已经包含 Magic 和 Length 共 4
字节,而公式又计算了这 4 字节,所以要减去 4。
5. 如果长度字段记录 Version + Body,adjustment 是多少?
如果协议是 [Magic 2][Length 2][Version 1][Body 10],且 Length =
11,那么公式 2 + 2 + 11 已经得到完整帧长度 15,因此 adjustment = 0。
6. initialBytesToStrip 会影响完整帧长度吗?
不会。它只影响组帧后交给下一个 Handler
的数据内容,不参与完整帧长度计算。
7. maxFrameLength 限制的是正文长度吗?
不是。它限制完整帧长度,包含消息头和正文。
七、重点总结
lengthFieldOffset:长度字段从帧起始位置的哪个字节开始。lengthFieldLength:长度字段自身占多少字节。lengthFieldValue:长度字段内部存储的数值,具体含义由协议定义。lengthAdjustment:修正完整帧长度,计算公式为offset + fieldLength + fieldValue + adjustment。initialBytesToStrip:完整帧提取后,从帧开头跳过多少字节。maxFrameLength:限制完整帧的最大长度,包括消息头和正文。BodyLength = 10与完整帧长度为 14
并不矛盾:前者是正文长度,后者是消息头加正文的总长度。TotalLength = 14时,若长度字段结束位置为 4,则 adjustment 为-4,因为长度值已经包含了前面的 4 字节。计算参数时先画协议结构,标出字段位置和长度,再确认长度字段统计范围,最后代入公式,不要死记固定数值。
