图文并茂剖析Netty编解码以及背后的设计理念

简介: 图文并茂剖析Netty编解码以及背后的设计理念

本文主要介绍网络通信中与通信息息相关的重要部分:通信协议的底层实现原理:编码与解码


温馨提示:源码分析或许比较枯燥,在浏览的过程中建议重点关注黑色字体与流程图,是根据源码进行的提炼,突出源码背后的设计理念

1、Netty4编码解码概述


Netty中定义的编码解码器核心类图如下:

f9ccb890efecb46a6791cd1f381a9810.jpg

Decoder(解码器) 继承自 Inbound 事件处理器,而Encoder(编码器)继承自Outbound事件处理器。


其实不难理解,以服务端接收请求、返回响应结果的视角来看这个问题:


当服务端通过网络IO接收到字节序列时,从底层网络套接字中将字节流读取到接受缓存区(ByteBuf),服务端的职责首先需要从二进制流中解码出一个完整的请求,然后“读懂请求”的含义进行对应的业务逻辑处理,处理完毕后首先需要将响应结果(通常为JSON字符串)编码成二级制流,通过网络进行传递,客户端收到二进制流后同样进行解码。


解码:针对的是输入,故继承 InBound 入端事件。

编码:针对的是输出,故继承 OutBound 出端事件。


了解了上述基本点后,接下来对上述核心类一一做个介绍:


  • ByteToMessageDecoder
    解码器,将字节流解码成消息(message)。
  • MessageToByteEncoder
    编码器,将消息(message)编码成字节流。
  • MessageToMessageEncoder
    编码器,将消息编码成”另一种消息“,更通用,”另一种消息“由泛型指定。
  • MessageToMessageDecoder
    解码器,将消息解码成“另一种消息”,更通用,“另一种消息”由泛型指定。


下面介绍Netty4自带的协议解码器,是ByteToMessageDecoder的子类。


  • LineBasedFrameDecoder
    基于换行符的分隔符,使用\n或\r\n分隔符来标志一个字节序列的结束。
  • DelimiterBasedFrameDecoder
    基于自定义的分隔符,使用定义的分隔符来标志一个字节序列的结束。
  • FixedLengthFrameDecoder
    固定长度的编码器。在实际使用时,如果单条消息不足定义的长度,通常需要人为填充。
  • LengthFieldBasedFrameDecoder
    基于长度字段的协议,通过指定一个长度字段,该字段的存储字节固定,例如3个字节或4个字节等,然后该字段中存储消息的长度,这样在解码时可以非常方便的判断一条消息的长度,这是一个非常经典的client-server协议格式,下面会对其进行详细解读。


2、源码分析解码器实现原理


ByteToMessageDecoder 是 Netty 解码器实现的基类,典型的模板设计模式。

解码器引入的目的是为了解决网络编程中的“粘包问题”,网络传输基于字节流,客户端多个线程通过一条长连接向服务端发送多个请求,服务端在处理命令之前如何正确拆解出一条完整的请求信息呢?


例如客户端A的三个线程t1、t2、t3 使用同一条连接(类比Dubbo客户端)发送了3个请求,内容分别为 A, BCD, E 。


服务端基于NIO来处理,当请求陆续到达服务端的接受缓存区,NIO 读事件触发,可能第一次网络读,从网络中读取的内容为AB字节序列(包含第一个请求包全部,第二个请求包部分),紧接着再读取CDE序列,如果服务端每接受到一部分数据就当成一个完整的请求去处理的话,明显与客户端原始请求存在差别。


故为了解决服务端、客户端能对同一个字节流具有相同的理解语义,所谓的通信协议因此诞生了,通俗一点就是客户端、服务端如果界定一个完整请求包。


最常见的几种协议:


  • 每一行一个数据包,即在每一个请求包最后以 /r/n 结尾
  • 固定长度,请求内容不足使用特殊字符填充
  • 协议头 + 协议体 ,其中协议头定长,并且内部会含有一个表示包长度的字段。

上述具体协议,将在下篇文章中如何定制私有化协议(编码解码)


接下来将通过阅读源码的方式探究Netty中解码的实现原理,并总结其核心设计关键点

ByteToMessageDecoder Netty 网络解码器的模板父类, Netty 的扩展是基于事件链机制,即解码器实现的是 InBound 事件处理器。


在阅读解码器实现原理的同时,大家可以关注一个解码器实现的事件方法,再次感悟一下不同场景应该选用实现哪个事件方法。


2.1 channelRead


通道读时间,Netty底层通过Nio Socket 读取到的字节序列后通过传播 channelRead 事件,让上层的事件处理器对接受到的数据进行处理,解码器的职责就是从二进制流中解码出一条条消息。


其处理的代码如下图所示:

57f0418bacbb0c12581080d8fc766d0e.png

上面的实现要点如下:


