Rendu de fichier HTML: un chapitre du livre ReactPHP for Beginners de Skyeng


Le développeur du backend de l'application mobile Skyeng, Sergey Zhuk, continue d'écrire de bons livres. Cette fois, il a publié un manuel en russe pour un public maîtrisant PHP. J'ai demandé à Sergey de partager un chapitre utile et autosuffisant de son livre, et de donner aux lecteurs Habra un code de réduction. Ci-dessous, les deux.


Tout d'abord, disons ce que nous avons arrêté dans les chapitres précédents.

Nous avons écrit notre simple serveur HTTP en PHP. Nous avons le fichier index.php principal - le script qui démarre le serveur. Voici le code de plus haut niveau: nous créons une boucle d'événements, configurons le comportement du serveur HTTP et démarrons la boucle:


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

Pour acheminer les demandes, le serveur utilise un routeur:


 // src/Router.php use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; class Router { private $routes = []; public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request); } public function load($filename) { $routes = require $filename; foreach ($routes as $path => $handler) { $this->add($path, $handler); } } public function add($path, callable $handler) { $this->routes[$path] = $handler; } private function notFound($path) { return function () use ($path) { return new Response( 404, ['Content-Type' => 'text/html; charset=UTF-8'], "No request handler found for $path" ); }; } } 

Les routes du fichier routes.php sont chargées dans le routes.php . Maintenant, seuls deux itinéraires ont été annoncés ici:


 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' ); }, ]; 

Jusqu'à présent, tout est simple, et notre application asynchrone tient dans plusieurs fichiers.


Nous passons à des choses plus «utiles». Les réponses de quelques mots d'un texte simple que nous avons appris à tirer dans les chapitres précédents ne semblent pas très attrayantes. Nous devons renvoyer quelque chose de réel, comme une page HTML.


