IO 多路复用 — Reactor

Reactor 模式 --- 事件驱动的高并发服务器

一、为什么需要 Reactor 模式?

回顾之前的 epollserver,它存在几个问题:

问题 具体表现
功能耦合 IO 操作和业务逻辑混在一起
扩展性差 要改业务逻辑就得改服务器代码
代码重复 每个服务器都要写 Accepter/Recver/Sender
缺乏抽象 fd 管理、缓冲区管理都没有封装

Reactor 模式的核心思想

复制代码
┌─────────────────────────────────────────────────────┐
│                    Reactor 模式                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌────────────┐    ┌────────────┐    ┌────────────┐ │
│  │   Event    │───►│   Reactor  │───►│   Handler  │ │
│  │   Source   │    │ (事件循环) │    │ (回调处理) │ │
│  │ (epoll)    │    │            │    │            │ │
│  └────────────┘    └────────────┘    └────────────┘ │
│                                                     │
│  事件驱动:有事件才处理,没事件就等待                 │
│  回调机制:事件发生时调用注册好的处理函数              │
│  职责分离:Reactor 管 IO,Handler 管业务             │
│                                                     │
└─────────────────────────────────────────────────────┘

本项目的 Reactor 架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        应用层                                   │
│  ┌──────────────┐                                              │
│  │  Calculator  │ ← 业务逻辑(计算加减乘除)                     │
│  └──────────────┘                                              │
│         ▲                                                      │
│         │ DefaultMsg() 回调                                    │
│         ▼                                                      │
├─────────────────────────────────────────────────────────────────┤
│                        Reactor 层                               │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │                    TcpServer                              │ │
│  │  ┌─────────────┐    ┌─────────────────────────────────┐   │ │
│  │  │   Epoller   │◄───│         _conn_map              │   │ │
│  │  │ (事件检测)   │    │  fd → Connection 对象映射      │   │ │
│  │  └─────────────┘    └─────────────────────────────────┘   │ │
│  │         │                                    ▲             │ │
│  │         │ epoll_wait 返回就绪事件             │             │ │
│  │         ▼                                    │             │ │
│  │  ┌─────────────┐    ┌─────────────┐    ┌─────┴─────┐      │ │
│  │  │  Accepter   │    │   Recver    │    │   Sender   │      │ │
│  │  │ (新连接)    │    │ (读数据)    │    │ (写数据)   │      │ │
│  │  └─────────────┘    └─────────────┘    └───────────┘      │ │
│  └───────────────────────────────────────────────────────────┘ │
│         ▲                                                      │
│         │ Connection 对象                                      │
│         ▼                                                      │
├─────────────────────────────────────────────────────────────────┤
│                        网络层                                   │
│  ┌──────────────┐                                              │
│  │     Sock     │ ← Socket 封装(Socket/Bind/Listen/Accept)   │
│  └──────────────┘                                              │
└─────────────────────────────────────────────────────────────────┘

二、核心类详解

2.1 全局常量与类型定义

cpp 复制代码
uint32_t EVENT_IN = EPOLLIN | EPOLLET;    // 读事件 + 边缘触发
uint32_t EVENT_OUT = EPOLLOUT | EPOLLET;  // 写事件 + 边缘触发
const static int BUF_SIZE = 128;

using func_t = std::function<void(std::shared_ptr<Connection>)>;

EPOLLET ------ 边缘触发模式

  • 之前的 epollserver 用的是水平触发(LT,默认)
  • 边缘触发只在状态变化的瞬间通知一次
  • 必须配合非阻塞 IO 使用(否则会阻塞)

func_t ------ 回调函数类型

cpp 复制代码
std::function<void(std::shared_ptr<Connection>)>
  • 接受一个 shared_ptr<Connection> 参数
  • 无返回值
  • 可以绑定任意函数(成员函数、普通函数、lambda)

2.2 Connection 类 ------ 连接封装

