无需网络服务器即可通过php-fpm运行PHP脚本。 或您的FastCGI客户端(在后台)

我欢迎《哈伯》的所有读者。


免责声明


事实证明,这篇文章很长,对于那些不想阅读背景但又想直截了当地的人,请直接进入“解决方案”一章。


参赛作品


在本文中,我想谈谈解决工作过程中必须面对的一个非标准问题。 即,我们需要循环运行一堆PHP脚本。 我不会在本文中讨论这种体系结构解决方案的原因和争议,因为 实际上,这根本不是全部,它只是一项任务,必须解决,而且解决方案对我来说似乎很有趣,可以与您分享,特别是因为我根本没有在互联网上找到任何法术力(当然,除了官方规格外)。 斑点当然很好,当然一切都在其中,但是我想您会同意,如果您对主题不是特别熟悉,甚至时间有限,那么理解它们仍然是一种乐趣。


本文适用于谁?


对于使用Web和FastCgi协议的每个人, 都只知道这是Web服务器运行php脚本所依据的协议,但希望对其进行更详细的研究并深入研究。


理由(本文为何如此)


总的来说,正如我上面所写的那样,当我们需要在没有Web服务器的情况下运行许多php脚本的需要时(大概是从另一个php脚本开始),想到的第一件事是...


shell_exec('php \path\to\script.php') 

但是在每个脚本的开头,将创建一个环境,将启动一个单独的过程,通常来说,资源的开销似乎有些高昂。 此实现被拒绝。 我想到的第二件事当然是php-fpm ,它非常酷,它只启动一次环境,监视内存,正确地记录所有内容,启动和停止脚本,总的来说一切都很酷,当然我们喜欢这种方式更多。


但这很不幸,从理论上讲,我们从总体上了解了它的工作原理(事实证明,它非常通用),但是事实证明,如果没有Web服务器的参与,在实践中很难实施此协议。 阅读规范和几小时的失败尝试表明,实现它需要时间,而我们当时还没有。 对于这种合资企业的实现,没有任何法力可以简单而清楚地描述这种相互作用,我们也无法采用任何规范,从现成的解决方案中,我们在github上找到了Python脚本和Pykhov lib,最终他们不想将其拖到我的项目中(也许这是不正确的,但并非如此,我们喜欢各种各样的第三方库,甚至不是很受欢迎的第三方库,因此未经测试)。 总的来说,由于这个想法,我们通过旧的rabbitmq拒绝并实施了所有这些。


尽管问题终于得到解决,但我仍然决定详细了解FastCgi,此外,我决定写一篇有关它的文章,该文章将简单而详细地描述如何使php-fpm在没有Web服务器的情况下运行php脚本,或者更确切地说,是Web服务器将具有不同的脚本,那么我将其称为Fcgi客户端。 总的来说,我希望本文能对那些和我们一样面临同样任务的人有所帮助,并且在阅读之后能够根据他的需要快速编写所有内容。


广告搜索(错误路径)


因此,指示出了问题,我们必须继续解决。 自然,像任何“普通”程序员一样,要解决一个问题,即它没有写在任何地方做什么和要输入到控制台中的问题,所以我没有阅读和翻译该规范,而是立即想出了自己的“出色”解决方案。 其本质如下:我知道nginx (我们使用nginx以便不进一步编写愚蠢的东西 -网络服务器,我会写nginx,因为它更富有同情心)将某些内容传输到php-fpm ,它php-fpm处理为它在此基础上运行一个脚本,嗯,一切似乎都很简单,我接受它并保证它可以传输nginx,并且我将传递相同的内容。


伟大的netcat将在这里提供帮助(用于处理网络流量的UNIX实用程序,我认为这几乎可以做任何事情)。 因此,我们将netcat设置为侦听本地端口,并将nginx配置为通过套接字与php文件一起使用(当然,套接字位于netcat侦听的同一端口上)


监听9000端口


 nc -l 9000 

您可以检查一切正常,可以通过浏览器联系地址127.0.0.1:9000,以下图片应为



