Representación de archivos HTML: un capítulo del libro ReactPHP para principiantes de Skyeng


El desarrollador backend de la aplicación móvil Skyeng Sergey Zhuk continúa escribiendo buenos libros. Esta vez lanzó un libro de texto en ruso para una audiencia de dominio de PHP. Le pedí a Sergey que compartiera un capítulo útil y autosuficiente de su libro, y que les diera a los lectores de Habra un código de descuento. Debajo están los dos.


Primero, le diremos lo que detuvimos en los capítulos anteriores.

Hemos escrito nuestro servidor HTTP simple en PHP. Tenemos el archivo index.php principal: el script que inicia el servidor. Aquí está el código de nivel más alto: creamos un bucle de eventos, configuramos el comportamiento del servidor HTTP e iniciamos el bucle:


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

Para enrutar solicitudes, el servidor usa un enrutador:


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

Las rutas del archivo routes.php se cargan en el routes.php . Ahora solo se han anunciado dos rutas aquí:


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

Hasta ahora, todo es simple, y nuestra aplicación asincrónica cabe en varios archivos.


Pasamos a cosas más "útiles". Las respuestas de un par de palabras de un texto simple que aprendimos a derivar en capítulos anteriores no parecen muy atractivas. Necesitamos devolver algo real, como una página HTML.


Entonces, ¿dónde ponemos este HTML? Por supuesto, puede codificar los contenidos de la página web directamente dentro del archivo de rutas:


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

¡Pero no hagas eso! No puede mezclar lógica empresarial (enrutamiento) con presentación (página HTML). Por qué Imagine que necesita cambiar algo en el código HTML, por ejemplo, el color del botón. ¿Y qué archivo necesitará ser cambiado? Archivo con rutas router.php ? Suena raro, ¿verdad? Realice cambios en la ruta para cambiar el color del botón ...


Por lo tanto, dejaremos las rutas en paz, y para las páginas HTML crearemos un directorio separado. En la raíz del proyecto, agregue un nuevo directorio llamado páginas. Luego dentro de él creamos el archivo index.html . Esta será nuestra página principal. Aquí están sus contenidos:


 <!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 página es bastante simple, contiene solo un elemento: el formulario. El formulario dentro tiene un cuadro de texto y un botón para enviar. También agregué estilos Bootstrap para que nuestra página se vea mejor.


Lectura de archivos. Cómo no hacer


El enfoque más directo es leer el contenido del archivo dentro del controlador de solicitud y devolver ese contenido como el cuerpo de la respuesta. Algo como esto:


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

Y, por cierto, funcionará. Puede probarlo usted mismo: reinicie el servidor y vuelva a cargar la página http://127.0.0.1:8080/ en su navegador.



Entonces, ¿qué pasa aquí? ¿Y por qué no hacer eso? En resumen, porque habrá problemas si el sistema de archivos comienza a ralentizarse.


Bloqueo y no bloqueo de llamadas


Permítame mostrarle lo que quiero decir con "bloqueo" de llamadas y lo que puede suceder cuando uno de los manejadores de solicitudes contiene código de bloqueo. Antes de devolver el objeto de respuesta, agregue una llamada a la función 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' ); }, ]; 

Esto hará que el controlador de solicitudes se congele durante 10 segundos antes de que pueda devolver una respuesta con el contenido de la página HTML. Tenga en cuenta que no tocamos el controlador de la dirección /upload . Al llamar a la función sleep(10) , emulo la ejecución de algún tipo de operación de bloqueo.


Entonces, ¿qué tenemos? Cuando el navegador solicita la página / , el controlador espera 10 segundos y luego devuelve la página HTML. Cuando abrimos la dirección /upload , su controlador debe devolver inmediatamente una respuesta con la cadena 'Página de /upload '.


Ahora veamos qué sucede en la realidad. Como siempre, reiniciamos el servidor. Ahora, abra otra ventana en su navegador. En la barra de direcciones, ingrese http://127.0.0.1:8080/upload , pero no abra esta página de inmediato. Solo deje esta dirección en la barra de direcciones por ahora. Luego vaya a la primera ventana del navegador y abra la página http://127.0.0.1:8080/ en ella. Mientras se carga esta página (recuerde que tomará 10 segundos hacer esto), vaya rápidamente a la segunda ventana y presione “Entrar” para cargar la dirección que quedó en la barra de direcciones ( http://127.0.0.1:8080/upload ) .


Que conseguimos Sí, la dirección /, como se esperaba, tarda 10 segundos en cargarse. Pero, sorprendentemente, la segunda página tardó la misma cantidad de tiempo en cargarse, aunque no le agregamos ninguna llamada sleep() . ¿Alguna idea de por qué sucedió esto?


