在这里插入图片描述

🔥草莓熊Lotso:个人主页

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

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

🎬 博主简介:

在这里插入图片描述



前言:

HTTP 协议作为 Web 世界的基石,支撑着我们每天浏览的每一个网页、调用的每一个 API。虽然 Nginx、Apache 等成熟服务器已经足够强大,但亲手实现一个 HTTP 服务器,能让我们穿透抽象层,真正理解 “浏览器输入 URL 按下回车” 背后发生的一切。本文将从 HTTP 协议最核心的 GET/POST 请求讲起,一步步拆解 HTTP 服务器的分层架构,实现一个支持静态资源托管、动态路由注册、完整协议解析的 C++ HTTP 服务器。然后我们会深入讲解Nginx 生产级部署的最佳实践,最后对比自研实现与业界主流库 cpp-httplib 的差异,让你既能懂原理,又能快速落地。


一. HTTP 协议核心:GET 与 POST 的本质区别

1.1 表单提交:HTTP 请求的起点

所有 Web 交互的本质都是浏览器构造 HTTP 请求发送给服务器,服务器处理后返回响应。最常见的交互方式就是 HTML 表单:

<form action="/login" method="GET">
  用户名:<input type="text" name="username"><br><br>
  密码:<input type="password" name="passwd"><br><br>
  <input type="submit" value="登录">
</form>

这里有两个关键属性

  • action:指定数据提交的目标 URL
  • method:指定 HTTP 请求方法

1.2 GET 与 POST:参数传递的两种方式

这是 HTTP 最基础也最容易混淆的知识点,我们从报文结构层面彻底搞清楚:

特性 GET 方法 POST 方法
参数位置 拼接在 URL 中,格式为 ?key1=value1&key2=value2 放在 HTTP 请求体中
可见性 参数直接显示在浏览器地址栏 参数不可见(但抓包仍可获取)
长度限制 受浏览器 URL 长度限制(通常 2KB 左右) 理论上无限制,受服务器配置影响
缓存 可被浏览器缓存 默认不缓存
适用场景 获取资源、搜索、分页等无副作用操作 登录、注册、提交数据等有副作用操作

报文示例对比

// 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 方法详解

HTTP/1.1 协议共定义了 8 种请求方法,除了最常用的 GET 和 POST,还有以下 6 种:

方法 说明 支持的 HTTP 版本 生产环境状态
PUT 传输文件,将请求体中的内容保存到指定 URL 1.0、1.1 通常禁用,防止用户随意上传文件
HEAD 与 GET 类似,但只返回响应头,不返回响应体 1.0、1.1 常用,用于检查资源是否存在、获取资源大小
DELETE 删除指定 URL 的资源 1.0、1.1 通常禁用,防止用户随意删除文件
OPTIONS 查询服务器对指定资源支持的 HTTP 方法 1.1 通常禁用,防止攻击者探测服务器信息
TRACE 追踪请求 - 响应的传输路径,用于调试 1.1 几乎不用,存在安全风险
CONNECT 要求用隧道协议连接代理,用于 HTTPS 1.1 仅代理服务器使用
LINK/UNLINK 建立 / 断开资源之间的链接关系 1.0 已废弃,几乎不使用

关于这里更详细的解析可以看,包括后面对常见Header的补充也有

应用层协议:HTTP协议(四 – 1)
应用层协议:HTTP协议(四 – 2)

各方法实战演示

# 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 服务器应该采用分层架构,每一层只负责单一职责:

┌─────────────────────────────────┐
│        业务逻辑层               │
│  (登录、注册、搜索等具体服务)   │
├─────────────────────────────────┤
│        HTTP协议层               │
│  (请求解析、响应构造、路由分发) │
├─────────────────────────────────┤
│        TCP传输层                │
│  (连接管理、数据收发)           │
└─────────────────────────────────┘

2.1 底层:TCP 服务器封装(简版)

我们先封装一个基础的 TCP 服务器,负责处理连接建立和数据收发,这个更详细的封装实现,我们在前面讲过,这里是个简单版的,实际使用的是之前那个

// TcpServer.hpp 核心代码
using handler_t = std::function<std::string(std::string &)>;

