侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

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

2022-07-01 星期五 / 0 评论 / 0 点赞 / 103 阅读 / 14368 字

之前我写了一篇文章【PHP: 深入pack/unpack】介绍了如何在PHP中进行TCP打包和解包,以及通过分离数据层来实现可扩展和性能的提升。但是有时候性能不是衡量的唯一标准,通常需要兼顾性能和开发

之前我写了一篇文章【 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结构:

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规范定义好的。

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 getHeader($type, $requestId, $contentLength, $paddingLength, $reserved=0){	return pack("C2n2C2", FCGI_VERSION_1, $type, $requestId, $contentLength, $paddingLength, $reserved);}

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

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进行传递,比如:

$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协议的流程,代码未作任何优化,也未作任何错误处理:

<?phpdefine('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);


广告 广告

评论区