Netty提供的粘包拆包解决方案(二)

2021SC@SDUSC


LineBasedFrameDecoder

LineBasedFrameDecoder:通过换行符,即\n或者\r\n对数据进行处理,每当读到行分隔符(\n 或者\r\n)的时候,就解析出一个数据对象。

对于数据的编码,即在每个数据包最后添加换行符或者指定分割符的部分也需要用户自行进行处理

LineBaseFrameDecoder定义了如下几个成员变量:

// 解码的最大长度
private final int maxLength;

// 当通过换行符读出来的数据超过maxLength规定的长度后,是否立即抛出异常。true表示立即
private final boolean failFast;

// 解析数据时是否跳过换行符\r\n或者\n,true表示跳过,false表示不跳过
private final boolean stripDelimiter;

// 当超过maxLength的长度后,就不能解码,需要丢弃数据,此时会将discarding设置为true,表示丢弃数据
private boolean discarding;

// 记录已经丢弃了对少字节的数据
private int discardedBytes;

// 最后一次扫描的位置
private int offset;

在解码数据时,会先找到换行符的位置,然后计算从当前读指针的位置到换行符位置的长度,如果这个长度大于 maxLength,那么就表示这是一个无效数据,不能进行解码,需要丢弃,同时需要将 discarding 属性设置为 true。另外,如果要丢弃数据,什么时候丢弃数据呢?是立即丢弃数据?还是等到下次读数据时丢弃呢?可以通过 failFast 属性来控制,true 表示立即丢弃。

对于解码之后的数据是否保留换行符\r\n 或者\n,可以通过 stripDelimiter 属性控制,true 表示跳过换行符,解码出来的数据不保留换行符。

换行符解码器继承了抽象类解码器 ByteToMessageDecoder,并重写了抽象方法 decode()。

下面分析一下源码:

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    // 解码
    Object decoded = decode(ctx, in);
    // 能解码出来数据,就将解码出来的结果存放到out中
    if (decoded != null) {
        out.add(decoded);
    }
}

核心是另一个重载的 decode() 方法,这个方法的整体逻辑如下:

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
    // 返回\n或者\r\n的下标位置
    final int eol = findEndOfLine(buffer);
    if (!discarding) {
        // 找到换行符
        if (eol >= 0) {
            // 解码...
            return frame;
        } else {
            // ...
            return null;
        }
    } else {
        // 找到了换行符
        if (eol >= 0) {
            // ...
        } else {
            // ...
        }
        return null;
    }
}

首先会寻找出\n 或者\r\n 的下标位置,这个查找过程比较简单,就是遍历字节数组。如果没找到换行符,那么就会返回一个小于 0 的数值,如果找到了换行符,就会返回一个大于等于 0 的数。其中,如果找到的是\n,那么返回的是\n 的索引值;如果找到的是\r\n,那么返回的是\r 的索引值。

接着剩下的逻辑,就可以分为两部分了:是否处于丢弃模式。第一部分就是不处于丢弃模式下执行的逻辑,即:discarding = false,那么 !discarding 就为 true;第二部分就是处于丢弃模式下执行的逻辑。对于这两部分,每一部分又可以分为两种情况:找到了换行符 和 没有找到换行符 ,因此这里实际上是四种逻辑。当第一次调用解码器的解码方法时,此时 discarding = false ,即处于非丢弃模式。

第一种情况:非丢弃模式且找到换行符(eol >= 0),这种情况对应执行的具体代码如下所示。首先会计算出读指针到换行符之间数据的长度 length,然后判断这段的数据长度是否超过 maxLength 的限制,如果超过了,则表示数据是非法的,因此需要丢弃这段数据。如何丢弃呢?就是将 ByteBuf 的读指针移到换行符之后,然后调用 fail()方法,进行异常处理。如果数据没有超过最大限制,那么就表示数据是合法的,可以进行正常解码。接着在解码时,会根据 stripDelimiter 来判断是否保留换行符,最后将解码后的数据赋值给 frame,然后返回。

