Exécutez des scripts PHP via php-fpm sans serveur Web. Ou votre client FastCGI (sous le capot)

Je souhaite la bienvenue à tous les lecteurs de "Habr".


Clause de non-responsabilité


L'article s'est avéré assez long et pour ceux qui ne veulent pas lire le fond, mais veulent aller droit au but, je vous pose directement le chapitre "Solution".


Entrée


Dans cet article, je voudrais parler de la résolution d'un problème plutôt non standard auquel j'ai dû faire face pendant le processus de travail. À savoir, nous devions exécuter un tas de scripts php en boucle. Je ne discuterai pas des raisons et de la controverse d'une telle solution architecturale dans cet article, car en fait, il ne s’agissait pas du tout de ça, c’était juste une tâche, il fallait la résoudre et la solution me semblait suffisamment intéressante pour que je puisse la partager avec vous, d’autant plus que je n’ai trouvé aucun mana sur cette question sur Internet (enfin, bien sûr, sauf les spécifications officielles). Speck, bien sûr, est bon, et bien sûr tout y est, mais je pense que vous conviendrez que si vous n'êtes pas particulièrement familier avec le sujet, et même limité dans le temps, alors les comprendre est toujours un plaisir.


À qui s'adresse cet article?


Pour tous ceux qui travaillent avec le Web et le protocole FastCgi sait seulement que c'est le protocole selon lequel le serveur Web exécute des scripts PHP, mais veut l'étudier plus en détail et regarder sous le capot.


Justification (pourquoi cet article)


En général, comme je l'ai écrit ci-dessus, lorsque nous avons été confrontés à la nécessité d'exécuter de nombreux scripts php sans la participation d'un serveur Web (en gros, à partir d'un autre script php), la première chose qui m'est venue à l'esprit est ...


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

Mais au début de chaque script, un environnement sera créé, un processus séparé sera lancé, en général, cela semblait en quelque sorte coûteux pour les ressources. Cette implémentation a été rejetée. La deuxième chose qui m'est venue à l'esprit est bien sûr php-fpm , c'est tellement cool, il ne démarre l'environnement qu'une seule fois, surveille la mémoire, y enregistre tout correctement, démarre et arrête les scripts, en général tout se refroidit, et bien sûr nous avons aimé cette façon plus.


Mais c'est de la malchance, en théorie, nous savions comment cela fonctionne, en termes généraux (car il s'est avéré très général), mais il s'est avéré assez difficile de mettre en œuvre ce protocole dans la pratique sans la participation d'un serveur Web. La lecture des spécifications et quelques heures de tentatives infructueuses ont montré que la mise en œuvre prendrait du temps, ce que nous n'avions pas à l'époque. Il n'y a pas de mana pour la mise en œuvre de cette entreprise dans laquelle cette interaction pourrait être décrite simplement et clairement, nous ne pouvions pas non plus prendre de spécifications, à partir des solutions prêtes à l'emploi, nous avons trouvé un script Python et une bibliothèque Pykhov sur le github, qui au final ne voulait pas être traîné dans mon projet (peut-être ce n'est pas correct, mais pas vraiment, nous aimons toutes sortes de bibliothèques tierces et même pas très populaires, et donc non testées). En général, à la suite de cette idée, nous avons refusé et mis en œuvre tout cela à travers le bon vieux rabbitmq.


Bien que le problème ait finalement été résolu, j'ai quand même décidé de comprendre FastCgi en détail, et en plus j'ai décidé d'écrire un article à ce sujet, qui décrira simplement et en détail comment obtenir php-fpm pour exécuter un script php sans serveur web, ou plutôt, comme le serveur web aura un script différent, alors je l'appellerai le client Fcgi. En général, j'espère que cet article aidera ceux qui sont confrontés à la même tâche que nous et après avoir lu, il pourra écrire rapidement tout ce dont il a besoin.


Recherche créative (faux chemin)


Donc, le problème est indiqué, nous devons procéder à la solution. Naturellement, comme tout programmeur "normal", pour résoudre un problème sur lequel il n'est écrit nulle part quoi faire et quoi entrer dans la console, je n'ai pas lu et traduit la spécification, mais j'ai immédiatement trouvé ma propre solution "géniale". Son essence est la suivante, je sais que nginx (nous utilisons nginx et pour ne pas écrire plus de bêtises - un serveur web, j'écrirai nginx, car il est plus sympathique) transfère quelque chose à php-fpm , il traite également php-fpm à il exécute un script sur la base de celui-ci, eh bien, tout semble être simple, je vais le prendre et le promettre de transmettre nginx et je passerai la même chose.


