netty网络库分析

Netty特点

     一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持使

    ( 这里首先就要搞清楚异步的NIO框架是什么意思)

      用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方  式。

      采用多种decoder/encoder 支持,(后面我们会举例说明)

      对TCP粘包/分包进行自动化处理(后面也会演示说明)

      可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持可配置IO线程数、TCP参数, TCP接收和发送缓冲区

     使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf通过引用计数器及时申请释放不再引用的对象,降低了GC频率使用单线程串行化的方式,高效的Reactor线程模型大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用

Netty如何解决Selector BUG:

在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在某些场景下select方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。

Netty的解决办法:对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N(512)次空轮询,则触发了epoll死循环bug。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

空转的次数可以通过可以在应用层通过设置系统属性io.netty.selectorAutoRebuildThreshold传入

Netty优势:(为什么要使用网络框架)

  1. 对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱
  2. 简化了NIO的处理方式。采用多种decoder/encoder 支持,
  3. 对TCP粘包/分包进行自动化处理可使用接受/处理线程池。
  4. 提高连接效率,对重连、心跳检测的简单支持。
  5. 可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf。
  6. 通过引用计数器及时申请释放不再引用的对象,降低了GC频率。
  7. 使用单线程串行化的方式,高效的Reactor线程模型
  8. 大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用。

Netty线程模型

Netty通过Reactor模型基于多路复用器接收并处理用户请求

由于是基于reactor模型说明具有两个线程租:

  1. boos线程组:负责监听,处理所有accept事件
  2. Work线程组:负责请求的read和write事件,由对应的Handler处理

单线程模型:

所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。

多线程模型:

有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。

主从多线程模型:

Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;

Netty核心组件:

Bootstrap

Bootstrap 是 Netty 提供的一个便利的工厂类,可以通过它来完成 Netty 的客户端或服务器端的 Netty 初始化。

当然,Netty 的官方解释说,可以不用这个启动器。但是,一点点去手动创建channel 并且完成一些的设置和启动,会非常麻烦。还是使用这个便利的工具类,会比较好。

有两个启动器,分别应用在服务器和客户端。

服务器:ServerBootstrap

客户端:Bootstrap

        netty的辅助启动器,netty客户端和服务器的入口,Bootstrap是创建客户端连接的启动器,ServerBootstrap是监听服务端端口的启动器,是程序的入口。

Channel(通道)

serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);client.group(group).channel(NioSocketChannel.class)
关联jdk原生socket的组件,常用的是NioServerSocketChannel和NioSocketChannel,NioServerSocketChannel负责监听一个tcp端口有连接进来通过boss reactor创建一个NioSocketChannel将其绑定到worker reactor,然后worker reactor负责这个NioSocketChannel的读写等io事件。

     Channel是Netty的核心概念之一,它是Netty网络通信的主体由它负责同对端进行网络通信、注册和数据操作等功能。

  1. (一旦用户端连接成功,将新建一个channel同该用户端进行绑定
  2. channel从EventLoopGroup获得一个EventLoop,并注册到该EventLoop,channel生命周期内都和该EventLoop在一起(注册时获得selectionKey)
  3. channel同用户端进行网络连接、关闭和读写,生成相对应的event(改变selectinKey信息),触发eventloop调度线程进行执行
  4. 如果是读事件,执行线程调度pipeline来处理用户业务逻辑

EventLoop 

   EventLoopGroup bossGroup = new NioEventLoopGroup(10); // 创建接收线程池
   EventLoopGroup workerGroup = new NioEventLoopGroup(20); // 创建工作线程池

netty最核心的几大组件之一,就是我们常说的reactor,人为划分为boss reactor和worker reactor。

通过EventLoopGroup(Bootstrap启动时会设置EventLoopGroup)生成,最常用的是nio的NioEventLoop,就如同EventLoop的名字,EventLoop内部有一个无限循环,维护了一个selector,处理所有注册到selector上的io操作,在这里实现了一个线程维护多条连接的工作。

在Netty 中,每一个 channel 绑定了一个thread 线程。

  • 一个 thread 线程,封装到一个 EventLoop ,多个EventLoop ,组成一个线程组 EventLoopGroup。
  • 反过来说,EventLoop 这个相当于一个处理线程,是Netty接收请求和处理IO请求的线程。 EventLoopGroup 可以理解为将多个EventLoop进行分组管理的一个类,是EventLoop的一个组。

ChannelPipeline (传递途径)

         在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系ChannelPipeline是ChannelHandler实例的列表(或则说是容器)用于处理或截获通道的接收和发送数据。ChannelPipeline提供了一种高级的截取过滤器模式,让用户可以在ChannelPipeline中完全控制一个事件及如何处理ChannelHandler与ChannelPipeline的交互。

 

可以这样说,一个新的通道就对应一个新的ChannelPipeline并附加至通道。一旦连接,通道Channel和ChannelPipeline之间的耦合是永久性的。通道Channel不能附加其他的ChannelPipeline或从ChannelPipeline分离。

netty最核心的几大组件之一,ChannelHandler的容器,netty处理io操作的通道,与ChannelHandler组成责任链。write、read、connect等所有的io操作都会通过这个ChannelPipeline,依次通过ChannelPipeline上面的ChannelHandler处理,这就是netty事件模型的核心。ChannelPipeline内部有两个节点,head和tail,分别对应着ChannelHandler链的头和尾。

 

ChannelHandler

netty最核心的几大组件之一,netty处理io事件真正的处理单元,可以创建自己的ChannelHandler来处理自己的逻辑,完全控制事件的处理方式。ChannelHandler和ChannelPipeline组成责任链,使得一组ChannelHandler像一条链一样执行下去。ChannelHandler分为inBound和outBound,分别对应io的read和write的执行链。ChannelHandler用ChannelHandlerContext包裹着,有prev和next节点,可以获取前后ChannelHandler,read时从ChannelPipeline的head执行到tail,write时从tail执行到head,所以head既是read事件的起点也是write事件的终点,与io交互最紧密。

ByteBuf数据容器 堆缓冲区和直接缓冲区

       因为所有的网络通信都涉及字节序列的移动,所以高效易用的数据结构明显是必不可少的。 Netty 的 ByteBuf 实现满足并超越了这些需求。让我们首先来看看它是如何通过使用不同的索引 来简化对它所包含的数据的访问的吧。

ByteBuf指针处理过程

      ByteBuf提供了两个指针来支持顺序读取和写入:readerIndex()用来读操作,writerIndex()用来写操作。下图展示了一个buffer是如何通过2个指针来划分为3个区域的:

+----------------+-------------+--------------+      

| 可丢弃字节  |  可读字节 |  可写字节 |      

|                     |  (内容)      |                 |      

+-----------------+--------------+------------+    

|                      |                   |               |      

0      <=  读索引   <=  写索引 <=   容量

 

         可丢弃的字节这个区域包含了读操作已经读过了的字节。初始化时该区域的容量为0,但当读操作进行时它的容量会逐渐达到写索引。通过调用discardReadBytes()方法来声明不用区域,如下图描述所示:

discardReadBytes()方法前:

+--------------------------+------------------------+------------------+    

discardable bytes   | readable bytes   | writable  bytes  |     

+--------------------------+------------------------+----------------------+                     

   |                                |                            |                         |

   0    <=    readerIndex  <=  writerIndex   <=   capacity

 

discardReadBytes()方法后

+-----------------------+--------------------------------------+    

|  readable bytes   |   writable bytes (got more space)   

| +-----------------------+--------------------------------------+    

|                  |                                      |

 

readerIndex (0) <= writerIndex (decreased)   <=       capacity

清除buffer索引你可以通过调用clear()方法来设置readerIndex()和writerIndex()的值为0.clear()方法并没有清除buffer中的内容而仅仅是将两个指针的值设为0.请注意:ByteBuf的clear()方法的语法和ByteBuffer的clear()操作时完全不同的。

clear()调用前

+----------------------------+-----------------------+----------------------+    

  discardable bytes   | readable bytes   |  writable bytes  |    

  +--------------------------+------------------------+----------------------+                      

   |                                |                           |                  |       

 0     <=    readerIndex  <=  writerIndex   <=   capacity

clear()调用后

+------------------------------------------------------------------+                 

            writable bytes (got more space)                |     

+------------------------------------------------------------------+                                                         

 |                                                                                |

 0 =    readerIndex =    writerIndex      <=    capacity

ByteBuf的特点

(1)它可以被用户自定义的缓冲区类型扩展;

  1. 通过内置的复合缓冲区类型实现了透明的零拷贝;
  2. 容量可以按需增长(类似于 JDK 的 StringBuilder);
  3. 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法
  4. 读和写使用了不同的索引;
  5. 支持方法的链式调用;
  6. 支持引用计数;
  7. 支持池化

Heap Buffer 堆缓冲区

这是最常用的类型,ByteBuf将数据存储在JVM的堆空间,通过将数据存储在数组中实现的。

(1)堆缓冲的优点是:由于数据存储在JVM的堆中可以快速创建和快速释放,并且提供了数组的直接快速访问的方法。

(2)堆缓冲缺点是:每次读写数据都要先将数据拷贝到直接缓冲区再进行传递。

Direct Buffer 直接缓冲区

Direct Buffer在堆之外直接分配内存,直接缓冲区不会占用堆的容量。jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处

  1. 通过免去中间交换的内存拷贝, 提升IO处理速度;
  2. 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外

Direct Buffer的优点是:

  1. 在使用Socket传递数据时性能很好,由于数据直接在内存中,不存在从JVM拷贝数据到直接缓冲区的过程,性能好。
  2. DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响。

缺点是:

     因为Direct Buffer是直接在内存中,所以分配内存空间和释放内存比堆缓冲区更复杂和慢。

Composite Buffer 复合缓冲区

      这个是netty特有的缓冲类型。复合缓冲区就类似于一个ByteBuf的组合视图,在这个视图里面我们可以创建不同的ByteBuf(可以是不同类型的)。 这样,复合缓冲区就类似于一个列表,我们可以动态的往里面添加和删除其中的ByteBuf,JDK里面的ByteBuffer就没有这样的功能。

Netty的零拷贝实现

      Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig创建ByteBufAllocator默认使用Direct Buffer

        ByteBuf buf = Unpooled.buffer(data.length) 
       可以点进去看一下实现的源码
     CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体。
      通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题
​​​​​​​      通过 wrap方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。

你可能感兴趣的:(网络库)