2022-05-17
Node
0

目录

基本概念
同步与异步
阻塞和非阻塞
常见的 I/O 模型对比
BIO(Blocking I/O)
传统 BIO
缺点
应用场景
NIO(Non Blocking IO)
NIO核心组件
NIO的特性
IO流是阻塞的,NIO流不是阻塞的
IO 面向流(Stream oriented),NIO 面向缓冲区(Buffer oriented)
NIO 通过 Channel(通道)进行读写
NIO 有选择器,而 IO 没有
NIO 读数据和写数据
应用场景
总结
AIO(Asynchronous I/O)
应用场景
参考

在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 模型对比

所有的系统 I/O 都分为两个阶段:等待就绪 和 操作。

举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

需要说明的是等待就绪的阻塞是不使用 CPU 的,是在“空等”;而真正的读操作的阻塞是使用 CPU 的,真正在“干活”,而且这个过程非常快,属于 memory copy,带宽通常在 1GB/s 级别以上,可以理解为基本不耗时。

如下几种常见 I/O 模型的对比:

https://mmbiz.qpic.cn/mmbiz_png/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjU71A8l9b4J4MxvicXVh9YjUQ0tq4eqMGcKhEM2LFyme0bpv5othQXEYg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

以socket.read()为例子

  • 传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
  • 对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
  • AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。

NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

BIO(Blocking I/O)

同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成(一个客户端连接对于一个处理线程)。

传统 BIO

BIO通信(一请求一应答)模型图如下(图源网络):

https://mmbiz.qpic.cn/mmbiz_png/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjUYswQUd4ozJiaRLhP9pBS6J1s7hVrO2lLXkXb4y2UNDeLeGt4zL3kvlA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

采用 BIO 通信模型 的服务队,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待客户端连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待当前连接的客户端的操作执行完成,不过可以通过多线程来支持多个客户端的连接,如上图所示。

如果要让 BIO 通信模型 能够同时处理多个客户端的请求,就必须使用多线程(要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三个主要函数都是同步阻塞的),当一个连接在处理 I/O 的时候,系统是阻塞的,如果是单线程的必然就挂死在哪里。开启多线程,就可以让CPU去处理更多的事情。也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回给客户端,线程销毁。这就是典型的 一请求一应答通信模型

其实这也是所有使用多线程的本质:

  • 利用多核
  • 当 I/O 阻塞系统,但 CPU 空闲的时候,可以利用多线程使用 CPU 资源。

我们可以设想以下如果连接不做任何的事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。例如使用FixedTreadPool 可以有效的控制来线程的最大数量,保证来系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N可以远远大于M)。

我们再设想以下当客户端并发访问量增加后这种模型会出什么问题? 随着并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

缺点

  1. IO 代码里 read 操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源;
  2. 如果线程很多,会导致服务器线程太多,压力太大。

应用场景

BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,但程序简单理解。

https://mmbiz.qpic.cn/mmbiz_png/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjUzVFKibbJKQcm9KFvDibhXHl23532sttF5YCsPIS42YzPopibotficgy1LA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

NIO(Non Blocking IO)

同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到 多路复用器 selector 上,多路复用器轮询到连接有 IO 请求就进行处理。

它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。

  • 阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;
  • 非阻塞模式正好与之相反。

对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性; 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO核心组件

NIO 有三大核心组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。

https://mmbiz.qpic.cn/mmbiz_png/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjUzo9tBvUbAWmARt1Fq01PbZruZht5Gaico6y0iaD5Hf0THmqqLHhlv8IA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

  1. channel 类似于流,每个 channel 对应一个 buffer 缓冲区,buffer 底层就是个数组;
  2. channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理;
  3. selector 可以对应一个或多个线程
  4. NIO 的 Buffer 和 channel 既可以读也可以写

NIO的特性

我们从一个问题来总结:NIO 与 IO 的区别

如果是在面试中来回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO,而 IO 流是阻塞 IO说起。然后可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。

IO流是阻塞的,NIO流不是阻塞的

Java NIO 使我们可以进行非阻塞 IO 操作。比如说,单线程中从通道读取数据到 buffer,同时可以继续做别的事情,当数据读取到 buffer 中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是日常,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

Java IO 的各种流是阻塞的,这意味这,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或数据完全写入。该线程在此期间不能再干任何事情了。

IO 面向流(Stream oriented),NIO 面向缓冲区(Buffer oriented)

Buffer(缓冲区)

Buffer 是一个对象,它包含一些要写入或者要读出的数据。在 NIO 类库中加入 Buffer对象,体现了新库与原库 I/O的一个重要区别:

  • 在面向流的 I/O 中,可以直接将数据写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开通的扩展类,但只是流的包装类,还从流读到缓冲区。
  • NIO 是直接读到 Buffer 中进行操作。在 NIO 库中,所有的数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓存中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。

最常用的缓冲区是 ByteBuffer,ByteBuffer 提供流一组功能用于操作 byte 数组。除了 ByteBuffer 还有其他的一些缓冲区,事实上,每一种 Java 基本类型(除了 Boolean 类型)都对应有一种缓冲区。

NIO 通过 Channel(通道)进行读写

Channel(通道)

通道是双向的,可读也可以写,而流的读写是单向的。无论读写,通道只能和 Buffer 交互。因为 Buffer,通道可以异步地读写。

NIO 有选择器,而 IO 没有

Selectors(选择器)

选择器用于使用单线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。因此,为了提供系统效率选择器是有用的。

NIO 读数据和写数据

通常来说 NIO 中的所有 IO 都是从 Channel(通道)开始的。

  • 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据;
  • 从通道进行数据写入:创建一个缓冲区,填充数据,并要求通道写入数据。

数据读取和写入操作如下:

https://mmbiz.qpic.cn/mmbiz_jpg/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjUlexhrYibDtlno5kQhaRYD9mXrJ0hPrVzFh8oNgROlWPQ9od9DYQ0CEg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

应用场景

NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器、弹幕系统、服务器间通讯、编程比较复杂。

https://mmbiz.qpic.cn/mmbiz_png/x0aJCHEALOWFY8mMbsZ10A3HCohYKWjUEzOAgBicF9XBQOYLILtYVktZjtRBJdicggO4Y5mz4ibwFIjDCPd8Fa7dA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

总结

NIO 模型的 selector 就像一个大总管,负责监听各种 I/O 事件,然后转交给后端线程去处理。

NIO 相对于 BIO 非阻塞的体现就在:BIO 的后端线程需要阻塞等待客户端写数据(比如 read 方法),如果客户端不写数据线程就要阻塞。

NIO 把等到客户端操作的时候交给了大总管 selector ,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有就是 channel 的读写是非阻塞的。

Redis 就是典型的 NIO 线程模型,selector 收集所有的事件并且转给后端线程,线程连续执行所有事件命令并将结果写回客户端。

AIO(Asynchronous I/O)

异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用。

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的)

应用场景

AIO 方式适用于连接数目多且连接比较长(重操作)的架构。

参考

BIO 、NIO 、AIO 总结

本文作者:BARM

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!