CGI通用网关与Nginx


CGI (Common Gateway Interface,通用网关接口) 描述了客户端和服务器程序之间传输数据的一种标准,可以使用一个客户端从网页浏览器向执行在网络服务器上的程序请求数据。
在此我们看下具体的实现以及应用。本文参考了Nginx 通用网关,感谢作者。
CGI
CGI 定义了一种标准,实际是独立于任何语言的,CGI 程序可以用任何语言实现。最初 CGI 是 1993 年由美国国家超级电脑应用中心(NCSA)为 NCSA HTTPd Web 服务器开发的。这个 Web 服务器使用了 UNIX shell 环境变量来保存从 Web 服务器传递出去的参数,然后生成一个运行 CGI 的独立的进程。下图为 CGI 请求流程图。

执行过程如下:
1.web 服务器收到客户端(浏览器)的请求 Http Request,启动 CGI 程序,并通过环境变量、标准输入传递数据。
2.CGI 进程启动解析器、加载配置(如业务相关配置)、连接其它服务器(如数据库服务器)、逻辑处理等。
3.CGI 程将处理结果通过标准输出、标准错误,传递给 web 服务器。
4.web 服务器收到 CGI 返回的结果,构建 Http Response 返回给客户端,并杀死 CGI 进程。
在遇到请求时,一般先要创建 CGI 的子进程,然后 CGI 子进程处理请求,处理完之后结束这个子进程,这也就是 fork-and-execute 模式。
对于这种方式的服务器,有多少连接请求就会有多少 CGI 子进程,每个子进程都需要重起 CGI 解析器、加载配置、连接其它服务器等初始化工作,从而导致 CGI 性能低下。
CGI方式在遇到连接请求(用户请求)先要创建cgi的子进程,激活一个CGI进程,然后处理请求,处理完后结束这个子进程。这就是fork-and-execute模式。所以用cgi方式的服务器有多少连接请求就会有多少cgi子进程,子进程反复加载是cgi性能低下的主要原因。当用户请求数量非常多时,会大量挤占系统的资源如内存,CPU时间等,造成效能低下。

CGI脚本工作流程:
浏览器通过HTML表单或超链接请求指向一个CGI应用程序的URL。
服务器收发到请求。
服务器执行所指定的CGI应用程序。
CGI应用程序执行所需要的操作,通常是基于浏览者输入的内容。
CGI应用程序把结果格式化为网络服务器和浏览器能够理解的文档(通常是HTML网页)。
网络服务器把结果返回到浏览器中。
FastCGI(快速通用网关接口)
FastCGI 是通用 CGI 的改进,减少了 Web 服务器与 CGI 程式之间互动的开销。与为每个请求创建一个新的进程不同,FastCGI 使用持续的进程来处理一连串的请求,这些进程由 FastCGI 进程管理器管理,而不是 web 服务器。

当进来一个请求时,Web 服务器把环境变量和这个页面请求通过一个 unix domain socket (都位于同一物理服务器) 或者一个 IP Socket (FastCGI 部署在其它物理服务器) 传递给 FastCGI 进程。

FastCGI 执行的主要流程如下:
1.Web 服务器启动时载入初始化 FastCGI 执行环境,例如 IIS ISAPI、apache mod_fastcgi、nginx ngx_http_fastcgi_module、lighttpd mod_fastcgi。
2.FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程并等待来自 Web 服务器的连接。
3.当客户端请求到达 Web 服务器时,Web 服务器将请求采用 socket 方式转发到 FastCGI 主进程,FastCGI 主进程选择并连接到一个 CGI 解释器。Web 服务器将 CGI 环境变量和标准输入发送到 FastCGI 子进程。
4.FastCGI 子进程完成处理后将标准输出和错误信息从同一 socket 连接返回 Web 服务器,当 FastCGI 子进程关闭连接时,请求便处理完成。
5.FastCGI 子进程接着等待并处理来自 Web 服务器的下一个连接。
由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率,它的速度效率最少要比 CGI 技术提高 5 倍以上,它还支持分布式的部署, 即 FastCGI 程序可以在 web 服务器以外的主机上执行。
FastCGI是从CGI发展改进而来的。传统CGI接口方式的主要缺点是性能很差,因为每次HTTP服务器遇到动态程序时都需要重新启动脚本解析器来执行解析,然后结果被返回给HTTP服务器。这在处理高并发访问时,几乎是不可用的。FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率。它的速度效率最少要比CGI 技术提高 5 倍以上,它还支持分布式的运算,即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

