FastCGI协议规范中文版

FastCGI协议规范中文版

  

零、译者序

  

FastCGI 协议是 php-fpm 实现所依赖的标准,nginx 的实现中也有 FastCGI 相关模块。对于想深入了解 php-fpm 或 nginx 的人而言,先学习了解 FastCGI 协议是比较合理的需求。

译者搜索了互联网上的 FastCGI 规范文档,发现只有本文的原文(下称原文),描述的比较清晰、全面,但没有找到对应的中文版,因此才对原文进行了翻译,供中文地区的朋友阅读参考。

本文作为原文的译文,基于符合中文习惯的原则,对原文表述进行了一些替换和整理,并没有逐句逐字地对照翻译。当然,表达的含义是保证一致的。如果发现错误,可以到 www.yangyu.club 联系我,或者新浪微博联系 @程序员杨宇。

最后感谢原文作者 Mark R. Brown

原文链接是 https://fastcgi-archives.github.io/FastCGI_Specification.html

  
  
杨宇
个人网站:www.yangyu.club
新浪微博:程序员杨宇
2019年2月14日

  
  

一、引言

  
FastCGI 是一个开放的 CGI 扩展协议,可以通过非常简单地把 CGI 应用移植为 FastCGI 应用,让 CGI 应用表现出更高的性能。

本规范从实现一个 FastCGI 应用的角度出发,约定了应用和支持 FastCGI 协议的 Web 服务器之间的交互协议。另外,Web 服务器的一些其他的涉及到 FastCGI 的特性,比如对 FastCGI 应用的管理模块,这些模块和交互协议没关系不大,这些就不在本规范的叙述范围之内。

这份规范是基于 Unix 系统的,更确切地说是基于 POSIX 兼容系统 。本规范的主要内容是机器字节序类型无关的通信协议,因此这些协议可以扩展到其他非 Unix 系统上。

这里会参照 Unix 下 CGI/1.1 应用的各个模块,来一一描述 FastCGI 应用的实现逻辑。 FastCGI 和 CGI 应用实现上的最大的区别是,FastCGI 应用实现模块中会有一个常驻服务进程。一个 CGI 应用的典型实现是,每次响应一个 Web 服务器发送过来的请求时,都会创建和初始化一个新的处理进程,使用这个进程来产生此请求对应的响应数据,最后在处理结束之后,关闭此进程。FastCGI 应用的初始化要比 CGI/1.1的简化很多,其进程是固定常驻的,不需要管理生命周期,不需要初始化标准输入输出流 stdin、stdout、stderr,也不需要读取大量的环境变量。此初始化的核心部分是监听一个 socket(套接字),通过这个 socket 来接收 Web 服务器发来的请求。

当应用进程从当前监听的 socket 中成功建立了一个新的连接后,就会使用一个简单的 FastCGI 协议来与 Web 服务器进行通信。这个协议的设计支持了两种特性:第一个特性是,可以让多个 HTTP 请求使用同一个连接进行数据交互,这样应用的实现就可以采用事件驱动编程模型或者多线程编程模型;第二个特性是,同一个请求的多个数据流的传输可以复用同一个连接,这样 FastCGI 应用在输出响应数据时,就可以把 stdout、stderr 两个数据流的数据通过同一个连接发送给 Web 服务器,不用像 CGI/1.1应用那样必须使用两个连接才行。

本规范定义了几种 FastCGI 应用的角色类型。应用最常见的角色是作为响应器。在这种角色中,应用会从 Web 服务器获取到 HTTP 请求的所有数据,然后对其生成 HTTP 响应。此时 FastCGI应用表现得和一个 CGI/1.1的应用一模一样。第二种角色是鉴权器,在这种角色下,应用会从 Web 服务器获取到 HTTP 请求的所有数据,然后判定此请求是否被授权通过。第三种角色是过滤器,在这种角色下,应用会从 Web 服务器获取到 HTTP 请求的所有数据,附加一个位于 Web 服务器上的文件的输入流数据。应用会对这个额外的文件输入流数据进行过滤,然后把过滤后得到的结果组装成一个响应,发送给 Web 服务器。应用的角色类型是可以根据需求的扩展而扩展的。

为了防止读者产生误解,这里对本规范中的用词和概念做一个约定:

  • 应用、FastCGI 应用 : 指的是一个FastCGI 的实现,它接受 Web 服务器的请求,并返回应答。例如 php-fpm。

  • 服务器、Web 服务器 : 指的是一个 HTTP 服务器,它接受 HTTP 客户端的请求,并返回应答。例如 nginx。

  • 客户端 : 指的是一个 HTTP 客户端,它产生一个HTTP请求。例如浏览器。

  

