AMPHP es una colección de bibliotecas basadas en eventos para PHP diseñadas teniendo en cuenta las fibras y la concurrencia. Este paquete proporciona un servidor de aplicaciones HTTP/1.1 y HTTP/2 simultáneo y sin bloqueo para PHP basado en Revolt. Varias funciones se proporcionan en paquetes separados, como el componente WebSocket.
Este paquete se puede instalar como una dependencia de Composer.
composer require amphp/http-server
Además, es posible que desee instalar la biblioteca nghttp2
para aprovechar FFI para acelerar y reducir el uso de memoria.
Esta biblioteca proporciona acceso a su aplicación a través del protocolo HTTP, acepta solicitudes de clientes y reenvía esas solicitudes a los controladores definidos por su aplicación que devolverán una respuesta.
Las solicitudes entrantes están representadas por objetos Request
. Se proporciona una solicitud a un implementador de RequestHandler
, que define un método handleRequest()
que devuelve una instancia de Response
.
public function handleRequest( Request $ request ): Response
Los controladores de solicitudes se tratan con mayor detalle en la sección RequestHandler
.
Este servidor HTTP está construido sobre el bucle de eventos de Revolt y el marco de concurrencia sin bloqueo Amp. Por lo tanto, hereda el soporte total de todas sus primitivas y es posible utilizar todas las bibliotecas sin bloqueo construidas sobre Revolt.
Nota En general, debe familiarizarse con el concepto
Future
, con las corrutinas, y tener en cuenta las diversas funciones del combinador para tener realmente éxito en el uso del servidor HTTP.
Casi todas las funciones integradas de PHP bloquean E/S, es decir, el hilo de ejecución (en su mayor parte equivalente al proceso en el caso de PHP) se detendrá efectivamente hasta que se reciba la respuesta. Algunos ejemplos de dichas funciones: mysqli_query
, file_get_contents
, usleep
y muchas más.
Una buena regla general es: cada función PHP integrada que realiza E/S lo hace de forma bloqueante, a menos que esté seguro de que no es así.
Hay bibliotecas que proporcionan implementaciones que utilizan E/S sin bloqueo. Deberías utilizarlos en lugar de las funciones integradas.
Cubrimos las necesidades de E/S más comunes, como sockets de red, acceso a archivos, solicitudes HTTP y websockets, clientes de bases de datos MySQL y Postgres y Redis. Si es necesario utilizar E/S de bloqueo o cálculos largos para cumplir con una solicitud, considere usar la biblioteca Parallel para ejecutar ese código en un proceso o hilo separado.
Advertencia No utilice ninguna función de bloqueo de E/S en el servidor HTTP.
// Here's a bad example, DO NOT do something like the following!
$ handler = new ClosureRequestHandler ( function () {
sleep ( 5 ); // Equivalent to a blocking I/O function with a 5 second timeout
return new Response ;
});
// Start a server with this handler and hit it twice.
// You'll have to wait until the 5 seconds are over until the second request is handled.
Su aplicación será atendida por una instancia de HttpServer
. Esta biblioteca proporciona SocketHttpServer
, que será adecuado para la mayoría de las aplicaciones, basado en componentes que se encuentran en esta biblioteca y en amphp/socket
.
Para crear una instancia de SocketHttpServer
y escuchar solicitudes, se requieren como mínimo cuatro cosas:
RequestHandler
para responder a las solicitudes entrantes,ErrorHander
para proporcionar respuestas a solicitudes no válidas,PsrLogLoggerInterface
y <?php
use Amp ByteStream ;
use Amp Http HttpStatus ;
use Amp Http Server DefaultErrorHandler ;
use Amp Http Server Request ;
use Amp Http Server RequestHandler ;
use Amp Http Server Response ;
use Amp Http Server SocketHttpServer ;
use Amp Log ConsoleFormatter ;
use Amp Log StreamHandler ;
use Monolog Logger ;
use Monolog Processor PsrLogMessageProcessor ;
require __DIR__ . ' /vendor/autoload.php ' ;
// Note any PSR-3 logger may be used, Monolog is only an example.
$ logHandler = new StreamHandler ( ByteStream getStdout ());
$ logHandler -> pushProcessor ( new PsrLogMessageProcessor ());
$ logHandler -> setFormatter ( new ConsoleFormatter ());
$ logger = new Logger ( ' server ' );
$ logger -> pushHandler ( $ logHandler );
$ requestHandler = new class () implements RequestHandler {
public function handleRequest ( Request $ request ) : Response
{
return new Response (
status: HttpStatus:: OK ,
headers: [ ' Content-Type ' => ' text/plain ' ],
body: ' Hello, world! ' ,
);
}
};
$ errorHandler = new DefaultErrorHandler ();
$ server = SocketHttpServer:: createForDirectAccess ( $ logger );
$ server -> expose ( ' 127.0.0.1:1337 ' );
$ server -> start ( $ requestHandler , $ errorHandler );
// Serve requests until SIGINT or SIGTERM is received by the process.
Amp trapSignal ([ SIGINT , SIGTERM ]);
$ server -> stop ();
El ejemplo anterior crea un servidor simple que envía una respuesta de texto sin formato a cada solicitud recibida.
SocketHttpServer
proporciona dos constructores estáticos para casos de uso comunes además del constructor normal para usos más avanzados y personalizados.
SocketHttpServer::createForDirectAccess()
: utilizado en el ejemplo anterior, crea un servidor de aplicaciones HTTP adecuado para el acceso directo a la red. Se imponen límites ajustables a las conexiones por IP, el total de conexiones y las solicitudes simultáneas (10, 1000 y 1000 de forma predeterminada, respectivamente). La compresión de respuesta se puede activar o desactivar (activada de forma predeterminada) y los métodos de solicitud están limitados a un conjunto conocido de verbos HTTP de forma predeterminada.SocketHttpServer::createForBehindProxy()
: crea un servidor apropiado para su uso cuando está detrás de un servicio proxy como nginx. Este constructor estático requiere una lista de IP de proxy confiables (con máscaras de subred opcionales) y un caso de enumeración de ForwardedHeaderType
(correspondiente a Forwarded
o X-Forwarded-For
) para analizar la IP del cliente original a partir de los encabezados de solicitud. No se imponen límites en la cantidad de conexiones al servidor, sin embargo, la cantidad de solicitudes simultáneas está limitada (1000 de forma predeterminada, ajustable o se puede eliminar). La compresión de respuesta se puede activar o desactivar (activada de forma predeterminada). Los métodos de solicitud están limitados a un conjunto conocido de verbos HTTP de forma predeterminada. Si ninguno de estos métodos satisface las necesidades de su aplicación, se puede utilizar directamente el constructor SocketHttpServer
. Esto proporciona una enorme flexibilidad en la forma en que se crean y manejan las conexiones entrantes de los clientes, pero requerirá más código para crearlas. El constructor requiere que el usuario pase una instancia de SocketServerFactory
, utilizada para crear instancias Socket
de cliente (ambos componentes de la biblioteca amphp/socket
), y una instancia de ClientFactory
, que crea adecuadamente instancias Client
que se adjuntan a cada Request
realizada por el cliente. .
RequestHandler
Las solicitudes entrantes están representadas por objetos Request
. Se proporciona una solicitud a un implementador de RequestHandler
, que define un método handleRequest()
que devuelve una instancia de Response
.
public function handleRequest( Request $ request ): Response
Cada solicitud de cliente (es decir, llamada a RequestHandler::handleRequest()
) se ejecuta dentro de una corrutina separada para que las solicitudes se manejen automáticamente de manera cooperativa dentro del proceso del servidor. Cuando un controlador de solicitudes espera E/S sin bloqueo, otras solicitudes de clientes se procesan en rutinas simultáneas. Su controlador de solicitudes puede crear otras rutinas usando Ampasync()
para ejecutar múltiples tareas para una sola solicitud.
Por lo general, un RequestHandler
genera directamente una respuesta, pero también puede delegarla en otro RequestHandler
. Un ejemplo de tal RequestHandler
delegante es el Router
.
La interfaz RequestHandler
está destinada a ser implementada por clases personalizadas. Para casos de uso muy simples o burlas rápidas, puede usar CallableRequestHandler
, que puede empaquetar cualquier callable
y aceptar una Request
y devolver una Response
.
El middleware permite el preprocesamiento de solicitudes y el posprocesamiento de respuestas. Aparte de eso, un middleware también puede interceptar el procesamiento de la solicitud y devolver una respuesta sin delegar en el controlador de solicitudes pasado. Las clases tienen que implementar la interfaz Middleware
para eso.
Nota Middleware generalmente sigue a otras palabras como software y hardware con su plural. Sin embargo, utilizamos el término middlewares para referirnos a múltiples objetos que implementan la interfaz
Middleware
.
public function handleRequest( Request $ request , RequestHandler $ next ): Response
handleRequest
es el único método de la interfaz Middleware
. Si el Middleware
no maneja la solicitud por sí mismo, debe delegar la creación de la respuesta al RequestHandler
recibido.
function stackMiddleware( RequestHandler $ handler , Middleware ... $ middleware ): RequestHandler
Se pueden apilar varios middlewares utilizando AmpHttpServerMiddlewarestackMiddleware()
, que acepta un RequestHandler
como primer argumento y un número variable de instancias Middleware
. El RequestHandler
devuelto invocará cada middleware en el orden proporcionado.
$ requestHandler = new class implements RequestHandler {
public function handleRequest ( Request $ request ): Response
{
return new Response (
status: HttpStatus:: OK ,
headers: [ " content-type " => " text/plain; charset=utf-8 " ],
body: " Hello, World! " ,
);
}
}
$ middleware = new class implements Middleware {
public function handleRequest ( Request $ request , RequestHandler $ next ): Response
{
$ requestTime = microtime ( true );
$ response = $ next -> handleRequest ( $ request );
$ response -> setHeader ( " x-request-time " , microtime ( true ) - $ requestTime );
return $ response ;
}
};
$ stackedHandler = Middleware stackMiddleware ( $ requestHandler , $ middleware );
$ errorHandler = new DefaultErrorHandler ();
// $logger is a PSR-3 logger instance.
$ server = SocketHttpServer:: createForDirectAccess ( $ logger );
$ server -> expose ( ' 127.0.0.1:1337 ' );
$ server -> start ( $ stackedHandler , $ errorHandler );
ErrorHandler
El servidor HTTP utiliza un ErrorHander
cuando se recibe una solicitud con formato incorrecto o no válida. El objeto Request
se proporciona si se construye a partir de los datos entrantes, pero es posible que no siempre se establezca.
public function handleError(
int $ status ,
? string $ reason = null ,
? Request $ request = null ,
): Response
Esta biblioteca proporciona DefaultErrorHandler
que devuelve una página HTML estilizada como cuerpo de respuesta. Es posible que desee proporcionar una implementación diferente para su aplicación, posiblemente utilizando varias junto con un enrutador.
Request
Es raro que necesite construir un objeto Request
usted mismo, ya que normalmente el servidor lo proporcionará a RequestHandler::handleRequest()
.
/**
* @param string $method The HTTP method verb.
* @param array<string>|array<string, array<string>> $headers An array of strings or an array of string arrays.
*/
public function __construct(
private readonly Client $ client ,
string $ method ,
Psr Http Message UriInterface $ uri ,
array $ headers = [],
Amp ByteStream ReadableStream | string $ body = '' ,
private string $ protocol = ' 1.1 ' ,
? Trailers $ trailers = null ,
)
public function getClient(): Client
Devuelve el Сlient
que envía la solicitud
public function getMethod(): string
Devuelve el método HTTP utilizado para realizar esta solicitud, por ejemplo, "GET"
.
public function setMethod( string $ method ): void
Establece el método HTTP de solicitud.
public function getUri(): Psr Http Message UriInterface
Devuelve el URI
de solicitud.
public function setUri( Psr Http Message UriInterface $ uri ): void
Establece un nuevo URI
para la solicitud.
public function getProtocolVersion(): string
Devuelve la versión del protocolo HTTP como una cadena (por ejemplo, "1.0", "1.1", "2").
public function setProtocolVersion( string $ protocol )
Establece un nuevo número de versión de protocolo para la solicitud.
/** @return array<non-empty-string, list<string>> */
public function getHeaders(): array
Devuelve los encabezados como una matriz de matrices de cadenas indexadas por cadenas o una matriz vacía si no se han establecido encabezados.
public function hasHeader( string $ name ): bool
Comprueba si existe el encabezado dado.
/** @return list<string> */
public function getHeaderArray( string $ name ): array
Devuelve la matriz de valores para el encabezado dado o una matriz vacía si el encabezado no existe.
public function getHeader( string $ name ): ? string
Devuelve el valor del encabezado dado. Si hay varios encabezados presentes para el encabezado nombrado, solo se devolverá el primer valor del encabezado. Utilice getHeaderArray()
para devolver una matriz de todos los valores para el encabezado en particular. Devuelve null
si el encabezado no existe.
public function setHeaders( array $ headers ): void
Establece los encabezados de la matriz dada.
/** @param array<string>|string $value */
public function setHeader( string $ name , array | string $ value ): void
Establece el encabezado en los valores dados. Se reemplazarán todas las líneas de encabezado anteriores con el nombre de pila.
/** @param array<string>|string $value */
public function addHeader( string $ name , array | string $ value ): void
Agrega una línea de encabezado adicional con el nombre de pila.
public function removeHeader( string $ name ): void
Elimina el encabezado proporcionado si existe. Si existen varias líneas de encabezado con el mismo nombre, se eliminan todas.
public function getBody(): RequestBody
Devuelve el cuerpo de la solicitud. RequestBody
permite el acceso transmitido y almacenado en búfer a un InputStream
.
public function setBody( ReadableStream | string $ body )
Establece la secuencia para el cuerpo del mensaje.
Nota El uso de una cadena establecerá automáticamente el encabezado
Content-Length
en la longitud de la cadena dada. Configurar unReadableStream
eliminará el encabezadoContent-Length
. Si conoce la longitud exacta del contenido de su transmisión, puede agregar un encabezadocontent-length
después de llamarsetBody()
.
/** @return array<non-empty-string, RequestCookie> */
public function getCookies(): array
Devuelve todas las cookies en el mapa asociativo del nombre de la cookie a RequestCookie
.
public function getCookie( string $ name ): ? RequestCookie
Obtiene un valor de cookie por nombre o null
.
public function setCookie( RequestCookie $ cookie ): void
Agrega una Cookie
a la solicitud.
public function removeCookie( string $ name ): void
Elimina una cookie de la solicitud.
public function getAttributes(): array
Devuelve una matriz de todos los atributos almacenados en el almacenamiento local mutable de la solicitud.
public function removeAttributes(): array
Elimina todos los atributos de la solicitud del almacenamiento local mutable de la solicitud.
public function hasAttribute( string $ name ): bool
Compruebe si existe un atributo con el nombre de pila en el almacenamiento local mutable de la solicitud.
public function getAttribute( string $ name ): mixed
Recupera una variable del almacenamiento local mutable de la solicitud.
Nota El nombre del atributo debe tener un espacio de nombres con un proveedor y un espacio de nombres de paquete, como las clases.
public function setAttribute( string $ name , mixed $ value ): void
Asigne una variable al almacenamiento local mutable de la solicitud.
Nota El nombre del atributo debe tener un espacio de nombres con un proveedor y un espacio de nombres de paquete, como las clases.
public function removeAttribute( string $ name ): void
Elimina una variable del almacenamiento local mutable de la solicitud.
public function getTrailers(): Trailers
Permite acceder a los Trailers
de una solicitud.
public function setTrailers( Trailers $ trailers ): void
Asigna el objeto Trailers
que se utilizará en la solicitud.
Los detalles relacionados con el cliente se incluyen en los objetos AmpHttpServerDriverClient
devueltos por Request::getClient()
. La interfaz Client
proporciona métodos para recuperar las direcciones de socket locales y remotos y la información TLS (si corresponde).
Response
La clase Response
representa una respuesta HTTP. Los controladores de solicitudes y el middleware devuelven una Response
.
/**
* @param int $code The HTTP response status code.
* @param array<string>|array<string, array<string>> $headers An array of strings or an array of string arrays.
*/
public function __construct(
int $ code = HttpStatus:: OK ,
array $ headers = [],
Amp ByteStream ReadableStream | string $ body = '' ,
? Trailers $ trailers = null ,
)
Invoca controladores de eliminación (es decir, funciones que se registraron mediante el método onDispose()
).
Nota Las excepciones no detectadas de los controladores de eliminación se reenviarán al controlador de errores del bucle de eventos.
public function getBody(): Amp ByteStream ReadableStream
Devuelve la secuencia del cuerpo del mensaje.
public function setBody( Amp ByteStream ReadableStream | string $ body )
Establece la secuencia para el cuerpo del mensaje.
Nota El uso de una cadena establecerá automáticamente el encabezado
Content-Length
en la longitud de la cadena dada. Configurar unReadableStream
eliminará el encabezadoContent-Length
. Si conoce la longitud exacta del contenido de su transmisión, puede agregar un encabezadocontent-length
después de llamarsetBody()
.
/** @return array<non-empty-string, list<string>> */
public function getHeaders(): array
Devuelve los encabezados como una matriz de matrices de cadenas indexadas por cadenas o una matriz vacía si no se han establecido encabezados.
public function hasHeader( string $ name ): bool
Comprueba si existe el encabezado dado.
/** @return list<string> */
public function getHeaderArray( string $ name ): array
Devuelve la matriz de valores para el encabezado dado o una matriz vacía si el encabezado no existe.
public function getHeader( string $ name ): ? string
Devuelve el valor del encabezado dado. Si hay varios encabezados presentes para el encabezado nombrado, solo se devolverá el primer valor del encabezado. Utilice getHeaderArray()
para devolver una matriz de todos los valores para el encabezado en particular. Devuelve null
si el encabezado no existe.
public function setHeaders( array $ headers ): void
Establece los encabezados de la matriz dada.
/** @param array<string>|string $value */
public function setHeader( string $ name , array | string $ value ): void
Establece el encabezado en los valores dados. Se reemplazarán todas las líneas de encabezado anteriores con el nombre de pila.
/** @param array<string>|string $value */
public function addHeader( string $ name , array | string $ value ): void
Agrega una línea de encabezado adicional con el nombre de pila.
public function removeHeader( string $ name ): void
Elimina el encabezado proporcionado si existe. Si existen varias líneas de encabezado con el mismo nombre, se eliminan todas.
public function getStatus(): int
Devuelve el código de estado de respuesta.
public function getReason(): string
Devuelve la frase de motivo que describe el código de estado.
public function setStatus( int $ code , string | null $ reason ): void
Establece el código de estado HTTP numérico (entre 100 y 599) y la frase de motivo. Utilice nulo como frase de motivo para utilizar la frase predeterminada asociada con el código de estado.
/** @return array<non-empty-string, ResponseCookie> */
public function getCookies(): array
Devuelve todas las cookies en un mapa asociativo del nombre de la cookie a ResponseCookie
.
public function getCookie( string $ name ): ? ResponseCookie
Obtiene un valor de cookie por nombre o null
si no hay ninguna cookie con ese nombre presente.
public function setCookie( ResponseCookie $ cookie ): void
Agrega una cookie a la respuesta.
public function removeCookie( string $ name ): void
Elimina una cookie de la respuesta.
/** @return array<string, Push> Map of URL strings to Push objects. */
public function getPushes(): array
Devuelve una lista de recursos push en un mapa asociativo de cadenas de URL a objetos Push
.
/** @param array<string>|array<string, array<string>> $headers */
public function push( string $ url , array $ headers ): void
Indique los recursos que un cliente probablemente necesite recuperar. (por ejemplo Link: preload
o HTTP/2 Server Push).
public function isUpgraded(): bool
Devuelve true
si se ha configurado una devolución de llamada de desconexión, false
si no hay ninguna.
/** @param Closure(DriverUpgradedSocket, Request, Response): void $upgrade */
public function upgrade( Closure $ upgrade ): void
Establece una devolución de llamada que se invocará una vez que la respuesta se haya escrito en el cliente y cambia el estado de la respuesta a 101 Switching Protocols
. La devolución de llamada recibe una instancia de DriverUpgradedSocket
, la Request
que inició la actualización y esta Response
.
La devolución de llamada se puede eliminar cambiando el estado a otro que no sea 101.
public function getUpgradeCallable(): ? Closure
Devuelve la función de actualización si está presente.
/** @param Closure():void $onDispose */
public function onDispose( Closure $ onDispose ): void
Registra una función que se invoca cuando se descarta la respuesta. Una respuesta se descarta una vez escrita en el cliente o si se reemplaza en una cadena de middleware.
public function getTrailers(): Trailers
Permite acceder a los Trailers
de una respuesta.
public function setTrailers( Trailers $ trailers ): void
Asigna el objeto Trailers
que se utilizará en la respuesta. Los avances se envían una vez que se ha configurado todo el cuerpo de la respuesta para el cliente.
RequestBody
, devuelto por Request::getBody()
, proporciona acceso almacenado en búfer y transmitido al cuerpo de la solicitud. Utilice el acceso transmitido para manejar mensajes grandes, lo cual es particularmente importante si tiene límites de mensajes mayores (como decenas de megabytes) y no desea almacenarlos todos en la memoria intermedia. Si varias personas cargan cuerpos grandes al mismo tiempo, la memoria podría agotarse rápidamente.
Por lo tanto, el manejo incremental es importante, accesible a través de la API read()
de AmpByteStreamReadableStream
.
En caso de que un cliente se desconecte, read()
falla con una AmpHttpServerClientException
. Esta excepción se produce tanto para la API read()
como para la API buffer()
.
Nota No es necesario detectar
ClientException
s. Puedes atraparlos si quieres continuar, pero no es necesario. El servidor finalizará silenciosamente el ciclo de solicitud y luego descartará esa excepción.
En lugar de establecer un límite de cuerpo genérico alto, debería considerar aumentar el límite de cuerpo solo cuando sea necesario, lo cual es dinámicamente posible con el método increaseSizeLimit()
en RequestBody
.
Nota
RequestBody
en sí no proporciona análisis de los datos del formulario. Puede utilizaramphp/http-server-form-parser
si lo necesita.
Al igual que Request
, es raro que sea necesario construir una instancia RequestBody
, ya que se proporcionará una como parte de Request
.
public function __construct(
ReadableStream | string $ stream ,
? Closure $ upgradeSize = null ,
)
public function increaseSizeLimit( int $ limit ): void
Aumenta dinámicamente el límite de tamaño del cuerpo para permitir que los controladores de solicitudes individuales manejen cuerpos de solicitud más grandes que el conjunto predeterminado para el servidor HTTP.
La clase Trailers
permite el acceso a los avances de una solicitud HTTP, accesible a través de Request::getTrailers()
. Se devuelve null
si no se esperan remolques en la solicitud. Trailers::await()
devuelve un Future
que se resuelve con un objeto HttpMessage
que proporciona métodos para acceder a los encabezados del avance.
$ trailers = $ request -> getTrailers ();
$ message = $ trailers ?->await();
El servidor HTTP no será el cuello de botella. La mala configuración, el uso de bloqueo de E/S o aplicaciones ineficientes lo son.
El servidor está bien optimizado y puede manejar decenas de miles de solicitudes por segundo en hardware típico mientras mantiene un alto nivel de simultaneidad de miles de clientes.
Pero ese rendimiento disminuirá drásticamente con aplicaciones ineficientes. El servidor tiene la gran ventaja de que las clases y los controladores siempre están cargados, por lo que no se pierde tiempo con la compilación e inicialización.
Una trampa común es comenzar a operar con big data con operaciones de cadenas simples, lo que requiere muchas copias grandes e ineficientes. En cambio, se debe utilizar la transmisión por secuencias siempre que sea posible para cuerpos de solicitud y respuesta más grandes.
El problema realmente es el costo de la CPU. La gestión de E/S ineficiente (¡siempre que no bloquee!) solo está retrasando las solicitudes individuales. Se recomienda enviar simultáneamente y eventualmente agrupar múltiples solicitudes de E/S independientes a través de los combinadores de Amp, pero un controlador lento también ralentizará todas las demás solicitudes. Mientras un controlador está calculando, todos los demás controladores no pueden continuar. Por tanto, es imperativo reducir al mínimo los tiempos de cálculo de los manejadores.
Se pueden encontrar varios ejemplos en el directorio ./examples
del repositorio. Estos se pueden ejecutar como scripts PHP normales en la línea de comando.
php examples/hello-world.php
Luego puede acceder al servidor de ejemplo en http://localhost:1337/
en su navegador.
Si descubre algún problema relacionado con la seguridad, utilice el informe de problemas de seguridad privado en lugar del rastreador de problemas público.
La Licencia MIT (MIT). Consulte LICENCIA para obtener más información.