FastCGI简介
FastCGI是用于让Web服务器与应用程序进行通信的二进制协议(而不是人类可读的文本协议)。 它是早期的通用网关接口(CGI)的变体。 FastCGI的主要目的是减少Web服务器和CGI程序之间的接口相关的资源开销,从而使服务器在单位时间内能够处理更多的Web请求。

FastCGI是CGI协议的改进,它可以持续执行,而不需要像CGI一样,每次都需要重新fork,正因为如此,它处理请求的效率很高。由于FastCGI是对CGI的改进,协议底层依然是与CGI兼容的,所以,普通的CGI程序可以很快的改造升级成支持FastCGI的版本。

FastCGI的运行原理

  1. FastCGI 进程管理器启动时会创建一个主进程(Master,它是进程管理器)和多个 CGI 解释器进程(Worker),然后等待 Web 服务器的连接。
  2. Web 服务器接收 HTTP 请求后,会与 FastCGI 进程管理器进行 Socket 通信,向FastCGI进程管理器发送二进制数据,FastCGI进程管理器一般会根据负载情况,转发这些包含CGI 环境变量和标准输入数据的消息给 相应的CGI 解释器进程。
  3. CGI 解释器进程接收到消息,完成处理后将标准输出和错误信息从同一连接返回 Web服务器。
  4. CGI 解释器进程接着会持续监听,等待来自 Web 服务器的下一个请求。
    看下面的图片更加直观的了解整个流程:
(by mengkang)

有了以上的基本概念时候,接下来我们深入了解下FastCGI协议内容。

FastCGI协议

与HTTP协议不同,FastCGI协议直接使用二进制传递数据,而不像HTTP协议使用人类可读的文本。所以,我们需要先对FastCGI协议消息传递的内容和流程有所了解。

在FastCGI中,每一个HTTP请求(或者响应)消息都分为若干个记录(Record)进行传递, 每个Record又由头部(Header)和数据(Body)组成。

Header是针对这个Record的一些元信息描述(如请求ID、Record类型、Data长度等等),Body是指这个Record中实际的有效数据,下面是1个Record的结构示意图:
|———-—-Record————-|
| Header | Body |
|————————-————-|

使用Record进行消息传递有2个好处:

  • 多个请求的数据可以复用同一个连接进行传输,这样应用的实现就可以采用事件驱动的编程模型或者多线程编程模型以提升效率;
  • 同一个请求中的多个数据流的数据可以通过封装成不同记录的形式在同一个连接上传输,例如 STDOUT 和 STDERR 两个输出流的数据可以通过同一个连接返回给 Web 服务器,而不是不得不使用2个连接。

有了以上的基本概念后,我们可以来看看FastCGI请求具体是如何传递的:

Web服务器发送FastCGI请求时:依次发送了3类Record,类型分别为BEGIN_REQUEST、PARAMS和STDIN
FastCGI进程返回FastCGI响应时:依次返回了3类Record,类型分别为STDOUT、STDERR、END_REQUEST

注意,上面我写的是“3类”Record而不是“3个”Record,那是因为每个Record头部(Header)长度有限,所以,在发送过程中PARAMS和STDIN类型的Record可能会多次发送(而且在PARAMS和STDIN数据发送完后,还要发送一个不携带数据的空PARAMS或STDIN类型的Record,来表示这类Record已经发送完了),同样的道理,在响应的过程中,STDOUT和STDERR类型的Record也可能会多次发送,也就是说,这些类型的Record,都可能会发送(接收)到好几个。

虽然FastCGI协议在逻辑上是通过多个Record进行发送的,但实际上整个消息是二进制连续传递的,所以我们接收和发送的数据是没有明显分隔的,就像下面这样,前一个Record和后一个Recor的Header、Body是紧密相连的:
|———-—-Record————……………—-—-Record—————-—-Record—|
|HeaderBodyHeaderBody……….HeaderBodyHeaderBodyHeaderBody|
|————————-————………..—————-————-————————- —|

对应的二进制表示大概这样:
01010100101010101100101010101010101010101100101010101010…….

我们必须逐步读取二进制数据,先解析出FastCGI协议预定义结构的消息头部(Header),然后根据消息头部里面提供的消息类型和消息体(Body)长度等信息,读取紧接着消息头部的消息体内容,通过这种方式来完成数据的切割。

这种数据切割的手段在网络通讯中是非常常见的,掌握了这种技术之后,你可以轻松的理解其他流行的网络协议,如知名的SOCKS5代理协议等。

