从零手写高性能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 类不需要实例化,所有成员均为静态方法,主要包含:

  1. 两个全局的 std::unordered_map :

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

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

  2. 静态方法接口:

    • 字符串分割: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 服务器,敬请期待!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