目录
还记得我们在Connection类里面预留的那个成员变量------协议上下文吗?
cpp
// 连接类,继承enable_shared_from_this以支持在类内部获取自身的shared_ptr
class Connection : public std::enable_shared_from_this<Connection>
{
private:
// 协议上下文:可存储任意类型的协议处理相关状态和数据
// 例如HTTP请求的解析状态、WebSocket的握手信息等
std::any _context; // C++17的any对象,表示任意类型
......
}
// 协议切换函数(在EventLoop线程中执行)
// 用于动态修改连接的处理协议和回调函数
void UpgradeInLoop(const std::any &context,
const ConnectedCallback &conn,
const MessageCallback &msg,
const ClosedCallback &closed,
const AnyEventCallback &event)
{
// 更新协议上下文和各回调函数
_context = context;
_connected_callback = conn;//更新用户设置的连接建立回调函数
_message_callback = msg;//更新用户设置的消息到达回调函数
_closed_callback = closed;//更新用户设置的连接关闭回调函数
_event_callback = event;//更新用户设置的任意事件回调函数
}
// 设置协议上下文
void SetContext(const std::any &context)
{
_context = context;
}
// 获取协议上下文指针
std::any *GetContext()
{
return &_context;
}
在HTTP服务器中,每个连接(Connection)可能需要处理不同的协议(例如HTTP、WebSocket等)。协议上下文(Protocol Context)就是一个与特定协议相关的数据结构,它保存了在协议处理过程中需要保持的状态信息。
**在我们这个例子里面HttpContext 类就是用于处理HTTP协议的上下文。**它保存了HTTP请求的解析状态(如当前解析到哪一步、已经解析出的请求信息等)以及解析过程中产生的中间数据(如请求行、头部、正文等)。
当我们在一个连接中处理HTTP协议时,我们需要一个HttpContext对象来保存这个连接上HTTP请求的解析状态。这样,每次有数据到来时,我们就可以根据这个上下文继续解析,而不是从头开始。
一.HttpContext子模块
1.1.设计思路
HttpContext 模块是 HTTP 服务器框架的核心状态管理组件,负责记录并维护 HTTP 请求的接收与解析进度。
在异步网络编程中,一个完整的 HTTP 请求数据可能分多次到达服务器,该模块通过状态机模型来跟踪处理进度,确保请求的完整性。
- 分片数据传输问题
在 TCP/IP 网络通信中,HTTP 请求报文可能被分割为多个数据包传输:
-
第一次接收:请求行和部分头部
-
第二次接收:剩余头部
-
第三次接收:请求正文
需要记录当前处理到哪个阶段,以便后续继续处理。
- 状态持久化需求
如果没有状态记录,每次收到新数据都需要重新解析整个请求,效率低下且容易出错。
- 错误处理的复杂性
解析过程中可能出现多种错误(格式错误、长度超限、非法路径等),需要精准定位错误类型并返回适当的 HTTP 状态码。
由于HTTP请求可能被分多次到达(例如在网络传输中分成多个数据包),我们需要记录当前处理到哪一步,以便在下次接收到数据时从上次中断的地方继续处理。
为此,我们定义了以下状态,表示当前请求的接收阶段:
-
接收请求行(RECV_HTTP_LINE): 正在接收或等待接收请求行(即HTTP请求的第一行)
-
接收请求头部(RECV_HTTP_HEAD): 正在接收请求头部,直到遇到空行表示头部结束
-
接收请求正文(RECV_HTTP_BODY): 正在接收请求正文,根据Content-Length等头部信息来确定正文长度
-
接收完毕(RECV_HTTP_OVER): 请求数据已经全部接收完毕,可以开始处理请求并生成响应
-
接收错误(RECV_HTTP_ERROR): 在接收或解析过程中出现错误,如格式错误、超出长度限制等
在请求的接收和解析过程中,可能会遇到各种错误,例如:
-
请求行格式不符合HTTP规范
-
请求头部格式错误
-
请求正文长度超过限制
-
资源路径包含非法字符或路径遍历攻击(如包含"..")
对于不同的错误,我们需要返回不同的HTTP状态码,例如400(Bad Request)、414(URI Too Long)等。
HttpContext模块提供以下接口:
-
接收并处理请求数据(RecvHttpRequest):
根据当前状态,从缓冲区中读取数据并解析,逐步完成请求行、请求头部和请求正文的接收。
-
获取解析完毕的请求信息(Request):
当状态为RECV_HTTP_OVER时,返回解析完成的HttpRequest对象,用于后续的业务处理。
-
获取响应状态码(RespStatu):
在解析过程中如果出现错误,会设置响应状态码,以便后续生成错误响应。
-
获取当前接收状态(RecvStatu):
返回当前接收状态,用于判断请求是否接收完毕或是否出现错误。
此外,还提供了重置功能(ReSet),用于在处理完一个请求后重置状态,以便复用该对象处理下一个请求。
1.2.分模块讲解
1.2.1.各个状态
首先我们需要去了解HTTP请求的请求报头长什么样子

