从源码重新学习Netty的解码器

引言

在入门篇的学习中,我们曾经阐述过工作于TCP协议的应用出现的粘包和拆包现象。当时我们介绍了Netty用于解决拆包粘包问题的三种常见的解码器实现:

  • 处理定长协议的FixedLengthFrameDecoder
  • 处理固定消息分隔符的DelimiterBasedFrameDecoder
  • 处理报文头+报文体模式协议的LengthFieldBasedFrameDecoder

今天这篇文章,我们不再介绍更多编解码器的使用,而是从这些解码器的背后实现入手,弄明白他们的处理机制,以及其在Netty整个读写流程中所处的地位。

解码

将socket上读取到的二进制数据转换为后端业务可以理解的消息实体,这个过程我们称之为解码,执行这个任务的组件,我们称之为解码器。而在Netty当中,有非常多内建的解码支持,涵盖了各种市面上的协议。每一种协议的解码需求,都有一个特定的解码器来承担,解码器都位于io.netty.handler.codec包路径下。而所有的解码器都继承了ByteToMessageDecoder,该类是解码器的顶级父类,定义了这个解码过程中的模板动作。首先来看下该类的类视图,如下

重写的channelRead

从继承关系再配合该类的定义,可以猜到其重写了channelRead方法用于完成解码任务。来看其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();
            //代码⑦
            firedChannelRead |= out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    }
    else
    {
        ctx.fireChannelRead(msg);
    }
}

首先来看代码①CodecOutputList继承于ArrayList,是一个轻量级的包装类,内部多添加了一些属性。其起到的作用就是存放解码后的消息对象,以及支持以线程变量的形式被缓存起来。所以方法CodecOutputList.newInstance()并不是新建了实例,而是从线程变量中获取了一个实例。在整个方法调用结束后,finally代码块中,也会调用CodecOutputList#recycle方法回收至线程变量中。

接着来看代码②ByteToMessageDecoder对于二进制数拆包粘包的处理思路是:首先尽可能的读取二进制数据,对于这些读取到的数据执行解码操作,解码生成的消息对象传递到后端的处理器中处理;而不能完整解码为一个消息对象的不完整报文的二进制数据,则驻留在ByteToMessageDecoder内部。Netty的设计上,一个ChannelHandler只会运行在一个绑定的线程上,数据并没有竞争,不存在并发可能引发的数据安全问题。因此可以将不完整报文的二进制数据保留在自身的存储中,也就是cumulation,其类型为ByteBuf。而每次从通道中读取到新的数据,首先需要将这些数据与之前留存的cumulation进行合并,合并后才能进行解码处理。

接着来看代码③。上面我们讲到,每次从通道中读取到数据,需要将读取到的数据合并到之前的累积部分,也就是合并到cumulation中。将新读取到的数据添加到之前的累积数据上,Netty设计了接口ByteToMessageDecoder.Cumulator来完成这个功能,并且提供了两个内置实现:

  • 将新读取到

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>

全部评论

相关推荐

10-22 12:34
测试工程师
点赞 评论 收藏
分享
活泼的代码渣渣在泡池...:哈哈哈挺好的,我也上岸美团了,不说了,我又接了一单
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务