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,没有被及时清除。导致以上的结果。(目前的水平只能这么解释了,虽然还有很多地方说不通)

ABNF格式说明

一、简介

ABNF全称是Augmented Backus-Naur Form,广泛用于很多的互联网文档说明中。主要作用就是以简洁的字符串来描述某些规范。使用了ABNF的标准说明有:电子邮件的标准说明[RFC733]和之后的[RFC822],HTTP1.1协议的[RFC7230]。因此,要想阅读这些文档,必须了解ABNF的格式。ABNF在RFC5234中进行了详细的说明。

二、规则定义

2.1 规则命名

ABNF中规则的命名是大小写不敏感的,由字母开头,后面跟上字母、数字或连字符

2.2 规则格式

一个规则是如下格式定义的:

name = elements crlf

name指的是规则名,elements是一个或多个规则名,或者是终端字符,crlf也就是我们常说的\r\n

2.3 终端值

一个规则被解释成一个字符串。每个字符都是一个非负的数字(比如ASCII码中a对应十进制的97)。终端值就是这些数字。目前定义了以下几种进制:

b     = binary          ;二进制
d     = decimal         ;十进制
x     = hexadecimal     ;十六进制

因此:

CR = %d13
CR = %x0D

使用”.”号来分割字符

CRLF = %d13.10

2.4 额外的编码

根据编码不同,所显示的值可能也不同。比如7-bit的US-ASCII和16-bit的unicode编码,结果是截然不同的。目前7-bit的US-ASCII编码是最常用的。

三、 运算符

3.1 连接: Rule1 Rule2

连接的意思就是值一个规则可能由其他规则连接而成。比如

foo = %x61 ; a
bar = %x62 ; b
mumble = foo bar foo

因此规则mumnle = aba

3.2 选择: Rule1 / Rule2

选择就是多选一的意思。比如

rule = foo / bar

那么rule是foo或者bar都接受的

3.3 扩展的选择: Rule1 =/ Rule2

ruleset = rule1 / rule2
ruleset =/ rule3
ruleset =/ rule4 / rule5

那么ruleset最终为

ruleset = rule1 / rule2 / rule3 / rule4 / rule5

3.4 范围选择: %c##-##

DIGIT = %x30-39

等价于

DIGIT = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"

3.5 序列组: (Rule1 Rule2)

序列组主要是为了阅读上的方便

elem (foo / bar) blat

等价于

(elem foo blat) or (elem bar blat)

elem foo / bar blat

等价于

(elem foo) or (bar blat)

3.6 变量重复: *Rule

完整的格式为:<a>*<b>element
<a><b>是可选的数字值,代表最少a个,最多b个

因此:

*<element> 0到任意多个
1*<element> 至少1个
3*3<element> 只能是3个
1*2<element> 1到2个

3.7 指定的重复: nRule

n<element>等价于n*n<element>

3.8 可选的序列: [Rule]

[Rule]代表这个规则可有可无。因此[foo bar]等价于*1[foo bar]

3.9 注释: ;Comment

使用;来表示注释

3.10 运算符优先级

运算符优先级从上往下排序如下:

规则名, 单值, 终端值
注释
范围取值
重复
组, 可选
连接
选择

四、使用ABNF定义ABNF

rulelist = 1*( rule / (*c-wsp c-nl) )

rule = rulename defined-as elements c-nl
; continues if next line starts
; with white space

rulename = ALPHA *(ALPHA / DIGIT / "-")

defined-as = *c-wsp ("=" / "=/") *c-wsp
; basic rules definition and
; incremental alternatives

elements = alternation *c-wsp

c-wsp = WSP / (c-nl WSP)

c-nl = comment / CRLF
; comment or newline

comment = ";" *(WSP / VCHAR) CRLF

alternation = concatenation
*(*c-wsp "/" *c-wsp concatenation)

concatenation = repetition *(1*c-wsp repetition)

repetition = [repeat] element

repeat = 1*DIGIT / (*DIGIT "*" *DIGIT)

element = rulename / group / option /
char-val / num-val / prose-val

group = "(" *c-wsp alternation *c-wsp ")"

option = "[" *c-wsp alternation *c-wsp "]"

char-val = DQUOTE *(%x20-21 / %x23-7E) DQUOTE
; quoted string of SP and VCHAR
; without DQUOTE

num-val = "%" (bin-val / dec-val / hex-val)

bin-val = "b" 1*BIT
[ 1*("." 1*BIT) / ("-" 1*BIT) ]
; series of concatenated bit values
; or single ONEOF range

dec-val = "d" 1*DIGIT
[ 1*("." 1*DIGIT) / ("-" 1*DIGIT) ]

hex-val = "x" 1*HEXDIG
[ 1*("." 1*HEXDIG) / ("-" 1*HEXDIG) ]

prose-val = "<" *(%x20-3D / %x3F-7E) ">"
; bracketed string of SP and VCHAR
; without angles
; prose description, to be used as
; last resort

附录:核心规则

ALPHA = %x41-5A / %x61-7A ; A-Z / a-z

BIT = "0" / "1"

CHAR = %x01-7F
; any 7-bit US-ASCII character,
; excluding NUL

CR = %x0D
; carriage return

CRLF = CR LF
; Internet standard newline

CTL = %x00-1F / %x7F
; controls

DIGIT = %x30-39
; 0-9

DQUOTE = %x22
; " (Double Quote)

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"

HTAB = %x09
; horizontal tab

LF = %x0A
; linefeed

LWSP = *(WSP / CRLF WSP)
; Use of this linear-white-space rule
; permits lines containing only white
; space that are no longer legal in
; mail headers and have caused
; interoperability problems in other
; contexts.
; Do not use when defining mail
; headers and use with caution in
; other contexts.

OCTET = %x00-FF
; 8 bits of data

SP = %x20

VCHAR = %x21-7E
; visible (printing) characters

WSP = SP / HTAB
; white space

RFC5234地址:https://tools.ietf.org/html/rfc5234

Canvas性能优化小结

简介

H5中引入了对canvas的支持,使得网页的表达能力更加丰富了。程序员可以通过canvas来绘制复杂的图形,甚至是游戏。因为工作中的需求,需要使用canvas在网页上绘制家谱。具体的算法可以看之前的一篇总结:树的可视化以及家谱绘制的算法
。当家谱中的数据量变大之后,整个绘制过程会变的很卡(800左右人物的家谱1000次绘制居然需要25s左右)。但是在做完优化后,1000次绘制只需要0.15s左右。

家谱示例

