文章目录

  • Pre
  • I/O 的编程模型
  • 数据的传输和转化成本
  • 数据结构运用

 

计网 - 网络 I/O 模型:BIO、NIO 和 AIO 有什么区别?_BIO、NIO 和 AIO


Pre

我们在处理网络问题时,经常是处理 I/O 问题——输入和输出。看上去很复杂,但说白了就是如何把网卡收到的数据给到指定的程序,然后程序如何将数据拷贝到网卡。

在处理 I/O 的时候,要结合具体的场景来思考程序怎么写。从程序的 API 设计上,我们经常会看到 3 类设计:BIO、NIO 和 AIO 。

从本质上说,讨论 BIO、NIO、AIO 的区别,其实就是在讨论 I/O 的模型,我们可以从下面 3 个方面来思考 。

  • 编程模型:合理设计 API,让程序写得更舒服。

  • 数据的传输和转化成本:比如减少数据拷贝次数,合理压缩数据等。

  • 高效的数据结构:利用好缓冲区、红黑树等


I/O 的编程模型

我们先从编程模型上讨论下 BIO、NIO 和 AIO 的区别。

BIO(Blocking I/O,阻塞 I/O),API 的设计会阻塞程序调用。比如:

byte a = readKey()

假设readKey方法从键盘读取一个按键,如果是非阻塞 I/O 的设计,readKey不会阻塞当前的线程。你可能会问:那如果用户没有按键怎么办?在阻塞 I/O 的设计中,如果用户没有按键线程会阻塞等待用户按键,在非阻塞 I/O 的设计中,线程不会阻塞,没有按键会返回一个空值,比如 null。

最后我们说说 AIO(Asynchronous I/O, 异步 I/O),API 的设计会多创造一条时间线。比如

func callBackFunction(byte keyCode) {

  // 处理按键

}

readKey( callBackFunction )

在异步 I/O 中,readKey方法会直接返回,但是没有结果。结果需要一个回调函数callBackFunction去接收。从这个角度看,其实有两条时间线。第一条是程序的主干时间线,readKey的执行到readKey下文的程序都在这条主干时间线中。而callBackFunction的执行会在用户按键时触发,也就是时间不确定,因此callBackFunction中的程序是另一条时间线也是基于这种原因产生的,我们称作异步,异步描述的就是这种时间线上无法同步的现象,你不知道callbackFunction何时会执行。

但是我们通常说某某语言提供了异步 I/O,不仅仅是说提供上面程序这种写法,上面的写法会产生一个叫作回调地狱的问题,本质是异步程序的时间线错乱,导致维护成本较高。

request("/order/123", (data1) -> {

  //..

  request("/product/456", (data2) -> {

    // ..

    request("/sku/789", (data3) -> {

      //...

    })

  })

})

比如上面这段程序(称作回调地狱)维护成本较高,因此通常提供异步 API 编程模型时,我们会提供一种将异步转化为同步程序的语法。比如下面这段伪代码:

Future future1 = request("/order/123")

Future future2 = request("/product/456")

Future future3 = request("/sku/789")

// ...

// ...

order = future1.get()

product = future2.get()

sku = future3.get()

request 函数是一次网络调用,请求订单 ID=123 的订单数据。本身 request 函数不会阻塞,会马上执行完成,而网络调用是一次异步请求,调用不会在request("/order/123")下一行结束,而是会在未来的某个时间结束。因此,我们用一个 Future 对象封装这个异步操作。future.get()是一个阻塞操作,会阻塞直到网络调用返回。

在request和future.get之间,我们还可以进行很多别的操作,比如发送更多的请求。 像 Future 这样能够将异步操作再同步回主时间线的操作,我们称作异步转同步,也叫作异步编程。


数据的传输和转化成本

上面我们从编程的模型上对 I/O 进行了思考,接下来我们从内部实现分析下 BIO、NIO 和 AIO。无论是哪种 I/O 模型,都要将数据从网卡拷贝到用户程序(接收),或者将数据从用户程序传输到网卡(发送)。另一方面,有的数据需要编码解码,比如 JSON 格式的数据。还有的数据需要压缩和解压。数据从网卡到内核再到用户程序是 2 次传输。注意,将数据从内存中的一个区域拷贝到另一个区域,这是一个 CPU 密集型操作。数据的拷贝归根结底要一个字节一个字节去做。

从网卡到内核空间的这步操作,可以用 DMA(Direct Memory Access)技术控制。DMA 是一种小型设备,用 DMA 拷贝数据可以不使用 CPU,从而节省计算资源。遗憾的是,通常我们写程序的时候,不能直接控制 DMA,因此 DMA 仅仅用于设备传输数据到内存中。不过,从内核到用户空间这次拷贝,可以用内存映射技术,将内核空间的数据映射到用户空间。


数据结构运用

在处理网络 I/O 问题的时候,还有一个重点问题要注意,就是数据结构的运用。