首先,我们需要知道,这里我们其实是分了3部分进行处理
- 处理请求行
- 处理请求报头
- 处理正文
至于空行,只是作为请求报头和正文的分隔而已。
那么我们处理HTTP请求的时候就可能位于上面三个状态任意一个而已。
但是我们还需要考虑两个状态
- 发生错误
- 处理完毕
至此,我们就算是能根据状态来知道我们的HTTP请求处理的怎么样了。
HttpContext 定义了五个核心状态,构成完整的状态机:
cpp
typedef enum {
RECV_HTTP_ERROR, // 接收错误状态(解析失败、非法请求等)
RECV_HTTP_LINE, // 正在接收并解析请求行(HTTP方法、路径、协议版本)
RECV_HTTP_HEAD, // 正在接收并解析请求头部(键值对格式)
RECV_HTTP_BODY, // 正在接收请求正文(根据Content-Length确定长度)
RECV_HTTP_OVER // 请求接收完毕,可以进行业务处理
} HttpRecvStatu;
状态转换流程
cpp
RECV_HTTP_LINE → RECV_HTTP_HEAD → RECV_HTTP_BODY → RECV_HTTP_OVER
↓ ↓ ↓
└──────────────────┴───────────────────┘
RECV_HTTP_ERROR(任何阶段都可能出错)
1.2.2.从缓冲区接收并解析HTTP请求行
首先我们需要知道,HTTP请求从哪里来?
其实很简单,根据我们的设计 就是在输入缓冲区Buffer里面!!!
而我们的Buffer类里面其实也刚好准备了这么一些接口,直接读取一行数据即可。


我们只需要去调用输入缓冲区的GetLineAndPop()函数,就能读取出请求行来
那万一,我们没有读取到一行数据呢??也就是说,我们读取的数据是读取不到换行符的呢?
大家注意:这种情况只能说明这一个请求行太长了,长的不正常了。
太长了,就说明其实是有大问题的!!这个发请求的人肯定是不怀好意的!!
因此,我们必须得对请求行进行长度限制
cpp
// 定义最大行长度限制
#define MAX_LINE 8192
在代码中,MAX_LINE 是一个宏定义,用于限制 HTTP 请求中单行的最大长度。具体来说,它被设置为 8192 字节(8KB)。
-
8192 字节(8KB) 是一个常见的选择
-
足够容纳绝大多数合法的 HTTP 请求行和头部行
-
既不会过于严格影响正常使用,又能有效防止攻击
如果没有行长度限制,恶意客户端可以发送非常长的单行数据,可能导致服务器内存耗尽或缓冲区溢出
这是 Web 服务器常见的安全防护措施
注意:
我们输入缓冲区的大小其实是做了下面这个初始化的,有的人可能会问,为什么是 8192不是比1024大吗?为什么可以设置为8192?我缓冲区存不下啊!!
cpp// 定义缓冲区的默认大小 #define BUFFER_DEFAULT_SIZE 1024事实上我们设置的Buffer的默认大小是1024,但它是可以动态扩容的,最大不封顶。所以,我们这里就可以设置为8192。
这样子就有两种情况
- 缓冲区中没有完整的一行(即没有遇到换行符)
- 缓冲区中有完整的一行,但是这一行已经超过了长度限制
这个大家做一点简单的处理即可。
cpp
// 从缓冲区接收并解析HTTP请求行
bool RecvHttpLine(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_LINE) return false; // 状态检查
// 1. 获取一行数据,带有末尾的换行
std::string line = buf->GetLineAndPop();
// 2. 需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
if (line.size() == 0)
{
// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR;//更新状态为接收错误状态
_resp_statu = 414; // URI Too Long
return false;
}
// 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
//读取的数据够一行了,但是还是超出最大限制了
if (line.size() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR;//更新状态为接收错误状态
_resp_statu = 414; // URI Too Long
return false;
}
// 解析请求行
bool ret = ParseHttpLine(line);
if (ret == false) {
return false;
}
// 首行处理完毕,进入头部获取阶段
_recv_statu = RECV_HTTP_HEAD;
return true;
}
现在,我们读取到了请求行,我们就需要去解析这个请求行了吧!!
至于解析请求行,就不是我们这个函数需要做的事情,我们把解析请求行放到了另外一个函数
1.2.3.解析请求行
我们上面从输入缓冲区里面读取到了请求行,但是我们需要对他进行解析。
- 什么叫解析呢?
很简单,就是把请求行里面有效的数据提取出来,然后将这些有效的数据赋值给对应的成员变量。
对应的成员变量又是哪一些呢?其实已经很明确了,就是下面这几个成员变量(因为我们是请求)
cpp
class HttpRequest {
public:
// 请求方法(GET、POST、PUT、DELETE等)
std::string _method;
// 请求的资源路径
std::string _path;
// HTTP协议版本(如HTTP/1.1)
std::string _version;
// 请求正文内容
std::string _body;
// 存储从路径正则匹配中提取的数据
std::smatch _matches;
// 存储HTTP请求头部的键值对
std::unordered_map<std::string, std::string> _headers;
// 存储URL查询参数的键值对
std::unordered_map<std::string, std::string> _params;
......
}
好了,话不多说,我们直接开始
首先,我们必须得知道HTTP请求行长啥样是吧?