Un grand netcat vous aidera ici (utilitaire UNIX pour travailler avec le trafic réseau, qui à mon avis peut faire presque n'importe quoi). Donc, nous configurons netcat pour écouter sur le port local, et configurons nginx pour fonctionner avec les fichiers php via le socket (naturellement, le socket sur le même port que netcat écoute)


écoute du port 9000


 nc -l 9000 

Vous pouvez vérifier que tout va bien, vous pouvez contacter l'adresse 127.0.0.1:9000 via un navigateur et l'image suivante doit être



configurer nginx pour qu'il traite les scripts php via une socket sur le port 9000 (dans les paramètres '/ etc / nginx / sites-available / default', bien sûr, ils peuvent différer)


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

Après ces manipulations, nous vérifierons ce qui s'est passé en contactant le script php via le navigateur



On peut voir que nginx a envoyé des variables d'environnement, ainsi que des caractères non imprimables, c'est-à-dire que les données ont été transmises en encodage binaire, ce qui signifie qu'elles ne peuvent pas être copiées si facilement et envoyées au socket php-fpm . Si vous les enregistrez dans un fichier, par exemple, puis ils sont enregistrés dans le codage hexadécimal, il semblera que cela s'applique



Mais cela ne nous donne pas grand-chose non plus, probablement purement théoriquement, ils peuvent être convertis en encodage binaire, d'une manière ou d'une autre (je ne peux même pas imaginer comment) pour les envoyer à la prise fpm, et il y a même une chance que cette moto entière fonctionne d'une manière ou d'une autre et même démarre une sorte de un script, mais en quelque sorte tout est laid et maladroit.


Il est devenu clair que ce chemin était complètement faux, vous pouvez voir par vous-même à quel point tout cela semble misérable, et plus encore, toutes ces actions ne nous permettront pas de contrôler la connexion, ni de nous rapprocher de la compréhension de l'interaction entre php-fpm et nginx .


Tout est parti, la spécification ne peut être évitée!


Solution (ici commence tout le sel de cet article)


Formation théorique


Voyons maintenant comment il existe tout de même une connexion et un échange de données entre nginx et php-fpm . Un peu de théorie, toutes les communications s'effectuent comme cela est déjà clair à travers les sockets, nous considérerons plus spécifiquement une connexion via une socket TCP.


L'unité d'informations dans le protocole FastCgi est un enregistrement cgi . Le serveur envoie ces enregistrements à l'application et reçoit exactement les mêmes enregistrements en réponse.


Un peu de théorie (structure)

Ensuite, considérez la structure de l'enregistrement. Pour comprendre en quoi consiste un enregistrement, vous devez comprendre à quoi ressemblent les structures C et comprendre leurs désignations. Pour ceux qui ne savent pas plus, ce sera brièvement (mais suffisant pour comprendre) décrit. Je vais essayer de le décrire aussi simple que possible, il est inutile d’entrer dans les détails, et je crains de me perdre dans les détails, l’essentiel est d’avoir une compréhension commune.


Les structures sont simplement une collection d'octets et leur notation permet de les interpréter. Autrement dit, vous avez juste une séquence de zéros et de uns, et certaines données sont cryptées dans cette séquence, mais jusqu'à présent, vous n'avez aucune annotation pour cette séquence, alors ces données ne représentent aucune valeur pour vous, car vous ne pouvez pas les interpréter.


 //     1101111000000010010110000010011100010000 

Ce qui est visible ici, nous avons quelques bits, quel genre de bits nous n'avons aucune idée. Eh bien, essayons par exemple de les diviser en octets et de les représenter en système décimal


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

Eh bien, nous les avons interprétés et avons obtenu des résultats, disons que ces données sont responsables de la quantité d'électricité qu'un appartement doit. Il s'avère que dans la maison 222 l'appartement numéro 2 doit payer 88 roubles. Et quoi d'autre pour deux chiffres, que faire avec juste pour laisser tomber? Bien sûr que non! le fait est que nous n'avions pas de notation (format) qui nous dirait comment interpréter les données, et les interpréter à notre manière, à cet égard, nous avons reçu non seulement des résultats inutiles, mais aussi nuisibles. En conséquence, l'appartement 2 n'a absolument pas payé ce qu'il aurait dû. (les exemples sont certainement farfelus et ne servent qu'à expliquer plus clairement la situation)


Voyons maintenant comment interpréter correctement ces données, en ayant une notation (format). De plus, j'appellerai un chat un chat, à savoir notation = format ( ici les formats ).


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

Maintenant, tout converge dans la maison n ° 222 appartement 600 pour l'électricité devrait être de 1000 roubles. Je pense que maintenant l'importance du format est claire, et maintenant il est clair à quoi ressemble à peu près une structure similaire. (veuillez faire attention, ici le but n'est pas d'expliquer en détail ce que sont ces structures, mais de donner une compréhension générale de ce que c'est et comment cela fonctionne)


Le symbole de cette structure sera


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

Un peu plus de théorie (entrées FastCgi)

Comme je l'ai dit ci-dessus, l'unité d'information dans le protocole FastCgi est les enregistrements. Le serveur envoie les enregistrements à l'application et reçoit les mêmes enregistrements en réponse. Un enregistrement se compose d'un en-tête et d'un corps contenant des données.


Structure d'en-tête:


  1. la version du protocole (toujours 1) est indiquée par 1 octet ('C')
  2. type d'enregistrement. Pour ouvrir, fermer la connexion, etc. Je ne considérerai pas tout, alors je ne considérerai que ce qui est nécessaire pour une tâche spécifique, si d'autres sont nécessaires, accueillez la spécification ici. Il est indiqué par 1 octet («C»).
  3. L'ID de la demande, un nombre arbitraire, est désigné par 2 octets ('n')
  4. la longueur du corps de l'enregistrement (données), indiquée par 2 octets ('n')
  5. la longueur des données d'alignement et des données réservées, un octet chacune (il n'est pas nécessaire de porter une attention particulière pour ne pas être distrait de la principale dans notre cas il y aura toujours 0)

