一.实现目标
通过我们实现的高并发服务器组件,开发者可以简洁、快速地搭建起一个高性能服务器。该组件内置对多种应用层协议的支持,能够进一步加速 高性能应用服务器的构建过程(为方便项目演示,当前版本已集成了HTTP协议模块)。需要强调的是,本项目定位为一个高并发服务器组件,因此目前的实现中不包含具体的业务逻辑内容。(具体的业务逻辑可以自行实现)
二.项目详情
1. HTTP
HTTP(Hyper Text Transfer Protocol,超文本传输协议)是一种运行在应用层的简单请求-响应协议。它的工作机制可以概括为:客户端根据需求向服务器发起请求,服务器根据请求内容提供相应服务,通信完成后连接即释放。
需要明确的是,HTTP协议本身是建立在TCP协议之上的应用层协议。这意味着,从本质上看,HTTP服务器其实就是一个TCP服务器,只不过它在应用层按照HTTP协议的格式对数据进行解析和封装,以此来识别客户端的请求意图,并完成相应的业务处理。
因此,要理解HTTP服务器的实现原理,可以简化为以下几个步骤:
- 搭建TCP服务器:负责接收客户端发起的连接请求。
- 解析HTTP请求:按照HTTP协议格式,解析客户端发送的数据,明确其具体需求。
- 处理业务逻辑:根据解析结果,执行相应的服务操作。
- 封装HTTP响应:将处理结果按照HTTP协议格式进行封装,并返回给客户端。
从上述流程可以看出,实现一个基本的HTTP服务器并不复杂,但要构建一个高性能的HTTP服务器,则面临更多挑战。在本项目中,我们将基于Reactor模式来实现高性能服务器的核心架构 。更准确地说,由于我们所构建的服务器本身不包含具体业务逻辑,因此它本质上是一个高性能服务器的基础库或基础组件,为上层应用提供高并发的网络通信能力。
2. Reactor模型
2.1 概念
Reactor模式是一种事件驱动型的处理模型,其核心思想是:当一个或多个输入请求同时到达服务器时,服务器能够统一接收并进行处理。
在该模式下,服务端程序会集中处理多路并发的请求,并将这些请求同步分派给对应的处理线程去执行。正因如此,Reactor模式也被称为Dispatcher模式(分派模式)。
可以这样通俗地理解:Reactor模式利用 "I/O多路复用" 技术,统一监听多个连接上的事件。一旦有事件发生,它就负责将这些事件分发给相应的处理线程或进程去处理。这种机制能够有效提升系统的并发处理能力,因此成为编写高性能网络服务器时不可或缺的关键技术之一。
2.2 分类

2.2.1 单Reactor单线程模型:I/O多路复用与业务处理合并在同一线程
在这种模式下,整个服务器只使用一个线程来完成所有工作,具体流程如下:
- 事件监听:通过I/O多路复用机制(如select、poll、epoll等),统一监控客户端的各类请求。
- 事件处理:当监控到事件触发时,根据事件类型进行相应处理 :
如果是新连接请求 :接收客户端的连接请求,建立新连接,并将该连接的文件描述符添加到多路复用模型中,以便后续监控其上的事件。
如果是数据通信请求:对已建立的连接进行数据读写操作,包括接收客户端发送的数据、处理业务逻辑、然后将响应数据发送回客户端。
3.优点:
所有操作都在同一个线程中顺序执行,整体架构简单清晰。由于没有多线程或多进程参与,也就不存在线程间通信、数据同步、资源竞争等问题,开发和调试相对容易。
4.缺点:
无法充分利用现代CPU的多核处理能力。随着并发量增大或业务处理变慢,单个线程很快就会达到处理瓶颈,导致整体性能受限。
5.适用场景:
适合客户端数量较少、每个请求处理速度较快的场景。如果业务处理耗时较长,或者活跃连接数较多,由于所有请求是串行处理的,后续的连接请求可能会长时间得不到响应,造成明显的延迟甚至超时。
2.2.2 单Reactor多线程模型:I/O多路复用与业务处理分离
这种模型在保留单Reactor线程的基础上,引入了线程池来处理业务逻辑,具体工作流程如下:
- 事件监听:Reactor 线程通过I/O多路复用技术,统一监控所有客户端的事件请求。
- 事件分发与处理:当事件触发时,Reactor线程根据不同事件类型采取不同策略:
新连接请求:接收客户端连接,并将新连接加入多路复用模型的监控列表,继续由Reactor线程监听其后续事件。
数据通信请求:Reactor线程负责读取数据,然后将读取到的数据交给子线程中的工作线程去处理具体的业务逻辑。 - 响应发送:工作线程处理完业务后,将处理结果返回给Reactor线程(主线程),由Reactor线程负责将响应数据发送给客户端。
4.优点:
将耗时的业务处理从Reactor线程中剥离出来,交给线程池并行处理,从而能够充分利用CPU的多核资源,提高系统的整体处理能力。
5.缺点:
多线程引入了数据共享和资源竞争问题,需要处理好线程间的同步与互斥 。
虽然业务处理被分流了,但Reactor线程仍然承担着所有事件(包括新连接、数据读写)的监听和响应发送工作。在超高并发场景下**,**单Reactor线程可能会成为新的瓶颈,无法充分发挥多线程处理的能力。
2.2.3 多Reactor多线程模型:主从Reactor分工协作 + 线程池处理业务
这种模型在单Reactor多线程的基础上进一步优化,将I/O事件监听的工作拆分到多个Reactor线程中,形成主从架构,具体工作流程如下:
-
主Reactor负责连接建立:主Reactor线程专门通过I/O多路复用监听新连接请求 。当有新的客户端连接到来时,主Reactor负责接受连接,并将该连接分配给某个子Reactor进行后续管理。
-
子Reactor线程负责通信事件:子Reactor线程负责监控已分配连接上的读写事件。当某个连接上有数据到达时,对应的子Reactor线程负责读取数据,然后将数据交给子线程池处理。
-
子线程池处理业务:线程池中的工作线程并行处理具体的业务逻辑。处理完成后,将响应结果返回给对应的子Reactor线程。
-
子Reactor负责发送响应:子Reactor收到业务处理结果后,负责将响应数据发送回客户端。
5.优点:
- 充分利用CPU多核资源:通过多个Reactor线程和线程池的配合,能够更好地发挥多核处理器的并行计算能力。
主从分工明确:主Reactor专注于处理新连接建立,子Reactor专注于处理已连接的通信事件,线程池专注于业务处理。各司其职,互不干扰,有效避免了单Reactor模型在高并发场景下的瓶颈问题。
扩展性好:可以根据实际负载情况,灵活调整子Reactor数量和线程池大小,以适应不同的并发压力。
2.3 核心设计思想
我们将采用主从Reactor架构来构建高性能服务器,其核心设计思想如下:
-
主Reactor的职责
主Reactor线程专门负责监听服务端的监听描述符,只处理新连接建立事件 。这样设计的目的是确保获取新连接的操作足够高效,从而提升服务器的整体并发处理能力。 -
子Reactor的职责
当主Reactor接收到新连接后,会将该连接分发给某个子Reactor线程**。**每个子Reactor线程独立运行,负责监控分配给它的多个连接上的读写事件,并进行数据的读写操作以及业务处理 。 -
One Thread One Loop 的设计理念
整个模型遵循"一线程一循环"的原则,即每个线程都运行着自己独立的事件循环 ,负责处理分配给它的所有I/O事件。这种设计使得每个线程的职责清晰,避免了复杂的锁竞争。(即在每一个线程在自己的线程里面运行) -
关于业务处理层的说明
需要注意的是,由于我们的项目定位是一个高并发服务器组件,无法预知使用者的具体业务场景,因此本次实现只包含主从Reactor模型的核心框架。至于业务层的子线程池,我们将不作为组件的一部分提供,而是留给组件使用者根据自身业务需求,自行决定是否引入以及如何实现。
三.前置知识技术点功能用例
1. C++11中的bind
1.1 概念
std::bind 是 C++11 标准库中提供的一个函数模板,位于**<functional>** 头文件中。它本质上是一个函数适配器,可以将一个可调用对象(函数、函数指针、成员函数、函数对象等)与其参数绑定在一起,生成一个新的可调用对象。
html
#include <functional>
// 基本语法
auto newFunc = std::bind(可调用对象, 参数列表...);
1.2 为什么要使用 bind ?
在像我们之前讨论的服务器组件中,bind的主要用途包括:
回调函数注册:将成员函数与对象实例绑定,作为回调传递给另一个对象。
参数预绑定:固定某些参数,生成新的函数签名。

占位符使用:延迟提供部分参数。
示例1:

