云计算百科
云计算领域专业知识百科平台

从零手写高性能C++ TCP 服务器框架(十五) --- Util工具实现

从零手写高性能C++ TCP 服务器框架(十五) — Util工具实现

        在前面的文章中,我们已经完成了 服务端搭建 的搭建。在构建 HTTP 服务器的过程中,总会遇到许多细小但高频的操作:比如把 URL 里的特殊符号编码、解码,从文件后缀名推断 MIME 类型,判断客户端请求的路径是否安全等。如果把这些逻辑零散地写在各个模块里,代码会变得难以维护。因此,我们专门抽出一个工具类 Util ,用静态方法的形式提供这些通用能力。

本文将详细介绍 Util 的实现思路、接口设计、技术选型以及完整的代码实现,为后续的 HTTP 服务器组装做好最后的工具准备。


一、实现目的

Util 工具类为整个 HTTP 服务器提供一组无状态的、通用的静态工具函数,主要用于:

  • 字符串分割:按指定分隔符拆分字符串,用于解析 URL 路径、HTTP 头部等。

  • 文件读写:以二进制方式高效读取 / 写入文件内容,用于响应静态资源请求。

  • URL 编解码:处理浏览器请求中特殊字符的转义,避免歧义。

  • HTTP 状态码与 MIME 映射:根据状态码得到描述信息,根据文件名后缀得到 Content-Type。

  • 文件属性判断:判断一个路径是普通文件还是目录,用于实现静态资源目录索引。

  • 路径安全性校验:防止客户端通过 ../ 等方式访问服务器根目录之外的敏感文件。

这些工具函数彼此独立,不依赖外部对象,非常适合作为静态方法放在一个工具类中。


二、技术选型

  • 容器与基础库:全部使用 C++ 标准库,避免引入第三方依赖。

  • 文件操作:使用 std::ifstream / std::ofstream 以二进制方式操作,避免文本模式下换行符转换等问题。

  • URL 编解码:严格按照 RFC 3986 规范实现,将非保留字符转为 %HH 格式,同时兼容 W3C 标准中 + 与空格的转换。

  • 映射表:使用 std::unordered_map 存储 HTTP 状态码描述和 MIME 类型映射,查询平均 O(1) 效率,代码清晰易扩展。

  • 路径验证:基于相对根目录思想,通过解析路径中 .. 的层级,保证客户端无法跳出根目录。


三、工具类要素

