
Der Backend-Entwickler der Skyeng Mobile App, Sergey Zhuk, schreibt weiterhin gute Bücher. Diesmal veröffentlichte er ein Lehrbuch in russischer Sprache für ein PHP-Mastering-Publikum. Ich bat Sergey, ein nützliches, autarkes Kapitel aus seinem Buch zu teilen und den Habra-Lesern einen Rabattcode zu geben. Unten ist beides.
Lassen Sie uns zunächst sagen, was wir in den vorherigen Kapiteln gestoppt haben.Wir haben unseren einfachen HTTP-Server in PHP geschrieben. Wir haben die Hauptdatei index.php
- das Skript, mit dem der Server index.php
. Hier ist der Code der höchsten Ebene: Wir erstellen eine Ereignisschleife, konfigurieren das Verhalten des HTTP-Servers und starten die Schleife:
use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; $loop = React\EventLoop\Factory::create(); $router = new Router(); $router->load('routes.php'); $server = new Server( function (ServerRequestInterface $request) use ($router) { return $router($request); } ); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); $loop->run();
Zum Weiterleiten von Anforderungen verwendet der Server einen Router:
Routen aus der Datei routes.php
werden in den routes.php
geladen. Jetzt wurden hier nur zwei Routen angekündigt:
use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Main page' ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ];
Bisher ist alles einfach und unsere asynchrone Anwendung passt in mehrere Dateien.
Wir gehen zu „nützlicheren“ Dingen über. Die Antworten aus ein paar Wörtern eines einfachen Textes, die wir in den vorherigen Kapiteln gelernt haben, sehen nicht sehr attraktiv aus. Wir müssen etwas Reales zurückgeben, beispielsweise eine HTML-Seite.
Wo platzieren wir diesen HTML-Code? Natürlich können Sie den Inhalt der Webseite direkt in der Routendatei fest codieren:
Aber tu das nicht! Sie können Geschäftslogik (Routing) nicht mit Präsentation (HTML-Seite) mischen. Warum? Stellen Sie sich vor, Sie müssen etwas im HTML-Code ändern, z. B. die Farbe der Schaltfläche. Und welche Datei muss geändert werden? Datei mit Routen router.php
? Klingt komisch, oder? Nehmen Sie Änderungen am Routing vor, um die Farbe der Schaltfläche zu ändern ...
Daher lassen wir die Routen in Ruhe und erstellen für die HTML-Seiten ein separates Verzeichnis. Fügen Sie im Stammverzeichnis des Projekts ein neues Verzeichnis mit dem Namen pages hinzu. Dann erstellen wir darin die Datei index.html
. Dies wird unsere Hauptseite sein. Hier sind die Inhalte:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ReactPHP App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" > </head> <body> <div class="container"> <div class="row"> <form action="/upload" method="POST" class="justify-content-center"> <div class="form-group"> <label for="text">Text</label> <textarea name="text" id="text" class="form-control"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </body> </html>
Die Seite ist recht einfach, sie enthält nur ein Element - das Formular. Das Formular enthält ein Textfeld und eine Schaltfläche zum Senden. Ich habe auch Bootstrap-Stile hinzugefügt, damit unsere Seite schöner aussieht.
Dateien lesen. Wie man es NICHT macht
Am einfachsten ist es, den Inhalt der Datei im Anforderungshandler zu lesen und diesen Inhalt als Antworttext zurückzugeben. So etwas wie das:
Und übrigens wird es funktionieren. Sie können es selbst versuchen: Starten Sie den Server neu und laden Sie die Seite http://127.0.0.1:8080/
in Ihrem Browser neu.

