Execute scripts PHP via php-fpm sem um servidor web. Ou seu cliente FastCGI (sob o capô)

Congratulo-me com todos os leitores de "Habr".


Isenção de responsabilidade


O artigo acabou sendo bastante longo e, para aqueles que não querem ler os antecedentes, mas querem ir direto ao ponto, peço-lhe diretamente no capítulo "Solução".


Entrada


Neste artigo, gostaria de falar sobre a solução de um problema não padronizado que tive que enfrentar durante o processo de trabalho. Ou seja, precisávamos executar vários scripts php em um loop. Não discutirei os motivos e controvérsias de uma solução arquitetônica nesse artigo, porque na verdade, não era nada disso, era apenas uma tarefa, tinha que ser resolvida e a solução parecia interessante o suficiente para eu compartilhar com você, tanto mais que não encontrei nenhuma mana sobre esse assunto na Internet (bem, é claro, exceto as especificações oficiais). Speck, é claro, é bom, e é claro que tudo está neles, mas acho que você concorda que, se você não estiver particularmente familiarizado com o tópico, e mesmo limitado no tempo, entendê-lo ainda é um prazer.


Para quem é este artigo?


Para quem trabalha com a web e com o protocolo FastCgi , apenas sabe que esse é o protocolo segundo o qual o servidor da web executa scripts php, mas deseja estudá-lo com mais detalhes e olhar sob o capô.


Justificação (por que este artigo)


Em geral, como escrevi acima, quando nos deparamos com a necessidade de executar muitos scripts php sem a participação de um servidor web (grosso modo, de outro script php), a primeira coisa que veio à mente é ...


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

Porém, no início de cada script, um ambiente será criado, um processo separado será lançado; em geral, parecia de alguma forma caro para recursos. Esta implementação foi rejeitada. A segunda coisa que veio à mente é, obviamente, o php-fpm , é tão legal que só inicia o ambiente uma vez, monitora a memória, registra tudo corretamente, inicia e interrompe scripts, em geral tudo fica legal e, é claro, gostamos dessa maneira mais


Mas é azar, em teoria, sabíamos como funciona, em termos gerais (como se mostrou muito geral), mas acabou sendo muito difícil implementar esse protocolo na prática sem a participação de um servidor da web. A leitura das especificações e algumas horas de tentativas frustradas mostraram que levaria tempo para implementar, o que não tínhamos naquele momento. Não há mana para a implementação desse empreendimento, no qual essa interação possa ser descrita de maneira simples e clara, também não poderíamos tirar nenhuma especificação, das soluções prontas que encontramos um script Python e uma lib Pykhov no github, que no final não queriam ser arrastadas para o meu projeto (talvez não está correto, mas na verdade não, nós amamos todos os tipos de bibliotecas de terceiros e até mesmo não muito populares e, portanto, não testadas). Em geral, como resultado dessa idéia, recusamos e implementamos tudo isso através do bom e velho rabbitmq.


Embora o problema finalmente tenha sido resolvido, eu ainda decidi entender o FastCgi em detalhes e, além disso, decidi escrever um artigo sobre ele, que descreverá de maneira simples e detalhada como fazer com que o php-fpm execute um script php sem um servidor da Web, ou melhor, como o servidor web terá um script diferente, então chamarei de cliente Fcgi. Em geral, espero que este artigo ajude aqueles que enfrentam a mesma tarefa que nós e, após a leitura, seja capaz de escrever rapidamente tudo o que precisar.


Pesquisa de criativos (caminho falso)


Portanto, o problema é indicado, devemos prosseguir para a solução. Naturalmente, como qualquer programador "normal", para resolver um problema sobre o qual não está escrito em nenhum lugar o que fazer e o que inserir no console, não li e traduzi a especificação, mas imediatamente criei minha própria solução "brilhante". Sua essência é a seguinte: eu sei que o nginx (usamos o nginx e para não escrever mais coisas tolas - um servidor web, escreverei o nginx, pois é mais simpático) transfere algo para o php-fpm , ele também processa o php-fpm para ele executa um script com base nisso, bem, tudo parece simples, eu pego e prometo que transmite nginx e passarei a mesma coisa.


O ótimo netcat ajudará aqui (utilitário UNIX para trabalhar com tráfego de rede, que na minha opinião pode fazer quase qualquer coisa). Então, configuramos o netcat para escutar na porta local e configuramos o nginx para trabalhar com arquivos php através do soquete (naturalmente, o soquete na mesma porta em que o netcat escuta)


ouvindo a porta 9000


 nc -l 9000 

