Muduo的本质:

Muduo 是一个基于 Reactor 模式、One loop per thread 线程模型的高性能 C++ 网络库。

关键词对应章节大白话
Reactor 模式第一章事件驱动,来了事件才干活,不傻等
One loop per thread第四章一个线程配一个事件循环
高性能各章非阻塞 + epoll + 缓冲区复用

整个库的零件清单如下,后面逐章拆解:

零件职责一句话
EventLoop事件循环大总管,负责分发所有事件
Channel事件载体每个 fd 配一个,知道"我对什么事件感兴趣"
Buffer数据缓冲区每个连接配一个,暂存收发数据
Poller多路复用器封装 epoll,负责"等事件"
TimerQueue定时器队列管理所有定时任务
Acceptor连接接收器只干一件事:accept 新连接
TcpConnection连接对象代表一条已建立的连接
TcpServer服务器把上面所有零件组装起来

简记:TcpServer 是工厂,Acceptor 是门卫,EventLoop 是车间主任,Channel 是工人,Buffer 是工人手里的货筐。


一、Reactor 模型

1. 什么是 Reactor

通俗解释:传统 I/O 像"派个人守在门口,挨个问’你好了没’",Reactor 像"装了个门铃,谁按门铃我才去开门"。

Reactor 是一种基于事件驱动的 I/O 处理模式:

  • 程序不主动轮询每个连接。
  • 把连接交给内核的 I/O 复用机制(epoll / poll)监听。
  • 事件就绪了,内核通知程序,程序再去处理。

传统轮询 vs Reactor

2. 三种 Reactor 变体

Reactor 按线程数量演进出三种形态,一张表对比:

变体线程结构谁 accept谁处理读写瓶颈
单 Reactor 单线程1 个线程同线程同线程任意一处阻塞全卡死
单 Reactor 多线程1 接收 + N 工作接收线程工作线程池accept 仍是单点
多 Reactor 多线程1 主 + N 子主 Reactor子 Reactor 各管一摊无明显瓶颈,主流方案

Muduo采用的就是第三种:多 Reactor 多线程。

3. Muduo 的多 Reactor 分工

多 Reactor 分工

  • 主 Reactor(Main Reactor):只 accept 新连接,做最轻的事。
  • 子 Reactor(Sub Reactor):每个子线程跑一个 EventLoop,各自负责一部分连接的读写。
  • 连接处理器(Connection Handler):把新连接轮询分配到某个子 Reactor,实现负载均衡。

一句话:主 Reactor 只"接客",子 Reactor 负责"干活",各忙各的,互不阻塞。

4. Reactor vs Proactor

对比项ReactorProactor
I/O 操作同步(内核只通知"就绪",程序自己 read/write)异步(内核直接完成 read/write,再通知"完成")
代表libevent、libuv、Muduo、Netty、RedisIOCP(Windows)、Boost.Asio
复杂度简单、主流复杂,依赖操作系统异步 I/O 支持

5. 为什么底层选择使用 epoll

对比项select / pollepoll
查找就绪事件每次遍历所有 fd(O(n))直接返回就绪列表(O(1))
fd 数量上限select 默认 1024无上限
拷贝每次把 fd 集合在用户态/内核态来回拷只注册一次
触发方式仅水平触发支持水平 + 边缘

水平触发(LT):只要缓冲区还有数据,就反复通知,不漏数据但可能重复。 边缘触发(ET):只在状态变化时通知一次,高效但要一次读干净。

Muduo 用的是 LT + 非阻塞 I/O,稳妥不易出错。

简记:

  • 为什么不用 select?—— O(n) 遍历 + fd 上限 + 每次拷贝。
  • LT 和 ET 的区别?—— LT 反复通知,ET 只通知一次。
  • Muduo 为什么选 LT?—— 简单可靠,配合非阻塞读也不会出错。

二、Socket 编程关键点

1. TCP 连接与 Socket API 的对应

TCP 连接与 Socket API 对应

