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
之外,还提供了许多事件循环实现。
所有事件循环都支持以下功能:
文件描述符轮询
一次性定时器
周期性定时器
未来循环tick 上的延迟执行
对于这个包的大多数使用者来说,底层事件循环实现是一个实现细节。您应该使用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 的包列表。