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

简介: 【毕业项目】自主设计HTTP(三)

设计CGI程序

当浏览器请求的资源是一个可执行文件的时候

57f06abe09cc4d72aa056f7e3b3ab8d2.png

此时我们的服务器就会触发CGI模式

ea5d8d9a9137436fb22fa367eb7b05e2.png

现在我们就可以开始编写CGI程序了

ed5975c662a44509ac5570300ec4bc83.png

根据上面的原理图我们可以知道

  • httpsever是一个进程
  • CGI程序也是一个进程
  • 那么我们应该如何用一个进程去执行另外一个进程呢?

其实早在进程控制章节我们就学过了方法 那就是进程替换

当然我们在讲解程序替换的时候也说过 我们不能使用主进程进行进程替换(使用主进程该进程就变成一次性的了 处理不了下一个任务) 而应该使用子进程

整体的代码如下

    int ProcessCgi()    
    {    
      pid_t pid = fork();    
      if (pid == 0)    
      {    
        // child    
      }    
      else if (pid < 0)    
      {    
        // error    
        LOG(ERROR , "fork error");    
        return 404;    
      }    
      else    
      {    
        // father                                                                                                     
        waitpid(pid , nullptr , 0);    
      }    
      return OK;    
    }  

我们创建子进程的目的当然是为了让他去执行目标程序

那么目标程序是什么呢 它实际上就是浏览器传输给我们的path

进程间通信

建立管道

httpsever需要将数据传输给cgi程序 cgi程序处理完数据之后也需要将数据回传给httpsever

所以说我们这里就要用到进程间通信

因为httpsever进程和我们设计的cgi进程之间本质上是一个父子进程的关系 所以说我们选用进程间通信中的匿名管道

而由于此时我们需要数据进行双向传递 所以说我们可以设计一个双向的管道

为了不混淆这个双向管道的读取 我们约定 所有操作都站在父进程的视角上命名

代码表示如下

        int input[2];
        int output[2]; 
        if (pipe(input) < 0)
        {   
          return 404;
        }
        if (pipe(output) < 0)
        {    
          return 404;    
        }  
       pid_t pid = fork();
        if (pid == 0)
        {
          // child
          close(input[0]);
          close(output[1]);    
        }    
        else if (pid < 0)
        {    
          // error    
          LOG(ERROR , "fork error");                                                                                  
          return 404;
        }
        else 
        {
          // father
          close(input[1]);
          close(output[0]);
          waitpid(pid , nullptr , 0);
        }
        return OK;
      }

进程替换

我们选择使用execl函数来进行进程替换

函数原型如下

int execl(const char *path, const char *arg, ...);

我们先看这个函数的名字 相比我们的exec多了一个l

这个l其实就是列表的意思 意味着它的参数要使用列表的形式传入

它的第一个参数是 const char *path 它代表着要执行程序的路径

它的第二个参数是 const char *arg, ... 它代表着可变参数列表 是使用NULL结尾的

例如我们要执行ls程序的话 就可以写出下面的代码

execl("/usr/bin/ls" , "ls" , "-a" , "-i" , NULL);

当然如果我们直接使用路径作为可执行程序也是可以的 比如

execl("/usr/bin/ls" , "/usr/bin/ls" , "-a" , "-i" , NULL);

所以说我们程序替换的代码是

execl(bin.c_str() , bin.c_str() , nullptr);

我们在进行程序替换之后把原先子进程的程序和代码全部替换了

那么替换后的子进程如何得知原先的管道信息呢

代码和数据全部没有了 但是我们要知道的是也仅仅是代码和数据没有了

它并不替换内核进程相关的数据结构 实际上原先子进程的文件描述符表依旧存在

但是数据已经被我们全部删除了 我们要怎么找到呢这两个文件描述符呢?

此时我们可以做出以下的约定

  • 让读取管道等价于读取标准输入
  • 让写入管道等价于写入标准输出

而由于标准输入和标准输出的文件描述符是固定的

所以说我们直接进行重定向即可

dup2(input[1] , 1);
        dup2(output[0], 0);

交互数据

在交互数据之前我们首先要知道父进程的数据在哪里

对于GET方法来说父进程的数据一定是在uri当中

对于POST方法来说 父进程的数据一定是在正文当中