目前的家谱绘制共有两部分。一个是家谱的主体,用户可以通过鼠标、滚轮去任意的浏览;一个是左上角的小地图功能,帮助用户了解到当前浏览的位置,小地图中的有色方块就是用户整个屏幕显示的内容。

优化措施一:缓存

缓存是在绝大多数系统中经常用到的提升性能的方法,典型的以空间换时间。在canvas中当然也可以使用缓存。在家谱的每一次绘制中,都要遍历所有的人物,然后计算他们的位置大小,然后绘制到界面上,然后还要遍历所有的路径再次进行绘制。而使用缓存的目的就是避免这一部分的计算。因此,在任何复杂计算后的绘制,都能使用缓存来提升性能。

在canvas中有这样一个方法,CanvasRenderingContext2D.drawImage(image, dx, dy)image参数代表要绘制的图片源,不仅仅可以是一个image对象,还可以是一个canvas对象。所以如果我们将一个复杂的图形作为canvas对象缓存起来,然后直接使用drawImage方法去绘制到画面上,就能减少很多的重复计算,来提升性能。

例如以下的伪代码

function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i < 10000; i++) {
        paintPerson()
    }
}

// 用户移动鼠标就需要重绘族谱
dom.addEventListener("mousemove", function() {
    paintFamilyTree()
}

用户每一次移动鼠标,都需要10000次的循环去画。所以我们使用下面的方法,使得只需要计算一次

function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i < 10000; i++) {
        paintPerson()
    }
}

var canvasObj = cache(paintFamilyTree) // 缓存这次绘制的结果。

dom.addEventListener("mousemove", function() {
    ctx.drawImage(canvasObj,0,0)
}

那么如何去缓存这个 canvas 对象呢?可以使用document.createElement('canvas')来创建一个不在页面上显示的 canvas 对象,一般也称它为离屏画布,这里用变量 offScreenCanvas 来称呼。然后将绘制的图形画在这个 canvas 上,之后调用 ctx.drawImage(offScreenCanvas,0,0) 即可。

在创建这个离屏画布的时候,我们也要设置合适的宽高,这样也有助于提升性能。

可以参考这个例子来感受一下性能差距:
离屏画布缓存来提升页面性能
例子代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>离屏canvas实例</title>
        <style>
            .main-wrapper {
                width: 800px;
                margin: 10px auto;
            }
        </style>
    </head>

    <body onload="init()">
        <div class="main-wrapper">
            <canvas width="800" height="600" style="border: 1px solid #ccc" id="canvas">
                你的浏览器不支持canvas
            </canvas>
            <br><br><br><br><br><br><br><br><br>
            <div id="result"></div>
            <br>
            渲染次数:<input type="number" id="times" value="1000">,共耗时(ms)<span id="time_used">0</span><br>
            <button onclick="doTest(false,false)">不使用离屏canvas</button>
            <button onclick="doTest(true,false)">使用离屏canvas</button>
            <button onclick="doTest(true,true)">使用离屏canvas并设置正确的高度</button>
        </div>
        <script>
            /**
             * 离屏缓存,num为缓存canvas的数量
             */
            var OffScreenCache = function (num) {
                this.canvases = [];
                for (i = 0; i < num; i++) {
                    this.canvases.push(document.createElement("canvas"));
                }
            }
            OffScreenCache.prototype = {
                pop: function() {
                    return this.canvases.pop();
                },
                push: function(canvas) {
                    this.canvases.push(canvas);
                },
                destroy: function() {
                    this.canvases = null;
                }
            }
            var Ball = function (color) {
                this.radius = 50;
                this.lineWidth = 4;
                this.cache = null;
                this.color = color;
            }
            Ball.prototype = {
                paint: function(ctx, x, y) {
                    ctx.save();
                    ctx.lineWidth = this.lineWidth;
                    ctx.strokeStyle = this.color;
                    for (i = 1; i < this.radius; i+= this.lineWidth) {
                        ctx.beginPath();
                        ctx.arc(x, y, i, 0, Math.PI*2,true); // 绘制
                        ctx.stroke();
                    }
                    ctx.restore();
                },
                useCache: function (cacheCanvas, autoSet) {
                    if (autoSet) {
                        cacheCanvas.width = this.radius * 2;
                        cacheCanvas.height = this.radius * 2;
                    }
                    cacheCtx = cacheCanvas.getContext('2d')
                    this.paint(cacheCtx, cacheCanvas.width / 2, cacheCanvas.height / 2)
                    this.cache = cacheCanvas
                }
            }
            /******************** 下面是执行代码 **************************/
            var g_canvas, g_ctx;
            var g_width = 800;
            var g_height = 600;
            function init() {
                g_canvas = document.getElementById("canvas");
                g_ctx = g_canvas.getContext('2d');
            }
            function getRandomPos() {
                var x = Math.random() * g_width
                var y = Math.random() * g_height
                return {x:x, y:y}
            }
            function showResult(ballCount) {
                var dom = document.getElementById("result");
                dom.innerHTML = "";
                var str = "";
                for (var key of Object.keys(ballCount)) {
                    str += "<span>" + key + "ball:" + ballCount[key] + "</span>    "
                }
                dom.innerHTML = str;
            }
            function doTest(useCache, autoSet) {
                g_ctx.clearRect(0, 0, g_width, g_height)
                var startTime = Date.now();
                var times = document.getElementById("times").value
                var colorArray = ['red', 'blue', 'green', 'black', 'pink']   // 共绘制5种颜色
                var ballCount = {};
                colorArray.forEach(function(color) {
                    ballCount[color] = 0;
                })
                if (useCache) {
                    var colorBall = [];
                    var cacheCanvases = new OffScreenCache(colorArray.length);
                    for (var i = 0; i < colorArray.length; i++) {
                        var ball = new Ball(colorArray[i]);
                        var cacheCanvas = cacheCanvases.pop();
                        ball.useCache(cacheCanvas, autoSet)
                        colorBall.push(ball)
                    }
                    for (var i = 0; i < times; i++) {
                        ball = colorBall[i % 5]
                        ballCount[ball.color]++;
                        var pos = getRandomPos()
                        g_ctx.drawImage(ball.cache, pos.x, pos.y)       // 开始画
                    }
                } else {
                    for (var i = 0; i < times; i++) {
                        var color = colorArray[i % 5];
                        var ball = new Ball(color)
                        var pos = getRandomPos()
                        ball.paint(g_ctx, pos.x, pos.y)               // 开始画
                        ballCount[color]++;
                    }
                }
                var endTime = Date.now();
                document.getElementById("time_used").innerText = endTime - startTime;
                showResult(ballCount)
            }
        </script>
    </body>
</html>

优化措施二:分层

在上述的族谱图片中,左上角的小地图其实是有两个canvas重叠而成。底层的canvas绘制族谱的缩略图,上层的canvas绘制小方框。这样在小方框移动时,只需清空上层canvas的局部画布重绘小方框即可,不需要重绘底层的缩略图。

接口请求速率(接口防刷)限制方案

1.场景

当前业务中接口防刷的场景有:

  • 1.短信/邮件接口。这个接口不做好防刷策略,损失是很大的。
  • 2.涉及到安全问题的接口调用。比如注册接口,登录接口等等。过于频繁的请求可能代表着用户的批量注册,用户密码的暴力破解(需要考虑到可能是用户忘记密码了)等等。

2.要求

  • 1.与当前业务系统解耦合
  • 2.可以很方便的对不同的接口设置不同的请求频率限制
  • 3.具有较好的性能。在大并发的情况下,需要有正确的结果

3.方案提出

3.1 token bucket(令牌桶)

token bucket算法的如下图:

token_bucket

描述如下:

1.假设有一个桶,不断的向桶中投放token,并且我们也可以从桶中取出token。
2.投放token的速率是恒定的r1,桶满了token数量则不会再增长。
3.取出的速率是随机的r2。如果桶是空的,则无法取出token。
4.所有的请求都必须从桶中取出token才是有效的。否则该请求会被拒绝。

举个例子,假设桶的容量是10,每秒放2个token进去。

  • 如果现在每秒2个请求,那么所有这样的请求都是没有问题的,都会拿到token。
  • 如果现在每秒4个请求,10 + 2x = 4x。也就是x=5秒后请求就没法及时拿到token,被丢弃。

总结出以下特点:
– 如果请求速率过快,到一段时间后会受到限制
– 允许突发请求

3.2 redis数据库

使用redis数据库完全是因为方便以及速度。只要注意这其中存在的CAS问题即可

4.具体实现

首先,为了解决redis中检查和存储数据存在的CAS问题,我们需要使用lua脚本。因此,对于token bucket的主要实现是用lua脚本实现,然后通过php调用。

local function mysplit(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; 
    local i=1
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            t[i] = str
            i = i + 1
    end
    return t
end

local res = redis.pcall('get',KEYS[1])

if (res == false) then
    -- bucket不存在,初始化为capacity - 1
    local str = string.format("%d %d",ARGV[1] - 1,ARGV[2])  
    return redis.pcall('set',KEYS[1],str)
end

local arr_str = mysplit(res, " ")

local num = arr_str[1]
local timestamp = arr_str[2]

-- 根据时间戳来计算num,这里一秒2个令牌
num = num + (ARGV[2] - timestamp) * ARGV[3]
-- 不超过10个令牌
if num > 10 then
    num = 10
end

if (tonumber(num) > 0) then
    -- 拿到token并减一
    local str = string.format("%d %d",num - 1,ARGV[2])
    return redis.pcall('set', KEYS[1], str)
else 
    -- 没有拿到token
    return false
end
<?php

$sha = '2ff7ad2d1b49da8430e5adc8675e';

/**
 * 初始化lua脚本
 */
function initScript($redis) {
    global $sha;
    //不存在脚本,load进去
    $script = file_get_contents('check_and_set.lua');
    $sha = $redis->script('load', $script);
    if($redis->getLastError() !== NULL){
        echo "出错了:".$redis->getLastError().'\n';
    }
    echo "初始化的脚本sha:".$sha.PHP_EOL;
}

function getTokenFromBucket($redis, $bucket) {
    global $sha;
    $capacity = 10;
    $time = time();
    $inputRate = 2;     //一秒2个token
    $params = array($bucket, $capacity, $time, $inputRate);
    $result = $redis->evalSha($sha, $params, 1);
    if($redis->getLastError() !== NULL){
        echo "出错了:".$redis->getLastError().'\n';
    }
    return $result;
}

$redis = new Redis();
$redis->pconnect("127.0.0.1");

initScript($redis);

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, 'bucket1');
    if (!$result) {
        break;
    } else {
        echo time()."--拿到令牌".PHP_EOL;
        usleep(250000);    //每秒请求4次,10 + 2x = 4x, x = 5。5秒左右无法拿到令牌
    }
}

