一、HTTP 协议基本概念
虽然我们说, 应⽤层协议是我们程序猿自己定的. 但实际上, 已经有⼤佬们定义了⼀些现成的, ⼜⾮常好⽤的应⽤层协议, 供我们直接参考使⽤. HTTP(超⽂本传输协议)就是其中之⼀。
在互联⽹世界中,HTTP(HyperText Transfer Protocol,超⽂本传输协议)是⼀个⾄关重要的协议。它定义了客⼾端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。
HTTP协议是客⼾端与服务器之间通信的基础。客⼾端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是⼀个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客⼾端的状态信息。
由于 "无状态" 无法满足登录验证、购物车、用户会话等场景需求,实际开发中会通过以下技术实现 "状态保持":
Cookie
- 原理:服务器在响应头中通过
Set-Cookie字段,向客户端发送一段标识信息(如会话 ID);客户端后续请求时,会自动在Cookie字段中携带该信息,服务器通过解析 Cookie 识别客户端。- 示例:登录成功后,服务器返回
Set-Cookie: session_id=abc123;客户端下次请求时,自动携带Cookie: session_id=abc123,服务器通过session_id确认 "该客户端已登录"。Session
- 原理:服务器为每个客户端创建独立的 "会话对象"(存储登录状态、用户信息等),并生成唯一的
session_id;session_id通过 Cookie 发送给客户端,客户端后续请求携带session_id,服务器据此找到对应的会话对象。- 特点:状态数据存储在服务器端,比 Cookie 更安全(避免敏感信息暴露在客户端)。
Token
- 原理:客户端登录成功后,服务器生成一段加密的 "令牌(Token)" 并返回;客户端后续请求时,在请求头(如
Authorization: Bearer <Token>)或参数中携带 Token,服务器解密 Token 即可验证身份和状态。- 适用场景:跨域请求、移动端 APP(无需依赖 Cookie)。
二、URL 相关
平时我们俗称的 "⽹址" 其实就是说的 URL

- URL:统一资源定位符,即 "网址",用于标识互联网中的资源位置。
- urlencode 与 urldecode :
- 转义规则:特殊字符(如
/、?、:)转为 16 进制,前加%(如+转义为%2B)。 - urldecode 是 urlencode 的逆过程,用于解析转义后的字符。
- 转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不⾜4位直接处理),每2位做⼀位,前⾯加
上%,编码成%XY格式
例如:

