CGI机制及实现

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CGI机制及实现相关的知识,希望对你有一定的参考价值。

CGI机制及实现

CGI

概念部分摘自具体参考:Nginx运行FastCGI程序(ngx_http_fastcgi_module模块、fcgi库、spwan-fcgi进程管理器)
CGI:

  • 通用网关接口(Common Gateway Interface、CGI)描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。 CGI 独立于任何语言的,CGI 程序可以用任何脚本语言或者是完全独立编程语言实现,只 要这个语言可以在这个系统上运行。Unix shell script、Python、Ruby、PHP、perl、Tcl、C/C++ 和 Visual Basic 都可以用来编写 CGI 程序

过程:

  1. web 服务器收到客户端(浏览器)的请求 Http Request,启动 CGI 程序( fork-and-execute),并通过环境变量、 标准输入传递数据
  2. CGI 进程启动解析器、加载配置(如业务相关配置)、连接其它服务器(如数据库服务器)、 逻辑处理等
  3. CGI 进程将处理结果通过标准输出、标准错误,传递给 web 服务器
  4. web 服务器收到 CGI 返回的结果,构建 Http Response 返回给客户端,并杀死 CGI 进程

常用环境变量:

  • AUTH_TYPE: 存取认证类型
  • CONTENT_LENGTH: 由标准输入传递给 CGI 程序的数据长度,以bytes或字元数来 计算
  • CONTENT_TYPE: 请求的 MIME 类型
  • GATEWAY_INTERFACE: 服务器的CGI版本编号
  • HTTP_ACCEPT: 浏览器能直接接收的Content-types, 可以有 HTTP Accept header 定义
  • HTTP_USER_AGENT: 递交表单的浏览器的名称、版本和其他平台性的附加信息
  • HTTP_REFERER: 递交表单的文本的 URL,不是所有的浏览器都发出这个信息, 不要依赖它
  • PATH_INFO: 传递给CGI程序的路径信息QUERY_STRING传递给 CGI 程序的请求参数,也就是用"?"隔开,添加在 URL 后面的字串
  • REMOTE_ADDR: client端的host 名称
  • REMOTE_HOST: client端的IP 位址
  • REMOTE_USER: client端送出来的使用者名称
  • REMOTE_METHOD: client端发出请求的方法(如 get、post)
  • SCRIPT_NAME: CGI程序所在的虚拟路径,如/cgi-bin/echoe
  • SERVER_NAME: server的host名称或IP地址
  • SERVER_PORT: 收到request的server端口
  • SERVER_PROTOCOL: 所使用的通讯协定和版本编号
  • SERVER_SOFTWARE: server程序的名称和版本

缺陷:

  • CGI 方式是客户端有多少个请求,就开辟多少个子进程,每个子进程都需要启动自己的 解释器、加载配置,连接其他服务器等初始化工作,这是 CGI 进程性能低下的主要原因。当用户请求非常多的时候,会占用大量的内存、cpu 等资源,造成性能低下
  • CGI 使外部程序与 Web 服务器之间交互成为可能。CGI 程序运行在独立的进程中,并对每 个 Web 请求建立一个进程,这种方法非常容易实现,但效率很差,难以扩展。面对大量请 求,进程的大量建立和消亡使操作系统性能大大下降。此外,由于地址空间无法共享,也限 制了资源重用

环境变量的大小是有一定的限制的,当需要传送的数据量大时,储存环境变量的空间可能会不足,造成数据接收不完全,甚至无法执行 CGI 程序,因此后来又发展出另外一种方法:POST,也就是利用 I/O 重新导向的技巧,让 CGI 程序可以由 stdin 和 stdout 直接跟浏览器沟通

  • 当我们指定用这种方法传递请求的数据时,web 服务器收到数据后会先放在一块输入缓冲区中,并且将数据的大小记录在 CONTENT_LENGTH 这个环境变量,然后调用 CGI 程序并将 CGI 程序的 stdin 指向这块缓冲区,于是我们就可以很顺利的通过 stdin 和环境变数 CONTENT_LENGTH 得到所有的信息,再没有信息大小的限制了

CGI实现


服务器是一个可执行程序,加载到内存中,变成了进程,CGI程序也是一个程序、进程,如何用一个进程,去执行另一个程序------程序替换: 使用exec系列函数


通过管道与重定向实现CGI:

站父进程角度:

  • input[2];用于读出(0读 1写)
  • output[2];用于写入

