IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【项目实战】自主实现 HTTP 项目(六)——CGI机制 -> 正文阅读

[网络协议]【项目实战】自主实现 HTTP 项目(六)——CGI机制

目录

?CGI的引入及原理

子进程创建与程序替换

管道问题

子进程的重定向

利用环境变量传参

环境变量简单测试

?实现参数分离

处理CGI的反馈

?一张图说明CGI机制与概念提升


?

?CGI的引入及原理

? ? ? 我们在之前的篇目中讲到了,在我们客户端向服务器发送请求的时候,如果发送的内容是一个目录或者一个普通文件,我们给他们返回的是一个静态的网页。而接下来我们就讲解另一种情况,如果请求资源路径下面是一个可执行文件,那么这个时候就要我们使用(CGI)机制。

那么什么是CGI机制呢,我们在http的角度来看,实际上HTTP对于可执行文件是不会直接处理的,他会把客户端传来的参数传递给其可执行程序,可执行程序进行程序的执行,当结束的时候,再把处理的结果返回给我们的HTTP,HTTP拿到处理结果再返回给我们的客户端,这套机制就是CGI机制

? ? ? ? 但是这个时候又有一个问题了,就是说我们CGI这个机制是一个程序,当被加载到内存的时候是一个进程,我们怎么通过进程来去执行另一部分的程序呢?——这就用到了程序替换,但是直接替换的话,我们之前的代码就都被覆盖了,我们肯定不能直接替换,那么应该怎么做呢,我们可以来创建子进程帮助我们完成程序替换,然后再把结果返回给我们。

?这个时候问题又来了,我们需要执行的资源在哪里?简而言之,经过了前面的分析之后,请求资源可执行程序其实就是我们的路径

对于GET方法而言:就是路径path+query_string。

对于POST的方法而言:就是路径path+body(传参)。

在我们之前的博客中已经将需要启用cgi的处理的请求标注了出来,所以在这里我们来测试以下我们的代码,当需要执行普通文件和可执行性程序的时候,会把他们进行分别处理。

int ProcessCgi()
 {
  std::cout<<"debug"<<"Use CGI Model"<<std::endl;
}

?我们在这里用本地环回进行测试,我们输入请求行,请求的是普通文件,然后回车。

? ?我们可以看到的是给我们返回的就是一个普通文件的内容,而接下来我在页面上创建一个可执行文件,并给它赋上可执行权限,然后进行请求。

然后我们在对对这个可执行程序发起请求。

?我们看到CGI测试的代码,说明现在已经执行到了CGI程序。

子进程创建与程序替换

? ?我们刚才讲到,因为执行cgi程序需要进行程序替换,而如果直接进行程序替换,那么剩下的代码和程序就找不到了,所以我们可以创建子进程,让它帮我们完成程序替换。

 pid_t pid = fork(); 
            if(pid == 0 )
               { //child
             }
            else if(pid < 0)
            { //error
            }
            else{ //parent         
             }

我们创建完了子进程,让其帮助我们处理cgi机制,并返回给结果给我们的父进程, 而想要让程序去执行另一部分的代码,需要进行程序替换,程序替换有六个程序替换函数,我们应该选择哪个来进行程序替换呢?

所以我这里选择的是execl这个替换函数。

?在这里说明以下参数的问题:

path:表示执行目标文件的路径

arg:表示你想怎么样来执行这个文件,这里可以直接把文件名(可带路径直接传给他)

最终以nullptr作为结尾。

管道问题

? ? ? ? 在刚刚我们谈论完了让子进程利用程序替换来执行我们的目标可执行程序,但是现在问题又来了,因为父子进程在创建的那一刻分开了,当父进程想要告知子进程文件路径和想要把最终子进程处理的结果给父进程,这个时候就需要用到进程间通信了。

? ? ? 而这里我们用的是最简单的管道通信,为了实现双向通信,我们在这里创建两个管道,因为管道是半双工的,一个管道只能实现单项通信。