Você pode verificar se está tudo bem, pode entrar em contato com o endereço 127.0.0.1:9000 através de um navegador e a imagem a seguir deve ser



configure o nginx para que ele processe scripts php através de um soquete na porta 9000 (nas configurações '/ etc / nginx / sites-available / default', é claro, eles podem ser diferentes)


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

Após essas manipulações, verificaremos o que aconteceu entrando em contato com o script php através do navegador



Pode-se observar que o nginx enviou variáveis ​​de ambiente e caracteres não imprimíveis, ou seja, os dados foram transmitidos em codificação binária, o que significa que eles não podem ser tão facilmente copiados e enviados para o soquete php-fpm . Se você salvá-los em um arquivo, por exemplo, eles serão salvos em codificação hexadecimal, será a seguinte:



Mas isso também não nos dá muito, provavelmente puramente teoricamente eles podem ser convertidos em codificação binária, de alguma forma (eu nem imagino como) enviá-los para o soquete de fpm, e há ainda a chance de que toda essa bicicleta funcione de alguma forma e até inicie algum tipo de um script, mas de alguma forma é tudo feio e estranho.


Ficou claro que esse caminho estava completamente errado, você pode ver por si mesmo como tudo isso parece miserável e, mais ainda, todas essas ações não nos permitem controlar a conexão, nem nos aproximam da compreensão da interação entre php-fpm e nginx .


Tudo se foi, a especificação não pode ser evitada!


Solução (aqui todo o sal deste artigo realmente começa)


Treinamento teórico


Vamos agora considerar como existe uma conexão e troca de dados entre nginx e php-fpm . Um pouco de teoria, toda a comunicação ocorre como já está clara através dos soquetes; consideraremos ainda mais especificamente uma conexão através de um soquete TCP.


A unidade de informação no protocolo FastCgi é um registro cgi . O servidor envia esses registros para o aplicativo e recebe exatamente os mesmos registros em resposta.


Um pouco de teoria (estrutura)

Em seguida, considere a estrutura do registro. Para entender em que consiste um registro, você precisa entender como são as estruturas C e entender suas designações. Para aqueles que não sabem mais, isso será descrito brevemente (mas o suficiente para a compreensão). Vou tentar descrever da maneira mais simples possível, é inútil entrar em detalhes aqui e tenho medo de ficar confuso com os detalhes. O principal é ter um entendimento comum.


Estruturas são simplesmente uma coleção de bytes, e uma notação para elas permite que sejam interpretadas. Ou seja, você só tem uma sequência de zeros e uns, e alguns dados são criptografados nessa sequência, mas até agora você não tem anotação para essa sequência, esses dados não representam nenhum valor para você, porque você não pode interpretá-los.


 //     1101111000000010010110000010011100010000 

O que é visível aqui, temos alguns bits, que tipo de bits não temos idéia. Bem, vamos tentar, por exemplo, dividi-los em bytes e representar no sistema decimal


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

Bem, nós os interpretamos e obtivemos alguns resultados, digamos que esses dados sejam responsáveis ​​pelo quanto um determinado apartamento deve pela eletricidade. Acontece que na casa 222 o apartamento número 2 deve pagar 88 rublos. E o que mais para dois dígitos, o que fazer com eles apenas para soltar? Claro que não! o fato é que não possuíamos uma notação (formato) que nos diria como interpretar os dados e interpretá-los à nossa maneira, nesse sentido, recebemos não apenas resultados inúteis, mas também prejudiciais. Como resultado, o apartamento 2 não pagou absolutamente o que deveria ter. (os exemplos certamente são absurdos e servem apenas para explicar mais claramente a situação)


Agora vamos ver como devemos interpretar esses dados corretamente, tendo uma notação (formato). Além disso, chamarei uma pá de pá, nomeadamente notação = formato ( aqui formatos ).


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

Agora, tudo converge na casa nº 222, apartamento 600, para eletricidade, deve ser de 1000 rublos.Eu acho que agora a importância do formato é clara, e agora está claro a aparência de uma estrutura semelhante. (preste atenção, aqui o objetivo não é explicar em detalhes o que são essas estruturas, mas fornecer uma compreensão geral do que é e como funciona)


O símbolo dessa estrutura será


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

Um pouco mais de teoria (entradas do FastCgi)

Como eu disse acima, a unidade de informação no protocolo FastCgi é registros. O servidor envia os registros para o aplicativo e recebe os mesmos registros em resposta. Um registro consiste em um cabeçalho e um corpo com dados.


