在Node.js中,有三种I/O模型:BIO(Blocking I/O)、NIO(Non-blocking I/O)和AIO(Asynchronous I/O)。这些I/O模型有不同的实现方式和特点,适用于不同的应用场景。了解它们的区别和优缺点,对于我们编写高性能、高并发的Node.js应用程序非常重要。在本文中,我们将深入讲解Node.js中的这三种I/O模型,包括它们的定义、工作原理、适用场景、优缺点。
IO模型就是说用什么样的通道进行数据的发送和接收,Java 共支持3中网络变成 IO 模式:BIO、NIO、AIO。Java 中的 BIO、NIO 和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。我们在使用这些 API 的时候,不需要关系操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。
在讲 BIO、NIO、AIO 之前先回顾几个概念:同步与异步、阻塞与非阻塞、I/O模型。
同步和异步的区别最大在于异步的话调用者不需要等待结果处理,被调用者会通过回调等机制来通知调用者返回结果。
所有的系统 I/O 都分为两个阶段:等待就绪 和 操作。
举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用 CPU 的,是在“空等”;而真正的读操作的阻塞是使用 CPU 的,真正在“干活”,而且这个过程非常快,属于 memory copy,带宽通常在 1GB/s 级别以上,可以理解为基本不耗时。
如下几种常见 I/O 模型的对比:
以socket.read()为例子:
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成(一个客户端连接对于一个处理线程)。
BIO通信(一请求一应答)模型图如下(图源网络):
采用 BIO 通信模型 的服务队,通常由一个独立的 Acceptor
线程负责监听客户端的连接。我们一般通过在 while(true)
循环中服务端会调用 accept()
方法等待客户端连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待当前连接的客户端的操作执行完成,不过可以通过多线程来支持多个客户端的连接,如上图所示。
如果要让 BIO 通信模型 能够同时处理多个客户端的请求,就必须使用多线程(要原因是 socket.accept()
、 socket.read()
、 socket.write()
涉及的三个主要函数都是同步阻塞的),当一个连接在处理 I/O 的时候,系统是阻塞的,如果是单线程的必然就挂死在哪里。开启多线程,就可以让CPU去处理更多的事情。也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回给客户端,线程销毁。这就是典型的 一请求一应答通信模型。
其实这也是所有使用多线程的本质:
我们可以设想以下如果连接不做任何的事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。例如使用FixedTreadPool 可以有效的控制来线程的最大数量,保证来系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N可以远远大于M)。
我们再设想以下当客户端并发访问量增加后这种模型会出什么问题? 随着并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
read
操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源;BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,但程序简单理解。
同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到 多路复用器 selector 上,多路复用器轮询到连接有 IO 请求就进行处理。
它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性; 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
NIO 有三大核心组件:
整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。
我们从一个问题来总结:NIO 与 IO 的区别?
如果是在面试中来回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO,而 IO 流是阻塞 IO说起。然后可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。
Java NIO 使我们可以进行非阻塞 IO 操作。比如说,单线程中从通道读取数据到 buffer,同时可以继续做别的事情,当数据读取到 buffer 中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是日常,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO 的各种流是阻塞的,这意味这,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或数据完全写入。该线程在此期间不能再干任何事情了。
Buffer(缓冲区)
Buffer 是一个对象,它包含一些要写入或者要读出的数据。在 NIO 类库中加入 Buffer对象,体现了新库与原库 I/O的一个重要区别:
最常用的缓冲区是 ByteBuffer,ByteBuffer 提供流一组功能用于操作 byte 数组。除了 ByteBuffer 还有其他的一些缓冲区,事实上,每一种 Java 基本类型(除了 Boolean 类型)都对应有一种缓冲区。
Channel(通道)
通道是双向的,可读也可以写,而流的读写是单向的。无论读写,通道只能和 Buffer 交互。因为 Buffer,通道可以异步地读写。
Selectors(选择器)
选择器用于使用单线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。因此,为了提供系统效率选择器是有用的。
通常来说 NIO 中的所有 IO 都是从 Channel(通道)开始的。
数据读取和写入操作如下:
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器、弹幕系统、服务器间通讯、编程比较复杂。
NIO 模型的 selector 就像一个大总管,负责监听各种 I/O 事件,然后转交给后端线程去处理。
NIO 相对于 BIO 非阻塞的体现就在:BIO 的后端线程需要阻塞等待客户端写数据(比如 read 方法),如果客户端不写数据线程就要阻塞。
NIO 把等到客户端操作的时候交给了大总管 selector ,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有就是 channel 的读写是非阻塞的。
Redis 就是典型的 NIO 线程模型,selector 收集所有的事件并且转给后端线程,线程连续执行所有事件命令并将结果写回客户端。
异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用。
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的)
AIO 方式适用于连接数目多且连接比较长(重操作)的架构。
本文作者:BARM
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!