CGI,FastCGI,PHP-CGI,PHP-FPM

CGI 简介

CGI全称是“通用网关接口”(Common Gateway Interface),它可让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何语言的,因此CGI能够用任何一种语言编写,只要这种语言具备标准输入、输出和环境变量。 如php,perl,tcl等。php

CGI 的运行原理

  1. 客户端访问某个 URL 地址以后,经过 GET/POST/PUT 等方式提交数据,并经过 HTTP 协议向 Web 服务器发出请求。
  2. 服务器端的 HTTP Daemon(守护进程)启动一个子进程。而后在子进程中,将 HTTP 请求里描述的信息经过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果经过标准输出 stdout 返回给 HTTP Daemon 子进程。
  3. 再由 HTTP Daemon 子进程经过 HTTP 协议返回给客户端。

上面的这段话理解可能仍是比较抽象,下面咱们就经过一次 GET 请求为例进行详细说明。html

 

图2.7 CGI 运行原理示举例示意图
图2.7 CGI 运行原理示举例示意图

 

如图所示,本次请求的流程以下:linux

  1. 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
  2. 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
  3. 经过解析 HTTP 头信息,得知是 GET 请求,而且请求的是 /cgi-bin/ 目录下的 user 文件。
  4. 将 uri 里的 id=1 经过存入 QUERY_STRING 环境变量。
  5. Web 守护进程 fork 一个子进程,而后在子进程中执行 user 程序,经过环境变量获取到id
  6. 执行完毕以后,将结果经过标准输出返回到子进程。
  7. 子进程将结果返回给客户端。

FastCGI 简介

FastCGI是Web服务器和处理程序之间通讯的一种协议, 是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI, 它能够一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI最为人诟病的fork-and-execute模式)。 正是由于他只是一个通讯协议,它还支持分布式的运算,因此 FastCGI 程序能够在网站服务器之外的主机上执行,而且能够接受来自其它网站服务器的请求。api

FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此得到较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要缘由,若是 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则能够提供良好的性能、伸缩性、Fail-Over 特性等。浏览器

FastCGI 工做流程以下:

  1. FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的链接。
  2. Web 服务器与 FastCGI 进程管理器进行 Socket 通讯,经过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
  3. CGI 解释器进程完成处理后将标准输出和错误信息从同一链接返回 Web Server。
  4. CGI 解释器进程接着等待并处理来自 Web Server 的下一个链接。

 

图2.8 FastCGI 运行原理示举例示意图
图2.8 FastCGI 运行原理示举例示意图

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;

消息的发送顺序

下图是一个比较常见消息传递流程socket

 

图2.9 FastCGI 消息传递流程示意图
图2.9 FastCGI 消息传递流程示意图

 

最早发送的是FCGI_BEGIN_REQUEST,而后是FCGI_PARAMSFCGI_STDIN,因为每一个消息头(下面将详细说明)里面可以承载的最大长度是65535,因此这两种类型的消息不必定只发送一次,有可能连续发送屡次。

FastCGI 响应体处理完毕以后,将发送FCGI_STDOUTFCGI_STDERR,同理也可能屡次连续发送。最后以FCGI_END_REQUEST表示请求的结束。 须要注意的一点,FCGI_BEGIN_REQUESTFCGI_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组件的字节数。

关于上面的xxB1xxB0的协议说明:当两个相邻的结构组件除了后缀“B1”和“B0”以外命名相同时,它表示这两个组件可视为估值为B1<<8 + B0的单个数字。该单个数字的名字是这些组件减去后缀的名字。这个约定概括了一个由超过两个字节表示的数字的处理方式。

好比协议头中requestIdcontentLength表示的最大值就是 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的服务端,通常会执行这样几个操做步骤:

  1. 调用socket函数建立一个TCP用的流式套接字;
  2. 调用bind函数将服务器的本地地址与前面建立的套接字绑定;
  3. 调用listen函数将新建立的套接字做为监听,等待客户端发起的链接,当客户端有多个链接链接到这个套接字时,可能须要排队处理;
  4. 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而创建起一个链接;
  5. 当与客户端建立链接后,服务器调用read_stream函数读取客户的请求;
  6. 处理完数据后,服务器调用write函数向客户端发送应答。

TCP上客户-服务器事务的时序如图2.6所示:

 

图2.6 TCP上客户-服务器事务的时序
图2.6 TCP上客户-服务器事务的时序

 

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的不足

  一、php-cgi变动php.ini配置后需重启php-cgi才能让新的php-ini生效,不能够平滑重启

  二、直接杀死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文件 
相关文章
相关标签/搜索