Boost.Asio 是什么
Boost.Asio 是一个跨平台的 C++ 网络库,支持 socket、定时器、SSL 等 I/O 对象。理解它之前,需要先弄清两个词:Reactor 和 Proactor——它们是两类 I/O 事件处理模型,也是贯穿本系列的核心概念。
Reactor 与 Proactor:两种 I/O 模型
Reactor(反应器)——同步 I/O,采用”事件就绪”通知:
- 应用程序向事件分发器(select/poll/epoll)注册关注的 fd 和事件
- 分发器阻塞等待,某个 fd 就绪后返回
- 应用程序自己调用 read/write 完成实际的 I/O 操作
它通知你的是”可以读了”,读这个动作还是要你自己做。Linux 下的 nginx、Redis、libevent 都是典型的 Reactor 实现。
Proactor(前摄器)——异步 I/O,采用”操作完成”通知:
- 应用程序发起异步操作(如 async_read),同时交出缓冲区和完成回调
- 框架或内核替应用程序完成整个 I/O,数据直接落入应用缓冲区
- I/O 完成后,才回调应用程序处理
它通知你的是”已经读完了”,你拿到手的就是数据本身。Windows 的 IOCP 是原生 Proactor 实现,Linux 新的 io_uring 也走向了真正的异步。
一句话对比:
| 对比项 | Reactor | Proactor |
|---|---|---|
| 通知内容 | 事件就绪(可以读了) | 操作完成(已经读好了) |
| 谁执行 I/O | 应用程序自己 | 内核/框架代办 |
| 缓冲区分配 | 就绪之后再分配 | 发起时预先提供 |
| 典型实现 | epoll + nginx/Redis | Windows IOCP、io_uring |
两种模型在编程实践中的具体差异,本站的《Boost.Asio:同步 vs 异步》有更深入的讨论。
Boost.Asio 怎么实现的
理解了两种模型的差别,再看 Boost.Asio 的实现选择就顺理成章了。Linux 下高性能网络库大多使用 Reactor 模式去实现,而 Boost.Asio 在 Linux 下用 epoll 和 select 去模拟 Proactor 模式,这影响了它的效率和实现复杂度。为什么 Boost.Asio 坚持使用 Proactor 模式呢?借用知乎上陈硕的回答来解释:
Windows 下很难实现高效可伸缩的 Reactor。首先,Win32 API 里 WaitForMultipleObjects 只能同时等待 64 个 handle (MAXIMUM_WAIT_OBJECTS);其次 WinSock 的 select() 实现又很 buggy,特别是在错误处理方面有很多奇葩行为(具体见各种跨平台网络库代码中对此的注释);最后,Windows Vista 新增的 WSAPoll() 函数与 POSIX 的 poll() 又不尽兼容(http://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/)。
Windows 有自己的一套高效异步IO模型(几乎等同于Proactor),同时支持文件IO和网络IO;但 Linux 只有高效的网络同步IO(epoll 之类的 io multiplexing 是同步的Reactor,且不支持磁盘文件),二者的高效IO编程模型从根本上不兼容(Windows 可以把网络事件发到 GUI 线程的事件队列中,有点类似 Reactor,但是似乎一个进程只能有一个 GUI 线程,因此在多核系统上其伸缩性受限)。
因此,ASIO 要想高效且跨平台,只能用 Proactor 模型了。不可避免地会在 Linux 上损失一点儿效率。
为什么 Boost.Asio 选择了 Proactor?
Proactor 也并非全无缺点,缺点就是内存占用比 Reactor 大:Proactor 需要先分配内存而后处理 IO,而 Reactor 是先等待 IO 而后分配内存。相对地,Proactor 却获得了 Zero-copy 的好处:因为内存已经分配好了,操作系统可以将接收到的网络数据直接从网络接口拷贝到应用程序内存,而无需经过内核中转。
了解了 Boost.Asio 的设计思想之后,可以进入本站的《Boost.Asio C++ 网络编程:实战精要》和《Boost.Asio 入门》开始动手实践;需要深入 API 细节时,再读《Boost.Asio 基本原理》。