- 转义的规则如下:
- 转义规则:特殊字符(如
三、HTTP 请求与响应格式
1. HTTP 请求格式

- 首行 :
[方法] + [url] + [版本](如GET /index.html HTTP/1.1)。 - Header :键值对(冒号分隔),每组用
\r\n分隔,空行表示 Header 结束(如Host: www.example.com)。 - Body :空行后的内容,可为空;若存在,Header 中用
Content-Length标识长度。

请求是怎么表示自己要请求什么资源的?
一、资源请求的表示:URI
客户端通过 URI(统一资源标识符) 向服务器表示要请求的资源,例如:

- HTTP 请求行示例:其中
/a/b/c.html就是 URI,明确了要请求的资源路径。 - 浏览器地址栏的完整 URL(如
8137.19.140:8080/a/b/c.html),包含服务器地址、端口和 URI 部分,共同定位网络中的资源。
二、服务器端资源的存储与定位
服务器的资源并非存放在 Linux 系统根目录,而是在web 根目录 下。资源的实际路径通过 "web 根目录 + URI" 拼接得到:
- 若 URI 是
/a/b/c.html,则实际文件路径为./wwwroot/a/b/c.html。
三、首页的自动拼接规则
当客户端请求根路径(URI 为/)时,服务器会自动拼接默认首页文件 (如index.html或index.htm):
- 实际路径为
./wwwroot/index.html,确保用户访问根路径时能加载到首页。
四、网页资源的类型与存储
网页依赖的所有资源(如 html、css、js、图片、视频等),都需存放在 web 根目录下,通过上述 "web 根目录 + URI" 的规则被客户端请求和加载。
五、代码实现示例(资源路径拼接)
通过 C++ 代码示例,直观展示路径拼接逻辑:
cpp
// 定义web根目录和默认首页路径
const std::string webroot = "./wwwroot";
const std::string homepage = "/index.html";
// 拼接实际文件路径并读取内容
std::string filename = webroot + homepage; // 结果为./wwwroot/index.html
bool res = Util::ReadFileContent(filename, &(resp._text));
2. HTTP 响应格式

- 首行 :
[版本号] + [状态码] + [状态码解释](如HTTP/1.1 200 OK)。 - Header:同请求格式,键值对描述响应属性。
- Body :空行后的内容(如 HTML 页面内容),长度由
Content-Length标识。

四、HTTP 方法(常用)
| 方法 | 用途 | 特性 | 示例 |
|---|---|---|---|
| GET | 请求 URL 指定资源 | 数据在 URL 中,长度有限,常用于查询 | GET /index.html HTTP/1.1 |
| POST | 提交表单数据等实体主体 | 数据在 Body 中,可传输大量数据 | POST /submit.cgi HTTP/1.1 |
| HEAD | 类似 GET,但仅返回响应头 | 用于确认 URL 有效性或资源更新时间 | HEAD /index.html HTTP/1.1 |
| PUT | 传输文件到指定 URL 位置 | 用于更新资源(较少用) | PUT /example.html HTTP/1.1 |
| DELETE | 删除指定 URL 资源 | PUT 的相反操作(较少用) | DELETE /example.html HTTP/1.1 |
| OPTIONS | 查询资源支持的方法 | 返回允许的方法(如 GET、POST) | OPTIONS * HTTP/1.1 |



GET:提交参数的方式通过uri进行提交的。会回显参数。
POST:提交参数的方式通过http request正文提交的。可以传递长数据。不会回显参数。
两者都不安全,都可以通过抓包工具抓取!
五、HTTP 状态码

(一)常见状态码汇总
| 状态码 | 含义 | 应用样例 |
|---|---|---|
| 100 | Continue | 上传大文件时,服务器告诉客户端可以继续上传 |
| 200 | OK | 访问网站首页,服务器返回网页内容 |
| 201 | Created | 发布新文章,服务器返回文章创建成功的信息 |
| 204 | No Content | 删除文章后,服务器返回 "无内容" 表示操作成功 |
| 301 | Moved Permanently | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
| 302 | Found 或 See Other | 用户登录成功后,重定向到用户首页 |
| 304 | Not Modified | 浏览器缓存机制,对未修改的资源返回 304 状态码 |
| 400 | Bad Request | 填写表单时,格式不正确导致提交失败 |
| 401 | Unauthorized | 访问需要登录的页面时,未登录或认证失败 |
| 403 | Forbidden | 尝试访问没有权限查看的页面 |
| 404 | Not Found | 访问不存在的网页链接 |
| 500 | Internal Server Error | 服务器崩溃或数据库错误导致页面无法加载 |
| 502 | Bad Gateway | 使用代理服务器时,代理服务器无法从上游服务器获取有效响应 |
| 503 | Service Unavailable | 服务器维护或过载,暂时无法处理请求 |
(二)重定向相关状态码
| 状态码 | 含义 | 是否为临时重定向 | 应用样例 |
|---|---|---|---|
| 301 | Moved Permanently | 否 | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
| 302 | Found 或 See Other | 是 | 用户登录成功后,重定向到用户首页 |
| 307 | Temporary Redirect | 是 | 临时重定向资源到新的位置(较少使用) |
| 308 | Permanent Redirect | 否 | 永久重定向资源到新的位置(较少使用) |
(三)重定向验证(以 301 为例)
-
依赖 Location 选项:HTTP 状态码 301(永久重定向)和 302(临时重定向)均依赖 Location 选项指定资源新位置。
-
HTTP 状态码 301(永久重定向):
-
含义:请求的资源已永久移动到新位置。
-
Location 头部作用:服务器在响应中添加 Location 头部,包含新 URL 地址,浏览器自动重定向到该地址。
-
响应示例:
HTTP/1.1 301 Moved Permanently\r\n Location: https://www.new-url.com\r\n
-
-
HTTP 状态码 302(临时重定向):
-
含义:请求的资源临时移动到新位置。
-
Location 头部作用:服务器添加 Location 头部指定新位置,浏览器暂时用新 URL 进行后续请求,不缓存重定向。
-
响应示例:
HTTP/1.1 302 Found\r\n Location: https://www.new-url.com\r\n
-

六、HTTP 常见 Header
(一)核心 Header 说明
| 字段名 | 含义 | 样例 |
|---|---|---|
| Accept | 客户端可接受的响应内容类型 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8 |
| Accept-Encoding | 客户端支持的数据压缩格式 | Accept-Encoding: gzip, deflate, br |
| Accept-Language | 客户端可接受的语言类型 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
| Host | 请求的主机名和端口号 | Host: www.example.com:8080 |
| User-Agent | 客户端的软件环境信息 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 |
| Cookie | 客户端发送给服务器的 HTTP cookie 信息 | Cookie: session_id=abcdefg12345; user_id=123 |
| Referer | 请求的来源 URL | Referer: http://www.example.com/previous_page.html |
| Content-Type | 实体主体的媒体类型 | Content-Type: application/x-www-form-urlencoded(表单提交)或 Content-Type: application/json(JSON 数据) |
| Content-Length | 实体主体的字节大小 | Content-Length: 150 |
| Authorization | 认证信息,如用户名和密码 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==(Base64 编码后的用户名:密码) |
| Cache-Control | 缓存控制指令 | 请求时:Cache-Control: no-cache 或 Cache-Control: max-age=3600;响应时:Cache-Control: public, max-age=3600 |
| Connection | 请求完后是关闭还是保持连接 | Connection: keep-alive 或 Connection: close |
| Date | 请求或响应的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
| Location | 重定向的目标 URL(与 3xx 状态码配合使用) | Location: http://www.example.com/new_location.html(与 302 状态码配合使用) |
| Server | 服务器类型 | Server: Apache/2.4.41 (Unix) |
| Last-Modified | 资源的最后修改时间 | Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT |
| ETag | 资源的唯一标识符,用于缓存 | ETag: "3f80f-1b6-5f4e2512a4100" |
| Expires | 响应过期的日期和时间 | Expires: Wed, 21 Oct 2023 08:28:00 GMT |
(二)Connection 报头补充
- 核心作用:控制和管理客户端与服务器之间的连接状态,管理持久连接(长连接)。
- 持久连接(长连接):
- HTTP/1.1:默认使用持久连接,客户端和服务器未明确指定关闭连接时,连接保持打开,供后续请求和响应复用。
- HTTP/1.0:默认连接是非持久的,需在请求头中显式设置
Connection: keep-alive实现持久连接。
- 语法格式:
Connection: keep-alive:希望保持连接以复用 TCP 连接。Connection: close:请求 / 响应完成后,关闭 TCP 连接。
七、文件读取辅助函数
cpp
std::string GetFileContentHelper(const std::string &path)
{
// 一份简单的读取二进制文件的代码
std::ifstream in(path, std::ios::binary);
if (!in.is_open())
return "";
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
std::string content;
content.resize(filesize);
in.read((char *)content.c_str(), filesize);
// std::vector<char> content(filesize);
// in.read(content.data(), filesize);
in.close();
return content;
}
八、网页报错信息(补充)
| 网页 URL | 系统报错信息 |
|---|---|
| http://127.0.0.1/ | URL 拼写可能存在错误,请检查 |
| https://www.new-url.com | 网页解析失败,可能是不支持的网页类型,请检查网页或稍后重试 |
| http://www.example.com/previous_page.html | 网页解析失败,可能是不支持的网页类型,请检查网页或稍后重试 |
| http://www.example.com/new_location.html | 网页解析失败,可能是不支持的网页类型,请检查网页或稍后重试 |
九、HTTP服务器
Http.hpp
cpp
#pragma once
#include "Socket.hpp"
#include "TcpServer.hpp"
#include <iostream>
#include <string>
#include <memory>
#include "Util.hpp"
#include <sstream>
#include <unordered_map>
#include "Log.hpp"
using namespace SocketModule;
using namespace LogModule;
const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";
const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string page_404 = "/404.html";
// 请求报头
class HttpRequest
{
public:
HttpRequest() : _is_interact(false)
{
}
std::string Serialize()
{
return "";
}
void PaseReqLine(std::string &reqline)
{
// GET / HTTP/1.1
// 将请求行字符串按reqline分割,得到三个字段
std::stringstream ss(reqline);
// 依次提取三个关键部分
ss >> _method >> _uri >> _version;
}
bool Deserialize(std::string &reqstr)
{
// 1. 以换行符作为分隔符,读取请求行
std::string reqline;
bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
LOG(LogLevel::DEBUG) << "reqline: " << reqline;
// 2. 对请求行进行反序列化
// GET / HTTP/1.1
PaseReqLine(reqline);
// 得到请求路径
if (_uri == "/")
_uri = webroot + _uri + homepage;
else
_uri = webroot + _uri;
if (_method == "POST")
{
_is_interact = true;
// 3. 以换行符作为分隔符,读取每个header
while (Util::ReadOneLine(reqstr, &reqline, glinespace))
{
if (reqline.empty())
break;
size_t pos = reqline.find(glinesep);
if (pos == std::string::npos)
{
//...
LOG(LogLevel::ERROR) << "请求报头格式错误";
return false;
}
std::string key = reqline.substr(0, pos);
std::string value = reqline.substr(pos + glinesep.length());
_headers.insert(std::make_pair(key, value));
}
// 4. 读取请求正文
if (_headers.find("Content-Length") != _headers.end())
{
int content_length = std::stoi(_headers["Content-Length"]);
_text = reqstr.substr(0, content_length);
reqstr.erase(0, content_length);
}
}
LOG(LogLevel::DEBUG) << "_method: " << _method;
LOG(LogLevel::DEBUG) << "_uri: " << _uri;
LOG(LogLevel::DEBUG) << "_version: " << _version;
// 0.0.0.0:8080/login?username=zhangsan&password=123456
// 从 URI 中提取查询参数, 并分离出纯资源路径
const std::string temp = "?";
auto pos = _uri.find(temp);
if (pos != std::string::npos)
{
_args = _uri.substr(pos + temp.size());
_uri = _uri.substr(0, pos);
// 标记该请求包含交互参数
_is_interact = true;
}
return true;
}
std::string Uri() { return _uri; }
bool isInteract() { return _is_interact; }
std::string Args() { return _args; }
~HttpRequest()
{
}
private:
std::string _method;
std::string _uri;
std::string _version;
std::unordered_map<std::string, std::string> _headers;
std::string _blankline;
std::string _text;
std::string _args; // 查询参数
bool _is_interact; // 是否交互模式
};
// 响应报头
class HttpResponse
{
public:
HttpResponse() : _blankline(glinespace), _version("HTTP/1.1")
{
}
std::string Serialize()
{
std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace; // 状态行
std::string resp_header; // 响应报头
for (auto &header : _headers)
{
std::string line = header.first + glinesep + header.second + glinespace;
resp_header += line;
}
return status_line + resp_header + glinespace + _text; // 状态行 + 响应报头 + 空行 + 响应正文
}
bool Deserialize(std::string &reqstr)
{
return true;
}
void SetTargetFile(const std::string &target)
{
_targetfile = target;
}
void SetCode(int code)
{
_code = code;
switch (_code)
{
case 200:
_desc = "OK";
break;
case 404:
_desc = "Not Found";
break;
case 302:
_desc = "Found";
break;
case 301:
_desc = "Moved Permanently";
break;
default:
_desc = "Unknown";
break;
}
}
void SetHeader(const std::string &key, const std::string &value)
{
auto iter = _headers.find(key);
if (iter != _headers.end())
return;
_headers.insert(std::make_pair(key, value));
}
std::string Uri2Suffix(const std::string &targetfile)
{
// 得到文件后缀,根据后缀返回Content-Type
auto pos = targetfile.rfind('.');
if (pos == std::string::npos)
{
return "text/html";
}
std::string suffix = targetfile.substr(pos);
if (suffix == ".html" || suffix == ".htm")
{
return "text/html";
}
else if (suffix == ".jpg")
{
return "image/jpeg";
}
else if (suffix == ".png")
{
return "image/png";
}
else if (suffix == ".gif")
{
return "image/gif";
}
else if (suffix == ".css")
{
return "text/css";
}
else if (suffix == ".js")
{
return "application/javascript";
}
else if (suffix == ".json")
{
return "application/json";
}
else if (suffix == ".xml")
{
return "text/xml";
}
else if (suffix == ".txt")
{
return "text/plain";
}
else
{
return "text/html";
}
}
bool MakeResponse()
{
if (_targetfile == "./wwwroot/favicon.ico")
{
LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
return false;
}
if (_targetfile == "./wwwroot/redir_test") // 重定向测试
{
SetCode(302);
SetHeader("Location", "https://www.baidu.com");
return true;
}
int filesize = 0;
bool res = Util::ReadFileContent(_targetfile, &_text);
if (!res)
{
// _text = "";
// SetCode(404);
// _targetfile = webroot + page_404;
// filesize = Util::FileSize(_targetfile);
// Util::ReadFileContent(_targetfile, &_text);
// std::string suffix = Uri2Suffix(_targetfile);
// SetHeader("Content-Type", suffix);
// SetHeader("Content-Length", std::to_string(filesize));
SetCode(302);
SetHeader("Location", "https://www.baidu.com");
}
else
{
LOG(LogLevel::DEBUG) << "读取文件:" << _targetfile;
SetCode(200);
filesize = Util::FileSize(_targetfile);
std::string suffix = Uri2Suffix(_targetfile);
SetHeader("Content-Type", suffix);
SetHeader("Content-Length", std::to_string(filesize));
}
return true;
}
void SetText(const std::string &text)
{
_text = text;
}
~HttpResponse() {}
public:
std::string _version;
int _code;
std::string _desc;
std::unordered_map<std::string, std::string> _headers;
std::string _blankline;
std::string _text;
std::string _targetfile;
};
using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;
// 处理HTTP请求
class Http
{
public:
Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
{
}
void HandlerHttpRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// #ifndef DEBUG
// #define DEBUG
// 接收HTTP请求
std::string httpreqstr;
HttpResponse resp; // 一个应答
int n = sock->Recv(&httpreqstr);
if (n > 0)
{
HttpRequest req; // 一个请求
req.Deserialize(httpreqstr); // 请求反序列化,将uri解析为文件路径
if (req.isInteract()) // 交互模式
{
if (_route.find(req.Uri()) == _route.end())
{
}
else // 有注册的服务
{
_route[req.Uri()](req, resp);
std::string response_str = resp.Serialize(); // 应答序列化
sock->Send(response_str); // 发送应答
}
}
else
{
resp.SetTargetFile(req.Uri());
if (resp.MakeResponse()) // 根据uri生成响应报文
{
std::string response_str = resp.Serialize(); // 应答序列化
sock->Send(response_str); // 发送应答
}
}
// std::string filename = req.Uri();
// HttpResponse resp;
// resp._version = "HTTP/1.1";
// resp._code = 200;
// resp._desc = "OK";
// // LOG(LogLevel::DEBUG) << "用户请求:" << filename;
// bool res = Util::ReadFileContent(filename, &(resp._text)); // 读取文件内容
// (void)res;
}
#ifdef DEBUG
std::string httpreqstr;
sock->Recv(&httpreqstr);
std::cout << httpreqstr << std::endl;
//
HttpResponse resp;
resp._version = "HTTP/1.1";
resp._code = 200;
resp._desc = "OK";
std::string filename = webroot + homepage; // "./wwwroot/index.html" web根目录
bool res = Util::ReadFileContent(filename, &(resp._text));
(void)res;
std::string response_str = resp.Serialize();
sock->Send(response_str);
#endif
}
void Start()
{
tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
{ this->HandlerHttpRequest(sock, client); });
}
void RegisterService(std::string name, http_func_t h)
{
name = webroot + name;
auto iter = _route.find(name);
if (iter == _route.end())
{
_route.insert(std::make_pair(name, h));
}
}
~Http()
{
}
private:
std::unique_ptr<TcpServer> tsvrp;
std::unordered_map<std::string, http_func_t> _route;
};
Main.cc
cpp
#include "Http.hpp"
void Usage(std::string programName)
{
std::cout << "Usage: " << programName << " <port>" << std::endl;
}
void Login(HttpRequest &req, HttpResponse &resp)
{
LOG(LogLevel::INFO) << req.Args() << " login";
std::string text = "";
resp.SetCode(200);
resp.SetHeader("Content-Type", "text/html");
text += "<html><head><title>Login</title></head><body>";
text += "<form method='POST' action='/login'><label>Username:</label><input type='text' name='username'><br>";
text += "<label>Password:</label><input type='password' name='password'><br>";
text += "<input type='submit' value='Login'></form>";
text += "</body></html>";
resp.SetText(text);
}
void Regiter(HttpRequest &req, HttpResponse &resp)
{
LOG(LogLevel::INFO) << req.Args() << " register";
std::string text = "";
resp.SetCode(200);
resp.SetHeader("Content-Type", "text/html");
text += "<html><head><title>Register</title></head><body>";
text += "<form method='POST' action='/register'><label>Username:</label><input type='text' name='username'><br>";
text += "<label>Password:</label><input type='password' name='password'><br>";
text += "<label>Confirm Password:</label><input type='password' name='confirm_password'><br>";
text += "<input type='submit' value='Register'></form>";
text += "</body></html>";
resp.SetText(text);
}
void VipCheck(HttpRequest &req, HttpResponse &resp)
{
LOG(LogLevel::INFO) << req.Args() << " vip_check";
std::string text = "";
resp.SetCode(200);
resp.SetHeader("Content-Type", "text/html");
text += "<html><head><title>VIP Check</title></head><body>";
text += "<form method='POST' action='/vip_check'><label>Username:</label><input type='text' name='username'><br>";
text += "<label>Password:</label><input type='password' name='password'><br>";
text += "<input type='submit' value='VIP Check'></form>";
text += "</body></html>";
resp.SetText(text);
}
int main(int argc, char* argv[])
{
if (argc!= 2)
Usage(argv[0]);
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
httpsvr->RegisterService("/login", Login);
httpsvr->RegisterService("/register", Regiter);
httpsvr->RegisterService("/vip_check", VipCheck);
// httpsvr->RegisterService("/profile", Login);
httpsvr->Start();
return 0;
}