步骤Socket API说明
建立连接connect() / accept()背后是 TCP 三次握手
传输数据send() / recv()背后是内核缓冲区的搬运
关闭连接close()背后是 TCP 四次挥手

2. TIME_WAIT 与 SO_REUSEADDR

通俗解释:服务器 close() 后,端口不会立刻释放,而是进入 TIME_WAIT 状态"停留几十秒",防止旧连接的迟到数据干扰新连接。如果此时重启服务器,会报 Address already in use。

  • SO_REUSEADDR:允许新 Socket 绑定到仍处于 TIME_WAIT 的端口。
  • 作用:服务器重启后能立刻再绑定同一个端口,不用干等几十秒。

服务器几乎必开 SO_REUSEADDR。

3. 粘包 / 拆包

通俗解释:TCP 是"字节流",没有消息边界。你发两个"包裹",对端可能一次收到一个半,或两个一起收到。

粘包与拆包

三种常见解决方案:

方案做法优缺点
定长包每个包固定长度简单,但浪费空间
分隔符用特殊字符隔开(如 \r\n)简单,但数据里不能出现分隔符
长度前缀包头 4 字节存长度 + 数据体最通用,Muduo 推荐

4. SO_KEEPALIVE vs 应用层心跳

对比项SO_KEEPALIVE应用层心跳
谁实现内核应用程序
默认探测间隔很长(通常 2 小时)自定义(如 30 秒)
能发现对端"进程死了"能能
能发现对端"进程活着但业务卡死"不能能

结论:SO_KEEPALIVE 是兜底,业务上通常还要自己做应用层心跳。

5. send / recv 的正确姿势

情况返回值正确处理
recv 返回 0对端关闭了连接关闭连接
recv 返回 -1 且 errno==EAGAIN缓冲区暂时无数据等下次事件,不是错误
send 返回 -1 且 errno==EAGAIN缓冲区满关注写事件,稍后再发
send 返回 < 预期长度只发了一部分把剩余数据存起来继续发

关键:非阻塞 I/O 下,EAGAIN 不是错误,是"现在没数据,别急"。这是最容易误解的点。


三、Channel —— 事件的载体

1. Channel 是什么

通俗解释:每个 fd 配一个 Channel,它就像 fd 的"秘书"——记着"老板对什么事件感兴趣"(events_),以及"刚才发生了什么事件"(revents_)。

Channel 与 fd 绑定

核心规则:一个 Channel 始终对应一个 fd,两者生命周期绑定。

2. 关键成员深挖

成员类型含义大白话
fd_int监听的 fd盯哪个人
events_int(位掩码)感兴趣的事件我想知道啥
revents_int(位掩码)就绪的事件刚才发生了啥
index_int在 Poller 中的状态门卫登记没
tie_weak_ptr防悬空防止主人没了

events_ / revents_ 的位掩码(用二进制的位表示不同事件):

事件宏含义
POLLIN / EPOLLIN可读
POLLOUT / EPOLLOUT可写
POLLERR出错
POLLHUP挂断(对端关闭)

events_ 是我主动注册的,revents_ 是 epoll 返回的。epoll_wait 后,revents_ 会被内核填上实际就绪的事件。

3. tie_ 如何防悬空(重点)

场景:Channel 的回调里可能要用到所属的 TcpConnection 对象。如果连接已经销毁了,回调还在跑,就会访问已释放的内存(悬空指针),导致崩溃。

解决办法:tie_ 是一个 std::weak_ptr,指向所属的连接对象。

tie_ 防悬空

关键点:Channel 不拥有 TcpConnection,只持有 weak_ptr,这样连接销毁时 Channel 能感知到,避免悬空。

4. Channel 的状态机

Channel 在 Poller 中有三种状态,用 index_ 记录:

状态值含义
kNew-1还没注册到 Poller
kAdded>= 1已注册
kDeleted2已注册但待删除

常用方法:

方法作用
enableReading()关注读事件,events_ |= POLLIN
enableWriting()关注写事件,events_ |= POLLOUT
disableAll()取消所有关注
setReadCallback()绑定读回调

