ReactiveX 集成

hyperf/reactive-x 组件提供了 Swoole/Hyperf 环境下的 ReactiveX 集成。

ReactiveX 的历史

ReactiveX 是 Reactive Extensions 的缩写,一般简写为 Rx,最初是 LINQ 的一个扩展,由微软的架构师 Erik Meijer 领导的团队开发,在 2012 年 11 月开源,Rx 是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流,Rx 库支持.NET、JavaScript 和 C ++,Rx 近几年越来越流行了,现在已经支持几乎全部的流行编程语言了,Rx 的大部分语言库由 ReactiveX 这个组织负责维护,比较流行的有 RxJava/RxJS/Rx.NET,社区网站是 reactivex.io

什么是 ReactiveX

微软给的定义是,Rx 是一个函数库,让开发者可以利用可观察序列和 LINQ 风格查询操作符来编写异步和基于事件的程序,使用 Rx,开发者可以用 Observables 表示异步数据流,用 LINQ 操作符查询异步数据流, 用 Schedulers 参数化异步数据流的并发处理,Rx 可以这样定义:Rx = Observables + LINQ + Schedulers。

Reactivex.io 给的定义是,Rx 是一个使用可观察数据流进行异步编程的编程接口,ReactiveX 结合了观察者模式、迭代器模式和函数式编程的精华。

以上两节摘自RxDocs

使用前请考虑

正面

  • 通过响应式编程的思考方式,可以将一些复杂异步问题化繁为简。

  • 如果您已经在其他语言有过响应式编程经验(如 RxJS/RxJava),本组件可以帮助您将这种经验移植到 Hyperf 上。

  • 尽管 Swoole 中推荐通过协程像编写同步程序一样编写异步程序,但 Swoole 中仍然包含了大量事件,而处理事件正是 Rx 的强项。

  • 如果您业务中包含流处理,如 WebSocket,gRPC streaming 等,Rx 也可以发挥重要作用。

负面

  • 响应式编程的思维方式和传统面向对象思维方式差异较大,需要开发者适应。

  • Rx 只是提供了思维方式,并没有额外的魔法。通过响应式编程能够解决的问题通过传统方式一样能够解决。

  • RxPHP 并不是 Rx 家族中的佼佼者。

安装

  1. composer require hyperf/reactive-x

封装

下面我们结合示例来介绍本组件的一些封装,并展示 Rx 的强大能力。全部示例可以在本组件 src/Example 下找到。

Observable::fromEvent

Observable::fromEvent 将 PSR 标准事件转为可观察序列。

在 hyperf-skeleton 骨架包中默认提供了打印 SQL 语句的事件监听,默认位置于 app/Listener/DbQueryExecutedListener.php。下面我们对这个监听做一些优化:

  1. 只打印超过 100ms 的 SQL 查询。

  2. 每个连接最多 1 秒打印 1 次,避免硬盘被问题程序刷爆。