示例2:
html
#include <iostream>
#include <functional>
#include <unistd.h>
class Test
{
public:
Test() { std::cout << "构造" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
void del(const Test *t, int num)
{
std::cout << num << std::endl;
delete t;
}
int main()
{
Test *t = new Test;
//bind作⽤也可以简单理解为给⼀个函数绑定好参数,然后返回⼀个参数已经设定好或者预留好的函数,可以在合适的时候进⾏调⽤
//⽐如,del函数,要求有两个参数,⼀个Test*, ⼀个int,
//⽽这⾥,想要基于del函数,适配⽣成⼀个新的函数,这个函数固定第1个参数传递t变量,
//第⼆个参数预留出来,在调⽤的时候进⾏设置
std::function<void(int)> cb = std::bind(del, t, std::placeholders::_1);
cb(10);
while (1)
sleep(1);
return 0;
}
示例3:
html
#include <iostream>
#include <functional>
#include <string>
class Connection
{
public:
void onMessage(const std::string &data)
{
std::cout << "收到数据: " << data << std::endl;
}
void onClose()
{
std::cout << "连接关闭" << std::endl;
}
};
class Channel
{
public:
// 设置无参回调
void setReadCallback(std::function<void()> cb)
{
_readCallback = cb;
}
// 设置带参回调
void setMessageCallback(std::function<void(const std::string &)> cb)
{
_messageCallback = cb;
}
// 模拟事件触发
void handleRead()
{
if (_readCallback)
{
_readCallback(); // 调用无参回调
}
}
// 模拟收到消息
void onDataReceived(const std::string &data)
{
if (_messageCallback)
{
_messageCallback(data); // 调用带参回调,传入数据
}
}
private:
std::function<void()> _readCallback;
std::function<void(const std::string &)> _messageCallback;
};
int main()
{
// 创建对象
Connection conn;
Channel channel;
using namespace std::placeholders;
// onClose(无参回调)
std::cout << "--- 测试 onClose ---" << std::endl;
// 将 Connection::onClose 绑定到 conn 对象 然后设置为Channel的读回调函数
channel.setReadCallback(std::bind(&Connection::onClose, &conn));
// 模拟触发读事件(实际上会调用 onClose)
channel.handleRead();
std::cout << "------------------------------------------------------------" << std::endl;
// onMessage(带参回调)
std::cout << "--- 测试 onMessage ---" << std::endl;
// 将 Connection::onMessage 绑定到 conn 对象,使用占位符
channel.setMessageCallback(std::bind(&Connection::onMessage, &conn, _1));
// 模拟收到数据
channel.onDataReceived("Hello, World!");
channel.onDataReceived("第二次消息");
// 输出: 收到数据: Hello, World!
// 输出: 收到数据: Hello, World
std::cout << "------------------------------------------------------------" << std::endl;
std::cout << "--- 测试提前绑定数据 ---" << std::endl;
// 提前将数据绑定好,生成无参回调
std::string fixedData = "fixed Data test";
channel.setReadCallback(std::bind(&Connection::onMessage, &conn, fixedData));
// 触发读事件,会调用 onMessage 并传入 fixed Data test
channel.handleRead();
// 输出: 收到数据: fixed Data test
std::cout << "------------------------------------------------------------" << std::endl;
return 0;
}


也可以使用lambda表达式

cpp
// 方式1:无参回调 - 用于 onClose
std::bind(&Connection::onClose, &conn)
// 相当于: []() { conn.onClose(); }
// 方式2:带参回调 - 用于 onMessage
std::bind(&Connection::onMessage, &conn, _1)
// 相当于: [&conn](const std::string& data) { conn.onMessage(data); }
基于**std::bind的特性,在设计和实现线程池或任务池时,我们可以采用一种更加灵活优雅的方式:将任务池中的每个任务统一表示为"可调用对象"(通常是 std::function<void()> 类型)。当外部模块添加任务时,通过std::bind**将实际的函数调用及其参数打包成一个无参的函数对象,然后放入任务池。任务池中的工作线程只需要简单地执行这些无参函数对象即可。
示例4:

示例5:
html
#include <iostream>
#include <functional>
#include <vector>
#include <string>
class ThreadPool {
public:
// 添加任务 - 接受任何可调用对象
template<typename F, typename... Args>
void addTask(F&& f, Args&&... args) {
// 使用 bind 将函数和参数打包成无参函数对象
auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
// 统一存储为 std::function<void()> 类型
_tasks.push_back([task]() {
task(); // 执行绑定的任务
});
}
void executeTasks() {
for (auto& task : _tasks) {
task(); // 直接执行,无需关心参数
}
}
// 获取任务数量
size_t taskCount() const {
return _tasks.size();
}
// 清空任务
void clear() {
_tasks.clear();
}
private:
std::vector<std::function<void()>> _tasks;
};
void download(const std::string& url, int timeout) {
std::cout << "下载文件: " << url << ",超时: " << timeout << "秒" << std::endl;
}
void simpleTask() {
std::cout << "执行简单任务" << std::endl;
}
int calculate(int a, int b) {
int result = a + b;
std::cout << "计算: " << a << " + " << b << " = " << result << std::endl;
return result;
}
class Database {
public:
void query(const std::string& sql) {
std::cout << "执行SQL: " << sql << std::endl;
}
};
int main() {
ThreadPool pool;
Database db;
using namespace std::placeholders;
// 添加各种任务
pool.addTask(download, "http://example.com", 30);
pool.addTask(simpleTask);
pool.addTask(calculate, 10, 20);
pool.addTask(&Database::query, &db, "SELECT * FROM users");
// 执行所有任务
std::cout << "开始执行 " << pool.taskCount() << " 个任务:" << std::endl;
pool.executeTasks();
return 0;
}

这种设计的好处:
这种基于 std::bind的任务池设计最大的好处在于实现了任务的生产者与执行者之间的完全解耦,任务池的设计者无需预知所有可能的任务类型、函数签名或参数个数,只需要将任务统一封装为无参的可调用对象,而任务的使用者则可以通过std::bind 自由地将任何函数、成员函数或函数对象与任意参数组合打包后提交,工作线程在执行时只需简单地调用这些统一格式的任务即可,这种设计不仅让任务池的核心逻辑保持简洁稳定,还赋予了系统极大的灵活性和可扩展性,使得添加新的任务类型完全不需要修改任务池的代码,从而显著降低了模块间的耦合度。
2. Any类
2.1 正则库的简单使用
正则表达式(Regular Expression)是一种用于描述字符串匹配规则的工具,它通过定义特定的匹配模式(pattern),对字符串进行查找、提取、替换或校验等操作。借助正则表达式,可以方便地判断一个字符串是否包含某种特定结构的子串,也可以从复杂文本中提取出符合条件的内容,或者对匹配到的部分进行替换处理。
在 HTTP 请求解析过程中,正则表达式能够有效简化字符串处理逻辑,使代码结构更加清晰,从而降低开发难度并提高组件的灵活性。通过引入正则库,可以减少大量重复且繁琐的字符串操作代码,使实现的 HTTP 组件库在功能扩展和规则调整方面更加便捷。
需要说明的是,正则表达式提升的是开发效率和代码可维护性,而非运行效率。与直接使用底层字符串处理函数相比,正则匹配通常在执行性能上略低。因此,在对性能要求较高的核心路径中,应根据实际情况权衡使用方式。
测试用例
html
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "contact me at example@test.com";
// 定义邮箱匹配规则
std::regex email_pattern(R"((\w+)(\.?\w+)*@(\w+)(\.\w+)+)");
std::smatch match;
if (std::regex_search(text, match, email_pattern)) {
std::cout << "匹配成功:" << match.str() << std::endl;
std::cout << "用户名部分:" << match[1] << std::endl;
std::cout << "域名部分:" << match[3] << std::endl;
} else {
std::cout << "未匹配到邮箱地址" << std::endl;
}
return 0;
}

cpp
#include <iostream>
#include <string>
#include <regex>
int main()
{
std::string str = "/numbers/1234";
// 匹配以 /numbers/起始,后边跟了一个或多个数字字符的字符串,
// 并且在匹配的过程中提取这个匹配到的数字字符串
std::regex e("/numbers/(\\d+)");
std::smatch matches;
bool ret = std::regex_match(str, matches, e);
if (ret == false)
{
return -1;
}
for (auto &s : matches)
{
std::cout << s << std::endl;
}
return 0;
}

html
#include <iostream>
#include <regex>
#include <string>
#include <map>
int main()
{
std::string http_request =
"GET /index.html HTTP/1.1\r\n"
"Host: www.example.com\r\n"
"Connection: keep-alive\r\n"
"User-Agent: TestClient\r\n"
"\r\n";
// 1. 解析请求行
std::regex request_line_pattern(R"((GET|POST|PUT|DELETE)\s+(\S+)\s+(HTTP/\d\.\d))");
std::smatch request_match;
if (std::regex_search(http_request, request_match, request_line_pattern))
{
std::cout << "Method: " << request_match[1] << std::endl;
std::cout << "URL: " << request_match[2] << std::endl;
std::cout << "Version: " << request_match[3] << std::endl;
}
// 2. 解析 Header
std::regex header_pattern(R"((\w+):\s*(.+))");
std::sregex_iterator it(http_request.begin(), http_request.end(), header_pattern);
std::sregex_iterator end;
std::map<std::string, std::string> headers;
for (; it != end; ++it)
{
headers[(*it)[1]] = (*it)[2];
}
std::cout << "\nHeaders:\n";
for (const auto &h : headers)
{
std::cout << h.first << " => " << h.second << std::endl;
}
return 0;
}

2.2 通用类型any类型的实现
在连接对象对连接进行全生命周期管理的过程中,不可避免地需要介入应用层协议的处理逻辑。因此 , Connection类(关于连接的整合类)内部需维护一个协议处理上下文 ,用以调度和控制协议的处理流程。然而,由于应用层协议种类繁多,为实现较低耦合度的设计,该协议解析上下文不应与特定协议绑定,而应具备容纳任意协议类型数据的能力。为此,需要一种通用的数据类型,能够存储不同类型的协议上下文信息。
在 C 语言中,此类通用类型通常通过 void* 指针实现。而在C++中,Boost 库及C++17标准引入了**any** 类型,作为一种更灵活、类型安全的通用容器。若需兼顾代码的可移植性并降低对第三方库的依赖,可选择使用 C++17 标准库提供的**std::any** ,或自行实现一个简易版本的通用类型类。事实上,实现一个支持任意类型存储的**any**容器并不复杂,其核心机制较为明确。以下给出该类的部分简化实现,以作说明。
首先需要明确的是,Any类本身不能设计为模板类。如果将其定义为模板类,例如 "Any<int> a" 或 "Any<float> b",则在编译时就必须指定具体的类型作为模板参数。这种设计在实际场景中并不可行,因为Any对象所存储的协议上下文信息,在定义对象时往往还无法确定其具体类型------连接所承载的应用层协议可能是动态变化的,因此无法在编译期传递类型参数。
为解决这一问题,可以在Any类内部设计一个模板容器类 "holder",使其能够保存任意类型的数据。然而,Any类本身由于不知道即将存储的数据类型,也无法直接定义这个holder对象或指针------因为定义时需要传递具体的类型参数。
因此,需要引入一个基类 "placeholder",让模板类 "holder" 继承自该基类。这样,Any类中只需要保存一个指向 "placeholder" 的基类指针即可。当需要存储具体数据时,可以new一个带有模板参数的子类 "holder<T>" 对象,用它来保存实际数据,然后将Any类中的基类指针指向这个子类对象。通过这种方式,Any类实现了对任意类型数据的存储,而无需在定义时知晓具体类型,这便是典型的类型擦除技术。
2.2.1 Any 类的实现
html
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());
// 先把_content强转为placeholder类型再取地址
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)
{
// 为val构造一个临时的通用容器,然后与当前容器自身进行指针的交换
// 临时对象释放的时候,原先保存的数据也就被释放了
Any(other).swap(*this);
return *this;
}
};
2.2.2Any类测试
test_1
html
#include <iostream>
#include <typeinfo>
#include <cassert>
#include <unistd.h>
#include <any>
//any 类的实现...
class Test
{
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test &t) { std::cout << "拷贝" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
int main()
{
//C++17才能用
// std::any a;
// a = 10;
// int *pi = std::any_cast<int>(&a);
// std::cout << *pi << std::endl;
// a = std::string("hello");
// std::string *ps = std::any_cast<std::string>(&a);
// std::cout << *ps << std::endl;
Any a;
{
Test t;
a = t;
}
a = 10;
int *pa = a.get<int>();
std::cout << *pa << std::endl;
a = std::string("nihao");
std::string *ps = a.get<std::string>();
std::cout << *ps << std::endl;
return 0;
}


test_2
html
#include <iostream>
#include <typeinfo>
#include <cassert>
#include <unistd.h>
#include <string>
#include <vector>
//any类的实现
class TestClass
{
private:
static int count;
int id;
std::string name;
public:
TestClass() : id(++count), name("default")
{
std::cout << "TestClass默认构造: " << id << std::endl;
}
TestClass(const std::string &n) : id(++count), name(n)
{
std::cout << "TestClass参数构造: " << id << " - " << name << std::endl;
}
TestClass(const TestClass &other) : id(++count), name(other.name)
{
std::cout << "TestClass拷贝构造: " << id << " <- " << other.id << std::endl;
}
~TestClass()
{
std::cout << "TestClass析构: " << id << " - " << name << std::endl;
}
void show() const
{
std::cout << "TestClass对象: " << id << " - " << name << std::endl;
}
std::string getName() const { return name; }
int getId() const { return id; }
};
int TestClass::count = 0;
// 测试1:基本类型测试
void testBasicTypes()
{
std::cout << "\n========== 测试1:基本类型 ==========" << std::endl;
Any a1 = 42; // int
Any a2 = 3.14; // double
Any a3 = 'A'; // char
Any a4 = true; // bool
Any a5 = std::string("Hello"); // string
int *p1 = a1.get<int>();
double *p2 = a2.get<double>();
char *p3 = a3.get<char>();
bool *p4 = a4.get<bool>();
std::string *p5 = a5.get<std::string>();
std::cout << "int: " << *p1 << std::endl;
std::cout << "double: " << *p2 << std::endl;
std::cout << "char: " << *p3 << std::endl;
std::cout << "bool: " << *p4 << std::endl;
std::cout << "string: " << *p5 << std::endl;
// 验证正确性
assert(*p1 == 42);
assert(*p2 == 3.14);
assert(*p3 == 'A');
assert(*p4 == true);
assert(*p5 == "Hello");
}
// 测试2:拷贝构造和赋值
void testCopyAndAssignment()
{
std::cout << "\n========== 测试2:拷贝构造和赋值 ==========" << std::endl;
Any a1 = 100;
// 拷贝构造
Any a2(a1);
int *p1 = a1.get<int>();
int *p2 = a2.get<int>();
std::cout << "a1: " << *p1 << ", a2: " << *p2 << std::endl;
assert(*p1 == *p2);
// 赋值运算
Any a3;
a3 = a1;
int *p3 = a3.get<int>();
std::cout << "a3: " << *p3 << std::endl;
assert(*p3 == 100);
// 链式赋值
Any a4, a5;
a4 = a5 = a1;
assert(*a4.get<int>() == 100);
assert(*a5.get<int>() == 100);
// 自赋值
a1 = a1;
assert(*a1.get<int>() == 100);
}
// 测试3:不同类型之间的赋值
void testTypeSwitching()
{
std::cout << "\n========== 测试3:类型切换 ==========" << std::endl;
Any a;
a = 42;
std::cout << "int: " << *a.get<int>() << std::endl;
a = 3.14159;
std::cout << "double: " << *a.get<double>() << std::endl;
a = std::string("World");
std::cout << "string: " << *a.get<std::string>() << std::endl;
a = TestClass("Dynamic");
a.get<TestClass>()->show();
}
// 测试4:空对象测试
void testEmptyAny()
{
std::cout << "\n========== 测试4:空对象 ==========" << std::endl;
Any a1; // 默认构造
Any a2(nullptr); // 用nullptr构造
// 拷贝空对象
Any a3(a1);
Any a4 = a2;
// 赋值空对象
Any a5;
a5 = a1;
std::cout << "所有空对象构造完成" << std::endl;
// 给空对象赋值
a1 = 999;
std::cout << "a1 now: " << *a1.get<int>() << std::endl;
// 空对象之间赋值
Any empty;
// 自赋值
empty = empty;
// 赋值临时空对象
empty = Any();
}
// 测试5:自定义类测试
void testCustomClass()
{
std::cout << "\n========== 测试5:自定义类 ==========" << std::endl;
// 直接构造
TestClass t1("Object1");
Any a1 = t1;
// 临时对象构造
Any a2 = TestClass("Object2");
// 获取并操作
TestClass *pt1 = a1.get<TestClass>();
TestClass *pt2 = a2.get<TestClass>();
pt1->show();
pt2->show();
// 修改值(通过指针)
*pt1 = TestClass("Modified");
pt1->show();
// 验证
assert(pt1->getName() == "Modified");
assert(pt2->getName() == "Object2");
}
// 测试6:容器类型测试
void testContainerTypes()
{
std::cout << "\n========== 测试6:容器类型 ==========" << std::endl;
// vector测试
std::vector<int> vec = {1, 2, 3, 4, 5};
Any a1 = vec;
auto pvec = a1.get<std::vector<int>>();
std::cout << "vector size: " << pvec->size() << std::endl;
for (int x : *pvec)
{
std::cout << x << " ";
}
std::cout << std::endl;
// pair测试
std::pair<int, std::string> p = {100, "pair test"};
Any a2 = p;
auto ppair = a2.get<std::pair<int, std::string>>();
std::cout << "pair: " << ppair->first << ", " << ppair->second << std::endl;
}
// 测试7:类型安全测试
void testTypeSafety()
{
std::cout << "\n========== 测试7:类型安全 ==========" << std::endl;
Any a = 42; // int类型
// 正确获取
int *p1 = a.get<int>();
std::cout << "int value: " << *p1 << std::endl;
// 错误获取
// 断言失败:typeid(int) != typeid(double)
// double* p2 = a.get<double>();
std::cout << "类型安全检查通过" << std::endl;
}
// 测试8:swap测试
void testSwap()
{
std::cout << "\n========== 测试8:swap ==========" << std::endl;
Any a1 = 123;
Any a2 = std::string("swap test");
std::cout << "交换前: a1=int(" << *a1.get<int>()
<< "), a2=string(" << *a2.get<std::string>() << ")" << std::endl;
a1.swap(a2);
std::cout << "交换后: a1=string(" << *a1.get<std::string>()
<< "), a2=int(" << *a2.get<int>() << ")" << std::endl;
assert(a1.get<std::string>()->compare("swap test") == 0);
assert(*a2.get<int>() == 123);
}
void testComplexExpressions()
{
std::cout << "\n========== 测试9:复杂表达式 ==========" << std::endl;
Any a;
// 测试1:基本类型
a = 1.23;
std::cout << "double: " << *a.get<double>() << std::endl;
// 测试2:正确使用swap
Any temp(456);
temp.swap(a);
std::cout << "after swap with temp: " << *a.get<int>() << std::endl;
// 测试3:临时对象直接swap
a = 3.14;
std::cout << "reset to double: " << *a.get<double>() << std::endl;
Any(789).swap(a);
std::cout << "after swap with temporary: " << *a.get<int>() << std::endl;
// 测试4:链式操作
Any b, c;
b = 100;
c = std::string("hello");
b.swap(c);
std::cout << "b: " << *b.get<std::string>() << ", c: " << *c.get<int>() << std::endl;
}
// 测试10:性能/压力测试
void testStress()
{
std::cout << "\n========== 测试10:压力测试 ==========" << std::endl;
const int N = 1000;
std::vector<Any> vec;
vec.reserve(N);
// 插入大量数据
for (int i = 0; i < N; ++i)
{
vec.push_back(Any(i));
}
// 验证
for (int i = 0; i < N; ++i)
{
int *p = vec[i].get<int>();
assert(*p == i);
}
std::cout << "成功处理 " << N << " 个元素" << std::endl;
// 随机访问
for (int i = 0; i < 10; ++i)
{
int index = rand() % N;
std::cout << "vec[" << index << "] = " << *vec[index].get<int>() << std::endl;
}
}
int main()
{
std::cout << "========== Any类测试开始 ==========" << std::endl;
// 运行所有测试
testBasicTypes();
testCopyAndAssignment();
testTypeSwitching();
testEmptyAny();
testCustomClass();
testContainerTypes();
testTypeSafety();
testSwap();
testComplexExpressions();
testStress();
std::cout << "\n========== 所有测试通过! ==========" << std::endl;
return 0;
}
// 打印信息
// ========== Any类测试开始 ==========
// ========== 测试1:基本类型 ==========
// int: 42
// double: 3.14
// char: A
// bool: 1
// string: Hello
// ========== 测试2:拷贝构造和赋值 ==========
// a1: 100, a2: 100
// a3: 100
// ========== 测试3:类型切换 ==========
// int: 42
// double: 3.14159
// string: World
// TestClass参数构造: 1 - Dynamic
// TestClass拷贝构造: 2 <- 1
// TestClass析构: 1 - Dynamic
// TestClass对象: 2 - Dynamic
// TestClass析构: 2 - Dynamic
// ========== 测试4:空对象 ==========
// 所有空对象构造完成
// a1 now: 999
// ========== 测试5:自定义类 ==========
// TestClass参数构造: 3 - Object1
// TestClass拷贝构造: 4 <- 3
// TestClass参数构造: 5 - Object2
// TestClass拷贝构造: 6 <- 5
// TestClass析构: 5 - Object2
// TestClass对象: 4 - Object1
// TestClass对象: 6 - Object2
// TestClass参数构造: 7 - Modified
// TestClass析构: 7 - Modified
// TestClass对象: 7 - Modified
// TestClass析构: 6 - Object2
// TestClass析构: 7 - Modified
// TestClass析构: 3 - Object1
// ========== 测试6:容器类型 ==========
// vector size: 5
// 1 2 3 4 5
// pair: 100, pair test
// ========== 测试7:类型安全 ==========
// int value: 42
// 类型安全检查通过
// ========== 测试8:swap ==========
// 交换前: a1=int(123), a2=string(swap test)
// 交换后: a1=string(swap test), a2=int(123)
// ========== 测试9:复杂表达式 ==========
// double: 1.23
// after swap with temp: 456
// reset to double: 3.14
// after swap with temporary: 789
// b: hello, c: 100
// ========== 测试10:压力测试 ==========
// 成功处理 1000 个元素
// vec[383] = 383
// vec[886] = 886
// vec[777] = 777
// vec[915] = 915
// vec[793] = 793
// vec[335] = 335
// vec[386] = 386
// vec[492] = 492
// vec[649] = 649
// vec[421] = 421
// ========== 所有测试通过! ==========
四.项目模块组成
基于以上的理解,我们要实现的是⼀个带有协议支持的 Reactor 模型高性能服务器,因此将整个项目的实现划分为两个大的模块:
• Server 模块(Server.hpp):实现Reactor模型的 TCP服务器。
• 协议模块(Http.hpp):对当前的 Reactor 模型服务器提供应用层协议支持。
1. Server 模块
Server 模块是整个高性能服务器组件的"总指挥",它的职责是统一管理所有的网络连接和线程资源,确保各个部分能够协调运作、各司其职,最终构建出一个完整的高性能服务器框架。
具体来说,Server 模块的管理工作主要围绕以下三个方面展开:
1.监听连接管理 :负责管理服务端用于接收新连接的监听套接字,确保能够及时响应客户端的连接请求。
2.通信连接管理 :负责管理已建立的客户端连接,处理这些连接上的数据读写事件,保证正常通信的顺畅进行。
- 超时连接管理 :负责检测和处理那些长时间没有数据交互的超时连接,及时释放系统资源,防止资源泄露。
基于上述管理职责,我们可以将 Server 模块细分为以下几个子模块,每个子模块负责特定的管理功能。
1.1 日志宏(小组件)
这个宏方便我们打印日志进行调试。
html
// 打印日志宏
#define LOG(level, format, ...) \
do \
{ \
if (level < LOG_LEVEL) \
break; \
time_t t = time(NULL); \
struct tm *ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "[%p %s %s:%d] " format "\n", (void *)pthread_self(), tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
} while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
html
#define LOG(level, format, ...) \
do { \
// 日志逻辑
} while(0)
- do-while(0):这是一个常见的宏定义技巧,确保宏在使用时像普通语句一样工作(例如后面加
;不会出错),同时保证宏内的变量作用域正确。
html
if (level < LOG_LEVEL) \
break;
-
level:传入的日志级别(如 INF、DBG、ERR)。 -
LOG_LEVEL:应该是一个全局定义的宏,表示当前系统允许输出的最低日志级别。 -
如果当前日志级别低于系统设置级别,则直接跳出 do-while,不打印日志。
html
time_t t = time(NULL); // 获取当前时间戳
struct tm *ltm = localtime(&t); // 转换为本地时间结构
char tmp[32] = {0}; // 存储格式化后的时间字符串
strftime(tmp, 31, "%H:%M:%S", ltm); // 格式化为"时:分:秒"
-
time(NULL):获取从1970-01-01到现在的秒数。 -
localtime():将时间戳转换为本地时间的结构体。 -
strftime():将时间格式化为指定格式(这里只显示时分秒)。
html
fprintf(stdout, "[%p %s %s:%d] " format "\n",
(void *)pthread_self(), // 线程ID
tmp, // 时间字符串
__FILE__, // 文件名
__LINE__, // 行号
##__VA_ARGS__); // 可变参数
-
%p:打印线程ID(pthread_self()返回的值)。 -
%s:打印格式化后的时间。 -
%s:%d:打印文件名和行号。 -
format:用户传入的格式化字符串。 -
##__VA_ARGS__:处理可变参数。
cpp
format, ##__VA_ARGS__
-
__VA_ARGS__:代表宏定义中的...可变参数部分。 -
##:这是一个特殊语法,作用是当可变参数为空时,删除前面的逗号,避免编译错误。
html
//具体的日志宏
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
这三个宏是对**LOG**宏的封装,分别对应:
-
INF_LOG:信息级别。 -
DBG_LOG:调试级别。 -
ERR_LOG:错误级别。
调整日志等级:
html
// 想要查看所有日志
#define LOG_LEVEL INF // 0: INF、DBG、ERR 都会输出
// 只想看错误和重要信息
#define LOG_LEVEL DBG // 1: DBG 和 ERR 会输出,INF 被过滤
// 只记录错误
#define LOG_LEVEL ERR // 2: 只有 ERR 会输出

1.2 Buffer模块
Buffer模块是⼀个缓冲区模块,用于实现通信中用户态的接收缓冲区和发送缓冲区功能。
1.2.1 std::copy
std::copy 是 C++ 标准库 **<algorithm>**中的一个非常常用的算法,用于将一个容器(或范围)中的元素复制到另一个容器(或范围)中。
html
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
int src[] = {1, 2, 3, 4, 5};
int dest[5];
std::copy(std::begin(src), std::end(src), dest);
for (int x : dest) {
std::cout << x << " ";
// 输出: 1 2 3 4 5
}
return 0;
}
1.2.2 memchr
函数原型:
html
void *memchr(const void *str, int c, size_t n);
-
str:指向要执行搜索的内存块的指针 , 告诉函数"从这里开始找"。 -
c:要搜索的字符 。虽然它以 int 形式传递,但函数在搜索时,实际上会把它当作unsigned char来处理 。 -
n:要搜索的字节数 。这个参数决定了函数会检查从**str** 开始的多少个字节。它让memchr可以安全地处理那些不以 '\0' 结尾的内存数据。
html
#include <stdio.h>
#include <string.h>
int main()
{
const char str[] = "Hello, World!";
char target = 'W';
// 计算要搜索的字节数,通常用整个数组的大小
size_t n = strlen(str);
// 在 str 的前 n 个字节中搜索字符 'W'
char *ret = (char *)memchr(str, target, n);
if (ret != NULL)
{
// 如果找到,ret 指向 'W' 的位置
// 通过指针减法可以计算出它在数组中的索引
printf("找到了字符 '%c',它在索引 %ld 处。\n", target, ret - str);
// 也可以直接打印从该位置开始的字符串
printf("从该字符开始的剩余字符串是: %s\n", ret);
}
else
{
printf("未找到字符 '%c'。\n", target);
}
return 0;
}

1.2.3 Buffer类完整实现
html
#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(); }
// 获取当前写入起始地址:_buffer的空间起始地址,加上写偏移量
char *WritePosition() { return Begin() + _writer_idx; }
// 获取当前读取起始地址:_buffer的空间起始地址,加上读偏移量
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 <= HeadIdleSize() + TailIdleSize())
{
// 将数据移到起始位置
// 把当前数据大小先保存起来
uint64_t rsz = ReadAbleSize();
// 把可读数据拷贝到起始位置 std::copy
std::copy(ReadPosition(), ReadPosition() + rsz, Begin());
// 将读偏移归0
_reader_idx = 0;
// 将写位置偏移量改为可读数据大小
_writer_idx = rsz;
}
// 总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可
else
{
_buffer.resize(_writer_idx + len);
DBG_LOG("RESIZE %ld", _writer_idx + len);
}
}
// 写入数据到data所指向的一段空间
void Write(const void *data, uint64_t len)
{
// 确保有足够空间
if (len == 0)
return;
EnsureWriteSpace(len);
// 将data强转为const char* 赋值给 d
const char *d = (const char *)data;
// 拷贝数据进入d
std::copy(d, d + len, WritePosition());
}
// 写并且移动
void WriteAndPush(const void *data, uint64_t len)
{
// Write len data
Write(data, len);
// move len writeoffset
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());
}
// 写一段buffer
void WriteBuffer(Buffer &data)
{
return Write(data.ReadPosition(), data.ReadAbleSize());
}
// 写一段buffer并移动
void WriteBufferAndPush(Buffer &data)
{
WriteBuffer(data);
MoveWriteOffset(data.ReadAbleSize());
}
// 从某段空间读取数据
void Read(void *buf, uint64_t len)
{
// 要求要获取的数据大小必须小于可读数据大小
assert(len <= ReadAbleSize());
// 把ReadPosition 到 ReadPosition + len 拷贝到强转为 char* 的buf
std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
}
// 从某段空间读取数据然后移动读偏移
void ReadAndPop(void *buf, uint64_t len)
{
Read(buf, len);
MoveReadOffset(len);
}
// 读 len 长度的缓冲区内容 作为 string 返回
std::string ReadAsString(uint64_t len)
{
// 要求要获取的数据大小必须小于可读数据大小
assert(len <= ReadAbleSize());
std::string str;
// str resize
str.resize(len);
// 读取len长度到str[0]
Read(&str[0], len);
// 返回str
return str;
}
// 读 len 长度的缓冲区内容 作为 string 返回并移动读偏移
std::string ReadAsStringAndPop(uint64_t len)
{
// 要求要获取的数据大小必须小于可读数据大小
assert(len <= ReadAbleSize());
std::string str = ReadAsString(len);
MoveReadOffset(len);
return str;
}
// 找'\n'
char *FindCRLF()
{
// memchr字符串查找函数
char *res = (char *)memchr(ReadPosition(), '\n', ReadAbleSize());
return res;
}
// 获取一行数据
std::string GetLine()
{
// 先查找 '\n'
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;
}
// 往前追加数据 - 在可读数据前面插入新数据
void Prepend(const void *data, uint64_t len)
{
if (len == 0)
return;
// 检查头部空闲空间是否足够
if (HeadIdleSize() >= len)
{
// 头部空间足够,直接在读指针前面写入
_reader_idx -= len;
std::copy((const char *)data, (const char *)data + len, ReadPosition());
}
else
{
// 头部空间不足,需要整体移动数据或扩容
uint64_t readable = ReadAbleSize();
uint64_t new_total = readable + len;
// 如果整体空间足够,只是头部空间不够
if (_buffer.size() >= new_total)
{
// 将现有可读数据向后移动 len 个位置
std::copy_backward(ReadPosition(), ReadPosition() + readable,
Begin() + new_total);
_reader_idx = 0;
_writer_idx = new_total;
// 在新位置写入前置数据
std::copy((const char *)data, (const char *)data + len, Begin());
}
else
{
// 整体空间不够,需要扩容
std::vector<char> new_buffer(new_total); // 先将前置数据写入新缓冲区的开头
std::copy((const char *)data, (const char *)data + len, new_buffer.begin());
// 再将原有可读数据写入新缓冲区
std::copy(ReadPosition(), ReadPosition() + readable,
new_buffer.begin() + len);
_buffer.swap(new_buffer);
_reader_idx = 0;
_writer_idx = new_total;
}
}
}
// 往前追加字符串
void PrependString(const std::string &data)
{
Prepend(data.c_str(), data.size());
}
};
1.2.4 Buffer类接口测试
html
#include <iostream>
#include <cassert>
#include <string>
#include <cstring>
#include "../source/server.hpp"
void testBufferInitialization()
{
std::cout << "=== 测试初始化 ===" << std::endl;
Buffer buf;
assert(buf.ReadAbleSize() == 0);
assert(buf.TailIdleSize() == BUFFER_DEFAULT_SIZE);
assert(buf.HeadIdleSize() == 0);
assert(buf.Begin() != nullptr);
std::cout << " 初始化测试通过 " << std::endl;
}
void testWriteAndRead()
{
std::cout << "=== 测试网buffer中写入和读取 ===" << std::endl;
Buffer buf;
// 测试写入字符串
std::string testStr = "Hello, Buffer!";
buf.WriteStringAndPush(testStr);
assert(buf.ReadAbleSize() == testStr.size());
assert(buf.TailIdleSize() == BUFFER_DEFAULT_SIZE - testStr.size());
// 测试读取
char readBuf[100] = {0};
buf.ReadAndPop(readBuf, testStr.size());
assert(std::string(readBuf) == testStr);
assert(buf.ReadAbleSize() == 0);
std::cout << "写入和读取测试通过" << std::endl;
}
void testEnsureWriteSpace()
{
std::cout << "=== 测试空间管理 ===" << std::endl;
Buffer buf;
// 测试1:写入大量数据,触发扩容
std::string largeStr(BUFFER_DEFAULT_SIZE * 2, 'A');
buf.WriteStringAndPush(largeStr);
assert(buf.ReadAbleSize() == largeStr.size());
assert(buf.TailIdleSize() >= 0);
// 测试2:读取一部分,然后写入新数据,触发数据移动
char readBuf[100] = {0};
buf.ReadAndPop(readBuf, 100); // 读取100字节
size_t beforeWrite = buf.ReadAbleSize();
std::string newStr = "New data after read";
buf.WriteStringAndPush(newStr);
// 验证数据完整性
std::string remaining = buf.ReadAsStringAndPop(buf.ReadAbleSize());
assert(remaining.find(newStr) != std::string::npos);
std::cout << "空间管理测试通过" << std::endl;
}
void testFindCRLFAndGetLine()
{
std::cout << "=== 测试查找换行符和获取行 ===" << std::endl;
Buffer buf;
// 写入多行数据
buf.WriteStringAndPush("第一行\n");
buf.WriteStringAndPush("第二行\n");
buf.WriteStringAndPush("第三行");
// 测试获取第一行
std::string line1 = buf.GetLineAndPop();
assert(line1 == "第一行\n");
// 测试获取第二行
std::string line2 = buf.GetLineAndPop();
assert(line2 == "第二行\n");
// 测试获取第三行(没有换行符)
std::string line3 = buf.GetLine();
assert(line3 == ""); // 因为没有换行符
// 手动读取第三行
std::string line3_manual = buf.ReadAsStringAndPop(buf.ReadAbleSize());
assert(line3_manual == "第三行");
std::cout << "查找换行符和获取行测试通过" << std::endl;
}
void testBufferToBuffer()
{
std::cout << "=== 测试Buffer间操作 ===" << std::endl;
Buffer buf1;
Buffer buf2;
// 向buf1写入数据
std::string testData = "Buffer to Buffer transfer";
buf1.WriteStringAndPush(testData);
// 将buf1的数据写入buf2
buf2.WriteBufferAndPush(buf1);
assert(buf2.ReadAbleSize() == testData.size());
// 从buf2读取验证
std::string result = buf2.ReadAsStringAndPop(testData.size());
assert(result == testData);
// 验证buf1的数据还在(因为WriteBuffer不会移动buf1的读偏移)
assert(buf1.ReadAbleSize() == testData.size());
std::cout << "Buffer间操作测试通过" << std::endl;
}
void testEdgeCases()
{
std::cout << "=== 测试边界情况 ===" << std::endl;
Buffer buf;
// 测试空写入
buf.WriteStringAndPush("");
assert(buf.ReadAbleSize() == 0);
// 测试写入0长度
buf.Write(nullptr, 0);
assert(buf.ReadAbleSize() == 0);
// 测试读取0长度
char tmp[10];
buf.Read(tmp, 0);
// 测试清空
buf.WriteStringAndPush("Some data");
assert(buf.ReadAbleSize() > 0);
buf.Clear();
assert(buf.ReadAbleSize() == 0);
assert(buf.HeadIdleSize() == 0);
assert(buf.TailIdleSize() == BUFFER_DEFAULT_SIZE);
std::cout << "边界情况测试通过" << std::endl;
}
void testMoveOperations()
{
std::cout << "=== 测试偏移量移动 ===" << std::endl;
Buffer buf;
// 写入数据
buf.WriteStringAndPush("1234567890");
uint64_t oldRead = buf.ReadAbleSize();
uint64_t oldTail = buf.TailIdleSize();
// 移动读偏移 5 位
buf.MoveReadOffset(5);
assert(buf.ReadAbleSize() == oldRead - 5);
assert(buf.HeadIdleSize() == 5);
// 验证数据完整性 - 读取剩余数据
std::string remaining = buf.ReadAsStringAndPop(buf.ReadAbleSize());
assert(remaining == "67890");
std::cout << "偏移量移动测试通过" << std::endl;
}
int main()
{
try
{
testBufferInitialization();
testWriteAndRead();
testEnsureWriteSpace();
testFindCRLFAndGetLine();
testBufferToBuffer();
testEdgeCases();
testMoveOperations();
std::cout << "\n所有测试通过!" << std::endl;
}
catch (const std::exception &e)
{
std::cerr << "测试失败: " << e.what() << std::endl;
return -1;
}
return 0;
}
//测试结果
/*=== 测试初始化 ===
初始化测试通过
=== 测试网buffer中写入和读取 ===
写入和读取测试通过
=== 测试空间管理 ===
空间管理测试通过
=== 测试查找换行符和获取行 ===
查找换行符和获取行测试通过
=== 测试Buffer间操作 ===
Buffer间操作测试通过
=== 测试边界情况 ===
边界情况测试通过
=== 测试偏移量移动 ===
偏移量移动测试通过
所有测试通过!
*/
1.3 Socket模块
Socket模块是对套接字操作封装的⼀个模块,主要实现对socket的各项操作。
1.3.1 socket函数
这是一个非常核心的系统调用,用于创建一个通信端点(一个套接字)。它是网络编程以及 UNIX/Linux 系统上本地进程间通信的基础。
函数原型:
html
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数详解:
int domain (协议域/地址族)
指定套接字使用的通信介质和地址格式。通俗地说,就是决定这个套接字是要在网络里用,还是在本机用,以及 IP 地址是哪一种。
-
AF_INET:最常用 。IPv4 网络协议,地址是**ipv4地址 + 端口号**的组合。 -
AF_INET6:IPv6 网络协议。
int type (套接字类型)
-
SOCK_STREAM:面向连接的、可靠的、基于字节流的。-
特性:数据没有边界(就像水流,写 3 次可能一次读完),保证按序到达,无差错,无重复。
-
典型协议:TCP。
-
场景:需要可靠传输的场景,如 HTTP 网页浏览、文件下载。
-
-
SOCK_DGRAM:无连接的、不可靠的、基于数据报的。-
特性:数据有边界(发送 3 个包,必须接收 3 次,每次读一个完整包),大小受限,但传输效率高,不保证顺序和可靠性(丢包不管)。
-
典型协议:UDP。
-
场景:实时音视频通话、DNS 查询、游戏数据包。
-
-
SOCK_RAW:原始套接字。-
特性:允许直接操作网络层(如 IP 层)或链路层的数据包,用户可以自定义包头。
-
需要超级管理员权限才能创建。
-
场景:Ping 命令(ICMP)、路由协议、网络嗅探工具。
-
-
其他类型:
SOCK_SEQPACKET(有序可靠报文,较少用)等。
int protocol (协议)
指定实际使用的具体传输协议。通常,对于给定的 domain 和 type ,只有一种协议是标准的。此时可以填 0,由操作系统自动推导。
-
0 :让内核根据
domain和type自动选择默认协议(通常是正确的)。 -
IPPROTO_TCP:显式指定使用 TCP 协议(与 **SOCK_STREAM**配合)。 -
IPPROTO_UDP:显式指定使用 UDP 协议(与**SOCK_DGRAM**配合)。 -
IPPROTO_ICMP:与 **SOCK_RAW**配合,用于发送 ICMP 报文。
返回值
-
成功 :返回一个非负整数,这个数字叫作文件描述符。
-
失败 :返回 -1 ,并设置全局变量 **
errno**以指示错误原因。
在 UNIX/Linux 系统中,"一切皆文件"是一个核心设计哲学。根据这一理念,包括套接字在内的多种输入/输出资源均被抽象为文件描述符。套接字作为网络通信的端点,在创建后同样以一个整型文件描述符的形式返回给应用程序。该抽象使得后续的网络数据收发及连接管理操作,能够通过标准的文件 I/O 系统调用(如 read(), write(), close())或专门的网络通信函数(如 send(), recv())来完成。这些操作均依赖于最初获得的文件描述符,从而实现了统一的、简洁的编程接口。
典型组合示例
| 目标 | domain | type | protocol | 解释 |
|---|---|---|---|---|
| TCP Socket (IPv4) | AF_INET |
SOCK_STREAM |
0 | 创建一个 IPv4 的 TCP 套接字。 |
| UDP Socket (IPv6) | AF_INET6 |
SOCK_DGRAM |
0 | 创建一个 IPv6 的 UDP 套接字。 |
| Unix Domain Socket (流式) | AF_UNIX |
SOCK_STREAM |
0 | 在本机两个进程间建立可靠的字节流通信。 |
| Unix Domain Socket (数据报) | AF_UNIX |
SOCK_DGRAM |
0 | 在本机两个进程间建立报文通信。 |
| 原始 ICMP Socket | AF_INET |
SOCK_RAW |
IPPROTO_ICMP |
创建原始套接字,用来发送 Ping 包。 |
Create函数实现:

