最近在折腾项目的时候碰到了这个知识点,查了不少资料,索性整理出来分享给大家。
🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战
🌟心向往之行必能
🎥Cx330🌸的简介:
目录
前言
在 Web 开发与系统编程中,深入理解 HTTP 协议及服务器底层架构是迈向高级开发者的必经之路。本文将从 HTTP 协议核心概念出发,剖析 GET 与 POST 的本质区别,接着以“分层解耦”的思想自底向上设计并实现一个 C++ Web 服务器,最终结合 Nginx 反向代理与开源库 cpp-httplib 落地生产级部署实践。
无论你是想探究 Web 框架底层机制,还是准备应对高频的网络编程面试,本文都将为你提供一份条理清晰、即插即用的实战指南。
一. HTTP 协议核心:GET 与 POST 的本质区别
1.1 表单提交:HTTP 请求的起点
在 Web 早期,HTML 表单(<form>)是客户端与服务器交互的主要方式。表单通过指定 action(目标 URL)和 method(提交方式,如 GET 或 POST),将用户输入的数据打包并发送给服务器。
表单提交揭示了 HTTP 的核心本质:客户端通过特定的动作描述,将结构化数据传输至服务端并获取对应结果。
用户名:
密码:
1.2 GET 与 POST:参数传递的两种方式
GET 和 POST 是 HTTP 最常用的两种请求办法,两者的核心差异如下:
算是维度
GET 方法
POST 方法
参数位置
附加在 URL 查询字符串后(如 ?key=val)
放在 HTTP 请求体(Request Body)中
语义安全性
安全且幂等(只读请求,不改变服务器状态)
不安全且非幂等(常用于创建/修改数据)
数据长度
受到浏览器/服务器 URL 长度限制(通常 2KB-8KB)
理论上无限制(受服务器 Content-Length 配置控制)
缓存机制
可被浏览器强缓存、保存在历史记录或书签中
默认不被缓存,重新提交会触发警告
数据类型
只允许 ASCII 码字符(非 ASCII 需 URL 编码)
支持任意二进制数据、JSON、Form-data 等
报文示例对比:
// GET请求
GET /login?username=zhangsan&passwd=123456 HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
// POST请求
POST /login HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
username=zhangsan&passwd=123456
1.3 其他 6 种 HTTP 方法详解
除了 GET 和 POST,HTTP/1.1 标准还定义了另外 6 种请求方法,共同构成了标准的 RESTful 风格基础:
1. HEAD:与 GET 类似,但服务器只返回响应头(Response Headers),不返回响应体(Body)。常用于检测资源存在性或获取元信息。
2. PUT:向指定资源位置上传最新东西,通常用于替换/更新完整资源(具有幂等性)。
3. DELETE:请求服务器删除指定的资源。
4. OPTIONS:查询目标资源支持的 HTTP 请求方法,常用于跨域资源共享(CORS)中的预检请求(Preflight Request)。
5. CONNECT:HTTP/1.1 预留,用于建立管道图层的隧道,常见于 HTTPS 代理。
6. TRACE:回显服务器收到的请求,主要用于网络诊断(因安全隐患如 XST 攻击,生产环境中通常禁用)。
7. (补充) PATCH:对资源进行部分修改(非幂等)。
各方法实战演示
# 1. HEAD方法:只获取响应头
curl --head http://www.baidu.com
# 输出:
# HTTP/1.1 200 OK
# Content-Length: 0
# Content-Type: text/html
# Date: Mon, 18 May 2026 12:55:44 GMT
# 2. OPTIONS方法:查询支持的方法
curl -X OPTIONS -i http://127.0.0.1/
# 生产环境Nginx通常返回:
# HTTP/1.1 405 Not Allowed
# Server: nginx/1.24.0 (Ubuntu)
# 3. PUT方法:上传文件(生产环境禁用)
curl -X PUT -T test.txt http://127.0.0.1/test.txt
- 只开放必要的 GET 和 POST 方法
- 禁用 PUT、DELETE、OPTIONS、TRACE 等方法
- 对于 RESTful API,可以有选择地开放 PUT 和 DELETE,但一定要做好权限控制
二. HTTP 服务器架构设计:分层解耦的艺术
设计一个可扩展的 HTTP 服务器,切忌将 Socket 通信、协议解析与业务逻辑交织在一起。我们采用分层架构:
+-------------------------------------------------------+
| 应用层:业务路由 / 服务注册 |
+-------------------------------------------------------+
│
▼
+-------------------------------------------------------+
| 中间层:HTTP 协议处理 (解析/序列化/路由) |
+-------------------------------------------------------+
│
▼
+-------------------------------------------------------+
| 底层:TCP Socket 传输层 (简版封装) |
+-------------------------------------------------------+
我们先封装一个基础的 TCP 服务器,负责处理连接建立和数据收发,这个更详细的封装实现,我们在前面讲过,这里是个简单版的,实际使用的是之前那个
// TcpServer.hpp 核心代码
using handler_t = std::function;
class TcpServer
{
public:
TcpServer(int port) : _port(port), _listensockfd(std::make_unique())
{
_listensockfd->BulidSocketMethod(port); // 封装socket、bind、listen
}
void Run(handler_t handler)
{
_handlerstream = handler;
_isrunning = true;
signal(SIGCHLD, SIG_IGN); // 自动回收子进程
while(_isrunning)
{
InetAddr clientaddr;
auto sockfd = _listensockfd->Accepter(&clientaddr); // 接受连接
if(fork() == 0)
{ // 多进程处理每个连接
_listensockfd->Close();
HandlerIo(sockfd, clientaddr);
exit(0);
}
sockfd->Close();
}
}
private:
void HandlerIo(std::shared_ptr sockfd, InetAddr clientaddr)
{
std::string inbuffer;
int n = sockfd->Recv(&inbuffer); // 接收数据
if(n > 0 && _handlerstream)
{
std::string outbuffer = _handlerstream(inbuffer); // 回调上层处理
if(!outbuffer.empty()) sockfd->Send(outbuffer); // 发送响应
}
}
int _port;
std::unique_ptr _listensockfd;
handler_t _handlerstream;
};
这里采用多进程模型,每个连接由一个子进程处理,实现简单且稳定,适合入门学。
2.2 中间层:HTTP 协议处理
这是整个服务器的核心,负责将 TCP 收到的字节流解析成结构化的 HTTP 请求,再将结构化的响应序列化成字节流发送出去。
三. 核心模块实现:从字节流到 HTTP 请求
3.1 HTTP 请求反序列
我们需要将原始的 HTTP 字节流解析成HttpRequest对象:
// HttpProtocol.hpp HttpRequest类核心方法
void Deserialize(std::string &streamstr)
{
// 1. 读取并解析请求行
std::string request_line;
ReadOneLine(streamstr, &request_line);
ParseLine(request_line);
// 2. 解析请求头
std::string line;
while(ReadOneLine(streamstr, &line) > 0)
{
std::string key, value;
SplitString(line, &key, &value);
if(!key.empty() && !value.empty())
{
_request_headerkv[key] = value;
}
}
// 3. 解析请求体(如果有)
if(_request_headerkv.count("Content-Length"))
{
int len = std::stoi(_request_headerkv["Content-Length"]);
_body = streamstr.substr(0, len);
// POST方法参数在请求体中
if(_method == "POST" || _method == "post")
{
_args = _body;
}
}
}
// 解析请求行,提取方法、URI、版本,并处理参数
void ParseLine(std::string &request_line)
{
std::stringstream ss(request_line);
ss >> _method >> _uri >> _http_version;
// 处理根路径,默认返回index.html
if(_uri == "/") {
_path = webroot + _uri + homepage;
} else {
_path = webroot + _uri;
}
// GET方法参数在URL中
if(_method == "GET" || _method == "get")
{
auto pos = _path.find(argsep); // argsep = "?"
if(pos != std::string::npos) {
_args = _path.substr(pos + argsep.size());
_path = _path.substr(0, pos); // 分离路径和参数
}
}
// 提取文件后缀,用于设置Content-Type
auto pos = _path.rfind(suffixsep); // suffixsep = "."
_suffix = (pos == std::string::npos) ? ".html" : _path.substr(pos);
}
关键设计:
- 统一了 GET 和 POST 的参数获取方式,上层通过
req["args"]即可获取所有参数 - 自动处理根路径重定向到首页
- 提取文件后缀,为后续静态资源服务做准备
3.2 HTTP 响应序列化
将HttpResponse对象序列化成符合 HTTP 协议的字节流:
// HttpProtocol.hpp HttpResponse类核心方法
void Serialize(std::string *outstr)
{
// 1. 构造状态行
std::string status_line = _http_version + gspace +
std::to_string(_status_code) + gspace +
_status_code_desc + linesep;
// 2. 构造响应头
std::string response_header;
for(auto& it : _response_headerkv)
{
response_header += it.first + headersep + it.second + linesep;
}
// 3. 拼接完整响应:状态行 + 响应头 + 空行 + 响应体
*outstr = status_line + response_header + _blankline + _body;
}
3.3 静态资源服务
静态资源(HTML、CSS、JS、图片等)是 Web 服务器最基础的功能:
// HttpServer.hpp 静态资源处理核心代码
std::string GetFileContentHelper(const std::string &fileurl)
{
std::ifstream in(fileurl, std::ios::binary); // 二进制方式打开,避免图片损坏
if(!in.is_open()) return std::string();
// 获取文件大小并一次性读取
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
std::string content;
content.resize(filesize);
in.read(content.data(), filesize);
in.close();
return content;
}
// 文件后缀到MIME类型的映射
std::string Suffix2Type(const std::string &suffix)
{
static const std::unordered_map mime_map = {
{".html", "text/html"}, {".css", "text/css"},
{".js", "application/javascript"}, {".jpg", "image/jpeg"},
{".png", "image/png"}, {".json", "application/json"},
// 更多类型省略...
};
auto it = mime_map.find(suffix);
return (it != mime_map.end()) ? it->second : "application/octet-stream";
}
3.4 动态路由与服务注册
这是让 HTTP 服务器具备动态交互能力的关键。我们通过一个哈希表维护路由映射:
// HttpServer.hpp 路由相关代码
using route_t = std::function;
class HttpServer
{
public:
// 注册服务接口
void Register(std::string uri, route_t handler)
{
std::string key = webroot + uri; // 统一加上webroot前缀
_route[key] = handler;
}
std::string HandlerHttpRequest(std::string &streamstr)
{
HttpRequest httpreq;
httpreq.Deserialize(streamstr);
HttpResponse httpresp;
// 判断是否是动态路由请求
if(IsNeedRoute(httpreq["path"]))
{
_route[httpreq["path"]](httpreq, httpresp); // 调用注册的处理函数
}
else
{
// 处理静态资源
std::string filecontent = GetFileContentHelper(httpreq["path"]);
std::string suffix = httpreq["suffix"];
if(filecontent.empty())
{
// 返回404页面
std::string file404 = GetFileContentHelper("wwwroot/404.html");
httpresp.SetCode(404);
httpresp.SetHeader("Content-Length", file404.size());
httpresp.SetHeader("Content-Type", "text/html");
httpresp.SetBody(file404);
}
else
{
httpresp.SetCode(200);
httpresp.SetHeader("Content-Length", filecontent.size());
httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
httpresp.SetBody(filecontent);
}
}
httpresp.SetHeader("Connection", "close"); // 短连接模式
std::string httprespstr;
httpresp.Serialize(&httprespstr);
return httprespstr;
}
private:
bool IsNeedRoute(const std::string &key)
{
return _route.find(key) != _route.end();
}
std::unordered_map _route; // 路由表
};
四. 实战:搭建一个完整的 Web 服务
现在我们可以用自己写的 HTTP 服务器搭建一个包含登录、注册功能的 Web 服务,声明一下,我们这里就不展示里面我们封装好的组件代码了,这些之前都有讲过
Main.cpp
include "HttpServer.hpp"
include
// @brief 打印程序的使用说明
// @param procname 当前运行的程序名称(通常是 argv[0])
void Usage(std::string procname)
{
// 提示用户运行程序时需要附带端口号参数
std::cout
就写这么多吧,内容比较基础,适合入门回顾。有补充的地方欢迎留言一起完善。
评论 (0)
暂无评论