Netty底层通讯原理(NIO)
Netty的高性能主要体现在其NIO机制,用户层依赖reactor线程模型实现,底层又依赖于系统层的select/epoll
Netty-server端初始化的时候会构造两个线程池(bossEventLoopGroup,workEventLoopGroup),基于reactor线程池,原理如下:
bossEventLoopGroup根据参数指定启用多少个eventLoop来接收客户端连接,一个eventLoop就是对应一个线程。eventLoop会启用selector选择器,注册端口的connect事件,只要工作就是鉴权,三次握手,然后将accept()的channel注册到workEventLoopGroup中的一个eventLoop上,给这个channel在该workEventLoop的selector上注册读写事件。
workEventLoopGroup机制和boss差不多,也是根据初始化参数构造指定eventLoop,但是主要的业务处理是这个工作线程来处理的。
Netty的通讯方式是基于NIO实现,根据selector.select()方法获取注册的感兴趣的fd就绪事件,
selecor.select()方法默认是阻塞的(内核阻塞,如果有fd事件就绪会唤醒读取返回),如果select方法传了时间参数,则会在指定时间后返回。
Netty的底层通讯,获取就绪fd事件,也有多种实现机制,比如select/poll/epoll
select/poll
select的原理如下:
用户程序发起读操作后,将阻塞查询读数据是否可用,直到内核准备好数据后,用户程序才会真正的读取数据
服务端建立每个连接,相当于打开文件,会获得对应的文件描述符(fd),相同的源IP/源端口/目标IP/目标端口对应同一个fd。
select和poll是相似的,不一样的地方是,select是使用数组,有连接数限制,而poll使用链表,无连接数限制。
监听连接时,从用户层的角度看,
(1)会构建3个fd数组,分别关注读/写/异常事件,设置超时时间,调用系统提供的select方法。
(2)调用select方法时,需要将fd数组传到内核态,等待部分fd就绪后,把fd数组(包含就绪状态)返回到用户态
(3)用户程序对fd数组进行遍历,处理就绪的fd
(4)重新调用select方法。
可以看出不好的地方是
(1)每次都要传入fd数组,返回整个fd数组,导致了大量在用户空间和内核空间的相互拷贝。
(2)用户程序仍需要遍历fd数组才能找出就绪的fd
从系统层的角度看,调用select方法时
(1)遍历fd数组,对于每个fd,调用其对应的poll方法(由设备对应的驱动程序实现),将fd所在线程加入等待队列,并且检查就绪状态,记录感兴趣的就绪状态。
(2)如果存在感兴趣的就绪状态,直接返回
(3)如果不存在感兴趣的就绪状态,进入休眠,等待fd就绪后,会唤醒等待队列中的线程
(4)被唤醒后,重复1-4的操作。
可以看出不好的地方是每次都需要检查所有fd。
epoll
epoll原理图:
epoll使用“事件”的方式通知用户程序数据就绪,并且使用内存拷贝的方式使用户程序直接读取内核准备好的数据,不用再读取数据
epoll相对select改善了很多。
(1)在使用epoll时,首先会构建epoll对象。
(2)有连接接入时,会插入到epoll对象中,epoll对象里实际是一个红黑树+双向链表,fd插入到红黑树中,通过红黑树查找到是否重复
(3)一旦fd就绪,会触发回调把fd的插入到就绪链表中,并唤醒等待队列中的线程。
(4)调用epoll_wait方法时只需要检查就绪链表,如有则返回给用户程序,如没有进入等待队列。
由于epoll把fd管理起来,不需要每次都重复传入,而且只返回就绪的fd,因此减少了用户空间和内核空间的相互拷贝,在fd数量庞大的时候更加高效。
Netty可以选择使用不同的多路复用技术。
NioEventLoop
NioEventLoop底层会根据系统选择select或者epoll。如果是windows系统,则底层使用WindowsSelectorProvider(select实现多路复用;如果是linux,则使用epoll
当为select模式,在NioEventLoop对应的Selector中会维护着newKeys,updateKeys,cancelledKeys,分别是新增的fd,更新fd的感兴趣状态,取消fd监听。每当连接接入,或者断连,都会调用NioEventLoop的注册/解除注册方法,更新这几个集合。(这里是JDK11的实现,在JDK8中则直接更新PollArrayWrapper)
NioEventLoop在运行的时候,会不断的监听注册的连接,核心逻辑在doSelect方法中,主要的几个操作是
(1)processUpdateQueue,将newKeys,updateKeys,cancelledKeys中的key更新到PollArrayWrapper中
(2)subSelector.poll(),这里实际是调用native方法,会将PollArrayWrapper的fd拷贝至readFds/writeFds/exceptFds,并监听这些fd。
1 | private native int poll0(long pollAddress, int numfds, |
(3)updateSelectedKeys,遍历freadFds/writeFds/exceptFds,将就绪的fd存储到selectedKey中
当存在fd就绪后,doSelect方法返回,应用程序可以遍历selectedKey进行处理。
EpollEventLoop
EpollEventLoop底层使用epoll实现多路复用。
EpollEventLoop中会初始化epollFd、eventFd、timerFd。
- epollFd是调用系统方法生成的epoll对象,后续会使用其管理所有需要监听的fd
- eventFd是用于线程通讯,程序会把eventFd添加到epollFd中,监听eventFd,一旦eventFd有操作,则会唤醒调用epoll_wait的线程。
- timerFd是用于计时,同样的监听timerFd,一旦时间到达,则会唤醒调用epoll_wait的线程。
每当连接接入或者断连,都会调用epoll_ctl_add/epoll_ctl_del方法来操作epoll对象。
在EpollEventLoop的run方法中,会调用epoll_wait来监听所有fd,一旦有fd就绪,会拷贝至EpollEventArray中,应用程序遍历EpollEventArray处理所有就绪事件