1.3.2 bind函数
bind() 是一个非常重要的系统调用,它的作用是将一个本地协议地址(通常是 IP 地址 + 端口号)与一个套接字文件描述符绑定在一起。简单来说,就是给这个套接字一个"固定的身份"。
函数原型:
html
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解:
int sockfd
-
这是之前调用 **
socket()**返回的文件描述符。 -
表示你想要绑定的那个套接字。
const struct sockaddr *addr
这是一个指向协议地址结构的指针。关键点:这是一个通用指针类型,实际传入的是特定协议族的地址结构。
不同协议族对应的实际结构:
| 协议族 | 实际结构 | 说明 |
|---|---|---|
| AF_INET (IPv4) | struct sockaddr_in |
IPv4 地址 + 端口 |
| AF_INET6 (IPv6) | struct sockaddr_in6 |
IPv6 地址 + 端口 + 流信息等 |
| AF_UNIX (本地) | struct sockaddr_un |
socket 文件路径 |
IPv4 示例 (struct sockaddr_in):
struct sockaddr_in {
sa_family_t sin_family; // 地址族,AF_INET
in_port_t sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IPv4地址
char sin_zero[8]; // 填充,保持与sockaddr同大小
};
使用示例:
html
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 绑定到端口8080
addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有网卡
socklen_t addrlen
指定 addr 结构体的大小,通常使用**sizeof(addr)**。
Bind函数实现:

1.3.3 listen函数
**listen()**是一个关键的系统调用,它的作用是将一个主动套接字(主动用于连接的)转换为被动套接字(被动等待连接的),并设置连接请求队列的最大长度。
函数原型:
html
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数详解:
int sockfd
-
这是之前通过
socket()创建、通过 **bind()**绑定了地址的套接字文件描述符。 -
注意:这个套接字必须是
SOCK_STREAM类型(TCP)。
int backlog
这是最关键也最容易误解的参数,它指定了已完成连接队列的最大长度。

-
未完成连接队列(incomplete queue):
-
收到了 SYN,正在等待完成三次握手的连接。
-
状态:
SYN_RCVD(半连接)。
-
-
已完成连接队列(completed queue):
-
已完成三次握手,等待应用程序调用
accept()取走的连接。 -
状态:
ESTABLISHED。 -
backlog参数主要控制这个队列的大小。
-
Listen函数实现:

1.3.4 connect函数
connect() 是客户端用来建立连接的系统调用。对于 TCP 来说,它发起三次握手;对于 UDP 来说,它只是绑定对端地址。
函数原型:
html
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解:
int sockfd
-
之前通过**
socket()** 创建的套接字文件描述符。 -
可以是 TCP 或 UDP 套接字。
const struct sockaddr *addr
-
要连接的服务端地址(IP + 端口)。
-
结构和**
bind()**完全一样。
socklen_t addrlen
- 地址结构的大小。
Connect函数实现

1.3.5 accept函数
accept() 是服务器端用来接受连接请求 的系统调用。它从已完成连接队列中取出第一个连接,返回一个新的套接字文件描述符用于与客户端通信。
函数原型:
html
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数详解:
int sockfd
-
监听套接字(通过 **
socket()+bind()+listen()**得到的)。 -
这个套接字继续监听,不会用于数据传输。
struct sockaddr *addr
-
输出参数,用于获取客户端的地址信息(IP 和端口)。
-
如果不关心客户端地址,可以设为NULL
。
socklen_t *addrlen
-
值-结果参数
-
调用前:传入 **
addr**缓冲区的大小。 -
调用后:内核写入实际的地址结构大小。
Accept函数实现

1.3.6 recv函数
recv() 是用于从已连接的套接字接收数据 的系统调用。它是**read()**的增强版本,提供了更多的控制和选项。
函数原型:
html
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数详解:
int sockfd
-
已连接的套接字文件描述符(TCP)或已绑定的套接字(UDP)。
-
必须是通过**
accept()** (服务器端)或**connect()**(客户端)得到的套接字。
void *buf
-
接收数据缓冲区的指针。
-
函数会将接收到的数据写入这里。
size_t len
-
缓冲区的大小。
-
recv()最多读取 **len**字节。
int flags
这是 recv() 相对于 read() 的最大优势,可以通过以下标志的按位或组合来控制接收行为:
| 标志 | 含义 | 说明 |
|---|---|---|
MSG_DONTWAIT |
非阻塞 | 如果没有数据,立即返回 EAGAIN,不阻塞 |
MSG_WAITALL |
等待所有数据 | 阻塞直到收到 len 字节(除非被信号中断或连接关闭) |
MSG_PEEK |
窥探数据 | 查看数据但不从缓冲区移除(下次 recv 还能读到) |
MSG_OOB |
带外数据 | 接收紧急数据(out-of-band data) |
MSG_TRUNC |
返回真实长度 | 对于 UDP,返回数据报的真实长度,即使缓冲区太小 |
MSG_ERRQUEUE |
错误队列 | 从错误队列接收错误 |
返回值
-
成功:返回接收到的字节数。
-
对方关闭连接 :返回 0(TCP 的 FIN 标志)。
-
失败 :返回 -1 ,并设置**
errno**。
常见返回值场景
| 返回值 | 含义 | 处理方式 |
|---|---|---|
> 0 |
成功收到数据 | 处理数据 |
0 |
对方关闭连接 | 关闭套接字 |
-1 |
错误 | 检查 errno |
常见错误
| errno | 含义 | 原因 |
|---|---|---|
EAGAIN/EWOULDBLOCK |
暂无数据 | 非阻塞模式下,没有数据可读 |
EINTR |
系统调用中断 | 被信号中断,通常需要重试 |
ECONNRESET |
连接重置 | 对方崩溃或强制关闭 |
ENOTCONN |
未连接 | 套接字未连接 |
Recv函数的实现

1.3.7 send函数
send() 是用于向已连接的套接字发送数据 的系统调用。它是 write() 的增强版本,提供了更多的控制和选项。
函数原型:
html
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数详解:
int sockfd
- 已连接的套接字文件描述符(TCP)或已连接/已绑定的套接字(UDP)。
const void *buf
-
要发送的数据缓冲区的指针。
-
注意是**
const** ,表示**send()**不会修改你的数据。
size_t len
- 要发送的字节数。
int flags
send() 的标志位,可以通过按位或组合使用:
| 标志 | 含义 | 说明 |
|---|---|---|
MSG_DONTWAIT |
非阻塞 | 如果不能立即发送,返回 EAGAIN,不阻塞 |
MSG_NOSIGNAL |
不触发 SIGPIPE | 连接断开时不发送 SIGPIPE 信号,而是返回 EPIPE |
MSG_OOB |
带外数据 | 发送紧急数据(TCP 紧急指针) |
MSG_MORE |
还有更多数据 | TCP_CORK 的替代,告诉内核还有数据要发,不要立即发送 |
MSG_CONFIRM |
链路层确认 | (仅用于某些协议)防止 ARP 缓存过期 |
Send函数实现

1.3.8 setsockopt函数
setsockopt() 是用于控制套接字行为的系统调用。它允许你在各个协议层设置选项,是网络编程中非常重要的调优工具。
函数原型:
cpp
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
参数详解:
int sockfd
- 要设置的套接字文件描述符。
int level - 协议层
指定选项所在的协议层:
| level | 含义 | 说明 |
|---|---|---|
SOL_SOCKET |
套接字层 | 通用的套接字选项 |
IPPROTO_TCP |
TCP 层 | TCP 协议特定选项 |
IPPROTO_IP |
IP 层 | IPv4 协议特定选项 |
IPPROTO_IPV6 |
IPv6 层 | IPv6 协议特定选项 |
IPPROTO_UDP |
UDP 层 | UDP 协议特定选项 |
int optname - 选项名
取决于**level** 参数,具体见下表:
SOL_SOCKET 层选项(最常用)
| optname | optval 类型 | 说明 |
|---|---|---|
SO_REUSEADDR |
int | 允许重用本地地址(解决 TIME_WAIT) |
SO_KEEPALIVE |
int | 开启 TCP 保活机制 |
SO_LINGER |
struct linger | 设置 close() 的行为 |
SO_SNDBUF |
int | 设置发送缓冲区大小 |
SO_RCVBUF |
int | 设置接收缓冲区大小 |
SO_RCVTIMEO |
struct timeval | 设置接收超时 |
SO_SNDTIMEO |
struct timeval | 设置发送超时 |
SO_BROADCAST |
int | 允许发送广播消息(UDP) |
SO_OOBINLINE |
int | 将带外数据放入普通输入队列 |
SO_DONTROUTE |
int | 绕过路由表,直接发送 |
const void *optval
-
指向选项值的指针。
-
类型取决于具体选项(可能是 int、struct、char 等)。
socklen_t optlen
- **
optval**指向的数据的大小。
ReuseAddr函数实现

高性能 TCP 服务器配置往往需要启用地址端口重用。
1.3.9 fcntl函数
fcntl() 是 文件控制 系统调用,可以对打开的文件描述符执行各种操作。在网络编程中,它主要用于设置非阻塞模式 和修改文件描述符属性。
函数原型:
html
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
-
成功 :返回值取决于
cmd。 -
失败 :返回 -1 ,并设置
errno。
常用命令 (cmd)
文件描述符操作:
| cmd | 含义 | arg类型 | 返回值 |
|---|---|---|---|
F_GETFL |
获取文件状态标志 | 无 | 成功返回标志值 |
F_SETFL |
设置文件状态标志 | int | 成功返回0 |
文件锁操作:
| cmd | 含义 | arg类型 |
|---|---|---|
F_GETLK |
获取锁信息 | struct flock* |
F_SETLK |
设置锁(非阻塞) | struct flock* |
F_SETLKW |
设置锁(阻塞) | struct flock* |
文件描述符复制:
| cmd | 含义 | arg类型 |
|---|---|---|
F_DUPFD |
复制描述符 | int(最小新描述符号) |
F_DUPFD_CLOEXEC |
复制并设置 FD_CLOEXEC | int |
文件状态标志
通过 F_GETFL 和 F_SETFL 可以操作的标志:
| 标志 | 含义 | 网络编程用途 |
|---|---|---|
O_NONBLOCK |
非阻塞模式 | 最常用,设置套接字为非阻塞 |
O_ASYNC |
异步 I/O | 当可读/可写时发送 SIGIO 信号 |
O_APPEND |
追加模式 | 很少用于网络编程 |
O_DIRECT |
直接 I/O | 绕过缓存,用于磁盘文件 |
O_NOATIME |
不更新访问时间 | 不常用 |
NonBlock函数实现

1.3.10 Socket类完整实现
html
// 最大监听数量
#define MAX_LISTEN 1024
// 通信socket
class Socket
{
private:
int _sockfd;
public:
// 无参构造
Socket() : _sockfd(-1) {}
// fd构造
Socket(int fd) : _sockfd(fd) {}
// 析构 调用Close
~Socket() { Close(); }
// 返回Fd
int Fd() { return _sockfd; }
// 禁用拷贝构造和拷贝赋值
Socket(const Socket &) = delete;
Socket &operator=(const Socket &) = delete;
// 启用移动构造和移动赋值
Socket(Socket &&other) noexcept : _sockfd(other._sockfd)
{
other._sockfd = -1;
}
Socket &operator=(Socket &&other) noexcept
{
if (this != &other)
{
Close();
_sockfd = other._sockfd;
other._sockfd = -1;
}
return *this;
}
// 创建套接字
bool Create()
{
// int socket(int domin,int type,int protocol)
// 基于TCP面向字节流
_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 socketfd,struct sockaddr* addr,socklen_t len)
int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0)
{
ERR_LOG("BIND ADDRESS FAILED!!");
std::cout << errno << std::endl;
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 socketfd,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)
// flag = 0 默认阻塞接收
ssize_t ret = recv(_sockfd, buf, len, flag);
if (ret <= 0)
{
// EAGAIN 表示当前socket的接收缓冲区里面没有数据了,在非阻塞的情况下才会有这个错误
// EINTR 表示当前socket的阻塞等待被信号打断了
// std::cout << ret << std::endl;
// std::cout << strerror(errno) << std::endl;
// DBG_LOG("recv return: %ld, errno: %d (%s)", ret, errno, strerror(errno));
// Transport endpoint is not connected 传输端点未连接 ENOTCONN
if (errno == EAGAIN || errno == EINTR)
{
// 表示此次没有接收到数据
return 0;
}
ERR_LOG("SOCKET RECV FAILED !! ");
return -1;
}
// 返回实际接收的数据长度
return ret;
}
ssize_t NonBlockRecv(void *buf, size_t len)
{
// MSG_DONTWAIT 表示当前接收为非阻塞
return Recv(buf, len, 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)
{
// EAGAIN 表示当前socket的接收缓冲区里面没有数据了,在非阻塞的情况下才会有这个错误
// EINTR 表示当前socket的阻塞等待被信号打断了
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;
// MSG_DONTWAIT 表示当前接收为非阻塞
return Send(buf, len, 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;
ReuseAddr();
if (block_flag)
NonBlock();
if (Bind(ip, port) == false)
return false;
if (Listen() == false)
return false;
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 ReuseAddr()
{
// 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);
}
};
1.3.11 Socket类测试接口
1.3.11.1 /单个/
html
#include <iostream>
#include <thread>
#include <chrono>
#include <cstring>
#include "../source/server.hpp"
// 测试Socket类
void TestSocket()
{
std::cout << "========== 开始测试Socket类 ==========" << std::endl;
// 测试1:默认构造和析构
{
std::cout << "测试1:默认构造" << std::endl;
Socket s1;
std::cout << "默认socket fd: " << s1.Fd() << std::endl;
// 析构时自动调用Close
}
// 测试2:创建套接字
std::cout << "\n测试2:创建套接字" << std::endl;
Socket s2;
if (s2.Create())
{
std::cout << "套接字创建成功,fd: " << s2.Fd() << std::endl;
}
else
{
std::cout << "套接字创建失败" << std::endl;
}
// 测试3:绑定地址
std::cout << "\n测试3:绑定地址" << std::endl;
Socket s3;
if (s3.Create() && s3.Bind("127.0.0.1", 8080))
{
std::cout << "绑定成功" << std::endl;
}
// 测试4:监听
std::cout << "\n测试4:监听" << std::endl;
Socket s4;
if (s4.Create() && s4.Bind("127.0.0.1", 8081) && s4.Listen(5))
{
std::cout << "监听成功" << std::endl;
}
// 测试5:地址重用设置
std::cout << "\n测试5:地址重用" << std::endl;
Socket s5;
s5.Create();
s5.ReuseAddr();
std::cout << "地址重用设置完成" << std::endl;
// 测试6:非阻塞设置
std::cout << "\n测试6:非阻塞设置" << std::endl;
Socket s6;
s6.Create();
s6.NonBlock();
std::cout << "非阻塞设置完成" << std::endl;
// 测试7:CreateServer完整功能
std::cout << "\n测试7:CreateServer完整功能" << std::endl;
Socket server;
if (server.CreateServer(8082, "0.0.0.0", false))
{
std::cout << "服务器创建成功,监听端口8082" << std::endl;
}
// 测试8:客户端连接(需要配合服务器线程)
std::cout << "\n测试8:客户端连接测试" << std::endl;
// 创建服务器线程
Socket server2;
bool server_running = server2.CreateServer(8083, "0.0.0.0", false);
if (!server_running)
{
std::cout << "服务器创建失败" << std::endl;
return;
}
std::thread server_thread([&server2]()
{
std::cout << "服务器等待连接..." << std::endl;
int client_fd = server2.Accept();
if (client_fd > 0) {
std::cout << "服务器接受新连接,fd: " << client_fd << std::endl;
Socket client_socket(client_fd);
char buffer[1024] = {0};
ssize_t n = client_socket.Recv(buffer, sizeof(buffer));
if (n > 0) {
std::cout << "服务器收到: " << buffer << std::endl;
const char* response = "Hello from server";
client_socket.Send(response, strlen(response));
}
} });
// 稍等服务器启动
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 客户端连接
Socket client;
if (client.CreateClient(8083, "127.0.0.1"))
{
std::cout << "客户端连接成功" << std::endl;
const char *message = "Hello from client";
client.Send(message, strlen(message));
char buffer[1024] = {0};
ssize_t n = client.Recv(buffer, sizeof(buffer));
if (n > 0)
{
std::cout << "客户端收到: " << buffer << std::endl;
}
}
else
{
std::cout << "客户端连接失败" << std::endl;
}
server_thread.join();
// 测试9:非阻塞接收
std::cout << "\n测试9:非阻塞接收" << std::endl;
Socket s9;
s9.Create();
s9.NonBlock();
char buf[1024];
ssize_t ret = s9.NonBlockRecv(buf, sizeof(buf));
// 为-1说明没有数据
std::cout << "非阻塞接收返回值: " << ret << std::endl;
// 测试10:关闭套接字
std::cout << "\n测试10:关闭套接字" << std::endl;
Socket s10;
s10.Create();
std::cout << "关闭前fd: " << s10.Fd() << std::endl;
s10.Close();
std::cout << "关闭后fd: " << s10.Fd() << std::endl;
std::cout << "\n========== 测试完成 ==========" << std::endl;
}
int main()
{
TestSocket();
return 0;
}
1.3.11.2 /双通信/
test_socket_server.cpp
html
#include "../source/server.hpp"
int main()
{
// 1. 创建服务器套接字
Socket listen_sock;
if (!listen_sock.CreateServer(8080, "0.0.0.0")) {
std::cerr << "Failed to create server on port 8080" << std::endl;
return -1;
}
DBG_LOG("Server started, listening on port 8080...");
// 2. 等待客户端连接
DBG_LOG("Waiting for client connection...");
int client_fd = listen_sock.Accept();
if (client_fd < 0) {
std::cerr << "Failed to accept client" << std::endl;
return -1;
}
Socket client_sock(client_fd);
DBG_LOG("Client connected!");
// 3. 与客户端通信
char buf[1024];
while (true) {
// 接收客户端消息
memset(buf, 0, sizeof(buf));
ssize_t ret = client_sock.Recv(buf, sizeof(buf) - 1);
if (ret > 0) {
// 打印收到的消息
DBG_LOG("Received from client: %s", buf);
// 构造响应消息
std::string response = "Server received: " + std::string(buf);
// 发送响应给客户端
client_sock.Send(response.c_str(), response.size());
DBG_LOG("Sent response to client");
}
else if (ret == 0) {
// 客户端关闭连接
DBG_LOG("Client closed connection");
break;
}
else {
// 接收出错
DBG_LOG("Error receiving data");
break;
}
}
// 4. 清理
client_sock.Close();
listen_sock.Close();
DBG_LOG("Server shutdown");
return 0;
}
test_socket_client.cpp
html
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8080, "127.0.0.1");
for (int i = 0; i < 5; i++) {
std::string str = "hello hello";
cli_sock.Send(str.c_str(), str.size());
char buf[1024] = {0};
cli_sock.Recv(buf, 1023);
DBG_LOG("%s", buf);
sleep(1);
}
while(1) sleep(1);
return 0;
}
makefile
cpp
all:server client socket
server:test_socket_server.cpp
g++ -std=c++11 $^ -o $@
client:test_socket_client.cpp
g++ -std=c++11 $^ -o $@
socket:test_socket.cpp
g++ -std=c++11 $^ -o $@
.PHONY:clean
clean:
rm -f server client socket





1.4 Poller模块
Poller模块是对epoll进行封装的⼀个模块,主要实现epoll对IO事件添加,修改,移除,获取活跃连接功能。
1.4.1 epoll事件监控机制
epoll是Linux下高效处理大规模并发连接的I/O事件通知机制,是select/poll的增强替代方案。它通过内核中的事件通知和就绪列表,解决了传统模型在描述符数量和轮询效率上的瓶颈。
1.4.1.1 核心机制与优势
epoll的高性能主要源于其内核数据结构的设计。
-
核心数据结构 :每个epoll实例在内核中都对应一个**
eventpoll**对象,它主要包含:-
红黑树 (rbt):用于存储所有被监控的文件描述符及其事件。这使得添加、删除、修改描述符的操作都非常高效(时间复杂度O(log N))。
-
就绪列表 (rdllist):一个双向链表,用于存放那些发生了I/O事件的文件描述符。
-
-
工作原理 :当调用**
epoll_wait** 时,内核只需检查就绪列表中是否有元素,如果有则直接返回,无需遍历整个监控列表。这是epoll在描述符数量巨大但活跃连接少时,性能远超select/poll的根本原因。数据就绪时,内核通过回调机制将对应的描述符放入就绪列表。
1.4.1.2 三大系统调用
epoll机制通过以下三个函数协同工作:
| 函数 | 描述 | 关键点 |
|---|---|---|
epoll_create() |
创建一个epoll实例,返回一个指向该实例的文件描述符(句柄)。 | 后续所有操作都通过此文件描述符进行。 |
epoll_ctl() |
对epoll实例的兴趣列表进行管理,包括添加(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)和删除(EPOLL_CTL_DEL)被监控的文件描述符及其事件。 |
指定要监听的事件类型,如EPOLLIN(可读)、EPOLLOUT(可写)等。 |
epoll_wait() |
等待I/O事件的发生。它阻塞调用线程,直到监控的文件描述符上有事件发生或超时。 | 返回就绪列表中的事件数量及具体事件,供程序处理。 |
1.4.1.3 两种触发模式:LT 与 ET
这是epoll的核心概念,决定了事件通知的方式。
| 特性 | 水平触发 (LT, Level-Triggered) | 边缘触发 (ET, Edge-Triggered) |
|---|---|---|
| 工作方式 | 只要文件描述符上有数据可读(或可写),epoll_wait就会一直返回该事件。 |
仅在文件描述符的状态发生变化时才通知事件。例如,当数据从无到有时,只会触发一次可读事件。 |
| 编程特点 | 默认工作模式,编程简单,不易出错。可以不一次性读完所有数据,下次调用epoll_wait会再次提醒。 |
高速工作模式,编程复杂。要求使用非阻塞文件描述符,并且必须在收到事件后循环读写直到返回EAGAIN错误,否则可能遗漏事件。 |
| 类比 | 就像家门口的门铃一直响,直到你开门。 | 就像门铃只在有人第一次按响时才响,如果你没及时开门,它不会再响。 |
| 性能 | 相对较低,但更稳健。 | 相对更高,因为减少了事件被重复通知的次数,但若使用不当容易出错。 |
| 适用场景 | 对编程简洁性要求高,或无法保证一次性读写完所有数据的场景,如许多通用网络库。 | 对并发性能要求极高的场景,追求最大吞吐量。 |
1.4.2 epoll_ctl
这是epoll机制的核心控制函数,它负责所有监控描述符的管理操作。
函数原型:
html
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
参数详解
epfd - epoll实例的文件描述符
-
由
epoll_create()或epoll_create1()返回。 -
标识要进行操作的epoll实例。
op - 操作类型(三个宏定义)
| 宏 | 值 | 含义 | 典型场景 |
|---|---|---|---|
EPOLL_CTL_ADD |
1 | 添加一个新的文件描述符到epoll实例 | 新连接接入时 |
EPOLL_CTL_MOD |
2 | 修改已存在的文件描述符的监控事件 | 改变监听事件类型时 |
EPOLL_CTL_DEL |
3 | 删除一个文件描述符从epoll实例 | 连接关闭时 |
fd - 目标文件描述符
-
需要被监控的文件描述符(socket**、pipe、****eventfd等)**。
-
对于**
EPOLL_CTL_ADD**:要加入监控的fd。 -
对于**
EPOLL_CTL_MOD/EPOLL_CTL_DEL**:已存在的fd。
ev - 事件结构体指针
struct epoll_event {
uint32_t events; /* 需要监听的事件(位掩码) */
epoll_data_t data; /* 用户自定义数据(关键!) */
};
typedef union epoll_data {
void *ptr; /* 指针,可以指向任意数据结构 */
int fd; /* 最常用:直接存储文件描述符 */
uint32_t u32; /* 32位整数值 */
uint64_t u64; /* 64位整数值 */
} epoll_data_t;
返回值
-
成功 :返回
0。 -
失败 :返回
-1,并设置 **errno**指示错误原因。
常见错误码
| errno | 含义 | 可能原因 |
|---|---|---|
EBADF |
无效的文件描述符 | epfd 或 fd 不是有效的fd |
EEXIST |
已存在 | EPOLL_CTL_ADD 但fd已经在epoll中 |
ENOENT |
不存在 | EPOLL_CTL_MOD/DEL 但fd不在epoll中 |
ENOMEM |
内存不足 | 内核内存不足 |
ENOSPC |
已达上限 | /proc/sys/fs/epoll/max_user_watches 限制 |
Update函数实现