$end = microtime(true) * 1000;

echo "共耗时:".($end - $start)."毫秒".PHP_EOL.PHP_EOL;

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, 'bucket2');
    if (!$result) {
        break;
    } else {
        echo "拿到令牌".PHP_EOL;
        usleep(500000);    //每秒请求2次,永远可以拿到令牌
    }
}

$end = microtime(true) * 1000;

echo "共耗时:".($end - $start)."毫秒".PHP_EOL.PHP_EOL;

代码中主要部分概括如下:

  • 检查redis中是否存在桶bucket1
  • 如果不存在,那么默认的容量是10,将bucket1设为9 timestamp(timestamp为当前的时间戳)。返回true
  • 如果存在,则获取该bucket1的值,解析出实际token数量为num,以及上一次存储时时间戳为pre。如果rate为token的生成速率,那么现在的token数量应该是:num = num + (time() – pre) * rate。num最大为默认容量10。如果num大于0,则减一后重新设置,返回true。否则返回false。

代码的运行结果如下:

运行结果演示

5.扩展知识

与token bucket相似的算法还有leaky bucket(漏桶)。leaky bucket 就像是 token bucket的镜像一样。leaky bucket 一般用于Traffic shaping和Traffic policing等问题,与下面的两张图对应。

leaky bucket

leaky bucket

可以描述如下:

  • 1.有一个桶,其上方水龙头以变化的速率r1向里面滴水,桶底的水龙头则以恒定的速率向下漏水
  • 2.水桶装满后,上方水龙头的滴水就会溢出
  • 3.水桶空后,下方水龙头则不会滴水

这里举一个异步任务处理(任务队列的容量是有限的)的例子。

  • 上方水龙头相当于任务投递系统,下方水龙头相当于任务处理系统。
  • 任务投递的速率是变化的,因为不同时间段的系统负载可能不同。
  • 任务处理系统的速率是恒定的,因为机器的处理能力是恒定的。
  • 当任务投递的速率大于处理速率,填充满任务队列后,后面的任务都会被丢弃。

可以看出leaky bucket可以将不规则的抖动的请求序列变成规则的平滑的请求序列。leaky bucket通常有两个版本:作为一个评判的仪器(meter)或者是生成一个合法请求队列(queue)。

虽然在使用中leaky bucket和token bucket是不同的,但实际上他们是同一种思路。leaky bucket的变化的输入相当于token bucket的变化的输出,leaky bucket的恒定的输出相当于token bucket的恒定的输出。

