简介
在梳理各种事件模型时,我切身体会到基础知识掌握不牢的痛苦。因此,我打算整理一篇文章,把关键的前置知识系统记录下来,为理解后续更复杂的内容打好基础。
1. 什么是网络IO?
因为我的工作与数据缓存相关,所以提到IO我脑子里就蹦出来磁盘IO。这种深刻印象让我理解起后面的内容十分别扭,所以这里着重整理一遍网络IO加深印象。
- 网络 IO 指的是程序通过网络接口(通常是 socket)进行数据收发的过程。
- 简单说就是:程序和网络上的另一台机器交换数据。
- 在服务端和客户端通信场景中:
-
- 发送数据 → 写入 socket,传输到对方
- 接收数据 → 从 socket 读取对方发送的数据
可以理解为程序对网络数据的"读写操作":
读取数据(Receive / Read)
- 从网络缓冲区中获取客户端或远程主机发来的数据。
- 对服务端来说,就是"接收客户端请求"。
写入数据(Send / Write)
- 将数据写入网络缓冲区,由操作系统发送到远程主机。
- 对服务端来说,就是"给客户端发送响应"。
2. IO的两个阶段:数据就绪、数据读取
这两个阶段对所有IO操作适用,这里以网络IO为例.
2.1. 阶段一:数据就绪
这一阶段由 操作系统内核 负责。
- 当客户端(如浏览器)通过网络发送数据包时,这些数据首先会被操作系统接收到,并存放到 内核的 socket 接收缓冲区 中。
- 但对应用程序(例如服务器进程)来说,这时数据还"在内核里",还没被用户程序拿到。
- 如果应用程序此时调用
read()
去读取 socket:(以阻塞为例)
-
- 若数据还没到达,
read()
就会阻塞(等待数据就绪)。 - 一直到内核检测到数据到达,标记为"可读",这个阶段才算结束。
- 若数据还没到达,
总结:数据就绪阶段就是 等待数据到达网络并被内核接受好。(网络数据进入socket读缓冲区,缓冲区标记为"可读"态)
牛客的老师将分为数据就绪阶段分为阻塞和非阻塞两种方式。
也就是等待客户端发送的数据经过TCP协议栈,被内核接收完成的过程。
2.1.1. 阻塞
arduino
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
上面的recv函数通过调整sockfd(文件描述符)参数,可以设置recv为"阻塞"或"非阻塞"。
默认recv为阻塞。此时调用recv,若数据还未就绪,线程就会被挂起,一致等待数据准备好再返回。
2.1.2. 非阻塞
当recv为非阻塞时,如果发现数据还没就绪,直接返回。
2.2. 阶段二:数据读写
当内核通知"数据已就绪"之后:
- 应用程序再执行
read()
系统调用; - 操作系统将数据 从内核缓冲区拷贝到用户空间缓冲区(即应用程序可访问的内存中)。
- 拷贝完成后,应用程序才能真正处理这批数据。
总结:数据读写极端就是 从内核向用户空间传输数据的过程。 (从socket 读缓冲区读取数据,或者把数据写入socket写缓冲区)
老师将数据读写的机制分为了同步和异步两种。关键区别在于,数据是谁来搬运的?是我们的应用程序,还是操作系统?
2.2.1. 同步IO
内核准备好数据后,必须应用程序通过自己把数据读出来。
同步 I/O: 程序自己搬数据,得等着。
2.2.2. 异步IO
应用程序发起请求后,操作系统完成所有IO操作,包括数据拷贝。当整个过程完成后,内核再通知应用程序。再这期间应用程序完全可以做别的事情。
异步 I/O: 系统帮你搬数据,搬完再通知你。
2.3. 区分阻塞/非阻塞和同步/异步
老师有一句非常粗暴的话"阻塞、非阻塞都是同步IO,只有调用了系统异步IO的api函数的时候才是异步IO"。
但是老师又讲到 "异步IO一般和非阻塞组合使用,因为和阻塞使用没有意义。如果使用阻塞,内核处理数据的时候,应用程序仍然挂起,没有起到提高效率的效果。"
"异步IO一般和非阻塞组合使用"。看到这句话我完全懵了。
非阻塞不是同步吗?因为非阻塞本质上也是我们在主动recv,从socket那里面拿数据啊?
寻问AI后才获得答案:
- 真正的"异步 I/O"是
aio_read()
、io_uring
这种系统帮你全做完的。 - 但这种机制在早期 Linux 上不成熟,大部分网络服务器并不用它。
从实际工程角度来看:
- 我们通常想实现"异步处理"------线程不阻塞、一个线程能同时处理多个 socket。
- 所以我们采用:
-
- 把 socket 设置成 非阻塞;
- 用 epoll/select 等 I/O 复用 技术检测"哪些 socket 有事件";
- 一旦有事件就调用相应的 读写回调函数;
- 没数据就立即返回去干别的。
这整套机制(Reactor 模式)虽然在系统角度上是同步 I/O ,
但在框架层面实现了异步效果------应用不会被卡住。
我们通过"非阻塞 I/O + 事件驱动模型"模拟出了"异步"的行为效果,这其实是通过非阻塞+事件回调机制实现的 "伪异步"。