ReactPHP se ejecuta en un solo hilo. Puede parecer que en una aplicación asincrónica, las tareas se ejecutan en paralelo, pero en realidad esto no es así. La ilusión de paralelismo es creada por un ciclo de eventos que cambia constantemente entre varias tareas y las realiza. Pero en cierto momento, solo se realiza una tarea. Esto significa que si una de estas tareas lleva demasiado tiempo, bloqueará el bucle de eventos, lo que no podrá registrar nuevos eventos y controladores de llamadas para ellos. Y eso finalmente conduce a la "congelación" de toda la aplicación, simplemente perderá la asincronía.


Bien, pero ¿qué tiene esto que ver con llamar a file_get_contents('pages/index.h') ? El problema aquí es que estamos accediendo al sistema de archivos directamente. En comparación con otras operaciones, como trabajar con memoria o informática, trabajar con el sistema de archivos puede ser extremadamente lento. Por ejemplo, si el archivo resultó ser demasiado grande o el disco en sí es lento, entonces leer el archivo puede tomar un tiempo y, como resultado, bloquear el bucle de eventos.


En el modelo síncrono estándar, la solicitud-respuesta no es un problema. Si el cliente solicitó un archivo que es demasiado pesado, esperará hasta que se descargue este archivo. Una solicitud tan pesada no afecta a otros clientes. Pero en nuestro caso, estamos tratando con un modelo asíncrono orientado a eventos. Hemos lanzado un servidor HTTP que debe procesar constantemente las solicitudes entrantes. Si una solicitud tarda demasiado en completarse, esto afectará a todos los demás clientes del servidor.


Como regla, recuerde:


  • Nunca puedes bloquear un bucle de eventos.

Entonces, ¿cómo leemos el archivo asincrónicamente? Y aquí llegamos a la segunda regla:


  • Cuando no se puede evitar una operación de bloqueo, se debe bifurcar en el proceso secundario y continuar la ejecución asincrónica en el hilo principal.

Entonces, después de que aprendamos cómo no hacerlo, analicemos la solución correcta sin bloqueo.


Proceso hijo


Toda comunicación con el sistema de archivos en una aplicación asincrónica debe realizarse en procesos secundarios. Para administrar procesos secundarios en una aplicación ReactPHP, necesitamos instalar otro componente de "Proceso secundario " . Este componente le permite acceder a las funciones del sistema operativo para ejecutar cualquier comando del sistema dentro del proceso secundario. Para instalar este componente, abra una terminal en la raíz del proyecto y ejecute el siguiente comando:


composer require react/child-process


Compatibilidad con Windows


En el sistema operativo Windows, los subprocesos STDIN, STDOUT y STDERR están bloqueados, lo que significa que el componente Proceso secundario no podrá funcionar correctamente. Por lo tanto, este componente está diseñado principalmente para funcionar solo en sistemas nix. Si intenta crear un objeto de la clase Process en un sistema Windows, se generará una excepción. Pero el componente puede funcionar en el subsistema de Windows para Linux (WSL) . Si tiene la intención de utilizar este componente en Windows, deberá instalar WSL.


Ahora podemos ejecutar cualquier comando de shell dentro del proceso hijo. Abra el archivo routes.php y luego cambiemos el controlador para la ruta / . Cree un objeto de la clase React\ChildProcess\Process y, como comando, páselo ls para obtener el contenido del directorio actual:


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

Entonces necesitamos comenzar el proceso llamando al método start() . El problema es que el método start() necesita un objeto de bucle de eventos. Pero en el archivo routes.php no tenemos este objeto. ¿Cómo pasamos el bucle de eventos de index.php a rutas directamente al controlador de solicitudes? La solución a este problema es la "inyección de dependencia".


Inyección de dependencia


Entonces, una de nuestras rutas necesita un bucle de eventos para funcionar. En nuestra aplicación, solo un componente conoce la existencia de rutas: la clase Router . Resulta que es su responsabilidad proporcionar un circuito de eventos para las rutas. En otras palabras, el enrutador necesita un bucle de eventos, o depende del bucle de eventos. ¿Cómo expresamos explícitamente esta dependencia en el código? ¿Cómo hacer imposible incluso crear un enrutador sin pasarle un bucle de eventos? Por supuesto, a través del constructor de la clase Router . Abra Router.php y agregue el constructor a la clase 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; } // ... } 

Dentro del constructor, guarde el bucle de evento pasado en la propiedad privada $loop . Esta es una inyección de dependencia cuando proporcionamos a la clase los objetos que necesita para funcionar en el exterior.


Ahora que tenemos este nuevo constructor, necesitamos actualizar la creación del enrutador. Abra el archivo index.php y corrija la línea donde creamos el objeto de la clase Router :


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

Listo Volver a las routes.php . Como probablemente ya haya adivinado, aquí podemos usar la misma idea con la inyección de dependencia y agregar un bucle de eventos como segundo parámetro a nuestros manejadores de consultas. Cambie la primera devolución de llamada y agregue el segundo argumento: un objeto que implementa 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' ); }, ]; 

