🎬 个人主页:艾莉丝努力练剑
❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- 前言
-
- 一、 开头部分(框架引入)
-
- 导入语
- 思维导图
- 1 ~> AddEvents(Poller.hpp)
-
- 1.1 Connection基类抽象
-
- 1.1.1 基类核心定义
- 1.1.2 IOHandler派生类框架
- 1.2 Poller核心接口实现
-
- 1.2.1 AddEvents事件注册
- 1.2.2 WaitEvents事件等待
- 1.3 事件派发器核心框架
-
- 1.3.1 基础派发逻辑
- 1.3.2 timeout参数的两种模式
- 1.4 AddEvents知识图谱
- 2 ~> 以Recver(Listener.hpp)为主线——完善所涉及的细节
-
- 2.1 Listener类基础定义
-
- 2.1.1 类定义
- 2.2 ET模式下的Accept处理逻辑
-
- 2.2.1 Accepter接口改造
- 2.2.2 循环Accept与错误码判断
- 2.2.3 核心原则
- 2.3 非阻塞套接字实现
-
- 套接字创建时的统一设置
- 2.4 新连接的托管与回指机制
-
- 2.4.1 AddConnection完整实现
- 2.4.2 设计原理
- 2.5 工程细节:头文件包含顺序
- 2.6 Recver(Listener.hpp)知识图谱
- 3 ~> 以Recver(IOHandler.hpp)为主线——完善所涉及的细节
-
- 3.1 IOHandler类完整定义
- 3.2 Recver非阻塞循环读取
-
- 3.2.1 完整读取逻辑
- 3.3 协议层与业务层的解耦接入
-
- 3.3.1 分层架构
- 3.3.2 回调透传链路
- 3.3.3 协议层核心接口
- 3.4 Sender写事件的正确处理逻辑
-
- 3.4.1 核心原则
- 3.4.2 完整发送逻辑
- 3.4.3 EnableReadWrite事件修改接口
- 3.4.4 最后一个坑:多路转接select、poll、epoll当中如何正确理解发送问题!!!
- 3.5 Excepter统一异常处理
- 3.6 Recver(IOHandler.hpp)知识图谱
- 4 ~> Version1版本代码
-
- Reactor-V1版本代码知识图谱
- 5 ~> 本文总结
-
- 核心考点深度总结
- 结尾

