NIO(Non-blocking I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,是现今主流的大流量、高并发IO有效解决方案。

五种IO模型

在UNIX下,IO模式分为五类,分别是:阻塞式IO(bloking IO)、非阻塞式IO(non-blocking IO)、多路复用IO模型(multiplexing IO)、信号驱动IO模型(signal-driven IO)以及异步IO模型(asynchronous IO)。其中又以前三种模式最为常见。

5种IO模型对比图

传统的BIO模式

在阐述选择NIO的原因之前,首先说明一下阻塞和非阻塞的概念。阻塞和非阻塞的核心区别就在于,在IO就绪态(读就绪、写就绪、有新连接)到来之前是否会阻塞等待。

在最初的网络编程中,我们使用BIO模式构建编程模型,如下面的伪代码所示,这是经典的per thread per connection模型。这段代码的核心部分在于accept()、socket.read()、socket.write()三个函数,这三个函数在等待IO就绪态到来的过程中都将阻塞各自的线程。当连接数量达到一定程度之后,这样的阻塞、对线程资源的无效占用就变得不可容忍。后续的优化包括建立线程池,进行线程扩容,但这并没有根本解决问题。而NIO+多路复用的网络模型很好的解决了这个问题。

NIO + multiplexing IO

现今的高性能网络库基本采用了NIO+多路复用的模式构建,例如著名的netty,那么这是为什么呢?我们都知道,在网络IO最耗时的部分就在于等待IO就绪的过程,而真正的IO操作是一个高性能的过程,而NIO有一个重要的特点:socket的主要读、写、注册和接收函数,在等待就绪态前都是非阻塞的,只有在进行真正的IO操作时是同步阻塞的。结合多路复用带来的事件通知特性,就可以构建一套更高性能的网络模型。

读到这里你可能会有疑问,为什么是NIO + multiplexing IO,而不是BIO + multiplexing IO呢?以Linux下的Epoll举例,我们向Epoll的selector中注册一个socket并标示可读事件,当epoll_wait返回可读事件EPOLLIN到来,我们只知道socket可读,但不会知道有多少的数据可读,如果我们多次调用read函数将可能导致阻塞事件发生,所以如果是BIO+ multiplexing IO,我们必须每次read过后就马上返回epoll_wait,这种要求是苛刻的,在某些业务场景下也是不允许的,所以在实际的应用中,BIO + multiplexing IO的组合几乎不会出现。

golang的NIO

这是一个典型的Golang TCP Server示例

这段代码看起来和上文中的JAVA BIO代码很类似。那么这段Go代码里就绪态等待也会阻塞线程么?答案是并不会。 相比与java,golang应用直接调用的是更为轻量级的协程goroutine,当socket在进行就绪态等待的时候,会阻塞协程,但是并不会阻塞线程。 同时,golang的原生网络库底层同样实现了一套NIO + multiplexing IO的网络模型(netpoll),我们以Linux环境举例,在Linux下,netpoll的底层实现是Epoll, 我们的连接套接字被创建后会被设置为NO-BLOCK模式,而后加入到Epoll中进行监听,当读写就绪态到来之后,套接字阻塞的goroutine会被加入到可运行队列中,等待golang调度器的调度运行。

更多文章欢迎访问我的个人博客 http://fbelisk.github.io/