FastCGI 规范中文翻译

原文地址:https://fastcgi-archives.github.io/FastCGI_Specification.html

1.简介

FastCGI 是一种对 CGI 的开放扩展,在不改变 Web 服务的前提下,为所有的网络应用程序提供了很高的性能。

这个规范的目的很小:从应用程序角度来看,指定了一个 FastCGI 应用程序和一个支持 FaseCGI的 Web 服务之间的接口。许多 Web 服务的特性和 FastCGI相关,例如,应用程序管理工具,与Web服务器接口的应用程序无关,此处不再赘述。

这个规范适用于Unix(更确切的说,适用于支持 Berkeley Sockets 的 POSIX 系统)。规范的大部分是一个简单的通信协议,它独立于字节序,并将扩展到其他系统。

我们将通过比较 FastCGI 和常规的 CGI/1.1 的 Unix 实现来介绍它。 FastCGI 是被设计用于支持常驻内存的应用程序进程,例如,应用程序服务。常规的 CGI/1.1 的 Unix 实现的主要不同之处在于,CGI 会创建一个应用程序进程,响应一个请求之后就会退出。

FastCGI进程的初始状态比CGI / 1.1进程的初始状态更简洁,因为 FastCGI 进程在初始化时没有开始与任何事物连接。它没有常规的打开标准输入(stdin)、输出(stdout)和错误(stderr)流,并且它不会通过环境变量接受大量信息。在一个 FastCGI 进程中,关键的初始状态是监听一个 socket,这个socket会接收来自 Web 服务器的连接。

一个 FastCGI 进程在它监听的 socket 上接收一个连接时,进程会执行一个简单的协议去接收和发送数据。这个协议主要有两个目的。第一,在多个独立的 FastCGI 请求中,这个协议复用一个传输连接。这支持那些使用了事件驱动或多线程编程技术来处理并发请求的应用程序。第二,对于每一个请求,这个协议在每个传输方向上都提供了多个独立的数据流。这样,例如,stdout 和 stderr 数据都通过单个传输连接从应用程序传递到Web服务器,而不是像 CGI/1.1 那样需要单独的管道。

一个 FastCGI 应用程序扮演了明确定义的角色之一。我们最熟悉的是响应器角色,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个 HTTP 响应;这正是 CGI/1.1 程序所扮演的角色。第二个角色是认证器,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个认证通过/不通过的决定。第三个角色是过滤器,应用程序从一个 HTTP 请求中接收所有的信息,加上一个 Web 服务器中存储的额外的文件数据流,然后生成一个“过滤的”版本的数据流作为 HTTP 响应。这个框架是可扩展的,因此更多的 FastCGI 角色可以在以后定义。

在本说明书的其余部分中,术语“ FastCGI 应用程序”,“应用程序进程”或“应用程序服务器”在不会引起混淆的情况下缩写为“应用程序”

2.初始处理状态

2.1 参数列表

默认情况下,Web 服务器创建一个包含单个元素的参数列表,应用程序的名字会被当作可执行文件路径名的最后一部分。Web 服务器可能提供了一种方法来指明一个不同的应用程序名称,或者一个更详细的参数列表。

