LengthFieldBasedFrameDecoder

1. LengthFieldBasedFrameDecoder是什么

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

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

2. 参数

java 复制代码
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
text 复制代码
表示协议中Length字段的字节是大端还是小端
2.2. maxFrameLength
text 复制代码
表示协议中Content字段的最大长度,如果超出,则抛出TooLongFrameException异常。

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

经验值:

    内网服务:10MB 上限
    公网 API:1MB 上限
    IoT 设备:64KB 上限
2.3. lengthFieldOffset
text 复制代码
表示Length字段的偏移量,即在读取一个二进制流时,跳过指定长度个字节之后的才是Length字段。
如果Length字段之前没有其他报文头,指定为0即可。
如果Length字段之前还有其他报文头,则需要跳过之前的报文头的字节数。
2.4. lengthFieldLength
text 复制代码
表示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 典型协议示例
短消息协议 1 SMS, 即时通讯
传统二进制协议 2 Modbus, 游戏协议
自定义企业协议 4 金融交易系统
超大文件传输 8 视频流分片协议
2.5. lengthAdjustment
text 复制代码
Length字段补偿值。
对于绝大部分协议来说,Length字段的值表示的都是Content字段占用的字节数。
但是也有一些协议,Length字段表示的是Length字段本身占用的字节数+Content字段占用的字节数。
由于Netty中在解析Length字段的值是,默认是认为其只表示Content字段的长度,因此解析可能会失败,所以要进行补偿。

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

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

3. 案例

3.1.1. 公共代码
java 复制代码
/**
 * 执行长度字段解码器测试,并返回解码后的原始字节数组
 *
 * @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. 协议
text 复制代码
 编码前 (16 bytes)                     编码后 (16 bytes)
 +------------+----------------+      +------------+----------------+
 |   Length   | Actual Content |----->|   Length   | Actual Content |
 | 0x0000000C | "Hello, World" |      | 0x0000000C | "Hello, World" |
 +------------+----------------+      +------------+----------------+
3.2.2. 参数设置
text 复制代码
lengthFieldOffset = 0 //因为报文以Length字段开始,不需要跳过任何字节,所以offset为0
lengthFieldLength = 4 //因为我们规定Length字段占用字节数为2,所以这个字段值传入的是2
lengthAdjustment = 0 //这里Length字段值不需要补偿,因此设置为0
initialBytesToStrip = 0 //不跳过初始字节,意味着解码后的ByteBuf中,包含Length+Content所有内容
3.2.3. 代码示例
java 复制代码
@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. 协议
text 复制代码
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. 参数设置
text 复制代码
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. 代码示例
java 复制代码
@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. 协议
text 复制代码
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. 参数设置
text 复制代码
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. 代码示例
java 复制代码
@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:如何实现可靠的消息分割?

相关推荐
砍材农夫14 小时前
物联网 基于netty核心实战-握手与认证
java·后端·物联网·struts·netty
砍材农夫4 天前
物联网 基于netty构建mqtt协议规范(遗嘱与保留消息)
java·开发语言·物联网·netty
砍材农夫6 天前
物联网 基于netty构建mqtt协议规范(主题通配符订阅)
java·前端·javascript·物联网·netty
砍材农夫8 天前
物联网 基于netty构建mqtt协议规范(发布/订阅模式)
java·开发语言·物联网·netty
砍材农夫9 天前
物联网 基于netty构建mqtt服务协议支持
java·物联网·struts·netty
devilnumber9 天前
Netty 核心重点精讲(面试 + 工作 精简背诵版)
面试·netty
砍材农夫10 天前
物联网 基于netty心跳和ack机制
java·物联网·netty