Netty提供的粘包拆包解决方案(四)

本文详细介绍了Netty中的LengthFieldBasedFrameDecoder,用于处理基于长度字段解码的网络协议。讲解了lengthFieldOffset、lengthFieldLength、lengthAdjustment和initialBytesToStrip等关键参数的作用,并通过实例展示了如何配置和使用该解码器,以适应不同的数据帧结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021SC@SDUSC


LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder:基于长度字段的解码器。在发送的数据中,使用一个字段表示数据的长度,当接收方接收到数据后,先读出该长度字段,从而知道发送的数据有多长,根据这个长度对数据进行解码。

LengthFieldBasedFrameDecoder中定义的成员变量如下:

// 长度字段的偏移量
private final int lengthFieldOffset;
// 长度字段的长度
private final int lengthFieldLength;
// 长度调整值
private final int lengthAdjustment;
// 跳过的初始字节数
private final int initialBytesToStrip;
// 长度字段结束的偏移量,通过计算得到:lengthFieldOffset + lengthFieldLength
private final int lengthFieldEndOffset;
// 是否立即抛出失败信息,true表示立即
private final boolean failFast;
// 是否处于丢弃模式,true表示处于丢弃模式
private boolean discardingTooLongFrame;
// 需要丢弃的长度
private long tooLongFrameLength;
// 累计丢弃的字节数
private long bytesToDiscard;

下面解释一下各个变量的含义和作用:

(1)lengthFieldOffset即为表示长度的字段在整个数据中的位置

(2)lengthFieldLength为表示长度的字段所占用的字节数

举个例子:
在这里插入图片描述
长度字段位于第 3 个位置,即 lengthFieldOffset = 2(从0开始计数),并且长度字段自己占一个字节,其值为:0x05,十进制为 5,也就是真正的数据的长度是 5 个字节,从长度字段往后 5 个字节就是数据的内容

(3)initialBytesToStrip 字段用来表示在解码时跳过多少个字节的数据(有时希望解码出来的数据,不包含换行符或者分隔符,因此需要跳过一定的字节数后再解码数据)

(4)发送的数据中包含消息头消息体,在有些协议中直接使用长度字段来表示消息体的长度;而有些协议中,用长度字段来表示整个数据的长度,即:消息头的长度 + 消息体的长度,在这种情况下,数据的接收方在接收到消息后,怎么得到消息体的长度呢?需要另外一个字段:lengthAdjustment ,这个字段表示长度的调整值,即如果长度表示的是整个消息的长度,那么将 lengthAdjustment 设置为负数,用整个数据的长度加上这个负数,那就是消息体的长度

示例

在 LengthFieldBasedFrameDecoder 的源码中,官方给出了很多示例来解释上面 4 个重要属性的含义:

1、长度字段位于数据的第一个位置,后面是消息体,长度字段占用两个字节(0x000C 是一个 16 进制数,表示的是十进制的 12,占用两个字节)。解码之后的数据如下:

lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment  = 0
initialBytesToStrip = 0
BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+

2、长度字段位于数据的第一个位置,后面是消息体,长度字段占用两个字节,且 initialBytesToStrip = 2,即在解码时需要跳过两个字节,所以解码之后的数据不包含 0x000C 。解码之后的数据如下:

lengthFieldOffset   = 0
lengthFieldLength   = 2
lengthAdjustment    = 0
initialBytesToStrip = 2
BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
+--------+----------------+      +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
+--------+----------------+      +----------------+

3、使用长度字段表示整个数据的长度(消息头+消息体),长度值 0x000E 的十进制值是 14,包含了 2 个字节的长度字段,因此令 lengthAdjustment 为-2,消息体的长度是 14+(-2) =12 个字节。解码之后的数据如下:

lengthFieldOffset   =  0
lengthFieldLength   =  2
lengthAdjustment = -2
initialBytesToStrip =  0
BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+

4、在某些场景下,在发送数据时,业务要求上可能需要在长度字段的前面添加一些请求头内容,例如,在数据的最前面添加了两个字节长度的消息头(0xCAFE),这样长度字段就不位于第一个位置了,因此需要令 lengthFieldOffset=2 来标识长度字段的在数据中的偏移量是 2。

lengthFieldOffset = 2
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" |
+----------+----------+----------------+      +----------+----------+----------------+