我们的请求行就是里面的第一行(注意里面的URL可不是完整的URL)
它的格式就是
cpp
方法 路径[?查询字符串] 协议版本
那么我们怎么去进行匹配呢?
我们就借助正则表达式来对HTTP请求行进行匹配,分割
cpp
(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?
我们将这个正则表达式拆分成多个部分来进行讲解
cpp
(GET|HEAD|POST|PUT|DELETE)
-
(...|...|...):表示"或"的意思,匹配括号中的任意一个 -
支持的HTTP方法:GET、HEAD、POST、PUT、DELETE
-
捕获组:括号表示这是一个捕获组,匹配到的内容会被提取出来
-
作用:匹配HTTP请求方法
cpp
([^?]*)
-
...\]:字符集,**匹配**方括号内的任意字符
-
\^?\]:匹配除了问号之外的任何字符
-
\^?\]\*:匹配0个或多个非问号字符
cpp
(?:\?(.*))?
这是一个比较复杂的部分,我们分解来看:
(?: ... )
- 非捕获组:括号以?:开头表示这是一个非捕获组
- 不提取内容:匹配但不捕获到结果中
\?
- 转义问号:\是转义字符,?在正则中有特殊含义(表示0次或1次)
- 字面问号:\?表示匹配一个实际的问号字符
(.*)
- 捕获任意内容:匹配0个或多个任意字符
- 贪婪匹配:.*会尽可能多地匹配字符
?
- 整体可选:这个?属于整个(?:\?(.*))?分组
- 0次或1次:表示查询字符串部分是可选的
- 作用:匹配可选的查询字符串部分(以?开头)
cpp
(HTTP/1\.[01])
- HTTP/1\.:匹配字面字符串"HTTP/1."
- \.:点号需要转义,因为点号在正则中表示"任意字符"
-
01\]:匹配数字0或1
- 捕获组:括号表示这是一个捕获组
cpp
(?:\n|\r\n)?
- \n:匹配换行符(LF)
- \r\n:匹配回车+换行(CRLF)
- |:或,匹配\n或\r\n
- (?: ... ):非捕获组
- ?:整体可选,0次或1次
- 作用:匹配可选的换行符(HTTP请求行可能以换行符结尾)
就这样子,我们很快就能通过正则表达式来获取到这个HTTP请求行
cpp
// 解析HTTP请求行(第一行)
// 格式:方法 路径[?查询字符串] 协议版本
bool ParseHttpLine(const std::string &line) {
std::smatch matches;
// 正则表达式匹配HTTP请求行
// (GET|HEAD|POST|PUT|DELETE) - 匹配HTTP方法
// ([^?]*) - 匹配路径部分(不包含?)
// (?:\\?(.*))? - 匹配可选的查询字符串
// (HTTP/1\\.[01]) - 匹配协议版本(HTTP/1.0或HTTP/1.1)
// (?:\n|\r\n)? - 匹配可选的换行符
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
bool ret = std::regex_match(line, matches, e);
if (ret == false)
{
_recv_statu = RECV_HTTP_ERROR; // 设置错误状态
_resp_statu = 400; // Bad Request
return false;
}
// matches内容:
// 0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1(完整匹配)
// 1 : GET(方法)
// 2 : /bitejiuyeke/login(路径)
// 3 : user=xiaoming&pass=123123(查询字符串)
// 4 : HTTP/1.1(协议版本)
......
}
现在我们的matches就拆分了HTTP请求行的所有数据
但是,我们光拆分没有用啊,我们还需要对各部分进行解析
请求方法的解析
其实很简单,我们就只是单纯的将请求方法全部转换为大写字母
cpp
// 请求方法的获取和标准化(转为大写)
_request._method = matches[1];//请求方法
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
这段代码是C++标准库中的std::transform函数的一个应用。它的作用是将一个字符串(或更一般地,一个序列)中的每个字符转换为大写形式。让我详细解释一下:
-
std::transform是C++标准模板库(STL)中的一个算法,用于将一个序列(或两个序列)中的每个元素进行转换,并将结果存储到指定的目标序列中。
-
在这个具体的调用中,它使用了三个迭代器和一个函数指针(或函数对象):
-
第一个参数:_request._method.begin() 这是源序列的起始迭代器。
-
第二个参数:_request._method.end() 这是源序列的结束迭代器。
-
第三个参数:_request._method.begin() 这是目标序列的起始迭代器。注意,这里和第一个参数相同,这意味着转换后的结果将存回原序列(即就地转换)。
-
第四个参数:::toupper 这是一个函数指针,指向C标准库中的toupper函数(注意前面的::表示全局命名空间)。这个函数接受一个int类型的参数(字符的ASCII码),并返回该字符的大写形式(如果它是小写字母的话)。
-
-
因此,这行代码的作用是:遍历_request._method字符串中的每个字符,将每个字符转换为大写,并覆盖原来的字符。最终,_request._method字符串中的所有字母都会变成大写。
资源路径的获取的解析
对于HTTP协议里面的请求行的URL,它的格式是下面这样子的
cpp
路径[?查询字符串]
例如
URL: http://www.example.com:8080/api/v1/users?name=John\&age=20
HTTP请求报文里面的URL是/api/v1/users?name=John&age=20
- 资源请求路径: "/api/v1/users"
- 查询字符串部分会被解析成:_params = {"name":"John", "age":"20"}
那么对于资源请求路径,根据正则表达式,它被匹配到了matches[2],那么对于资源请求路径,我们首先需要进行URL解码处理,这样子我们才能获取到真实的资源请求路径
我们进行下面这样子的处理
cpp
// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格
_request._path = Util::UrlDecode(matches[2], false);
协议版本的解析
这个其实就很简单,就是赋值给对应的成员变量
cpp
// 协议版本的获取
_request._version = matches[4];
查询字符串的解析
这个就真每什么好说的,就是按照下面的注释来想即可
cpp
// 查询字符串的获取与处理
std::vector<std::string> query_string_arry;
std::string query_string = matches[3];//匹配到的查询字符串 key1=value1&key=value2
// 查询字符串的格式 key=val&key=val....., 先以 & 符号进行分割,得到各个子串
Util::Split(query_string, "&", &query_string_arry);
// 针对各个子串,以 = 符号进行分割,得到key和val,得到之后也需要进行URL解码
for (auto &str : query_string_arry) {
size_t pos = str.find("=");
if (pos == std::string::npos) //没有找到=号,就说明没有查询字符串
{
_recv_statu = RECV_HTTP_ERROR;//设置接受错误状态
_resp_statu = 400; // Bad Request
return false;
}
//找到了=号
std::string key = Util::UrlDecode(str.substr(0, pos), true); //对=左边的字符进行URL解码
std::string val = Util::UrlDecode(str.substr(pos + 1), true);//对=右边的字符进行URL解码
_request.SetParam(key, val);//将查询参数存放到对应的数据结构
}
完整代码
至此,我们完整的写出了对应的HTTP请求行的解析代码
cpp
// 解析HTTP请求行(第一行)
// 格式:方法 路径[?查询字符串] 协议版本
bool ParseHttpLine(const std::string &line) {
std::smatch matches;
// 正则表达式匹配HTTP请求行
// (GET|HEAD|POST|PUT|DELETE) - 匹配HTTP方法
// ([^?]*) - 匹配路径部分(不包含?)
// (?:\\?(.*))? - 匹配可选的查询字符串
// (HTTP/1\\.[01]) - 匹配协议版本(HTTP/1.0或HTTP/1.1)
// (?:\n|\r\n)? - 匹配可选的换行符
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
bool ret = std::regex_match(line, matches, e);
if (ret == false)
{
_recv_statu = RECV_HTTP_ERROR; // 设置错误状态
_resp_statu = 400; // Bad Request
return false;
}
// matches内容:
// 0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1(完整匹配)
// 1 : GET(方法)
// 2 : /bitejiuyeke/login(路径)
// 3 : user=xiaoming&pass=123123(查询字符串)
// 4 : HTTP/1.1(协议版本)
// 请求方法的获取和标准化(转为大写)
_request._method = matches[1];//请求方法
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格
_request._path = Util::UrlDecode(matches[2], false);
// 协议版本的获取
_request._version = matches[4];
// 查询字符串的获取与处理
std::vector<std::string> query_string_arry;
std::string query_string = matches[3];//匹配到的查询字符串 key1=value1&key=value2
// 查询字符串的格式 key=val&key=val....., 先以 & 符号进行分割,得到各个子串
Util::Split(query_string, "&", &query_string_arry);
// 针对各个子串,以 = 符号进行分割,得到key和val,得到之后也需要进行URL解码
for (auto &str : query_string_arry) {
size_t pos = str.find("=");
if (pos == std::string::npos) //没有找到=号,就说明没有查询字符串
{
_recv_statu = RECV_HTTP_ERROR;//设置接受错误状态
_resp_statu = 400; // Bad Request
return false;
}
//找到了=号
std::string key = Util::UrlDecode(str.substr(0, pos), true); //对=左边的字符进行URL解码
std::string val = Util::UrlDecode(str.substr(pos + 1), true);//对=右边的字符进行URL解码
_request.SetParam(key, val);//将查询参数存放到对应的数据结构
}
return true;
}
1.2.4.从缓冲区接收并解析HTTP请求头部
首先,HTTP请求头部是哪一部分?

