2025/11/26:
这两个星期课程测验和科研工作都挺忙的,希望每天抽出一两个小时能把这块内容完成和梳理一下。
原始代码来自于HTTPServer

2025/11/28 finished:
没想到两三天就把这块部分敲完了,接下来做一下路由方面的内容

系列文章:

数据库连接池

代码详解http请求报文解析流程

http框架路由匹配算法实现

session实现和http server类最终组装

参考:kama httpserver
复现:TinyHttpServer

一、当我们解析报文的时候,我们在解析什么?

这里我们给出需要处理的报文示例:

POST /api/login?debug=1&judge=2 HTTP/1.1\r\n
Host: example.com\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 27\r\n
\r\n
username=admin&password=123456

当然这个报文是我随便写的,稍微有点问题,只是为了尽量包含报文解析中的所有要素。http原理和报文中各种字段就不必在此讲解了,网上有讲的更好的。这里直入主题,在解析过程中,我们将报文分为三个部分:

  • 请求行
POST /api/login?debug=1&judge=2 HTTP/1.1\r\n

请求行包括请求的method(POST),URL(/api/login),参数列表(debug=1&judge=2)和协议版本(HTTP/1.1)

  • 请求头
Host: example.com\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 27\r\n

这里每行都是一个键值对

  • 请求体
\r\n(该记号用于分隔头和体)
username=admin&password=123456

当使用C++来处理请求报文的时候,可以思考一下,如何以面向对象的思维来将流式的报文转化为结构化的对象。

二、数据封装

稍作思考之后,我们可以得到下面这个数据结构:

#include <map>
#include <string>
#include <unordered_map>
#include <muduo/base/Timestamp.h>
#include <regex>
#include <utility>
#include <nlohmann/json.hpp>

class HttpRequest
{
public:
    // HTTP请求方法枚举
    enum Method
    {
        kInvalid, kGet, kPost, kHead, kPut, kDelete, kOptions
    };

    // 构造函数初始化成员变量
    HttpRequest()
        : method_(kInvalid)
        , version_("Unknown")
        , selfCheckFunc_({
            {kGet,   [this] { return checkGetLikeMethod(); }},
            {kPost,   [this] { return checkPostLikeMethod(); }},
            {kPut,    [this] { return checkPostLikeMethod(); }},
            {kDelete, [this] { return checkGetLikeMethod(); }}
        })
    {

    }

    // 设置和获取接收时间
    void setReceiveTime(muduo::Timestamp t);

    // 获取私有成员 receiveTime_
    muduo::Timestamp receiveTime() const { return receiveTime_; }

    // 设置和获取请求方法
    bool setMethod(const char* start, const char* end);

    // 获取请求方法私有变量
    Method method() const { return method_; }

    // 设置和获取请求路径
    void setPath(const char* start, const char* end);

    void setPath(const std::string& path)
    {
        path_ = path;
    }

    // 获取请求路径私有变量
    std::string path() const { return path_; }

    // 设置和获取路径参数
    void setPathParameters(const std::string &key, const std::string &value);

    /// 获取路径参数
    std::string getPathParameters(const std::string &key) const;

    // 设置和获取查询参数
    void setQueryParameters(const char* start, const char* end);

    // 获取查询参数
    std::string getQueryParameters(const std::string &key) const;

    // 设置HTTP版本
    void setVersion(std::string v)
    {
        version_ = std::move(v);
    }

    // 获取版本
    std::string getVersion() const
    {
        return version_;
    }

    // 设置和获取请求头 每次接收一行请求头调用一次
    void addHeader(const char* start, const char* end);
    // 获取请求头,根据字段名获取对应值
    std::string getHeader(const std::string& field) const;
    // 获取所有请求头
    const std::map<std::string, std::string>& headers() const
    { return headers_; }

    // 设置请求体
    void setBody(const std::string& body) { content_ = body; }
    // 通过头尾指针设置请求体
    void setBody(const char* start, const char* end)
    {
        if (end >= start)
        {
            content_.assign(start, end - start);
        }
    }

    // 获取请求体
    std::string getBody() const
    { return content_; }

    // 设置和获取请求体长度
    void setContentLength(uint64_t length)
    { contentLength_ = length; }
    uint64_t contentLength() const
    { return contentLength_; }

    // 交换所有成员
    void swap(HttpRequest& that) noexcept;

    // 将所有成员通过json格式打印在控制台中
    void showDetails() const;

