LengthFieldBasedFrameDecoder

1. LengthFieldBasedFrameDecoder是什么

LengthFieldBasedFrameDecoder是 Netty 中的一个解码器,用于处理粘包和拆包情况。
它能根据指定的长度字段解析数据帧,将输入的字节流分割成一系列固定大小的帧 Frames,并且每个帧的大小可以根据帧头信息中指定的长度进行动态调整。

通过这种方式,LengthFieldBasedFrameDecoder 能够自动地识别和处理 TCP 协议中存在的粘包和拆包情况。

2. 参数

public LengthFieldBasedFrameDecoder(
        int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
        int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        this( ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength,
            lengthAdjustment, initialBytesToStrip, failFast);
}
 
public LengthFieldBasedFrameDecoder(
        ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
        int lengthAdjustment, int initialBytesToStrip, boolean failFast)
2.1. byteOrder
表示协议中Length字段的字节是大端还是小端
2.2. maxFrameLength
表示协议中Content字段的最大长度,如果超出,则抛出TooLongFrameException异常。

maxFrameLength不仅是性能参数,更是安全防线。
设得太小会导致合法数据被拒绝,太大则可能引发 OOM。

经验值:

    内网服务:10MB 上限
    公网 API:1MB 上限
    IoT 设备:64KB 上限
2.3. lengthFieldOffset
表示Length字段的偏移量,即在读取一个二进制流时,跳过指定长度个字节之后的才是Length字段。
如果Length字段之前没有其他报文头,指定为0即可。
如果Length字段之前还有其他报文头,则需要跳过之前的报文头的字节数。
2.4. lengthFieldLength
表示Length字段占用的字节数。
指定为多少,需要看实际要求,不同的字节数,限制了Content字段的最大长度。

    如果lengthFieldLength是1个字节,那么限制为128bytes;
    如果lengthFieldLength是2个字节,那么限制为32767bytes(约等于32K);
    如果lengthFieldLength是3个字节,那么限制为8388608bytes(约等于8M);
    如果lengthFieldLength是4个字节,那么限制为2147483648bytes(约等于2G)。

lengthFieldLength与maxFrameLength并不冲突。
例如我们现在希望限制报文Content字段的最大长度为32M。
显然,我们看到了上面的四种情况,没有任何一个值,能刚好限制Content字段最大值刚好为32M。
那么我们只能指定lengthFieldLength为4个字节,其最大限制2G是大于32M的,因此肯定能支持。
但是如果Content字段长度真的是2G, server 端接收到这么大的数据,如果都放在内存中,很容易造成内存溢出。

为了避免这种情况,我们就可以指定maxFrameLength字段,来精确的指定Content部分最大字节数,显然,其值应该小于lengthFieldLength指定的字节数最大可以表示的值。
场景lengthFieldLength典型协议示例
短消息协议1SMS, 即时通讯
传统二进制协议2Modbus, 游戏协议
自定义企业协议4金融交易系统
超大文件传输8视频流分片协议
2.5. lengthAdjustment
Length字段补偿值。
对于绝大部分协议来说,Length字段的值表示的都是Content字段占用的字节数。
但是也有一些协议,Length字段表示的是Length字段本身占用的字节数+Content字段占用的字节数。
由于Netty中在解析Length字段的值是,默认是认为其只表示Content字段的长度,因此解析可能会失败,所以要进行补偿。

主要用于处理Length字段前后还有其他报文头的情况。

当 lengthAdjustment 计算错误时,最常见的症状是解析出的数据要么缺少尾部,要么包含了下个包的头部。
2.6. initialBytesToStrip:
解码后跳过的初始字节数,表示获取完一个完整的数据报文之后,忽略前面指定个数的字节。
例如报文头只有Length字段,占用2个字节,在解码后,我们可以指定跳过2个字节。
这样封装到ByteBuf中的内容,就只包含Content字段的字节内容不包含Length字段占用的字节。
2.7. failFast:
如果为true,则表示读取到Length字段时,如果其值超过maxFrameLength,就立马抛出一个 TooLongFrameException,
而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,
默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。

3. 案例

3.1.1. 公共代码
/**
 * 执行长度字段解码器测试,并返回解码后的原始字节数组
 *
 * @param decoder     待测试的长度字段解码器
 * @param testByteBuf 测试用的二进制报文
 * @return 解码完成后的字节数据
 */