配置nginx,以便它通过端口9000上的套接字处理php脚本(在设置“ / etc / nginx / sites-available / default”中,它们可能会有所不同)


 location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass 127.0.0.1:9000; } 

完成这些操作后,我们将通过浏览器联系php脚本来检查发生了什么



可以看出, nginx发送了环境变量以及不可打印的字符,即数据是以二进制编码传输的,这意味着它们不能很容易地复制并发送到php-fpm套接字 。 例如,如果将它们保存到文件中,然后将它们以十六进制编码保存,则看起来像这样



但这也不能给我们带来太多好处,可能纯粹从理论上讲,它们可以转换为二进制编码,以某种方式(我什至无法想象如何)将其发送到fpm插座,甚至有可能使整辆自行车以某种方式工作,甚至启动某种方式。脚本,但是总有点丑陋和尴尬。


显然,这条路径是完全错误的,您可以亲眼看到这一切看起来多么惨,而且,所有这些动作都将使我们无法控制连接,也不会使我们更了解php-fpmnginx之间的交互。


一切都荡然无存,规格无法避免!


解决方案(本文的所有内容实际上都从这里开始)


理论训练


现在,让我们考虑一下nginxphp-fpm之间的连接和数据交换如何完全一样。 有点理论,所有通信都是通过套接字进行的,因此我们将进一步具体考虑通过TCP套接字的连接。


FastCgi协议中的信息单位是cgi记录 。 服务器将此类记录发送到应用程序,并作为响应接收完全相同的记录。


一点理论(结构)

接下来,考虑记录的结构。 要了解记录的内容,您需要了解C结构是什么样的,并了解其名称。 对于那些不知道更多的人,将简要介绍(但足以理解)。 我将尝试尽可能简单地描述它,在这里详细介绍是没有意义的,而且我担心我会在细节上感到困惑,主要是要有一个共识。


结构只是字节的集合,对它们的表示法允许对它们进行解释。 也就是说,您只有一个零和一的序列,并且一些数据按此序列进行了加密,但是只要您没有对此序列的注释,该数据对您就没有任何价值,因为 您无法解释它们。


 //     1101111000000010010110000010011100010000 

在这里可见,我们有一些位,我们不知道什么样的位。 好吧,让我们尝试例如将它们分成字节并以十进制表示


 //   5  11011110 00000010 01011000 00100111 00010000 //    222 2 88 39 16 

好吧,我们对它们进行了解释并得出了一些结果,比方说,这些数据与某间公寓的电力欠款有关。 原来,在222号房屋中,2号公寓必须支付88卢布。 还有两位数又是什么,只是丢掉它们怎么办? 当然不是! 事实是,我们没有一种表示法(格式)可以告诉我们如何解释数据以及如何以自己的方式解释数据,在这一方面,我们不仅收到了无用的结果,而且还收到了有害的结果。 结果,公寓2绝对没有支付应有的金额。 (这些示例确实牵强,只能用来更清楚地说明情况)


现在,让我们看看如何正确地解释此数据,并带有一个符号(格式)。 另外,我将黑桃称为黑桃,即符号= format( 此处为format)。


 //  "Cnn" //  //C -   (char) (8 ) //n -  short (16 ) //      11011110 0000001001011000 0010011100010000 //    222 600 10000 

现在一切都收敛在222号房屋,600户电力公寓中,应该是1000卢布。我认为现在这种格式的重要性已经很明确,而且类似结构的外观也大致如此。 (请注意,这里的目的不是详细解释这些结构是什么,而是要大致了解它的结构及其工作原理)


该结构的符号将是


 struct { unsigned char houseNumber; unsigned char flatNumperA1; unsigned char flatNumperA2; unsigned char summB1; unsigned char summB2; }; // ,           // houseNumber -  // flatNumperA1 && flatNumperA2 -  // summB1 && summB2 -   

更多理论(FastCgi条目)