FastCGI的特点
打破传统页面处理技术。传统的页面处理技术,程序必须与 Web 服务器或 Application 服务器处于同一台服务器中。这种历史已经早N年被FastCGI技术所打破,FastCGI技术的应用程序可以被安装在服务器群中的任何一台服务器,而通过 TCP/IP 协议与 Web 服务器通讯,这样做既适合开发大型分布式 Web 群,也适合高效数据库控制。
明确的请求模式。CGI 技术没有一个明确的角色,在 FastCGI 程序中,程序被赋予明确的角色(响应器角色、认证器角色、过滤器角色)。
FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。FastCGI接口方式采用C/S结构,可以将HTTP服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或者多个脚本解析守护进程。当HTTP服务器每次遇到动态程序时,可以将其直接交付给FastCGI进程来执行,然后将得到的结果返回给浏览器。这种方式可以让HTTP服务器专一地处理静态请求或者将动态脚本服务器的结果返回给客户端,这在很大程度上提高了整个应用系统的性能。
FastCGI的工作流程
Web Server启动时载入FastCGI进程管理器(PHP-CGI或者PHP-FPM或者spawn-cgi)
FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web Server的连接。
当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi。
FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出。
总结:CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。FastCGI 像是一个常驻 (long-live) 型的 CGI,它可以一直执行着,不会每次都要花费时间去 fork 一次 (这是 CGI 最为人诟病的 fork-and-execute 模式)。
(u)WSGI(Web Server Gateway Interface)
WSGI 是 Python 中的一个规范协议,定义了一套接口,用来实现服务器端与应用端通信的规范化,定义为:
WSGI is the Web Server Gateway Interface. It is a specification for web servers and application servers to communicate with web applications (though it can also be used for more than that).
Python 中的 SimpleHTTPServer 模块提供了简单创建 http 服务的示例,不过 WEB 领域中却很少这么做,而是使用了另外一个 WSGI (Web Server Gateway Interface) 协议。Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。WSGI是作为Web服务器与Web应用程序或应用框架之间的一种低级别的接口,以提升可移植Web应用开发的共同点。WSGI是基于现存的CGI标准而设计的。

如下是一个简单的示例,可以直接启动并访问 http://127.1:8000:
from wsgiref.simple_server import make_server
# 可以做为单独的模块,通过 from hello import foobar 导入
def foobar(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return '<h1>Hello, web!</h1>'
# 创建一个服务器,IP地址为空,端口是8000,处理函数是foobar:
httpd = make_server('', 8000, foobar)
print "Serving HTTP on port 8000..."
# 开始监听HTTP请求
httpd.serve_forever()
WSGI 接口定义非常简单,如上所示,只需要实现一个函数,就可以响应 HTTP 请求;对于上述的 WSGI 标准处理函数,接收两个参数:
environ:一个包含所有 HTTP 请求信息的 dict 对象;
start_response:一个发送 HTTP 响应的函数。
在 foobar() 中,通过调用 start_response() 发送 HTTP 响应的 Header,注意 Header 只能发送一次;包括了两个参数:A) HTTP 响应码,B) 一组 list 表示的 HTTP Header。
然后,函数的返回值将作为 HTTP 响应的 Body 发送给浏览器。也就是说,通过 WSGI 定义的标准接口,只需要关心如何从 environ 这个字典中拿到 HTTP 请求信息,然后动态构造 HTML,通过 start_response() 发送 Header,最后返回 Body。
Python 内置了一个 WSGI 服务器,这个模块为 wsgiref,是一个用 Python 编写的 WSGI 服务器的参考实现。这也就意味这该实现完全符合 WSGI 标准,但是不考虑任何运行效率,仅供开发和测试使用。目前生产环境下最受推崇的方式是 uwsgi 服务器,其作用类似于 java 里的 Tomcat 服务器。
WSGI区分为两个部份:一为“服务器”或“网关”,另一为“应用程序”或“应用框架”。在处理一个WSGI请求时,服务器会为应用程序提供环境资讯及一个回呼函数(Callback Function)。当应用程序完成处理请求后,透过前述的回呼函数,将结果回传给服务器。所谓的 WSGI 中间件同时实现了API的两方,因此可以在WSGI服务和WSGI应用之间起调解作用:从WSGI服务器的角度来说,中间件扮演应用程序,而从应用程序的角度来说,中间件扮演服务器。“中间件”组件可以执行以下功能:
重写环境变量后,根据目标URL,将请求消息路由到不同的应用对象。
允许在一个进程中同时运行多个应用程序或应用框架。
负载均衡和远程处理,通过在网络上转发请求和响应消息。
进行内容后处理,例如应用XSLT样式表。
以前,如何选择合适的Web应用程序框架成为困扰Python初学者的一个问题,这是因为,一般而言,Web应用框架的选择将限制可用的Web服务器的选择,反之亦然。那时的Python应用程序通常是为CGI,FastCGI,mod_python中的一个而设计,甚至是为特定Web服务器的自定义的API接口而设计的。WSGI没有官方的实现, 因为WSGI更像一个协议。只要遵照这些协议,WSGI应用(Application)都可以在任何服务器(Server)上运行, 反之亦然。WSGI就是Python的CGI包装,相对于Fastcgi是PHP的CGI包装。
WSGI将 web 组件分为三类: web服务器,web中间件,web应用程序, wsgi基本处理模式为 : WSGI Server -> (WSGI Middleware)* -> WSGI Application 。
WSGI Server/gateway
wsgi server可以理解为一个符合wsgi规范的web server,接收request请求,封装一系列环境变量,按照wsgi规范调用注册的wsgi app,最后将response返回给客户端。文字很难解释清楚wsgi server到底是什么东西,以及做些什么事情,最直观的方式还是看wsgi server的实现代码。以Python自带的wsgiref为例,wsgiref是按照wsgi规范实现的一个简单wsgi server。它的代码也不复杂。

