<2021SC@SDUSC>ByteToMessageDecoder

本文详细介绍了Netty的ByteToMessageDecoder类,它是许多解码器的基础。文章重点分析了callDecode方法的执行流程,包括decode方法的调用以及解码过程中的数据处理。同时,探讨了channelRead方法如何处理ByteBuf数据,以及在解码前后触发fireChannelRead的时机。文章通过实例解析了FixedLengthFrameDecoder的decode方法,帮助读者理解解码过程。最后,总结了ByteToMessageDecoder的主要功能和工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021SC@SDUSC

前言

在之前的几篇博客中,分析了几种netty内置的解码器,它们都继承了ByteToMessageDecoder类,也分析了它们对消息的解码过程,也就是decode方法。在本篇博客中,只会简单地介绍ByteToMessageDecoder类调用decode的过程。

一、ByteToMessageDecoder

ByteToMessageDecoder类继承自ChannelInboundHandlerAdapter类,标识它确实是一个处理器,可以放入pipeline中。

    protected ByteToMessageDecoder() {
        ensureNotSharable();
    }

只有一个构造方法,权限是protected。在构造方法中只做了一件事,就是检查是否有@Sharable注解,如果有的话,抛出异常。在之前的博客中提到过,decoder,特别是继承自ByteToMessageDecoder的解码器,不能有@Sharable注解。
之后,会分析ByteToMessageDecoder类的channelRead(ChannelHandlerContext ctx, Object msg)方法和callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)方法。

二、callDecode

首先分析callDecode方法。

    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                final int outSize = out.size();

                if (outSize > 0) {
                    fireChannelRead(ctx, out, outSize);
                    out.clear();

                    // Check if this handler was removed before continuing with decoding.
                    // If it was removed, it is not safe to continue to operate on the buffer.
                    //
                    // See:
                    // - https://github.com/netty/netty/issues/4635
                    if (ctx.isRemoved()) {
                        break;
                    }
                }

                int oldInputLength = in.readableBytes();
                decodeRemovalReentryProtection(ctx, in, out);

                // Check if this handler was removed before continuing the loop.
                // If it was removed, it is not safe to continue to operate on the buffer.
                //
                // See https://github.com/netty/netty/issues/1664
                if (ctx.isRemoved()) {
                    break;
                }

                if (out.isEmpty()) {
                    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;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception cause) {
            throw new DecoderException(cause);
        }
    }

在方法中,有一个while循环,条件是in.isReadable(),即bytebuf中还有可读数据,可以按照用户自定义的方式解析。
重点是之后进入decodeRemovalReentryProtection方法。

    final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
        decodeState = STATE_CALLING_CHILD_DECODE;
        try {
            decode(ctx, in, out);
        } finally {
            boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
            decodeState = STATE_INIT;
            if (removePending) {
                fireChannelRead(ctx, out, out.size());
                out.clear();
                handlerRemoved(ctx);
            }
        }
    }

该方法是final方法,子类不可修改,毕竟是模板方法模式。而在这个方法中,实际调用了decode方法。

    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

这个decode方法没有任何实现,需要子类实现,为了方便回忆,这里看一下FixedLengthFrameDecoder类的重写的decode方法。

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

这个方法在之前的几篇博客中没有特别提过,实际上就是调用了另外的一个decode方法(没有继承)获取解析后的数据,再加入了out中。
而在decodeRemovalReentryProtection中,虽然不仅仅是调用了decode方法, 获取了解析后的数据,但是,剩下的部分我没有理解。
回到callDecode方法,当调用了decodeRemovalReentryProtection后,首先判断out是不是空,如果是空,那么意味着没有解析后的数据加入out。这时,如果可读字节在decode前后大小一致,说明不需要再解析了,直接退出循环;否则,如果大小不一致,有可能是读取的数据长度超过了协议的上限,数据被抛弃了,但依然需要进入下一侧循环,因为后面可能还有数据。
在这个方法中,我有一个问题没有理解,就是调用fireChannelRead方法的时机,代码中,是在实际decode之前先判断out中是否有数据,如果有的话,调用fireChannelRead,将数据传递给下一个处理器。这个处理逻辑出现在while循环较前面的位置。我没有理解的是,为什么不在实际读取数据之后,再进行这个处理过程。实际上,假设在最后一次循环中读取了数据,并将解析后的数据加入了out中,之后判断in.isReadable(),如果返回了false,那么就不会进入循环体,也不会去触发fireChannelRead方法,好像会在之后其他的方法中去调用,但为什么不选择更改代码的顺序,这里我不能理解。

三、channelRead

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                first = cumulation == null;
                cumulation = cumulator.cumulate(ctx.alloc(),
                        first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Exception e) {
                throw new DecoderException(e);
            } finally {
                try {
                    if (cumulation != null && !cumulation.isReadable()) {
                        numReads = 0;
                        cumulation.release();
                        cumulation = null;
                    } else if (++numReads >= discardAfterReads) {
                        // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                        // See https://github.com/netty/netty/issues/4275
                        numReads = 0;
                        discardSomeReadBytes();
                    }

                    int size = out.size();
                    firedChannelRead |= out.insertSinceRecycled();
                    fireChannelRead(ctx, out, size);
                } finally {
                    out.recycle();
                }
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }

首先判断msg是不是ByteBuf类的实例,如果不是的话,直接交给下一个处理器,所以一般建议在initChannel的时候,将解码器写在其它处理器的前面。
吐过msg是ByteBuf类的实例,实现创建一个CodecOutputList 类的对象out,之后,判断当前cummulation是否为空,如果是的话,说明是第一次,first为true。之后,将之前的数据与这次的数据连接起来,这里涉及是不是第一批数据的问题,逻辑比较简单。然后调用callDecode方法,在上面分析过了。
之后,进入finally语句块。根具条件,将资源释放,之后,将数据传递给下一个处理器。

四、总结

在本篇博客中,简单介绍了ByteToMessageDecoder类,并且,主要分析了调用decode方法的过程,可以清楚地看出,这是模板方法的设计模式,即父类定义了处理逻辑,而将某些过程的具体实现留给子类实现。显然,在本篇博客中,关于ByteToMessageDecoder类,有很多地方没有去分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东羚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值