Ich begrüße alle Leser von "Habr".
Haftungsausschluss
Der Artikel erwies sich als ziemlich lang und für diejenigen, die den Hintergrund nicht lesen wollen, aber direkt auf den Punkt kommen möchten, bitte ich Sie direkt zum Kapitel "Lösung".
Eintrag
In diesem Artikel möchte ich über die Lösung eines eher ungewöhnlichen Problems sprechen, mit dem ich während des Arbeitsprozesses konfrontiert war. Wir mussten nämlich eine Reihe von PHP-Skripten in einer Schleife ausführen. Ich werde die Gründe und Kontroversen einer solchen architektonischen Lösung in diesem Artikel nicht diskutieren, weil Tatsächlich ging es überhaupt nicht darum, es war nur eine Aufgabe, es musste gelöst werden und die Lösung schien mir interessant genug, um sie mit Ihnen zu teilen, zumal ich im Internet überhaupt kein Mana gefunden habe (natürlich mit Ausnahme der offiziellen Spezifikationen). Speck ist natürlich gut, und natürlich ist alles in ihnen, aber ich denke, Sie werden mir zustimmen, dass es immer noch ein Vergnügen ist, sie zu verstehen, wenn Sie mit dem Thema nicht besonders vertraut und zeitlich begrenzt sind.
Für wen ist dieser Artikel?
Jeder, der mit dem Web und dem FastCgi- Protokoll arbeitet , weiß nur , dass dies das Protokoll ist, nach dem der Webserver PHP-Skripte ausführt, möchte es aber genauer studieren und unter die Haube schauen.
Begründung (warum dieser Artikel)
Wie ich oben schrieb, als wir vor der Notwendigkeit standen, viele PHP-Skripte ohne die Teilnahme eines Webservers auszuführen (grob gesagt von einem anderen PHP-Skript), fiel mir als Erstes Folgendes ein ...
shell_exec('php \path\to\script.php')
Zu Beginn jedes Skripts wird jedoch eine Umgebung erstellt und ein separater Prozess gestartet. Im Allgemeinen schien dies für Ressourcen irgendwie kostspielig zu sein. Diese Implementierung wurde abgelehnt. Das zweite, was mir in den Sinn kam, ist natürlich php-fpm , es ist so cool, es startet die Umgebung nur einmal, überwacht den Speicher, protokolliert alles dort korrekt, startet und stoppt Skripte, im Allgemeinen ist alles cool, und natürlich hat uns das gefallen mehr.
Aber es ist ein Pech, theoretisch wussten wir, wie es im Allgemeinen funktioniert (da es sich als sehr allgemein herausstellte), aber es stellte sich als ziemlich schwierig heraus, dieses Protokoll in der Praxis ohne die Teilnahme eines Webservers zu implementieren. Das Lesen der Spezifikationen und einige Stunden erfolgloser Versuche zeigten, dass die Implementierung einige Zeit in Anspruch nehmen würde, was wir zu diesem Zeitpunkt noch nicht hatten. Es gibt kein Mana für die Implementierung dieses Vorhabens, in dem diese Interaktion einfach und klar beschrieben werden könnte. Wir konnten auch keine Spezifikationen übernehmen. Aus den vorgefertigten Lösungen haben wir ein Python-Skript und eine Pykhov-Bibliothek auf dem Github gefunden, die am Ende nicht in mein Projekt gezogen werden wollten (vielleicht) Es ist nicht richtig, aber nicht wirklich. Wir lieben alle Arten von Bibliotheken von Drittanbietern und sogar nicht sehr beliebte und daher nicht getestete Bibliotheken. Im Allgemeinen haben wir als Ergebnis dieser Idee all dies durch das gute alte Kaninchen abgelehnt und umgesetzt.
Obwohl das Problem endlich gelöst war, habe ich mich entschlossen, FastCgi im Detail zu verstehen, und außerdem habe ich beschlossen, einen Artikel darüber zu schreiben, in dem einfach und ausführlich beschrieben wird, wie man php-fpm dazu bringt , ein PHP-Skript ohne Webserver oder besser gesagt als auszuführen Der Webserver wird ein anderes Skript haben, dann werde ich es den Fcgi-Client nennen. Generell hoffe ich, dass dieser Artikel denjenigen hilft, die vor der gleichen Aufgabe stehen wie wir, und nach dem Lesen schnell alles schreiben kann, was er braucht.
Kreative Suche (falscher Pfad)
Damit das Problem angezeigt wird, müssen wir mit der Lösung fortfahren. Natürlich habe ich, wie jeder "normale" Programmierer, die Spezifikation nicht gelesen und übersetzt, um ein Problem zu lösen, über das nirgendwo geschrieben ist, was zu tun ist und was in die Konsole einzugeben ist, sondern sofort meine eigene "brillante" Lösung gefunden. Sein Wesen ist wie folgt: Ich weiß, dass Nginx (wir verwenden Nginx und um keine dummen Dinge weiter zu schreiben - ein Webserver, ich werde Nginx schreiben, da es sympathischer ist) etwas an php-fpm überträgt, es verarbeitet auch php-fpm an Auf dieser Grundlage wird ein Skript ausgeführt. Nun, alles scheint einfach zu sein. Ich nehme es und verspreche es, das Nginx überträgt, und ich werde das Gleiche bestehen.
Hier hilft ein großartiger Netcat (UNIX-Dienstprogramm für die Arbeit mit Netzwerkverkehr, das meiner Meinung nach fast alles kann). Also setzen wir netcat so , dass es den lokalen Port überwacht und nginx so konfiguriert, dass es mit PHP-Dateien über den Socket arbeitet (natürlich den Socket an demselben Port, den Netcat abhört).
9000 Port hören
nc -l 9000
Sie können überprüfen, ob alles in Ordnung ist, Sie können die Adresse 127.0.0.1:9000 über einen Browser kontaktieren und das folgende Bild sollte sein