二、应用初始化

  

2.1 参数列表

  

应用初始化时,会从 Web服务器接收到一个参数列表。在默认情况下,这个列表只包含一个参数,其值取自 Web服务器可执行程序的文件名,用来指定应用的进程名,当然 Web 服务器也可以为这个参数设置其他的值,这样也是生效的。这个参数列表也可以支持更复杂的情况,比如传递多个参数。

  

2.2 文件描述符

  
应用初始化过程中,会被绑定一个新的文件描述符 FCGI_LISTENSOCK_FILENO ,这个描述符是当前与 Web 服务器建立的连接的 socket 的引用。对应用来说,FCGI_LISTENSOCK_FILENO 的作用等同于标准输入流文件描述符 STDIN_FILENO ,而 标准输出流文件描述符 STDOUT_FILENO 和标准错误输出流文件描述符 STDERR_FILENO 则会被关闭。这里有一个可靠的方法来判定应用的运行模式:应用可以调用 getpeername (FCGI_LISTENSOCK_FILENO) ,获取其执行结果,如果结果等于 -1,且 errno 被设置成 ENOTCONN,则说明此应用当前处于 FastCGI 模式下,而非 CGI 模式。

Web 服务器和应用之间的传输模式设定,比如可信传输模式、Unix管道模式(AF_UNIX)、TCP/IP模式 (AF_INET),这些设定都记录在 FCGI_LISTENSOCK_FILENO 对应的 socket 的状态属性上。

  

2.3 环境变量

  
Web 服务器可以通过设置环境变量的方式传参给应用,本规范就涉及了这样一个环境变量 - FCGI_WEB_SERVER_ADDRS,随着规范的不断演进,新的环境变量很可能会被不断地添加进来。比如 Web 服务器可能会发送 PATH 过来。

  

2.4 其他状态

  

基于 Web 服务器的实现,应用进程初始化时涉及到的一些其他的属性,比如优先级、用户 ID、用户组 ID、根目录、活动目录等,也有可能被传送过来并指定。

  

三、协议基础

  

3.1 表达约定

  
本规范基于 C 语言的表示法来描述协议中涉及到的数据结构,这些数据结构的成员都是基于无符号字符型(unsigned char, 即 uint8 ) 的,且成员紧密排列无内存对齐处理。这样的好处是,这些结构在代码中看到的样子,和经过标准C编译器处理后在内存中的样子,是一模一样的。这些结构在传输时,第一个成员的第一个字节会被第一个传输,然后是第二个字节,然后是第二个成员,依次类推。

  
这里约定两种特殊的 C 语言简化表达方式:

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

第二种,允许struct(C 语言结构)含有变长数组成员。

struct {
    unsigned char mumbleLengthB1;
    unsigned char mumbleLengthB0;
    ... /* other stuff */
    unsigned char mumbleData[mumbleLength];
};

上例中 mumbleData 成员的长度就依赖于前面的成员 mumbleLength(双字节整数) 的值。

  

3.2 接受连接

  
一个 FastCGI 应用会监听 FCGI_LISTENSOCK_FILENO 对应的 socket,然后调用 accept() 来完成建立连接。当 accept() 调用成功时,应用可能会被绑定一个环境变量 FCGI_WEB_SERVER_ADDRS, 这个环境变量会存储一个逗号分隔的 Web 服务器的 IP 地址列表白名单。如果这个环境变量被绑定了,应用会校验当前发起连接的 Web 服务器的 IP 地址是否在这个列表里面,如果不在,或者这个连接的传输模式不是 TCP/ IP 模式,那么应用会直接关闭这个连接。

FCGI_WEB_SERVER_ADDRS 的值是逗号分隔的 IP 列表,其中每个 IP 表示为英文句号分隔的、范围为 [0, 255] 的十进制数。 比如 FCGI_WEB_SERVER_ADDRS 一个合法的绑定值为 199.170.183.28,199.170.183.71。

在应用的实现中,可以支持接受 Web 服务器并发的建立连接,但这个特性不是强制实现的。

  

3.3 记录

  

