CGI与FastCGI

CGI

概念

"common gateway interface",通用网关接口,CGI的存在,可以是用户通过浏览器来访问执行在服务器上的动态程序;CGI是Web服务器与CGI程序间传输数据的标准;

CGI协议

准确的说,CGI是一个协议,一个请求要想完成,需要Web服务器和CGI程序交互完成,而在传输数据过程中,数据内容就是CGI协议所定义的,比如一个HTTP请求,不管是GET还是POST方式,请求中的URL,参数等数据都需要通过CGI指定的数据格式进行传输,CGI程序开发者则通过解析这些数据以完成程序的后续处理。

CGI请求
  • 服务器根据 以 / 分隔的路径选择解释器;
  • 如果有 AUTH 字段,需要先执行 AUTH,再执行解释器;
  • 服务器确认 CONTENT-LENGTH 表示的是数据解析出来的长度,如果附带信息体,则必须将长度字段传送到解释器;
  • 如果有 CONTENT-TYPE 字段,服务器必须将其传给解释器;若无此字段,但有信息体,则服务器判断此类型或抛弃信息体;
  • 服务器必须设置 QUERY_STRING 字段,如果客户端没有设置,服务端要传一个空字符串""
  • 服务器必须设置 REMOTE_ADDR,即客户端请求IP;
  • REQUEST_METHOD 字段必须设置, GET POST 等,大小写敏感;
  • SCRIPT_NAME 表示执行的解释器脚本名,必须设置;
  • SERVER_NAME 和 SERVER_PORT 代表着大小写敏感的服务器名和服务器受理时的TCP/IP端口;
  • SERVER_PROTOCOL 字段指示着服务器与解释器协商的协议类型,不一定与客户端请求的SCHEMA 相同,如'https://'可能为HTTP;
  • 在 CONTENT-LENGTH 不为 NULL 时,服务器要提供信息体,此信息体要严格与长度相符,即使有更多的可读信息也不能多传;
  • 服务器必须将数据压缩等编码解析出来;
CGI响应
  • CGI解释器必须响应 至少一行头 + 换行 + 响应内容;
  • 解释器在响应文档时,必须要有 CONTENT-TYPE 头;
  • 在客户端重定向时,解释器除了 client-redir-response=绝对url地址,不能再有其他返回,然后服务器返回一个 302 状态码;
  • 解释器响应 三位数字状态码,具体配置可自行搜索;
  • 服务器必须将所有解释器返回的数据响应给客户端,除非需要压缩等编码,服务器不能修改响应数据;
CGI进程

一个请求的处理,当WebServer完成自有任务后,会启动一个响应的CGI解释器,以PHP语言来说,就是启动一个CGI进程(php-cgi),这个进程会先加载php.ini的配置,通过配置处理响应的工作,比如加载配置中的php扩展等,最后动态解析PHP程序,完成工作并响应结果。

FastCGI

概念

"fast common gateway interface/FastCGI",快速通用网关接口,是CGI的优化升级。

FastCGI协议

它是建立在CGI/1.1基础之上的,把CGI/1.1里面的要传递的数据通过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;
FastCGI消息头

FastCGI 消息分10种消息类型,有的是输入有的是输出。而所有的消息都以一个消息头开始。其结构体定义如下:

typedef struct _fcgi_header {
    unsigned char version; // 标识FastCGI协议版本
    unsigned char type; // 标识FastCGI记录类型,也就是记录执行的一般职能
    unsigned char requestIdB1; // 标识记录所属的FastCGI请求
    unsigned char requestIdB0; // 标识记录所属的FastCGI请求
    unsigned char contentLengthB1; // 记录的contentData组件的字节数
    unsigned char contentLengthB0; // 记录的contentData组件的字节数
    unsigned char paddingLength;
    unsigned char reserved;
} fcgi_header;

关于上面的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怎么办,则分割为多个相同类型的消息发送即可。

FCGI_BEGIN_REQUEST 的定义
typedef struct _fcgi_begin_request {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} fcgi_begin_request;

role表示Web服务器期望应用扮演的角色。分为三个角色(而我们这里讨论的情况一般都是响应器角色)

typedef enum _fcgi_role {
    FCGI_RESPONDER  = 1,
    FCGI_AUTHORIZER = 2,
    FCGI_FILTER     = 3
} fcgi_role;

而FCGI_BEGIN_REQUEST中的flags组件包含一个控制线路关闭的位:flags & FCGI_KEEP_CONN:如果为0,则应用在对本次请求响应后关闭线路。如果非0,应用在对本次请求响应后不会关闭线路;Web服务器为线路保持响应性。

FCGI_END_REQUEST 的定义
typedef struct _fcgi_end_request {
    unsigned char appStatusB3; // 组件是应用级别的状态码
    unsigned char appStatusB2; // 组件是应用级别的状态码
    unsigned char appStatusB1; // 组件是应用级别的状态码
    unsigned char appStatusB0; // 组件是应用级别的状态码
    unsigned char protocolStatus; // 组件是协议级别的状态码
    unsigned char reserved[3];
} fcgi_end_request;

protocolStatus的值可能是:
FCGI_REQUEST_COMPLETE:请求的正常结束。
FCGI_CANT_MPX_CONN:拒绝新请求。这发生在Web服务器通过一条线路向应用发送并发的请求时,后者被设计为每条线路每次处理一个请求。
FCGI_OVERLOADED:拒绝新请求。这发生在应用用完某些资源时,例如数据库连接。
FCGI_UNKNOWN_ROLE:拒绝新请求。这发生在Web服务器指定了一个应用不能识别的角色时。
需要注意dcgi_protocol_status和fcgi_role各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。

消息通讯样例

为了简单的表示,消息头只显示消息的类型和消息的 id,其他字段都不予以显示。下面的例子来自于官网

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDOUT,          1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
{FCGI_END_REQUEST,     1, {0, FCGI_REQUEST_COMPLETE}}

配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程:
首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDER,flag为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT消息体和FCGI_END_REQUEST消息体供 Web 服务器解析。

FastCGI进程管理器

基于FastCGI协议,为了提高CGI程序的性能,采用进程池的方式来解决每次form新的CGI进程的开销,并通过主进程统一管理调度CGI子进程,以PHP语言为例,PHP-FPM(FastCGI Process Manager)即实现了FastCGI进程管理器。

CGI和FastCGI的区别

准确的说,这个命题是在讨论两种协议的区别,或者说基于CGI/1.1进行升级有哪些,往往我们所说的却并不是这样,我们常常比较这两者是基于FastCGI的进程管理器相较过往的CGI程序执行,发生哪些改变,带来了哪些好处,却已经不单纯是在说两种协议的数据定义区别了。

  • 基于CGI/1.1,FastCGI把CGI/1.1里面的要传递的数据通过FastCGI协议定义的顺序、格式进行传递。
  • 基于FastCGI,通常使用一些FastCGI进程管理器来优化升级CGI程序的处理过程,相比传统的CGI程序执行过程,使用FastCGI进程管理器,会得到性能的大幅提升
传统的CGI程序执行过程
传统的CGI程序执行过程
基于FastCGI进程管理器程序执行过程
基于FastCGI进程管理器程序执行过程
  • FastCGI 与传统 CGI 模式的区别之一则是 Web 服务器不是直接执行 CGI 程序了,而是通过 socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,Web 服务器需要将 CGI 接口数据封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。正是由于 FastCGI 进程管理器是基于 socket 通信的,所以也是分布式的,Web服务器和CGI响应器服务器分开部署。

推荐阅读更多精彩内容