Estrutura do cabeçalho:


  1. A versão do protocolo (sempre 1) é indicada por 1 byte ('C')
  2. tipo de registro. Para abrir, fechar a conexão, etc. Não considerarei tudo, considerarei apenas o que é necessário para uma tarefa específica; se forem necessários outros, dê as boas-vindas à especificação aqui. É indicado por 1 byte ('C').
  3. O ID da solicitação, um número arbitrário, é indicado por 2 bytes ('n')
  4. o comprimento do corpo do registro (dados), indicado por 2 bytes ('n')
  5. o comprimento dos dados de alinhamento e dos dados reservados, um byte cada (não há necessidade de prestar atenção especial para não se distrair do principal no nosso caso, sempre haverá 0)

Em seguida é o corpo do registro:


  1. os dados em si (aqui são precisamente as variáveis ​​transferidas) podem ser bastante grandes (até 65535 bytes)

Aqui está um exemplo do registro binário FastCgi mais simples com formato


 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; }; 

Prática


Cliente de script e soquete de transmissão

Para transferência de dados, usaremos a extensão de soquete php padrão. E a primeira coisa que precisa ser feita é configurar o php-fpm para escutar na porta no host local, por exemplo, 9000. Isso é feito na maioria dos casos no arquivo '/etc/php/7.3/fpm/pool.d/www.conf', o caminho do curso Depende das configurações do seu sistema. Lá você precisa registrar algo como o seguinte (trago todo o calçado para que você possa navegar, a seção principal é ouvir aqui)


 ; 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 

Após configurar o fpm, o próximo passo é conectar ao soquete


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

Início da solicitação FCGI_BEGIN_REQUEST


Para abrir uma conexão, devemos enviar uma entrada com o tipo FCGI_BEGIN_REQUEST = 1 O título da entrada será assim (para converter os valores numéricos em uma sequência binária com o formato especificado, será utilizado o php function pack ())


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

O corpo de gravação para abrir uma conexão deve conter uma função de gravação e um sinalizador que controle a conexão


 //      //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    

Portanto, o registro para abrir a conexão foi enviado com sucesso, o php-fpm aceitará e continuará esperando de nós um registro adicional no qual precisamos transferir dados para implantar o ambiente e executar o script.


Passando parâmetros de ambiente FCGI_PARAMS


Nesse registro, passaremos todos os parâmetros necessários para implantar o ambiente, bem como o nome do script que precisaremos executar.


Configurações de ambiente mínimas necessárias


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

A primeira coisa que precisamos fazer aqui é preparar as variáveis ​​necessárias, ou seja, os pares nome => valor que passaremos para o aplicativo.


A estrutura do valor do nome dos pares será tal


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

Há 1 byte primeiro - o nome é longo e, em seguida, 1 byte é o valor


 //         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  

No nosso caso, tanto o nome quanto o significado são curtos e se encaixam na primeira opção; portanto, consideraremos isso.


Codifique nossas variáveis ​​de acordo com o formato


 $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; } 

Aqui, valores menores que 128 bits são codificados pela função chr ($ keyLen) , mais que pack ('N', $ valLen) , onde 'N' representa 4 bytes. E então tudo isso fica preso em uma linha, de acordo com o formato da estrutura. O corpo da gravação está pronto.


No cabeçalho do registro, transferimos tudo da mesma forma que no registro anterior, exceto o tipo (será FCGI_PARAMS = 4) e o comprimento dos dados (será igual ao comprimento dos pares name => value ou o comprimento da string $ keyValueFcgiString que formamos anteriormente).


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

Obtendo uma resposta de FCGI_PARAMS


Na verdade, depois que todo o anterior foi feito e tudo o que ele espera foi enviado para o aplicativo, ele começa a funcionar e só podemos tirar o resultado desse trabalho do soquete.
Lembre-se de que, em resposta, obtemos as mesmas notas e também precisamos interpretá-las.


Obtemos o cabeçalho, são sempre 8 bytes (receberemos dados por byte)


 $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); //      

Agora, de acordo com o comprimento do corpo da resposta recebida, faremos outra leitura no soquete


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

Viva, funcionou! Finalmente isso!
O que temos na resposta, se por exemplo neste arquivo


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

vamos escrever


 <?php echo "My fcgi script"; 

então na resposta que obtemos como resultado


imagem


Sumário


Não vou escrever muito aqui, então o longo artigo acabou. Espero que ela ajude alguém. E eu darei o script final, ele acabou sendo bem pequeno. Obviamente, ele pode fazer um pouco dessa forma, e ele não tem tratamento de erros e tudo isso, mas ele não precisa, ele precisa dele como um exemplo para mostrar o básico.


Versão completa do 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/pt472190/


All Articles