如果没有 ReactiveX,问题 1 还好说,而问题 2 应该就需要动一番脑筋了。而通过 ReactiveX,则可以通过下面的示例代码的方式轻松解决这些需求:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Hyperf\ReactiveX\Example;
  4. use Hyperf\Database\Events\QueryExecuted;
  5. use Hyperf\Event\Contract\ListenerInterface;
  6. use Hyperf\Framework\Event\BeforeWorkerStart;
  7. use Hyperf\Logger\LoggerFactory;
  8. use Hyperf\ReactiveX\Observable;
  9. use Hyperf\Utils\Arr;
  10. use Hyperf\Utils\Str;
  11. use Psr\Container\ContainerInterface;
  12. class SqlListener implements ListenerInterface
  13. {
  14. private $logger;
  15. public function __construct(ContainerInterface $container)
  16. {
  17. $this->logger = $container->get(LoggerFactory::class)->get('sql');
  18. }
  19. public function listen(): array
  20. {
  21. return [
  22. BeforeWorkerStart::class,
  23. ];
  24. }
  25. public function process(object $event)
  26. {
  27. Observable::fromEvent(QueryExecuted::class)
  28. ->filter(
  29. function ($event) {
  30. return $event->time > 100;
  31. }
  32. )
  33. ->groupBy(
  34. function ($event) {
  35. return $event->connectionName;
  36. }
  37. )
  38. ->flatMap(
  39. function ($group) {
  40. return $group->throttle(1000);
  41. }
  42. )
  43. ->map(
  44. function ($event) {
  45. $sql = $event->sql;
  46. if (! Arr::isAssoc($event->bindings)) {
  47. foreach ($event->bindings as $key => $value) {
  48. $sql = Str::replaceFirst('?', "'{$value}'", $sql);
  49. }
  50. }
  51. return [$event->connectionName, $event->time, $sql];
  52. }
  53. )->subscribe(
  54. function ($message) {
  55. $this->logger->info(sprintf('slow log: [%s] [%s] %s', ...$message));
  56. }
  57. );
  58. }
  59. }

Observable::fromChannel

将 Swoole 协程中的 Channel 转为可观察序列。

Swoole 协程中的 Channel 是读写一对一的。如果我们希望通过 Channel 来做多对多订阅和发布在 ReactiveX 下该怎么做呢?

请参阅下面这个例子。

  1. <?php
  2. declare(strict_types=1);
  3. use Hyperf\ReactiveX\Observable;
  4. use Swoole\Coroutine\Channel;
  5. $chan = new Channel(1);
  6. $pub = Observable::fromChannel($chan)->publish();
  7. $pub->subscribe(function ($x) {
  8. echo 'First Subscription:' . $x . PHP_EOL;
  9. });
  10. $pub->subscribe(function ($x) {
  11. echo 'Second Subscription:' . $x . PHP_EOL;
  12. });
  13. $pub->connect();
  14. $chan->push('hello');
  15. $chan->push('world');
  16. // First Subscription: hello
  17. // Second Subscription: hello
  18. // First Subscription: world
  19. // Second Subscription: world

Observable::fromCoroutine

创建一个或多个协程并将执行结果转为可观察序列。

我们现在让两个函数在并发协程中竞争,哪个先执行完毕的就返回哪个的结果。效果类似 JavaScript 中的 Promise.race

  1. <?php
  2. declare(strict_types=1);
  3. use Hyperf\ReactiveX\Observable;
  4. use Swoole\Coroutine\Channel;
  5. $result = new Channel(1);
  6. $o = Observable::fromCoroutine([function () {
  7. sleep(2);
  8. return 1;
  9. }, function () {
  10. sleep(1);
  11. return 2;
  12. }]);
  13. $o->take(1)->subscribe(
  14. function ($x) use ($result) {
  15. $result->push($x);
  16. }
  17. );
  18. echo $result->pop(); // 2;

Observable::fromHttpRoute

所有的 HTTP 请求其实也是事件驱动的。所以 HTTP 请求路由也可以用 ReactiveX 来接管。

由于我们要添加路由,所以务必要在 Server 启动前执行,如在 BootApplication 事件监听中。

假设我们有一个上传路由,流量很大,需要在内存中缓冲,上传十次以后再批量入库。

  1. <?php
  2. declare(strict_types=1);
  3. namespace Hyperf\ReactiveX\Example;
  4. use Hyperf\Database\Events\QueryExecuted;
  5. use Hyperf\Event\Contract\ListenerInterface;
  6. use Hyperf\Framework\Event\BootApplication;
  7. use Hyperf\ReactiveX\Observable;
  8. use Psr\Http\Message\RequestInterface;
  9. class BatchSaveRoute implements ListenerInterface
  10. {
  11. public function listen(): array
  12. {
  13. return [
  14. BootApplication::class,
  15. ];
  16. }
  17. /**
  18. * @param QueryExecuted $event
  19. */
  20. public function process(object $event)
  21. {
  22. Observable::fromHttpRoute(['POST', 'PUT'], '/save')
  23. ->map(
  24. function (RequestInterface $request) {
  25. return $request->getBody();
  26. }
  27. )
  28. ->bufferWithCount(10)
  29. ->subscribe(
  30. function (array $bodies) {
  31. echo count($bodies); //10
  32. }
  33. );
  34. }
  35. }

