目录
模块设计思想
上下文模块是Http协议模块中最重要的一个模块,他需要控制请求处理的节奏,需要保存一个HttpRequest对象,后续关于这个连接的http的处理的信息全部都是在这个上下文中保存。
首先,上下文模块需要控制http请求接收和处理的节奏,那么我们其实是需要用一个变量来表明当前处于处理报文的哪一个阶段。可以用一个枚举量来作为处理的进度或者状态。
//处理状态
enum HttpRecvStatu{
RECV_ERR, //接收错误
RECV_LINE, //接收请求行
RECV_HEAD, //接收头部
RECV_BODY, //接收正文
RECV_OVER //接收完毕
};
同时,由于收到的http请求报文可能是会出错的,而出错的话,我们是不会将这个报文进行业务的处理的,而是直接返回一个请求错误的状态码的报文,那么我们的上下文当中不可避免的还需要保存一个变量用来保存状态码。
当然还需要保存一个HttpRequest对象用来存储获取到的请求的要素。
//请求处理上下文
class HttpContext
{
public:
HttpRecvStatu _recv_statu; //处理进度
int _resp_statu; //响应状态码
HttpRequest _req; //请求
public:
HttpContext():_recv_statu(RECV_LINE),_resp_statu(200){}
};
接口分析
他提供给外部的接口其实很少,一个是获取响应状态码,一个是获取处理进度,还有就是获取到内部的Request对象,以及接收并解析http请求的接口,当然也还需要一个Reset接口,还是一样的,可能是长连接,后续还有报文需要处理。
HttpContext():_recv_statu(RECV_LINE),_resp_statu(200){}
void Reset();
int RespStatu()const;
HttpRecvStatu RecvStatu()const;
HttpRequest& GetRequest();
void RecvHttpRequest();
这其中最复杂的接口就是RecvHttpRequest也就是接收并解析Http请求的接口,这个接口我们需要使用多个接口进行辅助处理。
模块代码实现
首先实现前四个简单的接口:
void Reset()
{
_recv_statu = RECV_LINE;
_resp_statu = 200;
_req.Reset();
}
int RespStatu()const {return _resp_statu;}
HttpRecvStatu RecvStatu()const {return _recv_statu;}
HttpRequest& GetRequest() {return _req;}
然后就是最重要得接收数据得接口了。
我们先来把解析请求的几个小接口写出来。
首先,我们第一步需要解析请求行,要解析请求行首先需要获取请求行,那么我们就需要两个接口,一个是提取出请求行,一个是解析请求行。
解析请求行的时候我们使用的正则表达式是这个:
(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?
在这个正则表达式的匹配结果中,如果我们的url中没有携带参数,那么参数部分的匹配结果就是一个空串,他也是在matches里面的,这一点我们不需要关心,因为后续我们解析参数的时候会将这种情况给他处理了。
//解析处理请求行
void RecvLine(Buffer* buffer)
{
if(_recv_statu != RECV_LINE) return;
//1 获取请求行
std::string line = buffer->GetLineAndPop();
if(line == "") //没有获取到一行,此时需要判断是数据不够还是因为数据接受错误了
{
if(buffer->ReadSize() > MAX_LINE_SIZE) //大于8192
{
_recv_statu = RECV_ERR; //说明解析错误,报文在接收的时候有问题
_resp_statu = 414; //url too long
}
return;
}
//2 解析请求行
return HandlerLine(line);
}
void HandlerLine(const std::string& line)
{
if(_recv_statu != RECV_LINE) return;
//使用正则表达式进行解析
std::smatch matches;
std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
bool ret = std::regex_match(line,matches,e);
if(ret == false) //说明请求出错
{
_recv_statu = RECV_ERR;
_resp_statu = 400; //Bad Request
return;
}
//走到这里说明正则匹配成功,而smacth是重载了方括号运算符的,我们可以直接使用
_req._method = matches[1];
//这里我们需要对方法进行处理,将其转换成大写,因为可能会有不标准的请求将方法写成小写。
std::transform(_req._method.begin(),_req._method.end(),_req._method.begin(),::toupper);
//前两个参数表示转换的数据的范围,第三个参数表示转换之后的目的地址,第四个参数表示转换的方法,使用C库的全局的toupper函数来转大写
_req._path = Util::UrlDecode(matches[2],false); //url需要进行解码,不需要+转空格
std::string params = Util::UrlDecode(matches[3],true); //参数需要及逆行解码,需要+转空格
//然后就是将参数解析为kv的格式
std::vector<std::string> arr;
Util::Split(params,"&",&arr); // 参数以param进行分割
for(auto&s:arr) //然后逐个提取每一个kv式的参数
{
std::vector<std::string> kv;
int ret = Util::Split(s,"=",&kv);
if(ret != 2) //如果不是一个kv,那么就报错
{
_recv_statu = RECV_ERR;
_resp_statu = 400; //Bad Request
return;
}
//提取出来就放到参数的 map 中
_req.AddParam(kv[0],kv[1]);
}
//最后就是提取版本号
_req._version = matches[4];
}
然后就是获取解析头部字段
头部字段和正文之间的间隔就是一个 \r\n ,也就是说如果我们某一次提取一行内容,只提取到一个 \r\n 或者\n,那么说明这就是我们的空行了,头部字段提取完了。
//获取解析头部字段
void RecvHeader(Buffer* buffer)
{
if(_recv_statu != RECV_HEAD) return;
//提取每一行
while(1)
{
//1 获取头部字段
std::string line = buffer->GetLineAndPop();
if(line == "") //没有获取到一行,此时需要判断是数据不够还是因为数据接受错误了
{
if(buffer->ReadSize() > MAX_LINE_SIZE) //大于8192
{
_recv_statu = RECV_ERR; //说明解析错误,报文在接收的时候有问题
_resp_statu = 414; //url too long
}
return;
}
//2 解析头部字段
if(line == "\r\n" || line == "\n")
{
//头部字段解析完了
_recv_statu = RECV_BODY;
return;
}
//否则解析这一行
bool ret = HandlerHeader(line);
if(!ret) return; //因为头部字段可能会有问题,需要使用一个返回值来判别
}
}
bool HandlerHeader(std::string& line)
{
if(_recv_statu != RECV_HEAD) return false;
//先去掉回车和换行
if(line.back() == '\n') line.pop_back();
if(line.back() == '\r') line.pop_back();
std::vector<std::string> kv;
int ret = Util::Split(line,": ",&kv);
if(ret != 2)
{
_recv_statu = RECV_ERR;
_resp_statu = 400; //Bad Request
return false;
}
_req.AddHeader(kv[0],kv[1]);
return true;
}
最后就是获取正文的接口,正文的获取其实也很简答。 如果请求中携带正文,那么头部字段中就一定会携带 Content-Length 字段,所以我们可以通过这个正文长度字段来决定我们要获取的正文的大小。 但是并不是说必须一次就把所有的正文全部接受,有可能当前缓冲区的数据并不是完整的正文,而是正文的一部分,这时候我们也要先接收,不能让他烂在缓冲区中。 那么未来我们需要根据_body.size 和正文长度这两个字段来判断从缓冲区中拿多少数据。
//获取正文
void RecvBody(Buffer* buffer)
{
if(_recv_statu != RECV_BODY) return;
int len = _req.ContentLength();
if(len ==0)
{
_recv_statu = RECV_OVER; //没有正文,直接返回
return;
}
int size = len - _req._body.size(); //还需要接收的正文的长度
if(size > buffer->ReadSize()) //说明不够,能读多少就读多少
{
_req._body.append(buffer->ReadPosition(),buffer->ReadSize());
buffer->MoveReadOffset(buffer->ReadSize());
return;
}
//走到这里说明能读完当前请求的正文
_req._body.append(buffer->ReadPosition(),size);
buffer->MoveReadOffset(size);
_recv_statu = RECV_OVER;
}
那么功能接口都写出来了,读取请求的接口无非就是调用上面的这些接口了,那么怎么设计呢?很简单,使用一个switch case 语句就完事了。
void RecvHttpRequest(Buffer* buffer)
{
switch(_recv_statu)
{
case RECV_LINE: RecvLine(buffer); //不需要break,因为可能提取完请求行之后还有后续的内容可以提取,就算请求行没提取完,每一个提取的函数前面有一个判断,可以直接返回
case RECV_HEAD: RecvHeader(buffer);
case RECV_BODY: RecvBody(buffer);
}
}
那么我们的上下文模块的接口就实现完了,目前我们也只进行编译,没有发现问题。
后续测试整个服务器的时候如果出现问题我们会再来修正bug。