【仿Muduo库项目】HTTP模块3——HttpContext子模块

目录

一.HttpContext子模块

1.1.设计思路

1.2.分模块讲解

1.2.1.各个状态

1.2.2.从缓冲区接收并解析HTTP请求行

1.2.3.解析请求行

1.2.4.从缓冲区接收并解析HTTP请求头部

1.2.5.解析HTTP请求头部

1.2.6.从缓冲区接收HTTP请求正文

1.2.7.接收并解析HTTP请求

1.3.完整代码

二.核心总结


还记得我们在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 请求数据可能分多次到达服务器,该模块通过状态机模型来跟踪处理进度,确保请求的完整性。

  1. 分片数据传输问题

在 TCP/IP 网络通信中,HTTP 请求报文可能被分割为多个数据包传输:

  • 第一次接收:请求行和部分头部

  • 第二次接收:剩余头部

  • 第三次接收:请求正文

    需要记录当前处理到哪个阶段,以便后续继续处理。

  1. 状态持久化需求

如果没有状态记录,每次收到新数据都需要重新解析整个请求,效率低下且容易出错。

  1. 错误处理的复杂性

解析过程中可能出现多种错误(格式错误、长度超限、非法路径等),需要精准定位错误类型并返回适当的 HTTP 状态码。


由于HTTP请求可能被分多次到达(例如在网络传输中分成多个数据包),我们需要记录当前处理到哪一步,以便在下次接收到数据时从上次中断的地方继续处理。

为此,我们定义了以下状态,表示当前请求的接收阶段:

  1. 接收请求行(RECV_HTTP_LINE): 正在接收或等待接收请求行(即HTTP请求的第一行)

  2. 接收请求头部(RECV_HTTP_HEAD): 正在接收请求头部,直到遇到空行表示头部结束

  3. 接收请求正文(RECV_HTTP_BODY): 正在接收请求正文,根据Content-Length等头部信息来确定正文长度

  4. 接收完毕(RECV_HTTP_OVER): 请求数据已经全部接收完毕,可以开始处理请求并生成响应

  5. 接收错误(RECV_HTTP_ERROR): 在接收或解析过程中出现错误,如格式错误、超出长度限制等

在请求的接收和解析过程中,可能会遇到各种错误,例如:

  • 请求行格式不符合HTTP规范

  • 请求头部格式错误

  • 请求正文长度超过限制

  • 资源路径包含非法字符或路径遍历攻击(如包含"..")

对于不同的错误,我们需要返回不同的HTTP状态码,例如400(Bad Request)、414(URI Too Long)等。

HttpContext模块提供以下接口:

  1. 接收并处理请求数据(RecvHttpRequest):

    根据当前状态,从缓冲区中读取数据并解析,逐步完成请求行、请求头部和请求正文的接收。

  2. 获取解析完毕的请求信息(Request):

    当状态为RECV_HTTP_OVER时,返回解析完成的HttpRequest对象,用于后续的业务处理。

  3. 获取响应状态码(RespStatu):

    在解析过程中如果出现错误,会设置响应状态码,以便后续生成错误响应。

  4. 获取当前接收状态(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函数的一个应用。它的作用是将一个字符串(或更一般地,一个序列)中的每个字符转换为大写形式。让我详细解释一下:

  1. std::transform是C++标准模板库(STL)中的一个算法,用于将一个序列(或两个序列)中的每个元素进行转换,并将结果存储到指定的目标序列中。

  2. 在这个具体的调用中,它使用了三个迭代器和一个函数指针(或函数对象):

    • 第一个参数:_request._method.begin() 这是源序列的起始迭代器。

    • 第二个参数:_request._method.end() 这是源序列的结束迭代器。

    • 第三个参数:_request._method.begin() 这是目标序列的起始迭代器。注意,这里和第一个参数相同,这意味着转换后的结果将存回原序列(即就地转换)。

    • 第四个参数:::toupper 这是一个函数指针,指向C标准库中的toupper函数(注意前面的::表示全局命名空间)。这个函数接受一个int类型的参数(字符的ASCII码),并返回该字符的大写形式(如果它是小写字母的话)。

  3. 因此,这行代码的作用是:遍历_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响应报文里面的状态码,那么上层就可以根据这个状态码来判断,我们这个协议处理的过程中是不是发生了一些错误。

相关推荐
瓦尔登湖懒羊羊2 小时前
到底能不能把HTTP1.0讲明白了
http
杰克逊的日记2 小时前
网络问题定位与排查
网络·it
2502_911679143 小时前
重新定义测试边界:N5181A信号发生器,何以成为射频领域的性能标杆?
网络·科技·信号处理
小李独爱秋3 小时前
计算机网络经典问题透视:TLS协议工作过程全景解析
运维·服务器·开发语言·网络协议·计算机网络·php
亲爱的非洲野猪3 小时前
Java线程池深度解析:从原理到最佳实践
java·网络·python
以太浮标4 小时前
华为eNSP模拟器综合实验之- VLAN-QinQ技术解析
运维·网络·华为·信息与通信
Xの哲學4 小时前
Linux epoll 深度剖析: 从设计哲学到底层实现
linux·服务器·网络·算法·边缘计算
九成宫4 小时前
计算机网络期末复习——第4章:网络层 Part One
网络·笔记·计算机网络·软件工程
小白不想白a5 小时前
linux排障:服务端口被打满
linux·服务器·网络