基于 C++ 手写 Muduo 高性能网络库
大纲
前言
本文将基于 C++ 开发一个类似 Muduo 的高性能网络库,项目代码大部分都是从 Muduo 移值过来,同时去掉 Boost 依赖,并使用 C++ 11 进行代码重构,重点是学习 Muduo 的底层设计思想(尤其是 Multiple Reactors 模型)。
学习目标
- 1、理解阻塞、非阻塞、同步、异步
- 2、理解 Unix/Linux 上的五种 I/O 模型
- 3、epoll 的原理以及优势
- 4、深刻理解 Reactor 模型(基于 I/O 的事件驱动模型)
- 5、从开源 C++ 网络库 Muduo 的源码中,学习优秀的代码设计
- 6、掌握基于事件驱动和事件回调的 epoll + 线程池的面向对象编程
- 7、通过深入理解 Muduo 源码,加深对于网络相关项目的深刻理解
- 8、改造 Muduo,不依赖 Boost,使用 C++ 11 进行代码重构
知识储备
在使用 C++ 开发高性能的网路库之前,要求先掌握以下前置知识:
- 1、TCP 协议和 UDP 协议
- 2、Linux 的 TCP 网络编程和 UDP 网络编程
- 3、I/O 多路复用编程,包括 select、poll、epoll 库的使用
- 4、Linux 的多线程编程(
pthread)、进程和线程模型 - 5、C++ 20 标准新加入的协程支持
推荐阅读的书籍
《UNIX 环境高级编程》、《Linux 高性能服务器编程》、《 Linux 多线程服务端编程 - 使用 Muduo C++ 网络库》《鸟哥的 Linux 私房菜》
开发工具
| 软件 | 版本 | 说明 |
|---|---|---|
| C++ 标准 | 11 | C++ 标准的版本 |
| G++(GCC) | 12.2.0 | 建议使用 9 版本的 G++(GCC) 编译器 |
| CMake | 3.25.1 | C/C++ 项目构建工具 |
| Linux | Debian 12 | Muduo 库不支持 Windows 平台 |
| Visual Studio Code | 1.100.2 | 使用 VSCode 远程开发特性 |
基础概念
阻塞、非阻塞、同步、异步
提示
- 下面提到的 "I/O 操作" 并不局限于网络 I/O,而是一个广义的 I/O 概念,它既包含网络 I/O(Socket 读写),也包含磁盘 I/O(文件读写)等所有涉及内核态与用户态之间数据交换的操作。
- I/O 模型(如阻塞、非阻塞、同步、异步)更多用于讨论网络 I/O,原因是磁盘 I/O 的异步化由操作系统内核自动管理(页缓存 + 异步调度),应用层很少直接干预。
同步与异步的区别
- 同步:
- 请求方 A 发起 I/O 调用后,由 A 自身完成数据的读写;
- 无论阻塞与否,A 都要亲自执行数据的读写,将数据从内核缓冲区拷贝到用户空间(或反之)。
- 异步:
- 请求方 A 发起 I/O 调用后,仅仅发出请求,并由操作系统内核来完成数据的读写;
- A 不需要等待操作完成,可以继续做其他事情;当操作系统内核完成读写操作后,会通过回调、事件通知等机制通知 A 结果。
- 同步:
阻塞与非阻塞的区别
- 阻塞:
- 调用未完成前,调用线程会一直等待;
- 非阻塞:
- 调用立即返回,即使操作未完成,也会返回错误码或状态提示(例如
EAGAIN)。
- 调用立即返回,即使操作未完成,也会返回错误码或状态提示(例如
- 阻塞:
典型的一次 I/O 操作可以分为两个阶段
- 数据准备(阶段一):该阶段取决于系统 I/O 操作的就绪状态,即数据是否已经可以被读写
- 阻塞:调用会等待数据准备好后再继续执行。
- 非阻塞:调用会立即返回,无论数据是否就绪。
- 数据读写(阶段二):该阶段取决于应用程序与操作系统内核之间的交互方式
- 同步:由应用程序主动完成数据的读写,将数据从内核缓冲区拷贝到用户空间(或反之)。
- 异步:由操作系统内核完成数据的读写,并在操作完成后通知应用程序。
- 数据准备(阶段一):该阶段取决于系统 I/O 操作的就绪状态,即数据是否已经可以被读写
总结
- 同步 / 异步区分的是谁来完成 I/O 读写(调用方自己还是操作系统内核来完成数据读写)。
- 阻塞 / 非阻塞区分的是调用方等待的方式(是否挂起等待处理结果)。
常见的四种 I/O 模型
| I/O 模型 | 数据准备阶段 | 数据读写阶段 | 调用方行为 | 示例说明 |
|---|---|---|---|---|
| 同步阻塞 | 阻塞等待数据准备好 | 调用方执行读写 | 整个过程会阻塞当前线程 | int size = recv(fd, buf, 1024, 0);(若无数据则阻塞等待) |
| 同步非阻塞 | 非阻塞轮询数据准备好 | 调用方执行读写 | 调用立即返回,但需要反复尝试调用 | 设置 O_NONBLOCK,多次调用 recv() 检查是否有数据可读 |
| 异步阻塞 | 阻塞等待事件完成 | 操作系统内核完成读写 | 等待通知,但数据读写由操作系统内核完成 | 例如 Windows OVERLAPPED I/O + GetOverlappedResult 阻塞等待 |
| 异步非阻塞 | 非阻塞提交请求 | 操作系统内核完成读写并通知 | 完全不阻塞,结果通过回调 / 事件返回 | 例如 Linux aio_read() 或 io_uring 提交请求后立即返回 |
陈硕大神的原话:在处理 I/O 的时候,阻塞和非阻塞都是同步 I/O,只有使用了特殊的 API 才是异步 I/O(如下图所示)。

特别注意
select/poll/epoll本身只是事件就绪通知机制,它们并不直接完成数据读写,调用它们的线程仍然需要自己去read()或write()数据。- 因此,从严格意义上看,它们属于同步 I/O 实现方式,因为最终的 I/O 读写(即数据读写)是由调用线程自己完成的。
- 但它们提供了非阻塞的事件等待,使得一个线程可以同时监听多个
fd,而不用一个线程阻塞在一个fd上。 - 真正的异步 I/O 实现,在 Linux 上需要使用
aio_*系列系统函数或者使用io_uring。
Unix/Linux 的五种 I/O 模型
Unix/Linux 支持以下五种 I/O 模型:
| I/O 模型 | 阻塞 / 非阻塞 | 事件通知方式 | 适用场景 |
|---|---|---|---|
| 阻塞 I/O | 阻塞 | 同步返回 | 简单程序,低并发 |
| 非阻塞 I/O | 非阻塞 | 轮询 | 少量 I/O,CPU 可支撑 |
| I/O 多路复用 | 阻塞或非阻塞 | 操作系统内核返回就绪事件列表 | 高并发网络服务器 |
| 信号驱动 I/O | 非阻塞 | 信号 | 小规模异步通知 |
| 异步 I/O | 非阻塞 | 回调 / 事件 | 高并发、对延迟敏感场景 |
阻塞 I/O(Blocking I/O)
- 特征:应用程序调用 I/O 函数后,如果数据未就绪,调用线程会被阻塞,直到数据准备完成。
- 优点:编程实现简单、逻辑直观。
- 缺点:线程无法同时处理多个 I/O,吞吐量受限。

非阻塞 I/O(Non-Blocking I/O)
- 特征:I/O 调用立即返回,即使数据未就绪也不会阻塞。应用程序需要通过轮询(Polling)或循环检查,目的是不断检测数据是否已经就绪,以便及时进行数据读写操作。
- 优点:单线程可以处理多个 I/O。
- 缺点:轮询会浪费 CPU 资源,逻辑较复杂。

I/O 多路复用(I/O Multiplexing)
- 典型机制:
select、poll、epoll。 - 特征:单个线程可以同时监听多个
fd,通过操作系统内核返回就绪事件列表,再进行读写操作。 - 优点:高效管理大量并发连接,避免轮询浪费。
- 缺点:处理非常大量
fd时,某些实现(如select、poll)效率有限。 - 注意:在 I/O 多路复用中,复用的线程而不是 TCP 连接。由于最终的 I/O 读写(即数据读写)是由调用线程自己完成的,因此从严格意义上看,I/O 多路复用属于同步 I/O 实现方式。

信号驱动 I/O(Signal-Driven I/O)
- 特征:应用程序注册信号处理函数(如
SIGIO),当fd可读或可写时,操作系统内核发送信号通知。 - 优点:异步通知,无需轮询。
- 缺点:信号处理复杂,信号丢失或竞态问题较多,不易大规模使用。
- 注意:操作系统内核在第一个阶段(数据准备)是异步,在第二个阶段(数据读写)是同步;与非阻塞 I/O 的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统 API 的调用次数,提高了效率。

异步 I/O(Asynchronous I/O)
- 特征:应用程序发起 I/O 调用后,立即返回;当数据准备好后,由操作系统内核完成数据读写;当数据读写操作完成后,通过信号、回调函数或事件机制通知应用程序。
- 优点:真正的异步,高效利用 CPU,可处理大量并发 I/O。
- 缺点:编程复杂,Linux 支持有限(传统 AIO 对网络 I/O 支持不好,
io_uring是新方案)。 - 注意:这是真正的异步 I/O 实现,在 Linux 上需要使用
aio_*系列系统函数或者使用io_uring,Node.js 采用了该 I/O 模型。

优秀的网络服务器设计
在这个 CPU 多核时代,服务端网络编程如何选择线程模型呢?赞同 libev 作者的观点:”one loop perthread is usually a good model”,这样多线程服务端编程的问题就转换为如何设计一个高效且易于使用的 Event Loop, 然后每个线程运行一个 Event Loop 就行了(当然,线程间的同步、互斥少不了,还有其它的耗时事件需要起另外的线程来做)。Event Loop 是 Non-Blocking 网络编程的核心,可以简单理解为 Non-Blocking + epoll + thread-pool 的结合。在实际应用中,Non-Blocking 几乎总是与 I/O Multiplexing 一起使用,原因有以下两点:
- 实际上没有人会采用轮询(Busy-Polling)方式不断检查某个 Non-Blocking I/O 操作是否完成,因为这会严重浪费 CPU 资源。
- I/O Multiplexing 通常无法与 Blocking I/O 一起使用,因为在 Blocking I/O 中,
accept()、connect()、read()、write()等调用都有可能阻塞当前线程,从而导致线程无法继续处理其他 Socket 上的 I/O 事件。
所以,当日常提到 Non-Blocking I/O 时,实际上指的是 Non-Blocking + I/O Multiplexing(如 epoll + thread-pool) 的组合,如何单独使用其中任意一种,都无法很好地实现高效的网络 I/O。
在网络编程领域中,主流的网络 I/O 模型有以下几种(不限于),Muduo 采用的是第四种(reactors in threads - one loop per thread)。
(1)
accept + read/write- 不适用于并发服务器
(2)
accept + fork - process-pre-connection- 适合并发连接数不大,计算任务工作量大于 Fork 的开销。
(3)
accept + thread - thread-pre-connection- 比第二种网络 I/O 模型的开销小了一点,但是并发造成的线程堆积过多。
(4)
reactors in threads - one loop per thread- 这是 Muduo 库的网络设计方案,底层实质上是基于 Linux 的
epoll+pthread线程池实现,且依赖了 Boost 库,适用于并发连接数较大的场景。 - 有一个 Main Reactor 负载 Accept 连接,然后将连接分发给某个 SubReactor(采用轮询的方式来选择 SubReactor),该连接的所用操作都在那个 SubReactor 所处的线程中完成。多个连接可能被分派到多个线程中被处理,以充分利用 CPU。
- Main Reactor 中有一个 Base I/O Thread 负责 Accept 新的连接,接收到新的连接以后,使用轮询的方式在 Reactor Pool 中找到合适的 SubReactor 将这个连接挂载上去,这个连接上的所有任务都在这个 SubReactor 所处的线程中完成。
- Reactor Poll 的大小是固定的,根据 CPU 的核心数量来确定。如果有过多的耗费 CPU 资源的计算任务,可以提交到 ThreadPool 线程池中专门处理耗时的计算任务。
- 这是 Muduo 库的网络设计方案,底层实质上是基于 Linux 的
(5)
reactors in process - one loop pre process- 这是 Nginx 服务器的网络设计方案,基于进程设计,采用多个 Reactors 充当 I/O 进程和工作进程,通过一个
accept锁,完美解决多个 Reactors 之间的 “惊群现象”。
- 这是 Nginx 服务器的网络设计方案,基于进程设计,采用多个 Reactors 充当 I/O 进程和工作进程,通过一个
reactors in process + fork 不如 reactors in threads 吗?
答案肯定是否定的,强大的 Nginx 服务器采用了 reactors in process 模型作为网络模块的架构设计,实现了简单好用的负载算法,使各个 fork 网络进程不会忙的越忙、闲的越闲,并且通过引入一把乐观锁解决了该模型导致的服务器惊群现象,功能十分强大。
Reactor 网络 I/O 模型
Reactor 模型的介绍
维基百科对 Reactor 的描述
The reactor design pattern is an event handling pattern for handling service requestsdelivered concurrently to a service handler by one or more inputs. The service handlerthen demultiplexes the incoming requests and dispatches them synchronously to theassociated request handlers. 翻译后:Reactor(反应器)设计模式是一种事件处理模式,用于处理由一个或多个输入并发传递到服务处理器的服务请求。然后,服务处理器对传入的请求进行多路分解,并同步地将它们分派给相应的请求处理器。
Reactor 是一种基于事件驱动(Event Driven)的网络 I/O 模型,核心思想是:
- 主线程(或 I/O 线程)通过 I/O 多路复用(I/O Multiplexing)机制(如
select、poll、epoll),监听多个连接的 I/O 事件。 - 当某个事件就绪后,再分发(Dispatch)给对应的事件处理器(EventHandler)进行处理。
- 主线程(或 I/O 线程)通过 I/O 多路复用(I/O Multiplexing)机制(如
Reactor 虽然是网络 I/O 模型,但它通常与线程模型结合使用:
- 单线程 Reactor:所有 I/O 事件的监听与处理都在同一个线程中完成。
- 多线程 Reactor:I/O 事件的监听与业务处理分离,通常用线程池来处理业务逻辑。
- 主从 Reactor:主 Reactor(即 MainReactor)负责连接建立,从 Reactor(即 SubReactor)负责 I/O 读写(即数据读写),结合多线程提升并发性能。
Reactor 的五大核心组件:
核心组件 作用 Muduo 网络库中对应的核心类 Event(事件) 表示 I/O 事件的抽象,如连接建立、可读、可写等,用于描述发生了什么类型的网络事件。 ChannelDemultiplexer(事件分离器) 负责监听并检测多个 I/O 事件的就绪状态(通常由 select、poll、epoll等系统调用实现),并将已就绪的事件返回给 Reactor。Poller、EPollPollerReactor(反应堆) 事件分发器,负责从 Demultiplexer 获取就绪事件,并将事件分发给对应的 EventHandler 处理。 EventLoopEventHandler(事件处理器) 负责具体的事件处理逻辑,如读、写、连接、业务处理等,是应用层的回调逻辑。 回调函数 + TcpConnection的handleRead()/handleWrite()等Acceptor(连接接收器) 负责监听服务器端口并接收新的客户端连接,在多 Reactor 模型中通常独立运行,仅负责建立连接并将连接交给子 Reactor 处理。 AcceptorReactor 核心组件的工作流程:

- Muduo 库的 Multiple Reactors 模型:

Reactor 模型与 Proactor 模型的区别
- Reactor 模型与 Proactor 模型的主要区别
| 模型 | 内核通知的事件 | 谁负责实际 I/O 读写 | 用户线程需要做什么 |
|---|---|---|---|
| Reactor | 可以读 / 可以写 | 用户线程做读写 | 用户线程收到可读 / 可写通知后,调用 read / write,并处理数据 |
| Proactor | 读完了 / 写完了 | 内核做读写 (异步完成读 / 写后,再通知用户线程) | 用户线程收到读 / 写完成通知后,直接处理已读 / 已写的数据 |
- 常见库 / 系统采用模型对比
| 库 / 系统 | 模型 | 平台 |
|---|---|---|
| Muduo | Reactor | Linux |
| Netty(NIO) | Reactor | Linux |
| libevent / libev | Reactor | Linux |
| Boost.Asio(Linux) | Reactor | Linux |
| IOCP | Proactor | Windows |
| Boost.Asio(Windows) | Proactor | Windows |
- 为什么 Linux 基本用不到 Proactor?
- 因为 Linux 的
aio不是真正意义上的内核异步 I/O:- 文件 I/O 是异步的
- 网络 I/O 仍然是阻塞式的(内核不自动读取)
- 所以 Linux 上的高性能网络库几乎都是:
- epoll(Reactor)
- epoll + thread pool(高级 Reactor)
- 因为 Linux 的
I/O 多路复用技术概述
跨平台特性的对比
| 技术 | 是否支持跨平台 | 支持的平台 | 特点 |
|---|---|---|---|
select | ✅ 广泛跨平台 | Linux / macOS / BSD / Windows / Unix | 最老的接口,POSIX 标准定义 |
poll | ⚠️ 支持类 Unix 跨平台(不支持 Windows) | Linux /macOS/ BSD / Solaris 等 | select 的改进版 ,无 fd 数量限制 |
epoll | ❌ Linux 独有 | 仅 Linux(2.6+) | 高性能 I/O 多路复用技术 |
kqueue | ❌ BSD /macOS 独有 | FreeBSD / macOS / NetBSD / OpenBSD | epoll 的 BSD 平台对应物 |
select 与 poll 的缺点
I/O 多路复用技术 select 有以下缺点:
(1) 文件描述符数量限制:
- 单个进程可监视的文件描述符数量存在上限,通常为 1024(可修改)。但由于
select采用轮询扫描方式检查文件描述符,随着监视数量的增加,性能会明显下降。 - 在 Linux 内核头文件中有如下定义:
#define __FD_SETSIZE 1024。
- 单个进程可监视的文件描述符数量存在上限,通常为 1024(可修改)。但由于
(2) 内核与用户空间的数据拷贝开销大:
- 每次调用
select都需要在内核空间与用户空间之间复制大量的文件描述符集合,这会造成显著的性能开销。
- 每次调用
(3) 结果集遍历效率低:
select返回的是一个包含所有文件描述符的数组,应用程序需要遍历整个数组才能判断哪些描述符处于就绪状态,效率较低。
(4) 水平触发机制(Level Trigger):
select采用水平触发方式,如果应用程序没有及时处理已就绪的文件描述符,那么在后续的每次select调用中,这些描述符仍会被重复通知。
I/O 多路复用技术 poll 跟 select 相比,使用链表来保存文件描述符,不再受文件描述符数量上限的限制,但仍然存在与 select 相同的其他三个缺点(数据拷贝开销大、结果集遍历效率低、水平触发),这里不再累述。
select 无法支持高并发连接
以 select 为例,若服务器需支持 100 万并发连接,在 __FD_SETSIZE 为 1024 的情况下,至少需要创建约 1000 个进程才能满足要求。如此不仅会带来大量的进程上下文切换开销,还会因频繁的内核空间 / 用户空间句柄拷贝与数组遍历操作,导致系统性能急剧下降。因此,基于 select 模型的服务器要实现百万级并发几乎是不可能的。
epoll 的原理以及优势
设想这样一个场景:有 100 万个客户端同时与一个服务器进程保持 TCP 连接,但在任意时刻,通常只有几百到上千个连接是活跃的(这也是现实中最常见的情况)。如何高效地支撑如此庞大的并发连接呢?在 select / poll 时代,服务器每次调用都需要将这 100 万个连接的文件描述符从用户态复制到内核态,让内核轮询这些套接字上是否有事件发生;轮询完成后,再将结果从内核态复制回用户态,供应用程序继续遍历处理。这种方式带来了巨大的内存拷贝和遍历开销,因此基于 select / poll 通常只能处理几千个并发连接。
epoll 的设计思想与 select 完全不同,因此它们的缺点在 epoll 中已不复存在。epoll 在 Linux 内核中引入了一种专用的事件管理机制,通过红黑树(用于管理所有已注册的文件描述符)和就绪链表(用于管理已触发事件的文件描述符)来组织事件,大幅降低了事件查找和分发的开销,使大规模并发连接的事件管理更加高效。
- (1)
epoll_create():创建一个epoll对象(内核在epoll文件系统中为该对象分配资源)。 - (2)
epoll_ctl():向epoll对象中添加、修改或删除需要监听的套接字(例如 100 万个 TCP 连接)。 - (3)
epoll_wait():等待并收集有事件发生的文件描述符。
其中 epoll_create() 在内核上创建的 eventpoll 结构如下:
1 | struct eventpoll { |
得益于这种设计,只需在服务器启动时创建一次 epoll 对象,然后在连接建立或关闭时动态地添加或移除对应的套接字即可。更重要的是,epoll_wait() 的调用效率极高:
- 它不需要在每次调用时复制所有文件描述符。
- 内核也无需遍历全部连接,而是通过回调机制主动将就绪的文件描述符加入到就绪队列中。
因此,epoll 能够在单进程中轻松支撑数十万甚至上百万级的并发连接,这正是它区别于 select / poll 的根本优势所在。
epoll 的 LT 模式与 ET 模式
epoll 支持 LT(水平触发)与 ET(边缘触发),而 select、poll 在设计上只支持 LT(水平触发),没有 ET(边缘触发)的概念。
LT 模式(Level Triggered,水平触发)
- 语义:只要
fd上有数据未被读取完,就会一直被epoll通知。 - 特点:更 “宽松”,即使一次没读完,下次还会被提醒。
- 行为示例:
- 缓冲区有 100 字节可读;
- 应用程序只读了 60 字节;
- 下次
epoll_wait()还会再次返回该fd。
- 优点:编程简单、不易漏数据。
- 缺点:频繁触发,效率略低。
- 语义:只要
ET 模式(Edge Triggered,边缘触发)
- 语义:只有当状态发生变化(从无到有)时才触发一次事件。
- 特点:仅在 “边缘” 通知,比如缓冲区从空变为非空。
- 行为示例:
- 缓冲区变为可读时触发;
- 应用程序必须一次性读完所有数据(直到返回
EAGAIN); - 如果应用程序没读完,下次不会再收到通知。
- 优点:减少系统调用次数,效率高。
- 缺点:编程复杂,稍有疏忽就可能会 “丢事件”。
Muduo 采用的是 LT(水平触发)模式
- 不会丢失数据或者消息
- 应用程序没有读取完数据,内核是会不断上报数据的
- 低延迟处理
- 每次读数据只需要一次系统调用,照顾了多个连接的公平性,不会因为某个连接上的数据量过大而影响其他连接处理消息
- 跨平台处理
- 像
select一样可以跨平台使用
- 像
- 不会丢失数据或者消息
项目介绍
项目结构
1 | c++-project-mymuduo |
| 目录名称 | 目录说明 |
|---|---|
build | CMake 编译构建项目的目录(项目首次编译后才会有) |
bin | 存放项目编译生成的可执行文件的目录(项目首次编译后才会有) |
lib | 存放项目编译生成的 MyMuduo 动态链接库的目录(项目首次编译后才会有) |
src | MyMuduo 网络库的源码 |
src/include | MyMuduo 网络库的头文件 |
test | MyMuduo 网络库的的测试代码 |
example | 各种案例代码 |
example/epoll | epoll 的使用案例代码 |
example/mymuduo | MyMuduo 网络库的使用案例代码 |
autobuild.sh | 项目一键编译构建的脚本文件 |
项目技术栈
基于 C++ 开发网络库时,使用到以下技术:
- 单例设计模式
- epoll 等 I/O 多路复用技术
- Linux 网络编程基础(
socket()、bind()、listen()、accept()、readv()、write()等) - C++ 11 多线程编程(
std::thread、std::unique_lock、std::mutex、std::condition_variable等) - 使用 CMake 构建与集成项目的编译环境
项目整体架构

架构图说明
在上述的架构图中,mainLoop 运行在主线程,负责监听新 TCP 连接并分发给 subLoop;而 subLoop(也称 ioLoop)运行在子线程,负责处理 TCP 连接的具体 I/O 事件(比如,读和写等)。mainLoop 与 subLoop 通过 pendingFunctors 异步任务队列进行线程间通信,禁止直接跨线程操作,这是为了保证某个 TCP 连接的所有 I/O 事件和连接状态操作都在同一个线程中执行,从而保证线程安全。在 Muduo 库的 Multiple Reactors 模型,mainLoop 对应的就是 mainReactor(主 Reactor),而 subLoop 对应的就是 subReactor(子 Reactor)。
项目代码
代码下载
本文开发的 MyMuduo 网络库只实现了 Muduo 的核心功能,并不支持 Muduo 的定时事件机制(TimerQueue)、IPV6 / DNS / HTTP / RPC 协议等,完整的项目代码可以在 这里 下载得到。
copyable
copyable.h
1 |
|
noncopyable
noncopyable.h
1 |
|
Logger
Logger.h
1 |
|
Logger.cc
1 |
|
Timestamp
Timestamp.h
1 |
|
Timestamp.cc
1 |
|
InetAddress
InetAddress.h
1 |
|
InetAddress.cc
1 |
|
SocketsOps
SocketsOps.h
1 |
|
SocketsOps.cc
1 |
|
Channel
Channel.h
1 |
|
Channel.cc
1 |
|
Poller
Poller.h
1 |
|
Poller.cc
1 |
|
DefaultPoller.cc
1 |
|
Epoller
EPollPoller.h
1 |
|
EPollPoller.cc
1 |
|
EventLoop
EventLoop.h
1 |
|
EventLoop.cc
1 |
|
Thread
Thread.h
1 |
|
Thread.cc
1 |
|
CurrentThread
CurrentThread.h
1 |
|
CurrentThread.cc
1 |
|
EventLoopThread
EventLoopThread.h
1 |
|
EventLoopThread.cc
1 |
|
EventLoopThreadPool
EventLoopThreadPool.h
1 |
|
EventLoopThreadPool.cc
1 |
|
Socket
Socket.h
1 |
|
Socket.cc
1 |
|
Buffer
Buffer.h
1 |
|
Buffer.cc
1 |
|
TcpConnection
TcpConnection.h
1 |
|
TcpConnection.cc
1 |
|
Acceptor
Acceptor.h
1 |
|
Acceptor.cc
1 |
|
TcpServer
TcpServer.h
1 |
|
TcpServer.cc
1 |
|
Connector
Connector.h
1 |
|
Connector.cc
1 |
|
TcpClient
TcpClient.h
1 |
|
TcpClient.cc
1 |
|
项目测试
测试代码
ChatClient.h
1 | /** |
ChatClient.cc
1 | /** |
ChatServer.h
1 | /** |
ChatServer.cc
1 | /** |
main.cc
1 | /** |
测试步骤
- 编译项目代码
1 | # 进入项目根目录 |
- 运行测试程序
1 | # 执行 MyMuduo 网络库使用案例的可执行文件 |
- 测试程序输出的日志信息如下:
1 | 2025-11-15 22:10:01 => 6609 [INFO] ChatServer - start success, listening on 127.0.0.1:6000 |
项目扩展
上面的 MyMuduo 网络库代码只实现了 Muduo 的核心功能,并不支持 Muduo 的定时事件机制(TimerQueue)、IPV6 / DNS / HTTP / RPC 协议等,日后可以从以下几方面继续对其进行扩展:
(1) 定时事件机制
- TimerQueue:支持 EventLoop 内的定时任务调度,常见实现方式包括:
- 链表队列:实现简单,但不适合大量定时器场景(需要线性扫描)。
- 红黑树(如
nginx):按照到期时间排序,可快速找到最早到期的定时器,插入 / 删除的时间复杂度为O(logN)。 - 时间轮(如
libevent):适合大量、定时精度要求不高的场景,插入 / 删除的时间复杂度为O(1),整体性能出色。
- TimerQueue:支持 EventLoop 内的定时任务调度,常见实现方式包括:
(2) IPV6 / DNS / HTTP / RPC 协议支持
- IPV6:支持 IPv6 套接字、地址解析与双栈接入,确保网络库的所有连接与事件处理流程均可透明兼容 IPv6。
- DNS:实现异步域名解析(如
getaddrinfo_a),将域名解析和网络事件循环结合,避免阻塞 I/O。 - HTTP:构建基础的 HTTP 请求解析、响应封装,可扩展为简单的 Web 服务器或客户端;需要支持 Keep-Alive、Chunked 等机制。
- RPC:在已有 TCP 框架上封装请求 / 响应协议,实现序列化、服务注册、方法调用、超时与重试等功能(可仿照 gRPC 实现)。
(3) 服务器性能测试
项目问答
新 TCP 连接的派发问题
在 Muduo 网络库中,mainLoop 是如何将新来的 TCP 连接派发给 subLoop 的,同时还让新 TCP 连接的所有 I/O 事件回调操作都在 subLoop 所在的线程上执行?
- (1) Acceptor 在 mainLoop(运行在主线程)上监听
listenfd - (2) mainLoop 在收到新连接事件时,会调用
Acceptor::handleRead(),得到connfd(新连接的文件描述符) - (3) mainLoop 选择一个 subLoop(通过
EventLoopThreadPool的轮询) - (4) mainLoop 创建
TcpConnection,并把它的所有回调操作注册到 subLoop - (5) mainLoop 调用
subLoop->runInLoop(),将注册 connfd 读写事件到 subLoop 的 Poller 的任务丢给 subLoop - (6) subLoop 线程最终向自己的 Poller 注册事件,使得
connfd的所有读写事件(包括 I/O 事件、回调处理等)永远在 subLoop 上执行
1 | Acceptor::listen() |
特别注意
- 在 Muduo 中,新连接的建立仅发生在 mainLoop:它负责监听
listenfd,并在有新连接到来时调用accept()。mainLoop 只负责接受连接,不参与任何与该连接相关的后续 I/O 操作(读和写等)。在 mainLoop 完成accept()后,Muduo 会将得到的新连接文件描述符connfd分发给某个 subLoop(由EventLoopThreadPool按轮询算法选择)。之后,该新连接的所有读写事件(包括 I/O 事件、回调处理等)都由对应的 subLoop 独立处理,与 mainLoop 无关。
EventLoop 之间的通信问题
mainLoop 与 subLoop 分别运行在不同的线程上,它们之间是如何进行通信的,也就是说 mainLoop 是如何将新来的 TCP 连接派发给 subLoop 的,还有 mainLoop 是如何唤醒 subLoop 的?
- (1) mainLoop 与 subLoop 分别运行在不同线程中,每个 EventLoop 拥有自己独立的线程与 Poller。
- (2) 它们之间通过 EventLoop 的异步任务队列(
pendingFunctors)进行通信,任何跨线程的操作,都会封装成回调函数投递到目标 EventLoop 的异步任务队列中。 - (3) mainLoop 接收(
accept())到新连接后,调用subLoop->runInLoop(),将 TcpConnection 的初始化任务(如connectEstablished())投递给指定的 subLoop 执行。 - (4) mainLoop 向 subLoop 的任务队列中插入新任务后,会向 subLoop 的
wakeupFd写入一个字节,目的是唤醒 subLoop 去执行pendingFunctors队列中的任务。 - (5) 写入
wakeupFd会触发 subLoop 的wakeupChannel可读事件,wakeupChannel 是注册在 subLoop 上的一个 Channel,用来专门处理 “被唤醒” 事件。 - (6) 被唤醒的 subLoop 从阻塞的
epoll_wait()中立即返回,然后执行wakeupChannel的读事件回调。 - (7) subLoop 随后继续执行其
pendingFunctors队列中的任务,包括由 mainLoop 投递过来的 TcpConnection 初始化操作。 - (8) 从此以后,该 TcpConnection 的所有 I/O 事件都由该 subLoop 负责处理,包括读写事件回调、关闭回调、错误回调等全部在 subLoop 所在线程执行。
1 | Acceptor::listen() |