Also, was ist hier falsch? Und warum nicht? Kurz gesagt, da es Probleme gibt, wenn das Dateisystem langsamer wird.
Blockierende und nicht blockierende Anrufe
Lassen Sie mich Ihnen zeigen, was ich unter "Blockieren" von Anrufen verstehe und was passieren kann, wenn einer der Anforderungshandler Blockierungscode enthält. Fügen Sie der Funktion sleep()
einen Aufruf hinzu, bevor Sie das Antwortobjekt zurückgeben:
Dadurch friert der Anforderungshandler 10 Sekunden lang ein, bevor er eine Antwort mit dem Inhalt der HTML-Seite zurückgeben kann. Bitte beachten Sie, dass wir den Handler für die /upload
Adresse nicht berührt haben. Durch Aufrufen der Funktion sleep(10)
emuliere ich die Ausführung einer Art Blockierungsoperation.
Was haben wir also? Wenn der Browser die Seite /
anfordert, wartet der Handler 10 Sekunden und gibt dann die HTML-Seite zurück. Wenn wir die /upload
Adresse öffnen, sollte der Handler sofort eine Antwort mit der Zeichenfolge 'Upload-Seite' zurückgeben.
Nun wollen wir sehen, was in der Realität passiert. Wie immer starten wir den Server neu. Öffnen Sie jetzt ein weiteres Fenster in Ihrem Browser. Geben Sie in die Adressleiste http://127.0.0.1:8080/upload ein , öffnen Sie diese Seite jedoch nicht sofort. Lassen Sie diese Adresse vorerst in der Adressleiste. Gehen Sie dann zum ersten Browserfenster und öffnen Sie die Seite http://127.0.0.1:8080/ darin. Während diese Seite geladen wird (denken Sie daran, dass dies 10 Sekunden dauert), gehen Sie schnell zum zweiten Fenster und drücken Sie die Eingabetaste, um die Adresse zu laden, die in der Adressleiste verblieben ist ( http://127.0.0.1:8080/upload ). .
Was haben wir bekommen? Ja, das Laden der Adresse / dauert erwartungsgemäß 10 Sekunden. Überraschenderweise dauerte das Laden der zweiten Seite genauso lange, obwohl wir keine sleep()
-Aufrufe hinzugefügt haben. Irgendeine Idee, warum das passiert ist?
ReactPHP wird in einem einzelnen Thread ausgeführt. Es mag den Anschein haben, dass in einer asynchronen Anwendung Aufgaben parallel ausgeführt werden, in Wirklichkeit ist dies jedoch nicht der Fall. Die Illusion der Parallelität entsteht durch einen Zyklus von Ereignissen, der ständig zwischen verschiedenen Aufgaben wechselt und diese ausführt. Zu einem bestimmten Zeitpunkt wird jedoch immer nur eine Aufgabe ausgeführt. Dies bedeutet, dass wenn eine dieser Aufgaben zu lange dauert, die Ereignisschleife blockiert wird, wodurch keine neuen Ereignisse registriert und Handler für sie aufgerufen werden können. Und das führt letztendlich zum „Einfrieren“ der gesamten Anwendung, sie verliert einfach die Asynchronität.
OK, aber was hat das mit dem Aufruf von file_get_contents('pages/index.h')
zu tun? Das Problem hierbei ist, dass wir direkt auf das Dateisystem zugreifen. Im Vergleich zu anderen Vorgängen wie dem Arbeiten mit Speicher oder Computer kann das Arbeiten mit dem Dateisystem extrem langsam sein. Wenn sich beispielsweise herausstellt, dass die Datei zu groß ist oder die Festplatte selbst langsam ist, kann das Lesen der Datei einige Zeit dauern und die Ereignisschleife blockieren.
Im Standard-Synchronmodell ist die Anforderung-Antwort kein Problem. Wenn der Client eine zu schwere Datei angefordert hat, wartet er, bis diese Datei heruntergeladen wurde. Eine solch schwere Anfrage betrifft andere Kunden nicht. In unserem Fall handelt es sich jedoch um ein asynchrones ereignisorientiertes Modell. Wir haben einen HTTP-Server gestartet, der eingehende Anfragen ständig verarbeiten muss. Wenn eine Anforderung zu lange dauert, wirkt sich dies auf alle anderen Server-Clients aus.
Denken Sie in der Regel daran:
- Sie können niemals eine Ereignisschleife blockieren.
Wie lesen wir dann die Datei asynchron? Und hier kommen wir zur zweiten Regel:
- Wenn eine Blockierungsoperation nicht vermieden werden kann, sollte sie in den untergeordneten Prozess eingebunden werden und die asynchrone Ausführung im Hauptthread fortsetzen.
Nachdem wir gelernt haben, wie man es nicht macht, wollen wir die richtige nicht blockierende Lösung diskutieren.
Untergeordneter Prozess
Die gesamte Kommunikation mit dem Dateisystem in einer asynchronen Anwendung muss in untergeordneten Prozessen erfolgen. Um untergeordnete Prozesse in einer ReactPHP-Anwendung zu verwalten, müssen wir eine weitere Komponente " untergeordneter Prozess" installieren. Mit dieser Komponente können Sie auf die Funktionen des Betriebssystems zugreifen, um einen beliebigen Systembefehl innerhalb des untergeordneten Prozesses auszuführen. Öffnen Sie zum Installieren dieser Komponente ein Terminal im Stammverzeichnis des Projekts und führen Sie den folgenden Befehl aus:
composer require react/child-process
Windows-Kompatibilität
Unter Windows werden die Threads STDIN, STDOUT und STDERR blockiert, was bedeutet, dass die untergeordnete Prozesskomponente nicht ordnungsgemäß funktionieren kann. Daher ist diese Komponente hauptsächlich für die Verwendung auf nix-Systemen ausgelegt. Wenn Sie versuchen, ein Objekt der Process-Klasse auf einem Windows-System zu erstellen, wird eine Ausnahme ausgelöst. Die Komponente kann jedoch unter Windows Subsystem for Linux (WSL) ausgeführt werden . Wenn Sie diese Komponente unter Windows verwenden möchten, müssen Sie WSL installieren.
Jetzt können wir jeden Shell-Befehl innerhalb des untergeordneten Prozesses ausführen. Öffnen Sie die Datei routes.php
und ändern Sie den Handler für die Datei /
route. Erstellen Sie ein Objekt der Klasse React\ChildProcess\Process
, und übergeben Sie als Befehl ls
an das Objekt, um den Inhalt des aktuellen Verzeichnisses React\ChildProcess\Process
:
Dann müssen wir den Prozess starten, indem wir die start()
-Methode aufrufen. Der Haken ist, dass die start()
-Methode ein Ereignisschleifenobjekt benötigt. In der Datei routes.php
wir dieses Objekt jedoch nicht. Wie übergeben wir die Ereignisschleife von index.php
an Routen direkt an den Anforderungshandler? Die Lösung für dieses Problem ist "Abhängigkeitsinjektion".
Abhängigkeitsinjektion
Eine unserer Routen benötigt also eine Ereignisschleife, um zu funktionieren. In unserer Anwendung kennt nur eine Komponente die Existenz von Routen - die Router
Klasse. Es stellt sich heraus, dass es in seiner Verantwortung liegt, eine Ereignisschleife für die Routen bereitzustellen. Mit anderen Worten, der Router benötigt eine Ereignisschleife oder hängt von der Ereignisschleife ab. Wie drücken wir diese Abhängigkeit explizit im Code aus? Wie kann man es unmöglich machen, einen Router zu erstellen, ohne eine Ereignisschleife an ihn zu übergeben? Natürlich über den Konstruktor der Router
Klasse. Öffnen Sie Router.php
und fügen Sie den Konstruktor der Router
Klasse hinzu:
use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; }
Speichern Sie im Konstruktor die übergebene Ereignisschleife in der Eigenschaft private $loop
. Dies ist eine Abhängigkeitsinjektion, wenn wir der Klasse die Objekte zur Verfügung stellen, die sie benötigt, um außen zu arbeiten.
Nachdem wir diesen neuen Konstruktor haben, müssen wir die Erstellung des Routers aktualisieren. Öffnen Sie die Datei index.php
und korrigieren Sie die Zeile, in der wir das Objekt der Router
Klasse erstellen:
Fertig. Gehen Sie zurück zu routes.php
. Wie Sie wahrscheinlich bereits vermutet haben, können wir hier dieselbe Idee mit der Abhängigkeitsinjektion verwenden und unseren Abfragehandlern eine Ereignisschleife als zweiten Parameter hinzufügen. Ändern Sie den ersten Rückruf und fügen Sie das zweite Argument hinzu: ein Objekt, das LoopInterface
implementiert:
Als nächstes müssen wir die Ereignisschleife an die start()
-Methode des untergeordneten Prozesses übergeben. Und woher bekommt der Handler die Ereignisschleife? Und es ist bereits im Router in der privaten Eigenschaft $loop
gespeichert. Wir müssen es nur übergeben, wenn der Handler aufgerufen wird.
Öffnen Sie die Router
Klasse, aktualisieren Sie die __invoke()
-Methode und fügen Sie dem Aufruf des Anforderungshandlers das zweite Argument hinzu:
public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request, $this->loop); }
Das ist alles! Dies ist wahrscheinlich genug Abhängigkeitsinjektion . Eine ziemlich große Reise des Zyklus der Ereignisse ist passiert, richtig? Von der Datei index.php
zur Router
Klasse und dann von der Router
Klasse zur Datei routes.php
direkt in den Rückrufen.
Um zu bestätigen, dass der untergeordnete Prozess seine nicht blockierende Magie ping 8.8.8.8
ersetzen wir den einfachen ls
durch den schwereren ping 8.8.8.8
. Starten Sie den Server neu und versuchen Sie erneut, zwei Seiten in zwei verschiedenen Fenstern zu öffnen. Zuerst http://127.0.0.1:8080/
und dann /upload
. Beide Seiten werden schnell und ohne Verzögerung geöffnet, obwohl der ping
Befehl im ersten Handler im Hintergrund ausgeführt wird. Dies bedeutet übrigens, dass wir jeden teuren Vorgang (z. B. die Verarbeitung großer Dateien) verzweigen können, ohne die Hauptanwendung zu blockieren.
Binden Sie den untergeordneten Prozess und die Antwort mithilfe von Threads
Kehren wir zu unserer Anwendung zurück. Wir haben also einen untergeordneten Prozess erstellt und gestartet, aber unser Browser zeigt die Ergebnisse einer gegabelten Operation in keiner Weise an. Lass es uns reparieren.
Wie können wir mit dem untergeordneten Prozess kommunizieren? In unserem Fall wird ein ls
, der den Inhalt des aktuellen Verzeichnisses anzeigt. Wie kommen wir zu dieser Schlussfolgerung und senden sie dann an den Hauptteil der Antwort? Die kurze Antwort lautet: Threads.
Lassen Sie uns ein wenig über Prozesse sprechen. Jeder Shell-Befehl, den Sie ausführen, verfügt über drei Datenströme: STDIN, STDOUT und STDERR. Stream zur Standardausgabe und -eingabe sowie Stream für Fehler. Wenn wir beispielsweise den ls
ausführen, wird das Ergebnis dieses Befehls direkt an STDOUT (auf dem Terminalbildschirm) gesendet. Wenn wir also die Ausgabe eines Prozesses erhalten möchten, ist der Zugriff auf den Ausgabestream erforderlich. Und es ist so einfach wie das Schälen von Birnen. Ersetzen Sie beim file_get_contents()
des file_get_contents()
Aufruf file_get_contents()
durch $childProcess->stdout
:
return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout );
Alle untergeordneten Prozesse haben drei Eigenschaften, die sich auf stdio
Streams beziehen: stdout
, stdin
und stderr
. In unserem Fall möchten wir die Ausgabe des Prozesses auf einer Webseite anzeigen. Anstelle einer Zeichenfolge im Konstruktor der Response
Klasse übergeben wir einen Stream als drittes Argument. Die Response
Klasse ist intelligent genug, um zu erkennen, dass sie den Stream empfangen hat, und ihn entsprechend zu verarbeiten.
Wie immer starten wir den Server neu und sehen, was wir getan haben. Öffnen wir die Seite http://127.0.0.1:8080/
im Browser: Sie sollten eine Liste der Dateien des Projektstammordners sehen.