A continuación, debemos pasar el bucle de eventos al método start() del proceso secundario. ¿Y de dónde obtiene el controlador el bucle de eventos? Y ya está almacenado dentro del enrutador en la propiedad privada $loop . Solo tenemos que pasarlo cuando se llama al controlador.


__invoke() abrir la clase Router y actualizar el método __invoke() , agregando el segundo argumento a la llamada del manejador de solicitudes:


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

Eso es todo! Probablemente sea suficiente inyección de dependencia . Un gran viaje del ciclo de eventos sucedió, ¿verdad? Desde el archivo index.php a la clase Router , y luego desde la clase Router al archivo routes.php dentro de las devoluciones de llamada.


Entonces, para confirmar que el proceso secundario hará su magia sin bloqueo, reemplacemos el ls simple con el ping 8.8.8.8 más pesado ping 8.8.8.8 . Reinicie el servidor e intente nuevamente abrir dos páginas en dos ventanas diferentes. Primero, http://127.0.0.1:8080/ , y luego /upload . Ambas páginas se abren rápidamente, sin demora, aunque el comando ping se ejecuta en el primer controlador en segundo plano. Esto, por cierto, significa que podemos bifurcar cualquier operación costosa (por ejemplo, procesar archivos grandes), sin bloquear la aplicación principal.


Vincula el proceso secundario y la respuesta usando hilos


Volvamos a nuestra aplicación. Entonces, creamos un proceso hijo, lo iniciamos, pero nuestro navegador no muestra los resultados de una operación bifurcada de ninguna manera. Vamos a arreglarlo


¿Cómo podemos comunicarnos con el proceso secundario? En nuestro caso, tenemos un ls ejecución que muestra el contenido del directorio actual. ¿Cómo obtenemos esta conclusión y luego la enviamos al cuerpo de la respuesta? La respuesta corta es: hilos.


Hablemos un poco sobre los procesos. Cualquier comando de shell que ejecute tiene tres flujos de datos: STDIN, STDOUT y STDERR. Transmita a la salida y entrada estándar, además de la transmisión de errores. Por ejemplo, cuando ejecutamos el ls , el resultado de este comando se envía directamente a STDOUT (en la pantalla del terminal). Entonces, si necesitamos obtener la salida de un proceso, se requiere acceso a la secuencia de salida. Y esto es tan simple como eso. Al crear el objeto de respuesta, reemplace la llamada file_get_contents() con $childProcess->stdout :


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

Todos los procesos secundarios tienen tres propiedades que se relacionan con stdio secuencias stdio : stdout , stdin y stderr . En nuestro caso, queremos mostrar el resultado del proceso en una página web. En lugar de una cadena en el constructor de la clase Response , pasamos una secuencia como tercer argumento. La clase Response es lo suficientemente inteligente como para darse cuenta de que recibió la secuencia y procesarla en consecuencia.


Entonces, como siempre, reiniciamos el servidor y vemos lo que hemos hecho. Abramos la página http://127.0.0.1:8080/ en el navegador: debería ver una lista de archivos de la carpeta raíz del proyecto.



El último paso es reemplazar el ls con algo más útil. Comenzamos este capítulo renderizando el archivo pages/index.html usando la función file_get_contents() . Ahora, podemos leer este archivo absolutamente asincrónicamente, sin preocuparnos de que bloquee nuestra aplicación. Reemplace el ls con cat pages/index.html .


Si no está familiarizado con el cat , se utiliza para concatenar y generar archivos. Muy a menudo, este comando se usa para leer un archivo y enviar su contenido a la salida estándar. El comando cat pages/index.html lee el archivo cat pages/index.html e imprime su contenido en STDOUT. Y ya estamos enviando stdout como el cuerpo de respuesta. Aquí está la versión final del archivo 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' ); }, ]; 

Como resultado, todo este código era necesario solo para reemplazar una llamada a la función file_get_contents() . Inyección de dependencias, pasar un objeto de bucle de eventos, agregar procesos secundarios y trabajar con subprocesos. Todo esto es solo para reemplazar una llamada de función. ¿Valió la pena? Respuesta: sí, valió la pena. Cuando algo puede bloquear el bucle de eventos, y el sistema de archivos puede definitivamente, asegúrese de que eventualmente se bloqueará, y en el momento más inoportuno.


Crear un proceso secundario cada vez que necesitemos acceder al sistema de archivos puede parecer una sobrecarga adicional que afectará la velocidad y el rendimiento de nuestra aplicación. Desafortunadamente, en PHP no hay otra forma de trabajar con el sistema de archivos de forma asincrónica. Todas las bibliotecas asíncronas de PHP usan procesos secundarios (o extensiones que los abstraen).


Los lectores de Habra pueden comprar el libro completo con un descuento en este enlace .


¡Y le recordamos que siempre estamos en busca de desarrolladores geniales ! ¡Ven, nos divertimos!

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


All Articles