1.4.3 Poller类完整实现
html
//封装 epoll 系统调用,是事件监听的核心
#define MAX_EPOLLEVENTS 1024
class Poller
{
private:
int _epfd;
//epoll管理的事件
struct epoll_event _evs[MAX_EPOLLEVENTS];
//建立fd 到 Channel 的映射
std::unordered_map<int, Channel *> _channels;
private:
// 总的Update -- 对epoll的直接操作
void Update(Channel *channel, int op)
{
// int epoll_ctl(int epfd,int op,int fd,struct epoll_event* ev);
// 从channel中获取fd
int fd = channel->Fd();
// 定义struct epoll_event
struct epoll_event ev;
// 把fd channel->Events() 设置进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无参构造
Poller()
{
// 创建epoll 得到_epfd
_epfd = epoll_create(MAX_EPOLLEVENTS);
if (_epfd < 0)
{
ERR_LOG("EPOLLFD CREATE FAILED!!");
}
}
// 添加或修改监控事件
void UpdateEvent(Channel *channel)
{
// 先判断channel是否在_channels中
bool ret = HasChannel(channel);
if (ret == false)
{
// 不存在则添加
_channels.insert(std::make_pair(channel->Fd(), channel));
return Update(channel, EPOLL_CTL_ADD);
}
// 存在则修改EPOLL_CTL_MOD
return Update(channel, EPOLL_CTL_MOD);
}
// 移除对事件的监控
void RemoveEvent(Channel *channel)
{
// 从_channels中移除并且更新对应事件的监控
auto it = _channels.find(channel->Fd());
if (it != _channels.end())
{
_channels.erase(it);
}
//EPOLL_CTL_DEL
Update(channel, EPOLL_CTL_DEL);
}
// 开始监控,阻塞等待新的事件到来,返回活跃的 Channel 列表
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);
// 如果epoll_wait失败
if (nfds < 0)
{
// 如果errno = EINTR 事件还没有到来 直接return
if (errno == EINTR)
{
return;
}
// 否则出错,打印errno对应的string
ERR_LOG("EPOLL WAIT ERROR:%s\n", strerror(errno));
// abort 退出程序
abort();
}
// 成功 -- 遍历_evs中的事件
for (int i = 0; i < nfds; i++)
{
auto it = _channels.find(_evs[i].data.fd);
// assert这个事件应该早被添加在_evs中
assert(it != _channels.end());
// 设置实际就绪的事件
it->second->SetREvents(_evs[i].events);
// 把这个事件(it->second)放入活跃队列(active)中
active->push_back(it->second);
}
return;
}
};
1.5 Channel模块
Channel模块是 对⼀个描述符需要进行的IO事件管理 的模块,实现对描述符可读,可写,错误...任意事件的管理操作,以及对Poller模块对描述符进行IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能。
1.5.1 连接建立触发的事件
EPOLLIN (可读事件)
-
含义与触发条件 : 表示对应的文件描述符可以无阻塞地读取数据。触发场景 包括:接收缓冲区有新数据到达 ;监听套接字上有新的连接请求;对端正常关闭连接(此时读操作会返回0,表示EOF)。
-
是否需要显式设置: 是,需要在**
epoll_ctl**中设置以监控。
EPOLLOUT (可写事件)
-
含义与触发条件 : 表示对应的文件描述符可以无阻塞地写入 数据。连接建立后,发送缓冲区通常为空,此时会触发一次可写事件。在后续的通信中,当发送缓冲区从"满"变为"不满" (即腾出空间)时,会再次触发**
EPOLLOUT**。 -
是否需要显式设置: 是,需要在**
epoll_ctl**中设置以监控。
EPOLLRDHUP (对端关闭连接事件)
-
含义与触发条件 : Stream socket对端关闭连接,或关闭了写方向的连接 。这是Linux 2.6.17版本引入的专门用于检测对端关闭的事件。当对端正常调用**
close()** 或**shutdown(SHUT_WR)**时,本端会触发此事件。 -
是否需要显式设置: 是,需要在**
epoll_ctl**中设置以监控。
EPOLLPRI (带外数据事件)
-
含义与触发条件 : 表示有紧急数据(Out-of-Band data) 可读。例如,TCP协议中带
MSG_OOB标志发送的数据到达时,会触发此事件,而不会触发EPOLLIN。 -
是否需要显式设置: 是,需要在**
epoll_ctl**中设置以监控。
EPOLLERR (错误事件)
-
含义与触发条件 : 表示对应的文件描述符发生错误。触发场景包括:对端发送了RST包重置连接(如在已关闭的socket上写入数据);本端socket发生异步错误。
-
是否需要显式设置 : 否 。**
epoll_wait总会监听这个事件,无需在epoll_ctl**中显式设置。
EPOLLHUP (挂起事件)
-
含义与触发条件 : 表示对应的文件描述符被挂起 。触发场景包括:管道的读端关闭时,写端会触发
EPOLLHUP;socket的一端关闭连接后,本端在某些情况下(如尝试写入已关闭的socket收到RST后)也可能触发。 -
是否需要显式设置 : 否 。**
epoll_wait总会监听这个事件,无需在epoll_ctl**中显式设置。
核心事件对比一览表
| 事件宏 | 核心含义 | 典型触发场景 | 是否需要显式设置 |
|---|---|---|---|
EPOLLIN |
可读 | 数据到达、新连接到达、对端关闭连接(EOF) | 是 |
EPOLLOUT |
可写 | 连接建立、发送缓冲区从满变为不满 | 是 |
EPOLLRDHUP |
对端关闭连接 | 对端调用close()或shutdown(SHUT_WR) |
是 |
EPOLLPRI |
有带外数据 | 收到TCP紧急数据 | 是 |
EPOLLERR |
发生错误 | 对端发送RST包、本端异步错误 | 否 (始终监听) |
EPOLLHUP |
被挂起 | 管道读端关闭、socket连接异常 | 否 (始终监听) |
1.5.2 Channel类的实现
html
class Poller;
class EventLoop;
class Channel
{
private:
int _fd;
//每个线程对应一个loop
EventLoop *_loop;
// uint32_t : unsigned int
uint32_t _events; // 当前需要监控的事件 感兴趣的事件(EPOLLIN / EPOLLOUT)
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) {}
// 获取fd
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); }
// 启动读事件监控 // 需要在EventLoop中运行 _loop->UpdateEvent(this) 这个函数又会调用Poller模块
// _poller.UpdateEvent(channel) -> 然后调用Update -> 然后最后调用epoll_ctl进行设置管理
// 这样设置主要是让每一个线程独享一个loop,不用加锁,保证线程安全问题
//启动读事件监控
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))
{
//如果设置了_read_callback,就调用
if (_read_callback)
{
_read_callback();
}
}
// 如果触发了可写事件
if (_revents & EPOLLOUT)
{
//如果设置了_write_callback,就调用
if (_write_callback)
_write_callback();
}
// 如果触发了错误事件
else if (_revents & EPOLLERR)
{
//如果设置了_error_callback,就调用
if (_error_callback)
_error_callback();
}
//如果触发了挂起事件
else if (_revents & EPOLLRDHUP)
{
//如果设置了_close_callback,就调用
if (_close_callback)
_close_callback();
}
// 不管任何事件,都调用的回调函数
if (_event_callback)
_event_callback();
}
};
1.6 Connection模块
Connection模块是对Buffer模块,Socket模块,Channel模块的⼀个整体封装,实现了对一个通信套接字的整体的管理,每⼀个进行数据通信的套接字(也就是accept获取到的新连接)都会使用
Connection进行管理。
1.Connection模块内部包含由组件使用者传入的回调函数:连接建立完成回调,事件回调,
新数据回调,关闭回调等。

2.Connection模块内部包含有两个组件使用者提供的接口:数据发送接口,连接关闭接口。

3.Connection模块内部包含有两个用户态缓冲区:用户态接收缓冲区,用户态发送缓冲区。
4.Connection模块内部包含有⼀个Socket对象:完成描述符面向系统的IO操作。
5.Connection模块内部包含有⼀个Channel对象:完成描述符IO事件就绪的处理。

1.6.1 Connection类数据处理的完整流程说明
具体处理流程如下:
- 实现向Channel提供可读,可写,错误等不同事件的IO事件回调函数,然后将Channel和对应的描述符添加到Poller事件监控中。

- 当描述符在Poller模块中就绪了IO可读事件,则调用描述符对应Channel中保存的读事件处理函
数,进行数据读取,将socket接收缓冲区中的数据全部读取到Connection管理的用户态接收缓冲区中。然后调用由组件使用者传入的新数据到来回调函数进行处理。

- 组件使用者进行数据的业务处理完毕后,通过Connection向使用者提供的数据发送接口,将数据写入Connection的发送缓冲区中。

- 启动描述符在Poll模块中的IO写事件监控,就绪后,调用Channel中保存的写事件处理函数,将发送缓冲区中的数据通过Socket进行面向系统的实际数据发送。
1.6.2 Connection 类的实现
html
// Connection类 封装一个 TCP 连接
class Connection;
// DISCONNECTED -- 连接关闭状态 ,CONNECTED --连接建立成功 --待处理状态
// CONNECTED -- 连接建立完成,各种设置完成,可以通信的状态,DISCONNECTED --待关闭状态
typedef enum
{
DISCONNECTED,
CONNECTING,
CONNECTED,
DISCONNECTING
} ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection>
{
private:
// 连接的唯一ID,便于连接的管理和查找
uint64_t _conn_id;
// 定时器ID,必须是唯一的,为了简化把_conn_id作为定时器ID
// uint64_t _timer_fd;
// 连接关联的文件描述符
int _sockfd;
// 连接是否启动非活跃超时销毁的判断标致,默认为false
bool _enable_inactive_release;
// 连接所关联的一个EventLoop
EventLoop *_loop;
// 连接状态
ConnStatu _statu;
// 套接字操作管理
Socket _socket;
// 连接的事件管理
Channel _channel;
// 输入缓冲区 -- 存放从socket中读取到的数据
Buffer _in_buffer;
// 输出缓冲区 -- 存放要发送给对端的数据
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的事件回调函数
// fd可读事件触发后调用的函数,接收socket数据放到接收缓冲区中,然后调用_message_callback
void HandleRead()
{
// 1.接收socket的数据,放到缓冲区 _socket非阻塞接收
char buf[65536];
ssize_t ret = _socket.NonBlockRecv(buf, 65536);
if (ret < 0)
{
// 接收ret<0 出错了,不能直接关闭连接,要考虑有没有数据还没有处理干净调用ShutdownInLoop
return ShutdownInLoop();
}
// 2.将数据放入输入缓冲区,写入之后顺便将写偏移向后移动
_in_buffer.WriteAndPush(buf, ret);
// 3.如果_in_buffer的ReadAbleSize > 0 , 调用_message_callback进行业务处理,触发可写事件
if (_in_buffer.ReadAbleSize() > 0)
{
return _message_callback(shared_from_this(), &_in_buffer);
}
}
// fd可写事件触发后调用的函数,将发送缓冲区中的数据进行发送
void HandleWrite()
{
//_out_buffer 中保存的就是要发送的数据
// 1._socket.NonBlockSend
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);
}
// 然后再关闭 -- 调用实际的关闭操作Release
return Release();
}
// 移动写偏移
_out_buffer.MoveReadOffset(ret);
// 如果_out_buffer可读数据为空,说明没有数据待发送了
if (_out_buffer.ReadAbleSize() == 0)
{
// 关闭写事件监控
_channel.DisableWrite();
// 如果当前是半连接关闭状态,如果有数据,发送完数据再释放连接,没有数据则直接释放
if (_statu == DISCONNECTING)
{
return Release();
}
}
return;
}
// fd触发挂断事件
void HandleClose()
{
// 一旦挂断了,_socket就什么都干不了了,因此_in_buffer有数据待处理就处理一下,然后关闭连接
if (_in_buffer.ReadAbleSize() > 0)
{
_message_callback(shared_from_this(), &_in_buffer);
}
return Release();
}
// fd触发出错事件
void HandleError()
{
// 就直接关闭连接调用HandleClose()
return HandleClose();
}
// fd触发任意事件
void HandleEvent()
{
// 1.刷新连接的活跃度:延迟定时销毁任务
// 如果开启了非活跃超时连接
if (_enable_inactive_release == true)
{
_loop->TimerRefresh(_conn_id);
}
// 2.调用任意事件回调
if (_event_callback)
{
_event_callback(shared_from_this());
}
}
// 建立连接就绪之后,设置当前连接所处的状态,进行channel回调设置:启动读监控,调用_connected_callback
void EstablishedInLoop()
{
// 1.修改连接状态
// assert 当前的状态必须一定是半连接状态
assert(_statu == CONNECTING);
// 设置当前进入已连接状态
_statu = CONNECTED;
// 2.启动读事件监控
_channel.EnableRead();
// 3.调用回调函数
// 如果设置了_connected_callback就调用
if (_connected_callback)
_connected_callback(shared_from_this());
}
// ReleaseInLoop 才是实际的释放连接的接口
void ReleaseInLoop()
{
// 1.修改连接状态为DISCONNECTION
_statu = DISCONNECTED;
// 2.移除连接的事件监控
_channel.Remove();
// 3.关闭fd
_socket.Close();
// 4.如果当前定时器队列中还有定时销毁任务(要执行的任务),则取消定时销毁任务
if (_loop->HasTimer(_conn_id))
CancelInactiveReleaseInLoop();
// 5.调用关闭回调函数前,避免先移除服务器的管理的连接信息而导致的Connextion被释放
// 再去处理会出错,因此先调用用户的连接关闭回调函数
// 如果_closed_callback设置了,就执行
if (_closed_callback)
_closed_callback(shared_from_this());
// 如果_server_closed_callback设置了,就执行
if (_server_closed_callback)
_server_closed_callback(shared_from_this());
}
// 这个接口并不是实际的发送接口:主要是将数据发送到缓冲区,启动写事件监控
void SendInLoop(Buffer &buf)
{
// 如果当前状态为DISCONNECTED直接返回
if (_statu == DISCONNECTED)
return;
// 把数据放到_out_buffer发送缓冲区
_out_buffer.WriteBufferAndPush(buf);
// 如果写事件监控没开,就开启可写事件监控
if (_channel.WriteAble() == false)
{
_channel.EnableWrite();
}
}
// 这个关闭的操作并非实际的连接释放操作,还需要判断还有没有待处理的数据,待发送的数据
void ShutdownInLoop()
{
// 设置连接为半关闭状态
_statu = DISCONNECTING;
// 如果输入缓冲区ReadAbleSize > 0 还需要处理
if (_in_buffer.ReadAbleSize() > 0)
{
// 如果设置了 _message_callback 就执行 _message_callback 函数
if (_message_callback)
{
// shared_from_this() 传递用 shared_ptr 管理的 Connection 本身
_message_callback(shared_from_this(), &_in_buffer);
}
}
// 如果输出缓冲区ReadAbleSize > 0 还需要处理
if (_out_buffer.ReadAbleSize() > 0)
{
// 如果当前没有监控可写事件
if (_channel.WriteAble() == false)
{
// 启动可写事件监控
_channel.EnableWrite();
}
}
// 如果输出缓冲区ReadAbleSize = 0 没有需要待发送的数据,直接关闭
if (_out_buffer.ReadAbleSize() == 0)
{
// 这个时候真正调用Release,关闭连接
Release();
}
}
// 启动非活跃连接超时规则
void EnableInactiveReleaseInLoop(int sec)
{
// 1.将标志位置为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()
{
// 把标志位设为false
_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 socketfd) : _conn_id(conn_id), _sockfd(socketfd),
_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); }
// 获取管理的fd
int Fd() { return _sockfd; }
// 获取连接ID
int Id() { return _conn_id; }
// 判断是否处于CONNECTED状态
bool IsConnected()
{
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; }
// 建立连接
void Established()
{
// 将操作压入任务池
_loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
}
// 发送数据
void Send(const char *data, size_t len)
{
// 外界传入的data,可能是个临时空间,我们只是将发送操作任务压入任务池,有可能这个任务并没有立即被执行
// 因此有可能执行的时候,data指向的空间有可能已经被释放了,所有构造一个Buffer存储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));
}
// 考虑test_client4的情况,并不能直接对连接进行释放,而应该将释放操作压入到任务池中
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));
}
// 切换协议
void Upgrade(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,
const ClosedCallback &closed, const AnyEventCallback &event)
{
// assert必须在EventLoop线程中立即执行
// 以防新的事件触发后,处理的时候,切换任务还没有被执行.然后导致数据使用原协议了
_loop->AssertInLoop();
_loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this, context, conn, msg, closed, event));
}
};
1.7 Acceptor模块
Acceptor模块是对Socket模块和Channel模块的整合与封装,目的是实现对监听套接字的统一管理。它包含两个核心组成部分:
Socket对象:负责封装监听套接字的底层操作,如创建套接字、绑定地址、开始监听等基础功能。
Channel对象:负责处理监听套接字上的IO事件就绪情况,当有新连接到来时能够及时响应。
1.7.1 Acceptor模块的处理逻辑
- 注册可读事件回调:首先,Acceptor 会为内部的 Channel 对象设置一个可读事件的回调函数。这个回调函数的核心功能就是获取新连接------当监听套接字上有新连接请求到达时,该函数会被自动调用。

- 创建Connection对象:在可读事件的回调函数中,Acceptor 通过 accept 系统调用获取新建立的连接,然后为该连接创建一个 Connection 对象。这个 Connection 对象将负责后续的通信管理,包括数据的读写、事件的监控等。
简单来说,Acceptor就是监听套接字的管家:它通过 Socket 管理套接字的生命周期,通过Channel 监控连接请求的到来,并在新连接到来时创建对应的 Connection 对象,交给后续模块去处理。
1.7.2 Acceptor类的实现
html
// 用于专门接收新连接
class Acceptor
{
private:
// 用于创建监听套接字
Socket _socket;
// 用于对监听套接字进行事件监控
EventLoop *_loop;
// 用于对监听套接字进行事件管理
Channel _channel;
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback;
private:
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();
}
};
1.8 TimerWheel模块
TimerWheel 模块是一个定时任务管理器,用于实现固定时间的定时任务调度。它的核心功能可以这样理解:向 TimerWheel 中添加一个任务后,这个任务将在指定的时间间隔后被自动执行。同时还支持刷新定时任务,即重置任务的执行时间,从而实现任务的延迟执行。
在本项目中 , TimerWheel模块的核心作用是管理 Connection 对象的生命周期------具体来说,就是监控那些长时间没有数据交互的非活跃连接,当连接超时时,自动释放相关资源,避免连接泄露和资源浪费 。
1.8.1 TimerWheel的内部构成
-
timerfd对象:这是Linux系统提供的一种定时器机制,它像文件描述符一样工作,可以方便地集成到I/O多路复用模型中。TimerWheel 通过 timerfd 来精确控制定时任务的触发时机。
-
Channel对象:负责封装timerfd的事件处理。当 timerfd 上的定时器超时时,Channel 中注册的回调函数会被触发,从而执行对应的定时任务。
1.8.2 作流程简要说明
TimerWheel的工作流程非常简洁:通过timerfd设置定时器,当定时器超时时,timerfd变为可读状态,Channel监听到这一事件后,调用相应的回调函数,执行定时任务。
简单来说,TimerWheel 就是一个基于 timerfd 实现的、与事件循环无缝集成的定时器管理器,它让定时任务的调度变得简单而高效。
1.8.3 简单的秒级定时任务实现
在当前的高并发服务器中,我们不得不考虑一个问题,那就是连接的超时关闭问题。我们需要避免⼀个连接长时间不通信,但是也不关闭,空耗资源的情况。这时候我们就需要⼀个定时任务,定时的将超时过期的连接进行释放。
1.8.3.1 timerfd_create函数
timerfd_create() 是 Linux 提供的一个将定时器抽象为文件描述符 的接口。它创建的定时器可以通过 read() 来读取其到期通知 ,并能无缝地与 epoll、select 等I/O多路复用机制结合,是构建高性能、事件驱动程序的理想选择。
函数原型:
html
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
参数详解:
clockid - 定时器使用的时钟类型
这个参数指定了定时器基于哪个系统时钟来计时。
| 时钟类型 | 描述 | 典型使用场景 |
|---|---|---|
CLOCK_REALTIME |
系统范围的实时时钟,即可设定的墙上时间。如果系统时间被管理员修改,此时钟会受到影响。 | 需要与系统墙上时间对齐的定时器(注意其不连续性)。 |
CLOCK_MONOTONIC |
单调递增时钟,从系统启动后的某个不确定时间点开始计时,不受系统时间跳变的影响。 | 最常用的时钟,适用于绝大多数不需要挂起感知的定时任务。 |
flags - 控制文件描述符的行为
这个参数可以设置为 0,或者通过按位或(or)组合以下选项:
| 标志 | 描述 |
|---|---|
TFD_NONBLOCK |
将新文件描述符设置为非阻塞模式。当定时器没有到期时,对它的 read() 操作会立即返回 EAGAIN 错误,而不是阻塞等待。这相当于调用 fcntl() 设置 O_NONBLOCK。 |
TFD_CLOEXEC |
为新的文件描述符设置执行时关闭标志。当程序调用 execve() 执行新程序时,这个文件描述符会被自动关闭,防止资源泄漏。 |
1.8.3.2 timerfd_settime函数
timerfd_settime() 是 timerfd 系列函数中最关键 的一个,它负责启动、停止或修改定时器的参数。掌握这个函数是正确使用 timerfd 的基础。
函数原型:
html
#include <sys/timerfd.h>
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
参数详解:
| 参数 | 描述 |
|---|---|
fd |
timerfd_create() 返回的定时器文件描述符。 |
flags |
控制定时器行为:0 或 TFD_TIMER_ABSTIME。 |
new_value |
核心参数:设置新的定时器参数(首次到期时间和间隔)。 |
old_value |
输出参数:返回之前的定时器设置(如果不为NULL)。 |
核心数据结构:struct itimerspec
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 (0-999999999) */
};
struct itimerspec {
struct timespec it_interval; /* 周期性定时器的间隔时间 */
struct timespec it_value; /* 首次到期时间 */
};
定时器会在每次超时时,自动给fd中写⼊8字节的数据,表示在上⼀次读取数据到当前读取数据期间超时了多少次。
it_value 的含义
| 设置 | 效果 |
|---|---|
| 非零值 | 启动定时器,指定首次到期时间 |
全零 ({0,0}) |
停止定时器(disarm) |
it_interval 的含义
| 设置 | 效果 |
|---|---|
| 非零值 | 定时器变为周期性,按此间隔重复到期 |
全零 ({0,0}) |
定时器为一次性,首次到期后即停止 |
flags 参数详解
0 - 相对时间模式
struct itimerspec new_value;
new_value.it_value.tv_sec = 5; // 5秒后首次到期
new_value.it_value.tv_nsec = 0;
new_value.it_interval.tv_sec = 2; // 之后每2秒到期一次
new_value.it_interval.tv_nsec = 0;
timerfd_settime(tfd, 0, &new_value, NULL); // 相对时间
-
时间从调用时刻开始计算。
-
如果程序暂停或阻塞,计时仍然继续。
-
最常用、最直观的模式。
TFD_TIMER_ABSTIME - 绝对时间模式
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
struct itimerspec new_value;
new_value.it_value.tv_sec = now.tv_sec + 10; // 绝对时间:10秒后的时刻
new_value.it_value.tv_nsec = now.tv_nsec;
new_value.it_interval.tv_sec = 0; // 一次性定时器
new_value.it_interval.tv_nsec = 0;
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &new_value, NULL);
-
时间基于系统时钟的绝对时刻。
-
适用于需要在特定时间点触发的场景(如定时任务调度)。
-
与 **
CLOCK_REALTIME**配合时需注意系统时间调整的影响。
1.8.3.3 定时器示例
html
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/timerfd.h>
int main()
{
//int timerfd_create(int clockid, int flags);
int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (timerfd < 0) {
perror("timerfd_create error");
return -1;
}
//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);
while(1) {
uint64_t times;
int ret = read(timerfd, ×, 8);
if (ret < 0) {
perror("read error");
return -1;
}
printf("超时了,距离上一次超时了%ld次\n", times);
}
close(timerfd);
return 0;
}