class TcpServer 
{
public:
    TcpServer(int port) : _port(port), _listensockfd(std::make_unique<TcpSocket>()) 
    {
        _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<Socket> 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<Socket> _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<std::string, std::string> 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<void(const HttpRequest &req, HttpResponse &resp)>;

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<std::string, route_t> _route; // 路由表
};

四. 实战:搭建一个完整的 Web 服务(附加所有完整代码,前面不想看的可以直接看这里)

现在我们可以用自己写的 HTTP 服务器搭建一个包含登录、注册功能的 Web 服务,声明一下,我们这里就不展示其中我们封装好的组件代码了,这些之前都有讲过,前端代码这里也不展示了。

  • Main.cpp
#include "HttpServer.hpp"
#include <memory>

// @brief 打印程序的使用说明
// @param procname 当前运行的程序名称(通常是 argv[0])
void Usage(std::string procname)
{
    // 提示用户运行程序时需要附带端口号参数
    std::cout << "Usage: " << procname << " ServerPort" << std::endl;
}

// =========================================================
// 以下是具体的业务逻辑处理函数 (Callback Handlers)
// 当底层的 HttpServer 匹配到对应的路由路径时,会回调这些函数
// =========================================================

// @brief 处理登录请求的业务函数
void Login(const HttpRequest &req, HttpResponse &resp)
{
    // 1. 从 HTTP 请求对象中获取由前端传递过来的参数信息 (通常在 URL 或 Body 中解析得出)
    std::string args = req["args"];
    std::cout << "Login service: haha: " << args  << std::endl;

    // 2. 准备要返回给客户端的 HTTP 响应体数据
    std::string body = "Login success!\n";
    
    // 3. 构建 HTTP 响应 (HttpResponse)
    resp.SetCode(200);                                  // 设置 HTTP 状态码为 200 OK,表示请求成功
    resp.SetHeader("Content-Type", "text/plain");       // 告诉客户端返回的内容类型是纯文本
    resp.SetHeader("Content-Length", body.size());      // 设置响应体的长度,这是 HTTP 协议标准要求
    resp.SetHeader("Connection", "close");              // 告知客户端本次交互结束后关闭 TCP 连接 (短连接模式)
    resp.SetBody(body);                                 // 将具体数据写入响应体
}

// @brief 处理注册请求的业务函数
void Register(const HttpRequest &req, HttpResponse &resp)
{
    // 提取请求参数并打印日志,便于服务端调试
    std::string args = req["args"];
    std::cout << "-----> Register service, args: " << args << std::endl;

    // 注册通常代表在服务端创建了新资源,按 RESTful 规范返回 201 Created 状态码
    resp.SetCode(201);
}

// @brief 处理搜索请求的业务函数
void Search(const HttpRequest &req, HttpResponse &resp)
{
    std::string args = req["args"];
    std::cout << "-----> Search service, args: " << args << std::endl;
    // 提示:此处目前仅做了日志打印,实际开发中需要补充构建 HttpResponse 的逻辑 (例如查询数据库并返回结果)
}

// @brief 处理调用大模型请求的业务函数
void CallBigModel(const HttpRequest &req, HttpResponse &resp)
{
    std::string args = req["args"];
    std::cout << "-----> CallBigModel service, args: " << args << std::endl;
    // 提示:此处实际开发中可能会涉及网络请求其他大模型 API,并处理异步/等待逻辑
}

// =========================================================
// 主函数:服务器程序的入口点
// 负责参数校验、环境初始化、路由注册以及启动服务
// =========================================================
int main(int argc, char *argv[])
{
    // 1. 命令行参数校验:强制要求运行时必须提供端口号
    if (argc != 2)
    {
        Usage(argv[0]); // 如果参数不对,打印正确用法并退出程序
        exit(1);
    }

    // 2. 初始化环境
    ENABLE_CONSOLE_LOG_STRATEGY();      // 宏调用:启用向控制台输出日志的策略
    uint16_t port = std::stoi(argv[1]); // 将命令行传入的端口号字符串转换为 uint16_t 整数

    // 3. 实例化 HTTP 服务器
    // 使用 std::unique_ptr 管理 HttpServer 对象的生命周期,防止内存泄漏
    // 我们现在这里不用lambda了,在HttpServer中实现了
    std::unique_ptr<HttpServer> hsvr = std::make_unique<HttpServer>(port);
    
    // 4. 路由注册 (URL Mapping)
    // 将特定的 URI 路径与上面定义的业务逻辑函数绑定起来
    // 当客户端请求这些路径时,HttpServer 内部会自动调用对应的函数
    hsvr->Register("/app/login", Login);
    hsvr->Register("/app/register", Register);
    hsvr->Register("/app/search", Search);
    hsvr->Register("/app/model", CallBigModel);
    
    // 5. 启动服务器,进入事件循环,开始监听和处理网络请求
    hsvr->Run();

    return 0;
}
  • HttpServer.hpp
#ifndef __HTTPSERVER__HPP
#define __HTTPSERVER__HPP

#include <cstdint>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include "TcpServer.hpp"
#include "Logger.hpp"
#include "HttpProtocol.hpp"

using namespace LogModule;

// @brief 路由回调函数类型定义
// 使用 std::function 定义了一个统一的接口,任何业务逻辑(如 Login、Search 等)
// 只要满足参数是 (HttpRequest, HttpResponse),就可以被注册到路由表中
using route_t = std::function<void(const HttpRequest &req, HttpResponse &resp)>;

class HttpServer
{
public:
    // @brief 构造函数,初始化 HTTP 服务器
    // 在这里实例化了底层的 TcpServer,HTTP 服务完全依赖这个 TCP 实例进行网络数据的收发
    HttpServer(uint16_t port)
        : _port(port)
        , _tsvr(std::make_unique<TcpServer>(port))
    {}