5. 回调绑定与分发

Channel 上挂着几个回调函数,EventLoop 分发事件时直接调用:

回调触发时机
readCallback_可读事件就绪
writeCallback_可写事件就绪
closeCallback_连接关闭
errorCallback_出错

分发逻辑一句话:epoll_wait 返回就绪事件 → 找到对应 Channel → 按 revents_ 调用对应回调。

简记:

  • events_ 和 revents_ 的区别?—— 前者是"关心",后者是"实际发生"。
  • tie_ 为什么用 weak_ptr?—— 防连接销毁后回调悬空。
  • Channel 和 fd 的关系?—— 一一对应。

四、EventLoop —— 事件循环(核心)

1. 什么是 EventLoop

通俗解释:EventLoop 是"车间主任",它永远在一个循环里干活——“等事件 → 处理事件 → 再等”。

EventLoop::loop() 核心逻辑

2. One loop per thread(核心原则)

一个线程至多有一个 EventLoop,一个 EventLoop 只属于一个线程。

为什么这样设计?

好处说明
无锁所有操作都在本线程,不需要加锁
简单不用考虑跨线程访问数据的安全问题
高效避免线程切换和锁竞争

这就是 Muduo 高性能的根基:用"一个线程一个 loop"换掉"一把大锁"。

3. runInLoop / queueInLoop(源码级)

因为 EventLoop 属于某个线程,别的线程不能直接操作它。所以提供两个安全入口:

void EventLoop::runInLoop(Functor cb) {
    if (isInLoopThread()) {     // 就在 loop 线程里
        cb();                    // 直接执行
    } else {
        queueInLoop(cb);         // 塞进队列,唤醒 loop
    }
}

void EventLoop::queueInLoop(Functor cb) {
    pendingFunctors_.push_back(cb);   // 加入任务队列
    if (!isInLoopThread() || callingPendingFunctors_) {
        wakeup();                     // 唤醒阻塞中的 loop
    }
}

两者的区别:

接口语义适用场景
runInLoop在 loop 线程里执行;不在则转投队列用户想"确保在 loop 线程里跑"
queueInLoop无条件丢进队列,异步执行不需要立即执行,排队即可

4. wakeup() 与 eventfd(唤醒机制)

问题:loop 线程正阻塞在 epoll_wait 上睡觉,别的线程往队列塞了任务,怎么叫醒它?

答案:用 eventfd 创建一个"唤醒管道",把它也注册进 epoll。

eventfd 唤醒机制

eventfd 比 pipe 轻量,且是 Linux 专门为"线程间唤醒"设计的。

5. EventLoopThread / EventLoopThreadPool

多 Reactor 需要多个 loop 线程,靠这两个类管理:

类作用
EventLoopThread封装"一个线程 + 一个 EventLoop"的启动与停止
EventLoopThreadPool管理多个 EventLoopThread,提供轮询分配

分配策略:TcpServer 拿到新连接后,从池子里**轮询(round-robin)**挑一个 loop 给它。

新连接1 → 子loop0
新连接2 → 子loop1
新连接3 → 子loop2
新连接4 → 子loop0   ← 轮询回来了

简记:

  • One loop per thread 为什么?—— 避免加锁,简化模型,提高性能。
  • runInLoop 和 queueInLoop 区别?—— 前者同线程直接跑,后者总是排队。
  • 怎么唤醒阻塞的 loop?—— eventfd,写字节触发 epoll 可读。

五、Buffer —— 数据缓冲区

1. 为什么需要 Buffer

通俗解释:TCP 是字节流,一次 recv 读到的数据量不确定。你得有个"货筐"先把数据接着,攒够了再按业务处理。

Muduo 里,每个 TcpConnection 都有一个输入 Buffer 和一个输出 Buffer。

2. Buffer 的内存布局(核心)

Buffer 底层用 vector<char>,用两个下标管理数据,布局如下:

Buffer 内存布局

