文章目录
- Pre
- I/O 的编程模型
- 数据的传输和转化成本
- 数据结构运用
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 问题的时候,还有一个重点问题要注意,就是数据结构的运用。