代码@1:对待处理数据类型进行判断,如果是ByteBuf,则尝试从流中解码请求,如果不是合适的类型,直接调用ctx.fireChannelRead 方法继续向事件链进行传播。


代码@2:构造CodecOutputList out对象,用来存储经过该解码器解码出来的的消息,其内部数据结构为List。


代码@3:如果该解码器中的接收缓冲区(累积缓存区)为空,表示第一次接码消息,设置 first 为 true,并直接将接收到的数据设置为接收缓存区。


代码@4:如果当前累积缓存区不为空,需要计算累积缓存区是否能容纳当前接收到的数据,如果无法存储,则需要对累积缓存区进行扩容,扩容的套路就是先申请一个容量大的缓存区,然后将原先的累积缓存区中的数据复制到新的缓存区,然后释放旧的缓存区。


代码@5:调用callDecode方法对累积缓存区中的数据,进行尝试解码,将解码后的结果存放在out对象中(稍后会对该方法详细进行讲解)。


在调用完用户自定义的协议解码后,开始进行资源的回收逻辑


代码@6:如果累积缓存区不为空,并且读写缓存区中所有的数据已全部处理,重置numReads与累积缓存区cumulation。


代码@7:如果 numReads 超过 discardAfterReads,需要对累积缓存去进行压缩

设计目的:主要是避免内存泄漏,节省内存空间。numReads 表示的含义是对累积缓存区解码的次数,如果多次解码都未全部将累积缓存区全部处理完成,当新的数据到达累积缓存区,极大可能需要进行扩容,从而造成累积缓存区的膨胀,如果不丢弃已处理的数据,及时释放内存空间,避免扩容,否则会导致累积缓存区无限扩容,内存资源得到极大的消耗。


代码@8:将解码后的请求继续向事件链进行传播,例如业务处理器,业务处理器可以基于请求对象进行编码的根本原因就是首先进入的解码器,解码出一个一个请求后,业务处理器根据请求进行对应的业务逻辑处理。


代码@9:处理完后,对out结果list对象进行回收,这里使用了Netty的对象缓存机制(对象池)。


接下来探究一下 callDecode 的核心实现逻辑:


c378a7fcf80047b0642c611c772a950e.png

该方法的实现要点如下:


代码@1:首先对参数进行一个详细介绍:


  • ChannelHandlerContext ctx:事件处理器链当前处理器的上下文环境。
  • ByteBuf in:累积缓存区。
  • List< Object > out:解码后的结果列表。


代码@2:while (in.isReadable()) ,NIO读取经典写法,判断读缓存区是否还有可读字节,从@3到@8都是对该缓存区的处理。


代码@3:由于这里处于一个循环中,一次循环后如果out解码结果列表不为空,会立即将解码后的请求通过调用 fireChannelRead 向后面的事件处理其传播。


代码@4:oldInputLength,当前累积缓存区可读大小。


代码@5:decode 该方法是一个抽象方法,尝试从累积缓存区中解码出完整的请求,由具体的协议实现类去实现。


代码@6,7:如果累积缓存区中不包含一条完整的请求,本次解码结束,等待更多数据到达接受缓存区(下一次读事件触发,继续通过网络读API从Socket中读取字节流)。


代码@8:如果singleDecode=true,表示不支持多次解码,故跳出。


为了加深理解上述流程,Netty 解码器的核心实现流程如下:

c9740d02a8a66f74452733a526b69b05.jpg



2.3 channelReadComplete 事件


通道读完成事件,这是每一次读就绪事件处理完成后,会传播该事件。

f875cfeb16bfa6f0086dfc44ba80128b.png

在每一次读处理完成后,Netty为了保证累积缓存区不至于浪费空间,进行一次压缩,其设计理念在上文已提到。


2.4 channelInactive事件


通道在非激活状态时会触发该事件。

3b09ef34141f92fce6e49a0c46eee7fc.png

代码@1:尝试通过调用 channelInputClosed方法最后尝试进行解码。

代码@2:如果累积缓存区不为空,释放累积缓存区。

代码@3:传播一次通道读事件。

代码@4:如果代码@1在通道非激活时还解码到新数据了,则传播一次通道完成度事件。

代码@5:根据callChannelInactive参数,决定是否传播通道非激活事件。

9427bfd89fa0d7588d48c30b9f0e3fd3.png

代码@1:如果累积缓冲区不为空,则调用callDecode方法,对累积缓存区进行解码,因为累积缓存区中的数据的读取已经和底层网络通道无关了,通道关闭后,该部分数据还是要尽量处理。


代码@2:再解码一次,由于这个方法,是直接调用抽象方法decode,最终解码的结果放在out中,解码后,如果有消息,最终还会触发一次通道读事件和通道读完成事件。


2.5 handlerRemoved 事件