下标含义
readIndex_可读数据的起点
writeIndex_可写空间的起点
  • readIndex_ 和 writeIndex_ 之间的,是已写入、还没读走的数据。
  • readIndex_ 前面留了一段 prependable 空间。

3. prepend 区的作用

为什么读数据前面要留 8 字节?

当需要在数据前面插入东西(比如粘包处理里补一个长度头)时,可以直接往 prepend 区写,不用把整个数据后移,省一次拷贝。

4. Buffer 的四种状态

Buffer 的四种状态

关键:读写都是在原地滑动指针,不用反复 malloc/free,这就是 Buffer 高效的原因。

5. 分散读 readv(源码级)

readFd 用 readv 一次读多个缓冲区,减少系统调用:

ssize_t Buffer::readFd(int fd, int* savedErrno) {
    char extrabuf[65536];
    struct iovec vec[2];
    vec[0].iov_base = writable();            // 主缓冲区可写区
    vec[0].iov_len  = writableBytes();
    vec[1].iov_base = extrabuf;              // 栈上备用缓冲
    vec[1].iov_len  = sizeof(extrabuf);
    // 一次 readv,能读多少读多少
    ssize_t n = readv(fd, vec, 2);
    ...
}

好处:数据量小时直接读进 Buffer;数据量大时溢出到 extrabuf,一次系统调用搞定,不用先探测再读。

6. 扩容策略

  • Buffer 的 writable 空间不够时,会自动扩容(vector 增长)。
  • 扩容时通常按需增长,而不是固定翻倍,避免内存浪费。

7. 基于 Buffer 拆包

结合第二章的粘包问题,用 Buffer 的 readIndex_ 就能实现拆包:

recv 数据 → 追加到 Buffer
         → 检查 Buffer 里可读数据够不够一个完整包(长度前缀)
         → 够 → 取出来处理,readIndex_ 前移
         → 不够 → 继续等下一次事件

这就是"长度前缀"方案在 Muduo 上的落地:Buffer 负责攒数据,业务负责判断边界。

简记:

  • 为什么用两个下标不用一个?—— 支持"读了不消费"和 prepend 预留。
  • prepend 区干嘛的?—— 前面插数据不用搬移。
  • readv 干嘛的?—— 一次系统调用读多块缓冲。

六、Timer —— 定时器

1. Timer 是什么

通俗解释:让程序能"到点了自动干某件事",是心跳、超时、限流等功能的地基。

2. 常见应用场景

场景说明
定时发送周期上报、定时推送
会话超时连接长时间无响应就清理
心跳包定期发心跳,检测对端是否存活
限流控制按时间窗口控制速率
延迟重试失败后隔一段时间再试

3. Timer 相关组件

组件作用
Timer单个定时器:到期时间 + 回调 + 是否周期
TimerQueue定时器队列,按到期时间排序
TimerId定时器句柄,用于取消
timerfd内核定时器,用于唤醒 loop

4. TimerQueue 的底层结构(源码级)

TimerQueue 内部用 std::set 存储定时器,按到期时间排序:

std::set<Timer>  (按 expiration 排序)
   ├── Timer(10:00:01)   ← 最近到期
   ├── Timer(10:00:05)
   └── Timer(10:00:30)
        ↑
   每次只看最前面一个,到期就执行

用 set 的好处:最早到期的永远在最前面,取出来 O(1),插入/删除 O(log n)。

5. timerfd_create 的作用

问题:loop 线程阻塞在 epoll_wait,怎么在"定时器到期"时精确醒来?

答案:用 timerfd_create 创建一个内核定时器,到期时它对应的 fd 会变可读,正好能被 epoll 监听到。

定时器到期 → 内核把 timerfd 置为可读 → epoll 返回 → loop 醒来处理定时任务

这样定时器事件和 I/O 事件统一了——都通过 epoll 通知,loop 不需要额外 sleep 或轮询时间。

6. 定时器的生命周期