POST方法处理

代码表示如下

          if (method == "POST")
          {
            const char* start = body_text.c_str();
            int total = 0;
            int size = 0;
            while(1)
            {
              size = write(output[1] , start+total , body_text.size()-total);
              if (size > 0)
              {
                total+=size;
              }                                                                                                       
              else 
              {
                break;
              }
            }
          }

上面的代码我们做了一个小处理 让write一直写 直到写入成功的数据为0为止 这主要是为了防止数据太多 一次write写不完的情况出现

GET方法处理

首先我们要知道一点 进程替换是不会替换环境变量的 而子进程会继承父进程的环境变量 所以说我们可以直接使用父进程或者没有替换过的子进程的环境变量给替换后的子进程传递数据

代码标识如下

          if (method == "GET")    
          {            
            query_string_env = "QUERY_STRING=";    
            query_string_env += query_string;    
           putenv(query_string.c_str());                       
          }   

传递传参方法

在子进程接收数据之前我们还要让子进程确认一点

  • 浏览器传递参数使用的到底是什么方法

此时我们还是可以通过环境变量让子进程知道是用什么方法传递的参数

        method_env = "METHOD=";    
        method_env += method;    
        putenv((char *)method_env.c_str());   

CGI程序接收数据

我们首先写出一个CGI程序编译并且将这个程序放到wwwroot目录中

CGI程序代码如下

#include <iostream>    
#include <cstdlib>    
using namespace std;    
int main()    
{    
  cerr  << "Debug Test :" << getenv("METHOD") << endl;          
  return 0;    
}   

这里需要注意的是我们使用的是cerr而不是cout 这是因为我们的cout使用的是标准输出 而标准输出已经被我们重定向了 如果使用cout将不会在显示器上输出任何结果

22cde123a71d4008ac2559e68556cd54.png

此时我们发现确实cgi程序确实能够得到浏览器传参的方法

接下来就是根据传参方法的不同使用不同的方式去获得数据了 具体为

  • GET方法 使用环境变量获得数据
  • POST方法 使用管道获得数据

代码表示如下

  if (method  == "GET")    
  {    
    query_string = getenv("QUERY_STRING");    
    cerr << "Debug QUERY_STRING: " << query_string << endl;       }    
  else if (method == "POST")    
  {    
    int cl = atoi(getenv("CONTENT_LENGTH"));    
    char c = 0;    
    while(cl)    
    {    
      read(0 , &c , 1);    
      query_string.push_back(c);    
      cl--;    
    }    
  }    
  else     
  {    
    ;                                                           
  } 

CGI程序处理数据

我们假设接收的数据是 x=100&y=200

那么首先我们先要得到各个参数的名称和值 很简单的一个字符串分隔即可

void CutString(string& in, const string& sep, string& out1 , string& out2)
{
  auto pos = in.find(sep);
  if (string::npos == pos)
  {    
    return;    
  }    
  out1 = in.substr(0 , pos);                                                                                 
  out2 = in.substr(pos+sep.size());    
} 

值得注意的是 第二个参数最好加上const修饰 原因有二

  • 分隔符一般是不做修改的
  • 如果我们不加const修饰 则分隔符必须要用string对象 而不能进行隐式类型转换 比如说填写“=” 这样子就是不可以的

does not name a type 错误

博主在使用auto推导pos类型的时候遇到了这个错误

实际上在这里这个错误的产生的原因是在makefile文件中没有使用c++11来编译该文件的 加上-std=c++11之后错误即可解决

之后我们调用函数处理这批数据即可

  string str1;
  string str2;
  CutString(query_string , "&" , str1 , str2);
  string name1;    
  string value1;    
  CutString(str1 , "=" , name1 , value1);    
  string name2;                                                 
  string value2;    
  CutString(str2 , "=" , name2 , value2);   

8b53b7abb4b4405cb254a9523534e493.png

父进程读取处理完的数据

在cgi程序中 我们让程序向管道中写入了自己处理完的数据

到了父进程当中 我们只需要让父进程从管道中读取数据即可 代码如下

        char ch = 0;    
        while(read(input[0] , &ch , 1))                                                                               
        {    
          response_body.push_back(ch);    
        } 


CGI程序总结