    // 进行自检验
    bool selfCheck() const;

private:
    std::unordered_map<Method, std::function<bool()>> selfCheckFunc_;

    bool checkGetLikeMethod() const;

    bool checkPostLikeMethod();

    Method                                       method_; // 请求方法
    std::string                                  version_; // http版本
    std::string                                  path_; // 请求路径
    std::unordered_map<std::string, std::string> pathParameters_; // 路径参数
    std::unordered_map<std::string, std::string> queryParameters_; // 查询参数
    muduo::Timestamp                             receiveTime_; // 接收时间
    std::map<std::string, std::string>           headers_; // 请求头
    std::string                                  content_; // 请求体
    uint64_t                                     contentLength_ { 0 }; // 请求体长度
};

这里面路径参数可以暂时不用管,这玩意的存储格式是类似于{“param1”:“api”,“param2”:“login”}这种格式的,之后如果做到路由的时候才有用处,这里暂时不讲。这里我们构造了一个HttpRequest类,将报文中的数据做了一个基础的封装。经过理想的处理之后,我们想看到的对象封装结果如下:

{
  method: POST,
  path: "/api/login",
  queryParameters: { "debug": "1" },
  version: "HTTP/1.1",
  receiveTime: 2025-11-26 10:00:00,
  headers: {
    "Host": "example.com",
    "Content-Type": "application/x-www-form-urlencoded",
    "Content-Length": "27"
  },
  contentLength: 27,
  body: "username=admin&password=123456"
}

解析的流程在这里其实也很明确了。源代码采用的是状态机的形式以确保解析过程的有序,不过直接按照顺序分别解析三大块内容其实区别不大(我这里的代码就是这么写的),编写代码的过程中可能需要注意一下对于报文中潜在错误的处理。

在解析的过程中,这里使用HttpContext类再做一层封装。对于这层封装我们可以这么理解:在server中始终操作的都是当前的一个context,而request是context的一个成员,每次收到新的报文都会有变动。

简单来说,在server中context的调用可以简单由以下代码表示:

// context表示解析过程没有问题且完成
if (context->gotAll())
{
	  // 通过解析得到的结构化数据来封装对应的响应报文
    onRequest(conn, context->request());
    // 上一个request用完就已经没用了,直接置空context
    context->reset();
}

由此我们可以得到context的成员大概有哪些:

#pragma once

#include <muduo/net/TcpServer.h>
#include <muduo/base/Logging.h>
#include "HttpRequest.h"

class HttpContext
{
public:
    HttpContext()
    : complete_(false)
    {

    }

    // 解析HTTP请求
    bool parseRequest(muduo::net::Buffer* buf, muduo::Timestamp receiveTime);

    bool parseComplete() const
    { return complete_ == true;  }

    void reset()
    {
        complete_ = false;
        HttpRequest dummyData;
        request_.swap(dummyData);
    }

    const HttpRequest& request() const
    { return request_;}

    HttpRequest& request()
    { return request_;}

    void showRequest() const
    {
        request_.showDetails();
    }

private:
    // 解析请求行
    bool processRequestLine(const char* begin, const char* end);
    // 解析请求头
    bool processHeaders(const char* begin, const char* end);
    // 解析请求体
    bool processBody(const char* begin, const char* end);

    bool complete_;
    HttpRequest request_;
};

这里也没啥好说的 ,基本就按照 行 - 头 - 体 的解析顺序来。

三、方法实现

接下来是具体的方法实现:

HttpRequest.cpp:

#include "http/HttpRequest.h"

#include <muduo/base/Logging.h>

void HttpRequest::setReceiveTime(muduo::Timestamp t)
{
    // 设置请求接收时间
    receiveTime_ = t;
}

void HttpRequest::setPath(const char* start, const char* end)
{
    path_ = std::string(start, end);
}

void HttpRequest::setPathParameters(const std::string& key, const std::string& value)
{
    pathParameters_[key] = value;
}

std::string HttpRequest::getPathParameters(const std::string& key) const
{
    auto it = pathParameters_.find(key);
    if (it != pathParameters_.end())
    {
        return it->second;
    }
    return "";
}

// 通过头尾指针设置查询键值对
// username=admin&password=123456
void HttpRequest::setQueryParameters(const char* start, const char* end)
{
    std::string qs(start, end);
    // 构造正则表达式进行键值对匹配
    std::regex re(R"(([^&=]+)=([^&]*))");
    const auto itBegin = std::sregex_iterator(qs.begin(), qs.end(), re);
    const auto itEnd = std::sregex_iterator();
    for (auto it = itBegin; it != itEnd; ++it) {
        const std::smatch& m = *it;
        queryParameters_[m[1]] = m[2];
    }
}

