amphp/http-server

基于Amp的非阻塞HTTP应用程序服务器

资助包维护!
amphp

安装次数: 1,761,526

依赖者: 77

建议者: 2

安全性: 0

星标: 1,286

关注者: 59

分支: 100

开放问题: 13

v3.3.1 2024-04-21 15:46 UTC

README

AMPHP是一组为PHP设计的基于事件驱动的库,考虑到纤维和并发。此包提供了一个基于Revolt的非阻塞、并发HTTP/1.1和HTTP/2应用程序服务器。一些功能以单独的包提供,例如WebSocket组件

功能

要求

  • 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构建的非阻塞库。

注意 通常,您应该熟悉Future 概念协程,并了解几个组合器函数,才能真正成功地使用HTTP服务器。

阻塞I/O

PHP的几乎所有内置函数都在执行阻塞I/O,这意味着,执行线程(在PHP的情况下,通常相当于进程)将有效地停止,直到收到响应。此类函数的几个示例:mysqli_queryfile_get_contentsusleep等等。

一个很好的规则是:每个执行I/O的内置PHP函数都在以阻塞方式进行,除非您确信它不是。

有一些库提供了使用非阻塞I/O的实现。您应该使用这些库而不是内置函数。

我们涵盖了最常见的I/O需求,例如网络套接字文件访问HTTP请求WebSocketMySQLPostgres数据库客户端,以及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的枚举值(对应于ForwardedX-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

handleRequestMiddleware接口的唯一方法。如果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()访问。如果请求上不期望有尾迹,则返回nullTrailers::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