PHP: 使用FastCGI协议打造高性能网站服务

之前我写了一篇文章【 PHP: 深入pack/unpack 】介绍了如何在PHP中进行TCP打包和解包,以及通过分离数据层来实现可扩展和性能的提升。但是有时候性能不是衡量的唯一标准,通常需要兼顾性能和开发效率。您可能会说基于HTTP接口的开发效率不错。是的,基于HTTP协议的开发效率很高,而且它适合各种网络环境。但是由于HTTP协议需要发送大量的头部,所以导致性能不是很理想。那么有没有一种比HTTP协议性能好并且比基于TCP接口的开发效率高的解决方案呢?答案是肯定的,就是本文接下来要介绍的基于FastCGI的接口开发。

 

之前我写了一篇文章【 PHP: 深入pack/unpack 】介绍了如何在PHP中进行TCP打包和解包,以及通过分离数据层来实现可扩展和性能的提升。但是有时候性能不是衡量的唯一标准,通常需要兼顾性能和开发效率。您可能会说基于HTTP接口的开发效率不错。是的,基于HTTP协议的开发效率很高,而且它适合各种网络环境。但是由于HTTP协议需要发送大量的头部,所以导致性能不是很理想。那么有没有一种比HTTP协议性能好并且比基于TCP接口的开发效率高的解决方案呢?答案是肯定的,就是本文接下来要介绍的基于FastCGI的接口开发。

CGI是什么

CGI 意思为 Common Gateway Interface(公共网关接口),它是一种规范,一种基于浏览器的输入、在Web服务器上运行的程序方法。

FastCGI是什么

FastCGI是对CGI的开放的扩展,它为所有因特网应用提供高性能。

为什么是FastCGI

大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会进程管理,所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。PHP-FPM也是类似的程序,在长时间的发展后,逐渐得到了大家的认可,也越来越流行。最开始的时候PHP-FPM没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的PHP-FPM对内核打补丁,然后再编译。后来PHP内核集成了PHP-FPM之后就方便多了。

那么CGI程序的性能问题在哪呢?PHP解析器每次都会解析php.ini文件,初始化执行环境。标准的CGI对每个请求都会执行这些步骤,所以处理每个时间的时间会比较长。那么FastCGI是怎么做的呢?首先,FastCGI会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是FastCGI的对进程的管理。

FastCGI协议规范

英文版: FastCGI Specification ,中文版: http://www.itcoder.me/?p=235 。本文不打算概括FastCGI的全貌,只是针对需求实现通过POST提交数据到接口。

首先以一张图来大概了解流程:

                                                                                                            图片来自 ITCoder

上图中的webserver称为web服务器,php称为应用。对应我们目前的需求来说,webserver就是client,php就是FastCGI管理进程。本文通篇使用web服务器和应用来描述。

请求由FCGI_BEGIN_REQUEST开始,FCGI_PARAMS表示需要传递环境变量(PHP中的$_SERVER数组就是通过FCGI_PARAMS来传递的,当然您还可以附加自定义的数据)。FCGI_STDIN表示一个输入的开始,比如您需要POST过去的数据。FCGI_STDOUT和FCGI_STDERR标识应用开始响应。FCGI_END_REQUEST表示一次请求的完成,由应用发送。

FastCGI是基于流的协议,并且是8字节对齐,因此不需要考虑字节序,但是要考虑填充。FastCGI的包头是固定的8字节,不同的请求有不同的包体结构。包头和包体组成一个Record(记录)。具体请参考协议规范。下面是Record结构:

1
2
3
4
5
6
7
8
9
10
11
12
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;

 

对此 ,我们可以独立出包头,再结合各种不同的包体,即实现了Record包。但是要注意的是填充和多字节的实现。尤其是在发送名值对参数时有不同的组合方式,需要仔细处理。

先来定义常量。这些常量都是FastCGI规范定义好的。

