在这里插入图片描述

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!

🎬 博主简介:

在这里插入图片描述



前言:

上一篇我们从零实现了 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 等功能。下一篇我们将继续深入,实现动态资源处理,让我们的服务器能够接收用户提交的表单数据,实现登录注册等交互功能。如果你觉得本文对你有帮助,欢迎点赞、收藏、关注!有任何问题也可以在评论区留言交流。

✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