【Linux网络】深入理解 HTTP 协议(三):静态资源服务、状态码与重定向实战
上一篇我们从零实现了 HTTP 协议的反序列化与基础响应,让浏览器能够成功访问我们的服务器并显示 “Hello World”。但一个真正的 Web 服务器远不止于此:它需要能够正确返回 HTML 页面、图片、视频等各种静态资源,能够处理用户访问不存在页面的情况,能够实现页面跳转,还需要能够接收用户提交的数据。本文将基于我们已有的模块化框架,一步步完善这些核心功能。我们会深入理解 HTTP 状态码的

🎬 博主简介:

文章目录
前言:
上一篇我们从零实现了 HTTP 协议的反序列化与基础响应,让浏览器能够成功访问我们的服务器并显示 “Hello World”。但一个真正的 Web 服务器远不止于此:它需要能够正确返回 HTML 页面、图片、视频等各种静态资源,能够处理用户访问不存在页面的情况,能够实现页面跳转,还需要能够接收用户提交的数据。本文将基于我们已有的模块化框架,一步步完善这些核心功能。我们会深入理解 HTTP 状态码的语义,掌握 Content-Type 的作用,实现优雅的 404 错误页面,并彻底搞懂重定向的底层原理。所有代码均基于 C++17 标准(并且利用工具补充了详细的注释解析),采用分层设计,可直接编译运行。
一. 完善 HTTP 响应系统与状态码机制
一个完整的 HTTP 响应不仅包含数据本身,还需要明确告诉客户端请求的处理结果。这就是 HTTP 状态码的作用。
1.1 HttpResponse 类的完整实现
我们首先完善HttpResponse类,使其能够构建符合标准的 HTTP 响应报文。
// HTTP 响应类:用于在应用层构建结构化的 HTTP 响应对象,最终将其序列化为网络字节流发给客户端
class HttpResponse
{
public:
// 构造函数:初始化 HTTP 默认版本号(如 HTTP/1.0)以及标准的空行分隔符(\r\n)
HttpResponse(): _http_version(gdefaulthttpversion), _blankline(linesep)
{}
// 核心序列化函数:将对象内存储的各个离散字段,严格按照 HTTP 协议规范拼接成一个巨大的纯文本字符串
void Serialize(std::string *outstr)
{
// 1. 拼接【状态行】格式:HTTP版本 + 空格 + 状态码 + 空格 + 状态码描述 + 换行符
std::string status_line = _http_version + gspace + _status_code + gspace + _status_code_desc + linesep;
// 2. 拼接【响应报头】格式:遍历哈希表,生成如 "Content-Length: 100\r\n" 的多行字符串
std::string response_header;
for(auto& it: _response_headerkv)
{
std::string header = it.first + headersep + it.second + linesep;
response_header += header;
}
// 3. 终极组装:状态行 + 响应报头 + 空行 (极度关键的分界线) + 响应正文
*outstr = status_line + response_header + _blankline + _body;
}
// 设置响应正文(有效载荷 Data,比如读取到的 HTML 网页代码或图片二进制流)
void SetBody(const std::string &content)
{
_body = content;
}
// 设置状态码核心接口:传入数字状态码,内部自动完成数字与标准英文描述的双重绑定
void SetCode(int code)
{
_status_code = code; // 记录状态码(注意:实际 C++ 中给 string 赋 int 会转为 ASCII 字符,若无隐式转换包装,业务层需注意传参处理)
_status_code_desc = Code2Desc(code); // 调用翻译官函数,自动匹配对应的状态短语
}
// 设置响应报头 (Headers) 的基础接口 (纯字符串 Key-Value 对)
void SetHeader(const std::string &key, const std::string &value)
{
_response_headerkv[key] = value;
}
// 设置响应报头的重载接口:为了方便外部直接传入整型 value(如 Content-Length: 1024),在此处自动完成 to_string 转换
void SetHeader(const std::string &key, int value)
{
_response_headerkv[key] = std::to_string(value);
}
// 析构函数
~HttpResponse(){}
private:
// 内部“翻译官”函数:充当字典,将传入的数字状态码映射为 HTTP 协议规定的标准原因短语 (Reason Phrase)
std::string Code2Desc(int code)
{
switch (code)
{
// 1xx: 信息响应
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing"; // WebDAV
case 103:
return "Early Hints";
// 2xx: 成功
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status"; // WebDAV
case 208:
return "Already Reported";
case 226:
return "IM Used";
// 3xx: 重定向
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 306:
return "Switch Proxy"; // 已废弃,但仍保留
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
// 4xx: 客户端错误
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot"; // 愚人节笑话,但常被实现
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Entity"; // WebDAV
case 423:
return "Locked";
case 424:
return "Failed Dependency";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
// 5xx: 服务端错误
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage"; // WebDAV
case 508:
return "Loop Detected";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
// 未知状态码
default:
return "Unknown";
}
}
private:
std::string _http_version; // HTTP 版本信息 (第一行,如 "HTTP/1.1")
std::string _status_code; // 响应状态码数字序列 (第一行,如 "200")
std::string _status_code_desc; // 响应状态描述短语 (第一行,如 "OK")
std::unordered_map<std::string,std::string> _response_headerkv; // 响应报头字典,方便动态追加和修改 Key-Value 属性
std::string _blankline; // 协议规定的空行 ("\r\n"),作为报头和正文的分界线
std::string _body; // 响应正文数据区
};
代码解读:
- 序列化函数:严格按照 HTTP 响应格式拼接四个部分:状态行、响应头、空行、响应体
- SetCode 方法:通过
Code2Desc函数自动关联状态码和对应的描述文本,避免手动输入错误 - 重载的 SetHeader 方法:同时支持字符串和整数类型的值,方便设置
Content-Length等数字类型的头字段
1.2 HTTP 状态码详解
HTTP 状态码分为 5 大类,每一类都有明确的语义:
| 类别 | 含义 | 核心特点 | 常见示例 |
|---|---|---|---|
| 1xx | 信息性状态码 | 服务器已收到请求,正在处理 | 100 Continue |
| 2xx | 成功状态码 | 请求已成功处理 | 200 OK, 201 Created |
| 3xx | 重定向状态码 | 需要客户端进一步操作 | 301 Moved Permanently, 302 Found |
| 4xx | 客户端错误状态码 | 请求有错误,服务器无法处理 | 400 Bad Request, 404 Not Found |
| 5xx | 服务器错误状态码 | 服务器处理请求时出错 | 500 Internal Server Error |


