💻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 变体:

  1. 单 Reactor 单线程:所有事件在一个线程处理。简单但不能利用多核。Redis 6.0 之前的模型。

  2. 单 Reactor 多线程:Reactor 线程负责 IO,业务逻辑交给线程池。

  3. 主从 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 对比:

对比项ReactorProactor
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 · 计算机网络