C++ 网络编程 Muduo 库使用
大纲
前言
本文将介绍 Muduo 网络库的使用,在使用 Muduo 之前,建议先熟悉并掌握以下技术内容:
- 熟悉 C++ 11 语法
- 掌握事件驱动模型(Reactor 模式)
- 熟悉 C/C++ 多线程并发和线程安全设计
- 熟悉 Linux 网络编程(如使用
epoll
、socket
)
学习资源
版本说明
本文使用的各软件版本如下:
软件 | 版本 | 说明 |
---|---|---|
C++ | 11 | |
Muduo | 2.0.2 | |
Linux | Debian 12 | Muduo 只支持 Linux 平台,不支持 Windows 平台 |
Muduo 的介绍
网络 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。
- 有一个 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 进程和工作进程,通过一个
Muduo 的简介
Muduo 是一个用 C++ 编写的高性能、基于事件驱动的网络库,专门设计用于构建 Linux 下高并发、低延迟的网络服务,特别适合开发分布式系统、微服务、消息中间件、网络游戏服务器等后端程序。
核心特性
- 基于事件驱动模型:使用 Reactor 模式,即单线程 I/O + 多线程计算。
- 高性能:使用
epoll
I/O 多路复用技术、非阻塞 I/O、零内存拷贝技术。 - 线程安全:网络部分是线程安全的,使用线程池和回调。
- C++ 11 标准:需要使用支持 C++ 11 的编译器。
- 仅支持 Linux 平台:利用 Linux 特性优化性能,不支持跨平台。
- 可组合性强:解耦的模块设计,便于扩展和组合。
核心模块
base
(基础模块)- 非网络相关的通用工具
- 如线程池、时间戳、日志系统、原子操作等
net
(网络模块)- TCP 服务器 / 客户端模型
- Reactor 事件分发器
- Buffer、Channel、EventLoop、TcpConnection 等核心组件
核心组件
- EventLoop
- 事件循环,是每个线程的核心对象
- 封装了
epoll
库,处理文件描述符的读写事件
- Channel
- 表示一个
fd
(文件描述符)及其感兴趣的事件(如读写) - 是 EventLoop 与具体 I/O 事件之间的桥梁
- 表示一个
- Poller
- 封装
epoll
或poll
的接口(Muduo 默认用epoll
)
- 封装
- TcpServer / TcpClient
- 高层封装,简化服务端和客户端的使用
- 支持多线程连接处理
- Callback 机制
- 所有 I/O 事件都通过用户注册的回调函数处理(高扩展性)
- EventLoop
性能优势
- 完全采用非阻塞、异步 I/O 模型
- 使用智能指针管理资源(如
std::shared_ptr<TcpConnection>
) - 零内存拷贝的数据缓冲机制(Buffer)
- 合理利用多线程资源(EventLoopThreadPool)
适用场景
- 高并发 TCP 服务器(如 Redis、MQTT、游戏网关)
- 微服务通信框架(可自定义通信协议)
- 高性能 HTTP 服务(支持 HTTP 1.0/1.1)
- 自研 RPC 系统
平台兼容性
- Muduo 库只支持 Linux 平台,不兼容 Windows 平台,因为其底层使用了 Linux 平台的
pthread
和epoll
。
Muduo 的 Reactor 模型
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs.
The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
从上面的描述,可以看出以下关键点:
- 事件驱动处理
- 可以处理一个或多个输入源
- 通过 Service Handler 同步地将输入事件(Event)采用多路复用分发给相应的 RequestHandler(多个)来处理
学习建议
建议阅读 Muduo 的源码,从 TcpServer
的 start()
方法开始,阅读一下 Muduo 的底层源码实现,理解 MainReactor 和 SubReactor 的工作原理,这样有利于应付 Muduo 相关的面试问题,也能更深入的去表达 Muduo 相关的内容。
Muduo 的安装
安装 Boost 库
1 | # 安装 Boost 的所有组件和头文件 |
提示
由于 Muduo 使用了 Boost 库(如 boost::any
),因此需要安装 Boost 库。
安装 Muduo 库
- 编译安装
1 | # Git 克隆代码 |
- 验证安装
1 | # 查看 Muduo 库的头文件 |
提示
- Muduo 的编译依赖 CMake 和 Boost 库,默认编译生成的是静态库(
.a
),如果需要编译生成共享库(.so
),可以自行修改CMakeLists.txt
中的配置。 - Muduo 支持 C++ 11,仅支持 Linux 平台,不支持 Windows 平台,建议使用
7.x
及以后版本的g++
编译器。
Muduo 的使用
使用 Muduo 的日志系统
在开发软件产品过程中,日志的输出非常重要,可以记录很多软件运行过程中的信息,方便定位调试问题和跟踪统计信息等等。
- Muduo 提供的日志级别
1 |
- Muduo 日志级别的使用
1 |
|
程序运行输出的结果:
1 | 20250522 22:34:32.067870Z 22233 INFO 这是一个 info 日志 - main.cpp:45 |
实现 TCP 客户端和服务端
实现基于 Muduo 网络库的 TCP 客户端和服务端程序,只需要简单地组合使用 TcpServer
和 TcpClient
就可以。
TCP 服务端代码
muduo_server.h
源文件
1 |
|
- 当需要测试 TCP 服务端代码时,可以执行
telnet 127.0.0.1 6000
命令启动一个 TCP 客户端,然后输入要发送的内容,最后按下回车键即可发送数据给 TCP 服务端。 - 当需要断开 Telnet 连接(TCP 客户端连接)时,可以使用快捷键
ctrl + ]
切换到一个特殊的 Telnet 命令行界面;然后在这个界面输入quit
命令,就可以退出 Telnet 命令行界面。 - 或者,推荐直接使用 Linux 的
nc 127.0.0.1 6000
命令来启动一个 TCP 客户端,然后再发送数据给 TCP 服务端,断开 TCP 客户端连接只需要使用快捷键ctrl + c
关闭nc
命令的进程即可。
TCP 客户端代码
muduo_client.h
源文件
1 |
|
- 当需要测试 TCP 客户端代码时,可以执行 Linux 的
nc -l 127.0.0.1 6000
命令启动一个 TCP 服务端,然后输入要发送的内容,最后按下回车键即可发送数据给 TCP 客户端。 - 当需要关闭 TCP 服务端(断开 TCP 客户端连接)时,只需要使用快捷键
ctrl + c
关闭nc
命令的进程即可。
TCP 连接测试代码
test.cpp
源文件
1 |
|
程序运行后输出的结果:
1 | ==> client 127.0.0.1:45636 -> 127.0.0.1:6000 state: connected |
使用 Muduo 的线程池执行任务
Muduo 线程池的介绍
采用 Muduo 进行服务器编程时,如果遇到需要开辟多个线程来单独处理复杂的计算任务或者其它阻塞任务,那么就不需要手动调用 pthread_create()
来创建线程了;因为 Muduo 提供的 ThreadPool
线程池管理类已经将 Linux 的线程创建完全封装起来了。如果想研究 Muduo 线程池的底层源码,可以剖析 Muduo 项目中的 ThreadPool.cc
和 Thread.cc
源文件。ThreadPool 的使用示例如下:
1 | // 线程池 |
1 | // 客户端与服务器连接成功 |
Muduo 线程池的使用
这里将介绍如何使用 Muduo 的线程池来执行多个计算任务(并行计算)。
1 |
|
程序运行后输出的结果:
1 | 1 + 2 + ... + 100000000 = 5000000050000000 |
Muduo 的原理
Muduo 是一个高性能的 Reactor 模式网络库,采用 one loop per thread + thread pool
的架构实现,其代码简洁、逻辑清晰,是学习高性能网络编程的优秀范例。
整体类图
下面这张图是陈硕提供的 Muduo 网络库的类图(图中灰色的类是内部类,白色的是外部类)。
整体架构
Muduo 的代码结构主要分为两个模块:base
和 net
,其中 base
模块实现了一些通用的基础功能,而 net
模块基于 base
提供的工具类,构建了更高层次的网络编程抽象。
base 模块
base
模块实现了一些通用的基础功能,例如日志系统(log
)、线程(thread
)、线程池(threadpool
)、互斥锁(mutex
)、队列(queue
)等。这些组件不仅在网络模块中被广泛复用,也为构建高性能程序提供了良好的基础。base
模块的设计强调高内聚、低耦合,模块之间依赖关系清晰,职责划分明确,代码风格一致,逻辑简单直接。因此,即使是初学者,在阅读 base
源码时也能较容易理解其实现思路和设计意图。由于 base
模块本身并不涉及复杂的网络细节,这里不做深入探讨,重点将放在更具挑战性的 net
模块上,分析其在事件驱动、连接管理及多线程调度等方面的实现细节。
net 模块
net
模块基于 base
提供的工具类,构建了更高层次的网络编程抽象。网络编程的本质在于对底层 socket
接口以及 I/O 多路复用机制(如 poll
、epoll
)的封装,目的在于提升易用性、可维护性,同时屏蔽掉底层 API 中存在的一些坑。在实现了基本的网络 I/O 能力后,系统性能与并发处理能力便成为网络库设计的关键。Muduo 使用基于 poll
/ epoll
的 I/O 多路复用模型构建事件驱动系统,并采用 one loop per thread
的架构设计,即每个 I/O 线程拥有一个独立的事件循环 EventLoop
。这种设计结合线程池机制,使得多个线程可以并行处理不同连接或任务,从而充分发挥多核硬件的性能优势,实现高性能、高并发的网络处理能力。net
模块的封装较为彻底,向上层应用提供了简洁易用的接口,隐藏了复杂的内部实现逻辑。得益于这种设计,开发者在使用 Muduo 进行网络编程时,能够专注于业务逻辑本身,而无需深入底层处理细节。
整体设计
EventLoop
EventLoop
事件循环(即 Reactor 反应器),负责 I/O 事件和定时器事件的分派,每个线程只能有一个 EventLoop
实体。
- 使用
TimerQueue
作为计时器管理。 - 使用
Poller
作为 I/O 多路复用机制。
TimerQueue
TimerQueue
底层使用 timerfd_*
系列函数将定时器转换为 fd
,并添加到事件循环中。
- 当时间到达后,这些
fd
会自动触发事件。 - 内部使用
std::set
管理所有注册的Timer
。 - 由于
set
有自动排序功能,所以事件循环中总是优先处理最早到期的 Timer。
Poller 抽象类
Poller
是 I/O 多路复用的抽象类。
- 其具体实现由子类
PollPoller
(封装了poll
)和EpollPoller
(封装了epoll
)完成。 Pooler
是 Muduo 中唯一使用面向对象(虚函数)实现的模块,提供了回调功能。Poller
中的updateChannel()
函数用于注册和更新fd
的事件,所有的fd
都必须通过它添加到事件循环中。
任务队列
除了管理定时器和 I/O 事件外,EventLoop
还包含一个任务队列。
- 任务队列用于执行一些计算任务,可由其他线程添加到任务进队列中。
EventLoop
在处理完 I/O 事件后,会依次执行队列中的任务。- 多线程处理共享资源时,可以通过将操作提交到某个
EventLoop
的任务队列中,让该线程负责资源管理,其他线程无需加锁,只需加锁队列即可,从而简化并发模型,减少锁的使用。
异步唤醒线程:
- 如果
EventLoop
阻塞在epoll_wait
中,则会无法及时处理队列中的任务。 - 因此需要一种通信机制来唤醒
EventLoop
所在线程,被唤醒后就取出队列中的任务进行执行。 - Muduo 使用
eventfd(2)
来实现异步唤醒。
Channel 事件包装器
在 Muduo 中,通过 Channel
对 fd
进行封装。更准确地说,是对 fd
的事件相关方法的封装。
- 它负责注册
fd
的可读或者可写事件到EventLoop
。 - 以及事件产生时,定义事件如何响应。
- 每个
fd
对应一个Channel
(聚合关系)。 Channel
析构时不会close
掉这个fd
。- 提供
handleEvent()
函数,当fd
有事件产生时,EventLoop
会调用它进行处理。 handleEvent()
的内部会根据可读或者可写事件分别调用已注册的回调函数。
通常 Channel
被作为其他类的成员来使用,例如:
EventLoop
内部通过std::vector<Channel*>
管理多个注册的fd
。EventLoop
与多个Channel
形成一对多的关系。
Socket 封装类
Socket
也是对 fd
的封装,但不同于 Channel
:
Socket
仅封装由::socket
系统调用产生的fd
。- 提供获取 / 设置网络连接属性的方法(如地址复用、关闭 Nagle 算法等)。
- 与
fd
是组合关系,Socket
析构时会自动close
这个fd
。
尽管有
Socket
和Channel
的封装,某些系统调用仍然需要直接传递原始fd
,因此实际代码中常见同时持有fd
、Socket
和Channel
的情况。
TcpConnection 抽象类
TcpConnection
是对一个 TCP 连接的抽象。
- 一个
TcpConnection
包含一个Socket
和一个Channel
。 - 在构造时就为
Channel
注册好了事件回调:handleRead()
:处理可读事件handleWrite()
:处理可写事件
当有事件触发后:
handleRead()
会先处理接收数据。- 然后转交给上层逻辑(调用上层注册的回调函数)。
- 回调函数由用户通过
TcpConnection
注册,内部会传递给Channel
。 - 因为
Channel
是个内部类,所以回调函数的注册只能由TcpConnection
负责,上层无需接触Channel
,只需将回调函数注册到TcpConnection
即可。
为什么需要 TcpConnection?
- 为了解决 TCP 协议下收发数据的阻塞问题
- 比如调用
::write
时,若内核缓冲区已满,则写操作会阻塞。 - 一个优秀的网络库应允许上层调用一次
sendMessage()
就能完成数据发送,而非频繁判断是否可写。 TcpConnection
内部维护了inputBuffer
和outputBuffer
:- 支持数据缓存和分批发送。
- 保证在连接断开前,数据能够全部发送完成。
Acceptor 连接接收器
Acceptor
用来接受新连接。
- 内部维护一个
listenfd
和对应的Channel
。 - 初始化时执行:
::bind
::listen
- 并将
listenfd
注册到事件循环。
当有新连接到来:
- 触发
Channel
的handleRead()
,调用accept()
接收连接。 - 再调用上层注册的
newConnectionCallback
。 Acceptor
是TcpServer
的内部类,回调由TcpServer
注册。TcpServer
会为每个新连接创建一个TcpConnection
,并设置相应的回调函数。- 所有连接通过一个
map
被TcpServer
管理。
并发连接的处理:
TcpServer
使用线程池处理并发请求。- 线程池包含多个 I/O 线程(即多个
EventLoop
)。 - 每个连接会自动分配给某个线程的
EventLoop
,在其上创建TcpConnection
。 - 大幅提高了服务器的并发处理能力。
Connector 和 TcpClient
Connector
与TcpClient
的关系类似于Acceptor
与TcpServer
。- 区别在于
Connector
用于主动发起连接。 - 负责连接重试、超时等连接建立相关逻辑。