我们是站在父进程的角度对这些管道进行命名的,然后我们将对应的管道口进行关闭,最终就实现了上图两个管道,进行相互通信。

        int ProcessCgi()
        {
            LOG(INFO, "process cgi mthod!");

            //站在父进程角度
            int input[2];
            int output[2];

            if(pipe(input) < 0){
                LOG(ERROR, "pipe input error");
                return 404;
            }
            if(pipe(output) < 0){
                LOG(ERROR, "pipe output error");
                return 404;
            }

    
            pid_t pid = fork();
            if(pid == 0 ){ //child
                close(input[0]);
                close(output[1]);


                execl(bin.c_str(), bin.c_str(), nullptr);
                exit(1);
            }
            else if(pid < 0){ //error
                LOG(ERROR, "fork error!");
                return 404;
            }
            else{ //parent
                close(input[1]);
                close(output[0]);
                }
              waitpid(pid, nullptr, 0);

                close(input[0]);
                close(output[1]);
            }
            return OK;
        }

子进程的重定向

由于管道创建是在子进程程序替换之前完成的,程序替换之后,把原来的代码已经覆盖了,子进程也不知道之前的两个管道是哪个文件描述符,这个时候怎么办呢,我们可以在程序替换前进行重定向,文件的重定向,我们让标准输入就是从管道里输入,标准输出就是从管道里输出,让管道对应0与1号文件描述符下标,即实现子进程的重定向。

我们需要用到的函数是dup2?

?这个时候可能有一个疑问,就是程序重定向后程序替换不会影响文件描述符吗?

答:程序替换,只替换代码和数据结构,并不替换内核进程相关的数据结构,包括文件描述符。所以说程序替换后文件描述符还是之前的结构,只不过它是它不知道2以后的文件描述符对应的打开的文件是什么了,但这些文件依然存在,所以说我们需要重定向。

利用环境变量传参

除了管道的方式之外,我们还可以利用环境变量进行一些值的传递与或获取,因为环境变量也是不受程序替换的影响而改变的。?

但是现在还有一个问题,我们知道传参有两种方式,在前面也介绍过,一个是GET方法通过URL进行传参,一个是POST方法通过正文传参,而子进程执行cgi获取参数,如果是POST方法,我们用环境变量把对应的content-length传给子进程,让子进程知道我们需要读多少个字节,然后再管道正文部分的参数传给子进程,如果是GET方法,我们就通过环境变量把对应的参数资源给子进程即可,但是现在问题来啦

他怎么知道是从环境变量里读还是从管道里读还是从环境变量中读呢。所以也要让子进程知道我们的传参方法。

int ProcessCgi()
{
   // std::cout<<"debug"<<"Use CGI Model"<<std::endl;
          LOG(INFO, "process cgi mthod!");
            //父进程数据
            auto &method = http_request.method;
            auto &query_string =  http_request.query_string; //GET
            auto &body_text = http_request.request_body;     //POST
            auto &bin = http_request.path; //要让子进程执行的目标程序,一定存在
            int content_length = http_request.content_length;

            std::string query_string_env;
            std::string method_env;
            std::string content_length_env;

            //站在父进程角度
            int input[2];
            int output[2];

            if(pipe(input) < 0){
                LOG(ERROR, "pipe input error");
                return 404;
            }
            if(pipe(output) < 0){
                LOG(ERROR, "pipe output error");
                return 404 ;
            }

            //新线程,但是从头到尾都只有一个进程,就是httpserver!
            pid_t pid = fork();
            if(pid == 0 ){ //child
                close(input[0]);
                close(output[1]);

                method_env = "METHOD=";
                method_env += method;

                putenv((char*)method_env.c_str());

                if(method == "GET"){
                    query_string_env = "QUERY_STRING=";
                    query_string_env += query_string;
                    putenv((char*)query_string_env.c_str());
                    LOG(INFO, "Get Method, Add Query_String Env");
                }
                else if(method == "POST"){
                    content_length_env = "CONTENT_LENGTH=";
                    content_length_env += std::to_string(content_length);
                    putenv((char*)content_length_env.c_str());
                    LOG(INFO, "Post Method, Add Content_Length Env");
                }
                else{
                    //Do Nothing
                }

                //替换成功之后,目标子进程如何得知,对应的读写文件描述符是多少呢?不需要,只要读0, 写1即可
                //站在子进程角度
                //input[1]: 写出  -> 1 -> input[1] 
                //output[0]: 读入 -> 0 -> output[0]
                

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

                execl(bin.c_str(), bin.c_str(), nullptr);
                exit(1);
            }
            else if(pid < 0){ //error
                LOG(ERROR, "fork error!");
                return 404;
            }
            else{ //parent
                close(input[1]);
                close(output[0]);

                if(method == "POST"){
                    const char *start = body_text.c_str();
                    int total = 0;
                    int size = 0;
                    while(total < content_length && (size= write(output[1], start+total, body_text.size()-total)) > 0){
                        total += size;
                    }
                }
                waitpid(pid,nullptr,0);
            }
    return OK;

}

