AMPHP 是 PHP 事件驱动库的集合,在设计时考虑了光纤和并发性。该包为基于 Revolt 的 PHP 提供了一个非阻塞、并发的 HTTP/1.1 和 HTTP/2 应用服务器。多个功能在单独的包中提供,例如 WebSocket 组件。
该软件包可以作为 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 服务器。
几乎 PHP 的每个内置函数都在执行阻塞 I/O,这意味着执行线程(主要相当于 PHP 中的进程)将有效地停止,直到收到响应。此类函数的一些示例: mysqli_query 、 file_get_contents 、 usleep等等。
一个好的经验法则是:每个执行 I/O 的内置 PHP 函数都是以阻塞方式执行的,除非您确定它不会。
有些库提供了使用非阻塞 I/O 的实现。您应该使用这些函数而不是内置函数。
我们涵盖最常见的 I/O 需求,例如网络套接字、文件访问、HTTP 请求和 websockets、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.您的应用程序将由HttpServer的实例提供服务。该库提供SocketHttpServer ,它适用于大多数应用程序,基于该库和amphp/socket中的组件构建。
要创建SocketHttpServer实例并侦听请求,至少需要四件事:
RequestHandler实例,ErrorHander实例,用于提供对无效请求的响应,PsrLogLoggerInterface的实例,以及 <?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 时,其他客户端请求将在并发协程中处理。您的请求处理程序本身可能会使用Ampasync()创建其他协程来为单个请求执行多个任务。
通常RequestHandler直接生成响应,但它也可能委托给另一个RequestHandler 。这种委托RequestHandler的一个例子是Router 。
RequestHandler接口旨在由自定义类实现。对于非常简单的用例或快速模拟,您可以使用CallableRequestHandler ,它可以包装任何callable并接受Request并返回Response 。
中间件允许预处理请求和后处理响应。除此之外,中间件还可以拦截请求处理并返回响应,而无需委托给传递的请求处理程序。类必须为此实现Middleware接口。
注意中间件通常跟在其他词后面,例如软件和硬件及其复数形式。但是,我们使用术语中间件来指代实现
Middleware接口的多个对象。
public function handleRequest( Request $ request , RequestHandler $ next ): Response handleRequest是Middleware接口的唯一方法。如果Middleware本身不处理请求,它应该将响应创建委托给接收到的RequestHandler 。
function stackMiddleware( RequestHandler $ handler , Middleware ... $ middleware ): RequestHandler可以使用AmpHttpServerMiddlewarestackMiddleware()堆叠多个中间件,它接受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 );ErrorHandler当收到格式错误或无效的请求时,HTTP 服务器将使用ErrorHander 。如果Request对象,但可能并不总是被设置。
public function handleError(
int $ status ,
? string $ reason = null ,
? Request $ request = null ,
): Response该库提供了DefaultErrorHandler ,它返回一个风格化的 HTML 页面作为响应正文。您可能希望为您的应用程序提供不同的实现,可能将多个实现与路由器结合使用。
Request您很少需要自己构造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返回发送请求的Сlient
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 名称关联映射中的所有 cookie 返回到RequestCookie 。
public function getCookie( string $ name ): ? RequestCookie按名称或null获取 cookie 值。
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对象。
客户端相关的详细信息被捆绑到从Request::getClient()返回的AmpHttpServerDriverClient对象中。 Client端接口提供了检索远程和本地套接字地址以及 TLS 信息(如果适用)的方法。
Response 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 ,
)调用处置处理程序(即通过onDispose()方法注册的函数)。
注意:处置处理程序中未捕获的异常将被转发到事件循环错误处理程序。
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 名称关联映射中的所有 cookie 返回到ResponseCookie 。
public function getCookie( string $ name ): ? ResponseCookie按名称获取 cookie 值,如果不存在具有该名称的 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(DriverUpgradedSocket, Request, Response): void $upgrade */
public function upgrade( Closure $ upgrade ): void设置将响应写入客户端后调用的回调,并将响应的状态更改为101 Switching Protocols 。该回调接收DriverUpgradedSocket的实例、启动升级的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对象。一旦将整个响应正文设置到客户端,就会发送预告片。
RequestBody从Request::getBody()返回,提供对请求正文的缓冲和流式访问。使用流式访问来处理大消息,如果您有较大的消息限制(例如数十兆字节)并且不想将其全部缓冲在内存中,这一点尤其重要。如果多人同时上传大型正文,内存可能很快就会耗尽。
因此,增量处理很重要,可以通过AmpByteStreamReadableStream的read() API 进行访问。
如果客户端断开连接,则read()会失败并出现AmpHttpServerClientException 。 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 ,该 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)。请参阅许可证了解更多信息。