Jump to Navigation

fastcgi客户端PHP语言实现

在一个项目中,希望使用php直接与PHP-FPM进程通信,
跳过nginx代理,减少一点中间过程的效率损耗,
同时,更重要的是把PHP-FPM当作一个PHP进程池使用,
不要受到关于nginx的超时、缓冲设置的影响,让不同处理程序之间通信更直接,
特意使用PHP语言编写了一个fastcgi客户端类,实现协议的打包、发送与接收工作。

在经过一段时间的完善之后,确定这种方式非常适用我们的需求场景,
花了一些时间,重新使用C语言编写了一个中间代理服务,
并集成了一个C版本的fastcgi客户端实现,现在已经不再需要这个PHP版本的了。

现张贴在此,给有兴趣的朋友一个示例,给需要的朋友当作参考,
在实现fastcgi类的过程中,总结下来,需要注意的一些点,
对二进制数据包的封装,像处理C语言中的不同长度的整数,字符串在PHP语言中的处理方式。
对C语言中的变长结构体类型的数据结构,在PHP中不太好表达,需要使用比较原始的分支逻辑来实现,
还可以了解网络协议的封闭数据包,解析数据包的基本模式。

如果用C语言实现,也许可以更简洁明了。
不过fastcgi协议本身非常简单,所以,PHP语言版本的实现也不算太复杂。