服务器创建socket,监听端口,等待客户端连接。
当有请求来时,服务器解析客户端信息放到环境变量environ中,并调用绑定的handler来处理请求。
handler解析这个http请求,将请求信息例如method,path等放到environ中。
wsgi handler再将一些服务器端信息也放到environ中,最后服务器信息,客户端信息,本次请求信息全部都保存到了环境变量environ中。
wsgi handler 调用注册的wsgi app,并将environ和回调函数传给wsgi app
wsgi app 将reponse header/status/body 回传给wsgi handler
最终handler还是通过socket将response信息塞回给客户端。
WSGI Application
wsgi application就是一个普通的callable对象,当有请求到来时,wsgi server会调用这个wsgi app。这个对象接收两个参数,通常为environ,start_response。environ就像前面介绍的,可以理解为环境变量,跟一次请求相关的所有信息都保存在了这个环境变量中,包括服务器信息,客户端信息,请求信息。start_response是一个callback函数,wsgi application通过调用start_response,将response headers/status 返回给wsgi server。此外这个wsgi app会return 一个iterator对象 ,这个iterator就是response body。这么空讲感觉很虚,对着下面这个简单的例子看就明白很多了。
WSGI MiddleWare
有些功能可能介于服务器程序和应用程序之间,例如,服务器拿到了客户端请求的URL, 不同的URL需要交由不同的函数处理,这个功能叫做 URL Routing,这个功能就可以放在二者中间实现,这个中间层就是 middleware。middleware对服务器程序和应用是透明的,也就是说,服务器程序以为它就是应用程序,而应用程序以为它就是服务器。这就告诉我们,middleware需要把自己伪装成一个服务器,接受应用程序,调用它,同时middleware还需要把自己伪装成一个应用程序,传给服务器程序。
其实无论是服务器程序,middleware 还是应用程序,都在服务端,为客户端提供服务,之所以把他们抽象成不同层,就是为了控制复杂度,使得每一次都不太复杂,各司其职。
uWSGI
uWSGI 项目旨在为部署分布式集群的网络应用开发一套完整的解决方案。uWSGI主要面向web及其标准服务,已经成功的应用于多种不同的语言。由于uWSGI的可扩展架构,它能够被无限制的扩展用来支持更多的平台和语言。目前,你可以使用C,C++和Objective-C来编写插件。项目名称中的“WSGI”是为了向同名的Python Web标准表示感谢,因为WSGI为该项目开发了第一个插件。
uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。uWSGI,既不用wsgi协议也不用FastCGI协议,而是自创了一个uwsgi的协议,uwsgi协议是一个uWSGI服务器自有的协议,它用于定义传输信息的类型(type of information),每一个uwsgi packet前4byte为传输信息类型描述,它与WSGI相比是两样东西。据说该协议大约是fcgi协议的10倍那么快。uWSGI的主要特点如下:
超快的性能。
低内存占用(约为apache2的mod_wsgi的一半左右)。
多app管理。
详尽的日志功能(可以用来分析app性能和瓶颈)。
高度可定制(内存大小限制,服务一定次数后重启等)。
ISAPI
ISAPI(Internet Server Application Program Interface)是微软提供的一套面向WEB服务的API接口,它能实现CGI提供的全部功能,并在此基础上进行了扩展,如提供了过滤器应用程序接口。ISAPI应用大多数以DLL动态库的形式使用,可以在被用户请求后执行,在处理完一个用户请求后不会马上消失,而是继续驻留在内存中等待处理别的用户输入。此外,ISAPI的DLL应用程序和WEB服务器处于同一个进程中,效率要显著高于CGI(由于微软的排他性,只能运行于windows环境)。

ISAPI服务器扩展为使用 Internet 服务器的通用网关接口(CGI) 应用程序提供了另一种选择。与 CGI 应用程序不同,ISA 在 HTTP服务器所在的同一地址空间运行,并且可以访问可由 HTTP 服务器使用的所有资源。ISA 的系统开销比 CGI 应用程序低,因为它们不要求创建其他进程,也不执行需要越过进程边界的通信,而这种通信非常耗时。如果内存被其他进程所需要,扩展和筛选器DLL 都可能被卸载。ISAPI 允许在一个 DLL 中有多个命令,这些命令作为 DLL 中CHttpServer对象的成员函数来实现。CGI 要求每个任务有一个单独的名称和一个到单独的可执行文件的 URL 映射。每个新的 CGI 请求启动一个新进程,而每个不同的请求包含在各自的可执行文件中,这些文件根据每个请求加载和卸载,因此系统开销高于 ISA。
PHP-CGI
PHP-CGI是PHP自带的FastCGI管理器。PHP-CGI的不足:
php-cgi变更ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
直接杀死php-cgi进程php就不能运行了。(PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程)
Spawn-FCGI
Spawn-FCGI是一个通用的FastCGI管理服务器,它是lighttpd中的一部份,很多人都用Lighttpd的Spawn-FCGI进行FastCGI模式下的管理工作,不过有不少缺点。而PHP-FPM的出现多少缓解了一些问题,但PHP-FPM有个缺点就是要重新编译,这对于一些已经运行的环境可能有不小的风险),在php 5.3.3中可以直接使用PHP-FPM了。Spawn-FCGI的代码很少,全部才630行,用c语言编写,最近一次提交是5年前。代码主页:https://github.com/lighttpd/spawn-fcgi
Spawn-FCGI代码分析如下:
spawn-fcgi 首先create socket,bind,listen 3步创建服务器socket,(把这个socket叫做 fcgi_fd)
用dup2,把fcgi_fd 交换给 FCGI_LISTENSOCK_FILENO (FCGI_LISTENSOCK_FILENO数值上等于0,这是fastcgi协议当中指定用来listen的socket id)
执行execl ,replaces the current process image with a new process image. process image 进程在运行空间的代码段
很显然,Spawn-FCGI也是 pre-fork 模型,只是用了上古C语言编写,充满了N多 unix下暗黑编程技巧。
Spawn-FCGI功能很单一:
只管fork进程,子进程挂了,主进程仅仅log记录一次,根本不会重新fork。在2009年一段时间内,我曾经用spawn-fcgi部署php-cgi,当跑一段时间就会全挂掉,只能用crontab定时重启spawn-fcgi
不负责子进程中的网络IO,把socket放到指定位置就完了,接下来的事情由被spawn的程序处理
Spawn-FCGI是一个很早期的程序,瞻仰一下即可。另外有:1996年的一段代码在此可以看到,和spawn-fcgi一个风格
PHP-FPM
PHP-FPM是一个PHP FastCGI管理器,是只用于PHP的,它其实是PHP源代码的一个补丁,旨在将FastCGI进程管理整合进PHP包中。必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。FPM(FastCGI 进程管理器)用于替换 PHP-CGI 的大部分附加功能,对于高负载网站是非常有用的。