前言
一、 开头部分(框架引入)
导入语
本文围绕基于epoll实现的单Reactor事件驱动网络模型展开,从底层Poller系统调用封装、监听连接处理、业务IO读写,到分层解耦架构落地,完整覆盖Reactor模型代码实现的全流程。核心攻克ET模式下的非阻塞IO处理规范、事件按需注册机制、连接生命周期管理、双缓冲区设计、系统调用错误边界处理等高频易错痛点,所有代码均经过边界条件审计与逻辑校验,可直接作为高并发网络编程考核的核心复习依据。
思维导图
Reactor 代码细节核心知识体系
|— 1. Poller层:epoll内核接口封装
| |— Connection基类抽象
| | |— 纯虚接口:Sockfd/Recver/Sender/Excepter
| | |— 公共成员:事件集合_events、对端地址_clientaddr
| | `— Reactor回指指针_R
| |— Poller核心接口
| | |— AddEvents:epoll_ctl_ADD注册事件
| | `— WaitEvents:epoll_wait等待就绪事件
| `— 事件派发器
| |— LoopOnce单次事件循环
| |— 事件分发:读事件/写事件/异常事件
| `— timeout参数:阻塞/非阻塞模式
|— 2. Listener层:监听连接处理
| |— Listener类定义
| | |— 继承Connection基类
| | |— 监听套接字_listensock
| | `— 业务回调_on_message透传
| |— ET模式Accept逻辑
| | |— 循环accept获取全部就绪连接
| | |— 错误码分支:EAGAIN/EWOULDBLOCK/EINTR
| | `— 强制非阻塞要求
| |— 新连接生命周期
| | |— 设置非阻塞属性
| | |— 封装为IOHandler对象
| | |— 注册EPOLLIN | EPOLLET事件
| | `— 托管至Reactor并建立回指
| `— 工程细节:头文件依赖与编译顺序
|— 3. IOHandler层:业务IO处理
| |— IOHandler类定义
| | |— 套接字描述符_sockfd
| | |— 双缓冲区:_inbuffer/_outbuffer
| | `— 消息处理回调_on_message
| |— Recver读处理
| | |— 非阻塞循环读取
| | |— 返回值三分支:>0/==0/<0
| | |— 错误码边界判断
| | `— 协议回调与报文处理
| |— Sender写处理
| | |— 非阻塞循环发送
| | |— 发送缓冲区移除逻辑
| | |— EPOLLOUT按需注册机制
| | `— 发送错误处理
| `— Excepter统一异常与资源清理
|— 4. 分层解耦架构
| |— 网络层:Reactor + IOHandler
| |— 协议层:Protocol报文封解包
| |— 业务层:Calculator业务计算
| `— 回调机制:跨层调用解耦
`— 核心易错考点
|— ET模式必须搭配非阻塞IO
|— EPOLLIN常设、EPOLLOUT按需注册
|— 系统调用错误码三分支判断
|— 智能指针循环引用问题
|— 连接生命周期空指针防护
`– 异常事件的正确处理逻辑
1 ~> AddEvents(Poller.hpp)
本章是Reactor模型的底层基础,完成epoll系统调用的封装、连接基类的抽象以及事件派发的核心框架,是整个事件驱动模型与内核交互的核心层级。
1.1 Connection基类抽象
Connection是所有连接对象的抽象基类,定义了事件处理的统一接口,通过多态实现事件的统一派发。
1.1.1 基类核心定义
class Reactor; // 前置声明,解决循环依赖
class Connection
{
public:
Connection() : _events(0), _R(nullptr) {}
uint32_t Events() { return _events; }
void SetEvents(uint32_t events) { _events = events; }
void SetAddress(const InetAddr &addr) { _clientaddr = addr; }
virtual int Sockfd() = 0;
virtual void Recver() = 0;
virtual void Sender() = 0;
virtual void Excepter() = 0;
virtual ~Connection() = default;
protected:
InetAddr _clientaddr; // 对端套接字地址信息
uint32_t _events; // 当前连接关心的epoll事件集合
Reactor* _R; // 所属Reactor实例的弱引用回指指针
};
[审计批注]
1.1.2 IOHandler派生类框架
IOHandler是普通业务连接的实现类,继承Connection基类,负责具体的读写IO操作,每个连接持有独立的读写缓冲区。
class IOHandler : public Connection
{
public:
IOHandler(int sockfd) : _sockfd(sockfd) {}
int Sockfd() override { return _sockfd; }
void Recver() override;
void Sender() override;
void Excepter() override;
private:
int _sockfd;
std::string _inbuffer; // 接收缓冲区
std::string _outbuffer; // 发送缓冲区
};
1.2 Poller核心接口实现
Poller类是对epoll系统调用的直接封装,屏蔽内核接口细节,向上提供统一的事件注册与等待接口。
1.2.1 AddEvents事件注册
将指定套接字的指定事件注册到epoll内核事件表中,完成用户态事件到内核态的透传。
void AddEvents(int sockfd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (n < 0)
{
LOG(LogLevel::FATAL) << "epoll_ctl error";
}
}
[审计批注] 此处使用ev.data.fd携带文件描述符,依赖上层Reactor的_connections哈希表完成fd到Connection对象的映射。若追求更高派发效率,可使用ev.data.ptr直接携带Connection对象指针,省去哈希表查找开销,但需严格保证指针生命周期安全。
1.2.2 WaitEvents事件等待
阻塞等待epoll事件就绪,返回就绪事件的数量。
int WaitEvents(struct epoll_event revs[], int num, int timeout)
{
int n = epoll_wait(_epfd, revs, num, timeout);
if (n < 0)
{
LOG(LogLevel::FATAL) << "epoll_wait error!";
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "epoll_wait timeout!";
}
return n;
}
[审计批注] 原笔记中将超时返回的日志级别错误写为FATAL,属于严重级别错误。epoll_wait返回0是超时正常现象,并非系统错误,必须修正为INFO级别。若使用FATAL级别,在非阻塞轮询模式下会产生大量致命级错误日志,严重干扰问题排查。
1.3 事件派发器核心框架
Reactor的Dispatcher是事件循环的核心,负责从epoll获取就绪事件,分发给对应连接的处理函数。
1.3.1 基础派发逻辑
void LoopOnce(int timeout)
{
int n = _epoller->WaitEvents(revs, gnum, timeout);
for (int i = 0; i < n; i++)
{
int sockfd = revs[i].data.fd;
uint32_t revents = revs[i].events;
// 异常事件转换为读写事件
if ((revents & EPOLLERR) || (revents & EPOLLHUP))
{
revents |= (EPOLLIN | EPOLLOUT);
}
// 分发给读事件处理
if ((revents & EPOLLIN) && IsConnectionExists(sockfd))
{
_connections[sockfd]->Recver();
}
// 分发给写事件处理
if ((revents & EPOLLOUT) && IsConnectionExists(sockfd))
{
_connections[sockfd]->Sender();
}
}
}
void Dispatcher()
{
int timeout = –1;
while (true)
{
LoopOnce(timeout);
}
}
[审计批注]
1.3.2 timeout参数的两种模式
- timeout = 0:非阻塞轮询模式,epoll_wait立即返回,无事件时返回0。该模式会占用100%CPU,仅用于特殊调试场景,生产环境禁止使用。
- timeout = -1:永久阻塞模式,无事件时线程挂起,有事件就绪才被唤醒。该模式CPU利用率最高,是Reactor模型的标准配置。
1.4 AddEvents知识图谱

2 ~> 以Recver(Listener.hpp)为主线——完善所涉及的细节
本章聚焦监听连接的事件处理,核心解决ET模式下如何正确接收所有就绪连接、如何管理新连接生命周期、如何完成新连接与Reactor的托管绑定等核心问题。
2.1 Listener类基础定义
Listener是监听套接字的封装,继承Connection基类,负责处理新连接接入事件,本身只关心读事件(新连接就绪等价于读事件就绪)。
2.1.1 类定义
class Listener : public Connection
{
public:
Listener(uint16_t port, OnMessage_t on_message)
: _port(port), _on_message(on_message), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildSocketMethod_port(port);
LOG(LogLevel::INFO) << "Listen sockfd create success: " << _listensock->Socketfd();
}
int Sockfd() override { return _listensock->Socketfd(); }
void Recver() override;
void Sender() override {} // 监听套接字不关心写事件
void Excepter() override {} // 监听套接字异常暂不处理
private:
uint16_t _port;
OnMessage_t _on_message;
std::unique_ptr<Socket> _listensock;
};
[审计批注] 原笔记中未实现Listener的Sockfd()纯虚函数,会导致类无法实例化。监听套接字的文件描述符由_listensock管理,必须重写该接口返回对应fd,满足基类的多态约定。
2.2 ET模式下的Accept处理逻辑
ET(边缘触发)模式下,连接就绪事件只会通知一次,因此必须一次性循环读取所有已就绪的连接,否则剩余连接不会再触发事件,造成连接饥饿。
2.2.1 Accepter接口改造
Accept接口必须支持非阻塞错误码返回,用于区分“真错误”和“无更多连接”。
int Accepter(InetAddr *clientaddr, int *errcode) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_sockfd, CONV(&peer), &len);
if (sockfd < 0)
{
*errcode = errno;
return –1;
}
*clientaddr = peer;
*errcode = 0;
return sockfd;
}
[审计批注] 原笔记中注释掉了*errcode = errno;赋值语句,会导致外层错误判断完全失效,属于严重逻辑漏洞。非阻塞accept返回-1时,必须将errno保存到输出参数中,外层才能区分错误类型。
2.2.2 循环Accept与错误码判断
void Recver() override
{
while (true)
{
int errcode = 0;
InetAddr clientaddr;
int sockfd = _listensock->Accepter(&clientaddr, &errcode);
if (sockfd >= 0)
{
// 成功获取新连接,完成封装与托管
LOG(LogLevel::INFO) << "get a new sockfd success: " << sockfd;
SetNonBlock(sockfd);
std::shared_ptr<Connection> conn = std::make_shared<IOHandler>(sockfd, _on_message);
conn->SetEvents(EPOLLIN | EPOLLET);
conn->SetAddress(clientaddr);
_R->AddConnection(conn);
}
else
{
if (errcode == EAGAIN || errcode == EWOULDBLOCK)
{
// 所有连接已处理完毕,退出循环
LOG(LogLevel::INFO) << "accepter Finish!";
break;
}
if (errcode == EINTR)
{
// 被信号中断,重试accept
LOG(LogLevel::INFO) << "accepter interrupt!";
continue;
}
// 真正的系统错误
LOG(LogLevel::ERROR) << "accepter error, errno: " << errcode;
break;
}
}
}
2.2.3 核心原则
ET模式下,包括监听套接字在内的所有文件描述符,必须设置为非阻塞模式。原因在于:循环读取的最后一次调用必然会因为无数据而返回,若为阻塞模式,最后一次调用会导致线程永久挂起,整个Reactor停滞。
2.3 非阻塞套接字实现
通过fcntl系统调用修改文件描述符的状态标志,设置非阻塞模式。
void SetNonBlock(int fd)
{
int flags = fcntl(fd, F_GETFL);
if (flags < 0)
{
LOG(LogLevel::ERROR) << "fcntl error, set " << fd << " non block failed";
return;
}
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
[审计批注] 必须采用“先获取原有标志,再或上新标志”的写法,严禁直接fcntl(fd, F_SETFL, O_NONBLOCK),否则会覆盖文件描述符原有的其他状态位,引发不可预知的错误。
套接字创建时的统一设置
在TcpSocket创建套接字时就统一设置非阻塞,同时配置地址端口复用选项:
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
SetNonBlock(_sockfd); // 创建即设置非阻塞
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
LOG(LogLevel::INFO) << "create socket success";
}
选项说明:
- SO_REUSEADDR:允许绑定处于TIME_WAIT状态的端口,服务器重启时可立即绑定端口。
- SO_REUSEPORT:允许多个套接字绑定同一个端口,内核自动负载均衡,用于多Reactor场景。
2.4 新连接的托管与回指机制
新连接创建后,必须完成三个核心动作:设置事件、加入Reactor连接管理、回指所属Reactor。
2.4.1 AddConnection完整实现
void AddConnection(std::shared_ptr<Connection> &conn)
{
int sockfd = conn->Sockfd();
uint32_t events = conn->Events();
// 1. 事件写透到内核epoll
_epoller->AddEvents(sockfd, events);
// 2. 连接对象托管到连接表
_connections[sockfd] = conn;
// 3. 连接回指当前Reactor实例
conn->_R = this;
LOG(LogLevel::INFO) << "insert " << conn->Sockfd() << " into reactor!";
}
2.4.2 设计原理
连接与Reactor的双向引用设计,与内核中struct file和struct socket的互指设计思想一致:
- Reactor通过_connections哈希表管理所有连接,可以根据fd快速找到对应连接对象。
- 连接通过_R指针找到所属Reactor,可以调用Reactor的能力(如修改事件、移除连接等)。
2.5 工程细节:头文件包含顺序
由于Connection中仅做了Reactor的前置声明,在Listener的实现中调用_R->AddConnection时,编译器必须已经看到Reactor的完整定义,否则会报“非法使用不完整类型”错误。
正确的包含顺序为:
#include "Connection.hpp"
#include "Reactor.hpp" // 必须在Listener之前包含Reactor完整定义
#include "Listener.hpp"
2.6 Recver(Listener.hpp)知识图谱

3 ~> 以Recver(IOHandler.hpp)为主线——完善所涉及的细节
本章聚焦普通业务连接的读写处理,是Reactor模型的核心业务承载层,涵盖非阻塞读取、协议解析、按需发送、异常处理等完整IO生命周期。
3.1 IOHandler类完整定义
IOHandler是业务连接的核心实现,负责数据的读取、发送、缓冲区管理,并通过回调机制对接上层协议与业务。
static const int gbuffersize = 1024;
using OnMessage_t = std::function<std::string(std::string &, int *)>;
class IOHandler : public Connection
{
public:
IOHandler(int sockfd, OnMessage_t on_message)
: _sockfd(sockfd), _on_message(on_message) {}
int Sockfd() override { return _sockfd; }
void Recver() override;
void Sender() override;
void Excepter() override;
private:
int _sockfd;
OnMessage_t _on_message; // 消息处理回调
std::string _inbuffer; // 接收缓冲区(字节队列)
std::string _outbuffer; // 发送缓冲区(字节队列)
};
3.2 Recver非阻塞循环读取
ET模式下读事件就绪后,必须循环读取直到底层无数据,保证本次事件触发的所有数据都被读取完毕。
3.2.1 完整读取逻辑
void Recver() override
{
LOG(LogLevel::INFO) << "IOHandler event ready, sockfd is: " << _sockfd;
char buffer[gbuffersize];
while (true)
{
ssize_t n = recv(_sockfd, buffer, gbuffersize – 1, 0);
if (n > 0)
{
buffer[n] = '\\0';
_inbuffer += buffer;
}
else if (n == 0)
{
// 对端正常关闭连接
LOG(LogLevel::INFO) << "client quit, address is: "
<< _clientaddr.StringAddress()
<< " sockfd:" << _sockfd;
Excepter();
return;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 本轮数据全部读取完毕
break;
}
if (errno == EINTR)
{
// 被信号中断,重试读取
continue;
}
// 读取发生真正错误
LOG(LogLevel::ERROR) << "recv error, address is: "
<< _clientaddr.StringAddress()
<< " sockfd: " << _sockfd;
Excepter();
return;
}
}
// 读取完成后,尝试处理完整报文
int code = 0;
std::string result = _on_message(_inbuffer, &code);
if (code == 0)
{
_outbuffer += result;
}
else
{
Excepter();
return;
}
// 尝试直接发送响应
if (!_outbuffer.empty())
{
Sender();
}
}
[审计批注]
3.3 协议层与业务层的解耦接入
通过回调函数机制,将网络IO层与协议层、业务层完全解耦,各层可独立开发与替换。
3.3.1 分层架构
3.3.2 回调透传链路
- 主函数中定义协议与业务对象,将业务处理回调注入协议层
- 协议层的处理回调通过Listener构造函数透传
- Listener创建IOHandler时,将回调注入每个业务连接
- IOHandler读取到数据后,通过回调调用协议层处理
3.3.3 协议层核心接口
// 返回值:响应报文序列化结果
// 参数:inbuffer-输入字节流,code-输出型错误码(0成功,-1失败)
std::string HandlerRequest(std::string &streamstr, int *code)
{
std::string resp_package;
while (true)
{
std::string jsonstring;
int n = UnPackage(streamstr, &jsonstring);
if (n == 0)
{
// 无完整报文,退出
*code = 0;
return resp_package;
}
else if (n == –1)
{
// 报文解析错误
*code = –1;
return resp_package;
}
// 反序列化 -> 业务处理 -> 序列化响应
Request req;
req.Deserialize(jsonstring);
Response resp = _callback(req);
std::string resp_str = resp.Serialize();
resp_package += Package(resp_str);
}
}
[审计批注] 原笔记中解析失败直接调用exit(1)终止整个进程,属于严重设计缺陷。单个客户端的非法报文不应导致整个服务崩溃,正确做法是返回错误码,由上层关闭对应连接,保证服务可用性。
3.4 Sender写事件的正确处理逻辑
写事件的处理与读事件有本质区别:读事件是常设的,而写事件必须按需注册,否则会导致epoll频繁触发空转,CPU占用100%。
3.4.1 核心原则
- 读事件EPOLLIN:常设到epoll中,因为数据何时到来是不可预知的。
- 写事件EPOLLOUT:按需注册。套接字创建之初,内核发送缓冲区为空,写事件默认就绪,若常设EPOLLOUT会导致epoll不断返回就绪,造成忙等。仅当发送缓冲区满、无法继续发送时,才注册EPOLLOUT事件,待缓冲区可写时触发一次,写完后立即移除。
3.4.2 完整发送逻辑
void Sender() override
{
while (true)
{
ssize_t n = send(_sockfd, _outbuffer.c_str(), _outbuffer.size(), 0);
if (n >= 0)
{
// 移除已发送的数据
_outbuffer.erase(0, n);
if (_outbuffer.empty())
{
// 全部发送完毕
break;
}
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 发送缓冲区已满,本轮无法继续发送
break;
}
if (errno == EINTR)
{
// 被信号中断,重试发送
continue;
}
// 发送发生真正错误
LOG(LogLevel::ERROR) << "send error, address is: "
<< _clientaddr.StringAddress()
<< " sockfd:" << _sockfd;
Excepter();
return;
}
}
// 根据发送结果调整事件注册
if (_outbuffer.empty())
{
// 数据发完,关闭写事件监听
_R->EnableReadWrite(_sockfd, true, false);
}
else
{
// 数据未发完,开启写事件监听,等待缓冲区可写
_R->EnableReadWrite(_sockfd, true, true);
}
}
3.4.3 EnableReadWrite事件修改接口
该接口由Reactor提供,用于动态修改连接关心的事件集合:
void EnableReadWrite(int sockfd, bool isread, bool iswrite)
{
uint32_t events = 0;
if (isread) events |= EPOLLIN;
if (iswrite) events |= EPOLLOUT;
events |= EPOLLET; // 保持ET模式
struct epoll_event ev;
ev.events = events;
ev.data.fd = sockfd;
epoll_ctl(_epfd, EPOLL_CTL_MOD, sockfd, &ev);
// 同步更新Connection对象的事件记录
_connections[sockfd]->SetEvents(events);
}
3.4.4 最后一个坑:多路转接select、poll、epoll当中如何正确理解发送问题!!!

3.5 Excepter统一异常处理
所有读写错误、对端关闭等异常场景,最终都会进入异常处理函数,完成连接资源清理工作。
void Excepter() override
{
LOG(LogLevel::ERROR) << "Excepter, address is: "
<< _clientaddr.StringAddress()
<< " sockfd:" << _sockfd;
// 1. 从epoll中移除事件
_R->RemoveConnection(_sockfd);
// 2. 关闭套接字
close(_sockfd);
// 3. 连接对象会因引用计数归零自动释放
}
[审计批注] 移除连接时必须先从epoll事件表中删除,再关闭文件描述符。虽然关闭fd会自动从epoll中移除,但显式调用EPOLL_CTL_DEL是更严谨的做法,避免时序问题引发的事件误触发。
3.6 Recver(IOHandler.hpp)知识图谱

4 ~> Version1版本代码
Reactor-V1版本代码知识图谱

5 ~> 本文总结
核心考点深度总结
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|
艾莉丝努力练剑
C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 👀 ❤️ ⭐ 💬 不要忘记给博主“一键四连”哦! “今日练剑达成!” “技术之路难免有困惑,但同行的人会让前进更有方向。” |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!
往期回顾:
【Linux网络】多路转接epoll(三)Reactor模式:基于epoll的高性能网络服务器设计与实现(上)代码框架
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡
૮₍ ˶ ˊ ᴥ ˋ˶₎ა

网硕互联帮助中心







评论前必须登录!
注册