Doy la bienvenida a todos los lectores de "Habr".
Descargo de responsabilidad
El artículo resultó ser bastante largo y para aquellos que no quieren leer los antecedentes, pero quieren ir al grano, les pido directamente al capítulo "Solución".
Entrada
En este artículo me gustaría hablar sobre la solución de un problema no estándar que tuve que enfrentar durante el proceso de trabajo. Es decir, necesitábamos ejecutar un montón de scripts php en un bucle. No discutiré las razones y la controversia de tal solución arquitectónica en este artículo, porque de hecho, no se trataba de eso en absoluto, era solo una tarea, tenía que resolverse y la solución me pareció lo suficientemente interesante como para compartirla contigo, especialmente porque no encontré ningún maná en Internet (bueno, por supuesto, excepto por las especificaciones oficiales). Speck, por supuesto, es bueno y, por supuesto, todo está en ellos, pero creo que estará de acuerdo en que si no está particularmente familiarizado con el tema, e incluso tiene un tiempo limitado, comprenderlos sigue siendo un placer.
¿Para quién es este artículo?
Para todos los que trabajan con la web y el protocolo FastCgi solo saben que este es el protocolo según el cual el servidor web ejecuta scripts php, pero quiere estudiarlo con más detalle y mirar debajo del capó.
Justificación (por qué este artículo)
En general, como escribí anteriormente, cuando nos enfrentamos con la necesidad de ejecutar muchos scripts php sin la participación de un servidor web (aproximadamente hablando de otro script php), lo primero que se me ocurrió fue ...
shell_exec('php \path\to\script.php')
Pero al comienzo de cada script, se creará un entorno, se lanzará un proceso separado, en general, de alguna manera parecía costoso para los recursos. Esta implementación fue rechazada. La segunda cosa que me vino a la mente es, por supuesto, php-fpm , es genial, solo inicia el entorno una vez, monitorea la memoria, registra todo allí correctamente, inicia y detiene los scripts, en general todo funciona bien y, por supuesto, nos gustó de esta manera más
Pero es mala suerte, en teoría sabíamos cómo funciona, en términos generales (ya que resultó ser muy general), pero resultó bastante difícil implementar este protocolo en la práctica sin la participación de un servidor web. La lectura de las especificaciones y un par de horas de intentos fallidos mostraron que llevaría tiempo implementarlo, lo que no teníamos en ese momento. No hay maná para la implementación de esta empresa en la que esta interacción podría describirse de manera simple y clara, tampoco pudimos tomar ninguna especificación, de las soluciones ya preparadas encontramos un script de Python y una lib de Pykhov en el github, que al final no quería ser arrastrado a mi proyecto (tal vez no es correcto, pero en realidad no, nos encantan todo tipo de bibliotecas de terceros e incluso las que no son muy populares, y por lo tanto no se han probado). En general, como resultado de esta idea, rechazamos e implementamos todo esto a través del viejo y bueno conejomq.
Aunque el problema finalmente se resolvió, todavía decidí entender FastCgi en detalle, y además decidí escribir un artículo sobre él, que describirá de manera simple y detallada cómo hacer que php-fpm ejecute un script php sin un servidor web, o mejor dicho, el servidor web tendrá un script diferente, luego lo llamaré el cliente Fcgi. En general, espero que este artículo ayude a aquellos que se enfrentan a la misma tarea que nosotros y después de leerlo podrá escribir rápidamente todo lo que necesite.
Búsqueda creativa (ruta falsa)
Entonces el problema está indicado, debemos proceder a la solución. Naturalmente, como cualquier programador "normal", para resolver un problema sobre el que no está escrito en ningún lado qué hacer y qué ingresar a la consola, no leí ni traduje la especificación, pero inmediatamente se me ocurrió mi propia solución "brillante". Su esencia es la siguiente, sé que nginx (usamos nginx y para no escribir más tonterías : un servidor web, escribiré nginx, ya que es más comprensivo) transfiere algo a php-fpm , también procesa php-fpm a ejecuta un script en base a él, bueno, todo parece ser simple, lo tomaré y prometo que transmite nginx y pasaré lo mismo.
Great netcat ayudará aquí (utilidad UNIX para trabajar con tráfico de red, que en mi opinión puede hacer casi cualquier cosa). Entonces configuramos netcat para escuchar en el puerto local y configuramos nginx para que funcione con archivos php a través del socket (por supuesto, el socket en el mismo puerto que netcat escucha)
escuchando el puerto 9000
nc -l 9000
Puede verificar que todo esté bien, puede comunicarse con la dirección 127.0.0.1:9000 a través de un navegador y la siguiente imagen debe ser

configure nginx para que procese los scripts php a través de un socket en el puerto 9000 (en la configuración '/ etc / nginx / sites-available / default', por supuesto, pueden diferir)
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass 127.0.0.1:9000; }
Después de estas manipulaciones, verificaremos qué sucedió contactando el script php a través del navegador