其功能包括:
支持平滑停止/启动的高级进程管理功能;
可以工作于不同的 uid/gid/chroot 环境下,并监听不同的端口和使用不同的ini 配置文件(可取代 safe_mode 的设置);
stdout 和 stderr 日志记录;
在发生意外情况的时候能够重新启动并缓存被破坏的 opcode;
文件上传优化支持;
“慢日志” – 记录脚本(不仅记录文件名,还记录 PHP backtrace 信息,可以使用 ptrace或者类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢;
fastcgi_finish_request() – 特殊功能:用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等);
动态/静态子进程产生;
基本 SAPI 运行状态信息(类似Apache的 mod_status);
基于ini 的配置文件。
与Nginx服务器的结合使用
nginx 不能直接执行外部可执行程序,并且 cgi 是接收到请求时才会启动 cgi 进程,不像 fastcgi 会在一开就启动好,这样 nginx 天生是不支持 cgi 的。nginx 不像 apache 那样可以直接执行外部可执行程序,但 nginx 可以作为代理服务器,将请求转发给后端服务器。其中 nginx 就支持 FastCGI 代理,接收客户端的请求,然后将请求转发给后端 fastcgi 进程。
1、Nginx+CGI
Nginx 支持 FastCGI,可以考虑使用 fastcgi 包装来支持 cgi,原理大致如下图所示:pre-fork 几个通用的代理 fastcgi 程序 fastcgi-wrapper,fastcgi-wrapper 启动执行 cgi 然后将 cgi 的执行结果返回给 nginx(fork-and-exec)。

明白原理之后,编写一个 fastcgi-warpper 也比较简单,网上流传比较多的一个解决方案是,来自 nginx wiki 上的使用 perl 的 fastcgi 包装脚本 cgiwrap-fcgi.pl。
其实编写 C 的 fastcgi-wrapper 就类似 C 的 fastcgi 程序,可以参考 fastcgi-wrapper 。
下载源码之后通过如下命令安装,默认安装在 /usr/local/sbin/fcgiwrap
$ autoreconf -i
$ ./configure && make
# make install
仍以上述的示例为例,通过如下命令启动
# spawn-fcgi -a 127.0.0.1 -p 7001 -u nginx -f /usr/local/sbin/fcgiwrap
配置文件中添加如下内容
... ...
location ~ .*\.cgi$ {
root /path/to/modules
fastcgi_pass 127.0.0.1:7001;
fastcgi_index index.cgi;
include fastcgi.conf;
}
... ...
2、Nginx+FastCGI
fastcgi 进程由 FastCGI 进程管理器所管理,而不是 nginx,这样就需要一个程序管理我们编写的 fastcgi 程序,在此使用 spawn-fcgi。它是一个通用的 FastCGI 进程管理器,简单小巧,原属于 lighttpd 的一部分,后来由于使用比较广泛,所以就迁移出来作为独立项目了。
spawn-fcgi 使用 pre-fork 模型,功能主要是打开监听端口,绑定地址,然后 fork-and-exec 创建我们编写的 fastcgi 应用程序进程,退出完成工作。可以通过如下方法测试 C 的 FastCGI 程序。
在 CentOS 中可以通过如下命令直接安装 (依赖 EPEL 源),当然也可以通过源码安装。
# yum install spawn-fcgi fcgi fcgi-devel
新建 FastCGI 应用程序
$ cat examples/foobar.c # 查看源码
#include <fcgi_stdio.h>;
int main( int argc, char *argv[] ){
int count = 0;
while( FCGI_Accept() >= 0 )
FCGI_printf( "Status: 200 OK\r\n" );
FCGI_printf( "Content-Type: text/html\r\n\r\n" );
FCGI_printf( "Hello world in C, #%d running on host %s\n",++count, getenv("SERVER_NAME"));
}
return 0;
}
$ gcc -o foobar.cgi foobar.c -L /usr/local/lib/ -lfcgi # 直接编译
启动 spawn-fcgi
$ spawn-fcgi -a 127.0.0.1 -p 7000 -u nginx -f /tmp/examples/foobar/foobar.cgi
修改 nginx 配置文件,添加如下内容,然后通过 kill -HUP PID 重新加载配置文件。
... ...
location = /hello.cgi {
fastcgi_pass 127.0.0.1:7000;
fastcgi_index index.cgi;
fastcgi_param SCRIPT_FILENAME fcgi$fastcgi_script_name;
include fastcgi_params;
}
... ...
然后可以通过 http://localhost/hello.cgi 查看。如果报错可以查看日志,最好通过 setenforce 0 将 selinux 关闭,否则可能会有 502 Bad Gateway 报错。
参考
FastCGI.com Archives,包括了 FastCGI 标准,以及 C 的实现 fcgi library。
网关协议CGI、FastCGI、WSGI的区别
在此我们看下具体的实现以及应用。本文参考了Nginx 通用网关,感谢作者。
CGI
CGI 定义了一种标准,实际是独立于任何语言的,CGI 程序可以用任何语言实现。最初 CGI 是 1993 年由美国国家超级电脑应用中心(NCSA)为 NCSA HTTPd Web 服务器开发的。这个 Web 服务器使用了 UNIX shell 环境变量来保存从 Web 服务器传递出去的参数,然后生成一个运行 CGI 的独立的进程。下图为 CGI 请求流程图。