如前所述,FastCgi协议中的信息单位是记录。 服务器将记录发送到应用程序,并作为响应接收相同的记录。 记录由标题和带有数据的正文组成。


标题结构:


  1. 协议版本(始终为1)由1个字节(“ C”)表示
  2. 记录类型。 要打开,关闭连接等。我将不考虑所有内容,然后仅考虑特定任务所需的内容,如果需要其他内容,请在此处提供规范 。 它由1个字节('C')表示。
  3. 请求ID(任意数字)由2个字节('n')表示
  4. 记录(数据)正文的长度,以2个字节('n')表示
  5. 对齐数据和保留数据的长度,每个字节一个字节(无需特别注意,以免分散主数据的注意力,在我们的情况下,总为0)

接下来是记录的正文:


  1. 数据本身(此处正是要传输的变量)可能会非常大(最多65535字节)

这是格式最简单的FastCgi二进制记录的示例


 struct { // unsigned char version; unsigned char type; unsigned char idA1; unsigned char idA2; unsigned char bodyLengthB1; unsigned char bodyLengthB2; unsigned char paddingLength; unsigned char reserved; //  unsigned char contentData; // 65535  unsigned char paddingData; }; 

练习


脚本客户端和传输套接字

对于数据传输,我们将使用标准的php套接字扩展。 要做的第一件事是将php-fpm配置为侦听本地主机上的端口,例如9000。在大多数情况下,这是在文件'/etc/php/7.3/fpm/pool.d/www.conf'中完成的,当然这是路径取决于您的系统设置。 在那里,您需要注册如下内容(我带了整个鞋带,以便您可以浏览,主要部分在此处收听)


 ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on ; a specific port; ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on ; a specific port; ; 'port' - to listen on a TCP socket to all addresses ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. ;listen = /run/php/php7.3-fpm.sock listen = 127.0.0.1:9002 

设置fpm后,下一步是连接到套接字


 $service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port); 

开始FCGI_BEGIN_REQUEST请求


要打开连接,我们必须发送类型为FCGI_BEGIN_REQUEST = 1的条目。条目的标题将如下所示(要将数值转换为具有指定格式的二进制字符串,将使用php函数pack())


 socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0)); //  - 1 //  - 1 - FCGI_BEGIN_REQUEST //id - 1 //   - 8  // - 0 

用于打开连接的记录主体必须包含记录角色和控制连接的标志


 //      //struct { // unsigned char roleB1; // unsigned char roleB0; // unsigned char flags; // unsigned char reserved[5]; //}; //php  socket_write($socket, pack('nCxxxxx', 1, 0)); // - 1 -  // - 1 -    1    

因此,打开连接的记录已成功发送, php-fpm将接受该记录,并会继续期望我们提供更多记录,在该记录中,我们需要传输数据以部署环境并运行脚本。


传递环境参数FCGI_PARAMS


在此记录中,我们将传递部署环境所需的所有参数,以及将需要运行的脚本的名称。


所需的最低环境设置


 $url = '/path/to/script.php' $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ]; 

我们在这里要做的第一件事是准备必要的变量,即将要传递给应用程序的名称=>值对。


对名称值的结构将是这样


 //          128  typedef struct { unsigned char nameLength; unsigned char valueLength; unsigned char nameData unsigned char valueData; }; //    1  

首先有1个字节-名称长,然后是1个字节的值


 //         128  typedef struct { unsigned char nameLengthA1; unsigned char nameLengthA2; unsigned char nameLengthA3; unsigned char nameLengthA4; unsigned char valueLengthB1; unsigned char valueLengthB2; unsigned char valueLengthB3; unsigned char valueLengthB4; unsigned char nameData unsigned char valueData; }; //    4  

在我们的例子中,名称和含义都简短并且适合第一个选项,因此我们将考虑它。