上边例子,是⼀个定时器的使用示例,是每隔1s钟触发⼀次定时器超时,否则就会阻塞在read读取数据这里。基于这个例子,则我们可以实现每隔1s,检测⼀下哪些连接超时了,然后将超时的连接释放掉。
1.8.3.4 定时加法器示例
html
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
// 全局计数器
// volatile 防止编译器优化 告诉编译器去内存中取
volatile int counter = 0;
// 错误处理函数
void handle_error(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
// 处理定时器事件
void handle_timer(uint64_t expirations)
{
// 定时器每秒触发一次,expirations表示自上次读取后到期的次数
// 在正常负载下,expirations通常为1,但如果系统繁忙,可能会累积多次
counter += expirations; // 每次到期计数器加1
printf("[定时器] 已过去 %d 秒 (expirations=%lu)\n", counter, expirations);
}
// 处理标准输入
void handle_stdin()
{
char buf[256];
if (fgets(buf, sizeof(buf), stdin))
{
// 去除换行符
buf[strcspn(buf, "\n")] = 0;
if (strcmp(buf, "add") == 0 || strcmp(buf, "a") == 0)
{
counter += 5;
printf("[手动] 手动增加5,当前计数: %d\n", counter);
}
else if (strcmp(buf, "reset") == 0 || strcmp(buf, "r") == 0)
{
counter = 0;
printf("[手动] 计数器已重置为0\n");
}
else if (strcmp(buf, "quit") == 0 || strcmp(buf, "q") == 0)
{
printf("[系统] 程序退出\n");
exit(0);
}
else if (strcmp(buf, "status") == 0 || strcmp(buf, "s") == 0)
{
printf("[状态] 当前计数: %d\n", counter);
}
else if (strcmp(buf, "help") == 0 || strcmp(buf, "h") == 0)
{
printf("\n可用命令:\n");
printf(" add/a - 手动增加5\n");
printf(" reset/r - 重置计数器为0\n");
printf(" status/s- 显示当前计数\n");
printf(" quit/q - 退出程序\n");
printf(" help/h - 显示此帮助\n\n");
}
else
{
printf("[手动] 未知命令: '%s' (输入 'help' 查看可用命令)\n", buf);
}
}
}
int main()
{
printf("===== 定时加法器示例 =====\n");
printf("定时器每秒自动加1,也可手动输入命令\n");
printf("输入 'help' 查看可用命令\n\n");
// 1. 创建定时器文件描述符
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (tfd == -1)
handle_error("timerfd_create");
// 2. 设置定时器:2秒后首次到期,之后每隔1秒到期一次
struct itimerspec ts;
ts.it_value.tv_sec = 2; // 首次到期时间:2秒后
ts.it_value.tv_nsec = 0;
ts.it_interval.tv_sec = 1; // 间隔时间:1秒
ts.it_interval.tv_nsec = 0;
if (timerfd_settime(tfd, 0, &ts, NULL) == -1)
{
close(tfd);
handle_error("timerfd_settime");
}
// 3. 创建 epoll 实例
int epfd = epoll_create1(0);
if (epfd == -1)
{
close(tfd);
handle_error("epoll_create1");
}
// 4. 将定时器fd加入epoll监听
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = tfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev) == -1)
{
close(tfd);
close(epfd);
handle_error("epoll_ctl: timerfd");
}
// 5. 将标准输入也加入epoll监听(实现交互式命令)
ev.events = EPOLLIN;
ev.data.fd = STDIN_FILENO; // 标准输入的文件描述符是0
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1)
{
close(tfd);
close(epfd);
handle_error("epoll_ctl: stdin");
}
// 6. 设置标准输入为非阻塞模式(可选,这里保持阻塞以便fgets工作)
// 注意:由于我们使用epoll等待stdin,即使stdin是阻塞的也不影响
printf("定时器已启动,2秒后开始计数,之后每秒自动加 1\n");
// 命令提示符
printf("> ");
fflush(stdout);
// 7. 事件循环
struct epoll_event events[10];
while (1)
{
int nfds = epoll_wait(epfd, events, 10, -1);
if (nfds == -1)
{
if (errno == EINTR)
continue; // 被信号中断,继续
handle_error("epoll_wait");
}
for (int i = 0; i < nfds; i++)
{
if (events[i].data.fd == tfd)
{
// 定时器事件
if (events[i].events & EPOLLIN)
{
uint64_t expirations;
ssize_t s = read(tfd, &expirations, sizeof(expirations));
if (s == sizeof(expirations))
{
handle_timer(expirations);
printf("> "); // 重新显示提示符
fflush(stdout);
}
else if (s == -1 && errno != EAGAIN)
{
perror("read timerfd");
}
}
}
else if (events[i].data.fd == STDIN_FILENO)
{
// 标准输入事件(用户输入命令)
if (events[i].events & EPOLLIN)
{
handle_stdin();
printf("> "); // 重新显示提示符
fflush(stdout);
}
}
}
}
// 8. 清理
close(tfd);
close(epfd);
printf("程序正常退出\n");
return 0;
}
1.8.4 时间轮思想
上述定时器的例子,存在⼀个很大的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率无疑是较为低下的。这时候我们就想到可以针对所有的连接,根据每个连接最近⼀次通信的系统时间建立⼀个小根堆,这样只需要每次针对堆顶部分的连接逐个释放,直到没有超时的连接为止,这样也可以大大提高处理的效率。上述方法可以实现定时任务,但是这里给大家介绍另⼀种方案:时间轮。
时间轮的思想来源于钟表,如果我们定了⼀个3点钟的闹铃,则当时针走到3的时候,就代表时间到
了。同样的道理,如果我们定义了⼀个数组,并且有⼀个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定⼀个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走⼀步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。但是,同⼀时间可能会有大批量的定时任务,因此我们可以给数组对应位置下拉⼀个数组,这样就可以在同⼀个时刻上添加多个定时任务了。

当然,上述操作也有⼀些缺陷,比如我们如果要定义⼀个60s后的任务,则需要将数组的元素个数设置为60才可以,如果设置⼀小时后的定时任务,则需要定义3600个元素的数组,这样无疑是比较麻烦的。因此,可以采用多层级的时间轮,有秒针轮,分针轮,时针轮, 60< time <3600则time/60就是分针轮对应存储的位置,当tick/3600等于对应位置的时候,将其位置的任务向分针,秒针轮进行移动。当前我们的应用中,不用设计的这么麻烦,因为我们的定时任务通常设置的30s以内,所以简单的单层时间轮就够用了。
但是,我们也得考虑⼀个问题,当前的设计是时间到了,则主动去执行定时任务,释放连接,那能不能在时间到了后,自动执行定时任务呢,这时候我们就想到⼀个操作"类的析构函数"。⼀个类的析构函数,在对象被释放时会自动被执行,那么我们如果将⼀个定时任务作为⼀个类的析构函数内的操作,则这个定时任务在对象被释放的时候就会执行。
但是仅仅为了这个目的,而设计⼀个额外的任务类,好像有些不划算,但是,这里我们又要考虑另⼀个问题,那就是假如有⼀个连接建立成功了,我们给这个连接设置了⼀个30s后的定时销毁任务,但是在第10s的时候,这个连接进行了⼀次通信(读写操作),那么我们应该时在第30s的时候关闭,还是第40s的时候关闭呢?无疑应该是第40s的时候。也就是说,这时候,我们需要让这个第30s的任务失效,但是我们该如何实现这个操作呢?
这里,我们就可以使用智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放⼀个对象,那么如果连接在第10s进行了⼀次通信,则我们继续向定时任务中,添加⼀个30s后(也就是第40s)的任务类对象的 shared_ptr ,则这时候两个任务 shared_ptr 计数为2,则第30s的定时任务被释放的时候,计数减少1,变为1,并不为0,所以这时候并不会执行实际的析构函数,那么就相当于这个第30s的任务失效了,只有在第40s的时候,这个任务才会被真正释放。
上述过程就是时间轮定时任务的思想了,当然这里为了更加简便的实现,进行了⼀些小的调整实
现。
1.8.4.1 TimerTask
html
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
// 定时任务类
class TimerTask
{
private:
// 定时器任务ID
uint64_t _id;
// 定时任务的超时时间
uint32_t _timeout;
// false -- 表示没有被取消 true -- 表示被取消
bool _canceled;
// 定时器对象要执行的定时任务
TaskFunc _task_cb;
// 用于删除TimerWheel中保存的定时器对象信息
ReleaseFunc _release;
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删除TimerWheel中保存的定时器对象信息
_release();
}
// 设置为任务被取消了
void Cancel() { _canceled = true; }
// 设置_release回调函数
void SetRelease(const ReleaseFunc &cb) { _release = cb; }
// 返回延时时间
uint32_t DelayTime() { return _timeout; }
};
class TimerWheel
{
private:
// 使用 weak_ptr 和 shared_ptr
using PtrTask = std::shared_ptr<TimerTask>;
using WeakTask = std::weak_ptr<TimerTask>;
// 当前的秒针走到哪里就释放哪里,然后就执行哪里的任务
int _tick;
// 表盘的最大数量 -- 最大的延迟时间
int _capacity;
// 二维数组保存各种任务
std::vector<std::vector<PtrTask>> _wheel;
// 使用unordered_map建立定时器id和定时器任务之间的映射关系
std::unordered_map<uint64_t, WeakTask> _timers;
// run in loop
EventLoop *_loop;
// 定时器描述符 -- 可读事件回调就是读取计数器,执行定时任务
int _timerfd;
// unique_ptr管理timer_channel
std::unique_ptr<Channel> _timer_channel;
private:
// 删除定时器任务
void RemoveTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it != _timers.end())
{
_timers.erase(it);
}
}
// 创建定时器fd
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 ittimerspec* old);
struct itimerspec itime;
// 设置为第一次超时时间为1s后
itime.it_value.tv_sec = 1;
itime.it_value.tv_nsec = 0;
// 设置第一次超时后,每次超时的间隔时间
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之后超时的次数
// 从_timerfd中读取,timers作为输出型参数,固定8字节
int ret = read(_timerfd, ×, 8);
if (ret < 0)
{
ERR_LOG("READ TIMEFD FAILED!!");
abort();
}
return times;
}
// RunTimerTask函数应该每秒钟被执行一次,相当于秒针往后走一步
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;
// 清空指定位置的数组,就会把数组中保存的所管理TimerTask对象释放掉,然后执行TimerTask的析构函数
// 如果任务没有被取消就执行对应的定时任务(实现了_tick走到哪里释放哪里,执行哪里的任务)
_wheel[_tick].clear();
}
// OnTime
void OnTime()
{
// 根据实际超时的次数,执行一次或者多次的超时任务
int times = ReadTimefd();
for (int i = 0; i < times; i++)
{
RunTimerTask();
}
}
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
// 用shared_ptr new一个TimerTask(由id,delay,cd)构造
PtrTask pt(new TimerTask(id, delay, cb));
// 给TimerTask设置销毁函数 bind TimerWheel::RemoveTimer 函数
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
// 计算对应位置
int pos = (_tick + delay) % _capacity;
// 把这个任务推入时间轮中
_wheel[pos].push_back(pt);
// 建立id和task的映射
_timers[id] = WeakTask(pt);
}
// 刷新定时任务在loop中
void TimerRefreshInLoop(uint64_t id)
{
// 通过保存的定时器对象的weak_ptr构造出一个新的shared_ptr,添加到_wheel中
auto it = _timers.find(id);
if (it == _timers.end())
{
// 没有找到定时任务,没法刷新,没法延迟
return;
}
// 通过it->second.lock获取weak_ptr管理的对象对应的shared_ptr
PtrTask pt = it->second.lock();
// 获取延迟时间
int delay = pt->DelayTime();
// 计算位置
int pos = (_tick + delay) % _capacity;
// 再把这个shared_ptr管理的对象推入时间轮中执行
_wheel[pos].push_back(pt);
}
// 取消任务在loop中
void TimerCancelInLoop(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
// 没有找到,直接return
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->EnableRead();
// 为_timer_channel设置读事件回调bind TimerWheel的onTime函数
_timer_channel->SetReadCallBack(std::bind(&TimerWheel::OnTime, this));
}
// 定时器中有个_timers成员,对定时器信息的操作有可能在多线程中进行,因此需要考虑线程安全问题
// 所有如果不想加锁,那就把对定时器的所有操作,都放到一个线程中
// 声明 TimerAdd (添加定时任务)
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;
}
};
1.8.4.2 TimerWheel
html
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:
// 定时器任务ID
uint64_t _id;
// 定时任务的超时时间
uint32_t _timeout;
// false -- 表示没有被取消 true -- 表示被取消
bool _canceled;
// 定时器对象要执行的定时任务
TaskFunc _task_cb;
// 用于删除TimerWheel中保存的定时器对象信息
ReleaseFunc _release;
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删除TimerWheel中保存的定时器对象信息
_release();
}
// 设置为任务被取消了
void Cancel() { _canceled = true; }
// 设置_release回调函数
void SetRelease(const ReleaseFunc &cb) { _release = cb; }
// 返回延时时间
uint32_t DelayTime() { return _timeout; }
};
class TimerWheel
{
private:
// 使用 weak_ptr 和 shared_ptr
using PtrTask = std::shared_ptr<TimerTask>;
using WeakTask = std::weak_ptr<TimerTask>;
// 当前的秒针走到哪里就释放哪里,然后就执行哪里的任务
int _tick;
// 表盘的最大数量 -- 最大的延迟时间
int _capacity;
// 二维数组
std::vector<std::vector<PtrTask>> _wheel;
// 使用unordered_map建立定时器id和定时器任务之间的映射关系
std::unordered_map<uint64_t, WeakTask> _timers;
// run in loop
EventLoop *_loop;
// 定时器描述符 -- 可读事件回调就是读取计数器,执行定时任务
int _timerfd;
// unique_ptr管理timer_channel
std::unique_ptr<Channel> _timer_channel;
private:
// 删除定时器任务
void RemoveTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it != _timers.end())
{
_timers.erase(it);
}
}
// 创建定时器fd
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 ittimerspec* old);
struct itimerspec itime;
// 设置为第一次超时时间为1s后
itime.it_value.tv_sec = 1;
itime.it_value.tv_nsec = 0;
// 第一次超时后,每次超时的间隔时间
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之后超时的次数
// 从_timerfd中读取,timers作为输出型参数,固定8字节
int ret = read(_timerfd, ×, 8);
if (ret < 0)
{
ERR_LOG("READ TIMEFD FAILED!!");
abort();
}
return times;
}
// RunTimerTask函数应该每秒钟被执行一次,相当于秒针往后走一步
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;
// 清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉
_wheel[_tick].clear();
}
// OnTime 根据实际超时的次数,执行对应的超时任务
void OnTime()
{
// 根据实际超时的次数,执行一次或者多次的超时任务
int times = ReadTimefd();
for (int i = 0; i < times; i++)
{
RunTimerTask();
}
}
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
// 用shared_ptr new一个TimerTask(由id,delay,cd)构造
PtrTask pt(new TimerTask(id, delay, cb));
// 给TimerTask设置销毁函数 bind TimerWheel::RemoveTimer 函数
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
// 计算对应位置
int pos = (_tick + delay) % _capacity;
// 把这个任务推入时间轮中
_wheel[pos].push_back(pt);
// 建立id和task的映射
_timers[id] = WeakTask(pt);
}
// 刷新定时任务在loop中
void TimerRefreshInLoop(uint64_t id)
{
// 通过保存的定时器对象的weak_ptr构造出一个shared_ptr,添加到_wheel中
auto it = _timers.find(id);
if (it == _timers.end())
{
// 没有找到定时任务,没法刷新,没法延迟
return;
}
// 通过it->second.lock获取weak_ptr管理的对象对应的shared_ptr
PtrTask pt = it->second.lock();
// 获取延迟时间
int delay = pt->DelayTime();
// 计算位置
int pos = (_tick + delay) % _capacity;
// 再把这个shared_ptr管理的对象推入时间轮中执行
_wheel[pos].push_back(pt);
}
// 取消任务在loop中
void TimerCancelInLoop(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
// 没有找到,直接return
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设置读事件回调bind TimerWheel的onTime函数
_timer_channel->SetReadCallBack(std::bind(&TimerWheel::OnTime, this));
// 启动读事件监控
_timer_channel->EnableRead();
}
// 定时器中有个_timers成员,定时器信息的操作有可能在多线程中进行,因此需要考虑线程安全问题
// 所有如果不想加锁,那就把对定时器的所有操作,都放到一个线程中
// 声明 TimerAdd
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;
}
};
1.8.4.3 时间轮 test
html
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <functional>
#include <memory>
#include <unistd.h>
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;
private:
void RemoveTimer(uint64_t id) {
auto it = _timers.find(id);
if (it != _timers.end()) {
_timers.erase(it);
}
}
public:
TimerWheel():_capacity(60), _tick(0), _wheel(_capacity) {}
void TimerAdd(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 TimerRefresh(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 TimerCancel(uint64_t id) {
auto it = _timers.find(id);
if (it == _timers.end()) {
return;//没找着定时任务,没法刷新,没法延迟
}
PtrTask pt = it->second.lock();
if (pt) pt->Cancel();
}
//这个函数应该每秒钟被执行一次,相当于秒针向后走了一步
void RunTimerTask() {
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear();//清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr释放掉
}
};
class Test {
public:
Test() {std::cout << "构造" << std::endl;}
~Test() {std::cout << "析构" << std::endl;}
};
void DelTest(Test *t) {
delete t;
}
int main()
{
TimerWheel tw;
Test *t = new Test();
tw.TimerAdd(888, 5, std::bind(DelTest, t));
for(int i = 0; i < 5; i++) {
sleep(1);
tw.TimerRefresh(888);//刷新定时任务
tw.RunTimerTask();//向后移动秒针
std::cout << "刷新了一下定时任务,重新需要5s中后才会销毁\n";
}
tw.TimerCancel(888);
while(1) {
sleep(1);
std::cout << "-------------------\n";
tw.RunTimerTask();//向后移动秒针
}
return 0;
}


1.9 EventLoop模块
EventLoop模块可以理解就是我们上边所说的Reactor模块,它是对Poller模块,TimerWheel模块, Socket模块的⼀个整体封装,进行所有描述符的事件监控。
EventLoop模块必然是⼀个对象对应⼀个线程的模块,线程内部的目的就是运行EventLoop的启动函数。 EventLoop模块为了保证整个服务器的线程安全问题,因此要求使用者对于Connection的所有操作⼀定要在其对应的EventLoop线程内完成,不能在其他线程中进行(比如组件使用者使用Connection发送数据,以及关闭连接这种操作)。
EventLoop模块保证自己内部所监控的所有描述符,都要是活跃连接,非活跃连接就要及时释放避免资源浪费。
1.9.1 EventLoop模块设计与实现
EventLoop模块是事件驱动架构的核心组件,设计遵循单线程串行化处理原则,确保每个连接的操作始终在同一个线程中执行。模块内部包含以下关键组件:
-
eventfd事件通知机制:模块内部封装了一个eventfd文件描述符。eventfd是Linux内核提供的专门用于事件通知的机制,可实现线程间的高效唤醒操作。
-
Poller事件监控器:模块包含一个Poller对象,基于epoll等多路复用技术实现,负责对模块管理的所有文件描述符进行IO事件监控。
-
TimerWheel定时器管理:模块包含一个TimerWheel对象,用于统一管理所有定时任务,实现定时事件的触发与处理。
-
任务队列(_tasks):模块维护一个待处理任务队列,所有针对Connection对象的操作均被封装为任务加入该队列,由EventLoop在其所属线程中顺序执行。

1.9.2 工作流程
首先,通过Poller模块对当前管理范围内的所有描述符进行IO事件监控。当有描述符事件就绪时,通过该描述符对应的Channel对象进行具体的事件处理。
其次,所有就绪描述符的IO事件处理完毕后,EventLoop会顺序执行任务队列中的所有待处理任务,确保所有操作在同一线程中串行完成。
最后,为解决epoll监控可能因无事件到来而持续阻塞的问题,模块引入了eventfd机制 。将该eventfd添加到Poller的事件监控中,当向任务队列添加新任务时,通过向eventfd写入数据来唤醒epoll阻塞,确保任务能够及时得到处理。
这种设计保证了每个Connection对象与EventLoop的绑定关系,实现了连接操作的线程封闭性,有效避免了多线程环境下的竞态条件。
1.9.2.1 eventfd函数
eventfd 是 Linux 特有的一个系统调用,用于创建事件通知文件描述符。它可以实现:线程间的事件通知、进程间的事件通知,需要配合 epoll 实现异步事件处理和读写操作read和write一起使用。
函数原型
html
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
参数详解
-
initval = 0: 初始化计数器值为 0。
-
flags: 使用按位或组合两个标志
-
EFD_CLOEXEC: 在执行 exec() 时自动关闭文件描述符。 -
EFD_NONBLOCK: 设置为非阻塞模式。
-
示例代码
html
#include <sys/eventfd.h>
#include <unistd.h>
#include <stdint.h>
#include <errno.h>
int main() {
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd == -1) {
perror("eventfd");
return 1;
}
// 写入事件通知
uint64_t u = 1;
ssize_t s = write(efd, &u, sizeof(uint64_t));
// 读取事件通知
uint64_t val;
s = read(efd, &val, sizeof(uint64_t));
close(efd);
return 0;
}
html
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/eventfd.h>
int main()
{
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd < 0) {
perror("eventfd failed!!");
return -1;
}
uint64_t val = 1;
write(efd, &val, sizeof(val));
write(efd, &val, sizeof(val));
write(efd, &val, sizeof(val));
uint64_t res = 0;
read(efd, &res, sizeof(res));
//写了三次 res 为 3
printf("%ld\n", res);
close(efd);
return 0;
}
1.9.3 EventLoop类实现
html
class EventLoop
{
private:
// 包装一个Functor
using Functor = std::function<void()>;
// 线程ID
std::thread::id _thread_id;
// eventfd唤醒IO事件监控有可能导致的阻塞
int _event_fd;
// unique_ptr管理event_channel
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;
}
static int CreateEventFd()
{
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd < 0)
{
// 创建eventfd失败让程序异常退出
ERR_LOG("CREATE EVENTFD FAILED!!");
abort();
}
return efd;
}
// 读取eventfd事件通知次数
void ReadEventfd()
{
uint64_t res = 0;
int ret = read(_event_fd, &res, sizeof(res));
if (ret < 0)
{
// 被打断或者无数据可读直接返回
if (errno == EINTR || errno == EAGAIN)
{
return;
}
// error 让程序退出
ERR_LOG("READ EVENTFD FAILED!");
abort();
}
return;
}
// 唤醒事件 其实就是给eventfd写入一个数据,eventfd就会触发可读事件
void WeakEventfd()
{
// 也就是往_event_fd写入数据
uint64_t val = 1;
int ret = write(_event_fd, &val, sizeof(val));
if (ret < 0)
{
// 如果被打断直接返回
if (errno == EINTR)
{
return;
}
ERR_LOG("READ EVENTFD FAILED!!");
// error 让程序退出
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的读事件监控
_event_channel->EnableRead();
// 给eventfd添加可读事件回调函数
_event_channel->SetReadCallBack(std::bind(&EventLoop::ReadEventfd, this));
}
// 开启EventLoop 1.事件监控 2.就绪事件处理 3.执行任务
void Start()
{
while (1)
{
// 1.事件监控
std::vector<Channel *> actives;
// Poll 开始监控,返回活跃连接
_poller.Poll(&actives);
// 2.遍历活跃连接
for (auto &channel : actives)
{
// 处理事件
channel->HandleEvent();
}
// 3.执行任务
RunAllTask();
}
}
// 判断当前线程是否是EventLoop对应的线程
bool IsInLoop()
{
return (_thread_id == std::this_thread::get_id());
}
// assert一定要在loop内
void AssertInLoop()
{
assert(_thread_id == std::this_thread::get_id());
}
// 判断将要执行的任务是否处于当前线程中,如果在则执行,不在则压入任务队列
// RunInLoop把所有的任务操作都放在同一个线程里面了
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就会触发可读事件
WeakEventfd();
}
// 添加/修改fd的事件监控
void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
// 移除对fd的监控
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);
}
};
1.9.4 Eventloop简单服务器的测试
1.9.4.1 tcp_server.cpp(EventLoop + Channel + Socket)
html
#include "../source/server.hpp"
void HandleClose(Channel *channel)
{
DBG_LOG("close fd:%d", channel->Fd());
channel->Remove(); // 移除监控
delete channel;
}
void HandleRead(Channel *channel)
{
// 获取新channel的fd
int fd = channel->Fd();
char buf[1024] = {0};
// recv
int ret = recv(fd, buf, 1023, 0);
if (ret <= 0)
{
// 读取失败 关闭释放
return HandleClose(channel);
}
DBG_LOG("%s", buf);
// 启动可写事件监控
channel->EnableWrite();
}
void HandleWrite(Channel *channel)
{
int fd = channel->Fd();
const char *data = "今天天气好吗?";
int ret = send(fd, data, strlen(data), 0);
if (ret < 0)
{
// 发送失败 关闭释放
return HandleClose(channel);
}
// 关闭写监控
channel->DisableWrite();
}
void HandleError(Channel *channel)
{
ERR_LOG("ERROR!!");
}
void HandleEvent(EventLoop *loop, Channel *channel, uint64_t timerid)
{
// 刷新定时器
loop->TimerRefresh(timerid);
}
void Acceptor(EventLoop *loop, Channel *lst_channel)
{
// 获取监听套接字
int fd = lst_channel->Fd();
// accept接收新连接
int newfd = accept(fd, nullptr, nullptr);
if (newfd < 0)
{
return;
}
// 随机设置定时器id
uint64_t timerid = rand() % 10000;
// 为新连接创建的Fd创建channel
Channel *channel = new Channel(loop, newfd);
// 给新连接设置对应的回调 -- 设置对应的处理函数
channel->SetReadCallBack(std::bind(HandleRead, channel));
channel->SetWriteCallBack(std::bind(HandleWrite, channel));
channel->SetCloseCallBack(std::bind(HandleClose, channel));
channel->SetErrorCallBack(std::bind(HandleError, channel));
channel->SetEventCallBack(std::bind(HandleEvent, loop, channel, timerid));
// 非活跃连接的超时释放,5s后关闭连接
loop->TimerAdd(timerid, 10, std::bind(HandleClose, channel));
channel->EnableRead();
}
int main()
{
srand(time(NULL));
EventLoop loop;
Socket lst_sock;
// 监听套接字 createserver
lst_sock.CreateServer(8081);
// 为监听套接字创建一个Channel进行事件的管理,以及事件的处理
Channel channel(&loop, lst_sock.Fd());
// 给channel设置回调,回调中,获取新连接,然后为新连接创建新的Channel并且添加监控
channel.SetReadCallBack(std::bind(Acceptor, &loop, &channel));
// 启动channel的可读事件监控
channel.EnableRead();
while (1)
{
loop.Start();
}
lst_sock.Close();
return 0;
}
tcp_client.cpp
html
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
for (int i = 0; i < 5; i++) {
std::string str = "client say : hello hello ";
cli_sock.Send(str.c_str(), str.size());
char buf[1024] = {0};
cli_sock.Recv(buf, 1023);
DBG_LOG("%s", buf);
sleep(1);
}
while(1) sleep(1);
return 0;
}
关系图



