目录
1.2.1.细节一:HTTP状态码和Content-Type字段
从这篇博客开始,就是顶层协议处理模块了,只不过我们这里设置的协议其实是HTTP协议。
HTTP 协议模块旨在为高并发服务器提供协议层面的支持,基于该模块可以更便捷地搭建符合特定协议的服务端。
该模块的实现可进一步划分为以下几个子模块:
-
Util 工具模块
提供 HTTP 协议处理过程中所需的各类工具函数,例如 URL 编解码、文件读写等公共功能,为上层模块提供基础支持。
-
HttpRequest 模块
负责封装 HTTP 请求数据。当请求被解析后,其方法、路径、头部、主体等信息将存储于该模块的对象中,供后续处理流程使用。
-
HttpResponse 模块
用于构造 HTTP 响应数据。业务逻辑处理完成后,可通过该模块设置状态码、响应头、响应体等元素,最终组织成符合 HTTP 协议规范的响应消息并发送至客户端。
-
HttpContext 模块
作为 HTTP 请求接收与解析的上下文管理模块,主要用于处理 TCP 流式传输中可能出现的报文不完整情况。当单次接收的数据不足以构成完整 HTTP 请求时,该模块会保存当前解析状态,待后续数据到达后继续进行分析,最终生成完整的 HttpRequest 对象,确保请求解析的连贯性与正确性。
-
HttpServer 模块
面向组件使用者提供的 HTTP 服务器封装模块,通过简洁的接口即可快速搭建 HTTP 服务。其内部主要包括以下组成部分:
-
TcpServer 对象:负责底层 TCP 通信与连接管理。
-
两个核心回调接口 :分别用于处理连接建立 事件(设置上下文)与数据接收事件。
-
路由映射表:采用哈希表结构维护请求路径与处理函数之间的映射关系。使用者可通过注册路由来指定不同请求对应的处理逻辑,当 TcpServer 接收到相应请求时,将自动调用对应的处理函数。
-
通过以上模块的协同工作,HTTP 协议模块实现了从连接管理、协议解析到业务路由的完整流程,为构建高性能、可扩展的 HTTP 服务器提供了清晰且灵活的支持。
一.Util子模块
1.1.功能分析
这个模块的功能其实很简单,就是
- 读取文件内容
- 向文件写入内容
- URL编码
- URL解码
- HTTP状态码&描述信息
- 根据文件后缀名获取mime
- 判断一个文件是否是目录
- 判断一个文件是否是普通文件
- HTTP资源路径的有效性判断
根据上面的需求,我们很快就能写出下面这个代码

