raimx / event-loop
ReactPHP 的核心反应器事件循环,库可以使用它进行事件驱动的 I/O。
Requires
- php: >=5.3.0
Requires (Dev)
- phpunit/phpunit: ^7.0 || ^6.4 || ^5.7 || ^4.8.35
Suggests
- ext-event: ~1.0 for ExtEventLoop
- ext-pcntl: For signal handling support when using the StreamSelectLoop
- ext-uv: * for ExtUvLoop
README
ReactPHP 的核心反应器事件循环,库可以使用它进行事件驱动的 I/O。
为了使基于异步的库能够互操作,它们需要使用相同的事件循环。此组件提供了一个通用的 LoopInterface
,任何库都可以针对它。这使得它们可以在同一个循环中使用,通过用户控制的单个 run()
调用来使用。
目录
快速入门示例
以下是一个仅使用事件循环构建的异步 HTTP 服务器。
$loop = React\EventLoop\Factory::create(); $server = stream_socket_server('tcp://127.0.0.1:8080'); stream_set_blocking($server, false); $loop->addReadStream($server, function ($server) use ($loop) { $conn = stream_socket_accept($server); $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n"; $loop->addWriteStream($conn, function ($conn) use (&$data, $loop) { $written = fwrite($conn, $data); if ($written === strlen($data)) { fclose($conn); $loop->removeWriteStream($conn); } else { $data = substr($data, $written); } }); }); $loop->addPeriodicTimer(5, function () { $memory = memory_get_usage() / 1024; $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); $loop->run();
另请参阅 示例。
用法
典型应用程序使用单个事件循环,它在程序开始时创建,在程序结束时运行。
// [1] $loop = React\EventLoop\Factory::create(); // [2] $loop->addPeriodicTimer(1, function () { echo "Tick\n"; }); $stream = new React\Stream\ReadableResourceStream( fopen('file.txt', 'r'), $loop ); // [3] $loop->run();
- 循环实例在程序开始时创建。此库提供了一个便利的工厂
React\EventLoop\Factory::create()
,它选择最佳可用的 循环实现。 - 循环实例直接使用或传递给库和应用程序代码。在此示例中,将周期性计时器注册到事件循环,每秒简单地输出
Tick
,并使用 ReactPHP 的 可读流 组件创建一个可读流,用于演示目的。 - 在程序结束时通过单个
$loop->run()
调用来运行循环。
工厂
类 Factory
存在是为了方便选择最佳可用的 事件循环实现。
create()
可以使用 create(): LoopInterface
方法创建一个新的事件循环实例
$loop = React\EventLoop\Factory::create();
此方法始终返回一个实现 LoopInterface
的实例,实际 事件循环实现 是实现细节。
通常,此方法应该在程序开始时仅调用一次。
循环实现
除了 LoopInterface
,还提供了一系列事件循环实现。
所有事件循环都支持以下功能
- 文件描述符轮询
- 一次性定时器
- 周期性定时器
- 在未来的事件循环中延迟执行
对于大多数此包的消费者来说,底层事件循环实现是实现细节。您应该使用 Factory
来自动创建一个新的实例。
高级!如果您需要特定的事件循环实现,您可以手动实例化以下类之一。请注意,您可能必须首先安装相应事件循环实现的必需PHP扩展,否则在创建时将抛出 BadMethodCallException
。
StreamSelectLoop
基于 stream_select()
的事件循环。
这使用 stream_select()
函数,并且是与PHP完全兼容的唯一实现。
此事件循环在PHP 5.3到PHP 7+和HHVM上开箱即用。这意味着不需要安装,并且此库在所有平台上以及支持的PHP版本上运行。相应地,如果您没有安装以下列出的任何事件循环扩展,则 Factory
将默认使用此事件循环。
底层,它执行简单的 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()
。
ExtEventLoop
基于 ext-event
的事件循环。
这使用 event
PECL 扩展。它支持与libevent相同的后端。
此循环已知与PHP 5.4到PHP 7+兼容。
ExtEvLoop
基于 ext-ev
的事件循环。
此循环使用 ev
PECL 扩展,该扩展提供对 libev
库的接口。
此循环已知与PHP 5.4到PHP 7+兼容。
ExtUvLoop
基于 ext-uv
的事件循环。
此循环使用uv
PECL 扩展,它为 libuv
库提供了一个接口。
已知此循环与 PHP 7+ 兼容。
ExtLibeventLoop
基于 ext-libevent
的事件循环。
这使用libevent
PECL 扩展。 libevent
本身支持多种系统特定的后端(epoll,kqueue)。
此事件循环仅与 PHP 5 兼容。存在一个针对 PHP 7 的非官方更新,但它由于 SEGFAULT
而导致频繁崩溃。重申一点:不建议在 PHP 7 上使用此事件循环。因此,Factory
将不会尝试在 PHP 7 上使用此事件循环。
已知此事件循环仅在流 变为 可读时触发可读监听器(边缘触发),并且如果流从一开始就已被读,则可能不会触发。这也意味着,当 PHP 内部流缓冲区中仍有数据时,流可能不会被识别为可读。因此,建议在此情况下使用 stream_set_read_buffer($stream, 0);
禁用 PHP 的内部读缓冲区。有关更多详细信息,请参阅addReadStream()
。
ExtLibevLoop
基于 ext-libev
的事件循环。
这使用一个非官方 libev
扩展。它支持与 libevent 相同的后端。
此循环仅与 PHP 5 兼容。PHP 7 的更新不太可能很快发生。
LoopInterface
run()
run(): void
方法可用于运行事件循环,直到没有更多任务要执行。
对于许多应用程序,此方法是事件循环上唯一直接可见的调用。通常,建议将所有内容附加到同一个循环实例,然后在应用程序的底部运行一次循环。
$loop->run();
此方法将保持循环运行,直到没有更多任务要执行。换句话说:此方法将阻塞,直到最后一个计时器、流和/或信号被移除。
同样,确保应用程序实际上调用此方法一次至关重要。向循环添加监听器而未实际运行它会导致应用程序在没有等待任何附加监听器的情况下退出。
当循环正在运行时,不得调用此方法。此方法可以在显式调用stop()
后多次调用,或者在它自动停止(因为它之前不再有事情要做)后调用。
stop()
stop(): void
方法可用于指示正在运行的事件循环停止。
此方法被认为是高级用法,应谨慎使用。通常,建议仅在循环不再有事情要做时自动停止循环。
可以使用此方法显式指示事件循环停止
$loop->addTimer(3.0, function () use ($loop) { $loop->stop(); });
在非运行状态或已经停止的循环实例上调用此方法没有任何效果。
addTimer()
addTimer(float $interval, callable $callback): TimerInterface
方法可以用来在指定的时间间隔后执行回调。
定时器回调函数必须能够接受一个参数,即定时器实例,也可以使用没有参数的函数。
定时器回调函数不得抛出异常。定时器回调函数的返回值将被忽略,因此出于性能考虑,建议不要返回任何过多的数据结构。
与 addPeriodicTimer()
不同,此方法将确保在给定的时间间隔后只调用回调一次。您可以调用 cancelTimer
来取消挂起的定时器。
$loop->addTimer(0.8, function () { echo 'world!' . PHP_EOL; }); $loop->addTimer(0.3, function () { echo 'hello '; });
另请参阅示例 #1。
如果您想在回调函数中访问任何变量,可以像这样将任意数据绑定到回调闭包
function hello($name, LoopInterface $loop) { $loop->addTimer(1.0, function () use ($name) { echo "hello $name\n"; }); } hello('Tester', $loop);
此接口不强制执行任何特定的定时器分辨率,因此如果您依赖于毫秒级或更高精度的极高精度,可能需要特别注意。事件循环实现应该尽最大努力工作,并且除非另有说明,至少应提供毫秒级精度。许多现有的事件循环实现已知提供微秒级精度,但通常不建议依赖于这种高精度。
同样,同一时间(在其可能的精度内)安排执行的定时器的执行顺序没有保证。
此接口建议,如果可用,事件循环实现应该使用单调时间源。鉴于默认情况下只有从PHP 7.3开始单调时间源才可用,事件循环实现可能回退到使用系统时间。虽然这不会影响许多常见用例,但对于依赖于高时间精度或受不连续时间调整(时间跳跃)影响的系统,这是一个重要的区别。这意味着如果您安排定时器在30秒后触发,然后调整系统时间向前20秒,定时器仍然应该在30秒后触发。有关更多信息,请参阅事件循环实现。
addPeriodicTimer()
addPeriodicTimer(float $interval, callable $callback): TimerInterface
方法可以用来在指定的时间间隔后重复执行回调。
定时器回调函数必须能够接受一个参数,即定时器实例,也可以使用没有参数的函数。
定时器回调函数不得抛出异常。定时器回调函数的返回值将被忽略,因此出于性能考虑,建议不要返回任何过多的数据结构。
与 addTimer()
不同,此方法将确保在给定的时间间隔后无限期地调用回调,直到您调用 cancelTimer
。
$timer = $loop->addPeriodicTimer(0.1, function () { echo 'tick!' . PHP_EOL; }); $loop->addTimer(1.0, function () use ($loop, $timer) { $loop->cancelTimer($timer); echo 'Done' . PHP_EOL; });
另请参阅示例 #2。
如果您想限制执行次数,可以像这样将任意数据绑定到回调闭包
function hello($name, LoopInterface $loop) { $n = 3; $loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) { if ($n > 0) { --$n; echo "hello $name\n"; } else { $loop->cancelTimer($timer); } }); } hello('Tester', $loop);
此接口不强制执行任何特定的定时器分辨率,因此如果您依赖于毫秒级或更高精度的极高精度,可能需要特别注意。事件循环实现应该尽最大努力工作,并且除非另有说明,至少应提供毫秒级精度。许多现有的事件循环实现已知提供微秒级精度,但通常不建议依赖于这种高精度。
同样,同一时间(在其可能的精度内)安排执行的定时器的执行顺序没有保证。
此接口建议,如果可用,事件循环实现应该使用单调时间源。鉴于默认情况下只有从PHP 7.3开始单调时间源才可用,事件循环实现可能回退到使用系统时间。虽然这不会影响许多常见用例,但对于依赖于高时间精度或受不连续时间调整(时间跳跃)影响的系统,这是一个重要的区别。这意味着如果您安排定时器在30秒后触发,然后调整系统时间向前20秒,定时器仍然应该在30秒后触发。有关更多信息,请参阅事件循环实现。
此外,周期性定时器可能因每次调用后的重新安排而受到计时器漂移的影响。因此,通常不建议用于毫秒级或更高精度的高精度间隔。
cancelTimer()
cancelTimer(TimerInterface $timer): void
方法可以用来取消挂起的定时器。
另请参阅 addPeriodicTimer()
和 示例 #2。
在未添加到循环实例的定时器实例或已取消的定时器上调用此方法没有任何效果。
futureTick()
futureTick(callable $listener): void
方法可用于在事件循环的将来滴答时调度回调。
这与零秒间隔的定时器非常相似,但不需要调度定时器队列的开销。
滴答回调函数必须能够接受零个参数。
滴答回调函数不得抛出 Exception
。滴答回调函数的返回值将被忽略并没有任何效果,因此出于性能考虑,建议不要返回任何过多的数据结构。
如果您想在回调函数中访问任何变量,可以像这样将任意数据绑定到回调闭包
function hello($name, LoopInterface $loop) { $loop->futureTick(function () use ($name) { echo "hello $name\n"; }); } hello('Tester', $loop);
与定时器不同,滴答回调保证按照入队顺序执行。此外,一旦回调入队,就无法取消此操作。
这通常用于将更大的任务分解成更小的步骤(一种合作多任务的形式)。
$loop->futureTick(function () { echo 'b'; }); $loop->futureTick(function () { echo 'c'; }); echo 'a';
另请参阅 示例 #3。
addSignal()
addSignal(int $signal, callable $listener): void
方法可用于注册一个监听器,当此进程捕获到信号时通知。
这有助于捕获用户中断信号或来自 supervisor
或 systemd
等工具的关闭信号。
监听器回调函数必须能够接受单个参数,即由此方法添加的信号,或者您可以使用没有任何参数的函数。
监听器回调函数不得抛出 Exception
。监听器回调函数的返回值将被忽略并没有任何效果,因此出于性能考虑,建议不要返回任何过多的数据结构。
$loop->addSignal(SIGINT, function (int $signal) { echo 'Caught user interrupt signal' . PHP_EOL; });
另请参阅 示例 #4。
信号仅在类Unix平台可用,由于操作系统限制,Windows不受支持。如果此平台不支持信号,则此方法可能抛出 BadMethodCallException
,例如当缺少必需的扩展时。
注意:监听器只能添加一次到同一个信号,任何尝试添加多次都将被忽略。
removeSignal()
removeSignal(int $signal, callable $listener): void
方法可用于删除之前添加的信号监听器。
$loop->removeSignal(SIGINT, $listener);
尝试删除未注册的监听器的任何尝试都将被忽略。
addReadStream()
高级!请注意,此低级API被认为是高级用法。大多数用例可能应该使用更高级的 可读流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的内部读取缓冲区。
addWriteStream()
高级!请注意,此低级API被认为是高级用法。大多数情况下可能应该使用更高级的可写流API。
可以使用addWriteStream(resource $stream, callable $callback): void
方法注册一个监听器,以便在流准备好写入时得到通知。
第一个参数必须是支持通过此循环实现检查是否可写的有效流资源。单个流资源不得重复添加。相反,要么先调用removeWriteStream()
,要么通过单个监听器响应此事件,然后再从该监听器进行分发。如果给定的资源类型不支持此循环实现,则此方法可能会抛出Exception
异常。
监听器回调函数必须能够接受一个参数,即由该方法添加的流资源,或者您可以使用没有任何参数的函数。
监听器回调函数不得抛出 Exception
。监听器回调函数的返回值将被忽略并没有任何效果,因此出于性能考虑,建议不要返回任何过多的数据结构。
如果您想在回调函数中访问任何变量,可以像这样将任意数据绑定到回调闭包
$loop->addWriteStream($stream, function ($stream) use ($name) { fwrite($stream, 'Hello ' . $name); });
请参阅示例 #12。
您可以通过调用removeWriteStream()
来移除此流的写入事件监听器。
当多个流同时准备好时,监听器的执行顺序没有保证。
removeReadStream()
可以使用removeReadStream(resource $stream): void
方法移除给定流的读取事件监听器。
从循环中移除已移除的流或尝试移除从未添加或无效的流没有任何效果。
removeWriteStream()
可以使用removeWriteStream(resource $stream): void
方法移除给定流的写入事件监听器。
从循环中移除已移除的流或尝试移除从未添加或无效的流没有任何效果。
安装
安装此库的推荐方法是通过Composer。 对Composer不熟悉?
此项目遵循SemVer。这将安装最新支持版本。
$ composer require react/event-loop:^1.1
有关版本升级的详细信息,请参阅变更日志。
本项目旨在在任何平台上运行,因此不需要任何PHP扩展,并支持在旧版PHP 5.3到当前PHP 7+以及HHVM上运行。强烈建议使用PHP 7+进行此项目。
建议安装事件循环扩展,但这完全可选。有关详细信息,请参阅事件循环实现。
测试
要运行测试套件,您首先需要克隆此存储库,然后通过Composer安装所有依赖项
$ composer install
要运行测试套件,请转到项目根目录并运行
$ php vendor/bin/phpunit
许可证
MIT,请参阅许可证文件。
更多信息
- 有关如何在现实应用中使用流的信息,请参阅我们的流组件。
- 请参阅我们的用户Wiki和Packagist上的依赖项,以获取使用EventLoop的现实应用中包的列表。