netty-读半包处理--ByteToMessageDecoder

Netty作为网络应用框架,在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节,而java应用传输又是POJO,这就要序列化成字节再网络上传输。Netty是使用TCP/UDP在互联网上传输数据,由于Netty异步的特性,当使用TCP时,不可避免就会出现TCP粘包/拆包现象。对于TCP粘包/拆包,业界公开的有一下几种方法:

  1. 使用分隔符分割
  2. 使用定长的消息,不足补空格之类的
  3. 在传输的消息上制定长度

Netty使用ByteToMessageDecoder进行读半包处理,ByteToMessageDecoder是一个抽象类,继承于ChannelInboundHandlerAdapter,首先看下ByteToMessageDecoder的属性:

netty-读半包处理--ByteToMessageDecoder_第1张图片

cumulation-用来保存累计读取到的字节

cumulator-累计器,把从channel获取到的字节累计起来有两种实现,首先看下定义

netty-读半包处理--ByteToMessageDecoder_第2张图片

Cumulator只有一个方法 cumulate(),用于累计字节,该接口有两个实现,默认使用的是MERGE_CUMULATOR

public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            final ByteBuf buffer;
            if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                    || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
                // Expand cumulation (by replace it) when either there is not more room in the buffer
                // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
                // duplicate().retain() or if its read-only.
                //
                // See:
                // - https://github.com/netty/netty/issues/2327
                // - https://github.com/netty/netty/issues/1764
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
            } else {
                buffer = cumulation;
            }
            buffer.writeBytes(in);
            in.release();
            return buffer;
        }
    };

 in 参数表示新读取到的数据,该方法只是简单的判断了下是否要扩容,是的话就扩容,然后把新读取到的数据写入进来,释放in的内存,在返回新ByteBuf。另外一个实现是COMPOSITE_CUMULATOR

 public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in){
            ByteBuf buffer;
            if (cumulation.refCnt() > 1) {
                // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user
                // use slice().retain() or duplicate().retain().
                //
                // See:
                // - https://github.com/netty/netty/issues/2327
                // - https://github.com/netty/netty/issues/1764
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
                buffer.writeBytes(in);
                in.release();
            } else {
                CompositeByteBuf composite;
                if (cumulation instanceof CompositeByteBuf) {
                    composite = (CompositeByteBuf) cumulation;
                } else {
                    composite = alloc.compositeBuffer(Integer.MAX_VALUE);
                    composite.addComponent(true, cumulation);
                }
                composite.addComponent(true, in);
                buffer = composite;
            }
            return buffer;
        }
    };

该 累计器在用于调用slice().retain()duplicate().retain()时扩容,扩容的方式和MERGE_CUMULATOR一致,只不过返回的是CompositeByteBuf。

singleDecode-设置为true后,单个解码器只会解码出一个结果

decodeWasNull-解码结果为空

first-是否是第一次读取数据

discardAfterReads-多少次读取后,丢弃数据 默认16次

numReads-已经累加了多少次数据了

ByteToMessageDecoder的属性就是这些了。下面看下ByteToMessageDecoder的事件处理方法channelRead(ChannelHandlerContext ctx, Object msg)。

        if (msg instanceof ByteBuf) {//只针对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;
                //判断反复累加次数是不是大于设置的discardAfterReads,当达到了discardAfterReads,就要去丢弃一些数据,防止OOM
                } else if (++ numReads >= discardAfterReads) {
                    numReads = 0;
                    discardSomeReadBytes();
                }
                
                int size = out.size();
                decodeWasNull = !out.insertSinceRecycled();
                //触发渠道的channelRead事件,size是触发多少次事件
                fireChannelRead(ctx, out, size);
                //回收解码结果
                out.recycle();
            }
        } else {
            ctx.fireChannelRead(msg);
        }

CodecOutputList是Netty定制的一个列表,实现了RandomAccess。该列表循环使用缓存解码结果,减少不必要的实例创建,提升性能,且与线程绑定。