掌握了上面这些内容后,我们来看看FastCGI的Record里面具体有哪些内容,Record结构体如下:

typedef struct {
  /* Header */
  unsigned char version; // FastCGI版本
  unsigned char type; // 当前Record的类型
  unsigned char requestIdB1; // 当前Record对应的请求id
  unsigned char requestIdB0;
  unsigned char contentLengthB1; // 当前Record中Body体数据的长度
  unsigned char contentLengthB0;
  unsigned char paddingLength; // Body中填充块的长度
  unsigned char reserved; 

  /* Body */
  unsigned char contentData[contentLength]; // Body体数据
  unsigned char paddingData[paddingLength]; // Body中填充块长度
} FCGI_Record;

/上面数据结构中两个相邻的成员变量,它们的名字除了结尾的 “B1”、”B0” 不同外,其他部分都是相同的,这意味着,这两个成员应该被当做一个两字节整数读取,其整数值为 B1 << 8 + B0,真实的变量名字为原始名字去除了 B1、B0 后缀的样子。这是一种多字节整数的简化表达方法。/

从上面 Record结构体的定义可以看出,Record的头部(Header)共由8个char组成,即8个字节(其中requestId和contentLength字段每个各占2个字节,其他的4个字段每个各占1个字节)

每个字段的具体含义如下:

  • version:表示FastCGI协议版本号,目前它的值固定为1
  • type:表示此记录(Record)的类型,标记此记录的主要功能。目前FastCGI协议定义了11种类型的记录(后面会具体解释)。
  • requestId :表示此记录属于哪个 FastCGI 请求。
  • contentLength : 表示紧接着的 contentData 的长度,占2个字节,共16位,所以能够表示0~65535直接的数字。
  • paddingLength : 表示紧接着的 paddingData 的长度。
  • contentData : 字节数组,表示有效数据,长度范围只能在 [0, 65535]之间,根据记录类型不同需要采用不同的解析方法。
  • paddingData : 填充的字节数组,无实际意义,长度范围在 [0, 255],处理时忽略该内容。

除了paddingLength和paddingData字段外,相信其他的字段都比较好理解的。实际上,FastCGI协议考虑到效率问题,允许发送者对要发送的数据进行填充,使数据保持对齐,并要求接收者能够解析出paddingLength、跳过 paddingData部分,这样可以达到更高效的数据处理。
FastCGI协议推荐记录的长度是8字节的整数倍。一个 FastCGI 记录的固定长度部分(头部)正好是8个字节。

另外,因为FastCGI与CGI不同,需要常驻后台,连续处理多个请求,所以需要通过requestId来对请求进行识别,不然会造成混淆。

至此,我们知道了,每个Record里都包含了Header,Header里面的type字段描述了该Record所记录的数据的类型,接下来,我们来一起看看,FastCGI协议中具体定义了哪11种类型的记录(Record)。

FastCGI记录(Record)类型

我们先来看看FastCGI协议定义的记录类型有哪些:

  /*
   * Values for type component of FCGI_Header
   */
#define FCGI_BEGIN_REQUEST       1         // 应用类记录类型
#define FCGI_ABORT_REQUEST       2         // 应用类记录类型
#define FCGI_END_REQUEST         3         // 应用类记录类型
#define FCGI_PARAMS              4         // 应用类记录类型
#define FCGI_STDIN               5         // 应用类记录类型
#define FCGI_STDOUT              6         // 应用类记录类型
#define FCGI_STDERR              7         // 应用类记录类型
#define FCGI_DATA                8         // 应用类记录类型
#define FCGI_GET_VALUES          9         // 管理类记录类型
#define FCGI_GET_VALUES_RESULT  10         // 管理类记录类型
#define FCGI_UNKNOWN_TYPE       11         // 管理类记录类型

实际上,FastCGI针对以上记录类型的分类方法有2种,但我更倾向于分为管理类记录应用类记录

管理类记录:管理类记录不和任何一个具体的请求相关联,所以这类记录头部中的requestId都是0,这类记录主要用于管理FastCGI管理器自身的运行状态。目前管理类记录有3个:
FCGI_GET_VALUES:用于查询FastCGI管理器的运行状态数据,例如支持的最高并发连接数(FCGI_MAX_CONNS)、最高并发请求数(FCGI_MAX_REQS)、是否支持连接复用(FCGI_MPXS_CONNS)等等。
FCGI_GET_VALUES_RESULT:用于返回FastCGI管理器的运行状态数据
FCGI_UNKNOWN_TYPE:用于未来FastCGI协议的扩展