二. 静态资源服务的实现
Web 服务器最基本的功能就是返回静态资源:HTML 页面、CSS 样式表、JavaScript 脚本、图片、视频等。
2.1 二进制文件读取工具函数
静态资源可能是文本文件 (如 HTML),也可能是二进制文件 (如图片、视频)。我们必须使用二进制模式读取文件,否则会导致图片、视频等文件损坏。
private:
// 辅助函数:根据提供的文件路径,将整个文件的内容一次性完整读取到 string 内存缓冲区中
std::string GetFileContentHelper(const std::string &fileurl)
{
// 尝试打开文件输入流 (注:若此服务器需要处理图片/视频等二进制资源,未来建议显式追加 std::ios::binary 模式)
std::ifstream in(fileurl);
// 安全校验:检查文件是否真正成功打开(防范文件不存在、拼写错误或 Linux 权限不足等问题)
if(!in.is_open())
{
// 打开失败时,返回一个空的 string 作为异常标识,交由上层业务逻辑去判断和兜底(比如返回 404 页面)
return std::string();
}
// 获取文件长度 (经典的 C++ 文件光标“三步曲”操作)
// 1. 将文件内部的读取光标直接移动到文件的最末尾 (偏移量为0,基准点为 in.end)
in.seekg(0, in.end);
// 2. 询问当前光标距离文件头部的偏移量,这个返回值恰好就是整个文件的总字节数大小
int filesize = in.tellg();
// 3. 测量完毕,务必将光标重新拨回文件最开头 (in.beg),否则接下来的 read 操作会读不到任何数据
in.seekg(0, in.beg);
// 把文件内容全部读进Content
std::string content;
// 性能优化核心所在:根据刚量出的文件大小,提前为 string 申请好足量的内存空间。
// 这可以彻底避免读取大文件时,string 底层因容量不足而引发的多次动态扩容与数据搬写开销。
content.resize(filesize);
// 以二进制块的方式,一次性将 filesize 个字节的数据从磁盘暴力拷贝到 string 的底层连续内存中
in.read(content.data(), filesize); // 用data,cpp17
// 养成良好的 Linux 系统编程习惯:显式关闭文件流,防止大量并发请求耗尽操作系统的文件描述符 (fd) 资源
in.close();
// 返回装载了完整文件二进制流数据的缓冲区
return content;
}
代码解读:
- 为什么不用文本模式? :文本模式会自动转换换行符,在 Windows 和 Linux 之间会导致问题,而且会将文件中的
\0视为结束符,导致二进制文件读取不完整 - seekg 和 tellg 的组合:是获取文件大小的标准方法,比逐个读取字符高效得多
- content.data() :是 C++17 引入的,返回指向字符串内部数据的非 const 指针,比
(char*)content.c_str()更安全


2.2 Web 根目录与默认首页
我们需要定义一个 Web 根目录,所有静态资源都存放在这个目录下,防止用户通过../等方式访问服务器上的其他文件(目录遍历攻击)。
// 在HttpProtocol.hpp中定义全局常量
const std::string webroot = "wwwroot"; // Web根目录
const std::string homepage = "index.html"; // 默认首页
// 在HttpRequest::ParseLine中处理根目录请求
void ParseLine(std::string &request_line)
{
std::stringstream ss(request_line);
ss >> _method >> _uri >> _http_version;
// 处理根目录请求:自动返回默认首页
if(_uri == "/")
{
_path = webroot + _uri + homepage; // 变成 wwwroot/index.html
}
else
{
_path = webroot + _uri; // 拼接Web根目录和请求路径
}
// ... 后续处理 -- TODO
}
关键设计:
- 所有请求路径都会被限制在
wwwroot目录下,从根本上防止了目录遍历攻击 - 当用户访问根目录
/时,自动返回index.html,这是 Web 服务器的标准行为