Konfigurieren Sie nginx so, dass es PHP-Skripte über einen Socket an Port 9000 verarbeitet (in den Einstellungen '/ etc / nginx / sites-available / default' können sie natürlich abweichen).
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass 127.0.0.1:9000; }
Nach diesen Manipulationen werden wir überprüfen, was passiert ist, indem wir das PHP-Skript über den Browser kontaktieren

Es ist ersichtlich, dass nginx Umgebungsvariablen sowie nicht druckbare Zeichen gesendet hat, dh die Daten wurden in binärer Codierung übertragen, was bedeutet, dass sie nicht so einfach kopiert und an den PHP-Fpm-Socket gesendet werden können . Wenn Sie sie beispielsweise in einer Datei speichern, werden sie in hexadezimaler Codierung gespeichert. Dies sieht anscheinend so aus

Aber das gibt uns auch nicht viel, wahrscheinlich rein theoretisch können sie in eine binäre Codierung konvertiert werden, irgendwie (ich kann mir nicht einmal vorstellen, wie), um sie an die fpm-Buchse zu senden, und es besteht sogar die Möglichkeit, dass dieses ganze Fahrrad irgendwie funktioniert und sogar eine Art startet ein Skript, aber irgendwie ist alles hässlich und umständlich.
Es wurde klar, dass dieser Weg völlig falsch ist. Sie können selbst sehen, wie miserabel das alles aussieht, und noch mehr, all diese Aktionen werden es uns nicht ermöglichen, die Verbindung zu kontrollieren, und sie werden uns auch nicht näher bringen, die Interaktion zwischen php-fpm und nginx zu verstehen.
Alles ist weg, die Spezifikation kann nicht vermieden werden!
Lösung (hier beginnt tatsächlich das ganze Salz dieses Artikels)
Theoretische Ausbildung
Betrachten wir nun, wie es trotzdem eine Verbindung und einen Datenaustausch zwischen nginx und php-fpm gibt . Eine kleine Theorie, alle Kommunikation findet statt, wie bereits durch Sockets klar ist, wir werden weiter speziell eine Verbindung über einen TCP-Socket betrachten.
Die Informationseinheit im FastCgi- Protokoll ist ein CGI-Datensatz . Der Server sendet solche Datensätze an die Anwendung und empfängt als Antwort genau dieselben Datensätze.
Ein bisschen Theorie (Struktur)
Betrachten Sie als nächstes die Struktur des Datensatzes. Um zu verstehen, woraus ein Datensatz besteht, müssen Sie verstehen, wie C-Strukturen aussehen, und ihre Bezeichnungen verstehen. Für diejenigen, die es nicht weiter wissen, wird dies kurz beschrieben (aber genug zum Verständnis). Ich werde versuchen, es so einfach wie möglich zu beschreiben. Es ist sinnlos, hier auf Details einzugehen, und ich befürchte, dass ich in den Details verwirrt werde. Hauptsache, ich habe ein gemeinsames Verständnis.
Strukturen sind einfach eine Sammlung von Bytes, und eine Notation für sie ermöglicht die Interpretation. Das heißt, Sie haben nur eine Folge von Nullen und Einsen, und einige Daten werden in dieser Folge verschlüsselt. Solange Sie jedoch keine Anmerkung zu dieser Folge haben, sind diese Daten für Sie wertlos, da Sie können sie nicht interpretieren.
// 1101111000000010010110000010011100010000
Was hier sichtbar ist, haben wir einige Bits, welche Art von Bits wir keine Ahnung haben. Versuchen wir zum Beispiel, sie in Bytes zu unterteilen und im Dezimalsystem darzustellen
// 5 11011110 00000010 01011000 00100111 00010000 // 222 2 88 39 16
Nun, wir haben sie interpretiert und einige Ergebnisse erzielt. Nehmen wir an, diese Daten sind dafür verantwortlich, wie viel eine bestimmte Wohnung für Strom schuldet. Es stellt sich heraus, dass in Haus 222 Wohnung Nummer 2 88 Rubel bezahlen muss. Und was noch für zwei Ziffern, was tun mit ihnen, nur um fallen zu lassen? Natürlich nicht! Tatsache ist, dass wir keine Notation (Format) hatten, die uns sagen würde, wie wir die Daten interpretieren und auf unsere eigene Weise interpretieren sollen. In dieser Hinsicht haben wir nicht nur nutzlose, sondern auch schädliche Ergebnisse erhalten. Infolgedessen zahlte Wohnung 2 absolut nicht das, was es haben sollte. (Die Beispiele sind sicherlich weit hergeholt und dienen nur dazu, die Situation klarer zu erklären)
Nun wollen wir sehen, wie wir diese Daten mit einer Notation (Format) richtig interpretieren sollen. Weiter werde ich einen Spaten einen Spaten nennen, nämlich Notation = Format ( hier Formate ).
// "Cnn" // //C - (char) (8 ) //n - short (16 ) // 11011110 0000001001011000 0010011100010000 // 222 600 10000
Jetzt läuft alles im Haus Nr. 222 zusammen, Wohnung 600 für Strom sollte 1000 Rubel betragen. Ich denke, jetzt ist die Bedeutung des Formats klar, und jetzt ist klar, wie ungefähr eine ähnliche Struktur aussieht. (Bitte beachten Sie, hier geht es nicht darum, diese Strukturen im Detail zu erklären, sondern ein allgemeines Verständnis dafür zu vermitteln, was es ist und wie es funktioniert.)
Das Symbol dieser Struktur wird sein
struct { unsigned char houseNumber; unsigned char flatNumperA1; unsigned char flatNumperA2; unsigned char summB1; unsigned char summB2; }; // , // houseNumber - // flatNumperA1 && flatNumperA2 - // summB1 && summB2 -
Noch etwas Theorie (FastCgi-Einträge)
Wie oben erwähnt, besteht die Informationseinheit im FastCgi-Protokoll aus Aufzeichnungen. Der Server sendet die Datensätze an die Anwendung und empfängt als Antwort dieselben Datensätze. Ein Datensatz besteht aus einem Header und einem Body mit Daten.
Header-Struktur:
- Die Protokollversion (immer 1) wird mit 1 Byte ('C') bezeichnet.
- Datensatztyp. Um die Verbindung zu öffnen, zu schließen usw. Ich werde nicht alles berücksichtigen, dann werde ich nur berücksichtigen, was für eine bestimmte Aufgabe benötigt wird. Wenn andere benötigt werden, begrüßen Sie die Spezifikation hier. Es wird durch 1 Byte ('C') angezeigt.
- Die Anforderungs-ID, eine beliebige Zahl, wird mit 2 Bytes ('n') bezeichnet.
- die Länge des Hauptteils des Datensatzes (Daten), angegeben durch 2 Bytes ('n')
- Die Länge der Ausrichtungsdaten und der reservierten Daten beträgt jeweils ein Byte (es ist nicht erforderlich, besondere Aufmerksamkeit zu schenken, um nicht von der Hauptdaten abgelenkt zu werden. In unserem Fall gibt es immer 0).
Als nächstes folgt der Hauptteil der Aufzeichnung:
- Die Daten selbst (hier werden genau die Variablen übertragen) können sehr groß sein (bis zu 65535 Byte).
Hier ist ein Beispiel für den einfachsten FastCgi-Binärdatensatz mit 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; };
Übe
Skript-Client und Sende-Socket
Für die Datenübertragung verwenden wir die Standard-PHP-Socket-Erweiterung. Als erstes muss php-fpm so konfiguriert werden, dass der Port auf dem lokalen Host überwacht wird, z. B. 9000. Dies geschieht in den meisten Fällen in der Datei '/etc/php/7.3/fpm/pool.d/www.conf', dem Pfad natürlich Hängt von Ihren Systemeinstellungen ab. Dort müssen Sie etwas wie das Folgende registrieren (ich bringe das ganze Fußtuch mit, damit Sie navigieren können, der Hauptabschnitt ist hier zu hören)
; 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
Nach dem Einrichten von fpm besteht der nächste Schritt darin, eine Verbindung zum Socket herzustellen
$service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port);
Start der FCGI_BEGIN_REQUEST-Anforderung
Um eine Verbindung herzustellen, müssen wir einen Eintrag vom Typ FCGI_BEGIN_REQUEST = 1 senden. Der Titel des Eintrags lautet wie folgt (um numerische Werte in eine Binärzeichenfolge mit dem angegebenen Format zu konvertieren, wird das PHP-Funktionspaket () verwendet).
socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0));
Der Aufzeichnungskörper zum Öffnen einer Verbindung muss eine Aufzeichnungsrolle und ein Flag enthalten, das die Verbindung steuert
Der Datensatz zum Öffnen der Verbindung wurde erfolgreich gesendet. PHP-fpm akzeptiert ihn und erwartet weiterhin von uns einen weiteren Datensatz, in dem wir Daten übertragen müssen, um die Umgebung bereitzustellen und das Skript auszuführen.
Übergeben von Umgebungsparametern FCGI_PARAMS
In diesem Datensatz übergeben wir alle Parameter, die zum Bereitstellen der Umgebung erforderlich sind, sowie den Namen des Skripts, das ausgeführt werden muss.
Minimal erforderliche Umgebungseinstellungen
$url = '/path/to/script.php' $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ];
Das erste, was wir hier tun müssen, ist, die erforderlichen Variablen vorzubereiten, dh die Namen => Wertepaare, die wir an die Anwendung übergeben.
Die Struktur des Namensnamens des Paares wird so sein
// 128 typedef struct { unsigned char nameLength; unsigned char valueLength; unsigned char nameData unsigned char valueData; }; // 1
Zuerst gibt es 1 Byte - der Name ist lang, dann ist 1 Byte der Wert
// 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
In unserem Fall sind sowohl der Name als auch die Bedeutung kurz und passen zur ersten Option, daher werden wir darüber nachdenken.
Codieren Sie unsere Variablen entsprechend dem Format
$keyValueFcgiString = ''; foreach ($env as $key => $value) {
Hier werden Werte von weniger als 128 Bit von der Funktion chr ($ keyLen) codiert, mehr als pack ('N', $ valLen) , wobei 'N' für 4 Bytes steht. Und dann wird all dies in einer Zeile entsprechend dem Format der Struktur zusammengeklebt. Der Hauptteil der Aufnahme ist fertig.
In der Kopfzeile des Datensatzes übertragen wir alles wie im vorherigen Datensatz, mit Ausnahme des Typs (FCGI_PARAMS = 4) und der Datenlänge (entspricht der Länge der Namen => Wertepaare oder der Länge der Zeichenfolge $ keyValueFcgiString, die wir zuvor gebildet haben).
Eine Antwort von FCGI_PARAMS erhalten
Nachdem alle vorherigen Schritte ausgeführt wurden und alles, was erwartet wird, an die Anwendung gesendet wurde, funktioniert sie tatsächlich und wir können das Ergebnis dieser Arbeit nur aus dem Socket entnehmen.
Denken Sie daran, dass wir als Antwort dieselben Notizen erhalten und diese auch interpretieren müssen.
Wir bekommen den Header, es sind immer 8 Bytes (wir erhalten Daten pro Byte)
$buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL);
In Übereinstimmung mit der Länge des empfangenen Antwortkörpers werden wir nun eine weitere Messung aus dem Socket durchführen
$buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result));
Hurra, es hat funktioniert! Endlich das!
Was haben wir in der Antwort, wenn zum Beispiel in dieser Datei
$url = '/path/to/script.php'
wir werden schreiben
<?php echo "My fcgi script";
dann in der Antwort erhalten wir als Ergebnis

Zusammenfassung
Ich werde hier nicht viel schreiben, daher stellte sich der lange Artikel heraus. Ich hoffe sie hilft jemandem. Und ich werde das endgültige Drehbuch selbst geben, es stellte sich als ziemlich klein heraus. Natürlich kann er in dieser Form einiges tun, und er hat keine Fehlerbehandlung und all dies, aber er braucht es nicht, er braucht ihn als Beispiel, um die Grundlagen zu zeigen.
Vollversion des Skripts <?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);