    // @brief 核心处理器:将 TCP 收到的纯字节流,转化为 HTTP 请求,并生成响应字节流
    std::string HandlerHttpRequest(std::string &streamstr)
    {
        // 1. 检查报文完整性 -- 我们今天默认报文是完整的, 这里就不处理了
        // 2. 对收到的请求进行反序列化
        HttpRequest httpreq;
        // @brief 反序列化:将网络中收到的扁平字符串(streamstr),解析成有结构的 HttpRequest 对象
        // 这样后续就可以通过键值对的方式方便地获取 method、path 等信息
        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/");
        
        // 处理动态资源
        // @brief 路由分发:如果客户端请求的路径(path)在我们的注册表(_route)中存在,说明这是一个后端业务接口
        if (IsNeedRoute(httpreq["path"])) 
        {
            // 直接调用对应的业务回调函数(例如上一份代码里的 Login/Search),将控制权交给上层
            _route[httpreq["path"]](httpreq, httpresp);
        } 
        else 
        {
          // 处理静态资源
          // @brief 如果不在路由表中,说明客户端只是想要请求一个普通的网页文件、图片或 CSS 等
          std::string filecontent = GetFileContentHelper(httpreq["path"]);
          std::string suffix = httpreq["suffix"];
          
          if (filecontent.empty()) {
            // 如果为空, 返回404页面
            // @brief 找不到文件时的异常处理(Not Found)
            std::string page404 = "wwwroot/404.html";
            httpresp.SetCode(404);
            std::string file404 = GetFileContentHelper(page404);
            suffix = ".html";
            
            // 构建 404 响应的 Header 报头
            httpresp.SetHeader("Content-Length", file404.size());
            httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
            httpresp.SetHeader("Connection", "close");
            httpresp.SetBody(file404);
          } 
          else 
          {
            // @brief 文件成功找到,构建标准的 200 OK 响应
            httpresp.SetCode(200);
            httpresp.SetHeader("Content-Length", filecontent.size());
            httpresp.SetHeader("Content-Type", Suffix2Type(suffix)); // 根据文件后缀自动推导 MIME 类型
            httpresp.SetHeader("Connection", "close"); // 应答告诉浏览器我是短链接
            httpresp.SetBody(filecontent);
          }
        }

        // 4. 应答进行序列化
        std::string httprespstr;
        // @brief 序列化:将我们刚刚填充好的 HttpResponse 结构体对象,重新打包成符合 HTTP 标准规范的纯字符串格式
        httpresp.Serialize(&httprespstr); // 带出来

        // 5. 返回
        // @brief 将序列化后的字符串返回,这串数据最终会被底层的 TcpServer 发送给客户端(浏览器)
        return httprespstr;
    }

