目录
请求读取
class HttpRequest{
public:
//读取报文后的内容填充
std::string request_line;
std::vector<std::string> request_header;
std::string blank;
std::string request_body;
};
我们在上一篇讲解了日志的实现与应用,本篇我们来讲解请求的读取与解析,我们构建好了http服务端,我们需要对发来的请求进行读取和处理,使得我们的http了解建立链接的客户端的详细的信息,这个时候我们就需要构建一套读取分析请求报文的一套体系。
class EndPoint{
private:
int sock;
HttpRequest http_request;
HttpResponse http_response;
public:
void RecvHttpPoint()
{
RecvHttpRequestLine();//读取请求行
RecvHttpRequestHeader();//读取请求报头
}
};
读取请求行
我们如何来读取请求行呢,其实我们在第一篇的时候讲过,报文的第一行就是请求行,我们直接获取报文的第一行即可。
【项目实战】自主实现 HTTP 项目(一)_萌小檬的博客-CSDN博客
void RecvHttpRequestLine()
{
auto &line =http_request.request_line;
Util::ReadLine(sock,line);
line.resize(line.size()-1);
LOG(INFO,http_request.request_line);
}
这个地方我们要注意的是,我们不想让其读取到 \n也带入其中,使得打印出来的内容有少许散乱,我们这个可以用resize把\n给删去。
(以下为之前第一篇演示行输出的截图)
读取报头
读取报头其实就涉及到报头和有效载荷分离的问题,简而言之就是你怎么保证报头读完了而且没有多读其他内容,我们可以两方面保证,学习过http报文的都知道,第一,报头和有效载荷中间是以空行作为分离的,所以我们当读取到空行的时候,就证明我们报头读取完成了。第二,报头的形式都是以key:value按行陈列的,所以读取的时候我们依旧可以按行读取,当读取到空行的时候,说明已经读取完成了。
void RecvHttpRequestHeader()
{
std::string line;
while(true)
{
line.clear();
Util::ReadLine(sock,line);
if(line == "\n")
{
http_request.blank = line;
break;
}
line.resize(line.size()-1);
http_request.request_header.push_back(line);
LOG(INFO,line);
}
}
需要注意的是:
1.我们每次获取一行之后,把它插入到我们的报头字符串中,获取的line需要重新清空才可以继续获取,否则就会把之前的内容再插入一遍。
2.和之前一样,我们不希望把\n写道日志中,这里可以利用resize处理一下。
我们这里再进行一下测试
用浏览器去链接一下:
我们就读取到了其请求行和报文,以key:value的形式显示出来。
分析请求
void ParseHttpRequest()
{
ParseHttpRequestLine();//分析请求行
RarseHttpRequestHeader();//分析报头
}
分析请求行
我们之前已经知道了,在请求行包括的内容有其请求方法[GET 或者是POST],请求内容[uri],以及版本号,现在我们想要把他们在请求行上提取出来,这个时候我们这个时候我们可以用之前C语言中分割字符串的一些函数接口,但是这里我们更推荐的是stringstream这个函数。
为了明确这个函数的用法,我们可以做一个小小的测试,我们模拟一个请求行,然后尝试让stringstream函数进行分割,看一下效果:
#include<iostream>
#include<string>
#include<sstream>
int main()
{
std::string msg ="GET /a/b/c.html http/1.0";
std::string method;
std::string uri;
std::string version;
std::stringstream ss(msg);
ss >> method >> uri >> version;
std::cout<< method << std::endl;
std::cout<< uri << std::endl;
std::cout<< version << std::endl;
return 0;
}
编译链接通过,运行,我们看到了下面的现象:
这个时候我们就明确了,stringstream函数是可以把请求行分割的,这个时候,我们就可以进行分析请求行的编写了。
void ParseHttpRequestLine()
{ auto &line = http_request.request_line;
std::stringstream ss(line);
ss >> http_request.method >> http_request.uri >>http_request.version ;
LOG(INFO,http_request.method);
LOG(INFO,http_request.uri);
LOG(INFO,http_request.version);
}
我们把line分割成三部分,以此填充到http_server的对应内容,这样我们就实现完成了分析请求行的任务。
我们用浏览器来进行一下测试,这里我们带一个自己编的网址,来进行一下验证
我们可以看到下面的现象:
验证了我们分析请求行成功了。
分析请求报头
我们想要分析一组报头,最关键的就是分割他们 key 和value的值,并且我们想要哪找哪一个key就可以找到对应的value,这里我们推荐用 map 这样的存储结构,但是问题来了,我们怎么才能保证我们把key和value分离开来呢?
在这里,我们介绍两个函数:
我们知道,find函数可以帮助我们找到对应内容位置的下标,并且返回对应下标
substr可以帮助我们从0开始截取长度为len的字符串,这个地方我们要注意,因为key和value中间是以冒号和空格来进行区分的,我们肯定上来查找的是冒号,假设冒号之前有pos个字符,又因为下标是从0开始,所以冒号的下标就是pos,那么len的长度就相当于是pos-0=pos,而substr是一个左闭右开的区间,也就是说我们从0位置读取长度为pos的值就是key的值,而pos+分隔符长度到这一行的结尾是value的值。
static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
{
size_t pos = target.find(sep);
if(pos != std::string::npos){
sub1_out = target.substr(0, pos);
sub2_out = target.substr(pos+sep.size());
return true;
}
return false;
}
#define SEP ": "
void ParseHttpRequestHeader()
{
std::string key;
std::string value;
for(auto &iter : http_request.request_header)
{
if(Util::CutString(iter, key, value, SEP)){
http_request.header_kv.insert({key, value});
}
}
}
这里我们可以做一个简单的测试,看看是否把key与value分开了(debug测试后删除)
我们可以看到实现了key与value的分离
处理报文
报文的存在
因为有些请求是没有报文的,我们在这里明确一下,首先常见的请求类型有GET和POST两种,而GET类型是没有报文的,所以处理请求一般都是POST类型。
这个时候问题就来了,我们应该读取多少报文呢,如果读取报文不是正确的,有可能会造成数据包的粘包问题,所以这个时候,我们就用到了报文中的“content_lenth”这个内容,由它来决定读取多少字节。
bool IsNeedRecvHttpRequestBody()
{
auto &method = http_request.method;
if(method == "POST"){
auto &header_kv = http_request.header_kv;
auto iter = header_kv.find("Content-Length");
if(iter != header_kv.end()){
LOG(INFO, "Post Method, Content-Length: "+iter->second);
http_request.content_length = atoi(iter->second.c_str());
return true;
}
}
return false;
}
【提醒】:因为在unorder_map中value的值是string的形式,我们想要把它变成具体的数值,就应该atoi转化为整数的值。
报文的读取
因为我们已经有了具体的报文长度,我们就按照报文的长度读取即可。
void RecvHttpRequestBody()
{
if(IsNeedRecvHttpRequestBody()){
int content_length = http_request.content_length;
auto &body = http_request.request_body;
char ch = 0;
while(content_length){
ssize_t s = recv(sock, &ch, 1, 0);
if(s > 0){
body.push_back(ch);
content_length--;
}
else{
break;
}
}
LOG(INFO, body);
}
}
【说明】:这里我们是按照content_length--一个字符一个字符读取的,因为我们这里是对content_length的拷贝,所以我们--不影响原来content_length的内容。
我在这里在梳理一下我们这一部分的逻辑,我们是先判断是否是POST的方法,如果不是,就没有报文,如果是,那么就从map中查找content_length,并把其长度的具体值填充到对象的body长度中,然后我们拿到了报文长度,就一个字符一个字符的读取即可。
总结
截至目前,我们其实可以把之前的处理步骤做一个合并,其实就是我们处理请求其实就是五步,第一步读取请求行,第二步读取请求报头,第三步分析请求行,第四步分析请求报头,第五步处理报文(如果有)。
void RecvHttpPoint()
{
RecvHttpRequestLine();//读取请求行
RecvHttpRequestHeader();//读取报头
RecvHttpRequestBody(); //分析请求行
ParseHttpRequestLine();//分析报头
ParseHttpRequestHeader();//处理报文
}