执行过程如下:
1.web 服务器收到客户端(浏览器)的请求 Http Request,启动 CGI 程序,并通过环境变量、标准输入传递数据。
2.CGI 进程启动解析器、加载配置(如业务相关配置)、连接其它服务器(如数据库服务器)、逻辑处理等。
3.CGI 程将处理结果通过标准输出、标准错误,传递给 web 服务器。
4.web 服务器收到 CGI 返回的结果,构建 Http Response 返回给客户端,并杀死 CGI 进程。
在遇到请求时,一般先要创建 CGI 的子进程,然后 CGI 子进程处理请求,处理完之后结束这个子进程,这也就是 fork-and-execute 模式。
对于这种方式的服务器,有多少连接请求就会有多少 CGI 子进程,每个子进程都需要重起 CGI 解析器、加载配置、连接其它服务器等初始化工作,从而导致 CGI 性能低下。
CGI方式在遇到连接请求(用户请求)先要创建cgi的子进程,激活一个CGI进程,然后处理请求,处理完后结束这个子进程。这就是fork-and-execute模式。所以用cgi方式的服务器有多少连接请求就会有多少cgi子进程,子进程反复加载是cgi性能低下的主要原因。当用户请求数量非常多时,会大量挤占系统的资源如内存,CPU时间等,造成效能低下。

CGI脚本工作流程:
浏览器通过HTML表单或超链接请求指向一个CGI应用程序的URL。
服务器收发到请求。
服务器执行所指定的CGI应用程序。
CGI应用程序执行所需要的操作,通常是基于浏览者输入的内容。
CGI应用程序把结果格式化为网络服务器和浏览器能够理解的文档(通常是HTML网页)。
网络服务器把结果返回到浏览器中。
FastCGI(快速通用网关接口)
FastCGI 是通用 CGI 的改进,减少了 Web 服务器与 CGI 程式之间互动的开销。与为每个请求创建一个新的进程不同,FastCGI 使用持续的进程来处理一连串的请求,这些进程由 FastCGI 进程管理器管理,而不是 web 服务器。

当进来一个请求时,Web 服务器把环境变量和这个页面请求通过一个 unix domain socket (都位于同一物理服务器) 或者一个 IP Socket (FastCGI 部署在其它物理服务器) 传递给 FastCGI 进程。

FastCGI 执行的主要流程如下:
1.Web 服务器启动时载入初始化 FastCGI 执行环境,例如 IIS ISAPI、apache mod_fastcgi、nginx ngx_http_fastcgi_module、lighttpd mod_fastcgi。
2.FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程并等待来自 Web 服务器的连接。
3.当客户端请求到达 Web 服务器时,Web 服务器将请求采用 socket 方式转发到 FastCGI 主进程,FastCGI 主进程选择并连接到一个 CGI 解释器。Web 服务器将 CGI 环境变量和标准输入发送到 FastCGI 子进程。
4.FastCGI 子进程完成处理后将标准输出和错误信息从同一 socket 连接返回 Web 服务器,当 FastCGI 子进程关闭连接时,请求便处理完成。
5.FastCGI 子进程接着等待并处理来自 Web 服务器的下一个连接。
由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率,它的速度效率最少要比 CGI 技术提高 5 倍以上,它还支持分布式的部署, 即 FastCGI 程序可以在 web 服务器以外的主机上执行。
FastCGI是从CGI发展改进而来的。传统CGI接口方式的主要缺点是性能很差,因为每次HTTP服务器遇到动态程序时都需要重新启动脚本解析器来执行解析,然后结果被返回给HTTP服务器。这在处理高并发访问时,几乎是不可用的。FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率。它的速度效率最少要比CGI 技术提高 5 倍以上,它还支持分布式的运算,即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

FastCGI的特点
打破传统页面处理技术。传统的页面处理技术,程序必须与 Web 服务器或 Application 服务器处于同一台服务器中。这种历史已经早N年被FastCGI技术所打破,FastCGI技术的应用程序可以被安装在服务器群中的任何一台服务器,而通过 TCP/IP 协议与 Web 服务器通讯,这样做既适合开发大型分布式 Web 群,也适合高效数据库控制。
明确的请求模式。CGI 技术没有一个明确的角色,在 FastCGI 程序中,程序被赋予明确的角色(响应器角色、认证器角色、过滤器角色)。
FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。FastCGI接口方式采用C/S结构,可以将HTTP服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或者多个脚本解析守护进程。当HTTP服务器每次遇到动态程序时,可以将其直接交付给FastCGI进程来执行,然后将得到的结果返回给浏览器。这种方式可以让HTTP服务器专一地处理静态请求或者将动态脚本服务器的结果返回给客户端,这在很大程度上提高了整个应用系统的性能。
FastCGI的工作流程
Web Server启动时载入FastCGI进程管理器(PHP-CGI或者PHP-FPM或者spawn-cgi)
FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web Server的连接。
当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi。
FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出。
总结:CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。FastCGI 像是一个常驻 (long-live) 型的 CGI,它可以一直执行着,不会每次都要花费时间去 fork 一次 (这是 CGI 最为人诟病的 fork-and-execute 模式)。
(u)WSGI(Web Server Gateway Interface)
WSGI 是 Python 中的一个规范协议,定义了一套接口,用来实现服务器端与应用端通信的规范化,定义为:
WSGI is the Web Server Gateway Interface. It is a specification for web servers and application servers to communicate with web applications (though it can also be used for more than that).
Python 中的 SimpleHTTPServer 模块提供了简单创建 http 服务的示例,不过 WEB 领域中却很少这么做,而是使用了另外一个 WSGI (Web Server Gateway Interface) 协议。Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。WSGI是作为Web服务器与Web应用程序或应用框架之间的一种低级别的接口,以提升可移植Web应用开发的共同点。WSGI是基于现存的CGI标准而设计的。