bool HttpRequest::setMethod(const char* start, const char* end)
{
    const std::string method(start, end);

    std::unordered_map<std::string, Method> parseMethods
    {
        {"GET", kGet}, {"POST", kPost}, {"DELETE", kDelete},
        {"PUT", kPut}, {"OPTIONS", kOptions}, {"HEAD", kHead}
    };

    if (const auto it = parseMethods.find(method); it != parseMethods.end())
    {
        method_ = it->second;
        return true;
    }
    return false;
}

std::string HttpRequest::getQueryParameters(const std::string &key) const
{
    auto it = queryParameters_.find(key);
    if (it != queryParameters_.end())
    {
        return it->second;
    }
    return "";
}

void HttpRequest::addHeader(const char* start, const char* end)
{
    std::string qs(start, end);
    // 构造正则表达式进行键值对匹配,只匹配第一个":"
    std::regex re(R"(([^&:=]+):\s*([^&]*))");
    std::smatch m;
    std::regex_search(qs, m, re);
    headers_[m[1]] = m[2];

    // 特殊处理Content-Length头
    if (m[1] == "Content-Length")
    {
        contentLength_ = std::stoull(m[2]);
    }
}

std::string HttpRequest::getHeader(const std::string& field) const
{
    auto it = headers_.find(field);
    if (it != headers_.end())
    {
        return it->second;
    }
    return "";
}

// 交换所有元素
void HttpRequest::swap(HttpRequest& that) noexcept
{
    std::swap(method_, that.method_);
    std::swap(path_, that.path_);
    std::swap(queryParameters_, that.queryParameters_);
    std::swap(pathParameters_, that.pathParameters_);
    std::swap(headers_, that.headers_);
    std::swap(contentLength_, that.contentLength_);
    std::swap(receiveTime_, that.receiveTime_);
}

void HttpRequest::showDetails() const
{
    nlohmann::json j;

    // method -> string
    auto methodToString = [](Method m) -> std::string {
        switch (m) {
        case kGet:     return "GET";
        case kPost:    return "POST";
        case kHead:    return "HEAD";
        case kPut:     return "PUT";
        case kDelete:  return "DELETE";
        case kOptions: return "OPTIONS";
        default:       return "INVALID";
        }
    };

    j["method"] = methodToString(method_);
    j["version"] = version_;
    j["path"] = path_;

    // pathParameters (unordered_map)
    nlohmann::json pathParams = nlohmann::json::object();
    for (const auto &p : pathParameters_) pathParams[p.first] = p.second;
    j["pathParameters"] = std::move(pathParams);

    // queryParameters (unordered_map)
    nlohmann::json queryParams = nlohmann::json::object();
    for (const auto &p : queryParameters_) queryParams[p.first] = p.second;
    j["queryParameters"] = std::move(queryParams);

    // receiveTime as microseconds since epoch (muduo::Timestamp)
    j["receiveTime_us"] = static_cast<long long>(receiveTime_.microSecondsSinceEpoch());
    // headers (std::map)
    nlohmann::json hdrs = nlohmann::json::object();
    for (const auto &h : headers_) hdrs[h.first] = h.second;
    j["headers"] = std::move(hdrs);

    j["content"] = content_;
    j["contentLength"] = contentLength_;

    LOG_INFO << "HttpRequest Details:\n" << j.dump(4);
}

// 简单的自检函数,检查必要字段是否存在,比如get和delete没有body,post和put必须有body和content-length > 0 content-Type 为规定值
bool HttpRequest::selfCheck() const
{
    // 检查selfCheckwFunc_是否注册了对应method的检查函数
    auto it = selfCheckFunc_.find(method_);
    if (it != selfCheckFunc_.end())
    {
        return it->second(); // 调用对应的检查函数
    }
    // 如果没有注册对应的检查函数,默认返回true
    return true;
}

bool HttpRequest::checkGetLikeMethod() const
{
    if (!content_.empty())
    {
        LOG_WARN << "GET request should not have a body.";
        return false;
    }
    return true;
}