注意,由 Web 服务器执行的文件可能是一个解释性脚本(一个文本文件,以#!开头),这种情况下,应用程序参数的构建如在execve联机帮助页中所述那样。

2.2 文件描述符

Web 服务器在应用程序开始执行时打开单个文件描述符FCGI_LISTENSOCK_FILENO。这个描述符指向由 Web 服务器创建的监听的socket。

FCGI_LISTENSOCK_FILENO 等价于 STDIN_FILENO 。标准的描述符 STDOUT_FILENO 和 STDERR_FILENO 在应用程序开始执行时被关闭。判断一个应用程序是被 CGI 还是 FastCGI 调用的可靠方法是:调用 getpeername(FCGI_LISTENSOCK_FILENO),返回 -1 并将errno设置为ENOTCONN的就是 FastCGI 程序。

Web服务器选择可靠的传输,Unix流管道(AF_UNIX)或TCP/IP(AF_INET),隐含在FCGI_LISTENSOCK_FILENO套接字的内部状态中。

2.3 环境变量

Web 服务器可以使用环境变量去传递参数给应用程序。这个规范定义了一个这样的变量:FCGI_WEB_SERVER_ADDRS。我们期待随着规范的演变,会有更多的变量会被传递。Web 服务器可以提供一种方法去绑定其他的环境变量,比如 PATH 变量。

2.4 其他状态

Web 服务器可以提供一种方法去指明一个应用程序的初始处理状态的其他部分,比如优先级,用户 ID,用户组 ID,根目录,以及进程的工作目录。

3.协议基础

3.1 符号

我们使用 C 语言符号去定义协议信息的格式。所有的结构体元素都使用 unsigned char 类型定义,并安排使ISO C编译器以常规方式将它们排列,没有填充。在结构体中,第一个字节会被第一个传输,第二个会被第二个传输,以此类推。

我们使用两个公约来概括我们的定义。

第一,当两个相邻的结构体组件名称相同时,除了后缀”B1″和”B0″,这意味着这两个组件可以被视为单个数字,计算为B1<<8 + B0。

第二,我们扩展 C 的结构体,允许以下的形式

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

这代表着一个变长的结构体,它的长度是由前面的组件的值决定的。

3.2 接受传输连接

一个 FastCGI 应用程序在由文件描述符 FCGI_LISTENSOCK_FILENO 引用的 socket 上调用 accept() 去接收一个新的传输连接。如果 accept() 成功了,FCGI_WEB_SERVER_ADDRS 环境变量被绑定,应用程序立即执行以下的特殊操作:

  • FCGI_WEB_SERVER_ADDRS: 这个值是 Web 服务器的有效的 ip 地址列表。
  • 如果 FCGI_WEB_SERVER_ADDRS 绑定了,应用程序检查新连接的对等 IP 地址是否在列表中。如果检查失败了(包括连接没有使用 TCP/IP 这种可能性),应用程序关掉连接来响应。
    • FCGI_WEB_SERVER_ADDRS 是由英文逗号分割的 IP 地址列表。每一个 IP 地址是由点号分割的4个0~255内的数字组成。例如:FCGI_WEB_SERVER_ADDRS=199.170.183.28,199.170.183.71 。

应用程序可以接收多个并发传输连接,但是它不一定需要这样做。

3.3 记录

应用程序使用一个简单的协议从 Web 服务器获取请求并执行。协议的具体内容视应用程序的角色而定,但是一般来说,Web 服务器首先发送参数和其他数据到应用程序,之后应用程序发送结果数据给 Web 服务器,最终应用程序告诉 Web 服务器请求处理已经结束。

所有通过传输连接的数据都是在 FastCGI 记录(records)里的。FastCGI 记录完成两件事。第一,记录在多个独立的请求之间复用传输连接。这种复用支持使用事件驱动模型或多线程技术来处理并发请求的应用程序。第二,在同一个请求中,记录提供了在不同方向上多个独立的数据流。这样,stdout 和 stderr 可以使用同一个传输连接来传输,而不是需要不同的连接。

        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;

一个 FastCGI 记录包含一个定长的前缀,以及变长的内容和填充字节。一条记录包含7个部分:

  • 版本号(version):指定 FastCGI 协议的版本号。这个规范文档的版本号是 FCGI_VERSION_1。
  • 类型(type):指定这条记录的类型。例如,记录的功能函数。具体的记录类型和功能函数在之后的章节有详细介绍。
  • 请求ID(requestId):指定这条记录属于哪个 FastCGI 请求。
  • 内容长度(contentLength):在contentData部分存储的字节数。
  • 填充长度(paddingLength):在paddingData部分存储的字节数。
  • 内容数据(contentData):在0到65535字节之间的数据,根据记录类型进行解释。
  • 填充数据(paddingData):0到255个字节的数据,被忽略。

我们使用宽松的C struct初始化语法来指定常量FastCGI记录。我们省略了版本号部分,忽略填充部分,并将requestId视为一个数字。因此 {FCGI_END_REQUEST, 1, {FCGI_REQUEST_COMPLETE,0} 是一个 type == FCGI_END_REQUEST, requestId == 1, and contentData == {FCGI_REQUEST_COMPLETE,0} 的记录。

Padding

协议允许发送者填充发送的记录,然后要求接收者解释 paddingLength,跳过 paddingData。Padding 允许发送者保持数据对齐,达到更高效的数据处理。使用X窗口系统协议的经验显示了这种对齐的性能优势。

我们推荐记录的长度是8字节的整数倍。一个 FastCGI 的固定长度部分正好是8个字节。

处理请求ID

Web服务器重用 FastCGI 的请求ID;在一个给定的传输连接上,应用程序追踪每个请求 ID 的当前状态。当应用程序收到一条记录{FCGI_BEGIN_REQUEST, R, …},一个请求 ID R 置为活跃状态。当应用程序发送一条记录 {FCGI_END_REQUEST, R, …} 给 Web 服务器时,请求 ID R置为非活跃状态。

当请求 ID R 是非活跃的,应用程序会忽略所有的 requestId R 的记录,除了如上所述的 FCGI_BEGIN_REQUEST 记录。

Web 服务器试图保持 FastCGI 请求 ID 是一个很小的数字。这样应用程序就可以使用一个很短的数组来追踪请求 ID 的状态,而不是一个长的数组或是一个哈希表。应用程序可以选择在同一时间仅仅接收一条请求。这样应用程序可以简单的根据当前连接请求 ID 来检查 requestId。

记录类型

有两种阐述 FastCGI 记录类型的方法。

第一个区别是管理记录和应用程序记录。管理记录包含非特定于任何 Web 服务器请求的信息,例如有关应用程序的协议功能的信息。应用程序记录包含有关requestId组件标识的特定请求的信息。

第二个区别是离散记录和流记录。离散记录本身包含有意义的数据单元。流记录是流的一部分,例如,一系列的0或更多的非空记录(length != 0),之后紧跟着一个空记录(length 0)。流记录的 contentData 部分是一连串的字节组成。这个字节序列就是流的值。因此流的值是独立于它包含多少条记录,以及它的字节在非空记录中如何划分。

这两点解释是不相关的。在当前版本的 FastCGI 协议定义的记录类型中,所有的管理记录类型都是离散的记录类型,几乎所有的应用程序记录类型都是流记录类型。但是有三个应用程序记录类型是离散的,也不能保证在之后的版本中,一个管理记录类型是流式的。

3.4 键值对

在这些角色中,FastCGI 应用程序需要读写边长值的不同数字。因此采用一个标准格式去编码一个键值对是有用的。

FastCGI 发送的键值对格式:键长,值长,键,值。小于等于127字节可以用一个字节编码,大于127字节的用4个字节编码:

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

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;

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;

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表示4个字节编码。

这样的键值对格式允许发送方发送二进制数据,使得接受者可以立即分配正确大小的存储空间,即使是很大的值。

3.5 关闭传输连接

Web 服务器控制传输连接的生命周期。Web 服务器可以在没有活跃请求时关闭连接。或者 Web 服务器可以将关闭权限委托给应用程序(请参阅FCGI_BEGIN_REQUEST)。在这种情况下,应用程序在指定的请求之后关闭连接。

这种灵活设计可以包容不同的应用程序风格。简单的应用程序一次只处理一个请求,每个请求都会建立一个连接。更复杂的应用将会处理并发请求,一个和多个传输连接,会长时间保持传输连接。

通过在完成写入响应时关闭传输连接,简单的应用程序可以显着提升性能。Web服务器需要控制长期连接的连接生存期。

当应用程序关闭连接或发现连接已关闭时,应用程序将启动新连接。

4.管理记录类型

4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT

Web 服务器可以查询应用程序中的特定变量。服务器通常会在应用程序启动时执行查询,以便自动化系统配置的某些方面。

应用程序接受一个查询,比如{FCGI_GET_VALUES, 0, …}。FCGI_GET_VALUES 记录的 contentData 部分包含一系列的具有空值的键值对。

应用程序通过发送一个带有值的记录{FCGI_GET_VALUES_RESULT, 0, …}来响应。如果应用程序不理解在查询中的某个变量名,它会从响应中忽略该名称。FCGI_GET_VALUES 被设计成允许一个开放结束集合的变量。初始集合变量提供信息去帮助服务器操作应用,以及连接管理:

  • FCGI_MAX_CONNS:应用程序接收的并发传输连接的最大值。比如,1或10。
  • FCGI_MAX_REQS:应用程序接收的并发请求的最大值。比如1或50。
  • FCGI_MPXS_CONNS:如果应用程序不复用连接,这个值是0(例如,一个请求一个连接)。否则是1。

4.2 FCGI_UNKNOWN_TYPE

管理记录类型集可能会在此协议的未来版本中增长。为了提供这种演变,该协议包括 FCGI_UNKNOWN_TYPE 管理记录。当应用程序收到其类型T不理解的管理记录时,应用程序将使用{FCGI_UNKNOWN_TYPE,0,{T}}进行响应。

FCGI_UNKNOWN_TYPE记录的contentData部分具有以下形式:

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

类型组件是无法识别的管理记录的类型。

5.应用的记录类型

5.1 FCGI_BEGIN_REQUEST, FCGI_GET_VALUES_RESULT

Web服务发送一个 FCGI_BEGIN_REQUEST 记录来开始一个请求。

一个 FCGI_BEGIN_REQUEST 记录的 contentData 部分有以下形式:

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

角色组件设置Web服务器期望应用程序扮演的角色。当前定义的角色是:

  • FCGI_RESPONDER
  • FCGI_AUTHORIZER
  • FCGI_FILTER

角色定义具体在第六章描述。

flags部分包含一个控制连接关闭的位:

  • flags & FCGI_KEEP_CONN: 如果是0,应用程序在响应请求后关闭连接。如果不是0,应用程序在响应请求后不关闭连接;Web 服务器保持对连接的管理权限。

5.2 键值对流:FCGI_PARAMS, FCGI_RESULTS

FCGI_PARAMS

是一种流记录类型,用于从Web服务器向应用程序发送键值对。名称 – 值对一个接一个地沿着流向下发送,没有指定的顺序。

5.3 字节流:FCGI_STDIN, FCGI_DATA, FCGI_STDOUT, FCGI_STDERR

FCGI_STDIN

是一种流记录类型,用于从Web服务器向应用程序发送任意数据。 FCGI_DATA是第二个流记录类型,用于向应用程序发送其他数据。

FCGI_STDOUT和FCGI_STDERR是流记录类型,用于分别从应用程序向Web服务器发送任意数据和错误数据。

5.4 FCGI_ABORT_REQUEST

Web服务器发送 FCGI_ABORT_REQUEST 记录以中止请求。收到{FCGI_ABORT_REQUEST,R}后,应用程序会尽快响应{FCGI_END_REQUEST,R,{FCGI_REQUEST_COMPLETE,appStatus}}。这确实是来自应用程序的响应,而不是来自FastCGI库的低级别确认。

当HTTP客户端关闭其传输连接而来自客户端FastCGI请求正运行到一半时,Web服务器将中止FastCGI请求。这种情况似乎不太可能,大多数FastCGI请求的响应时间都很短,如果客户端速度很慢,Web服务器会提供输出缓冲。但FastCGI应用程序可能与其他系统通信有延迟或正执行服务器推送。

当Web服务器未通过传输连接复用请求时,Web服务器可以通过关闭请求的传输连接来中止请求。但是对于多路复用的请求,关闭传输连接会导致中止连接上的所有请求,这是一种令人遗憾的结果。

5.5 FCGI_END_REQUEST

应用程序发送FCGI_END_REQUEST记录以终止请求,既可能因为应用程序已处理请求,也可能应用程序已拒绝该请求。

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组件是应用程序级状态代码。每个角色都在文档上记录了它对appStatus的使用。

protocolStatus组件是协议级状态代码;可能的protocolStatus值是:

  • FCGI_REQUEST_COMPLETE:正常的请求结束。
  • FCGI_CANT_MPX_CONN:拒绝新请求。当Web服务器通过一个连接将并发请求发送到旨在每个连接一次处理一个请求的应用程序时,就会发生这种情况。
  • FCGI_OVERLOADED:拒绝新请求。当应用程序耗尽某些资源时会发生这种情况,例如:数据库连接。
  • FCGI_UNKNOWN_ROLE:拒绝新请求。当Web服务器指定了应用程序未知的角色时,会发生这种情况。

6.角色

6.1 角色协议

角色协议仅包括具有应用程序记录类型的记录。它们都使用流传输几乎所有的数据。

为了使协议可靠并简化应用程序编程,角色协议被设计使用几乎连续的编组(nearly sequential marshalling.)。具有严格连续编组(strictly sequential marshalling)的协议中,应用程序接收其第一个输入,然后是第二个输入,等等。直接所有数据接受完成。类似地,应用程序发送它的第一个输出,然后发送它的第二个输出,直到它发送它们全部。输入不相互交错,输出不相互交错。

连续编组规则对某些FastCGI角色限制太多。因为 CGI 程序没有时间上的限制,可以同时使用 stdout和stderr。因此角色协议使用FCGI_STDOUT 和FCGI_STDERR来允许这两个流交错。

所有角色协议都使用FCGI_STDERR流,就像在传统应用程序编程中使用stderr一样:以可理解的方式报告应用程序级错误。使用FCGI_STDERR流始终是可选的。如果应用程序没有要报告的错误,它将不发送FCGI_STDERR记录或一个零长度FCGI_STDERR记录。

当角色协议要求传输FCGI_STDERR以外的流时,即使流是空的,也总是传输至少一个流类型的记录

再次为了可靠的协议和简化的应用程序编程,角色协议被设计成几乎连续的编组(nearly sequential marshalling.)。在真正的请求-响应协议中,应用程序在发送其第一个输出记录之前接收其所有输入记录。请求-响应协议不允许流水线操作。

请求-响应规则对某些FastCGI角色限制太多;毕竟,在开始写stdout之前,CGI程序不限制读取所有stdin。因此一些角色协议允许这种特定的可能性。首先,应用程序接收除最终流输入之外的所有输入。当应用程序开始接收最终流输入时,它可以开始写入其输出。

当角色协议使用FCGI_PARAMS传输文本值时,例如CGI程序从环境变量中获取的值,值的长度不包括终止空字节,且值本身不包含空字节。需要提供environ(7)格式键值对的应用程序必须在键和值之间插入等号,并在值后附加空字节。

角色协议不支持CGI的非解析头功能。FastCGI应用程序使用 Status 和Location CGI头设置响应状态。

6.2 响应器

一个响应器角色的FastCGI应用程序与CGI / 1.1程序具有相同的目的:它接收与HTTP请求关联的所有信息并生成HTTP响应。

下面将解释响应器如何模拟CGI/1.1的每个元素:

  • 响应器应用程序通过FCGI_PARAMS从Web服务器接收CGI/1.1环境变量。
  • 接下来,响应器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。 (仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)
  • 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_STDIN。
  • 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。

响应者执行更新,例如实现POST方法时,应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH进行比较,如果两个数字不相等则中止更新。

6.3 授权器

授权器FastCGI应用程序接收与HTTP请求相关的所有信息,并生成授权/未授权的决策。在授权决策的情况下,授权者还可以将键值对与HTTP请求相关联;在做出未经授权的决定时,授权器会向HTTP客户端发送完整的响应。

由于CGI / 1.1定义了一种表示与HTTP请求相关的信息的完美方法,因此授权器使用相同的表示:

  • 授权器应用程序通过FCGI_PARAMS流从Web服务器接收HTTP请求信息,与响应器的格式相同。Web服务器不发送CONTENT_LENGTH,PATH_INFO,PATH_TRANSLATED和SCRIPT_NAME头。
  • 授权器应用程序以与Responder相同的方式发送stdout和stderr数据。CGI/1.1响应状态指明了请求的 处置方式。如果应用程序发送状态200(OK),则Web服务器允许访问。根据其配置,Web服务器可以继续进行其他访问检查,包括对其他授权器的请求。

授权器应用程序的200响应可能包括名称以Variable-为前缀的标头。这些头将应用程序中的键值对传递给Web服务器。例如,响应头:

Variable-AUTH_METHOD: database lookup

使用名称AUTH-METHOD传输值“database lookup”。服务器将这些键值对与HTTP请求相关联,并将它们包含在处理HTTP请求时执行的后续CGI或FastCGI请求中。当应用程序提供200响应时,服务器会忽略名称不带Variable-前缀的响应头,并忽略任何响应内容。

对于除“200”(OK)以外的授权器响应状态值,Web服务器拒绝访问并将响应状态,标头和内容发送回HTTP客户端。

6.4 过滤器

过滤器FastCGI应用程序接收与HTTP请求相关的所有信息,以及来自存储在Web服务器上的文件的额外数据流,并生成数据流的“过滤”版本作为HTTP响应。

过滤器的功能类似于将数据文件作为参数的响应器程序。区别在于使用过滤器,数据文件和过滤器本身都可以使用Web服务器的访问控制机制进行访问控制,将数据文件名称作为参数的响应程序必须对数据文件执行自己的访问控制检查。

过滤器采取的步骤类似于响应者的步骤。服务器首先向Filter提供环境变量,然后是标准输入(通常是POST数据),最后是数据文件输入:

- 与响应器一样,过滤器应用程序通过FCGI_PARAMS从Web服务器接收键值对。过滤器应用程序接收两个专属的变量:FCGI_DATA_LAST_MOD和FCGI_DATA_LENGTH。
- 接下来,过滤器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。(仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)

– 接下来,过滤器应用程序通过FCGI_DATA从Web服务器接收文件数据。该文件的最后修改时间(表示为1970年1月1日UTC以来的整数秒)为FCGI_DATA_LAST_MOD;应用程序可以查阅此变量并从缓存中进行响应而无需读取文件数据。在接收流结束指示之前,应用程序从该流中读取最多FCGI_DATA_LENGTH个字节。
– 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_DATA。
– 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。

过滤器应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH和FCGI_DATA上的FCGI_DATA_LENGTH进行比较。如果数字不匹配且过滤器是一次查询,过滤器响应应提供数据丢失的指示。如果数字不匹配且过滤器是一次更新,则过滤器应中止更新。

7.错误

FastCGI应用程序以零状态退出,表示它是故意终止的,例如为了执行原始形式的垃圾收集。FastCGI应用程序以非零状态退出,表示它崩溃了。Web服务器或其他应用程序管理器如何响应以零或非零状态退出的应用程序超出了本规范的范围。

Web服务器可以通过发送SIGTERM来请求FastCGI应用程序退出。如果应用程序忽略SIGTERM,则Web服务器可以使用SIGKILL。

astCGI应用程序使用FCGI_STDERR流和FCGI_END_REQUEST记录的appStatus部分报告应用程序级错误。在许多情况下,将通过FCGI_STDOUT流直接向用户报告错误。

Unix上,应用程序向syslog报告较低级别的错误,包括FastCGI协议错误和FastCGI环境变量中的语法错误。根据错误的严重程度,应用程序可以继续或以非零状态退出。

8.类型和常量

/*
 * 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;

9.参考文献

The WWW Common Gateway Interface at W3C

A.表:记录类型的属性

下表列出了所有记录类型,并指出了每种记录的属性:

  • WS->App: 此类记录只能由Web服务器发送到应用程序。其他类型的记录只能由应用程序发送到Web服务器。
  • management: 此类型的记录包含不是特定于Web服务器请求的信息,并使用的的请求ID。其他类型的记录包含特定于请求的信息,不能使用空的请求ID。
  • stream: 此类型的记录形成一个流,由具有空contentData的记录终止。其他类型的记录是离散的;每个都带有一个有意义的数据单元。
                               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. 典型的协议消息流

示例的其他符号约定:

  • 流记录的contentData(FCGI_PARAMS,FCGI_STDIN,FCGI_STDOUT和FCGI_STDERR)表示为字符串。以“…”结尾的字符串太长而无法显示,因此仅显示前缀。
  • 发送到Web服务器的消息相对于从Web服务器接收的消息缩进。
  • 消息按应用程序所经历的时间顺序显示。
  1. 一个没有stdin数据的简单请求,以及一个成功的响应:

(注:\013\016这里是8进制编码。这里所有的记录都省略了Version和PaddingData,因此类似于FCGI_BEGIN_REQUEST代表Type,1代表RequestId)

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{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,将页面返回给客户端,并将非零退出状态返回给Web服务器。应用程序选择使用更多FCGI_STDOUT记录发送页面:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{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_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_BEGIN_REQUEST,   2, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          2, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{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}}

redis使用总结

1.redis的使用场景

项目中存在多种redis的使用场景

1.1 场景一:缓存(key,value)

缓存是一个非常常见的场景,在项目中,可以将mysql中的一部分数据查询的结果缓存到redis中,以此来获取更快的查询速度。

以用户信息缓存为例,我的做法如下:

  • 1.设定用户信息缓存的规则,比如userinfo128代表用户id为128的用户信息缓存。
  • 2.在query语句执行时,先检查userinfo128是否存在,如果存在,直接取出结果返回,否则执行sql语句进行查询,并将查询结果缓存。
  • 3.在update和delete语句执行时,删除userinfo128的缓存,这样下一次query时就会自动更新缓存了。

1.2 timeline(sorted set)

timeline非常经典的场景就是微博,朋友圈这种,每个用户都能在自己的timeline上获取到按时间排序的其他用户发布的动态。这里有以前总结过的一篇文章:朋友圈式的TIMELINE设计方案

其实这个 timeline 我一开始的实现是用 list 去做的,但是用list会存在一个问题:因为推送动态可能同时发生,导致不是严格的按照时间排序。

1.3 推送用户集合(set)

这个功能其实就相当于维持一个用户的粉丝列表。这个列表是一个会经常发生变化的集合,并且在项目中是根据用户的关系链计算出来的,单次的查询会消耗很多的时间,因此做成一个集合,在需要推送动态之类的内容时,直接从redis的集合中查询,会节约很多的时间。

1.4 任务队列(list)

整个项目中很多的操作都是异步的,比如发短信,发邮件,推送用户动态等等,使用redis作为任务队列是很简单的,使用它的list结构,然后使用lpush/rpop对,一边push进任务,另一边有一个单独的后台进程pop出任务进行执行。

当然lpush/rpop并不是很好的一个选择,更好的选择是lpush/brpop,使用阻塞版本的pop指令,可以减少很多不必要的轮询。

在使用这样的任务队列时,还需要考虑到一个问题,如果在取出一个任务时进程崩溃,那么这个任务就彻底的丢失了。因此还可以使用 rpoplpush 或者阻塞版本的 brpoplpush ,取出一个任务的同时备份到另一个队列。如果执行成功的话就再lrem掉这个备份即可。关于队列的更详细的使用在第7大点有更详细的说明。

当然, Redis其实并不推荐作为任务队列的实现,如果需要的话,可以尝试使用Redis作者的另一个项目:disque,或者是kafka。

 

1.5 计数器(hash)

计数器我认为也算是 redis 一个常用的功能了,我认为原因有以下:

  • 1.很多场景下的计数功能都是一个非常高频的操作,使用 redis 会拥有极高的性能。
  • 2.redis支持原子性的自增(incre)操作,不用担心CAS(check and set)的问题。
  • 3.传统数据库,如mysql,如果是MyISAM,单次的更新会带来表锁,如果是InnoDB,则带来行锁,影响并发度。

计数器的使用很简单,直接对某个key做 incr 操作,或者对某个 hash 的 key 做 hincrby 操作即可。

 

2.php在使用redis时,多个数据库切换的困扰

项目中使用的是phpredis这个扩展,在使用pconnect保持redis长连接时,所有对redis的操作会共用同一个redis连接。这就导致:多个进程同时使用一个redis连接,并且多个进程使用的数据库不同时导致错误。比如下方的操作:

// 进程1做以下操作
redis->select(0);redis->set("key1", "val1");

// 进程2做以下操作
redis->select(1);redis-set("key2", "val2");

但是在redis的server端,所做的操作可能如下:

select(0)
select(1)
set("key2", "val2")
set("key1", "val1")

这样就导致key1的存储错误

所以我必须在所有这样的操作中,使用MULTI/EXEC对去解决这个问题。以上代码变成:

// 进程1做以下操作
redis->multi();redis->select(0);
redis->set("key1", "val1");result->exec();

// 进程2做以下操作
redis->multi();redis->select(1);
redis-set("key2", "val2");result->exec();

事实上,使用 redis 时,同时使用多个数据库并不推荐。因为在 redis 集群中是不支持 select 命令的。

3.redis多个数据库之间的切换,对性能有影响吗?

在探讨这个问题之前,摘录官网上对 select 命令的说明:

Since the currently selected database is a property of the connection, clients should track the currently selected database and re-select it on reconnection. While there is no command in order to query the selected database in the current connection, the CLIENT LIST output shows, for each client, the currently selected database.

大致意思可以翻译为:

因为当前选中的数据库是连接的一个属性,每个客户端连接都跟踪记录了当前选中的数据库,在重新连接时会重新选择数据库。虽然没有命令是为了查询当前连接选中的数据库,但是 CLIENT LIST 的输出会显示,每个客户端当前选中的是哪个数据库。

因此 select 操作只是修改了当前连接的属性。

4.redis 有 16 个数据库,目的是什么,最佳的使用方式是什么?为什么 redis 集群不支持 select?

同样的摘录一段官网的介绍

Redis different selectable databases are a form of namespacing: all the databases are anyway persisted together in the same RDB / AOF file. However different databases can have keys having the same name, and there are commands available like FLUSHDB, SWAPDB or RANDOMKEY that work on specific databases.

  In practical terms, Redis databases should mainly used in order to, if needed, separate different keys belonging to the same application, and not in order to use a single Redis instance for multiple unrelated applications.

When using Redis Cluster, the SELECT command cannot be used, since Redis Cluster only supports database zero. In the case of Redis Cluster, having multiple databases would be useless, and a worthless source of complexity, because anyway commands operating atomically on a single database would not be possible with the Redis Cluster design and goals.

大意如下:

Redis 多个不同的可选择的数据库是命名空间的一个表现形式:所有的数据库都会在同一个 RDB/AOF 文件中进行持久化。当然不同的数据库可以拥有同样的名字的键,同样的也有一些类似 FLUSHDB, SWAPDB 或 RANDOMKEY 这样的命名专门在数据库上工作的。

实际上,Redis 数据库应该主要用来分离属于一个应用的不同的键,而不是为了使用一个单独的 Redis 实例服务于多个不相关的应用。

当使用 Redis 集群时,SELECT 命名就不能使用了,因为 Redis 集群仅仅支持数据库0。在 Redis 集群的案例中,拥有多个数据库是无用的,是一种毫无价值的复杂性的来源,因为 Redis 集群的设计和目标是不可能支持 SELECT 命令的。

这里解释了为什么 Redis 被设计为有多个数据库,是为了分离同一个应用中不同的键而设计的,但是不能在多个不相关的应用中使用同一个 Redis 实例。并且还要注意在 Redis 集群中无法使用 SELECT ,因此在项目中还是不用为好。

5.redis是单进程的,如何理解?

我在第一次看到这句话时,是很不理解的。对于这种应用,不可能只有一个进程在工作啊。但是在深入了解之后,明白了这里的单进程指的是:处理 Redis 命令是单进程的。

也就是说,同一时间,在并发和并行的层面上来说,都只有一个 Redis 命令被执行。这样设计的理由我的理解有以下:

  • Redis是内存数据库,所有的操作耗时都视CPU的运行速度而定,IO不可能是瓶颈,并行/并发处理带来的意义不大。
  • 并行/并发会增加应用的复杂度

6.redis中使用队列的问题

在1.4小节中我提到了使用Redis作为任务队列的场景。在使用时遇到了程序运行一段时间之后,无法使用brpop获取数据的问题,并且这个程序的连接依然是存活的。

于是我用 CLIENT LIST 查看当前的连接客户端。发现服务器中有大量连接,但是很多连接的 idle 特别长,明显是很久以前的连接,这些连接我可以肯定是已经断开的。经过检查之后,发现 Redis.conf 中的 tcp-keepalive 项我设置为0了,设置为0就不会检查连接是否存活,从而导致连接一直存在。以前将 tcp-keepalive 设置为60,

那这跟 brpop 无法从 Redis 中获取数据有什么关系呢?以下是个人的猜想时间。

这要从Redis的block模型说起。Redis的网络连接是epoll模型的,所以是一个异步的io,肯定不会block一个连接。那么Redis server为了实现这样的block操作,会维持一个内部的哈希表,这个哈希表保存了哪个key上阻塞了哪些客户端。如下图所示:
此处输入图片的描述

如果此时list key1中被push进了一个值,key1就被置为ready状态,然后从链表头部取出client2,将值传给它。可能在我自己的测试中,有大量的已经断开连接客户端阻塞在key1上,但是因为tcp-keepalive为0,没有被及时清除。导致以上的结果。(目前的水平只能这么解释了,虽然还有很多地方说不通)