目录
- class HttpRequest{
-
- public:
- //读取报文后的内容填充
- std::string request_line;
- std::vector
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
- #include
- #include
-
- 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();//处理报文
- }