// 非丢弃模式且找到换行符
if (eol >= 0) {
    final ByteBuf frame;
    // 计算出要截取的数据长度
    final int length = eol - buffer.readerIndex();
    // 判断是\r\n还是\n,如果是\r\n返回2,如果是\n返回1
    final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

    // 如果读取的数据长度,超过最大长度,那么就不能读这一段数据,需要跳过这段数据,即将读指针直接\n之后。
    if (length > maxLength) {
        // 跳过这段数据
        buffer.readerIndex(eol + delimLength);
        // 进入失败模式
        fail(ctx, length);
        return null;
    }

    // 是否跳过换行符
    if (stripDelimiter) {
        // 读数据时不读取换行符
        frame = buffer.readRetainedSlice(length);
        // 跳过换行符
        buffer.skipBytes(delimLength);
    } else {
        // 读取到的数据包含换行符
        frame = buffer.readRetainedSlice(length + delimLength);
    }
    return frame;
}

第二种情况,非丢弃模式但没有找到换行符(eol < 0) ,这种情况对应执行的具体代码如下所示。因为此时没有找到换行符,所以肯定是解码不出来数据的。但是由于我们有 maxLength 的限制,所以此时需要判断一下当前 buffer 中可读的数据是否超过了这个最大限制。如果超过了,那数据肯定就不合法了,所以这段数据 全部 需要被丢弃,什么时候丢弃呢?是现在立即丢弃还是下一次来解码数据时丢?这取决于 failFast 成员变量的值,true 表示立即丢弃,false 则表示下一次解码时丢弃。

else {
    // 没有找到换行符,则判断可读的数据长度是否超过最大长度,如果超过,则需要丢弃数据
    final int length = buffer.readableBytes();
    if (length > maxLength) {
        // 设置丢弃的长度为本次buffer的可读取长度
        discardedBytes = length;
        // 修改读指针,跳过这段数据
        buffer.readerIndex(buffer.writerIndex());
        // 设置为丢弃模式
        discarding = true;
        offset = 0;
        // 是否快速进入丢弃模式
        if (failFast) {
            fail(ctx, "over " + discardedBytes);
        }
    }
    return null;
}

第三种情况,丢弃模式且找到了换行符(eol >= 0),对应的代码如下。由于在父类解码其中会循环调用子类的解码方法 decode(),所以当前面出现需要丢弃数据时,就会进入丢弃模式中。此时虽然找到了换行符,由于前一次的数据是需要被丢弃的,所以此时,会将本次找到的换行符之前的数据全部丢弃(包括上一次循环中需要被丢弃的数据),最后将丢弃模式设置为 false,因为此时已经将数据丢弃过了,下一次循环读的时候,就是正常解码判断了。

// 丢弃模式且找到了换行符
if (eol >= 0) {
    // 以前丢弃的数据长度+本次可读的数据长度
    final int length = discardedBytes + eol - buffer.readerIndex();
    // 拿到分隔符的长度
    final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
    // 跳过丢弃的数据
    buffer.readerIndex(eol + delimLength);
    // 设置丢弃数据长度为0
    discardedBytes = 0;
    // 设置非丢弃模式
    discarding = false;
    if (!failFast) {
        fail(ctx, length);
    }
}

第四种情况,丢弃模式且没有找到换行符(eol < 0),对应的代码如下。此时因为没有找到换行符,所以肯定不能正确解码,而且又处于丢弃模式,因此本次读到的数据全部都是无效的,都需要被丢弃,然而在这一段的代码中,我们发现,并没有立即丢弃数据,为什么呢?因为还要丢弃掉下一次读取到的数据的前半部分,如果此时将数据丢弃了,那么下一次读取数据时,可能找到的数据长度小于 maxLength 规定的长度,这样我们就会将它拿去解码,实际上这段数据是不可用的。

else {
    // 没找到换行符
    // 以前丢弃的数据 + 本次所有可读的数据
    discardedBytes += buffer.readableBytes();
    // 跳过本次所有可读的数据
    buffer.readerIndex(buffer.writerIndex());
    // 我们跳过缓冲区中的所有内容,需要再次将偏移量设置为0。
    offset = 0;
}

无论是第三种情况,还是第四种情况,只要处于丢弃模式,都不能正常解码,所以最后返回的是 null,即没有解码出对象。

从前面的分析中,可以看到,当要丢弃数据时,会调用 fail()方法,它有几个重载的方法,但最终都会调用到如下重载的方法。

private void fail(final ChannelHandlerContext ctx, String length) {
    ctx.fireExceptionCaught(
            new TooLongFrameException(
                    "frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')'));
}

实际上就是创建了一个异常,这个异常信息,在实际应用中我们可能会经常见到,然后通过 pipeline 向下传播这个异常,最终会调用到 handler 的 exceptionCaught() 方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值