cpp 复制代码
class Connection
{
public:
    Connection(int sock, std::shared_ptr<TcpServer> tcpserver)
        : _sock(sock)
        , _tcpserver(tcpserver)
    {}

成员变量

变量 类型 作用
_sock int 连接的 socket fd
_inbuffer std::string 读缓冲区(接收的数据)
_outbuffer std::string 写缓冲区(待发送的数据)
_recvCallback func_t 读就绪时的回调函数
_sendCallback func_t 写就绪时的回调函数
_exceptCallback func_t 异常时的回调函数
_tcpserver shared_ptr<TcpServer> 回指指针(指向所属的 TcpServer)
_ip std::string 客户端 IP
_port uint16_t 客户端端口

_tcpserver 回指指针的作用

  • Connection 对象需要调用 TcpServer 的方法(如 SenderEnableEvent
  • 通过回指指针可以访问 TcpServer 的成员
  • shared_ptr 防止悬空指针

缓冲区设计

复制代码
Connection 对象
    │
    ├── _inbuffer: "3\n{\"x\":1,\"y\":2,\"op\":\"+\"}\n"
    │              ↑ 收到的数据先存这里,再解码
    │
    └── _outbuffer: "4\n{\"result\":3,\"code\":0}\n"
                   ↑ 要发送的数据先存这里,再发送

SetHandler() ------ 设置回调函数

cpp 复制代码
void SetHandler(func_t recvCallback, func_t sendCallback, func_t exceptCallback)
{
    _recvCallback = recvCallback;
    _sendCallback = sendCallback;
    _exceptCallback = exceptCallback;
}

在创建连接时设置三个回调:

  • recvCallback:有数据可读时调用
  • sendCallback:可写时调用
  • exceptCallback:出错或断开时调用

缓冲区操作

cpp 复制代码
void AppendInBuffer(const std::string &info) { _inbuffer += info; }
void AppendOutBuffer(const std::string &info) { _outbuffer += info; }
std::string &InBuffer() { return _inbuffer; }
std::string &OutBuffer() { return _outbuffer; }

2.3 TcpServer 类 ------ Reactor 核心

cpp 复制代码
class TcpServer : public nocopy
{
    static const int MAX_EVENTS = 64;

private:
    std::shared_ptr<Epoller> _epoller_ptr;
    std::shared_ptr<Sock> _listensocket_ptr;
    uint16_t _port;
    bool _quit;
    struct epoll_event revs[MAX_EVENTS];
    std::unordered_map<int, std::shared_ptr<Connection>> _conn_map;  // fd → Connection 映射
    func_t _OnMessage;  // 上层业务回调
};

_conn_map ------ 连接管理哈希表

  • key:socket fd
  • value:对应的 Connection 对象指针
  • 快速查找:O(1)
  • 用于 Dispatcher 根据 fd 找到对应的 Connection

_OnMessage ------ 业务层回调

  • TcpServer 只负责 IO 操作(读/写)
  • 数据读完后调用 _OnMessage 交给业务层处理
  • 实现了 IO 层和业务层的分离
2.3.1 构造函数
cpp 复制代码
TcpServer(uint16_t port, func_t onMessageCallback)
    : _port(port)
    , _quit(true)
    , _listensocket_ptr(new Sock())
    , _epoller_ptr(new Epoller())
    , _OnMessage(onMessageCallback)
{}

参数 onMessageCallback

  • 这是业务层传入的回调函数
  • 当收到完整数据后,TcpServer 会调用这个函数
  • 实现了"服务器只负责 IO,业务逻辑由上层决定"的设计
2.3.2 Init() ------ 初始化
cpp 复制代码
void Init()
{
    _listensocket_ptr->Socket();
    SetNonBlockOrDie(_listensocket_ptr->Fd());  // 设置非阻塞!
    _listensocket_ptr->Bind(_port);
    _listensocket_ptr->Listen();
    logger(INFO, "TcpServer listen success: %d", _listensocket_ptr->Fd());
    
    // 注册监听 fd 的回调
    AddConnection(_listensocket_ptr->Fd(), EVENT_IN, 
        std::bind(&TcpServer::Accepter, this, std::placeholders::_1), 
        nullptr, nullptr);  // 监听 fd 不需要写和异常回调
}

关键改动SetNonBlockOrDie()

因为 Reactor 使用了 边缘触发(EPOLLET),必须配合非阻塞 IO:

触发模式 是否需要非阻塞 原因
水平触发(LT) 可选 数据没读完下次还会通知
边缘触发(ET) 必须 只通知一次,如果阻塞在 read/write 会卡死
2.3.3 SetNonBlockOrDie() ------ 设置非阻塞
cpp 复制代码
void SetNonBlockOrDie(int socketfd)
{
    int fl = fcntl(socketfd, F_GETFL);
    if(fl < 0)
    {
        exit(NONBLOCKERR);
    }
    fcntl(socketfd, F_SETFL, fl | O_NONBLOCK);
}

原理

  1. fcntl(F_GETFL) 获取当前文件状态标志
  2. fcntl(F_SETFL, fl | O_NONBLOCK) 添加非阻塞标志
  3. 设置后,read()/write()/accept() 在没有数据时会立即返回 EWOULDBLOCK
2.3.4 AddConnection() ------ 添加连接
cpp 复制代码
void AddConnection(int sock, uint32_t event, func_t recvCallback, 
    func_t sendCallback, func_t exceptCallback,
    const std::string &ip = "0.0.0.0", uint16_t port = 0)
{
    // 第1步:注册到 epoll
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, event);
    
    // 第2步:创建 Connection 对象
    std::shared_ptr<Connection> new_connection = 
        std::make_shared<Connection>(sock, (std::shared_ptr<TcpServer>)(this));
    new_connection->SetHandler(recvCallback, sendCallback, exceptCallback);
    new_connection->_ip = ip;
    new_connection->_port = port;
    
    // 第3步:加入连接管理表
    _conn_map.insert(std::make_pair(sock, new_connection));
    
    logger(DEBUG, "Add a new connection, sock: %d", sock);
}

三步操作