应用类记录:应用类记录是与某一个请求相关联的,头部中的 requestId 表明了该记录与哪一个请求进行关联。除了上面提到的3个管理类记录类型,剩下的都是应用类记录类型,用于在Web服务器和FastCGI应用之间传递HTTP请求数据。

FCGI_BEGIN_REQUEST:
Web 服务器发送一个 FCGI_BEGIN_REQUEST 记录(Record)给 FastCGI 应用,来通知FastCGI,一个新的请求开始了。
前面已经提到,每一个Record中都包含头部(Header)和实际的数据部分,FCGI_BEGIN_REQUEST类型的Record当然也不例外。一个 FCGI_BEGIN_REQUEST 类型记录的数据部分( 也就是 contentData )占8个字节,结构如下:

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

其中 role 成员指明了 Web服务器希望FastCGI应用处理此请求所使用的角色类型,当前有3种定义好的角色类型:

  • FCGI_RESPONDER 作为响应器(最常用)
  • FCGI_AUTHORIZER 作为鉴权器
  • FCGI_FILTER 作为过滤器

其中 flags 中含有一个控制连接关闭方式(是否Keep-Alive)的标志位。
当 flags & FCGI_KEEP_CONN 的值为 0 时,表明需要FastCGI应用在响应此请求之后,主动关闭对应的连接。如果非 0,则不需要关闭,这时,此连接的生命期由 Web 服务器控制。

FCGI_PARAMS
FCGI_PARAMS 类型的记录被 Web 服务器用来向FastCGI应用传递键值对数据,键值对数据会被封装成若干各这类记录,形成一个数据流,依次传递。键值对间不区分先后顺序。这个类型的记录通常用于传递一些环境变量,例如下面这些,相信做Web开发的都很熟悉:

Array
(
    [SCRIPT_FILENAME] => /usr/local/Cellar/nginx/1.15.0/html/fastcgi
    [QUERY_STRING] => user=aiddroid
    [REQUEST_METHOD] => POST
    [CONTENT_TYPE] => application/x-www-form-urlencoded
    [CONTENT_LENGTH] => 14
    [SCRIPT_NAME] => /fastcgi
    [REQUEST_URI] => /fastcgi?uid=1
    [DOCUMENT_URI] => /fastcgi
    [DOCUMENT_ROOT] => /usr/local/Cellar/nginx/1.15.0/html
    [SERVER_PROTOCOL] => HTTP/1.1
    [REQUEST_SCHEME] => http
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_SOFTWARE] => nginx/1.15.0
    [REMOTE_ADDR] => 127.0.0.1
    [REMOTE_PORT] => 50384
    [SERVER_ADDR] => 127.0.0.1
    [SERVER_PORT] => 8080
    [SERVER_NAME] => localhost
    [REDIRECT_STATUS] => 200
    [HTTP_USER_AGENT] => Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 …
    [HTTP_SEC_FETCH_USER] => ?1
    [HTTP_ACCEPT] => text/html,application/xhtml+xml
    [HTTP_SEC_FETCH_SITE] => none
    [HTTP_SEC_FETCH_MODE] => navigate
    [HTTP_ACCEPT_ENCODING] => gzip, deflate, br
    [HTTP_ACCEPT_LANGUAGE] => zh-CN,zh-TW;q=0.9
)

但是这里肯定有人要问:前面已经提到,FastCGI请求中,数据是以二进制进行连续传输的,数据之间并没有明显的分割,FCGI_PARAMS记录中的一堆字段,是怎么解析出来的呢?
实际上,FastCGI协议对FCGI_PARAMS数据的传输,做了编码规范:
FastCGI应用在传输键值对时,需要先传送键(key)的长度,再传递值(value)的长度,然后是键的值,最后是值的值。如果键或值的内容长度不大于 127 字节,那么它的长度就可以使用 1 个字节进行编码,如果长度大于 127 字节,那么统一使用 4 字节进行编码。

