【毕业项目】自主设计HTTP(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【毕业项目】自主设计HTTP(二)

请求读取与解析

一个完整的请求报文如下

89f31e9143b140f3a572db79f8c1efe9.png

我们读取请求行只需要读取第一行即可

void RecvRequestLine()    
    {    
      Util::ReadLine(_sock ,http_request._request_line);    
    }

那么 我们应该如何读取行数不确定的请求报头呢?

我们现在知道的有两点

  1. 请求报头是一行一行发送的
  2. 请求报头读取完毕之后下一个一定是空行

有了上面这两点之后我们就能很轻松的读取完所有的请求报头

只需要按行读取并且读取到\n结束即可

    void RecvRequestLine()    
    {    
      Util::ReadLine(_sock ,http_request._request_line);    
    }    
    void RecvRequestHeader()    
    {    
      std::string line;    
      while(true)    
      {    
        line.clear();    
        Util::ReadLine(_sock , line);                                                                                 
        if (line == "\n")    
        {    
          break;    
        }    
        line.resize(line.size()-1); // remove \n    
        http_request._request_header.push_back(line);    
      }    
      if (line == "\n")    
      {    
        http_request._blank = line;    
      }    
    } 

解析请求行

读取到请求行和请求报头之后我们就开始解析它们

首先是请求行 格式如下

d6176826387f47658254f3be4c971b86.png

std::string method;
    std::string uri;
    std::string version;

所以说我们只需要按照空格作为分隔符 将请求行的代码分隔为三部分即可

而我们这里推荐使用stringstream 这个类中重载了流插入运算符默认会按照空格来进行分隔给字符串赋值 用法代码示例如下

    void ParseHttpRequestLine()
    {
      auto& line = http_request._request_line;
      std::stringstream ss(line);
      ss >> http_request.method >> http_request.uri >> http_request.version;                                          
    }

解释下上面这段代码 我们首先使用line拷贝构造一个stringstream对象 之后让这个对象分别以空格为分隔符将它的内容赋值给http_request.method http_request.uri http_request.version

解析请求报头

80f819f7ead3464c9a7ae1fb06059d1d.png

经过观察我们不难发现 这里其实就是一个键值对结构 所以说我们使用哈希表来存储即可

有关于哈希表部分知识不理解的同学可以参考我的这篇博客

unordered_map

但是首先我们需要写一个方法让一个字符串按照冒号分隔为两部分

具体实现方法如下

   static bool CutString(const std::string& target , std::string& key_out , std::string& value_out , std::string sep){      
  size_t pos = target.find(sep);
  if (pos != std::string::npos)     
  {                                
    key_out = target.substr(0 ,pos);
    value_out = target.substr(pos + sep.size());
    return true;                                    
  }                 
  return false;                                                                                                       
}

之后我们只需要遍历整个请求报头 一个个分割key和value之后插入哈希表中就可以

代码表示如下

    void ParseHttpRequestHeader()    
    {    
      std::string key;    
      std::string value;    
      for (auto& iter : http_request._request_header)    
      {    
        Util::CutString(iter , key , value , SEP);    
        http_request.header_kv.insert({key,value});    
      }                                                                                                               
    } 

处理请求正文

处理完上面的两个部分之后我们再回过来看请求报文的图

c848f5ce957e4625bc2ecc05a8eff619.png

此时我们就面临两个问题了

  1. 是否存在正文
  2. 如果存在正文 那么正文有多少个字节

关于第一个问题

一般来说我们的请求方法如果是GET 则一般没有正文

如果我们的请求方法是POST 则一般有正文

关于第二个问题

我们可以由请求报头中的 Content–legth字段来知晓

判断是否存在正文的代码如下

    bool IsRecvBody()    
    {    
      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())    
        {    
          http_request.content_length = atoi(iter->second.c_str());     
          return true;    
        }    
      }                                                                                                               
      return false;    
    }   

读取正文代码如下

   void RecvRequestBody()    
    {    
      if (IsRecvBody())
      {
        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;
          }
        }
      }
    }

至此 我们的请求和解析报文全部完成

处理请求并且构建响应

在处理这个请求之前 我们首先要考虑这个请求是否是合法的请求

如果是非法的请求 我们应该怎么操作

假设我们现在的Http服务器只接收GET和POST方法的请求 其他的请求只给予一个错误的响应即可 (我们这里最开始默认为404 NOT FOUND)

代码如下

    void BuildResponse()    
    {    
      if (http_request.method != "GET" && http_request.method != "POST")    
      {    
      }    
    } 

