文章目录
- 前言
- Server部分
- 核心四大模块设计思想与架构拆解
-
- 第一部分:底层基建与资源管理(基础组件)
- [第二部分:事件驱动与多路复用(Reactor 心脏)](#第二部分:事件驱动与多路复用(Reactor 心脏))
- [第三部分:TCP 服务层(连接与生命周期管理)](#第三部分:TCP 服务层(连接与生命周期管理))
- 第四部分:并发与线程模型(底层架构支撑)
- 第五部分:架构联动与业务流转
- 核心模块全景解析与源码展示
-
- 第一部分:基础组件层(网络通信的基石)
-
- [Buffer 模块:抗住 TCP 粘包的流式内存池](#Buffer 模块:抗住 TCP 粘包的流式内存池)
- [Socket 模块:RAII 与非阻塞 I/O 的护城河](#Socket 模块:RAII 与非阻塞 I/O 的护城河)
- [第二部分:事件驱动层(Reactor 的心脏)](#第二部分:事件驱动层(Reactor 的心脏))
-
- [Channel 模块:事件分发模型的核心枢纽](#Channel 模块:事件分发模型的核心枢纽)
- [Poller 模块:Epoll 多路复用的引擎封装](#Poller 模块:Epoll 多路复用的引擎封装)
- [EventLoop 模块:跨线程并发与唤醒的博弈](#EventLoop 模块:跨线程并发与唤醒的博弈)
- [TimerWheel 模块:时间轮与智能指针的生死哲学](#TimerWheel 模块:时间轮与智能指针的生死哲学)
- [第三部分:TCP 服务与并发层(主从架构落地)](#第三部分:TCP 服务与并发层(主从架构落地))
-
- [Acceptor 模块:主线程的专属迎宾员](#Acceptor 模块:主线程的专属迎宾员)
- [LoopThread & LoopThreadPool 模块:多线程并发兵工厂](#LoopThread & LoopThreadPool 模块:多线程并发兵工厂)
- [Connection 模块:全生命周期的连接大管家](#Connection 模块:全生命周期的连接大管家)
- [TcpServer 模块:主从 Reactor 的终极装配厂](#TcpServer 模块:主从 Reactor 的终极装配厂)
- [Any 模块:连接的业务上下文容器](#Any 模块:连接的业务上下文容器)
- http协议部分
- 核心四大模块设计思想与架构拆解
- 核心模块全景解析与源码展示
-
- [1. HttpRequest / HttpResponse 模块:双向交互的数据载体](#1. HttpRequest / HttpResponse 模块:双向交互的数据载体)
- [2. HttpContext 模块:应对半包与粘包的状态机引擎](#2. HttpContext 模块:应对半包与粘包的状态机引擎)
- [3. Util 模块:协议辅助与安全防线](#3. Util 模块:协议辅助与安全防线)
- [4. HttpServer 模块:路由分发与响应组装](#4. HttpServer 模块:路由分发与响应组装)
- 总结
前言
在 C++ 后端开发领域,网络编程与高并发处理始终是绕不开的核心难点。为了深入探究底层的运行机制,我决定从零开始,仿照经典的 muduo 网络库,手写一个高性能的 C++ Web 服务器。
项目已开源,欢迎大家交流指正:
https://gitee.com/xiao-jia-can-learn/cfangxiemuduogaobingfafuwuqi/tree/master/muduo
整个项目分为两大核心支柱:
第一部分是底层的网络基础架构(Server 模块),负责处理并发、事件驱动与 I/O 多路复用;
第二部分是应用层的 HTTP 协议实现(Http 模块),负责解析报文、路由分发与业务交互。
这不仅是一次对操作系统的系统调用(如 epoll、socket)的实战演练,更是对 Reactor 架构模式、C++ 面向对象设计与泛型编程的深度应用。本系列博客将完整记录我从零构建这个服务器的架构设计思想与源码落地细节,带大家一同揭开高性能服务器的神秘面纱。

Server部分
核心四大模块设计思想与架构拆解
第一部分:底层基建与资源管理(基础组件)
这部分是网络运行的基石,决定了下层系统的性能上限。
- Buffer(应用层缓冲区)
-
设计思想 :TCP是流式协议,存在粘包/半包问题。本模块彻底摒弃了
std::string,因为string带有字符串语义。(遇到 \0 截断、resize自动填充),不仅开销大,在 C++17 之前其data()更是无法直接写入的。我们采用std::vector<char>构建纯字节容器,配合双指针(读偏移_reader_idx、写偏移_writer_idx)维护"已读废弃区、可读数据区、尾部空闲区"三块内存。 -
技术难点与解决(阶梯式扩容策略)
当尾部空间不足时,如果直接 resize 向内核申请内存,会导致严重的性能惩罚(会经历"向内核申请、全量拷贝、释放旧内存"这三步。这三步极度耗时)。EnsureWriteSpace 的解决方案是:
cpp
void EnsureWriteSpace(uint64_t len) {
if (TailIdleSize() >= len) return; // 1. 尾部够用,直接返回
if (len <= TailIdleSize() + HeadIdleSize()) { // 2. 空间复用(核心)
uint64_t rsz = ReadAbleSize();
std::copy(ReadPosition(), ReadPosition() + rsz, Begin()); // 内存前移
_reader_idx = 0; _writer_idx = rsz;
} else {
_buffer.resize(_writer_idx + len); // 3. 彻底不够,才向内核扩容
}
}
工程权衡 :优先进行 O(N) 的内存前移(CPU 拷贝),以复用头部的废弃空间;只有物理内存确实不足时,才触发 resize。用廉价的 CPU 周期换取昂贵的系统内存分配,这是高并发场景下的极致压榨。
- Socket(套接字封装)
- 设计思想 :基于 RAII 机制封装 fd,确保异常发生时也能安全 close。具体体现在
~Socket() { Close(); } - 技术难点(非阻塞 I/O 错误码陷阱):在 LT(水平触发)模式下,recv 返回 0 并非断开,而是 EAGAIN(无数据);返回 -1 可能是 EINTR(被信号打断)。必须精准区分并转化为"本轮无数据"放行,交由下一轮 epoll 继续驱动,避免误杀连接。
第二部分:事件驱动与多路复用(Reactor 心脏)
这是整个并发引擎的驱动力,负责将操作系统的 I/O 事件精准路由到对应的业务回调。1. Channel 与 Poller(事件分发枢纽)
-
设计思想 :
epoll_wait仅返回就绪的fd集合。Poller 负责底层监控,内部维护unordered_map<int, Channel*>实现 O(1) 的路由;Channel 负责将fd+ 关注事件(_events) + 实际事件(_revents) + 回调函数封装在一起。 -
技术难点(多事件并发下的防御性分发)
看
Channel::HandleEvent的源码逻辑:
cpp
void HandleEvent() {
// 独立 if:读写事件无副作用,可并存处理
if (_revents & (EPOLLIN | EPOLLRDHUP)) { if (_read_callback) _read_callback(); }
if (_revents & EPOLLOUT) { if (_write_callback) _write_callback(); }
// 互斥 else if:错误与挂断会触发连接销毁!
else if (_revents & EPOLLERR) { if (_error_callback) _error_callback(); }
else if (_revents & EPOLLHUP) { if (_close_callback) _close_callback(); }
}
底层洞察 :如果 EPOLLOUT 和 EPOLLERR 同时触发,若使用独立 if,会先发数据再处理错误(或者反过来),这极易引发对象的重复释放(Double Free)导致 Core Dump。else if 是 C++ 后端开发的保命底线。
- EventLoop(并发博弈引擎)
- 设计思想 :
One Loop Per Thread。每个线程跑一个循环,所有对连接的操作都回到该线程闭环执行,从架构层面消灭了锁竞争。 - 技术难点 1(跨线程唤醒内核机制)
当业务线程投递任务,而 IO 线程正阻塞在 epoll_wait 时,如何叫醒它?引入 Linux 特有的 eventfd(轻量级计数器)。
cpp
void QueueInLoop(const Functor &cb) {
{ std::unique_lock<std::mutex> lock(_mutex); _tasks.push_back(cb); }
WeakUpEventFd(); // 向 eventfd 写入 uint64_t,强刷 EPOLLIN 唤醒底层
}
- 技术难点 2(任务队列防死锁与锁降维)
RunAllTask如果直接加锁遍历任务,任务内部若再次调用QueueInLoop就会死锁。解决方案是Swap
技巧: 将锁的持有时间从 O(N) 降维到 O(1) 的交换动作,彻底释放并发性能。
cpp
void RunAllTask() {
std::vector<Functor> functor;
{ std::unique_lock<std::mutex> lock(_mutex); _tasks.swap(functor); } // 锁内交换 O(1)
for (auto &f : functor) f(); // 锁外执行,完美防死锁
}
第三部分:TCP 服务层(连接与生命周期管理)
在这一层,原始的字节流被赋予了状态与生命。
- 设计思想 :一个客户端对应一个 Connection,内部通过状态机
(CONNECTING -> CONNECTED -> DISCONNECTING -> DISCONNECTED)驱动全生命周期。 - 技术难点 1(致命的悬空指针与回调顺序)
Connection 的销毁由ReleaseInLoop负责。
cpp
void ReleaseInLoop() {
_statu = DISCONNECTED;
_channel.Remove(); _socket.Close();
if (_closed_callback) _closed_callback(shared_from_this()); // 1. 先回调业务层
if (_server_closed_callback) _server_closed_callback(shared_from_this()); // 2. 最后擦除表记录
}
深度剖析 :_server_closed_callback 会从 TcpServer 的 _conns 哈希表中擦除当前连接。如果先擦除,对象的 shared_ptr 引用计数瞬间归零,this 对象直接析构!此时再去调用 _closed_callback 就会访问野指针导致段错误。必须先执行业务回调,最后再让系统收回控制权(擦除表)。同时,类必须继承 std::enable_shared_from_this,回调中只能传shared_from_this(),绝不能用 this。
- 技术难点 2(三段式异步安全发送)
Send:跨线程屏障,先拷贝数据(防原数据析构悬空),通过RunInLoop抛回IO线程。
SendInLoop:将数据放入_out_buffer,开启EPOLLOUT监听。(不直接发,因为内核发送缓冲区可能已满)。
HandleWrite:epoll驱动,真正调用send发包,发完DisableWrite,防止 LT 模式下的 Busy Loop。
- Acceptor(监听器)
专职监听socket。其构造函数绝对不能开启读监控(EnableRead)。因为监听回调_accept_callback是后续由 TcpServer 设置的,如果提前开启,立即到达的连接触发HandleRead时,回调为空会导致新连接的 fd 直接泄漏。
第四部分:并发与线程模型(底层架构支撑)
- TimerWheel(时间轮)
-
设计思想:用 60 格时间轮实现 O(1) 级别的海量非活跃连接清理。
-
技术难点(智能指针驱动执行)
任务不是被遍历调用的,而是被析构触发的。
格子
_wheel里存shared_ptr<TimerTask>,秒针每到一格直接清空,引用计数归零触发 TimerTask 析构,在析构函数里执行真正的清理。
cpp
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
PtrTask pt(new TimerTask(id, delay, cb));
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt); // 强引用:决定任务生死
_timers[id] = WeakTask(pt); // 弱引用:仅做索引,绝不影响生死
}
底层洞察 :为什么 _timers 用 weak_ptr?如果用 shared_ptr,刷任务时对象被双持(轮子一份、表一份),秒针扫过时引用计数减不到 0,对象不析构,任务永远不执行。weak_ptr 是这里的唯一正解。
- LoopThreadPool(主从线程池)
支撑主从 Reactor 模型。必须保证 EventLoop 在其归属的子线程内部实例化(保证 _thread_id 正确)。主线程通过条件变量等待子线程构造完毕后再取走指针,分配连接时采用轮询取模策略实现负载均衡。
第五部分:架构联动与业务流转
server.hpp 的优雅在于各模块的严丝合缝:
客户端 connect
│
▼
主线程 baseloop ──(Poller 监控监听fd)──> accept 获取 newfd
│
▼ 轮询分配
子线程 subloop <──(eventfd 唤醒)── 将 newfd 挂载至子 Poller
│
▼ epoll_wait 就绪
Channel 分发\] -\> \[Connection 读写\] -\> \[Buffer 存取\] -\> \[业务逻辑
│
▼ 断开或超时
Release 安全注销 -> 智能指针归零,资源回收
底层流转机制:
- 接入 :主
_baseloop捕获监听事件,Acceptor执行accept,TcpServer 轮询分配子EventLoop,新 Connection 诞生。 - 唤醒 :主线程通过
RunInLoop打包任务,写入eventfd。子线程从epoll_wait惊醒,接手新 fd。 - 通信 :子线程
epoll_wait再次触发,Channel路由,Connection从Socket读到Buffer,唤醒上层应用逻辑。 - 闭环:若连接空闲,时间轮析构触发清理;若连接断开,执行严格的"先回调、后擦除"注销逻辑,彻底防止野指针。
核心模块全景解析与源码展示
这一个部分专门介绍每一个模块的功能以及运行逻辑,与此同时我还对每个模块的功能不同进行了分类处理。方便大家理解和阅读。
第一部分:基础组件层(网络通信的基石)
这一部分主要包括 Buffer 模块和 Socket 模块。
Buffer 模块:抗住 TCP 粘包的流式内存池
功能与运行逻辑深度解析
Buffer 类是网络通信的数据集散地,专治 TCP 流式协议带来的"粘包/半包"问题。它摒弃了带有字符串语义的 std::string,底层采用 std::vector<char> 作为纯字节容器。其内部维护了读偏移(_reader_idx)和写偏移(_writer_idx),将内存划分为已读废弃区、可读区和尾部空闲区。当发生数据写入时,会先通过 EnsureWriteSpace 检查空间,优先复用头部废弃区,不够才扩容;读取时则直接移动偏移量。对外提供按行读取等接口,供上层 HTTP 协议解析使用。
难点与解决(踩坑实录)
-
为什么不直接用 std::string?
- 困难 :最初想用
std::string存字节流,但string带有强烈的"字符串语义"。比如在resize时会自动用\0填充,遇到二进制数据时会导致截断,且 C++17 之前data()无法直接写入。 - 解决 :改用
std::vector<char>,它就是一块纯粹的内存,没有任何冗余的语义干预,且配合&vec[0]可以无缝传给底层的recv/send。
- 困难 :最初想用
-
频繁 resize 导致的性能灾难(CPU 飙升)
- 困难 :最初只要发现尾部空间不够,就直接
resize扩容。但在高并发压测时,频繁向内核申请和释放内存,导致 CPU 占用率极高,拖垮了吞吐量。 - 解决 :设计了阶梯式扩容机制(
EnsureWriteSpace)。如果头部废弃区和尾部空闲区加起来够用,就调用std::copy把可读数据整体前移复用。用一次廉价的 CPU 内存拷贝,换取了极其昂贵的系统内核内存分配,实现了极致的压榨。
- 困难 :最初只要发现尾部空间不够,就直接
-
读取时指针越界导致的段错误(Segmentation Fault)
- 困难 :在实现
Read函数时,如果没有严格校验len,一旦上层传入的读取长度大于当前的可读数据,就会直接发生内存越界,服务器瞬间崩溃。 - 解决 :在
MoveReadOffset和Read等核心操作中,强制加入assert(len <= ReadAbleSize())断言,把防御性编程做在最底层。
- 困难 :在实现
-
HTTP 解析时的"半包"死循环(查找换行符陷阱)
- 困难 :由于网络数据到达的不确定性,
memchr查找\n时可能找不到。如果没有处理找不到的情况,直接返回,会导致上层业务死循环等待。 - 解决 :引入了
GetLineAndPop()。如果没找到\n,直接返回空字符串,且绝对不移动读指针。让上层框架(基于 LT 模式)等待下一次数据到达后,再重新拼接解析。
- 困难 :由于网络数据到达的不确定性,
代码展示
cpp
#define BUFFER_DEFAULT_SIZE 1024
class Buffer
{
private:
std::vector<char> _buffer; // 使用vector进行内存空间管理
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移
public:
Buffer() : _reader_idx(0), _writer_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}
char *Begin() { return &*_buffer.begin(); } // return &_buffer[0];
// 获取当前写入起始地址, _buffer的空间起始地址,加上写偏移量
char *WritePosition() { return Begin() + _writer_idx; }
// 获取当前读取起始地址
char *ReadPosition() { return Begin() + _reader_idx; }
// 获取缓冲区末尾空闲空间大小--写偏移之后的空闲空间, 总体空间大小减去写偏移
uint64_t TailIdleSize() { return _buffer.size() - _writer_idx; }
// 获取缓冲区起始空闲空间大小--读偏移之前的空闲空间
uint64_t HeadIdleSize() { return _reader_idx; }
// 获取可读数据大小 = 写偏移 - 读偏移
uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }
// 将读偏移向后移动
void MoveReadOffset(uint64_t len)
{
if (len == 0)
return;
// 向后移动的大小,必须小于可读数据大小
assert(len <= ReadAbleSize());
_reader_idx += len;
}
// 将写偏移向后移动
void MoveWriteOffset(uint64_t len)
{
// 向后移动的大小,必须小于当前后边的空闲空间大小
assert(len <= TailIdleSize());
_writer_idx += len;
}
// 确保可写空间足够(整体空闲空间够了就移动数据,否则就扩容)
void EnsureWriteSpace(uint64_t len)
{
// 如果末尾空闲空间大小足够,直接返回
if (TailIdleSize() >= len)
{
return;
}
// 末尾空闲空间不够,则判断加上起始位置的空闲空间大小是否足够, 够了就将数据移动到起始位置
if (len <= TailIdleSize() + HeadIdleSize())
{
// 将数据移动到起始位置
uint64_t rsz = ReadAbleSize(); // 把当前数据大小先保存起来
std::copy(ReadPosition(), ReadPosition() + rsz, Begin()); // 把可读数据拷贝到起始位置
_reader_idx = 0; // 将读偏移归0
_writer_idx = rsz; // 将写位置置为可读数据大小, 因为当前的可读数据大小就是写偏移量
}
else
{
// 总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可
DBG_LOG("RESIZE %ld", _writer_idx + len);
_buffer.resize(_writer_idx + len);
}
}
// 写入数据
void Write(const void *data, uint64_t len)
{
// 1. 保证有足够空间,2. 拷贝数据进去
if (len == 0)
return;
EnsureWriteSpace(len);
const char *d = (const char *)data;
std::copy(d, d + len, WritePosition());
}
void WriteAndPush(const void *data, uint64_t len)
{
Write(data, len);
MoveWriteOffset(len);
}
void WriteString(const std::string &data)
{
return Write(data.c_str(), data.size());
}
void WriteStringAndPush(const std::string &data)
{
WriteString(data);
MoveWriteOffset(data.size());
}
void WriteBuffer(Buffer &data)
{
return Write(data.ReadPosition(), data.ReadAbleSize());
}
void WriteBufferAndPush(Buffer &data)
{
WriteBuffer(data);
MoveWriteOffset(data.ReadAbleSize());
}
// 读取数据
void Read(void *buf, uint64_t len)
{
// 要求要获取的数据大小必须小于可读数据大小
assert(len <= ReadAbleSize());
std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
}
void ReadAndPop(void *buf, uint64_t len)
{
Read(buf, len);
MoveReadOffset(len);
}
std::string ReadAsString(uint64_t len)
{
// 要求要获取的数据大小必须小于可读数据大小
assert(len <= ReadAbleSize());
std::string str;
str.resize(len);
Read(&str[0], len);
return str;
}
std::string ReadAsStringAndPop(uint64_t len)
{
assert(len <= ReadAbleSize());
std::string str = ReadAsString(len);
MoveReadOffset(len);
return str;
}
char *FindCRLF()
{
char *res = (char *)memchr(ReadPosition(), '\n', ReadAbleSize());
return res;
}
/*通常获取一行数据,这种情况针对是*/
std::string GetLine()
{
char *pos = FindCRLF();
if (pos == NULL)
{
return "";
}
// +1是为了把换行字符也取出来。
return ReadAsString(pos - ReadPosition() + 1);
}
std::string GetLineAndPop()
{
std::string str = GetLine();
MoveReadOffset(str.size());
return str;
}
// 清空缓冲区
void Clear()
{
// 只需要将偏移量归0即可
_reader_idx = 0;
_writer_idx = 0;
}
};
Socket 模块:RAII 与非阻塞 I/O 的护城河
功能与运行逻辑深度解析
Socket 类是对 Linux 原生 API 的面向对象封装。它最核心的价值在于实现了 RAII(资源获取即初始化)机制:对象构造时接管 fd,析构时自动 close(),从根本上杜绝了文件描述符泄漏。在收发数据时,它作为底层的护城河,精确拦截了非阻塞模式下 EAGAIN(缓冲区没数据)和 EINTR(被信号打断)这两个错误码,将其转化为"本轮无数据",保护上层连接不被误杀。
难点与解决(踩坑实录)
-
非阻塞 I/O 的"死亡陷阱":EAGAIN 与 EINTR
- 困难 :在非阻塞模式下,
recv经常返回 -1。如果像阻塞模式那样,遇到 -1 就判定为连接断开并关闭 socket,会导致大量正常连接被"误杀",服务器疯狂掉线。 - 解决 :在
Recv和Send封装里,必须严格检查errno。如果是EAGAIN(当前缓冲区没数据)或EINTR(系统调用被信号打断),说明连接依然健康,强行转为返回 0(表示本轮无数据)。交由下一轮 epoll 事件继续驱动。
- 困难 :在非阻塞模式下,
-
accept 出来的新连接,默认是阻塞的!
- 困难 :监听套接字设置了非阻塞,但
accept返回的新连接fd默认是阻塞模式。如果在Connection中直接用这个新fd去recv,一旦没有数据,整个子线程都会被卡死。 - 解决 :在
accept成功后,立即调用fcntl,通过F_GETFL和F_SETFL强制把新连接也设置为O_NONBLOCK。绝不能让任何一个阻塞的fd进入EventLoop。
- 困难 :监听套接字设置了非阻塞,但
-
SIGPIPE 信号:会导致服务器瞬间暴毙
- 困难 :当客户端异常断开(比如直接拔网线或强杀进程),服务端底层的 TCP 连接已经收到 RST 包。如果此时业务层还在往这个死连接里
send数据,内核会向进程发送SIGPIPE信号。这个信号的默认行为是直接终止整个进程。 - 解决 :在网络库启动的最早期,必须调用
signal(SIGPIPE, SIG_IGN);忽略该信号。让底层send正常返回 -1,由上层通过Release优雅关闭连接,而不是让服务器崩溃。
- 困难 :当客户端异常断开(比如直接拔网线或强杀进程),服务端底层的 TCP 连接已经收到 RST 包。如果此时业务层还在往这个死连接里
代码展示
cpp
#define MAX_LISTEN 1024
class Socket
{
private:
int _sockfd;
public:
Socket() : _sockfd(-1) {}
Socket(int fd) : _sockfd(fd) {}
~Socket() { Close(); }
int Fd() { return _sockfd; }
// 创建套接字
bool Create()
{
// int socket(int domain, int type, int protocol)
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
ERR_LOG("CREATE SOCKET FAILED!!");
return false;
}
return true;
}
// 绑定地址信息
bool Bind(const std::string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
// int bind(int sockfd, struct sockaddr*addr, socklen_t len);
int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0)
{
ERR_LOG("BIND ADDRESS FAILED!");
return false;
}
return true;
}
// 开始监听
bool Listen(int backlog = MAX_LISTEN)
{
// int listen(int backlog)
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
ERR_LOG("SOCKET LISTEN FAILED!");
return false;
}
return true;
}
// 向服务器发起连接
bool Connect(const std::string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
// int connect(int sockfd, struct sockaddr*addr, socklen_t len);
int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0)
{
ERR_LOG("CONNECT SERVER FAILED!");
return false;
}
return true;
}
// 获取新连接
int Accept()
{
// int accept(int sockfd, struct sockaddr *addr, socklen_t *len);
int newfd = accept(_sockfd, NULL, NULL);
if (newfd < 0)
{
ERR_LOG("SOCKET ACCEPT FAILED!");
return -1;
}
return newfd;
}
// 接收数据
ssize_t Recv(void *buf, size_t len, int flag = 0)
{
// ssize_t recv(int sockfd, void *buf, size_t len, int flag);
ssize_t ret = recv(_sockfd, buf, len, flag);
if (ret <= 0)
{
// EAGAIN 当前socket的接收缓冲区中没有数据了,在非阻塞的情况下才会有这个错误
// EINTR 表示当前socket的阻塞等待,被信号打断了,
if (errno == EAGAIN || errno == EINTR)
{
return 0; // 表示这次接收没有接收到数据
}
ERR_LOG("SOCKET RECV FAILED!!");
return -1;
}
return ret; // 实际接收的数据长度
}
ssize_t NonBlockRecv(void *buf, size_t len)
{
return Recv(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前接收为非阻塞。
}
// 发送数据
ssize_t Send(const void *buf, size_t len, int flag = 0)
{
// ssize_t send(int sockfd, void *data, size_t len, int flag);
ssize_t ret = send(_sockfd, buf, len, flag);
if (ret < 0)
{
if (errno == EAGAIN || errno == EINTR)
{
return 0;
}
ERR_LOG("SOCKET SEND FAILED!!");
return -1;
}
return ret; // 实际发送的数据长度
}
ssize_t NonBlockSend(void *buf, size_t len)
{
if (len == 0)
return 0;
return Send(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前发送为非阻塞。
}
// 关闭套接字
void Close()
{
if (_sockfd != -1)
{
close(_sockfd);
_sockfd = -1;
}
}
// 创建一个服务端连接
bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
{
// 1. 创建套接字,2. 绑定地址,3. 开始监听,4. 设置非阻塞, 5. 启动地址重用
if (Create() == false)
return false;
if (block_flag)
NonBlock();
if (Bind(ip, port) == false)
return false;
if (Listen() == false)
return false;
ReuseAddress();
return true;
}
// 创建一个客户端连接
bool CreateClient(uint16_t port, const std::string &ip)
{
// 1. 创建套接字,2.指向连接服务器
if (Create() == false)
return false;
if (Connect(ip, port) == false)
return false;
return true;
}
// 设置套接字选项---开启地址端口重用
void ReuseAddress()
{
// int setsockopt(int fd, int leve, int optname, void *val, int vallen)
int val = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
val = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
}
// 设置套接字阻塞属性-- 设置为非阻塞
void NonBlock()
{
// int fcntl(int fd, int cmd, ... /* arg */ );
int flag = fcntl(_sockfd, F_GETFL, 0);
fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}
};
第二部分:事件驱动层(Reactor 的心脏)
这一部分呢主要包括四个模块,这四个模块是reactor中最核心的模块,其中包括:Channel模块,Poller模块和EventLoop模块,TimerWheel模块。
Channel 模块:事件分发模型的核心枢纽
功能与运行逻辑深度解析
Channel 是 Reactor 模型中承上启下的组件。它把底层的 fd、想要监控的事件(_events)、实际发生的事件(_revents)以及 5 个核心业务回调打包在了一起。当底层事件就绪时,Channel 会触发 HandleEvent,通过按位与(&)操作解析 _revents,精准分发并执行对应的读、写、错误或挂断回调,完成了底层事件到上层业务的翻译。
难点与解决(踩坑实录)
-
多事件并发引发的"悬空指针"崩溃
- 困难 :如果底层的同一个
fd同时触发了EPOLLOUT(可写)和EPOLLERR(错误),最初我把读、写、错误全部写成了独立的if分支。结果错误事件会触发连接对象的销毁,紧接着代码又去执行写回调,直接访问了已经被delete的野指针,服务器当场 Core Dump。 - 解决 :在
HandleEvent中严格规定:无副作用的读、写事件用独立if;而必定会导致对象析构的错误(ERR)和挂断(HUP)事件,必须用else if互斥处理。保证在一次事件分发中,最多只处理一个会销毁连接的操作,这是 C++ 网络编程的保命底线。
- 困难 :如果底层的同一个
-
EPOLLRDHUP 事件的特殊处理
- 困难 :当客户端半关闭(如调用
shutdown)时,底层触发的是EPOLLRDHUP而不是普通的EPOLLIN。如果不处理,缓冲区内残留的最后一点数据就会丢失。 - 解决 :在
HandleEvent的读事件判断中,把EPOLLRDHUP也纳入触发_read_callback的条件里,确保先把残余数据读完,再触发关闭逻辑。
- 困难 :当客户端半关闭(如调用
代码展示
cpp
class EventLoop;
class Channel
{
private:
int _fd;
EventLoop *_loop;
uint32_t _events; // 当前需要监控的事件
uint32_t _revents; // 当前连接触发的事件
using EventCallback = std::function<void()>;
EventCallback _read_callback; // 可读事件被触发的回调函数
EventCallback _write_callback; // 可写事件被触发的回调函数
EventCallback _error_callback; // 错误事件被触发的回调函数
EventCallback _close_callback; // 连接断开事件被触发的回调函数
EventCallback _event_callback; // 任意事件被触发的回调函数
public:
Channel(EventLoop *loop, int fd) : _fd(fd), _events(0), _revents(0), _loop(loop) {}
int Fd() { return _fd; }
uint32_t Events() { return _events; } // 获取想要监控的事件
void SetREvents(uint32_t events) { _revents = events; } // 设置实际就绪的事件
void SetReadCallback(const EventCallback &cb) { _read_callback = cb; }
void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; }
void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; }
void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; }
void SetEventCallback(const EventCallback &cb) { _event_callback = cb; }
// 当前是否监控了可读
bool ReadAble() { return (_events & EPOLLIN); }
// 当前是否监控了可写
bool WriteAble() { return (_events & EPOLLOUT); }
// 启动读事件监控
void EnableRead()
{
_events |= EPOLLIN;
Update();
}
// 启动写事件监控
void EnableWrite()
{
_events |= EPOLLOUT;
Update();
}
// 关闭读事件监控
void DisableRead()
{
_events &= ~EPOLLIN;
Update();
}
// 关闭写事件监控
void DisableWrite()
{
_events &= ~EPOLLOUT;
Update();
}
// 关闭所有事件监控
void DisableAll()
{
_events = 0;
Update();
}
// 移除监控
void Remove();
void Update();
// 事件处理,一旦连接触发了事件,就调用这个函数,自己触发了什么事件如何处理自己决定
void HandleEvent()
{
if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
{
/*不管任何事件,都调用的回调函数*/
if (_read_callback)
_read_callback();
}
/*有可能会释放连接的操作事件,一次只处理一个*/
if (_revents & EPOLLOUT)
{
if (_write_callback)
_write_callback();
}
else if (_revents & EPOLLERR)
{
if (_error_callback)
_error_callback(); // 一旦出错,就会释放连接,因此要放到前边调用任意回调
}
else if (_revents & EPOLLHUP)
{
if (_close_callback)
_close_callback();
}
if (_event_callback)
_event_callback();
}
};
Poller 模块:Epoll 多路复用的引擎封装
功能与运行逻辑深度解析
Poller 是对 Linux epoll 机制的直接封装,负责底层的事件监控与阻塞等待。为了解决 epoll_wait 返回的一堆裸 fd 无法精准匹配上层业务对象的问题,Poller 内部维护了一个 unordered_map<int, Channel*> 哈希表。在添加或修改事件时,它通过哈希表判断该执行 EPOLL_CTL_ADD 还是 EPOLL_CTL_MOD;当事件就绪时,它以 O(1) 的时间复杂度查找到对应的 Channel 并将事件分发下去。
难点与解决(踩坑实录)
-
EPOLL_CTL_ADD 与 MOD 的选择陷阱
- 困难 :最初写
UpdateEvent时,想统一用ADD来处理,或者统一用MOD。但底层内核规定,如果对一个已存在的fd再次ADD,epoll_ctl会直接报错EEXIST;如果对未注册的fd执行MOD,同样会报错。 - 解决 :必须维护一个
fd到Channel的哈希表。每次更新事件前,先find一下。找不到说明是首次注册,用ADD;找到了说明是状态变更(比如从只读变成读写),用MOD。
- 困难 :最初写
-
惊群效应与触发模式(LT vs ET)的抉择
- 困难 :
ET(边缘触发)模式虽然性能高,但必须配合非阻塞 I/O 一次性把数据读干净,否则事件会彻底丢失,引发连接假死。且在多线程下ET的编程难度极大。 - 解决 :本项目坚定选择了
LT(水平触发)模式,容错率极高。配合上层的非阻塞 I/O,即使一次没读完,下次epoll_wait还会继续通知,保证了网络流的平稳。
- 困难 :
代码展示
cpp
#define MAX_EPOLLEVENTS 1024
class Poller
{
private:
int _epfd;
struct epoll_event _evs[MAX_EPOLLEVENTS];
std::unordered_map<int, Channel *> _channels;
private:
// 对epoll的直接操作
void Update(Channel *channel, int op)
{
// int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
int fd = channel->Fd();
struct epoll_event ev;
ev.data.fd = fd;
ev.events = channel->Events();
int ret = epoll_ctl(_epfd, op, fd, &ev);
if (ret < 0)
{
ERR_LOG("EPOLLCTL FAILED!");
}
return;
}
// 判断一个Channel是否已经添加了事件监控
bool HasChannel(Channel *channel)
{
auto it = _channels.find(channel->Fd());
if (it == _channels.end())
{
return false;
}
return true;
}
public:
Poller()
{
_epfd = epoll_create(MAX_EPOLLEVENTS);
if (_epfd < 0)
{
ERR_LOG("EPOLL CREATE FAILED!!");
abort(); // 退出程序
}
}
// 添加或修改监控事件
void UpdateEvent(Channel *channel)
{
bool ret = HasChannel(channel);
if (ret == false)
{
// 不存在则添加
_channels.insert(std::make_pair(channel->Fd(), channel));
return Update(channel, EPOLL_CTL_ADD);
}
return Update(channel, EPOLL_CTL_MOD);
}
// 移除监控
void RemoveEvent(Channel *channel)
{
auto it = _channels.find(channel->Fd());
if (it != _channels.end())
{
_channels.erase(it);
}
Update(channel, EPOLL_CTL_DEL);
}
// 开始监控,返回活跃连接
void Poll(std::vector<Channel *> *active)
{
// int epoll_wait(int epfd, struct epoll_event *evs, int maxevents, int timeout)
int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);
if (nfds < 0)
{
if (errno == EINTR) // 被信号打断了
{
return;
}
ERR_LOG("EPOLL WAIT ERROR:%s\n", strerror(errno));
abort(); // 退出程序
}
for (int i = 0; i < nfds; i++)
{
auto it = _channels.find(_evs[i].data.fd);
assert(it != _channels.end());
it->second->SetREvents(_evs[i].events); // 设置实际就绪的事件
active->push_back(it->second);
}
return;
}
};
EventLoop 模块:跨线程并发与唤醒的博弈
功能与运行逻辑深度解析
EventLoop 是 Reactor 模型的绝对心脏。在 One Loop Per Thread 架构下,它死死绑定一个独立的线程,死循环驱动 Poller 监听事件、执行回调。为了让其他线程能够安全地向当前线程投递任务,它内部维护了任务队列,并引入了 Linux 专属的 eventfd。只要有任务入队,就向 eventfd 写入数据,强行唤醒阻塞在 epoll_wait 的线程,随后一次性把任务池里的任务执行完。
难点与解决(踩坑实录)
-
跨线程投递任务:如何"叫醒"沉睡的引擎?
- 困难 :当主线程向子线程的
EventLoop投递任务时,子线程正阻塞在epoll_wait里死等网络事件,根本不知道有新任务来了。 - 解决 :引入
eventfd。其他线程在往任务队列push完数据后,立刻向eventfd写一个 8 字节的整数。这会让eventfd变成可读状态,触发底层epoll_wait返回。子线程醒来后清空eventfd计数,再去处理任务队列。
- 困难 :当主线程向子线程的
-
任务队列的致命死锁
- 困难 :如果在
RunAllTask中直接加锁遍历执行任务,一旦某个任务内部(比如发数据时触发了重连逻辑)再次调用了QueueInLoop,就会因为互斥锁未释放而陷入无限等待,整个服务器瞬间卡死。 - 解决 :使用了极其经典的
Swap技巧。在锁的保护下,将_tasks与局部变量tasks交换,然后立刻释放锁;最后在完全无锁的状态下遍历执行tasks。不仅解决了死锁,还将持锁时间压缩到了微秒级。
- 困难 :如果在
-
eventfd 读出数据的"毫无用处"
- 困难 :最初以为
read(_eventfd)读出来的数字有什么特殊含义,尝试对它进行判断。 - 解决 :实际上,
eventfd读出来的数值(代表累加唤醒次数)没有任何实际用处。我们执行read的唯一目的,就是为了把内核里的计数器清零。如果不读,它会一直处于可读状态(LT),导致epoll_wait疯狂空转,CPU 直接 100%。
- 困难 :最初以为
代码展示
cpp
class EventLoop
{
private:
using Functor = std::function<void()>;
std::thread::id _thread_id; // 线程ID
int _event_fd; // eventfd唤醒IO事件监控有可能导致的阻塞
std::unique_ptr<Channel> _event_channel;
Poller _poller; // 进行所有描述符的事件监控
std::vector<Functor> _tasks; // 任务池
std::mutex _mutex; // 实现任务池操作的线程安全
TimerWheel _timer_wheel; // 定时器模块
public:
// 执行任务池中的所有任务
void RunAllTask()
{
std::vector<Functor> functor;
{
std::unique_lock<std::mutex> _lock(_mutex);
_tasks.swap(functor); // 这里要交换的原因是因为提高效率,如果直接遍历,那么在执行的时候被锁管着,
// 这时如果别的线程想添加新任务,就必须先等你执行完才能添加,不利于高并发。然而交换只是交换两个指针,速度极快
} // 到这里锁就释放了
// 执行这里就没有锁了
for (auto &f : functor)
{
f();
}
return;
}
// 创建一个event文件描述符
static int CreateEventFd()
{
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd < 0)
{
ERR_LOG("CREATE EVENTFD FAILED!!");
abort(); // 让程序异常退出
}
return efd;
}
// 读数据 → 清空事件,防止重复触发
void ReadEventfd()
{
uint64_t res = 0;
int ret = read(_event_fd, &res, sizeof(res));
if (ret < 0)
{
// EINTR -- 被信号打断; EAGAIN -- 表示无数据可读
if (errno == EINTR || errno == EAGAIN)
{
return;
}
ERR_LOG("READ EVENTFD FAILED!");
abort();
}
return;
}
// 唤醒epoll函数
void WeakUpEventFd()
{
uint64_t val = 1;
int ret = write(_event_fd, &val, sizeof(val));
if (ret < 0)
{
if (errno == EINTR)
{
return;
}
ERR_LOG("READ EVENTFD FAILED!");
abort();
}
return;
}
public:
EventLoop() : _thread_id(std::this_thread::get_id()),
_event_fd(CreateEventFd()),
_event_channel(new Channel(this, _event_fd)),
_timer_wheel(this)
{
// 给eventfd添加可读事件回调函数,读取eventfd事件通知次数
_event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
// 启动eventfd的读事件监控
_event_channel->EnableRead();
}
// 三步走--事件监控-》就绪事件处理-》执行任务
void Start()
{
while (1)
{
// 1. 事件监控,
std::vector<Channel *> actives;
_poller.Poll(&actives);
// 2. 事件处理。
for (auto &channel : actives)
{
channel->HandleEvent();
}
// 3. 执行任务
RunAllTask();
}
}
// 用于判断当前线程是否是EventLoop对应的线程;
bool IsInLoop()
{
return (_thread_id == std::this_thread::get_id());
}
// 检查当前执行代码的线程,是不是这个 EventLoop 所属的线程。
void AssertInLoop()
{
assert(_thread_id == std::this_thread::get_id());
}
// 判断将要执行的任务是否处于当前线程中,如果是则执行,不是则压入队列。
void RunInLoop(const Functor &cb)
{
if (IsInLoop())
{
return cb();
}
return QueueInLoop(cb);
}
// 将操作压入任务池
void QueueInLoop(const Functor &cb)
{
{
std::unique_lock<std::mutex> _lock(_mutex);
_tasks.push_back(cb);
}
// 唤醒有可能因为没有事件就绪,而导致的epoll阻塞;
// 其实就是给eventfd写入一个数据,eventfd就会触发可读事件
WeakUpEventFd();
}
// 添加/修改描述符的事件监控
void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
// 移除描述符的监控
void RemoveEvent(Channel *channel) { return _poller.RemoveEvent(channel); }
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) { return _timer_wheel.TimerAdd(id, delay, cb); }
void TimerRefresh(uint64_t id) { return _timer_wheel.TimerRefresh(id); }
void TimerCancel(uint64_t id) { return _timer_wheel.TimerCancel(id); }
bool HasTimer(uint64_t id) { return _timer_wheel.HasTimer(id); }
};
TimerWheel 模块:时间轮与智能指针的生死哲学
功能与运行逻辑深度解析
TimerWheel 用于高效清理非活跃连接。借助 Linux 的 timerfd 秒级驱动,它实现了一个无锁的 O(1) 定时任务分发器。其运行逻辑非常巧妙:_wheel(时间轮盘)的每个格子存放 shared_ptr 来管理任务对象的生死;而 _timers(索引表)存放 weak_ptr 仅用于按 ID 查找。当秒针走到某一格并调用 clear() 时,强引用计数归零触发 TimerTask 的析构函数,我们在析构函数里执行真正的清理业务。这实现了"用对象的生死来驱动任务的执行"。
难点与解决(踩坑实录)
-
强引用的死局:任务永远不析构
- 困难 :最初为了按 ID 能够找到定时任务,我在
_wheel(轮盘)和_timers(索引表)里都存了shared_ptr。结果导致时间轮清空格子时,引用计数因为索引表的存在无法归零,任务永远不死,既不执行回调也不释放内存,造成了严重的泄漏。 - 解决 :采用一强一弱的设计。
_wheel存shared_ptr控制生命周期;_timers改存weak_ptr。这样时间轮清空时,强引用归零,对象顺利析构,而weak_ptr会自动失效,不影响内存回收。
- 困难 :最初为了按 ID 能够找到定时任务,我在
-
解决"误杀"与"漏执行":timerfd 的累加特性
- 困难 :如果服务器因为处理其他高并发事件卡了 3 秒,
timerfd在这期间其实已经超时了 3 次。如果只走 1 步秒针,那么剩下 2 步里的定时任务就会被永远错过(漏执行)。 - 解决 :在
OnTime回调中,read(_timerfd)读出来的是从上一次读取后累积的超时次数。必须用一个for循环连续执行多次RunTimerTask(),把欠下的秒针步数全部补齐,确保任务不被遗漏。
- 困难 :如果服务器因为处理其他高并发事件卡了 3 秒,
-
刷新定时器的"幽灵指针"风险
- 困难 :客户端发来数据需要刷新定时器时,我们是从
_timers里通过weak_ptr.lock()拿到对象。但如果该任务刚好在上一秒被轮盘扫过并析构了,lock()会返回空指针。如果直接访问空指针,直接崩溃。 - 解决 :在
TimerRefreshInLoop中,先调用lock()得到强指针后,必须判空(if (!pt) return;),确保持有有效对象后再进行计算和重新加入轮盘。
- 困难 :客户端发来数据需要刷新定时器时,我们是从
-
跨线程操作的安全隐患
- 困难 :
_wheel和_timers的增删改查,很容易在业务线程(如发数据时刷新)和 IO 线程(时间轮触发)中同时发生,直接引发数据竞争。 - 解决 :所有的操作接口(
TimerAdd、TimerRefresh、TimerCancel)对外统一封装,在内部全部通过_loop->RunInLoop投递到该EventLoop绑定的 IO 线程中串行执行,彻底消灭了锁的开销。
- 困难 :
代码展示
cpp
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:
uint64_t _id; // 定时器任务对象ID
uint32_t _timeout; // 定时任务的超时时间
bool _canceled; // false-表示没有被取消, true-表示被取消
TaskFunc _task_cb; // 定时器对象要执行的定时任务
ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:
TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) : _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
~TimerTask()
{
if (_canceled == false)
_task_cb();
_release();
}
void Cancel() { _canceled = true; }
void SetRelease(const ReleaseFunc &cb) { _release = cb; }
uint32_t DelayTime() { return _timeout; }
};
class TimerWheel
{
private:
using WeakTask = std::weak_ptr<TimerTask>;
using PtrTask = std::shared_ptr<TimerTask>;
int _tick; // 当前的秒针,走到哪里释放哪里,释放哪里,就相当于执行哪里的任务
int _capacity; // 表盘最大数量---其实就是最大延迟时间
std::vector<std::vector<PtrTask>> _wheel;
std::unordered_map<uint64_t, WeakTask> _timers;
EventLoop *_loop;
int _timerfd; // 定时器描述符--可读事件回调就是读取计数器,执行定时任务
std::unique_ptr<Channel> _timer_channel;
private:
void RemoveTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it != _timers.end())
{
_timers.erase(it);
}
}
static int CreateTimerfd()
{
int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (timerfd < 0)
{
ERR_LOG("TIMERFD CREATE FAILED!");
abort();
}
// int timerfd_settime(int fd, int flags, struct itimerspec *new, struct itimerspec *old);
struct itimerspec itime;
itime.it_value.tv_sec = 1;
itime.it_value.tv_nsec = 0; // 第一次超时时间为1s后
itime.it_interval.tv_sec = 1;
itime.it_interval.tv_nsec = 0; // 第一次超时后,每次超时的间隔时
timerfd_settime(timerfd, 0, &itime, NULL);
return timerfd;
}
int ReadTimefd()
{
uint64_t times;
// 有可能因为其他描述符的事件处理花费事件比较长,然后在处理定时器描述符事件的时候,有可能就已经超时了很多次
// read读取到的数据times就是从上一次read之后超时的次数
int ret = read(_timerfd, ×, 8);
if (ret < 0)
{
ERR_LOG("READ TIMEFD FAILED!");
abort();
}
return times;
}
// 这个函数应该每秒钟被执行一次,相当于秒针向后走了一步
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear(); // 清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉
}
void OnTime()
{
// 根据实际超时的次数,执行对应的超时任务
int times = ReadTimefd();
for (int i = 0; i < times; i++)
{
RunTimerTask();
}
}
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
PtrTask pt(new TimerTask(id, delay, cb));
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
void TimerRefreshInLoop(uint64_t id)
{
// 通过保存的定时器对象的weak_ptr构造一个shared_ptr出来,添加到轮子中
auto it = _timers.find(id);
if (it == _timers.end())
{
return; // 没找着定时任务,没法刷新,没法延迟
}
PtrTask pt = it->second.lock(); // lock获取weak_ptr管理的对象对应的shared_ptr
int delay = pt->DelayTime();
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
}
void TimerCancelInLoop(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
return; // 没找着定时任务,没法刷新,没法延迟
}
PtrTask pt = it->second.lock();
if (pt)
pt->Cancel();
}
public:
TimerWheel(EventLoop *loop) : _capacity(60), _tick(0), _wheel(_capacity), _loop(loop),
_timerfd(CreateTimerfd()), _timer_channel(new Channel(_loop, _timerfd))
{
_timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
_timer_channel->EnableRead(); // 启动读事件监控
}
/*定时器中有个_timers成员,定时器信息的操作有可能在多线程中进行,因此需要考虑线程安全问题*/
/*如果不想加锁,那就把对定期的所有操作,都放到一个线程中进行*/
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
// 刷新/延迟定时任务
void TimerRefresh(uint64_t id);
void TimerCancel(uint64_t id);
/*这个接口存在线程安全问题--这个接口实际上不能被外界使用者调用,只能在模块内,在对应的EventLoop线程内执行*/
bool HasTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
return false;
}
return true;
}
};
第三部分:TCP 服务与并发层(主从架构落地)
这个部分包括四个模块,我会按照:**主线程接客(Acceptor) -> 并发分配(ThreadPool) -> 分配后的连接管理(Connection) -> 总装配(TcpServer)**来介绍一下。但是为了照顾上下文的衔接,我们还需要一个Any通用类。
Acceptor 模块:主线程的专属迎宾员
功能与运行逻辑深度解析
Acceptor 内部封装了监听套接字和 Channel,只负责 accept 新连接,拿到 newfd 后通过回调抛给 TcpServer,自身完全不参与数据的收发。为了保证时序安全,它不能在构造时就开启读监控,必须在设置好接客回调后,由外部显式调用 Listen()。
难点与解决(踩坑实录)
-
构造函数中直接监听导致的连接泄漏
- 困难:如果在构造函数中直接监听,一旦有新连接,上层可能还没设置回调,导致连接无人处理、资源泄漏。
- 解决 :将"初始化"与"启动"分离,外部设置好回调后再调用
Listen()。
代码展示
cpp
class Acceptor
{
private:
Socket _socket; // 用于创建监听套接字
EventLoop *_loop; // 用于对监听套接字进行事件监控
Channel _channel; // 用于对监听套接字进行事件管理
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback;
private:
/*监听套接字的读事件回调处理函数---获取新连接,调用_accept_callback函数进行新连接处理*/
void HandleRead()
{
int newfd = _socket.Accept();
if (newfd < 0)
{
return;
}
if (_accept_callback)
_accept_callback(newfd);
}
int CreateServer(int port)
{
bool ret = _socket.CreateServer(port);
assert(ret == true);
return _socket.Fd();
}
public:
/*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*/
/*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新连接得不到处理,且资源泄漏*/
Acceptor(EventLoop *loop, int port) : _socket(CreateServer(port)), _loop(loop),
_channel(loop, _socket.Fd())
{
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
void SetAcceptCallback(const AcceptCallback &cb) { _accept_callback = cb; }
void Listen() { _channel.EnableRead(); }
};
LoopThread & LoopThreadPool 模块:多线程并发兵工厂
功能与运行逻辑深度解析
这两个类是"One Loop Per Thread"核心架构的物理落地基石。LoopThread 负责封装一个独立的物理子线程,并强制在该子线程内部实例化一个 EventLoop 对象。由于子线程创建后需要时间初始化,主线程通过条件变量(std::condition_variable)阻塞等待,直到子线程将 EventLoop 实例化完毕并发出通知,主线程才能安全地获取该指针。LoopThreadPool 则是这些子线程的管理池。当主线程(TcpServer)接客拿到新连接时,通过轮询(Round-Robin)策略,从池中均匀地取出一个子 EventLoop 分配给新连接,从而实现子线程之间的负载均衡与多核并发。
难点与解决(踩坑实录)
-
EventLoop 归属权与初始化的"时序竞争"
- 困难 :
EventLoop必须绑定在正确的子线程上(记录正确的_thread_id),因此绝不能在主线程new出来再传进去。但如果在主线程创建完LoopThread后立刻调GetLoop()获取指针,子线程可能还没来得及把EventLoop实例化出来,导致主线程拿到空指针直接段错误。 - 解决 :引入
std::mutex和std::condition_variable。在GetLoop中,如果发现_loop == NULL,主线程就阻塞等待;子线程在ThreadEntry中实例化好EventLoop后,立刻赋值给_loop并调用_cond.notify_all()唤醒主线程。确保拿到的指针绝对安全有效。
- 困难 :
-
局部变量的"栈帧延寿"魔法
- 困难 :在
ThreadEntry函数里定义的是局部变量EventLoop loop;,按照 C++ 规则,函数一旦执行完退出,局部变量就会被析构。那主线程拿到的指针岂不是变成了野指针? - 解决 :这是利用了栈帧延寿的机制。实例化完成后,子线程紧接着调用了
loop.Start(),进入了死循环。只要子线程还在运行,ThreadEntry函数就永远不会返回,栈帧就不会弹出,这个局部的EventLoop对象就能安然存活,直到整个服务器进程结束。
- 困难 :在
-
灵活的单线程降级与负载均衡
- 困难:如果用户的服务器配置很低,只想用单线程,或者用户设置了线程数为 0,代码会不会发生除零异常?
- 解决 :在
LoopThreadPool::NextLoop()中加入了防御性判断。如果_thread_count == 0,直接降级返回主线程的_baseloop;否则,通过_next_idx = (_next_idx + 1) % _thread_count取模算法,完美实现了连接的均匀轮询分配。
代码展示
cpp
class LoopThread
{
private:
/*用于实现_loop获取的同步关系,避免线程创建了,但是_loop还没有实例化之前去获取_loop*/
std::mutex _mutex; // 互斥锁
std::condition_variable _cond; // 条件变量
EventLoop *_loop; // EventLoop指针变量,这个对象需要在线程内实例化
std::thread _thread; // EventLoop对应的线程
private:
/*实例化 EventLoop 对象,唤醒_cond上有可能阻塞的线程,并且开始运行EventLoop模块的功能*/
void ThreadEntry()
{
EventLoop loop;
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁
_loop = &loop;
_cond.notify_all(); // 唤醒有可能阻塞的线程
}
loop.Start();
}
public:
/*创建线程,设定线程入口函数*/
LoopThread() : _loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
/*返回当前线程关联的EventLoop对象指针*/
EventLoop *GetLoop()
{
EventLoop *loop = NULL;
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁
_cond.wait(lock, [&]()
{ return _loop != NULL; }); // loop为NULL就一直阻塞
loop = _loop;
}
return loop;
}
};
class LoopThreadPool
{
private:
int _thread_count;
int _next_idx;
EventLoop *_baseloop;
std::vector<LoopThread *> _threads;
std::vector<EventLoop *> _loops;
public:
LoopThreadPool(EventLoop *baseloop) : _thread_count(0), _next_idx(0), _baseloop(baseloop) {}
void SetThreadCount(int count) { _thread_count = count; }
void Create()
{
if (_thread_count > 0)
{
_threads.resize(_thread_count);
_loops.resize(_thread_count);
for (int i = 0; i < _thread_count; i++)
{
_threads[i] = new LoopThread();
_loops[i] = _threads[i]->GetLoop();
}
}
return;
}
EventLoop *NextLoop()
{
if (_thread_count == 0)
{
return _baseloop;
}
_next_idx = (_next_idx + 1) % _thread_count;
return _loops[_next_idx];
}
};
Connection 模块:全生命周期的连接大管家
功能与运行逻辑深度解析
Connection 是整个网络库中最为复杂的类,它是将底层基础组件组装起来的"集大成者"。从架构分层来看,EventLoop 充当着"调度者"(负责死循环监控事件、唤醒线程),而 Connection 则是"实现者"(负责具体的状态机流转、数据收发与资源清理)。
它的运行逻辑建立在 Channel 的事件分发之上:当底层 epoll 监听到该连接的读写事件时,EventLoop 会调用 Connection 内部绑定的 Channel 回调。此时 Connection 接手,执行具体的业务:
- 建立连接 :通过
EstablishedInLoop切换状态并开启读监控。 - 接收数据 :在
HandleRead中触发底层读取,将数据塞入输入缓冲区,并唤醒业务层的解析回调。 - 发送数据 :采用三段式设计。业务线程调用
Send后,通过EventLoop::RunInLoop将数据投递给 IO 线程的SendInLoop,放入发送缓冲区并开启写监控;等底层可写时,再由HandleWrite真正推给内核。 - 断开与销毁 :通过状态机流转实现优雅关闭。所有涉及数据修改的操作,都严格依赖
EventLoop投递回专属线程执行,保证无锁并发安全。
难点与解决(踩坑实录)
-
智能指针"秒死"引发的悬空指针灾难
- 困难 :测试时,我在接客回调里直接
new了一个Connection局部对象,结果回调结束一离开作用域,智能指针引用计数瞬间归零,连接对象被当场析构。底层epoll触发事件时拿到了野指针,服务器直接 Segment Fault 崩溃。 - 解决 :在
TcpServer中引入了_conns哈希表。新连接创建后立刻存入表内,强行将引用计数保底为 1,为连接续命。并在所有异步回调中,强制使用shared_from_this()而非this。
- 困难 :测试时,我在接客回调里直接
-
致命的数据截断:优雅关闭顺序的错误
- 困难 :业务层调用断开时,如果直接
close(fd),但此时_out_buffer里还有没发完的数据,会导致客户端收到一半的报文,产生严重 Bug。 - 解决 :引入了
DISCONNECTING状态。调用ShutDown时,状态先切为待关闭,开启EPOLLOUT监听。等HandleWrite把发送缓冲区里的数据全部推给内核后,再由内部触发真正的Release()销毁。
- 困难 :业务层调用断开时,如果直接
-
跨线程销毁的"死亡顺序"
- 困难 :在
ReleaseInLoop中,如果先通知服务器从哈希表移除自己,引用计数可能会瞬间归零导致对象析构。此时如果再去执行业务层的_closed_callback,就会访问已被释放的this指针,当场崩溃。 - 解决 :严格遵循"先回调业务,后通知销毁"的顺序。先调用
_closed_callback(此时对象依然存活),最后再调用_server_closed_callback从全局表移除,确保对象在无任何代码引用时才安全析构。
- 困难 :在
-
跨线程发送数据的"悬空指针"
- 困难 :业务层在主线程调用
Send时,如果把const char* data指针直接传给RunInLoop投递,一旦原字符串被析构,子线程执行时就会拿到野指针。 - 解决 :
Send内部先将数据深拷贝到一个临时Buffer中,再将Buffer移动(std::move)给投递任务,保证数据生命周期跟着任务一起走。
- 困难 :业务层在主线程调用
-
HandleRead 的异常穿透漏洞
- 困难 :如果底层
recv返回出错(-1),代码调用了ShutDownInLoop关闭连接,但忘记写return。代码会继续往下执行,把负数的长度传给Buffer::WritePush,转化为巨大的无符号整数,直接引发内存越界崩溃。 - 解决 :在
HandleRead里加上严格判断if (ret <= 0) { if (ret < 0) ShutDownInLoop(); return; },彻底封死向下穿透的可能。
- 困难 :如果底层
代码展示
cpp
class Connection;
typedef enum
{
DISCONNECTED, // DISCONECTED -- 连接关闭状态;
CONNECTING, // CONNECTING -- 连接建立成功-待处理状态
CONNECTED, // CONNECTED -- 连接建立完成,各种设置已完成,可以通信的状态;
DISCONNECTING // DISCONNECTING -- 待关闭状态
} ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:
uint64_t _conn_id; // 连接的唯一ID,便于连接的管理和查找
// uint64_t _timer_id; //定时器ID,必须是唯一的,这块为了简化操作使用conn_id作为定时器ID
int _sockfd; // 连接关联的文件描述符
bool _enable_inactive_release; // 连接是否启动非活跃销毁的判断标志,默认为false
EventLoop *_loop; // 连接所关联的一个EventLoop
ConnStatu _statu; // 连接状态
Socket _socket; // 套接字操作管理
Channel _channel; // 连接的事件管理
Buffer _in_buffer; // 输入缓冲区---存放从socket中读取到的数据
Buffer _out_buffer; // 输出缓冲区---存放要发送给对端的数据
Any _context; // 请求的接收处理上下文
/*这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)*/
/*换句话说,这几个回调都是组件使用者使用的*/
using ConnectedCallback = std::function<void(const PtrConnection &)>;
using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;
using ClosedCallback = std::function<void(const PtrConnection &)>;
using AnyEventCallback = std::function<void(const PtrConnection &)>;
ConnectedCallback _connected_callback; // 代表:客户端连接成功时,要做什么事
MessageCallback _message_callback; // 收到客户端消息时,要做什么事
ClosedCallback _closed_callback; // 代表:客户端断开连接时,要做什么事
AnyEventCallback _event_callback; // 代表:连接有任何动静时,要做什么事
/*组件内的连接关闭回调--组件内设置的,因为服务器组件内会把所有的连接管理起来,一旦某个连接要关闭*/
/*就应该从管理的地方移除掉自己的信息*/
ClosedCallback _server_closed_callback;
private:
/*五个channel的事件回调函数*/
// 描述符可读事件触发后调用的函数,接收socket数据放到接收缓冲区中,然后调用_message_callback
void HandleRead() // 收到消息,交给业务层
{
// 1. 接收socket的数据,放到缓冲区
char buf[65536];
ssize_t ret = _socket.NonBlockRecv(buf, 65535);
if (ret < 0)
{
// 出错了,不能直接关闭连接
return ShutdownInLoop();
}
// 这里的等于0表示的是没有读取到数据,而并不是连接断开了,连接断开返回的是-1
// 将数据放入输入缓冲区,写入之后顺便将写偏移向后移动
_in_buffer.WriteAndPush(buf, ret);
// 2. 调用message_callback进行业务处理
if (_in_buffer.ReadAbleSize() > 0)
{
// shared_from_this--从当前对象自身获取自身的shared_ptr管理对象
return _message_callback(shared_from_this(), &_in_buffer);
}
}
// 描述符可写事件触发后调用的函数,将发送缓冲区中的数据进行发送
void HandleWrite()
{
//_out_buffer中保存的数据就是要发送的数据
ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
if (ret < 0)
{
// 发送错误就该关闭连接了,
if (_in_buffer.ReadAbleSize() > 0)
{
_message_callback(shared_from_this(), &_in_buffer); // shared_from_this(): 从对象自己身上,获取一个管理自己的 shared_ptr 智能指针
}
return Release(); // 这时候就是实际的关闭释放操作了。
}
_out_buffer.MoveReadOffset(ret); // 千万不要忘了,将读偏移向后移动
if (_out_buffer.ReadAbleSize() == 0)
{
_channel.DisableWrite(); // 没有数据待发送了,关闭写事件监控
// 如果当前是连接待关闭状态,则有数据,发送完数据释放连接,没有数据则直接释放
if (_statu == DISCONNECTING)
{
return Release();
}
}
return;
}
// 描述符触发挂断事件
void HandleClose()
{
/*一旦连接挂断了,套接字就什么都干不了了,因此有数据待处理就处理一下,完毕关闭连接*/
if (_in_buffer.ReadAbleSize() > 0)
{
_message_callback(shared_from_this(), &_in_buffer);
}
return Release();
}
// 描述符触发出错事件
void HandleError()
{
return HandleClose();
}
// 描述符触发任意事件: 1. 刷新连接的活跃度--延迟定时销毁任务; 2. 调用组件使用者的任意事件回调
void HandleEvent()
{
if (_enable_inactive_release == true)
{
_loop->TimerRefresh(_conn_id);
}
if (_event_callback)
{
_event_callback(shared_from_this());
}
}
// 连接获取之后,所处的状态下要进行各种设置(启动读监控,调用回调函数)
void EstablishedInLoop()
{
// 1. 修改连接状态; 2. 启动读事件监控; 3. 调用回调函数
assert(_statu == CONNECTING); // 当前的状态必须一定是上层的半连接状态
_statu = CONNECTED; // 当前函数执行完毕,则连接进入已完成连接状态
// 一旦启动读事件监控就有可能会立即触发读事件,如果这时候启动了非活跃连接销毁,那么连接建立后,可能马上就被销毁了因为读事件处理里会调 HandleEvent,HandleEvent 会刷新定时器,但读事件处理完可能就释放连接了。
_channel.EnableRead();
if (_connected_callback) // 客户端连好了,可以通信了
_connected_callback(shared_from_this());
}
// 这个接口才是实际的释放接口
void ReleaseInLoop()
{
// 1. 修改连接状态,将其置为DISCONNECTED
_statu = DISCONNECTED;
// 2. 移除连接的事件监控
_channel.Remove();
// 3. 关闭描述符
_socket.Close();
// 4. 如果当前定时器队列中还有定时销毁任务,则取消任务
if (_loop->HasTimer(_conn_id))
CancelInactiveReleaseInLoop();
// 5. 调用关闭回调函数,避免先移除服务器管理的连接信息导致Connection被释放,再去处理会出错,因此先调用用户的回调函数
if (_closed_callback)
_closed_callback(shared_from_this());
// 移除服务器内部管理的连接信息
if (_server_closed_callback)
_server_closed_callback(shared_from_this());
}
// 这个接口并不是实际的发送接口,而只是把数据放到了发送缓冲区,启动了可写事件监控
void SendInLoop(Buffer &buf)
{
if (_statu == DISCONNECTED)
return;
_out_buffer.WriteBufferAndPush(buf);
if (_channel.WriteAble() == false)
{
_channel.EnableWrite();
}
}
// 这个关闭操作并非实际的连接释放操作,需要判断还有没有数据待处理,待发送
void ShutdownInLoop()
{
_statu = DISCONNECTING; // 设置连接为半关闭状态
if (_in_buffer.ReadAbleSize() > 0)
{
if (_message_callback)
_message_callback(shared_from_this(), &_in_buffer);
}
// 要么就是写入数据的时候出错关闭,要么就是没有待发送数据,直接关闭
if (_out_buffer.ReadAbleSize() > 0)
{
if (_channel.WriteAble() == false)
{
_channel.EnableWrite();
}
}
if (_out_buffer.ReadAbleSize() == 0)
{
Release();
}
}
// 启动非活跃连接超时释放规则
void EnableInactiveReleaseInLoop(int sec)
{
// 1. 将判断标志 _enable_inactive_release 置为true
_enable_inactive_release = true;
// 2. 如果当前定时销毁任务已经存在,那就刷新延迟一下即可
if (_loop->HasTimer(_conn_id))
{
return _loop->TimerRefresh(_conn_id);
}
// 3. 如果不存在定时销毁任务,则新增
_loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
}
void CancelInactiveReleaseInLoop()
{
_enable_inactive_release = false;
if (_loop->HasTimer(_conn_id))
{
_loop->TimerCancel(_conn_id);
}
}
void UpgradeInLoop(const Any &context,
const ConnectedCallback &conn,
const MessageCallback &msg,
const ClosedCallback &closed,
const AnyEventCallback &event)
{
_context = context;
_connected_callback = conn;
_message_callback = msg;
_closed_callback = closed;
_event_callback = event;
}
public:
Connection(EventLoop *loop, uint64_t conn_id, int sockfd) : _conn_id(conn_id), _sockfd(sockfd),
_enable_inactive_release(false), _loop(loop), _statu(CONNECTING), _socket(_sockfd),
_channel(loop, _sockfd)
{
_channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
}
~Connection() { DBG_LOG("RELEASE CONNECTION:%p", this); }
// 获取管理的文件描述符
int Fd() { return _sockfd; }
// 获取连接ID
int Id() { return _conn_id; }
// 是否处于CONNECTED状态
bool Connected() { return (_statu == CONNECTED); }
// 设置上下文--连接建立完成时进行调用
void SetContext(const Any &context) { _context = context; }
// 获取上下文,返回的是指针
Any *GetContext() { return &_context; }
void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }
void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }
void SetClosedCallback(const ClosedCallback &cb) { _closed_callback = cb; }
void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }
void SetSrvClosedCallback(const ClosedCallback &cb) { _server_closed_callback = cb; }
// 连接建立就绪后,进行channel回调设置,启动读监控,调用_connected_callback
void Established()
{
_loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
}
// 发送数据,将数据放到发送缓冲区,启动写事件监控
void Send(const char *data, size_t len)
{
// 外界传入的data,可能是个临时的空间,我们现在只是把发送操作压入了任务池,有可能并没有被立即执行
// 因此有可能执行的时候,data指向的空间有可能已经被释放了。
Buffer buf;
buf.WriteAndPush(data, len);
_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
// 提供给组件使用者的关闭接口--并不实际关闭,需要判断有没有数据待处理
void Shutdown()
{
_loop->RunInLoop(std::bind(&Connection::ShutdownInLoop, this));
}
void Release()
{
_loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, this));
}
// 启动非活跃销毁,并定义多长时间无通信就是非活跃,添加定时任务
void EnableInactiveRelease(int sec)
{
_loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
}
// 取消非活跃销毁
void CancelInactiveRelease()
{
_loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop, this));
}
// 切换协议---重置上下文以及阶段性回调处理函数 -- 而是这个接口必须在EventLoop线程中立即执行
// 防备新的事件触发后,处理的时候,切换任务还没有被执行--会导致数据使用原协议处理了。
void Upgrade(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,
const ClosedCallback &closed, const AnyEventCallback &event)
{
_loop->AssertInLoop();
_loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this, context, conn, msg, closed, event));
}
};
TcpServer 模块:主从 Reactor 的终极装配厂
功能与运行逻辑深度解析
TcpServer 是对外提供的最高层封装,是整个服务器程序的"大脑"。从架构职责来看,它统筹了主线程的 EventLoop(负责监听和分配)、Acceptor(负责接客)、LoopThreadPool(提供 IO 子线程),以及一个全局的哈希表 _conns(负责托管所有存活的 Connection 对象)。
它的运行逻辑是典型的主从协作:主线程的 EventLoop 驱动 Acceptor 接收新连接,拿到 newfd 后,TcpServer 通过轮询算法从线程池中选取一个子线程 EventLoop,为其创建 Connection 并注册业务回调,随后通过 eventfd 唤醒子线程接管该连接。当连接断开时,TcpServer 接收底层通知,安全地将该连接从全局哈希表中移除,完成资源回收。
难点与解决(踩坑实录)
-
跨线程删除哈希表引发的"并发血案"
- 困难 :当客户端主动断开时,子线程(IO 线程)内部的
Connection会触发关闭逻辑。如果直接在子线程里操作主线程维护的_conns哈希表(执行erase),由于unordered_map不是线程安全的,这会导致严重的内存数据踩踏,服务器随机崩溃。 - 解决 :利用主线程
_baseloop的RunInLoop机制。将"从哈希表移除该连接"这个动作打包成任务,通过eventfd抛回给主线程去安全地串行执行,完美杜绝了跨线程数据竞争。
- 困难 :当客户端主动断开时,子线程(IO 线程)内部的
-
惊险的"生命周期交接"
- 困难 :在
NewConnection中,我们创建了Connection的shared_ptr并加入_conns。如果中途代码抛出异常,或者忘了把指针存表,这个新创建的连接立刻就会被析构,导致底层的Channel悬空。 - 解决 :严格遵守顺序:创建对象 -> 设置各种回调 -> 存入
_conns续命 -> 最后调用Established启动监控。先确保对象被安全托管,再启动可能触发回调的底层事件。
- 困难 :在
-
文件描述符(fd)复用导致的"幽灵误杀"
- 困难 :Linux 系统会复用已经被
close的fd编号。如果一个老连接断开,它的定时清理任务还没结束,而此时内核刚好把同一个fd号分配给了新连接。老任务的超时触发时,如果按fd去查表,就会把无辜的新连接给误杀踢下线。 - 解决 :绝对不能使用
fd作为连接的唯一标识。为每个Connection分配一个单调递增且全局唯一的_next_id。不管是插入哈希表还是注册定时器,全部基于_next_id处理,彻底隔离了底层fd的复用隐患。
- 困难 :Linux 系统会复用已经被
-
非活跃连接销毁的全局开关
- 困难:服务器上有些是短连接(请求完立刻断),有些是长连接。如果一律开启超时踢人,短连接会频繁触发定时器的增删;如果全不开启,长连接又会变成僵尸连接拖垮内存。
- 解决 :在
TcpServer中提供EnableInactiveRelease(timeout)全局开关。只有当用户显式开启时,NewConnection才会为新连接绑定时间轮的定时清理任务。这种做法既保证了灵活性,又避免了不必要的性能开销。
代码展示
cpp
class TcpServer
{
private:
uint64_t _next_id; // 这是一个自动增长的连接ID,
int _port;
int _timeout; // 这是非活跃连接的统计时间---多长时间无通信就是非活跃连接
bool _enable_inactive_release; // 是否启动了非活跃连接超时销毁的判断标志
EventLoop _baseloop; // 这是主线程的EventLoop对象,负责监听事件的处理
Acceptor _acceptor; // 这是监听套接字的管理对象
LoopThreadPool _pool; // 这是从属EventLoop线程池
std::unordered_map<uint64_t, PtrConnection> _conns; // 保存管理所有连接对应的shared_ptr对象
using ConnectedCallback = std::function<void(const PtrConnection &)>;
using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;
using ClosedCallback = std::function<void(const PtrConnection &)>;
using AnyEventCallback = std::function<void(const PtrConnection &)>;
using Functor = std::function<void()>;
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
private:
void RunAfterInLoop(const Functor &task, int delay)
{
_next_id++;
_baseloop.TimerAdd(_next_id, delay, task);
}
// 为新连接构造一个Connection进行管理
void NewConnection(int fd)
{
_next_id++;
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_event_callback);
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
if (_enable_inactive_release)
conn->EnableInactiveRelease(_timeout); // 启动非活跃超时销毁
conn->Established(); // 就绪初始化
_conns.insert(std::make_pair(_next_id, conn));
}
void RemoveConnectionInLoop(const PtrConnection &conn)
{
int id = conn->Id();
auto it = _conns.find(id);
if (it != _conns.end())
{
_conns.erase(it);
}
}
// 从管理Connection的_conns中移除连接信息
void RemoveConnection(const PtrConnection &conn)
{
_baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));
}
public:
TcpServer(int port) : _port(port),
_next_id(0),
_enable_inactive_release(false),
_acceptor(&_baseloop, port),
_pool(&_baseloop)
{
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen(); // 将监听套接字挂到baseloop上
}
void SetThreadCount(int count) { return _pool.SetThreadCount(count); }
void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }
void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }
void SetClosedCallback(const ClosedCallback &cb) { _closed_callback = cb; }
void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }
void EnableInactiveRelease(int timeout)
{
_timeout = timeout;
_enable_inactive_release = true;
}
// 用于添加一个定时任务
void RunAfter(const Functor &task, int delay)
{
_baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
}
void Start()
{
_pool.Create();
_baseloop.Start();
}
};
void Channel::Remove() { return _loop->RemoveEvent(this); }
void Channel::Update() { return _loop->UpdateEvent(this); }
void TimerWheel::TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
_loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
}
// 刷新/延迟定时任务
void TimerWheel::TimerRefresh(uint64_t id)
{
_loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
}
void TimerWheel::TimerCancel(uint64_t id)
{
_loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));
}
Any 模块:连接的业务上下文容器
功能与运行逻辑深度解析
Any 是一个基于类型擦除(Type Erasure)技术实现的通用容器。在网络库中,Connection 作为底层连接,并不应该知道上层用的到底是 HTTP、WebSocket 还是自定义协议。为了让 Connection 能够存储各种未知的业务上下文(比如 HTTP 解析状态机 HttpContext),我们引入了 Any。
它的运行逻辑是通过多态的基类指针来管理任意类型的数据。在 Connection 建立时,业务层可以通过 conn->SetContext(Any(HttpContext())) 将状态机挂载到连接上;后续收到数据时,再通过 conn->GetContext()->get<HttpContext>() 取出状态机继续解析。这彻底切断了底层网络模块对上层业务模块的编译依赖。
难点与解决(踩坑实录)
-
虚析构函数缺失引发的内存泄漏
- 困难 :
Any内部是通过基类指针holder*来指向模板子类placeholder<T>的。如果基类holder没有声明虚析构函数,当Any对象销毁、执行delete _content时,只会调用基类的析构,子类对象中实际保存的数据完全得不到释放,导致严重内存泄漏。 - 解决 :在基类
holder中必须声明纯虚析构函数virtual ~holder() {},确保多态删除时,子类的析构函数能被正确调用。
- 困难 :
-
深拷贝导致的"双重释放"
- 困难 :如果使用编译器默认生成的拷贝构造函数,两个
Any对象会指向同一个holder底层内存。当其中一个对象析构时释放了内存,另一个对象再去访问时就会变成野指针,甚至引发 Double Free 崩溃。 - 解决 :在
Any的拷贝构造中,利用虚函数clone()实现深拷贝,让新的Any对象持有独立的内存副本。同时配合拷贝交换惯用法(Copy-and-Swap)重载赋值运算符,保证异常安全。
- 困难 :如果使用编译器默认生成的拷贝构造函数,两个
代码展示
cpp
class Any
{
private:
class holder
{
public:
virtual ~holder() {}
virtual const std::type_info &type() = 0;
virtual holder *clone() = 0;
};
template <class T>
class placeholder : public holder
{
public:
placeholder(const T &val) : _val(val) {}
// 获取子类对象保存的数据类型
virtual const std::type_info &type() { return typeid(T); }
// 针对当前的对象自身,克隆出一个新的子类对象
virtual holder *clone() { return new placeholder(_val); }
public:
T _val;
};
holder *_content;
public:
Any() : _content(NULL) {}
template <class T>
Any(const T &val) : _content(new placeholder<T>(val)) {}
Any(const Any &other) : _content(other._content ? other._content->clone() : NULL) {}
~Any() { delete _content; }
Any &swap(Any &other)
{
std::swap(_content, other._content);
return *this;
}
// 返回子类对象保存的数据的指针
template <class T>
T *get()
{
// 想要获取的数据类型,必须和保存的数据类型一致
assert(typeid(T) == _content->type());
return &((placeholder<T> *)_content)->_val;
}
// 赋值运算符的重载函数
template <class T>
Any &operator=(const T &val)
{
// 为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时对象释放的时候,原先保存的数据也就被释放
Any(val).swap(*this);
return *this;
}
Any &operator=(const Any &other)
{
Any(other).swap(*this);
return *this;
}
};