举个例子:如果要传输REQUEST_METHOD:POST这个键值对,需要按照如下流程进行处理:
1、对键值对进行分析,得知键(key)为“REQUEST_METHOD”,值(value)为“POST”
2、计算键长度(keyLength)为strlen(“REQUEST_METHOD”) = 14、计算值(valueLength)长度为strlen(“POST”) = 4
3、进行编码,编码结果为:144REQUEST_METHODPOST。其中14和4由于都没有大于127,均只占1个字节。
如果传输的键值对有很多个,只需要重复1~3的步骤,对其他键值对进行编码,并把每个键值对的编码结果直接进行字符串拼接即可得到最终编码。

FCGI_STDIN、FCGI_DATA、FCGI_STDOUT和FCGI_STDERR
FCGI_STDIN 类型记录被 Web 服务器用来向FastCGI应用传输任意格式的数据,如用户POST提交的数据,FCGI_DATA 类型记录是类似的用法。
FCGI_STDOUT 类型记录被FastCGI应用当做标准输出,用于向 Web 服务器返回任意类型的数据,如HTML页面等。
FCGI_STDERR 类型记录被FastCGI应用当做标准错误输出,用来向 Web 服务器返回任意类型的数据。

FCGI_ABORT_REQUET
Web 服务器通过向 FastCGI 应用发送一个 FCGI_ABORT_REQUEST 类型记录来要求终止一个请求的处理。当FastCGI应用收到该记录时,需要立即回复一个 FCGI_END_REQUEST类型的记录,并终止处理相应的请求。

当一个HTTP请求经由 Web 服务器进入到 FastCGI 应用中,当处理尚未结束时,如果此时 HTTP 客户端断开了与 Web 服务器的连接,那么 Web 服务器就会触发此请求的终止操作。这种情况发生比例较低,因为大多数请求在FastCGI应用中都会在较短的时间内完成并返回。但是在某些特殊情况下,FastCGI应用可能会因为外部依赖或者执行某个推送任务而耗时过长。

FCGI_END_REQUEST
FastCGI 应用会通过发送一个 FCGI_END_REQUEST 类型记录给 Web 服务器,来通知它某个请求结束执行了。这种情况发生在请求已经被正常处理完成,或者请求被拒绝执行时。
一个 FCGI_END_REQUEST 类型记录的 contentData 数据部分的结构如下:

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

appStatus 字段是FastCGI应用实现级别的状态码,表示请求处理的状态结果。
protocolStatus 字段是 FastCGI 协议级别的状态码,可能的取值如下:

  • FCGI_REQUEST_COMPLETE :表明正常结束一个请求。
  • FCGI_CANT_MPX_CONN :表明拒绝新请求进入。 这种情况发生在 Web服务器在不允许复用的连接上发送了第二个请求过来时。
  • FCGI_OVERLOADED :表明拒绝新请求进入。这种情况发生在FastCGI应用的资源不足时,比如数据库连接不足时。
  • FCGI_UNKNOWN_ROLE :表明拒绝新请求进入。这种情况发生在 Web 服务器指定了一个FastCGI应用不理解的角色类型时。

FastCGI应用的角色
讨论完FastCGI各种Record类型之后,我们来看看FastCGI应用最常见的角色 =》 作为响应器是如何运作的:
响应器角色下的 FastCGI 应用和 CGI/1.1 的应用表现得基本一致:接收 HTTP 请求的输入数据,返回 HTTP 响应数据。

下面看看该角色下的 FastCGI 协议是如何兼容 CGI/1.1协议:

  • FastCGI应用在收到Web服务器发来的 FCGI_BEGIN_REQUEST 类型记录后,开始处理一个新的请求。
  • FastCGI应用通过读取 Web 服务器传送的 FCGI_PARAMS 记录来实现 CGI/1.1 协议下获取环境变量的模块。
  • 接下来,FastCGI应用通过读取 Web 服务器传送的 FCGI_STDIN 记录来实现 CGI/1.1 协议下读取 stdin 数据的模块。应用最多读取长度为 CONTENT_LENGTH 的数据,且在收到数据结束标记(一个不带数据的FCGI_STDIN记录)时停止接收。
  • FastCGI应用通过发送 FCGI_STDOUT 记录来实现 CGI/1.1 协议的 stdout 输出模块,通过发送 FCGI_STDERR 记录实现 CGI/1.1 协议的 stderr 输出模块。这两个输出流是支持交叉写入的,不限制必须发送完一个再发送另一个。发送数据前要求必须已经完成读取 FCGI_PARAMS 记录数据,但是不要求读取 FCGI_STDIN 记录的流数据完成。
  • 当FastCGI应用已经发送完成 stdout 和 stderr 的数据时,需要再发送一个FCGI_END_REQUEST记录给 Web 服务器,此记录中 protocolStatus 成员值设置为 FCGI_REQUEST_COMPLETE,appStatus 成员值设置成 CGI 协议中 status 字段约定的有效范围内的值,这个 appStatus 会被 exit() 系统调用返回给上层。
    此响应器角色下,如果FastCGI应用处理一个请求时,发现接收的数据长度和 CONTENT_LENGTH 不符,应该立即终止处理这个请求。

