本篇博文是《从0到1学习 Netty》中进阶系列的第二篇博文,主要内容是通过不同的应用案例来了解 LengthFieldBasedFrameDecoder 是如何处理不同的消息,实现自动分割,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;
LengthFieldBasedFrameDecoder
是 Netty 中的一个解码器,用于处理粘包和半包情况。它能根据指定的长度字段解析数据帧,将输入的字节流分割成一系列固定大小的帧 Frames
,并且每个帧的大小可以根据帧头信息中指定的长度进行动态调整。通过这种方式,LengthFieldBasedFrameDecoder
能够自动地识别和处理 TCP 协议中存在的粘包和半包情况。
使用 LengthFieldBasedFrameDecoder
需要指定几个参数,包括要解码的最大数据包长度、长度域的偏移量、长度域所占用的字节数等。在解码过程中,解码器会读取指定位置的长度域,并计算出数据包的实际大小,然后从输入流中截取相应长度的字节作为一个完整的数据包进行处理。
LengthFieldBasedFrameDecoder
的构造器代码如下所示:
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip) {
this(
maxFrameLength,
lengthFieldOffset, lengthFieldLength, lengthAdjustment,
initialBytesToStrip, true);
}
参数解析:
maxFrameLength
:最大允许的帧长度,即字节数组的最大长度,包括附加信息、长度表示等内容。如果帧的长度大于此值,将抛出 TooLongFrameException
异常。lengthFieldOffset
:长度字段在字节数组中的偏移量。lengthFieldLength
:长度字段的字节数。lengthAdjustment
:长度字段值需要调整的值。例如,如果长度字段表示的是整个字节数组的长度,但是在传输过程中还包含了一些其他的信息,那么就需要将长度字段的值减去这些额外信息的长度。initialBytesToStrip
:解码器在返回帧之前应该跳过的字节数。例如,如果帧包含了长度字段本身的字节,那么这些字节就需要被跳过。接下来,博主将讲解 LengthFieldBasedFrameDecoder
源码中列举的一些例子,并结合应用案例进行讲解,为了方便演示,将使用 EmbeddedChannel
函数进行测试。
从0开始即为长度字段,长度字段的长度为两个字节,0x000C
就是后面 HELLO, WORLD
的长度表示。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
这个例子中,lengthFieldLength = 2
表示长度字段所占的字节数为2,长度字段的值为12(0x0C),它表示 HELLO, WORLD
的长度。默认情况下,解码器假定长度字段表示紧随长度字段后面的字节数量。因此,可以使用简单的参数组合对其进行解码。
测试代码:
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(new byte[]{0x00, 0x0c});
buffer.writeBytes("HELLO, WORLD".getBytes());
channel.writeInbound(buffer);
运行结果:
从0开始即为长度字段,长度字段的长度为两个字节,但是读取时从第3个字节开始读取,即跳过长度字段,直接读取内容 HELLO, WORLD
。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
如果需要通过调用 ByteBuf.readableBytes()
获取内容长度,那么可以通过指定 initialBytesToStrip
来剥离长度字段。在这个例子中,指定了 initialBytesToStrip = 2
,这与长度字段的长度相同,可以剥离前两个字节。
测试代码与例一相同,运行结果:
从0开始即为长度字段,长度字段的长度为两个字节,0x000E
表示长度字段的长度与内容 HELLO, WORLD
的长度总和,即 2 + 12 = 14 = 0x0E。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2 (= the length of the Length field)
initialBytesToStrip = 0
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
在大多数情况下,长度字段仅表示消息正文的长度,就像前面的例子所示。然而,在某些协议中,长度字段表示整个消息(包括消息头)的长度。在这种情况下,我们需要指定一个非零的 lengthAdjustment
参数来进行修正。由于这个例子消息中的长度值 0x0E
比正文长度大2,所以我们要指定-2作为 lengthAdjustment
参数来进行补偿。
测试代码:
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(new byte[]{0x00, 0x0e});
buffer.writeBytes("HELLO, WORLD".getBytes());
channel.writeInbound(buffer);
运行结果:
长度字段前面还有两个字节的其他内容 Header 1 (0xCAFE)
,第3个字节开始才是长度字段,长度字段为3个字节,并且 Header1
中有附加信息,读取长度字段时需要跳过这些附加信息来获取长度。
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
这个例子是第一个示例的简单变体。在消息前面添加了一个额外的标头值。lengthAdjustment
再次为零,因为解码器始终考虑到在帧长度计算期间将预先添加的数据的长度。
测试代码:
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeByte(0xCA);
buffer.writeByte(0xFE);
buffer.writeBytes(new byte[]{0x00, 0x00, 0x0c});
buffer.writeBytes("HELLO, WORLD".getBytes());
channel.writeInbound(buffer);
运行结果:
从0开始即为长度字段,长度字段的长度为3个字节,长度字段之后还有两个字节的其他内容 0xCAFE
,0x00000C
表示的是 lengthAdjustment
之后开始的数据的长度,即 HELLO, WORLD
,不包括 0xCAFE
。
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
这个例子是一个高级示例,展示了在长度字段和消息体之间存在额外标头的情况。这里必须指定一个正的 lengthAdjustment
值,以便解码器将额外的标头计入帧长度的计算中。
测试代码:
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(new byte[]{0x00, 0x00, 0x0c});
buffer.writeByte(0xCA);
buffer.writeByte(0xFE);
buffer.writeBytes("HELLO, WORLD".getBytes());
channel.writeInbound(buffer);
运行结果:
长度字段前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时将会忽略3个字节,即 HDR1
+ LEN
。
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
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
,而额外标头影响 lengthAdjustment
。我们还指定了非零的 initialBytesToStrip
以从帧中剥离长度字段和前置标头。如果不想剥离前置标头,则可以将 initialBytesToSkip
指定为0。
测试代码:
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeByte(0xCA);
buffer.writeBytes(new byte[]{0x00, 0x0c});
buffer.writeByte(0xFE);
buffer.writeBytes("HELLO, WORLD".getBytes());
channel.writeInbound(buffer);
运行结果:
当 initialBytesToSkip = 0
时,运行结果如下所示:
如果 initialBytesToSkip = 4
时,0xfe
也将不再显示。
总之,通过本文对 LengthFieldBasedFrameDecoder
的深入解析,我们了解了它的工作原理以及如何实现可靠的消息分割。LengthFieldBasedFrameDecoder
可以根据消息长度对网络流进行自动切割,并将每个消息的内容分别处理,从而使得处理网络数据变得更加方便和高效。但是,开发者在使用 LengthFieldBasedFrameDecoder
时需要仔细考虑各种情况,保证其正确性和健壮性。
以上就是 浅谈 LengthFieldBasedFrameDecoder:如何实现可靠的消息分割? 的所有内容了,希望本篇博文对大家有所帮助!
参考:
📝 上篇精讲:「优化进阶」(一)粘包半包问题及解决方案 💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注,创作不易,请多多支持; 👍 公众号:sidiot的技术驿站; 🔥 系列专栏:探索 Netty:源码解析与应用案例分享