1.9.4.2 tcp_server.cpp(EventLoop + Connection + Socket)
html
//管理所有的连接
std::unordered_map<uint64_t,PtrConnection> _conns;
uint64_t conn_id = 0;
void ConnectionDestroy(const PtrConnection& conn)
{
_conns.erase(conn->Id());
}
void OnConnected(const PtrConnection& conn)
{
DBG_LOG("NEW CONNECTION:%p",conn.get());
}
void OnMessage(const PtrConnection& conn,Buffer * buf)
{
DBG_LOG("%s",buf->ReadPosition());
buf->MoveReadOffset(buf->ReadAbleSize());
std::string str = "server say : 今天天气真好";
conn->Send(str.c_str(),str.size());
conn->Shutdown();
}
void Acceptor(EventLoop* loop,Channel* lst_channel)
{
int fd = lst_channel->Fd();
int newfd = accept(fd,nullptr,nullptr);
if(newfd < 0){return;}
//DBG_LOG("获取到了一个新的fd:%d",newfd);
conn_id++;
PtrConnection conn(new Connection(loop,conn_id,newfd));
//DBG_LOG("构造了一个新的Connection");
conn->SetMessageCallback(std::bind(OnMessage,std::placeholders::_1,std::placeholders::_2));
conn->SetSrvClosedCallback(std::bind(ConnectionDestroy,std::placeholders::_1));
conn->SetConnectedCallback(std::bind(OnConnected,std::placeholders::_1));
conn->EnableInactiveRelease(5);
//DBG_LOG("启动了非活跃超时连接");
conn->Established();
//DBG_LOG("就绪初始化完成");
_conns.insert(std::make_pair(conn_id,conn));
//DBG_LOG("把新的Connection插入_conn");
}
int main()
{
srand(time(NULL));
EventLoop loop;
Socket lst_sock;
lst_sock.CreateServer(8081);
//为监听套接字创建一个Channel进行事件的管理,以及事件的处理
Channel channel(&loop,lst_sock.Fd());
//回调中,获取新连接,为新连接创建Channel并且添加监控
channel.SetReadCallBack(std::bind(Acceptor,&loop,&channel));
//启动listen_socket可读事件监控
channel.EnableRead();
while(1)
{
loop.Start();
}
lst_sock.Close();
return 0;
}


通信连接模块关系图

1.9.4.3 tcp_server.cpp(EventLoop + Connection + Socket + Acceptor)
html
//Acceptor ---------------test
//管理所有的连接
std::unordered_map<uint64_t,PtrConnection> _conns;
uint64_t conn_id = 0;
EventLoop loop;
void ConnectionDestroy(const PtrConnection& conn)
{
_conns.erase(conn->Id());
}
void OnConnected(const PtrConnection& conn)
{
DBG_LOG("NEW CONNECTION:%p",conn.get());
}
void OnMessage(const PtrConnection& conn,Buffer * buf)
{
DBG_LOG("%s",buf->ReadPosition());
buf->MoveReadOffset(buf->ReadAbleSize());
std::string str = "server say: hello 今天天气好吗?";
conn->Send(str.c_str(),str.size());
conn->Shutdown();
}
void NewConnection(int fd)
{
conn_id++;
PtrConnection conn(new Connection(&loop,conn_id,fd));
conn->SetMessageCallback(std::bind(OnMessage,std::placeholders::_1,std::placeholders::_2));
conn->SetSrvClosedCallback(std::bind(ConnectionDestroy,std::placeholders::_1));
conn->SetConnectedCallback(std::bind(OnConnected,std::placeholders::_1));
conn->EnableInactiveRelease(10);//启动非活跃超时连接
conn->Established();//就绪初始化
_conns.insert(std::make_pair(conn_id,conn));
}
int main()
{
srand(time(NULL));
Acceptor acceptor(&loop,8081);
acceptor.SetAcceptCallback(std::bind(NewConnection,std::placeholders::_1));
acceptor.Listen();
while(1)
{
loop.Start();
}
return 0;
}


1.10 LoopThread and LoopThreadPool模块
1.10.1 实现
html
// 用于实现_loop获取的同步关系,避免线程创建了,但是_loop还没有去实例化之前去获取_loop
class LoopThread
{
private:
// 互斥锁
std::mutex _mutex;
// 条件变量:让一个或多个线程在满足某个特定条件之前进入等待状态,并且在条件可能满足时由其他线程唤醒它们
std::condition_variable _cond;
// EventLoop 指针变量,这个对象需要在线程内实例化
EventLoop *_loop;
// EventLoop对应的线程
std::thread _thread;
private:
// 实例化EventLoop对象,唤醒_cond上有可能阻塞的线程,并且开始运行EventLoop模块的Start()
// 将一个线程和一个loop相关联
void ThreadEntry()
{
EventLoop loop;
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
// 设置loop给_loop
_loop = &loop;
// 唤醒_cond上有可能阻塞的线程
_cond.notify_all();
}
// loop开启事件监控
loop.Start();
}
public:
// 创建线程,设定线程的入口函数
// 传入LoopThread的ThreadEntry函数构造线程,将线程和loop相关联
LoopThread() : _loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
// 返回当前线程关联的EventLoop对象指针
EventLoop *GetLoop()
{
EventLoop *loop = NULL;
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex); // 加锁
// _cond.wait loop为NULL就一直阻塞
_cond.wait(lock, [&]()
{ return _loop != NULL; });
loop = _loop;
}
return loop;
}
};
class LoopThreadPool
{
private:
//线程数量
int _thread_count;
//对应下标
int _next_idx;
//主Rector
EventLoop *_baseLoop;
//threads
std::vector<LoopThread *> _threads;
//从属Rector -- loops
std::vector<EventLoop *> _loops;
public:
LoopThreadPool(EventLoop *baseloop) : _thread_count(0), _next_idx(0), _baseLoop(baseloop) {}
void SetThreadCount(int count) { _thread_count = count; }
// 创建_thread_count大小个LoopThread 和 从属loop
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();
//给每一个thread GetLoop
_loops[i] = _threads[i]->GetLoop();
}
}
return;
}
// 获取下一个loop -- 主loop存在时候就是获取从属loop
EventLoop *NextLoop()
{
if (_thread_count == 0)
{
return _baseLoop;
}
_next_idx = (_next_idx + 1) % _thread_count;
return _loops[_next_idx];
}
};
1.10.2 LoopThread Test
html
// //----------LoopThread test
//管理所有的连接
std::unordered_map<uint64_t,PtrConnection> _conns;
uint64_t conn_id = 0;
std::vector<LoopThread> threads(2);
EventLoop base_loop;
int next_loop = 0;
void ConnectionDestroy(const PtrConnection& conn)
{
_conns.erase(conn->Id());
}
void OnConnected(const PtrConnection& conn)
{
DBG_LOG("NEW CONNECTION:%p",conn.get());
}
void OnMessage(const PtrConnection& conn,Buffer * buf)
{
DBG_LOG("%s",buf->ReadPosition());
buf->MoveReadOffset(buf->ReadAbleSize());
std::string str = "server say: hello world";
conn->Send(str.c_str(),str.size());
conn->Shutdown();
}
void NewConnection(int fd)
{
conn_id++;
next_loop = (next_loop + 1) % 2;
PtrConnection conn(new Connection(threads[next_loop].GetLoop(),conn_id,fd));
conn->SetMessageCallback(std::bind(OnMessage,std::placeholders::_1,std::placeholders::_2));
conn->SetSrvClosedCallback(std::bind(ConnectionDestroy,std::placeholders::_1));
conn->SetConnectedCallback(std::bind(OnConnected,std::placeholders::_1));
conn->EnableInactiveRelease(10);//启动非活跃超时连接
conn->Established();//就绪初始化
_conns.insert(std::make_pair(conn_id,conn));
DBG_LOG("NEW -----------------------------------");
}
int main()
{
srand(time(NULL));
Acceptor acceptor(&base_loop,8081);
acceptor.SetAcceptCallback(std::bind(NewConnection,std::placeholders::_1));
acceptor.Listen();
while(1)
{
base_loop.Start();
}
return 0;
}

1.10.3 LoopThreadPool Test
html
// ----------LoopThreadPool test--------------
// 管理所有的连接
std::unordered_map<uint64_t,PtrConnection> _conns;
uint64_t conn_id = 0;
EventLoop base_loop;
LoopThreadPool* loop_pool;
void ConnectionDestroy(const PtrConnection& conn)
{
_conns.erase(conn->Id());
}
void OnConnected(const PtrConnection& conn)
{
DBG_LOG("NEW CONNECTION:%p",conn.get());
}
void OnMessage(const PtrConnection& conn,Buffer * buf)
{
DBG_LOG("%s",buf->ReadPosition());
buf->MoveReadOffset(buf->ReadAbleSize());
std::string str = "server say : hello world";
conn->Send(str.c_str(),str.size());
conn->Shutdown();
}
void NewConnection(int fd)
{
conn_id++;
PtrConnection conn(new Connection(loop_pool->NextLoop(),conn_id,fd));
conn->SetMessageCallback(std::bind(OnMessage,std::placeholders::_1,std::placeholders::_2));
conn->SetSrvClosedCallback(std::bind(ConnectionDestroy,std::placeholders::_1));
conn->SetConnectedCallback(std::bind(OnConnected,std::placeholders::_1));
conn->EnableInactiveRelease(10);//启动非活跃超时连接
conn->Established();//就绪初始化
_conns.insert(std::make_pair(conn_id,conn));
DBG_LOG("NEW -----------------");
}
int main()
{
loop_pool = new LoopThreadPool(&base_loop);
loop_pool->SetThreadCount(2);
loop_pool->Create();
Acceptor acceptor(&base_loop,8080);
acceptor.SetAcceptCallback(std::bind(NewConnection,std::placeholders::_1));
acceptor.Listen();
base_loop.Start();
return 0;
}


1.11 TcpServer模块
这个模块是⼀个整体Tcp服务器模块的封装,内部封装了Acceptor模块,EventLoop、ThreadPool模块。
1.11.1 TcpServer模块设计与实现
TcpServer模块是基于EventLoop模型实现的TCP服务器核心类,其设计旨在提供灵活的多线程处理能力,模块内部包含以下核心组件:
-
主EventLoop对象:模块内部包含一个基础的EventLoop对象,无需创建EventLoop线程池,直接在主线中完成所有操作。
-
EventLoopThreadPool线程池:模块内部包含一个LoopThreadPool对象,即子Reactor线程池。该线程池管理多个EventLoop线程,用于处理已建立连接的IO事件,实现多线程并发处理能力。
-
Acceptor连接接收器:模块内部包含一个Acceptor对象,负责管理监听套接字。Acceptor完成新客户端连接的获取与处理,是服务器接收新连接的核心组件。
-
Connection连接管理器 :模块内部包含一个以**
std::shared_ptr<Connection>**为值的哈希表,用于保存所有新建连接对应的Connection对象。所有Connection对象均使用shared_ptr进行智能指针管理,确保在从哈希表中删除连接信息后,当shared_ptr引用计数为零时,能够自动完成Connection资源的释放操作。
1.11.2 工作流程
首先,在实例化TcpServer对象的过程中,完成基础EventLoop(BaseLoop)的设置,实例化Acceptor对象和LoopThreadPool线程池,同时创建用于管理Connection对象的哈希表。
其次,为Acceptor对象设置新连接回调函数。当获取到新连接后,执行以下操作:
-
为新连接构建Connection对象。
-
设置Connection的各项事件回调函数。
-
使用shared_ptr智能指针管理Connection对象。
-
将Connection对象添加到哈希表中进行统一管理。
-
为Connection选择一个合适的EventLoop线程(从线程池中选取)。
-
为Connection添加定时销毁任务,实现连接的超时管理机制。
-
为Connection添加IO事件监控,使其能够处理读写事件。
最后,启动基础EventLoop(BaseLoop),开始事件循环,驱动整个服务器正常运行。
该设计通过主从Reactor多线程模型,实现了高性能的并发连接处理能力,同时通过智能指针管理连接资源,确保资源的安全释放。
1.11.3 TcpServer的实现
html
// 整合模块
class TcpServer
{
private:
// 自增长的连接ID
uint64_t _next_id;
// 端口
int _port;
// 超过_timeout秒,为非活跃连接
int _timeout;
// 是否启动了非活跃超时连接销毁的判断标志
bool _enable_inactive_release;
// 主线程的EventLoop对象,负责监听事件的处理
EventLoop _baseloop;
// 监听套接字管理对象
Acceptor _acceptor;
// 从属EventLoop线程池
LoopThreadPool _pool;
// 保存管理所有连接对应的shared_ptr对象
std::unordered_map<uint64_t, PtrConnection> _conns;
// 一系列的回调函数
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 ++
_next_id++;
// 构造一个PtrConnection
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
// 为新Connection设置回调 TcpServer的回调函数--之后传入设置
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中管理
_conns.insert(std::make_pair(_next_id, conn));
}
// 从管理Connection的_conns中移除连接信息 InLoop
void RemoveConnectionInLoop(const PtrConnection &conn)
{
// 找到conn对应的id
int id = conn->Id();
auto it = _conns.find(id);
// it != _conns.end() 找到了 删除
if (it != _conns.end())
{
_conns.erase(id);
}
}
void RemoveConnection(const PtrConnection &conn)
{
// 在_baseloop中运行
_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设置accept回调调用TcpServer的NewConnection
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
// 将_acceptor挂到_baseloop上 开始监听
_acceptor.Listen();
}
// 设置线程数量
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));
}
// tcpserver start
void Start()
{
//_pool create
_pool.Create();
// baseloop start
_baseloop.Start();
}
};
1.11.4 TcpServer 测试
html
#include "../source/server.hpp"
//------TcpServer----test------最终整合--------
void OnConnected(const PtrConnection &conn)
{
DBG_LOG("NEW CONNECTION:%p", conn.get());
}
void OnClosed(const PtrConnection &conn)
{
DBG_LOG("CLOSE CONNECTION:%p", conn.get());
}
void OnMessage(const PtrConnection &conn, Buffer *buf)
{
DBG_LOG("%s", buf->ReadPosition());
buf->MoveReadOffset(buf->ReadAbleSize());
std::string str = "server say : Hello World!!!!!!!!!";
conn->Send(str.c_str(), str.size());
}
int main()
{
TcpServer server(8081);
server.SetThreadCount(2);
server.EnableInactiveRelease(10);
server.SetClosedCallback(OnClosed);
server.SetConnectedCallback(OnConnected);
server.SetMessageCallback(OnMessage);
server.Start();
return 0;
}


1.11.5 使用webbench工具测试
1.11.5.1 webbench
webbench 是由 Lionbridge 公司开发的一款在 Linux 下使用的简单网站压力测试工具。它的核心原理是使用 fork() 系统调用创建多个子进程,来模拟多个客户端同时访问指定的 URL,从而测试 web 服务器在高并发压力下的性能表现。
基本命令:
bash
webbench -c <并发数> -t <测试时间(秒)> <目标URL>
前置设置:







通过对4核4GB配置的Ubuntu服务器进行Webbench压力测试,结果显示服务目前在6000并发时达到性能峰值,QPS为517.3,总请求数31040次/分钟,传输速率20.4KB/s,而在100到5000并发范围内表现稳定且呈线性增长,但并发数达到7000时因系统资源限制(如进程数、文件描述符或内存不足)导致连接失败。

2核2G的云服务器连接情况

