网络 IO 流程
一次完整的网络 IO 操作,整体可以划分为数据准备 、数据读写拷贝两个核心阶段,所有网络读写行为都围绕这两个阶段展开。

日常开发中经常使用int size = recv(sockfd,buf,1024,0);函数读取套接字数据,该接口默认工作在阻塞读取模式下。函数执行后会返回实际读取到的数据字节大小,我们可以依靠返回值判断当前网络连接与数据状态。
如果主动将 socket 套接字设置为非阻塞模式,即便当前内核缓冲区没有任何待接收数据,recv函数也不会卡住线程,而是会立刻返回结果。针对返回值有一套固定判断逻辑:
size == -1:代表本次接收操作出现异常,需要进一步排查错误类型size == -1且errno == EAGAIN:属于正常业务状态,含义是当前远端暂无数据发送过来size == 0:代表通信对端主动关闭了网络连接size > 0:成功读取到有效数据,数值即为本次读取的数据长度

从数据流转层面来看,应用程序调用 recv 函数时,本质是从操作系统内核的 TCP 接收缓冲区中拉取数据,将内核缓冲区的数据不断拷贝存入我们自定义的应用层缓冲区 buf 当中。只要返回值大于 0,就说明缓冲区中已经存入有效数据,业务代码便可访问 buf 处理业务数据。
根据 IO 两个阶段的执行特性,又可以划分出同步 IO 与异步 IO两种模型。
同步 IO:当数据准备阶段完成、内核缓冲区数据就绪后,后续的数据读写拷贝动作,需要由应用程序主动执行完成,整个数据拷贝的耗时过程都会占用应用程序自身线程时间。
异步 IO :应用程序向操作系统发起 IO 请求之后,不会原地等待数据处理,转而继续执行自身业务逻辑。整个数据接收、数据存入缓冲区的过程全部交由操作系统内核全权处理,当内核完成全部数据读写工作后,会通过 SIGIO 信号等指定方式通知应用程序,告知数据处理完毕。典型的异步 IO 接口包含aio_read、aio_write等。
我们熟知的 Node.js 高性能服务框架,底层就是依托异步非阻塞的 IO 模式实现高并发处理。
这里存在极易混淆的知识点:代码层面的阻塞、非阻塞模式,本质上都归属于同步 IO 范畴。只有调用操作系统专门提供的异步类 API,才能算作真正意义上的异步 IO。日常高频使用的 epoll 多路复用技术,从 IO 模型本质上来说,依旧属于同步 IO。

除了内核 IO 层面的定义,从业务代码逻辑角度,也有同步、异步的通俗区分逻辑。 业务逻辑中的同步:A 执行某项操作后,必须暂停自身流程,等待 B 操作全部执行完毕并拿到返回结果,才能继续往后执行自身后续逻辑。 业务逻辑中的;
异步:A 只需提前告知 B 自身关注的事件类型、事件触发后的通知方式,之后 A 不会停留等待,直接继续运行自身业务代码。当 B 监测到对应事件发生后,再主动向 A 发送通知,A 收到通知后再执行对应的数据处理逻辑。
阻塞、非阻塞、同步、异步四个词汇,都是用来描述网络 IO 运行时的不同状态属性。结合 IO 两阶段再完整梳理一遍执行逻辑: 一次标准网络 IO 分为数据准备、数据读写拷贝两大阶段。数据准备阶段主要监测远端设备是否下发数据,判断内核 socket 对应的 TCP 接收缓冲区是否存在可读数据。
当 socket 文件描述符工作在阻塞模式时,一旦应用程序调用 recv 读取数据,若此时缓冲区数据尚未就绪,当前调用线程就会被阻塞挂起,无法执行其他任务。
当 socket 文件描述符工作在非阻塞模式时,调用 recv 函数不会阻塞线程,函数会立即给出返回结果,开发者依靠返回值就能判定当前数据状态。返回值大于 0 代表成功接收远端数据;返回值等于 0 代表对端断开网络连接;返回值为 - 1 且错误码匹配 EAGAIN,是非阻塞 IO 下无数据可读的正常反馈。
一旦远端数据完成传输、内核缓冲区数据就绪,应用程序再次发起 recv 调用就可以正常执行。数据会从内核缓冲区拷贝至应用层自定义缓冲区,整个拷贝过程会消耗应用程序线程资源,程序必须等待拷贝动作结束,recv 函数才会正式返回,这整套流程就是同步 IO 的运行逻辑。
而异步 IO 的执行逻辑截然不同,程序需要调用系统提供的异步IO接口(如aio_recv),传入套接字描述符、数据缓冲区、事件通知规则等参数,后续数据监测、数据接收、数据拷贝全部由内核独立完成。内核处理完全部 IO 流程后,再主动推送通知给到应用程序即可。
总结:
一个典型的网络 IO 接口调用,分为两个阶段,分别是 "数据就绪" 和 "数据读写",数据就绪阶段分为阻塞和非阻塞,表现得结果就是,阻塞当前线程或是直接返回。
同步表示 A 向 B 请求调用一个网络 IO 接口时(或者调用某个业务逻辑 API 接口时),数据的读写都是由请求方 A 自己来完成的(不管是阻塞还是非阻塞);异步表示 A 向 B 请求调用一个网络 IO 接口时(或者调用某个业务逻辑 API 接口时),向 B 传入请求的事件以及事件发生时通知的方式,A 就可以处理其它逻辑了,当 B 监听到事件处理完成后,会用事先约定好的通知方式,通知 A 处理结果。
四大 IO 模型
1. 同步阻塞
int size = recv(fd,buf,1024,0);
总结:线程原地死等数据,等到了自己亲自读取,全程卡住不动
逻辑:标准阻塞模式,没数据就卡死线程,直到网络数据到达
2. 同步非阻塞
int size = recv(fd,buf,1024,0);(仅修改 socket 为非阻塞模式)
总结 :线程不等待、立刻返回,需要自己反复调用recv轮询检查数据
逻辑:非阻塞但同步,没数据直接返回,自己不停问 "有没有数据"
3. 异步阻塞
总结 :逻辑矛盾,现实中完全不存在
逻辑:异步 = 内核通知(不让你等),阻塞 = 原地死等,二者天生冲突
4. 异步非阻塞
int size = recv(fd,buf,1024,0);(仅修改 socket 为异步非阻塞模式)
总结 :线程不等待、立刻去做别的事,内核监听数据,就绪后主动通知你,再调用recv读取
逻辑:最高效模式,代码和同步阻塞一致,靠模式区分,全程不浪费资源