Se puede ver que nginx envió variables de entorno, así como caracteres no imprimibles, es decir, los datos se transmitieron en codificación binaria, lo que significa que no se pueden copiar y enviar tan fácilmente al socket php-fpm . Si los guarda en un archivo, por ejemplo, se guardan en codificación hexadecimal, parecerá que esto se aplica

Pero esto tampoco nos da mucho, probablemente puramente teóricamente se pueden convertir a codificación binaria, de alguna manera (ni siquiera puedo imaginar cómo) enviarlos al zócalo fpm, e incluso existe la posibilidad de que esta bicicleta funcione de alguna manera, e incluso comience algún tipo de un guión, pero de alguna manera es todo feo e incómodo.
Quedó claro que este camino estaba completamente equivocado, puedes ver por ti mismo cuán miserable se ve todo esto, y aún más, todas estas acciones no nos permitirán controlar la conexión, ni nos acercarán a comprender la interacción entre php-fpm y nginx .
¡Todo se ha ido, la especificación no se puede evitar!
Solución (aquí comienza toda la sal de este artículo)
Entrenamiento teórico
Ahora consideremos cómo de todos modos hay una conexión y un intercambio de datos entre nginx y php-fpm . Una pequeña teoría, toda la comunicación se lleva a cabo como ya está claro a través de sockets, consideraremos más específicamente una conexión a través de un socket TCP.
La unidad de información en el protocolo FastCgi es un registro cgi . El servidor envía dichos registros a la aplicación y recibe exactamente los mismos registros en respuesta.
Un poco de teoría (estructura)
A continuación, considere la estructura del registro. Para comprender en qué consiste un registro, debe comprender cómo son las estructuras C y comprender sus designaciones. Para aquellos que no saben más, esto se describirá brevemente (pero suficiente para comprender). Trataré de describirlo de la manera más simple posible, no tiene sentido entrar en detalles aquí, y me temo que me confundiré con los detalles, lo principal es tener un entendimiento común.
Las estructuras son simplemente una colección de bytes, y una notación para ellos les permite ser interpretadas. Es decir, solo tiene una secuencia de ceros y unos, y algunos datos están encriptados en esta secuencia, pero mientras no tenga una anotación para esta secuencia, estos datos no tienen valor para usted, porque No puedes interpretarlos.
// 1101111000000010010110000010011100010000
Lo que es visible aquí, tenemos algunos bits, qué tipo de bits no tenemos idea. Bueno, intentemos, por ejemplo, dividirlos en bytes y representarlos en un sistema decimal
// 5 11011110 00000010 01011000 00100111 00010000 // 222 2 88 39 16
Bueno, los interpretamos y obtuvimos algunos resultados, digamos que estos datos son responsables de cuánto debe un determinado departamento por la electricidad. Resulta que en la casa 222 el apartamento número 2 debe pagar 88 rublos. ¿Y qué más para dos dígitos, qué hacer con ellos solo para soltar? Por supuesto que no! El hecho es que no teníamos una notación (formato) que nos dijera cómo interpretar los datos e interpretarlos a nuestra manera, en este sentido recibimos no solo resultados inútiles, sino también dañinos. Como resultado, el apartamento 2 pagó absolutamente no lo que debería. (los ejemplos son ciertamente exagerados y solo sirven para explicar más claramente la situación)
Ahora veamos cómo debemos interpretar estos datos correctamente, teniendo una notación (formato). Además, llamaré a las cosas por su nombre, a saber notación = formato ( aquí formatos ).
// "Cnn" // //C - (char) (8 ) //n - short (16 ) // 11011110 0000001001011000 0010011100010000 // 222 600 10000
Ahora todo converge en la casa No. 222, el apartamento 600 para la electricidad debería ser de 1000 rublos. Creo que ahora la importancia del formato es clara, y ahora está claro qué aspecto tiene una estructura similar. (preste atención, aquí el objetivo no es explicar en detalle cuáles son estas estructuras, sino dar una comprensión general de lo que es y cómo funciona)
El símbolo de esta estructura será
struct { unsigned char houseNumber; unsigned char flatNumperA1; unsigned char flatNumperA2; unsigned char summB1; unsigned char summB2; }; // , // houseNumber - // flatNumperA1 && flatNumperA2 - // summB1 && summB2 -
Algo más de teoría (entradas FastCgi)
Como dije anteriormente, la unidad de información en el protocolo FastCgi son los registros. El servidor envía los registros a la aplicación y recibe los mismos registros en respuesta. Un registro consta de un encabezado y un cuerpo con datos.
Estructura del encabezado:
- la versión del protocolo (siempre 1) se denota por 1 byte ('C')
- tipo de registro Para abrir, cerrar la conexión, etc. No consideraré todo, solo consideraré lo que se necesita para una tarea específica, si se necesitan otras, agradecemos la especificación aquí. Está indicado por 1 byte ('C').
- El ID de solicitud, un número arbitrario, se denota por 2 bytes ('n')
- La longitud del cuerpo del registro (datos), indicado por 2 bytes ('n')
- la longitud de los datos de alineación y los datos reservados, un byte cada uno (no es necesario prestar especial atención para no distraerse del principal en nuestro caso siempre habrá 0)
El siguiente es el cuerpo del registro:
- los datos en sí (aquí son precisamente las variables que se transfieren) pueden ser bastante grandes (hasta 65535 bytes)
Aquí hay un ejemplo del registro binario FastCgi más simple con 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; };
Practica
Cliente de script y socket de transmisión
Para la transferencia de datos, utilizaremos la extensión de socket php estándar. Y lo primero que hay que hacer es configurar php-fpm para escuchar en el puerto del host local, por ejemplo 9000. Esto se hace en la mayoría de los casos en el archivo '/etc/php/7.3/fpm/pool.d/www.conf', la ruta de acceso por supuesto Depende de la configuración de su sistema. Allí debe registrar algo como lo siguiente (traigo todo el calzado para que pueda navegar, la sección principal es escuchar aquí)
; 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
Después de configurar fpm, el siguiente paso es conectarse al zócalo
$service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port);
Inicio de la solicitud FCGI_BEGIN_REQUEST
Para abrir una conexión, debemos enviar una entrada con el tipo FCGI_BEGIN_REQUEST = 1 El título de la entrada será así (para convertir los valores numéricos a una cadena binaria con el formato especificado, se utilizará el paquete de funciones php)
socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0));
El cuerpo de grabación para abrir una conexión debe contener un rol de grabación y una bandera que controle la conexión.
Entonces, el registro para abrir la conexión se envió con éxito, php-fpm lo aceptará y seguirá esperando de nosotros un registro adicional en el que necesitemos transferir datos para implementar el entorno y ejecutar el script.
Parámetros de entorno de paso FCGI_PARAMS
En este registro, pasaremos todos los parámetros necesarios para implementar el entorno, así como el nombre del script que necesitaremos ejecutar.
Configuración mínima del entorno requerida
$url = '/path/to/script.php' $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ];
Lo primero que debemos hacer aquí es preparar las variables necesarias, es decir, los pares nombre => valor que pasaremos a la aplicación.
La estructura del valor del nombre de los pares será tal
// 128 typedef struct { unsigned char nameLength; unsigned char valueLength; unsigned char nameData unsigned char valueData; }; // 1
Primero hay 1 byte: el nombre es largo, luego 1 byte es el 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
En nuestro caso, tanto el nombre como los significados son cortos y se ajustan a la primera opción, por lo que lo consideraremos.
Codificar nuestras variables de acuerdo con el formato
$keyValueFcgiString = ''; foreach ($env as $key => $value) {
Aquí los valores de menos de 128 bits están codificados por la función chr ($ keyLen) , más que el paquete ('N', $ valLen) , donde 'N' representa 4 bytes. Y luego todo esto se une en una línea de acuerdo con el formato de la estructura. El cuerpo de la grabación está listo.
En el encabezado del registro, transferimos todo igual que en el registro anterior, excepto el tipo (será FCGI_PARAMS = 4) y la longitud de los datos (será igual a la longitud de los pares nombre => valor, o la longitud de la cadena $ keyValueFcgiString que formamos anteriormente).
Obteniendo una respuesta de FCGI_PARAMS
En realidad, después de todo lo anterior, y todo lo que espera ha sido enviado a la aplicación, comienza a funcionar y solo podemos tomar el resultado de este trabajo desde el socket.
Recuerde que en respuesta obtenemos las mismas notas y también tenemos que interpretarlas.
Obtenemos el encabezado, siempre es de 8 bytes (recibiremos datos por byte)
$buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL);
Ahora, de acuerdo con la longitud del cuerpo de respuesta recibida, haremos otra lectura desde el socket
$buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result));
¡Hurra, funcionó! Por fin eso!
¿Qué tenemos en la respuesta, si por ejemplo en este archivo
$url = '/path/to/script.php'
nosotros escribiremos
<?php echo "My fcgi script";
entonces en la respuesta obtenemos como resultado

Resumen
No escribiré mucho aquí, así que resultó el largo artículo. Espero que ella ayude a alguien. Y daré el guión final en sí mismo, resultó ser bastante pequeño. Por supuesto, puede hacer bastante en esta forma, y no tiene manejo de errores y todo esto, pero no lo necesita, lo necesita como ejemplo para mostrar lo básico.
Versión completa del guión <?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);