fork父子进程都能看到两个管道(部分代码共享)

  • 子进程:close(input[0]), close(output[1])
    execl(bin.c_str(),bin.c_str(),nullptr) (注意进程替换不会替换当前进程的内核数据结构(文件描述符表))
    子进程程序替换后数据两个fd释放,之后就找不到管道了,所以在exec*前使用dup2重定向(stdin->input[1], stdout->output[0])
  • 父进程:close(input[1]), close(output[0])
    waitpid()

Method:

  • POST:父进程将body通过output[1]给子进程
  • GET:父进程通过环境变量putevn给子进程(环境变量也不受exec*系列函数影响)

子进程判断是GET还是POST也要 通过环境变量拿到method
注意:

  • GET为什么参数不能太长,因为通过环境变量来导入给子进程,环境变量限制
  • POST是正文,通过正文传递,参数大小没有限制

httpserver

实现
ProcessCgi():

int ProcessCgi()

   int code = OK;
   //父进程数据
   auto &method = http_request.method;
   auto &query_string =  http_request.query_string; //GET
   auto &body_text = http_request.requestBody;     //POST
   auto &bin = http_request.path; //要让子进程执行的目标程序,一定存在
   int ContentLength = http_request.ContentLength;
   auto &response_body = http_response.response_body;
   //利用环境变量将值传递给程序替换后的进程
   std::string query_string_env;
   std::string method_env;
   std::string ContentLength_env;

   //站在父进程角度 创建两个管道(父子进程分别占有input和output的两个fd)
   int input[2];
   int output[2];
   
   if(pipe(input) < 0)
       LOG(ERROR, "pipe input error");
       code = SERVER_ERROR;
       return code;
   
   if(pipe(output) < 0)
       LOG(ERROR, "pipe output error");
       code = SERVER_ERROR;
       return code;
   
   //std::cerr<<"*****"<<code<<std::endl;
   //新线程,但是从头到尾都只有一个进程,就是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(LOH_LEVEL_INFO, "Get Method, Add Query_String Env");
       
       else if(method == "POST")
           ContentLength_env = "CONTENT_LENGTH=";
           ContentLength_env += std::to_string(ContentLength);
           putenv((char*)ContentLength_env.c_str());
           LOG(LOH_LEVEL_INFO, "Post Method, Add ContentLength Env");
       
       else
           //Do Nothing
         
       //std::cout << "bin: " << bin << std::endl;
       dup2(output[0], 0);
       dup2(input[1], 1);
	   //程序替换
       execl(bin.c_str(), bin.c_str(), nullptr);
       exit(1);
   

替换成功之后,目标子进程如何得知,对应的读写文件描述符是多少?重定向到stdin,stdout,只要读0, 写1即可
站在子进程角度

  • input[1]: 写出 -> 1 -> input[1]
  • output[0]: 读入 -> 0 -> output[0]

出错:

else if(pid < 0) //error
       LOG(ERROR, "fork error!");
       return 404;

Father:

  • 写入stdin
	   else //parent
       close(input[1]);
       close(output[0]);
       if(method == "POST")//
           //std::cerr<<"=FATHER====METHOD"<<method<<std::endl;
           const char *start = body_text.c_str();
           int total = 0;
           int size = 0;
           while(total < ContentLength && (size= write(output[1], start+total, body_text.size()-total)) > 0)
               total += size;
           
       
       char ch = 0;
       while(read(input[0], &ch, 1) > 0)
           response_body.push_back(ch);
       
       int status = 0;
       pid_t ret = waitpid(pid, &status, 0);
       if(ret == pid)
           if(WIFEXITED(status))//WIFEXITED(status) 若此值为非0 表明进程正常结束
               if(WEXITSTATUS(status) == 0)
                   code = OK;
               
               else
                   code = BAD_REQUEST;
               
           
           else
               code = SERVER_ERROR;
           
       

       close(input[0]);
       close(output[1]);
   
   return code;

cgi程序

获取参数

std::string method = getenv("METHOD");
//std::cerr<<"====="<<method<<"========="<<std::endl;
if(method == "GET")
   //std::cerr<<"====="<<method<<"========="<<std::endl;
   query_string = getenv("QUERY_STRING");

else if(method == "POST")
   //std::cerr<<"====="<<method<<"========="<<std::endl;
   int content_length = atoi(getenv("CONTENT_LENGTH"));
   //std::cerr<<"=====Length"<<content_length<<std::endl;
   char c = 0;
   while(content_length)
       read(0, &c, 1);
       query_string.push_back(c);
       content_length--;
   

