0Pricing
PHP Academy · 课时

使用 ReactPHP 的事件循环

使用 ReactPHP 事件循环运行非阻塞 I/O。

使用 ReactPHP 的事件循环 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

一句话介绍 ReactPHP

ReactPHP 是一个纯 PHP、无依赖的非阻塞输入/输出工具集,围绕单个事件循环构建。无需扩展——它使用标准 PHP 中的 stream_select(如果可用,也可以使用 ext-event/ext-ev)。借助它,您可以使用普通 PHP 编写常驻服务器、并行 HTTP 客户端和流式处理管道。

全局循环

现代 ReactPHP(v1.2+)通过静态 React\EventLoop\Loop 门面提供默认循环,因此无需到处传递循环实例。使用计时器调度回调;循环会一直运行,直到没有更多工作。

<?php
require 'vendor/autoload.php';
use React\EventLoop\Loop;

Loop::addTimer(1.0, function () {
    echo "fired after 1 second\n";
});
echo "scheduled\n";
// Loop::run() is auto-invoked at script shutdown in v1.2+

计时器与周期性工作

非阻塞计时取代了 sleep()。addTimer 触发一次;addPeriodicTimer 重复触发。取消任一计时器即可停止。由于这些操作由循环驱动,计时器等待期间其他任务仍会继续运行。

<?php
use React\EventLoop\Loop;

$count = 0;
$timer = Loop::addPeriodicTimer(0.5, function () use (&$count, &$timer) {
    echo 'tick ' . (++$count) . "\n";
    if ($count === 4) {
        Loop::cancelTimer($timer); // stop after 4 ticks
    }
});

异步结果的承诺对象

ReactPHP 会为任何稍后完成的操作返回一个承诺对象。使用 then() 附加成功回调,再附加第二个回调处理失败。承诺对象可以组合,因此您可以转换并串联结果,而无需深度嵌套回调。

<?php
use React\Promise\Promise;

function asyncDouble(int $n): Promise {
    return new Promise(function ($resolve) use ($n) {
        \React\EventLoop\Loop::addTimer(0.1, fn() => $resolve($n * 2));
    });
}

asyncDouble(21)
    ->then(fn($r) => printf("result: %d\n", $r))   // 42
    ->catch(fn(\Throwable $e) => printf("err: %s\n", $e->getMessage()));

使用异步客户端并行发送 HTTP 请求

最大的优势是:从单个线程并发发起许多 HTTP 请求。React\Http\Browser 返回承诺对象;React\Promise\all() 会等待整个批次完成。十个耗时 1 秒的请求大约 1 秒即可完成,而不是 10 秒。

<?php
use React\Http\Browser;
use function React\Promise\all;

$browser = new Browser();
$urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/1',
];
$requests = array_map(fn($u) => $browser->get($u), $urls);

all($requests)->then(function (array $responses) {
    echo 'all done: ' . count($responses) . " responses\n";
});

借助纤程让代码读起来像同步代码

回调链会变得难以维护。ReactPHP 与 react/async 集成,后者使用 PHP 8.1 的纤程,因此您可以对承诺对象调用 await(),并编写直线式代码。coroutine()/async() 包装器会在纤程中运行您的函数。

<?php
use function React\Async\async;
use function React\Async\await;
use React\Http\Browser;

$main = async(function () {
    $browser = new Browser();
    $a = await($browser->get('https://httpbin.org/uuid'));
    $b = await($browser->get('https://httpbin.org/uuid'));
    echo (string) $a->getBody();
    echo (string) $b->getBody();
});
$main();

非阻塞 TCP 服务器

ReactPHP 非常适合服务器。SocketServer 接受连接并发出事件;每个连接本身也是一个非阻塞流。单个进程可以处理数千个同时连接的客户端。

<?php
use React\Socket\SocketServer;
use React\Socket\ConnectionInterface;

