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:
- A versão do protocolo (sempre 1) é indicada por 1 byte ('C')
- 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').
- O ID da solicitação, um número arbitrário, é indicado por 2 bytes ('n')
- o comprimento do corpo do registro (dados), indicado por 2 bytes ('n')
- 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:
- 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));
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
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) {
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).
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);
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));
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

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