开发者社区> 中间件兴趣圈> 正文

图文并茂剖析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:对内存进行回收。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23570 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
22326 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
19638 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
14729 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
36405 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
16298 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
14893 0
+关注
中间件兴趣圈
Apache RocketMQ社区首席布道师、《RocketMQ技术内幕》作者、维护公众号「中间件兴趣圈」,主打成体系地分享互联网主流中间件技术。目前已输出RocketMQ、Kafka、Dubbo、Sentienl等15个技术专栏共300+原创博文,3本原创电子书。
183
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载