brunonatali/event-loop

ReactPHP的核心反应器事件循环,库可以使用它来实现基于事件的网络输入/输出。

1.0 2019-09-20 17:31 UTC

This package is auto-updated.

Last update: 2024-09-21 20:38:36 UTC


README

  • 最初从'ReactPHP'复制,包括一些测试函数和特定的使用实现更改。
  • 移除了'测试'和'示例'原始文件夹。

Build Status

ReactPHP的核心反应器事件循环,库可以使用它来实现基于事件的网络输入/输出。

为了使基于异步的库可以互操作,它们需要使用相同的事件循环。此组件提供了一个通用的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();
  1. 事件循环实例在程序开始时创建。此库提供了一个方便的工厂React\EventLoop\Factory::create(),它会选择最佳可用的循环实现
  2. 事件循环实例可以直接使用或传递给库和应用程序代码。在这个例子中,使用事件循环注册了一个周期性定时器,每秒简单地输出Tick,并且使用ReactPHP的可读流组件创建了一个可读流,用于演示目的。
  3. 事件循环在程序结束时通过一个单一的$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扩展。本身支持多种系统特定的后端(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方法将回调函数排入队列,在给定间隔后调用一次。

定时器回调函数必须能够接受单个参数,即此方法也返回的定时器实例,或者您可以使用没有任何参数的函数。

定时器回调函数不得抛出Exception。定时器回调函数的返回值将被忽略,因此出于性能考虑,建议不要返回任何大量数据结构。

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 方法将回调排队,以便在给定间隔后重复调用。

定时器回调函数必须能够接受单个参数,即此方法也返回的定时器实例,或者您可以使用没有任何参数的函数。

定时器回调函数不得抛出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 '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方法来注册一个监听器,当该进程捕获到信号时被通知。

这适用于捕获用户中断信号或来自supervisorsystemd等工具的关闭信号。

监听器回调函数必须能够接受一个参数,即由该方法添加的信号,或者您可以使用没有任何参数的函数。

监听器回调函数不得抛出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的新用户,请参阅Composer入门指南

此项目遵循SemVer。这将安装最新的支持版本。

$ composer require react/event-loop:^1.1

有关版本升级的详细信息,请参阅变更日志

此项目旨在在任何平台上运行,因此不需要任何PHP扩展,并支持在从PHP 5.3到当前PHP 7+和HHVM的旧版PHP上运行。强烈建议为此项目使用PHP 7+。

建议安装事件循环扩展,但这完全是可选的。有关更多详细信息,请参阅事件循环实现

测试

要运行测试套件,首先需要克隆此仓库,然后通过Composer安装所有依赖项。

$ composer install

要运行测试套件,请转到项目根目录并运行

$ php vendor/bin/phpunit

许可

MIT,请参阅LICENSE文件

更多