2.3 Content-Type 与 MIME 类型映射
浏览器需要知道服务器返回的是什么类型的数据,才能正确渲染。这就是Content-Type响应头的作用,它的值是 MIME 类型。
// 根据文件后缀返回对应的MIME类型
std::string Suffix2Type(const std::string &suffix)
{
// 使用静态映射表提高效率,避免每次调用都重新创建
static const std::unordered_map<std::string, std::string> mime_map = {
// 文本类型
{".html", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".txt", "text/plain"},
// 图片类型
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
{".webp", "image/webp"},
{".svg", "image/svg+xml"},
{".ico", "image/x-icon"},
// 视频类型
{".mp4", "video/mp4"},
{".webm", "video/webm"},
// 音频类型
{".mp3", "audio/mpeg"},
{".wav", "audio/wav"},
// 应用类型
{".json", "application/json"},
{".pdf", "application/pdf"},
{".zip", "application/zip"}
};
auto it = mime_map.find(suffix);
if (it != mime_map.end())
{
return it->second;
}
// 默认返回二进制流类型
return "application/octet-stream";
}
代码解读:
- 静态 const 映射表:只在第一次调用时初始化,后续调用直接使用,性能极高
- 覆盖常见文件类型:包含了 Web 开发中最常用的文件类型
- 默认值:对于未知类型的文件,返回
application/octet-stream,浏览器会提示下载




2.4 完整的请求处理流程(HttpServer.hpp)
现在我们可以实现完整的静态资源请求处理逻辑了:
std::string HandlerHttpRequest(std::string &streamstr)
{
// 1. 反序列化HTTP请求
HttpRequest httpreq;
httpreq.Deserialize(streamstr);
// 2. 构建HTTP响应
HttpResponse httpresp;
// 3. 读取请求的文件内容
std::string filecontent = GetFileContentHelper(httpreq["path"]);
std::string suffix = httpreq["suffix"];
if(filecontent.empty())
{
// 文件不存在,返回404错误
httpresp.SetCode(404);
std::string page404 = GetFileContentHelper("wwwroot/404.html");
httpresp.SetHeader("Content-Length", page404.size());
httpresp.SetHeader("Content-Type", "text/html");
httpresp.SetBody(page404);
}
else
{
// 文件存在,返回200成功
httpresp.SetCode(200);
httpresp.SetHeader("Content-Length", filecontent.size());
httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
httpresp.SetBody(filecontent);
}
// 4. 序列化响应并返回
std::string httprespstr;
httpresp.Serialize(&httprespstr);
return httprespstr;
}




三. 重定向机制的原理与实现
重定向是 Web 开发中非常常用的功能,比如用户登录成功后跳转到首页,旧域名跳转到新域名等。
3.1 重定向的本质
重定向的本质非常简单:
- 服务器返回3xx 状态码
- 在响应头中添加Location 字段,指定重定向的目标 URL
- 浏览器收到响应后,自动向 Location 指定的 URL 发起新的请求


3.2 301 永久重定向 vs 302 临时重定向
这是面试中最常考的知识点之一,两者的核心区别在于语义和缓存行为:
| 特性 | 301 Moved Permanently | 302 Found |
|---|---|---|
| 语义 | 资源永久移动到新地址 | 资源临时移动到新地址 |
| 缓存行为 | 浏览器会永久缓存重定向结果 | 默认不缓存,每次都请求原地址 |
| SEO 影响 | 会将原网址的权重转移到新网址 | 不会转移权重 |
| 典型场景 | 网站域名变更、URL 重构 | 用户登录成功跳转、临时维护页面 |

代码示例:重定向到外部网站
// 测试重定向功能:所有请求都重定向到腾讯网
std::string HandlerHttpRequest(std::string &streamstr)
{
HttpResponse httpresp;
httpresp.SetCode(302); // 临时重定向
httpresp.SetHeader("Location", "https://www.qq.com/");
// 重定向不需要响应体
std::string httprespstr;
httpresp.Serialize(&httprespstr);
return httprespstr;
}
3.3 优雅的 404 重定向实现
我们可以将 404 错误处理改为重定向到统一的 404 页面,这样用户体验更好:
if(filecontent.empty())
{
// 文件不存在,永久重定向到404页面
httpresp.SetCode(301);
httpresp.SetHeader("Location", "/404.html"); // 注意:必须以/开头,表示根目录
}
常见坑点:相对路径 vs 绝对路径
- 错误写法 ❌️:
httpresp.SetHeader("Location", "404.html");- 当用户访问
/a/b/c.html时,会重定向到/a/b/404.html - 如果这个路径也不存在,会导致重定向循环
- 当用户访问
- 正确写法 ✅️ :
httpresp.SetHeader("Location", "/404.html");- 无论用户访问什么路径,都会重定向到根目录下的
404.html
- 无论用户访问什么路径,都会重定向到根目录下的


