读完这篇,你能从零跑起一个 ReactPHP 的异步 HTTP 服务,并看懂“为什么单线程也能扛并发”这件事。
ReactPHP 不是框架,而是一组基于事件循环的 PHP 组件库。它把 Node.js 那套回调/Promise 模型搬到了 PHP 里:进程只有一个主线程,靠事件循环调度所有 I/O,等待期间不占 CPU。下面按步骤走。
第一步:装依赖
新建目录,用 Composer 拉取核心组件:
mkdir react-demo && cd react-demo
composer require react/http react/event-loop
react/http 会自动带上 react/socket、react/stream、react/promise。
注意:ReactPHP 官方要求 PHP 7.1+,实际生产建议 8.1 以上。别在 Windows 的老版本 PHP 上折腾,事件循环底层依赖 stream_select,行为差异会让你排查到怀疑人生。
第二步:先看懂事件循环
写个最小例子 loop.php,看清“注册回调,而不是顺序执行”这个核心:
<?php
require __DIR__ . '/vendor/autoload.php';
use React\EventLoop\Loop;
$n = 0;
Loop::addPeriodicTimer(1, function () use (&$n) {
echo "tick " . (++$n) . PHP_EOL;
if ($n >= 3) {
Loop::stop(); // 不 stop 就永远跑下去
}
});
echo "开始\n";
Loop::run();
echo "结束\n";
运行 php loop.php,会看到每秒一次 tick,三次后退出。关键点:Loop::run() 之前的代码是“注册”,之后才是“循环调度”。没有 Loop::stop(),进程不会自己停。
注意:事件循环里绝对不能写 sleep()、file_get_contents()、同步 PDO 查询。这些会卡住整个进程,所有并发请求一起等。要延时用 Loop::addTimer(),要异步 HTTP 用 react/http 的客户端,要异步数据库用 react/mysql。
第三步:写第一个异步 HTTP 服务
新建 server.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
$server = new HttpServer(function (ServerRequestInterface $request) {
$path = $request->getUri()->getPath();
if ($path === '/slow') {
// 返回一个 Promise,2 秒后才 resolve
return new React\Promise\Promise(function ($resolve) {
Loop::addTimer(2, function () use ($resolve) {
$resolve(Response::plaintext("done after 2s\n"));
});
});
}
return Response::plaintext("hello, path = {$path}\n");
});
$socket = new SocketServer('0.0.0.0:8080');
$server->listen($socket);
echo "listening on http://0.0.0.0:8080\n";
启动:php server.php。访问 http://127.0.0.1:8080/ 能立刻拿到响应。
回调的返回值可以是 Response,也可以是 Promise<Response>。后者就是异步的关键——先返回“还没结果”的承诺,事件循环继续处理别的请求,定时器到点了再兑现。
第四步:把异步串起来
真实业务里更常见的是链式调用。以 react/mysql 为例(composer require react/mysql:^2.0):
$pool = new React\Mysql\MysqlConnectionPool([
'host' => '127.0.0.1', 'user' => 'root',
'password' => '', 'database' => 'demo',
]);
$pool->query('SELECT id, title FROM post LIMIT 10')
->then(function ($rows) {
foreach ($rows as $row) {
echo $row->title . PHP_EOL;
}
})
->catch(function (Throwable $e) {
echo '查询失败: ' . $e->getMessage() . PHP_EOL;
});
或者用 async/await 语法糖(需要 react/async,PHP 8.1+):
$rows = await($pool->query('SELECT 1'));
注意:Promise 链一定要写 catch。未捕获的异常在事件循环里不会像普通 PHP 脚本那样直接把错误打到终端,排查会非常痛苦。可以在入口处统一挂 $loop->addError() 或给最外层包一层 catch。
第五步:验证并发是真的
开两个终端,同时请求慢接口:
time curl -s http://127.0.0.1:8080/slow & time curl -s http://127.0.0.1:8080/slow & wait
如果总耗时约 2 秒而不是 4 秒,说明并发成立。用 ab -n 100 -c 100 http://127.0.0.1:8080/ 也能看到快速接口不被慢接口拖住。
注意:单线程意味着 CPU 密集任务依然会阻塞。图片处理、大文件压缩、复杂计算请交给 react/child-process 起子进程,或干脆丢进消息队列由常驻 worker 处理。
小结
- ReactPHP 是组件库不是框架,用 Composer 按需引入;
- 一切从
Loop::run() 开始,注册回调 → 事件循环调度 → 结果通过 Promise 兑现;
- 慢接口的正确写法是
addTimer + Promise,不是 sleep;
- 数据库、HTTP 客户端都有异步实现(
react/mysql、react/http 的 Browser),别把同步扩展塞进循环里;
- 单线程适合 I/O 密集,CPU 密集要靠子进程或队列分流;
- 每个 Promise 链都写
catch,否则错误会静默消失。