Alors, où mettons-nous ce HTML? Bien sûr, vous pouvez coder en dur le contenu de la page Web directement dans le fichier routes:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { $html = <<<HTML <!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8”> <title>ReactPHP App</title> </head> <body> Hello, world </body> </html> HTML; return new Response( 200, ['Content-Type' => 'text/html'], $html ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Mais ne fais pas ça! Vous ne pouvez pas mélanger la logique métier (routage) avec la présentation (page HTML). Pourquoi? Imaginez que vous devez changer quelque chose dans le code HTML, par exemple, la couleur du bouton. Et quel fichier devra être changé? Fichier avec routes router.php ? Ça a l'air bizarre, non? Modifiez le routage pour modifier la couleur du bouton ...


Par conséquent, nous laisserons les routes seules et pour les pages HTML, nous créerons un répertoire séparé. À la racine du projet, ajoutez un nouveau répertoire appelé pages. Ensuite, à l'intérieur, nous créons le fichier index.html . Ce sera notre page principale. Voici son contenu:


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

La page est assez simple, elle ne contient qu'un seul élément - le formulaire. Le formulaire à l'intérieur a une zone de texte et un bouton pour soumettre. J'ai également ajouté des styles Bootstrap pour rendre notre page plus belle.


Lecture de fichiers. Comment NE PAS faire


L'approche la plus simple consiste à lire le contenu du fichier à l'intérieur du gestionnaire de demande et à renvoyer ce contenu en tant que corps de réponse. Quelque chose comme ça:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

Et en passant, cela fonctionnera. Vous pouvez l'essayer vous-même: redémarrez le serveur et rechargez la page http://127.0.0.1:8080/ dans votre navigateur.



Alors qu'est-ce qui ne va pas ici? Et pourquoi ne pas faire ça? En bref, car il y aura des problèmes si le système de fichiers commence à ralentir.


Appels bloquants et non bloquants


Permettez-moi de vous montrer ce que j'entends par «bloquer» les appels et ce qui peut se produire lorsque l'un des gestionnaires de demandes contient du code de blocage. Avant de renvoyer l'objet de réponse, ajoutez un appel à la fonction sleep() :


 // routes.php return [ '/' => function (ServerRequestInterface $request) { sleep(10); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Cela fera geler le gestionnaire de requêtes pendant 10 secondes avant de pouvoir renvoyer une réponse avec le contenu de la page HTML. Veuillez noter que nous n'avons pas touché le gestionnaire pour l'adresse /upload . En appelant la fonction sleep(10) , j'émule l'exécution d'une sorte d'opération de blocage.


Alors qu'avons-nous? Lorsque le navigateur demande la page / , le gestionnaire attend 10 secondes, puis renvoie la page HTML. Lorsque nous ouvrons l'adresse /upload , son gestionnaire doit immédiatement renvoyer une réponse avec la chaîne 'Upload page'.


Voyons maintenant ce qui se passe dans la réalité. Comme toujours, nous redémarrons le serveur. Maintenant, veuillez ouvrir une autre fenêtre dans votre navigateur. Dans la barre d'adresse, entrez http://127.0.0.1:8080/upload , mais n'ouvrez pas cette page immédiatement. Laissez cette adresse dans la barre d'adresse pour l'instant. Ensuite, allez dans la première fenêtre du navigateur et ouvrez la page http://127.0.0.1:8080/ . Pendant le chargement de cette page (rappelez-vous que cela prendra 10 secondes), accédez rapidement à la deuxième fenêtre et appuyez sur «Entrée» pour charger l'adresse qui a été laissée dans la barre d'adresse ( http://127.0.0.1:8080/upload ) .


Qu'avons-nous obtenu? Oui, l'adresse /, comme prévu, prend 10 secondes à charger. Mais, étonnamment, la deuxième page a pris le même temps à charger, bien que nous n'y ayons ajouté aucun appel sleep() . Une idée de pourquoi c'est arrivé?


ReactPHP s'exécute dans un seul thread. Il peut sembler que dans une application asynchrone, les tâches soient exécutées en parallèle, mais en réalité ce n'est pas le cas. L'illusion du parallélisme est créée par un cycle d'événements qui bascule constamment entre diverses tâches et les exécute. Mais à un certain moment, une seule tâche est toujours effectuée. Cela signifie que si l'une de ces tâches prend trop de temps, elle bloquera la boucle d'événements, qui ne pourra pas enregistrer de nouveaux événements et appeler des gestionnaires pour eux. Et cela conduit finalement au «gel» de l'ensemble de l'application, elle perdra tout simplement l'asynchronie.


OK, mais qu'est-ce que cela a à voir avec l'appel de file_get_contents('pages/index.h') ? Le problème ici est que nous accédons directement au système de fichiers. Par rapport à d'autres opérations, telles que l'utilisation de la mémoire ou de l'informatique, l'utilisation du système de fichiers peut être extrêmement lente. Par exemple, si le fichier s'avère trop volumineux ou si le disque lui-même est lent, la lecture du fichier peut prendre un certain temps et, par conséquent, bloquer la boucle d'événements.


Dans le modèle synchrone standard, la demande-réponse n'est pas un problème. Si le client a demandé un fichier trop lourd, il attendra que ce fichier soit téléchargé. Une telle demande lourde n'affecte pas les autres clients. Mais dans notre cas, nous avons affaire à un modèle orienté événement asynchrone. Nous avons lancé un serveur HTTP qui doit constamment traiter les demandes entrantes. Si une demande prend trop de temps, cela affectera tous les autres clients du serveur.


En règle générale, n'oubliez pas:


  • Vous ne pouvez jamais bloquer une boucle d'événements.

Alors, comment lisons-nous le fichier de manière asynchrone? Et nous arrivons ici à la deuxième règle:


  • Lorsqu'une opération de blocage ne peut être évitée, elle doit être bifurquée dans le processus enfant et continuer l'exécution asynchrone dans le thread principal.

Donc, après avoir appris comment ne pas le faire, discutons de la bonne solution non bloquante.


Processus enfant


Toutes les communications avec le système de fichiers dans une application asynchrone doivent être effectuées dans des processus enfants. Pour gérer les processus enfants dans une application ReactPHP, nous devons installer un autre composant «Processus enfant» . Ce composant vous permet d'accéder aux fonctions du système d'exploitation pour exécuter n'importe quelle commande système à l'intérieur du processus enfant. Pour installer ce composant, ouvrez un terminal à la racine du projet et exécutez la commande suivante:


composer require react/child-process


Compatibilité Windows


Dans le système d'exploitation Windows, les threads STDIN, STDOUT et STDERR se bloquent, ce qui signifie que le composant Processus enfant ne pourra pas fonctionner correctement. Par conséquent, ce composant est principalement conçu pour fonctionner uniquement sur les systèmes nix. Si vous essayez de créer un objet de la classe Process sur un système Windows, une exception sera levée. Mais le composant peut fonctionner sous Windows Subsystem for Linux (WSL) . Si vous avez l'intention d'utiliser ce composant sous Windows, vous devrez installer WSL.


Nous pouvons maintenant exécuter n'importe quelle commande shell à l'intérieur du processus enfant. Ouvrez le fichier routes.php , puis changeons le gestionnaire pour / route. Créez un objet de la classe React\ChildProcess\Process et en tant que commande, passez-lui ls pour obtenir le contenu du répertoire courant:


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request) { $childProcess = new Process('ls'); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

Ensuite, nous devons démarrer le processus en appelant la méthode start() . Le hic est que la méthode start() besoin d'un objet de boucle d'événement. Mais dans le fichier routes.php , nous n'avons pas cet objet. Comment passer la boucle d'événements de index.php aux routes directement au gestionnaire de requêtes? La solution à ce problème est «l'injection de dépendance».


Injection de dépendance


Ainsi, l'un de nos itinéraires a besoin d'une boucle d'événements pour fonctionner. Dans notre application, un seul composant connaît l'existence des routes - la classe Router . Il s'avère qu'il est de sa responsabilité de fournir une boucle d'événement pour les itinéraires. En d'autres termes, le routeur a besoin d'une boucle d'événements, ou cela dépend de la boucle d'événements. Comment exprimer explicitement cette dépendance dans le code? Comment rendre impossible de créer même un routeur sans lui passer une boucle d'événement? Bien sûr, via le constructeur de la classe Router . Ouvrez Router.php et ajoutez le constructeur à la classe Router :


 use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; /** * @var LoopInterface */ private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; } // ... } 

À l'intérieur du constructeur, enregistrez la boucle d'événements passée dans la propriété privée $loop . Il s'agit d'une injection de dépendance lorsque nous fournissons à la classe les objets dont elle a besoin pour travailler à l'extérieur.


Maintenant que nous avons ce nouveau constructeur, nous devons mettre à jour la création du routeur. Ouvrez le fichier index.php et corrigez la ligne où nous créons l'objet de la classe Router :


 // index.php $loop = React\EventLoop\Factory::create(); $router = new Router($loop); $router->load('routes.php'); 

C'est fait. Revenez à routes.php . Comme vous l'avez probablement déjà deviné, ici nous pouvons utiliser la même idée avec l' injection de dépendances et ajouter une boucle d'événement comme deuxième paramètre à nos gestionnaires de requêtes. Modifiez le premier rappel et ajoutez le deuxième argument: un objet qui implémente LoopInterface :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('ls'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Ensuite, nous devons passer la boucle d'événements à la méthode start() du processus enfant. Et où le gestionnaire obtient-il la boucle d'événements? Et il est déjà stocké à l'intérieur du routeur dans la propriété privée $loop . Nous avons juste besoin de le passer lorsque le gestionnaire est appelé.


__invoke() la classe Router et __invoke() jour la méthode __invoke() , en ajoutant le deuxième argument à l'appel du gestionnaire de requêtes:


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

C'est tout! C'est probablement une injection de dépendance suffisante. Un assez grand voyage du cycle des événements s'est produit, non? Du fichier index.php à la classe Router , puis de la classe Router au fichier routes.php à l'intérieur des rappels.


Donc, pour confirmer que le processus enfant fera sa magie non bloquante, remplaçons la ls simple par le ping 8.8.8.8 plus lourd ping 8.8.8.8 . Redémarrez le serveur et essayez à nouveau d'ouvrir deux pages dans deux fenêtres différentes. D'abord, http://127.0.0.1:8080/ , puis /upload . Les deux pages s'ouvrent rapidement, sans délai, bien que la commande ping soit exécutée dans le premier gestionnaire en arrière-plan. En passant, cela signifie que nous pouvons débourser toute opération coûteuse (par exemple, le traitement de gros fichiers), sans bloquer l'application principale.


Lier le processus enfant et la réponse à l'aide de threads


Revenons à notre application. Nous avons donc créé un processus enfant, l'avons démarré, mais notre navigateur n'affiche en aucune façon les résultats d'une opération bifurquée. Corrigeons-le.


Comment pouvons-nous communiquer avec le processus enfant? Dans notre cas, nous avons une ls cours d'exécution qui affiche le contenu du répertoire courant. Comment pouvons-nous obtenir cette conclusion, puis l'envoyer au corps de la réponse? La réponse courte est: les fils.


Parlons un peu des processus. Toute commande shell que vous exécutez possède trois flux de données: STDIN, STDOUT et STDERR. Diffusez vers la sortie et l'entrée standard, plus le flux pour les erreurs. Par exemple, lorsque nous exécutons la ls , le résultat de cette commande est envoyé directement à STDOUT (sur l'écran du terminal). Donc, si nous devons obtenir la sortie d'un processus, l'accès au flux de sortie est requis. Et c'est aussi simple que ça. Lors de la création de l'objet de réponse, remplacez l'appel file_get_contents() par $childProcess->stdout :


 return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout ); 