bool HttpRequest::checkPostLikeMethod()
{
    if (content_.empty() || contentLength_ == 0)
    {
        LOG_ERROR << "POST request must have a body and Content-Length > 0.";
        return false;
    }

    auto contentType = getHeader("Content-Type");
    if (contentType != "application/x-www-form-urlencoded" && contentType != "application/json")
    {
        LOG_ERROR << "Unsupported Content-Type for POST request: " << contentType;
        return false;
    }

    // 校验contentLength_是否与content_长度匹配
    if (contentLength_ != content_.size())
    {
        LOG_WARN << "Content-Length does not match actual content size.";
        // 优先修正contentLength_
        contentLength_ = content_.size();
    }
    return true;
}

HttpContext.cpp:

#include "http/HttpContext.h"

#include <regex>

/*
POST /api/login?debug=1 HTTP/1.1\r\n
Host: example.com\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 27\r\n
\r\n
username=admin&password=123456
*/

// 解析HTTP请求报文
bool HttpContext::parseRequest(muduo::net::Buffer* buf, muduo::Timestamp receiveTime)
{
    // 解析请求行
    const char *crlf = buf->findCRLF();
    if (crlf)
    {
        if (!processRequestLine(buf->peek(), crlf))
        {
            return false;
        }
        buf->retrieveUntil(crlf + 2); // 移动读指针
        request_.setReceiveTime(receiveTime);
    }
    else
    {
        return false; // 请求行不完整
    }

    // 解析请求头 请求头的最后有两个crlf字符
    // 记录上一个CRLF位置
    const char* preCRLF = buf->findCRLF();
    while (preCRLF && preCRLF + 2 <= buf->peek() + buf->readableBytes())
    {
        const char* nextCRLF = buf->findCRLF(preCRLF + 2);
        if (nextCRLF == preCRLF + 2)
        {
            // 找到连续空行,解析请求头
            if (!processHeaders(buf->peek(), nextCRLF))
            {
                return false;
            }
            buf->retrieveUntil(nextCRLF + 2);
            break;
        }
        preCRLF = nextCRLF;
    }

    // 解析请求体
    if (buf->readableBytes() > 0)
    {
        // 读取剩余的请求体数据
        if (buf->readableBytes() < request_.contentLength())
        {
            LOG_ERROR << "Buffer overflow: Body length less than Content-Length";
            return false;
        }

        // 只读取 Content-Length 指定的长度
        auto body = std::string(buf->peek(), buf->peek() + request_.contentLength());
        request_.setBody(body);
        buf->retrieve(request_.contentLength());
    }

    // 最后request自校验
    complete_ =  request_.selfCheck();
    return complete_;
}

// 解析请求行
bool HttpContext::processRequestLine(const char* begin, const char* end)
{
    // 解析请求类型、路径、路径参数、HTTP版本
    // 按照空格分割,找到第一个空格位置作为请求类型的end
    const char* space = std::find(begin, end, ' ');
    if (space == end)
    {
        LOG_ERROR << "Invalid RequestLine";
        return false;
    }
    // 设置请求方法
    request_.setMethod(begin, space);
    // 移动起始指针
    const char* pathBegin = space + 1;
    space = std::find(pathBegin, end, ' ');
    const char* argumentBegin = std::find(pathBegin, space, '?');
    if (space == end)
    {
        LOG_ERROR << "Invalid RequestLine";
        return false;
    }
    // 设置请求路径
    request_.setPath(pathBegin, argumentBegin);
    // 设置查询参数
    if (argumentBegin != space)
    {
        request_.setQueryParameters(argumentBegin + 1, space);
    }
    const char* versionBegin = space + 1;
    // 检查HTTP版本格式
    if (std::string(versionBegin, end) != "HTTP/1.1" &&
        std::string(versionBegin, end) != "HTTP/1.0")
    {
        LOG_ERROR << "Invalid HTTP Version";
        return false;
    }
    // 设置HTTP版本
    request_.setVersion(std::string(versionBegin, end));
    return true;
}

// 解析请求头
bool HttpContext::processHeaders(const char* begin, const char* end)
{
    // 请求头格式 "sentence/r/n sentence/r/n"
    // 每次处理/r/n前面的部分
    const char* lineStart = begin;
    while (lineStart < end)
    {
        const char* lineEnd = std::find(lineStart, end, '\r');
        if (lineEnd == end || lineEnd + 1 == end || *(lineEnd + 1) != '\n')
        {
            LOG_ERROR << "Invalid Header Line";
            return false;
        }
        // 解析单个请求头
        request_.addHeader(lineStart, lineEnd);
        // 移动到下一行
        lineStart = lineEnd + 2;
    }
    return true;
}