四. HTTP 方法与参数处理
HTTP 定义了多种请求方法,最常用的是 GET 和 POST,它们用于不同的场景。

4.1 GET 方法与查询参数
GET 方法用于从服务器获取资源,参数通过 URL 中的查询字符串传递:
http://example.com/login?username=zhangsan&password=123456
我们在HttpRequest::ParseLine中已经实现了查询参数的提取:
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); // 保留实际文件路径
}
}
4.2 POST 方法与请求体
POST 方法用于向服务器提交数据,数据放在请求体中,适合提交大量数据或敏感数据:
// 在HttpRequest::Deserialize中处理POST请求体
if(_request_headerkv.find("Content-Length") != _request_headerkv.end())
{
int len = std::stoi(_request_headerkv["Content-Length"]);
_body = streamstr.substr(0, len);
// POST请求的参数在请求体中
if(_method == "POST" || _method == "post")
{
_args = _body;
}
}
4.3 GET vs POST:面试高频考点
| 特性 | GET | POST |
|---|---|---|
| 数据位置 | URL 查询字符串 | 请求体 |
| 数据长度限制 | 受 URL 长度限制(一般 2KB 左右) | 无限制 |
| 安全性 | 参数明文显示在 URL 中,不安全 | 参数在请求体中,相对安全 |
| 幂等性 | 是(多次请求结果相同) | 否(多次提交可能创建多个资源) |
| 缓存 | 可以被浏览器缓存 | 默认不缓存 |
| 书签 | 可以收藏为书签 | 不可以 |
| 典型场景 | 获取资源、搜索、分页 | 提交表单、上传文件、登录注册 |