1.12 EchoServer
实现完上面的server.hpp之后,我们就可以实现一个简单的Echo回显服务器了,我们只需要再给封装一层TcpServer就好了。
1.12.1 Echo.hpp
html
#include "../server.hpp"
// EchoServer 封装了 TcpServer
class EchoServer
{
private:
TcpServer _server;
private:
void OnConnected(const PtrConnection &conn)
{
DBG_LOG("新的连接到来了!!!,分配的conn_id为:%d", conn->Id());
DBG_LOG("NEW CONNECTION:%p", conn.get());
}
void OnClosed(const PtrConnection &conn)
{
DBG_LOG("连接关闭,关闭的conn_id为:%d", conn->Id());
DBG_LOG("CLOSE CONNECTION:%p", conn.get());
}
void OnMessage(const PtrConnection &conn, Buffer *buf)
{
//echo回显client发送到缓冲区的信息
//发送数据,从ReadPosition()发送ReadAble个数据
buf->PrependString("server say: ");
conn->Send(buf->ReadPosition(),buf->ReadAbleSize());
//更改buf的读偏移
buf->MoveReadOffset(buf->ReadAbleSize());
//通信一次就关闭
//conn->Shutdown();
}
// void OnMessage(const PtrConnection &conn, Buffer *buf)
// {
// // 发送更大的数据包来测试带宽
// std::string large_data(1024, 'X');
// conn->Send(large_data.c_str(), large_data.size());
// buf->MoveReadOffset(buf->ReadAbleSize());
// conn->Shutdown();
// }
public:
EchoServer(int port) : _server(port)
{
//_server设置线程数量
_server.SetThreadCount(5);
//_server设置非活跃超时连接的秒数
_server.EnableInactiveRelease(10);
// 往_server中设置关闭连接的回调函数OnClosed,设置占位符
_server.SetClosedCallback(std::bind(&EchoServer::OnClosed, this, std::placeholders::_1));
// 往_server中设置建立完成连接的回调函数OnConnected,设置占位符
_server.SetConnectedCallback(std::bind(&EchoServer::OnConnected, this, std::placeholders::_1));
// 往_server中设置回显消息的回调函数OnMessage,设置占位符
_server.SetMessageCallback(std::bind(&EchoServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
// 调用_server的Start启动EchoServer
void Start()
{
_server.Start();
}
};
html
#include "echo.hpp"
int main()
{
EchoServer server(8081);
server.Start();
return 0;
}



2. HTTP协议模块
HTTP协议模块用于对高并发服务器模块进行协议支持,基于提供的协议支持能够更方便的完成指定协议服务器的搭建。
2.1 Util模块
这个模块是⼀个工具模块,主要提供HTTP协议模块所用到的⼀些工具函数,比如url编解码,⽂件读 写....等。
2.1.1 ifstream ofstream
ifstream 和 ofstream是 C++ 中用于文件操作 的两个核心类,它们属于 文件流(File Stream)库。简单来说:
-
ifstream:i nput file stream (输入文件流),用于从文件中读取数据。 -
ofstream:o utput file stream (输出文件流),用于向文件中写入数据。
它们都定义在 <fstream> 头文件中,是处理文件读写的标准方式。
基本用法
html
#include <iostream>
#include <fstream>
using namespace std;
int main() {
// 1. 创建 ofstream 对象并打开文件
// 如果 "example.txt" 不存在,会自动创建
ofstream outFile("example.txt");
// 2. 检查是否成功打开
if (!outFile.is_open()) {
cerr << "无法打开文件进行写入!" << endl;
return 1;
}
// 3. 向文件写入数据 (就像使用 cout 一样)
outFile << "Hello, World!" << endl;
outFile << "这是第二行。" << endl;
outFile << 42 << " " << 3.14159 << endl;
// 4. 关闭文件 (对象销毁时也会自动关闭,但显式关闭是好习惯)
outFile.close();
cout << "写入完成。" << endl;
return 0;
}

html
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// 1. 创建 ifstream 对象并打开文件
ifstream inFile("example.txt");
// 2. 检查是否成功打开
if (!inFile.is_open()) {
cerr << "无法打开文件进行读取!" << endl;
return 1;
}
// 3. 从文件读取数据
string line;
int num;
double pi;
// 逐行读取
while (getline(inFile, line)) {
cout << "读取到: " << line << endl;
}
// 4. 关闭文件
inFile.close();
return 0;
}

重要模式标志
| 标志 | 作用 |
|---|---|
ios::in |
打开文件用于读取 (ifstream默认) |
ios::out |
打开文件用于写入 (ofstream默认) |
ios::binary |
以二进制模式打开(不进行文本转换,如 \n 到 \r\n) |
ios::app |
写入时追加到文件末尾(不覆盖原有内容) |
ios::trunc |
如果文件存在,清空内容(ofstream默认会这样做) |
ios::ate |
打开文件后,移动到文件末尾 |
2.1.2 UrlDecode and UrlEncode
统一资源定位符(URL)作为互联网资源定位与访问 的核心机制,其设计与实现必须确保在不同系统、不同协议以及不同字符集环境下能够被准确解析与传输 。URL 最初只支持英文和数字。如果你想在网址里放中文、空格、表情符号等,就必须把它们转换成浏览器和服务器都能认的"代号"(也就是 %HH 格式),否则网络设备看不懂就会出错。URL 里有些字符是有特殊功能的,比如 ? 用来分隔参数、# 用来定位、/ 用来分隔路径。如果你的文件名或参数里恰好也包含这些字符(比如文件名叫 关于?我们.txt ),不编码的话,服务器会搞不清这个 ? 到底是"参数开始"还是"文件名的一部分"。编码后(%3F),服务器就知道它只是普通数据。
所以在构建服务器时需要:
-
收到请求后要解码 :比如把
%E5%85%B3%E4%BA%8E还原成**"关于"**,才能找到文件。 -
返回链接时要编码:确保生成的所有 URL 都是标准格式,不会因为特殊字符而损坏。
2.1.3 stat函数
stat 是 Linux/Unix 系统中的一个非常重要的系统调用函数,用于获取文件或目录的详细信息(如大小、权限、创建时间、文件类型等)。
函数原型:
html
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
-
返回值:成功返回 0,失败返回 -1(并设置 errno)。
-
pathname:文件路径。
-
statbuf :指向**
struct stat**结构体的指针,用于接收文件信息。
struct stat 结构体
html
struct stat {
dev_t st_dev; // 文件所在设备的ID
ino_t st_ino; // inode编号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者用户ID
gid_t st_gid; // 所有者组ID
dev_t st_rdev; // 设备文件类型
off_t st_size; // 文件大小(字节)
blksize_t st_blksize; // 块大小
blkcnt_t st_blocks; // 占用块数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};
使用示例
html
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
struct stat fileStat;
// 获取文件信息
if (stat("example.txt", &fileStat) == -1) {
perror("stat 失败");
return 1;
}
// 文件大小
printf("文件大小: %ld 字节\n", fileStat.st_size);
// 文件权限(以八进制显示)
printf("文件权限: %o\n", fileStat.st_mode & 0777);
// 判断文件类型
if (S_ISREG(fileStat.st_mode)) {
printf("这是一个普通文件\n");
} else if (S_ISDIR(fileStat.st_mode)) {
printf("这是一个目录\n");
}
return 0;
}

2.1.4 mime类型
MIME 类型由两部分组成:主类型/子类型。它用来标识文件的本质。如:
text/html:这是一个文本文件,具体来说是 HTML 网页。
image/jpeg:这是一个图片,具体来说是 JPEG 格式。
application/pdf:这是一个二进制文档,具体来说是 PDF 格式。
application/json:这是一个数据文件,具体来说是 JSON 格式。
....
根据文件扩展名(文件后缀)获取mime
就是告诉浏览器"你接下来收到的这个文件是什么东西,该怎么处理它"。
比如当你在浏览器里访问一个网址,比如 http://example.com/report.pdf:
1.服务器在硬盘上找到了 report.pdf 这个文件。
2.服务器需要把这个文件的二进制数据通过网络发回去。
3.关键问题:浏览器收到这一堆字节流时,它怎么知道这是 PDF、是图片、还是一个网页?
答案是:服务器必须在返回数据(Response)的最前面加上一行说明,也就是 Content-Type 响应头。
4.如果服务器说 Content-Type: application/pdf,浏览器就知道调用内置的 PDF 阅读器来展示。
5.如果服务器说 Content-Type: image/png,浏览器就知道把它当作图片渲染。
6.如果服务器没说,或者说错了,浏览器可能就会乱猜,导致页面显示乱码、图片不显示、或者直接把文件当成乱码文本显示出来。
所以我们的服务器维护了这么一张"表": 来根据文件扩展名获取mime让浏览器正确解析。
html
// 文件后缀扩展信息
std::unordered_map<std::string, std::string> _mime_msg = {
{".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"}};
2.1.5 Util类实现及测试
html
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <cassert>
#include <cstdio>
#include <unordered_map>
#ifdef _WIN32
#include <direct.h>
#define mkdir _mkdir
#else
#include <unistd.h>
#endif
std::unordered_map<int, std::string> _statu_msg = {
{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"}};
// 文件后缀扩展信息
std::unordered_map<std::string, std::string> _mime_msg = {
{".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"}};
// 工具类 这个类里面的函数设置为static,这样提供给其他模块的时候就不需要传递this指针
class Util
{
public:
// 字符串分割函数,将src字符串安装sep字符进行分割,得到的各个子串放入arry中,然后返回子串数量
static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry)
{
// offset为偏移量,起始为0
size_t offset = 0;
// 循环分割
while (offset < src.size())
{
// 在src字符串偏移量offset处,开始向后查找sep字符(子串),然后返回查找到的位置
size_t pos = src.find(sep, offset);
// 没有找到特定的sep就将剩余部分作为一个子串放入array中
if (pos == std::string::npos)
{
// 如果posy已到src结尾直接break
if (pos == src.size())
break;
// substr一直截取offset到结尾
arry->push_back(src.substr(offset));
// 返回子串个数
return arry->size();
}
// 如果pos == offset 说明当前子串是空的,没有内容
// offset移动pos + sep.size()大小 continue
if (pos == offset)
{
offset = pos + sep.size();
continue;
}
// 从src中截取offset到pos -offset的子串放入arry中
arry->push_back(src.substr(offset, pos - offset));
// offset后移
offset = pos + sep.size();
}
// 跳出循环,返回子串个数
return arry->size();
}
// 读取文件的所有内容,将读取的内容放入到一个string中
static bool ReadFile(const std::string &filename, std::string *buf)
{
// 以二进制流的方式读取文件
// ifstream binary
std::ifstream ifs(filename, std::ios::binary);
if (ifs.is_open() == false)
{
printf("OPEN %s FILE FAILED!!", filename.c_str());
return false;
}
size_t fsize = 0;
// seekg 跳转读写位置到末尾 ifs.end
ifs.seekg(0, ifs.end);
// 获取文件大小:tellg -- 获取当前读写位置相对于起始位置的偏移量
fsize = ifs.tellg();
// 然后再跳转到起始位置 ifs.beg
ifs.seekg(0, ifs.beg);
// 开辟文件大小空间
buf->resize(fsize);
// 读取文件到buf中
ifs.read(&(*buf)[0], fsize);
// 如果读取失败
if (ifs.good() == false)
{
printf("READ %s FILE FALIED!!", filename.c_str());
// 注意关闭文件
ifs.close();
return false;
}
// 读取成功关闭文件
ifs.close();
return true;
}
// 向文件写入数据
static bool WriteFile(const std::string &filename, const std::string &buf)
{
// 二进制流的形式打开文件 trunc选项 -- 每次写入清空之前的内容
std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);
if (ofs.is_open() == false)
{
// 打开失败
printf("OPEN %s FILE FAILED!!", filename.c_str());
return false;
}
// ofs write to buf
ofs.write(buf.c_str(), buf.size());
if (ofs.good() == false)
{
printf("WRITE %s FILE FAILED!!", filename.c_str());
// 注意关闭文件
ofs.close();
return false;
}
// 关闭文件然后返回
ofs.close();
return true;
}
// 比如 https://example.com?name=张三&age=25 中,查询字符串为 name=张三&age=25
// URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中的特殊字符产生歧义
// 确保数据能准确无误地从浏览器传递到你的服务器代码中
// 编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀为% 如: C++ -> %2B2B
// RFC3986文档规定不编码的特殊字符: . - _ ~ 字母,数字属于绝对不编码字符
// W3C标准中规定,查询字符串中的空格,需要编码为 + ,解码则是 + 转空格
static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
{
// 定义res
std::string res;
// 遍历url
for (auto &c : url)
{
// 不编码的特殊字符串判断处理 isalnum判断是否是字母或者数字
if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c))
{
res += c;
continue;
}
// W3C标准判断是否空格转 '+'
if (c == ' ' && convert_space_to_plus == true)
{
res += '+';
continue;
}
// 剩下的字符都是要转成%HH格式的
char tmp[4] = {0};
// snprintf格式化字符串,放入到一个空间中
snprintf(tmp, 4, "%%%02X", c);
res += tmp;
}
return res;
}
// 16进制转数字
static char HEXTOI(char c)
{
// 0 - 9 return c - '0'
if (c >= '0' && c <= '9')
{
return c - '0';
}
// a - z return c - 'a' + 10
else if (c >= 'a' && c <= 'z')
{
return c - 'a' + 10;
}
// A - Z return c - 'A' + 10
else if (c >= 'A' && c <= 'Z')
{
return c - 'A' + 10;
}
// 出错返回-1
return -1;
}
// 将url解码 /%E5%85%B3%E4%BA%8E
static std::string UrlDecode(const std::string url, bool convert_plus_to_space)
{
// 定义res
std::string res;
// 遍历url
for (int i = 0; i < url.size(); i++)
{
// 判断W3C标准中解码 '+' 是否转 空格
if (url[i] == '+' && convert_plus_to_space == true)
{
res += ' ';
continue;
}
// 遍历到%,将%后面的字符进行转换
if (url[i] == '%' && (i + 2) < url.size())
{
// 转换:将%后面的两个字符转换为数字,第一个数字*16 + 第二个数字
char v1 = HEXTOI(url[i + 1]);
char v2 = HEXTOI(url[i + 2]);
char v = v1 * 16 + v2;
res += v;
i += 2;
continue;
}
res += url[i];
}
return res;
}
// 获取响应的状态码的描述信息
static std::string StatuDesc(int statu)
{
// 在_statu_msg中find
auto it = _statu_msg.find(statu);
if (it != _statu_msg.end())
{
return it->second;
}
// 没有找到返回 "UNKnown"
return "Unknown";
}
// 判断一个文件是否是普通文件
static bool IsRegular(const std::string &filename)
{
// 获取文件属性信息 -- struct stat
struct stat st;
// stat 函数
int ret = stat(filename.c_str(), &st);
if (ret < 0)
{
// ret < 0
return false;
}
// 使用宏S_ISREG判断
return S_ISREG(st.st_mode);
}
// 判断一个文件是否是一个目录
static bool IsDirectory(const std::string &filename)
{
// 和判断文件类似
// 获取文件属性信息 -- struct stat
struct stat st;
// stat 函数
int ret = stat(filename.c_str(), &st);
if (ret < 0)
{
// ret < 0
return false;
}
// 使用宏S_ISDTR判断
return S_ISDIR(st.st_mode);
}
// 根据文件后缀名获取文件mine(MIME 类型是告诉浏览器"这是一个什么类型的文件,你应该怎么处理它"的标识)
// 比如:application/pdf,这是一个 PDF 文件,告诉浏览器可以调用内置的PDF阅读器来展示pdf文件
static std::string ExtMime(const std::string &filename)
{
// a.b.txt
// 先获取文件扩展名 从filename后面查找'.'
size_t pos = filename.find_last_of('.');
if (pos == std::string::npos)
{
// 没有找到返回"application/octet-stream"
return "application/octet-stream";
}
// 截取ext扩展名 -- 从pos到末尾
std::string ext = filename.substr(pos);
// 然后再_mime_msg中查找
auto it = _mime_msg.find(ext);
if (it == _mime_msg.end())
{
// 没找到返回"application/octet-stream"
return "application/octet-stream";
}
// 找到了返回对应的mime
return it->second;
}
// http请求路径的有效性判断
// 注意:/index.html 这里的'/'是相对根目录,映射的是服务器上的子目录
// 客户端只能请求相对根目录中的资源,其他地方的资源都不予理会,不能被访问
// 比如在 /../login 这个路径中的..操作会让路径的查找跑到相对于根目录之外,这样是不合理的不安全的,所以要处理一下这个问题
static bool ValidPath(const std::string &path)
{
// 思想:按照/进行路径分割,根据有多少子目录,计算目录深度(有多少层) 且要求深度不能小于0
// 任意一层走出根目录都是有问题的
std::vector<std::string> subdir;
// 将path分割的子串放入subdir
Split(path, "/", &subdir);
// 定义层数
int level = 0;
// 遍历subdir
for (auto &dir : subdir)
{
// 如果dir为".."则level--
if (dir == "..")
{
level--;
// 如果level < 0 说明不是有效路径 return false
if (level < 0)
{
return false;
}
continue;
}
// 正常level++
level++;
}
return true;
}
};
// int main()
// {
// // std::cout << Util::ValidPath("/html/../../index.html") << std::endl;
// // std::cout << Util::IsRegular("regex.cpp") << std::endl;
// // std::cout << Util::IsRegular("testdir") << std::endl;
// // std::cout << Util::IsDirectory("regex.cpp") << std::endl;
// // std::cout << Util::IsDirectory("testdir") << std::endl;
// // std::string str = "C ";
// // std::string res = Util::UrlEncode(str, true);
// // std::string tmp = Util::UrlDecode(res, true);
// // std::cout << "[" <<res << "]\n";
// // std::cout << "[" <<tmp << "]\n";
// // std::string buf;
// // bool ret = Util::ReadFile("./stat.cpp", &buf);
// // if (ret == false)
// // {
// // return false;
// // }
// // ret = Util::WriteFile("./tttttttt.c", buf);
// // if (ret == false)
// // {
// // return false;
// // }
// std::string str = "abc,abcd,,,123121";
// std::vector<std::string> arry;
// Util::Split(str, ",", &arry);
// for (auto &s : arry)
// {
// std::cout << "[" << s << "]\n";
// }
// return 0;
// }
void TestSplit()
{
std::cout << "=== Test Split ===" << std::endl;
std::vector<std::string> result;
// 正常分割
Util::Split("a,b,c", ",", &result);
assert(result.size() == 3 && result[0] == "a" && result[1] == "b" && result[2] == "c");
result.clear();
// 连续分隔符(空串被忽略)
Util::Split("a,,b", ",", &result);
assert(result.size() == 2 && result[0] == "a" && result[1] == "b");
result.clear();
// 分隔符在开头和结尾
Util::Split(",a,b,", ",", &result);
assert(result.size() == 2 && result[0] == "a" && result[1] == "b");
result.clear();
// 无分隔符
Util::Split("hello", ",", &result);
assert(result.size() == 1 && result[0] == "hello");
result.clear();
// 空字符串
Util::Split("", ",", &result);
assert(result.empty());
// 多字符分隔符
Util::Split("a--b--c", "--", &result);
assert(result.size() == 3 && result[0] == "a" && result[1] == "b" && result[2] == "c");
result.clear();
// 分隔符连续出现
Util::Split("a----b", "--", &result);
assert(result.size() == 2 && result[0] == "a" && result[1] == "b");
result.clear();
std::cout << "Split tests passed." << std::endl;
}
void TestReadWriteFile()
{
std::cout << "=== Test Read/Write File ===" << std::endl;
std::string filename = "test_temp.txt";
std::string content = "Hello, 世界!";
// 写入文件
bool ret = Util::WriteFile(filename, content);
assert(ret == true);
// 读取文件
std::string read_buf;
ret = Util::ReadFile(filename, &read_buf);
assert(ret == true);
assert(read_buf == content);
// 读取不存在的文件
ret = Util::ReadFile("nonexistent_file", &read_buf);
assert(ret == false);
// 清理
std::remove(filename.c_str());
std::cout << "Read/Write file tests passed." << std::endl;
}
void TestUrlEncoding()
{
std::cout << "=== Test URL Encoding/Decoding ===" << std::endl;
// 普通字符不编码
std::string plain = "abc123.-_~";
std::string encoded = Util::UrlEncode(plain, false);
assert(encoded == plain);
// 特殊字符编码
plain = "C++";
encoded = Util::UrlEncode(plain, false);
assert(encoded == "C%2B%2B");
// 空格转 + (W3C标准)
plain = "hello world";
encoded = Util::UrlEncode(plain, true);
assert(encoded == "hello+world");
// 空格不转 +
encoded = Util::UrlEncode(plain, false);
assert(encoded == "hello%20world");
// 混合字符
plain = "?&=#";
encoded = Util::UrlEncode(plain, false);
assert(encoded == "%3F%26%3D%23");
// 解码测试
std::string url = "hello+world";
std::string decoded = Util::UrlDecode(url, true);
assert(decoded == "hello world");
url = "hello%20world";
decoded = Util::UrlDecode(url, false);
assert(decoded == "hello world");
url = "C%2B%2B";
decoded = Util::UrlDecode(url, false);
assert(decoded == "C++");
// 无效的 % 序列(应保留原样)
url = "abc%2";
decoded = Util::UrlDecode(url, false);
assert(decoded == "abc%2");
url = "abc%";
decoded = Util::UrlDecode(url, false);
assert(decoded == "abc%");
std::cout << "URL encoding/decoding tests passed." << std::endl;
}
void TestStatuDesc()
{
std::cout << "=== Test Status Description ===" << std::endl;
assert(Util::StatuDesc(200) == "OK");
assert(Util::StatuDesc(404) == "Not Found");
// 注意 map 中无 500,返回 "Unknown"
assert(Util::StatuDesc(500) == "Unknown");
assert(Util::StatuDesc(999) == "Unknown");
std::cout << "Status description tests passed." << std::endl;
}
void TestFileType()
{
std::cout << "=== Test IsRegular / IsDirectory ===" << std::endl;
std::string filename = "test_regfile.txt";
std::string dirname = "test_dir";
// 创建普通文件
Util::WriteFile(filename, "dummy");
assert(Util::IsRegular(filename) == true);
assert(Util::IsDirectory(filename) == false);
// 创建目录(需平台兼容)
#ifdef _WIN32
_mkdir(dirname.c_str());
#else
mkdir(dirname.c_str(), 0755);
#endif
assert(Util::IsDirectory(dirname) == true);
assert(Util::IsRegular(dirname) == false);
// 不存在的路径
assert(Util::IsRegular("no_such_file") == false);
assert(Util::IsDirectory("no_such_dir") == false);
// 清理
std::remove(filename.c_str());
rmdir(dirname.c_str());
std::cout << "File type tests passed." << std::endl;
}
void TestExtMime()
{
std::cout << "=== Test Extension to MIME ===" << std::endl;
assert(Util::ExtMime("index.html") == "text/html");
assert(Util::ExtMime("style.css") == "text/css");
assert(Util::ExtMime("script.js") == "text/javascript");
assert(Util::ExtMime("image.jpg") == "image/jpeg");
//assert(Util::ExtMime("file.PDF") == "application/pdf");
// 后缀不区分大小写?map中是".pdf",需要小写,所以会找不到返回默认
// 注意:map中键是".pdf",而".PDF"不同,所以应返回默认
assert(Util::ExtMime("file.PDF") == "application/octet-stream");
// 无扩展名
assert(Util::ExtMime("README") == "application/octet-stream");
// 未知扩展名
assert(Util::ExtMime("file.xyz") == "application/octet-stream");
std::cout << "Extension to MIME tests passed." << std::endl;
}
void TestValidPath()
{
std::cout << "=== Test ValidPath ===" << std::endl;
// 有效路径
assert(Util::ValidPath("/index.html") == true);
assert(Util::ValidPath("/a/b/c") == true);
assert(Util::ValidPath("/a/../b") == true); // a/../b 相当于 b,在根目录内
assert(Util::ValidPath("a/b/c") == true); // 相对路径,无前导/也可
assert(Util::ValidPath("/") == true); // 根目录
// 无效路径(试图跳出根目录)
assert(Util::ValidPath("/../etc/passwd") == false);
assert(Util::ValidPath("/a/../../b") == false); // 超过根目录
assert(Util::ValidPath("..") == false); // 直接 .. 深度负
assert(Util::ValidPath("a/../../../b") == false);
assert(Util::ValidPath("") == true);
std::cout << "ValidPath tests passed." << std::endl;
}
int main()
{
TestSplit();
TestReadWriteFile();
TestUrlEncoding();
TestStatuDesc();
TestFileType();
TestExtMime();
TestValidPath();
std::cout << "\nAll tests passed successfully!" << std::endl;
return 0;
}
2.2 HttpRequest模块
这个模块是HTTP请求数据模块,用于保存HTTP请求数据被解析后的各项请求元素信息。
HTTP请求数据的一般格式主要包含以下三个部分:
第一,请求行(HttpLine),位于请求数据的第一行,由请求方法、请求目标资源路径和HTTP协议版本三个要素组成,三者之间以空格分隔,例如GET /index.html HTTP/1.1。
第二,请求头(HttpHeader),位于请求行之后,由多行键值对构成,每行格式为"字段名: 字段值",用于传递客户端环境、请求属性和内容描述等元数据信息,常见的请求头字段包括Host、User-Agent、Content-Type等。
第三,请求体(HttpBody),位于请求头之后的空行之后,是请求中的可选部分,主要用于在POST或PUT等请求方法中向服务器提交数据,其内容格式和长度由请求头中的Content-Type和Content-Length字段指定。
html
// 这个模块是HTTP请求数据模块,用于保存HTTP请求数据被解析后的各项请求元素信息。
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:
// 构造 _version 为 "HTTP/1.1"
HttpRequest() : _version("HTTP/1.1") {}
// http请求重置
void ReSet()
{
_method.clear();
_path.clear();
_version = "HTTP/1.1";
std::smatch match;
//_matches和match交换
_matches.swap(match);
_headers.clear();
_params.clear();
}
// 插入头部字段
void SetHeader(const std::string &key, const std::string &val)
{
// 在_headers中插入键值对
_headers.insert(std::make_pair(key, val));
}
// 判断是否存在指定的头部字段
bool HasHeader(const std::string key) const
{
auto it = _headers.find(key);
if (it == _headers.end())
{
return false;
}
return true;
}
// 获取指定头部字段对应的val
std::string GetHeader(const std::string key) const
{
auto it = _headers.find(key);
if (it == _headers.end())
{
// 没找到 返回空串
return " ";
}
return it->second;
}
// 插入查询字符串
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 false;
}
return true;
}
// 获取指定查询字符串对应的val
std::string GetParam(const std::string &key) const
{
auto it = _params.find(key);
if (it == _params.end())
{
// 没找到 返回空串
return " ";
}
return it->second;
}
// 获取正文长度
size_t ContentLength() const
{
// 直接在正文字段中查找然后获取
// Content-Length: 1234\r\n
bool ret = HasHeader("Content-Length");
if (ret == false)
{
return 0;
}
std::string clen = GetHeader("Content-Length");
// stol 字符串转数字
return std::stol(clen);
}
// 短连接(Connection: close):每次请求都要重新建立TCP连接,这个过程需要三次握手,
// 会额外消耗时间和服务器资源,导致更高的延迟。
// 长连接(Connection: Keep-Alive):多个请求可以复用同一个连接,
// 省去了重复建立连接的开销,页面加载和资源获取速度更快。
// 判断是否是短链接
bool Close() const
{
// 没有Connection字段或者有Connection字段但是val为close,则都为短连接,否则都是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};
2.3 HttpResponse模块
这个模块是HTTP响应数据模块,用于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终会被按照HTTP协议响应格式组织成为响应信息发送给客户端。
HTTP响应数据的一般格式主要包含以下三个部分:
第一,状态行(RespStatu),位于响应数据的第一行,由HTTP协议版本、状态码和状态描述三个要素组成,三者之间以空格分隔,例如HTTP/1.1 200 OK。
第二,响应头(Header),位于状态行之后,由多行键值对构成,每行格式为"字段名: 字段值",用于传递服务器信息、响应属性和内容描述等元数据,常见的响应头字段包括Content-Type、Content-Length、Server、Set-Cookie等。
第三,响应体(Body),位于响应头之后的空行之后,是响应中的主要数据部分,用于向客户端返回请求的资源内容,如HTML页面、图片数据或JSON字符串等,其格式和长度由响应头中的Content-Type和Content-Length字段指定。
html
// 这个模块是HTTP响应数据模块,⽤于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终
// 会被按照HTTP协议响应格式组织成为响应信息发送给客⼾端。
class HttpResponse
{
public:
// 状态码
int _statu;
// 是否重定向标志
bool _redirect_flag;
// 正文
std::string _body;
// 重定向地址
std::string _redirect_url;
// 头部字段
std::unordered_map<std::string, std::string> _headers;
public:
// 无参构造 _redirect_flag 默认设为false _statu 为 200 正常
HttpResponse() : _redirect_flag(false), _statu(200) {}
// statu构造
HttpResponse(int statu) : _redirect_flag(false), _statu(200) {}
// 重置 HttpResponse
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 false;
}
return true;
}
// 获取指定头部字段的val
std::string GetHeader(const std::string &key) const
{
auto it = _headers.find(key);
if (it == _headers.end())
{
// 返回空串
return "";
}
return it->second;
}
// 设置响应正文 默认为"text/html"
void SetContent(const std::string &body, const std::string &type = "text/html")
{
_body = body;
SetHeader("Content-Type", type);
}
// 设置重定向 状态码为302
void SetRedirect(const std::string &url, int statu = 302)
{
_statu = statu;
_redirect_flag = true;
_redirect_url = url;
}
// 判断是否是短连接
bool Close() const
{
// 没有Connection字段,或者有Connection但是值是close,则都是短连接,否则就是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};

2.4 HttpContext模块
该模块是一个HTTP请求接收的上下文模块,主要功能是应对网络传输中数据分片到达的场景 。由于单次接收的数据可能不足以构成一个完整的HTTP请求,导致解析过程无法一次完成,请求处理也就无法立即进行。该模块通过维护解析上下文,在每次接收到新数据时能够基于已有状态继续解析,最终拼接并构建出完整的HttpRequest请求对象。这一机制有效协调了请求数据的接收与解析过程,确保即使在分块传输的情况下,也能准确还原出完整的请求信息。
html
// http请求上下文
class HttpContext
{
private:
// 响应状态码
int _resp_statu;
// 当前接收及解析的阶段状态
HttpRecvStatu _recv_statu;
// 已经解析到的请求信息
HttpRequest _request;
private:
// 解析http首行
bool ParseHttpLine(const std::string &line)
{
// 定义matches;
std::smatch matches;
// 正则表达式匹配规则
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
// regex_match 进行匹配提取放到matches中
bool ret = std::regex_match(line, matches, e);
// 如果匹配失败返回fslse
if (ret == false)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400; // bad request
return false;
}
// HTTP首行请求行格式: GET /aaa/login?user=xiaoming&pass=123123 HTTP/1.1\r\n
// matches 中存储的信息例如:
// 0 : GET /aaa/login?user=xiaoming&pass=123123 HTTP/1.1
// 1 : GET
// 2 : /aaa/login
// 3 : user=xiaoming&pass=123123
// 4 : HTTP/1.1
// 请求方法的获取
_request._method = matches[1];
// transform 将 _request._method变大写,然后将结果放入_request._method中
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
// url的获取,需要对url进行解码,但是不要"+"转空格
_request._path = Util::UrlDecode(matches[2], false);
// 协议版本的获取
_request._version = matches[4];
// 查询字符串的获取和管理 用query_string_array 存储
std::vector<std::string> query_string_array;
// user=xiaoming&pass=123123
std::string query_string = matches[3];
// 查询字符串格式: key=val&key=val... 先以&符号进行分割,得到各个子串放入 query_string_array
Util::Split(query_string, "&", &query_string_array);
// 针对各个字符串,以"="进行分割,得到key和val,得到之后也就要进行URL解码
for (auto &str : query_string_array)
{
// 查找"="
size_t pos = str.find("=");
// 如果没有找到
if (pos == std::string::npos)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 400 -> bad request
_resp_statu = 400;
return false;
}
// 在str中截取key 需要"+" 转空格
std::string key = Util::UrlDecode(str.substr(0, pos), true);
// 在str中截取val 需要"+" 转空格
std::string val = Util::UrlDecode(str.substr(pos + 1), true);
_request.SetParam(key, val);
}
return true;
}
// 从buffer中接收http首行
bool RecvHttpLine(Buffer *buf)
{
// 如果目前的_recv_statu不为RECV_HTTP_LINE 则出错返回
if (_recv_statu != RECV_HTTP_LINE)
{
return false;
}
// 获取一行的数据,带有末尾的换行
std::string line = buf->GetLineAndPop();
// 如果没有接收到一行的数据
if (line.size() == 0)
{
// 缓冲区数据不足一行则需要判断目前buf的可读数据长度是不是很长
// 但是很长都不满足一行,说明接收有问题,出错返回 MAX_LINE -> 8192
if (buf->ReadAbleSize() > MAX_LINE)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 414 -> URL TOO LONG
_resp_statu = 414;
}
// 缓冲区数据不足一行,但是也有数据,那就等待新数据的到来,返回true
return true;
}
// 如果读取到的数据太长也出错返回
if (line.size() > MAX_LINE)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 414 -> URL TOO LONG
_resp_statu = 414;
return false;
}
// 接收到正确的首行后开始解析
bool ret = ParseHttpLine(line);
if (ret == false)
{
// 如果解析失败,出错返回
return false;
}
// 成功! 别忘了设置目前的_recv_statu 进入获取头部阶段
_recv_statu = RECV_HTTP_HEAD;
return true;
}
// 接收并解析头部
bool RecvHttpHead(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_HEAD)
{
return false;
}
// 一行一行的取出直到遇到空行为止
// 头部字段的格式:key: val\r\nkey: val\r\n...
// 一直循环进行
while (1)
{
// 从buf中获取一行
std::string line = buf->GetLineAndPop();
// 如果缓冲区数据不足一行
if (line.size() == 0)
{
// 缓冲区数据不足一行则需要判断目前buf的可读数据长度是不是很长
// 但是很长都不满足一行,说明接收有问题,出错返回 MAX_LINE -> 8192
if (buf->ReadAbleSize() > MAX_LINE)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 414 -> URL TOO LONG
_resp_statu = 414;
return false;
}
// 缓冲区数据不足一行,但是也有数据,那就等待新数据的到来,返回true
return true;
}
// 如果读取到的数据太长也出错返回
if (line.size() > MAX_LINE)
{
// 设置_recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 414 -> URL TOO LONG
_resp_statu = 414;
return false;
}
// 直到遇到空行为止
if (line == "\n" || line == "\r\n")
{
break;
}
// 解析头部字段
bool ret = ParseHttpHead(line);
if (ret == false)
{
return false;
}
}
// 头部字段处理完毕,进入正文获取阶段
_recv_statu = RECV_HTTP_BODY;
return true;
}
bool ParseHttpHead(std::string &line)
{
// key: val\r\nkey: val\r\n...
// 末尾是换行则去掉换行字符
if (line.back() == '\n')
line.pop_back();
// 末尾是回车则去掉回车字符
if (line.back() == '\r')
line.pop_back();
// 在line中查找 ": "
size_t pos = line.find(": ");
if (pos == std::string::npos)
{
// 没找到,头部字段格式有问题,出错返回
// 设置 _recv_statu _resp_statu
_recv_statu = RECV_HTTP_ERROR;
// 400 -> BAD REQUEST
_resp_statu = 400;
return false;
}
// 从line中截取key 找到了pos在':'的位置
std::string key = line.substr(0, pos);
// 在line中截取val
std::string val = line.substr(pos + 2);
// 把key和val设置进_request中
_request.SetHeader(key, val);
return true;
}
// 接收正文部分
bool RecvHttpBody(Buffer *buf)
{
// 如果_recv_statu != RECV_HTTP_BODY 出错返回
if (_recv_statu != RECV_HTTP_BODY)
{
return false;
}
// 1.获取正文长度
size_t content_length = _request.ContentLength();
if (content_length == 0)
{
// 没有正文,则请求接收解析完毕,设置_recv_statu,成功返回
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 2.计算实际还需要接受的正文长度,当前实际接收了多少正文:_request_body的大小
size_t real_len = content_length - _request._body.size();
// 3.将接收的正文放入_request._body中,但是也要考虑当前接收缓冲区中的数据,是否是全部的正文
// 如果当前接收缓冲区中的数据,包含了当前请求的正文,则取出所需的正文数据
if (buf->ReadAbleSize() >= real_len)
{
//_request._body再追加从buf的readposition开始的real_len大小的string
_request._body.append(buf->ReadPosition(), real_len);
// 别忘了移动当前的可读数据大小!!!
buf->MoveReadOffset(real_len);
// 这样当前的正文获取完全,请求接收解析完毕,设置_recv_statu返回真
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 如果当前缓冲区的数据不包含一个完成的正文大小,数据不足,则取出有的部分(buf->ReadAbleSize()),然后等待新的数据到来
_request._body.append(buf->ReadPosition(), buf->ReadAbleSize());
// 别忘了移动读偏移
buf->MoveReadOffset(buf->ReadAbleSize());
return true;
}
public:
// 构造HttpContext(),初始化_resp_statu为200,_recv_statu为接收首行状态
HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
// 重置HttpContext
void ReSet()
{
_resp_statu = 200;
_recv_statu = RECV_HTTP_LINE;
_request.ReSet();
}
// 返回_resp_statu
int RespStatu() { return _resp_statu; }
// 返回目前的接收状态
HttpRecvStatu RecvStatu() { return _recv_statu; }
// 返回当前的请求
HttpRequest &Request() { return _request; }
// 接收并解析httpRequest
void RecvAndAnalyzeHttpRequest(Buffer *buf)
{
// 不同的接收状态做不同的事情,但是这里不要break,因为处理请求首行后,
// 应该立即处理头部,而不是退出等待新数据
switch (_recv_statu)
{
case RECV_HTTP_LINE:
RecvHttpLine(buf);
case RECV_HTTP_HEAD:
RecvHttpHead(buf);
case RECV_HTTP_BODY:
RecvHttpBody(buf);
}
return;
}
};
2.5 HttpServer模块
HttpServer模块是为组件使用者提供的HTTP服务器模块,旨在通过简洁的接口实现HTTP服务器的快速搭建。在内部实现上,HttpServer模块包含一个TcpServer对象,由该对象负责底层服务器的具体搭建工作。同时,HttpServer模块为TcpServer对象提供了两个关键接口,分别是连接建立成功时的上下文设置接口以及数据处理接口。此外,HttpServer模块内部维护了一个哈希映射表,用于存储HTTP请求与处理函数之间的对应关系。我们可以通过该映射表向HttpServer注册特定请求所对应的处理函数,当TcpServer接收到相应请求时,即可调用已注册的函数完成请求处理。

html
// httpServer整合模块
class HttpServer
{
private:
using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
Handlers _get_route;
Handlers _post_route;
Handlers _put_route;
Handlers _delete_route;
// 静态资源根目录
std::string _basedir;
TcpServer _server;
private:
// 将HttpResponse中的要素按照http协议格式组织发送
void OrganizeResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp)
{
// 1.先完善头部
// 如果req为短链接则设置给rsp的"Connection"为"close" -- 短连接
if (req.Close() == true)
{
//DBG_LOG("CLOSE IS A CLOSE");
rsp.SetHeader("Connection", "close");
}
else
{
//DBG_LOG("CLOSE IS A KEEP_ALIVE");
// 否则设置为长连接
rsp.SetHeader("Connection", "keep-alive");
}
// 如果rsp的正文不为空且不存在"Content-Length"
if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
{
// 则设置给rsp的Content-Length为rsp的正文大小
rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
}
// 如果rsp的正文不为空且不存在"Content-Type"
if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
{
// 则设置给rsp的Content-Type为application/octet-stream 告诉浏览器:这是一个二进制文件,请下载处理
// 不指定特定格式
rsp.SetHeader("Content-Type", "applaction/octet-stream");
}
// 如果设置了重定向操作
if (rsp._redirect_flag == true)
{
// 则设置给rsp的Location为重定向的地址
rsp.SetHeader("Location", rsp._redirect_url);
}
// 2.将http response 中的要素按照http协议格式进行组织
std::stringstream rsp_str;
// 首行:req._version + 响应状态码 + 响应状态码对应的具体内容
rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
// 头部(刚刚设置进去的)
for (auto &head : rsp._headers)
{
// 组织头部的格式
rsp_str << head.first << ": " << head.second << "\r\n";
}
// 空行
rsp_str << "\r\n";
// 正文
rsp_str << rsp._body;
// 组织完成将完整的 rsp_str 由tcpserver发送数据
conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
}
// 判断请求的是否是静态资源
bool IsFileHandler(const HttpRequest &req)
{
// 1.必须设置了静态根目录
if (_basedir.empty())
{
return false;
}
// 2.请求方法必须为GET/HEAD
if (req._method != "GET" && req._method != "HEAD")
{
return false;
}
// 3.请求的路径必须是合法的路径
if (Util::ValidPath(req._path) == false)
{
return false;
}
// 4.请求的路径资源必须是存在且是一个普通文件
// 真正的req_path不要忘记加上前缀的相对根目录,也就是将请求路径转换为实际存在的路径
// 比如将 /image/a.png 转换为 ./wwwroot/image/a.png
// req_path = 静态资源根目录 + req请求的路径 (因为我们解析request的时候是按照'/'分割的_path)
std::string req_path = _basedir + req._path;
// 有一种情况比较特殊 -- 请求目录 "/" 给这种情况后面默认追加一个首页 index.html
if (req._path.back() == '/')
{
req_path += "index.html";
}
// 如果请求的不是普通文件返回false
if (Util::IsRegular(req_path) == false)
{
return false;
}
return true;
}
// 静态资源的请求处理
void FileHandler(const HttpRequest &req, HttpResponse *rsp)
{
std::string req_path = _basedir + req._path;
// 如果req._path的最后为'/'则返回给req_path加上首页
if (req._path.back() == '/')
{
req_path += "index.html";
}
// 根据 req_path 将静态资源文件的数据读取出来,放到rsp的_body中
// 并根据文件类型获取mime设置进rsp的头部字段中
bool ret = Util::ReadFile(req_path, &rsp->_body);
if (ret == false)
{
// 读取错误直接返回
return;
}
// 获取文件后缀对应的mime
std::string mime = Util::ExtMime(req_path);
// 设置进rsp的头部字段中 Content-Type 为 mime
rsp->SetHeader("Content-Type", mime);
return;
}
// 功能性请求的分类处理
void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
{
// using Handlers = std::vector<std::pair<std::regex, Handler>>;
// 在各个方法对应的请求路由表中查找是否有对资源的处理函数,有则调用,没有则返回404
// 思路:路由表存储的是键值对 -- 正则表达式 & 处理函数
// 可以使用正则表达式,将请求的资源进行正则匹配,匹配成功就调用对应的函数进行处理
for (auto &handler : handlers)
{
// re -- 匹配规则
const std::regex &re = handler.first;
// functor -- 处理函数
const Handler &functor = handler.second;
// 在req._path中按照re的匹配规则,匹配好的内容放入req._mathes中,调用regex_match
bool ret = std::regex_match(req._path, req._matches, re);
// 如果匹配失败,continue继续匹配
if (ret == false)
{
continue;
}
// 传入req和rsp,执行处理函数
return functor(req, rsp);
}
rsp->_statu = 404;
}
// 方法路由
void Route(HttpRequest &req, HttpResponse *rsp)
{
// 1.对请求进行辨析,是静态资源请求还是功能性请求,如果两个都不是则返回405
// 对静态资源请求调用FileHandler处理函数
// 对于功能性请求则需要通过对应的路由表来确定是否有对应的处理函数
if (IsFileHandler(req) == true)
{
// 是一个静态资源的请求,则调用FileHandler处理函数
return FileHandler(req, rsp);
}
if (req._method == "GET" || req._method == "HEAD")
{
return Dispatcher(req, rsp, _get_route);
}
if (req._method == "POST")
{
return Dispatcher(req, rsp, _post_route);
}
if (req._method == "PUT")
{
return Dispatcher(req, rsp, _put_route);
}
else if (req._method == "DELETE")
{
return Dispatcher(req, rsp, _delete_route);
}
// 405 -> METHOD NOT ALLOWED
rsp->_statu = 405;
return;
}
// 错误情况处理
void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
{
// 1.组织一个错误展示的页面
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(rsp->_statu);
body += " ";
// 获取响应状态码的描述信息
body += Util::StatuDesc(rsp->_statu);
body += "</h1>";
body += "</body>";
body += "</html>";
// 2.将页面数据,当作响应正文,放入rsp中
rsp->SetContent(body, "text/html");
}
// 设置一系列的回调函数
void OnConnected(const PtrConnection &conn)
{
// conn设置上下文 -- 构造http请求上下文
DBG_LOG("NEW CONNECTION %p",&conn);
conn->SetContext(HttpContext());
}
// 缓冲区数据解析 + 处理
void OnMessage(const PtrConnection &conn, Buffer *buffer)
{
// buffer中有数据就一直循环进行处理
while (buffer->ReadAbleSize() > 0)
{
// 1.获取上下文
HttpContext *context = conn->GetContext()->get<HttpContext>();
// 2.通过上下文对缓冲区数据进行解析
// 1.如果缓冲区的数据解析错误,就直接回复出错响应
// 2.如果解析正常,且请求已经获取完毕,才开始去进行处理
context->RecvAndAnalyzeHttpRequest(buffer);
// 3.从context中得到HttpRequest对象
HttpRequest &req = context->Request();
// 4.构造HttpResponse对象
HttpResponse rsp(context->RespStatu());
// 如果context的响应状态码 > 400 存在问题 -- 出错设置
if (context->RespStatu() >= 400)
{
// 进行错误响应,关闭连接,然后填充一个错误显示页面到rsp中
ErrorHandler(req, &rsp);
// 组织响应发送给客户端
OrganizeResponse(conn, req, rsp);
// 然后把http请求上下文状态重置
context->ReSet();
// 出错了就清空buffer中的可读数据
buffer->MoveReadOffset(buffer->ReadAbleSize());
// 关闭连接
conn->Shutdown();
return;
}
// 如果当前context的接收状态不为RECV_HTTP_OVER,
// 说明当前请求还没有接收完整,直接退出,等到新数据到来时候再重新继续处理
if (context->RecvStatu() != RECV_HTTP_OVER)
{
return;
}
// 走到这一切正常
// 3.路由请求 + 业务处理
Route(req, &rsp);
// 4.对HttpResponse进行组织发送
OrganizeResponse(conn, req, rsp);
// 5.发送完,重置上下文
context->ReSet();
// 6.根据长短连接判断是否关闭或者继续处理
// 短连接则直接关闭
if (rsp.Close() == true)
{
conn->Shutdown();
}
}
return;
}
public:
HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port)
{
_server.EnableInactiveRelease(timeout);
_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
_server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
void SetBaseDir(const std::string &path)
{
// assert要设置的路径要存在
assert(Util::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 Start()
{
_server.Start();
}
};
2.6 HttpServer Test
2.6.1 client1
html
/*长连接测试1:创建一个客户端持续给服务器发送数据,直到超过超时时间看看是否正常*/
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}
2.6.2 client2
cpp
/*超时连接测试1:创建一个客户端,给服务器发送一次数据后,不动了,查看服务器是否会正常的超时关闭连接*/
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(15);
}
cli_sock.Close();
return 0;
}
大概十秒client2被释放连接


