Laravel11 从0开发 Swoole-Reverb 扩展包(四) - 触发一个广播事件是如何到reverb服务的呢?

前情提要

今天这节开始,我们就从reverb启动这个过程进行源代码的学习分析。

广播驱动

但是在看reverb启动过程前,这节我们先看看laravel Broadcasting 的新驱动的这部分源码,当我们使用reverb后,广播事件的触发等操作就由新的驱动负责了。

追踪源码的技巧

我是根据reverb是一个新的driver,于是我通过ReverbDrvier在laravel包目录进行了查找(搜索或者 ctrl+p),然后快速的就在vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php
找到了我们新的驱动创建的方法:

   protected function createReverbDriver(array $config)
    {
        return $this->createPusherDriver($config);
    }

BroadcastManager 的 driver方法 通过 工厂模式来驱动实例的创建,通过策略模式来替换不同的驱动。
通过代码我们知道reverb是兼容适配的pusher,因此看到这里的朋友可以去看看我第二节写的pusher 的知识。

触发一个广播事件是如何到reverb服务的呢?

要找到这个具体的实现,我们还是从追源代码开始,一步步找。首先,我们知道触发事件的调用方法有好几个操作方式,我大致罗列如下:

//        event(new \App\Events\DemoPushEvent('你好呀,欢迎你们使用我们的聊天软件'));
//        \App\Events\DemoPushEvent::dispatch('你好呀,欢迎你们使用我们的聊天软件');
// broadcast(new \App\Events\DemoPushEvent(Auth::user()->getAuthIdentifier(), '你好呀,欢迎你们使用我们的聊天软件'))->toOthers();

我们就用 \App\Events\DemoPushEvent::dispatch 这种方式来追代码。我们通过命令行生成的事件,都会使用一个特性:Dispatchable,dispatch就是里面的方法,而dispatch实际调用的代码是:

 app('events')->dispatch(...$args);

接下来,我们就追到了vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php里的public function dispatch($event, $payload = [], $halt = false)方法了,这个dispatch方法的作用就是 触发事件并调用侦听器。继续往下,我们看到了找到核心的方法:protected function invokeListeners($event, $payload, $halt = false),这个方法里面我们要找的代码是:

        if ($this->shouldBroadcast($payload)) {
            $this->broadcastEvent($payload[0]);
        }

shouldBroadcast也对应了文档说明的,如何确定是:确定负载是否具有可广播事件,同时可以广播的事件。关键条件:实现ShouldBroadcast和broadcastWhen是否 为真

    protected function shouldBroadcast(array $payload)
   {
       return isset($payload[0]) &&
              $payload[0] instanceof ShouldBroadcast &&
              $this->broadcastWhen($payload[0]);
   }

在继续往下走,进入broadcastEvent后,我们回到了vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.phpBroadcastManager里面,然后我们就开始分析这个queue方法,它的作用实际就是:将给定事件排队以进行广播,不过如果事件实现的是ShouldBroadcastNow接口,那么就会立即广播出去。

public function queue($event)
    {
        if ($event instanceof ShouldBroadcastNow ||
            (is_object($event) &&
             method_exists($event, 'shouldBroadcastNow') &&
             $event->shouldBroadcastNow())) {
            return $this->app->make(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event));
        }

        $queue = null;

        if (method_exists($event, 'broadcastQueue')) {
            $queue = $event->broadcastQueue();
        } elseif (isset($event->broadcastQueue)) {
            $queue = $event->broadcastQueue;
        } elseif (isset($event->queue)) {
            $queue = $event->queue;
        }

        $broadcastEvent = new BroadcastEvent(clone $event);

        if ($event instanceof ShouldBeUnique) {
            $broadcastEvent = new UniqueBroadcastEvent(clone $event);

            if ($this->mustBeUniqueAndCannotAcquireLock($broadcastEvent)) {
                return;
            }
        }

        $this->app->make('queue')
            ->connection($event->connection ?? null)
            ->pushOn($queue, $broadcastEvent);
    }

因此,我们就走到了vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php文件里来分析。