对于响应报文来说 最重要的一部分就是我们的状态码了

fcbb16febd054c8bb71ac79643c2b916.png

HTTP状态码

HTTP的状态码如下:

编号 类别 意义
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕

3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出错

其中我们要记住的有下面这几个

101 信息请求中

101表示客户端发送的请求正在处理中 但是因为网速变快 这种状态已经不怎么常见了

200 OK

这是最常见的一个状态码 也就是我们访问网页成功的时候网页返回的响应行

301 永久重定向

比如果一个老的网站废弃不用了使用一个新的网站 那么此时这个网站就可以使用永久重定向 如果有人还在访问这个网站就会跳转到新网站上

此外如果收藏夹中收藏了老的网站 新的网站会覆盖收藏夹中老的网站

302 307 临时重定向

从名字看就更好理解了 和301永久重定向相比一个是永久的一个是临时的 它并不会覆盖掉收藏夹中的老网站

403 权限不足

这个常见于我们去实习的时候 自己的权限特别低 如果leader丢给你一个文档而你没有观看的权限就会出现这个状态码

404 NOT FOUND

常见于资源消失不见(被删除或过期) 又或者说资源根本不存在

比如说你访问一个网站的时候带上一个不存在的资源路径你就会看到这个状态码

504 Bad Gateway

常见于服务器出现问题 和客户端无关

Redirection(重定向状态码)

除了上面那些要记住的状态码之外 我们还需要更深入的理解重定向状态码

重定向又分为永久重定向和临时重定向 其中301表示永久重定向 302 307表示临时重定向

临时重定向和永久重定向本质是影响客户端的标签 决定客户端是否需要更新目标地址

如果某个网站是永久重定向 那么第一次访问该网站时由浏览器帮你进行重定向 但后续再访问该网站时就不需要浏览器再进行重定向了 此时你访问的直接就是重定向后的网站

而如果某个网站是临时重定向 那么每次访问该网站时如果需要进行重定向 都需要浏览器来帮我们完成重定向跳转到目标网站

获取参数

其实目前从宏观的角度上来说 我们的上网行为可以分为两种

  1. 浏览器向服务器上传数据
  2. 浏览器向服务器请求数据f8a0e90c809b4ded8ea96519d6281f34.png

其中向浏览器上传数据的时候我们有两种方法

  1. 使用GET方法 通过url来进行传参
  2. 使用POST方法 通过正文来进行传参

客户端为什么要将数据上传到服务器呢?

当然是为了让服务器对客户端传输上来的数据进行处理

这里根据GET和POST传参方式的不同 我们处理请求的方式也需要发生一点变化

由于POST方法传参是通过正文传递参数的 而正文我们已经获取到了 所以说不用关心

但是GET方法传参是通过url传参 而url在请求行中 需要我们特殊处理一下

处理的代码如下 其实就是复用了我们前面剪切字符串的功能函数

      auto& code = http_response.response_code;     
      if (http_request.method != "GET" && http_request.method != "POST")    
      {    
        // waring request     
        code = NOT_FOUND;    
        goto END;    
      }    
      if (http_request.method == "GET")    
      {    
        size_t pos = http_request.uri.find("?");    
        if (pos != std::string::npos)    
        {    
          Util::CutString(http_request.uri,http_request.path, http_request.query_string , "?");    
        }    
        else    
        {    
          http_request.path = http_request.uri;                                                                                
        }    
      }    
      std::cout << "debug url: " << http_request.uri << std::endl;    
      std::cout << "debug path: " << http_request.path << std::endl;    
      std::cout << "debug query_string: " << http_request.query_string << std::endl; 

下面是运行结果

d6b54ca0647d4535bf2e192eb2ba2c9b.png

Web根目录

我们得到rul之后可以看到这样的一串标识符

44c615a85d5b4c7b918e6c7b02adf96e.png

  1. 这里的路径表明了是请求Linux服务器上的某种资源 那么这种资源是从哪里开始的呢?是根目录嘛?
  2. 这个路径对应的资源是如何判断存在的

问题一:资源是从哪里开始的

这个资源不一定是从根目录开始的

一般来说我们会自己指定一个web根目录 所有的资源都在这个web根目录当中

一般来说web根目录的名称是wwwroot如下

34524937618f4fa8a17e9d6eb6966922.png

如果说访问我们服务器的客户端没有指明想要获取什么资源的话我们肯定不可能将web根目录下的所有资源全部发出去

所以说这个时候我们就要指定一个默认的资源 一般来说这个资源就是index.html

6e9250ab2fa74e319ae4868dd835f079.png

