ReactPHP-EventLoop
事件循环组件
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();
1.循环在程序开始时创建。这个库提供了一个很方便的工厂React\EventLoop\Factory::create()
,可以挑选可用的最佳循环实现。
2.循环被直接使用或者传给库和应用使用。一个周期性定时器被注册到循环中,每秒输出Tick
,ReactPHP的流组建创建了一个可以读的流,用于演示。
3.在程序最后,通过方法$loop->run()
运行循环。
工厂
工厂类提供了一个简单的方法挑选一个最佳的可以用事件循环实现。
create()
create(): LoopInterface
方法可以创建一个新的事件循环实例。
$loop = React\EventLoop\Factory::create();
这个方法总是返回一个实现LoopInterface
的实例。实际的事件循环实现是实现细节。
这个方法应该只在程序开始时被调用。
循环实现
除了LoopInterface
,还提供了很多事件循环实现。
所有事件循环都支持这些特性:
- 文件描述轮询
- 一次性计时器
- 周期性计时器
- 在之后的循环中延迟执行
对于大多数包的消费者来说,底层事件循环实现是实现细节。你应该用工厂(Factory)自动创建一个新的实例。
高级操作!如果你需要一个明确类型的事件循环实现,你可以手动创建一个以下类型的时间循环。注意,第一次实现事件循环,你可能必须安装对应事件循环所需的PHP扩展,不然他们会在创建时抛出一个BadMethodCallException
异常。
StreamSelectLoop
一个基于stream_select()
的事件循环。
它使用了stream_select()函数,这是PHP中唯一可以使用的实现。
这个事件循环适用于PHP5.3到PHP7+版本和HHVM。这意味着不需要安装,这个库可以在所有平台和支持的PHP版本上运行。因此,如果你没有安装任何下列事件循环扩展的情况下,工厂类会默认使用这个事件循环。
在幕后,它执行一个简单的select
系统调用。此系统调用仅限于FD_SETSIZE的最大文件描述符数量(取决于平台,通常为1024),并且以O(m)进行缩放(m是传递的最大文件描述符数量)。这意味着同时处理数千个流时可能会有问题,你可能希望在下列事件循环实例中选一种可替代的循环来处理这种情况。如果您的用例是许多常见的用例之一,其中涉及一次只处理几十或几百个流,那么这个事件循环实现非常适用。
如果你想用标志处理(参考下面的addSignal()
),这个事件循环实现需要ext-pcntl
。这个扩展仅支持类Unix平台,不支持Windows系统。它通常作为PHP众多发行版的一部分安装。如果没有安装这个扩展(或者在Windows系统运行),将不支持标志处理,并会抛出一个BadMethodCallException
异常。
众所周知,这个事件循环依赖于挂钟时间(系统时间?)来安排未来的定时器,因为默认情况下PHP中没有单调时间源。虽然这不会影响许多常见用例,但这对于依赖高时间精度的程序或者受到不连续时间调整(时间跳跃)的系统来说是一个重要的区别。这意味着,如果你设置一个触发器在30s后触发,然后把你系统时间向前调整20s,这个触发器可能会在10s后就触发。详情请参考addTimer()
。
ExtEventLoop
基于ext-event
的事件循环。
此循环使用 event PECL扩展。它支持与libevent相同的后端。
已知这个循环可以在PHP5.4到PHP7+的版本中运行。
ExtEvLoop
基于ext-ev
的事件循环。
此循环使用ev PECL 扩展,它为libev库提供了一个接口。
已知这个循环可以在PHP5.4到PHP7+的版本中运行。
ExtLibeventLoop
基于ext-libevent
的事件循环
循环将使用 libevent PECL 扩展。libevent本身支持许多系统特定的后端(epoll, kqueue)。
这个事件循环只能在PHP5上运行。一个非官方的PHP7更新确实存在,但是众所周知它因SEGFAUL原因导致定期崩溃。重申:不推荐在PHP7版本使用这个事件循环。因此,工厂类不会在PHP7版本使用这个事件循环。
已知此事件循环仅在流变得可读(边触发)时触发可读侦听器,如果流从一开始就已可读,则可能不触发。这也意味着,当数据仍然留在PHP的内部流缓冲区中时,流可能不会被识别为可读的。因此,建议使用stream_set_read_buffer($stream,0)
在这种情况下禁用PHP的内部读缓冲区。参阅addReadStream()
查询更多详情。
ExtLibevLoop
基于ext-libev
的事件循环。
这个循环使用非官方的 libev 扩展。它支持与lbevent相同的后端。
这个循环仅仅支持PHP5版本,段时间内不太可能支持PHP7。
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默认情况下不可用单调时间源,事件循环实例也许会倒回去使用挂钟时间。虽然这不会影响许多常见用例,但这对于依赖高时间精度的程序或者受到不连续时间调整(时间跳跃)的系统来说是一个重要的区别。这意味着你设置一个定时器30s后执行,然后将你的系统时间向前调整20s,这个计时器应该依旧在30s后执行。