直接上代码,

  1. <?php
  2.  
  3. // 内部类不会有重复包含类名冲突问题
  4. class FastCGIClientImpl
  5. {
  6. const FCGI_HEADER_LEN = 0x08;
  7. const FCGI_VERSION_1 = 0x01;
  8.  
  9. // 可用于FCGI_Header的type组件的值
  10. const FCGI_BEGIN_REQUEST = 1;
  11. const FCGI_ABORT_REQUEST = 2;
  12. const FCGI_END_REQUEST = 3;
  13. const FCGI_PARAMS = 4;
  14. const FCGI_STDIN = 5;
  15. const FCGI_STDOUT = 6;
  16. const FCGI_STDERR = 7;
  17. const FCGI_DATA = 8;
  18. const FCGI_GET_VALUES = 9;
  19. const FCGI_GET_VALUES_RESULT = 10;
  20. const FCGI_UNKNOWN_TYPE = 11;
  21. const FCGI_MAXTYPE = self::FCGI_UNKNOWN_TYPE;
  22.  
  23.  
  24. //
  25. const FCGI_NULL_REQUEST_ID = 0x00;
  26.  
  27. // 可用于FCGI_BeginRequestBody的flags组件的掩码
  28. const FCGI_KEEP_CONN = 1;
  29. // 可用于FCGI_BeginRequestBody的role组件的值
  30. const FCGI_RESPONDER = 1;
  31. const FCGI_AUTHORIZER = 2;
  32. const FCGI_FILTER = 3;
  33.  
  34. // Values for protocolStatus component of FCGI_EndRequestBody
  35. const FCGI_REQUEST_COMPLETE = 0;
  36. const FCGI_CANT_MPX_CONN = 1;
  37. const FCGI_OVERLOADED = 2;
  38. const FCGI_UNKNOWN_ROLE = 3;
  39.  
  40.  
  41. // members
  42. public $_sock = null;
  43. public $_app_status = 0;
  44. public $_fcgi_status_code = -1;
  45. public $_stderr_content = '';
  46. public $_stdout_raw_content = '';
  47. public $_stdout_real_content = '';
  48. public $_response = '';
  49. public $_http_status_code = 200;
  50. public $_http_status_msg = 'OK';
  51. public $_http_resp_headers = array();
  52.  
  53.  
  54. // 构造函数
  55. function __construct()
  56. {
  57. }
  58.  
  59. // 是否是可添加padding的协议记录类型
  60. function isPadableRecord($appRecordType)
  61. {
  62. if ($appRecordType == self::FCGI_BEGIN_REQUEST
  63. || $appRecordType == self::FCGI_ABORT_REQUEST
  64. || $appRecordType == self::FCGI_END_REQUEST
  65. || $appRecordType == self::FCGI_STDIN
  66. ) {
  67. return true;
  68. }
  69.  
  70. return false;
  71. }
  72.  
  73. // 构造fastcgi头
  74. function fastcgiHeader($appRecordType, $contentLength)
  75. {
  76. assert($appRecordType >= self::FCGI_BEGIN_REQUEST);
  77. assert($contentLength >= 0);
  78.  
  79. $hdr = '';
  80. $hdr .= pack('C', self::FCGI_VERSION_1); // version
  81. $hdr .= pack('C', $appRecordType); // type: self::FCGI_BEGIN_REQUEST
  82.  
  83. $hdr .= pack('n', rand(1, 65535)); // rid1,rid0
  84.  
  85. $hdr .= pack('n', $contentLength); // clen1,clen0
  86.  
  87. if ($this->isPadableRecord($appRecordType)) {
  88. $hdr .= pack('C', rand(0, 255)); // padlen
  89. } else {
  90. $hdr .= pack('C', 0x00); // padlen
  91. }
  92. $hdr .= pack('C', 0x00); // reserved
  93.  
  94. assert(strlen($hdr) == self::FCGI_HEADER_LEN);
  95.  
  96. return $hdr;
  97. }
  98.  
  99. // Build a FastCGI packet
  100. function buildPacket($appRecordType, $content)
  101. {
  102. $hdr = $this->fastcgiHeader($appRecordType, strlen($content));
  103. $pkt = $hdr . $content;
  104.  
  105. $padtext = '';
  106. $padlen = unpack('C', $hdr{6})[1];
  107. if ($this->isPadableRecord($appRecordType)) {
  108. if ($padlen > 0) {
  109. $padtext = str_repeat(chr(rand(0, 255)), $padlen);
  110. $pkt .= $padtext;
  111. assert(strlen($padtext) == $padlen);
  112. }
  113.  
  114. // 为什么FastCGIConst:FCGI_PARAMS加padding后协议有问题
  115. }
  116.  
  117. assert(strlen($pkt) == ($padlen + strlen($content) + self::FCGI_HEADER_LEN));
  118. return $pkt;
  119. }
  120.  
  121. // Build an FastCGI Name value pair
  122. function buildNvpair($name, $value)
  123. {
  124. $nlen = strlen($name);
  125. $vlen = strlen($value);
  126.  
  127. $nvpair = '';
  128. $nvpair .= $nlen < 128 ? pack('C', $nlen) : pack('N', $nlen | (0x01 << 31));
  129. $nvpair .= $vlen < 128 ? pack('C', $vlen) : pack('N', $vlen | (0x01 << 31));
  130.  
  131. assert(strlen($nvpair) == 2 || strlen($nvpair) == 5 || strlen($nvpair) == 8);
  132.  
  133. $nvpair .= $name . $value;
  134. return $nvpair;
  135. }
  136.  
  137. // 构造协议开始请求记录包
  138. function buildBeginRequest()
  139. {
  140. $beginRequestBody = pack('n', self::FCGI_RESPONDER) // role1,role0
  141. . pack('CNC', 0x00, rand(), 0x00) // flag, reserved5
  142. ;
  143. assert(strlen($beginRequestBody) == 8);
  144.  
  145. $pkt = $this->buildPacket(self::FCGI_BEGIN_REQUEST, $beginRequestBody);
  146. $padlen = unpack('C', $pkt{6})[1];
  147. assert(strlen($pkt) == (self::FCGI_HEADER_LEN * 2 + $padlen));
  148.  
  149. return $pkt;
  150. }
  151.  
  152. // 构造协议中断请求记录包
  153. function buildAbortRequest()
  154. {
  155. $pkt = $this->buildPacket(self::FCGI_ABORT_REQUEST, '');
  156. return $pkt;
  157. }
  158.  
  159. // 构造协议参数请求记录包
  160. function buildParamsRequest(array $params)
  161. {
  162. $pstr = '';
  163. foreach ($params as $name => $value) {
  164. $pstr .= $this->buildNvpair($name, $value);
  165. }
  166.  
  167. $pkt = '';
  168. $pkt .= $this->buildPacket(self::FCGI_PARAMS, $pstr);
  169. $pkt .= $this->buildPacket(self::FCGI_PARAMS, ''); // 为什么需要构建一个空包
  170.  
  171. return $pkt;
  172. }
  173.  
  174. // 构造协议标准输入请求记录包
  175. function buildStdinRequest($stdin)
  176. {
  177. $pkt = $this->buildPacket(self::FCGI_STDIN, $stdin);
  178. $pkt .= $this->buildPacket(self::FCGI_STDIN, ''); // 为什么需要构建一个空包
  179. return $pkt;
  180. }
  181.  
  182. // 构造协议结束请求记录包
  183. function buildEndRequest()
  184. {
  185. $body = pack('N', 301)
  186. . pack('C', 0x01)
  187. . pack('C*', 0x00, 0x00, 0x00)
  188. ;
  189. assert(strlen($body) == self::FCGI_HEADER_LEN);
  190.  
  191. $pkt = $this->buildPacket(self::FCGI_END_REQUEST, $body);
  192.  
  193. return $pkt;
  194. }
  195.  
  196. /**
  197.   * Decode a FastCGI Packet
  198.   *
  199.   * @param String $data String containing all the packet
  200.   * @return array
  201.   */
  202. function decodePacketHeader($data)
  203. {
  204. $ret = array();
  205. $ret['version'] = unpack('C', $data{0})[1];
  206. $ret['type'] = unpack('C', $data{1})[1];
  207. $ret['requestId'] = unpack('n', substr($data, 2, 2))[1];
  208. $ret['contentLength'] = unpack('n', substr($data, 4, 2))[1];
  209. $ret['paddingLength'] = unpack('C', $data{6})[1];
  210. $ret['reserved'] = unpack('C', $data{7})[1];
  211. return $ret;
  212. }
  213.  
  214. /**
  215.   * Read a FastCGI Packet
  216.   *
  217.   * @return array
  218.   */
  219. function readPacket()
  220. {
  221. if ($packet = fread($this->_sock, self::FCGI_HEADER_LEN)) {
  222. $resp = $this->decodePacketHeader($packet);
  223. $resp['content'] = '';
  224. if ($resp['contentLength']) {
  225. $len = $resp['contentLength'];
  226. while ($len && $buf = fread($this->_sock, $len)) {
  227. $len -= strlen($buf);
  228. $resp['content'] .= $buf;
  229. $buf = '';
  230. }
  231. }
  232. if ($resp['paddingLength']) {
  233. $buf = fread($this->_sock, $resp['paddingLength']);
  234. }
  235. return $resp;
  236. } else {
  237. return false;
  238. }
  239. }
  240.  
  241. // 连接到FastCGI服务器
  242. function connect($host, $port)
  243. {
  244. $fp = null;
  245. if ($this->_sock == null) {
  246. $fp = fsockopen($host, $port);
  247. if (!$fp) {
  248. return false;
  249. }
  250. }
  251. $this->_sock = $fp;
  252. assert($this->_sock != null);
  253. return true;
  254. }
  255.  
  256. // 断开到FastCGI服务器的连接
  257. function disconnect()
  258. {
  259. if (is_resource($this->_sock)) {
  260. fclose($this->_sock);
  261. $this->_sock = null;
  262. }
  263. }
  264.  
  265. // 解析HTTP响应头信息
  266. function parseHttpHeader($stdout)
  267. {
  268. $hdr_end_pos = strpos($stdout, "\r\n\r\n");
  269. if ($hdr_end_pos < 0) {
  270. return false;
  271. }
  272.  
  273. $raw_hdr = explode("\r\n", substr($stdout, 0, $hdr_end_pos));
  274. foreach ($raw_hdr as $lineno => $row) {
  275. $kvpair = explode(': ', $row);
  276. if ($lineno == 0 && $kvpair[0] == 'Status') {
  277. $this->_http_status_code = explode(' ', trim($kvpair[1]))[0];
  278. $this->_http_status_msg = substr($kvpair[1], strlen($this->_http_status_code)+1);
  279. $this->_http_status_msg = trim($this->_http_status_msg);
  280. } else {
  281. $this->_http_resp_headers[$kvpair[0]] = $kvpair[1];
  282. }
  283. }
  284.  
  285. $this->_stdout_real_content = substr($stdout, $hdr_end_pos + 4);
  286.  
  287. return true;
  288. }
  289.  
  290. // 比较全面的测试方法
  291. public function requestFullTest(array $params, $stdin)
  292. {
  293. $fp = fsockopen('127.0.0.1', 9000);
  294. var_dump($fp);
  295. $this->_sock = $fp;
  296.  
  297. $breq = $this->buildBeginRequest();
  298. $data = $breq;
  299.  
  300. $str = $this->buildParamsRequest($params);
  301. $data .= $str;
  302.  
  303. $str = $this->buildStdinRequest($stdin);
  304. $data .= $str;
  305.  
  306. $str = $this->buildAbortRequest();
  307. $data .= $str;
  308.  
  309. $str = $this->buildEndRequest();
  310. $data .= $str;
  311.  
  312. $ret = fwrite($fp, $data, strlen($data));
  313. // var_dump($ret);
  314.  
  315. // $res = fread($fp, 3000);
  316. // $res = $readPacket();
  317. $response = '';
  318. $stdout_content = '';
  319. $stderr_content = '';
  320. $cnter = 0;
  321. do {
  322. $btime = microtime(true);
  323. $resp = $this->readPacket();
  324. $now = microtime(true);
  325. $dtime = $now - $btime;
  326. echo "read a pkt: {$cnter} on {$now}, used: {$dtime}\n"; $cnter++;
  327. if ($resp['type'] == self::FCGI_STDOUT || $resp['type'] == self::FCGI_STDERR) {
  328. $response .= $resp['content'];
  329. }
  330. if ($resp['type'] == self::FCGI_STDOUT) {
  331. $stdout_content .= $resp['content'];
  332. }
  333. if ($resp['type'] == self::FCGI_STDERR) {
  334. $stderr_content .= $resp['content'];
  335. }
  336. } while ($resp && $resp['type'] != self::FCGI_END_REQUEST);
  337.  
  338. print_r($resp);
  339. var_dump("response={$response}", "stdout={$stdout_content}", "stderr={$stderr_content}");
  340. sleep(5);
  341.  
  342. fclose($fp);
  343. return true;
  344. }
  345.  
  346.  
  347. // 对外执行fastcgi调用的方法
  348. function request(array $params, $stdin)
  349. {
  350. $breq = $this->buildBeginRequest();
  351. $data = $breq;
  352.  
  353. $str = $this->buildParamsRequest($params);
  354. $data .= $str;
  355.  
  356. $str = $this->buildStdinRequest($stdin);
  357. $data .= $str;
  358.  
  359. $ret = fwrite($this->_sock, $data, strlen($data));
  360. if (!$ret) {
  361. assert($ret == strlen($data));
  362. return false;
  363. }
  364.  
  365. $resp = null;
  366. $cnter = 0;
  367. do {
  368. $btime = microtime(true);
  369. $resp = $this->readPacket();
  370. $now = microtime(true);
  371. $dtime = $now - $btime;
  372. echo "read a pkt: {$cnter} on {$now}, used: {$dtime}\n"; $cnter++;
  373. if ($resp['type'] == self::FCGI_STDOUT || $resp['type'] == self::FCGI_STDERR) {
  374. $this->_response .= $resp['content'];
  375. }
  376. if ($resp['type'] == self::FCGI_STDOUT) {
  377. $this->_stdout_raw_content .= $resp['content'];
  378. }
  379. if ($resp['type'] == self::FCGI_STDERR) {
  380. $this->_stderr_content .= $resp['content'];
  381. }
  382. } while ($resp && $resp['type'] != self::FCGI_END_REQUEST);
  383.  
  384. if (!$resp) {
  385. return false;
  386. }
  387.  
  388. assert(strlen($resp['content']) == self::FCGI_HEADER_LEN);
  389. $this->_app_status = unpack('N', substr($resp['content'], 0, 4))[1];
  390. $this->_fcgi_status = unpack('C', $resp['content']{4})[1];
  391.  
  392. return true;
  393. }
  394.  
  395. }; // end class FastCGIClientImpl
  396. ?>

