💻CS 计算机网络
网络编程模型
难度:⭐⭐ | 高频指数:🔥🔥
面试回答
常见问法
- 五种 IO 模型分别是什么?
- 阻塞 IO 和非阻塞 IO 有什么区别?
- IO 多路复用和异步 IO 有什么区别?
- Reactor 和 Proactor 模式有什么区别?
- C++ 常用的网络库有哪些?
回答
Unix 网络编程中有五种 IO 模型:
| 模型 | 等待数据 | 拷贝数据 | 特点 |
|---|---|---|---|
| 阻塞 IO | 阻塞 | 阻塞 | 最简单,一个连接一个线程 |
| 非阻塞 IO | 不阻塞(轮询) | 阻塞 | 需要不断轮询,CPU 浪费 |
| IO 多路复用 | 阻塞在 select/epoll | 阻塞 | 一个线程监听多个 fd |
| 信号驱动 IO | 不阻塞(信号通知) | 阻塞 | 很少使用 |
| 异步 IO(AIO) | 不阻塞 | 不阻塞 | 内核完成所有工作后通知 |
关键区分:
- 前四种都是同步 IO:数据从内核拷贝到用户空间时,进程是阻塞的
- 只有异步 IO 是真正的异步:内核完成数据拷贝后才通知进程
Reactor vs Proactor:
- Reactor(同步 IO + 事件通知):内核通知”数据就绪了”,应用自己读取。Linux 主流方案(epoll + Reactor)。
- Proactor(异步 IO + 完成通知):应用发起异步读,内核完成读取后通知”数据已经读好了”。Windows IOCP 是典型实现。
追问
1. 为什么 Linux 主要用 Reactor 而不是 Proactor?
Linux 的异步 IO(AIO)支持不完善:
- 早期 POSIX AIO 是用户态线程池模拟的,不是真正异步
- io_uring(Linux 5.1+)才提供了真正高效的异步 IO
- epoll + Reactor 已经足够高效,生态成熟
2. C++ 常用网络库?
- muduo(陈硕):基于 Reactor,one loop per thread,适合学习
- Boost.Asio:跨平台,支持 Reactor 和 Proactor,C++ 标准网络库的基础
- libevent/libev:C 库,轻量级事件循环
- brpc(百度):高性能 RPC 框架
原理展开
1. 阻塞 IO 模型
// 最简单的模型:一个连接一个线程
void handle_client(int connfd) {
char buf[1024];
int n = read(connfd, buf, sizeof(buf)); // 阻塞,直到有数据
// 处理数据
write(connfd, response, len); // 阻塞,直到写完
}
问题:
- 每个连接需要一个线程
- 线程数量受限(通常几千个)
- 大量线程导致上下文切换开销大
- 适合连接数少、每个连接处理时间长的场景
2. 非阻塞 IO 模型
// 设置非阻塞
fcntl(fd, F_SETFL, O_NONBLOCK);
// 轮询
while (true) {
int n = read(fd, buf, sizeof(buf));
if (n == -1 && errno == EAGAIN) {
// 没有数据,继续轮询
continue;
}
// 有数据,处理
break;
}
问题:
- 需要不断轮询,浪费 CPU
- 实际很少单独使用,通常配合 IO 多路复用
3. IO 多路复用模型
// 一个线程监听多个 fd
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nready; i++) {
if (events[i].events & EPOLLIN) {
int n = read(events[i].data.fd, buf, sizeof(buf));
// 处理数据
}
}
核心思想:
- 用一个系统调用(select/poll/epoll)同时监听多个 fd
- 哪个 fd 就绪就处理哪个
- 避免了为每个连接创建线程
4. 异步 IO 模型
// Linux io_uring 示例(简化)
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
// 提交异步读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, 0);
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 此时数据已经在 buf 中了
和 IO 多路复用的区别:
- IO 多路复用:内核告诉你”可以读了”,你自己调 read
- 异步 IO:内核帮你读完,告诉你”读好了”
5. Reactor 模式详解
Reactor 模式组件:
┌─────────────────────────────────────┐
│ Event Loop(事件循环) │
│ ┌─────────┐ │
│ │ Demux │ epoll_wait │
│ │(解多路) │ 监听所有 fd │
│ └────┬────┘ │
│ │ 事件就绪 │
│ ▼ │
│ ┌─────────┐ │
│ │Dispatch │ 根据 fd 分发到对应 │
│ │(分发器) │ Handler │
│ └────┬────┘ │
│ │ │
│ ┌────▼────┐ ┌─────────┐ │
│ │Handler A│ │Handler B│ ... │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────────┘
三种 Reactor 变体:
-
单 Reactor 单线程:所有事件在一个线程处理。简单但不能利用多核。Redis 6.0 之前的模型。
-
单 Reactor 多线程:Reactor 线程负责 IO,业务逻辑交给线程池。
-
主从 Reactor:主 Reactor 负责 accept,子 Reactor 负责已连接 socket 的 IO。Netty、muduo 的模型。
6. Proactor 模式详解
Proactor 模式:
1. 应用发起异步操作(如 AsyncRead)
2. 操作系统在后台完成 IO
3. 完成后通知应用(通过回调或完成队列)
4. 应用处理已经准备好的数据
Windows IOCP 流程:
应用 → CreateIoCompletionPort → 发起异步操作
→ GetQueuedCompletionStatus(等待完成通知)
→ 处理完成的 IO
Reactor vs Proactor 对比:
| 对比项 | Reactor | Proactor |
|---|---|---|
| IO 操作 | 应用自己做 | 内核/系统做 |
| 通知内容 | ”就绪了,你来读" | "读完了,给你数据” |
| 典型实现 | epoll(Linux) | IOCP(Windows) |
| 编程复杂度 | 中等 | 较高 |
| 性能 | 高 | 理论更高 |
7. muduo 网络库架构
muduo 的 one loop per thread 模型:
Main Thread(Acceptor)
├── EventLoop(主循环)
├── 监听 listen socket
├── accept 新连接
└── 轮询分配给 Sub Thread
Sub Thread 1(IO Thread)
├── EventLoop(子循环)
├── 监听分配到的连接
└── 处理读写事件
Sub Thread 2(IO Thread)
├── EventLoop(子循环)
└── ...
Thread Pool(可选,计算线程)
├── 处理耗时的业务逻辑
└── 避免阻塞 IO 线程
核心设计:
- 每个线程一个 EventLoop,线程间通过 eventfd 通知
- 连接的所有操作都在同一个线程,避免加锁
- 如果业务逻辑耗时,可以交给线程池
8. Boost.Asio 的设计
// Asio 异步读示例
boost::asio::io_context io;
tcp::socket socket(io);
// 发起异步读
boost::asio::async_read(socket, buffer,
[](boost::system::error_code ec, std::size_t length) {
if (!ec) {
// 读取完成,处理数据
}
});
// 运行事件循环
io.run();
Asio 的特点:
- 跨平台:Linux 用 epoll,macOS 用 kqueue,Windows 用 IOCP
- 支持同步和异步两种模式
- C++20 协程支持
- 是 C++ Networking TS 的基础
易错点
- 说”IO 多路复用是异步 IO”——IO 多路复用是同步的,数据拷贝阶段进程仍然阻塞。
- 混淆”非阻塞 IO”和”异步 IO”——非阻塞 IO 只是等待阶段不阻塞,拷贝阶段仍阻塞。
- 说”Reactor 只能单线程”——Reactor 有多种变体,主从 Reactor 是多线程的。
- 不知道 io_uring——这是 Linux 真正的异步 IO 方案,面试加分项。
- 说”epoll 是异步的”——epoll 是同步 IO 多路复用,不是异步 IO。
- 混淆 Reactor 和 Proactor 的通知内容——Reactor 通知”就绪”,Proactor 通知”完成”。
记忆技巧
- 五种模型排列:阻塞 → 非阻塞 → 多路复用 → 信号驱动 → 异步
- 同步 vs 异步判断标准:数据拷贝阶段是否阻塞
- Reactor 一句话:通知你”好了你来读”
- Proactor 一句话:通知你”读好了给你”
- muduo 口诀:one loop per thread,主线程 accept,子线程 IO
面试速答版
五种 IO 模型:阻塞 IO、非阻塞 IO、IO 多路复用、信号驱动 IO、异步 IO。前四种是同步 IO(数据拷贝阶段阻塞),只有异步 IO 是真正异步。IO 多路复用用一个线程监听多个 fd(select/poll/epoll),是 Linux 高并发服务器的主流方案。Reactor 模式是”事件就绪通知 + 应用自己读写”,Proactor 是”内核完成 IO 后通知应用”。Linux 主要用 epoll + Reactor(因为 AIO 支持不完善),Windows 用 IOCP(Proactor)。C++ 常用网络库:muduo(one loop per thread)、Boost.Asio(跨平台)、libevent。
Related · 计算机网络