netty源码解析之编码和解码

粘包 和拆包

TCP是基于流传输的,而流是没有边界的,TCP相对于上层业务属于底层协议,对于TCP而言,并不关心上层的业务,所以TCP都在进行数据发送的时候只会按照自己发送的规则进行发送,导致的结果就是在业务上的一个完整的数据包可能会被TCP拆成多个包进行发送,这就是拆包,同时也有可能将业务上多个小数据包合成一个包进行发送,这就是粘包

如何解决粘包和拆包

对于粘包和拆包常用的解决方案由三种
1.固定消息和报文的长度
2.通过添加分隔符
3.单独传输一个消息长度的属性

ByteToMessageDecoder

ByteToMessageDecoder 在 netty 中是很多解码器的父类,但是这个抽象的解码器没有解决粘包拆包的问题,而是交给子类去解决
如果用户想自定义解码器只需要重写 decode()这个方法就好了
不论是编码器还是解码器,都是一个handler,所以我们来看看这个解码器的channelRead方法

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                first = cumulation == null;
                if (first) {
                    cumulation = data;
                } else {
                    //累加
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Exception e) {
                throw new DecoderException(e);
            } finally {
                if (cumulation != null && !cumulation.isReadable()) {
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++ numReads >= discardAfterReads) {
                    numReads = 0;
                    discardSomeReadBytes();
                }
                int size = out.size();
                decodeWasNull = !out.insertSinceRecycled();
                fireChannelRead(ctx, out, size);
                out.recycle();
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }

first = cumulation == null ,first 这个变量是什么意思呢?
其实不难猜出,就是去判断这是不是第一次从管道里面拿数据,再来看看cumulation,可以发现这个属性的类型是 netty 中的 ByteBuf,所以,netty 解决拆包的思路就是,将没有读完的数据存储到这个ByteBuf 里面,直到读完了才会进行数据的发送
如果是第一次读取就会将传入的数据直接进行赋值,如果不是第一次,就会将之前保存的数据和传入的数据进行累加,然后调用
cumulator.cumulate(ctx.alloc(), cumulation, data),在调用 callDecode(ctx, cumulation, out) 进行数据的解析
finally部分如果cumulation 不为null 并且没有可以读的字节,那么就会释放,fireChannelRead(ctx, out, size)在进行向下传播

首先来看一看cumulator这个对象到底是个啥玩意,是如何进行累加的

 private Cumulator cumulator = MERGE_CUMULATOR;
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            try {
                final ByteBuf buffer;
                if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                    || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
                    buffer = expandCumulation(alloc, cumulation, in.readableBytes());
                } else {
                    buffer = cumulation;
                }
                buffer.writeBytes(in);
                return buffer;
            } finally {
                in.release();
            }
        }
    };

这个if判断是一个扩容判断 cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
cumulation的写指针>cumulation.最大容量-in.的可读数据
也就是说 in的数据+comlation的数据 是否大于 cumlation的最大容量,如果大于,那么就是需要扩容,然后调用 buffer.writeBytes(in) 将in的数据写入 cumlation,最后将 in的数据释放

接下来就是调用callDecode() 这个方法

 protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
		...
            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();
                decodeRemovalReentryProtection(ctx, in, out);
                if (ctx.isRemoved()) {
                    break;
                }
                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                        continue;
                    }
                }
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                                    ".decode() did not read anything but decoded a message.");
                }
                if (isSingleDecode()) {
                    break;
                }
            }
    ...
    }

参数:
ChannelHandlerContext ctx:客户端 channel
ByteBuf in:cumulation中保存的数据
List out:一个netty自定义的list集合
首先是判断当前 list 里面是否有对象,第一次的话一定是没有的,接着调用了一个非常重要的方法
decodeRemovalReentryProtection(ctx, in, out);在这个方法的里面调用的是 decode(ctx, in, out) ,这个方法就是前面说过的交给子类去实现的方法,这个方法做了什么事情后面举例分析
执行完decode(ctx, in, out) 后进行判断 ,就是判断解码前后这个集合的大小有没有发生改变,如果相同就说明没有解析到数据,又会进一步判断 oldInputLength == in.readableBytes(),解析前后的长度是不是相同,如果说长度都相同,说明当前数据解析玩了,那么就会break,等待下次解析;如果说长度不同,就说明解没有解析到足够数据,但是解析到了数据,就contiune,继续解析

netty提供的解码器

LineBasedFrameDecoder

LineBasedFrameDecoder是一种以换行符(\r\n)
对于LineBasedFrameDecoder的decode而言有两种模式,一是丢弃模式,二是非丢弃模式

非丢弃模式下

		if (eol >= 0) {
                final ByteBuf frame;
                //算出本次要截取数据的长度
                final int length = eol - buffer.readerIndex();
                //判断\n前面是否是\r  是返回2  不是返回1
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

                //如果读取的长度>指定的最大长度
                if (length > maxLength) {
                    //设置此缓冲区的readerIndex。  跳过这段数据
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }

                //判断解析的数据是否要带\r\n
                if (stripDelimiter) {
                    //返回从当前readerIndex开始的缓冲区子区域的一个新保留的片
                    //返回到\r\n的有效数据 不包括\r\n
                    frame = buffer.readRetainedSlice(length);
                    //跳过\r\n
                    buffer.skipBytes(delimLength);
                } else {
                    //截取到\r\n的有效数据 包括\r\n
                    frame = buffer.readRetainedSlice(length + delimLength);
                }
                return frame;
            } else {
                //如果没有找到\r\n
                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返回的是 /r/n 的下标的位置,如果找到了下标就会进入if ,反之进入else
首先会获取本次读取数据的长度,然后 如果读取的长度>指定的最大长度,因为当前是非丢弃模式,就会抛出异常,
从而回调 exceptionCaught()

如果说没有找到/r/n的下标位置,如果本次数据的可读长度》最大可读长度,那么就会跳过本次数据,然后设置为丢弃模式
返回null

丢弃模式

		 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);
                }
            } else {
                //没找到\r\n
                //丢弃的数据长度+本次可读数据的长度
                discardedBytes += buffer.readableBytes();
                //跳过本次可读取的数据
                buffer.readerIndex(buffer.writerIndex());
                // 我们跳过缓冲区中的所有内容,需要再次将偏移量设置为0。
                offset = 0;
            }
            return null;

在丢弃模式下如果找到了 /r/n,那么这次要进行丢弃的就是/r/n之前的内容了,也就是说
上一次丢弃的内容 + 本次丢弃的内容 == 一条完整的消息,然后设置为非丢弃模式

如果说没有找到 /r/n 呢?还会继续进行丢弃,直到丢弃了一条完整的数据

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值