  1. 告诉 epoll 要监听这个 fd 的什么事件
  2. 创建一个 Connection 对象封装这个连接
  3. 把 Connection 对象加入 _conn_map 方便查找

std::bind 的使用

cpp 复制代码
std::bind(&TcpServer::Accepter, this, std::placeholders::_1)
  • 把成员函数 Accepter 绑定成 func_t 类型的回调
  • this 是 TcpServer 对象指针
  • _1 是占位符,表示第一个参数(Connection 对象)
2.3.5 Accepter() ------ 接受新连接(循环 accept)
cpp 复制代码
void Accepter(std::shared_ptr<Connection> connection)
{
    while(1)  // 循环 accept!
    {
        struct sockaddr_in peer;
        socklen_t peerlen = sizeof(peer);
        int sock = accept(connection->SockFd(), (struct sockaddr *)&peer, &peerlen);
        
        if(sock > 0)
        {
            uint16_t peerport = ntohs(peer.sin_port);
            char ip[128];
            inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip));
            logger(DEBUG, "Get a new client ip: %s, port: %d", ip, peerport);
            
            SetNonBlockOrDie(sock);  // 新连接也要设为非阻塞
            
            AddConnection(sock, EVENT_IN, 
                std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                std::bind(&TcpServer::Excepter, this, std::placeholders::_1),
                ip, peerport);
        }
        else
        {
            if(errno == EWOULDBLOCK) break;       // 没有更多连接,退出循环
            else if(errno == EINTR) continue;     // 被信号打断,继续
            else break;                            // 其他错误,退出
        }
    }
}

为什么要用 while(1) 循环?

