素のPHPからfast cgiを呼び出す(PHP Fast CGI Client)

こちらで、http接続を閉じた後も処理を続行する方法を調べました。
PHP クライアントに応答した後、http connectionを閉じて重い処理を継続する

これがうまく動作しない場合があり、しょうがなく

  1. exec("nohup php background.php > /dev/null &");


でバックグランド実行していたのですが、毎回PHPプロセスを起動するので負荷が高い。

せっかくphp-fpmサービスを動作させているのだから、
PHPからphp-fpm(fcgi)を呼び出して処理を継続できないか。


2019/2/11 追記
こちらでソースを公開しています。
簡単なPHP Fast CGI Client




Fast CGI Client



FastCGIの仕様はこちら。
FastCGI Specification

さっぱりわからんなーと思っていたら、JavaでFast CGI Clientを
実装されている方がいました。
JavaのゆるいFast CGI Client

こちらを参考にPHPを実装してみます。


実装



試行錯誤した結果の実装はこちら。
php-fpmは「/var/run/php/php7.2-fpm.sock」ソケットをリッスンしている設定です。

sock.phpと同じ階層にある「call.php」を呼び出します。

・sock.php


  1. <?php
  2. // http://saburi380.blogspot.com/2014/11/javafast-cgi-client.html
  3. class FCGIConnection {
  4.     //
  5.     // 8. Types and Constants
  6.     //
  7.     const FCGI_VERSION_1 = 1;
  8.     const FCGI_BEGIN_REQUEST = 1;
  9.     const FCGI_ABORT_REQUEST = 2;
  10.     const FCGI_END_REQUEST = 3;
  11.     const FCGI_PARAMS = 4;
  12.     const FCGI_STDIN = 5;
  13.     const FCGI_STDOUT = 6;
  14.     const FCGI_STDERR = 7;
  15.     const FCGI_DATA = 8;
  16.     const FCGI_GET_VALUES = 9;
  17.     const FCGI_GET_VALUES_RESULT = 10;
  18.     const FCGI_UNKNOWN_TYPE = 11;
  19.     const FCGI_MAXTYPE = self::FCGI_UNKNOWN_TYPE;
  20.     const FCGI_KEEP_CONN = 1;
  21.     const FCGI_RESPONDER = 1;
  22.     const FCGI_AUTHORIZER = 2;
  23.     const FCGI_FILTER = 3;
  24.     private $_socket;
  25.     private $_stream;
  26.     public function __construct() {
  27.         
  28.         
  29.         $this->_socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
  30.         $ret = socket_connect($this->_socket, '/var/run/php/php7.2-fpm.sock');
  31.         if ($ret === false) {
  32.             $this->_log('open error.');
  33.         } else {
  34.             $this->_log('open success.');
  35.         }
  36.         
  37.         /*
  38.         $this->_socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  39.         $ret = socket_connect($this->_socket, '127.0.0.1', 9000);
  40.         if ($ret === false) {
  41.             $this->_log('connect error.');
  42.         } else {
  43.             $this->_log('connect success.');
  44.         }
  45.         */
  46.     }
  47.     //
  48.     // 5. Application Record Types
  49.     //
  50.     //
  51.     // 5.1 FCGI_BEGIN_REQUEST
  52.     //
  53.     public function sendBeginRequest($requestID, $keepalive) {
  54.         $this->_stream = '';
  55.         $this->sendRecordHeader($requestID, self::FCGI_BEGIN_REQUEST, 8);
  56.         $role = self::FCGI_RESPONDER;
  57.         $stream = '';
  58.         $stream .= chr($role >> 8);
  59.         $stream .= chr($role);
  60.         $stream .= $keepalive ? chr(self::FCGI_KEEP_CONN) : chr(0);
  61.     
  62.         for($i = 0; $i < 5; $i++) {
  63.             $stream .= chr(0); // padding = 5
  64.         }
  65.         $this->_send($stream);
  66.     }
  67.     //
  68.     // 5.2 Name-Value Pair Stream: FCGI_PARAMS
  69.     //
  70.     public function sendParams($requestID, $params) {
  71.         $intParamsLength = 0;
  72.         foreach($params as $key => $value) {
  73.             $intParamsLength += $this->_calcParamLength($key, $value);
  74.         }
  75.         $this->_log("FCGI_PARAMS: length=" . $intParamsLength);
  76.         $pad = $this->sendRecordHeader($requestID, self::FCGI_PARAMS, $intParamsLength);
  77.         foreach($params as $key => $value) {
  78.             $this->_sendParam($requestID, $key, $value);
  79.         }
  80.         for($i = 0; $i < $pad; $i++) {
  81.             $this->_send(chr(0));
  82.         }
  83.         $this->flush();
  84.     }
  85.     //
  86.     // 3. Protocol Basics
  87.     //
  88.     //
  89.     // 3.3 Records (All data is carried in "records")
  90.     //
  91.     public function sendRecordHeader($requestID, $recordType, $contentLength) {
  92.         $intPaddingLength = $contentLength % 8;
  93.         if($intPaddingLength != 0) {
  94.             $intPaddingLength = (8 - $intPaddingLength);
  95.         }
  96.         $this->_log(" Record Header: " . $recordType . ", pad=" . $intPaddingLength);
  97.         $stream = '';
  98.         $stream .= chr(self::FCGI_VERSION_1);
  99.         $stream .= chr($recordType);
  100.         $stream .= chr($requestID >> 8);
  101.         $stream .= chr($requestID);
  102.         $stream .= chr($contentLength >> 8);
  103.         $stream .= chr($contentLength);
  104.         $stream .= chr($intPaddingLength); // paddingLength
  105.         $stream .= chr(0); // reserved
  106.         $this->_send($stream);
  107.         return $intPaddingLength;
  108.     }
  109.     //
  110.     // 3.4 Name-Value Pairs
  111.     //
  112.     private function _calcParamLength($name, $value){
  113.         if (empty($name) || empty($value)) {
  114.             return 0;
  115.         }
  116.         $nameLength = strlen($name);
  117.         $valueLength = strlen($value);
  118.         if($nameLength < 0x80){
  119.             if($valueLength < 0x80){
  120.                 // FCGI_NameValuePair11
  121.                 return $nameLength + $valueLength + 2;
  122.             }else{
  123.                 // FCGI_NameValuePair14
  124.                 return $nameLength + $valueLength + 5;
  125.             }
  126.         }else{
  127.             if($valueLength < 0x80){
  128.                 // FCGI_NameValuePair41
  129.                 return $nameLength + $valueLength + 5;
  130.             }else{
  131.                 // FCGI_NameValuePair44
  132.                 return $nameLength + $valueLength + 8;
  133.             }
  134.         }
  135.     }
  136.     public function _sendParam($requestID, $name, $value) {
  137.         if (empty($name) || empty($value)) {
  138.             return;
  139.         }
  140.     
  141.         $nameLength = strlen($name);
  142.         $valueLength = strlen($value);
  143.         $stream = '';
  144.     
  145.         if($nameLength < 0x80){
  146.             if($valueLength < 0x80){
  147.                 // FCGI_NameValuePair11
  148.                 $stream .= chr($nameLength);
  149.                 $stream .= chr($valueLength);
  150.             }else{
  151.                 // FCGI_NameValuePair14
  152.                 $stream .= chr($nameLength);
  153.                 $stream .= chr(0x80 | $valueLength >> 24);
  154.                 $stream .= chr($valueLength >> 16);
  155.                 $stream .= chr($valueLength >> 8);
  156.                 $stream .= chr($valueLength);
  157.             }
  158.         }else{
  159.             if($valueLength < 0x80){
  160.                 // FCGI_NameValuePair41
  161.                 $stream .= chr(0x80 | $nameLength >> 24);
  162.                 $stream .= chr($nameLength >> 16);
  163.                 $stream .= chr($nameLength >> 8);
  164.                 $stream .= chr($nameLength);
  165.                 $stream .= chr($valueLength);
  166.             }else{
  167.                 // FCGI_NameValuePair44
  168.                 $stream .= chr(0x80 | $nameLength >> 24);
  169.                 $stream .= chr($nameLength >> 16);
  170.                 $stream .= chr($nameLength >> 8);
  171.                 $stream .= chr($nameLength);
  172.                 $stream .= chr(0x80 | $valueLength >> 24);
  173.                 $stream .= chr($valueLength >> 16);
  174.                 $stream .= chr($valueLength >> 8);
  175.                 $stream .= chr($valueLength);
  176.             }
  177.         }
  178.         $stream .= $name;
  179.         $stream .= $value;
  180.         $this->_send($stream);
  181.     }
  182.     //
  183.     // 5.3 Byte Streams: FCGI_STDIN
  184.     //
  185.     public function sendStdin($requestID, $body) {
  186.         $this->_log("FCGI_STDIN: length=" . strlen($body));
  187.         $pad = $this->sendRecordHeader($requestID, self::FCGI_STDIN, strlen($body));
  188.         $this->_send($body);
  189.         for($i = 0; $i < $pad; $i++) {
  190.             $this->_send(chr(0));
  191.         }
  192.         $this->flush();
  193.         
  194.     }
  195.     //
  196.     // 5.3 Byte Streams: FCGI_STDOUT, FCGI_STDERR, 5.5 FCGI_END_REQUEST
  197.     //
  198.     public function recvStdoutStderrAndWaitEndRequest() {
  199.         
  200.         $version;
  201.         $recordType = -1;
  202.         $requestID = -1;
  203.         $contentLength = 0;
  204.         $paddingLength = 0;
  205.         while(true){
  206.             if($recordType < 0){
  207.                 $raw = $this->_read();
  208.                 if ($raw === '') {
  209.                     break;
  210.                 }
  211.                 $version = ord($raw);
  212.                 if($version < 0) {
  213.                     break;
  214.                 }
  215.                 if($version != self::FCGI_VERSION_1){
  216.                     $this->_log('recv record version error: ' . $version);
  217.                     break;
  218.                 }
  219.                 
  220.                 $recordType = ord($this->_read());
  221.                 $requestID = (ord($this->_read()) << 8) + ord($this->_read());
  222.                 $contentLength = (ord($this->_read()) << 8) + ord($this->_read());
  223.                 $paddingLength = ord($this->_read());
  224.                 $this->_read(); // reserved
  225.             }
  226.             switch ($recordType) {
  227.                 case self::FCGI_STDOUT:
  228.                     $this->_log('FCGI_STDOUT: requestID=' . $requestID . ', contentLength=' . $contentLength . ', paddingLength=' . $paddingLength);
  229.                     $readed = $this->_read($contentLength);
  230.                     $this->_read($paddingLength);
  231.                     echo $readed . PHP_EOL;
  232.                     $this->_log('FCGI_STDOUT: done');
  233.                     $recordType = -1;
  234.                 break;
  235.                 case self::FCGI_STDERR:
  236.                     $this->_log('FCGI_STDERR: requestID=' . $requestID . ', contentLength=' . $contentLength . ', paddingLength=' + $paddingLength);
  237.                     $readed = $this->_read($contentLength);
  238.                     $this->_read($paddingLength);
  239.                     echo $readed;
  240.                     $this->_log('FCGI_STDERR: done');
  241.                     $recordType = -1;
  242.                 break;
  243.                 case self::FCGI_END_REQUEST:
  244.                     $startStat = (ord($this->_read()) << 24) + (ord($this->_read()) << 16) + (ord($this->_read()) << 8) + ord($this->_read());
  245.                     $endStat = ord($this->_read());
  246.                     $recordType = -1;
  247.                     $this->_log('FCGI_END_REQUEST: requestID=' . $requestID . ', appStatus=' . $startStat . ', protocolStatus=' . $endStat);
  248.                     $this->_read(3);
  249.                 break;
  250.                 default:
  251.                     $this->_log('recv record type error: ' . $recordType);
  252.                 break;
  253.             }
  254.         }
  255.         //return appStatAndProtStat;
  256.     }
  257.     private function _send($msg) {
  258.         $this->_stream .= $msg;
  259.     }
  260.     public function flush() {
  261.         
  262.         
  263.         $ret = socket_write($this->_socket, $this->_stream);
  264.         if ($ret === false) {
  265.             $this->_log('flush error!');
  266.             $this->_log($this->_stream);
  267.         } else {
  268.             $this->_log('--> flush ok');
  269.         }
  270.         $this->_stream = '';
  271.     }
  272.     private function _read($lentgh = 1) {
  273.         return socket_read($this->_socket, $lentgh);
  274.     }
  275.     public function close() {
  276.         socket_close($this->_socket);
  277.     }
  278.     private function _log($msg) {
  279.         echo $msg.PHP_EOL;
  280.     }
  281. }
  282. $fcgic = new FCGIConnection();
  283. $method = "POST";
  284. $scriptFileName = realpath(__DIR__).DIRECTORY_SEPARATOR.'call.php';
  285. $queryString = "get1=get_hello&get2=get_world";
  286. $requestBodyString = "post1=post_hello&post2=post_world";
  287. $requestID = 1;
  288. // B. Typical Protocol Message Flow
  289. // {FCGI_BEGIN_REQUEST, 1, {FCGI_RESPONDER, 0}}
  290. $fcgic->sendBeginRequest($requestID, false);
  291. // {FCGI_PARAMS,         1, "..."}
  292. $params = [
  293.     'QUERY_STRING' => $queryString,
  294.     'REQUEST_METHOD' => $method,
  295.     'SCRIPT_FILENAME' => $scriptFileName,
  296.     'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
  297.     'CONTENT_LENGTH' => strlen($requestBodyString)
  298. ];
  299. $fcgic->sendParams($requestID, $params);
  300. if(count($params) > 0){
  301.     // {FCGI_PARAMS,         1, ""}
  302.     $params = [];
  303.     $fcgic->sendParams($requestID, $params);
  304. }
  305. // {FCGI_STDIN,         1, "..."}
  306. $fcgic->sendStdin($requestID, $requestBodyString);
  307. // {FCGI_STDIN,         1, ""}
  308. $fcgic->sendStdin($requestID, "");
  309. // {FCGI_STDOUT,     1, "Content-type: text/html\r\n\r\n\n ... "}
  310. // {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
  311. $fcgic->recvStdoutStderrAndWaitEndRequest();
  312. $fcgic->close();



