Netty源码分析——编/解码
前言
这一篇看一下Netty的编码和解码的工作原理。编码解码器其实都是一种特殊的Handler
,既然是Handler
,那就有inBound
和outBound
的区别。
我们这篇主要是梳理一下,从入站解码到业务处理,再到出站编码的过程,让大家对编解码的流程有个了解。
Decoder
我们随便挑选一种Decoder看一下实现就可以了,比较简单。这篇的内容相比之前的都简单一些、轻松一些。
选ByteToMessageDecoder
看一下,首先这个Decoder
继承自ChannelInboundHandlerAdapter
,这个类其实是一个InBoundHandler
,实际上处理的是读事件。
那么看看这个Decoder是怎么把数据转成我们想要的类对象的。先看下channelRead
方法:
1 | // 只能处理ByteBuf |
这里的步骤拆解一下。首先,ByteToMessageDecoder
只能处理ByteBuf
。然后创建一个CodecOutputList
这个数据结构。这里这个结构其实就是一个ArrayList
,但是其中的所有方法都重写了。区别不是很大,但是这个类有一个getUnsafe
方法,传入的index不会被校验。这里这个数据结构主要是为了提升性能。
然后把数据放到cumulation
里。这其实是一个ByteBuf
,保存的是所有还没被解码的数据。
然后进行真正的解码操作。这里需要注意finally
里的一段代码,是避免OOM的,这说的是一种场景:如果这个cumulation
一直有数据可以读,且channelRead
被调用了超过16次,就强行执行一次discardSomeReadBytes
,把已经读取过的数据扔掉,防止OOM,主要是避免了读取操作老是不结束,已经读取过的字节又一直存放在ByteBuf
中的情况。
我们看下callDecode
方法:
1 | while (in.isReadable()) { |
如果out
里本来就有数据,就触发channelRead
,把数据向后传递:
1 | static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) { |
注意这里是把out
里的每个数据都向后传递。
另外一个细节注意一下这里第二个fireChannelRead
用的是CodecOutputList.getUnsafe
,这个Unsafe
方法的入参就是数组下标,跟ArrayList
一样,但是不会校验下标的合法性,主要是为了提升性能。
另外注意我们会在channelRead
的finally
块中,不管怎样都会触发一次fireChannelRead
。
然后下面的逻辑是先看看有没有解析出数据包,如果没有,看看是不是从in
中读取过数据,如果读取过,就继续解码,如果没有读取过数据,说明可能我们in
中的数据不够我们读取的,这时候就继续读操作(终止解码操作)。
如果解析出了数据包,但是却没读取数据,抛出一个异常告诉你,你解析出了一个数据包,但是没从in
中读取数据,这是有问题的,举个例子:
1 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { |
Encoder
同样选MessageToByteEncoder
,实际上是一个outBoundHandler
。看write
方法:
1 | ByteBuf buf = null; |
这里比较简单了。没有很多细节,看上面基本能够看懂,就不细说了。
清理字节容器
业务拆包完成之后,只是从字节容器中取走了数据,但是这部分空间对于字节容器来说依然保留着。如果不对字节容器做清理,那么时间一长就会OOM。
这里我们要重新看回到channelRead
方法里,在finally
中有这么一段代码:
1 | int size = out.size(); |
fireChannelRead
方法干什么的我们已经说过了,这里看看decodeWasNull
这个成员变量。这是一个boolean
值,记录的是到fireChannelRead
为止,是否已经成功解码了一个数据包。
看下CodecOutputList#insertSinceRecycled
方法:return insertSinceRecycled;
,再追一下这个insertSinceRecycled
成员变量的位置,我们可以看到只有在CodecOutputList#insert
方法中被设置为了true,insert方法的调用方在add
和set
方法中,即:如果向CodecOutputList
中放入数据,则会设置insertSinceRecycled
为true,当CodecOutputList
被回收的时候,insertSinceRecycled
被重置为false。
那么decodeWasNull = !out.insertSinceRecycled();
这句的意思就很明白了,如果这个解码器目前连一个数据包都没解(这里指Decode
操作)出来,则decodeWasNull
设置为true。
我们再看下清理逻辑,ByteToMessageDecoder
会在channelReadComplete
的时候,进行清理字节容器:
1 | numReads = 0; |
discardSomeReadBytes
操作之前,cumulation
中的字节分布情况:
readed | unread | writable |
---|---|---|
之后,字节分布情况:
unread | writable |
---|---|
实际上就是把已读的字节扔掉。
这里注意一个细节。我们之前花了一些篇幅说了decodeWasNull
的含义和赋值过程,也是为这个细节做铺垫。之前说过decodeWasNull
的意思是说,当前解码器到读事件结束,仍然没有解(指代Decode
操作)出一个数据包,这个时候,即使该channel
的设置为非自动读取(非自动读取的部分,可以看下我的另外一篇文章AUTOREAD
),也会触发一次读取操作ctx.read()
。这个操作会向selector
中注册一个READ,以便我们下次读可以读取一个完整的数据包:
1 | if (decodeWasNull) { |
done
内容至此,其实我们已经可以明白编/解码的逻辑了,其实这中间还有一些拆包粘包的内容,比如我们的解码器,里面除了解码,其实还有一个组装数据包的逻辑,对TCP的拆包粘包有了解的同学,看过这些内容应该可以比较快速的理解Netty对于拆包粘包的内容了~