http协议部分
这一部分相比上面的 server.hpp 部分要简单一些,本质上就是一个字符串处理的过程,但其中仍然有一些值得琢磨的难点。下面我从两个方面来展示 Http.hpp 这个模块的功能与设计思想。
核心四大模块设计思想与架构拆解
第一部分:应用层与网络层的解耦(架构基石)
这部分决定了我们的 HTTP 服务器能否毫无违和感地插拔在底层 Reactor 模型之上,也决定了服务器的扩展性上限。
-
Any 容器与上下文解耦(Context 机制)
- 设计思想 :底层
TcpServer的职责仅仅是处理字节流的收发(TCP 层),绝不能让它认识HttpRequest这种应用层结构。如果底层绑死了 HTTP 协议,那以后想支持 WebSocket 或自定义 RPC 协议时,底层代码将完全作废。我们利用一个泛型容器Any,将解析状态机HttpContext作为"上下文"绑定到每一个Connection上。底层只负责把收到的数据扔给_message_callback,完全实现了网络层与业务层的解耦。 - 工程权衡 :使用
Any会引入类型擦除(Type Erasure)带来的微小虚函数开销(通过基类指针管理子类),但换来的是底层网络库的绝对纯粹,这在架构设计上是极为划算的。
- 设计思想 :底层
-
HttpRequest / HttpResponse(数据载体)
- 设置思想:极其纯粹的数据"麻袋"。把 HTTP 请求里的请求行(方法、路径)、请求头、URL 参数和正文,全部老老实实地装进几个简单的变量里。这个类自己不干任何复杂的活儿(不负责解析,也不负责发网络包),只负责把数据整齐地打包好,当成一个"干净的数据包裹"递给后面负责分发的路由器去处理。
- 工程权衡 :为了提升业务开发的便捷性,在对象内部直接集成了如
SetRedirect(重定向)、ContentLength()(内容长度解析)等便捷方法,将业务层的复杂判断下推到应用层模块内部。
第二部分:流式数据的边界界定(状态机心法)
HTTP 协议是基于行的文本协议,但在 TCP 流式传输中,没有"行"的概念,只有连篇累牍的 char。如何应对半包和粘包是最大的难点。
-
HttpContext 模块:有限状态机(FSM)
- 设计思想 :彻底摒弃"一次性收完再解析"或"阻塞等待"的做法。采用状态机驱动,状态分为:
RECV_HTTP_LINE(解析请求行) ->RECV_HTTP_HEAD(解析请求头) ->RECV_HTTP_BODY(接收请求体) ->RECV_HTTP_OVER(解析完成)。 - 技术难点与解决(半包与粘包的抗性) :
- 应对半包(数据不全) :按行读取时,如果找不到
\r\n或\n,直接返回,状态机停留在当前阶段,等待下一次底层可读事件触发。为了防止恶意构造超长行导致内存耗尽,加入了MAX_LINE长度校验,一旦超过立刻返回 414 状态码。 - 应对粘包(数据过多) :在
HttpServer::OnMessage中,使用while (buffer->ReadAbleSize() > 0)循环驱动状态机。当解析完一个完整的请求后,重置状态机,继续解析缓冲区里剩下的数据,直至数据耗尽。
- 应对半包(数据不全) :按行读取时,如果找不到
- 设计思想 :彻底摒弃"一次性收完再解析"或"阻塞等待"的做法。采用状态机驱动,状态分为:
-
Buffer 扩展模块(GetLineAndPop)
- 设计思想 :为了配合 HTTP 按行解析,在底层
Buffer基础上封装了GetLineAndPop。通过memchr快速定位换行符,提取一整行数据,并将_reader_idx向前移动。 - 工程权衡 :
memchr是非常底层的 C 库函数,通常经过编译器高度优化,比传统的std::string::find性能更好,适合高并发网络场景下的字符串检索。
- 设计思想 :为了配合 HTTP 按行解析,在底层
第三部分:动静分离与路由分发(业务调度中心)
不是所有的请求都需要经过复杂的业务逻辑,路由层的性能直接决定了服务器的高并发上限。
-
静态资源处理(FileHandler)
- 设计思想 :对于 HTML、CSS、JS、图片等静态文件,直接通过文件 I/O 读取,并利用
Util::ExMime映射对应的Content-Type。 - 技术难点与解决(防范路径穿越攻击) :必须防范"路径穿越攻击"(Directory Traversal)。如果客户端恶意请求
/../../etc/passwd,直接拼接基础目录会导致严重的安全漏洞。我们在Util::ValidPath中,按/分割请求路径,维护一个目录深度变量level,遇到..就让level--,一旦level < 0代表试图越出根目录,立即拦截。
- 设计思想 :对于 HTML、CSS、JS、图片等静态文件,直接通过文件 I/O 读取,并利用
-
正则路由表(Dispatcher)
- 设计思想 :功能性请求被放入以请求方法(GET/POST/PUT/DELETE)分类的路由表中。路由表本质是一个
pair<std::regex, Handler>的数组。当请求到来时,遍历该方法的数组,利用std::regex_match进行匹配。 - 工程权衡(正则 vs 字符串匹配) :虽然
unordered_map做精确字符串匹配是 O(1),但无法处理动态路由。我们采用了std::regex向量表,虽然遍历匹配的复杂度是 O(N),但由于真实业务中同一方法下的路由数量通常很少,这种以极小的微秒级匹配开销换取极高开发灵活性(支持类似/numbers/(\d+)的参数提取)的权衡,是非常划算的。
- 设计思想 :功能性请求被放入以请求方法(GET/POST/PUT/DELETE)分类的路由表中。路由表本质是一个
第四部分:响应组装与长连接博弈
HTTP 是"请求-响应"模型,如何高效地返回数据并管理 TCP 连接,是应用层的最后一环。
-
WriteResponse 响应序列化
- 设计思想 :必须严格按照 HTTP 报文格式:
[状态行] + [响应头] + [空行] + [正文]进行序列化。业务层只负责填充HttpResponse的字段,序列化工作对业务层完全透明。 - 工程权衡 :为了避免多次系统调用,序列化时使用
std::stringstream拼装整个报文后,一次性通过底层的conn->Send()投递到发送缓冲区,极大地减少了网络 I/O 次数。
- 设计思想 :必须严格按照 HTTP 报文格式:
-
长短连接的博弈(Keep-Alive vs Close)
- 设计思想 :根据请求头中的
Connection字段决定 TCP 的生死。如果是keep-alive,在响应头中写回Connection: keep-alive,并保持底层 Socket 的开启,实现连接的复用。 - 架构联动(极度重要) :如果是长连接,此时不能立刻释放,而是交由底层的
TimerWheel定时器模块进行管理。如果客户端在设定时间(如 10s)内没有发起新请求,底层时间轮将无情触发Release回调,回收连接,完美防止了连接泄漏。这体现了应用层与底层基础设施的完美联动。
- 设计思想 :根据请求头中的
核心模块全景解析与源码展示
这一部分主要包括 HttpRequest / HttpResponse、HttpContext、HttpServer 以及 Util 四大核心模块。底层通信基建(Buffer / Socket / EventLoop)已在 Server 篇详细剖析,本篇聚焦 HTTP 应用层的数据流转与业务调度。
1. HttpRequest / HttpResponse 模块:双向交互的数据载体
功能与运行逻辑深度解析
这两个类是 HTTP 通信的"集装箱",属于纯粹的 POJO(Plain Old C++ Object)数据结构。HttpRequest 内部使用 std::unordered_map 存储 Header 和 URL 查询参数(Params),并提供极简的存取接口(如 GetHeader、HasParam),同时内置了 Close() 方法用于判断客户端是否要求断开连接。HttpResponse 则封装了状态码、响应头和正文,并提供了 SetContent 和 SetRedirect 等便捷方法。业务开发者只需像填表一样读写这两个对象即可,无需关心底层。
难点与解决(踩坑实录)
-
302 重定向的响应格式不标准
- 困难 :实现重定向(302)时,如果直接修改状态码而不做特殊处理,浏览器端往往会因为缺少
Location头或者响应格式不标准而解析失败。 - 解决 :在
HttpResponse中专门封装了SetRedirect(url)方法,不仅修改_statu为 302,同时将_redirect_flag置为true,后续在WriteResponse组装报文时,会自动补全Location: url头部。
- 困难 :实现重定向(302)时,如果直接修改状态码而不做特殊处理,浏览器端往往会因为缺少
-
缺失 Content-Length 导致的异常崩溃
- 困难 :在解析客户端请求时,如果没有
Content-Length字段,直接调用std::stol转换会导致程序直接抛出异常崩溃。 - 解决 :在
ContentLength()中先调用HasHeader("Content-Length")进行判断,若不存在则直接返回 0,保证程序健壮性。
- 困难 :在解析客户端请求时,如果没有
代码展示
cpp
class HttpRequest
{
public:
std::string _method;
std::string _path;
std::string _version;
std::string _body;
std::smatch _matches;
std::unordered_map<std::string, std::string> _headers; // 头部字段
std::unordered_map<std::string, std::string> _params; // 查询字符串
public:
HttpRequest() : _version("HTTP/1.1") {}
// 清理资源
void ReSet()
{
_method.clear();
_path.clear();
_version = "HTTP/1.1";
_body.clear();
std::smatch matches;
_matches.swap(matches);
_headers.clear();
_params.clear();
}
// 设置头部字段
void SetHeader(const std::string &key, const std::string &val)
{
_headers.insert(std::make_pair(key, val));
}
// 判断是否有指定头部字段
bool HasHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return true;
}
return false;
}
// 获取头部字段
std::string GetHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return it->second;
}
return "";
}
// 添加字符串
void SetParam(const std::string &key, const std::string &val)
{
_params.insert(std::make_pair(key, val));
}
// 查看是否有指定的字符串
bool HasParam(const std::string &key) const
{
auto it = _params.find(key);
if (it != _params.end())
{
return true;
}
return false;
}
// 获取指定的字符串
std::string GetParam(const std::string &key) const
{
auto it = _params.find(key);
if (it != _params.end())
{
return it->second;
}
return "";
}
// 获取正文长度
size_t ContentLength() const
{
auto it = HasHeader("Content-Length");
if (!it)
return 0;
return std::stol(GetHeader("Content-Length"));
}
bool Close() const // 判断是否是短连接
{
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};
class HttpResponse
{
public:
int _statu; // 状态吗
bool _redirect_flag; // 是否需要重定向
std::string _body; // 响应正文
std::string _redirect_url; // 重定向到目标地址
std::unordered_map<std::string, std::string> _headers; // 相应头键值对
public:
HttpResponse(int statu) : _statu(statu), _redirect_flag(false) {}
HttpResponse() : _statu(200), _redirect_flag(false) {}
void ReSet()
{
_statu = 200;
_redirect_flag = false;
_body.clear();
_redirect_url.clear();
_headers.clear();
}
// 设置头部字段
void SetHeader(const std::string &key, const std::string &val)
{
_headers.insert(std::make_pair(key, val));
}
// 判断是否有指定头部字段
bool HasHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return true;
}
return false;
}
// 获取头部字段
std::string GetHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return it->second;
}
return "";
}
// 设置正文+自动匹配 Content-Type
void SetContent(const std::string &body, const std::string &type = "text/html")
{
_body = body;
SetHeader("Content-Type", type);
}
// 设置重定向
void SetRedirect(const std::string &url, int statu = 302)
{
_statu = statu;
_redirect_flag = true;
_redirect_url = url;
}
bool Close() const
{
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};
2. HttpContext 模块:应对半包与粘包的状态机引擎
功能与运行逻辑深度解析
这是应用层最核心的解析枢纽。由于 TCP 是流式协议,一次网络事件触发收到的数据,可能只有半个 HTTP 请求(半包),也可能包含两个完整的请求(粘包)。HttpContext 内部维护了一个状态机:RECV_HTTP_LINE(行) -> RECV_HTTP_HEAD(头) -> RECV_HTTP_BODY(体) -> RECV_HTTP_OVER(完毕)。在 RecvHttpRequest 中,通过 switch-case 驱动,巧妙利用了 C++ switch 的 fall-through 特性(不写 break),让请求行解析完后立刻顺延进入头部解析,形成无间断的流水线。
难点与解决(踩坑实录)
-
半包防御:数据不足一行时的解析与内存风险
- 困难 :当缓冲区数据不够一行(没有找到
\r\n)时,如果强行解析会出错。更危险的是,如果遭遇恶意客户端一直发送大量数据但永不发送换行符,会导致服务器缓冲区内存被无限撑爆。 - 解决 :引入
MAX_LINE(8192)宏定义限制。如果缓冲区数据不足一行且未超限,状态机直接return,保留解析现场等待下一次 socket 事件;如果超限,直接判定为非法请求(414 状态码),立刻中断解析并关闭连接。
- 困难 :当缓冲区数据不够一行(没有找到
-
粘包处理:多请求堆积与换行符兼容
- 困难 :长连接下,如果浏览器一次性发来两个请求,解析完第一个就退出,会导致第二个请求的数据丢失,页面卡死。此外,如果换行符是
\n而不是标准的\r\n,也会导致查找失败。 - 解决 :在
HttpServer::OnMessage外层加上while (buffer->ReadAbleSize() > 0)循环。只要状态机报告RECV_HTTP_OVER处理完毕,立刻ReSet()重置上下文,继续解析剩余数据。同时,在提取头部时,兼容处理末尾的\n和\r字符。
- 困难 :长连接下,如果浏览器一次性发来两个请求,解析完第一个就退出,会导致第二个请求的数据丢失,页面卡死。此外,如果换行符是
代码展示:
cpp
typedef enum
{
RECV_HTTP_ERROR, // 出错
RECV_HTTP_LINE, // 第一行解析
RECV_HTTP_HEAD, // 头部字段解析
RECV_HTTP_BODY, // 正文解析
RECV_HTTP_OVER // 解析结束
} HttpRecvStatu;
// 处理一次http请求
#define MAX_LINE 8192
class HttpContext
{
private:
int _resp_statu;
HttpRecvStatu _recv_statu;
HttpRequest _request;
private:
bool ParseHttpLine(const std::string &line)
{
std::smatch matches;
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
bool ret = std::regex_match(line, matches, e);
if (ret == false)
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
_request._method = matches[1];
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
_request._path = Uitl::UrlDecode(matches[2], false);
_request._version = matches[4];
std::vector<std::string> query_string_arry;
std::string query_string = matches[3];
Uitl::Split(query_string, "&", &query_string_arry);
for (auto &str : query_string_arry)
{
auto pos = str.find('=');
if (pos == std::string::npos)
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
std::string key = Uitl::UrlDecode(str.substr(0, pos), true);
std::string val = Uitl::UrlDecode(str.substr(pos + 1), true);
_request.SetParam(key, val);
}
return true;
}
// 处理请求头
bool RecvHttpLine(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_LINE)
return false;
std::string line = buf->GetLineAndPop();
if (line.size() == 0)
{
if (buf->ReadSisz() > MAX_LINE)
{
_resp_statu = 414;
_recv_statu = RECV_HTTP_ERROR;
return false;
}
return true; // 这里说明是半包的情况
}
if (line.size() > MAX_LINE) // 请求头太长了就认为是错的
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI TOO LONG
return false;
}
bool ret = ParseHttpLine(line);
if (!ret)
return false;
_recv_statu = RECV_HTTP_HEAD;
return true;
}
// 处理请求头部字段
bool RecvHttpHead(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_HEAD)
return false;
while (true)
{
std::string line = buf->GetLineAndPop();
if (line.size() == 0)
{
if (buf->ReadSisz() > MAX_LINE)
{
_resp_statu = 414;
_recv_statu = RECV_HTTP_ERROR;
return false;
}
return true; // 这里说明是半包的情况
}
if (line.size() > MAX_LINE) // 请求头太长了就认为是错的
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI TOO LONG
return false;
}
if (line == "\n" || line == "\r\n")
{
break;
}
bool ret = ParseHttpHead(line);
if (!ret)
return false;
}
_recv_statu = RECV_HTTP_BODY;
return true;
}
bool ParseHttpHead(std::string &line)
{
if (line.empty())
return false;
if (line.back() == '\n')
line.pop_back();
if (line.back() == '\r')
line.pop_back();
auto pos = line.find(": ");
if (pos == std::string::npos)
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400;
return false;
}
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 2);
_request.SetHeader(key, val);
return true;
}
// 处理请求正文
bool RecvHttpBody(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_BODY)
return false;
size_t content_length = _request.ContentLength();
if (content_length == 0) // 没有正文,那我就认为处理完毕
{
_recv_statu = RECV_HTTP_OVER;
return true;
}
size_t real_len = content_length - _request._body.size(); // 还需要写入的长度
if (real_len <= buf->ReadSisz())
{
_request._body.append(buf->ReadPosition(), real_len);
buf->MoveReadOffset(real_len);
_recv_statu = RECV_HTTP_OVER;
return true;
}
_request._body.append(buf->ReadPosition(), buf->ReadSisz());
buf->MoveReadOffset(buf->ReadSisz());
return true;
}
public:
HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
void ReSet()
{
_resp_statu = 200;
_recv_statu = RECV_HTTP_LINE;
_request.ReSet();
}
int RespStatu() { return _resp_statu; }
HttpRecvStatu RecvStatu() { return _recv_statu; }
HttpRequest &Request() { return _request; }
void RecvHttpRequest(Buffer *buf)
{
switch (_recv_statu)
{
case RECV_HTTP_LINE:
RecvHttpLine(buf);
case RECV_HTTP_HEAD:
RecvHttpHead(buf);
case RECV_HTTP_BODY:
RecvHttpBody(buf);
}
return;
}
};
3. Util 模块:协议辅助与安全防线
功能与运行逻辑深度解析
Util 是一个纯静态工具库,负责提供 URL 编解码、文件读取、MIME 类型映射以及路径安全校验。在静态资源请求中,ExMime 通过提取文件后缀(如 .html、.png),快速查 unordered_map 表返回对应的 Content-Type,确保浏览器正确渲染。而 ValidPath 则是守护服务器本地文件系统安全的关键。
难点与解决(踩坑实录)
-
路径穿越攻击
- 困难 :当处理静态文件时,如果客户端恶意请求
/../../etc/passwd,直接与根目录字符串拼接,会导致客户端越权读取到服务器上的密码文件,存在极大的安全漏洞。 - 解决 :在
ValidPath中实现目录深度监控。按/分割请求路径,遇到..就让层级level--。一旦发现level < 0(意味着试图越出根目录),立刻判定为非法请求并拦截。
- 困难 :当处理静态文件时,如果客户端恶意请求
-
URL 编码与路径拼接瑕疵
- 困难 :客户端传来的 URL 往往带有
%20等编码字符,导致路径识别失败。另外,根目录的_basedir以/结尾,请求路径/index.html也以/开头,拼接后产生./wwwroot//index.html的双斜杠,不仅日志难看,在某些特定系统下也可能引发问题。 - 解决 :实现了
UrlDecode将特殊字符还原。对于路径拼接,在设置基础目录时统一剥离末尾的/,或者拼接时判断,确保最终路径格式绝对规范。
- 困难 :客户端传来的 URL 往往带有
代码展示:
cpp
class Uitl
{
public:
// 字符串按照要求的字符分割然后放入arry
static ssize_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry)
{
ssize_t offset = 0;
while (offset < src.size())
{
int pos = src.find(sep, offset);
if (pos == std::string::npos)
{
arry->push_back(src.substr(offset));
return arry->size();
}
if (pos == offset)
{
offset = pos + sep.size();
continue;
}
arry->push_back(src.substr(offset, pos - offset));
offset = pos + sep.size();
}
return arry->size();
}
// 以二进制的方式想文件里面写入数据
static bool WriteFile(const std::string &filename, const std::string &buf)
{
std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // 以二进制的方式读取,读取之前先清空一下
if (!ofs.is_open())
{
std::cout << filename.c_str() << "open failed" << std::endl;
return false;
}
// 写数据
ofs.write(buf.c_str(), buf.size());
if (ofs.good())
{
std::cout << filename.c_str() << "write success" << std::endl;
ofs.close();
return true;
}
else
{
std::cout << filename.c_str() << "write failed" << std::endl;
ofs.close();
return false;
}
}
// 以二进制的方式读取文件中的数据
static bool ReadFile(const std::string &filename, std::string *buf)
{
std::ifstream ifs(filename, std::ios::binary);
if (!ifs.is_open())
{
std::cout << filename.c_str() << "open failed" << std::endl;
return false;
}
ssize_t filesize = 0;
ifs.seekg(0, ifs.end);
filesize = ifs.tellg();
ifs.seekg(0, ifs.beg);
if (filesize > 0)
{
buf->resize(filesize);
ifs.read(&(*buf)[0], buf->size()); // 先解引用拿到buf对象,在调用[0]拿到第一个元素,然后在取地址
}
if (ifs.good())
{
std::cout << filename.c_str() << "read success" << std::endl;
ifs.close();
return true;
}
else
{
std::cout << filename.c_str() << "read failed" << std::endl;
ifs.close();
return false;
}
}
static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
{
std::string ret;
for (auto &c : url)
{
if (c == '.' || c == '~' || c == '-' || c == '_' || isalnum(c))
{
ret += c;
continue;
}
if (c == ' ' && convert_space_to_plus == true)
{
ret += '+';
continue;
}
char tmp[4] = {0};
snprintf(tmp, 4, "%%%02X", (unsigned char)c);
ret += tmp;
}
return ret;
}
static char HEXTOI(char c) // 把把一个 16 进制字符 → 转成数字
{
if (c >= '0' && c <= '9')
{
return c - '0';
}
else if (c >= 'a' && c <= 'z')
{
return c - 'a' + 10;
}
else if (c >= 'A' && c <= 'Z')
{
return c - 'A' + 10;
}
return -1;
}
// 对url进行解码
static std::string UrlDecode(const std::string url, bool convert_plus_to_space)
{
std::string res;
for (int i = 0; i < url.size(); i++)
{
if (url[i] == '+' && convert_plus_to_space == true)
{
res += ' ';
continue;
}
if (url[i] == '%' && (i + 2) < url.size()) //%E6
{
char v1 = HEXTOI(url[i + 1]); // E
char v2 = HEXTOI(url[i + 2]); // 6
char c = v1 * 16 + v2;
res += c;
i += 2;
continue;
}
res += url[i];
}
return res;
}
// 获取状态码的信息
static std::string StatuDesc(int statu)
{
std::unordered_map<int, std::string> _status = {
{100, "Continue"},
{101, "Switching Protocol"},
{102, "Processing"},
{103, "Early Hints"},
{200, "OK"},
{201, "Created"},
{202, "Accepted"},
{203, "Non-Authoritative Information"},
{204, "No Content"},
{205, "Reset Content"},
{206, "Partial Content"},
{207, "Multi-Status"},
{208, "Already Reported"},
{226, "IM Used"},
{300, "Multiple Choice"},
{301, "Moved Permanently"},
{302, "Found"},
{303, "See Other"},
{304, "Not Modified"},
{305, "Use Proxy"},
{306, "unused"},
{307, "Temporary Redirect"},
{308, "Permanent Redirect"},
{400, "Bad Request"},
{401, "Unauthorized"},
{402, "Payment Required"},
{403, "Forbidden"},
{404, "Not Found"},
{405, "Method Not Allowed"},
{406, "Not Acceptable"},
{407, "Proxy Authentication Required"},
{408, "Request Timeout"},
{409, "Conflict"},
{410, "Gone"},
{411, "Length Required"},
{412, "Precondition Failed"},
{413, "Payload Too Large"},
{414, "URI Too Long"},
{415, "Unsupported Media Type"},
{416, "Range Not Satisfiable"},
{417, "Expectation Failed"},
{418, "I'm a teapot"},
{421, "Misdirected Request"},
{422, "Unprocessable Entity"},
{423, "Locked"},
{424, "Failed Dependency"},
{425, "Too Early"},
{426, "Upgrade Required"},
{428, "Precondition Required"},
{429, "Too Many Requests"},
{431, "Request Header Fields Too Large"},
{451, "Unavailable For Legal Reasons"},
{501, "Not Implemented"},
{502, "Bad Gateway"},
{503, "Service Unavailable"},
{504, "Gateway Timeout"},
{505, "HTTP Version Not Supported"},
{506, "Variant Also Negotiates"},
{507, "Insufficient Storage"},
{508, "Loop Detected"},
{510, "Not Extended"},
{511, "Network Authentication Required"}};
auto it = _status.find(statu);
if (it != _status.end())
{
return it->second;
}
return "Nokonwn";
}
// 根据文件名过去文件的后续
static std::string ExMime(const std::string &filename)
{
std::unordered_map<std::string, std::string> _mimes = {
{".aac", "audio/aac"},
{".abw", "application/x-abiword"},
{".arc", "application/x-freearc"},
{".avi", "video/x-msvideo"},
{".azw", "application/vnd.amazon.ebook"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".bz", "application/x-bzip"},
{".bz2", "application/x-bzip2"},
{".csh", "application/x-csh"},
{".css", "text/css"},
{".csv", "text/csv"},
{".doc", "application/msword"},
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".eot", "application/vnd.ms-fontobject"},
{".epub", "application/epub+zip"},
{".gif", "image/gif"},
{".htm", "text/html"},
{".html", "text/html"},
{".ico", "image/vnd.microsoft.icon"},
{".ics", "text/calendar"},
{".jar", "application/java-archive"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "text/javascript"},
{".json", "application/json"},
{".jsonld", "application/ld+json"},
{".mid", "audio/midi"},
{".midi", "audio/x-midi"},
{".mjs", "text/javascript"},
{".mp3", "audio/mpeg"},
{".mpeg", "video/mpeg"},
{".mpkg", "application/vnd.apple.installer+xml"},
{".odp", "application/vnd.oasis.opendocument.presentation"},
{".ods", "application/vnd.oasis.opendocument.spreadsheet"},
{".odt", "application/vnd.oasis.opendocument.text"},
{".oga", "audio/ogg"},
{".ogv", "video/ogg"},
{".ogx", "application/ogg"},
{".otf", "font/otf"},
{".png", "image/png"},
{".pdf", "application/pdf"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".rar", "application/x-rar-compressed"},
{".rtf", "application/rtf"},
{".sh", "application/x-sh"},
{".svg", "image/svg+xml"},
{".swf", "application/x-shockwave-flash"},
{".tar", "application/x-tar"},
{".tif", "image/tiff"},
{".tiff", "image/tiff"},
{".ttf", "font/ttf"},
{".txt", "text/plain"},
{".vsd", "application/vnd.visio"},
{".wav", "audio/wav"},
{".weba", "audio/webm"},
{".webm", "video/webm"},
{".webp", "image/webp"},
{".woff", "font/woff"},
{".woff2", "font/woff2"},
{".xhtml", "application/xhtml+xml"},
{".xls", "application/vnd.ms-excel"},
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".xml", "application/xml"},
{".xul", "application/vnd.mozilla.xul+xml"},
{".zip", "application/zip"},
{".3gp", "video/3gpp"},
{".3g2", "video/3gpp2"},
{".7z", "application/x-7z-compressed"}};
// 可能是这种情况a.b.txt这种情况,所以我们从后面开始查找
auto pos = filename.find_last_of('.');
if (pos == std::string::npos)
{
return "application/octet-stream";
}
std::string s = filename.substr(pos);
auto it = _mimes.find(s);
if (it != _mimes.end())
{
return it->second;
}
return "application/octet-stream";
}
// 判断是不是一个目录
static bool IsDirectory(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st); // 操作系统接到请求后,去磁盘上找这个文件。如果找到了,把信息填进 st 里,返回 0。如果文件不存在,或者没有权限,返回 -1。
if (ret < 0)
{
return false;
}
return S_ISDIR(st.st_mode);
}
// 判断一个文件是否是一个普通文件
static bool IsRegular(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st);
if (ret < 0)
{
return false;
}
return S_ISREG(st.st_mode);
}
static bool ValidPath(const std::string &path)
{
std::vector<std::string> str;
Split(path, "/", &str);
int ret = 0;
for (auto &ch : str)
{
if (ch == "." || ch.empty())
{
continue;
}
if (ch == "..")
{
ret--;
if (ret < 0)
return false;
continue;
}
ret++;
}
return true;
}
};
// 请求报文
class HttpRequest
{
public:
std::string _method;
std::string _path;
std::string _version;
std::string _body;
std::smatch _matches;
std::unordered_map<std::string, std::string> _headers; // 头部字段
std::unordered_map<std::string, std::string> _params; // 查询字符串
public:
HttpRequest() : _version("HTTP/1.1") {}
// 清理资源
void ReSet()
{
_method.clear();
_path.clear();
_version = "HTTP/1.1";
_body.clear();
std::smatch matches;
_matches.swap(matches);
_headers.clear();
_params.clear();
}
// 设置头部字段
void SetHeader(const std::string &key, const std::string &val)
{
_headers.insert(std::make_pair(key, val));
}
// 判断是否有指定头部字段
bool HasHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return true;
}
return false;
}
// 获取头部字段
std::string GetHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it != _headers.end())
{
return it->second;
}
return "";
}
// 添加字符串
void SetParam(const std::string &key, const std::string &val)
{
_params.insert(std::make_pair(key, val));
}
// 查看是否有指定的字符串
bool HasParam(const std::string &key) const
{
auto it = _params.find(key);
if (it != _params.end())
{
return true;
}
return false;
}
// 获取指定的字符串
std::string GetParam(const std::string &key) const
{
auto it = _params.find(key);
if (it != _params.end())
{
return it->second;
}
return "";
}
// 获取正文长度
size_t ContentLength() const
{
auto it = HasHeader("Content-Length");
if (!it)
return 0;
return std::stol(GetHeader("Content-Length"));
}
bool Close() const // 判断是否是短连接
{
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};
4. HttpServer 模块:路由分发与响应组装
功能与运行逻辑深度解析
HttpServer 是总指挥部。其内部的 Route 函数首先通过 IsFileHandler 进行动静分离。如果是静态文件,交由 FileHandler 读取;如果是动态接口(如 /login),则根据请求方法(GET/POST 等)去对应的 std::vector<std::pair<std::regex, Handler>> 路由表中遍历。利用 std::regex_match 进行正则匹配,匹配成功后执行绑定的回调函数。最后由 WriteResponse 将对象序列化为标准 HTTP 报文发送。
难点与解决(踩坑实录)
-
路由匹配的灵活性
- 困难 :最初想用
unordered_map做 O(1) 的精确字符串匹配,但遇到了像/numbers/(\d+)这种带参数的 RESTful 接口就彻底没辙了。 - 解决 :改用
std::regex存入vector中遍历。虽然复杂度变为 O(N),但考虑到真实业务中同一种请求方法的路由通常只有几十个,现代 C++ 的正则匹配速度极快,以微秒级的耗时换取极大的业务灵活性是绝对划算的。
- 困难 :最初想用
-
302 重定向的"假 Bug"
- 困难:在测试登录接口时,日志里发现服务器连续响应了两次读取,一度以为是底层连接复用或数据包解析出了严重问题。
- 解决 :查阅 HTTP 协议发现,这是因为服务器返回了 302 状态码和
Location头后,浏览器自动发起了第二次全新的 GET 请求,并非服务器 bug。随后在WriteResponse中完善了Connection: keep-alive与close的保活逻辑,如果是长连接,响应后不立刻断开,交由底层的TimerWheel进行生命周期管理。
代码展示:
cpp
#define DEFALT_TIMEOUT 10
class HttpServer
{
private:
using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
Handlers _get_route; // GET 请求的路由表
Handlers _post_route; // POST 请求的路由表
Handlers _put_route; // PUT 请求的路由表
Handlers _delete_route; // DELETE 请求的路由表
std::string _basedir; // 静态资源根目录
TcpServer _server; // 底层Tcp服务器(封装了Accept、EventLoop、Connection)
public:
void ErrorHandler(HttpRequest &req, HttpResponse *res)
{
std::string body;
body += "<html>";
body += "<head>";
body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
body += "</head>";
body += "<body>";
body += "<h1>";
body += std::to_string(res->_statu);
body += " ";
body += Uitl::StatuDesc(res->_statu);
body += "</h1>";
body += "</body>";
body += "</html>";
res->SetContent(body, "text/html");
}
void WriteReponse(const PtrConnection &conn, HttpRequest &req, HttpResponse &rsp)
{
// 1. 先完善头部字段
if (req.Close() == true)
{
rsp.SetHeader("Connection", "close");
}
else
{
rsp.SetHeader("Connection", "keep-alive");
}
if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
{
rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
}
if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
{
rsp.SetHeader("Content-Type", "application/octet-stream");
}
if (rsp._redirect_flag == true)
{
rsp.SetHeader("Location", rsp._redirect_url);
}
// 按照http的格式组织应答然后发送给客户端
std::stringstream rsp_str;
rsp_str << req._version << " " << rsp._statu << " " << Uitl::StatuDesc(rsp._statu) << "\r\n";
for (auto &ch : rsp._headers)
{
rsp_str << ch.first << ": " << ch.second << "\r\n";
}
rsp_str << "\r\n"
<< rsp._body;
conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
}
bool IsFileHandler(const HttpRequest &req)
{
if (_basedir.empty())
return false;
if (req._method != "GET" && req._method != "HEAD")
{
return false;
}
if (!Uitl::ValidPath(req._path))
{
return false;
}
std::string req_path = _basedir + req._path; // 为了避免直接修改请求的资源路径,因此定义一个临时对象
if (req._path.back() == '/')
{
req_path += "index.html";
}
if (!Uitl::IsRegular(req_path))
{
return false;
}
return true;
}
// 将静态资源读出来放在应答里面
void FileHandler(const HttpRequest &req, HttpResponse *rsp)
{
std::string _basedir_line = _basedir + req._path;
if (req._path.back() == '/')
{
_basedir_line += "index.html";
}
bool ret = Uitl::ReadFile(_basedir_line, &rsp->_body);
if (ret == false)
{
return;
}
std::string mime = Uitl::ExMime(_basedir_line);
rsp->SetHeader("Content-Type", mime);
return;
}
void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
{
for (auto &handler : handlers)
{
const std::regex &re = handler.first;
const Handler &functor = handler.second;
bool ret = std::regex_match(req._path, req._matches, re);
if (ret == false)
{
continue;
}
return functor(req, rsp); //// 传入请求信息,和空的rsp,执行处理函数
}
rsp->_statu = 404;
}
void Route(HttpRequest &req, HttpResponse *rsp)
{
if (IsFileHandler(req) == true) // 看看是不是静态资源
{
return FileHandler(req, rsp);
}
if (req._method == "GET" || req._method == "HEAD")
{
return Dispatcher(req, rsp, _get_route);
}
else if (req._method == "POST")
{
return Dispatcher(req, rsp, _post_route);
}
else if (req._method == "PUT")
{
return Dispatcher(req, rsp, _put_route);
}
else if (req._method == "DELETE")
{
return Dispatcher(req, rsp, _delete_route);
}
rsp->_statu = 405;
return;
}
void OnMesssage(const PtrConnection &conn, Buffer *buf)
{
while (buf->ReadSisz() > 0)
{
// 1.获取上下文
HttpContext *context = conn->GetContext()->get<HttpContext>();
// 2.解析请求
context->RecvHttpRequest(buf);
// 进行错误回响
HttpRequest &req = context->Request();
HttpResponse rsp(context->RespStatu());
// 错误
if (context->RespStatu() >= 400)
{
// 1.填充一个错误的页面给回响
ErrorHandler(req, &rsp);
// 2.发送给客户端
WriteReponse(conn, req, rsp);
// 3.重置上下文---代表这条请求处理完毕
context->ReSet();
// 4.清理缓冲区中的数据
buf->MoveReadOffset(buf->ReadSisz());
// 5.处理完数据就关闭连接
conn->ShutDown();
return;
}
// 如果是半包或者没有解析完
if (context->RecvStatu() != RECV_HTTP_OVER)
{
return;
}
// 3.解析完请求然后去路由相关业务处理
Route(req, &rsp);
// 4.对应答进行组织发给客户端
WriteReponse(conn, req, rsp);
// 5.发完数据就要重置上下文
context->ReSet();
// 6.根据长短连接判断是否关闭连接或者继续处理
if (rsp.Close() == true)
conn->ShutDown(); // 短链接则直接关闭
}
return;
}
// 设置上下文
void OnConnected(const PtrConnection &conn)
{
conn->SetContext(HttpContext());
DBG_LOG("NEW CONNECTION %p", conn.get());
}
public:
HttpServer(uint16_t port, int timerout = DEFALT_TIMEOUT) : _server(port)
{
_server.EnableInactiveRelease(timerout);
_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
_server.SetMessageCallback(std::bind(&HttpServer::OnMesssage, this, std::placeholders::_1, std::placeholders::_2));
}
void SetBaseDir(const std::string &path)
{
assert(Uitl::IsDirectory(path) == true);
_basedir = path;
}
void Get(const std::string &pattern, const Handler &handler)
{
_get_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Post(const std::string &pattern, const Handler &handler)
{
_post_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Put(const std::string &pattern, const Handler &handler)
{
_put_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void Delete(const std::string &pattern, const Handler &handler)
{
_delete_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void SetThreadCount(int count)
{
_server.SetThreadCount(count);
}
void Listen()
{
_server.Start();
}
};

总结
纸上得来终觉浅。希望这个项目的源码和这套架构解析,能给同样在 C++ 后端道路上探索的朋友们带来一些启发。
最后,用一段话概括整个项目的设计脉络:底层以 epoll 驱动的多线程 Reactor 为核心,通过 Buffer 与 Socket 夯实字节流处理与资源管理,借助 Channel、Poller、EventLoop、TimerWheel 构建事件分发与并发调度引擎,再由 Connection、Acceptor、TcpServer 完成连接的全生命周期管理;上层则通过 Any 容器解耦网络层与业务层,用 HttpContext 状态机应对半包粘包,配合正则路由、动静分离与路径穿越防护完成 HTTP 请求处理,最终形成一套结构清晰、可扩展的高并发服务器实现。