我们的CGI程序总结可以浓缩为下面的一张图

5363c06e33194fb193a8f3c6c68251c7.png简单介绍下

  1. 首先浏览器通过GET或者POST方法将想要请求的资源和参数(如果有的话)上传服务器
  2. 服务器进行判断方法是POST还是GET 是否带参
  3. 如果不带参则直接构建响应返回
  4. 如果携带参数则根据方法获得参数
  5. 父进程创建管道之后使用fork函数创建子进程(cgi程序)
  6. 之后通过管道传递数据给子进程处理
  7. 子进程处理完数据之后将处理完的数据传递给父进程
  8. 之后父进程构建响应将响应传递给浏览器

我们如果将中间的步骤全部省略 就能得到这样一个图


b72fb7e230124bcda21708dc926cbcdd.png

  • cgi程序从浏览器获取数据
  • cgi程序加工完数据之后再传递给浏览器

这样设计有什么好处呢?

实际上我们使用cgi模式将通信和服务高度解耦了

这使得我们的cgi程序不必关注通信细节 只需要专心设计好服务即可

状态码介绍

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表示临时重定向

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

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

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

本项目中只使用了状态码404来减少工作量

在同学们围绕这个项目做拓展的时候可以参考上面的状态码写出更多 代码大同小异 难度也不大 主要是html页面的编写

处理读取出错

一般来说我们的http项目会出现两种类型的错误

  • 逻辑错误 读取完毕 我们要给予对方回应 告诉对方为什么出错
  • 读取错误 读取不一定完毕 此时不给对方回应 退出即可

此处我们针对读取错误做出一些处理

首先我们在EndPoint类中设置一个成员变量bool stop 并且在构造函数中将它默认设定为false

之后我们将EndPoint里面的一些读取函数中加上读取错误判定

一旦我们判定此处读取错误则设置stop = true

此处列举几处需要添加读取判断的地方 具体内容可以参考gitee上的源码(在文章的最后)

    bool RecvRequestLine()                                                                                            
    {    
      if(Util::ReadLine(_sock ,http_request._request_line) >0 )    
      {    
        std::cout << http_request._request_line ;    
      }    
      else    
      {    
        stop = true;    
      }    
      return stop;    
    }  
    bool RecvRequestHeader()
    {
      std::string line;
      while(true)
      {
        line.clear();
        if (Util:: ReadLine(_sock , line) <= 0)
        {
          stop = true;
        }
        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;
      }
        return stop;
    }

如果说stop为true 则我们在最后就不构建和发送数据了 代码表示如下

          if (!ep->Stop())                                                                                                     
          {    
            ep->BuildResponse();    
            ep->SendResponse();    
          }  

处理写入错误

进程间通信这一章中我们讲过两个进程通信的四种特殊情况

这其中有一种情况就是 读取端关闭 写入端就会强制关闭

实际上我们在深入理解之后也知道了 这是因为操作系统发送了13号信号的缘故

而在我们这次的项目当中 服务器是要尽量保持24h开机的 不可能因为读取端退出就关闭

所以说我们要忽略13号信号的作用 代码表示如下

signal(SIGPIPE , SIG_IGN);

多线程转化线程池

目前我们所使用的执行任务的方式是多线程 这种方式有以下的缺点

  • 每次都是任务来之后再创建线程 浪费时间
  • 每次都需要创建和销毁线程 浪费时间
  • 如果线程数量并发过多会影响系统性能 从而导致卡顿

而以上的问题我们都可以通过线程池来解决一部分

所以说为了增强代码的健壮性我们将原本的多线程模式转化为线程池模式

c1f09bb3eb1d4c41ad1cdd090b16daff.png


设计思路如下

  1. 在服务器中设计一个任务队列
  2. 每次浏览器上传任务就上传到服务器的任务队列中
  3. 在服务器中设计一个线程池
  4. 线程池中的线程从task_queue中拿任务来执行

有细心的同学可能发现了 这实际上就是一个生产者消费者模型

那么我们首先来设计一个任务类

设计任务类

任务类只需要有两个成员变量

  • 套接字
  • 处理方法

套接字是为了让线程知道要处理什么

处理方法是为了让线程只要要用什么方法处理

