跳到主要内容HTTP 协议深度解析(三):完整 HTTP 服务器实现 | 极客日志C++
HTTP 协议深度解析(三):完整 HTTP 服务器实现
HTTP 协议深度解析系列第三篇,重点讲解完整 HTTP 服务器的 C++ 实现。内容涵盖 Web 根目录概念与路径映射规则、文件读取与 MIME 类型判断、HTTP 请求解析(首行、Header、Body、参数)、响应构造(状态码、Header、Body)。通过 HttpServer 类展示 Socket 通信、多线程处理连接及静态资源服务。最后对比了 HTTP 0.9 至 3.0 的版本演进特性。掌握该实现有助于深入理解网络编程与 Web 通信基础。
奇形怪状2 浏览 HTTP 协议深度解析(三):完整 HTTP 服务器实现
一、web 根目录的概念
1.1 什么是 web 根目录
**web 根目录(Document Root)**是 HTTP 服务器存放网页文件的目录。
web 根目录:/var/www/html 目录结构: /var/www/html/ ├── index.html ├── about.html ├── css/ │ └── style.css ├── js/ │ └── app.js └── images/ └── logo.png
http://example.com/ → /var/www/html/index.html http://example.com/about.html → /var/www/html/about.html http://example.com/css/style.css → /var/www/html/css/style.css http://example.com/images/logo.png → /var/www/html/images/logo.png
1.2 路径映射规则
规则:URL 路径 = web 根目录 + URL 路径部分
std::string GetFilePath(const std::string& url_path, const std::string& web_root) {
if (url_path == "/") {
return web_root + "/index.html";
}
return web_root + url_path;
}
web 根目录:./wwwroot URL:/test.html 文件路径:./wwwroot/test.html URL:/images/photo.jpg 文件路径:./wwwroot/images/photo.jpg
1.3 安全性考虑
GET /../../../etc/passwd HTTP/1.1
./wwwroot/../../../etc/passwd → /etc/passwd
bool IsSafePath(const std::string& path, const std::string& web_root) {
if (path.find("..") != std::string::npos) {
return false;
}
char real_path[PATH_MAX];
if (realpath(path.c_str(), real_path) == NULL) {
return false;
}
char real_root[PATH_MAX];
realpath(web_root.c_str(), real_root);
return strncmp(real_path, real_root, strlen(real_root)) == 0;
}
二、文件读取与 MIME 类型
2.1 读取文件内容
std::string ReadFile(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
return "";
}
file.seekg(0, std::ios::end);
size_t filesize = file.tellg();
file.seekg(0, std::ios::beg);
std::string content;
content.resize(filesize);
file.read(&content[0], filesize);
file.close();
return content;
}
std::ios::binary:二进制模式,避免文本模式的换行符转换
seekg和 tellg:获取文件大小
resize:预分配内存,提高效率
read:读取指定字节数
2.2 MIME 类型判断
MIME(Multipurpose Internet Mail Extensions)类型用于标识文件的媒体类型。
浏览器根据 Content-Type 判断如何处理响应内容:
text/html:渲染 HTML
image/png:显示图片
application/json:解析 JSON
text/plain:显示纯文本
std::string GetMimeType(const std::string& filepath) {
size_t pos = filepath.rfind('.');
if (pos == std::string::npos) {
return "application/octet-stream";
}
std::string ext = filepath.substr(pos);
static std::map<std::string, std::string> mime_types = {
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".json", "application/json"},
{".xml", "application/xml"},
{".txt", "text/plain"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
{".svg", "image/svg+xml"},
{".ico", "image/x-icon"},
{".pdf", "application/pdf"},
{".zip", "application/zip"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"}
};
auto it = mime_types.find(ext);
if (it != mime_types.end()) {
return it->second;
}
return "application/octet-stream";
}
2.3 文件是否存在
bool FileExists(const std::string& filepath) {
struct stat info;
return stat(filepath.c_str(), &info) == 0 && S_ISREG(info.st_mode);
}
S_ISREG宏:判断是否为普通文件(不是目录、链接等)。
三、HTTP 请求解析
3.1 请求结构体
struct HttpRequest {
std::string method;
std::string url;
std::string version;
std::map<std::string, std::string> headers;
std::string body;
std::map<std::string, std::string> query_params;
std::map<std::string, std::string> post_params;
};
3.2 解析首行
bool ParseRequestLine(const std::string& line, HttpRequest* req) {
std::istringstream iss(line);
iss >> req->method >> req->url >> req->version;
if (req->method.empty() || req->url.empty() || req->version.empty()) {
return false;
}
return true;
}
bool ParseHeader(const std::string& line, HttpRequest* req) {
size_t pos = line.find(':');
if (pos == std::string::npos) {
return false;
}
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
size_t start = value.find_first_not_of(' ');
if (start != std::string::npos) {
value = value.substr(start);
}
req->headers[key] = value;
return true;
}
输入:"Host: www.example.com" 解析:headers["Host"]="www.example.com"
3.4 完整解析流程
bool ParseHttpRequest(const std::string& raw_request, HttpRequest* req) {
std::istringstream stream(raw_request);
std::string line;
if (!std::getline(stream, line)) {
return false;
}
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (!ParseRequestLine(line, req)) {
return false;
}
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (line.empty()) {
break;
}
ParseHeader(line, req);
}
std::string body_line;
while (std::getline(stream, body_line)) {
req->body += body_line;
if (stream.peek() != EOF) {
req->body += "\n";
}
}
return true;
}
3.5 解析 URL 参数
URL 格式:/search?keyword=Linux&page=2
void ParseQueryParams(HttpRequest* req) {
size_t pos = req->url.find('?');
if (pos == std::string::npos) {
return;
}
std::string query = req->url.substr(pos + 1);
req->url = req->url.substr(0, pos);
std::istringstream stream(query);
std::string pair;
while (std::getline(stream, pair, '&')) {
size_t eq = pair.find('=');
if (eq != std::string::npos) {
std::string key = pair.substr(0, eq);
std::string value = pair.substr(eq + 1);
req->query_params[key] = UrlDecode(value);
}
}
}
3.6 urldecode 实现
std::string UrlDecode(const std::string& str) {
std::string result;
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == '%' && i + 2 < str.size()) {
int value;
std::istringstream iss(str.substr(i + 1, 2));
if (iss >> std::hex >> value) {
result += static_cast<char>(value);
i += 2;
} else {
result += str[i];
}
} else if (str[i] == '+') {
result += ' ';
} else {
result += str[i];
}
}
return result;
}
输入:"C%2B%2B%20%E7%BC%96%E7%A8%8B" 输出:"C++ 编程"
3.7 解析 POST 参数
void ParsePostParams(HttpRequest* req) {
if (req->method != "POST") {
return;
}
auto it = req->headers.find("Content-Type");
if (it == req->headers.end() || it->second.find("application/x-www-form-urlencoded") == std::string::npos) {
return;
}
std::istringstream stream(req->body);
std::string pair;
while (std::getline(stream, pair, '&')) {
size_t eq = pair.find('=');
if (eq != std::string::npos) {
std::string key = pair.substr(0, eq);
std::string value = pair.substr(eq + 1);
req->post_params[key] = UrlDecode(value);
}
}
}
四、HTTP 响应构造
4.1 响应结构体
struct HttpResponse {
std::string version;
int status_code;
std::string status_text;
std::map<std::string, std::string> headers;
std::string body;
std::string Build() {
std::ostringstream oss;
oss << version << " " << status_code << " " << status_text << "\r\n";
for (auto& pair : headers) {
oss << pair.first << ": " << pair.second << "\r\n";
}
oss << "\r\n";
oss << body;
return oss.str();
}
};
4.2 状态码对应的文本
std::string GetStatusText(int code) {
static std::map<int, std::string> status_texts = {
{200, "OK"},
{201, "Created"},
{204, "No Content"},
{301, "Moved Permanently"},
{302, "Found"},
{304, "Not Modified"},
{400, "Bad Request"},
{401, "Unauthorized"},
{403, "Forbidden"},
{404, "Not Found"},
{500, "Internal Server Error"},
{502, "Bad Gateway"},
{503, "Service Unavailable"}
};
auto it = status_texts.find(code);
if (it != status_texts.end()) {
return it->second;
}
return "Unknown";
}
4.3 构造 200 响应
HttpResponse Build200Response(const std::string& content, const std::string& content_type) {
HttpResponse resp;
resp.version = "HTTP/1.1";
resp.status_code = 200;
resp.status_text = "OK";
resp.headers["Content-Type"] = content_type;
resp.headers["Content-Length"] = std::to_string(content.size());
resp.headers["Connection"] = "close";
resp.body = content;
return resp;
}
4.4 构造 404 响应
HttpResponse Build404Response() {
std::string html = "<html>\n"
"<head><title>404 Not Found</title></head>\n"
"<body>\n"
"<h1>404 Not Found</h1>\n"
"<p>The requested resource was not found on this server.</p>\n"
"</body>\n"
"</html>";
HttpResponse resp;
resp.version = "HTTP/1.1";
resp.status_code = 404;
resp.status_text = "Not Found";
resp.headers["Content-Type"] = "text/html";
resp.headers["Content-Length"] = std::to_string(html.size());
resp.body = html;
return resp;
}
五、完整 HTTP 服务器实现
5.1 HttpServer 类
class HttpServer {
public:
HttpServer(int port, const std::string& web_root)
: _port(port), _web_root(web_root), _listen_fd(-1) {}
bool Start() {
_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_fd < 0) {
perror("socket");
return false;
}
int opt = 1;
setsockopt(_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(_port);
if (bind(_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
return false;
}
if (listen(_listen_fd, 10) < 0) {
perror("listen");
return false;
}
std::cout << "HTTP Server started on port " << _port << std::endl;
std::cout << "Web root: " << _web_root << std::endl;
while (true) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &len);
if (client_fd < 0) {
perror("accept");
continue;
}
std::thread t(&HttpServer::HandleClient, this, client_fd);
t.detach();
}
return true;
}
private:
void HandleClient(int client_fd) {
char buffer[8192];
ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n <= 0) {
close(client_fd);
return;
}
buffer[n] = '\0';
std::string raw_request(buffer);
HttpRequest req;
if (!ParseHttpRequest(raw_request, &req)) {
close(client_fd);
return;
}
ParseQueryParams(&req);
ParsePostParams(&req);
std::cout << req.method << " " << req.url << std::endl;
HttpResponse resp = ProcessRequest(req);
std::string response_str = resp.Build();
write(client_fd, response_str.c_str(), response_str.size());
close(client_fd);
}
HttpResponse ProcessRequest(const HttpRequest& req) {
if (req.method == "GET") {
return HandleStaticFile(req);
}
if (req.method == "POST") {
return HandlePostRequest(req);
}
return Build404Response();
}
HttpResponse HandleStaticFile(const HttpRequest& req) {
std::string filepath = _web_root + req.url;
if (req.url == "/") {
filepath = _web_root + "/index.html";
}
if (!FileExists(filepath)) {
return Build404Response();
}
std::string content = ReadFile(filepath);
if (content.empty()) {
return Build404Response();
}
std::string mime_type = GetMimeType(filepath);
return Build200Response(content, mime_type);
}
HttpResponse HandlePostRequest(const HttpRequest& req) {
if (req.url == "/api/echo") {
std::string json = "{\"received\": \"" + req.body + "\"}";
return Build200Response(json, "application/json");
}
return Build404Response();
}
int _port;
std::string _web_root;
int _listen_fd;
};
5.2 main 函数
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "Usage: " << argv[0] << " <port> <web_root>" << std::endl;
return 1;
}
int port = std::atoi(argv[1]);
std::string web_root = argv[2];
HttpServer server(port, web_root);
server.Start();
return 0;
}
六、测试验证
6.1 准备测试文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试页面</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>欢迎访问 HTTP 服务器</h1>
<p>这是一个静态 HTML 页面</p>
<img src="/logo.png" alt="Logo">
<script src="/app.js"></script>
</body>
</html>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 50px;
}
h1 {
color: #333;
}
console.log('JavaScript loaded!');
alert('Hello from HTTP Server!');
6.2 编译运行
g++ -o http_server http_server.cpp -std=c++11 -lpthread
./http_server 9090 ./wwwroot
HTTP Server started on port 9090 Web root: ./wwwroot
6.3 浏览器测试
浏览器显示 index.html 内容,并自动加载了 CSS 和 JS 文件。
GET / GET /style.css GET /app.js GET /logo.png GET /favicon.ico
6.4 curl 测试
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 285
Connection: close
<!DOCTYPE html><html>... </html>
HTTP/1.1 404 Not Found
Content-Type: text/html
Content-Length: 158
<html><head><title>404 Not Found</title></head>... </html>
curl -i "http://127.0.0.1:9090/search?keyword=Linux&page=2"
query_params["keyword"]="Linux" query_params["page"]="2"
七、HTTP 版本演进
7.1 HTTP/0.9(1991 年)
- 只支持 GET 方法
- 只能传输 HTML
- 无 Header
- 连接立即关闭
请求:GET /index.html 响应:<html>...</html>
7.2 HTTP/1.0(1996 年)
- 支持 POST、HEAD 方法
- 引入 Header(可以传输多种数据类型)
- 状态码
- 缓存机制
- 每次请求都要建立新的 TCP 连接(短连接)
- 性能差
7.3 HTTP/1.1(1999 年)
- 持久连接(Connection: keep-alive)
- 管道化(Pipelining)
- Host 字段(虚拟主机)
- 分块传输编码(Chunked Transfer Encoding)
- 复用 TCP 连接,减少握手开销
- 一个连接可以发送多个请求
- 队头阻塞(Head-of-Line Blocking)
- Header 冗余(每次请求都要发送完整 Header)
7.4 HTTP/2.0(2015 年)
- 多路复用(Multiplexing):一个 TCP 连接上并行处理多个请求
- 二进制帧(Binary Framing):效率更高
- Header 压缩(HPACK 算法)
- 服务器推送(Server Push)
- 解决了 HTTP/1.1 的队头阻塞问题
- 显著提高性能
7.5 HTTP/3.0(2022 年)
- 基于 QUIC 协议(Quick UDP Internet Connections)
- QUIC 基于 UDP,而不是 TCP
- 减少连接建立时间
- 解决 TCP 的队头阻塞问题
- 连接建立更快(0-RTT 或 1-RTT)
- 更好的移动网络适应性
- 连接迁移(IP 地址变化时连接不断)
八、本篇总结
8.1 核心要点
- 存放网页文件的目录
- URL 路径映射到文件系统路径
- 注意路径穿越攻击
- 二进制模式读取
- seekg/tellg 获取文件大小
- resize 预分配内存
- 根据文件扩展名判断
- 告诉浏览器如何处理响应内容
- 常见类型:text/html、image/png、application/json 等
- 首行:方法 URL 版本
- Header:键值对
- Body:可选
- GET 参数从 URL 解析
- POST 参数从 Body 解析
- 首行:版本 状态码 状态描述
- Header:Content-Type、Content-Length 等
- Body:HTML、JSON、图片等
- socket→bind→listen→accept 循环
- 多线程处理连接
- 解析请求→路由分发→构造响应→发送
- 静态资源服务
- 动态 API 处理
- HTTP/0.9:最简单,只有 GET
- HTTP/1.0:引入 Header、状态码
- HTTP/1.1:持久连接、管道化
- HTTP/2.0:多路复用、二进制帧、Header 压缩
- HTTP/3.0:基于 QUIC(UDP)、更快的连接建立
8.2 容易混淆的点
- web 根目录和文件路径:URL 的/对应 web 根目录,不是系统根目录。
- MIME 类型的重要性:Content-Type 错误会导致浏览器无法正确显示内容(如图片显示为乱码)。
- GET 参数和 POST 参数的位置:GET 在 URL,POST 在 Body。
- urldecode 的必要性:URL 中的中文、特殊字符都是编码后的,必须 decode 才能正确处理。
- HTTP/1.1 的持久连接:默认就是 keep-alive,不需要显式设置。只有想关闭时才设置 Connection: close。
- HTTP/2 和 HTTP/3 的区别:HTTP/2 基于 TCP,HTTP/3 基于 UDP(QUIC 协议)。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online