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 的方法(如
Sender、EnableEvent) - 通过回指指针可以访问 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);
}
原理:
fcntl(F_GETFL)获取当前文件状态标志fcntl(F_SETFL, fl | O_NONBLOCK)添加非阻塞标志- 设置后,
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);
}
三步操作:
- 告诉 epoll 要监听这个 fd 的什么事件
- 创建一个 Connection 对象封装这个连接
- 把 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());
}
三步清理:
- 告诉 epoll 不再监听这个 fd
- 关闭 socket,释放资源
- 从
_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;
}
简单但核心:
- 调用
Dispatcher(-1)等待事件 - 事件发生时调用对应的回调
- 处理完继续等待
- 直到
_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 回调 |