handlerRemoved事件,该事件的触发有两种情况:


  • 在调用handlerAdd事件失败后,接着调用handlerRemoved事件。
  • 在通道关闭后,DefaultChannelPipeline 的 HeadContext 的 channelUnregistered 中传播完通道事件取消注册事件后,会销毁注册在该通道上的事件注册器,此时也会触发handlerRemoved事件。

5d9abe65e619bf6fe3db21ade9f53e04.png

从实现来看,也非常简单,就是将累积缓存区中未处理的数据传播到其下游的事件处理器,传播之后再从事件链中移除,体现了其“高度负责”的一面。


对于解码的核心设计理念再做一个总结:


  • 引入累积缓存区,存储从网络底层接受的数据。
  • 对累积缓存区中的数据尝试解码,如果能解码出一条请求,就解码并将数据传入到后续处理器。
  • 如果累积缓存区中不包含一条完整的消息,则结束本次解码,等待后续更多的数据到达缓存区。


那问题又来了,如何判别累积缓存区中是否包含一条完整的消息呢?如何进行协议的设计呢?


此部分内容将在下文:如何使用Netty设计一款通信协议。


3、源码分析编码器实现原理


Netty将消息(请求对象、响应结果) 按特定格式转换为二进制流。


MessageToByteEncoder的核心类图如下:

0c6c1071794da0936cb165344c685e64.png


其核心属性如下:


  • private final TypeParameterMatcher matcher
    参数类型匹配器,其实就是匹配MessageToByteEncoder的泛型参数。
  • private final boolean preferDirect
    在解码时,是否倾向与使用堆外内存。


MessageToByteEncoder是outbound处理器,只需 wrtie 事件做处理。


6e32c86b2ce3c073d03543a789a4cfe1.png

代码@1:如果待处理的对象类型符合该编码器期待的类型,则对数据进行编码,否则直接调用ctx.write方法@6,传播write事件。

代码@2:根据是否使用堆外内存,使用内存分配器分配堆内存或堆外内存, 其bufer默认的大小为256字节。

代码@3:根据协议将数据编码到ByteBuf中,由协议设计者去实现。

代码@4:对输入参数进行回收因为经过该方法的处理,已经将输入参数转换为其他形式的数据,该数据的生命周期结束了,尝试回收(引用计数法)。

代码@5:如果byteBuf可读,则将这些数据传播到下一个事件处理器处理。

代码@7:对内存进行回收。

相关文章
|
6月前
|
编解码
Netty Review - 优化Netty通信:如何应对粘包和拆包挑战_自定义长度分包编解码码器
Netty Review - 优化Netty通信:如何应对粘包和拆包挑战_自定义长度分包编解码码器
92 0
|
6月前
|
编解码 JSON 网络协议
Netty使用篇:Http协议编解码
Netty使用篇:Http协议编解码
|
编解码 网络协议 Java
搞懂Netty(3)使用MessagePack解决编解码问题
使用Netty主要是为了进行网络通信,而网络通信就要涉及到传输数据,数据是不能直接传递的,需要进行一系列处理。java序列化就是其中一种处理方式,但是由于各种各样的缺点,一般不会用,在这里我们介绍一个比较优秀的编码解码技术MessagePack。 这篇文章是我的《搞懂Netty》系列的第三篇,也是在前两篇文章的基础之上进行讲解的。我们使用的是Springboot整合的Netty。
232 0
搞懂Netty(3)使用MessagePack解决编解码问题
|
消息中间件 编解码 移动开发
Netty常用招式——ChannelHandler与编解码(二)
Netty常用招式——ChannelHandler与编解码(二)
178 0
Netty常用招式——ChannelHandler与编解码(二)
|
存储 编解码 网络协议
Netty常用招式——ChannelHandler与编解码(一)
Netty常用招式——ChannelHandler与编解码(一)
218 0
Netty常用招式——ChannelHandler与编解码(一)
|
XML 存储 编解码
如何修正Netty编解码的缺陷
如何修正Netty编解码的缺陷
125 0
如何修正Netty编解码的缺陷
|
编解码 数据安全/隐私保护
netty之编解码
  1、netty的编码和解码,在数据传输的时候,考虑数据安全,数据完整性都是很有必要的。这里主要是介绍netty3和netty5的编解码方式。其实从StringEncoder和StringDecoder中也可以获取源码的编解码规则。
1014 0
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
13497 1
|
6月前
|
消息中间件 Oracle Dubbo
Netty 源码共读(一)如何阅读JDK下sun包的源码
Netty 源码共读(一)如何阅读JDK下sun包的源码
127 1
|
11月前
|
NoSQL Java Redis
跟着源码学IM(十二):基于Netty打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。 原本打算做个多人斗地主练习程序,但那需要织入过多的业务逻辑,因此一方面会带来不必要的理解难度,让案例更为复杂化,另一方面代码量也会偏多,所以最终依旧选择实现基本的IM聊天程序,既简单,又能加深对Netty的理解。
160 1