操作行为
addTimer新建 Timer 插入 set,更新 timerfd 的到期时间
cancel从 set 移除
到期执行回调;若是周期定时器,重新计算时间再插回 set

7. 时间精度与开销

  • timerfd 精度可达纳秒,实际使用毫秒级足够。
  • 定时器太多会带来 set 维护开销,但 Muduo 场景下可忽略。

简记:

  • TimerQueue 用什么结构?—— std::set,按到期时间排序。
  • 为什么要 timerfd?—— 让定时事件统一走 epoll,精确唤醒 loop。

七、TcpConnection —— 一条连接的完整生命周期

1. 什么是 TcpConnection

通俗解释:TcpConnection 是"一条已建立连接的化身",代表你和某个客户端的通话。它拥有这条连接的 Socket、Channel、输入/输出 Buffer。

TcpConnection 拥有的东西:
   ├── Socket(套接字)
   ├── Channel(事件载体)
   ├── input Buffer(收数据)
   └── output Buffer(发数据)

2. 状态机(四态)

一条连接从建立到关闭,经历四种状态:

状态含义触发
kConnecting正在建立刚 accept(客户端是 connect)
kConnected已连接握手完成
kDisconnecting正在断开主动 close 发起
kDisconnected已断开断开完成

TcpConnection 状态机

为什么有 kDisconnecting 这个"中间态"?—— 因为发送缓冲里可能还有数据没发完,不能立刻断开,得先等数据发完。

3. 关键回调

TcpConnection 对外暴露几个回调,用户实现业务逻辑:

回调触发时机
connectionCallback连接建立/断开时
messageCallback收到完整数据时
writeCompleteCallback数据全部写完时

4. 优雅关闭(重点)

通俗解释:不能"啪"地挂电话,要先把手头的话说完(把缓冲区数据发完)再挂。

close() 请求
   ↓
  如果 output buffer 还有数据 → 进入 kDisconnecting,先发完
  如果 buffer 空了 → 真正 close

这解释了为什么需要 kDisconnecting 状态——优雅关闭的关键。

5. 连接销毁时机(配合 Channel 的 tie_)

连接关闭时,TcpConnection 会被销毁。但因为 Channel 持有的是 weak_ptr(tie_),不会导致悬空,回调里能安全判断连接是否还活着。

简记:

  • TcpConnection 有哪些状态?—— 四态:connecting/connected/disconnecting/disconnected。
  • 为什么要有 kDisconnecting?—— 等输出缓冲发完再关,实现优雅关闭。

八、Acceptor —— 迎接新连接

1. 什么是 Acceptor

通俗解释:Acceptor 是"门卫",只干一件事——守监听套接字,来了新连接就 accept。

2. 内部组成

Acceptor
   ├── listenfd(监听套接字,绑定在 listen 端口)
   ├── acceptChannel(监听 listenfd 的 Channel)
   └── newConnectionCallback(拿到新连接后回调)

3. accept 流程

accept 流程

4. 处理并发到达

  • 一次只 accept 一个:用 LT 触发,只要还有连接没 accept,epoll 会反复通知,不会漏。
  • 也可能在循环里连续 accept 多个,直到 EAGAIN。

为什么用 LT?—— LT 保证"只要还有未 accept 的连接,就会再通知",天然不会漏接。


九、TcpServer —— 把所有零件组装起来

1. 什么是 TcpServer

通俗解释:TcpServer 是"工厂总装线",把前面所有零件组装成一个能跑的服务器。

2. 组装清单

零件数量职责
Acceptor1主 loop 里 accept 新连接
EventLoopThreadPool1管理 N 个子 loop
TcpConnection每连接 1代表一条连接

3. 启动流程

TcpServer 构造:传入主 loop、端口
   ↓
start():
   ├── 启动 EventLoopThreadPool(子 loop 们跑起来)
   └── 启动 Acceptor 监听
   ↓
新连接到来 → accept → 从池子轮询选子 loop → 建 TcpConnection → 绑定到该子 loop

4. 对外接口