3.3.1 基本结构

  
Web 服务器和 FastCGI 应用之间的通信采用了一个简单的协议,这个协议的细节依赖于应用当前的角色类型。整个流程大体上说,是 Web 服务器先发送参数和数据给应用,然后应用产生并发送结果数据给服务器,当结果数据发送完成的时候,应用需要发送一个请求处理完成标志给服务器。

数据在传输中,会被统一地编制成一个个记录 (FCGI_Record) 的形式。这种做法有两种好处:一是,多个请求的数据可以复用同一个连接进行传输,这样应用的实现就可以采用事件驱动的编程模型或者多线程编程模型;二是,同一个请求中的多个数据流的数据可以通过封装成不同记录的形式在同一个连接上传输,例如 stdout 和 stderr 两个输出流的数据可以通过同一个连接发送给 Web 服务器,而不是不得不使用两个连接。

  

// FCGI_Record 结构定义
typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
    unsigned char contentData[contentLength];
    unsigned char paddingData[paddingLength];
} FCGI_Record;

一个记录的标准结构如上所示,前面包含一些定长的成员,尾部则是变长的内容成员和对齐用字节成员。下面分别介绍各个成员的含义:

  • version : 表示 FastCGI 协议的版本号,本规范基于 FCGI_VERSION_1
  • type :表示此记录的类型,预示着此记录的主要功能。记录的各种类型和功能会在后文表述。
  • requestId : 表示此记录属于哪个 FastCGI 请求。
  • contentLength : 表示后面成员 contentData 的长度。
  • paddingLength : 表示后面成员 paddingData 的长度。
  • contentData : 字节数组,长度范围在 [0, 65535],根据记录类型不同需要采用不同的解读方法。
  • paddingData : 字节数组,长度范围在 [0, 255],处理时忽略该内容。

  
这里使用一种简化的 C 语言 struct 初始化语法来表达一个记录的值:忽略 version、padding相关成员,把 requestIdB1、 requestIdB0 当做 requestId 处理。

这样,一个 {FCGI_END_REQUEST, 1, { FCGI_REQUEST_COMPLETE, 0}} 语句表达了这样一个记录:它的 type 值为 FCGI_END_REQUEST,requestId 值为 1,contentData 值为 {FCGI_REQUEST_COMPLETE, 0}。

  

3.3.2 字节对齐

  
本规范允许数据发送方发送字节对齐处理后的记录。这种情况下,数据接收方必须理解记录的 paddingLength 字段,且忽略 paddingData 的内容。字节对齐的手段可以提升数据在内存中处理的效率, X window system 协议中也使用了这种手段。

本规范建议在内存中处理记录时,尽量保持每个记录的起始指针地址为8字节的整数倍。一个记录的定长部分长度也是8字节。

  

3.3.3 管理请求ID

  
Web 服务器在运行中会重复分配并使用请求 ID,在每一个 FastCGI 应用与 Web 服务器的连接上,应用都需要自行管理当前连接上的所有使用中的请求 ID。当应用收到记录 {FCGI_BEGIN_REQUEST, R, …} 时,说明要把请求 ID R 设置为使用中了,当收到记录 {FCGI_END_REQUEST, R, …} 时,说明要把请求ID R 设置成空闲状态了。当一个 ID R 处于空闲状态,则应用只需要响应属于请求 R 的类型为上面提到的 FCGI_BEGIN_REQUEST 的记录,而忽略其他类型的记录。

Web 服务器会尽量保证请求 ID 局限在一个较小的范围,这样,应用就可以使用 short 数组管理请求 ID,而不是不得不使用 long 数组或者哈希表。应用的实现中,可以选择一次只接受一个请求的方案,在这种情况下,应用管理请求 ID 会比较简单,只需要比较新接收记录的请求 ID 和当前处理中的请求的 ID 是否一致即可。

  

3.3.4 记录的分类

  
这里有两种分类方法。
  
第一种分类方法是把记录分为管理类记录和应用类记录。一个管理类记录不和任何一个具体的请求相关联,比如此记录可能是 Web 服务器在查询应用的容量。一个应用类记录则是与某一个请求相关联的,它的 requestId 成员表明了本记录需要关联到哪一个请求。

管理类记录的 requestId 成员的值都是 0,或者叫 ID 为 null。应用类记录的 requestId 都不为 0。
  
