写在最前面

在深入nginx源码过程中,发现对fastcgi协议有点陌生,nginx与fpm到底是怎么转换的,于是有了该篇学习笔记,详细的nginx的源码分析可以查询《nginx深入解剖》,目前正处于编写阶段,暂不对外开放,请关注本人仓库
gitee: https://gitee.com/xqhero
github: https://github.com/xqhero

实验过程发现的问题

  • 根据cgi的规定,从fastcgi服务端返回的内容必须是http报文格式,否则报502

fastcgi协议–以nginx与fpm通信为例

nginxnginxfpmfpm发送fastcgi协议报文1. FCGI_BEGIN_REQUEST解析,新建request数据2. FCGI_PARAMS逐个获取params类型包,等收到一个内容长度为0的params包后开始处理3. FCGI_STDIN逐个获取stdin类型包,并将其写入到可存储位置 接收完毕后进行处理nginx实现:1. 逐步发送协议报文2. 接收响应3. 进行http报文的封装响应fastcgi协议报文返回http格式报文1. FCGI_STDERR2. FCGI_STDOUT3. 空FCGI_STDOUT4. FCGI_END_REQUEST1. 接收解析fastcgi报文2. 逻辑处理3. 发送FCGI_STDOUT报文4. 发送结束请求标记

消息头

对于fastcgi这种数据收发协议来说,它所发送的每次请求或是回复(我之后的叙述中叫它们为消息)都有一个可提取的公共部分就是FCGI_Header(请求头),及不管每次发送的是什么消息,都必须会有一个如下格式的请求头

typedef struct 
{
    unsigned char version;              //版本
    unsigned char type;                 //操作类型
    unsigned char requestIdB1;          //请求id
    unsigned char requestIdB0;          
    unsigned char contentLengthB1;      //内容长度
    unsigned char contentLengthB0;
    unsigned char paddingLength;        //填充字节的长度
    unsigned char reserved;             //保留字节
}FCGI_Header;
  • version:用来表示FCGI的版本信息,如果是web服务器给php-fpm发送的消息,请求头中只需要将其置0就可以
  • type:此字段用来说明每次所发送消息的类型,其具体值可以为如下
    • 1 : 在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息
    • 2 : 异常断开与php-fpm的交互
    • 3 : 在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束
    • 4 : 在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对
    • 5 : web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
    • 6 : php-fpm给web服务器回的正常响应消息的type就设为6
    • 7 : php-fpm给web服务器回的错误响应设为7
  • requestId:此字段占俩个字节,它表示这某个特有的交互,因为php-fpm(可以理解为服务器)可以同时处理多个交互
  • contentLength:此字段也占2个字节,它用来表示此消息中的消息体中数据的长度(我们上面一直说的请求头也可以叫其消息头),我们可以据此在读消息时,能够知道读多长能读出一条完整的消息
  • paddingLength:填充长度的值,为了提高处理消息的能力,我们的每个消息大小都必须为8的倍数,此长度标示,我们在消息的尾部填充的长度
  • reserved:保留字段

消息体

所有的消息都共用这个消息头, 对于请求开始(及一次交互的第一个)的消息,有其自己的消息体格式,对于请求结束(一次交互的最后一个)的消息,有其自己的消息体格式,对于传递PARAMS参数……消息头type字段标示的消息类型不同,对应的消息体的格式就可能不同

type 为 1

typedef struct 
{
    unsigned char roleB1;       //web服务器所期望php-fpm扮演的角色,具体取值下面有
    unsigned char roleB0;
    unsigned char flags;        //确定php-fpm处理完一次请求之后是否关闭
    unsigned char reserved[5];  //保留字段
}FCGI_BeginRequestBody;
  • role:此字段占2个字节,用来说明我们对php-fpm发起请求时,我们想让php-fpm为我们扮演什么角色(做什么,或理解为杂么做),其常见的3个取值如下
    • 1: 最常用的值,php-fpm接受我们的http所关联的信息,并产生个响应
    • 2: php-fpm会对我们的请求进行认证,认证通过的其会返回响应,认证不通过则关闭请求
    • 3: 过滤请求中的额外数据流,并产生过滤后的http响应
  • flags:字段确定是否与php-fpm建立长连接,为1长连接,为0则在每次请求处理结束之后关闭连接
  • reserved:保留字段

type 为 2

FCGI_ABORT_REQUEST,该类型主要是给web服务器提供主动结束通道的功能,场景为当web服务器需要尽快结束并关闭通道,则会发送该请求给Fastcgi服务器,这样Fastcgi服务会尽快的将数据处理完并返回关闭通道。

type 为 3

typedef struct 
{
    unsigned char appStatusB3;      //结束状态,0为正常
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;   //协议状态
    unsigned char reserved[3];
}FCGI_EndRequestBody;
  • appStatus:此字段共4个字节,用来表示结束状态,0为正常结束

  • protocolStatus:为协议所处的状态,0为正常状态

    #define FCGI_REQUEST_COMPLETE 0
    #define FCGI_CANT_MPX_CONN    1
    #define FCGI_OVERLOADED       2
    #define FCGI_UNKNOWN_ROLE     3

    FCGI_REQUEST_COMPLETE:请求的正常结束。
    FCGI_CANT_MPX_CONN:拒绝新请求。这发生在Web服务器通过一条线路向应用发送并发的请求时,后者被设计为每条线路每次处理一个请求。
    FCGI_OVERLOADED:拒绝新请求。这发生在应用用完某些资源时,例如数据库连接。
    FCGI_UNKNOWN_ROLE:拒绝新请求。这发生在Web服务器指定了一个应用不能识别的角色时

  • reserved:为保留字节

type 为 4

此值表示此消息体为传递PARAMS(环境参数),环境参数其实就是name-value对,我们可以使用自己定义的name-value传给php-fpm或者传递php-fpm已有的name-value对

 typedef struct {
     unsigned char nameLengthB3; /* nameLengthB0 >> 7 == 0 */
     unsigned char nameLengthB2;
     unsigned char nameLengthB1;
     unsigned char nameLengthB0;
     unsigned char valueLengthB3; /* nameLengthB0 >> 7 == 0 */
     unsigned char valueLengthB2;
     unsigned char valueLengthB1;
     unsigned char valueLengthB0;
     unsigned char nameData[(B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
     unsigned char valueData[valueLength
     ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    } FCGI_NameValue;

key长度和value长度的存储为1字节或者4字节,判断第一个字节的最高位是否为1,若为1则是用4字节描述的长度,若为0则用1字节

  • nameLength:此字段占用4字节,用来说明name的长度
  • valueLength:此字段为4个字节,用来说明value的长度
  • 前8个字节之后紧跟的为nameLength长度的name值,接着是valueLength长度的value值

type 为5,6,7,8

类型中的FCGI_STDIN、FCGI_STDOUT、FCGI_STDERR和FCGI_DATA都是数据流传输,不存在什么结构体,内容中只有数据信息

type 为 9, 10

FCGI_GET_VALUES、FCGI_GET_VALUES_RESULT
FCGI_GET_VALUES查询fastcgi服务器的相关性能参数,结构体复用了FCGI_PARAMS类型的结构体,其中name设置为相应的值,而value为空即可。之后由fastcgi服务返回FCGI_GET_VALUES_RESULT类型的数据并填充value即可

完整消息record

fastcgi将一个完整的消息称为record,我们每次发送的单位就是record, 常见的记录格式:

  • type=1 : header(消息头) + 开始请求体(8字节)
  • type=3 : header + 结束请求体(8字节)
  • type=4 : header + name-value长度(8字节) + 具体的name-value
  • type=5,6,7 : header + 具体内容

数据发送和接收过程

当FastCGI程序从web服务器读取数据时,总是先读取一个8字节的消息头,然后得到消息的类型和长度信息,然后再读取消息体,一种消息过长可以切割成多个消息传输,当一个消息头里的 contentLength 为0(也即 contentLengthB1和contentLengthB0 的值都为0) 时,则表明这种消息传输完毕,然后我们可以把之前读到的这种类型的多个消息合并得到最终完整的消息。反之,当我们要从FastCGI程序向web服务器返回数据时,总是每发送一个8字节消息头,紧接发送一次消息体,循环往复,直到最后发送 FCGI_END_REQUEST类型的消息头 和消息体结束请求

文档更新时间: 2021-01-22 18:05   作者:周国强