呼び出すプログラムはこちら。

・call.php

  1. <?php
  2. echo 'get1:'.$_GET['get1'].PHP_EOL;
  3. echo 'get2:'.$_GET['get2'].PHP_EOL;
  4. echo 'post1:'.$_POST['post1'].PHP_EOL;
  5. echo 'post2:'.$_POST['post2'].PHP_EOL;



※こちらに添付しておきます。
https://bitbucket.org/snippets/symfo/nedeK9




実行



ソケットはwww-dataユーザーで起動しているのでユーザーを変更して実行します。


$ sudo -u www-data php sock.php



うまく行きました。


open success.
Record Header: 1, pad=0
FCGI_PARAMS: length=176
Record Header: 4, pad=0
--> flush ok
FCGI_PARAMS: length=0
Record Header: 4, pad=0
--> flush ok
FCGI_STDIN: length=33
Record Header: 5, pad=7
--> flush ok
FCGI_STDIN: length=0
Record Header: 5, pad=0
--> flush ok
FCGI_STDOUT: requestID=1, contentLength=106, paddingLength=6
Content-type: text/html; charset=UTF-8

get1:get_hello
get2:get_world
post1:post_hello
post2:post_world

FCGI_STDOUT: done
FCGI_END_REQUEST: requestID=1, appStatus=0, protocolStatus=0






重い処理の実行



call.phpを変更し、10秒待たせてみます。

・call.php


  1. <?php
  2. echo 'get1:'.$_GET['get1'].PHP_EOL;
  3. echo 'get2:'.$_GET['get2'].PHP_EOL;
  4. echo 'post1:'.$_POST['post1'].PHP_EOL;
  5. echo 'post2:'.$_POST['post2'].PHP_EOL;
  6. // 10秒待つ
  7. sleep(10);
  8. file_put_contents('/tmp/debug.txt', 'fin!');




この状態でsock.phpを実行すると10秒待たされますが、結果を取得する箇所をコメントします。


// {FCGI_STDOUT,     1, "Content-type: text/html\r\n\r\n\n ... "}
// {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
//$fcgic->recvStdoutStderrAndWaitEndRequest();



コメント後実行すると、sock.php自体はすぐに終了しますが、
10秒後/tmp/debug.txtが作成されることが確認できると思います。

これで重い処理をphp-fpmに委譲することができました。
関連記事

コメント

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
https://symfo.web.fc2.com/

PR

検索フォーム

月別アーカイブ