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)。请参阅许可证了解更多信息。