    // @brief 启动服务器
    void Run()
    {
        // @brief 经典的回调闭包:TcpServer 收到数据后,会调用传入的这个 lambda 表达式。
        // 而这个 lambda 内部又调用了本类的 HandlerHttpRequest 方法,实现了 TCP 层到 HTTP 层的联动。
        _tsvr->Run([this](std::string &streamstr){
            return this->HandlerHttpRequest(streamstr);
        });
    }

    // @brief 路由注册接口,供 Main 函数调用,用于挂载后端业务功能
    void Register(std::string uri, route_t handler)
    {
        // @note 注意:这里的 webroot 变量需要在前面有定义(可能是全局或宏),用于拼接完整的映射路径
        std::string key = webroot + uri;
        _route[key] = handler;
    }
    
    ~HttpServer()
    {}
    
private:
    // @brief 检查当前请求路径是否需要走动态路由逻辑
    bool IsNeedRoute(const std::string &key)
    {
        return _route.find(key) != _route.end();
    }
    
    // @brief 辅助函数:根据文件路径读取整个文件的内容到字符串中
    std::string GetFileContentHelper(const std::string &fileurl)
    {
        std::ifstream in(fileurl);
        if(!in.is_open())
        {
            return std::string();
        }
        
        // 获取文件长度
        // @brief C++ 常见的文件大小测算方式:将文件读写指针移动到末尾(end),获取当前偏移量即为大小,再移回头部(beg)
        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 (C++17 允许直接往 string 的 data 指针写入数据)
        in.close();

        return content;
    }

    // suffix: .html
    // return: text/html
    // @brief MIME 类型映射转换函数,用于告诉浏览器返回的正文内容到底是什么类型的数据
    std::string Suffix2Type(const std::string &suffix)
    {
        // 使用静态映射表提高效率,避免每次调用都重新创建
        // @brief static 关键字确保这巨大的哈希表在整个程序运行期间只会被初始化一次,极大地节省了性能开销
        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;
        }

        // 默认返回二进制流,或根据需求返回其他默认值
        // @brief 当遇到不认识的文件后缀时,作为“通用二进制数据”返回,此时浏览器通常会直接触发下载行为
        return "application/octet-stream";
    }
private:
    uint16_t _port;                                  // 服务器绑定的端口
    std::unique_ptr<TcpServer> _tsvr;                // 独占底层的 TCP 服务器指针,负责真正的网络通信
    std::unordered_map<std::string, route_t> _route; // 注册服务 (哈希表:实现 URI 到回调函数的映射)
};


#endif
  • HttpProtocol.hpp
#ifndef __HTTPPROTOCOL__HPP
#define __HTTPPROTOCOL__HPP
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <sstream>
#include "Logger.hpp"

using namespace LogModule;

// =========================================================
// @brief 定义 HTTP 协议中常用的一些标准分隔符和默认常量
// =========================================================
const std::string linesep = "\r\n";         // HTTP 报文的标准换行符 (CRLF)
const std::string headersep = ": ";         // HTTP 报头中键值对的分隔符
const std::string webroot = "wwwroot";      // Web 服务器的根目录,存放 HTML/CSS/图片等静态资源
const std::string homepage = "index.html";  // 默认的主页文件名 (访问 / 时自动替换)
const std::string gdefaulthttpversion = "HTTP/1.0"; // 默认响应的 HTTP 版本
const std::string gspace = " ";             // 状态行/请求行中的空格分隔符
const std::string suffixsep = ".";          // 文件后缀的分隔符 (用于后续推导 Content-Type)
const std::string argsep = "?";             // URL 中携带 GET 传参的分隔符

// =========================================================
// @brief HTTP 请求类:负责将网络收到的原始字节流,反序列化解析成结构化数据
// =========================================================
class HttpRequest
{
private:
    // @brief 工具函数:从网络数据流中提取出一整行
    // @param streamstr 原始的网络数据流 (传引用,提取完一行后会从原始流中把该行删掉)
    // @param 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. 先拿到这行
        *line = streamstr.substr(0, pos);
        // b. 删掉原始字符串中的这一行, 加上分隔符号
        streamstr = streamstr.erase(0, pos + linesep.size());