再看discardSomeReadBytes()

    protected final void discardSomeReadBytes() {
        if (cumulation != null && !first && cumulation.refCnt() == 1) {
            
            cumulation.discardSomeReadBytes();
        }
    }

 大家可以看到,只有cumulation.refCnt() == 1的时候才会丢弃数据,那是因为,在用户显示的调用slice().retain()duplicate().retain()时,会使refCnt>1,这时说明缓存的数据用户另有他用,所有就不丢弃数据。

 fireChannelRead(ctx, out, size) 

    static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
        for (int i = 0; i < numElements; i ++) {
            ctx.fireChannelRead(msgs.getUnsafe(i));
        }
    }

循环调用out.size次,发布channelRead事件。

下面看看核心方法callDecode(ctx, cumulation, out);

     protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List out) {
        try {
            //循环读取数据
            while (in.isReadable()) {
                int outSize = out.size();

                if (outSize > 0) {//大于0表示已经解码出数据了
                    fireChannelRead(ctx, out, outSize);//立即发布channelRead事件
                    out.clear();//清空结果集
                    if (ctx.isRemoved()) {//用户是否主动删除handler,删除了就跳出循环
                        break;
                    }
                    outSize = 0;
                }
                //获取可读锁定
                int oldInputLength = in.readableBytes();
                //调用解码方法
                decodeRemovalReentryProtection(ctx, in, out);
                if (ctx.isRemoved()) {//用户是否主动删除handler,删除了就跳出循环
                    break;
                }
                 //没有解码出数据
                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {//没有读取出任何数据
                        break;
                    } else {
                        //已经读取部分数据,但数据还不够解码,继续读取
                        continue;
                    }
                }

                //能运行到这说明outSize >0,即已经解码出数据了
                //可读索引不变,说明自定义的decode有问题,所以抛出一个异常
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                                    ".decode() did not read anything but decoded a message.");
                }
                //判断singleDecode属性是否为true,=true就跳出循环,用于特殊解码
                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception cause) {
            throw new DecoderException(cause);
        }
    } 
  

下面看看decodeRemovalReentryProtection(ctx, in, out)方法

     final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List out)
            throws Exception {
        //设置解码器正在解码状态,防止解码过程中另一个线程调用 handlerRemoved(ctx)销毁数据
        decodeState = STATE_CALLING_CHILD_DECODE;
        try {
            //抽象方法,子类实现
            decode(ctx, in, out);
        } finally {
            //decodeState == STATE_HANDLER_REMOVED_PENDING 表示在解码过程中,有另外的线程把ctx移除了,这是需要由当前线程调用handlerRemoved(ctx)方法来完成数据销毁
            boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
            decodeState = STATE_INIT;
            if (removePending) {
                handlerRemoved(ctx);
            }
        }
    } 
  

该方法处理调用抽象方法decode(ctx, in, out)外,就是做了下并发处理。

到此,ByteToMessageDecoder就基本解析完了,在看一看一些细节方法

handlerRemoved(ChannelHandlerContext ctx)

     @Override
     public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //是否正在解码中,
        if (decodeState == STATE_CALLING_CHILD_DECODE) {
            //正在解码中就把状态标志为STATE_HANDLER_REMOVED_PENDING,表示要销毁数据,等解码线程解完码后销毁数据,自己直接返回
            decodeState = STATE_HANDLER_REMOVED_PENDING;
            return;
        }
        ByteBuf buf = cumulation;
        //累加区有数据,
        if (buf != null) {
            //GC回收
            cumulation = null;
            //缓冲区还有数据
            int readable = buf.readableBytes();
            if (readable > 0) {
                //把缓冲区的数据全部读取出来,发布channelread事件,释放资源
                ByteBuf bytes = buf.readBytes(readable);
                buf.release();
                ctx.fireChannelRead(bytes);
            } else {
                buf.release();
            }
            //读取次数置为0,发布channelreadComplete事件
            numReads = 0;
            ctx.fireChannelReadComplete();
        }
        //空方法,子类可以覆盖
        handlerRemoved0(ctx);
    }

 

你可能感兴趣的:(netty)