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


    目录

    请求读取

    读取请求行

    读取报头

    分析请求

    分析请求行

    分析请求报头

    处理报文   

    报文的存在

     报文的读取

    总结 


     

    请求读取

    1. class HttpRequest{
    2. public:
    3. //读取报文后的内容填充
    4. std::string request_line;
    5. std::vector request_header;
    6. std::string blank;
    7. std::string request_body;
    8. };

    我们在上一篇讲解了日志的实现与应用,本篇我们来讲解请求的读取与解析,我们构建好了http服务端,我们需要对发来的请求进行读取和处理,使得我们的http了解建立链接的客户端的详细的信息,这个时候我们就需要构建一套读取分析请求报文的一套体系。

    1. class EndPoint{
    2. private:
    3. int sock;
    4. HttpRequest http_request;
    5. HttpResponse http_response;
    6. public:
    7. void RecvHttpPoint()
    8. {
    9. RecvHttpRequestLine();//读取请求行
    10. RecvHttpRequestHeader();//读取请求报头
    11. }
    12. };

    读取请求行

    我们如何来读取请求行呢,其实我们在第一篇的时候讲过,报文的第一行就是请求行,我们直接获取报文的第一行即可。

    【项目实战】自主实现 HTTP 项目(一)_萌小檬的博客-CSDN博客

    1. void RecvHttpRequestLine()
    2. {
    3. auto &line =http_request.request_line;
    4. Util::ReadLine(sock,line);
    5. line.resize(line.size()-1);
    6. LOG(INFO,http_request.request_line);
    7. }

    这个地方我们要注意的是,我们不想让其读取到 \n也带入其中,使得打印出来的内容有少许散乱,我们这个可以用resize把\n给删去。

    (以下为之前第一篇演示行输出的截图)

    读取报头

    读取报头其实就涉及到报头和有效载荷分离的问题,简而言之就是你怎么保证报头读完了而且没有多读其他内容,我们可以两方面保证,学习过http报文的都知道,第一,报头和有效载荷中间是以空行作为分离的,所以我们当读取到空行的时候,就证明我们报头读取完成了。第二,报头的形式都是以key:value按行陈列的,所以读取的时候我们依旧可以按行读取,当读取到空行的时候,说明已经读取完成了。

    1. void RecvHttpRequestHeader()
    2. {
    3. std::string line;
    4. while(true)
    5. {
    6. line.clear();
    7. Util::ReadLine(sock,line);
    8. if(line == "\n")
    9. {
    10. http_request.blank = line;
    11. break;
    12. }
    13. line.resize(line.size()-1);
    14. http_request.request_header.push_back(line);
    15. LOG(INFO,line);
    16. }
    17. }

    需要注意的是:

    1.我们每次获取一行之后,把它插入到我们的报头字符串中,获取的line需要重新清空才可以继续获取,否则就会把之前的内容再插入一遍。

    2.和之前一样,我们不希望把\n写道日志中,这里可以利用resize处理一下。

    我们这里再进行一下测试 

    用浏览器去链接一下:

     

     

     我们就读取到了其请求行和报文,以key:value的形式显示出来。

    分析请求

    1. void ParseHttpRequest()
    2. {
    3. ParseHttpRequestLine();//分析请求行
    4. RarseHttpRequestHeader();//分析报头
    5. }

    分析请求行

    我们之前已经知道了,在请求行包括的内容有其请求方法[GET 或者是POST],请求内容[uri],以及版本号,现在我们想要把他们在请求行上提取出来,这个时候我们这个时候我们可以用之前C语言中分割字符串的一些函数接口,但是这里我们更推荐的是stringstream这个函数。

     为了明确这个函数的用法,我们可以做一个小小的测试,我们模拟一个请求行,然后尝试让stringstream函数进行分割,看一下效果:

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. std::string msg ="GET /a/b/c.html http/1.0";
    7. std::string method;
    8. std::string uri;
    9. std::string version;
    10. std::stringstream ss(msg);
    11. ss >> method >> uri >> version;
    12. std::cout<< method << std::endl;
    13. std::cout<< uri << std::endl;
    14. std::cout<< version << std::endl;
    15. return 0;
    16. }

    编译链接通过,运行,我们看到了下面的现象: 

     这个时候我们就明确了,stringstream函数是可以把请求行分割的,这个时候,我们就可以进行分析请求行的编写了。

    1. void ParseHttpRequestLine()
    2. { auto &line = http_request.request_line;
    3. std::stringstream ss(line);
    4. ss >> http_request.method >> http_request.uri >>http_request.version ;
    5. LOG(INFO,http_request.method);
    6. LOG(INFO,http_request.uri);
    7. LOG(INFO,http_request.version);
    8. }

    我们把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的值。

    1. static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
    2. {
    3. size_t pos = target.find(sep);
    4. if(pos != std::string::npos){
    5. sub1_out = target.substr(0, pos);
    6. sub2_out = target.substr(pos+sep.size());
    7. return true;
    8. }
    9. return false;
    10. }
    1. #define SEP ": "
    2. void ParseHttpRequestHeader()
    3. {
    4. std::string key;
    5. std::string value;
    6. for(auto &iter : http_request.request_header)
    7. {
    8. if(Util::CutString(iter, key, value, SEP)){
    9. http_request.header_kv.insert({key, value});
    10. }
    11. }
    12. }

    这里我们可以做一个简单的测试,看看是否把key与value分开了(debug测试后删除)

     我们可以看到实现了key与value的分离

     

    处理报文   

     

    报文的存在

    因为有些请求是没有报文的,我们在这里明确一下,首先常见的请求类型有GET和POST两种,而GET类型是没有报文的,所以处理请求一般都是POST类型。

    这个时候问题就来了,我们应该读取多少报文呢,如果读取报文不是正确的,有可能会造成数据包的粘包问题,所以这个时候,我们就用到了报文中的“content_lenth”这个内容,由它来决定读取多少字节。

    1. bool IsNeedRecvHttpRequestBody()
    2. {
    3. auto &method = http_request.method;
    4. if(method == "POST"){
    5. auto &header_kv = http_request.header_kv;
    6. auto iter = header_kv.find("Content-Length");
    7. if(iter != header_kv.end()){
    8. LOG(INFO, "Post Method, Content-Length: "+iter->second);
    9. http_request.content_length = atoi(iter->second.c_str());
    10. return true;
    11. }
    12. }
    13. return false;
    14. }

    【提醒】:因为在unorder_map中value的值是string的形式,我们想要把它变成具体的数值,就应该atoi转化为整数的值。

     报文的读取

    因为我们已经有了具体的报文长度,我们就按照报文的长度读取即可。

    1. void RecvHttpRequestBody()
    2. {
    3. if(IsNeedRecvHttpRequestBody()){
    4. int content_length = http_request.content_length;
    5. auto &body = http_request.request_body;
    6. char ch = 0;
    7. while(content_length){
    8. ssize_t s = recv(sock, &ch, 1, 0);
    9. if(s > 0){
    10. body.push_back(ch);
    11. content_length--;
    12. }
    13. else{
    14. break;
    15. }
    16. }
    17. LOG(INFO, body);
    18. }
    19. }

    【说明】:这里我们是按照content_length--一个字符一个字符读取的,因为我们这里是对content_length的拷贝,所以我们--不影响原来content_length的内容。

    我在这里在梳理一下我们这一部分的逻辑,我们是先判断是否是POST的方法,如果不是,就没有报文,如果是,那么就从map中查找content_length,并把其长度的具体值填充到对象的body长度中,然后我们拿到了报文长度,就一个字符一个字符的读取即可。

    总结 

    截至目前,我们其实可以把之前的处理步骤做一个合并,其实就是我们处理请求其实就是五步,第一步读取请求行,第二步读取请求报头,第三步分析请求行,第四步分析请求报头,第五步处理报文(如果有)。

    1. void RecvHttpPoint()
    2. {
    3. RecvHttpRequestLine();//读取请求行
    4. RecvHttpRequestHeader();//读取报头
    5. RecvHttpRequestBody(); //分析请求行
    6. ParseHttpRequestLine();//分析报头
    7. ParseHttpRequestHeader();//处理报文
    8. }

  • 相关阅读:
    VUE3,AXIOS
    Zookeeper集群Leader选举源码剖析
    web前端期末大作业:基于HTML+CSS+JavaScript制作我的音乐网站(带设计报告)
    java虚拟机垃圾回收相关概念
    2022年8月15日陌陌推荐算法工程师面试题5道|含解
    【语义分割】DeepLab v1
    哪些专业跟芯片有关?
    MMDetection 使用示例:从入门到出门
    springSecurity登录的全过程
    《王者荣耀》怎么代理?今天又有个朋友问这个话题
  • 原文地址:https://blog.csdn.net/m0_61703823/article/details/126599536