参考文献

token bucket
leaky bucket

redis中的事务与锁

这篇文章是我在查找如何对redis中的值做原子操作时的一系列笔记,虽然最初的目的只是研究有哪些方式可以实现事务(transaction)操作,但是后来的延伸很多,所以我认为有必要做一些笔记防止忘记。

1.redis中的事务

redis中的事务其实并不满足数据库事务的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离型(Isolation)、持久性(Durability),简称ACID。redis只能在一致性和隔离性上提供保证,而原子性和持久性是无法保证的。因为redis的事务操作如果中断,无法回滚,满足不了原子性,并且redis的持久化策略有很多中,比如在纯内存模式下是无法持久化数据库更改的,满足不了持久性。

所以下面针对与redis事务的讨论都是有局限性的。仅仅是指对redis数据进行操作时,不会受到其他客户端的干扰。举例来说:客户端A读取keyA,修改keyA中间,不会受到客户端B的影响。

    //客户端A执行以下代码
    //获取user1的性别,如果性别是男,头像设置为男性角色
    //否则头像设置为女性角色
    $gender = $redis->get('user1_gender');
    if($gender === 'male'){
        $redis->set('user1_photo','male.png');
    }else{
        $redis->set('user1_photo','female.png');
    }
    //客户端B执行以下代码
    //更改user1的性别,如果是男则改为女,如果是女则改为男
    //并根据性别设置头像
    $gender = $redis->get('user1_gender');
    if($gender === 'male'){
        $redis->set('user1_gender','female');
        $redis->set('user1_photo','female.png');
    }else{
        $redis->set('user1_gender','male');
        $redis->set('user1_photo','male.png');
    }

如果客户端A、B同时只有一个执行,那么性别和头像一定是对应的。但是A、B同时执行,并且执行顺序如下:
时序图

那么执行结果就是user1性别为女,但头像为男性角色。

所以针对于这种情况,我们需要一定的措施去预防。

redis的事务实现方式有多种。据我所知有三种方式:

  • 1.MULTI/EXEC
  • 2.WATCH/UNWATCH
  • 3.lua脚本

1.1 MULTI/EXEC

MULTI/EXEC是成对出现的指令。大家都知道,redis执行指令是单线程的,也就是说所有指令都处于一个队列,一个一个执行。MULTI/EXEC可以保证所有的指令都会被同时放松到redis中,中间不会掺杂来自其他客户端的指令。

但是这个针对于上述情况并不适用。因为我们需要在GET到数据之后,才能做下面的更新操作。

1.2 WATCH/UNWATCH

在redis中我们可以使用watch来监视一个值。

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

比如这段代码,在我们WATCH name后,如果另外一个客户端修改了name的值,那么这个客户端再次修改name则无法成功。这其实就是乐观锁的一种实现。它保证了代码的执行结果不会错乱。用一开始的例子来说,就是不会出现性别和头像不对应的情况。

1.3 lua脚本
redis中可以执行lua脚本来完成事务操作。lua脚本和MULTI/EXEC很像,也就是说在lua脚本执行的过程中,redis是不执行其他客户端的指令的。

但是lua脚本的不同之处在于,你可以使用GET操作来获取数据并判断,再执行后来的操作。

2.在redis操作中使用锁

在多线程环境中,为了防止资源出现race condition,需要借助锁来互斥的访问资源。在这里也是一样的,user1_gender就是我们要互斥访问的资源。

还是上述那个例子,如果我们使用悲观锁,只有获得锁的客户端才能读取和修改user1的的值,也可以很好的解决这个问题。

伪代码如下:

$can_lock = lock('user1_gender'); //得到锁
if($can_lock){
    do_something();
    $release_lock('user1_gender');  //释放锁
}

不同于多线程环境下的是,我们这里的锁的范围是针对于不同的客户端。因此没法使用基于系统的、或者基于语言的锁,而是得使用分布式的锁。这样的分布式锁我们同样可以借助于redis来实现。

整个分布式锁的实现可以概括为以下几个步骤:

    1. 获得锁。得到要锁的资源的唯一hash:lockname,以及一个随机字符串:identifier,设置expire:20s,这个expire就是锁的有效期,在有效期后锁会自动释放,防止出现死锁。在redis中使用setnx(lockname,identifier,expire)。这句代码的意思是:如果redis中不存在lockname,则存入lockname,值为identifier,过期时间是expire。
    1. 第一步我们就得到了一个锁,这一步我们开始执行获得锁之后的代码
    1. 释放锁。我们根据lockname来从redis中查找。如果get(lockname) == identifier,则表示我们仍然持有这把锁,使用delete(lockname)来释放锁。如果不等于,说明我们已经不持有这把锁了,则什么也不做。

那么lock函数可以用以下代码来描述:

function lock($lockname,$identifier,$expire = 20){
    $acquire_timeout = 10;      //花费10秒去获得锁,否则就放弃
    $end = time() + $acquire_timeout
    while (time() < $end){
        $can_lock = $redis->setnx($lockname,$identifier,$expire);
        if($can_lock){
            return true;
        }
        sleep(0.1);
    }

    return false;
}
    function release_lock($lockname,$identifier){
        $redis->watch($lockname);
        try{
            if($redis->get($lockname) === $identifier){
                //锁仍然持有,释放锁
                $redis->delete($lockname);
                return true;
            }

            $redis->unwatch();
        }catch(Exception $e){

        }

        return false;
    }

这样我们就很轻松的得到了借助于redis实现的分布式锁。但是这样的实现方式依然是有问题的。

问题1:如果在某个客户端获得锁后,redis主服务器宕机了,那么即使我们使用了主从备份,从属服务器被提升为主服务器,因为redis备份是异步的原因,这里的锁是没法及时同步到从属服务器的。

问题2:如果一个客户端在获得锁后,执行的操作超过了锁的有效期,锁被自动释放了。那么后续的操作是没法受到锁的保护的。

问题2的解决方案可以是watch,在获取锁后,可以立刻watch资源,然后再执行余下操作。

问题1的解决方案则是接下来要介绍的redlock算法

3.redlock