五. 常见 HTTP Header 实战
HTTP 头字段是 HTTP 协议的重要组成部分,它们传递了请求和响应的元数据。
5.1 重要的请求头
- Host:指定请求的主机名和端口号,是 HTTP/1.1 协议必须的头字段,支持虚拟主机(一个 IP 地址部署多个网站)
- User-Agent:客户端的浏览器和操作系统信息,服务器可以根据它返回不同的内容(如移动端和 PC 端)
- Referer:表示当前请求是从哪个页面跳转过来的,用于统计来源和防盗链
- Accept:客户端能够接受的响应内容类型,服务器可以根据它返回最合适的内容类型
5.2 重要的响应头
- Content-Length:响应体的字节长度,客户端根据它判断响应是否完整
- Content-Type:响应体的 MIME 类型,告诉浏览器如何渲染内容
- Location:与 3xx 状态码配合使用,指定重定向的目标 URL
- Server:服务器的软件信息,如
nginx/1.18.0
六. 完整项目测试
6.1 项目目录结构
HttpServer/
├── HttpProtocol.hpp # HTTP协议实现
├── HttpServer.hpp # HTTP服务器实现
├── TcpServer.hpp # TCP服务器框架
├── Socket.hpp # 套接字封装
├── InetAddr.hpp # 网络地址封装
├── Logger.hpp # 日志系统
├── Mutex.hpp # 互斥锁封装
├── Main.cpp # 主函数
├── Makefile # 编译脚本
└── wwwroot/ # Web根目录
├── index.html # 首页 -- 博客主页
├── 404.html # 404错误页面
├── Login.html # 登录页面
├── Register.html # 注册页面
├── image/ # 图片目录
│ ├── peach.jpg # 桃花图片
│ └── pomegranate.jpg # 石榴花图片
└── video/ # 视频目录
6.2 完整核心后端代码展示(前面讲过多次的那些组件封装以及前端代码就不详细展示了)
HttpServer.hpp
#ifndef __HTTPSERVER__HPP
#define __HTTPSERVER__HPP
#include <cstdint>
#include <fstream>
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
#include "Logger.hpp"
#include "HttpProtocol.hpp"
using namespace LogModule;
// HTTP 服务器应用层核心类
// 职责:作为网络底层(TcpServer)和协议层(HttpProtocol)的桥梁,负责业务流转与分发
class HttpServer
{
public:
// 构造函数:记录端口号,并实例化底层的 TCP 服务器对象
HttpServer(uint16_t port)
: _port(port)
, _tsvr(std::make_unique<TcpServer>(port))
{}
// 核心业务处理接口:将从网络层收到的纯文本转换为结构化请求,并构建对应的纯文本响应
// 入参 streamstr: 底层收到的原生 TCP 字节流
// 返回值: 序列化完毕的 HTTP 响应报文,交给底层发送给客户端
std::string HandlerHttpRequest(std::string &streamstr)
{
// 1. 检查报文完整性 -- 我们今天默认报文是完整的, 这里就不处理了
// 2. 对收到的请求进行反序列化
HttpRequest httpreq;
httpreq.Deserialize(streamstr);
// 打印基础路由和传参信息,方便后台调试
std::cout << "method: " << httpreq["method"]<< std::endl;
std::cout << "path: " << httpreq["path"]<< std::endl;
std::cout << "args: " << httpreq["args"]<< std::endl;
// 3. httpreq -> httpresp
// 准备一个空白的响应对象,接下来为其填充状态码、报头和正文
HttpResponse httpresp;
// 我们想测试重定向的话 -- 下面我们404页面的方法2其实也测试到了
// httpresp.SetCode(302);
// httpresp.SetHeader("Location", "https://www.qq.com/");
// 尝试去本地硬盘读取用户请求的物理资源文件
std::string filecontent = GetFileContentHelper(httpreq["path"]);
// 提取文件后缀,用于后续判断 MIME 类型 (Content-Type)
std::string suffix = httpreq["suffix"];
// 业务分流:判断文件是否成功读取
if(filecontent.empty())
{
// 分支 A:文件读取失败(通常是文件不存在)
// 如果为空, 我们加上404页面
// 方法一
// std::string page404 = "wwwroot/404.html";
// httpresp.SetCode(404);
// std::string file404 = GetFileContentHelper(page404);
// suffix = ".html";
// httpresp.SetHeader("Content-Length", file404.size());
// httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
// httpresp.SetBody(file404);
// 方法二: 重定向
// 通过 3xx 状态码和 Location 报头,引导浏览器去请求指定的错误页面
httpresp.SetCode(301); // 试试 301 也可以
httpresp.SetHeader("Location", "/404.html");
}
else
{
// 分支 B:文件读取成功,构建标准的 200 OK 响应
httpresp.SetCode(200);
httpresp.SetHeader("Content-Length", filecontent.size()); // 告知浏览器正文大小
httpresp.SetHeader("Content-Type", Suffix2Type(suffix)); // 告知浏览器如何渲染该文件
httpresp.SetBody(filecontent); // 装载文件数据
}
// 4. 应答进行序列化
// 将结构化的 HttpResponse 对象转换为网络可发送的纯文本字符串
std::string httprespstr;
httpresp.Serialize(&httprespstr); // 带出来
// 5. 返回
// 向上层(底层的 TcpServer 侧)交差
return httprespstr;
}
// 启动服务器的核心接口
void Run()
{
// 巧妙利用 C++11 Lambda 表达式,将当前对象的 HandlerHttpRequest 方法
// 包装并注册给底层的 TcpServer。当底层有数据读入时,会自动回调该逻辑。
_tsvr->Run([this](std::string &streamstr){
return this->HandlerHttpRequest(streamstr);
});
}
~HttpServer()
{}
private:
// 私有辅助函数:根据文件路径读取完整的二进制数据
std::string GetFileContentHelper(const std::string &fileurl)
{
std::ifstream in(fileurl);
if(!in.is_open())
{
return std::string();
}
// 获取文件长度
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
// 把文件内容全部读进Content
// 提前开辟足量内存,避免读取时频繁扩容导致的性能损耗
std::string content;
content.resize(filesize);
in.read(content.data(), filesize); // 用data,cpp17
in.close();
return content;
}
// 私有辅助函数:Content-Type 映射表
// 解决浏览器如何识别并渲染不同文件的问题
// suffix: .html
// return: text/html
std::string Suffix2Type(const std::string &suffix)
{
// 使用静态映射表提高效率,避免每次调用都重新创建
// 静态局部变量生命周期随程序,只有第一次调用时会初始化
static const std::unordered_map<std::string, std::string> mime_map = {
// 文本类型
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".mjs", "application/javascript"},
{".json", "application/json"},
{".xml", "application/xml"},
{".txt", "text/plain"},
{".csv", "text/csv"},
{".md", "text/markdown"},
// 图片类型
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
{".bmp", "image/bmp"},
{".webp", "image/webp"},
{".svg", "image/svg+xml"},
{".ico", "image/x-icon"},
{".tiff", "image/tiff"},
{".tif", "image/tiff"},
// 字体类型
{".woff", "font/woff"},
{".woff2", "font/woff2"},
{".ttf", "font/ttf"},
{".otf", "font/otf"},
// 视频类型
{".mp4", "video/mp4"},
{".webm", "video/webm"},
{".ogv", "video/ogg"},
{".avi", "video/x-msvideo"},
{".mov", "video/quicktime"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
// 音频类型
{".mp3", "audio/mpeg"},
{".wav", "audio/wav"},
{".ogg", "audio/ogg"},
{".flac", "audio/flac"},
{".m4a", "audio/mp4"},
{".aac", "audio/aac"},
// 应用类型
{".pdf", "application/pdf"},
{".zip", "application/zip"},
{".rar", "application/vnd.rar"},
{".7z", "application/x-7z-compressed"},
{".tar", "application/x-tar"},
{".gz", "application/gzip"},
{".exe", "application/vnd.microsoft.portable-executable"},
{".dll", "application/x-msdownload"},
{".msi", "application/x-msi"},
{".doc", "application/msword"},
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".wasm", "application/wasm"},
// 其他常见类型
{".jsonld", "application/ld+json"},
{".rss", "application/rss+xml"},
{".atom", "application/atom+xml"},
{".manifest", "text/cache-manifest"},
{".map", "application/json"}, // source maps
{".ts", "video/mp2t"}, // MPEG transport stream
{".m3u8", "application/vnd.apple.mpegurl"}
};
auto it = mime_map.find(suffix);
if (it != mime_map.end())
{
return it->second;
}
// 默认返回二进制流,或根据需求返回其他默认值
// 当浏览器遇到 application/octet-stream 时,通常会触发“下载文件”的动作
return "application/octet-stream";
}
private:
uint16_t _port; // 绑定的端口号
std::unique_ptr<TcpServer> _tsvr; // 底层网络模块引擎句柄
};
#endif
HttpProtocol.hpp
#ifndef __HTTPPROTOCOL__HPP
#define __HTTPPROTOCOL__HPP
#include <iostream>
#include <string>
#include <cstring>
#include <unordered_map>
#include <sstream>
#include "Logger.hpp"
using namespace LogModule;
// HTTP 协议中的各种标准分隔符与服务器默认配置
const std::string linesep = "\r\n"; // HTTP 报文的标准换行符
const std::string headersep = ": "; // 请求/响应报头中 Key 与 Value 的分隔符
const std::string webroot = "wwwroot"; // 服务器的 Web 根目录(安全沙箱)
const std::string homepage = "index.html"; // 默认首页文件名
const std::string gdefaulthttpversion = "HTTP/1.0"; // 默认 HTTP 版本
const std::string gspace = " "; // 请求行/状态行中的空格分隔符
const std::string suffixsep = "."; // 文件后缀提取的边界符
const std::string argsep = "?"; // GET 方法 URI 传参的分界符
// HTTP 请求类:负责将网络中接收到的杂乱字符串(序列化流),精准拆解并提取为结构化的 C++ 数据(反序列化)
class HttpRequest
{
private:
// 核心字符串处理工具:“吃甘蔗”法则读取单行
// 从传入的 streamstr 中提取一行放入 line,并在原串中将该行连同 \r\n 一并抹除
int ReadOneLine(std::string &streamstr, std::string *line)
{
// 我们先去找 linesep
auto pos = streamstr.find(linesep);
if(pos == std::string::npos)
return -1; // 出现了问题 (没找到换行符,说明这行数据还不完整)
// 走到这里就读到了一行 -- 我们需要处理一下
// a. 先拿到这行 (前闭后开区间,正好避开 \r\n)
*line = streamstr.substr(0, pos);
// b. 删掉原始字符串中的这一行, 加上分隔符号 (让长字符串的开头永远是下一行)
streamstr = streamstr.erase(0, pos + linesep.size());
// n == 0 证明读到了空行 (即这一行只有 \r\n,截取出来的 line 是空的)
return line->size();
}
// 解析请求行 (Request Line):提取 Method, URI, Version,并完成本地路径映射与 GET 参数剥离
void ParseLine(std::string &request_line)
{
// 利用 stringstream 按空格优雅打散请求行
std::stringstream ss(request_line);
ss >> _method >> _uri >> _http_version;
// 1. URI 本地化映射 (拼接 webroot)
if(_uri == "/")
{
_path = webroot + _uri + homepage; // 默认首页补全
}
else
{
_path = webroot + _uri; // 正常拼接
}
// 2. 剥离 GET 请求的参数 (解决 /content.html?username=xx 导致的 404 问题)
// /content.html?username=zhangsan&passwd=123456
if(_method == "GET" || _method == "get")
{
auto pos = _path.find(argsep);
if(pos != std::string::npos)
{
// 将 ? 右边的内容截取给 _args
_args = _path.substr(pos + argsep.size());
// 将 ? 以及右边的内容从 _path 中砍掉,还原纯净的文件路径
_path = _path.substr(0, pos);
}
}
// 3. 提取文件后缀 (为后续判断 Content-Type 奠定基础)
// 解析后缀
// 分析请求的资源的后缀! // wwwroot + / + index.html wwwroot/image/th.jpg wwwroot/css/XXX.css
auto pos = _path.rfind(suffixsep); // 注意是 rfind,从右往左找,防止出现 a.b.tar.gz 这样的坑
if(pos == std::string::npos)
{
_suffix = ".html"; // 如果没后缀,默认当 html 处理
}
else
{
_suffix = _path.substr(pos); // .css .jpg ....
}
std::cout << "=======: PATH: " << _path << std::endl;
}
// 报头键值对拆解工具:按 ": " 切割
void SplitString(std::string &line, std::string *key, std::string *value, const std::string sep = headersep)
{
// a. 先找分隔符号
auto pos = line.find(sep);
if(pos == std::string::npos)
return;
// 冒号前面是 Key,冒号加空格后面是 Value
*key = line.substr(0, pos);
*value = line.substr(pos + sep.size());
}
// 后台调试打印函数,观察反序列化的成果
void PrintDebug()
{
// 打印进行观察
std::cout << "reqline:" << _method << "#" << _uri << "#" << _http_version << std::endl;
for (auto &item : _request_headerkv)
{
std::cout << item.first << "->" << item.second << std::endl;
}
std::cout << "blankline: " << _blankline << std::endl;
std::cout << "body: " << _body << std::endl;
std::cout << "suffix: " << _suffix << std::endl;
}
public:
HttpRequest(){}
// 核心反序列化引擎:将一整块字符串,按 HTTP 报文四部分结构(请求行、报头、空行、正文)有条不紊地拆解
void Deserialize(std::string &streamstr)
{
// 1. 我们先读取第一行 -- 请求行
std::string request_line;
// streamstr给的引用, 是因为里面要找到一行就删掉这一行
// request_line给的是指针, 我们可以通过这个拿到读取到的
int n = ReadOneLine(streamstr, &request_line);
// 可以进行简单的判断,我这里就不处理了
(void)n;
// 2. 解析请求行 (完成拆分与本地路径运算)
ParseLine(request_line);
// 3. 继续去解析其他行 (循环读取报头,直到遭遇空行)
n = 0;
do
{
std::string line;
n = ReadOneLine(streamstr, &line);
if(n > 0)
{
// 这里就需要处理: 读到了报头
std::string key, value;
SplitString(line, &key, &value, headersep);
// 这里判断一下
if(!key.empty() && !value.empty())
{
_request_headerkv[key] = value; // 存进去 (装入哈希表,方便后续按 O(1) 复杂度查找)
}
}
else if(n < 0)
{
LOG(LogLevel::DEBUG) << "ReadOneLine, bug?";
break;
}
else
{
// n == 0,极度关键的分界线:读到了空行!说明报头结束了
_blankline = "\r\n";
break; // 跳出循环,剩下的全都是正文了
}
}while (n > 0);
// 4. 读取正文(有效载荷)并处理 POST 参数
if(_request_headerkv.find("Content-Length") != _request_headerkv.end())
{
int len = std::stoi(_request_headerkv["Content-Length"]); // 去哈希表里面找这个属性看看有没有 -- 判断有没有正文
_body = streamstr.substr(0, len); // 可以这样找, 是因为前面我们都是找一行删一行
// 如果是 POST 请求,它的正文本身就是前端表单传来的参数数据
if(_method == "POST" || _method == "post")
{
_args = _body;
}
}
PrintDebug();
}
// 极其优雅的接口封装:重载 [] 运算符,让外部业务代码(如路由处理函数)可以像查字典一样随意获取请求信息
// 方便外部可以获取我请求里面的东西
std::string operator[](const std::string& key) const
{
if(key == "method")
return _method;
else if(key == "uri")
return _uri;
else if(key == "httpversion")
return _http_version;
else if(key == "body")
return _body;
else if(key == "path")
return _path;
else if(key == "suffix")
return _suffix;
else if(key == "args")
return _args;
else
{
// 基础属性查不到,就去报头哈希表里找(例如找 "User-Agent")
auto iter = _request_headerkv.find(key);
if(iter != _request_headerkv.end())
return iter->second;
}
// 再就是直接返回空串了
return std::string();
}
~HttpRequest(){}
private:
std::string _method; // 请求方法 (GET, POST等)
std::string _uri; // URL -> a/b/c/index.html (浏览器发来的原始路径)
std::string _http_version; // HTTP 版本号
std::unordered_map<std::string,std::string> _request_headerkv; // 报头字典
std::string _blankline; // 报文分界线
std::string _body; // 报文正文
private:
std::string _path; // 映射后的本地 Linux 文件系统路径 (wwwroot/...)
std::string _suffix; // 后缀 (如 .html, .jpg)
std::string _args; // 提参 (整合了 GET/POST 的纯净参数字符串)
};
// HTTP 响应类:用于在应用层构建结构化的应答信息,并最终序列化发给浏览器
class HttpResponse
{
public:
HttpResponse(): _http_version(gdefaulthttpversion), _blankline(linesep)
{}
// 将本对象内的所有成员,按照 HTTP 响应的严格格式,拼接为一个长字符串
void Serialize(std::string *outstr)
{
// 拼接状态行 (利用 std::to_string 解决状态码为 int 类型的转换问题)
std::string status_line = _http_version + gspace + std::to_string(_status_code) + gspace + _status_code_desc + linesep;
std::string response_header;
// 拼接报头多行字符串
for(auto& it: _response_headerkv)
{
std::string header = it.first + headersep + it.second + linesep;
response_header += header;
}
// 组合 4 大部分
*outstr = status_line + response_header + _blankline + _body;
}
void SetBody(const std::string &content)
{
_body = content;
}
void SetCode(int code)
{
_status_code = code;
// 数字状态码与对应英文短语的自动绑定
_status_code_desc = Code2Desc(code);
}
void SetHeader(const std::string &key, const std::string &value)
{
_response_headerkv[key] = value;
}
// 重载 SetHeader:方便直接设置 Content-Length 这种数字类型的报头
void SetHeader(const std::string &key, int value)
{
_response_headerkv[key] = std::to_string(value);
}
~HttpResponse(){}
private:
// 巨型字典:涵盖 HTTP/1.1 全套状态码规范
std::string Code2Desc(int code)
{
switch (code)
{
// 1xx: 信息响应
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing"; // WebDAV
case 103:
return "Early Hints";
// 2xx: 成功
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status"; // WebDAV
case 208:
return "Already Reported";
case 226:
return "IM Used";
// 3xx: 重定向
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 306:
return "Switch Proxy"; // 已废弃,但仍保留
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
// 4xx: 客户端错误
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot"; // 愚人节笑话,但常被实现
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Entity"; // WebDAV
case 423:
return "Locked";
case 424:
return "Failed Dependency";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
// 5xx: 服务端错误
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage"; // WebDAV
case 508:
return "Loop Detected";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
// 未知状态码
default:
return "Unknown";
}
}
private:
std::string _http_version; // HTTP 版本
int _status_code; // 状态码整数型 (如 200, 404)
std::string _status_code_desc; // 状态码描述 (如 "OK", "Not Found")
std::unordered_map<std::string,std::string> _response_headerkv; // 报头字典
std::string _blankline; // 空行
std::string _body; // 正文
};
#endif
Main.cpp
#include "HttpServer.hpp"
#include <memory>
// 帮助手册函数:当用户在 Linux 命令行输入的启动参数有误时,提示正确的操作姿势
// 参数 procname: 当前运行的程序名称(通常是 argv[0])
void Usage(std::string procname)
{
std::cout << "Usage: " << procname << " ServerPort" << std::endl;
}
// 主函数:程序的入口,负责解析命令行参数、初始化环境并拉起服务器进程
int main(int argc, char *argv[])
{
// 1. 命令行参数校验
// 期望的正确运行方式例如: ./httpserver 8080
// 此时 argc 必须为 2(argv[0]是"./httpserver",argv[1]是"8080")
if (argc != 2)
{
// 若参数数量不对,打印使用说明并异常退出程序 (exit code 1)
Usage(argv[0]);
exit(1);
}
// 2. 全局环境与配置初始化
// 启用控制台日志策略(宏定义),确保后续代码中的 DEBUG/INFO/FATAL 等日志能直接打印到终端,方便调试
ENABLE_CONSOLE_LOG_STRATEGY();
// 将命令行传入的端口参数(纯字符串格式,如 "8080")转换为 16 位无符号整数 (uint16_t)
uint16_t port = std::stoi(argv[1]);
// 3. 服务器实例化与启动
// 我们现在这里不用lambda了,在HttpServer中实现了
// 采用 C++14 的 std::make_unique 结合智能指针来管理服务器对象。
// 优势:将堆内存的生命周期交给栈上的对象管理,无论后续程序如何运行,都能保证绝对的内存安全,防止内存泄露。
std::unique_ptr<HttpServer> hsvr = std::make_unique<HttpServer>(port);
// 启动服务器的核心循环(底层的 TcpServer 将开始 bind、listen 并死循环 accept 新连接)
hsvr->Run();
// 正常情况下,服务器作为常驻进程会一直阻塞在 Run() 中,直到收到 `ctrl+c` 或 kill 信号才会退出
return 0;
}
前端代码有需要的可以私信我,我发给你
6.3 测试场景
- 访问首页:在浏览器中输入
http://你的服务器IP:8080,应该显示index.html的内容,并且我们点击页面底部的可以跳转到对应界面,这里以合作方为例