方法作用
setConnectionCallback设置连接建立/断开回调
setMessageCallback设置收到消息回调
setThreadNum设置子 Reactor 数量
start启动服务器

十、完整调用链

把前面所有零件串起来,一条数据从进到出的完整路径:

完整调用链

一条数据经历:accept → 分配 loop → 注册 Channel → epoll 通知 → Buffer 缓冲 → 业务处理 → 回写。


十一、Echo 服务器完整示例

一个最简的 echo 服务器,对照上面所有概念:

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/base/Logging.h>
#include <string>

using namespace muduo;
using namespace muduo::net;

// 收到消息的回调:原样回发
void onMessage(const TcpConnectionPtr& conn,
               Buffer* buf,
               Timestamp time)
{
    std::string msg(buf->retrieveAllAsString());  // 取出 Buffer 里全部数据
    conn->send(msg);                               // 原样发回(echo)
}

// 连接建立/断开的回调
void onConnection(const TcpConnectionPtr& conn)
{
    if (conn->connected()) {
        LOG_INFO << "新连接来自 " << conn->peerAddress().toIpPort();
    } else {
        LOG_INFO << "连接断开";
    }
}

int main()
{
    EventLoop loop;              // 主 Reactor
    InetAddress addr(8888);      // 监听 8888 端口
    TcpServer server(&loop, addr, "EchoServer");

    server.setConnectionCallback(onConnection);  // 绑定连接回调
    server.setMessageCallback(onMessage);        // 绑定消息回调
    server.setThreadNum(4);      // 4 个子 Reactor

    server.start();              // 启动
    loop.loop();                 // 主 loop 跑起来
    return 0;
}

代码对照概念:

代码对应概念
EventLoop loop主 Reactor
server.setThreadNum(4)4 个子 Reactor(多 Reactor 多线程)
setMessageCallback注册 TcpConnection 的消息回调
buf->retrieveAllAsString()从 Buffer 读数据(readIndex 前移)
conn->send写 output Buffer

问题解决

  1. 什么是 Reactor 模式? —— 事件驱动 + I/O 复用,epoll 监听就绪后回调处理。
  2. One loop per thread 有什么好处? —— 避免加锁,简化模型,提高性能。
  3. epoll 的 LT 和 ET 区别?Muduo 用哪个? —— LT 反复通知,ET 只通知一次;Muduo 用 LT。
  4. events_ 和 revents_ 的区别? —— 前者是"关心的事件",后者是"实际就绪的事件"。
  5. Channel 的 tie_ 为什么用 weak_ptr? —— 防连接销毁后回调访问悬空内存。
  6. runInLoop 和 queueInLoop 的区别? —— 同线程直接执行 vs 总是排队。
  7. 怎么唤醒阻塞在 epoll 的 loop? —— eventfd,写字节触发可读。
  8. Buffer 为什么要 prepend 区? —— 前面插数据(如长度头)不用搬移。
  9. TimerQueue 用什么数据结构? —— std::set,按到期时间排序。
  10. 为什么要 timerfd? —— 让定时事件统一走 epoll,精确唤醒 loop。
  11. TcpConnection 有哪些状态? —— 四态:connecting/connected/disconnecting/disconnected。
  12. 什么是优雅关闭? —— 等输出缓冲发完再 close,对应 kDisconnecting 态。

总结

概念一句话
Reactor事件驱动 + I/O 复用,epoll 监听、就绪后回调
One loop per thread一个线程一个事件循环,天然无锁
EventLoop事件循环,分发 I/O / 定时 / 用户任务
Channelfd + 事件 + 回调的封装,最小事件单位
Poller封装 epoll,负责"等事件"
Buffer连接级缓冲,readIndex/writeIndex 双指针
TimerQueueset 排序的定时器队列
Acceptor门卫,只 accept 新连接
TcpConnection一条连接的化身,四态状态机
TcpServer总装线,把所有零件拼成服务器
eventfd唤醒阻塞中 loop 的"闹钟"
timerfd让定时事件走 epoll 的内核定時器