ShouldBroadcastNow 立即触发

    public function dispatchNow($command, $handler = null)
    {
        $uses = class_uses_recursive($command);

        if (isset($uses[InteractsWithQueue::class], $uses[Queueable::class]) && ! $command->job) {
            $command->setJob(new SyncJob($this->container, json_encode([]), 'sync', 'sync'));
        }

        if ($handler || $handler = $this->getCommandHandler($command)) {
            $callback = function ($command) use ($handler) {
                $method = method_exists($handler, 'handle') ? 'handle' : '__invoke';

                return $handler->{$method}($command);
            };
        } else {
            $callback = function ($command) {
                $method = method_exists($command, 'handle') ? 'handle' : '__invoke';

                return $this->container->call([$command, $method]);
            };
        }

        return $this->pipeline->send($command)->through($this->pipes)->then($callback);
    }

这个方法的作用是:将命令分派到当前进程中的相应处理程序,而不使用同步队列。
整个方法还是有些晦涩,我就替观众老爷用权威的AI给我们进行这个代码分析:
这段代码来自 Laravel 11 的 Illuminate\Bus\Dispatcher,它的 dispatchNow() 方法用于同步地执行 Laravel 任务(Command)。在 Laravel 的 任务派发机制 中,这个方法用于立即执行给定的命令(Command),而不会通过队列异步执行。


1. 处理命令的队列属性
$uses = class_uses_recursive($command);

if (isset($uses[InteractsWithQueue::class], $uses[Queueable::class]) && ! $command->job) {
    $command->setJob(new SyncJob($this->container, json_encode([]), 'sync', 'sync'));
}
解析
  • class_uses_recursive($command):获取 $command 类及其所有父类、trait 使用的 trait 列表。
  • InteractsWithQueue::classQueueable::class
    • 如果 $command 使用了这两个 trait,说明它是一个可以被队列化的任务。
    • ! $command->job:如果这个任务没有被分配到具体的 Job,说明它还没有真正进入队列。
    • setJob(new SyncJob(...)):如果满足条件,就创建一个同步任务 (SyncJob),表示这个任务将在当前进程内立即执行,而不是放入队列异步执行。

2. 确定任务执行的处理器
if ($handler || $handler = $this->getCommandHandler($command)) {
    $callback = function ($command) use ($handler) {
        $method = method_exists($handler, 'handle') ? 'handle' : '__invoke';

        return $handler->{$method}($command);
    };
} else {
    $callback = function ($command) {
        $method = method_exists($command, 'handle') ? 'handle' : '__invoke';

        return $this->container->call([$command, $method]);
    };
}
解析
  • getCommandHandler($command):获取与 $command 关联的处理器(Handler)。
  • 如果 $command 具有一个特定的 Handler 类,那么它将使用 handle()__invoke() 方法来执行该任务。
  • 如果 $command 自身 具有 handle() 方法,则直接调用 $command->handle()
  • $this->container->call([...]):使用 Laravel 服务容器 调用 handle() 方法,支持 依赖注入

3. 通过管道(Pipeline)执行命令
return $this->pipeline->send($command)->through($this->pipes)->then($callback);
解析
  • Laravel Pipeline 是一个中间件管道,类似 HTTP 中间件:
    • send($command): 将 $command 任务送入处理流程。
    • through($this->pipes): 通过 任务中间件 进行处理,比如日志、权限检查等。
    • then($callback): 执行最终的任务逻辑(上面定义的 $callback)。

流程总结

  1. 检查 $command 是否支持队列

    • 如果它实现了 Queueable,但没有 job,就为它创建一个 SyncJob,让它立即执行,而不是进入队列。
  2. 确定如何执行 $command

    • 如果 $command 有专门的 处理器 (Handler),就用 handle()__invoke() 方法执行。
    • 如果 $command 自身handle() 方法,就直接调用它。
  3. 通过 Laravel Pipeline 处理任务

    • 任务可能会经过多个中间件(类似 HTTP 请求的中间件)。
    • 最终,then($callback) 负责真正执行任务。

  • dispatchNow() 强制同步执行 Laravel 任务,不会进入队列。
  • 如果命令使用了 Queueable,但未进入队列dispatchNow() 会创建 SyncJob,仍然同步执行。
  • 任务可以有单独的 Handler,Laravel 会自动解析并执行它。
  • 使用 Pipeline 允许任务经过多个中间件,例如日志、权限检查等。

好的,上面的,AI说的很清楚了,那么我通过dump给大家看下拿到的$command到底是啥:

