从零手写高性能C++ TCP 服务器框架(十五) --- Util工具实现
本文介绍了在C++ TCP服务器框架中实现Util工具类的设计与实现。该工具类为HTTP服务器提供了一系列静态方法,包括字符串分割、文件读写、URL编解码、状态码与MIME类型映射、文件属性判断和路径安全性校验等功能。通过使用C++标准库实现,避免第三方依赖,采用unordered_map存储映射关系提高查询效率,并严格按照RFC3986规范处理URL编解码。工具类采用完全静态设计,所有方法无状态
从零手写高性能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 服务器,敬请期待!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)