Netty 核心组件笔记

Netty是一款高效的NIO框架和工具,基于JAVA NIO提供的API实现。

在JAVA NIO方面Selector给Reactor模式提供了基础,Netty结合Selector和Reactor模式设计了高效的线程模型,Reactor模式的参与者主要有下面一些组件:

    

  1. Channel

  2. Selector
  3. EventLoopGroup/EventLoop

  4. ChannelPipeline

Channel

在网络IO方面,Channel的主要实现是ServerSocketChannel和SocketChannel。他们都代表一个面向流的可监听读写事件的socket。ServerSocketChannel是用于服务器端的socket,他提供了一个静态工具方法open来为用户提供获取Channel的工具:

public static ServerSocketChannel open() throws IOException {
    return SelectorProvider.provider().openServerSocketChannel();
}

ServerSocketChannel的使用方式是面向服务器端的,一般的开发流程是:

  1. 获取一个ServerSocketChannel。

  2. 设置网络操作,这些参数主要是和TCP协议有关。

  3. 将ServerSocketChannel注册到Selector(多路复用器)。

  4. 将ServerSocketChannel和某个具体的地址绑定。

  5. 用户像多路复用器设置感兴趣的IO事件。

  6. 用户线程以阻塞或非阻塞方式轮询Selector来查看是否有就绪的IO事件。

  7. 用户针对不同的IO事件对Channel进行具体的IO操作。

SocketChannel主要是面向客户端的开发的,也是以open方式获取channel,客户端的开发流程大致如下:

  1. 获取一个SocketChannel。

  2. 设置Channel为非阻塞方式。

  3. 获取Selector。

  4. 将channel注册到Selector,并监听CONNECT事件。

  5. 调用channel的connect方法连接指定的服务器和端口。

  6. 如果连接成功则进行IO操作,如果没成功则轮询Selector处理CONNECT事件。

Selector

Selector是JAVA NIO提供的SelectableChannel多路复用器,它内部维护着三个SelectionKey集合,负责配合select操作将就绪的IO事件分离出来,落地为SelectionKey,我前面有一篇文章的一部分对Selector进行了相对详细的介绍(这里)。

在Netty线程模型中,我认为Selector充当着demultiplexer的角色,而对于SelectionKey我们可以将它看成Reactor模式中的资源。

EventLoopGroup/EventLoop

EventLoopGroup是一组EventLoop的抽象,由于Netty对Reactor模式进行了变种,实际上为更好的利用多核CPU资源,Netty实例中一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例,类似单线程Reactor模式地工作着。

EventLoopGroup提供next接口,可以总一组EventLoop里面按照一定规则获取其中一个EventLoop来处理任务.

1. BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例,

2. BoosEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,

3. 然后将accept得到的SocketChannel(注意不是ServerSocketChannel)交给WorkerEventLoopGroup,

4. WorkerEventLoopGroup会由next选择其中一个EventLoopGroup来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件(OP_READ, OP_WRITE等)进行处理。

在Reactor模式中BossEventLoopGroup主要是对多线程的扩展,而每个EventLoop的实现涵盖IO事件的分离,和分发(Dispatcher)。

定时任务和一般任务

在Netty的EventLoop线程中,这个线程主要需要处理IO事件和其他两种任务,分别为定时任务和一般任务。Netty提供可一个参数ioRatio用于用户调整单线程对于IO处理时间和任务处理时间的分配的比率。这样根据实际应用场景用户可以对这个值进行调整,默认值是50,也就是这个线程会将处理IO的时间和处理任务的时间控制为1:1。

final long ioStartTime = System.nanoTime();

processSelectedKeys();//处理IO事件

final long ioTime = System.nanoTime() - ioStartTime;//处理IO事件的时间
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);//计算用于处理任务的时间

  

这样尽管一个EventLoop会关联多个Channel,这些Channel在单个线程下并不会出现并发问题, 而是串行的顺序执行,同时对于异步任务的处理也一样,Netty这样设计即免去了并发问题的烦恼,有减少了多线程上下文切换带来的性能损耗,同时基于EventLoopGroup实现的有限的线程数能够充分利用CPU处理能力。

ChannelPipeline

在Netty中ChannelPipeline维护着一个ChannelHandler的链表队列,每个SocketChannel都有一个维护着一个ChannelPipeline实例,而每个ChannelPipeline实例通常维护着一个ChannelHandler链表队列,由于SocketChannel是和SelectionKey关联的,也就是Reactor模式中的资源,当EventLoop将SelectionKey分离出来的时候会将SelectionKey关联的Channel交给Channel关联的ChannelHandler链来处理,那么ChannelPipeline其实是担任着Reactor模式中的请求处理器这个角色。

ChannelPipeline的默认实现是DefaultChannelPipeline,DefaultChannelPipeline本身维护着一个用户不可见的tail和head的ChannelHandler,他们分别位于链表队列的头部和尾部。tail在更上从的部分,而head在靠近网络层的方向。

在Netty中关于ChannelHandler有两个重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound可以理解为网络数据从外部流向系统内部,而outbound可以理解为网络数据从系统内部流向系统外部。用户实现的ChannelHandler可以根据需要实现其中一个或多个接口,将其放入Pipeline中的链表队列中,ChannelPipeline会根据不同的IO事件类型来找到相应的Handler来处理,同时链表队列是责任链模式的一种变种,自上而下或自下而上所有满足事件关联的Handler都会对事件进行处理.

其他扩展

select、poll和epoll

JAVA对NIO的支持是从1.4版本开始的,是基于多路复用技术,而在linux操作系统方面多路复用技术有三种常用的机制:select、poll和epoll,epoll的支持也只是linux2.6版本之后才提供,java在jdk5.0的update 9之后才对epoll进行支持。这三种机制本质上都是同步IO,主要是由于他们都需要在读写事件就绪的时候需要自己进行读写,也就是这个这个读写过程是阻塞的。下面对着三种机制进行简单总结:

  1. select函数:改函数允许进程指示内核等待多个事件中的任何一个发生的时候或者在一定时间之后被唤醒,select有个致命的缺点即在多路复用中文件描述符的数量有限制,如果需要突破限制需要重新编译操作系统内核。

  2. poll函数:poll机制与select机制类似,区别是poll没有最大描述符限制

  3. epoll函数:epoll在linux2.6内核中被提出来,是之前的select和poll的增强版本。epoll也没有文件描述符数量限制,而且是用一个文件描述符来管理多个描述符。在性能上相比上面两种有了很大的优化。

    epoll原理图如下:

  4. epoll使用“事件”的方式通知用户程序数据就绪,并且使用内存拷贝的方式使用户程序直接读取内核准备好的数据,不用再读取数据

ByteBuffer

JAVA NIO直接和Channel打交道的Buffer是ByteBuffer,ByteBuffer接口提供主要的内存分配、IO读写等相关接口。

值得注意的是JAVA NIO提供了两种Buffer内存分配机制,一种是堆内存,另一种是直接内存,主要区别:

  1. 堆内存分配和回收比较快,但是网络数据需要从内核copy到堆中。

  2. 直接内存分配和回收比较慢,但是免去了从内核copy到堆中的一次copy。

这两种内存各有千秋,使用的时候要根据实际情况去选择。

参考: https://my.oschina.net/andylucc/blog/614295

原文地址:https://www.cnblogs.com/snow-man/p/9968008.html