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.php
BroadcastManager里面,然后我们就开始分析这个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::class
和Queueable::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
)。
流程总结
-
检查
$command
是否支持队列:- 如果它实现了
Queueable
,但没有job
,就为它创建一个SyncJob
,让它立即执行,而不是进入队列。
- 如果它实现了
-
确定如何执行
$command
:- 如果
$command
有专门的 处理器 (Handler),就用handle()
或__invoke()
方法执行。 - 如果
$command
自身 有handle()
方法,就直接调用它。
- 如果
-
通过 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的通信对接数据协议。