如下是一个简单的示例,可以直接启动并访问 http://127.1:8000:
from wsgiref.simple_server import make_server
# 可以做为单独的模块,通过 from hello import foobar 导入
def foobar(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return '<h1>Hello, web!</h1>'
# 创建一个服务器,IP地址为空,端口是8000,处理函数是foobar:
httpd = make_server('', 8000, foobar)
print "Serving HTTP on port 8000..."
# 开始监听HTTP请求
httpd.serve_forever()
WSGI 接口定义非常简单,如上所示,只需要实现一个函数,就可以响应 HTTP 请求;对于上述的 WSGI 标准处理函数,接收两个参数:
environ:一个包含所有 HTTP 请求信息的 dict 对象;
start_response:一个发送 HTTP 响应的函数。
在 foobar() 中,通过调用 start_response() 发送 HTTP 响应的 Header,注意 Header 只能发送一次;包括了两个参数:A) HTTP 响应码,B) 一组 list 表示的 HTTP Header。
然后,函数的返回值将作为 HTTP 响应的 Body 发送给浏览器。也就是说,通过 WSGI 定义的标准接口,只需要关心如何从 environ 这个字典中拿到 HTTP 请求信息,然后动态构造 HTML,通过 start_response() 发送 Header,最后返回 Body。
Python 内置了一个 WSGI 服务器,这个模块为 wsgiref,是一个用 Python 编写的 WSGI 服务器的参考实现。这也就意味这该实现完全符合 WSGI 标准,但是不考虑任何运行效率,仅供开发和测试使用。目前生产环境下最受推崇的方式是 uwsgi 服务器,其作用类似于 java 里的 Tomcat 服务器。
WSGI区分为两个部份:一为“服务器”或“网关”,另一为“应用程序”或“应用框架”。在处理一个WSGI请求时,服务器会为应用程序提供环境资讯及一个回呼函数(Callback Function)。当应用程序完成处理请求后,透过前述的回呼函数,将结果回传给服务器。所谓的 WSGI 中间件同时实现了API的两方,因此可以在WSGI服务和WSGI应用之间起调解作用:从WSGI服务器的角度来说,中间件扮演应用程序,而从应用程序的角度来说,中间件扮演服务器。“中间件”组件可以执行以下功能:
重写环境变量后,根据目标URL,将请求消息路由到不同的应用对象。
允许在一个进程中同时运行多个应用程序或应用框架。
负载均衡和远程处理,通过在网络上转发请求和响应消息。
进行内容后处理,例如应用XSLT样式表。
以前,如何选择合适的Web应用程序框架成为困扰Python初学者的一个问题,这是因为,一般而言,Web应用框架的选择将限制可用的Web服务器的选择,反之亦然。那时的Python应用程序通常是为CGI,FastCGI,mod_python中的一个而设计,甚至是为特定Web服务器的自定义的API接口而设计的。WSGI没有官方的实现, 因为WSGI更像一个协议。只要遵照这些协议,WSGI应用(Application)都可以在任何服务器(Server)上运行, 反之亦然。WSGI就是Python的CGI包装,相对于Fastcgi是PHP的CGI包装。
WSGI将 web 组件分为三类: web服务器,web中间件,web应用程序, wsgi基本处理模式为 : WSGI Server -> (WSGI Middleware)* -> WSGI Application 。
WSGI Server/gateway
wsgi server可以理解为一个符合wsgi规范的web server,接收request请求,封装一系列环境变量,按照wsgi规范调用注册的wsgi app,最后将response返回给客户端。文字很难解释清楚wsgi server到底是什么东西,以及做些什么事情,最直观的方式还是看wsgi server的实现代码。以Python自带的wsgiref为例,wsgiref是按照wsgi规范实现的一个简单wsgi server。它的代码也不复杂。

