Netty解决粘包和分包问题

Netty提供的解码器

Netty提供的解码器的基类是ByteToMessageDecoder, netty默认提供的几个非常有用的解码器都是它的子类

  • FixedLengthFrameDecoder: 适用于业务包长度固定的情况, 比如TS流, 构造器传入每个业务包的固定长度值, 解码器接收到数据后, 会按照定长来划分业务包并包业务交给后续的处理器(如自定义的handler), 如果当前接收的数据在划分后还有剩余字节, 则会跟后续接收的数据拼接在一起继续划分
  • LengthFieldBasedFrameDecoder: 适用于有协议头(并含有数据长度字段)的情况, 比如PS流, 构造器分别传入如下四个参数:
    • lengthFieldOffset: 长度字段在数据包的位置
    • lengthFieldLength: 长度字段的字节数
    • lengthAdjustment: 长度调整值, 用来确定最终返回的业务包的数据, 计算方式: 从长度字段后, 取长度+该值作为负载数据长度, 如果只需要返回负载数据, 比如数据包长16字节, 长度字段是2字节, 偏移是1字节, 负载数据紧随长度字段之后, 长度是13字节, 那么: 情形1)长度字段是数据包总长16, 调整值就是13-16=-3; 情形2) 长度是负载数据长度13, 调整值就是13-13=0,
    • initialBytesToStrip: 表示被抛弃的头部字节数, 该值=长度字段值+长度条件值
  • LineBasedFrameDecoder: 把数据包按照换行符划分为业务包, 换行符是\n\r\n
  • DelimiterBasedFrameDecoder: 用自定义的分隔符来划分业务包
  • HttpRequestDecoder/HttpResponseDecoder: 支持HTTP协议的解码器
  • RtspDecoder: 继承自HttpObjectDecoder, 支持RTSP协议的解码器

此外, 也可以通过继承ByteToMessageDecoder来实现自定义解码器

Netty实现粘包和拆包

  • 粘包: 每次接收到的数据包中有若干个业务包
  • 分包: 一个业务包被分隔为了若干个数据包

二者都是数据接收过程中产生的问题, 所以需要在解码的过程中做处理. 具体解决办法有两种:

  1. 使用Netty提供的解码器, 优点是编写的代码更少, 因为解码器会根据解码器类型和应用设置自动去处理业务部的划分, 所以可以自动处理粘包和分包
  2. 自定义解码器, 优点是灵活度更高, 适合自定义协议, 因为粘包和分包可以同时处理, 所以下面的例子放在一起实现:
public class MyProtocolHandler extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        byte[] msg = new byte[16];
        while (in.readableBytes() > 16) {
            in.readBytes(msg);
            out.add(new String(msg));
        }
    }
}

说明:

  • 首先自定义解码器继承了ByteToMessageDecoder并重载了decode方法
  • 为了示例简便, 假定每个业务包长度是16字节
  • 粘包处理: 每次读取16字节后把业务包加入到out列表中
  • 分包处理: 如果剩余字节数不足16字节则返回, Netty会在收到后续数据后, 和上次处理剩余数据一起传入decode

Netty实现拆包处理的原理

核心代码是如下方法, 已添加关键注释说明流程:

 @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        // out封装了业务包, 一般是某个类对象的列表
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 判断是否第一次处理数据
            first = cumulation == null;
            // 根据first来决定是否合并遗留数据
            cumulation = cumulator.cumulate(ctx.alloc(),
                    first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
            // 内部用一个循环来调用decode方法, out返回处理出来的业务对象
            callDecode(ctx, cumulation, out);
        } finally {
            // 如果遗留数据被处理完, 则清空遗留数据缓冲
            if (cumulation != null && !cumulation.isReadable()) {
                numReads = 0;
                cumulation.release();
                cumulation = null;
            } else if (++ numReads >= discardAfterReads) {
            // 如果遗留数据太多超过了缓冲区大小, 则丢弃最早收到的部分字节
                numReads = 0;
                discardSomeReadBytes();
            }
            // 到这里就是cumulation不为空, 且有可读数据, 即遗留数据
            //------------
            int size = out.size();
            firedChannelRead |= out.insertSinceRecycled();
            // 把本解码器解码出来的业务包投递给下一个解码器
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}
  • cumulation: 保存了遗留数据, 类型是ByteBuf
  • cumulator: Cumulator接口对象, 接口cumulate可以把遗留数据和新收到的数据拼接在一起
  • ByteToMessageDecoder中有两个Cumulator的实现类: MERGE_CUMULATORCOMPOSITE_CUMULATOR
  • MERGE_CUMULATOR: 通过合并缓冲区的方式把遗留数据和新数据拼接起来, 需要数据拷贝, 默认采用这种方式
  • COMPOSITE_CUMULATOR: 通过指针计算的方式把遗留数据和新数据拼接起来, 需要计算索引位置, 实现复杂, 但是没有数据拷贝
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
    while (in.isReadable()) {
        int outSize = out.size();
        if (outSize > 0) {
            fireChannelRead(ctx, out, outSize);
            out.clear();
            // 每次继续循环前先判断当前处理器是否被移除了
            if (ctx.isRemoved()) {
                break;
            }
            outSize = 0;
        }
        int oldInputLength = in.readableBytes();
        // 内部调用decode方法
        decodeRemovalReentryProtection(ctx, in, out);
        // 同上
        if (ctx.isRemoved()) {
            break;
        }
        // 如果经过一次decode方法后, 业务对象数量没有变化
        if (outSize == out.size()) {
            if (oldInputLength == in.readableBytes()) {
            // 并且in内部的数据没有被读取, 即调用decode前后没有变化, 说明没有新的业务对象可以读取, 所以退出循环
                break;
            } else {
                continue;
            }
        }
        if (oldInputLength == in.readableBytes()) {
        // 如果in的数据没有被读取, 但是业务对象数量变化了, 则抛出异常
            throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
        }
        // 如果循环只处理一次decode方法, 则退出循环
        if (isSingleDecode()) {
            break;
        }
    }
}
  • 所以在自定义实现时, decode()中一定要注意如果没有获取到业务对象, 千万不要out.add()
  • in是ByteBuf类型, 内部包含一个读索引一个写索引, 在decode中用in.read()读取数据后会移动读索引
  • ByteBufisReadable()返回是否还有可读数据, 一般就是判断写索引是否大于读索引

你可能感兴趣的:(Java基础)