Netty原理浅析
⼀、Netty简介
1、Netty是异步的、基于事件驱动的⽹络应⽤框架,它以⾼性能、⾼并发著称。基于事件驱动,简单点说就是 Netty 会根据客户端的连接请求、读、写等事件做出相应的响应。
2、Netty 主要⽤于开发基于 TCP 协议的⽹络 IO 程序。例如构建⾼性能RPC,实现⾼性能服务器/客户端程序等等。同时Netty也⽀持UDP、HTTP、WebSocket等多种主流协议。
3、Netty 是基于 Java NIO 构建出来的,NIO是指⾮阻塞式IO,利⽤它可以提升并发能⼒
图1 是Netty的功能特性图
1、在传输服务⽅⾯:它⽀持TCP UDP传输; ⽀持HTTP 隧道等
2、在协议⽀持⽅⾯:它⽀持多种协议如HTTP WebSocket。并且它提供了⼀些开箱即⽤的协议例如可以⽤其提供的SSL ⽅便的进⾏认证与数据加密解密,利⽤其提供的zlib/gzip 可以⽅便的进⾏数据的压缩和解压缩,并且⽀持了google的protobuf序列化⽅式。并且⽀持⼤⽂件传输,实时的流传输
3、它的核⼼功能包括三⽅⾯:
3.1利⽤其提供的可拓展事件模型,我们可以⽅便的添加⾃⼰的业务逻辑
3.2利⽤其提供的通⽤通信API,我们可以告别java NIO 的繁琐复杂的代码
3.3⽀持零拷贝,零拷贝可以减少数据在内存中的拷贝,可以⼤幅提⾼IO性能
1、⾸先Netty可以⽤于分布式应⽤开发中,Netty 作为异步⾼并发的⽹络组件,常⽤于构建⾼性能 RPC 框架,以提升分布式服务之间的服务调⽤或数据传输的并发度和速度。例如阿⾥ Dubbo 就可以使⽤ Netty 作为其⽹络层reactor 原理
2、Netyy还可以⽤于⼤数据基础设施的构建:⽐如 Hadoop在处理海量数据的时候,数据在多个计算节点之中传输,为了提⾼传输性能,也采⽤ Netty 构建性能⾼的⽹络 IO 层
3、⽤Netyy还可以实现应⽤层基于公有协议或私有协议的服务器
⼆、Netty原理
零拷贝技术
1)  Netty 利⽤了零拷贝技术提升了IO 性能
2)  零拷贝指的是数据在内存中的拷贝次数为0次
3)图2 代表了磁盘中的⼀个数据发给⽹络的过程,如果不利⽤零拷贝磁盘的数据要先拷贝到内核缓冲区,再拷贝到应⽤程序内存,再拷贝到Socket缓冲区,最后再发向⽹络。不利⽤零拷贝,数据在内存中拷贝了两次,⼀次是内核缓冲区到⽤户程序内存,另⼀次是应⽤程序内存到Socket缓冲区。
⽽零拷贝技术,将内核缓冲区与应⽤程序内存和Socket缓冲区建⽴了地址映射,这样数据在内存中的拷贝次数就是0次,减少了拷贝次数,可以⼤幅提升IO性能。
1) Netty 是基于NIO的,NIO的特点是可以利⽤⼀个线程,并发处理多个连接也称为IO多路复⽤
2)图3是 NIO 的⽰意图,服务器中⼀个线程可以⾮阻塞地处理多个客户端的IO请求。具体过程为服务器为每个客户端分配Channel和Buffer,数据是通过通道 Channel 传输的,往Channel中读写数据需要先经过缓冲区Buffer。接着将每个客户端对应的Channel的IO事件注册到多路复⽤器 Selector上,Selector通过轮询,就可以到有IO活动的channel并进⾏处理,这就是NIO的具体流程。以这种IO处理模式也称为Reactor模式。
3)这种模式⾮阻塞的原因是:若某通道⽆可⽤数据,线程不会阻塞在这个通道上等数据准备好,⽽是可以处理其他通道的读写。⽽传统的阻塞式IO,采⽤⼀个线程对应⼀个客户端的⽅式,若客户端数据未
准备好,则线程⼀直阻塞。传统的阻塞式IO,线程利⽤率不⾼,且⾼并发是需要建⽴⼤量的线程。⽽NIO降低了线程数量,提⾼了线程的利⽤率实现了IO 多路复⽤。Netty 正是利⽤这种⾮阻塞式的IO,实现了单个线程就可以并发处理多个连接。
Channel 通道:
1)数据是通过通道传输的,它为应⽤提供I/O操作接⼝,定义了与socket交互的操作集⽐如读、写、连接、绑定等。
2)表1是⼀些常⽤的 Channel 类型,不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,,TCP连接中客户端和服务器⽤不同的Channel,linux下可以使⽤EpollSocketChannel建⽴⾮阻塞的TCP连接,它是⽤linux的epoll命令实现的效率更⾼。
1)ChannelHandler 通道处理接⼝:传递到通道的数据或者通道传来的数据要利⽤ChannelHandler进⾏处理,例如可以进⾏编码、解码、加密、解密等
2) Netty 中流向Chnannel的有两个⽅向的数据,⼊站数据指的是从⽹络发⾄客户端或者服务器的数据;出站数据指的是客户端或服务器发到⽹络中的数据。
3)因此也有两个⽅向的通道处理接⼝,ChannelInboundHanlder 继承⾃ChanelHandler 专门⽤于处理
⼊站数据
4) ChanneloutboundHandler 处理出站数据
5)编码器都继承了ChanneloutboundHandler 因为发向⽹络的数据⼀般要先经过编码,⽐如说要将对象转化成字节序列,再在⽹络中传输。解码器都继承了ChannelintboundHandler,因为需要将字节序列转化成对象。同理,加密继承于ChanneloutboundHandler,解密继承于ChannelintboundHandler。
1)数据处理链是包含多个ChannelHandler的双向链表。图5 是ChannelPipline的⽰意图,从⽹络中接收的数据从左边的Socket中传⼊ChannelPipline,⼊站的时候从链表头部,依次传⼊所有的ChannelInboundHandler中进⾏处理。出站的从链表尾部依次传⼊所有的CahnneloutboundHandler进⾏处理。
2、 ChannelPipeline其实就是⼀种⾼级形式的拦截过滤器。我们可以⽅便的增加删除ChannelPipline中的ChannelHanlder,也可以⾃⼰实现ChannelHandler,这样就能完全控制数据从⼊站到出战的处理⽅式,以及各个ChannelHandler 之间的相互交互⽅式。
1)⼀个事件循环对应⼀个线程,如图6所⽰,⼀个事件循环内维护了⼀个多路复⽤器,selector,和⼀个任务队列taskQueue。
2)服务器给每个客户端分配⼀个通道Channel,并将该通道的IO事件注册到Selector上,Selector ⽤于轮询各个Channel的IO事件
3)任务队列可以异步执⾏提交的IO任务与⾮IO任务任务,还可以执⾏定时任务,⽐如说我们可以利⽤任务队列,向给建⽴连接的客户端定时发消息。
如图6所⽰ EventLoop 其实就循环执⾏三件事情
1、轮询注册在selector上的channel的IO事件
2、在对应的Channel处理IO事件
3、执⾏任务队列中的任务
每个EventLoop可以负责处理多个Channel上的事件
⼀个Channel只对应于⼀个EventLoop (防⽌并发操作出现Bug)
5) EvenLoopGroup 事件循环组
EvenLoopGroup中含有多个的EventLoop
可以简单理解为⼀个线程池,内部维护了⼀组线程,
EvenLoopGroup 默认初始化 CPU核⼼数*2 个EventLoop
6) Bootstrap 引导类
⼀个Netty应⽤由⼀个Bootstrap开始,主要是⽤来配置整个 Netty 程序、设置业务处理类(Handler)、绑定端⼝、发起连接等
7) ChannelFuture 异步结果占位符
Netty的I/O操作是异步的,操作可能⽆法⽴即返回
ChannelFuture对象作为异步操作结果的占位符可确定异步执⾏的结果
通过addListener⽅法可注册了⼀个监听ChannelFutureListener,当操作完成时,⾃动触发注册的监听事件
图7 是Netty 服务端的⼯作架构图:该图中有两个事件循环组:BossGroup 和 WorkerGroup,BossGroup 中的事件循环专门和客户端建⽴连接,WorkerGroup 中的EventLoop专门负责处理连接上的读写。
在这⾥,我通过模拟⼀个客户端给服务器发消息来解释图7:
1、⾸先初始化ServerSocketChannel 并将建⽴连接的事件Accept,注册到BoosGroup的⼀个事件循环的Selector上
2、接着事件循环就会轮询Channel上的建⽴连接事件
3、⼀个客户端发来建⽴连接请求后,Seletor通过轮询可以发现此请求,并通过processSeleterKeys 处理处理连接请求
4、怎么处理连接请求呢?⾸先是为这个连接分配⼀个SocketChannel,并将这个Channel的读写事件注册到⼀个WorkerGroup的事件循环的selector上,这时连接就建⽴好了,并且WorkerGroup会轮询SocketChannel的读写事件。
5、当这个客户端再发送消息时,事件循环会轮询到写事件,并通过processSeleterKeys处理消息
6、processSeleterKeys通过刚刚讲的数据处理链过ChannelPipline来进⾏处理,可能包含先解码、再进⾏业务处理,再编码,再发送到SocketChannel中。
以上是服务端的具体流程,客户端也会建⽴⼀个Channel ,也有⼀个Seletor轮询IO事件,当消息到达时,也可以通过客户端的ChannelPipline进⾏处理。
到现在,我们已经⼤概了解了Netty的⼯作原理,BoosGroup ⽤于专门创建连接,其中有多个事件循环线程,每个事件循环都监听对应通道的建⽴连接请求并进⾏处理。WorkGroup 中也有多个事件循环线程,负责对应通道的IO事件。⼀个线程可以负责多个通道的IO,实现了IO 多路复⽤。
建⽴连接、IO处理都由多个线程去做,提⾼了并发能⼒,也提⾼了系统的可靠性(在之前的单线程处理IO的情况下若意外终⽌则服务不可⽤)。
三、ByteBuf和引⽤计数
Netty 利⽤ByteBuf作为缓冲区,利⽤Channel进⾏读写都要经过缓冲区,因此需要了解ByteBuf 的基本概念和操作才能更好的利⽤Netty编程。
ByteBuf  是存储字节的容器类似于NIO中的 ByteBuffer
ByteBuf 中存在
1、写索引: writerIndex  (当数据写⼊ByteBuf时 writerIndex增加)
2、读索引: readerIndex  (当从ByteBuf读数据时 readerIndex增加)
当writerIndex==readerIndex时:代表⽆数据可以读
capacity (ByteBuf的容量):默认为Integer.MAX_VALUE
因此可将ByteBuf 分为三个部分
1. 可以被丢弃字节
2. 可读字节
3. 可写字节
ByteBuf 共有三种使⽤模式
模式1:Heap Buffer(堆缓冲区)
它是将数据存储在JVM的堆空间(通过将数据存储在数组中实现)
堆缓冲区可以通过JVM快速分配与释放
模式2 :Direct Buffer 直接缓冲区
不在JVM的堆中分配内存,⽽是在JVM外通过本地⽅法调⽤分配虚拟机外内存
优点:免去中间交换的内存拷贝,提升IO处理速度:若在堆,则需要将数据先复制到直接缓冲区,再复制到堆这体现了Netty的零拷贝特性
模式3:Composite Buffer 复合缓冲区
是⼀种视图,不实际存数据,它可以由多个堆缓冲区和直接缓冲区复合组成
优点:可将消息拆分为多个部分,若某部分不变,则不⽤每次都分配新的缓冲区存不变的部分(向多个客户端发相同的消息body不变header变可以复⽤body)
有两种ByteBuf 分配⽅式,
1.⼀种是通过ByteBufAllcator类,它可以分配池化或者池化的ByteBuf实例,利⽤池化技术可以改进性能降低内存使⽤率。可以通过channel 和channelhandlercontext 获得该实例,代码如图11所⽰。
2.第⼆种分配⽅式是利⽤Unpooled类提供的静态⽅法,可以创建⾮池化的ByteBuf实例
上⾯我们讲到,ByteBuf可以利⽤直接内存避免拷贝数据到⽤户空间,并且Netty还使⽤池化技术降低内存使⽤率。因为⽤到了池化技
术,Netty需要将⽤完的对象放回池中,java的垃圾回收器⽆法完成此功能,因此引⼊了引⽤计数,将⽤完的对象放回池中。
如图所⽰每个对象的初始引⽤计数为1
当引⽤计数为0时释放对象,并返回对象池。
ByteBuf引⽤计数的原则是:谁最后使⽤,谁负责释放
Netty提供了检查内存泄漏的⽅式,通过配置JVM 的leakDetectionLevel 可以开启指定级别的泄漏检测
默认是简单级别,它会抽样百分之1的样本,并告诉我们是否发⽣内存泄漏。
⾼级级别可以告诉我们内存泄漏发⽣的地⽅。
偏执级别会检测所有样本。
参考资料:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。