服务器创建socket,监听端口,等待客户端连接。
当有请求来时,服务器解析客户端信息放到环境变量environ中,并调用绑定的handler来处理请求。
handler解析这个http请求,将请求信息例如method,path等放到environ中。
wsgi handler再将一些服务器端信息也放到environ中,最后服务器信息,客户端信息,本次请求信息全部都保存到了环境变量environ中。
wsgi handler 调用注册的wsgi app,并将environ和回调函数传给wsgi app
wsgi app 将reponse header/status/body 回传给wsgi handler
最终handler还是通过socket将response信息塞回给客户端。
WSGI Application
wsgi application就是一个普通的callable对象,当有请求到来时,wsgi server会调用这个wsgi app。这个对象接收两个参数,通常为environ,start_response。environ就像前面介绍的,可以理解为环境变量,跟一次请求相关的所有信息都保存在了这个环境变量中,包括服务器信息,客户端信息,请求信息。start_response是一个callback函数,wsgi application通过调用start_response,将response headers/status 返回给wsgi server。此外这个wsgi app会return 一个iterator对象 ,这个iterator就是response body。这么空讲感觉很虚,对着下面这个简单的例子看就明白很多了。
WSGI MiddleWare
有些功能可能介于服务器程序和应用程序之间,例如,服务器拿到了客户端请求的URL, 不同的URL需要交由不同的函数处理,这个功能叫做 URL Routing,这个功能就可以放在二者中间实现,这个中间层就是 middleware。middleware对服务器程序和应用是透明的,也就是说,服务器程序以为它就是应用程序,而应用程序以为它就是服务器。这就告诉我们,middleware需要把自己伪装成一个服务器,接受应用程序,调用它,同时middleware还需要把自己伪装成一个应用程序,传给服务器程序。
其实无论是服务器程序,middleware 还是应用程序,都在服务端,为客户端提供服务,之所以把他们抽象成不同层,就是为了控制复杂度,使得每一次都不太复杂,各司其职。
uWSGI
uWSGI 项目旨在为部署分布式集群的网络应用开发一套完整的解决方案。uWSGI主要面向web及其标准服务,已经成功的应用于多种不同的语言。由于uWSGI的可扩展架构,它能够被无限制的扩展用来支持更多的平台和语言。目前,你可以使用C,C++和Objective-C来编写插件。项目名称中的“WSGI”是为了向同名的Python Web标准表示感谢,因为WSGI为该项目开发了第一个插件。
uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。uWSGI,既不用wsgi协议也不用FastCGI协议,而是自创了一个uwsgi的协议,uwsgi协议是一个uWSGI服务器自有的协议,它用于定义传输信息的类型(type of information),每一个uwsgi packet前4byte为传输信息类型描述,它与WSGI相比是两样东西。据说该协议大约是fcgi协议的10倍那么快。uWSGI的主要特点如下:
超快的性能。
低内存占用(约为apache2的mod_wsgi的一半左右)。
多app管理。
详尽的日志功能(可以用来分析app性能和瓶颈)。
高度可定制(内存大小限制,服务一定次数后重启等)。
ISAPI
ISAPI(Internet Server Application Program Interface)是微软提供的一套面向WEB服务的API接口,它能实现CGI提供的全部功能,并在此基础上进行了扩展,如提供了过滤器应用程序接口。ISAPI应用大多数以DLL动态库的形式使用,可以在被用户请求后执行,在处理完一个用户请求后不会马上消失,而是继续驻留在内存中等待处理别的用户输入。此外,ISAPI的DLL应用程序和WEB服务器处于同一个进程中,效率要显著高于CGI(由于微软的排他性,只能运行于windows环境)。

ISAPI服务器扩展为使用 Internet 服务器的通用网关接口(CGI) 应用程序提供了另一种选择。与 CGI 应用程序不同,ISA 在 HTTP服务器所在的同一地址空间运行,并且可以访问可由 HTTP 服务器使用的所有资源。ISA 的系统开销比 CGI 应用程序低,因为它们不要求创建其他进程,也不执行需要越过进程边界的通信,而这种通信非常耗时。如果内存被其他进程所需要,扩展和筛选器DLL 都可能被卸载。ISAPI 允许在一个 DLL 中有多个命令,这些命令作为 DLL 中CHttpServer对象的成员函数来实现。CGI 要求每个任务有一个单独的名称和一个到单独的可执行文件的 URL 映射。每个新的 CGI 请求启动一个新进程,而每个不同的请求包含在各自的可执行文件中,这些文件根据每个请求加载和卸载,因此系统开销高于 ISA。
PHP-CGI
PHP-CGI是PHP自带的FastCGI管理器。PHP-CGI的不足:
php-cgi变更ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
直接杀死php-cgi进程php就不能运行了。(PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程)
Spawn-FCGI
Spawn-FCGI是一个通用的FastCGI管理服务器,它是lighttpd中的一部份,很多人都用Lighttpd的Spawn-FCGI进行FastCGI模式下的管理工作,不过有不少缺点。而PHP-FPM的出现多少缓解了一些问题,但PHP-FPM有个缺点就是要重新编译,这对于一些已经运行的环境可能有不小的风险),在php 5.3.3中可以直接使用PHP-FPM了。Spawn-FCGI的代码很少,全部才630行,用c语言编写,最近一次提交是5年前。代码主页:https://github.com/lighttpd/spawn-fcgi
Spawn-FCGI代码分析如下:
spawn-fcgi 首先create socket,bind,listen 3步创建服务器socket,(把这个socket叫做 fcgi_fd)
用dup2,把fcgi_fd 交换给 FCGI_LISTENSOCK_FILENO (FCGI_LISTENSOCK_FILENO数值上等于0,这是fastcgi协议当中指定用来listen的socket id)
执行execl ,replaces the current process image with a new process image. process image 进程在运行空间的代码段
很显然,Spawn-FCGI也是 pre-fork 模型,只是用了上古C语言编写,充满了N多 unix下暗黑编程技巧。
Spawn-FCGI功能很单一:
只管fork进程,子进程挂了,主进程仅仅log记录一次,根本不会重新fork。在2009年一段时间内,我曾经用spawn-fcgi部署php-cgi,当跑一段时间就会全挂掉,只能用crontab定时重启spawn-fcgi
不负责子进程中的网络IO,把socket放到指定位置就完了,接下来的事情由被spawn的程序处理
Spawn-FCGI是一个很早期的程序,瞻仰一下即可。另外有:1996年的一段代码在此可以看到,和spawn-fcgi一个风格
PHP-FPM
PHP-FPM是一个PHP FastCGI管理器,是只用于PHP的,它其实是PHP源代码的一个补丁,旨在将FastCGI进程管理整合进PHP包中。必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。FPM(FastCGI 进程管理器)用于替换 PHP-CGI 的大部分附加功能,对于高负载网站是非常有用的。