- 访问图片:输入
http://你的服务器IP:8080/image/th.jpg,应该显示桃花图片。当然视频也可以,下来可以自己试试,顺便补充一下关于图床和CDN



- 访问不存在的页面:输入
http://你的服务器IP:8080/nonexist.html,应该重定向到 404 页面

- 测试重定向:取消重定向代码的注释,访问任何页面都会跳转到腾讯网,这个就不测试了,大家可以自己试试。
这个太长了,我就不放全部了

核心考点提炼
- HTTP 响应格式:状态行 + 响应头 + 空行 + 响应体
- 状态码分类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务器错误
- 301 vs 302:永久重定向 vs 临时重定向,缓存行为和 SEO 影响的区别
- GET vs POST:数据位置、安全性、幂等性、缓存等方面的区别
- Content-Type:MIME 类型的作用,常见文件类型对应的 MIME 值重定向原理:3xx 状态码 + Location 头,浏览器自动跳转
- 静态资源服务:Web 根目录的作用,二进制文件读取,防止目录遍历攻击
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:到这里,我们已经实现了一个能够处理静态资源、错误页面和重定向的 Web 服务器。但这只是 Web 开发的开始,一个完整的 Web 服务器还需要支持动态内容生成、Cookie 与 Session 管理、长连接、HTTPS 等功能。下一篇我们将继续深入,实现动态资源处理,让我们的服务器能够接收用户提交的表单数据,实现登录注册等交互功能。如果你觉得本文对你有帮助,欢迎点赞、收藏、关注!有任何问题也可以在评论区留言交流。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)