代码表示如下

  #pragma once     
  #include <iostream>                                                                                                          
  class Task    
  {    
    private:    
      int sock;    
      CallBack handler;    
    public:    
      Task()    
      {}    
      Task(int _sock)    
        :sock(_sock)
      {}    
      void ProcessOn()    
      {    
        handler(sock);    
      }    
      ~Task()    
      {}    
  }; 

设计回调函数

调用函数处理这个工作之前已经有一个Entrance 类做到过了 我们要做的只是给他改个名字

class CallBack
{
  public:    
    CallBack()    
    {}    
    void operator()(int sock)    
    {    
      HandlerRequest(sock);    
    }    

之后再给这个类加上一个仿函数方便我们后续调用

设计线程池

线程池的设计思路如下

  • 首先要有一个任务队列来存放任务
  • 要有一个num来标志任务队列的最大值
  • 有个锁和条件变量来保证同步和互斥

代码表示如下

  class ThreadPool
  {
    private:
      std::queue<Task> task_queue;
      int num;
      bool stop;
      pthread_mutex_t lock;
      pthread_cond_t cond;

之后就是一些常见的函数编写

比如说 加锁 解锁 睡眠 唤醒 添加任务 删除任务等等 这些代码在生产者消费者模型那一章节已经写过 这里就不再赘述 需要看全部代码的同学可以在文末连接处查看

补充内容

表单测试

此处会涉及到一些前端知识 有兴趣的同学可以去深入了解下


一般来说我们可以通过表单从前端页面向后端提交数据

表单的格式如下

<form>
 .
form elements
 .
</form>


我们可以写一个表单网页来测试我们的程序

它使用GET方法传参 分别传递两个数据x和y

<!DOCTYPE html>
<html>
<body>
<form action="/testcgi" method="GET">
First name:<br>
<input type="text" name="date_x" value="0">
<br>
Last name:<br>
<input type="text" name="date_y" value="1">
<br><br>
<input type = "submit" value="submit">
</form>
</body>
</html>

我们前面学过了 GET方法是通过url传参的

0de033162dcf43bcb321ea84bd35bde8.png

那么当我们点击submit提交的时候在url中也应该出现参数



458606c0d51c44c68df67c0f787f91e0.png我们发现结果确实符合预期

cgi程序支持多种语言

除了c++之外 我们的cgi程序还可以使用其他多种语言编写

比如说c语言 python java php

由于博主目前为止只学过c/c++ 没办法给大家演示 同学们如果有什么有意思的程序也可以在cgi上做一些扩展

项目代码

gitee

相关文章
|
1月前
|
Java API Spring
SpringBoot项目调用HTTP接口5种方式你了解多少?
SpringBoot项目调用HTTP接口5种方式你了解多少?
100 2
|
2月前
|
前端开发
webpack如何设置devServer启动项目为https协议
webpack如何设置devServer启动项目为https协议
186 0
|
5月前
|
JavaScript
如何让Vue项目本地运行的时候,同时支持http://localhost和http://192.168.X.X访问?
如何让Vue项目本地运行的时候,同时支持http://localhost和http://192.168.X.X访问?
|
6天前
|
JSON 前端开发 搜索推荐
BoostCompass( http_server 模块 | 项目前端代码 )
BoostCompass( http_server 模块 | 项目前端代码 )
24 4
|
5月前
|
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/)
69 0
git上传项目一直报一个文件没有添加任何内容(git上拉去别人的项目上传到自己的仓库/error: failed to push some refs to ‘https://gitee.com/)
|
6月前
|
JavaScript
Vue项目启动报错-http://eslint.org/
Vue项目启动报错-http://eslint.org/
41 0
|
6月前
|
XML 消息中间件 传感器
HTTP 与 MQTT:为您的 IoT 项目选择最佳协议
HTTP 与 MQTT:为您的 IoT 项目选择最佳协议
396 2
|
7天前
|
存储 算法 安全
[计算机网络]---Https协议
[计算机网络]---Https协议
|
14天前
|
安全 网络协议 算法
【计算机网络】http协议的原理与应用,https是如何保证安全传输的
【计算机网络】http协议的原理与应用,https是如何保证安全传输的
|
14天前
|
网络协议 安全 算法
HTTP协议与HTTPS协议
HTTP协议与HTTPS协议