2.6.3 client3
情况1:
html
/*给服务器发送一个数据,告诉服务器要发送100字节的数据,但是实际发送的数据不足100,查看服务器处理结果*/
/* 两种情况
1. 如果数据只发送一次,服务器将得不到完整请求,就不会进行业务处理,客户端也就得不到响应,最终超时关闭连接
2. 连着给服务器发送了多次小的请求,服务器会将后边的请求当作前边请求的正文进行处理,而后便处理的时候有可能就会因为处理错误而关闭连接
*/
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellohellohello";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
// assert(cli_sock.Send(req.c_str(), req.size()) != -1);
// assert(cli_sock.Send(req.c_str(), req.size()) != -1);
// assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}


情况2:发送多条
html
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellohellohello";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}


2.6.4 client4
html
/* 业务处理超时,查看服务器的处理情况
当服务器达到了一个性能瓶颈,在一次业务处理中花费了太长的时间(超过了服务器设置的非活跃超时时间)
1. 在一次业务处理中耗费太长时间,导致其他的连接也被连累超时,其他的连接有可能会被拖累超时释放
例如:假设现在有12345描述符就绪了在处理1的时候花费了30s处理完,超时了导致2345描述符因为长时间没有刷新活跃度
1. 如果接下来的2345描述符都是通信连接描述符,如果都就绪了,则并不影响,因为接下来会进行处理并刷新活跃度
2. 如果接下来的2号描述符是定时器事件描述符,定时器触发超时,执行定时任务,就会将345描述符给释放掉
这时候一旦345描述符对应的连接被释放,接下来在处理345事件的时候就会导致程序崩溃(内存访问错误)
因此这时候,在本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,
等到事件处理完了执行任务池中的任务的时候,再去释放
*/
#include "../source/server.hpp"
int main()
{
//对讲述进程进行一个处理SIG_IGN
signal(SIGCHLD, SIG_IGN);
for (int i = 0; i < 10; i++) {
pid_t pid = fork();
if (pid < 0) {
DBG_LOG("FORK ERROR");
return -1;
}else if (pid == 0) {
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
}
cli_sock.Close();
exit(0);
}
}
while(1) sleep(1);
return 0;
}





2.6.5 client5
html
/*一次性给服务器发送多条数据,然后查看服务器的处理结果*/
/*每一条请求都应该得到正常处理*/
#include "../source/server.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1) {
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
}
cli_sock.Close();
return 0;
}

2.6.6 client6
html
/*大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果*/
/*
上传的文件,和服务器保存的文件一致
*/
#include "../source/http/http.hpp"
int main()
{
Socket cli_sock;
cli_sock.CreateClient(8081, "127.0.0.1");
std::string req = "PUT /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";
std::string body;
Util::ReadFile("./hello.txt", &body);
req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";
assert(cli_sock.Send(req.c_str(), req.size()) != -1);
assert(cli_sock.Send(body.c_str(), body.size()) != -1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf, 1023));
DBG_LOG("[%s]", buf);
sleep(3);
cli_sock.Close();
return 0;
}
先创建大文件




2.6.7 webbench Test






2.6.7.1 压力测试结果分析
测试环境:
-
服务器配置:4核4G内存,带宽 3Mbps的云服务器。
-
客户端环境:本机(127.0.0.1)使用Webbench进行压力测试。
-
测试接口 :
http://127.0.0.1:8081/get(GET请求回显接口)。 -
测试工具:Webbench 1.5。
测试结果数据:
| 并发数 | 测试时间 | 成功请求数 | 失败请求数 | 页面速率 (pages/min) | 字节速率 (bytes/sec) |
|---|---|---|---|---|---|
| 4000 | 60秒 | 192,756 | 0 | 192,756 | 477,899 |
| 6000 | 60秒 | 174,421 | 0 | 174,421 | 432,421 |
| 7000 | 60秒 | 218,272 | 0 | 218,272 | 540,510 |
| 8000 | 60秒 | 15,695 | 0 | 15,695 | 41,346 |
性能分析:
-
并发处理能力:
-
在4000-7000并发范围内,服务器表现稳定,无请求失败。
-
7000并发时达到峰值性能:218,272 pages/min。
-
服务器能稳定处理约3000-3600 QPS(每秒请求数)。
-
-
资源利用情况:
-
4核CPU在7000并发时应该接近满载。
-
4GB内存足够支撑这些并发连接。
-
3Mbps带宽在本地测试中不是瓶颈(本地回环测试)。
-
-
性能瓶颈分析:
-
8000并发时性能急剧下降(从218,272降至15,695 pages/min)。
-
这表明服务器的最大并发处理能力在7000-8000之间。
-
超过阈值后,系统资源耗尽,性能断崖式下跌。
-
-
SSH连接断开原因:
-
8000并发时,系统资源(特别是文件描述符、进程/线程资源)耗尽。
-
云服务器的连接跟踪表(conntrack)可能被填满。
-
系统为了维持Web服务,可能会终止其他连接(包括SSH)。
-
内存或CPU耗尽导致SSH守护进程无法及时响应。
-
结论:
服务器在4000-7000并发范围内表现稳定,请求全部成功,最高处理能力达到218,272 pages/min(约3637 QPS)。在7000并发时达到性能峰值,响应速度和吞吐量最优。
当并发数提升至8000时,性能急剧下降至15,695 pages/min,仅为峰值的7.2%。同时,由于系统资源耗尽(文件描述符、连接跟踪表、CPU等),导致SSH连接断开。这表明服务器的最大稳定并发处理能力约为7000,超过此阈值会出现资源竞争和性能崩溃。
优化:调整系统内核参数
bash
# 增加连接跟踪表大小
sysctl -w net.netfilter.nf_conntrack_max=262144
# 增加文件描述符限制
sysctl -w fs.file-max=1000000
# 缩短TIME_WAIT超时
sysctl -w net.ipv4.tcp_fin_timeout=30





进程限制(14401)大于9000,最关键的问题在于内存不足。
当前系统已经有3180个线程 , 每个新进程/线程需要消耗一定的内存 , 9000个WebBench客户端意味着要创建9000个新进程 , 总共约 3180 + 9000 = 12180 个进程/线程 , 内存只有3.6GB,每个进程分配一点内存,很快就会耗尽**。**

2.6.8 功能性接口测试



五.结语
本项目实现了一个高性能的C++ HTTP服务器 ,支持多种HTTP方法和文件操作功能,并配套提供了一个功能完备的Web测试平台。
1.核心功能实现
1.1. 服务器核心模块 (server.hpp)
-
Reactor事件驱动模型 - 基于epoll的高并发IO多路复用。
-
多线程处理 - 主Reactor + 从Reactor线程池,支持设置线程数量。
-
定时器模块 - 基于时间轮的定时任务管理,支持非活跃连接超时销毁。
-
缓冲区管理 - 自定义Buffer类,支持动态扩容和数据读写。
-
连接管理 - Connection类封装TCP连接,管理连接状态和回调函数。
1.2. HTTP协议解析 (http.hpp)
-
完整的HTTP请求解析 - 支持解析请求行、请求头、请求体。
-
多种HTTP方法 - GET、POST、PUT、DELETE、HEAD。
-
路由匹配 - 支持正则表达式路由匹配。
-
静态资源服务 - 支持从**
wwwroot**目录提供静态文件。 -
MIME类型识别 - 根据文件扩展名自动设置Content-Type。
1.3. 业务功能模块 (main.txt)
| 路由 | 方法 | 功能 | 处理函数 |
|---|---|---|---|
/get |
GET | 回显请求信息 | GET() |
.*\.png$ |
GET | 获取PNG图片 | GET_PNG() |
/large/image |
GET | 图片放大查看 | GetLargeImage() |
/post |
POST | 回显POST数据 | POST() |
/put |
PUT | 上传文件 | PutFile() |
/delete |
DELETE | 删除文件 | DelFile() |
/myfiles |
GET | 列出用户文件 | ListUserFiles() |
1.4. 工具类功能 (Util类)
-
文件操作 - 读写文件、删除文件、判断文件类型。
-
URL编解码 - 支持中文和特殊字符的URL编码/解码。
-
路径安全 - 防止路径遍历攻击(
..检查)。 -
MIME类型映射 - 支持70+种文件类型的MIME映射。
-
用户目录管理 - 基于IP+User-Agent创建用户专属目录。
1.5. 智能文件管理
-
用户隔离 - 每个用户有自己的文件目录(
./wwwroot/clientputfile/user_[hash]/)。 -
自动重命名 - 文件重名时自动生成唯一文件名。
-
文本文件优化 - 自动在末尾添加换行符。
-
文件类型识别 - 区分图片、视频、音频、文本等类型。
1.6. Web测试平台 (HTML页面)
提供了一个功能完备的可视化测试界面:
1.6.1 测试功能
-
GET请求测试 - 测试**
/get**回显功能。 -
PNG图片测试 - 测试**
.*\.png$**路由,支持图片预览。 -
图片放大查看 - 测试**
/large/image**,支持全屏查看、缩放、旋转。 -
POST请求测试 - 测试**
/post**回显功能。 -
PUT文件上传 - 支持文本文件和二进制文件上传。
-
DELETE文件删除 - 用户友好的删除界面,只需输入文件名。
-
文件列表查看 - 实时显示用户目录中的文件。
1.6.2 用户体验优化
-
全屏图片查看器 - 支持缩放、旋转、拖拽、下载。
-
动画效果 - 文件添加/删除有平滑动画。
-
Toast通知 - 操作成功/失败有弹出提示。
-
实时统计 - 文件数量、大小、类型的实时统计。
-
键盘快捷键 - ESC退出全屏,+/-缩放,R旋转。
1.7 技术亮点
-
高性能架构
-
基于epoll的Reactor模型,非阻塞IO。
-
线程池处理业务逻辑,避免阻塞事件循环。
-
时间轮定时器,O(1)复杂度管理大量定时任务。
-
-
安全性考虑
-
路径遍历防护(检查
..)。 -
用户目录隔离。
-
文件名安全检查。
-
请求大小限制。
-
-
用户体验
-
用户无需知道物理路径。
-
自动处理中文文件名。
-
友好的错误提示。
-
实时反馈操作结果。
-
-
可扩展性
-
基于正则表达式的路由匹配。
-
模块化的设计,易于添加新功能。
-
支持自定义业务处理函数。
-
1.8 项目成果
通过这个项目,我们成功实现了一个功能完整、性能优良、用户体验良好的HTTP服务器,能够:
-
处理并发HTTP请求。
-
提供静态文件服务。
-
支持文件上传下载。
-
实现用户文件管理。
-
提供图片预览和放大功能。
-
多种HTTP方法。
-
具备完善的测试界面。
这个项目不仅是一个HTTP服务器,更是一个完整的Web服务解决方案,可以用于学习、测试或作为实际项目的基础框架。
完整代码链接:
Project: project-------
https://gitee.com/frost-descending/project/tree/master/OneThreadOneLoop_2