所以说此时我们的path就不能简单的是/a/b/c了

4ffbb0846fc84cb99eca24381255b7cb.png

我们要在前面加上web根目录在Linux服务器中的定位 比如说像这样

wwwroot(web根目录的路径) 加上 /a/b/c

定位代码如下

#define WEB_ROOT "wwwroot/"
std::string _path = http_request.path;
http_request.path = WEB_ROOT;
http_request.path += _path;
std::cout << "debug: " << http_request.path << std::endl;   

演示效果如下

e49b4db4dd164f6b8c9ec83a32737d7f.png

我们前面也说过了 如果客户端请求的是Web根目录的话我们不可能将整个Web根目录的所有资源全部给他 所以说针对于请求web根目录的情况我们要做一些特殊处理

如果请求的是我们的web根目录我们就返回index.html页面给它

代码表示如下

      #define HOME_PAGE "index.html"
      http_request.path = WEB_ROOT;    
      http_request.path += _path;    
      if (http_request.path[http_request.path.size()-1] == '/')    
      {    
        http_request.path += HOME_PAGE;    
      }    
      std::cout << "debug: " << http_request.path << std::endl;  

演示效果如下

7c2550e4f5de411994184187945f3b4b.png

问题二 : 如何确认这个资源是存在的

我们首先使用百度来试验下 如果资源不存在会怎么样

be824eb408d0458987135d5d91c192a1.png

可以发现 百度服务器直接给我们返回了一个404告知我们该资源不存在

所以说我们在返回给客户端请求之前需要确认一个资源是否存在

这里使用确认资源是否存在的函数是stat函数

stat函数

函数原型如下

int stat(const char* path , struct stat *buf)

参数说明:

  • const char* path 是我们要寻找的路径(是一个字符串)
  • struct stat *buf 这是一个结构体 我们通过该结构体来查看文件的信息

返回值说明:

如果找到该文件返回0 如果没找到返回-1

      struct stat st;
      if (stat(http_request.path.c_str() , &st) == 0)
      {     
        // exist                                                
      }     
      else 
      {     
        // not exist 
        code = NOT_FOUND;
        goto END;
      }

所有的目录中都有一个index.html嘛?

是的 因为一个目录中可能有着大量的网页资源 如果我们不设置一个默认的index.html则系统就不知道应该返回哪个资源给客户端了

在回答了问题二之后便会衍生出一个问题三

存在的资源就是可以读取的资源嘛?

  • 存在的资源可能是一个目录
  • 存在的资源可能是一个可执行程序

请求的资源是目录

我们可以使用下面的方法来判断是否请求的资源是一个目录

S_ISDIR(st.st_mode)

S_ISDIR是一个宏 而st_mode则是stat结构体中的一个成员变量

使用这个宏我们就能够判断当前请求的资源是否是一个目录

如果是一个目录 我们前面介绍过 每个目录都会有一个index.html 所以我们直接在请求的路径上加上即可

请求的资源是可执行程序

一般来说 客户端请求可执行程序是被允许的

但是我们这里要对于这种情况做一下特殊处理

我们通过下面的方法来确定文件是否是一个可执行文件

        if ((st.st_mode&S_IXUSR) || (st.st_mode&S_IXGRP) || (st.st_mode&S_IXOTH))    
        {                                                                  
          // special treatment                                                                                        
        }    

关于如何特殊处理 本文后面会详细讲解

HTTP CGI机制简单介绍

CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一 有着不可替代的重要地位

CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准 是在CGI程序和Web服务器之间传递信息的过程

其实 要真正理解CGI并不简单 首先我们从现象入手

浏览器除了从服务器下获得资源(网页 图片 文字等) 有时候还有能上传一些东西(提交表单 注册用户之类的) 看看我们目前的http只能进行获得资源 并不能够进行上传资源所以目前http并不具有交互式 为了让我们的网站能够实现交互式 我们需要使用CGI完成 时刻记着 我们目前是要写一个http 所以 CGI的所有交互细节 都需要我们来完成


理论上 可以使用任何语言来编写CGI程序


需要注意的是 http提供CGI机制 和CGI程序是两码事 就好比学校(http)提供教学(CGI机制)平台 学生(CGI程序)来学习

反应到具体的web服务器中 什么是CGI机制呢

我们首先创建一个handerdate.exe作为一个可执行文件

8834fb3198d6404b896755e38a7d1b8d.png