redlock算法是Redis的作者antirez提出来的。
可以被概述为以下几个步骤:

  • 1.获取当前时间(毫秒数):start_time。

  • 2.按顺序获得N个Redis节点的锁(使用相同的key和identifier,并设置初始有效时间:init_validity_time)。在获得每个redis结点的锁的时候,都要设置一个timeout参数,这个timeout要远小于锁的自动释放时间。例如:如果锁的自动释放时间是10s,timeout应该为~5-55ms(还得视网络情况决定)。这样可以防止在获取锁时,节点宕机,导致耗时过长锁被释放了。如果获取锁失败则立刻获取下一个redis节点的锁

  • 3.client计算为了获取锁花了多长时间:used_time = current_time – start_time。当且仅当client可以获取大多数实例的时候(至少N / 2 + 1个),所花费的时间小于锁的有效时间,才认为获得了锁。

  • 4.如果获得了锁,重新计算锁的有效时间:validity_time = init_validity_time – used_time

  • 5.如果锁获取失败(无法获取N/2 + 1个节点的锁,或者有效时间validity_time是负数),则释放所有实例的锁(即使获得锁的时候失败了,这主要是考虑到有的时候锁获得成功了,但是告知客户端时网络异常)。

这很好的解决了上述的问题1,通过多个redis节点了来保证分布式锁服务的可靠性。

参考文章

Distributed locks with Redis

基于Redis的分布式锁到底安全吗(上)?

基于Redis的分布式锁到底安全吗(下)?

redis事务

6.2.3 Building a lock in Redis

系统权限设计中RBAC模型的使用

什么是RBAC

RBAC,全称是Role-Based-Access-Control,可以译为基于角色的权限控制,是一种应用非常广泛的权限控制模型。

什么是角色(Role)

角色不是一个用户实体,而是代表了一组行为能力或责任的命名实体,以我们常用的QQ群为例,有以下角色:超级管理员、群主、管理员、普通成员、非群成员。

每个角色都有不同的权限:

  • 超级管理员可以管理任何的QQ群,但是一般只能禁言、删除群,不能管理具体的某个群成员,也不能在群里聊天;
  • 群主可以管理自己的QQ群,可以在群里聊天;
  • 管理员可以管理自己的QQ群,可以聊天,但是不能解散群,也不能任命或撤销其他管理员;
  • 普通成员则没有管理权限,只有在群里聊天的权限
  • 非群成员无管理权限,也无法在群里聊天。

所以可以知道,每个角色都对应着一组行为能力或责任。

隐式的基于角色的权限控制

对于QQ群的例子来说。如果我们要做一个删除某个群成员的动作,伪代码可能如下:

if( user.hasRole('Group Owner') || user.hasRole('Group Manager') ){
    // delete the Group Member
} else {
    // Permission Denied
}

那么现在,如果腾讯的权限策略发生了改变,超级管理员也可以删除某个群的群成员来防止某些不当言论的传播,那么代码就要改为

if( user.hasRole('Group Owner') || user.hasRole('Group Manager') || user.hasRole('Super Manager') ){
    // delete the Group Member
} else {
    // Permission Denied
}

上面这种权限的管理方式,就可以称为隐式的基于角色的权限控制。因为Group OwnerGroup ManagerSuper Manager并不能显式的表达出:它们的角色具有删除群成员的权限。没有任何的代码显式的定义了这些角色的权限。我们只能从代码中隐式的推测出:这三个角色拥有删除群成员的权限。所以程序员们使用if/else语句来反映这些假设。

显式的基于角色的权限控制

了解了隐式的基于角色的权限控制,那么我们就可以知道,显式的基于角色的权限控制要有能力直接表达出:当前用户有权限去删除群成员。这样我们的代码可以调整如下:

if( user.isPermitted('GroupMember:delete:478') ){
    // delete the Group Member
} else {
    // Permission Denied
}

这样从代码中可以看到,如果当前用户被允许删除ID为478的群成员,那么就去删除,否则报权限不足的错误。至于isPermitted()中如何去判断权限的,可能依然是回到了哪些角色有哪些权限的问题。但是不同之处在于,应对上面的需求变更问题时,我们只需要更改user的isPermitted的判断规则,而不用去更改散布在代码中各个地方的if语句。可以做到以最小的变更来应对复杂的需求变化。

隐式 vs 显式

隐式和显式在我看来,其内在的权限控制仍然是一样的,都是基于角色在做权限判断。但对外的抽象则是截然不同的:隐式侧重于某个用户是否有某些角色,显式则直接将问题聚焦于某个用户是否有对某个资源的某个操作的权限。这在写代码中给程序员带来的影响是不一样的。截取一段项目中的代码来佐证:

//检查操作的权限
if(
    !$familyDB->isUserForFamily($familyId,$userId)&&
    !$familyDB->isAdminForFamily($familyId,$userId)&&
    !$familyDB->isOriginatorForFamily($familyId,$userId)
){
    Util::printResult( $GLOBALS['ERROR_PERMISSION'], "操作权限错误");
    exit;
}

这段代码判断了用户是否对家族有读取权限。因为我们是隐式的基于角色的权限控制,很直观的想法就是:家族成员、家族管理员、家族创始人这三个角色都有对家族的读取权限。所以这里判断了三个权限。事实上,家族创始人的判断是多余的,因为家族创始人肯定属于家族成员。但是真正在写代码的时候,很有可能考虑不到这个问题。

但如果是显式的基于角色的权限控制,这个if语句就是:

//检查操作的权限
if(
    !$familyDB->isUserHasReadPermission($familyId,$userId))
){
    Util::printResult( $GLOBALS['ERROR_PERMISSION'], "操作权限错误");
    exit;
}

程序员写代码时就不会做出多余的判断。可以得出,显式的抽象表达能力是明显更强的。

新的RBAC:Resource-Based-Access-Control

通过对比隐式显式的区别,我们知道显式是直接检查某个用户对某个资源(Resource)是否有某个权限。所以不如直接抛弃角色(Role)的概念,将资源这个概念引入。这样就有了基于资源的权限控制(Resource-Based-Access-Control)

xdebug的简易使用教程

1.ubuntu下的安装

通过 pecl 安装

pecl install xdebug

然后将xdebug.so加入php.ini中,注意如果使用的是fpm,则需要加入到fpm下的php.ini,同理cli环境下则需要向cli下的php.ini添加

zend_extension=”/usr/local/php/modules/xdebug.so”

注意:xdebug是zend的拓展,不需要添加extension=xdebug.so

通过编译安装

git clone git://github.com/xdebug/xdebug.git
cd xdebug
phpize
./configure –enable-xdebug
make
make install

2.配置php使用xdebug

xdebug有许多特性。

2.1 通过设置来影响var_dump()

影响var_dump()的属性有:

  • xdebug.var_display_max_children,
  • xdebug.var_display_max_data
  • xdebug.var_display_max_depth

这三个属性的值都是数字类型的。会影响var_dump()函数显示的变量的内容长度和深度。