// 解析请求体,get和delete方法一般没有body
bool HttpContext::processBody(const char* begin, const char* end)
{
    // 直接设置请求体内容
    try
    {
        request_.setBody(begin, end);
    }
    catch (const std::exception& e)
    {
        LOG_ERROR << "Invalid Request Body " << e.what();
    }
    return true;
}

这里其实也没什么好讲的。

四、单元测试

单元测试部分主要测试一下HttpContext是否可以正常解析报文,代码如下:

// 单元测试 HttpContext 类
#include "include/http/HttpContext.h"

#include <iostream>
#include <cassert>
#include <muduo/base/Timestamp.h>
#include <muduo/net/Buffer.h>

void testGet(HttpContext& context)
{
    // 创建 Buffer 并填充一个简单的 HTTP 请求报文
    muduo::net::Buffer buffer;
    std::string httpRequest =
        "GET /index.html?debug=1&judge=2 HTTP/1.1\r\n"
        "Host: www.example.com\r\n"
        "User-Agent: TestAgent/1.0\r\n"
        "Accept: */*\r\n"
        "\r\n";
    buffer.append(httpRequest);

    // 解析请求
    muduo::Timestamp receiveTime = muduo::Timestamp::now();
    bool result = context.parseRequest(&buffer, receiveTime);
    assert(result == true);

    // 获取解析后的 HttpRequest 对象
    const HttpRequest& request = context.request();

    // 验证请求行解析结果
    assert(request.method() == HttpRequest::kGet);
    assert(request.path() == "/index.html");
    assert(request.getHeader("Host") == "www.example.com");
    assert(request.getHeader("User-Agent") == "TestAgent/1.0");
    assert(request.getHeader("Accept") == "*/*");

    context.showRequest();

    std::cout << "HttpContext test passed!" << std::endl;
}

// 测试post
void testPost(HttpContext& context)
{
    // 创建 Buffer 并填充一个简单的 HTTP POST 请求报文
    muduo::net::Buffer buffer;
    std::string httpRequest =
        "POST /submit HTTP/1.1\r\n"
        "Host: www.example.com\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        "Content-Length: 27\r\n"
        "\r\n"
        "field1=value1&field2=value2";
    buffer.append(httpRequest);

    // 解析请求
    muduo::Timestamp receiveTime = muduo::Timestamp::now();
    bool result = context.parseRequest(&buffer, receiveTime);
    assert(result == true);

    // 获取解析后的 HttpRequest 对象
    const HttpRequest& request = context.request();

    // 验证请求行解析结果
    assert(request.method() == HttpRequest::kPost);
    assert(request.path() == "/submit");
    assert(request.getHeader("Host") == "www.example.com");
    assert(request.getHeader("Content-Type") == "application/x-www-form-urlencoded");
    assert(request.getHeader("Content-Length") == "27");

    context.showRequest();

    std::cout << "HttpContext POST test passed!" << std::endl;
}


int main() {
    // 创建 HttpContext 对象
    HttpContext context;

    // 测试 GET 请求解析
    testGet(context);
    // 重置 HttpContext 对象以测试 POST 请求
    context.reset();
    // 测试 POST 请求解析
    testPost(context);

    return 0;
}

执行代码后返回结果如下:

/root/projects/clionProjects/networkProgramLearning/tinyHTTP/cmake-build-debug/tinyHTTP
20251128 07:43:15.271095Z 87219 INFO HttpRequest Details:
{
“content”: “”,
“contentLength”: 0,
“headers”: {
“Accept”: “/”,
“Host”: “www.example.com”,
“User-Agent”: “TestAgent/1.0”
},
“method”: “GET”,
“path”: “/index.html”,
“pathParameters”: {},
“queryParameters”: {
“debug”: “1”,
“judge”: “2”
},
“receiveTime_us”: 1764315795269150,
“version”: “HTTP/1.1”
} - HttpRequest.cpp:153
HttpContext test passed!
20251128 07:43:15.273369Z 87219 INFO HttpRequest Details:
{
“content”: “field1=value1&field2=value2”,
“contentLength”: 27,
“headers”: {
“Content-Length”: “27”,
“Content-Type”: “application/x-www-form-urlencoded”,
“Host”: “www.example.com”
},
“method”: “POST”,
“path”: “/submit”,
“pathParameters”: {},
“queryParameters”: {},
“receiveTime_us”: 1764315795271413,
“version”: “HTTP/1.1”
} - HttpRequest.cpp:153
HttpContext POST test passed!

测试结果也没啥问题,解析得到的内容均正常

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