CGI,FastCGI,PHP-CGI,PHP-FPM
CGI 简介
CGI全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。 如php,perl,tcl等。
CGI 的运行原理
- 客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。
- 服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。
- 再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端。
上面的这段话理解可能还是比较抽象,下面我们就通过一次 GET 请求为例进行详细说明。
如图所示,本次请求的流程如下:
- 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
- 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
- 通过解析 HTTP 头信息,得知是 GET 请求,并且请求的是
/cgi-bin/目录下的user文件。 - 将 uri 里的
id=1通过存入QUERY_STRING环境变量。 - Web 守护进程 fork 一个子进程,然后在子进程中执行 user 程序,通过环境变量获取到
id。 - 执行完毕之后,将结果通过标准输出返回到子进程。
- 子进程将结果返回给客户端。
FastCGI 简介
FastCGI是Web服务器和处理程序之间通信的一种协议, 是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI, 它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI最为人诟病的fork-and-execute模式)。 正是因为他只是一个通信协议,它还支持分布式的运算,所以 FastCGI 程序可以在网站服务器以外的主机上执行,并且可以接受来自其它网站服务器的请求。
FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等。
FastCGI 工作流程如下:
- FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的连接。
- Web 服务器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
- CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。
- CGI 解释器进程接着等待并处理来自 Web Server 的下一个连接。
FastCGI 与传统 CGI 模式的区别之一则是 Web 服务器不是直接执行 CGI 程序了,而是通过 Socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,也正是由于 FastCGI 进程管理器是基于 Socket 通信的,所以也是分布式的,Web 服务器可以和 CGI 响应器服务器分开部署。Web 服务器需要将数据 CGI/1.1 的规范封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。
FastCGI 协议
可能上面的内容理解起来还是很抽象,这是由于第一对FastCGI协议还没有一个大概的认识,第二没有实际代码的学习。所以需要预先学习下 FastCGI 协议,不一定需要完全看懂,可大致了解之后,看完本篇再结合着学习理解消化。
下面结合 PHP 的 FastCGI 的代码进行分析,不作特殊说明以下代码均来自于 PHP 源码。
FastCGI 消息类型
FastCGI 将传输的消息做了很多类型的划分,其结构体定义如下:
typedef enum _fcgi_request_type {
FCGI_BEGIN_REQUEST = 1, /* [in] */
FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */
FCGI_END_REQUEST = 3, /* [out] */
FCGI_PARAMS = 4, /* [in] environment variables */
FCGI_STDIN = 5, /* [in] post data */
FCGI_STDOUT = 6, /* [out] response */
FCGI_STDERR = 7, /* [out] errors */
FCGI_DATA = 8, /* [in] filter data (not supported) */
FCGI_GET_VALUES = 9, /* [in] */
FCGI_GET_VALUES_RESULT = 10 /* [out] */
} fcgi_request_type;
消息的发送顺序
下图是一个比较常见消息传递流程
最先发送的是FCGI_BEGIN_REQUEST,然后是FCGI_PARAMS和FCGI_STDIN,由于每个消息头(下面将详细说明)里面能够承载的最大长度是65535,所以这两种类型的消息不一定只发送一次,有可能连续发送多次。
FastCGI 响应体处理完毕之后,将发送FCGI_STDOUT、FCGI_STDERR,同理也可能多次连续发送。最后以FCGI_END_REQUEST表示请求的结束。 需要注意的一点,FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应。
由于整个消息是二进制连续传递的,所以必须定义一个统一的结构的消息头,这样以便读取每个消息的消息体,方便消息的切割。这在网络通讯中是非常常见的一种手段。
FastCGI 消息头
如上,FastCGI 消息分10种消息类型,有的是输入有的是输出。而所有的消息都以一个消息头开始。其结构体定义如下:
typedef struct _fcgi_header {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} fcgi_header;
字段解释下:
version标识FastCGI协议版本。 type 标识FastCGI记录类型,也就是记录执行的一般职能。 requestId标识记录所属的FastCGI请求。 contentLength记录的contentData组件的字节数。
关于上面的xxB1和xxB0的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定归纳了一个由超过两个字节表示的数字的处理方式。
比如协议头中requestId和contentLength表示的最大值就是 65535。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
unsigned char requestIdB1 = UCHAR_MAX;
unsigned char requestIdB0 = UCHAR_MAX;
printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535
}
你可能会想到如果一个消息体长度超过65535怎么办,则分割为多个相同类型的消息发送即可。
PHP中的CGI实现
PHP的CGI实现了FastCGI协议,是一个TCP或UDP协议的服务器接受来自Web服务器的请求, 当启动时创建TCP/UDP协议的服务器的socket监听,并接收相关请求进行处理。随后就进入了PHP的生命周期: 模块初始化,sapi初始化,处理PHP请求,模块关闭,sapi关闭等就构成了整个CGI的生命周期。
以TCP为例,在TCP的服务端,一般会执行这样几个操作步骤:
- 调用socket函数创建一个TCP用的流式套接字;
- 调用bind函数将服务器的本地地址与前面创建的套接字绑定;
- 调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
- 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
- 当与客户端创建连接后,服务器调用read_stream函数读取客户的请求;
- 处理完数据后,服务器调用write函数向客户端发送应答。
TCP上客户-服务器事务的时序如图2.6所示:
PHP的CGI实现从cgi_main.c文件的main函数开始,在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。 对比TCP的流程,我们查看PHP对TCP协议的实现,虽然PHP本身也实现了这些流程,但是在main函数中一些过程被封装成一个函数实现。 对应TCP的操作流程,PHP首先会执行创建socket,绑定套接字,创建监听:
if (bindpath) {
fcgi_fd = fcgi_listen(bindpath, 128); // 实现socket监听,调用fcgi_init初始化
...
}
在fastcgi.c文件中,fcgi_listen函数主要用于创建、绑定socket并开始监听,它走完了前面所列TCP流程的前三个阶段,
if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||
...
bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||
listen(listen_socket, backlog) < 0) {
...
}
当服务端初始化完成后,进程调用accept函数进入阻塞状态,在main函数中我们看到如下代码:
while (parent) {
do {
pid = fork(); // 生成新的子进程
switch (pid) {
case 0: // 子进程
parent = 0;
/* don't catch our signals */
sigaction(SIGTERM, &old_term, 0); // 终止信号
sigaction(SIGQUIT, &old_quit, 0); // 终端退出符
sigaction(SIGINT, &old_int, 0); // 终端中断符
break;
...
default:
/* Fine */
running++;
break;
} while (parent && (running < children));
...
while (!fastcgi || fcgi_accept_request(&request) >= 0) {
SG(server_context) = (void *) &request;
init_request_info(TSRMLS_C);
CG(interactive) = 0;
...
}
如上的代码是一个生成子进程,并等待用户请求。在fcgi_accept_request函数中,程序会调用accept函数阻塞新创建的进程。 当用户的请求到达时,fcgi_accept_request函数会判断是否处理用户的请求,其中会过滤某些连接请求,忽略受限制客户的请求, 如果程序受理用户的请求,它将分析请求的信息,将相关的变量写到对应的变量中。 其中在读取请求内容时调用了safe_read方法。如下所示: [main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]
static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
size_t n = 0;
do {
... // 省略 对win32的处理
ret = read(req->fd, ((char*)buf)+n, count-n); // 非win版本的读操作
... // 省略
} while (n != count);
}
如上对应服务器端读取用户的请求数据。
在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。 假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。 在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码, 并执行zend_execute函数,从而执行这些中间代码。关于整个脚本的执行请参见第三节 脚本的执行。
在处理完用户的请求后,服务器端将返回信息给客户端,此时在main函数中调用的是fcgi_finish_request(&request, 1); fcgi_finish_request函数定义在fastcgi.c文件中,其代码如下:
int fcgi_finish_request(fcgi_request *req, int force_close)
{
int ret = 1;
if (req->fd >= 0) {
if (!req->closed) {
ret = fcgi_flush(req, 1);
req->closed = 1;
}
fcgi_close(req, force_close, 1);
}
return ret;
}
如上,当socket处于打开状态,并且请求未关闭,则会将执行后的结果刷到客户端,并将请求的关闭设置为真。 将数据刷到客户端的程序调用的是fcgi_flush函数。在此函数中,关键是在于答应头的构造和写操作。 程序的写操作是调用的safe_write函数,而safe_write函数中对于最终的写操作针对win和linux环境做了区分, 在Win32下,如果是TCP连接则用send函数,如果是非TCP则和非win环境一样使用write函数。如下代码:
#ifdef _WIN32
if (!req->tcp) {
ret = write(req->fd, ((char*)buf)+n, count-n);
} else {
ret = send(req->fd, ((char*)buf)+n, count-n, 0);
if (ret <= 0) {
errno = WSAGetLastError();
}
}
#else
ret = write(req->fd, ((char*)buf)+n, count-n);
#endif
在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。 fcgi_close函数在前面提的fcgi_finish_request函数中,在请求应答完后执行。同样,对于win平台和非win平台有不同的处理。 其中对于非win平台调用的是write函数。
以上是一个TCP服务器端实现的简单说明。这只是我们PHP的CGI模式的基础,在这个基础上PHP增加了更多的功能。
php-fpm
FastCGI接口方式在脚本解析服务器上启动一个或者多个守护进程对动态脚本进行解析,这些进程就是FastCGI进程管理器,或者称之为FastCGI引擎, spawn-fcgi与PHP-FPM就是支持PHP的两个FastCGI进程管理器。
FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。
它的功能包括:
-
支持平滑停止/启动的高级进程管理功能;
-
可以工作于不同的 uid/gid/chroot 环境下,并监听不同的端口和使用不同的 php.ini 配置文件(可取代 safe_mode 的设置);
-
stdout 和 stderr 日志记录;
-
在发生意外情况的时候能够重新启动并缓存被破坏的 opcode;
-
文件上传优化支持;
-
"慢日志" - 记录脚本(不仅记录文件名,还记录 PHP backtrace 信息,可以使用 ptrace或者类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢;
-
fastcgi_finish_request() - 特殊功能:用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等);
-
动态/静态子进程产生;
-
基本 SAPI 运行状态信息(类似Apache的 mod_status);
-
基于 php.ini 的配置文件。
使用PHP-FPM来控制PHP-CGI的FastCGI进程
什么是PHP-CGI
PHP-CGI是PHP自带的FastCGI管理器。
启动PHP-CGI,使用如下命令:
php-cgi -b 127.0.0.1:9000
PHP-CGI的不足
1、php-cgi变更php.ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
2、直接杀死php-cgi进程,php就不能运行了。(PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程。)
什么是PHP-FPM
PHP-FPM是一个PHP FastCGI管理器,是只用于PHP的,可以在 http://php-fpm.org/download下载得到.
PHP-FPM其实是PHP源代码的一个补丁,旨在将FastCGI进程管理整合进PHP包中。必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。
现在我们可以在最新的PHP 5.3.2的源码树里下载得到直接整合了PHP-FPM的分支,据说下个版本会融合进PHP的主分支去。相对Spawn-FCGI,PHP-FPM在CPU和内存方面的控制都更胜一筹,而且前者很容易崩溃,必须用crontab进行监控,而PHP-FPM则没有这种烦恼。
PHP5.3.3已经集成php-fpm了,不再是第三方的包了。PHP-FPM提供了更好的PHP进程管理方式,可以有效控制内存和进程、可以平滑重载PHP配置,比spawn-fcgi具有更多有点,所以被PHP官方收录了。在./configure的时候带 –enable-fpm参数即可开启PHP-FPM。
使用PHP-FPM来控制PHP-CGI的FastCGI进程
/usr/local/php/sbin/php-fpm{start|stop|quit|restart|reload|logrotate}
--start 启动php的fastcgi进程
--stop 强制终止php的fastcgi进程
--quit 平滑终止php的fastcgi进程
--restart 重启php的fastcgi进程
--reload 重新平滑加载php的php.ini
--logrotate 重新启用log文件
浙公网安备 33010602011771号