测试代码:

  1. $client = new FastCGIClientImpl();
  2. $content = 'key123=value456&keyabc=valueefggg&中文=abcdefg&hehe=汉字utf8的';
  3. $res = $client->__invoke(
  4. 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
  5. 'REQUEST_METHOD' => 'POST',
  6. 'DOCUMENT_ROOT' => '/data1/vhosts/photo.house.kitech.com.cn',
  7. 'SCRIPT_FILENAME' => '/data1/vhosts/photo.house.kitech.com.cn/index.php',
  8. 'SCRIPT_NAME' => '/index.php',
  9. 'REQUEST_URI' => '/test/test6/simpost',
  10. 'SERVER_SOFTWARE' => 'php/fastcgiclient',
  11. 'REMOTE_ADDR' => '127.0.0.1',
  12. 'REMOTE_PORT' => '9985',
  13. 'SERVER_ADDR' => '127.0.0.1',
  14. 'SERVER_PORT' => '80',
  15. 'SERVER_NAME' => 'photo.house.kitech.com.cn',
  16. 'HTTP_HOST' => 'photo.house.kitech.com.cn',
  17. 'SERVER_PROTOCOL' => 'HTTP/1.0',
  18. 'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
  19. 'CONTENT_LENGTH' => strlen($content),
  20. 'kitech.com.cn_CACHE_DIR' => '',
  21. 'kitech.com.cn_DATA_DIR' => '',
  22. 'kitech.com.cn_RSYNC_SERVER' => '',
  23. 'kitech.com.cn_STORAGE_SERVER' => '',
  24. 'kitech.com.cn_RSYNC_MODULES' => '',
  25. 'kitech.com.cn_RESOURCE_URL' => '',
  26. 'kitech.com.cn_DIST_URL' => '',
  27. 'kitech.com.cn_TAAA_127' => 'DallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDall123456789012345',
  28. 'kitech.com.cn_TAAA_128' => 'DallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDallasDall1234567890123456',
  29. ),
  30. $content
  31. );
  32.  
  33. var_dump($res);

通过代码,还可以看出,跳过nginx后,可以动态设置程序的DOCUMENT_ROOT目录,
能够实现一个比用nginx配置虚拟主机更快速灵活地方式,请求不同虚拟项目的程序接口。

参考资料:
http://www.fastcgi.com

Category:

添加新评论

Plain text

  • 不允许HTML标记。
  • 自动将网址与电子邮件地址转变为链接。
  • 自动断行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut