netty基础教程-5、拆包黏包LengthFieldBasedFrameDecoder和LengthFieldPrepender解析_netty 粘包 lengthfieldprepender

673 阅读7分钟

LengthFieldBasedFrameDecoder的构造方法

public LengthFieldBasedFrameDecoder(
            ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast){...} 

  1. byteOrder:ByteOrder定义了写入buffer时字节的顺序
  2. maxFrameLength:框架的最大长度。, 如果帧的长度大于此值,则将抛出TooLongFrameException。
  3. lengthFieldOffset:长度字段的偏移量:即对应的长度字段在整个消息数据中得位置
  4. lengthFieldLength:长度字段的长度:例如:长度字段是int型表示的,那么这个值就是4(long型就是8)
  5. lengthAdjustment:要添加到长度字段值的补偿值
  6. initialBytesToStrip:从解码帧中去除的第一个字节数
  7. failFast:如果true,TooLongFrameException将被抛出,因为解码器注意到帧的长度将超过 maxFrameLength,无论整个帧是否已经读完。如果 false,则在读取超过maxFrameLength 的整个帧后抛出TooLongFrameException。

读取数据时:先跳到lengthFieldOffset位置,读取长度域lengthFieldLength长度的字节,获取到整个消息数据的长度L;然后继续向后读取(L+lengthAdjustment) 长度的字节,
在这里插入图片描述
最后将整个数据从开头丢弃initialBytesToStrip长度的字节,即得到最终的消息数据

注: 0x000c==>12 ; 0x000e==>14

2、具体的使用场景
(1)场景一:

消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段

参数定义:
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" |
+--------+----------------+      +--------+----------------+

读取了Length值后,再向后读取0x000C(12)个字节(即Content)

(2)场景二:

通过ByteBuf.readableBytes()方法我们可以获取当前消息的长度,所以解码后的字节缓冲区可以不携带长度字段,由于长度字段在起始位置并且长度为2,所以将initialBytesToStrip设置为2。

参数定义:
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" |
+--------+----------------+      +----------------+

解码后的字节缓冲区丢弃了长度字段(2个字节),仅仅包含消息体,对于大多数的协议,解码之后消息长度没有用处,因此可以丢弃。

(3)场景三:

在大多数情况下,length字段仅表示消息正文的长度,如前面的示例所示。 但是,在某些协议中, length字段表示整个消息的长度,包括消息头。在这种情况下,我们指定一个非零 lengthAdjustment 。 由于此示例消息中的长度值始终大于 2 ,因此我们将 -2 指定为lengthAdjustment 进行补偿。

参数定义:
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" |
+--------+----------------+      +--------+----------------+

与场景1不同的是:场景3中长度域的值为14(0x000E)

读取了Length值后,再向后读取0x000C(14+(-2)个字节)个字节(即Content)

(4)场景四:

但是由于协议的种类繁多,并不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用lengthFieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题:

参数定义:
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" |
 +----------+----------+----------------+      +----------+----------+----------------+

由于消息头1的长度为2,所以长度字段的偏移量为2;消息长度字段Length为3,所以lengthFieldLength值为3。由于长度字段仅仅标识消息体的长度,所以lengthAdjustment和initialBytesToStrip都为0。

(5)场景五:

这是一个对场景4的高级版即使用lengthAdjustment对读取的消息长度进行补偿

参数定义:
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" |
+----------+----------+----------------+      +----------+----------+----------------+

这里的Length长度为12,Header 1的长度为2,所以使用lengthAdjustment将读取的长度调整为12+2=14即可将Header 1一并读入

(6)场景六:

这个场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其它消息头字段,在这种场景下如果想忽略长度字段以及其前面的其它消息头字段,则可以通过initialBytesToStrip参数来跳过要忽略的字节长度,它的组合配置示意如下:

参数定义:
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" |
+------+--------+------+----------------+      +------+----------------+

(7)场景七:

这个应该时最复杂的场景了,让我们再举一个例子。 与前一个示例的唯一区别在于,length字段表示整个消息的长度而不是消息正文,就像第三个示例一样。 我们必须将HDR1和Length的长度计入lengthAdjustment 。 请注意,我们不需要考虑HDR2的长度因为长度字段已包含。

参数定义:
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3 (= the length of HDR1 + LEN, negative)
initialBytesToStrip = 3

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

长度:
HDR1=1
Length=2
HDR2=1
Content =12
可以看出整个消息的长度为1+2+1+12=16;

解析过程:

  1. 定位到index为1(lengthFieldOffset)的位置
  2. 读取2(lengthFieldLength)个字节,值为16
  3. 继续向后读取16+(-3)=13个字节数据,(-3就是lengthAdjustment)
  4. 最后舍弃开头的3(initialBytesToStrip)个字节,得到最终的解码数据

三、 LengthFieldBasedFrameDecoder源码解析

下面我们就来看看基于消息长度的半包解码器,首先看看入口方法:

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}

内部调用decode(ChannelHandlerContext ctx, ByteBuf in) 如果解码成功,就将其加入到输出的List out列表中。该函数较长我们还是分几部分来分析:
(1)判断discardingTooLongFrame标识,看是否需要丢弃当前可读的字节缓冲区,如果为真,则执行求其操作。

if (discardingTooLongFrame) {
    //获取需要丢弃的长度
    long bytesToDiscard = this.bytesToDiscard;
    //丢弃的长度不能超过当前缓冲区可读的字节数
    int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
    //跳过需要忽略的字节长度
    in.skipBytes(localBytesToDiscard);
    //bytesToDiscard减去已经忽略的字节长度
    bytesToDiscard -= localBytesToDiscard;
    this.bytesToDiscard = bytesToDiscard;
    failIfNecessary(false);
}

(2)对当前缓冲区中可读字节数和长度偏移量进行对比,如果小于偏移量,谁明缓冲区数据报不够,直接返回null.

//数据报内数据不够,返回null,由IO线程继续读取数据。
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}

int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

if (frameLength < 0) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "negative pre-adjustment length field: " + frameLength);
}

frameLength += lengthAdjustment + lengthFieldEndOffset;

if (frameLength < lengthFieldEndOffset) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than lengthFieldEndOffset: " + lengthFieldEndOffset);
}

再后面的具体代码就不分析了,其实核心就是:对消息进行解码,解码之后将解码后的字节数据放到一个新的ByteBuf中返回,并更新原来的消息msg对象的读写索引值。

四、LengthFieldPrepender(编码)功能说明

public LengthFieldPrepender(
            ByteOrder byteOrder, int lengthFieldLength,
            int lengthAdjustment, boolean lengthIncludesLengthFieldLength) {...}

  1. byteOrder:ByteOrder定义了写入buffer时字节的顺序
  2. lengthFieldLength:前置长度字段的长度。 仅允许1,2,3,4和8
  3. lengthAdjustment:要添加到长度字段的值的补偿值
  4. lengthIncludesLengthFieldLength:为true时,length字段的值=length字段的长度+Content的长度。为false时,length字段的值=Content的长度。

如果协议中的第一个字段为长度字段,netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中

Before Encode           After Encode
+----------------+      +--------+----------------+
| "HELLO, WORLD" |----->| 0x000C | "HELLO, WORLD" |
+----------------+      +--------+----------------+

img img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取