cpp
class Util {
public:
// 字符串分割函数:将src字符串按照sep分隔符进行分割,分割得到的子字符串存入arry中,返回子字符串数量
static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry);
// 读取文件全部内容到buf字符串中
static bool ReadFile(const std::string &filename, std::string *buf);
// 将buf中的数据写入文件(覆盖写入)
static bool WriteFile(const std::string &filename, const std::string &buf);
// URL编码:将特殊字符转换为%HH格式,防止与HTTP特殊字符冲突
// convert_space_to_plus:true时将空格编码为+号(符合W3C查询字符串规范)
static std::string UrlEncode(const std::string url, bool convert_space_to_plus);
// 十六进制字符转十进制数值
static char HEXTOI(char c);
// URL解码:将%HH格式还原为原始字符
// convert_plus_to_space:true时将+号解码为空格
static std::string UrlDecode(const std::string url, bool convert_plus_to_space);
// 根据HTTP状态码返回对应的描述信息
static std::string StatuDesc(int statu);
// 根据文件扩展名获取MIME类型
static std::string ExtMime(const std::string &filename);
// 判断路径是否指向一个目录
static bool IsDirectory(const std::string &filename);
// 判断路径是否指向一个普通文件
static bool IsRegular(const std::string &filename);
// 验证HTTP请求资源路径的有效性:防止目录遍历攻击(如/../)
static bool ValidPath(const std::string &path);
private:
// 状态码到描述信息的映射
static std::map<int, std::string> _statu_msg;
// 文件扩展名到MIME类型的映射
static std::map<std::string, std::string> _mime_msg;
};
当然,这个其实也很好理解
1.2.注意细节
1.2.1.细节一:HTTP状态码和Content-Type字段
首先,我们需要知道,如果说我们需要使用HTTP协议作为应用层协议,那么我们就必须知道
- HTTP状态码的含义
- 用于HTTP响应头中的Content-Type字段,告诉浏览器如何解析返回的内容
就是下面这个
cpp
// HTTP状态码到标准描述信息的映射表
// 包含从1xx到5xx的常见HTTP状态码及其官方描述
std::unordered_map<int, std::string> _statu_msg = {
// 1xx: 信息性状态码 - 请求已接收,继续处理
{100, "Continue"}, // 继续:客户端应继续发送请求的剩余部分
{101, "Switching Protocol"}, // 切换协议:服务器已理解并同意客户端的协议切换请求
{102, "Processing"}, // 处理中:服务器已收到请求,正在处理但尚未完成
{103, "Early Hints"}, // 早期提示:用于在最终响应前预加载资源
// 2xx: 成功状态码 - 请求已成功接收、理解并接受
{200, "OK"}, // 成功:标准成功响应
{201, "Created"}, // 已创建:请求成功且服务器创建了新资源
{202, "Accepted"}, // 已接受:请求已接受但尚未处理完成
{203, "Non-Authoritative Information"},// 非权威信息:返回的元信息不是原始服务器确定的
{204, "No Content"}, // 无内容:请求成功但响应中没有内容
{205, "Reset Content"}, // 重置内容:请求成功,客户端应重置文档视图
{206, "Partial Content"}, // 部分内容:服务器成功处理了部分GET请求(用于断点续传)
{207, "Multi-Status"}, // 多状态:对于WebDAV操作,可能有多个独立响应
{208, "Already Reported"}, // 已报告:WebDAV绑定成员已在前一个响应中枚举
{226, "IM Used"}, // IM已使用:服务器已完成对资源的GET请求,响应是当前实例应用的一个或多个实例操作的结果
// 3xx: 重定向状态码 - 需要客户端采取进一步操作才能完成请求
{300, "Multiple Choice"}, // 多种选择:请求的资源有多个可供选择的响应
{301, "Moved Permanently"}, // 永久移动:请求的资源已永久移动到新URI
{302, "Found"}, // 临时移动:请求的资源临时从不同的URI响应请求
{303, "See Other"}, // 查看其他:对当前请求的响应可以在另一个URI上找到
{304, "Not Modified"}, // 未修改:资源自上次请求后未修改,使用缓存的版本
{305, "Use Proxy"}, // 使用代理:请求的资源必须通过代理访问
{306, "unused"}, // 已弃用:不再使用,保留状态码
{307, "Temporary Redirect"}, // 临时重定向:请求的资源临时从不同的URI响应请求
{308, "Permanent Redirect"}, // 永久重定向:请求的资源已永久移动到新URI
// 4xx: 客户端错误状态码 - 客户端请求有错误
{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"}, // 需要长度:服务器要求Content-Length头部字段
{412, "Precondition Failed"}, // 先决条件失败:请求的先决条件在服务器上评估失败
{413, "Payload Too Large"}, // 负载过大:请求实体超过服务器限制
{414, "URI Too Long"}, // URI过长:请求的URI超过服务器能处理的长度
{415, "Unsupported Media Type"}, // 不支持的媒体类型:请求的格式不受支持
{416, "Range Not Satisfiable"}, // 范围无法满足:请求的范围无效
{417, "Expectation Failed"}, // 期望失败:无法满足Expect请求头部字段的要求
{418, "I'm a teapot"}, // 我是茶壶:愚人节玩笑,表示服务器是茶壶无法煮咖啡
{421, "Misdirected Request"}, // 错误定向请求:请求被发送到无法产生响应的服务器
{422, "Unprocessable Entity"}, // 无法处理的实体:请求格式正确但语义错误
{423, "Locked"}, // 已锁定:WebDAV资源已锁定
{424, "Failed Dependency"}, // 依赖失败:WebDAV操作因前一个操作失败而失败
{425, "Too Early"}, // 过早:服务器不愿冒风险处理可能重播的请求
{426, "Upgrade Required"}, // 需要升级:客户端应切换到TLS/1.0等协议
{428, "Precondition Required"}, // 需要先决条件:原始服务器要求请求为条件请求
{429, "Too Many Requests"}, // 请求过多:用户在给定时间内发送了太多请求
{431, "Request Header Fields Too Large"}, // 请求头部字段过大:头部字段总大小或单个字段过大
{451, "Unavailable For Legal Reasons"},// 因法律原因不可用:因法律要求拒绝访问资源
// 5xx: 服务器错误状态码 - 服务器处理请求时出错
{501, "Not Implemented"}, // 未实现:服务器不支持请求的功能
{502, "Bad Gateway"}, // 错误网关:作为网关或代理的服务器从上游服务器收到无效响应
{503, "Service Unavailable"}, // 服务不可用:服务器暂时过载或维护中
{504, "Gateway Timeout"}, // 网关超时:作为网关或代理的服务器未及时从上游服务器收到响应
{505, "HTTP Version Not Supported"}, // HTTP版本不支持:服务器不支持请求的HTTP协议版本
{506, "Variant Also Negotiates"}, // 变体协商:透明内容协商存在循环引用
{507, "Insufficient Storage"}, // 存储空间不足:WebDAV操作因存储空间不足而无法完成
{508, "Loop Detected"}, // 检测到循环:WebDAV操作中检测到无限循环
{510, "Not Extended"}, // 未扩展:请求需要进一步扩展才能被服务器满足
{511, "Network Authentication Required"} // 需要网络认证:客户端需要进行认证才能获得网络访问权限
};
// 文件扩展名到MIME类型的映射表
// 用于HTTP响应头中的Content-Type字段,告诉浏览器如何解析返回的内容
std::unordered_map<std::string, std::string> _mime_msg = {
// 音频文件类型
{".aac", "audio/aac"}, // AAC音频
{".mid", "audio/midi"}, // MIDI音频
{".midi", "audio/x-midi"}, // MIDI音频(旧格式)
{".mp3", "audio/mpeg"}, // MP3音频
{".oga", "audio/ogg"}, // OGG音频
{".wav", "audio/wav"}, // WAV音频
{".weba", "audio/webm"}, // WebM音频
// 视频文件类型
{".avi", "video/x-msvideo"}, // AVI视频
{".mpeg", "video/mpeg"}, // MPEG视频
{".ogv", "video/ogg"}, // OGG视频
{".webm", "video/webm"}, // WebM视频
{".3gp", "video/3gpp"}, // 3GPP视频
{".3g2", "video/3gpp2"}, // 3GPP2视频
// 图像文件类型
{".bmp", "image/bmp"}, // BMP位图
{".gif", "image/gif"}, // GIF动图/图片
{".ico", "image/vnd.microsoft.icon"}, // ICO图标
{".jpeg", "image/jpeg"}, // JPEG图像
{".jpg", "image/jpeg"}, // JPEG图像(简写)
{".png", "image/png"}, // PNG图像
{".svg", "image/svg+xml"}, // SVG矢量图
{".tif", "image/tiff"}, // TIFF图像
{".tiff", "image/tiff"}, // TIFF图像(全称)
{".webp", "image/webp"}, // WebP图像
// 字体文件类型
{".otf", "font/otf"}, // OpenType字体
{".ttf", "font/ttf"}, // TrueType字体
{".woff", "font/woff"}, // WOFF字体
{".woff2", "font/woff2"}, // WOFF2字体
// 文本/文档文件类型
{".abw", "application/x-abiword"}, // AbiWord文档
{".css", "text/css"}, // CSS样式表
{".csv", "text/csv"}, // CSV数据表
{".doc", "application/msword"}, // Microsoft Word文档
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, // Word 2007+文档
{".htm", "text/html"}, // HTML网页
{".html", "text/html"}, // HTML网页(全称)
{".ics", "text/calendar"}, // iCalendar日程
{".js", "text/javascript"}, // JavaScript脚本
{".json", "application/json"}, // JSON数据
{".jsonld", "application/ld+json"}, // JSON-LD数据
{".mjs", "text/javascript"}, // ES模块JavaScript
{".odp", "application/vnd.oasis.opendocument.presentation"}, // OpenDocument演示文稿
{".ods", "application/vnd.oasis.opendocument.spreadsheet"}, // OpenDocument电子表格
{".odt", "application/vnd.oasis.opendocument.text"}, // OpenDocument文本
{".pdf", "application/pdf"}, // PDF文档
{".ppt", "application/vnd.ms-powerpoint"}, // PowerPoint演示文稿
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, // PowerPoint 2007+演示文稿
{".rtf", "application/rtf"}, // RTF富文本
{".sh", "application/x-sh"}, // Shell脚本
{".txt", "text/plain"}, // 纯文本
{".xhtml", "application/xhtml+xml"}, // XHTML网页
{".xls", "application/vnd.ms-excel"}, // Excel电子表格
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, // Excel 2007+电子表格
{".xml", "application/xml"}, // XML数据
// 应用程序/压缩文件类型
{".arc", "application/x-freearc"}, // 存档文件
{".azw", "application/vnd.amazon.ebook"}, // Amazon Kindle电子书
{".bin", "application/octet-stream"}, // 二进制数据(默认类型)
{".bz", "application/x-bzip"}, // BZIP压缩文件
{".bz2", "application/x-bzip2"}, // BZIP2压缩文件
{".csh", "application/x-csh"}, // C Shell脚本
{".eot", "application/vnd.ms-fontobject"}, // 嵌入式OpenType字体
{".epub", "application/epub+zip"}, // EPUB电子书
{".jar", "application/java-archive"}, // Java存档文件
{".mpkg", "application/vnd.apple.installer+xml"}, // Apple安装包
{".ogx", "application/ogg"}, // OGG容器
{".rar", "application/x-rar-compressed"}, // RAR压缩文件
{".swf", "application/x-shockwave-flash"}, // Adobe Flash动画
{".tar", "application/x-tar"}, // TAR存档文件
{".vsd", "application/vnd.visio"}, // Microsoft Visio文档
{".xul", "application/vnd.mozilla.xul+xml"}, // Mozilla XUL界面
{".zip", "application/zip"}, // ZIP压缩文件
{".7z", "application/x-7z-compressed"} // 7-Zip压缩文件
};
这里面对应的东西太多,我们必须写两个查询函数
这样子的话,我们就可以写出下面这2个函数了
cpp
// 根据HTTP状态码返回对应的描述信息
static std::string StatuDesc(int statu)
{
// _statu_msg是一个静态的std::map<int, std::string>
// 包含状态码到描述信息的映射
auto it = _statu_msg.find(statu);
if (it != _statu_msg.end()) {
return it->second;
}
return "Unknow";
}
// 根据文件扩展名获取MIME类型
static std::string ExtMime(const std::string &filename)
{
// 查找最后一个.的位置以获取扩展名
size_t pos = filename.find_last_of('.');
if (pos == std::string::npos) { // 没有扩展名
return "application/octet-stream"; // 默认MIME类型:二进制流
}
// 提取扩展名(包含.)
std::string ext = filename.substr(pos);
// _mime_msg是一个静态的std::map<std::string, std::string>
// 包含扩展名到MIME类型的映射
auto it = _mime_msg.find(ext);
if (it == _mime_msg.end()) //没有找到
{
return "application/octet-stream";
}
return it->second;
}
这两个函数就能
- 根据状态码查询出对应的描述
- 根据文件拓展类型获取MINE类型
1.2.2.细节二:C++文件读写的三个流
主要有三个类:ifstream(输入文件流,用于从文件读取数据)、ofstream(输出文件流,用于向文件写入数据)以及fstream(既可以读也可以写)。
- ifstream:用于从文件中读取数据。我们可以打开一个文件,然后像使用cin一样从文件中读取数据。
- ofstream:用于向文件中写入数据。我们可以打开一个文件,然后像使用cout一样向文件中写入数据。
- fstream:兼具读和写的功能。我们可以打开一个文件进行读写操作。
反正读取文件,和写入文件,你必须知道的清清楚楚好吧
1.2.3.细节三:read的第一个参数
cpp
// 读取文件全部内容到buf字符串中
static bool ReadFile(const std::string &filename, std::string *buf)
{
// 以二进制模式打开文件
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;
ifs.seekg(0, ifs.end); // 将文件指针移动到文件末尾
fsize = ifs.tellg(); // 获取文件大小(当前位置偏移量)
ifs.seekg(0, ifs.beg); // 将文件指针移回文件开头
buf->resize(fsize); // 调整buf大小为文件大小
// 读取整个文件内容到buf中
ifs.read(&(*buf)[0], fsize);
if (ifs.good() == false) { // 检查读取操作是否成功
printf("READ %s FILE FAILED!!", filename.c_str());
ifs.close();
return false;
}
ifs.close();
return true;
}
大家仔细看看
cpp
// 读取整个文件内容到buf中
ifs.read(&(*buf)[0], fsize);
我来详细解释一下 &(*buf)[0] 这个表达式的含义和为什么需要这样使用。
表达式分解理解
让我们把这个复杂的表达式分解开来理解:
- buf:这是一个 std::string* 类型,即指向 std::string 对象的指针。
- *buf:解引用这个指针,得到 buf 所指向的 std::string 对象本身。
- (*buf)[0]:访问这个 std::string 对象的第一个字符(索引为0的字符)。这实际上是一个 char 类型的引用(char&)。
- &(*buf)[0]:取这个字符的地址。因为 (*buf)[0] 是第一个字符的引用,取它的地址就得到了指向字符串内部字符数组第一个元素的指针,即 char* 类型。
为什么需要这样写?
- 核心原因:std::ifstream::read() 函数的第一个参数需要的是一个 char* 类型的指针,指向一个内存缓冲区,文件内容将被读取到这个缓冲区中。
- 但是 std::string 是一个高级的类对象,不是原生的字符数组。虽然它内部确实存储着字符数据,但这些数据被封装在类内部,不能直接获得 char* 指针。
有人问:为什么不使用c_str()?
- c_str() 返回的是 const char*(指向常量字符的指针),这意味着:
- 通过这个指针,只能读取数据,不能修改数据
- 如果试图通过这个指针修改数据,编译器会报错
- ifstream::read() 函数要求的是一个 char*(指向可变字符的指针),因为它需要向这个内存写入数据
你可能会想,std::string 不是有 data() 方法吗?为什么不用 buf->data()?
这里有一个重要的历史背景:
- 在 C++11 标准之前,std::string::data() 返回的是 const char*,即指向常量字符的指针。这意味着你不能通过这个指针修改字符串的内容。
- ifstream::read() 需要的是 char*(非const指针),因为它要向这个内存写入数据。
- 所以当时普遍使用 &(*buf)[0] 这种技巧来获取可写的 char* 指针。
C++11 及以后的变化
- 从 C++11 标准开始,情况发生了变化:
- std::string 的内部存储被保证是连续的(像数组一样连续排列)。
- std::string::data() 现在返回 char*(可修改的指针),而不仅仅是 const char*。
- 因此,在现代C++(C++11及以上)中,你可以直接使用 buf->data() 替代 &(*buf)[0]。
我们这里确实可以用buf->data(),但是如果我用了,就不好拓展大家的思维了,是吧!!
1.2.4.URL编码解码
首先,对于这个部分,其实大家是很陌生的,因为我们不太了解URL的格式。
> URL 就是你在浏览器地址栏里输入的那一串字符,用来告诉浏览器"去哪儿找资源"。
标准结构(由 6 部分组成):
协议://用户名:密码@主机名:端口号/路径?查询参数#片段
例子:
html
https://user:pass@example.com:8080/docs/index.html?page=2#section3
从上面可以看到,URL有特定是语法结构,某些字符在其中具有特殊含义(称为保留字符)。例如:
- / 路径段
- ? URL 和查询字符串
- & 查询参数
- = 在查询参数中键和值
-
标识片段标识符
- : 用于协议和端口号
- @ 用于用户信息
-
- 在查询字符串中有时表示空格(非标准,但常见)
如果这些字符需要作为普通数据(比如参数值的一部分)出现在 URL 中,就必须进行编码,否则会破坏 URL 的结构。
想象一下,如果查询参数的值本身包含一个 & 符号会发生什么?
例如,你想搜索 C++ & Java。如果不加处理,URL可能看起来像 ?q=C++ & Java。服务器会错误地将 & 解析为参数分隔符,认为这是两个参数 q=C++ 和 Java,这就会导致请求失败。因此,任何可能引发歧义的字符,当它们不作为语法功能,而作为普通数据的一部分出现时,都必须进行"转义"或"编码",这就是URL编码的核心原因。
此外,使用浏览器进行Http网络请求时,若请求query中包含中文,中文会被编码为 %+16进制+16进制形式,但你真的深入了解过,为什么要进行这种转移编码吗?
例如,浏览器中进行百度搜索"你好"时,链接地址会被自动进行URL编码:
这背后的原因更为根本:
-
历史与标准 :互联网的基础协议最初是基于ASCII字符集 设计的,它仅包含128个英文字母、数字和控制字符。URL的语法规则也是为此制定的。URL里面也是ASCII字符
-
二进制安全传输 :网络传输的底层本质是二进制字节流。 URL编码(
%HH格式)提供了一种标准方法,将任何字符 (无论是中文、表情符号,还是二进制数据)统一转换为由百分号和两个十六进制数字表示的ASCII安全字符。这两个十六进制数就代表该字符在特定编码(如UTF-8)下对应的字节值。例如,"你"字的UTF-8编码是三个字节E4 BD A0,因此被编码为%E4%BD%A0。 -
避免数据损坏与歧义 :直接传输非ASCII字符(如中文)是危险的。不同系统、浏览器或服务器的默认字符编码可能不同(如GBK, UTF-8),极易导致乱码。更严重的是,某些字节值可能与传输协议中的控制指令冲突,导致数据被截断或错误解析。通过URL编码,所有数据都被"打包"成百分号形式,确保了它在穿越复杂的网络路径时,内容能完整、精确、无歧义地抵达目的地。
从上面进行考虑,我们必须对URL进行编码
根据 RFC 3986 标准,URL 中的字符被分为两类:
- 保留字符 (Reserved Characters): 如
/,?,&,#,=,+等。它们在 URL 中具有特定含义(例如/分隔路径,?引导查询参数)。如果要在数据中传输这些字符本身,就必须编码。 - 非保留字符 (Unreserved Characters): 大写和小写字母、数字,以及
-,.,_,~四个符号。这些字符可以直接传输。 - 剩余的其他任何字符(包括中文,日文......等非ASCII字符),都必须进行编码
换句话说,只有大写和小写字母、数字,以及 -, ., _, ~ 四个符号不需要进行编码,其余都需要进编码。
注意这里其实有一个特殊情况: 空格: 在 URL 中是不允许的,可能导致截断或混淆。在查询字符串中常被编码为 + (非标准但广泛兼容) 或 %20 (标准)。我们这里就将空格编码为+
那么怎么进行编码呢?
编码规则: URL 编码通过以下三步实现:
- 将需要编码的字符(如中文、特殊符号)转换为其对应的 UTF-8 字节序列。
- 将每个字节转换为两位 十六进制数。
- 在每组十六进制数前加上 百分号(%)。
例如,中文的"你"字在 UTF-8 下是 E4 BD A0,编码后即为 %E4%BD%A0。
我们很快就能写出这个编码函数
cpp
//为了避免URL资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义,我们必须进行URL编码
// URL编码:将特殊字符转换为%HH格式,防止与HTTP特殊字符冲突
// convert_space_to_plus:true时将空格编码为+号(符合W3C查询字符串规范)
static std::string UrlEncode(const std::string url, bool convert_space_to_plus) {
std::string res;
for (auto &c : url) {
// RFC3986规定不编码的字符:字母数字、. - _ ~
if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c)) //isalnum(c)用于判断一个字符c是否是字母或数字。
{
res += c;
continue;
}
// W3C标准:查询字符串中的空格编码为+
if (c == ' ' && convert_space_to_plus == true) {
res += '+';
continue;
}
// 其他字符编码为%HH格式(H表示16进制数字)
char tmp[4] = {0};
//将字符c以%HH格式存储进编码后的URL路径
snprintf(tmp, 4, "%%%02X", c); // %%输出%字符,%02X输出两位大写16进制
res += tmp;
}
return res;
}
注意:isalnum是C和C++标准库中的一个函数,用于检查一个字符是否是字母或数字。
具体来说,它检查一个字符是否属于以下任意一类:
-
大写字母(A-Z)
-
小写字母(a-z)
-
数字(0-9)
如果 c 是字母或数字,函数返回非零值(真);否则返回 0(假)。
有编码,就必须有解码函数啊!!我们很快就能写出下面这个解码函数
cpp
// 十六进制字符转十进制数值
static char HEXTOI(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}else if (c >= 'a' && c <= 'z') {
return c - 'a' + 10;
}else if (c >= 'A' && c <= 'Z') {
return c - 'A' + 10;
}
return -1; // 非法字符
}
// URL解码:将%HH格式还原为原始字符
// convert_plus_to_space:true时将+号解码为空格
static std::string UrlDecode(const std::string url, bool convert_plus_to_space) {
std::string res;
for (int i = 0; i < url.size(); i++)
{
// +号解码为空格
if (url[i] == '+' && convert_plus_to_space == true)
{
res += ' ';
continue;
}
// 处理%HH格式:需要确保后面至少有2个字符
if (url[i] == '%' && (i + 2) < url.size())
{
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;
}
一点问题都没有
1.2.5.普通文件和目录文件的判断
我们这个类里面其实是有下面这两个函数
cpp
// 判断路径是否指向一个目录
static bool IsDirectory(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st); // 获取文件状态信息
if (ret < 0) {
return false; // 获取失败
}
return S_ISDIR(st.st_mode); // 判断是否为目录
}
// 判断路径是否指向一个普通文件
static bool IsRegular(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st);
if (ret < 0) {
return false;
}
return S_ISREG(st.st_mode); // 判断是否为普通文件
}
我们仔细观看一下,它们都是使用了stat函数
stat函数用于获取与指定路径名相关联的文件或目录的属性,并将这些属性填充到一个struct stat结构体中。以下是stat函数的函数原型:
cpp
int stat(const char *pathname, struct stat *statbuf);
- pathname是要获取属性的文件或目录的路径名;
- statbuf是一个指向struct stat结构体 的指针,用于存储获取到的属性信息;这是一个输出型参数,操作系统会将查询的到的信息放到这里面去
stat函数返回一个整数值,如果操作成功,返回0;
如果出现错误,返回-1,并设置errno全局变量以指示错误的类型。
- struct stat类型
我们来看看能获取到什么文件信息!
在C语言中,struct stat是一个用于表示文件或文件系统对象属性的结构体类型。
这个结构体通常用于与文件和目录相关的操作,例如获取文件的大小、访问权限、最后修改时间等信息。
struct stat类型的定义通常由操作系统提供,因此其具体字段可能会因操作系统而异。
以下是一个典型的struct stat结构体的字段,尽管具体字段可能会因操作系统而异:
cpp
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
off_t st_size; // 文件的大小(以字节为单位)
time_t st_atime; // 文件的最后访问时间
time_t st_mtime; // 文件的最后修改时间
time_t st_ctime; // 文件的最后状态改变时间
blksize_t st_blksize; // 文件系统I/O操作的最佳块大小
blkcnt_t st_blocks; // 文件占用的块数
};
我们也可以在上面的界面往下滑!看看我的系统的真实情况是啥