因为边缘触发(ET)模式下,accept 只通知一次。如果同时有多个连接到达:

复制代码
假设同时有 3 个客户端连接:
  Client1 → SYN → 服务端
  Client2 → SYN → 服务端  
  Client3 → SYN → 服务端
  
epoll 只通知一次 POLLIN(因为状态只变化一次)
如果只用一次 accept,只能拿到一个连接,另外两个就丢了!

所以必须循环 accept,直到返回 EWOULDBLOCK(没有更多连接)

三种错误处理

errno 含义 处理方式
EWOULDBLOCK 没有更多连接了 break 退出循环
EINTR 被信号打断(如 SIGCHLD) continue 继续 accept
其他 真正的错误 break 退出循环
2.3.6 Recver() ------ 接收数据(循环 recv)
cpp 复制代码
void Recver(std::shared_ptr<Connection> connection)
{
    int sock = connection->SockFd();
    
    while(1)  // 循环 recv!
    {
        char buffer[BUF_SIZE];
        memset(buffer, 0, BUF_SIZE);
        ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
        
        if(n > 0)
        {
            connection->AppendInBuffer(buffer);  // 数据追加到缓冲区
        }
        else if(n == 0)
        {
            logger(INFO, "sockfd: %d, client close", sock);
            connection->_exceptCallback(connection);  // 客户端断开
            return;
        }
        else
        {
            if(errno == EWOULDBLOCK)
                break;  // 数据读完了
            else if(errno == EINTR)
                continue;  // 被信号打断
            else
            {
                logger(WARNING, "Recver error");
                connection->_exceptCallback(connection);  // 读错误
                return;
            }
        }
    }
    
    // 数据读完,交给业务层处理
    _OnMessage(connection);
}

为什么也要循环 recv?

和 accept 同理,边缘触发只通知一次:

复制代码
客户端一次发了 2000 字节数据:
  第1次 recv 读了 127 字节(BUF_SIZE-1)
  还剩 2000-127 = 1873 字节在内核缓冲区
  
如果不循环读,这 1873 字节就永远留在缓冲区里了
(因为 ET 模式下不会再通知)

所以必须循环 recv,直到返回 EWOULDBLOCK(没有更多数据)

_OnMessage(connection) ------ 交给业务层

  • 数据读完后,调用上层传入的回调函数
  • Connection 对象包含了 _inbuffer(收到的数据)
  • 业务层从 _inbuffer 中解码、处理、生成响应
2.3.7 Sender() ------ 发送数据(循环 send)
cpp 复制代码
void Sender(std::shared_ptr<Connection> connection)
{
    auto &outbuffer = connection->OutBuffer();
    
    while(1)
    {
        ssize_t n = send(connection->SockFd(), outbuffer.c_str(), outbuffer.size(), 0);
        
        if(n > 0)
        {
            outbuffer.erase(0, n);  // 已发送的部分从缓冲区删除
            if(outbuffer.empty()) break;  // 全部发送完
        }
        else if(n == 0)
        {
            return;
        }
        else
        {
            if(errno == EWOULDBLOCK) break;  // 发送缓冲区满了
            else if(errno == EINTR) continue;
            else
            {
                logger(WARNING, "Sender error");
                connection->_exceptCallback(connection);
                return;
            }
        }
    }
    
    // 根据缓冲区是否为空,决定是否继续关心写事件
    if(!outbuffer.empty())
    {
        EnableEvent(connection->SockFd(), true, true);  // 还有数据,继续关心写事件
    }
    else
    {
        EnableEvent(connection->SockFd(), true, false);  // 数据发完,关闭写事件
    }
}

写事件的动态管理

复制代码
1. 业务层把响应数据放入 _outbuffer
2. 调用 Sender 尝试发送
3. 如果一次发不完(EWOULDBLOCK),设置 EPOLLOUT 事件
4. 当发送缓冲区有空间时,epoll 通知 EPOLLOUT
5. 再次调用 Sender 继续发送
6. 数据发完后,移除 EPOLLOUT(避免不必要的通知)

