关于NIO编程与epoll、IOCP大家应该耳熟能详了,先简单回顾一下常见的结论:

NIO是非阻塞IO

epoll和IOCP分别是linux上和windows上对NIO操作系统级别的实现。

NIO单机支持的连接数比BIO要高很多,解决了C10K问题。

在获取通信数据时,NIO使用轮询的方式代替了阻塞的方式。但这样做是否效率更高呢?如果提高了,那原因是什么呢?在看了很多的资料后我对以上问题依然不是特别清晰。不过最近通过研究epoll与java selector的实现,有了一些新的理解。

java中selector的select方法依然是阻塞的,其内部调用的是不同操作系统中的实现,windows下是IOCP的poll0(...)方法,这是一个native方法,如下。

private native int poll0(long pollAddress, int numfds,
int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
// These arrays will hold result of native select().
// The first element of each array is the number of selected sockets.
// Other elements are file descriptors of selected sockets.
private final int[] readFds = new int [MAX_SELECTABLE_FDS + 1];//保存发生read的FD
private final int[] writeFds = new int [MAX_SELECTABLE_FDS + 1]; //保存发生write的FD
private final int[] exceptFds = new int [MAX_SELECTABLE_FDS + 1]; //保存发生except的FD

这个poll0()会监听pollWrapper中的FD有没有数据进出,这会造成IO阻塞,直到有数据读写事件发生。比如,由于pollWrapper中保存的也有ServerSocketChannel的FD,所以只要ClientSocket发一份数据到ServerSocket,那么poll0()就会返回;又由于pollWrapper中保存的也有pipe的write端的FD,所以只要pipe的write端向FD发一份数据,也会造成poll0()返回;如果这两种情况都没有发生,那么poll0()就一直阻塞,也就是selector.select()会一直阻塞;如果有任何一种情况发生,那么selector.select()就会返回,所有在OperationServer的run()里要用while (true) {},这样就可以保证在selector接收到数据并处理完后继续监听poll();

所以可以看出,NIO依然是阻塞式的IO,那么它和BIO的区别究竟在哪呢。

其实它的区别在于阻塞的位置不同,BIO是阻塞在read方法(recvfrom),而NIO阻塞在select方法。那么这样做有什么好处呢。如果单纯的改变阻塞的位置,自然是没有什么变化的,但epoll等的实现的巧妙之处就在于,它利用回调机制,让监听能够只需要知晓哪些socket上的数据已经准备好了,只需要处理这些线程上面的数据就行了。采用BIO,假设有1000个连接,需要开1000个线程,然后有1000个read的位置在阻塞,采用NIO编程,只需要1个线程,它利用select的轮询方法配合epoll的事件机制及红黑树数据结构,降低了其内部轮询的开销,同时极大的减小了线程上下文切换的开销。

那么在一秒钟内,epoll的epoll.wait方法究竟会轮询多少次呢。这个问题我目前没有得到直接的答案。我调用java的NIO库的selector的selectNow()方法,然后不做任何业务处理,只增加了一个index++的操作,计算得到的结果大致是1秒钟index增加40w,这说明了epoll.wait是一个很快的的操作(有资料也表明了O(1)时间复杂度),看来NIO确实很适合处理大量并发连接的情况。