5、数据中包含了消息头(0xCAFE),但是消息头不位于第一个位置,而是位于长度字段和消息体的中间。长度字段的含义是从长度字段往后数多少个长度的字节,这段数据就是消息体的内容。由于此时在中间夹杂了请求头,再这样计算消息体就不对了,因此就需要使用长度调整值,令 lengthAdjustment=2。

lengthFieldOffset   = 0
lengthFieldLength   = 3
lengthAdjustment    = 2
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" |
+----------+----------+----------------+      +----------+----------+----------------+

6、在某些场景下,有多个请求头,而且长度字段处于这些请求头的中间,例如,有两个请求头 HDR1 和 HDR2,长度字段处于这两个请求头中间,这时就需要使用 lengthFieldOffset 来标识长度字段位于第几个字节处。由于长度字段后面还有一个请求头,因此需要使用 lengthAdjustment 来进行长度的调整。最后,由于 initialBytesToStrip 等于 3,这表示解码时跳过前面 3 个字节,因此解码后的数据不包含 HDR1 和长度字段。解码之后的数据如下:

lengthFieldOffset   = 1
lengthFieldLength   = 2
lengthAdjustment    = 1
initialBytesToStrip = 3
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、仍然是数据中包含多个请求头,但是长度字段表示的长度是整个数据的长度(0x0010 的值用十进制表示就是 16),这时就需要设置长度调整值为负数了,令 lengthAdjustment=-3,那么消息体+HDR2 的长度就是 13 。

lengthFieldOffset   =  1
lengthFieldLength   =  2
lengthAdjustment    = -3
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" |
+------+--------+------+----------------+      +------+----------------+

源码分析

与前面介绍的解码器一样,LengthFieldBasedFrameDecoder 继承了抽象类 ByteToMessageDecoder,因此会重写抽象方法 decode():

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    // 调用重载的decode()方法解码
    Object decoded = decode(ctx, in);
    // 能成功解码出对象,就放进out中
    if (decoded != null) {
        out.add(decoded);
    }
}

核心逻辑在重载的 decode(ctx, in) 方法中:

protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    // 判断是否处于丢弃模式
    if (discardingTooLongFrame) {
        discardingTooLongFrame(in);
    }

    // 可读字节数小于长度字段的结束索引,此时无法解码出数据
    //  lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength
    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }

    // 获取到长度字段的位置
    int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;

    // 根据长度字段的位置,以及长度字段的长度,计算出长度
    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

    // 计算出的长度值小于0,不合法,解码失败
    if (frameLength < 0) {
        failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
    }

    // 根据lengthAdjustment进行长度调整
    frameLength += lengthAdjustment + lengthFieldEndOffset;

    if (frameLength < lengthFieldEndOffset) {
        failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
    }

    // 超过最大长度
    if (frameLength > maxFrameLength) {
        exceededFrameLength(in, frameLength);
        return null;
    }

    // never overflows because it's less than maxFrameLength
    int frameLengthInt = (int) frameLength;
    if (in.readableBytes() < frameLengthInt) {
        return null;
    }
    // 跳过的字节数大于长度,说明数据不合法,无法解码出数据
    if (initialBytesToStrip > frameLengthInt) {
        failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
    }
    // 跳过指定的字节数
    in.skipBytes(initialBytesToStrip);

    // extract frame
    // 解码,即根据索引范围,从字节数组中,读取出数据
    int readerIndex = in.readerIndex();
    int actualFrameLength = frameLengthInt - initialBytesToStrip;
    ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
    // 移动读指针
    in.readerIndex(readerIndex + actualFrameLength);
    return frame;
}

在计算数据的长度时(getUnadjustedFrameLength方法),需要根据长度字段的位置,以及长度字段的长度来进行计算,源码如下:

protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
    buf = buf.order(order);
    long frameLength;
    switch (length) {
    case 1:
        frameLength = buf.getUnsignedByte(offset);
        break;
    case 2:
        frameLength = buf.getUnsignedShort(offset);
        break;
    case 3:
        frameLength = buf.getUnsignedMedium(offset);
        break;
    case 4:
        frameLength = buf.getUnsignedInt(offset);
        break;
    case 8:
        frameLength = buf.getLong(offset);
        break;
    default:
        throw new DecoderException(
                "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
    }
    return frameLength;
}

从代码中可以看出,长度字段的值只能是 1、2、3、4、8,如果是其他值就会抛出异常。(如果想支持其他值,可以通过继承来重写这个方法)

关于Netty提供的粘包拆包解决方案,就介绍到这里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值