为什么要移除 EPOLLOUT?

如果不移除,发送缓冲区一直有空间(因为数据发完了),epoll 会一直通知 EPOLLOUT,导致 CPU 空转。

2.3.8 EnableEvent() ------ 动态修改事件
cpp 复制代码
void EnableEvent(int sock, bool is_read, bool is_write)
{
    uint32_t events = 0;
    events |= (is_read ? EPOLLIN : 0);
    events |= (is_write ? EPOLLOUT : 0);
    events |= EPOLLET;  // 始终使用边缘触发
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_MOD, sock, events);
}

位运算构建事件掩码

参数 结果
is_read=true, is_write=false `EPOLLIN
is_read=true, is_write=true `EPOLLIN
is_read=false, is_write=false EPOLLET(无事件)
2.3.9 Excepter() ------ 异常处理
cpp 复制代码
void Excepter(std::shared_ptr<Connection> connection)
{
    logger(WARNING, "Excepter hander sock: %d", connection->SockFd());
    
    if(!IsConnectionSafe(connection->SockFd())) return;
    
    // 第1步:从 epoll 移除
    _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, connection->SockFd(), 0);
    
    // 第2步:关闭 socket
    close(connection->SockFd());
    
    // 第3步:从连接表删除
    _conn_map.erase(connection->SockFd());
    
    logger(INFO,"Close connection: %d", connection->SockFd());
}

三步清理

  1. 告诉 epoll 不再监听这个 fd
  2. 关闭 socket,释放资源
  3. _conn_map 移除,防止访问已释放的对象

IsConnectionSafe() ------ 双重检查

cpp 复制代码
bool IsConnectionSafe(int fd)
{
    auto iter = _conn_map.find(fd);
    return iter != _conn_map.end();
}

防止并发情况下,一个连接被多次关闭导致崩溃。

2.3.10 Dispatcher() ------ 事件分发
cpp 复制代码
void Dispatcher(int timeout)
{
    int nfds = _epoller_ptr->EpollerWait(revs, MAX_EVENTS, timeout);
    
    for(int i = 0; i < nfds; ++i)
    {
        uint32_t events = revs[i].events;
        int sock = revs[i].data.fd;
        
        // 把异常事件转换为读写事件处理
        if(events & EPOLLERR) events |= EPOLLIN | EPOLLOUT;    
        if(events & EPOLLHUP) events |= EPOLLIN | EPOLLOUT;
        
        // 处理读事件
        if(events & EPOLLIN && IsConnectionSafe(sock))
        {
            if(_conn_map[sock]->_recvCallback)
            {
                _conn_map[sock]->_recvCallback(_conn_map[sock]);
            }
        }
        
        // 处理写事件
        if(events & EPOLLOUT && IsConnectionSafe(sock))
        {
            if(_conn_map[sock]->_sendCallback)
            {
                _conn_map[sock]->_sendCallback(_conn_map[sock]);
            }
        }
    }
}

异常事件转换

  • EPOLLERR(错误)和 EPOLLHUP(挂起)不直接处理
  • 而是转换成 EPOLLIN | EPOLLOUT
  • 在 Recver 或 Sender 中检测到错误后,调用 _exceptCallback

设计优势

  • 统一的事件处理入口
  • 根据 fd 快速查找 Connection
  • 调用对应的回调函数处理
2.3.11 Loop() ------ 事件循环
cpp 复制代码
void Loop()
{
    _quit = false;
    while(!_quit)
    {
        Dispatcher(-1);  // 永久阻塞等待事件
        PrintConnection();  // debug 用,打印连接状态
    }
    _quit = true;
}

简单但核心

  1. 调用 Dispatcher(-1) 等待事件
  2. 事件发生时调用对应的回调
  3. 处理完继续等待
  4. 直到 _quit 为 true 退出