        // n == 0 证明读到了空行
        return line->size();
    }

    // @brief 解析 HTTP 请求行 (即报文的第一行),例如: "GET /index.html?name=zhangsan HTTP/1.1"
    void ParseLine(std::string &request_line)
    {
        // 使用 stringstream 按空格自动拆分出 方法、URI 和 版本号
        std::stringstream ss(request_line);
        ss >> _method >> _uri >> _http_version;

        // @brief 路径映射逻辑:将前端请求的 URI 映射到服务器本地磁盘的真实路径
        if(_uri == "/")
        {
            _path = webroot + _uri + homepage; // 如果只访问 /,默认拼接出 wwwroot/index.html
        }
        else
        {
            _path = webroot + _uri;
        }

        // /content.html?username=zhangsan&passwd=123456
        // @brief GET 方法参数提取:GET 的参数拼接在 URI 里面,以 ? 作为分隔
        if(_method == "GET" || _method == "get")
        {
            auto pos = _path.find(argsep);
            if(pos != std::string::npos)
            {
                _args = _path.substr(pos + argsep.size()); // 提取 ? 后面的所有内容作为参数
                _path = _path.substr(0, pos);              // 截断 ? 之前的内容,保留纯净的文件路径
            }
        }

        // 解析后缀
        // 分析请求的资源的后缀! // wwwroot + / + index.html wwwroot/image/th.jpg wwwroot/css/XXX.css
        auto pos = _path.rfind(suffixsep); // 从右向左找点号 "."
        if(pos == std::string::npos)
        {
            _suffix = ".html"; // 如果没找到后缀,默认当成 html 网页处理
        }
        else 
        {
            _suffix = _path.substr(pos); // .css .jpg ....
        }

        std::cout << "=======: PATH: " << _path << std::endl;
    }

    // @brief 将 HTTP 报头的一行按 ": " 拆解为 key 和 value (例如 "Content-Length: 100")
    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 = 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(){}

    // @brief 核心方法:反序列化
    // 严格按照 HTTP 报文格式解析: 1.请求行 -> 2.请求报头 -> 3.空行 -> 4.请求正文
    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. 继续去解析其他行 (循环读取 HTTP 请求头,直到遇到空行)
        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; // 存进去 (存入哈希表方便后续按 Key 查找)
                }
            }
            else if(n < 0)
            {
                LOG(LogLevel::DEBUG) << "ReadOneLine, bug?";
                break; 
            }
            else
            {
                // @brief 读到长度为 0 的行,说明碰到了 HTTP 协议规定的空行(\r\n)
                // 空行意味着 Header 结束,后面就是 Body(如果有的话)
                _blankline = "\r\n";
                break;
            }
        }while (n > 0);

         // 4. 判断并提取请求正文 (Body)
         if(_request_headerkv.find("Content-Length") != _request_headerkv.end())
        {
            int len = std::stoi(_request_headerkv["Content-Length"]); // 去哈希表里面找这个属性看看有没有 -- 判断有没有正文
            _body = streamstr.substr(0, len); // 可以这样找, 是因为前面我们都是找一行删一行
            
            // @brief 如果是 POST 方法,说明数据放在了正文里,将其提取到 _args 变量中统一处理
            if(_method == "POST" || _method == "post")
            {
                _args = _body;
            }
        }

        PrintDebug();
    }
    
    // 方便外部可以获取我请求里面的东西
    // @brief 运算符重载,提供一个统一、优雅的接口给外界获取各种请求参数
    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
            // 如果不是基础属性,则去 Header 的哈希表里找 (例如提取 User-Agent, Host 等)
        {
            auto iter = _request_headerkv.find(key);
            if(iter != _request_headerkv.end())
                return iter->second;
        }

        // 再就是直接返回空串了
        return std::string();
    }
    ~HttpRequest(){}
private:
    std::string _method;
    std::string _uri; // URL -> a/b/c/index.html
    std::string _http_version;
    std::unordered_map<std::string,std::string> _request_headerkv; // 存放 Headers 的哈希表
    std::string _blankline; // 协议规定的空行
    std::string _body;      // 报文正文
