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 程序
过程:
- web 服务器收到客户端(浏览器)的请求 Http Request,启动 CGI 程序( fork-and-execute),并通过环境变量、 标准输入传递数据
- CGI 进程启动解析器、加载配置(如业务相关配置)、连接其它服务器(如数据库服务器)、 逻辑处理等
- CGI 进程将处理结果通过标准输出、标准错误,传递给 web 服务器
- 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机制及实现的主要内容,如果未能解决你的问题,请参考以下文章