三、业务层集成 ------ Calculator

3.1 main.cc ------ 入口

cpp 复制代码
Calculator calculator;

void DefaultMsg(std::shared_ptr<Connection> connection)
{
    std::cout << "DefaultMsg: " << connection->InBuffer() << std::endl;
    
    // 业务处理:解码 → 计算 → 编码
    std::string response_str = calculator.Handler(connection->InBuffer());
    if(response_str.empty()) return;
    
    // 把响应放入写缓冲区
    connection->AppendOutBuffer(response_str);
    
    // 触发发送
    connection->_tcpserver->Sender(connection);
}

int main()
{
    std::unique_ptr<TcpServer> tcpserver(new TcpServer(8081, DefaultMsg));
    tcpserver->Init();
    tcpserver->Loop();
    return 0;
}

流程

复制代码
TcpServer 收到数据 → 调用 _OnMessage(DefaultMsg)
                          ↓
              DefaultMsg 调用 calculator.Handler()
                          ↓
              Handler 解码请求 → 计算 → 编码响应
                          ↓
              response_str 放入 OutBuffer
                          ↓
              调用 Sender 发送响应

3.2 Calculator 类 ------ 业务逻辑

cpp 复制代码
class Calculator
{
public:
    Response Calculate(const Request &req)
    {
        Response resp;
        resp.result = 0;
        resp.code = SUCCESS;
        
        switch (req.op)
        {
        case '+': resp.result = req.x + req.y; break;
        case '-': resp.result = req.x - req.y; break;
        case '*': resp.result = req.x * req.y; break;
        case '/': 
            if(req.y == 0) { resp.code = DIVIDE_BY_ZERO; break; }
            resp.result = req.x / req.y; break;
        case '%':
            if(req.y == 0) { resp.code = MODULO_BY_ZERO; break; }
            resp.result = req.x % req.y; break;
        default: resp.code = INVALID_OPERATOR; break;
        }
        return resp;
    }
    
    std::string Handler(std::string &package)
    {
        std::string content;
        bool r = Decode(package, &content);   // 解码
        if(!r) return "";
        
        Request req;
        r = req.Deserialize(content);         // 反序列化
        if(!r) return "";
        
        content = "";
        Response resp = Calculate(req);       // 计算
        
        resp.Serialize(&content);             // 序列化
        content = Encode(content);            // 编码
        
        return content;
    }
};

Handler 的五步流程

步骤 函数 作用
1 Decode() _inbuffer 中提取完整报文(解决粘包)
2 Deserialize() 把字符串解析成 Request 对象
3 Calculate() 执行加减乘除运算
4 Serialize() 把 Response 对象序列化成字符串
5 Encode() 给响应加长度前缀(解决粘包)

3.3 Protocol.hpp ------ 协议编解码

Encode() ------ 编码

cpp 复制代码
std::string Encode(std::string &content)
{
    std::string package = std::to_string(content.size()) + "\n" + content + "\n";
    return package;
}

格式:len\ncontent\n

Decode() ------ 解码

cpp 复制代码
bool Decode(std::string &package, std::string *content)
{
    if(package.find("\n") == std::string::npos) return false;  // 没有换行符,不完整
    
    std::string len = package.substr(0, package.find("\n"));
    if(package.size() < len.size() + std::stoi(len) + 2) return false;  // 长度不够,不完整
    
    *content = package.substr(package.find("\n") + 1, std::stoi(len));
    
    package.erase(0, len.size() + std::stoi(len) + 2);  // 移除已处理的报文
    
    return true;
}

解决 TCP 粘包问题

  • 用长度前缀标识报文长度
  • 只在收到完整报文时才处理
  • 不完整的数据留在 _inbuffer 中等待下次数据

四、完整工作流程

4.1 服务器启动

复制代码
main()
  ↓
new TcpServer(8081, DefaultMsg)
  ↓