private:
    std::string _path;   // 映射到服务端的真实文件路径
    std::string _suffix; // 后缀
    std::string _args;   // 提参 (无论是 GET 还是 POST 的参数最终都提取到这里)
};

// =========================================================
// @brief HTTP 响应类:提供接口供业务代码设置返回内容,并负责最终序列化成字符串发往网络
// =========================================================
class HttpResponse
{
public:
    // 构造时默认使用 HTTP/1.0 并且设置好标准的空行
    HttpResponse(): _http_version(gdefaulthttpversion), _blankline(linesep)
    {}

    // @brief 核心方法:序列化
    // 严格按照 HTTP 响应报文格式拼接: 1.状态行 -> 2.响应头 -> 3.空行 -> 4.响应正文
    void Serialize(std::string *outstr)
    {
        // 1. 拼接状态行 (例如: "HTTP/1.0 200 OK\r\n")
        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)
        {
            std::string header = it.first + headersep + it.second + linesep;
            response_header += header;
        }
        
        // 3. 最终整体拼接 (带上空行和正文)
        *outstr = status_line + response_header + _blankline + _body;
    }
    
    // @brief 设置响应正文内容
    void SetBody(const std::string &content)
    {
        _body = content;
    }
    
    // @brief 设置 HTTP 状态码,并自动映射其对应的英文状态描述
    void SetCode(int code)
    {
        _status_code = code;
        _status_code_desc = Code2Desc(code);
    }

    // @brief 设置响应 Header (字符串值)
    void SetHeader(const std::string &key, const std::string &value)
    {
        _response_headerkv[key] = value;
    }
    
    // @brief 设置响应 Header (整型值重载,例如用于设置 Content-Length: 1024)
    void SetHeader(const std::string &key, int value)
    {
        _response_headerkv[key] = std::to_string(value);
    }
    ~HttpResponse(){}
private:
    // @brief HTTP 状态码字典:根据状态码返回对应的英文标准描述短语
    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;
    int _status_code;
    std::string _status_code_desc;
    std::unordered_map<std::string,std::string> _response_headerkv;
    std::string _blankline;
    std::string _body;
};
#endif

五. 进阶:HTTP 常见 Header 全解析

HTTP 头是客户端和服务器之间传递元数据的重要方式,分为请求头响应头两大类。以下是生产环境中最常用的 10 个 Header:

5.1 通用请求头

Header 作用 示例 应用场景
Host 指定请求的主机和端口,用于虚拟主机 Host: www.baidu.com:80 一台服务器部署多个网站
User-Agent 客户端信息,包括操作系统、浏览器版本 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 反爬、适配不同设备
Referer 表示当前请求来自哪个页面 Referer: http://127.0.0.1:8080/index.html 防盗链、统计用户来源、CSRF 防护
Accept 客户端能接受的媒体类型 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 内容协商
Accept-Encoding 客户端支持的压缩编码 Accept-Encoding: gzip, deflate 减少传输数据量
Accept-Language 客户端偏好的语言 Accept-Language: zh-CN,zh;q=0.9 多语言网站
Cookie 客户端存储的会话信息 Cookie: JSESSIONID=D628A75845A74D29D991D47461E4FC 会话管理、用户登录状态

5.2 通用响应头

Header 作用 示例 应用场景
Content-Type 响应体的 MIME 类型 Content-Type: application/json 告诉客户端如何解析响应体
Content-Length 响应体的字节长度 Content-Length: 1024 客户端知道何时接收完成
Location 配合 3xx 状态码使用,指定重定向地址 Location: https://www.qq.com/ 页面跳转、登录后重定向
Connection 连接管理 Connection: keep-alive 长连接 / 短连接控制

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3 关键 Header 实战应用

Referer 防盗链
防止其他网站直接引用你的图片、视频等资源:

// 在静态资源处理前添加Referer检查
std::string referer = httpreq["Referer"];
if(!referer.empty() && referer.find("yourdomain.com") == std::string::npos) {
    // 返回默认防盗链图片
    std::string default_img = GetFileContentHelper("wwwroot/default.jpg");
    httpresp.SetCode(200);
    httpresp.SetHeader("Content-Type", "image/jpeg");
    httpresp.SetBody(default_img);
    return;
}

Location 重定向
实现登录后跳转到首页:

void Login(const HttpRequest &req, HttpResponse &resp) {
    std::string args = req["args"];
    // 验证用户名密码成功
    resp.SetCode(302); // 临时重定向
    resp.SetHeader("Location", "/index.html");
}

Cookie 会话管理
实现用户登录状态保持:

void Login(const HttpRequest &req, HttpResponse &resp) {
    std::string args = req["args"];
    // 验证用户名密码成功
    std::string session_id = GenerateSessionId(); // 生成唯一会话ID
    // 设置Cookie,有效期1小时
    resp.SetHeader("Set-Cookie", "session_id=" + session_id + "; Max-Age=3600; Path=/");
    resp.SetCode(302);
    resp.SetHeader("Location", "/index.html");
}

5.4 长连接 vs 短连接

  • 短连接(HTTP/1.0 默认):每次请求都建立一个新的 TCP 连接,请求完成后立即关闭。优点是实现简单,缺点是频繁建立连接开销大。
  • 长连接(HTTP/1.1 默认):一个 TCP 连接可以处理多个请求,减少了连接建立和关闭的开销。

实现长连接的关键

  • 服务器返回Connection: keep-alive
  • 服务器不立即关闭 socket,继续等待下一个请求
  • 正确处理多个请求的边界(通过Content-Length或分块传输)

六. 生产级部署:Nginx 静态资源服务与反向代理

自己实现的 HTTP 服务器适合学习和小型项目,但在生产环境中,我们几乎都会使用Nginx作为前端服务器。Nginx 是一个高性能的 HTTP 和反向代理服务器,以高并发、低内存消耗著称。

6.1 Nginx 核心特点

特性 说明
高并发 采用异步非阻塞事件驱动架构,单机可处理数万甚至数十万并发连接
低内存消耗 相比 Apache 等传统服务器,内存占用极低
反向代理 可将请求分发到后端多个应用服务器(如 Tomcat、Node.js、我们自己写的 C++ 服务器)
负载均衡 支持轮询、最少连接、IP 哈希等多种负载均衡算法
静态文件服务 处理静态资源(HTML/CSS/JS/ 图片)性能极高
HTTPS 支持 可配置 SSL 证书,提供安全的 HTTPS 服务

6.2 Nginx 安装与基本操作

在 Ubuntu 系统中,我们可以通过 apt 包管理器快速安装:

# 安装Nginx
sudo apt install nginx

# 启动Nginx
sudo nginx

# 停止Nginx
sudo nginx -s stop

# 重新加载配置(不中断服务)
sudo nginx -s reload

# 查看Nginx进程
ps ajx | grep nginx

6.3 Nginx 核心文件路径

不同安装方式的 Nginx 文件路径略有不同,以下是 apt 安装的默认路径:

# 主配置文件
/etc/nginx/nginx.conf

# 站点配置目录
/etc/nginx/conf.d/
/etc/nginx/sites-enabled/

# 二进制文件
/usr/sbin/nginx

# 日志文件
/var/log/nginx/access.log  # 访问日志
/var/log/nginx/error.log   # 错误日志

# 默认静态资源根目录
/var/www/html/

现在访问http://你的服务器IP,就能看到我们自己写的页面了。

6.5 Nginx 安全配置:禁用不安全的 HTTP 方法

生产环境中,为了安全起见,我们通常会禁用除 GET 和 POST 之外的所有 HTTP 方法:

测试当前支持的方法:

curl -X OPTIONS -i http://127.0.0.1/

如果返回405 Not Allowed,说明已经禁用;如果返回200 OKAllow头,说明支持。

禁用不安全方法的配置

编辑/etc/nginx/nginx.conf,在http块中添加以下内容:

server {
    listen 80;
    server_name localhost;

    # 只允许GET和POST方法
    if ($request_method !~ ^(GET|POST)$ ) {
        return 405;
    }

    location / {
        root /var/www/html;
        index index.html index.htm;
    }
}

保存后重新加载配置:

sudo nginx -s reload

6.6 Nginx 反向代理:转发动态请求

我们可以将 Nginx 作为反向代理,将动态请求转发到我们自己写的 C++ HTTP 服务器:

server {
    listen 80;
    server_name localhost;

    # 静态资源由Nginx直接处理
    location / {
        root /var/www/html;
        index index.html index.htm;
    }

    # 动态请求转发到后端C++服务器
    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这样所有以/api/开头的请求都会被转发到运行在 8080 端口的 C++ 服务器,而静态资源则由 Nginx 直接处理,充分发挥 Nginx 的性能优势。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


七. 生产级实践:使用 cpp-httplib 快速开发

虽然自己实现 HTTP 服务器能深入理解原理,但在实际项目中,我们更推荐使用成熟的第三方库。cpp-httplib是一个仅头文件、无依赖、接口优雅的 C++ HTTP 库,非常适合快速开发。

#include "httplib.h" // 引入 cpp-httplib 库,这是一个非常优秀的轻量级 C++ HTTP 框架 (Header-only)

int main() 
{
    // @brief 实例化一个 HTTP 服务器核心对象
    httplib::Server svr;
    
    // =========================================================
    // 模块一:静态资源托管 (取代了你以前手写读取文件的逻辑)
    // =========================================================
    // 1. 托管静态资源:将 ./static 目录下的文件作为静态资源服务
    //    访问 http://localhost:8080/index.html 会返回 ./static/index.html
    // @note 补充说明:你的代码中实际将 URL 的根路径 "/" 映射到了上一级目录的 "../wwwroot"。
    // 这意味着,只要浏览器请求的不是动态路由(如 /search),httplib 就会自动去 ../wwwroot 文件夹下找同名文件并返回给前端,极大简化了开发。
    svr.set_mount_point("/", "../wwwroot");
    
    // =========================================================
    // 模块二:动态路由与业务回调
    // =========================================================
    // 2. 提供搜索接口(回调路由)
    //    访问 http://localhost:8080/search?q=关键字
    // @brief 注册 GET 请求路由。当匹配到 "/search" 路径时,自动触发后面的 Lambda 匿名函数。
    // - req (Request): 框架已经帮你反序列化好的请求对象,里面有前端传来的参数、报头等。
    // - res (Response): 你需要将处理结果装填到这个对象里,框架最后会自动将其序列化发给浏览器。
    svr.Get("/search", [](const httplib::Request& req, httplib::Response& res) {
        
        // --- ⬇️ 以下是你注释掉的完整业务逻辑演示(提取参数 -> 校验 -> 构建JSON返回) ⬇️ ---
        // 获取查询参数 q
        // std::string query = req.get_param_value("q");
        
        // if (query.empty()) {
        //     res.set_content(R"({"error": "请提供搜索关键字 q"})", "application/json");
        //     return;
        // }
        
        // // 模拟搜索结果(实际可从数据库查询)
        // std::string results = R"([
        //     {"title": "关于 )" + query + R"( 的第一条结果", "url": "/page1"},
        //     {"title": "关于 )" + query + R"( 的第二条结果", "url": "/page2"},
        //     {"title": "关于 )" + query + R"( 的第三条结果", "url": "/page3"}
        // ])";
        
        // res.set_content(results, "application/json");
        // --- ⬆️ 注释结束 ⬆️ ---

        // @brief 当前生效的代码:无论前端查什么,直接暴力返回纯文本 "hello result"
        // 参数2 "text/plain" 设置了 Content-Type,告诉浏览器不用解析,直接显示文字即可。
        res.set_content("hello result", "text/plain");
    });
    
    // =========================================================
    // 模块三:启动服务器
    // =========================================================
    // 启动服务
    std::cout << "Server started at http://localhost:9090" << std::endl;
    
    // @brief 绑定 IP 和端口并启动监听
    // "0.0.0.0" 是一个特殊 IP,代表监听本机的所有网卡(这意味着无论是本地的 127.0.0.1,还是别人通过局域网/公网 IP 都能访问你的服务)。
    // 注意:listen() 是一个阻塞函数 (死循环),程序运行到这里就会停住,专门处理不断进来的网络请求,直到你按下 Ctrl+C 才会退出。
    svr.listen("0.0.0.0", 9090);
    
    return 0;
}

可以看到,cpp-httplib 的接口比我们自己实现的更加简洁和强大,支持路径参数、文件上传、HTTPS 等高级功能。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:

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

在这里插入图片描述

Logo

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

更多推荐