可以在php.ini做以下设置:

xdebug.var_display_max_depth = 2
xdebug.var_display_max_data = 8
xdebug.var_display_max_children = 3

具体的值可以自己手动调整

另外还有xdebug.cli_colorxdebug.overload_var_dump会影响到显示的效果

2.2 堆栈跟踪

演示脚本如下:

<?php
//这个脚本会超时
function foo( $a ) {
    for ($i = 1; $i < $a['foo']; $i++) {
        if ($i == 500000) xdebug_break();
    }
}

set_time_limit(1);
$c = new stdClass;
$c->bar = 100;
$a = array(
    42 => false, 'foo' => 9121240000000000,
    $c, new stdClass, fopen( '/etc/passwd', 'r' )
);
foo( $a );
?>

我们使用设置

xdebug.collect_params = 1

结果如下:
params为1

修改一下设置:

xdebug.collect_params = 3

params为3

可以看到浏览器中的报错信息更多了,体现在foo()函数中的参数数量

同样的,我们还可以设置

xdebug.dump_globals = On
xdebug.dump.SERVER = ‘REQUEST_URI’

这样则可以展示一些超全局变量。这里指定了请求的URI

超全局变量

还可以设置

xdebug.show_local_vars = On

来展示程序运行期间的本地变量
本地变量

2.3 函数跟踪

使用xdebug可以记录所有的函数调用。

测试脚本如下:

<?php

ini_set('xdebug.trace_format','0');

xdebug_start_trace();
$str = "Xdebug";
function ret_ord( $c )
{
    return ord( $c );
}

foreach ( str_split( $str ) as $char )
{
    echo $char, ": ", ret_ord( $char ), "\n";
}
xdebug_stop_trace();
?>

使用的设置如下:

;代码跟踪日志文件位置,注意要先新建这个/tmp/php_traces/fpm目录,并设置777
xdebug.auto_trace = Off
xdebug.trace_output_dir = /tmp/php_traces/fpm
;代码跟踪日志文件格式 
xdebug.trace_output_name = trace.%c.%p
;trace中显示函数的参数值,这个很有用,待会细说
xdebug.collect_params = 3
xdebug.collect_includes = On
xdebug.collect_return = On
xdebug.show_mem_delta = On
xdebug.var_display_max_depth = 2

结果如下:
此处输入图片的描述

通过调整xdebug.trace_format的值可以更改记录的格式。0是人类可读,1是机器可读,2是html

2.4远程调试

这里xdebug中非常好用的一个功能。通过设置,我们可以在IDE中单步调试。下面我会使用vscode来演示一遍。

2.4.1 环境准备

1.首先我们需要为vscode安装xdebug的插件。

2.配置好调试环境

此处输入图片的描述

这一步是在vscode左侧调试栏新增配置,然后选择php即可。

3.打上断点,启动调试

此处输入图片的描述

4.在浏览器中访问这个页面即可

此处输入图片的描述

参考链接

https://xdebug.org/docs/all#default

ubuntu服务器部署ipv6访问

整个部署过程分为:
1.启用服务器的ipv6支持
2.申请ipv6通道
3.开启nginx的ipv6地址监听

1.ubuntu上开启ipv6支持,需要修改几个地方

1.开启ipv6支持,/etc/sysctl.conf
确保有以下设置:

net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0

设置完毕后使用sysctl -p使设置生效

2.设置服务器的ipv6 dns服务器,/etc/network/interfaces
添加以下设置:

dns-nameserver 2001:4860:4860::8888
dns-nameserver 2001:4860:4860::8844

设置完毕后使用sudo resolvconf -u使设置生效

这样能够保证服务器在连接ipv6地址时有合适的dns服务器

测试:ping6 ipv6.google.com
如果能够ping通则表示已经设置成功

2.申请ipv6通道

因为我的服务器是没有分配ipv6地址的,所以需要去http://tunnelbroker.net申请ipv6的通道。
1.注册账号
2.邮箱激活
3.登录
4.create regular tunnel
5.输入你的服务器的ipv4地址,创建通道
6.在example configurations处选择操作系统,获取配置
7.在 /etc/network/interfaces中拷贝6中的配置
8.sudo resolvconf -u使配置生效
9.通过ifconfig查看是否设置成功,如果出现了ipv6的地址则表示成功

3.开启nginx的ipv6地址监听

在所有的站点配置文件中加上:

listen [::]:80
listen [::]:443

分别监听80端口和443端口(如果有https)
重启nginx即可。
通过http://ipv6-test.com/validate.php测试域名能否在ipv6下访问成功。如果是阿里云,那么有可能在IPv6 DNS server这一项上失败。因为阿里云的DNS服务器没有IPv6 DNS server。这种情况下,如果用户只有ipv6的环境,则会因为无法访问DNS服务器而失败

tcpdump分析php curl

所用工具

  • tcpdump:linux下分析网络的一个好用的终端工具
  • psysh:php的一个终端执行工具,可以很方便的在终端执行php代码
  • curl:php的curl库

tcpdump报文中Flags代表报文类型:
S (SYN), F (FIN), P (PUSH), R (RST), U (URG), W (ECN CWR), E (ECN-Echo) or ‘.’ (ACK), or ‘none’ if no flags are set.

分析背景

最近在了解swoole,因为公司的技术栈是php,所以之前用的java那一套在普通的php开发上完全用不了。为了让php有更好的性能,更广泛的使用场景,找了找相关资料,了解到swoole这个库。决定使用之后,项目中有个地方需要使用到http连接池,所以需要使用curl,但是curl虽然强大,但是参数实在是太多了,里面的实现细节也只能查看c的源代码才知道。所以只能曲线救国,通过tcpdump了解curl在http请求上的工作机制

tcpdump的使用方法是:

案例分析

案例一

只执行一次exec,等待一段时间,强制关闭终端进程

<?php
$context = curl_init();
curl_setopt($context,CURLOPT_URL,"http://www.izuqun.com");
curl_exec($context);

整个过程的报文如下

    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [S], cksum 0x4bdf (correct), seq 3687066847, win 29200, options [mss 1460,sackOK,TS val 5931640 ecr 0,nop,wscale 7], length 0
16:28:54.014280 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)

    `116.62.25.128.http > 192.168.1.107.44664`: Flags [S.], cksum 0xbcb5 (correct), seq 4174849842, ack 3687066848, win 28960, options [mss 1460,sackOK,TS val 3844377305 ecr 5931640,nop,wscale 7], length 0