在我们这个代码里面,请求头部就是里面的请求报头部分,大家可以看到,这个请求报头里面就是一堆键值对,我们只需要将这些键值对从输入缓冲区里面读取出来,然后我们再将这些键值对分离出来,并且保存到对应的成员变量即可。
首先,大家仔细看,请求报头部分里面的键值对都是一行只有一堆键值对,读取到一个空行的时候,我们就算是正式读取完了这个请求报头。
有了上面的经验,我们很快就能写出下面这个代码了
cpp
// 从缓冲区接收并解析HTTP请求头部
bool RecvHttpHead(Buffer *buf) {
if (_recv_statu != RECV_HTTP_HEAD) return false; // 状态检查
// 一行一行取出数据,直到遇到空行为止,头部的格式 key: val\r\nkey: val\r\n....
while(1)
{
std::string line = buf->GetLineAndPop();//读取一行
// 2. 需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
if (line.size() == 0)
{
// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI Too Long
return false;
}
// 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
if (line.size() > MAX_LINE) {
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI Too Long
return false;
}
// 遇到空行(\n 或 \r\n)表示头部结束
if (line == "\n" || line == "\r\n") {
break;
}
//注意每一行只有一对键值对
bool ret = ParseHttpHead(line);//解析这一对键值对
if (ret == false)
{
return false;
}
}
// 头部处理完毕,进入正文获取阶段
_recv_statu = RECV_HTTP_BODY;
return true;
}
注意是每一行里面都只有一对键值对
注意:键值对的解析不是在这个函数进行的,我们还是将这个解析工作交给了另外一个函数
1.2.5.解析HTTP请求头部
我们知道我们每一行读取到的是key: val\r\n,那么我们就需要进行拆分操作,并且赋值给对应的成员变量,那么具体是哪些成员变量呢?
其实也很容易去理解,就是下面这个
cpp
class HttpRequest {
public:
// 存储HTTP请求头部的键值对
std::unordered_map<std::string, std::string> _headers;
......
// 插入一个头部字段到_headers映射中
void SetHeader(const std::string &key, const std::string &val)
{
_headers.insert(std::make_pair(key, val));
}
};
那么具体的操作就如下了:每一行读取到的是key: val\r\n
cpp
// 解析单个HTTP头部行
bool ParseHttpHead(std::string &line)
{
// key: val\r\nkey: val\r\n....
if (line.back() == '\n') line.pop_back(); // 末尾是换行则去掉换行字符
if (line.back() == '\r') line.pop_back(); // 末尾是回车则去掉回车字符
size_t pos = line.find(": ");//寻找分隔符,注意分隔符里面有一个空格
if (pos == std::string::npos) //没有找到分隔符
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400; // Bad Request
return false;
}
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 2);
_request.SetHeader(key, val);//插入一个头部字段到
return true;
}
这个就很简单
1.2.6.从缓冲区接收HTTP请求正文
还记得正文是哪部分吗