1
2
3
4
5
6
7
8
define('FCGI_VERSION_1', 1);
define('FCGI_BEGIN_REQUEST', 1);
define('FCGI_RESPONDER', 1);
define('FCGI_END_REQUEST', 3);
define('FCGI_PARAMS', 4);
define('FCGI_STDIN', 5);
define('FCGI_STDOUT', 6);
define('FCGI_STDERR', 7);

 

1
2
3
4
function getHeader($type$requestId$contentLength$paddingLength$reserved=0)
{
    return pack("C2n2C2", FCGI_VERSION_1, $type$requestId$contentLength$paddingLength$reserved);
}

 

填充的计算通过取模就可以了。对于用多个字符来表示单个字符,请进行移位操作,并且起始字节最高位为1。显然如果nameLen或nameValue大于0x7f,则需要4个字节来表示。这里有一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function getNameValue($name$value)
{
    $nameLen  strlen($name);
    $valueLen strlen($value);
    $bin      '';
 
    // 如果大于127,则需要4个字节来存储,下面的$valueLen也需要如此计算
    if ($nameLen > 0x7f)
    {
        // 将$nameLen变成4个无符号字节
        $b0 $nameLen << 24;
        $b1 = ($nameLen << 16) >> 8;
        $b2 = ($nameLen << 8) >> 16;
        $b3 $nameLen >> 24;
        // 将最高位置1,表示采用4个无符号字节表示
        $b3 $b3 | 0x80;
        $bin = pack("C4"$b3$b2$b1$b0);
    }
    else
    {
        $bin = pack("C"$nameLen);
    }
 
    if ($valueLen > 0x7f)
    {
        // 将$nameLen变成4个无符号字节
        $b0 $valueLen << 24;
        $b1 = ($valueLen << 16) >> 8;
        $b2 = ($valueLen << 8) >> 16;
        $b3 $valueLen >> 24;
        // 将最高位置1,表示采用4个无符号字节表示
        $b3 $b3 | 0x80;
        $bin .= pack("C4"$b3$b2$b1$b0);
    }
    else
    {
        $bin .= pack("C"$valueLen);
    }
 
    $bin .= pack("a{$nameLen}a{$valueLen}"$name$value);
 
    return $bin;
}

 

将包头和包体组成Record进行传递,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$env    array(
    'SCRIPT_FILENAME' => FCGI_SCRIPT_FILENAME,
    'REQUEST_METHOD'  => FCGI_REQUEST_METHOD,
    'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
);
 
foreach ($env as $key=>$value)
{
    $body          = getNameValue($key$value);
    $paddingLength = getPaddingLength($body);
    $header        = getHeader(FCGI_PARAMS, FCGI_REQUEST_ID, strlen($body), $paddingLength, 0);
    $record        $header $body . getPaddingData($paddingLength);
    socket_write($sock$record);
}

 

web服务器由STDIN包来结束输入。如果需要使STDIN来传递数据,则仍需要额外发送一个空包体的STDIN包来结束这次请求。之后等待应用返回,具体请参考协议规范关于type的说明。还有一些要说明的事情就是关于对应用的配置使用FCGI_PARAMS来传递,相当于nginx的fastcgi_params配置文件的内容,具体如下:

最后web服务器解析应用返回的响应。github上有一个比较好的实现,大家可以去研究一下。有问题可以一起探讨,PHP-FastCGI-Client 。我这里大概实现了一部分,为了更接近FastCGI协议的流程,代码未作任何优化,也未作任何错误处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<?php
define('FCGI_HOST''127.0.0.1');
define('FCGI_PORT', 9000);
define('FCGI_SCRIPT_FILENAME''/home/goal/fcgiclient/www/test.php');
define('FCGI_REQUEST_METHOD''POST');
define('FCGI_REQUEST_ID', 1);
 
define('FCGI_VERSION_1', 1);
define('FCGI_BEGIN_REQUEST', 1);
define('FCGI_RESPONDER', 1);
define('FCGI_END_REQUEST', 3);
define('FCGI_PARAMS', 4);
define('FCGI_STDIN', 5);
define('FCGI_STDOUT', 6);
define('FCGI_STDERR', 7);
 
