LengthFieldBasedFrameDecoder 基于长度字段的帧解码器
核心参数理解
只要我们的协议格式是:“长度 + 内容”,它都能能完美解决粘包和拆包。有几个核心的参数需要了解下:
① maxFrameLength(最大帧长度):
解码器支持的最大帧长度。
② lengthFieldOffset(长度域偏移量):
表示长度字段从哪个字节开始。
③ lengthFieldLength(长度域占用的字节数):长度字段本身有多长,目前支持1,2,4,8个字节
getUnadjustedFrameLength方法就是从buf中的指定偏移量读取length个字节,你会发现length只能支持1,2,4,8。意思就是最多用8个字节的long来存储消息的长度。
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;
}
④ lengthAdjustment(长度修正值):
netty在读取完长度域上的数值之后,(我们叫它 VAL)。写下来要读取多少字节呢? 公式是:
默认情况下lengthAdjustment = 0 。也就是说netty读取完长度域之后,等到值=100。说明后面100个字节都是具体的数据。那么netty会继续读取后面的100个字节。这样就能读取到完整的一帧数据了。
但有时候协议不一定是长度后面紧跟着数据。比如:
[长度域: 4字节] [命令类型: 2字节] [校验码: 1字节] [数据内容]
如果“长度域”里的数值仅仅指的是 [数据内容] 的长度(假设为 ):那我们就需要设置 lengthAdjustment = 3,把后面的3个字节补回来。
⑤ initialBytesToStrip(跳过字节数):解出来的结果(传递给下一个 Handler 的 ByteBuf),要去掉前面的多少字节?
假设我们有一个协议,它的结构如下: [ 4字节长度域 ] [ 2字节版本号 ] [ 真实数据内容 ]
如果我们后面的业务 Handler 只关心“数据内容”,不关心长度和版本号,那就填 6(4字节长度 + 2字节版本)。
对应源码是:
in.skipBytes(initialBytesToStrip);
// extract frame
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
跳过initialBytesToStrip个字节,此时readerIndex指针向后移动对应的字节。然后就截取出剩余部分的ByteBuf。
丢弃模式
我们在创建解码器的时候,设置了maxFrameLength 用来指定最大的服务包大小。比如服务器设置 maxFrameLength = 500。如果此时服务器端在解析长度域字段,解析出来的数据长度,长度域写着 1000。
当上述场景发生的时候,意味着传递过来的是一个非法包,要进行丢弃。整体逻辑如下:
private void exceededFrameLength(ByteBuf in, long frameLength) {
long discard = frameLength - in.readableBytes();
tooLongFrameLength = frameLength;
if (discard < 0) {
// buffer contains more bytes then the frameLength so we can discard all now
in.skipBytes((int) frameLength);
} else {
// Enter the discard mode and discard everything received so far.
discardingTooLongFrame = true;
bytesToDiscard = discard;
in.skipBytes(in.readableBytes());
}
failIfNecessary(true);
}
这里面有几个变量需要说明:
long discard = frameLength - in.readableBytes();
discard代表还有多少字节要丢弃,framelengh是协议解析出来的数据长度,如果这个数字超过了maxFrameLength的限制,就要把frameLength字节全部丢弃。 in.readableBytes();当前缓冲区里已经收到的字节数。他俩的差值就表示,还要继续丢弃多少字节才能把这个非法包丢弃。
接下来逻辑分支判断:
discard < 0: 说明当前缓冲区里的数据已经够了,比如 frameLength = 1000 ,但是本次缓冲区in.readableBytes() = 2000. 那么就直接把1000扔掉就好了:in.skipBytes((int) frameLength)
否则进入else分支:
比如 frameLength = 1000 但是缓冲区in.readableBytes() = 200。也就是说还要继续丢弃800字节。所以
discardingTooLongFrame = true; —— 立个 Flag。表示进入丢弃模式
bytesToDiscard = discard; —— 记个账(还得再扔约 800字节)。
in.skipBytes(in.readableBytes()); —— 清空当前。把这 200字节直接扔了。
等到下一次decode方法触发时候,就会上来就会判断是否进入丢弃模式:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (discardingTooLongFrame) {
discardingTooLongFrame(in);
}
if (in.readableBytes() < lengthFieldEndOffset) {
return null;
}
// 省略其他代码
....
if (discardingTooLongFrame): 如果是丢弃模式 就调用 discardingTooLongFrame(in);继续丢弃
private void discardingTooLongFrame(ByteBuf in) {
long bytesToDiscard = this.bytesToDiscard;
int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
in.skipBytes(localBytesToDiscard);
bytesToDiscard -= localBytesToDiscard;
this.bytesToDiscard = bytesToDiscard;
failIfNecessary(false);
}
逻辑也是相对好理解:
1.long bytesToDiscard = this.bytesToDiscard; 先记录下还有多少字节要忽略
2.int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());本次要忽略的字节数,取当前缓冲区和bytesToDiscard的最小值。也就是看缓冲区够不够,不够的话有多少就忽略多少。
3.in.skipBytes(localBytesToDiscard); 直接忽略,跳过localBytesToDiscard
4.bytesToDiscard -= localBytesToDiscard; 更新计数。