private byte[] decodeByLengthFieldDecoder(LengthFieldBasedFrameDecoder decoder, ByteBuf testByteBuf) {
    // 固定模板逻辑
    EmbeddedChannel channel = new EmbeddedChannel(
            new LoggingHandler("【解码前 - 原始报文】", LogLevel.DEBUG),  // 1. 先打印:原始数据
            decoder,                                                     // 2. 再解码
            new LoggingHandler("【解码后 - 结果报文】", LogLevel.DEBUG)   // 3. 后打印:解码结果
    );
    //EmbeddedChannel channel = new EmbeddedChannel(decoder, new LoggingHandler(LogLevel.DEBUG));

    channel.writeInbound(testByteBuf);
    ByteBuf resultBuf = channel.readInbound();


    byte[] resultBytes = new byte[resultBuf.readableBytes()];
    resultBuf.readBytes(resultBytes);

    // 资源释放
    resultBuf.release();
    channel.close();

    return resultBytes;
}
3.2. lengthAdjustment=0
3.2.1. 协议
 编码前 (16 bytes)                     编码后 (16 bytes)
 +------------+----------------+      +------------+----------------+
 |   Length   | Actual Content |----->|   Length   | Actual Content |
 | 0x0000000C | "Hello, World" |      | 0x0000000C | "Hello, World" |
 +------------+----------------+      +------------+----------------+
3.2.2. 参数设置
lengthFieldOffset = 0 //因为报文以Length字段开始,不需要跳过任何字节,所以offset为0
lengthFieldLength = 4 //因为我们规定Length字段占用字节数为2,所以这个字段值传入的是2
lengthAdjustment = 0 //这里Length字段值不需要补偿,因此设置为0
initialBytesToStrip = 0 //不跳过初始字节,意味着解码后的ByteBuf中,包含Length+Content所有内容
3.2.3. 代码示例
@Test
public void test00() {
    LengthFieldBasedFrameDecoder decoder = new LengthFieldBasedFrameDecoder(
            1024,        // maxFrameLength
            0,           // lengthFieldOffset
            4,           // lengthFieldLength
            0,           // lengthAdjustment
            0            // initialBytesToStrip
    );


    byte[] bytes = "Hello, World".getBytes(StandardCharsets.UTF_8);
    ByteBuf byteBuf = Unpooled.buffer();
    byteBuf.writeInt(bytes.length);
    byteBuf.writeBytes(bytes);

    decodeByLengthFieldDecoder(decoder, byteBuf);
}
3.3. lengthAdjustment>0
3.3.1. 协议
BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
3.3.2. 参数设置
lengthFieldOffset = 1 //跳过HDR1占用的1个字节读取Length
lengthFieldLength = 2 //Length字段占用2个字段,其值为0x000C(12),表示Content字段长度
lengthAdjustment = 1 //由于Length字段之后,还有HDR2字段,因此需要+1个字节,读取HDR2+Content的内容
initialBytesToStrip = 3 //解码后,跳过前3个字节
3.3.3. 代码示例
@Test
public void test01() {
    LengthFieldBasedFrameDecoder decoder = new LengthFieldBasedFrameDecoder(
            1024,        // maxFrameLength
            1,           // lengthFieldOffset
            2,           // lengthFieldLength
            1,           // lengthAdjustment
            3            // initialBytesToStrip
    );


    byte[] bytes = "Hello, World".getBytes(StandardCharsets.UTF_8);
    ByteBuf byteBuf = Unpooled.buffer();
    byteBuf.writeByte(0xCA);
    byteBuf.writeShort(bytes.length);
    byteBuf.writeByte(0xFE);
    byteBuf.writeBytes(bytes);

    decodeByLengthFieldDecoder(decoder, byteBuf);
}
3.4. lengthAdjustment<0
3.4.1. 协议
BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
3.4.2. 参数设置
lengthFieldOffset = 1 //跳过HDR1占用的1个字节读取Length
lengthFieldLength = 2 //Length字段占用2个字段,其值为0x0010(16),表示HDR1+Length+HDR2+Content长度
lengthAdjustment = -3 //由于Length表示的是整个报文的长度,减去HDR1+Length占用的3个字节后,读取HDR2+Content长度
initialBytesToStrip = 3 //解码后,跳过前3个字节
3.4.3. 代码示例
@Test
public void test02() {
    LengthFieldBasedFrameDecoder decoder = new LengthFieldBasedFrameDecoder(
            1024,        // maxFrameLength
            1,           // lengthFieldOffset
            2,           // lengthFieldLength
            -3,           // lengthAdjustment
            3            // initialBytesToStrip
    );


    byte[] bytes = "Hello, World".getBytes(StandardCharsets.UTF_8);
    ByteBuf byteBuf = Unpooled.buffer();
    byteBuf.writeByte(0xCA);
    //包括自己的长度
    byteBuf.writeShort(bytes.length + 4);
    byteBuf.writeByte(0xFE);
    byteBuf.writeBytes(bytes);

    decodeByLengthFieldDecoder(decoder, byteBuf);
}

参考

LengthFieldBasedFrameDecoder
当LengthFieldBasedFrameDecoder解码失败:一份完整的Netty帧解码器问题排查清单
浅谈 LengthFieldBasedFrameDecoder:如何实现可靠的消息分割?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值