· 我们在从请求报文中的请求行行获取请求方法请求正文、content-length以及请求路径资源。

· 然后子进程在程序替换之前先判断方法

1.如果是GET方法则把资源参数在环境变量中注册

2.果是POST方法,那么就在环境变量中注册content-length来保存要在正文中获取多少数据

· 然后子进程在程序替换,执行cgi程序中的代码

·父进程拿到方法也要进行判断,如果是POST方法,就需要向管道中写入正文部分的参数,以供子进程读取。

·父进程等待进行进程等待,等待子进程的结束。

环境变量简单测试

?我们在这里做一个简单的测试,如果用GET方法传参的话,其程序替换后的子进程是否还可以拿到方法的环境变量,

我们已经完成了程序替换,现在0.1对应的文件描述符都是管道,只有2标准错误还没有重定向,为了让最终的结果打印到显示屏上,所以我们这里才用了标准错误来实现(后面用标准错误输出也是一样的原理,不再解释啦)。

?

? ? ? 我们链接之后,请求我们对应的text_cgi?,输入请求行,空行,回车我们看到其拿到了我们对应的请求方法的环境变量,我们这里使用环境变量传参成功。

? ? ? ? 我们再来检测以下POST的传参路径

?我们测试一下传参:

我们看到了其完成了完成了传参。

我们再尝试一下Post类型传参

?我们看到拿到了数据

?实现参数分离

我们传过来参数以后,要把每一个参数进行分离,这个时候我们就需要写一个分离函数,我们都知道参数与参数之间的分隔是用&代替的,我们之前的时候其实已经尝试过了类似的分离,所以我们在这里写一遍。

我们刚才已经实现了分别从post和get拿参数,(分别从url和正文中拿到参数)我们可以把其封装成函数,实现拿到参数资源,接下来就是实现分离。

bool GetQueryString(std::string &query_string)
{
    bool result = false;
    std::string method = getenv("METHOD");
    if(method == "GET"){
        query_string = getenv("QUERY_STRING");
        result = true;
    }
    else if(method == "POST"){
        //CGI如何得知需要从标准输入读取多少个字节呢??
        int content_length = atoi(getenv("CONTENT_LENGTH"));
        char c = 0;
        while(content_length){
            read(0, &c, 1);
            query_string.push_back(c);
            content_length--;
        }
        result = true;
    }
    else{
        result = false;
    }
    return result;
}

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

?我们可以看到的是,当我们传入参数资源的时候,我们就可以拿到分离以后的参数。

? ? ?

处理CGI的反馈

这个地方需要注意的是,我们在cgi处理完成之后,返回结果,并不是直接返回到我们的浏览器中,因为返回结果只是响应报文的正文部分,我们当前只需要将其加入到我们的正文部分即可。

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

我们除了需要拿到处理结果以外,我们还需要拿到子进程最终的退出状态,最终是正常退出还是异常退出,甚至处理中发生错误被中断,我们这里可以通过退出码来进行分析。

?一张图说明CGI机制与概念提升

?这是我们整个通信的过程,但是不知大家发现了没有,我们通过浏览器提交的参数,就是服务端中父进程给子进程CGI传递的参数,而服务端中子进程返回处理结果给父进程,就是服务器给浏览器返回的处理结果,这样的实际上就相当于可以看作浏览器于CGI进行数据交互,而剩余部分就是通信细节!

浏览器和server数据交互的本质?

浏览器和server进行数据交互的本质,就是进程间通信,这也是socket通信的本质浏览器和server无非两种形式的通信

1.浏览器拿下来数据

2.浏览器上传数据

?

?如何理解CGI?

子CGI程序的标准输入是浏览器!,子CGi程序的标准输出是浏览器!通信细节由http实现!

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 01:21:54  更:2022-09-30 01:22:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/19 19:24:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码