正文就是这里的有效载荷,也就是空行过后到数据的末尾都是正文。
有人可能就要问了,我怎么知道数据的末尾是哪里?
这里就需要借助一个HTTP请求报头里面的字段了
Content-Length:正文长度
通过这个,我们就能知道到达需要读取多大的范围了。
cpp
// 1. 获取正文长度
size_t content_length = _request.ContentLength();
if (content_length == 0)
{
// 没有正文,则请求接收解析完毕
_recv_statu = RECV_HTTP_OVER;
return true;
}
那么我们读取到的正文放到哪里呢?也就是下面这个成员变量里面
cpp
class HttpRequest {
public:
// 请求正文内容
std::string _body;
......
}
其次,我们可以遇到下面这种情况,
我们一次通信只获取了部分正文,我们还需要获取剩余的正文,那么我们就需要作出相应处理
我们需要检查缓冲区(buf)中可读的数据量(ReadAbleSize()):
a. 如果缓冲区可读数据量大于等于实际还需要接收的长度(real_len),
- 说明缓冲区中已经包含了剩余的完整正文。
- 那么就从缓冲区中读取real_len长度的数据,追加到_request._body中,
- 并移动缓冲区的读偏移(相当于把这部分数据从缓冲区中取出),
- 然后将状态改为接收完成(RECV_HTTP_OVER),返回true。
b. 如果缓冲区可读数据量小于实际还需要接收的长度(real_len),
- 说明缓冲区中的数据不足以完成整个正文的接收。
- 那么就将缓冲区中当前所有的可读数据全部取出,追加到_request._body中,并移动缓冲区的读偏移(相当于清空缓冲区,因为数据已经被取走了)。
- 但是注意,此时并没有将状态改为接收完成,而是保持为RECV_HTTP_BODY,等待下一次继续接收。
- 函数返回true,表示本次接收处理成功,但还没有接收完整。
cpp
// 从缓冲区接收HTTP请求正文
bool RecvHttpBody(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_BODY)
return false; // 状态检查
// 1. 获取正文长度
size_t content_length = _request.ContentLength();
if (content_length == 0)
{
// 没有正文,则请求接收解析完毕
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 2. 当前已经接收了多少正文,其实就是往 _request._body 中放了多少数据了
size_t real_len = content_length - _request._body.size(); // 实际还需要接收的正文长度
// 3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文
// 3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据
if (buf->ReadAbleSize() >= real_len) // 缓冲区可读数据>=实际还需接收的正文长度
{
_request._body.append(buf->ReadPosition(), real_len); // 往正文末尾添加 实际还需接收的正文
buf->MoveReadOffset(real_len); // 移动读偏移
_recv_statu = RECV_HTTP_OVER; // 更新接受状态为
return true;
}
// 3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来
// 缓冲区可读数据<实际还需接收的正文长度 ,说明还有正文没有到来
_request._body.append(buf->ReadPosition(), buf->ReadAbleSize()); // 往正文末尾添加 部分正文
buf->MoveReadOffset(buf->ReadAbleSize()); // 移动读偏移
return true;
}
这个函数看注释就很容易看明白。
1.2.7.接收并解析HTTP请求
这个其实是以HTTP请求的状态来进行驱动的
我们需要去了解HTTP请求的请求报头长什么样子