第二种分类方法是把记录分为自描述型记录和流数据型记录。一个自描述型记录本身自己就可以表达有完整意义的内容,而一个流数据型记录则是一份完整的流数据的其中一部分。一份完整的流数据由若干内容长度为 0 或非 0 的流数据型记录和一个内容长度为0的流数据型记录结尾构成。把这些记录的 contentData 成员的值拼接成一个字节序,这个字节序就是这份流数据的完整值。这种组织方式意味着,一份完整的流数据的值,不会因为拆分记录的粒度不同而不同。

这两种分类方法是相互独立的,在本规范对应的版本下,所有的管理类记录都是自描述型记录,几乎所有的应用类记录都是流数据型记录。有三种应用类记录是自描述型的,且在以后的规范版本中,可能有的管理类记录也会是流数据型的。

  

3.4 键值对

  
当 FastCGI 应用处于若干角色类型中,都需要读写一些键值对数据。因此有必要定义一份键值对数据的编码标准。

当应用传输键值对时,本规范的做法是先传送键的长度,再传递值的长度,然后是键的值,最后是值的值。如果键或值的内容长度不大于 127 字节,那么长度就可以使用 1 字节进行编码,如果长度大于 127 字节,那么统一使用 4 字节进行编码。

  
下面是各种不同长度的内容下,键值对编码的格式:

// name 长度不大于 127, value 长度不大于 127
typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

// name 长度不大于 127, value 长度大于 127
typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength
    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

// name 长度大于 127, value 长度不大于 127
typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength
    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

// name 长度大于 127, value 长度大于 127
typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength
    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength
    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

  
长度成员的第一个字节的最高位为标志位,为 0 则表示本长度编码为 1 字节,为 1 则表示编码为 4 字节。

这种编码方式允许数据发送者在不附加额外信息的条件下进行安全的二进制传输,同时数据接受者也能在处理初期就能确定此键值对需求的空间大小。

  

3.5 关闭连接

  
正常情况下,Web 服务器管理着连接的生命期。当没有待处理的请求时,Web 服务器就可以关闭此连接。Web 服务器也可以授权 FastCGI 应用代为关闭某个连接(见 FCGI_BEGIN_REQUEST 记录),在这种情况下,应用会在处理完成那个请求之后,自行关闭对应连接。

上述机制让应用在实现时可以选择不同的风格。一个简单的 FastCGI 的应用实现可能是,对同一个连接只处理一个请求,每个新请求都需要建立新连接传递过来。一种更加复杂的实现是,应用可以在同一个连接上并发处理多个请求,这样这个连接就会作为长连接存在。

应用在输出响应之后自行关闭连接,然后继续进行后续逻辑的处理,可以显著地降低响应时间。注意此时长连接的生命期只能由 Web 服务器自己管理。

当应用自行关闭了某个连接或者发现某连接已经被关闭了,在必要的时候,需要建立新连接来进行后续处理。

  

四、管理类记录

  

4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT

  
Web 服务器可以查询 FastCGI 应用的内部变量,一个 Web 服务器的典型实现是,在启动时查询应用的一些配置,然后根据这些数据调整自身的某些系统设置。

当应用收到一个{FCGI_GET_VALUES, 0, …} 记录时,表明 Web 服务器在请求查询。这个记录的 contentData 成员内容为,一串值均为空的键值对编码数据。

应用会解析这些键,填充上它们对应的值,最后构建一串新的键值对编码数据,放入 {FCGI_GET_VALUES_RESULT, 0, …} 记录中,返回给 Web 服务器。如果应用不理解某个键指向的数据,就不会在返回记录中包含这个键值对。

FCGI_GET_VALUES 记录可以查询的变量范围是允许扩展的,这里定义了首批 Web 服务器关心的变量名单。

  • FCGI_MAX_CONNS :这个应用支持的最高并发连接数。 比如 “1” 或 “10”。
  • FCGI_MAX_REQS :这个应用支持的最高并发请求数。比如 “1” 或 “50”。
  • FCGI_MPXS_CONNS :“0” 表示这个应用不支持连接复用,一个请求使用一个连接,1表示支持。

应用可能在任何时间收到 Web 服务器的查询请求,对此生成响应的应该是 FastCGI 底层库,不需要应用的实现层来发出响应。

  

4.2 FCGI_UNKNOWN_TYPE

  
在 FastCGI 协议的未来版本中,管理类记录的种类可能会扩充,这里引入 FCGI_UNKNOWN_TYPE 类型来兼容处理这种情况。当旧版应用收到一个新版的类型为 T 的管理类记录时,可以使用一个 {FCGI_UNKNOWN_TYPE, 0, {T}} 记录进行响应。

  
一个 FCGI_UNKNOWN_TYPE 记录的 contentData 成员结构如下:

typedef struct {
    unsigned char type;
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

其中 type 成员表示的就是未知的请求记录类型 T。

  

五、应用类记录

  

5.1 FCGI_BEGIN_REQUEST

  
Web 服务器发送一个 FCGI_BEGIN_REQUEST 记录给 FastCGI 应用来开始一个请求的处理。

一个 FCGI_BEGIN_REQUEST 记录的 contentData 成员结构如下:

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

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

  • FCGI_RESPONDER 响应器
  • FCGI_AUTHORIZER 鉴权器
  • FCGI_FILTER 过滤器

在第六章会详细介绍这些角色类型的细节。

其中 flags 成员含有一个控制连接关闭方式的标志位。

当 flags & FCGI_KEEP_CONN 的值为 0 时,表明需要应用在响应此请求之后,主动关闭对应的连接。如果非 0,则不需要,这时,此连接的生命期由 Web 服务器控制。

  

5.2 键值对数据记录:FCGI_PARAMS

  
FCGI_PARAMS 记录被 Web 服务器用来向应用传递键值对数据,键值对数据会被封装成若干这类记录,形成一个数据流,依次传递。键值对间不区分顺序。

  

5.3 字节流类记录: FCGI_STDIN,FCGI_DATA,FCGI_STDOUT,FCGI_STDERR

  
FCGI_STDIN 记录被 Web 服务器用来向应用传输任意格式的数据,FCGI_DATA 记录是类似的用法。

FCGI_STDOUT 记录被应用当做标准输出,用来向 Web 服务器传输任意类型的数据。

FCGI_STDERR 记录被应用当做标准错误输出,用来向 Web 服务器传输任意类型的数据。

  

5.4 FCGI_ABORT_REQUET

  
Web 服务器通过向 FastCGI 应用发送一个 FCGI_ABORT_REQUEST 记录来终止一个请求的处理。当应用收到一个 {FCGI_ABORT_REQUEST, R} 记录时,需要立即以 {FCGI_END_REQUEST, R, {FCGI_REQUEST_COMPLETE, appStatus}} 记录进行回复,这是一个应用实现级别的响应,并非 FastCGI 底层库自行发出的响应。

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

如果 Web 服务器和应用之间的通信没有复用连接,那么 Web 服务器可以通过关闭这个请求对应的连接来终止这个请求的处理。但是在连接复用的情况下,关闭这个连接会导致此连接上所有正在进行请求的执行终止。

  

5.5 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 成员是应用实现级别的状态码。这个值在不同的角色类型下代表的含义会在介绍角色类型时详细描述。

protocolStatus 成员是 FastCGI 协议级别的状态码,可能的取值如下:

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

  

六、角色

  

6.1 角色下的协议

  
应用作为任一角色类型处理请求时,传输中使用到的记录,都是应用类的记录,所以它们也都是流数据型记录。

综合考虑协议的可靠性和 FastCGI 应用实现的难易度,本规范采用了近似顺序传输模式。在严格顺序传输模式下,应用会顺序接收第一个输入、第二个输入等等直到输入全部接收完成,类似地,输出响应数据时,也会先输出第一个输出,其次第二个输出这样依次输出,直到输出全部完成。严格顺序模式下,在时间上,所有输入数据之间互不交叉,所有输出数据之间也互不交叉。

对于一些 FastCGI 应用的角色类型而言,严格顺序模式太过苛刻了。CGI 规范允许在交叉时间上分别输出数据到 stdout 和 stderr,所以 FastCGI 的协议也需要兼容这种特性,即允许交叉使用 FCGI_STDOUT 记录和 FCGI_STDERR 记录。

在任意角色下,应用都像使用 stderr 一样使用 FCGI_STDERR 记录,通过它可以输出应用级别的易于理解的错误报告信息。发送 FCGI_STDERR 记录是可选的,如果没有错误需要报告,可以不发送 FCGI_STDERR 记录,或者发送一个数据长度为0的记录过去。

在任意角色下,应用使用 FCGI_STDERR 对应之外的数据流时,必须至少发送一个记录过去,就算待传输数据长度为 0,也需要发送一个数据长度为 0 的记录。

同样的,综合考虑协议的可靠性和 FastCGI 应用实现的难易度,本规范采用了近似请求-响应模式。在严格请求-响应模式下,应用必须接收完成全部的请求数据之后,才允许输出响应,不允许边收边发式的流水线作业方式。

对一些 FastCGI 应用的角色类型而言,严格请求-响应模式太过苛刻了,毕竟 CGI 协议并没有要求应用一定要在全部输入读取完成之后才可以输出响应,所以 FastCGI 协议也需要兼容这个特性。本规范设定为这样:应用首先需要读取完成除最后一个流类型数据外的所有输入数据,然后在开始读取最后一个流类型数据的时候,就可以开始输出响应数据了。

在使用 FCGI_PARAMS 记录传输文本数据时,例如 CGI 应用从环境变量中读取到的那些数据,记录中的长度字段不包含文本结尾处额外的 null 字节("\0"),且文本尾部也没有额外设置 null 字节。如果应用自己需要依赖这些数据生成 environ 格式的数据,必须自己在这些键值对之间添加等于号,且在值的尾部添加 null 字节。

FastCGI 协议并没有使用与 CGI 协议不兼容的 header 字段,FastCGI 应用通过使用 CGI 协议中同样的 Status 和 Location 字段来返回状态码。

  

6.2 响应器

  
响应器角色下的 FastCGI 应用和 CGI/1.1 的应用表现得基本一致:接收 HTTP 请求的输入数据,产生一个 HTTP 响应数据。

这里介绍下该角色下的 FastCGI 协议如何兼容 CGI/1.1协议。

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

此角色下,如果应用处理一个写入请求时,发现接收的数据长度和 CONTENT_LENGTH 不符,应该立即终止处理这个请求。

  

6.3 鉴权器

  
一个鉴权器角色下的 FastCGI 应用会在获取 HTTP 请求的所有数据之后,生成一个鉴权通过或者不通过的结果。在通过的情况下,应用可以向这个请求中添加键值对数据,并让 Web 服务器对此请求采取下一步处理前添加这些额外信息给下游。鉴权不通过的情况下,应用会直接返回一个终结用的响应,这个响应最终会被给到 HTTP 客户端。

CGI/1.1 协议已经定义了此角色下请求所需的各个属性了。FastCGI 协议会使用这些既有字段进行处理。

  • 应用通过读取 Web 服务器传送的 FCGI_PARAMS 记录来获取请求的各项数据,FCGI_PARAMS 记录中的内容成员的格式类似于响应器模式下应用涉及到的那样,只是 CONTENT_LENGTH, PATH_INFO, PATH_TRANSLATED, SCRIPT_NAME 这些 header 字段不会被传送过来。
  • 应用发送 stdout 和 stderr 数据的方式,和响应器角色下基本一样,使用 CGI/1.1协议中的响应状态码表明请求的结果,如果应用传递了一个状态码为 200 的响应给 Web 服务器,那么 Web 服务器认为本鉴权处理通过。根据 Web 服务器设置的不同,Web 服务器可能还需要进行进一步的鉴权校验,或者向另一个鉴权器应用发起再次鉴权请求。

鉴权器应用发送状态码为 200 的响应到 Web 服务器时,可能会在 header 中添加一些 "Variable-" 开头的字段,这些字段是应用传送给 Web 服务器的额外数据,比如下面这个 header 字段传递了名为 "AUTH_METHOD"、值为 "database lookup" 的数据给 Web 服务器。在下游 CGI 应用或者 FastCGI 应用接手处理这个请求之前,Web 服务器需要把这个数据添加到该请求中。

 Variable-AUTH_METHOD: database lookup

鉴权器应用发送状态码为 200 的响应到 Web 服务器时,Web 服务器应该忽略该响应 header 中非 "Variable-" 开头的字段,也应该忽略响应中任何的非 header 数据。

鉴权器应用发送状态码为非 200 的响应到 Web 服务器时,Web 服务器应该终止执行这个请求,同时使用响应中的 status、headers 、content 组成响应发送给 HTTP 客户端。

  

6.4 过滤器

  
一个过滤器角色下的 FastCGI 应用,会从 Web 服务器获取到请求的所有相关数据,外加一个位于 Web 服务器的文件的内容数据流,然后对这个文件内容数据流进行过滤,把过滤得到的结果作为一个响应发送给 Web 服务器。

一个过滤器角色下的应用,表现上很像一个接受一个文件作为参数的响应器应用。它们两个的区别是,过滤器角色下,应用本身和输入文件数据流都会受限于 Web 服务器的权限控制,在响应器角色中,应用只能使用自身的权限校验机制。

一个过滤器应用的处理过程类似于响应器应用,Web 服务器首先发送环境变量给应用,然后是 stdin 数据(通常是 post 请求的表单数据),最后是待过滤的文件数据流。

  • 应用通过 Web 服务器传送的 FCGI_PARAMS 记录数据来获取请求的各项数据。在此角色下,应用还会获取到两个特有字段 FCGI_DATA_LAST_MODFCGI_DATA_LENGTH
  • 然后应用读取 FCGI_STDIN 记录数据,就像 CGI/1.1 应用读取 stdin 数据那样。读取数据的长度不会超过CONTENT_LENGTH,且在遇到数据结束标志时停止。(应用接收数据的长度少于 CONTENT_LENGTH 的情况,应该仅仅发生于发送者发送的数据不足时,例如发送者宕机了 )
  • 应用通过读取 FCGI_DATA 记录数据获得待过滤的文件内容。读取 FCGI_DATA_LAST_MOD 的值可以获取该文件的最后更改时间,其格式为一个整数,表示自 UTC 时间 1970-01-01 00:00:00 至今的秒数。应用可以根据此字段判断是否读可以取缓存文件,来代替读取记录内容。应用在读取数据时,最多读取 FCGI_DATA_LENGTH 长度的数据,且遇到流数据结束标志时停止。
  • 应用通过发送 FCGI_STDOUT 记录来实现 CGI/1.1 的 stdout 输出模块,通过发送 FCGI_STDERR 记录实现 CGI/1.1 的 stderr 输出模块。这两个输出流是支持交叉发送的,不要求必须发送完一个再发送另一个。输出数据要求当前必须已经读取完成 FCGI_PARAMS 记录数据,但是不要求读取完成 FCGI_STDIN 记录的数据。
  • 当应用已经发送完成 stdout 和 stderr 数据时,需要发送一个FCGI_END_REQUEST 记录给 Web 服务器,此记录中 protocolStatus 成员值设置为 FCGI_REQUEST_COMPLETE,appStatus 成员值设置成 CGI 协议中 status 字段有效范围内的值,这个 appStatus 会被 exit() 系统调用返回给上层。

过滤器应用应该校验 FCGI_STDIN 记录中读取到的数据长度,是否等于 CONTENT_LENGTH 的值, 从 FCGI_DATA 记录中读取的数据长度,是否等于 FCGI_DATA_LENGTH 的值。在处理只读请求时,如果出现了不一致的情况,应用应该返回一个标志给 Web 服务器,表示该请求的数据有丢失情况。如果是处理写入请求,应用则应该立即终止此请求的处理。

  

七、错误处理

  
如果一个 FastCGI 应用进程的退出码为 0,则表示这个进程是正常终止的,比如系统底层进行垃圾回收操作时回收了此进程。如果退出码不为 0,则表示此进程宕机了,Web 服务器怎么处理应用进程的退出码不在本规范的叙述范围之内。

Web 服务器可以通过发送 SIGTERM 信号给应用来让应用退出,如果应用忽略了 SIGTERM 信号,Web 服务器可以尝试发送 SIGKILL 信号来让应用退出。

应用会通过发送 FCGI_STDERR 记录来报告应用级别的错误,也可能会通过设置 FCGI_END_REQUEST 记录中的appStatus 字段来报告错误。在很多时候下,应用也会选择通过发送 FCGI_STDOUT 记录来发送错误报告,这样可以直接向 HTTP 客户端展示错误报告。

在 Unix 系统上,应用会把底层错误,包括 FastCGI 底层库级别错误、环境变量语法错误等写入到系统 syslog 目录中。根据错误的严重程度不同,应用可能会继续运行,也有可能直接使用非 0 退出码退出。

  

八、数据结构和常量定义

  

/*
 * Listening socket file number
 */
#define FCGI_LISTENSOCK_FILENO 0

typedef struct {
    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;

/*
 * Number of bytes in a FCGI_Header.  Future versions of the protocol
 * will not reduce this number.
 */
#define FCGI_HEADER_LEN  8

 /*
  * Value for version component of FCGI_Header
  */
#define FCGI_VERSION_1           1

  /*
   * 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
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

   /*
    * Value for requestId component of FCGI_Header
    */
#define FCGI_NULL_REQUEST_ID     0

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

typedef struct {
    FCGI_Header header;
    FCGI_BeginRequestBody body;
} FCGI_BeginRequestRecord;

/*
 * Mask for flags component of FCGI_BeginRequestBody
 */
#define FCGI_KEEP_CONN  1

 /*
  * Values for role component of FCGI_BeginRequestBody
  */
#define FCGI_RESPONDER  1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER     3

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

typedef struct {
    FCGI_Header header;
    FCGI_EndRequestBody body;
} FCGI_EndRequestRecord;

/*
 * Values for protocolStatus component of FCGI_EndRequestBody
 */
#define FCGI_REQUEST_COMPLETE 0
#define FCGI_CANT_MPX_CONN    1
#define FCGI_OVERLOADED       2
#define FCGI_UNKNOWN_ROLE     3

 /*
  * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
  */
#define FCGI_MAX_CONNS  "FCGI_MAX_CONNS"
#define FCGI_MAX_REQS   "FCGI_MAX_REQS"
#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS"

typedef struct {
    unsigned char type;
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

typedef struct {
    FCGI_Header header;
    FCGI_UnknownTypeBody body;
} FCGI_UnknownTypeRecord;

  

九、参考资料

  
The WWW Common Gateway Interface at W3C

  

附表A:各种类型记录的特性

  
下表列出了各类型记录所支持的特性情况(x表示勾选)

  • WS->App :这个特性勾选表示此类型记录只能被 Web 服务器发送给应用。非勾选类型的记录只能被应用发送给 Web 服务器。
  • management :这个特性勾选表示此类型记录是非关联到某个请求的记录,记录的 requestId 成员值为 null。非勾选类型的记录则有关联到某个请求,记录的 requestId 成员值不为 null。
  • stream : 这个特性勾选表示此类型记录的内容被用来组成一个数据流,这个数据流会使用一个 contentData 成员长度为 0 的记录作为终止标志。这种类型的记录都是非自描述的,每个记录都是整个流数据中的一部分。

  

记录类型 / 特性 WS->App management stream
FCGI_GET_VALUES x x
FCGI_GET_VALUES_RESULT x
FCGI_UNKNOWN_TYPE x
FCGI_BEGIN_REQUEST x
FCGI_ABORT_REQUEST x
FCGI_END_REQUEST
FCGI_PARAMS x x
FCGI_STDIN x x
FCGI_DATA x x
FCGI_STDOUT x
FCGI_STDERR x

  

附表B :典型数据流示例

  
这里引入一些简化表达方式来描述数据流:

  • 流数据式记录 (FCGI_PARAMSFCGI_STDINFCGI_STDOUTFCGI_STDERR) 中的 contentData 成员值使用字符串表示。 一个以 "..." 结尾的字符串表示此值过长,需要截断显示。
  • 为了区别显示 Web 服务器发送和接收的数据,Web 服务器接收的数据会缩进显示。
  • 数据流会按照实际的处理时间顺序排列显示。

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

{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中的请求,只是本例的请求中 stdin 数据不为空,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}}

  
3 :类似例子1中的请求,但是本例中,应用处理请求时,触发了一个错误。应用会把错误报告发送到 stderr 数据流,同时以非0的退出码退出。在退出之前,应用还使用 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}}

  
4 :本例由两个例子1中的请求构成,它们复用同一个连接进行处理。因为第一个请求比较复杂导致耗时较长,从而应用会首先完成并响应第二个请求,后续才响应第一个请求。

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_BEGIN_REQUEST,   2, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          2, "\013\002SERVER_PORT80\013\016SERVER_ADDR ... "}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n"}

{FCGI_PARAMS,          2, ""}
{FCGI_STDIN,           2, ""}

    {FCGI_STDOUT,      2, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      2, ""}
    {FCGI_END_REQUEST, 2, {0, FCGI_REQUEST_COMPLETE}}
    {FCGI_STDOUT,      1, "<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

  

附:原文地址和版权声明

  
原文地址:https://fastcgi-archives.github.io/FastCGI_Specification.html
  
原文版权声明:

Mark R. Brown

Open Market, Inc.

Document Version: 1.0
29 April 1996

Copyright © 1996 Open Market, Inc. 245 First Street, Cambridge, MA 02142 U.S.A.

  
  

附:本文版权声明

  
杨宇
个人网站:www.yangyu.club
新浪微博:程序员杨宇
2019年3月7日 第一版

  
  
全文完
  
  

阅读感受肿么样?

平均分: / 5. 打分次数:

发表评论

电子邮件地址不会被公开。 必填项已用*标注