之后的过程如下

  • 浏览器传输数据给服务器中的HTTPSERVER
  • HTTPSERVER接收到数据之后不做处理 将输出传递给HANDERDATE
  • HANDERDATE处理完数据之后再将处理完的数据传递给SERVER
  • HTTPSERVER接收到数据之后将处理过的数据传递给浏览器


ca23602bb9d74032b7209f3e6b72b814.png

调用目标程序 传递目标数据 拿到目标结果 这中间用到的就是CGI技术

那么我们什么时候需要用到CGI技术呢?

答案是只要用户上传上来数据此时我们就要用到CGI技术 此时我们只需要将cgi标志位设置为开启即可

最后我们通过判断cgi标志位是否开启来判断使用什么方法 代码表示如下

      if (http_request.cgi)      
      {      
         ProcessCgi();      
      }      
      else      
      {      
        ProcessNonCgi(); // return html                                                                               
      }    

关于大小写转化的问题

因为我们对于GET和POST方法并没有做出严格的大小写规定 而我们在项目中却使用了大写作为判定条件

d76dab72493f4a5983537a530ec03af8.png

这就有可能会导致一些错误的发生 所以说我们保证我们接收的method要转化为大写

C++提供了一个函数来实现这个功能

  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op)

参数说明:

  • 第一个参数是要转化起始位置的迭代器
  • 第二个参数是要转化末尾位置的迭代器
  • 第三个参数是最后要存放结果的位置的迭代器
  • 第四个参数是要转化的方式(大写或者小写)

代码表示如下

      auto& method = http_request.method;    
      std::transform(method.begin() , method.end() , method.begin() , ::toupper );    

构建响应

构建响应之前我们首先来回顾下响应的报文是什么样子的

f85cee8ec1904666b1f54ca7cad47573.png

它要有状态行 响应报头 空行 响应正文

所以说我们不单单要只返回一个静态网页(正文) 还需要加上前面的请求行 报头等信息

添加状态行

状态行由http版本 状态码 状态码描述符组成

其中我们默认http版本就是1.0版本

默认的状态码是200(OK)

状态码描述需要和状态码相匹配

此时我们只要设置一个函数 传入code输出一个状态码的string对象就可以 代码如下

static std::string Code2Desc(int code)    
{        
  std::string desc;    
  switch(code)    
  {      
    case 200:    
      desc = "OK";    
      break;    
    case 404:    
      desc="Not Found";    
      break;                                               
    default:                          
      break;                           
  }               
    return desc; 
 }

之后一步步写好状态行就好了

      http_response._response_line = HTTP_VERSION;             
      http_response._response_line += " ";                     
      http_response._response_line += std::to_string(http_response.response_code);    
      http_response._response_line += " ";                     
      http_request._request_line += Code2Desc(http_response.response_code); 

我们设置默认的响应行分隔符为 \r\n

#define LINE_END "\r\n"

之后响应报头的内容我们这里暂时跳过

添加响应正文

在前面我们已经获取了请求读取的路径

一般来说现在我们只需要根据那个路径打开对应的资源 之后将资源写到报文的正文中即可

但是在实际填写正文的过程中我们会遇到这样子的问题

  • 我们写的body是用户层的缓冲区
  • 我们需要的网页html是磁盘中的文件
  • 磁盘中的文件要到用户层必须要经历内核层
  • 所以说如果我们使用read write等函数则IO效率较低

857a369a494d4b46938331bee54b265c.png

这里给大家介绍一个函数 sendfile

它的作用是不用经历用户层 直接在内核缓冲区拷贝数据 从而提高效率

它的函数原型如下

ssize_t sendfile(int out_fd, int in_fd , off_t* set , size_t count)

参数说明:

  • out_fd 是我们要往这里写数据的文件描述符
  • in_fd 是我们要从这里读数据的文件描述符
  • set 我们不用管 设置为空即可
  • count 表示我们要拷贝的数据大小 以字节为单位

返回值说明:

如果成功拷贝则返回成功拷贝的字节数 失败返回-1

发送响应

我们一步步将状态行 响应报头 空行 正文发送即可

      void SendResponse()    
      {    
        write(_sock ,http_response._response_line.c_str() , http_response._response_line.size());    
        for(auto it : http_response._response_header)    
        {    
          write(_sock, it.c_str() , it.size());    
        }    
          write(_sock,http_response._blank.c_str() , http_response._blank.size());    
          sendfile(_sock , http_response.fd , nullptr , http_response.size);    
          close(http_response.fd);    
      }    

最后我们将index.html中写上hello world

编译运行后使用浏览器尝试接收响应

f34f936f856344f5b711bc5afe301d64.png

运行结果如下

