这个模块用于记录 HTTP 请求的接收和处理进度,因为有可能出现一种情况,就是接收的数据并不是一条完整的 HTTP 请求数据,也就是请求的处理需要在多次收到数据后才能处理完成,所以在每次处理的时候,就需要将处理进度记录起来,以便于下次从当前进度继续向下处理,最终得到一个完整 HttpRequest 请求信息对象,因此在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理的节奏。
这个模块要关心的要素如下所示:
- 已经接收并处理的请求信息
- 接收状态:(即当前处于何种接收阶段,方便根据不同的阶段调用不同的函数)
- 接收请求行(当前处于接收并处理请求行的阶段)
- 接收请求头部(表示请求头部的接收还没有完毕)
- 接收正文(表示还有正文没有接收完毕)
- 接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
- 接收处理请求出错
- 响应状态码:
- 在请求的接收并处理中,有可能会出现各种不同的问题,比如解析出错、访问的资源不对、没有权限等等,而这些错误的响应状态码都是不一样的。
所以需要提供以下接口:
- 接收并处理请求数据:
- 接收请求行
- 解析请求行
- 接收头部
- 解析头部
- 接收正文
- 获取解析完毕的请求信息
- 获取响应状态码
- 获取接收解析状态
// RECV_LINE:接收请求行(当前处于接收并处理请求行的阶段)
// RECV_HEADER:接收请求头部(表示请求头部的接收还没有完毕)
// RECV_BODY:接收正文(表示还有正文没有接收完毕)
// RECV_DONED:接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
// RECV_ERROR:接收处理请求出错
typedef enum {
RECV_LINE,
RECV_HEADER,
RECV_BODY,
RECV_DONED,
RECV_ERROR
} HTTP_RECV_STATUS;
const int MAX_LINE_SIZE = 8192;
class HttpContext
{
private:
int _response_status; // 响应状态码
HTTP_RECV_STATUS _recv_status; // 当前接收的阶段
HttpRequest _request; // 存放已经接收并处理的请求信息
public:
HttpContext();
// 获取响应状态码
int get_response_status();
// 获取接收解析状态
HTTP_RECV_STATUS get_recv_status();
// 获取解析完毕的请求信息
HttpRequest& get_request();
// 接收并处理请求数据
void recv_and_handle_request(Buffer* buffer);
void reset();
private:
// 接收请求行
bool recv_line(Buffer* buffer);
// 解析请求行(内部会解析完将各字段放到请求模块对象中)
bool parse_line(const std::string& line);
// 接收头部(大部分和上面的接收请求行是重合的,注意不同的地方即可)
bool recv_header(Buffer* buffer);
// 解析头部
bool parse_header(std::string& line);
// 接收正文
bool recv_body(Buffer* buffer);
};Ⅱ. 接口实现
1、几个核心接口
几个提供给服务器模块使用接口实现起来并不难,如下所示:
// RECV_LINE:接收请求行(当前处于接收并处理请求行的阶段)
// RECV_HEADER:接收请求头部(表示请求头部的接收还没有完毕)
// RECV_BODY:接收正文(表示还有正文没有接收完毕)
// RECV_DONED:接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
// RECV_ERROR:接收处理请求出错
typedef enum {
RECV_LINE,
RECV_HEADER,
RECV_BODY,
RECV_DONED,
RECV_ERROR
} HTTP_RECV_STATUS;
const int MAX_LINE_SIZE = 8192;
class HttpContext
{
private:
int _response_status; // 响应状态码
HTTP_RECV_STATUS _recv_status; // 当前接收的阶段
HttpRequest _request; // 存放已经接收并处理的请求信息
public:
HttpContext()
: _response_status(200)
, _recv_status(RECV_LINE)
{}
// 获取响应状态码
int get_response_status() { return _response_status; }
// 获取接收解析状态
HTTP_RECV_STATUS get_recv_status() { return _recv_status; }
// 获取解析完毕的请求信息
HttpRequest& get_request() { return _request; }
// 接收并处理请求数据
void recv_and_handle_request(Buffer* buffer)
{
// 不同的状态,做不同的事情,但是这里不能break,因为处理完请求行后,应该立即处理头部,而不是退出等新数据
switch(_recv_status)
{
case RECV_LINE: recv_line(buffer);
case RECV_HEADER: recv_header(buffer);
case RECV_BODY: recv_body(buffer);
}
}
void reset()
{
_response_status = 200;
_recv_status = RECV_LINE;
_request.reset();
}
};2、接收 && 解析请求行
下面几个接口都是私有的,就是内部的处理函数,其实就是分三个阶段进行处理:请求行阶段、头部阶段、正文阶段。这里我们先来介绍请求行阶段的两个处理函数:接收以及解析请求行!
首先就是接收请求行的操作,无非就是利用 Buffer 模块中的获取一行的接口 get_line() 获取请求行,然后判断两种特殊情况:一行没有读取完毕、一行数据超过服务器规定大小(一般是8K),做这个判断主要是防止恶意的传入过长请求而导致消耗过多服务器资源!
获取成功的话则调用解析请求行函数 parse_line() 开始解析请求行,在其内部解析完请求行后将各字段存放到请求模块对象中进行保存!
最后别忘了要移动 buffer 的读指针,还要将所处状态改为接收头部状态!
// 接收请求行
bool recv_line(Buffer* buffer)
{
// 1. 接收请求行之前,判断当前是否处于接收请求行的阶段
if(_recv_status != RECV_LINE)
return false;
// 2. 获取缓冲区中的一行
std::string line = buffer->get_line_andMove();
// 3. 判断两种特殊情况:请求行没有读取完毕、请求行超过服务器规定(一般是8K)
if(line.size() == 0)
{
/* 如果此时请求行没有读取完毕,而且缓冲区中的数据是超过MAX_LINE_SIZE的,
说明数据很长都不足一行,这已经是有问题的了,那么请求行肯定是超过MAX_LINE_SIZE了 */
if(buffer->get_sizeof_read() > MAX_LINE_SIZE)
{
_recv_status = RECV_ERROR;
_response_status = 414; // 414表示URI太长了
return false;
}
return true; // 返回true表示没有读取完毕,不算错误
}
if(line.size() > MAX_LINE_SIZE)
{
_recv_status = RECV_ERROR;
_response_status = 414; // 414表示URI太长了
return false;
}
// 4. 获取成功的话则调用parse_line()开始解析请求行(其内部会解析完将各字段放到请求模块对象中)
bool ret = parse_line(line);
if(ret == false)
return false;
// 5. 将所处状态改为接收头部状态
_recv_status = RECV_HEADER;
return true;
} 而对于解析请求行函数 parse_line(),它是由 recv_line() 调用的,此时拿到的是请求行的字符串,该函数的功能就是将该字符串中的各个字段解析出来,存放到请求模块对象中去,这里就用到了我们之前讲过的正则表达式来进行解析字段!
通过正则表达式解析完后结果放到了 std::smatch 类型的结果集中去,我们可以通过 [] 操作来获取它的结果,然后将它们各自放到请求模块中的对应字段去!
// 解析请求行(内部会解析完将各字段放到请求模块对象中)
bool parse_line(const std::string& line)
{
// (GET|POST|HEAD|PUT|DELETE) 表示匹配并提取其中任意一个字符串
// [^?]* [^?] 匹配非问号字符,后边的*表示 0次或多次
// \\?(.*) \\? 表示原始的 ? 字符,(.*)表示提取 ? 之后的任意字符 0 次或多次,直到遇到空格
// (?:\\?(.*))? (?: ...) 表示匹配某个格式字符串,但是不提取,所以就是表示匹配了上一行注释 0 次或 1 次,并且不获取该内容
// HTTP/1\\.[01] 表示匹配以 HTTP/1. 开始,后边有个 0 或 1 的字符串
// (?:\n|\r\n)? (?: ...) 表示匹配某个格式字符串,但是不提取,而最后的 ? 表示的是匹配前边的表达式 0 次或 1 次
// 注意要使用icase表示忽略请求方法的大小写
std::regex rule("(GET|POST|HEAD|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
std::smatch matches; // 结果集
bool ret = std::regex_match(line, matches, rule); // 进行表达式匹配,将匹配结果放到结果集中
if (ret == false)
{
_recv_status = RECV_ERROR;
_response_status = 400; // BAD REQUEST
return false;
}
// 举个例子,此时"GET /liren/login?user=xiaoming&pass=123123 HTTP/1.1\r\n" 的结果如下所示:
// 0 : GET /liren/login?user=xiaoming&pass=123123 HTTP/1.1
// 1 : GET
// 2 : /liren/login
// 3 : user=xiaoming&pass=123123
// 4 : HTTP/1.1
// 1. 请求方法的获取
_request._method = matches[1];
std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper); // 注意要将方法转化为大写
// 2. 资源路径的获取,需要对其进行url解码,但是不需要将+转化为空格
_request._path = Util::url_decode(matches[2], false);
// 3. 协议版本的获取
_request._version = matches[4];
// 4.1 查询字符串的获取,先获取每个key=val的结构也就是键值对组合
std::vector<std::string> strs;
int size = Util::split(matches[3], "&", &strs);
// 4.2 然后再分解获取每个key和val
for(int i = 0; i < size; ++i)
{
std::vector<std::string> key_val;
int n = Util::split(strs[i], "=", &key_val);
// 此时如果只有key没有val的话则是错误的
if(n == 1)
{
_recv_status = RECV_ERROR;
_response_status = 400;
return false;
}
// 正确获取的话则对key和val先进行url解析,此时就需要将+转化为空格,然后将它们设置进请求对象中保存
std::string key = Util::url_decode(key_val[0], true);
std::string val = Util::url_decode(key_val[1], true);
_request.set_queryString(key, val);
}
return true;
}3、接收 && 解析请求头部
其实这里请求部分的接收和请求行的处理是大体相同的,不同之处在于头部有多行,所以要用死循环,直到当前头部读到的字符串是 \r\n 或者 \n 的时候才退出循环,其它的操作都是类似的!
如果不是结束行的话,则会在获取到结果后调用解析头部函数 parse_header() 进行处理!
// 接收头部(大部分和上面的接收请求行是重合的,注意不同的地方即可)
bool recv_header(Buffer* buffer)
{
// 1. 接收头部之前,判断当前是否处于接收头部的阶段
if(_recv_status != RECV_HEADER)
return false;
// 因为头部有多行,所以要用死循环
while(true)
{
// 2. 获取缓冲区中的一行
std::string line = buffer->get_line_andMove();
// 3. 判断两种特殊情况:一行没有读取完毕、请求行超过服务器规定(一般是8K)
if(line.size() == 0)
{
/* 如果此时请求行没有读取完毕,而且缓冲区中的数据是超过MAX_LINE_SIZE的,
说明数据很长都不足一行,这已经是有问题的了,那么请求行肯定是超过MAX_LINE_SIZE了 */
if(buffer->get_sizeof_read() > MAX_LINE_SIZE)
{
_recv_status = RECV_ERROR;
_response_status = 414; // 表示一行数据太多
return false;
}
return true; // 返回true表示没有读取完毕,不算错误
}
if(line.size() > MAX_LINE_SIZE)
{
_recv_status = RECV_ERROR;
_response_status = 414; // 表示一行数据太多
return false;
}
// 4. 如果读的头部是\n或者\r\n的话,表示头部接收结束了,则将所处状态改为接收正文状态,然后退出循环,
if(line == "\r\n" || line == "\n")
break;
// 5. 获取成功的话则调用parse_line()开始解析请求行(其内部会解析完将各字段放到请求模块对象中)
bool ret = parse_header(line);
if(ret == false)
return false;
}
_recv_status = RECV_BODY;
return true;
} 而解析头部相比解析请求行要简单,因为头部是一个固定的 key: val 的格式,很好分割获得键值对!要注意的是因为我们接收头部的时候,调用的接收数据接口 get_line() 是将 \r\n 或者 \n 都读取上来了,但是它不应该被包含在键值对中,所以在解析头部之前,应该先将 \n 和 \r 去掉!
// 解析头部
bool parse_header(std::string& line)
{
// 1. 末尾是\n或者\r换行则要去掉
if (line.back() == '\n') line.pop_back();
if (line.back() == '\r') line.pop_back();
// 2. 根据key: val的格式,进行分割获取头部的key和val
std::vector<std::string> key_val;
int size = Util::split(line, ": ", &key_val);
if(size <= 1)
{
_recv_status = RECV_ERROR;
_response_status = 400;
return false;
}
// 3. 将key和val设置进请求对象中保存
_request.set_header(key_val[0], key_val[1]);
return true;
}4、接收正文
接收正文并不需要进行解析,只需要拿到正文后设置到请求模块对象中的正文字段中即可!
要注意的是因为当前读取到的正文还不是完整的,所以我们需要先计算还需要接收的正文长度 real_length,然后判断一下当前缓冲区数据是否大于等于 real_length,是的话则获取 real_length 部分的数据,就是当前正文的数据!
如果缓冲区数据小于 real_length 的话,那就只能先获取当前缓冲区的数据,然后直接返回 true,等待服务器模块再次调用该接收正文函数读取剩余的正文,而服务器判断是否需要再次调用该接收正文函数的依据就是根据当前的接收状态来判断,如果是接收完毕了,则状态为 RECV_DONED,如果不是的话则依然还是处于 RECV_BODY 状态!
// 接收正文
bool recv_body(Buffer* buffer)
{
// 1. 接收正文之前,判断当前是否处于接收正文的阶段
if(_recv_status != RECV_BODY)
return false;
// 2. 从头部中获取正文长度
size_t size = _request.get_body_length();
if(size == 0)
{
// 没有正文,则请求解析完毕
_recv_status = RECV_DONED;
return true;
}
// 3. 计算还需要接收的正文长度(因为可能前面因为数据只接收了部分)
size_t real_length = size - _request._body.size();
if(buffer->get_sizeof_read() >= real_length)
{
// 3.1 若缓冲区中的数据包含了当前请求的所有正文,则取出所需的数据,然后设置状态为接收完毕即可
_request._body.append(buffer->start_of_read(), real_length);
buffer->push_reader_back(real_length);
_recv_status = RECV_DONED;
return true;
}
// 3.2 若缓冲区中的数据无法满足当前正文的需要,也就是数据不足,则取出数据,然后等待新数据到来,不用修改接收状态
_request._body.append(buffer->start_of_read(), buffer->get_sizeof_read());
buffer->push_reader_back(buffer->get_sizeof_read());
return true;
}