文档章节

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

陈亦
 陈亦
发布于 2014/01/28 23:34
字数 2246
阅读 5364
收藏 106

之前我写了一篇文章【 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协议的流程,代码未作任何优化,也未作任何错误处理:

<?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);


© 著作权归作者所有

陈亦
粉丝 241
博文 23
码字总数 53194
作品 0
浦东
高级程序员
私信 提问
加载中

评论(6)

钛元素
钛元素
学习
陈亦
陈亦 博主

引用来自“pikeman_ff”的评论

引用来自“陈一回”的评论

引用来自“pikeman_ff”的评论

fastcgi之所以叫fast,是跟传统cgi比较的。总体而言,fastcgi性能不算太高。毕竟多出来一层进程间通信。而且,fastcgi为了显然是继承了cgi的很多缺点,比如,把http请求的头部转换成key=value的形式,低效啊。

fastcgi至少不会像cgi那样每次都初始化环境,而且对于做接口来说,使用http协议要发送大量的头部。而fastcgi可以根据需要发送头部。再者如果使用nginx之类的做转发,中间又多了一层http层啊。本文说的是直接跟php-fpm通信。0

是的。这是fcgi的优点。但毕竟是几年前的东西了。现在有更轻量,高效的东西。另外,你提到的转发,其实有点局限了。你完全可以在tcp层,或者更底层的ip层来转发。

我这个也是可以基于TCP转发的。只是本文没有提到。在前端和php-fpm之前加一层网关,做负载。而且前后都可以用PHP开发。只要写model,开发效率很高。之前我们基于这种架构开发过项目。网关是用C写的。
pikeman_ff
pikeman_ff

引用来自“陈一回”的评论

引用来自“pikeman_ff”的评论

fastcgi之所以叫fast,是跟传统cgi比较的。总体而言,fastcgi性能不算太高。毕竟多出来一层进程间通信。而且,fastcgi为了显然是继承了cgi的很多缺点,比如,把http请求的头部转换成key=value的形式,低效啊。

fastcgi至少不会像cgi那样每次都初始化环境,而且对于做接口来说,使用http协议要发送大量的头部。而fastcgi可以根据需要发送头部。再者如果使用nginx之类的做转发,中间又多了一层http层啊。本文说的是直接跟php-fpm通信。0

是的。这是fcgi的优点。但毕竟是几年前的东西了。现在有更轻量,高效的东西。另外,你提到的转发,其实有点局限了。你完全可以在tcp层,或者更底层的ip层来转发。
陈亦
陈亦 博主

引用来自“pikeman_ff”的评论

fastcgi之所以叫fast,是跟传统cgi比较的。总体而言,fastcgi性能不算太高。毕竟多出来一层进程间通信。而且,fastcgi为了显然是继承了cgi的很多缺点,比如,把http请求的头部转换成key=value的形式,低效啊。

fastcgi至少不会像cgi那样每次都初始化环境,而且对于做接口来说,使用http协议要发送大量的头部。而fastcgi可以根据需要发送头部。再者如果使用nginx之类的做转发,中间又多了一层http层啊。本文说的是直接跟php-fpm通信。0
pikeman_ff
pikeman_ff
fastcgi之所以叫fast,是跟传统cgi比较的。总体而言,fastcgi性能不算太高。毕竟多出来一层进程间通信。而且,fastcgi为了显然是继承了cgi的很多缺点,比如,把http请求的头部转换成key=value的形式,低效啊。
我不叫大脸猫
我不叫大脸猫
既然是说高性能,那么是否有做过性能对比测试呢,能否把测试结果贴一下.
PHP强化之22 - CGI、FastCGI与PHP-FPM

一、简介 在搭建 LAMP/LNMP 服务器时,会经常遇到 PHP-FPM、FastCGI和CGI 这几个概念。如果对它们一知半解,很难搭建出高性能的服务器。接下来我们就以图形方式,解释这些概念之间的关系。 ...

执杖天涯
02/28
8
0
nginx服务器出现504 gateway time-out怎么解决

做网站的同学经常会发现一些nginx服务器访问时候提示504 Gateway Time-out错误,而出现这种错误有两种情况,第一种可能是由于nginx默认的fastcgi进程响应的缓冲区太小造成的, 这将导致fastc...

Code辉
2018/08/28
47
0
深入理解nginx、php通讯机制 FastCGI 协议

nginx 和 php 通讯主要通过 FastCGI 通讯协议。 说起 FastCGI,需要先解释一下 CGI,通用网关接口 (Common Gateway Interface),是 Web Server 与后台语言交互的协议,有了这个协议,开发者可...

angkee
2018/07/26
0
0
全面解读python web 程序的9种部署方式

python有很多web 开发框架,代码写完了,部署上线是个大事,通常来说,web应用一般是三层结构 web server ---->application -----> DB server 主流的web server 一个巴掌就能数出来,apache,...

不必在乎朕是谁
2013/11/22
605
1
nginx的fastcgi模块配置使用记录

一、安装fastcgi模块 1、yum install php-fpm,php-mysql,php-mbstring,php-gd,php-xml php-fpm:PHP FastCGI Process Manager,基于fastcgi协议的php进程管理器。nginx可通过该程序与php完成动......

the_script
2018/12/10
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Giraph源码分析(八)—— 统计每个SuperStep中参与计算的顶点数目

作者|白松 目的:科研中,需要分析在每次迭代过程中参与计算的顶点数目,来进一步优化系统。比如,在SSSP的compute()方法最后一行,都会把当前顶点voteToHalt,即变为InActive状态。所以每次...

数澜科技
今天
2
0
Xss过滤器(Java)

问题 最近旧的系统,遇到Xss安全问题。这个系统采用用的是spring mvc的maven工程。 解决 maven依赖配置 <properties><easapi.version>2.2.0.0</easapi.version></properties><dependenci......

亚林瓜子
今天
7
0
Navicat 快捷键

操作 结果 ctrl+q 打开查询窗口 ctrl+/ 注释sql语句 ctrl+shift +/ 解除注释 ctrl+r 运行查询窗口的sql语句 ctrl+shift+r 只运行选中的sql语句 F6 打开一个mysql命令行窗口 ctrl+l 删除一行 ...

低至一折起
今天
8
0
Set 和 Map

Set 1:基本概念 类数组对象, 内部元素唯一 let set = new Set([1, 2, 3, 2, 1]); console.log(set); // Set(3){ 1, 2, 3 } [...set]; // [1, 2, 3] 接收数组或迭代器对象 ...

凌兮洛
今天
2
0
PyTorch入门笔记一

张量 引入pytorch,生成一个随机的5x3张量 >>> from __future__ import print_function>>> import torch>>> x = torch.rand(5, 3)>>> print(x)tensor([[0.5555, 0.7301, 0.5655],......

仪山湖
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部