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)。請參閱許可證以了解更多資訊。