网络 IO 的发展过程是随着 linux 的内核演变而变化,因此网络 IO 大致可以分为如下几个阶段:

  1. 阻塞 IO(BIO)
  2. 非阻塞 IO(NIO)
  3. IO 多路复用第一版(select/poll)
  4. IO 多路复用第二版(epoll)
  5. 异步 IO(AIO)

那么,BIO、NIO、AIO 有什么区别呢?

IO 模型

一个输入操作通常包括两个阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

阻塞非阻塞,同步异步,都是围绕这两个阶段所做的不同处理。

阻塞 IO 和非阻塞 IO 的区别

因为IO操作要分两步走,所以把从硬件到内核态这一阶段,是否发生阻塞等待,可以将网络分为阻塞 IO和非阻塞 IO。

举个例子:

BIO 在连接的IO读取上,无论是否真的有读/写发生,都需要阻塞住当前的线程,对于基于BIO实现的Server端,通常的实现方法都是用一个线程去accept连接,当连接建立后,将这个连接的IO读写放到一个专门的处理线程,所以当建立100个连接时,通常会产生1个Accept线程 + 100个处理线程。

NIO 比起 BIO 有了事件机制。

NIO通过事件来触发,这样就可以实现在有需要读/写的时候才处理,不用阻塞当前线程。

NIO在处理IO的读写时,当从网卡缓冲区读或写入缓冲区时,这个过程是串行的,所以用太多线程处理IO事件其实也没什么意义,连接事件由于通常处理比较快,用1个线程去处理就可以。

IO事件呢,通常会采用cpu core数+1或cpu core数 * 2,这个的原因是IO线程通常除了从缓冲区读写外,还会做些比较轻量的例如解析协议头等,这些是可以并发的。

为什么不只用1个线程处理,是因为当并发的IO事件非常多时,1个线程的效率不足以发挥出多core的CPU的能力,从而导致这个地方成为瓶颈。

这种在分布式cache类型的场景里会比较明显,按照这个,也就更容易理解为什么在基于Netty等写程序时,不要在IO线程里直接做过多动作,而应该把这些动作转移到另外的线程池里去处理,就是为了能保持好IO事件能被高效处理。

从上面可以看出,对于大多数需要建立大量连接,但并发读写并不会同时的场景而言,NIO的优势是非常明显的。

同步 IO 和异步 IO 的区别

从前面我们知道了,数据的传递需要两个阶段,在此处只要任何一个阶段会阻塞用户请求,都将其称为同步 IO,两个阶段都不阻塞,则称为异步 IO。

在目前所有的操作系统中,linux 中的 epoll、mac 的 kqueue 都属于同步 IO,因为其在第二阶段(数据从内核态到用户态)都会发生拷贝阻塞。 而只有 windows 中的 IOCP 才真正属于异步 IO,即 AIO。

总结

  • BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 
  • AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。