Vient ensuite le corps du dossier:


  1. les données elles-mêmes (ici ce sont précisément les variables qui sont transférées) peuvent être assez volumineuses (jusqu'à 65 535 octets)

Voici un exemple de l'enregistrement binaire FastCgi le plus simple au format


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

Pratique


Client de script et socket de transmission

Pour le transfert de données, nous utiliserons l'extension de socket php standard. Et la première chose à faire est de configurer php-fpm pour écouter sur le port de l'hôte local, par exemple 9000. Cela se fait dans la plupart des cas dans le fichier '/etc/php/7.3/fpm/pool.d/www.conf', le chemin bien sûr Dépend de vos paramètres système. Là, vous devez enregistrer quelque chose comme le suivant (j'apporte tout le footcloth pour que vous puissiez naviguer, la section principale est écouter ici)


 ; 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 

Après avoir configuré fpm, l'étape suivante consiste à se connecter à la prise


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

Début de la demande FCGI_BEGIN_REQUEST


Pour ouvrir une connexion, nous devons envoyer une entrée de type FCGI_BEGIN_REQUEST = 1 Le titre de l'entrée sera comme ceci (pour convertir les valeurs numériques en une chaîne binaire au format spécifié, le pack fonction php () sera utilisé)


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

Le corps d'enregistrement pour ouvrir une connexion doit contenir un rôle d'enregistrement et un indicateur contrôlant la connexion


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

Ainsi, l'enregistrement pour l'ouverture de la connexion a été envoyé avec succès, php-fpm l'acceptera et continuera à attendre de nous un autre enregistrement dans lequel nous devons transférer des données pour déployer l'environnement et exécuter le script.


Passage des paramètres d'environnement FCGI_PARAMS


Dans cet enregistrement, nous transmettrons tous les paramètres nécessaires au déploiement de l'environnement, ainsi que le nom du script à exécuter.


Paramètres d'environnement minimum requis


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

La première chose que nous devons faire ici est de préparer les variables nécessaires, c'est-à-dire les paires nom => valeur que nous transmettrons à l'application.


La structure de la valeur du nom des paires sera telle


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

Il y a 1 octet en premier - le nom est long, puis 1 octet est la valeur


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

Dans notre cas, le nom et les significations sont courts et correspondent à la première option, nous allons donc l'examiner.


Encodez nos variables selon le format


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

Ici, les valeurs inférieures à 128 bits sont codées par la fonction chr ($ keyLen) , plus que pack ('N', $ valLen) , où 'N' représente 4 octets. Et puis tout cela est collé sur une seule ligne conformément au format de la structure. Le corps de l'enregistrement est prêt.


Dans l'en-tête de l'enregistrement, nous transférons tout de la même manière que dans l'enregistrement précédent, à l'exception du type (ce sera FCGI_PARAMS = 4) et de la longueur des données (elle sera égale à la longueur des paires nom => valeur, ou à la longueur de la chaîne $ keyValueFcgiString que nous avons formée plus tôt).


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

Obtenir une réponse de FCGI_PARAMS


En fait, après que tout ce qui précède a été fait, et que tout ce qu'il attend a été envoyé à l'application, il commence à fonctionner et nous ne pouvons prendre le résultat de ce travail que depuis le socket.
N'oubliez pas qu'en réponse, nous obtenons les mêmes notes et nous devons également les interpréter.


Nous obtenons l'en-tête, c'est toujours 8 octets (nous recevrons les données par octet)


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

Maintenant, conformément à la longueur du corps de la réponse reçue, nous ferons une autre lecture à partir de la prise


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

Hourra, ça a marché! Enfin ça!
Qu'avons-nous dans la réponse, si par exemple dans ce fichier


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

nous écrirons


 <?php echo "My fcgi script"; 

puis dans la réponse que nous obtenons à la suite


image


Résumé


Je n’écrirai pas beaucoup ici, alors le long article s’est avéré. J'espère qu'elle aide quelqu'un. Et je vais donner le script final lui-même, il s'est avéré être assez petit. Bien sûr, il peut faire beaucoup de choses sous cette forme, et il ne gère pas les erreurs et tout cela, mais il n'en a pas besoin, il a besoin de lui comme exemple pour montrer les bases.


Version complète du 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/fr472190/


All Articles