【仿Muduo库项目】HTTP模块1——Util子模块

目录

一.Util子模块

1.1.功能分析

1.2.注意细节

1.2.1.细节一:HTTP状态码和Content-Type字段

1.2.2.细节二:C++文件读写的三个流

1.2.3.细节三:read的第一个参数

1.2.4.URL编码解码

1.2.5.普通文件和目录文件的判断

1.3.代码总览

1.4.测试


从这篇博客开始,就是顶层协议处理模块了,只不过我们这里设置的协议其实是HTTP协议。

HTTP 协议模块旨在为高并发服务器提供协议层面的支持,基于该模块可以更便捷地搭建符合特定协议的服务端。

该模块的实现可进一步划分为以下几个子模块:

  1. Util 工具模块

    提供 HTTP 协议处理过程中所需的各类工具函数,例如 URL 编解码、文件读写等公共功能,为上层模块提供基础支持。

  2. HttpRequest 模块

    负责封装 HTTP 请求数据。当请求被解析后,其方法、路径、头部、主体等信息将存储于该模块的对象中,供后续处理流程使用。

  3. HttpResponse 模块

    用于构造 HTTP 响应数据。业务逻辑处理完成后,可通过该模块设置状态码、响应头、响应体等元素,最终组织成符合 HTTP 协议规范的响应消息并发送至客户端。

  4. HttpContext 模块

    作为 HTTP 请求接收与解析的上下文管理模块,主要用于处理 TCP 流式传输中可能出现的报文不完整情况。当单次接收的数据不足以构成完整 HTTP 请求时,该模块会保存当前解析状态,待后续数据到达后继续进行分析,最终生成完整的 HttpRequest 对象,确保请求解析的连贯性与正确性。

  5. HttpServer 模块

    面向组件使用者提供的 HTTP 服务器封装模块,通过简洁的接口即可快速搭建 HTTP 服务。其内部主要包括以下组成部分:

    • TcpServer 对象:负责底层 TCP 通信与连接管理。

    • 两个核心回调接口 :分别用于处理连接建立 事件(设置上下文)与数据接收事件。

    • 路由映射表:采用哈希表结构维护请求路径与处理函数之间的映射关系。使用者可通过注册路由来指定不同请求对应的处理逻辑,当 TcpServer 接收到相应请求时,将自动调用对应的处理函数。

通过以上模块的协同工作,HTTP 协议模块实现了从连接管理、协议解析到业务路由的完整流程,为构建高性能、可扩展的 HTTP 服务器提供了清晰且灵活的支持。

一.Util子模块

1.1.功能分析

这个模块的功能其实很简单,就是

  1. 读取文件内容
  2. 向文件写入内容
  3. URL编码
  4. URL解码
  5. HTTP状态码&描述信息
  6. 根据文件后缀名获取mime
  7. 判断一个文件是否是目录
  8. 判断一个文件是否是普通文件
  9. 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编码:

这背后的原因更为根本:

  1. 历史与标准 :互联网的基础协议最初是基于ASCII字符集 设计的,它仅包含128个英文字母、数字和控制字符。URL的语法规则也是为此制定的。URL里面也是ASCII字符

  2. 二进制安全传输网络传输的底层本质是二进制字节流。 URL编码(%HH格式)提供了一种标准方法,将任何字符 (无论是中文、表情符号,还是二进制数据)统一转换为由百分号和两个十六进制数字表示的ASCII安全字符。这两个十六进制数就代表该字符在特定编码(如UTF-8)下对应的字节值。例如,"你"字的UTF-8编码是三个字节E4 BD A0,因此被编码为%E4%BD%A0

  3. 避免数据损坏与歧义 :直接传输非ASCII字符(如中文)是危险的。不同系统、浏览器或服务器的默认字符编码可能不同(如GBK, UTF-8),极易导致乱码。更严重的是,某些字节值可能与传输协议中的控制指令冲突,导致数据被截断或错误解析。通过URL编码,所有数据都被"打包"成百分号形式,确保了它在穿越复杂的网络路径时,内容能完整、精确、无歧义地抵达目的地。


从上面进行考虑,我们必须对URL进行编码

根据 RFC 3986 标准,URL 中的字符被分为两类:

  • 保留字符 (Reserved Characters):/, ?, &, #, =, + 等。它们在 URL 中具有特定含义(例如 / 分隔路径,? 引导查询参数)。如果要在数据中传输这些字符本身,就必须编码。
  • 非保留字符 (Unreserved Characters): 大写和小写字母、数字,以及 -, ., _, ~ 四个符号。这些字符可以直接传输。
  • 剩余的其他任何字符(包括中文,日文......等非ASCII字符),都必须进行编码

换句话说,只有大写和小写字母、数字,以及 -, ., _, ~ 四个符号不需要进行编码,其余都需要进编码。

注意这里其实有一个特殊情况: 空格: 在 URL 中是不允许的,可能导致截断或混淆。在查询字符串中常被编码为 + (非标准但广泛兼容) 或 %20 (标准)。我们这里就将空格编码为+

那么怎么进行编码呢?

编码规则: URL 编码通过以下三步实现:

  1. 将需要编码的字符(如中文、特殊符号)转换为其对应的 UTF-8 字节序列
  2. 将每个字节转换为两位 十六进制数
  3. 在每组十六进制数前加上 百分号(%)

例如,中文的"你"字在 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;
}

很好,非常完美

相关推荐
qq_401700412 小时前
带宽与网速是一回事吗
网络
C_心欲无痕2 小时前
网络相关 - Ngrok内网穿透使用
运维·前端·网络
嘿嘿2 小时前
charles iOS 配置证书,抓取https请求
http·测试
Lily48012 小时前
基于优先级的流量控制(PFC)
网络
go_bai2 小时前
Linux-网络基础
linux·开发语言·网络·笔记·学习方法·笔记总结
糖~醋排骨3 小时前
FW防火墙的配置
linux·服务器·网络
yintele3 小时前
类人机器人BMS的静电防护
网络·安全·机器人
CCPC不拿奖不改名3 小时前
网络与API:从HTTP协议视角理解网络分层原理+面试习题
开发语言·网络·python·网络协议·学习·http·面试
liulilittle4 小时前
OPENPPP2 网络驱动模式
开发语言·网络·c++·网络协议·信息与通信·通信