聊一聊netty中的解码器ByteToMessageDecoder-LengthFieldBasedFrameDecoder

1 阅读4分钟

LengthFieldBasedFrameDecoder 基于长度字段的帧解码器

核心参数理解

只要我们的协议格式是:“长度 + 内容”,它都能能完美解决粘包拆包。有几个核心的参数需要了解下:

image.png

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)。写下来要读取多少字节呢? 公式是:

实际待读字节数=VAL+lengthAdjustment实际待读字节数 = VAL + lengthAdjustment

默认情况下lengthAdjustment = 0 。也就是说netty读取完长度域之后,等到值=100。说明后面100个字节都是具体的数据。那么netty会继续读取后面的100个字节。这样就能读取到完整的一帧数据了。

但有时候协议不一定是长度后面紧跟着数据。比如:

[长度域: 4字节] [命令类型: 2字节] [校验码: 1字节] [数据内容]

如果“长度域”里的数值仅仅指的是 [数据内容] 的长度(假设为 100100):那我们就需要设置 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; 更新计数。