首先,我们需要知道,这里我们其实是分了3部分进行处理
- 处理请求行
- 处理请求报头
- 处理正文
正常的流程就是处理完这3个步骤即可。
至于空行,只是作为请求报头和正文的分隔而已。
那么我们处理HTTP请求的时候就可能位于上面三个状态任意一个而已。
但是我们还需要考虑两个状态
- 发生错误
- 处理完毕
至此,我们就算是能根据状态来知道我们的HTTP请求处理的怎么样了。
我们就能写出下面这个函数
cpp
// 接收并解析HTTP请求(主入口函数)
void RecvHttpRequest(Buffer *buf)
{
// 不同的状态,做不同的事情,但是这里不要break,因为处理完请求行后,应该立即处理头部,而不是退出等新数据
switch (_recv_statu)
{
case RECV_HTTP_LINE:
RecvHttpLine(buf); // 解析请求行
case RECV_HTTP_HEAD:
RecvHttpHead(buf); // 解析头部
case RECV_HTTP_BODY:
RecvHttpBody(buf); // 解析正文
}
return;
}
1.3.完整代码

cpp
#pragma once
#include <regex>
#include <unordered_map>
#include <string>
#include "util.hpp"
#include "httprequest.hpp"
#include "httpresponse.hpp"
// HTTP接收状态枚举,表示HTTP请求解析的不同阶段
typedef enum
{
RECV_HTTP_ERROR, // 接收错误状态
RECV_HTTP_LINE, // 正在接收请求行(第一行)
RECV_HTTP_HEAD, // 正在接收请求头部
RECV_HTTP_BODY, // 正在接收请求正文
RECV_HTTP_OVER // 请求接收完毕
} HttpRecvStatu;
// 定义最大行长度限制
#define MAX_LINE 8192
// HTTP上下文类,负责解析HTTP请求
class HttpContext
{
private:
int _resp_statu; // 响应状态码(解析错误时使用)
HttpRecvStatu _recv_statu; // 当前接收和解析的阶段状态
HttpRequest _request; // 已经解析得到的请求信息
private:
// 解析HTTP请求行(第一行)
// 格式:方法 路径[?查询字符串] 协议版本
bool ParseHttpLine(const std::string &line)
{
std::smatch matches;
// 正则表达式匹配HTTP请求行
// (GET|HEAD|POST|PUT|DELETE) - 匹配HTTP方法
// ([^?]*) - 匹配路径部分(不包含?)
// (?:\\?(.*))? - 匹配可选的查询字符串
// (HTTP/1\\.[01]) - 匹配协议版本(HTTP/1.0或HTTP/1.1)
// (?:\n|\r\n)? - 匹配可选的换行符
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
bool ret = std::regex_match(line, matches, e);
if (ret == false)
{
_recv_statu = RECV_HTTP_ERROR; // 设置错误状态
_resp_statu = 400; // Bad Request
return false;
}
// matches内容:
// 0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1(完整匹配)
// 1 : GET(方法)
// 2 : /bitejiuyeke/login(路径)
// 3 : user=xiaoming&pass=123123(查询字符串)
// 4 : HTTP/1.1(协议版本)
// 请求方法的获取和标准化(转为大写)
_request._method = matches[1]; // 请求方法
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格
_request._path = Util::UrlDecode(matches[2], false);
// 协议版本的获取
_request._version = matches[4];
// 查询字符串的获取与处理
std::vector<std::string> query_string_arry;
std::string query_string = matches[3]; // 匹配到的查询字符串 key1=value1&key=value2
// 查询字符串的格式 key=val&key=val....., 先以 & 符号进行分割,得到各个子串
Util::Split(query_string, "&", &query_string_arry);
// 针对各个子串,以 = 符号进行分割,得到key和val,得到之后也需要进行URL解码
for (auto &str : query_string_arry)
{
size_t pos = str.find("=");
if (pos == std::string::npos) // 没有找到=号,就说明没有查询字符串
{
_recv_statu = RECV_HTTP_ERROR; // 设置接受错误状态
_resp_statu = 400; // Bad Request
return false;
}
// 找到了=号
std::string key = Util::UrlDecode(str.substr(0, pos), true); // 对=左边的字符进行URL解码
std::string val = Util::UrlDecode(str.substr(pos + 1), true); // 对=右边的字符进行URL解码
_request.SetParam(key, val); // 将查询参数存放到对应的数据结构
}
return true;
}
// 从缓冲区接收并解析HTTP请求行
bool RecvHttpLine(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_LINE)
return false; // 状态检查
// 1. 获取一行数据,带有末尾的换行
std::string line = buf->GetLineAndPop();
// 2. 需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
if (line.size() == 0)
{
// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR; // 更新状态为接收错误状态
_resp_statu = 414; // URI Too Long
return false;
}
// 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
// 读取的数据够一行了,但是还是超出最大限制了
if (line.size() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR; // 更新状态为接收错误状态
_resp_statu = 414; // URI Too Long
return false;
}
// 解析请求行
bool ret = ParseHttpLine(line);
if (ret == false)
{
return false;
}
// 首行处理完毕,进入头部获取阶段
_recv_statu = RECV_HTTP_HEAD;
return true;
}
// 从缓冲区接收并解析HTTP请求头部
bool RecvHttpHead(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_HEAD)
return false; // 状态检查
// 一行一行取出数据,直到遇到空行为止,头部的格式 key: val\r\nkey: val\r\n....
while (1)
{
std::string line = buf->GetLineAndPop(); // 读取一行
// 2. 需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
if (line.size() == 0)
{
// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
if (buf->ReadAbleSize() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI Too Long
return false;
}
// 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
return true;
}
if (line.size() > MAX_LINE)
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 414; // URI Too Long
return false;
}
// 遇到空行(\n 或 \r\n)表示头部结束
if (line == "\n" || line == "\r\n")
{
break;
}
// 注意每一行只有一对键值对
bool ret = ParseHttpHead(line); // 解析这一对键值对
if (ret == false)
{
return false;
}
}
// 头部处理完毕,进入正文获取阶段
_recv_statu = RECV_HTTP_BODY;
return true;
}
// 解析单个HTTP头部行
bool ParseHttpHead(std::string &line)
{
// key: val\r\nkey: val\r\n....
if (line.back() == '\n')
line.pop_back(); // 末尾是换行则去掉换行字符
if (line.back() == '\r')
line.pop_back(); // 末尾是回车则去掉回车字符
size_t pos = line.find(": "); // 寻找分隔符,注意分隔符里面有一个空格
if (pos == std::string::npos) // 没有找到分隔符
{
_recv_statu = RECV_HTTP_ERROR;
_resp_statu = 400; // Bad Request
return false;
}
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 2);
_request.SetHeader(key, val); // 插入一个头部字段到
return true;
}
// 从缓冲区接收HTTP请求正文
bool RecvHttpBody(Buffer *buf)
{
if (_recv_statu != RECV_HTTP_BODY)
return false; // 状态检查
// 1. 获取正文长度
size_t content_length = _request.ContentLength();
if (content_length == 0)
{
// 没有正文,则请求接收解析完毕
_recv_statu = RECV_HTTP_OVER;
return true;
}
// 2. 当前已经接收了多少正文,其实就是往 _request._body 中放了多少数据了
size_t real_len = content_length - _request._body.size(); // 实际还需要接收的正文长度
// 3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文
// 3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据
if (buf->ReadAbleSize() >= real_len) // 缓冲区可读数据>=实际还需接收的正文长度
{
_request._body.append(buf->ReadPosition(), real_len); // 往正文末尾添加 实际还需接收的正文
buf->MoveReadOffset(real_len); // 移动读偏移
_recv_statu = RECV_HTTP_OVER; // 更新接受状态为
return true;
}
// 3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来
// 缓冲区可读数据<实际还需接收的正文长度 ,说明还有正文没有到来
_request._body.append(buf->ReadPosition(), buf->ReadAbleSize()); // 往正文末尾添加 部分正文
buf->MoveReadOffset(buf->ReadAbleSize()); // 移动读偏移
return true;
}
public:
// 构造函数,初始化状态
HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
// 重置上下文,恢复到初始状态
void ReSet()
{
_resp_statu = 200;
_recv_statu = RECV_HTTP_LINE;
_request.ReSet();
}
// 获取响应状态码
int RespStatu() { return _resp_statu; }
// 获取当前接收状态
HttpRecvStatu RecvStatu() { return _recv_statu; }
// 获取解析后的HttpRequest对象引用
HttpRequest &Request() { return _request; }
// 接收并解析HTTP请求(主入口函数)
void RecvHttpRequest(Buffer *buf)
{
// 不同的状态,做不同的事情,但是这里不要break,因为处理完请求行后,应该立即处理头部,而不是退出等新数据
switch (_recv_statu)
{
case RECV_HTTP_LINE:
RecvHttpLine(buf); // 解析请求行
case RECV_HTTP_HEAD:
RecvHttpHead(buf); // 解析头部
case RECV_HTTP_BODY:
RecvHttpBody(buf); // 解析正文
}
return;
}
};
二.核心总结
要想吃透这个类,就必须先看懂这3个成员变量到底是干啥的
cpp
// HTTP上下文类,负责解析HTTP请求
class HttpContext
{
private:
int _resp_statu; // 响应状态码(解析错误时使用)
HttpRecvStatu _recv_statu; // 当前接收和解析的阶段状态
HttpRequest _request; // 已经解析得到的请求信息
......
}
1. 状态跟踪器:HttpRecvStatu _recv_statu
这个变量是整个解析过程的状态控制中心,采用有限状态机的设计理念,精确记录当前HTTP请求的解析进度。
状态机的工作流程:
- RECV_HTTP_LINE:初始状态,等待解析HTTP请求行(如 "GET /index.html HTTP/1.1")
- RECV_HTTP_HEAD:请求行解析成功后,进入头部解析阶段,处理多个键值对头部字段
- RECV_HTTP_BODY:头部解析完成后(遇到空行),进入请求正文解析阶段
- RECV_HTTP_OVER:整个请求解析完成,可以交付上层业务处理
- RECV_HTTP_ERROR:解析过程中出现错误,需要终止并返回错误响应
关键作用:
- 确保请求按照HTTP协议的正确顺序逐步解析
- 在连接保持活动(Keep-Alive)时,能够正确处理多个连续请求
- 实现渐进式解析,即使请求数据分多次到达,也能保持解析状态不变
重要原则:只有 _recv_statu 处于 RECV_HTTP_OVER 状态,才表示一个完整的HTTP请求已经就绪,可以安全地进行后续业务处理。这是整个HTTP协议解析完成的最终标志。
2. 数据容器:HttpRequest _request
在协议处理上下文HttpContext类里面,我们完全实现了
- 确保从输入缓冲区里面读取我们HTTP的一个完整的请求
- 将读取到的数据解析存放到HttpContext类里面的HttpRequest _request;成员变量里面
- 后续操作我们完全只需要借助这个HttpContext类里面的HttpRequest _request;成员变量来对数据进行处理。
这个变量是解析结果的最终载体,用于存储经过结构化处理的完整HTTP请求信息。
存储内容:
- 请求行元素:HTTP方法(GET/POST等)、请求路径、协议版本
- 请求头部:以键值对形式存储的所有HTTP头部字段
- 请求参数:查询字符串
- 请求正文:适用于POST、PUT等方法的请求体数据
设计优势:
- 将原始字节数据转换为易于操作的结构化对象
- 提供统一的接口访问请求的各个部分,无需关心底层解析细节
- 支持复杂HTTP特性的扩展,如分块传输编码、多部分表单数据等
使用模式:只有状态是 RECV_HTTP_OVER 时,_request 对象已经包含了从当前连接输入缓冲区中提取出的完整HTTP请求信息。
3. 错误指示器:int _resp_statu
这个变量是协议层面的错误状态记录器,用于标识解析过程中遇到的各类协议错误。
错误类型与对应状态码:
- 400 Bad Request:请求格式错误,如非法请求行、无效头部格式
- 414 URI Too Long:请求行或头部超出最大长度限制
- 411 Length Required:缺失必要的Content-Length头部
- 413 Content Too Large:请求正文超过服务器处理能力
- 其他4xx错误:根据具体协议违规情况进行设置
工作模式:
- 在解析的任意阶段检测到协议违规时,将 _recv_statu 设置为 RECV_HTTP_ERROR
- 同时将 _resp_statu 设置为对应的HTTP标准错误状态码
- 上层处理程序可以直接使用该状态码构建错误响应,无需额外判断
重要特性: 这个变量是HTTP响应报文中的状态码来源之一,确保了协议层面的错误能够被准确地反馈给客户端。
如果说里面发生了错误,那么就会设置另外一个成员变量int _resp_statu; ,也就是 响应状态码(解析错误时使用),这个响应状态码是作为HTTP响应报文里面的状态码,那么上层就可以根据这个状态码来判断,我们这个协议处理的过程中是不是发生了一些错误。