我们发现,如果说需要判断一个文件是否是普通文件还是目录,就需要借助下面这个成员函数
cpp
mode_t st_mode; // 文件的访问权限和类型
struct stat结构体包含了文件的很多信息,其中有一个成员是st_mode,它记录了文件的类型和权限。
文件类型可以通过st_mode与一系列宏进行判断,这些宏包括:
- S_ISDIR(st_mode) 判断是否为目录
- S_ISREG(st_mode) 判断是否为普通文件
- S_ISCHR(st_mode) 判断是否为字符设备文件
- S_ISBLK(st_mode) 判断是否为块设备文件
- S_ISFIFO(st_mode) 判断是否为管道文件
- S_ISLNK(st_mode) 判断是否为符号链接
- S_ISSOCK(st_mode) 判断是否为套接字文件
而我们这里就是下面这两个
- S_ISREG 是一个宏,用于检查 st_mode 是否表示一个普通文件。
- S_ISDIR 是一个宏(macro),用于检查 st_mode 是否表示一个目录。
如果结果为真,他会返回true
如果结果为假,他会返回false
1.3.代码总览

cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <regex>
#include <sys/stat.h>
#include<unordered_map>
#include"../server/tcpserver.hpp"
// HTTP状态码到标准描述信息的映射表
// 包含从1xx到5xx的常见HTTP状态码及其官方描述
std::unordered_map<int, std::string> _statu_msg = {
// 1xx: 信息性状态码 - 请求已接收,继续处理
{100, "Continue"}, // 继续:客户端应继续发送请求的剩余部分
{101, "Switching Protocol"}, // 切换协议:服务器已理解并同意客户端的协议切换请求
{102, "Processing"}, // 处理中:服务器已收到请求,正在处理但尚未完成
{103, "Early Hints"}, // 早期提示:用于在最终响应前预加载资源
// 2xx: 成功状态码 - 请求已成功接收、理解并接受
{200, "OK"}, // 成功:标准成功响应
{201, "Created"}, // 已创建:请求成功且服务器创建了新资源
{202, "Accepted"}, // 已接受:请求已接受但尚未处理完成
{203, "Non-Authoritative Information"},// 非权威信息:返回的元信息不是原始服务器确定的
{204, "No Content"}, // 无内容:请求成功但响应中没有内容
{205, "Reset Content"}, // 重置内容:请求成功,客户端应重置文档视图
{206, "Partial Content"}, // 部分内容:服务器成功处理了部分GET请求(用于断点续传)
{207, "Multi-Status"}, // 多状态:对于WebDAV操作,可能有多个独立响应
{208, "Already Reported"}, // 已报告:WebDAV绑定成员已在前一个响应中枚举
{226, "IM Used"}, // IM已使用:服务器已完成对资源的GET请求,响应是当前实例应用的一个或多个实例操作的结果
// 3xx: 重定向状态码 - 需要客户端采取进一步操作才能完成请求
{300, "Multiple Choice"}, // 多种选择:请求的资源有多个可供选择的响应
{301, "Moved Permanently"}, // 永久移动:请求的资源已永久移动到新URI
{302, "Found"}, // 临时移动:请求的资源临时从不同的URI响应请求
{303, "See Other"}, // 查看其他:对当前请求的响应可以在另一个URI上找到
{304, "Not Modified"}, // 未修改:资源自上次请求后未修改,使用缓存的版本
{305, "Use Proxy"}, // 使用代理:请求的资源必须通过代理访问
{306, "unused"}, // 已弃用:不再使用,保留状态码
{307, "Temporary Redirect"}, // 临时重定向:请求的资源临时从不同的URI响应请求
{308, "Permanent Redirect"}, // 永久重定向:请求的资源已永久移动到新URI
// 4xx: 客户端错误状态码 - 客户端请求有错误
{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"}, // 需要长度:服务器要求Content-Length头部字段
{412, "Precondition Failed"}, // 先决条件失败:请求的先决条件在服务器上评估失败
{413, "Payload Too Large"}, // 负载过大:请求实体超过服务器限制
{414, "URI Too Long"}, // URI过长:请求的URI超过服务器能处理的长度
{415, "Unsupported Media Type"}, // 不支持的媒体类型:请求的格式不受支持
{416, "Range Not Satisfiable"}, // 范围无法满足:请求的范围无效
{417, "Expectation Failed"}, // 期望失败:无法满足Expect请求头部字段的要求
{418, "I'm a teapot"}, // 我是茶壶:愚人节玩笑,表示服务器是茶壶无法煮咖啡
{421, "Misdirected Request"}, // 错误定向请求:请求被发送到无法产生响应的服务器
{422, "Unprocessable Entity"}, // 无法处理的实体:请求格式正确但语义错误
{423, "Locked"}, // 已锁定:WebDAV资源已锁定
{424, "Failed Dependency"}, // 依赖失败:WebDAV操作因前一个操作失败而失败
{425, "Too Early"}, // 过早:服务器不愿冒风险处理可能重播的请求
{426, "Upgrade Required"}, // 需要升级:客户端应切换到TLS/1.0等协议
{428, "Precondition Required"}, // 需要先决条件:原始服务器要求请求为条件请求
{429, "Too Many Requests"}, // 请求过多:用户在给定时间内发送了太多请求
{431, "Request Header Fields Too Large"}, // 请求头部字段过大:头部字段总大小或单个字段过大
{451, "Unavailable For Legal Reasons"},// 因法律原因不可用:因法律要求拒绝访问资源
// 5xx: 服务器错误状态码 - 服务器处理请求时出错
{501, "Not Implemented"}, // 未实现:服务器不支持请求的功能
{502, "Bad Gateway"}, // 错误网关:作为网关或代理的服务器从上游服务器收到无效响应
{503, "Service Unavailable"}, // 服务不可用:服务器暂时过载或维护中
{504, "Gateway Timeout"}, // 网关超时:作为网关或代理的服务器未及时从上游服务器收到响应
{505, "HTTP Version Not Supported"}, // HTTP版本不支持:服务器不支持请求的HTTP协议版本
{506, "Variant Also Negotiates"}, // 变体协商:透明内容协商存在循环引用
{507, "Insufficient Storage"}, // 存储空间不足:WebDAV操作因存储空间不足而无法完成
{508, "Loop Detected"}, // 检测到循环:WebDAV操作中检测到无限循环
{510, "Not Extended"}, // 未扩展:请求需要进一步扩展才能被服务器满足
{511, "Network Authentication Required"} // 需要网络认证:客户端需要进行认证才能获得网络访问权限
};
// 文件扩展名到MIME类型的映射表
// 用于HTTP响应头中的Content-Type字段,告诉浏览器如何解析返回的内容
std::unordered_map<std::string, std::string> _mime_msg = {
// 音频文件类型
{".aac", "audio/aac"}, // AAC音频
{".mid", "audio/midi"}, // MIDI音频
{".midi", "audio/x-midi"}, // MIDI音频(旧格式)
{".mp3", "audio/mpeg"}, // MP3音频
{".oga", "audio/ogg"}, // OGG音频
{".wav", "audio/wav"}, // WAV音频
{".weba", "audio/webm"}, // WebM音频
// 视频文件类型
{".avi", "video/x-msvideo"}, // AVI视频
{".mpeg", "video/mpeg"}, // MPEG视频
{".ogv", "video/ogg"}, // OGG视频
{".webm", "video/webm"}, // WebM视频
{".3gp", "video/3gpp"}, // 3GPP视频
{".3g2", "video/3gpp2"}, // 3GPP2视频
// 图像文件类型
{".bmp", "image/bmp"}, // BMP位图
{".gif", "image/gif"}, // GIF动图/图片
{".ico", "image/vnd.microsoft.icon"}, // ICO图标
{".jpeg", "image/jpeg"}, // JPEG图像
{".jpg", "image/jpeg"}, // JPEG图像(简写)
{".png", "image/png"}, // PNG图像
{".svg", "image/svg+xml"}, // SVG矢量图
{".tif", "image/tiff"}, // TIFF图像
{".tiff", "image/tiff"}, // TIFF图像(全称)
{".webp", "image/webp"}, // WebP图像
// 字体文件类型
{".otf", "font/otf"}, // OpenType字体
{".ttf", "font/ttf"}, // TrueType字体
{".woff", "font/woff"}, // WOFF字体
{".woff2", "font/woff2"}, // WOFF2字体
// 文本/文档文件类型
{".abw", "application/x-abiword"}, // AbiWord文档
{".css", "text/css"}, // CSS样式表
{".csv", "text/csv"}, // CSV数据表
{".doc", "application/msword"}, // Microsoft Word文档
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, // Word 2007+文档
{".htm", "text/html"}, // HTML网页
{".html", "text/html"}, // HTML网页(全称)
{".ics", "text/calendar"}, // iCalendar日程
{".js", "text/javascript"}, // JavaScript脚本
{".json", "application/json"}, // JSON数据
{".jsonld", "application/ld+json"}, // JSON-LD数据
{".mjs", "text/javascript"}, // ES模块JavaScript
{".odp", "application/vnd.oasis.opendocument.presentation"}, // OpenDocument演示文稿
{".ods", "application/vnd.oasis.opendocument.spreadsheet"}, // OpenDocument电子表格
{".odt", "application/vnd.oasis.opendocument.text"}, // OpenDocument文本
{".pdf", "application/pdf"}, // PDF文档
{".ppt", "application/vnd.ms-powerpoint"}, // PowerPoint演示文稿
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, // PowerPoint 2007+演示文稿
{".rtf", "application/rtf"}, // RTF富文本
{".sh", "application/x-sh"}, // Shell脚本
{".txt", "text/plain"}, // 纯文本
{".xhtml", "application/xhtml+xml"}, // XHTML网页
{".xls", "application/vnd.ms-excel"}, // Excel电子表格
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, // Excel 2007+电子表格
{".xml", "application/xml"}, // XML数据
// 应用程序/压缩文件类型
{".arc", "application/x-freearc"}, // 存档文件
{".azw", "application/vnd.amazon.ebook"}, // Amazon Kindle电子书
{".bin", "application/octet-stream"}, // 二进制数据(默认类型)
{".bz", "application/x-bzip"}, // BZIP压缩文件
{".bz2", "application/x-bzip2"}, // BZIP2压缩文件
{".csh", "application/x-csh"}, // C Shell脚本
{".eot", "application/vnd.ms-fontobject"}, // 嵌入式OpenType字体
{".epub", "application/epub+zip"}, // EPUB电子书
{".jar", "application/java-archive"}, // Java存档文件
{".mpkg", "application/vnd.apple.installer+xml"}, // Apple安装包
{".ogx", "application/ogg"}, // OGG容器
{".rar", "application/x-rar-compressed"}, // RAR压缩文件
{".swf", "application/x-shockwave-flash"}, // Adobe Flash动画
{".tar", "application/x-tar"}, // TAR存档文件
{".vsd", "application/vnd.visio"}, // Microsoft Visio文档
{".xul", "application/vnd.mozilla.xul+xml"}, // Mozilla XUL界面
{".zip", "application/zip"}, // ZIP压缩文件
{".7z", "application/x-7z-compressed"} // 7-Zip压缩文件
};
class Util {
public:
// 字符串分割函数:将src字符串按照sep分隔符进行分割,分割得到的子字符串存入arry中,返回子字符串数量
static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry) {
size_t offset = 0; // 当前查找的起始位置
// 循环条件:offset未超出字符串长度范围(有效偏移量范围是0到src.size()-1)
while(offset < src.size()) {
// 从offset位置开始查找分隔符sep
size_t pos = src.find(sep, offset);//第一个参数是我们需要查找的字符串,第二个是偏移量(从字符串的哪里开始查找,包括这个位置)
if (pos == std::string::npos) // 未找到分隔符,说明从offset位置开始到字符串末尾都是一个字串
{
// 将剩余部分作为最后一个子串
if(pos == src.size())
{
break;
}
arry->push_back(src.substr(offset));//添加的是从字符串offset偏移量开始到字符串末尾
return arry->size();
}
if (pos == offset) // 分隔符出现在起始位置(offset),说明是空子串
{
offset = pos + sep.size(); // 跳过当前分隔符
continue;
}
// 提取非空子串:从offset开始,长度为pos-offset
arry->push_back(src.substr(offset, pos - offset));
offset = pos + sep.size(); // 更新offset,跳过已处理的分隔符
}
return arry->size();
}
// 读取文件全部内容到buf字符串中
static bool ReadFile(const std::string &filename, std::string *buf)
{
// 以二进制模式打开文件
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;
ifs.seekg(0, ifs.end); // 将文件指针移动到文件末尾
fsize = ifs.tellg(); // 获取文件大小(当前位置偏移量)
ifs.seekg(0, ifs.beg); // 将文件指针移回文件开头
buf->resize(fsize); // 调整buf大小为文件大小
// 读取整个文件内容到buf中
ifs.read(&(*buf)[0], fsize);
if (ifs.good() == false) // 检查读取操作是否成功
{
printf("READ %s FILE FAILED!!", filename.c_str());
ifs.close();
return false;
}
ifs.close();
return true;
}
// 将buf中的数据写入文件(覆盖写入)
static bool WriteFile(const std::string &filename, const std::string &buf)
{
// 以二进制和截断模式打开文件(清空原内容)
std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);
if (ofs.is_open() == false)
{
printf("OPEN %s FILE FAILED!!", filename.c_str());
return false;
}
// 写入数据
ofs.write(buf.c_str(), buf.size());
if (ofs.good() == false) // 检查写入操作是否成功
{
ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());
ofs.close();
return false;
}
ofs.close();
return true;
}
//为了避免URL资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义,我们必须进行URL编码
// URL编码:将特殊字符转换为%HH格式,防止与HTTP特殊字符冲突
// convert_space_to_plus:true时将空格编码为+号(符合W3C查询字符串规范)
static std::string UrlEncode(const std::string url, bool convert_space_to_plus) {
std::string res;
for (auto &c : url) {
// RFC3986规定不编码的字符:字母数字、. - _ ~
if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c)) //isalnum(c)用于判断一个字符c是否是字母或数字。
{
res += c;
continue;
}
// W3C标准:查询字符串中的空格编码为+
if (c == ' ' && convert_space_to_plus == true) {
res += '+';
continue;
}
// 其他字符编码为%HH格式(H表示16进制数字)
char tmp[4] = {0};
//将字符c以%HH格式存储进编码后的URL路径
snprintf(tmp, 4, "%%%02X", c); // %%输出%字符,%02X输出两位大写16进制
res += tmp;
}
return res;
}
// 十六进制字符转十进制数值
static char HEXTOI(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}else if (c >= 'a' && c <= 'z') {
return c - 'a' + 10;
}else if (c >= 'A' && c <= 'Z') {
return c - 'A' + 10;
}
return -1; // 非法字符
}
// URL解码:将%HH格式还原为原始字符
// convert_plus_to_space:true时将+号解码为空格
static std::string UrlDecode(const std::string url, bool convert_plus_to_space) {
std::string res;
for (int i = 0; i < url.size(); i++)
{
// +号解码为空格
if (url[i] == '+' && convert_plus_to_space == true)
{
res += ' ';
continue;
}
// 处理%HH格式:需要确保后面至少有2个字符
if (url[i] == '%' && (i + 2) < url.size())
{
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;
}
// 根据HTTP状态码返回对应的描述信息
static std::string StatuDesc(int statu)
{
// _statu_msg是一个静态的std::map<int, std::string>
// 包含状态码到描述信息的映射
auto it = _statu_msg.find(statu);
if (it != _statu_msg.end()) {
return it->second;
}
return "Unknow";
}
// 根据文件扩展名获取MIME类型
static std::string ExtMime(const std::string &filename)
{
// 查找最后一个.的位置以获取扩展名
size_t pos = filename.find_last_of('.');
if (pos == std::string::npos) { // 没有扩展名
return "application/octet-stream"; // 默认MIME类型:二进制流
}
// 提取扩展名(包含.)
std::string ext = filename.substr(pos);
// _mime_msg是一个静态的std::map<std::string, std::string>
// 包含扩展名到MIME类型的映射
auto it = _mime_msg.find(ext);
if (it == _mime_msg.end()) //没有找到
{
return "application/octet-stream";
}
return it->second;
}
// 判断路径是否指向一个目录
static bool IsDirectory(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st); // 获取文件状态信息
if (ret < 0) {
return false; // 获取失败
}
return S_ISDIR(st.st_mode); // 判断是否为目录
}
// 判断路径是否指向一个普通文件
static bool IsRegular(const std::string &filename)
{
struct stat st;
int ret = stat(filename.c_str(), &st);
if (ret < 0) {
return false;
}
return S_ISREG(st.st_mode); // 判断是否为普通文件
}
// 验证HTTP请求资源路径的有效性:防止目录遍历攻击(如/../)
static bool ValidPath(const std::string &path) {
std::vector<std::string> subdir;
Split(path, "/", &subdir); // 按/分割路径
int level = 0; // 当前目录深度(相对于根目录)
for (auto &dir : subdir)
{
if (dir == "..") { // 上级目录
level--; // 深度减1
if (level < 0)
{
return false; // 试图访问根目录之外
}
continue;
}
level++; // 正常目录,深度加1
}
return true;
}
};
这个还是很好理解的
1.4.测试
模块1:HTTP状态码和Content-Type字段查询测试
cpp
#include "util.hpp"
#include <iostream>
int main() {
std::cout << "HTTP状态码测试:\n";
std::cout << "200: " << Util::StatuDesc(200) << std::endl;
std::cout << "404: " << Util::StatuDesc(404) << std::endl;
std::cout << "500: " << Util::StatuDesc(500) << std::endl;
std::cout << "301: " << Util::StatuDesc(301) << std::endl;
std::cout << "999: " << Util::StatuDesc(999) << std::endl;
std::cout << "\nMIME类型测试:\n";
std::cout << "index.html -> " << Util::ExtMime("index.html") << std::endl;
std::cout << "style.css -> " << Util::ExtMime("style.css") << std::endl;
std::cout << "image.png -> " << Util::ExtMime("image.png") << std::endl;
std::cout << "unknown.xyz -> " << Util::ExtMime("unknown.xyz") << std::endl;
std::cout << "file_without_ext -> " << Util::ExtMime("file_without_ext") << std::endl;
return 0;
}

