amphp/http-server
基于Amp的非阻塞HTTP应用程序服务器
Requires
- php: >=8.1
- amphp/amp: ^3
- amphp/byte-stream: ^2
- amphp/cache: ^2
- amphp/hpack: ^3
- amphp/http: ^2
- amphp/pipeline: ^1
- amphp/socket: ^2.1
- amphp/sync: ^2
- league/uri: ^6.8 | ^7.1
- league/uri-interfaces: ^2.3 | ^7.1
- psr/http-message: ^1 | ^2
- psr/log: ^1 | ^2 | ^3
- revolt/event-loop: ^1
Requires (Dev)
- amphp/http-client: ^5
- amphp/log: ^2
- amphp/php-cs-fixer-config: ^2
- amphp/phpunit-util: ^3
- league/uri-components: ^2.4.2 | ^7.1
- monolog/monolog: ^3
- phpunit/phpunit: ^9
- psalm/phar: ~5.23
Suggests
- ext-zlib: Allows GZip compression of response bodies
- 3.x-dev
- v3.3.1
- v3.3.0
- v3.2.0
- v3.1.0
- v3.0.0
- v3.0.0-beta.8
- v3.0.0-beta.7
- v3.0.0-beta.6
- v3.0.0-beta.5
- v3.0.0-beta.4
- v3.0.0-beta.3
- v3.0.0-beta.2
- v3.0.0-beta.1
- 2.x-dev
- v2.1.8
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.1
- v2.0.0
- v2.0.0-rc4
- v2.0.0-rc3
- v2.0.0-rc2
- v2.0.0-rc1
- v1.x-dev
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.1
- v1.0.0
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.7
- v0.4.6
- v0.4.5
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-http3
- dev-h2-uploads
- dev-priority
This package is auto-updated.
Last update: 2024-09-21 16:38:31 UTC
README
AMPHP是一组为PHP设计的基于事件驱动的库,考虑到纤维和并发。此包提供了一个基于Revolt的非阻塞、并发HTTP/1.1和HTTP/2应用程序服务器。一些功能以单独的包提供,例如WebSocket组件。
功能
- 静态文件服务
- WebSockets
- 动态应用程序端点路由
- 请求体解析器
- 会话
- 完全支持TLS
- 可定制的GZIP压缩
- 支持HTTP/1.1和HTTP/2
- 中间件钩子
- CORS(第三方)
要求
- PHP 8.1+
安装
此包可以作为Composer依赖项安装。
composer require amphp/http-server
此外,您可能想要安装nghttp2
库,以利用FFI加快速度并减少内存使用。
用法
此库通过HTTP协议提供对您的应用程序的访问,接受客户端请求并将这些请求转发到由您的应用程序定义的处理程序,该处理程序将返回一个响应。
传入请求由Request
对象表示。将请求提供给实现RequestHandler
的实体,该实体定义一个返回Response
实例的handleRequest()
方法。
public function handleRequest(Request $request): Response
请求处理程序将在RequestHandler
部分中更详细地介绍。
此HTTP服务器是在Revolt事件循环和非阻塞并发框架Amp之上构建的。因此,它继承了所有它们的原语支持,并可以使用所有基于Revolt构建的非阻塞库。
阻塞I/O
PHP的几乎所有内置函数都在执行阻塞I/O,这意味着,执行线程(在PHP的情况下,通常相当于进程)将有效地停止,直到收到响应。此类函数的几个示例:mysqli_query
、file_get_contents
、usleep
等等。
一个很好的规则是:每个执行I/O的内置PHP函数都在以阻塞方式进行,除非您确信它不是。
有一些库提供了使用非阻塞I/O的实现。您应该使用这些库而不是内置函数。
我们涵盖了最常见的I/O需求,例如网络套接字、文件访问、HTTP请求和WebSocket、MySQL和Postgres数据库客户端,以及Redis。如果使用阻塞I/O或长时间计算是满足请求的必要条件,请考虑使用并行库在单独的进程或线程中运行该代码。
警告 请勿在HTTP服务器中使用任何阻塞I/O函数。
// 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.
创建HTTP服务器
您的应用将由HttpServer
的一个实例提供服务。此库提供了SocketHttpServer
,这对于大多数应用都是合适的,它是基于此库中的组件和amphp/socket
库中的组件构建的。
要创建一个SocketHttpServer
的实例并监听请求,至少需要以下四个条件:
- 一个
RequestHandler
的实例来响应传入的请求, - 一个
ErrorHander
的实例来对无效请求提供响应, - 一个
Psr\Log\LoggerInterface
的实例,以及 - 至少一个用于监听连接的主机+端口号。
<?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();
上面的示例创建了一个简单的服务器,该服务器对收到的每个请求发送纯文本响应。
SocketHttpServer
提供了两个静态构造函数,用于常见的用例,除了用于更高级和定制用途的正常构造函数。
SocketHttpServer::createForDirectAccess()
:如上例所示使用,它创建一个适合直接网络访问的HTTP应用服务器。对每个IP的连接数、总连接数和并发请求(默认分别为10、1000和1000)施加可调整的限制。默认开启响应压缩,并默认将请求方法限制为已知的一组HTTP动词。SocketHttpServer::createForBehindProxy()
:创建一个适合在如nginx之类的代理服务后使用的服务器。此静态构造函数需要一个受信任的代理IP列表(可选子网掩码)和一个ForwardedHeaderType
的枚举值(对应于Forwarded
或X-Forwarded-For
),以从请求头中解析原始客户端IP。对服务器的连接数不施加限制,但默认限制并发请求的数量(默认为1000,可调整或移除)。默认开启响应压缩,并默认将请求方法限制为已知的一组HTTP动词。
如果上述两种方法都不能满足您的应用需求,可以直接使用SocketHttpServer
构造函数。这提供了极大的灵活性,以创建和处理传入的客户端连接,但将需要更多的代码来实现。构造函数需要用户传递一个SocketServerFactory
的实例,用于创建客户端Socket
实例(amphp/socket
库的组件),以及一个ClientFactory
的实例,该实例创建与客户端发出的每个Request
相关联的Client
实例。
RequestHandler
传入请求由Request
对象表示。将请求提供给实现RequestHandler
的实体,该实体定义一个返回Response
实例的handleRequest()
方法。
public function handleRequest(Request $request): Response
每个客户端请求(即对RequestHandler::handleRequest()
的调用)都在一个单独的协程中执行,因此请求可以在服务器进程中自动协同处理。当一个请求处理器在非阻塞I/O上等待时,其他客户端请求将在并发协程中处理。您的请求处理器可以使用Amp\async()
创建其他协程,以对单个请求执行多个任务。
通常,RequestHandler
直接生成响应,但它也可能委派给另一个RequestHandler
。此类委派的RequestHandler
的例子是Router
。
RequestHandler
接口旨在由自定义类实现。对于非常简单的用例或快速模拟,您可以使用CallableRequestHandler
,它可以包装任何callable
,接受一个Request
并返回一个Response
。
中间件
中间件允许预处理请求和后处理响应。除此之外,中间件还可以拦截请求处理并返回响应,而不需要委派给传递的请求处理器。为此,类必须实现Middleware
接口。
注意中间件通常与其他复数名词如软硬件一起使用。然而,我们使用术语middlewares来指代实现
Middleware
接口的多个对象。
public function handleRequest(Request $request, RequestHandler $next): Response
handleRequest
是Middleware
接口的唯一方法。如果Middleware
本身不处理请求,它应该委派响应的创建给接收到的RequestHandler
。
function stackMiddleware(RequestHandler $handler, Middleware ...$middleware): RequestHandler
可以使用Amp\Http\Server\Middleware\stackMiddleware()
堆叠多个中间件,它接受一个作为第一个参数的RequestHandler
和一个可变数量的Middleware
实例。返回的RequestHandler
将按照提供的顺序调用每个中间件。
$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);
错误处理器
当HTTP服务器接收到格式错误或无效的请求时,将使用ErrorHander
。如果从传入的数据构建了一个Request
对象,将提供该对象,但可能不会总是设置。
public function handleError( int $status, ?string $reason = null, ?Request $request = null, ): Response
此库提供了DefaultErrorHandler
,它返回一个样式化的HTML页面作为响应体。您可能希望为您的应用程序提供不同的实现,可能使用router配合使用多个。
请求
构造函数
您很少需要自己构造Request
对象,因为它们通常由服务器通过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
返回发送请求的Client
public function getMethod(): string
返回用于此请求的HTTP方法,例如"GET"
。
public function setMethod(string $method): void
设置请求HTTP方法。
public function getUri(): Psr\Http\Message\UriInterface
返回请求URI
。
public function setUri(Psr\Http\Message\UriInterface $uri): void
为请求设置新的URI
。
public function getProtocolVersion(): string
以字符串形式返回HTTP协议版本(例如,“1.0”,“1.1”,“2”)。
public function setProtocolVersion(string $protocol)
为请求设置新的协议版本号。
/** @return array<non-empty-string, list<string>> */ public function getHeaders(): array
以字符串索引数组的形式返回头信息,或如果没有设置头信息,返回空数组。
public function hasHeader(string $name): bool
检查给定头信息是否存在。
/** @return list<string> */ public function getHeaderArray(string $name): array
返回给定头信息的值数组,如果头信息不存在,返回空数组。
public function getHeader(string $name): ?string
返回给定头部的值。如果存在多个具有相同名称的头,则仅返回第一个头部的值。使用 getHeaderArray()
返回特定头的所有值的数组。如果头部不存在,则返回 null
。
public function setHeaders(array $headers): void
从给定的数组中设置头部。
/** @param array<string>|string $value */ public function setHeader(string $name, array|string $value): void
将头部设置为给定的值。将替换所有具有相同名称的先前头部行。
/** @param array<string>|string $value */ public function addHeader(string $name, array|string $value): void
添加一个具有给定名称的附加头部行。
public function removeHeader(string $name): void
如果存在,则删除给定的头部。如果存在多个具有相同名称的头部行,则删除所有这些行。
public function getBody(): RequestBody
返回请求体。RequestBody
允许对 InputStream
进行流式和缓冲访问。
public function setBody(ReadableStream|string $body)
设置消息体的流。
注意 使用字符串将自动将
Content-Length
头设置为给定字符串的长度。设置ReadableStream
将删除Content-Length
头。如果您知道流的精确内容长度,您可以在调用setBody()
之后添加一个content-length
头。
/** @return array<non-empty-string, RequestCookie> */ public function getCookies(): array
以 cookie 名称到 RequestCookie
的关联映射的形式返回所有 cookies。
public function getCookie(string $name): ?RequestCookie
通过名称获取 cookie 值或 null
。
public function setCookie(RequestCookie $cookie): void
向请求添加一个 Cookie
。
public function removeCookie(string $name): void
从请求中删除 cookie。
public function getAttributes(): array
返回请求的可变本地存储中存储的所有属性的数组。
public function removeAttributes(): array
从请求的可变本地存储中删除所有请求属性。
public function hasAttribute(string $name): bool
检查请求的可变本地存储中是否存在具有给定名称的属性。
public function getAttribute(string $name): mixed
从请求的可变本地存储中检索变量。
注意 属性的名称应该使用供应商和包命名空间进行命名空间化,就像类一样。
public function setAttribute(string $name, mixed $value): void
将变量分配给请求的可变本地存储。
注意 属性的名称应该使用供应商和包命名空间进行命名空间化,就像类一样。
public function removeAttribute(string $name): void
从请求的可变本地存储中删除变量。
public function getTrailers(): Trailers
允许访问请求的 Trailers
。
public function setTrailers(Trailers $trailers): void
将用于请求的 Trailers
对象分配。
请求客户端
与客户端相关的详细信息被包装在 Amp\Http\Server\Driver\Client
对象中,该对象由 Request::getClient()
返回。该 Client
接口提供检索远程和本地套接字地址以及 TLS 信息(如果适用)的方法。
响应
Response
类表示 HTTP 响应。由 请求处理器 和 中间件 返回 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, )
析构函数
调用 dispose 处理程序(即通过 onDispose()
方法注册的函数)。
注意 dispose 处理程序中的未捕获异常将被转发到 事件循环 错误处理器。
方法
public function getBody(): Amp\ByteStream\ReadableStream
返回消息体的 流。
public function setBody(Amp\ByteStream\ReadableStream|string $body)
设置消息体的 流。
注意 使用字符串将自动将
Content-Length
头设置为给定字符串的长度。设置ReadableStream
将删除Content-Length
头。如果您知道流的精确内容长度,您可以在调用setBody()
之后添加一个content-length
头。
/** @return array<non-empty-string, list<string>> */ public function getHeaders(): array
以字符串索引数组的形式返回头信息,或如果没有设置头信息,返回空数组。
public function hasHeader(string $name): bool
检查给定头信息是否存在。
/** @return list<string> */ public function getHeaderArray(string $name): array
返回给定头信息的值数组,如果头信息不存在,返回空数组。
public function getHeader(string $name): ?string
返回给定头部的值。如果存在多个具有相同名称的头,则仅返回第一个头部的值。使用 getHeaderArray()
返回特定头的所有值的数组。如果头部不存在,则返回 null
。
public function setHeaders(array $headers): void
从给定的数组中设置头部。
/** @param array<string>|string $value */ public function setHeader(string $name, array|string $value): void
将头部设置为给定的值。将替换所有具有相同名称的先前头部行。
/** @param array<string>|string $value */ public function addHeader(string $name, array|string $value): void
添加一个具有给定名称的附加头部行。
public function removeHeader(string $name): void
如果存在,则删除给定的头部。如果存在多个具有相同名称的头部行,则删除所有这些行。
public function getStatus(): int
返回响应状态码。
public function getReason(): string
返回描述状态码的原因短语。
public function setStatus(int $code, string | null $reason): void
设置数字 HTTP 状态码(介于 100 和 599 之间)和原因短语。对于原因短语使用 null 以使用与状态码关联的默认短语。
/** @return array<non-empty-string, ResponseCookie> */ public function getCookies(): array
以 cookie 名称到 ResponseCookie
的关联映射的形式返回所有 cookies。
public function getCookie(string $name): ?ResponseCookie
通过名称获取一个cookie的值,如果不存在则返回null
。
public function setCookie(ResponseCookie $cookie): void
向响应中添加一个cookie。
public function removeCookie(string $name): void
从响应中移除一个cookie。
/** @return array<string, Push> Map of URL strings to Push objects. */ public function getPushes(): array
返回一个关联数组,包含URL字符串到Push
对象的映射。
/** @param array<string>|array<string, array<string>> $headers */ public function push(string $url, array $headers): void
指示客户端可能需要获取的资源。(例如Link: preload
或HTTP/2服务器推送)。
public function isUpgraded(): bool
如果设置了分离回调,则返回true
,如果没有,则返回false
。
/** @param Closure(Driver\UpgradedSocket, Request, Response): void $upgrade */ public function upgrade(Closure $upgrade): void
设置一个回调函数,在将响应写入客户端后调用一次,并将响应状态更改为101 Switching Protocols
。回调函数接收一个Driver\UpgradedSocket
实例、初始化升级的Request
以及当前的Response
。
可以通过将状态更改为除101以外的值来移除回调。
public function getUpgradeCallable(): ?Closure
如果存在,则返回升级函数。
/** @param Closure():void $onDispose */ public function onDispose(Closure $onDispose): void
注册一个函数,当响应被丢弃时调用。响应在被写入客户端或在中间件链中被替换时会被丢弃。
public function getTrailers(): Trailers
允许访问响应的Trailers
。
public function setTrailers(Trailers $trailers): void
将用于响应的Trailers
对象分配。当整个响应体被设置给客户端后,将发送Trailers。
主体
RequestBody
,由Request::getBody()
返回,提供了对请求体的缓冲和流式访问。使用流式访问来处理大型消息,这在您有较大的消息限制(如数十兆字节)且不想在内存中缓冲所有内容时尤其重要。如果有多个用户同时上传大型主体,内存可能会迅速耗尽。
因此,增量处理很重要,可以通过Amp\ByteStream\ReadableStream的read()
API访问。
如果客户端断开连接,read()
将因Amp\Http\Server\ClientException
失败。此异常在read()
和buffer()
API中都会抛出。
注意
ClientException
不需要被捕获。如果你想继续,你可以捕获它们,但不是必须的。服务器将静默地结束请求周期并丢弃该异常。
而不是将通用主体限制设置得过高,你应该考虑只在需要的地方增加主体限制,这可以通过RequestBody
上的increaseSizeLimit()
方法动态实现。
注意
RequestBody
本身不提供表单数据的解析。如果你需要,可以使用amphp/http-server-form-parser
。
构造函数
与Request
一样,很少需要构造一个RequestBody
实例,因为一个实例将作为Request
的一部分提供。
public function __construct( ReadableStream|string $stream, ?Closure $upgradeSize = null, )
方法
public function increaseSizeLimit(int $limit): void
动态增加主体大小限制,以允许单独的请求处理器处理比HTTP服务器默认设置更大的请求主体。
尾迹
Trailers
类允许访问HTTP请求的尾迹,可以通过Request::getTrailers()
访问。如果请求上不期望有尾迹,则返回null
。Trailers::await()
返回一个Future
,它被解析为一个提供访问尾迹头方法HttpMessage
对象。
$trailers = $request->getTrailers(); $message = $trailers?->await();
瓶颈
HTTP服务器不会成为瓶颈。误配置、使用阻塞I/O或效率低下的应用程序才是。
服务器经过良好优化,在典型硬件上可以每秒处理数万个请求,同时保持数千个客户端的高并发水平。
但是,如果应用程序效率低下,性能将急剧下降。服务器具有一个很好的优势,即类和处理器总是被加载,因此不需要在编译和初始化上浪费时间。
一个常见的陷阱是开始使用简单的字符串操作处理大数据,这需要许多低效的大数据复制。相反,在可能的情况下,应使用流式传输来处理更大的请求和响应体。
真正的问题是CPU成本。低效的I/O管理(只要是非阻塞的!)只是在延迟单个请求。建议同时调度,并最终通过Amp的组合器捆绑多个独立的I/O请求,但一个慢的处理程序会减慢其他每个请求。当一个处理器正在计算时,其他所有处理器都不能继续。因此,务必将处理器的计算时间降到最低。
示例
可以在./examples
目录下找到几个示例,这些示例可以在命令行上作为正常的PHP脚本执行。
php examples/hello-world.php
然后,您可以在浏览器中通过http://localhost:1337/
访问示例服务器。
安全性
如果您发现任何与安全相关的问题,请使用私人安全问题报告器,而不是使用公共问题跟踪器。
许可证
MIT许可证(MIT)。有关更多信息,请参阅LICENSE。