【项目实战】自主实现 HTTP 项目(三)——请求读取与解析以及报文处理

目录

请求读取

读取请求行

读取报头

分析请求

分析请求行

分析请求报头

处理报文   

报文的存在

 报文的读取

总结 


 

请求读取

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();//处理报文
     }

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值