从源码重新学习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>
