ReactPHP 的核心反應器事件循環,函式庫可用於事件 I/O。
開發版本:此分支包含即將發布的 v3 版本的程式碼。有關目前穩定 v1 版本的程式碼,請查看
1.x
分支。即將發布的 v3 版本將是該軟體包的前進方向。不過,我們仍然會積極支援尚未使用最新版本的 v1。另請參閱安裝說明以了解更多詳細資訊。
為了使基於非同步的庫能夠互通,它們需要使用相同的事件循環。該元件提供了任何函式庫都可以定位的通用LoopInterface
。這使得它們可以在同一個循環中使用,並透過使用者控制的一個run()
來呼叫。
目錄
快速入門範例
用法
跑步()
停止()
新增定時器()
addPeriodicTimer()
取消定時器()
futureTick()
添加訊號()
移除訊號()
新增讀流()
新增寫流()
移除ReadStream()
刪除WriteStream()
流選擇循環
外部事件循環
外部循環
外在紫外線循環
循環方法
循環自動運行
得到()
環形
循環實現
循環介面
安裝
測試
執照
更多的
這是一個僅使用事件循環建構的非同步 HTTP 伺服器。
<?phpuse ReactEventLoopLoop;需要 __DIR__ 。 '/vendor/autoload.php';$server = stream_socket_server('tcp://127.0.0.1:8080');stream_set_blocking($server, false); Loop::addReadStream($server, function ($server) {$conn = stream_socket_accept($server);$data = "HTTP/1.1 200 OKrnContent-Length: 3rnrnHin"; Loop::addWriteStream($conn, 函數 ($conn) use (&$data) {$writing = fwrite($conn, $data);if ($writing === strlen($data)) {fclose($conn ); 循環::removeWriteStream($conn); } else {$data = substr($data, $writing); } }); }); Loop::addPeriodicTimer(5, function () {$memory = memory_get_usage() / 1024;$formatted = number_format($memory, 3).'K';echo "目前記憶體使用:{$formatted}n"; });
另請參閱範例。
典型的應用程式將使用Loop
類別來使用預設事件循環,如下所示:
use ReactEventLoopLoop;$timer = Loop::addPeriodicTimer(0.1, function () {echo 'Tick' . PHP_EOL; }); Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer);echo '完成' 。 PHP_EOL; });
作為替代方案,您還可以在開始時明確建立一個事件循環實例,在整個程式中重複使用它,最後在程式末尾運行它,如下所示:
$loop = ReactEventLoopLoop::get();$timer = $loop->addPeriodicTimer(0.1, function () {echo 'Tick' . PHP_EOL; });$loop->addTimer(1.0, function () use ($loop, $timer) {$loop->cancelTimer($timer);echo '完成' . PHP_EOL; });$循環->運行();
前者較簡潔,後者較明確。在這兩種情況下,程式都會執行完全相同的步驟。
事件循環實例是在程式開始時建立的。這是在您第一次呼叫Loop
類別(或透過手動實例化任何循環實現)時隱式完成的。
事件循環直接使用或作為實例傳遞給庫和應用程式程式碼。在此範例中,週期計時器註冊到事件循環中,事件循環僅在每一小部分輸出Tick
直到另一個計時器在一秒後停止週期計時器。
事件循環在程式結束時運行。當使用Loop
類別或在程式末端明確呼叫單一run()
時,會自動完成此操作。
從v1.2.0
開始,我們強烈建議使用Loop
類別。顯式循環指令仍然有效,並且在某些應用程式中可能仍然有用,特別是在向更簡潔風格的過渡時期。
Loop
類別作為事件循環的便利全域存取器而存在。
Loop
類別提供LoopInterface
上存在的所有方法作為靜態方法:
跑步()
停止()
新增定時器()
addPeriodicTimer()
取消定時器()
futureTick()
添加訊號()
移除訊號()
新增讀流()
新增寫流()
移除ReadStream()
刪除WriteStream()
如果您在應用程式程式碼中使用事件循環,通常最簡單的方法是直接與Loop
類別中定義的靜態方法進行交互,如下所示:
use ReactEventLoopLoop;$timer = Loop::addPeriodicTimer(0.1, function () {echo 'Tick' . PHP_EOL; }); Loop::addTimer(1.0, function () use ($timer) { Loop::cancelTimer($timer);echo '完成' 。 PHP_EOL; });
另一方面,如果您熟悉物件導向程式設計 (OOP) 和相依性注入 (DI),您可能需要注入事件循環實例並呼叫LoopInterface
上的實例方法,如下所示:
使用 ReactEventLoopLoop;使用 ReactEventLoopLoopInterface;類別 Greeter {私有$loop;公用函數__construct(LoopInterface$loop) {$this->循環=$循環; }公用函數問候(字串$名稱) {$this->loop->addTimer(1.0, function () use ($name) {echo 'Hello ' . $name . '!' . PHP_EOL; }); } }$greeter = new Greeter(Loop::get());$greeter->greet('Alice');$greeter->greet('Bob');
每個靜態方法呼叫都將透過內部使用Loop::get()
呼叫原樣轉發到底層事件循環實例。有關可用方法的更多詳細信息,請參閱LoopInterface
。
當使用Loop
類別時,它會在程式結束時自動執行循環。這意味著以下範例將安排一個計時器並自動執行程序,直到計時器事件觸發:
使用ReactEventLoop循環; Loop::addTimer(1.0, function () {echo 'Hello' . PHP_EOL; });
從v1.2.0
開始,我們強烈建議以這種方式使用Loop
類別並省略任何明確的run()
呼叫。由於 BC 原因,顯式run()
方法仍然有效,並且在某些應用程式中可能仍然有用,特別是在向更簡潔風格的過渡時期。
如果您不希望Loop
自動運行,您可以明確run()
或stop()
它。如果您使用像這樣的全域異常處理程序,這會很有用:
使用ReactEventLoop循環; Loop::addTimer(10.0, function () {echo '永遠不會發生'; });set_exception_handler(function (Throwable $e) {echo '錯誤: ' . $e->getMessage() . PHP_EOL; 循環::停止(); });拋出新的RuntimeException('演示');
get(): LoopInterface
方法可用來取得目前活動的事件循環實例。
在應用程式的整個生命週期中,此方法將始終傳回相同的事件循環實例。
使用 ReactEventLoopLoop;使用 ReactEventLoopLoopInterface;$loop = Loop::get();assert($loop instanceof LoopInterface);assert($loop === Loop::get());
如果您使用物件導向程式設計 (OOP) 和相依性注入 (DI),這尤其有用。在這種情況下,您可能需要注入一個事件循環實例並呼叫LoopInterface
上的實例方法,如下所示:
使用 ReactEventLoopLoop;使用 ReactEventLoopLoopInterface;類別 Greeter {私有$loop;公用函數__construct(LoopInterface$loop) {$this->循環=$循環; }公用函數問候(字串$名稱) {$this->loop->addTimer(1.0, function () use ($name) {echo 'Hello ' . $name . '!' . PHP_EOL; }); } }$greeter = new Greeter(Loop::get());$greeter->greet('Alice');$greeter->greet('Bob');
有關可用方法的更多詳細信息,請參閱LoopInterface
。
除了LoopInterface
之外,還提供了許多事件循環實作。
所有事件循環都支援以下功能:
文件描述符輪詢
免一次定時器
週期性定時器
延遲執行未來循環標記
對於這個套件的大多數使用者來說,底層事件循環實作是一個實作細節。您應該使用Loop
類別自動建立一個新實例。
先進的!如果您明確需要某個事件循環實現,您可以手動實例化以下類別之一。請注意,您可能必須先為相應的事件循環實作安裝所需的 PHP 擴展,否則它們將在建立時拋出BadMethodCallException
。
基於stream_select()
的事件循環。
它使用了stream_select()
函數,而且是唯一可以在PHP中開箱即用的實作。
該事件循環在任何 PHP 版本上都可以開箱即用。這意味著無需安裝,該程式庫適用於所有平台和支援的 PHP 版本。因此,如果您不安裝下面列出的任何事件循環擴展, Loop
類別將預設使用此事件循環。
在底層,它執行一個簡單的select
系統呼叫。此系統呼叫僅限於FD_SETSIZE
的最大檔案描述符數量(取決於平台,通常為 1024),並以O(m)
進行擴充( m
是傳遞的最大檔案描述符數量)。這意味著您在同時處理數千個串流時可能會遇到問題,並且在這種情況下您可能需要考慮使用下面列出的替代事件循環實作之一。如果您的用例是涉及一次僅處理數十或數百個串流的許多常見用例之一,那麼此事件循環實現的效能非常好。
如果您想使用訊號處理(另請參閱下面的addSignal()
),此事件循環實作需要ext-pcntl
。此擴充僅適用於類別 Unix 平台,不支援 Windows。它通常作為許多 PHP 發行版的一部分安裝。如果缺少此擴充功能(或您在 Windows 上執行),則不支援訊號處理並拋出BadMethodCallException
。
已知在使用 PHP 7.3 之前的任何版本時,此事件循環依賴掛鐘時間來安排未來的計時器,因為單調時間來源僅在 PHP 7.3 ( hrtime()
) 中可用。雖然這不會影響許多常見用例,但對於依賴高時間精度的程式或受不連續時間調整(時間跳躍)影響的系統來說,這是一個重要的區別。這意味著,如果您在 PHP < 7.3 上安排計時器在 30 秒內觸發,然後將系統時間向前調整 20 秒,則計時器可能會在 10 秒內觸發。另請參閱addTimer()
以了解更多詳細資訊。
基於ext-event
的事件循環。
這使用了event
PECL 擴展,它提供了libevent
庫的介面。 libevent
本身支援許多特定於系統的後端(epoll、kqueue)。
已知此迴圈適用於 PHP 7.1 到 PHP 8+。
基於ext-ev
事件循環。
此循環使用ev
PECL 擴展,它提供了libev
庫的介面。 libev
本身支援許多特定於系統的後端(epoll、kqueue)。
已知此迴圈適用於 PHP 7.1 到 PHP 8+。
基於ext-uv
的事件循環。
此循環使用uv
PECL 擴展,它提供了libuv
庫的介面。 libuv
本身支援許多特定於系統的後端(epoll、kqueue)。
已知此迴圈適用於 PHP 7.1 到 PHP 8+。
run(): void
方法可用來執行事件循環,直到沒有更多任務要執行。
對於許多應用程式來說,此方法是事件循環上唯一直接可見的呼叫。根據經驗,通常建議將所有內容附加到同一個循環實例,然後在應用程式的底部執行一次循環。
$循環->運行();
此方法將保持循環運行,直到沒有更多任務要執行。換句話說:此方法將阻塞,直到最後一個計時器、串流和/或訊號被刪除。
同樣,必須確保應用程式實際呼叫此方法一次。將偵聽器新增至循環並錯過實際運行將導致應用程式退出,而不會實際等待任何附加的偵聽器。
當循環已經運行時,不得呼叫此方法。此方法可以在明確地被stop()
執行後或在自動停止後被呼叫多次,因為它以前不再有任何事情可做。
stop(): void
方法可用來指示正在運作的事件循環停止。
此方法被認為是高階用法,應謹慎使用。根據經驗,通常建議僅當循環不再有任何事情可做時才自動停止。
此方法可用於明確指示事件循環停止:
$loop->addTimer(3.0, function () use ($loop) {$loop->stop(); });
在目前未運行的循環實例或已停止的循環實例上呼叫此方法沒有任何效果。
addTimer(float $interval, callable $callback): TimerInterface
方法可用來將回調排入佇列,以便在給定時間間隔後呼叫一次。
第二個參數必須是計時器回呼函數,它接受計時器實例作為其唯一參數。如果您不在計時器回呼函數中使用計時器實例,您可以使用根本沒有參數的函數。
計時器回呼函數不得拋出Exception
。計時器回呼函數的傳回值將被忽略且不起作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
此方法傳回一個計時器實例。如上所述,相同的計時器實例也將傳遞到計時器回呼函數中。您可以呼叫cancelTimer
來取消掛起的計時器。與addPeriodicTimer()
不同,此方法將確保回呼僅在給定時間間隔後呼叫一次。
$loop->addTimer(0.8, function () {echo 'world!' . PHP_EOL; });$loop->addTimer(0.3, function () {echo '你好'; });
另請參見範例#1。
如果您想存取回調函數中的任何變量,您可以將任意資料綁定到回調閉包,如下所示:
函數 hello($name, LoopInterface $loop) {$loop->addTimer(1.0, function () use ($name) {echo "hello $namen"; } }); }hello('測試員', $loop);
此介面不會強制執行任何特定的計時器分辨率,因此如果您依賴毫秒精度或以下的非常高的精度,則可能必須特別小心。事件循環實現應該盡最大努力工作,並且應該提供至少毫秒的精度,除非另有說明。眾所周知,許多現有的事件循環實作可以提供微秒級的精度,但通常不建議依賴這種高精度。
同樣,不保證計劃同時執行(在其可能的精度範圍內)的定時器的執行順序。
此介面建議事件循環實作應該使用單調時間來源(如果可用)。鑑於預設情況下單調時間來源僅在 PHP 7.3 中可用,事件循環實作可能會回退到使用掛鐘時間。雖然這不會影響許多常見用例,但對於依賴高時間精度的程式或受不連續時間調整(時間跳躍)影響的系統來說,這是一個重要的區別。這意味著,如果您安排計時器在 30 秒內觸發,然後將系統時間向前調整 20 秒,則計時器仍應在 30 秒內觸發。另請參閱事件循環實作以了解更多詳細資訊。
addPeriodicTimer(float $interval, callable $callback): TimerInterface
方法可用來將回呼排入佇列,以便在給定時間間隔後重複呼叫。
第二個參數必須是計時器回呼函數,它接受計時器實例作為其唯一參數。如果您不在計時器回呼函數中使用計時器實例,您可以使用根本沒有參數的函數。
計時器回呼函數不得拋出Exception
。計時器回呼函數的傳回值將被忽略且不起作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
此方法傳回一個計時器實例。如上所述,相同的計時器實例也將傳遞到計時器回呼函數中。與addTimer()
不同,此方法將確保在給定間隔之後或直到您呼叫cancelTimer
為止無限地呼叫回調。
$timer = $loop->addPeriodicTimer(0.1, function () {echo 'tick!' . PHP_EOL; });$loop->addTimer(1.0, function () use ($loop, $timer) {$loop->cancelTimer($timer);echo '完成' . PHP_EOL; });
另請參見範例#2。
如果要限制執行次數,可以將任意資料綁定到回呼閉包,如下所示:
函數 hello($name, LoopInterface $loop) {$n = 3;$loop->addPeriodicTimer(1.0, 函數 ($timer) use ($name, $loop, &$n) {if ($n > 0) { --$n;echo "你好$namen"; } else {$loop->cancelTimer($timer); } }); }hello('測試員', $loop);
此介面不會強制執行任何特定的計時器分辨率,因此如果您依賴毫秒精度或以下的非常高的精度,則可能必須特別小心。事件循環實現應該盡最大努力工作,並且應該提供至少毫秒的精度,除非另有說明。眾所周知,許多現有的事件循環實作可以提供微秒級的精度,但通常不建議依賴這種高精度。
同樣,不保證計劃同時執行(在其可能的精度範圍內)的定時器的執行順序。
此介面建議事件循環實作應該使用單調時間來源(如果可用)。鑑於預設情況下單調時間來源僅在 PHP 7.3 中可用,事件循環實作可能會回退到使用掛鐘時間。雖然這不會影響許多常見用例,但對於依賴高時間精度的程式或受不連續時間調整(時間跳躍)影響的系統來說,這是一個重要的區別。這意味著,如果您安排計時器在 30 秒內觸發,然後將系統時間向前調整 20 秒,則計時器仍應在 30 秒內觸發。另請參閱事件循環實作以了解更多詳細資訊。
此外,由於每次呼叫後重新調度,週期性計時器可能會受到計時器漂移的影響。因此,通常不建議依賴此來實現毫秒精度或以下的高精度間隔。
cancelTimer(TimerInterface $timer): void
方法可用來取消掛起的計時器。
另請參見addPeriodicTimer()
和範例 #2。
在尚未新增至此循環實例的計時器實例上或在已取消的計時器上呼叫此方法沒有任何效果。
futureTick(callable $listener): void
方法可用來安排在事件循環的未來標記上呼叫的回呼。
這與間隔為零秒的計時器非常相似,但不需要調度計時器佇列的開銷。
刻度回呼函數必須能夠接受零個參數。
刻度回呼函數不得拋出Exception
。刻度回調函數的傳回值將被忽略且沒有任何作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
如果您想存取回調函數中的任何變量,您可以將任意資料綁定到回調閉包,如下所示:
函數 hello($name, LoopInterface $loop) {$loop->futureTick(function () use ($name) {echo "hello $namen"; } }); }hello('測試員', $loop);
與計時器不同,滴答回調保證按照它們排隊的順序執行。此外,一旦回呼被排隊,就無法取消此操作。
這通常用於將較大的任務分解為較小的步驟(協作多工處理的一種形式)。
$loop->futureTick(function () {echo 'b'; });$loop->futureTick(function () {echo 'c'; });回顯'a';
另請參見範例#3。
addSignal(int $signal, callable $listener): void
方法可用來註冊偵聽器,以便在該程序捕獲到訊號時收到通知。
這對於捕獲來自supervisor
或systemd
等工具的用戶中斷訊號或關閉訊號非常有用。
第二個參數必須是偵聽器回呼函數,它接受訊號作為其唯一參數。如果您不在偵聽器回呼函數中使用訊號,則可以使用根本沒有參數的函數。
偵聽器回呼函數不得拋出Exception
。偵聽器回呼函數的傳回值將被忽略且不起作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
$loop->addSignal(SIGINT, function (int $signal) {echo '捕獲使用者中斷訊號' . PHP_EOL; });
另請參見範例#4。
訊號僅在類 Unix 平台上可用,由於作業系統限制,不支援 Windows。如果此平台不支援訊號,例如缺少所需的擴充時,此方法可能會引發BadMethodCallException
。
注意:偵聽器只能向相同訊號新增一次,任何多次新增偵聽器的嘗試都會被忽略。
removeSignal(int $signal, callable $listener): void
方法可用來刪除先前新增的訊號偵聽器。
$loop->removeSignal(SIGINT, $listener);
任何刪除未註冊偵聽器的嘗試都將被忽略。
先進的!請注意,此低階 API 被視為進階用法。大多數用例可能應該使用更高層級的可讀 Stream API。
addReadStream(resource $stream, callable $callback): void
方法可用來註冊偵聽器,以便在流準備好讀取時收到通知。
第一個參數必須是有效的流資源,支援檢查它是否準備好透過此循環實作讀取。單一流資源不得添加多次。相反,要麼先呼叫removeReadStream()
,要麼使用單一偵聽器對此事件作出反應,然後從此偵聽器分派。如果此循環實作不支援給定的資源類型,則此方法可能會拋出Exception
。
第二個參數必須是偵聽器回呼函數,它接受流資源作為其唯一參數。如果您不在偵聽器回呼函數中使用串流資源,您可以使用根本沒有參數的函數。
偵聽器回呼函數不得拋出Exception
。偵聽器回呼函數的傳回值將被忽略且不起作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
如果您想存取回調函數中的任何變量,您可以將任意資料綁定到回調閉包,如下所示:
$loop->addReadStream($stream, function ($stream) use ($name) {echo $name . ' said: ' . fread($stream); });
另請參見範例#11。
您可以呼叫removeReadStream()
來刪除該流的讀取事件偵聽器。
不保證多個流同時就緒時監聽器的執行順序。
已知某些事件循環實作僅在流變得可讀(邊緣觸發)時才觸發偵聽器,而如果流從一開始就可讀,則可能不會觸發。這也意味著當資料仍保留在 PHP 的內部流緩衝區中時,流可能不會被識別為可讀。因此,建議使用stream_set_read_buffer($stream, 0);
在這種情況下會停用 PHP 的內部讀取緩衝區。
先進的!請注意,此低階 API 被視為進階用法。大多數用例可能應該使用更高層級的可寫入 Stream API。
addWriteStream(resource $stream, callable $callback): void
方法可用來註冊偵聽器,以便在流準備寫入時收到通知。
第一個參數必須是有效的流資源,支援透過此循環實現檢查它是否準備好寫入。單一流資源不得添加多次。相反,要麼先呼叫removeWriteStream()
,要麼使用單一偵聽器對此事件作出反應,然後從此偵聽器分派。如果此循環實作不支援給定的資源類型,則此方法可能會拋出Exception
。
第二個參數必須是偵聽器回呼函數,它接受流資源作為其唯一參數。如果您不在偵聽器回呼函數中使用串流資源,您可以使用根本沒有參數的函數。
偵聽器回呼函數不得拋出Exception
。偵聽器回呼函數的傳回值將被忽略且不起作用,因此出於效能原因,建議您不要傳回任何過多的資料結構。
如果您想存取回調函數中的任何變量,您可以將任意資料綁定到回調閉包,如下所示:
$loop->addWriteStream($stream, 函數 ($stream) use ($name) {fwrite($stream, 'Hello ' . $name); });
另請參閱範例#12。
您可以呼叫removeWriteStream()
來刪除該流的寫入事件偵聽器。
不保證多個流同時就緒時監聽器的執行順序。
removeReadStream(resource $stream): void
方法可用來刪除給定流的讀取事件偵聽器。
從循環中刪除已刪除的流或嘗試刪除從未新增或無效的流不會產生任何效果。
removeWriteStream(resource $stream): void
方法可用來刪除給定流的寫入事件偵聽器。
從循環中刪除已刪除的流或嘗試刪除從未新增或無效的流不會產生任何效果。
建議的安裝此程式庫的方法是透過 Composer。作曲家新手?
一旦發布,該專案將遵循 SemVer。目前,這將安裝最新的開發版本:
作曲家需要反應/事件循環:^3@dev
有關版本升級的詳細信息,請請參閱變更日誌。
該專案旨在在任何平台上運行,因此不需要任何 PHP 擴展,並支援在 PHP 7.1 到當前 PHP 8+ 上運行。強烈建議為此專案使用最新支援的 PHP 版本。
建議安裝任何事件循環擴展,但完全可選。另請參閱事件循環實作以了解更多詳細資訊。
要執行測試套件,您首先需要複製此儲存庫,然後透過 Composer 安裝所有相依性:
作曲家安裝
要運行測試套件,請前往專案根目錄並運行:
供應商/bin/phpunit
麻省理工學院,請參閱許可證文件。
有關如何在實際應用程式中使用流的更多信息,請參閱我們的 Stream 元件。
請參閱我們的使用者 wiki 和 Packagist 的依賴項,以了解在實際應用程式中使用 EventLoop 的套件清單。