接管路由后如果需要控制返回的 Response,可以在 fromHttpRoute 中增加第三个参数,与正常路由写法相同,如

  1. $observable = Observable::fromHttpRoute('GET', '/hello-hyperf', 'App\Controller\IndexController::hello');

此时 Observable 作用类似于中间件,获取请求对象可观察序列后会继续传递请求对象给真正的 Controller

IpcSubject

Swoole 的进程间通讯也是事件驱动的。本组件在 RxPHP 提供的四种 Subject 基础上额外提供了对应的跨进程 Subject 版本,可以用于在进程间共享信息。

例如,我们需要制作一个基于 WebSocket 的聊天室,需求如下:

  1. 聊天室的消息需要在 Worker 进程 之间共享。

  2. 用户第一次登录时显示最新的 5 条消息。

我们通过 ReplaySubject 的跨进程版本来实现。

  1. <?php
  2. declare(strict_types=1);
  3. namespace Hyperf\ReactiveX\Example;
  4. use Hyperf\Contract\OnCloseInterface;
  5. use Hyperf\Contract\OnMessageInterface;
  6. use Hyperf\Contract\OnOpenInterface;
  7. use Hyperf\ReactiveX\Contract\BroadcasterInterface;
  8. use Hyperf\ReactiveX\IpcSubject;
  9. use Rx\Subject\ReplaySubject;
  10. use Swoole\Http\Request;
  11. use Swoole\Server;
  12. use Swoole\WebSocket\Frame;
  13. use Swoole\WebSocket\Server as WebSocketServer;
  14. class WebSocketController implements OnMessageInterface, OnOpenInterface, OnCloseInterface
  15. {
  16. private IpcSubject $subject;
  17. private $subscriber = [];
  18. public function __construct(BroadcasterInterface $broadcaster)
  19. {
  20. $relaySubject = make(ReplaySubject::class, ['bufferSize' => 5]);
  21. // 第一个参数为原 RxPHP Subject 对象。
  22. // 第二个参数为广播方式,默认为全进程广播
  23. // 第三个参数为频道 ID, 每个频道只能收到相同频道的消息。
  24. $this->subject = new IpcSubject($relaySubject, $broadcaster, 1);
  25. }
  26. public function onMessage(WebSocketServer $server, Frame $frame): void
  27. {
  28. $this->subject->onNext($frame->data);
  29. }
  30. public function onClose(Server $server, int $fd, int $reactorId): void
  31. {
  32. $this->subscriber[$fd]->dispose();
  33. }
  34. public function onOpen(WebSocketServer $server, Request $request): void
  35. {
  36. $this->subscriber[$request->fd] = $this->subject->subscribe(function ($data) use ($server, $request) {
  37. $server->push($request->fd, $data);
  38. });
  39. }
  40. }

为了方便使用,本组件利用 IpcSubject 封装了一条 “消息总线” MessageBusInterface。只需要注入 MessageBusInterface 就可以收发全进程共享信息(包括自定义进程)。诸如配置中心一类的功能可以通过它来轻松实现。

  1. <?php
  2. $bus = make(Hyperf\ReactiveX\MessageBusInterface::class);
  3. // 全进程广播信息
  4. $bus->onNext('Hello Hyperf');
  5. // 订阅信息
  6. $bus->subscribe(function($message){
  7. echo $message;
  8. });

由于 ReactiveX 需要使用事件循环,请注意一定要在 Swoole Server 启动之后再调用 ReactiveX 相关 API 。

参考资料