FastCGI数据流示例

掌握了FastCGI协议内容和数据处理流程后,我们来看一些HTTP请求通过FastCGI传输的示例。

简单起见,我们引入一些简化表达方式来描述数据流,每一行表示一个Record,每个Record的消息头部(Header)只显示记录类型type和请求序号 requestId,其他字段都不显示,另外还有以下约定:

  • 应用类记录 (FCGI_PARAMS,FCGI_STDIN,FCGI_STDOUT, FCGI_STDERR) 中的 contentData 字段值直接使用字符串来表示。 一个以 “…” 结尾的字符串表示此值过长,显示时进行了省略。
  • 为了区别显示 Web 服务器发送和接收的数据,Web 服务器接收到的数据会右缩进显示。
  • 数据流会按照实际的处理顺序排列显示(从上到下)。
  • 反斜杠0(\0)开头的数据表示8进制数据。

示例1:一个简单的HTTP请求,其 stdin 数据为空,FastCGI应用成功处理了这个请求:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, “\013\002SERVER_PORT80\013\016SERVER_ADDR … “}
{FCGI_PARAMS,          1, “”}
{FCGI_STDIN,           1, “”}

    {FCGI_STDOUT,      1, “Content-type: text/html\r\n\r\n<html>\n<head> … “}
    {FCGI_STDOUT,      1, “”}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

示例2:与示例1中的请求类似,但是本例的FastCGI请求中 stdin 数据不为空(通常是HTTP POST、PUT、PATCH等请求),Web 服务器同时也使用多个 FCGI_PARAMS 记录来发送更多的参数。

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, “\013\002SERVER_PORT80\013\016SER”}
{FCGI_PARAMS,          1, “VER_ADDR199.170.183.42 … “}
{FCGI_PARAMS,          1, “”}
{FCGI_STDIN,           1, “quantity=100&item=3047936”}
{FCGI_STDIN,           1, “”}

    {FCGI_STDOUT,      1, “Content-type: text/html\r\n\r\n<html>\n<head> … “}
    {FCGI_STDOUT,      1, “”}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

可以明显看到,FCGI_PARAMS发送完成后,Web服务器又发送了1个不包含数据的FCGI_PARAMS记录来表示FCGI_PARAMS已经发送完了。同理,FCGI_STDIN发送完成后,Web服务器又发送了一个空的FCGI_STDIN记录来表示FCGI_STDIN已经发送完了。

示例3:与示例1中的请求类似,但是本例中FastCGI应用处理请求时,触发了一个错误。应用会把错误报告发送到 stderr 数据流,同时以非0的退出码退出。在退出之前,FastCGI应用还使用 FCGI_STDOUT 记录返回了一份数据作为响应。

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, “\013\002SERVER_PORT80\013\016SERVER_ADDR … “}
{FCGI_PARAMS,          1, “”}
{FCGI_STDIN,           1, “”}

    {FCGI_STDOUT,      1, “Content-type: text/html\r\n\r\n<ht”}
    {FCGI_STDERR,      1, “config error: missing SI_UID\n”}
    {FCGI_STDOUT,      1, “ml>\n<head> … “}
    {FCGI_STDOUT,      1, “”}
    {FCGI_STDERR,      1, “”}
    {FCGI_END_REQUEST, 1, {938, FCGI_REQUEST_COMPLETE}}

到这里,相信大家已经对FastCGI协议有了比较清晰的理解。

参考文档:
https://tools.ietf.org/html/rfc3875
FastCGI Specification
CGI 和 FastCGI 协议的运行原理 – itbsl – 博客园
https://my.oschina.net/lwl1989/blog/3084842
https://www.yangyu.club/p-214.html
FastCGI 规范中文翻译 – 一只安静的猫
【PHP源码分析】FastCGI协议浅析_慕课手记
php-src/fastcgi.c at master · php/php-src · GitHub

发表评论

Post Navigation