0f1aba604f4b47bdaa8954a22e72bc52.png

由于博主并没有系统的学习前端知识 所以说这里就不写网页的前端了

如果有同学感兴趣可以自己写一些前端的代码放到web根目录下

添加响应报头

响应报头有很多字段可以填充 我们这里只填充两个比较重要的字段

一个是Content-length 即正文的大小

一个是Content-type 即正文的类型

正文的大小其实我们之前已经有过了 这里我们只需要插入到报头中即可 代码如下

      std::string content_length_string = "Content-Length: ";
      content_length_string += std::to_string(size);  

而正文的类型则是我们比较难判断的一点

一般来说我们会根据文件名的后缀来判断这个文件是什么类型 当构建响应的时候我们也需要告知浏览器我们返回的是什么类型的资源

所以说我们的第一步操作就是后缀提取

      found = http_request.path.rfind(".");    
      if (found == std::string::npos)    
      {    
        http_request.suffix = ".html";    
      }    
      else    
      {    
        http_request.suffix = http_request.path.substr(found);    
      }  

Content-Type在文件后缀和自身之间有一张对照

329671ac74bf4d68ab65f2909a430ba0.png

所以说在我们截取了文件的后缀之后还需要在表中找到对应的内容

我们这里为了方便起见使用静态函数的方式来帮助我们找到后缀对应的内容

同学们也可以尝试使用类来封装

static std::string Suffix2Desc(const std::string& suffix)     
{    
  static std::unordered_map<std::string , std::string> suffix2desc ={    
    {".html" , "text/html"},    
    {".css" , "text/css"},    
    {".jpg" , "text/html"}    
  };    
  auto iter = suffix2desc.find(suffix);    
  if (iter != suffix2desc.end())    
  {    
    return iter->second;    
  }    
  return "text/html";                                                                                                 
} 
相关文章
|
6月前
|
Java API Spring
SpringBoot项目调用HTTP接口5种方式你了解多少?
SpringBoot项目调用HTTP接口5种方式你了解多少?
546 2
|
6月前
|
前端开发
webpack如何设置devServer启动项目为https协议
webpack如何设置devServer启动项目为https协议
1181 0
|
6月前
|
JavaScript
如何让Vue项目本地运行的时候,同时支持http://localhost和http://192.168.X.X访问?
如何让Vue项目本地运行的时候,同时支持http://localhost和http://192.168.X.X访问?
|
3月前
|
Linux Python
【Azure 应用服务】Azure App Service For Linux 上实现 Python Flask Web Socket 项目 Http/Https
【Azure 应用服务】Azure App Service For Linux 上实现 Python Flask Web Socket 项目 Http/Https
|
4月前
|
Java 数据库连接 应用服务中间件
表单数据返回不到,HTTP状态 404 - 未找未找到,解决方法,针对这个问题,写一篇文章,理一下思路,仔细与原项目比对,犯错的原因是Mapper层的select查询表单数据写错,注意打开的路径对不对
表单数据返回不到,HTTP状态 404 - 未找未找到,解决方法,针对这个问题,写一篇文章,理一下思路,仔细与原项目比对,犯错的原因是Mapper层的select查询表单数据写错,注意打开的路径对不对
|
5月前
|
开发工具 git
MAC如何使用Git命令行上传本地项目及理解,failed to push some refs to ‘https://gitee.com/brother-barking/spxx.git
MAC如何使用Git命令行上传本地项目及理解,failed to push some refs to ‘https://gitee.com/brother-barking/spxx.git
|
6月前
|
JSON 前端开发 搜索推荐
BoostCompass( http_server 模块 | 项目前端代码 )
BoostCompass( http_server 模块 | 项目前端代码 )
63 4
|
6月前
|
Shell 开发工具 数据安全/隐私保护
git上传项目一直报一个文件没有添加任何内容(git上拉去别人的项目上传到自己的仓库/error: failed to push some refs to ‘https://gitee.com/)
git上传项目一直报一个文件没有添加任何内容(git上拉去别人的项目上传到自己的仓库/error: failed to push some refs to ‘https://gitee.com/)
308 0
git上传项目一直报一个文件没有添加任何内容(git上拉去别人的项目上传到自己的仓库/error: failed to push some refs to ‘https://gitee.com/)
|
2月前
|
监控 安全 搜索推荐
设置 HTTPS 协议以确保数据传输的安全性
设置 HTTPS 协议以确保数据传输的安全性
|
30天前
|
安全 网络协议 算法
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
134 4
HTTPS网络通信协议揭秘:WEB网站安全的关键技术