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:如何实现可靠的消息分割?