fastcgi客户端PHP语言实现 |
发布: 2014-03-19 22:30 |
在一个项目中,希望使用php直接与PHP-FPM进程通信, 跳过nginx代理,减少一点中间过程的效率损耗, 同时,更重要的是把PHP-FPM当作一个PHP进程池使用, 不要受到关于nginx的超时、缓冲设置的影响,让不同处理程序之间通信更直接, 特意使用PHP语言编写了一个fastcgi客户端类,实现协议的打包、发送与接收工作。 在经过一段时间的完善之后,确定这种方式非常适用我们的需求场景, 花了一些时间,重新使用C语言编写了一个中间代理服务, 并集成了一个C版本的fastcgi客户端实现,现在已经不再需要这个PHP版本的了。 现张贴在此,给有兴趣的朋友一个示例,给需要的朋友当作参考, 在实现fastcgi类的过程中,总结下来,需要注意的一些点, 对二进制数据包的封装,像处理C语言中的不同长度的整数,字符串在PHP语言中的处理方式。 对C语言中的变长结构体类型的数据结构,在PHP中不太好表达,需要使用比较原始的分支逻辑来实现, 还可以了解网络协议的封闭数据包,解析数据包的基本模式。 如果用C语言实现,也许可以更简洁明了。 不过fastcgi协议本身非常简单,所以,PHP语言版本的实现也不算太复杂。 直接上代码, [code type="php"] // 内部类不会有重复包含类名冲突问题 class FastCGIClientImpl { const FCGI_HEADER_LEN = 0x08; const FCGI_VERSION_1 = 0x01; // 可用于FCGI_Header的type组件的值 const FCGI_BEGIN_REQUEST = 1; const FCGI_ABORT_REQUEST = 2; const FCGI_END_REQUEST = 3; const FCGI_PARAMS = 4; const FCGI_STDIN = 5; const FCGI_STDOUT = 6; const FCGI_STDERR = 7; const FCGI_DATA = 8; const FCGI_GET_VALUES = 9; const FCGI_GET_VALUES_RESULT = 10; const FCGI_UNKNOWN_TYPE = 11; const FCGI_MAXTYPE = self::FCGI_UNKNOWN_TYPE; // const FCGI_NULL_REQUEST_ID = 0x00; // 可用于FCGI_BeginRequestBody的flags组件的掩码 const FCGI_KEEP_CONN = 1; // 可用于FCGI_BeginRequestBody的role组件的值 const FCGI_RESPONDER = 1; const FCGI_AUTHORIZER = 2; const FCGI_FILTER = 3; // Values for protocolStatus component of FCGI_EndRequestBody const FCGI_REQUEST_COMPLETE = 0; const FCGI_CANT_MPX_CONN = 1; const FCGI_OVERLOADED = 2; const FCGI_UNKNOWN_ROLE = 3; // members public $_sock = null; public $_app_status = 0; public $_fcgi_status_code = -1; public $_stderr_content = ''; public $_stdout_raw_content = ''; public $_stdout_real_content = ''; public $_response = ''; public $_http_status_code = 200; public $_http_status_msg = 'OK'; public $_http_resp_headers = array(); // 构造函数 function __construct() { } // 是否是可添加padding的协议记录类型 function isPadableRecord($appRecordType) { if ($appRecordType == self::FCGI_BEGIN_REQUEST || $appRecordType == self::FCGI_ABORT_REQUEST || $appRecordType == self::FCGI_END_REQUEST || $appRecordType == self::FCGI_STDIN ) { return true; } return false; } // 构造fastcgi头 function fastcgiHeader($appRecordType, $contentLength) { assert($appRecordType >= self::FCGI_BEGIN_REQUEST); assert($contentLength >= 0); $hdr = ''; $hdr .= pack('C', self::FCGI_VERSION_1); // version $hdr .= pack('C', $appRecordType); // type: self::FCGI_BEGIN_REQUEST $hdr .= pack('n', rand(1, 65535)); // rid1,rid0 $hdr .= pack('n', $contentLength); // clen1,clen0 if ($this->isPadableRecord($appRecordType)) { $hdr .= pack('C', rand(0, 255)); // padlen } else { $hdr .= pack('C', 0x00); // padlen } $hdr .= pack('C', 0x00); // reserved assert(strlen($hdr) == self::FCGI_HEADER_LEN); return $hdr; } // Build a FastCGI packet function buildPacket($appRecordType, $content) { $hdr = $this->fastcgiHeader($appRecordType, strlen($content)); $pkt = $hdr . $content; $padtext = ''; $padlen = unpack('C', $hdr{6})[1]; if ($this->isPadableRecord($appRecordType)) { if ($padlen > 0) { $padtext = str_repeat(chr(rand(0, 255)), $padlen); $pkt .= $padtext; assert(strlen($padtext) == $padlen); } // 为什么FastCGIConst:FCGI_PARAMS加padding后协议有问题 } assert(strlen($pkt) == ($padlen + strlen($content) + self::FCGI_HEADER_LEN)); return $pkt; } // Build an FastCGI Name value pair function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); $nvpair = ''; $nvpair .= $nlen < 128 ? pack('C', $nlen) : pack('N', $nlen | (0x01 << 31)); $nvpair .= $vlen < 128 ? pack('C', $vlen) : pack('N', $vlen | (0x01 << 31)); assert(strlen($nvpair) == 2 || strlen($nvpair) == 5 || strlen($nvpair) == 8); $nvpair .= $name . $value; return $nvpair; } // 构造协议开始请求记录包 function buildBeginRequest() { $beginRequestBody = pack('n', self::FCGI_RESPONDER) // role1,role0 . pack('CNC', 0x00, rand(), 0x00) // flag, reserved5 ; assert(strlen($beginRequestBody) == 8); $pkt = $this->buildPacket(self::FCGI_BEGIN_REQUEST, $beginRequestBody); $padlen = unpack('C', $pkt{6})[1]; assert(strlen($pkt) == (self::FCGI_HEADER_LEN * 2 + $padlen)); return $pkt; } // 构造协议中断请求记录包 function buildAbortRequest() { $pkt = $this->buildPacket(self::FCGI_ABORT_REQUEST, ''); return $pkt; } // 构造协议参数请求记录包 function buildParamsRequest(array $params) { $pstr = ''; foreach ($params as $name => $value) { $pstr .= $this->buildNvpair($name, $value); } $pkt = ''; $pkt .= $this->buildPacket(self::FCGI_PARAMS, $pstr); $pkt .= $this->buildPacket(self::FCGI_PARAMS, ''); // 为什么需要构建一个空包 return $pkt; } // 构造协议标准输入请求记录包 function buildStdinRequest($stdin) { $pkt = $this->buildPacket(self::FCGI_STDIN, $stdin); $pkt .= $this->buildPacket(self::FCGI_STDIN, ''); // 为什么需要构建一个空包 return $pkt; } // 构造协议结束请求记录包 function buildEndRequest() { $body = pack('N', 301) . pack('C', 0x01) . pack('C*', 0x00, 0x00, 0x00) ; assert(strlen($body) == self::FCGI_HEADER_LEN); $pkt = $this->buildPacket(self::FCGI_END_REQUEST, $body); return $pkt; } /** * Decode a FastCGI Packet * * @param String $data String containing all the packet * @return array */ function decodePacketHeader($data) { $ret = array(); $ret['version'] = unpack('C', $data{0})[1]; $ret['type'] = unpack('C', $data{1})[1]; $ret['requestId'] = unpack('n', substr($data, 2, 2))[1]; $ret['contentLength'] = unpack('n', substr($data, 4, 2))[1]; $ret['paddingLength'] = unpack('C', $data{6})[1]; $ret['reserved'] = unpack('C', $data{7})[1]; return $ret; } /** * Read a FastCGI Packet * * @return array */ function readPacket() { if ($packet = fread($this->_sock, self::FCGI_HEADER_LEN)) { $resp = $this->decodePacketHeader($packet); $resp['content'] = ''; if ($resp['contentLength']) { $len = $resp['contentLength']; while ($len && $buf = fread($this->_sock, $len)) { $len -= strlen($buf); $resp['content'] .= $buf; $buf = ''; } } if ($resp['paddingLength']) { $buf = fread($this->_sock, $resp['paddingLength']); } return $resp; } else { return false; } } // 连接到FastCGI服务器 function connect($host, $port) { $fp = null; if ($this->_sock == null) { $fp = fsockopen($host, $port); if (!$fp) { return false; } } $this->_sock = $fp; assert($this->_sock != null); return true; } // 断开到FastCGI服务器的连接 function disconnect() { if (is_resource($this->_sock)) { fclose($this->_sock); $this->_sock = null; } } // 解析HTTP响应头信息 function parseHttpHeader($stdout) { $hdr_end_pos = strpos($stdout, "\r\n\r\n"); if ($hdr_end_pos < 0) { return false; } $raw_hdr = explode("\r\n", substr($stdout, 0, $hdr_end_pos)); foreach ($raw_hdr as $lineno => $row) { $kvpair = explode(': ', $row); if ($lineno == 0 && $kvpair[0] == 'Status') { $this->_http_status_code = explode(' ', trim($kvpair[1]))[0]; $this->_http_status_msg = substr($kvpair[1], strlen($this->_http_status_code)+1); $this->_http_status_msg = trim($this->_http_status_msg); } else { $this->_http_resp_headers[$kvpair[0]] = $kvpair[1]; } } $this->_stdout_real_content = substr($stdout, $hdr_end_pos + 4); return true; } // 比较全面的测试方法 public function requestFullTest(array $params, $stdin) { $fp = fsockopen('127.0.0.1', 9000); var_dump($fp); $this->_sock = $fp; $breq = $this->buildBeginRequest(); $data = $breq; $str = $this->buildParamsRequest($params); $data .= $str; $str = $this->buildStdinRequest($stdin); $data .= $str; $str = $this->buildAbortRequest(); $data .= $str; $str = $this->buildEndRequest(); $data .= $str; $ret = fwrite($fp, $data, strlen($data)); // var_dump($ret); // $res = fread($fp, 3000); // $res = $readPacket(); $response = ''; $stdout_content = ''; $stderr_content = ''; $cnter = 0; do { $btime = microtime(true); $resp = $this->readPacket(); $now = microtime(true); $dtime = $now - $btime; echo "read a pkt: {$cnter} on {$now}, used: {$dtime}\n"; $cnter++; if ($resp['type'] == self::FCGI_STDOUT || $resp['type'] == self::FCGI_STDERR) { $response .= $resp['content']; } if ($resp['type'] == self::FCGI_STDOUT) { $stdout_content .= $resp['content']; } if ($resp['type'] == self::FCGI_STDERR) { $stderr_content .= $resp['content']; } } while ($resp && $resp['type'] != self::FCGI_END_REQUEST); print_r($resp); var_dump("response={$response}", "stdout={$stdout_content}", "stderr={$stderr_content}"); sleep(5); fclose($fp); return true; } // 对外执行fastcgi调用的方法 function request(array $params, $stdin) { $breq = $this->buildBeginRequest(); $data = $breq; $str = $this->buildParamsRequest($params); $data .= $str; $str = $this->buildStdinRequest($stdin); $data .= $str; $ret = fwrite($this->_sock, $data, strlen($data)); if (!$ret) { assert($ret == strlen($data)); return false; } $resp = null; $cnter = 0; do { $btime = microtime(true); $resp = $this->readPacket(); $now = microtime(true); $dtime = $now - $btime; echo "read a pkt: {$cnter} on {$now}, used: {$dtime}\n"; $cnter++; if ($resp['type'] == self::FCGI_STDOUT || $resp['type'] == self::FCGI_STDERR) { $this->_response .= $resp['content']; } if ($resp['type'] == self::FCGI_STDOUT) { $this->_stdout_raw_content .= $resp['content']; } if ($resp['type'] == self::FCGI_STDERR) { $this->_stderr_content .= $resp['content']; } } while ($resp && $resp['type'] != self::FCGI_END_REQUEST); if (!$resp) { return false; } assert(strlen($resp['content']) == self::FCGI_HEADER_LEN); $this->_app_status = unpack('N', substr($resp['content'], 0, 4))[1]; $this->_fcgi_status = unpack('C', $resp['content']{4})[1]; return true; } }; // end class FastCGIClientImpl ?> [/code] 测试代码: [code type="php"] $client = new FastCGIClientImpl(); $content = 'key123=value456&keyabc=valueefggg&中文=abcdefg&hehe=汉字utf8的'; $res = $client->__invoke( array( 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 'REQUEST_METHOD' => 'POST', 'DOCUMENT_ROOT' => '/data1/vhosts/photo.house.kitech.com.cn', 'SCRIPT_FILENAME' => '/data1/vhosts/photo.house.kitech.com.cn/index.php', 'SCRIPT_NAME' => '/index.php', 'REQUEST_URI' => '/test/test6/simpost', 'SERVER_SOFTWARE' => 'php/fastcgiclient', 'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_PORT' => '9985', 'SERVER_ADDR' => '127.0.0.1', 'SERVER_PORT' => '80', 'SERVER_NAME' => 'photo.house.kitech.com.cn', 'HTTP_HOST' => 'photo.house.kitech.com.cn', 'SERVER_PROTOCOL' => 'HTTP/1.0', 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 'CONTENT_LENGTH' => strlen($content), 'kitech.com.cn_CACHE_DIR' => '', 'kitech.com.cn_DATA_DIR' => '', 'kitech.com.cn_RSYNC_SERVER' => '', 'kitech.com.cn_STORAGE_SERVER' => '', 'kitech.com.cn_RSYNC_MODULES' => '', 'kitech.com.cn_RESOURCE_URL' => '', 'kitech.com.cn_DIST_URL' => '', 'kitech.com.cn_TAAA_127' => 'DallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDall123456789012345', 'kitech.com.cn_TAAA_128' => 'DallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDall1234567890123456', ), $content ); var_dump($res); [/code] 通过代码,还可以看出,跳过nginx后,可以动态设置程序的DOCUMENT_ROOT目录, 能够实现一个比用nginx配置虚拟主机更快速灵活地方式,请求不同虚拟项目的程序接口。 参考资料: http://www.fastcgi.com |
原文: http://qtchina.tk/?q=node/795 |
Powered by zexport
|