Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork100
An advanced async HTTP server library for PHP, perfect for real-time apps and APIs with high concurrency demands.
License
amphp/http-server
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind.This package provides a non-blocking, concurrent HTTP/1.1 and HTTP/2 application server for PHP based onRevolt.Several features are provided in separate packages, such as theWebSocket component.
- Static file serving
- WebSockets
- Dynamic app endpoint routing
- Request body parser
- Sessions
- Full TLS support
- Customizable GZIP compression
- Supports HTTP/1.1 and HTTP/2
- Middleware hooks
- CORS (3rd party)
- PHP 8.1+
This package can be installed as aComposer dependency.
composer require amphp/http-server
Additionally, you might want to install thenghttp2 library to take advantage of FFI to speed up and reduce the memory usage.
This library provides access to your application through the HTTP protocol, accepting client requests and forwarding those requests to handlers defined by your application which will return a response.
Incoming requests are represented byRequest objects. A request is provided to an implementor ofRequestHandler, which defines ahandleRequest() method returning an instance ofResponse.
publicfunction handleRequest(Request$request):Response
Request handlers are covered in greater detail in theRequestHandler section.
This HTTP server is built on top ofthe Revolt event-loop andthe non-blocking concurrency framework Amp.Thus it inherits full support of all their primitives and it is possible to use all the non-blocking libraries built on top of Revolt.
NoteIn general, you should make yourself familiar withthe
Futureconcept, withcoroutines, and be aware of the severalcombinator functions to really succeed at using the HTTP server.
Nearly every built-in function of PHP is doing blocking I/O, that means, the executing thread (mostly equivalent to the process in the case of PHP) will effectively be halted until the response is received.A few examples of such functions:mysqli_query,file_get_contents,usleep and many more.
A good rule of thumb is: Every built-in PHP function doing I/O is doing it in a blocking way, unless you know for sure it doesn't.
There arelibraries providing implementations that use non-blocking I/O. You should use these instead of the built-in functions.
We cover the most common I/O needs, such asnetwork sockets,file access,HTTP requests andwebsockets,MySQL andPostgres database clients, andRedis. If using blocking I/O or long computations are necessary to fulfill a request, consider using theParallel library to run that code in a separate process or thread.
WarningDo not use any blocking I/O functions in the HTTP server.
// Here's a bad example, DO NOT do something like the following!$handler =newClosureRequestHandler(function () {sleep(5);// Equivalent to a blocking I/O function with a 5 second timeoutreturnnewResponse;});// 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.
Your application will be served by an instance ofHttpServer. This library providesSocketHttpServer, which will be suitable for most applications, built on components found in this library and inamphp/socket.
To create an instance ofSocketHttpServer and listen for requests, minimally four things are required:
- an instance of
RequestHandlerto respond to incoming requests, - an instance of
ErrorHanderto provide responses to invalid requests, - an instance of
Psr\Log\LoggerInterface, and - at least one host+port on which to listen for connections.
<?phpuseAmp\ByteStream;useAmp\Http\HttpStatus;useAmp\Http\Server\DefaultErrorHandler;useAmp\Http\Server\Request;useAmp\Http\Server\RequestHandler;useAmp\Http\Server\Response;useAmp\Http\Server\SocketHttpServer;useAmp\Log\ConsoleFormatter;useAmp\Log\StreamHandler;useMonolog\Logger;useMonolog\Processor\PsrLogMessageProcessor;require__DIR__.'/vendor/autoload.php';// Note any PSR-3 logger may be used, Monolog is only an example.$logHandler =newStreamHandler(ByteStream\getStdout());$logHandler->pushProcessor(newPsrLogMessageProcessor());$logHandler->setFormatter(newConsoleFormatter());$logger =newLogger('server');$logger->pushHandler($logHandler);$requestHandler =newclass()implements RequestHandler {publicfunctionhandleRequest(Request$request) :Response {returnnewResponse( status: HttpStatus::OK, headers: ['Content-Type' =>'text/plain'], body:'Hello, world!', ); }};$errorHandler =newDefaultErrorHandler();$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();
The above example creates a simple server which sends a plain-text response to every request received.
NoteNagle's algorithm can negatively affect benchmarks on some systems. It can be disabled using
Amp\Socket\BindContext::withTcpNoDelay(), passing the returned instance as the second argument toSocketHttpServer::expose().
SocketHttpServer provides two static constructors for common use-cases in addition to the normal constructor for more advanced and custom uses.
SocketHttpServer::createForDirectAccess(): Used in the example above, this creates an HTTP application server suitable for direct network access. Adjustable limits are imposed on connections per IP, total connections, and concurrent requests (10, 1000, and 1000 by default, respectively). Response compression may be toggled on or off (on by default), and request methods are limited to a known set of HTTP verbs by default.SocketHttpServer::createForBehindProxy(): Creates a server appropriate for use when behind a proxy service such as nginx. This static constructor requires a list of trusted proxy IPs (with optional subnet masks) and an enum case ofForwardedHeaderType(corresponding to eitherForwardedorX-Forwarded-For) to parse the original client IP from request headers. No limits are imposed on the number of connections to the server, however the number of concurrent requests is limited (1000 by default, adjustable or can be removed). Response compression may be toggled on or off (on by default). Request methods are limited to a known set of HTTP verbs by default.
If neither of these methods serves your application needs, theSocketHttpServer constructor may be used directly. This provides an enormous amount of flexibility in how incoming client connections are created and handled, but will require more code to create. The constructor requires the user to pass an instance ofSocketServerFactory, used to create clientSocket instances (both components of theamphp/socket library), and an instance ofClientFactory, which appropriately createsClient instances which are attached to eachRequest made by the client.
Incoming requests are represented byRequest objects. A request is provided to an implementor ofRequestHandler, which defines ahandleRequest() method returning an instance ofResponse.
publicfunction handleRequest(Request$request):Response
Each client request (i.e., call toRequestHandler::handleRequest()) is executed within a separatecoroutine so requests are automatically handled cooperatively within the server process. When a request handler waits onnon-blocking I/O, other client requests are processed in concurrent coroutines. Your request handler may itself create other coroutines usingAmp\async() to execute multiple tasks for a single request.
Usually aRequestHandler directly generates a response, but it might also delegate to anotherRequestHandler.An example for such a delegatingRequestHandler is theRouter.
TheRequestHandler interface is meant to be implemented by custom classes.For very simple use cases or quick mocking, you can useCallableRequestHandler, which can wrap anycallable and accepting aRequest and returning aResponse.
Middleware allows pre-processing of requests and post-processing of responses.Apart from that, a middleware can also intercept the request processing and return a response without delegating to the passed request handler.Classes have to implement theMiddleware interface for that.
NoteMiddleware generally follows other words like soft- and hardware with its plural.However, we use the termmiddlewares to refer to multiple objects implementing the
Middlewareinterface.
publicfunction handleRequest(Request$request,RequestHandler$next):Response
handleRequest is the only method of theMiddleware interface. If theMiddleware doesn't handle the request itself, it should delegate the response creation to the receivedRequestHandler.
function stackMiddleware(RequestHandler$handler,Middleware ...$middleware):RequestHandler
Multiple middlewares can be stacked by usingAmp\Http\Server\Middleware\stackMiddleware(), which accepts aRequestHandler as first argument and a variable number ofMiddleware instances. The returnedRequestHandler will invoke each middleware in the provided order.
$requestHandler =newclassimplements RequestHandler {publicfunctionhandleRequest(Request$request):Response {returnnewResponse( status: HttpStatus::OK, headers: ["content-type" =>"text/plain; charset=utf-8"], body:"Hello, World!", ); }}$middleware =newclassimplements Middleware {publicfunctionhandleRequest(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 =newDefaultErrorHandler();// $logger is a PSR-3 logger instance.$server = SocketHttpServer::createForDirectAccess($logger);$server->expose('127.0.0.1:1337');$server->start($stackedHandler,$errorHandler);
AnErrorHander is used by the HTTP server when a malformed or otherwise invalid request is received. TheRequest object is provided if one constructed from the incoming data, but may not always be set.
publicfunction handleError(int$status, ?string$reason =null, ?Request$request =null,):Response
This library providesDefaultErrorHandler which returns a stylized HTML page as the response body. You may wish to provide a different implementation for your application, potentially using multiple in conjunction with arouter.
It is rare you will need to construct aRequest object yourself, as they will typically be provided toRequestHandler::handleRequest() by the server.
/** * @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. */publicfunction __construct(privatereadonlyClient$client,string$method,Psr\Http\Message\UriInterface$uri,array$headers = [],Amp\ByteStream\ReadableStream|string$body ='',privatestring$protocol ='1.1', ?Trailers$trailers =null,)
publicfunction getClient():Client
Returns theСlient sending the request
publicfunction getMethod():string
Returns the HTTP method used to make this request, e.g."GET".
publicfunction setMethod(string$method):void
Sets the request HTTP method.
publicfunction getUri():Psr\Http\Message\UriInterface
Returns the requestURI.
publicfunction setUri(Psr\Http\Message\UriInterface$uri):void
Sets a newURI for the request.
publicfunction getProtocolVersion():string
Returns the HTTP protocol version as a string (e.g. "1.0", "1.1", "2").
publicfunction setProtocolVersion(string$protocol)
Sets a new protocol version number for the request.
/** @return array<non-empty-string, list<string>> */publicfunction getHeaders():array
Returns the headers as a string-indexed array of arrays of strings or an empty array if no headers have been set.
publicfunction hasHeader(string$name):bool
Checks if given header exists.
/** @return list<string> */publicfunction getHeaderArray(string$name):array
Returns the array of values for the given header or an empty array if the header does not exist.
publicfunction getHeader(string$name): ?string
Returns the value of the given header.If multiple headers are present for the named header, only the first header value will be returned.UsegetHeaderArray() to return an array of all values for the particular header.Returnsnull if the header does not exist.
publicfunction setHeaders(array$headers):void
Sets the headers from the given array.
/** @param array<string>|string $value */publicfunction setHeader(string$name,array|string$value):void
Sets the header to the given value(s).All previous header lines with the given name will be replaced.
/** @param array<string>|string $value */publicfunction addHeader(string$name,array|string$value):void
Adds an additional header line with the given name.
publicfunction removeHeader(string$name):void
Removes the given header if it exists.If multiple header lines with the same name exist, all of them are removed.
publicfunction getBody():RequestBody
Returns the request body. TheRequestBody allows streamed and buffered access to anInputStream.
publicfunction setBody(ReadableStream|string$body)
Sets the stream for the message body
NoteUsing a string will automatically set the
Content-Lengthheader to the length of the given string.Setting anReadableStreamwill remove theContent-Lengthheader.If you know the exact content length of your stream, you can add acontent-lengthheaderafter callingsetBody().
/** @return array<non-empty-string, RequestCookie> */publicfunction getCookies():array
Returns allcookies in associative map of cookie name toRequestCookie.
publicfunction getCookie(string$name): ?RequestCookie
Gets acookie value by name ornull.
publicfunction setCookie(RequestCookie$cookie):void
Adds aCookie to the request.
publicfunction removeCookie(string$name):void
Removes a cookie from the request.
publicfunction getAttributes():array
Returns an array of all the attributes stored in the request's mutable local storage.
publicfunction removeAttributes():array
Removes all request attributes from the request's mutable local storage.
publicfunction hasAttribute(string$name):bool
Check whether an attribute with the given name exists in the request's mutable local storage.
publicfunction getAttribute(string$name):mixed
Retrieve a variable from the request's mutable local storage.
NoteName of the attribute should be namespaced with a vendor and package namespace, like classes.
publicfunction setAttribute(string$name,mixed$value):void
Assign a variable to the request's mutable local storage.
NoteName of the attribute should be namespaced with a vendor and package namespace, like classes.
publicfunction removeAttribute(string$name):void
Removes a variable from the request's mutable local storage.
publicfunction getTrailers():Trailers
Allows access to theTrailers of a request.
publicfunction setTrailers(Trailers$trailers):void
Assigns theTrailers object to be used in the request.
Client-related details are bundled intoAmp\Http\Server\Driver\Client objects returned fromRequest::getClient(). TheClient interface provides methods to retrieve the remote and local socket addresses and TLS info (if applicable).
TheResponse class represents an HTTP response. AResponse is returned byrequest handlers andmiddleware.
/** * @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. */publicfunction __construct(int$code = HttpStatus::OK,array$headers = [],Amp\ByteStream\ReadableStream|string$body ='', ?Trailers$trailers =null,)
Invokes dispose handlers (i.e. functions that registered viaonDispose() method).
NoteUncaught exceptions from the dispose handlers will be forwarded to theevent loop error handler.
publicfunction getBody():Amp\ByteStream\ReadableStream
Returns thestream for the message body.
publicfunction setBody(Amp\ByteStream\ReadableStream|string$body)
Sets thestream for the message body.
NoteUsing a string will automatically set the
Content-Lengthheader to the length of the given string.Setting anReadableStreamwill remove theContent-Lengthheader.If you know the exact content length of your stream, you can add acontent-lengthheaderafter callingsetBody().
/** @return array<non-empty-string, list<string>> */publicfunction getHeaders():array
Returns the headers as a string-indexed array of arrays of strings or an empty array if no headers have been set.
publicfunction hasHeader(string$name):bool
Checks if given header exists.
/** @return list<string> */publicfunction getHeaderArray(string$name):array
Returns the array of values for the given header or an empty array if the header does not exist.
publicfunction getHeader(string$name): ?string
Returns the value of the given header.If multiple headers are present for the named header, only the first header value will be returned.UsegetHeaderArray() to return an array of all values for the particular header.Returnsnull if the header does not exist.
publicfunction setHeaders(array$headers):void
Sets the headers from the given array.
/** @param array<string>|string $value */publicfunction setHeader(string$name,array|string$value):void
Sets the header to the given value(s).All previous header lines with the given name will be replaced.
/** @param array<string>|string $value */publicfunction addHeader(string$name,array|string$value):void
Adds an additional header line with the given name.
publicfunction removeHeader(string$name):void
Removes the given header if it exists.If multiple header lines with the same name exist, all of them are removed.
publicfunction getStatus():int
Returns the response status code.
publicfunction getReason():string
Returns the reason phrase describing the status code.
publicfunction setStatus(int$code,string |null$reason):void
Sets the numeric HTTP status code (between 100 and 599) and reason phrase. Use null for the reason phrase to use the default phrase associated with the status code.
/** @return array<non-empty-string, ResponseCookie> */publicfunction getCookies():array
Returns allcookies in an associative map of cookie name toResponseCookie.
publicfunction getCookie(string$name): ?ResponseCookie
Gets acookie value by name ornull if no cookie with that name is present.
publicfunction setCookie(ResponseCookie$cookie):void
Adds acookie to the response.
publicfunction removeCookie(string$name):void
Removes acookie from the response.
/** @return array<string, Push> Map of URL strings to Push objects. */publicfunction getPushes():array
Returns list of push resources in an associative map of URL strings toPush objects.
/** @param array<string>|array<string, array<string>> $headers */publicfunction push(string$url,array$headers):void
Indicate resources which a client likely needs to fetch. (e.g.Link: preload or HTTP/2 Server Push).
publicfunction isUpgraded():bool
Returnstrue if a detach callback has been set,false if none.
/** @param Closure(Driver\UpgradedSocket, Request, Response): void $upgrade */publicfunction upgrade(Closure$upgrade):void
Sets a callback to be invoked once the response has been written to the client and changes the status of the response to101 Switching Protocols. The callback receives an instance ofDriver\UpgradedSocket, theRequest which initiated the upgrade, and thisResponse.
The callback may be removed by changing the status to something other than 101.
publicfunction getUpgradeCallable(): ?Closure
Returns the upgrade function if present.
/** @param Closure():void $onDispose */publicfunction onDispose(Closure$onDispose):void
Registers a function that is invoked when the Response is discarded. A response is discarded either once it has been written to the client or if it gets replaced in a middleware chain.
publicfunction getTrailers():Trailers
Allows access to theTrailers of a response.
publicfunction setTrailers(Trailers$trailers):void
Assigns theTrailers object to be used in the response. Trailers are sent once the entire response body has been set to the client.
RequestBody, returned fromRequest::getBody(), provides buffered and streamed access to the request body.Use the streamed access to handle large messages, which is particularly important if you have larger message limits (like tens of megabytes) and don't want to buffer it all in memory.If multiple people are uploading large bodies concurrently, the memory might quickly get exhausted.
Hence, incremental handling is important, accessible viatheread() API ofAmp\ByteStream\ReadableStream.
In case a client disconnects, theread() fails with anAmp\Http\Server\ClientException.This exception is thrown for both theread() andbuffer() API.
Note
ClientExceptions do notneed to be caught. You may catch them if you want to continue, but don't have to. The Server will silently end the request cycle and discard that exception then.
Instead of setting the generic body limit high, you should consider increasing the body limit only where needed, which is dynamically possible with theincreaseSizeLimit() method onRequestBody.
Note
RequestBodyitself doesn't provide parsing of form data. You can useamphp/http-server-form-parserif you need it.
LikeRequest, it is rare to need to construct aRequestBody instance as one will be provided as part of theRequest.
publicfunction __construct(ReadableStream|string$stream, ?Closure$upgradeSize =null,)
publicfunction increaseSizeLimit(int$limit):void
Increases the body size limit dynamically to allow individual request handlers to handle larger request bodies than the default set for the HTTP server.
TheTrailers class allows access to the trailers of an HTTP request, accessible viaRequest::getTrailers().null is returned if trailers are not expected on the request.Trailers::await() returns aFuture which is resolved with anHttpMessage object providing methods to access the trailer headers.
$trailers =$request->getTrailers();$message =$trailers?->await();
The HTTP server won't be the bottleneck. Misconfiguration, use of blocking I/O, or inefficient applications are.
The server is well-optimized and can handle tens of thousands of requests per second on typical hardware while maintaining a high level of concurrency of thousands of clients.
But that performance will decrease drastically with inefficient applications.The server has the nice advantage of classes and handlers being always loaded, so there's no time lost with compilation and initialization.
A common trap is to begin operating on big data with simple string operations, requiring many inefficient big copies.Instead, streaming should be used where possible for larger request and response bodies.
The problem really is CPU cost.Inefficient I/O management (as long as it is non-blocking!) is just delaying individual requests.It is recommended to dispatch simultaneously and eventually bundle multiple independent I/O requests via Amp's combinators, but a slow handler will slow down every other request too.While one handler is computing, all the other handlers can't continue.Thus it is imperative to reduce computation times of the handlers to a minimum.
Several examples can be found in the./examples directory of therepository.These can be executed as normal PHP scripts on the command line.
php examples/hello-world.php
You can then access the example server athttp://localhost:1337/ in your browser.
If you discover any security related issues, please use the private security issue reporter instead of using the public issue tracker.
The MIT License (MIT). Please seeLICENSE for more information.
About
An advanced async HTTP server library for PHP, perfect for real-time apps and APIs with high concurrency demands.
Topics
Resources
License
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.