写回处理结果:(1->input[1])

std::cout << name1 << " : " << value1 << std::endl;
std::cout << name2 << " : " << value2 << std::endl;

Fast CGI

Fast CGI

  • 快速通用网关接口(Fast Common Gateway Interface/FastCGI)是通用网关接口(CGI)的改进,描述了客户端和服务器程序之间传输数据的一种标准。FastCGI 致力于减少 Web 服务器 与 CGI 程式之间互动的开销,从而使服务器可以同时处理更多的 Web 请求。与为每个请求 创建一个新的进程不同,FastCGI 使用持续的进程来处理一连串的请求。这些进程由 FastCGI 进程管理器管理 而不是 web 服务器
  • 以独立的进程池运行来cgi,单独一个进程死掉,系统可以很轻易的丢弃,然后重新分配新的进程来运行逻辑
  • 由于 Fast CGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的 应用效率。它的速度效率最少要比 CGI 技术提高 5 倍以上。它还支持分布式的部署,即 Fast CGI 程序可以在 web 服务器以外的主机上执行
  • CGI 是所谓的短生存期应用程序,FastCGI 是所谓的长生存期应用程序。FastCGI 像是一个常驻(long-live)型的 CGI,它可以一直执行着,不会每次都要花费时间去 fork 一次(这 是 CGI 最为人诟病的 fork-and-execute 模式)

过程:

  • 1.Web 服务器启动时载入初始化 FastCGI 执行环境。 例如 IIS、ISAPI、apache mod_fastcgi、nginx ngx_http_fastcgi_module、lighttpd mod_fastcgi
  • 2.FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程并等待来自 Web 服务器 的连接。启动 FastCGI 进程时,可以配置以 ip 和 UNIX 域 socket 两种方式启动
  • 3.当客户端请求到达Web 服务器时,Web 服务器将请求采用socket方式转发FastCGI 主进程,FastCGI 主进程选择并连接到一个 CGI 解释器。Web 服务器将 CGI 环境变量和标准输入发送到 FastCGI 子进程
  • 4.FastCGI 子进程完成处理后将标准输出和错误信息从同一 socket 连接返回 Web 服务 器。当 FastCGI 子进程关闭连接时,请求便处理完成
  • 5.FastCGI 子进程接着等待并处理来自 Web 服务器的下一个连接

常用环境变量:

  • SCRIPT_FILENAME $document_root$fastcgi_script_name;#脚本文件请求的路径
  • QUERY_STRING $query_string; #请求的参数;如?app=123
  • REQUEST_METHOD $request_method; #请求的动作(GET,POST)
  • CONTENT_TYPE $content_type; #请求头中的 Content-Type 字段
  • CONTENT_LENGTH $content_length; #请求头中的 Content-length 字段。
  • SCRIPT_NAME $fastcgi_script_name; #脚本名称
  • REQUEST_URI $request_uri; #请求的地址不带参数
  • DOCUMENT_URI $document_uri; #与$uri 相同。
  • DOCUMENT_ROOT $document_root; #网站的根目录。在 server 配置中 root 指令中指定的值
  • SERVER_PROTOCOL $server_protocol; #请求使用的协议,通常是 HTTP/1.0 或 HTTP/1.1。
  • GATEWAY_INTERFACE CGI/1.1;#cgi 版本
  • SERVER_SOFTWARE nginx/$nginx_version;#nginx 版本号,可修改、隐藏
  • REMOTE_ADDR $remote_addr; #客户端 IP
  • REMOTE_PORT $remote_port; #客户端端口
  • SERVER_ADDR $server_addr; #服务器 IP 地址
  • SERVER_PORT $server_port; #服务器端口
  • SERVER_NAME $server_name; #服务器名,域名在 server 配置中指定的 server_name
  • PATH_INFO $path_info;#可自定义变量

具体参考:Nginx运行FastCGI程序(ngx_http_fastcgi_module模块、fcgi库、spwan-fcgi进程管理器)

以上是关于CGI机制及实现的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot整合RabbitMQ重试机制及配置

MySQL事务篇:ACID原则事务隔离级别及事务机制原理剖析

STL及一些容器底层实现机制

Java null最佳实践

Java代理机制之初见(理解及实现)

golang sync.Pool的用法及实现