Util 类不需要实例化,所有成员均为静态方法,主要包含:

  • 两个全局的 std::unordered_map :

    • _status_msg:状态码 → 状态描述的映射(如 200 → "OK")。

    • _mime_msg:文件后缀 → MIME 类型映射(如 ".html" → "text/html")。

  • 静态方法接口:

    • 字符串分割:Split()

    • 文件读写:ReadFile()、WriteFile()

    • URL 编解码:UrlEncode()、UrlDecode() 与辅助函数HEXTOI()

    • 状态码描述:StatusDesc()

    • MIME 类型获取:ExtMime()

    • 文件属性判断:IsDirectory()、IsRegular()

    • 路径有效性判断:ValidPath()


  • 四、相关操作与接口设计

    static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *array)

    4.1 字符串分割

    • 从 src 开头向后查找分隔符 sep ,将分隔出的子串放入 array中。

    • 支持多字符分隔符(如 ", ")。

    • 忽略开头的连续分隔符,避免产生空串。

    • 返回分割后的子串数量。

    4.2 文件读写

    static bool ReadFile(const std::string &filename, std::string *buf)
    static bool WriteFile(const std::string &filename, const std::string buf)

    • ReadFile: 以 std::ios::binary 打开文件,使用 seekg + tellg获取文件大小,预分配 buf 大小后一次性读取。

    • WriteFile:以 std::ios::trunc 和  std::ios::binary  模式打开,将 buf 全部写入文件。

    • 所有操作均进行错误检查并打印日志。

    4.3 URL 编解码

    static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
    static std::string UrlDecode(const std::string url, bool convert_plus_to_space)
    static char HEXTOI(char c)

    • UrlEncode:

      • 字母、数字、. – _ ~  直接保留;

      • 若 convert_space_to_plus为 true ,空格转为 +;

      • 其余字符按字节转为%XX 格式(XX 为十六进制 ASCII 值)。

    • UrlDecode:

      • 若 convert_space_to_plus为 true,将 + 转为空格;

      • 遇到 %HH 则使用私有函数 HEXTOI 将两个十六进制字符转换为对应数值,组合成原始字节;

      • 其他字符直接保留。

    • HEXTOI:将 0-9、A-F、a-f 转为对应的 0~15 值。

    4.4 状态码描述与 MIME 映射

    static std::string StatusDesc(int status)
    static std::string ExtMime(const std::string &filename)

    • StatusDesc:在 _status_msg 中查询状态码,未找到返回 "Unknow"。

    • ExtMime:取文件名最后一个 . 之后的后缀,查找 _mime_msg,未找到返回 "application/octet-stream"。

    4.5 文件类型判断

    static bool IsDirectory(const std::string &filename)
    static bool IsRegular(const std::string &filename)

    • 使用 POSIX 的stat()获取文件属性。

    • 通过 S_ISDIR 和 S_ISREG 宏进行判断。

    4.6 路径安全性校验

    static bool ValidPath(const std::string &path)

    • 将路径按 / 分割,遍历每一级目录:

      • 遇到 ".. "  则 level–,若 level<0 说明已经跳出根目录,返回 false;

      • 否则 level++。

    • 最终 level >= 0才认为是合法路径。


    五、代码实现

    核心头文件 util.hpp 如下:

    #ifndef __UTIL__
    #define __UTIL__
    #include <fstream>
    #include <cstring>
    #include <sys/stat.h>
    #include "../source/tcpserver.hpp"
    namespace xxhh
    {
    std::unordered_map<int, std::string> _status_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"}};
    class Util
    {
    // 字符串分割
    // 字符串分割函数,将 src 字符串按照 sep 字符进行分割,得到的各个字符放到 array中,最终返回子串的数量
    static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *array)
    {
    // find + substr 找到 截断
    size_t offset = 0;
    while (offset < src.size())
    {
    // abc,def,a
    int pos = src.find(sep);
    if (pos == std::string::npos)
    {
    // 没有找到特定字符,将剩余的部分当做一个字符,放入到array
    if (pos == src.size())
    break;
    array->push_back(src.substr(offset));
    return array->size();
    }
    if (pos == offset)
    {
    offset = pos + sep.size();
    continue; // 此处是空串
    }
    // 正常情况
    array->push_back(src.substr(offset, pos – offset));
    offset += sep.size();
    }
    return array->size();
    }
    // 读取文件内容
    // 读取文件的所有内容,将读取的内容放到一个Buffer中
    static bool ReadFile(const std::string &filename, std::string *buf)
    {
    // 以二进制方式读取
    std::ifstream ifs(filename, std::ios::binary);
    if (ifs.is_open() == false)
    {
    ERR_LOG("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);
    ifs.read(&(*buf)[0], fsize);
    if (ifs.good() == false)
    {
    ERR_LOG("Read %s File Failed", filename.c_str());
    ifs.close();
    return false;
    }
    ifs.close();
    return true;
    }
    // 向文件写入数据
    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)
    {
    ERR_LOG("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编码
    // 避免 URL 中资源路径与查询字符串中的特殊字符 与 HTTP 请求中特殊字符产生歧义
    // 编码格式:将 特殊字符的 ascii 值,转换成两个16禁止字符,前缀& C++ -> C%2B%2B
    // 不编码的特殊字符:RFC3986文档规定 . – _ ~ 字母,数字属于绝对不编码字符
    // W3C标准中规定,查询字符串中的空格,需要编码为+, 解码则是+ 转成 空格
    // convert_space_to_plus 为 true 时,空格(' ')会被编码为加号('+')
    static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
    {
    std::string res;
    for (auto &e : url)
    {
    unsigned char uc = static_cast<unsigned char>(e);
    if (uc == '.' || uc == '-' || uc == '_' || uc == '~' || isalnum(uc))
    {
    res += e;
    continue;
    }
    if (uc == ' ' && convert_space_to_plus)
    {
    res += '+';
    continue;
    }
    char tmp[4] = {0};
    snprintf(tmp, sizeof(tmp), "%%%02X", uc); // 大写 %X 更常见,也可用 %02x
    res += tmp;
    }
    return res;
    }
    // HEXTOI 函数的作用是将一个十六进制字符(0-9、a-f、A-F)转换为对应的整数值(0 到 15)。
    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解码
    // 遇到了%,则将紧随其后的2个字符,转换为数字,第一个数字左移4位,然后加上第二个数字 + -> 2b %2b->2 << 4 + 11
    static std::string UrlDecode(const std::string url, bool convert_plus_to_space)
    {
    std::string res;
    for (int i = 0; i < url.size(); i++)
    {
    if (url[i] == '+' && convert_plus_to_space == true)
    {
    res += ' ';
    continue;
    }
    if (url[i] == '%' && (i + 2) < url.size())
    {
    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 StatusDesc(int status)
    {
    auto it = _status_msg.find(status);
    if (it == _status_msg.end())
    {
    return "Unknow";
    }
    return it->second;
    }
    // 根据文件后缀名读取文件 mine
    static std::string ExtMime(const std::string &filename)
    {
    // a.b.txt 先获取文件扩展名
    size_t pos = filename.find_last_of('.');
    if (pos == std::string::npos)
    {
    return "application/octet-stream";
    }
    // 根据扩展名,获取mime
    std::string ext = filename.substr(pos);
    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)
    {
    // stat – display file or file system status
    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请求的资源路径有效性判断
    // /index.html — 前边的/叫做相对根目录 映射的是某个服务器上的子目录
    // 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会
    // /../login, 这个路径中的..会让路径的查找跑到相对根目录之外,这是不合理的,不安全的
    static bool ValidPath(const std::string &path)
    {
    std::vector<std::string> subdir;
    int level = 0;
    Split(path, "/", &subdir);
    for (auto &dir : subdir)
    {
    if (dir == "..")
    {
    level–;
    if (level < 0)
    return false;
    continue;
    }
    level++;
    }
    return true;
    }
    };
    }

    #endif

    实现要点说明:

    • Split 通过循环查找 sep,用 substr 截取子串,并跳过前导分隔符,避免空串污染结果。

    • ReadFile和 WriteFile都使用了 RAII 风格的 ifstream / ofstream,在函数结束时自动关闭文件。

    • UrlEncode 中对于需要编码的字符,使用 snprintf 格式化为 %02X,保证两位十六进制大写。

    • ValidPath 巧用层级计数代替真实路径解析,既高效又安全。


    六、总结

       Util 工具类是 HTTP 服务器框架中不可或缺的一环,它将零散的辅助功能统一管理,提高了代码复用性和可维护性。通过静态方法的设计,无需实例化即可在任何模块中方便调用。后续我们将结合 Buffer 、Tcpserver以及 Util ,正式组装出一个高性能的静态资源 HTTP 服务器,敬请期待!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零手写高性能C++ TCP 服务器框架(十五) --- Util工具实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!