C++基础:代码详解http请求报文解析流程
2025/11/26:
这两个星期课程测验和科研工作都挺忙的,希望每天抽出一两个小时能把这块内容完成和梳理一下。
原始代码来自于HTTPServer2025/11/28 finished:
没想到两三天就把这块部分敲完了,接下来做一下路由方面的内容
系列文章:
参考: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!
测试结果也没啥问题,解析得到的内容均正常
更多推荐


所有评论(0)