tcpserver->Init()
  ↓
Socket() → SetNonBlockOrDie() → Bind() → Listen()
  ↓
AddConnection(listen_fd, EPOLLIN|EPOLLET, Accepter, nullptr, nullptr)
  ↓
tcpserver->Loop()
  ↓
Dispatcher(-1) → epoll_wait() 阻塞等待...

4.2 客户端连接

复制代码
epoll_wait 返回 EPOLLIN (listen_fd)
  ↓
Dispatcher 调用 Accepter(connection)
  ↓
循环 accept() 直到 EWOULDBLOCK
  ↓
对每个新连接:
  SetNonBlockOrDie(new_fd)
  AddConnection(new_fd, EPOLLIN|EPOLLET, Recver, Sender, Excepter, ip, port)
  ↓
回到 epoll_wait() 继续等待...

4.3 客户端发送数据

复制代码
epoll_wait 返回 EPOLLIN (client_fd)
  ↓
Dispatcher 调用 Recver(connection)
  ↓
循环 recv() 直到 EWOULDBLOCK
  ↓
数据追加到 connection->_inbuffer
  ↓
调用 _OnMessage(connection) → DefaultMsg(connection)
  ↓
DefaultMsg 调用 calculator.Handler(_inbuffer)
  ↓
Handler: Decode → Deserialize → Calculate → Serialize → Encode
  ↓
响应追加到 connection->_outbuffer
  ↓
调用 Sender(connection)
  ↓
循环 send() 直到 EWOULDBLOCK 或发送完成
  ↓
如果没发完:EnableEvent(fd, true, true) 设置 EPOLLOUT
  ↓
回到 epoll_wait() 继续等待...

4.4 发送缓冲区就绪(EPOLLOUT)

复制代码
epoll_wait 返回 EPOLLOUT (client_fd)
  ↓
Dispatcher 调用 Sender(connection)
  ↓
继续循环 send() 发送剩余数据
  ↓
数据发完:EnableEvent(fd, true, false) 移除 EPOLLOUT
  ↓
回到 epoll_wait() 继续等待...

4.5 客户端断开

复制代码
epoll_wait 返回 EPOLLIN (client_fd)
  ↓
Dispatcher 调用 Recver(connection)
  ↓
recv() 返回 0(客户端断开)
  ↓
调用 _exceptCallback(connection) → Excepter(connection)
  ↓
EPOLL_CTL_DEL → close(fd) → _conn_map.erase(fd)
  ↓
回到 epoll_wait() 继续等待...

五、Reactor 模式的设计优势

5.1 职责分离

模块 职责
TcpServer IO 操作(accept/recv/send)、事件管理
Connection 连接状态、缓冲区、回调函数
Epoller epoll 系统调用封装
Calculator 业务逻辑(计算)
Protocol 协议编解码

5.2 扩展性

  • 添加新业务 :只需要实现新的 Handler 函数,传给 TcpServer 即可
  • 替换 IO 模型:把 Epoller 换成 Selector/Poller,对外接口不变
  • 添加新事件类型:在 Connection 中添加新的回调函数

5.3 高性能

  • 边缘触发(ET):减少 epoll_wait 的调用次数
  • 非阻塞 IO:不会因为等待数据而阻塞
  • 事件驱动:有事件才处理,没事件就 sleep

5.4 对比之前的版本

维度 epollserver Reactor
事件模式 水平触发(LT) 边缘触发(ET)
连接封装 无(直接用 fd) Connection 对象
缓冲区 InBuffer/OutBuffer
回调机制 无(硬编码) std::function 回调
业务分离 混在一起 完全分离
扩展性

六、边缘触发(ET)vs 水平触发(LT)

6.1 核心区别

复制代码
水平触发(LT):
  只要 fd 上有数据可读,epoll_wait 就会一直返回
  适合阻塞 IO(读完一次下次还会通知)
  