Illuminate\Broadcasting\BroadcastEvent Object ( 
[event] => App\Events\DemoPushEvent Object (
[userId] => 1 [message] => 你好呀,欢迎你们使用我们的聊天软件 [type] => system [socket] => ) [tries] => [timeout] => [backoff] => [maxExceptions] => [connection] => [queue] => [delay] => [afterCommit] => [middleware] => Array ( ) [chained] => Array ( ) [chainConnection] => [chainQueue] => [chainCatchCallbacks] => )

打印后,我们就又悟了,继续走到:Illuminate\Broadcasting\BroadcastEvent , 到了这里,我们就看handle方法:处理排队的作业

    public function handle(BroadcastingFactory $manager)
    {
        $name = method_exists($this->event, 'broadcastAs')
                ? $this->event->broadcastAs() : get_class($this->event);

        $channels = Arr::wrap($this->event->broadcastOn());

        if (empty($channels)) {
            return;
        }

        $connections = method_exists($this->event, 'broadcastConnections')
                            ? $this->event->broadcastConnections()
                            : [null];

        $payload = $this->getPayloadFromEvent($this->event);

        foreach ($connections as $connection) {
            $manager->connection($connection)->broadcast(
                $this->getConnectionChannels($channels, $connection),
                $name,
                $this->getConnectionPayload($payload, $connection)
            );
        }
    }

整个方法里,$connections 中的broadcastConnections也是文档上提到的,可以针对事件定义广播连接,获取应广播事件的广播连接。 事件如果没有连接,默认的值:[null],然后是$payload 拿到就是你事件定义的属性后解析的数组。再然后就是遍历连接执行广播了。
广播的操作又把我们带回到vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php Pusher广播驱动了,真的是山路十八弯,写到这里,突然想到了:“过度封装”,不过我个人还是觉得设计优雅。回到正题,我们来看public function broadcast(array $channels, $event, array $payload = [])这个方法:广播给定的事件

    public function broadcast(array $channels, $event, array $payload = [])
    {
        $socket = Arr::pull($payload, 'socket');

        $parameters = $socket !== null ? ['socket_id' => $socket] : [];

        $channels = new Collection($this->formatChannels($channels));

        try {
            $channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) {
                $this->pusher->trigger($channels->toArray(), $event, $payload, $parameters);
            });
        } catch (ApiErrorException $e) {
            throw new BroadcastException(
                sprintf('Pusher error: %s.', $e->getMessage())
            );
        }
    }

我们重点关心$this->pusher->trigger,触发请求这里。首先,我还是dump trigger的几个参数给大家看看:

array(1) { [0]=> string(11) "demo-push.1" } 
string(24) "App\Events\DemoPushEvent" 
array(3) { ["userId"]=> int(1) ["message"]=> 
string(51) "你好呀,欢迎你们使用我们的聊天软件" ["type"]=> string(6) "system" } 
array(0) { }

走到这里的朋友,🎉我们,已经到了最后一步了,因此,我们继续走到了vendor/pusher/pusher-php-server/src/Pusher.php,然后我们就来看trigger 方法。这个方法的作用是:通过提供事件名称和有效负载来触发事件。(可选)提供套接字 ID 以排除客户端(很可能是发送方)。
trigger实际就是发送了一个post请求,而这个post请求的地址是啥,这个是我们关心的,我继续给大家打印:
http://localhost:8083/apps/531292/events
那这个host和port 是在哪里来的呢,这个就是我们 broadcasting.php 广播配置文件下面配置:

connections' => [

        'reverb' => [
            'driver' => 'reverb',
            'key' => env('REVERB_APP_KEY'),
            'secret' => env('REVERB_APP_SECRET'),
            'app_id' => env('REVERB_APP_ID'),
            'options' => [
                'host' => env('REVERB_HOST'),
                'port' => env('REVERB_PORT', 443),
                'scheme' => env('REVERB_SCHEME', 'https'),
                'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
            ],
            'client_options' => [
                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
            ],
        ],

也就是我们reverb🉐️服务。

总结

带着大家走了一遍立即触发广播的流程,我们最终知道了:实际通过http请求与reverb通信。那么,延迟队列发送,最后也应该是这样的,我就不带着大家继续追代码了(如果有误,可以评论区留言指正,一起学习)。我们下一节就来给大家分析reverb服务端的程序代码。如果有兴趣的朋友,可以从第2节看起走,因为里面涉及到pusher的通信对接数据协议。

posted @ 2025-03-12 17:09  wanzij  阅读(26)  评论(0)    收藏  举报
TOP