Netty版本:4.1.17
Reactor模型是Doug Lea在《Scalable IO in Java》提出的,主要是针对NIO的。
其中的主从Reactor模式在Netty中的配置如下:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); 
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap(); 
serverBootstrap.group(bossGroup, workerGroup);基于此,我们来看下Netty是如何实现主从Reactor模式的。
EventLoop
EventLoop就是Netty中的Reactor,可以说它就是Netty的引擎,负责Channel上IO就绪事件的监听,IO就绪事件的处理,异步任务的执行驱动着整个Netty的运转。
Netty支持不同IO模型下,EventLoop有着不同的实现,我们只需要切换不同的实现类就可以完成对NettyIO模型的切换。
| BIO | NIO | AIO | 
|---|---|---|
| ThreadPerChannelEventLoop | NioEventLoop | AioEventLoop | 
EventLoopGroup
Netty中的Reactor是以Group的形式出现的,EventLoopGroup正是Reactor组的接口定义,负责管理Reactor,Netty中的Channel就是通过EventLoopGroup注册到具体的Reactor上的。
Netty的IO线程模型是主从Reactor多线程模型,主从Reactor线程组在Netty源码中对应的其实就是两个EventLoopGroup实例。
不同的IO模型也有对应的实现:
| BIO | NIO | AIO | 
|---|---|---|
| ThreadPerChannelEventLoopGroup | NioEventLoopGroup | AioEventLoopGroup | 
多种NIO的实现
| Common | Linux | Mac | 
|---|---|---|
| NioEventLoopGroup | EpollEventLoopGroup | KQueueEventLoopGroup | 
| NioEventLoop | EpollEventLoop | KQueueEventLoop | 
| NioServerSocketChannel | EpollServerSocketChannel | KQueueServerSocketChannel | 
| NioSocketChannel | EpollSocketChannel | KQueueSocketChannel | 
我们通常在使用NIO模型的时候会使用Common列下的这些IO模型核心类,Common类也会根据操作系统的不同自动选择JDK在对应平台下的IO多路复用技术的实现。
而Netty自身也根据操作系统的不同提供了自己对IO多路复用技术的实现,比JDK的实现性能更优。比如:
-  JDK的 NIO默认实现是水平触发,Netty 是边缘触发(默认)和水平触发可切换。
-  Netty 实现的垃圾回收更少、性能更好。 
我们编写Netty服务端程序的时候也可以根据操作系统的不同,采用Netty自身的实现来进一步优化程序。做法也很简单,直接将上图中红框里的实现类替换成Netty的自身实现类即可完成切换。
由此,可以看到,我们使用Common下的NIO实现时,在Linux环境下,会自动使用水平触发的epoll。
PS: 在NIO模型下Netty会自动根据操作系统以及版本的不同选择对应的IO多路复用技术实现。比如Linux 2.6版本以上用的是Epoll,2.6版本以下用的是Poll,Mac下采用的是Kqueue。
水平触发和边缘触发看附录二。
附录一:Netty如何根据操作系统选择JDK对应Selector的实现
 
Netty中,是通过SelectorProvider来根据操作系统的不同选择JDK在不同操作系统版本下的对应Selector的实现。Linux下会选择Epoll,Mac下会选择Kqueue。
SelectorProvider是在前面介绍的NioEventLoopGroup类构造函数中通过调用SelectorProvider.provider()被加载,并通过NioEventLoopGroup#newChild方法中的可变长参数Object... args传递到NioEventLoop中的private final SelectorProvider provider字段中。
SelectorProvider的加载过程:
    private static class Holder {static final SelectorProvider INSTANCE = provider();@SuppressWarnings("removal")static SelectorProvider provider() {PrivilegedAction<SelectorProvider> pa = () -> {SelectorProvider sp;if ((sp = loadProviderFromProperty()) != null)return sp;if ((sp = loadProviderAsService()) != null)return sp;return sun.nio.ch.DefaultSelectorProvider.get();};return AccessController.doPrivileged(pa);}...从SelectorProvider加载源码中我们可以看出,SelectorProvider的加载方式有三种,优先级如下:
-  通过系统变量 -D java.nio.channels.spi.SelectorProvider指定SelectorProvider的自定义实现类全限定名。通过应用程序类加载器(Application Classloader)加载。
-  通过 SPI方式加载。在工程目录META-INF/services下定义名为java.nio.channels.spi.SelectorProvider的SPI文件,文件中第一个定义的SelectorProvider实现类全限定名就会被加载
-  如果以上两种方式均未被定义,那么就采用 SelectorProvider系统默认实现sun.nio.ch.DefaultSelectorProvider。不同操作系统中JDK对于DefaultSelectorProvider会有所不同,可以看到,当我们在Mac下打开DefaultSelectorProvider的源码时,可以发现DefaultSelectorProvider自动适配了KQueue实现,为:KQueueSelectorProvider。在Windows下,DefaultSelectorProvider自动适配了Epoll实现,为WEPollSelectorProvider
附录二:再谈水平触发和边缘触发
网上有大量的关于这两种模式的讲解,大部分讲的比较模糊,感觉只是强行从概念上进行描述,看完让人难以理解。所以在这里,笔者想结合上边epoll的工作过程,再次对这两种模式做下自己的解读,力求清晰的解释出这两种工作模式的异同。
经过上边对epoll工作过程的详细解读,我们知道,当我们监听的socket上有数据到来时,软中断会执行epoll的回调函数ep_poll_callback,在回调函数中会将epoll中描述socket信息的数据结构epitem插入到epoll中的就绪队列rdllist中。随后用户进程从epoll的等待队列中被唤醒,epoll_wait将IO就绪的socket返回给用户进程,随即epoll_wait会清空rdllist。
水平触发和边缘触发最关键的区别就在于当socket中的接收缓冲区还有数据可读时。epoll_wait是否会清空rdllist。
-  水平触发:在这种模式下,用户线程调用 epoll_wait获取到IO就绪的socket后,对Socket进行系统IO调用读取数据,假设socket中的数据只读了一部分没有全部读完,这时再次调用epoll_wait,epoll_wait会检查这些Socket中的接收缓冲区是否还有数据可读,如果还有数据可读,就将socket重新放回rdllist。所以当socket上的IO没有被处理完时,再次调用epoll_wait依然可以获得这些socket,用户进程可以接着处理socket上的IO事件。
-  边缘触发: 在这种模式下, epoll_wait就会直接清空rdllist,不管socket上是否还有数据可读。所以在边缘触发模式下,当你没有来得及处理socket接收缓冲区的剩下可读数据时,再次调用epoll_wait,因为这时rdlist已经被清空了,socket不会再次从epoll_wait中返回,所以用户进程就不会再次获得这个socket了,也就无法在对它进行IO处理了。除非,这个socket上有新的IO数据到达,根据epoll的工作过程,该socket会被再次放入rdllist中。
如果你在
边缘触发模式下,处理了部分socket上的数据,那么想要处理剩下部分的数据,就只能等到这个socket上再次有网络数据到达。
在Netty中实现的EpollSocketChannel默认的就是边缘触发模式。JDK的NIO默认是水平触发模式。
参考:聊聊Netty那些事儿之Reactor在Netty中的实现(创建篇)
聊聊Netty那些事儿之从内核角度看IO模型