其功能包括:
支持平滑停止/启动的高级进程管理功能;
可以工作于不同的 uid/gid/chroot 环境下,并监听不同的端口和使用不同的ini 配置文件(可取代 safe_mode 的设置);
stdout 和 stderr 日志记录;
在发生意外情况的时候能够重新启动并缓存被破坏的 opcode;
文件上传优化支持;
“慢日志” – 记录脚本(不仅记录文件名,还记录 PHP backtrace 信息,可以使用 ptrace或者类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢;
fastcgi_finish_request() – 特殊功能:用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等);
动态/静态子进程产生;
基本 SAPI 运行状态信息(类似Apache的 mod_status);
基于ini 的配置文件。
与Nginx服务器的结合使用
nginx 不能直接执行外部可执行程序,并且 cgi 是接收到请求时才会启动 cgi 进程,不像 fastcgi 会在一开就启动好,这样 nginx 天生是不支持 cgi 的。nginx 不像 apache 那样可以直接执行外部可执行程序,但 nginx 可以作为代理服务器,将请求转发给后端服务器。其中 nginx 就支持 FastCGI 代理,接收客户端的请求,然后将请求转发给后端 fastcgi 进程。
1、Nginx+CGI
Nginx 支持 FastCGI,可以考虑使用 fastcgi 包装来支持 cgi,原理大致如下图所示:pre-fork 几个通用的代理 fastcgi 程序 fastcgi-wrapper,fastcgi-wrapper 启动执行 cgi 然后将 cgi 的执行结果返回给 nginx(fork-and-exec)。

明白原理之后,编写一个 fastcgi-warpper 也比较简单,网上流传比较多的一个解决方案是,来自 nginx wiki 上的使用 perl 的 fastcgi 包装脚本 cgiwrap-fcgi.pl。
其实编写 C 的 fastcgi-wrapper 就类似 C 的 fastcgi 程序,可以参考 fastcgi-wrapper 。
下载源码之后通过如下命令安装,默认安装在 /usr/local/sbin/fcgiwrap
$ autoreconf -i
$ ./configure && make
# make install
仍以上述的示例为例,通过如下命令启动
# spawn-fcgi -a 127.0.0.1 -p 7001 -u nginx -f /usr/local/sbin/fcgiwrap
配置文件中添加如下内容
... ...
location ~ .*\.cgi$ {
root /path/to/modules
fastcgi_pass 127.0.0.1:7001;
fastcgi_index index.cgi;
include fastcgi.conf;
}
... ...
2、Nginx+FastCGI
fastcgi 进程由 FastCGI 进程管理器所管理,而不是 nginx,这样就需要一个程序管理我们编写的 fastcgi 程序,在此使用 spawn-fcgi。它是一个通用的 FastCGI 进程管理器,简单小巧,原属于 lighttpd 的一部分,后来由于使用比较广泛,所以就迁移出来作为独立项目了。
spawn-fcgi 使用 pre-fork 模型,功能主要是打开监听端口,绑定地址,然后 fork-and-exec 创建我们编写的 fastcgi 应用程序进程,退出完成工作。可以通过如下方法测试 C 的 FastCGI 程序。
在 CentOS 中可以通过如下命令直接安装 (依赖 EPEL 源),当然也可以通过源码安装。
# yum install spawn-fcgi fcgi fcgi-devel
新建 FastCGI 应用程序
$ cat examples/foobar.c # 查看源码
#include <fcgi_stdio.h>;
int main( int argc, char *argv[] ){
int count = 0;
while( FCGI_Accept() >= 0 )
FCGI_printf( "Status: 200 OK\r\n" );
FCGI_printf( "Content-Type: text/html\r\n\r\n" );
FCGI_printf( "Hello world in C, #%d running on host %s\n",++count, getenv("SERVER_NAME"));
}
return 0;
}
$ gcc -o foobar.cgi foobar.c -L /usr/local/lib/ -lfcgi # 直接编译
启动 spawn-fcgi
$ spawn-fcgi -a 127.0.0.1 -p 7000 -u nginx -f /tmp/examples/foobar/foobar.cgi
修改 nginx 配置文件,添加如下内容,然后通过 kill -HUP PID 重新加载配置文件。
... ...
location = /hello.cgi {
fastcgi_pass 127.0.0.1:7000;
fastcgi_index index.cgi;
fastcgi_param SCRIPT_FILENAME fcgi$fastcgi_script_name;
include fastcgi_params;
}
... ...
然后可以通过 http://localhost/hello.cgi 查看。如果报错可以查看日志,最好通过 setenforce 0 将 selinux 关闭,否则可能会有 502 Bad Gateway 报错。
参考
FastCGI.com Archives,包括了 FastCGI 标准,以及 C 的实现 fcgi library。
网关协议CGI、FastCGI、WSGI的区别
该文章最后由 阿炯 于 2024-06-11 16:46:10 更新,目前是第 2 版。