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

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

前言
一、 开头部分(框架引入)
导入语
本文围绕基于epoll实现的单Reactor事件驱动网络模型展开,从底层Poller系统调用封装、监听连接处理、业务IO读写,到分层解耦架构落地,完整覆盖Reactor模型代码实现的全流程。核心攻克ET模式下的非阻塞IO处理规范、事件按需注册机制、连接生命周期管理、双缓冲区设计、系统调用错误边界处理等高频易错痛点,所有代码均经过边界条件审计与逻辑校验,可直接作为高并发网络编程考核的核心复习依据。
思维导图
bash
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 基类核心定义
cpp
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实例的弱引用回指指针
};
审计批注
- 原笔记中将
_R指针定义在public访问区,违反封装原则。修正为protected访问权限,仅允许派生类访问,外部无法直接修改回指指针,保证对象生命周期安全。- 原笔记基类析构函数未定义为虚函数,若通过基类指针释放派生类对象会发生内存泄漏。生产环境必须声明为虚析构函数,保证派生类析构函数被正确调用。
- 回指指针使用裸指针而非智能指针,核心目的是避免
shared_ptr循环引用:Reactor通过shared_ptr管理Connection,若Connection再用shared_ptr指向Reactor,会导致引用计数永远无法归零。裸指针仅作弱引用、不参与所有权管理,是该场景下的标准解决方案。
1.1.2 IOHandler派生类框架
IOHandler是普通业务连接的实现类,继承Connection基类,负责具体的读写IO操作,每个连接持有独立的读写缓冲区。
cpp
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内核事件表中,完成用户态事件到内核态的透传。
cpp
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事件就绪,返回就绪事件的数量。
cpp
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 基础派发逻辑
cpp
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);
}
}
审计批注
- 原逻辑中将EPOLLERR和EPOLLHUP直接转换为读写事件的处理方式存在缺陷。EPOLLERR表示套接字发生错误(如对端重置连接),EPOLLHUP表示对端挂断,正确做法是优先触发
Excepter()异常处理,而非直接走读写逻辑,否则可能引发无效读写导致程序崩溃。IsConnectionExists判断是必须的空指针防护:在处理事件的过程中,连接可能已被前序逻辑释放(如读处理中检测到对端关闭并移除连接),若不判断直接调用会触发空指针解引用。
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 类定义
cpp
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接口必须支持非阻塞错误码返回,用于区分"真错误"和"无更多连接"。
cpp
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与错误码判断
cpp
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系统调用修改文件描述符的状态标志,设置非阻塞模式。
cpp
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创建套接字时就统一设置非阻塞,同时配置地址端口复用选项:
cpp
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完整实现
cpp
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的完整定义,否则会报"非法使用不完整类型"错误。
正确的包含顺序为:
cpp
#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是业务连接的核心实现,负责数据的读取、发送、缓冲区管理,并通过回调机制对接上层协议与业务。
cpp
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 完整读取逻辑
cpp
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();
}
}
审计批注
- 对端关闭连接(n==0)属于正常生命周期结束,并非异常事件。原逻辑统一调用
Excepter()处理并不严谨,生产环境应单独实现连接关闭逻辑,完成资源清理、连接移除等工作,避免与真正的错误混淆。buffer[n] = '\0'配合_inbuffer += buffer的写法仅适用于文本协议,若传输二进制数据,string的+=运算符会因遇到'\0'截断数据。二进制场景应使用_inbuffer.append(buffer, n)明确指定追加长度。- 读取完成后立即调用
Sender()尝试发送是最优实践:此时内核发送缓冲区大概率为空,写事件处于就绪状态,直接发送可避免额外的epoll事件触发,降低延迟。
3.3 协议层与业务层的解耦接入
通过回调函数机制,将网络IO层与协议层、业务层完全解耦,各层可独立开发与替换。
3.3.1 分层架构
- 网络层:Reactor + IOHandler,只负责数据的收发与事件管理,不关心数据内容。
- 协议层:Protocol,负责报文的封包、解包、序列化、反序列化,判断报文完整性。
- 业务层:Calculator,负责具体的业务逻辑处理,只关心结构化的请求与响应。
3.3.2 回调透传链路
- 主函数中定义协议与业务对象,将业务处理回调注入协议层
- 协议层的处理回调通过Listener构造函数透传
- Listener创建IOHandler时,将回调注入每个业务连接
- IOHandler读取到数据后,通过回调调用协议层处理
3.3.3 协议层核心接口
cpp
// 返回值:响应报文序列化结果
// 参数: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 完整发送逻辑
cpp
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提供,用于动态修改连接关心的事件集合:
cpp
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统一异常处理
所有读写错误、对端关闭等异常场景,最终都会进入异常处理函数,完成连接资源清理工作。
cpp
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 ~> 本文总结
核心考点深度总结
- ET模式的铁则 ET模式是Reactor高性能的核心,但有两条不可突破的约束:所有文件描述符必须设置为非阻塞;所有IO操作必须循环执行直到返回EAGAIN。违反任意一条都会导致程序挂起或事件丢失。LT模式无此约束,但事件通知效率更低,适合快速开发的场景。
- 事件注册的黄金法则 读事件常设,写事件按需注册。这是epoll编程中最容易出错的点,很多初学者会将EPOLLOUT常设,导致CPU跑满。核心逻辑:发送数据时先尝试直接发送,发不完再注册写事件,写完立刻取消写事件监听。
- 系统调用错误码三分支判断 所有非阻塞IO系统调用(accept、recv、send)返回-1时,绝不能直接判定为错误,必须按优先级判断:
- EAGAIN/EWOULDBLOCK:正常结束,本轮IO完成
- EINTR:被信号中断,重试即可
- 其他错误码:真正的IO错误,走异常处理 该判断逻辑是所有非阻塞IO的标准范式,必须烂熟于心。
- 分层解耦的回调机制 Reactor网络层通过回调函数对接协议层与业务层,实现了"网络IO"与"业务逻辑"的完全解耦。替换业务逻辑无需修改网络代码,只需更换回调函数,这是工业级网络库的标准设计思想。
- 智能指针的循环引用陷阱 双向引用场景下严禁双向使用shared_ptr,必然导致循环引用内存泄漏。解决方案有两种:一方使用裸指针作弱引用(本文方案);一方使用weak_ptr。前者实现简单,后者更安全但有额外开销。
- 连接生命周期的空指针防护 事件派发时必须先判断连接是否存在。因为在处理读事件的过程中,可能已经触发异常并移除了连接,若继续处理写事件会访问空指针。这是多逻辑分支下极易忽略的边界问题。
- 异常事件的正确处理 EPOLLERR、EPOLLHUP属于套接字异常事件,应优先触发异常清理逻辑,而非简单转换为读写事件。对端正常关闭(recv返回0)属于正常生命周期,应与错误异常区分处理。
- 工程化细节坑点
- 头文件包含顺序与前置声明的配合使用,解决循环依赖问题
- 日志级别的正确使用,禁止将正常现象标记为致命错误
- 字符串操作的二进制兼容性问题,根据协议类型选择正确的追加方式
- 套接字创建时统一设置非阻塞与端口复用选项,避免遗漏
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux网络】多路转接epoll(三)Reactor模式:基于epoll的高性能网络服务器设计与实现(上)代码框架
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
