为什么 C++ 没有一个“最好”的网络库?
写在前面
网络是计算机发展过程中最重要的基础设施之一。
从阻塞 Socket,到 select/poll,再到 epoll、kqueue、IOCP,以及近年来 Linux 上的 io_uring,操作系统提供网络能力的方式不断变化,而这些变化也反过来影响了编程语言和运行时的设计。
很多刚接触 C++ 网络开发的人都会产生一个疑问:
为什么 C++ 发展了这么多年,却始终没有一个公认的“最佳网络库”?
Java 服务端开发中有 Netty,Go 语言本身就提供 goroutine 和网络运行时,Rust 生态中 Tokio 也占据了非常重要的位置。
但 C++ 不同。
在 C++ 中,不仅网络库很多,甚至连最基本的线程模型和异步模型都没有统一。
大致可以看到以下几条路线:
-
Thread-Per-Core
典型代表是 Seastar。
每个 CPU Core 运行自己的 EventLoop 和任务调度器,尽量避免线程迁移和跨核共享。
-
通用异步 I/O
典型代表是 Boost.Asio。
目标是在 Windows、Linux、macOS 等不同操作系统之上提供统一的异步 I/O 抽象。
-
One Loop Per Thread / Multi-Reactor
典型代表是 Muduo。
每个线程拥有自己的 EventLoop,连接在多个 Reactor 线程之间进行分配。
-
M:N 用户态线程
典型代表是 brpc 的 bthread。
大量用户态线程运行在少量 pthread worker 之上,通过用户态调度提高并发能力。
除此之外,还有 libevent、libev、libuv、Folly、ACE、uWebSockets 等大量网络或事件驱动基础设施。
简单列举如下:
| 项目 | 跨平台 | 异步 | 主要模型 | 特点 / 定位 |
|---|---|---|---|---|
| libevent | 是 | 是 | Reactor | 经典通用事件库 |
| libev | Unix 为主 | 是 | Reactor | 轻量事件循环 |
| libuv | 是 | 是 | Event Loop + Thread Pool | Node.js 底层 I/O 基础设施 |
| Boost.Asio | 是 | 是 | Proactor 风格接口 | 通用异步 I/O 抽象 |
| Muduo | Linux | 是 | Multi-Reactor | Linux 服务端网络库 |
| brpc | Linux 为主 | 是 | M:N bthread | RPC + 用户态线程 |
| Seastar | Linux 为主 | 是 | Thread-Per-Core | Shared-Nothing、高性能 Runtime |
| Folly | Linux 为主 | 是 | EventBase / Future / Coroutine | Meta 基础设施库 |
| ACE | 是 | 是 | Reactor / Proactor | 老牌 C++ 网络框架 |
| uWebSockets | 是 | 是 | Event Loop | HTTP / WebSocket、高性能网络服务 |
这些项目都很成熟,但它们解决的并不是完全相同的问题。
为什么这些模型不能最终统一成一个“最佳网络库”?
是否允许阻塞任务
以 Seastar 为代表的 Thread-Per-Core 模型追求极低的线程切换、锁竞争和跨核通信开销。
EventLoop 和任务通常固定在某个 CPU Core 上执行。
这样可以获得很好的 Cache Locality,也能减少传统多线程程序中的锁和线程迁移成本。
代价也很明显:
运行在 EventLoop 上的任务不能随意阻塞。
一旦某个任务执行长时间 CPU 计算或者阻塞系统调用,同一个 Core 上的其他任务都会受到影响。
而 brpc 的 bthread、传统线程池等模型,则愿意承担更多调度成本,以换取对同步和阻塞代码更好的兼容性。
这本身就是两种不同的设计取向。
是否需要跨平台
不同操作系统提供的事件机制并不相同。
Linux 有epoll,io_uring,BSD 和 macOS 有kqueue,Windows 有IOCP 如果一个网络库只服务于 Linux,那么它完全可以针对 Linux 内核能力进行深度优化。
而如果像 Boost.Asio 一样希望提供统一的跨平台 API,就必须在不同操作系统机制之上建立额外的抽象层。
Reactor 还是 Proactor
传统的 epoll、kqueue 更接近 Reactor:
操作系统告诉你某个 fd 已经 Ready,然后应用程序自己执行
read/write。
IOCP 以及部分 io_uring 使用方式则更接近 Completion-Based I/O:
应用提交操作,之后等待操作完成通知。
Boost.Asio 在 API 层提供了统一的异步操作模型,但不同平台的底层实现仍然可能完全不同。
这也意味着任何跨模型抽象都需要付出一定的状态管理和实现复杂度。至于asio为包装epoll付出的代价不在本文讨论范畴。
Timer 如何实现
计时无处不在:
- 连接超时
- RPC Timeout
- KeepAlive
- 重连
- 心跳
- 定时任务
常见实现主要包括最小堆和时间轮。
最小堆的插入和删除通常是:O(log N)
但实现简单,而且数组实现具有较好的内存连续性。
时间轮在大量 Timer 场景中可以获得接近:O(1)的插入和删除复杂度。
但它同时需要处理时间粒度、Bucket 和多层时间轮等问题。
因此即使只是 Timer,也很难存在一个对所有场景都最优的数据结构。
一个好的网络库是否一定要是异步 Runtime?
一种思路认为:
网络库应该尽可能简单,只负责高效网络 I/O。
线程如何组织、任务如何调度、业务代码是否使用协程,都应该由使用者决定。
另一种思路则认为:
网络 I/O、Timer、任务调度和 Coroutine 本来就应该属于同一个异步 Runtime。
现代网络框架越来越倾向于后者。
因为当网络服务复杂到一定程度以后: 网络 I/O,Timer, RPC,文件 I/O,数据库IO,实际上很难完全割裂。
但是 Runtime 越完整,也意味着使用者必须接受更多编码规则。
写在最后
现在再回到最开始的问题:
为什么 C++ 没有一个公认最好的网络库?
原因可能并不是 C++ 网络生态不够成熟。
恰恰相反。
正是因为 C++ 被用于太多完全不同的领域,不同场景对网络和并发模型的要求差异太大,才形成了今天这种多种架构长期并存的状态。
你的程序到底需要什么?
只有开发者自己能够回答。