Der letzte Schritt besteht darin, den ls
durch etwas Nützlicheres zu ersetzen. Wir haben dieses Kapitel mit dem Rendern der Datei pages/index.html
mit der Funktion file_get_contents()
. Jetzt können wir diese Datei absolut asynchron lesen, ohne befürchten zu müssen, dass sie unsere Anwendung blockiert. Ersetzen Sie den ls
durch cat pages/index.html
.
Wenn Sie mit dem cat
nicht vertraut sind, wird er zum Verketten und Ausgeben von Dateien verwendet. Meistens wird dieser Befehl verwendet, um eine Datei zu lesen und ihren Inhalt in die Standardausgabe auszugeben. Der Befehl cat pages/index.html
liest die Datei cat pages/index.html
und druckt ihren Inhalt in STDOUT. Und wir senden bereits stdout
als Antwortstelle. Hier ist die endgültige Version der Datei routes.php
:
Infolgedessen wurde der gesamte Code nur benötigt, um einen Aufruf der Funktion file_get_contents()
zu ersetzen. Abhängigkeitsinjektion, Übergeben eines Ereignisschleifenobjekts, Hinzufügen untergeordneter Prozesse und Arbeiten mit Threads. All dies dient nur dazu, einen Funktionsaufruf zu ersetzen. War es das wert? Antwort: Ja, es hat sich gelohnt. Wenn etwas die Ereignisschleife blockieren kann und das Dateisystem definitiv kann, stellen Sie sicher, dass es irgendwann blockiert wird, und zwar im ungünstigsten Moment.
Das Erstellen eines untergeordneten Prozesses jedes Mal, wenn wir auf das Dateisystem zugreifen müssen, kann wie zusätzlicher Aufwand aussehen, der sich auf die Geschwindigkeit und Leistung unserer Anwendung auswirkt. Leider gibt es in PHP keine andere Möglichkeit, asynchron mit dem Dateisystem zu arbeiten. Alle asynchronen PHP-Bibliotheken verwenden untergeordnete Prozesse (oder Erweiterungen, die sie abstrahieren).
Habra-Leser können das gesamte Buch unter diesem Link mit einem Rabatt kaufen.
Und wir erinnern Sie daran, dass wir immer auf der Suche nach coolen Entwicklern sind ! Komm, wir haben Spaß!