模块2:URL编码解码测试
cpp
#include "util.hpp"
#include <iostream>
int main() {
std::cout << "URL编码测试:\n";
std::string original = "Hello World & 测试";
std::string encoded = Util::UrlEncode(original, false);
std::cout << "原始: " << original << std::endl;
std::cout << "编码: " << encoded << std::endl;
// 解码测试
std::string decoded = Util::UrlDecode(encoded, false);
std::cout << "解码: " << decoded << std::endl;
// 测试空格转为+号
std::string with_space = "hello world";
std::string encoded_with_plus = Util::UrlEncode(with_space, true);
std::cout << "\n空格编码测试:\n";
std::cout << "原始: " << with_space << std::endl;
std::cout << "编码为+: " << encoded_with_plus << std::endl;
// 解码时+转为空格
std::string decoded_space = Util::UrlDecode(encoded_with_plus, true);
std::cout << "解码+: " << decoded_space << std::endl;
return 0;
}

模块3:文件和目录判断测试
cpp
#include "util.hpp"
#include <iostream>
int main() {
std::cout << "文件和目录判断测试:\n\n";
// 测试当前目录
std::cout << "当前目录(.):\n";
std::cout << " 是目录: " << (Util::IsDirectory(".") ? "是" : "否") << std::endl;
std::cout << " 是普通文件: " << (Util::IsRegular(".") ? "是" : "否") << std::endl;
std::cout << "\n上级目录(..):\n";
std::cout << " 是目录: " << (Util::IsDirectory("..") ? "是" : "否") << std::endl;
std::cout << " 是普通文件: " << (Util::IsRegular("..") ? "是" : "否") << std::endl;
// 测试不存在的文件
std::cout << "\n不存在的文件:\n";
std::cout << " 是目录: " << (Util::IsDirectory("nonexistent.txt") ? "是" : "否") << std::endl;
std::cout << " 是普通文件: " << (Util::IsRegular("nonexistent.txt") ? "是" : "否") << std::endl;
return 0;
}