Tous les processus enfants ont trois propriétés liées aux flux stdio : stdout , stdin et stderr . Dans notre cas, nous voulons afficher la sortie du processus sur une page Web. Au lieu d'une chaîne dans le constructeur de la classe Response , nous passons un flux comme troisième argument. La classe Response est suffisamment intelligente pour se rendre compte qu'elle a reçu le flux et le traiter en conséquence.


Donc, comme d'habitude, nous redémarrons le serveur et voyons ce que nous avons fait. Ouvrons la page http://127.0.0.1:8080/ dans le navigateur: vous devriez voir une liste des fichiers du dossier racine du projet.



La dernière étape consiste à remplacer la ls par quelque chose de plus utile. Nous avons commencé ce chapitre en rendant le fichier pages/index.html à l'aide de la fonction file_get_contents() . Maintenant, nous pouvons lire ce fichier de manière absolument asynchrone, sans craindre qu'il bloque notre application. Remplacez la ls par cat pages/index.html .


Si vous n'êtes pas familier avec la cat , elle est utilisée pour concaténer et sortir des fichiers. Le plus souvent, cette commande est utilisée pour lire un fichier et afficher son contenu sur la sortie standard. La commande cat pages/index.html lit le fichier cat pages/index.html et imprime son contenu dans STDOUT. Et nous envoyons déjà stdout comme organe de réponse. Voici la version finale du fichier routes.php :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('cat pages/index.html'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], $childProcess->stdout ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

Par conséquent, tout ce code n'était nécessaire que pour remplacer un appel à la fonction file_get_contents() . Injection de dépendances, passage d'un objet de boucle d'événements, ajout de processus enfants et utilisation de threads. Tout cela est juste pour remplacer un appel de fonction. Cela en valait-il la peine? Réponse: oui, ça valait le coup. Lorsque quelque chose peut bloquer la boucle d'événements et que le système de fichiers peut certainement, assurez-vous qu'il finira par se bloquer, et au moment le plus inopportun.


La création d'un processus enfant à chaque fois que nous devons accéder au système de fichiers peut sembler une surcharge supplémentaire qui affectera la vitesse et les performances de notre application. Malheureusement, en PHP, il n'y a pas d'autre moyen de travailler de manière asynchrone avec le système de fichiers. Toutes les bibliothèques PHP asynchrones utilisent des processus enfants (ou des extensions qui les font abstraction).


Les lecteurs Habra peuvent acheter l'intégralité du livre à prix réduit sur ce lien .


Et nous vous rappelons que nous sommes toujours à la recherche de développeurs sympas ! Venez, nous nous amusons!

Source: https://habr.com/ru/post/fr416003/


All Articles