function getBeginRequestBody()
{
    return pack("nC6", FCGI_RESPONDER, 0, 0, 0, 0, 0, 0);
}
 
function getHeader($type$requestId$contentLength$paddingLength$reserved=0)
{
    return pack("C2n2C2", FCGI_VERSION_1, $type$requestId$contentLength$paddingLength$reserved);
}
 
function getPaddingLength($body)
{
    $left strlen($body) % 8;
    if ($left == 0)
    {
        return 0;
    }
 
    return (8 - $left);
}
 
function getPaddingData($paddingLength=0)
{
    if ($paddingLength <= 0)
    {
        return '';
    }
    $paddingArray array_fill(0, $paddingLength, 0);
    return call_user_func_array("pack"array_merge(array("C{$paddingLength}"), $paddingArray));
}
 
function getNameValue($name$value)
{
    $nameLen  strlen($name);
    $valueLen strlen($value);
    $bin      '';
 
    // 如果大于127,则需要4个字节来存储,下面的$valueLen也需要如此计算
    if ($nameLen > 0x7f)
    {
        // 将$nameLen变成4个无符号字节
        $b0 $nameLen << 24;
        $b1 = ($nameLen << 16) >> 8;
        $b2 = ($nameLen << 8) >> 16;
        $b3 $nameLen >> 24;
        // 将最高位置1,表示采用4个无符号字节表示
        $b3 $b3 | 0x80;
        $bin = pack("C4"$b3$b2$b1$b0);
    }
    else
    {
        $bin = pack("C"$nameLen);
    }
 
    if ($valueLen > 0x7f)
    {
        // 将$nameLen变成4个无符号字节
        $b0 $valueLen << 24;
        $b1 = ($valueLen << 16) >> 8;
        $b2 = ($valueLen << 8) >> 16;
        $b3 $valueLen >> 24;
        // 将最高位置1,表示采用4个无符号字节表示
        $b3 $b3 | 0x80;
        $bin .= pack("C4"$b3$b2$b1$b0);
    }
    else
    {
        $bin .= pack("C"$valueLen);
    }
 
    $bin .= pack("a{$nameLen}a{$valueLen}"$name$value);
 
    return $bin;
}
 
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($sock, FCGI_HOST, FCGI_PORT);
 
$body   = getBeginRequestBody();
$paddingLength = getPaddingLength($body);
$header = getHeader(FCGI_BEGIN_REQUEST, FCGI_REQUEST_ID, strlen($body), $paddingLength, 0);
$record $header $body . getPaddingData($paddingLength);
socket_write($sock$record);
 
$env    array(
    'SCRIPT_FILENAME' => FCGI_SCRIPT_FILENAME,
    'REQUEST_METHOD'  => FCGI_REQUEST_METHOD,
    'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
);
 
foreach ($env as $key=>$value)
{
    $body          = getNameValue($key$value);
    $paddingLength = getPaddingLength($body);
    $header        = getHeader(FCGI_PARAMS, FCGI_REQUEST_ID, strlen($body), $paddingLength, 0);
    $record        $header $body . getPaddingData($paddingLength);
    socket_write($sock$record);
}
  
 
$body          "";
$paddingLength = getPaddingLength($body);
$header        = getHeader(FCGI_STDIN, FCGI_REQUEST_ID, 0, $paddingLength, 0);
$record        $header $body . getPaddingData($paddingLength);
socket_write($sock$record);
 
$body          "";
$paddingLength = getPaddingLength($body);
$header        = getHeader(FCGI_STDIN, FCGI_REQUEST_ID, 0, $paddingLength, 0);
$record        $header $body . getPaddingData($paddingLength);
socket_write($sock$record);
 
$header = socket_read($sock, 8);
$header = unpack("Cversion/Ctype/nrequestId/ncontentLength/CpaddingLength/Creserved"$header);
print_r($header);
socket_close($sock);
posted @ 2016-04-19 16:42  天涯逐梦  阅读(292)  评论(0编辑  收藏  举报