(本文是上一节的节选,已读可略过)
Netty中,基于长度字段的消息帧解码器LengthFieldBasedFrameDecoder
可根据数据包中的长度字段动态的解码出消息帧。一个推荐的二进制传输协议可设计为如下格式:
+----------+------+----------+------+
| 头部长度 | 头部 | 数据长度 | 数据 |
+----------+------+----------+------+
这样的协议可满足大多数场景使用,但不幸的是:很多情况下并不可以设计新的协议,往往要在老旧的协议上传输数据。由此,Netty将该解码器设计的十分通用,只要有类似的长度字段便能正确解码出消息帧。当然前提是:正确使用解码器。
没有什么是完美的,由于该解码器十分通用,所以有大量的配置变量:
private final ByteOrder byteOrder;
private final int maxFrameLength;
private final boolean failFast;
private final int lengthFieldOffset;
private final int lengthFieldLength;
private final int lengthAdjustment;
private final int initialBytesToStrip;
变量byteOrder
表示长度字段的字节序:大端或小端,默认为大端。如果对字节序有疑问,请查阅其他资料,不再赘述。maxFrameLength
和failFast
与其他解码器相同,控制最大帧长度和快速失败抛异常,注意:该解码器failFast
默认为true。
接下来将重点介绍其它四个变量:
-
lengthFieldOffset
表示长度字段偏移量即在一个数据包中长度字段的具体下标位置。标准情况,该长度字段为数据部分长度。 -
lengthFieldLength
表示长度字段的具体字节数,如一个int占4字节。该解码器支持的字节数有:1,2,3,4和8,其他则会抛出异常。另外,还需要注意的是:长度字段的结果为无符号数。 -
lengthAdjustment
是一个长度调节量,当数据包的长度字段不是数据部分长度而是总长度时,可将此值设定为头部长度,便能正确解码出包含整个数据包的结果消息帧。注意:某些情况下,该值可设定为负数。 -
initialBytesToStrip
表示需要略过的字节数,如果我们只关心数据部分而不关心头部,可将此值设定为头部长度从而丢弃头部。
下面我们使用具体的例子来说明:
- 需求1:如下待解码数据包,正确解码为消息帧,其中长度字段在最前面的2字节,数据部分为12字节的字符串"HELLO, WORLD",长度字段0x000C=12 表示数据部分长度,数据包总长度则为14字节。
解码前(14 bytes) 解码后(14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
正确配置(只列出四个值中不为0的值):
lengthFieldLength = 2;
- 需求2:需求1的数据包不变,消息帧中去除长度字段。
解码前(14 bytes) 解码后(12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
正确配置:
lengthFieldLength = 2;
initialBytesToStrip = 2;
- 需求3:需求1数据包中长度字段表示数据包总长度。
解码前(14 bytes) 解码后(14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
正确配置:
lengthFieldLength = 2;
lengthAdjustment = -2; // 调整长度字段的2字节
- 需求4:综合难度,数据包有两个头部HDR1和HDR2,长度字段以及数据部分组成,其中长度字段值表示数据包总长度。结果消息帧需要第二个头部HDR2和数据部分。请先给出答案再与标准答案比较,结果正确说明你已完全掌握了该解码器的使用。
解码前 (16 bytes) 解码后 (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
正确配置:
lengthFieldOffset = 1;
lengthFieldLength = 2;
lengthAdjustment = -3;
initialBytesToStrip = 3;
本解码器的解码过程总体上较为复杂,由于解码的代码是在while
循环里面,decode
方法return或者抛出异常时可看做一次循环结束,直到in中数据被解析完或者in的readerIndex读索引不再增加才会从while
循环跳出。使用状态的思路理解,每个return或者抛出异常看为一个状态:
- 状态1:丢弃过长帧状态,可能是用户设置了错误的帧长度或者实际帧过长。
if (discardingTooLongFrame) {
long bytesToDiscard = this.bytesToDiscard;
int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
in.skipBytes(localBytesToDiscard); // 丢弃实际的字节数
bytesToDiscard -= localBytesToDiscard;
this.bytesToDiscard = bytesToDiscard;
failIfNecessary(false);
}
变量localBytesToDiscard
取得实际需要丢弃的字节数,由于过长帧有两种情况:a.用户设置了错误的长度字段,此时in中并没有如此多的字节;b.in中确实有如此长度的帧,这个帧确实超过了设定的最大长度。bytesToDiscard
的计算是为了failIfNecessary()
确定异常的抛出,其值为0表示当次丢弃状态已经丢弃了in中的所有数据,可以对新读入in的数据进行处理;否则,还处于异常状态。
private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
if (bytesToDiscard == 0) {
long tooLongFrameLength = this.tooLongFrameLength;
this.tooLongFrameLength = 0;
// 由于已经丢弃所有数据,关闭丢弃模式
discardingTooLongFrame = false;
// 已经丢弃了所有字节,当非快速失败模式抛异常
if (!failFast || firstDetectionOfTooLongFrame) {
fail(tooLongFrameLength);
}
} else {
if (failFast && firstDetectionOfTooLongFrame) {
// 帧长度异常,快速失败模式检测到即抛异常
fail(tooLongFrameLength);
}
}
}
可见,首次检测到帧长度是一种特殊情况,在之后的一个状态进行分析。请注意该状态并不是都抛异常,还有可能进入状态2。
- 状态2:in中数据不足够组成消息帧,此时直接返回null等待更多数据到达。
if (in.readableBytes() < lengthFieldEndOffset) {
return null;
}
- 状态3:帧长度错误检测,检测长度字段为负值得帧以及加入调整长度后总长小于长度字段的帧,均抛出异常。
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
// 该方法取出长度字段的值,不再深入分析
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset,
lengthFieldLength, byteOrder);
if (frameLength < 0) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException("...");
}
frameLength += lengthAdjustment + lengthFieldEndOffset;
if (frameLength < lengthFieldEndOffset) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException("...");
}
- 状态4:帧过长,由前述可知:可能是用户设置了错误的帧长度或者实际帧过长。
if (frameLength > maxFrameLength) {
long discard = frameLength - in.readableBytes();
tooLongFrameLength = frameLength;
if (discard < 0) {
in.skipBytes((int) frameLength);
} else {
discardingTooLongFrame = true;
bytesToDiscard = discard;
in.skipBytes(in.readableBytes());
}
failIfNecessary(true);
return null;
}
变量discard<0
表示当前收到的数据足以确定是实际的帧过长,所以直接丢弃过长的帧长度;>0
表示当前in中的数据并不足以确定是用户设置了错误的帧长度,还是正确帧的后续数据字节还没有到达,但无论何种情况,将丢弃状态discardingTooLongFrame
标记设置为true,之后后续数据字节进入状态1处理。==0
时,在failIfNecessary(true)
无论如何都将抛出异常,><0
时,只有设置快速失败才会抛出异常。还需注意一点:failIfNecessary()
的参数firstDetectionOfTooLongFrame
的首次是指正确解析数据后发生的第一次发生的帧过长,可知会有很多首次。
- 状态5:正确解码出消息帧。
int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
return null; // 到达的数据还达不到帧长
}
if (initialBytesToStrip > frameLengthInt) {
in.skipBytes(frameLengthInt); // 跳过字节数错误
throw new CorruptedFrameException("...");
}
in.skipBytes(initialBytesToStrip);
// 正确解码出数据帧
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = in.slice(readerIndex, actualFrameLength).retain();
in.readerIndex(readerIndex + actualFrameLength);
return frame;
代码中混合了两个简单状态,到达的数据还达不到帧长和用户设置的忽略字节数错误。由于较为简单,故合并到一起。
至此解码框架分析完毕。可见,要正确的写出基于长度字段的解码器还是较为复杂的,如果开发时确有需求,特别要注意状态的转移。下面介绍较为简单的编码框架。