边缘触发(ET):
  只有当 fd 的状态从"不可读"变为"可读"时才通知一次
  必须配合非阻塞 IO(一次性读完所有数据)

6.2 图解对比

LT 模式

复制代码
时间线 ──────────────────────────────────────────────────►

客户端发送 1000 字节
  │
  ▼
epoll_wait 返回 POLLIN(通知1)
  │
  ▼
recv 读了 500 字节(还剩 500)
  │
  ▼
epoll_wait 返回 POLLIN(通知2)← LT 会再次通知
  │
  ▼
recv 读了 500 字节(读完了)
  │
  ▼
epoll_wait 阻塞...(没有新数据)

ET 模式

复制代码
时间线 ──────────────────────────────────────────────────►

客户端发送 1000 字节
  │
  ▼
epoll_wait 返回 POLLIN(通知1)← ET 只通知一次!
  │
  ▼
循环 recv 直到 EWOULDBLOCK(必须一次性读完)
  │
  ▼
epoll_wait 阻塞...(没有新数据不会再通知)

6.3 为什么 ET 更高效?

场景 LT ET
数据分多次到达 每次都通知 只在第一次到达时通知
CPU 使用率 较高(频繁通知) 较低(只通知状态变化)
编程复杂度 简单(可以阻塞读) 复杂(必须循环非阻塞读)

七、编译与运行

7.1 编译

bash 复制代码
cd IO/Reactor/build/
cmake ..
make

7.2 启动服务器

bash 复制代码
./reactorserver

7.3 启动客户端

bash 复制代码
./client 127.0.0.1 8081

7.4 客户端发送的数据格式

客户端自动生成随机计算请求,格式为 JSON:

json 复制代码
{"x": 5, "y": 3, "op": "+"}

服务器响应:

json 复制代码
{"result": 8, "code": 0}

八、总结

8.1 IO 多路复用学习路线回顾

复制代码
阻塞 IO (read 阻塞)
    ↓
非阻塞 IO + 轮询 (CPU 空转)
    ↓
select  (1024 限制,O(N))
    ↓
poll    (无数量限制,仍是 O(N))
    ↓
epoll   (事件驱动,O(1),Linux 最优)
    ↓
Reactor (基于 epoll 的设计模式,更高层的抽象)

8.2 Reactor 的关键要素

要素 实现
事件检测 epoll + ET
非阻塞 IO fcntl(O_NONBLOCK)
连接封装 Connection 类
回调机制 std::function
缓冲区管理 InBuffer/OutBuffer
业务分离 _OnMessage 回调
相关推荐
线束线缆组件品替网1 小时前
Amphenol ICC RJE1Y22610C42401线束组件应用解析
网络·数码相机·智能手机·计算机外设·电脑·电视盒子
农村小镇哥1 小时前
如何限定IP访问Oracle数据库
数据库·tcp/ip·oracle
cfm_29141 小时前
SpringBoot 数据库全栈整合
数据库·spring boot·后端
浅止菌2 小时前
嵌入式项目Makefile越写越乱?一张万能模板+5个技巧,告别重复劳动
linux
是上好佳佳佳呀2 小时前
MySQL 基础:从数据库概念到表结构管理DDL
数据库·mysql
Nturmoils2 小时前
Oracle 迁移评估时,我把这两类问题列在了检查清单最前面
数据库
IT小白杨2 小时前
多账号环境稳定性由什么决定:指纹、网络、存储如何共同决定账号安全
网络·安全·开源软件·业界资讯·指纹浏览器
Database_Cool_2 小时前
数据库性能不够用,升级到什么方案好?阿里云 PolarDB(云原生数据库领导者,兼容 MySQL/PostgreSQL/Oracle)平滑升级与百倍弹性
数据库·阿里云·云原生
Sagittarius_A*2 小时前
Web 渗透实战:服务器系统识别、端口与中间件全量探测
服务器·网络安全·中间件·渗透测试