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 作为其网络层

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

buf.retain() ,buf 引用计数加1

buf.release()  ,buf引用计数减1

当引用计数为0时 释放对象,并返回对象池。

ByteBuf引用计数的原则是:谁最后使用,谁负责释放

 

Netty提供了检查内存泄漏的方式,通过配置JVM 的leakDetectionLevel 可以开启指定级别的泄漏检测

默认是简单级别,它会抽样百分之1的样本,并告诉我们是否发生内存泄漏。

高级级别可以告诉我们内存泄漏发生的地方。

偏执级别会检测所有样本。

 

 

参考资料:

Netty官网

Netty的架构与原理初探

理解高性能网络模型

Netty试题

Netty实战精髓

Netty之有效规避内存泄漏

 

posted @ 2021-09-26 09:49  lancelee98  阅读(11335)  评论(0编辑  收藏  举报