根据格式对变量进行编码


 $keyValueFcgiString = ''; foreach ($env as $key => $value) { //        //  128         $keyLen = strlen($key); $lenKeyChar = $keyLen < 128 ? chr($keyLen) : pack('N', $keyLen); $valLen = strlen($value); $valLenChar = $valLen < 128 ? chr($valLen) : pack('N', $valLen); $keyValueFcgiString .= $lenKeyChar . $valLenChar . $key . $value; } 

小于128位的值在这里由chr($ keyLen)函数编码,大于pack('N',$ valLen) ,其中'N'代表4个字节。 然后,根据结构的格式将所有这些内容粘贴在一起。 记录的主体已准备就绪。


在记录的标题中,我们传输所有与上一条记录相同的东西,除了类型(它将是FCGI_PARAMS = 4)和数据长度(它将等于名称=>值对的长度,或者是我们前面形成的字符串$ keyValueFcgiString的长度)之外。


 //  socket_write($socket, pack('CCnnCx', 1, 4, 1, strlen($keyValueFcgiString), 0)); // body socket_write($socket, $keyValueFcgiString); //             //  body socket_write($socket, pack('CCnnCx', 1, 4, 1, 0, 0)); 

获得来自FCGI_PARAMS的回复


实际上,在完成所有先前的工作并将所有期望的内容发送到应用程序之后,它便开始工作,我们只能从套接字获取此工作的结果。
请记住,作为回应,我们会得到相同的注释,我们也需要对其进行解释。


我们得到标头,它总是8个字节(我们将按字节接收数据)


 $buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL); //   1       $arrData[] = $buf; $len--; } //      'CCnnCx' $protocol = unpack('C', $arrData[0]); $type = unpack('C', $arrData[1]); $id = unpack('n', $arrData[2] . $arrData[3]); $dataLen = unpack('n', $arrData[4] . $arrData[5])[1]; //   ,        (unpack  ,    ) $foo = unpack('C', $arrData[6]); var_dump($dataLen); //      

现在,根据接收到的响应主体长度,我们将再次从套接字读取


 $buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result)); //       socket_close($socket); 

万岁! 最后那个!
例如,如果在此文件中,我们在答案中有什么


 $url = '/path/to/script.php' //     

我们会写


 <?php echo "My fcgi script"; 

然后在答案中我们得到的结果


图片


总结


我在这里不会写太多,所以长篇文章被证明了。 我希望她能帮助某人。 我将给出最终的脚本本身,事实证明它很小。 当然,他可以用这种形式做很多事情,他没有错误处理及所有这些,但是他不需要它,他需要他作为例子来展示基础知识。


完整版本的脚本
 <?php $url = '/path/to/script.php'; $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ]; $service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port); //  //     php-fpm //    ,   (    ), id ,   ,     socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0)); //     // ,     socket_write($socket, pack('nCxxxxx', 1, 0)); $keyValueFcgiString = ''; foreach ($env as $key => $value) { //        //  128         $keyLen = strlen($key); $lenKeyChar = $keyLen < 128 ? chr($keyLen) : pack('N', $keyLen); $valLen = strlen($value); $valLenChar = $valLen < 128 ? chr($valLen) : pack('N', $valLen); $keyValueFcgiString .= $lenKeyChar . $valLenChar . $key . $value; } // ,      php-fpm           //      //1- ( ), 4-  (,    - FCGI_PARAMS), id  ( ),    (   -),     socket_write($socket, pack('CCnnCx', 1, 4, 1, strlen($keyValueFcgiString), 0)); //      socket_write($socket, $keyValueFcgiString); //  socket_write($socket, pack('CCnnCx', 1, 4, 1, 0, 0)); $buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL); //   1       $arrData[] = $buf; $len--; } //      'CCnnCx' $protocol = unpack('C', $arrData[0]); $type = unpack('C', $arrData[1]); $id = unpack('n', $arrData[2] . $arrData[3]); $dataLen = unpack('n', $arrData[4] . $arrData[5])[1]; //   ,        (unpack  ,    ) $foo = unpack('C', $arrData[6]); $buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result)); //       socket_close($socket); 

Source: https://habr.com/ru/post/zh-CN472190/


All Articles