16:28:54.014304 IP (tos 0x0, ttl 64, id 44712, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [.], cksum 0x5bba (correct), ack 1, win 229, options [nop,nop,TS val 5931643 ecr 3844377305], length 0
16:28:54.014334 IP (tos 0x0, ttl 64, id 44713, offset 0, flags [DF], proto TCP (6), length 105)

    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [P.], cksum 0xee60 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 5931643 ecr 3844377305], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: *\* 
16:28:54.023036 IP (tos 0x20, ttl 52, id 44520, offset 0, flags [DF], proto TCP (6), length 52)

    `116.62.25.128.http > 192.168.1.107.44664`: Flags [.], cksum 0x5b85 (correct), ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 0
16:28:54.023061 IP (tos 0x20, ttl 52, id 44521, offset 0, flags [DF], proto TCP (6), length 314)
    `116.62.25.128.http > 192.168.1.107.44664`: Flags [P.], cksum 0xba4d (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:28:54 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: "5a2deef5-4db"
        Accept-Ranges: bytes
16:28:54.023076 IP (tos 0x0, ttl 64, id 44714, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [.], cksum 0x5a73 (correct), ack 263, win 237, options [nop,nop,TS val 5931645 ecr 3844377307], length 0
16:28:54.024214 IP (tos 0x20, ttl 52, id 44522, offset 0, flags [DF], proto TCP (6), length 1295)

    `116.62.25.128.http > 192.168.1.107.44664`: Flags [P.], cksum 0x870b (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 1243: HTTP
16:28:54.024236 IP (tos 0x0, ttl 64, id 44715, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [.], cksum 0x5585 (correct), ack 1506, win 256, options [nop,nop,TS val 5931645 ecr 3844377307], length 0

1~3报文:tcp的三次握手,
4~5报文:curl发起的get请求、服务器的ack回应
6~9报文:服务器对get请求的返回、我的机器的内核的ack,因为报文长度是超出了最大的限制,所以分成两次发送

稍等一会儿,又有新的报文

    `116.62.25.128.http > 192.168.1.107.44664`: Flags [F.], cksum 0x161d (correct), seq 1506, ack 54, win 227, options [nop,nop,TS val 3844393567 ecr 5931645], length 0
16:29:59.097967 IP (tos 0x0, ttl 64, id 44716, offset 0, flags [DF], proto TCP (6), length 52)
    `192.168.1.107.44664 > 116.62.25.128.http`: Flags [.], cksum 0xd672 (correct), ack 1507, win 256, options [nop,nop,TS val 5947914 ecr 3844393567], length 0

这是服务器发起的tcp连接关闭的报文,我的机器响应了,但是没有主动再次发送Fin

主动关闭psysh进程,出现大量报文

    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x8765 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968150 ecr 3844393567], length 0
16:31:20.253999 IP (tos 0x0, ttl 64, id 44718, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x8730 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968203 ecr 3844393567], length 0
16:31:20.465940 IP (tos 0x0, ttl 64, id 44719, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x86fb (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968256 ecr 3844393567], length 0
16:31:20.890008 IP (tos 0x0, ttl 64, id 44720, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x8691 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968362 ecr 3844393567], length 0
16:31:21.737935 IP (tos 0x0, ttl 64, id 44721, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x85bd (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968574 ecr 3844393567], length 0
16:31:23.437992 IP (tos 0x0, ttl 64, id 44722, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x8414 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968999 ecr 3844393567], length 0
16:31:26.833967 IP (tos 0x0, ttl 64, id 44723, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x80c3 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5969848 ecr 3844393567], length 0
16:31:33.633977 IP (tos 0x0, ttl 64, id 44724, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x7a1f (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5971548 ecr 3844393567], length 0
16:31:47.218007 IP (tos 0x0, ttl 64, id 44725, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x6cdb (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5974944 ecr 3844393567], length 0
16:32:14.417998 IP (tos 0x0, ttl 64, id 44726, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 > 116.62.25.128.http: Flags [F.], cksum 0x524b (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5981744 ecr 3844393567], length 0

这些报文都是我的机器主动发送的关闭报文,但是服务器也不再响应

案例二

完整的使用curl_init和curl_close,显式关闭连接

<?php
$context = curl_init();
curl_setopt($context,CURLOPT_URL,"http://www.izuqun.com");
curl_exec($context);
curl_close($context);

报文如下

    192.168.1.107.44684 > 116.62.25.128.http: Flags [S], cksum 0x469f (correct), seq 2378041097, win 29200, options [mss 1460,sackOK,TS val 6028159 ecr 0,nop,wscale 7], length 0
16:35:20.090006 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    116.62.25.128.http > 192.168.1.107.44684: Flags [S.], cksum 0xd6a7 (correct), seq 3548165201, ack 2378041098, win 28960, options [mss 1460,sackOK,TS val 3844473825 ecr 6028159,nop,wscale 7], length 0
16:35:20.090050 IP (tos 0x0, ttl 64, id 64631, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 > 116.62.25.128.http: Flags [.], cksum 0x75ac (correct), ack 1, win 229, options [nop,nop,TS val 6028162 ecr 3844473825], length 0
16:35:20.090105 IP (tos 0x0, ttl 64, id 64632, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44684 > 116.62.25.128.http: Flags [P.], cksum 0x0853 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 6028162 ecr 3844473825], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:35:20.099626 IP (tos 0x20, ttl 52, id 47030, offset 0, flags [DF], proto TCP (6), length 52)
    116.62.25.128.http > 192.168.1.107.44684: Flags [.], cksum 0x7576 (correct), ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 0
16:35:20.099655 IP (tos 0x20, ttl 52, id 47031, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http > 192.168.1.107.44684: Flags [P.], cksum 0xda41 (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:35:20 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: "5a2deef5-4db"
        Accept-Ranges: bytes

16:35:20.099670 IP (tos 0x0, ttl 64, id 64633, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 > 116.62.25.128.http: Flags [.], cksum 0x7464 (correct), ack 263, win 237, options [nop,nop,TS val 6028164 ecr 3844473828], length 0
16:35:20.099680 IP (tos 0x20, ttl 52, id 47032, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http > 192.168.1.107.44684: Flags [P.], cksum 0xa0fc (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 1243: HTTP
16:35:20.099686 IP (tos 0x0, ttl 64, id 64634, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 > 116.62.25.128.http: Flags [.], cksum 0x6f76 (correct), ack 1506, win 256, options [nop,nop,TS val 6028164 ecr 3844473828], length 0

这边的报文一开始和案例一完全一致,但是在我使用curl_close()显式关闭curl后,并没有发送任何关闭tcp连接的报文。查了一下资料,在php7.0中,如果$context任何有全局引用,即使使用curl_close()显式关闭,也不会关闭连接。

案例3

完整的使用curl_init和curl_close,显式关闭连接,并且多次执行exec

16:38:26.590098 IP (tos 0x0, ttl 64, id 18826, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [S], cksum 0x520f (correct), seq 3672832041, win 29200, options [mss 1460,sackOK,TS val 6074787 ecr 0,nop,wscale 7], length 0
16:38:26.604823 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    116.62.25.128.http > 192.168.1.107.44714: Flags [S.], cksum 0xf795 (correct), seq 3884635295, ack 3672832042, win 28960, options [mss 1460,sackOK,TS val 3844520454 ecr 6074787,nop,wscale 7], length 0
16:38:26.604870 IP (tos 0x0, ttl 64, id 18827, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [.], cksum 0x969a (correct), ack 1, win 229, options [nop,nop,TS val 6074790 ecr 3844520454], length 0
16:38:26.604904 IP (tos 0x0, ttl 64, id 18828, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [P.], cksum 0x2941 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 6074790 ecr 3844520454], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:38:26.616651 IP (tos 0x20, ttl 52, id 28329, offset 0, flags [DF], proto TCP (6), length 52)
    116.62.25.128.http > 192.168.1.107.44714: Flags [.], cksum 0x9664 (correct), ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 0
16:38:26.616674 IP (tos 0x20, ttl 52, id 28330, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http > 192.168.1.107.44714: Flags [P.], cksum 0xf829 (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:38:26 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: "5a2deef5-4db"
        Accept-Ranges: bytes

16:38:26.616687 IP (tos 0x0, ttl 64, id 18829, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [.], cksum 0x9551 (correct), ack 263, win 237, options [nop,nop,TS val 6074793 ecr 3844520457], length 0
16:38:26.616701 IP (tos 0x20, ttl 52, id 28331, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http > 192.168.1.107.44714: Flags [P.], cksum 0xc1ea (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 1243: HTTP
16:38:26.616708 IP (tos 0x0, ttl 64, id 18830, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [.], cksum 0x9063 (correct), ack 1506, win 256, options [nop,nop,TS val 6074793 ecr 3844520457], length 0
16:38:28.241841 IP (tos 0x0, ttl 64, id 18831, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [P.], cksum 0x2174 (correct), seq 54:107, ack 1506, win 256, options [nop,nop,TS val 6075199 ecr 3844520457], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:38:28.253636 IP (tos 0x20, ttl 52, id 28332, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http > 192.168.1.107.44714: Flags [P.], cksum 0xeede (correct), seq 1506:1768, ack 107, win 227, options [nop,nop,TS val 3844520867 ecr 6075199], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:38:28 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: "5a2deef5-4db"
        Accept-Ranges: bytes

16:38:28.253689 IP (tos 0x0, ttl 64, id 18832, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [.], cksum 0x8be1 (correct), ack 1768, win 276, options [nop,nop,TS val 6075202 ecr 3844520867], length 0
16:38:28.253704 IP (tos 0x20, ttl 52, id 28333, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http > 192.168.1.107.44714: Flags [P.], cksum 0xb8a1 (correct), seq 1768:3011, ack 107, win 227, options [nop,nop,TS val 3844520867 ecr 6075199], length 1243: HTTP
16:38:28.253713 IP (tos 0x0, ttl 64, id 18833, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 > 116.62.25.128.http: Flags [.], cksum 0x86f3 (correct), ack 3011, win 295, options [nop,nop,TS val 6075202 ecr 3844520867], length 0

多次curl_exec()会复用之前的连接,但是curl_close()依然是不能关闭连接的。但当我使用unset($context)时,观察到了以下报文:

    192.168.1.107.49938 > 116.62.25.128.http: Flags [F.], cksum 0x8407 (correct), seq 54, ack 1506, win 256, options [nop,nop,TS val 274723 ecr 3860448314], length 0                                                                                                      
10:20:58.408173 IP (tos 0x20, ttl 52, id 25537, offset 0, flags [DF], proto TCP (6), length 52)                                      
    116.62.25.128.http > 192.168.1.107.49938: Flags [F.], cksum 0x5ca5 (correct), seq 1506, ack 55, win 227, options [nop,nop,TS val 3860458424 ecr 274723], length 0                                                                                                      
10:20:58.408218 IP (tos 0x0, ttl 64, id 34603, offset 0, flags [DF], proto TCP (6), length 52)                                       
    192.168.1.107.49938 > 116.62.25.128.http: Flags [.], cksum 0x5c85 (correct), ack 1507, win 256, options [nop,nop,TS val 274726 ecr 3860458424], length 0 

我的机器主动关闭连接发出Fin,服务器ack并发送Fin,我的机器也ack。两边顺利的完成了连接的关闭。

所以当前可以得出以下几点:

1.$context = curl_init(), $context是可以复用的,复用$context后,多次执行curl_exec()可以避免tcp的3次握手和4次断开的过程。

2.curl_close()在上面的试验中,并不能正常的关闭连接,这是因为在psysh的执行环境中,$context一直是在当前的作用域,会一直保持着变量的引用。而php7.0中,$context如果保持引用,curl_close则不会关闭连接,而unset($context)则会关闭。

3.服务器主动关闭连接后发出Fin,我的机器虽然响应了ack,但是没有主动发送Fin,这会不会导致服务器大量的fin_wait2?

第三点是很重要的,如果不解决会大量占用服务器资源。

所以再做一个实验,实验过程如下:

  • 我的机器初始化大量的curl_init(),然后不释放,等待服务器关闭。使用netstat查看服务器是否会出现大量的fin_wait2。

使用netstat -an|awk '/tcp/ {print $6}'|sort|uniq -c可以快速查看各种tcp连接状态的统计。
初始情况为:

20 CLOSE_WAIT
63 ESTABLISHED                                                                              
10 LISTEN

psysh中执行以下脚本

$contextArr = array();
for($i = 0; $i < 1000; $i++){
    $contextArr[$i] = curl_init();
    curl_setopt($contextArr[$i],CURLOPT_URL,"http://www.izuqun.com");
    curl_exec($contextArr[$i]);
}

脚本执行完后,服务器状态变化如下:

20 CLOSE_WAIT
1058 ESTABLISHED
5 FIN_WAIT2
10 LISTEN
2 TIME_WAIT
20 CLOSE_WAIT
58 ESTABLISHED
1001 FIN_WAIT2
10 LISTEN

20 CLOSE_WAIT
63 ESTABLISHED
10 LISTEN

可以看到,中间有1000个FIN_WAIT2状态,说明我们的猜想是正确的。