模块4:路径验证测试
cpp
#include "util.hpp"
#include <iostream>
int main() {
std::cout << "路径验证测试:\n\n";
// 安全路径
std::cout << "安全路径:\n";
std::string safe_paths[] = {"/index.html", "/static/css/style.css", "/api/users"};
for (const auto& path : safe_paths) {
std::cout << path << " -> "
<< (Util::ValidPath(path) ? "安全" : "危险") << std::endl;
}
// 危险路径(路径遍历攻击)
std::cout << "\n危险路径(路径遍历攻击):\n";
std::string dangerous_paths[] = {"/../etc/passwd", "/static/../../config.txt",
"/../../root", "/a/b/../../../c"};
for (const auto& path : dangerous_paths) {
std::cout << path << " -> "
<< (Util::ValidPath(path) ? "安全" : "危险") << std::endl;
}
// 边界情况
std::cout << "\n边界情况:\n";
std::string edge_cases[] = {"", "/", "..", ".", "/a/b/../../c"};
for (const auto& path : edge_cases) {
std::cout << "\"" << path << "\" -> "
<< (Util::ValidPath(path) ? "安全" : "危险") << std::endl;
}
return 0;
}

模块5:文件读写测试
cpp
#include "util.hpp"
#include <iostream>
int main() {
std::cout << "文件读写测试:\n\n";
// 写入测试文件
std::string test_file = "test_output.txt";
std::string content = "这是一个测试文件。\nHello, World!\nTest 123";
std::cout << "写入文件: " << test_file << std::endl;
bool write_success = Util::WriteFile(test_file, content);
std::cout << "写入结果: " << (write_success ? "成功" : "失败") << std::endl;
// 读取测试文件
std::string read_content;
std::cout << "\n读取文件..." << std::endl;
bool read_success = Util::ReadFile(test_file, &read_content);
std::cout << "读取结果: " << (read_success ? "成功" : "失败") << std::endl;
if (read_success) {
std::cout << "\n文件内容:\n" << read_content << std::endl;
std::cout << "\n内容比较: "
<< (content == read_content ? "一致" : "不一致") << std::endl;
}
// 测试读取不存在的文件
std::cout << "\n测试读取不存在的文件:\n";
std::string dummy;
bool fake_read = Util::ReadFile("this_file_does_not_exist.txt", &dummy);
std::cout << "读取结果: " << (fake_read ? "成功" : "失败") << std::endl;
return 0;
}

很好,非常完美