$socket = new SocketServer('127.0.0.1:8080');
$socket->on('connection', function (ConnectionInterface $conn) {
    $conn->write("Welcome\n");
    $conn->on('data', function ($data) use ($conn) {
        $conn->write('echo: ' . $data); // streamed back, non-blocking
    });
});
echo "listening on 127.0.0.1:8080\n";

用几行代码创建 HTTP 服务器

react/http 服务器通过回调处理请求,回调返回一个 Response(或返回该对象的承诺对象)。由于它是常驻进程,依赖只需初始化一次——但您也要负责管理跨请求的内存。

<?php
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
use React\Socket\SocketServer;

$http = new HttpServer(function (ServerRequestInterface $request) {
    return Response::json(['path' => $request->getUri()->getPath()]);
});
$http->listen(new SocketServer('0.0.0.0:8000'));
echo "http://0.0.0.0:8000\n";

无需缓冲的流式处理

ReactPHP 流实现了反压:您可以将可读流 pipe() 到可写流中,运行时会自动进行限速。这样,您就能以稳定且较小的内存占用传输数 GB 数据,而不必将文件加载到 RAM 中。

<?php
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$source = new ReadableResourceStream(fopen('big.log', 'r'));
$dest   = new WritableResourceStream(fopen('php://stdout', 'w'));
$source->pipe($dest); // back-pressure aware, never buffers whole file

最严重的错误:阻塞循环

只要您在处理器中调用阻塞函数,一切就会立刻出问题——sleep()、阻塞式 PDO、curl_exec,以及对网址调用 file_get_contents。它们会冻结单个线程,使每个连接都停滞。请使用 ReactPHP 的等价功能:使用 Loop::addTimer 实现延迟,使用 clue/reactphp-* 或异步数据库驱动执行查询,使用 Browser 发起 HTTP 请求。

<?php
use React\EventLoop\Loop;
use function React\Async\await;
use React\Promise\Deferred;

// BAD:  sleep(2);  // freezes the entire loop
// GOOD: async delay that yields control
function delay(float $s): \React\Promise\PromiseInterface {
    $d = new Deferred();
    Loop::addTimer($s, fn() => $d->resolve(null));
    return $d->promise();
}
// await(delay(2.0)); // inside a fiber/coroutine

限制并发性

无限制的扇出可能耗尽文件描述符,或给下游应用程序接口造成过大压力。ReactPHP 的 clue/mq-react 会在并发上限下对工作进行排队:一次处理 N 个承诺对象,其余任务等待。这会对您自身的出站负载施加反压。

<?php
use Clue\React\Mq\Queue;
use React\Http\Browser;

$browser = new Browser();
// At most 10 in-flight requests across 1000 URLs
$q = new Queue(10, null, fn($url) => $browser->get($url));

$promises = array_map(fn($u) => $q($u), $thousandUrls ?? []);
\React\Promise\all($promises)->then(function ($responses) {
    echo 'completed ' . count($responses) . " with cap of 10\n";
});

快速检查

如何在 ReactPHP 处理器中暂停 2 秒,同时不影响并发性?

回顾

ReactPHP 为原生 PHP 带来了非阻塞 I/O:

  • 单个事件循环(Loop 门面)驱动计时器、套接字和流——无需额外扩展。
  • 操作会返回承诺;all() 会并行运行请求,而 react/async 与纤程让您可以使用 await(),编写出看起来像同步代码的程序。
  • 构建常驻的TCP/HTTP 服务器和具备背压的流,让每个进程能够处理数千个连接。
  • 切勿阻塞事件循环——使用 React 计时器和异步驱动程序替代 sleep 及阻塞式 I/O。

常见问题解答

「使用 ReactPHP 的事件循环」课时是免费的吗?

是的 — 「使用 ReactPHP 的事件循环」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「使用 ReactPHP 的事件循环」这节课中我会学到什么?

使用 ReactPHP 事件循环运行非阻塞 I/O。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「使用 ReactPHP 的事件循环」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. PHP 并发模型
  2. PHP 8.1 Fibers
  3. 使用 ReactPHP 的事件循环
  4. 使用 Swoole 构建高性能服务器
← 返回 PHP Academy