Laravel11 从0开发 Swoole-Reverb 扩展包(四) - 触发一个广播事件到reverb服务之后是如何转发给前端订阅的呢(上)?
前情提要
我们在上一节分析了触发广播事件发送到reverb服务的过程,这一节我们就来分析,reverb的服务启动过程。在看源码之前,我们先说明一点,reverb混响服务(ws响应+http响应)是基于reactPHP实现的单线程+event loop(事件循环)。同时保持严谨和自我学习,我也会把一些重要的概念通过ai接收后写到文章内,但也不会太多了,影响观感,更多的知识点也需要我们自己去看其他的文章。
event loop
Event Loop(事件循环)是一种编程模式,主要用于处理异步任务,特别是在单线程环境下(如 JavaScript 和 Python 的 async/await 机制,swoole reactphp workerman的实现机制)。它的作用是管理和调度异步任务,确保非阻塞执行。
工作原理
同步任务先执行:程序先执行同步代码,遇到异步任务(如 I/O 操作、网络请求、定时器等)时,将其交给事件循环调度。
异步任务放入队列:异步任务(如 Promise、setTimeout、I/O 操作)被推入不同的任务队列(如微任务队列、宏任务队列)。
事件循环调度:
先执行微任务队列(Microtasks):如 Promise.then()、process.nextTick()。
再执行宏任务队列(Macrotasks):如 setTimeout、setImmediate、I/O 操作等。
循环执行:事件循环不断检查是否有新的任务可执行,直到程序结束。
并发和并行
并发(Concurrency) 和 并行(Parallelism) 是计算机科学中的两个概念,主要用于描述程序的执行方式。
1. 并发(Concurrency)
-
定义:多个任务在同一时间段交替执行,但不一定同时运行。
-
特点:
- 任务看起来是同时进行的,但实际上 CPU 在多个任务之间快速切换。
- 适用于 I/O 密集型任务(如网络请求、文件读写)。
- 主要依赖多线程(Threads)或协程(Coroutines),但仍运行在单核 CPU 上。
-
示例:
- 你在听音乐的同时浏览网页(实际上 CPU 在不同任务之间切换)。
- Python 的
asyncio
通过await
实现并发任务。
2. 并行(Parallelism)
-
定义:多个任务真正同时运行,通常依赖于多核 CPU。
-
特点:
- 任务同时执行,提高计算效率。
- 适用于 CPU 密集型任务(如视频渲染、科学计算)。
- 依赖多进程(Processes)或多线程(Threads),需要多个 CPU 核心。
-
示例:
- 你和朋友同时各自用一台电脑玩游戏(多个核心真正同时执行)。
- 使用
multiprocessing
在 Python 中进行并行计算。
3. 关键区别
并发(Concurrency) | 并行(Parallelism) | |
---|---|---|
执行方式 | 任务交替进行(看起来同时) | 任务真正同时执行 |
依赖 | 线程、协程 | 进程、多个 CPU 核心 |
适用场景 | I/O 密集型任务(网络请求、数据库操作) | CPU 密集型任务(科学计算、加密运算) |
示例 | Python asyncio ,JavaScript Promise |
Python multiprocessing ,多核计算 |
4. 总结
- 并发 = 任务交替执行,但不一定同时运行(单核 CPU 可实现)。
- 并行 = 任务真正同时运行,需要多核 CPU 支持。
- 并发提升程序响应速度,并行提升计算效率。
两者可以结合使用,比如在一个并发系统中,每个并发任务内部再使用并行计算来加速处理!
进程(Process)和线程(Thread)在操作系统中会经历多个状态切换,下面是它们的典型状态图:
1. 进程状态切换图
进程的生命周期通常包含以下 5 种状态:
- 新建(New):进程被创建,但尚未执行。
- 就绪(Ready):进程已准备好运行,但等待 CPU 调度。
- 运行(Running):进程占用 CPU,并执行任务。
- 等待(Waiting / Blocked):进程因 I/O 操作或其他事件阻塞,无法继续执行。
- 终止(Terminated):进程执行完毕或被终止,进入销毁状态。
状态切换图:
+-----------+
| New(新建) |
+-----------+
|
v
+-----------------+
| Ready(就绪) |
+-----------------+
| ↑
CPU调度 |
v
+-----------------+
| Running(运行) |
+-----------------+
| | |
运行完成 | I/O等待或资源不足
v v v
+-------------+ +------------------+
| Terminated | | Waiting(等待) |
|(终止) | |(阻塞/挂起) |
+-------------+ +------------------+
|
v
返回就绪队列
2. 线程状态切换图
线程的状态切换与进程类似,但线程是轻量级的,通常共享进程的资源。其状态如下:
- 新建(New):线程被创建但未启动。
- 就绪(Ready):线程可以运行,但等待 CPU 资源。
- 运行(Running):线程正在执行任务。
- 阻塞(Blocked / Waiting):线程等待 I/O 或其他线程释放资源。
- 终止(Terminated):线程任务完成或异常终止。
线程状态切换图:
+-----------+
| New(新建) |
+-----------+
|
v
+----------------+
| Ready(就绪) |
+----------------+
| ↑
CPU调度 |
v
+----------------+
| Running(运行) |
+----------------+
| | |
完成 | 等待I/O或锁
v v v
+-------------+ +------------------+
| Terminated | | Blocked(阻塞) |
|(终止) | |(等待资源) |
+-------------+ +------------------+
|
v
返回就绪队列
3. 进程 vs 线程 状态切换
状态 | 进程(Process) | 线程(Thread) |
---|---|---|
新建 | 创建新的进程 | 创建新的线程 |
就绪 | 进程等待 CPU 资源 | 线程等待 CPU 资源 |
运行 | 进程正在执行任务 | 线程正在执行任务 |
阻塞 | 进程等待 I/O 或资源 | 线程等待 I/O 或锁 |
终止 | 进程完成或被终止 | 线程完成或被终止 |
区别:
- 进程切换开销较大,涉及 CPU 状态、内存等信息。
- 线程切换较轻量,多个线程共享进程资源。
这两者的切换过程可以通过任务管理器或top 命令查看 CPU 占用情况!
启动流程分析
我们直接走到vendor/laravel/reverb/src/Servers/Reverb/Console/Commands/StartServer.php
,它就是启动服务的终端命令文件。在看我们启动服务前,我们先总结一个注解:
#[AsCommand(name: 'reverb:start')]
在 PHP 8 中,#[AsCommand(name: 'reverb:start')] 这个注解(Attribute)通常用于标记类或方法,以提供元数据。它主要用于 Symfony Console 命令 或 自定义框架组件,用于定义 CLI 命令。
回到代码,我们就看handle的方法。
public function handle(): void
{
if ($this->option('debug')) {
$this->laravel->instance(Logger::class, new CliLogger($this->output));
}
$config = $this->laravel['config']['reverb.servers.reverb'];
$loop = Loop::get();
$server = ServerFactory::make(
$host = $this->option('host') ?: $config['host'],
$port = $this->option('port') ?: $config['port'],
$hostname = $this->option('hostname') ?: $config['hostname'],
$config['max_request_size'] ?? 10_000,
$config['options'] ?? [],
loop: $loop
);
$this->ensureHorizontalScalability($loop);
$this->ensureStaleConnectionsAreCleaned($loop);
$this->ensureRestartCommandIsRespected($server, $loop, $host, $port);
$this->ensurePulseEventsAreCollected($loop, $config['pulse_ingest_interval']);
$this->ensureTelescopeEntriesAreCollected($loop, $config['telescope_ingest_interval'] ?? 15);
$this->components->info('Starting ' . ($server->isSecure() ? 'secure ' : '') . "server on {$host}:{$port}" . (($hostname && $hostname !== $host) ? " ({$hostname})" : ''));
$server->start();
}
我逐个说明下一些核心的地方:
ServerFactory::make
: 服务初始化, 支持自定义主机、端口、主机名以及请求大小等配置。ensureHorizontalScalability
:水平扩展分布式支持,通过广播(PubSub)启用水平扩展。连接 PubSubProvider 并订阅事件,以便在多实例部署中同步消息。这个在reverb.php
的配置文件对应的配置为:
//...
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
],
],
- ensureStaleConnectionsAreCleaned: 使用事件循环定期清理连接
- ensureRestartCommandIsRespected: 定时检查是否发送了重启信号,在
reverb:restart
的时候会设置重启时间,定时器发现重启时间对的上就重启服务 - ensurePulseEventsAreCollected:如果启用了,安排 Pulse 收集事件。这个就是要用到pulse兼容组件
接着,讲下ensureHorizontalScalability
方法:
它通过PubSubProvider
类来利用redis的发布订阅,在开启分布式scaling扩展时,多台服务共享通信的数据。
我们继续⬇️,来到vendor/laravel/reverb/src/Servers/Reverb/Factory.php
,这个类的用途:使用 Laravel 生态和 ReactPHP 创建了一个支持 Pusher 协议的 WebSocket 服务器。它支持 TLS(加密连接)以及自定义协议扩展(目前只支持 Pusher 协议)。
我们关注make方法,考虑到篇幅,我这边就直接说明整个作用:
- 事件循环初始化:若没有提供 $loop,则通过 Loop::get() 获取全局事件循环实例。
- 路由选择:使用 match 表达式,根据 $protocol 值调用对应的路由构造方法(此处只支持 'pusher' 协议,调用 static::makePusherRouter()),如果传入不支持的协议,则抛出异常。
- TLS 配置: 调用 static::configureTls() 方法,将传入的 $options['tls'] 与 $hostname 进行配置,返回处理后的 TLS 选项。随后,根据是否使用 TLS(由 static::usesTls() 判断),构建最终的 URI 字符串,决定是使用 tls:// 还是普通的 host:port 格式。
- 使用处理后的 URI、选项、路由以及事件循环,创建一个 HttpServer 实例,并将最大请求大小传入。
接下来,我们重点定位到:makePusherRouter
和pusherRoutes
,我们一个个来分析:
makePusherRouter
public static function makePusherRouter(): Router
{
app()->singleton(
ChannelManager::class,
fn () => new ArrayChannelManager
);
app()->bind(
ChannelConnectionManager::class,
fn () => new ArrayChannelConnectionManager
);
app()->singleton(
PubSubIncomingMessageHandler::class,
fn () => new PusherPubSubIncomingMessageHandler,
);
return new Router(new UrlMatcher(static::pusherRoutes(), new RequestContext));
}
依赖绑定:
使用 app()->singleton() 与 app()->bind() 将接口与具体实现绑定到 Laravel 服务容器中:
- ChannelManager::class 被绑定为 ArrayChannelManager 的单例,意味着在整个应用中只会有一个实例,用于管理频道信息。
- ChannelConnectionManager::class 被绑定为 ArrayChannelConnectionManager,用于管理连接信息。
- PubSubIncomingMessageHandler::class 被绑定为 PusherPubSubIncomingMessageHandler,用于处理从 Pub/Sub 系统传来的消息。
路由构造:
- 调用 static::pusherRoutes() 获取定义好的路由集合。
- 创建一个 UrlMatcher 对象,并传入路由集合和新的 RequestContext(用于匹配请求 URL)。
- 使用这个 UrlMatcher 构造一个 Router 实例,并返回。
ChannelManager和ArrayChannelManager这个就是维护整个通信流程中 应用-连接-通道(事件)关系的核心,在我后面用swoole实现多进程的reverb服务里,我使用了redis+lock分别实现了他们。大家有个印象就行。
pusherRoutes
protected static function pusherRoutes(): RouteCollection
{
// 详细解析:
// 路由集合构建:
//
// 创建一个新的 RouteCollection 实例用于存放所有的路由定义。
// 路由添加:
//
// 每一条路由使用 Route::get 或 Route::post 定义,其中包含 URL 模板和对应的控制器实例。
// 例如:
// sockets 路由:GET 请求 /app/{appKey},对应 PusherController,传入 PusherServer 与 ApplicationProvider 实例(通过 Laravel 容器解析)。
// events 路由:POST 请求 /apps/{appId}/events,对应 EventsController,用于处理事件推送请求。
// 其他路由同理,包括批量事件、连接查询、频道信息、用户终止连接等。
// 作用:
//
// 将所有 Pusher 协议相关的 API 接口集中管理,便于后续维护和扩展。
// 通过 URL 模板和控制器绑定,框架能够根据请求路径准确匹配到对应的处理逻辑。
$routes = new RouteCollection;
$routes->add('sockets', Route::get('/app/{appKey}', new PusherController(app(PusherServer::class), app(ApplicationProvider::class))));
$routes->add('events', Route::post('/apps/{appId}/events', new EventsController));
$routes->add('events_batch', Route::post('/apps/{appId}/batch_events', new EventsBatchController));
$routes->add('connections', Route::get('/apps/{appId}/connections', new ConnectionsController));
$routes->add('channels', Route::get('/apps/{appId}/channels', new ChannelsController));
$routes->add('channel', Route::get('/apps/{appId}/channels/{channel}', new ChannelController));
$routes->add('channel_users', Route::get('/apps/{appId}/channels/{channel}/users', new ChannelUsersController));
$routes->add('users_terminate', Route::post('/apps/{appId}/users/{userId}/terminate_connections', new UsersTerminateController));
$routes->add('health_check', Route::get('/up', new HealthCheckController));
return $routes;
}
路由组织了我们整个http接口请求的过程,我们也直观的知道pusher通信涉及到的处理,那么我就总结下每个路由的作用:
Pusher 是一个 WebSocket 推送服务,主要用于实现实时通信。以下是 pusherRoutes()
方法中定义的每个路由的作用解析:
-
sockets(WebSocket 连接路由)
$routes->add('sockets', Route::get('/app/{appKey}', new PusherController(app(PusherServer::class), app(ApplicationProvider::class))));
- 作用: 用于建立 WebSocket 连接。
- 请求类型:
GET
- URL:
/app/{appKey}
- 控制器:
PusherController
- 参数:
appKey
(应用的 API Key) - 解析:
- 客户端会通过 WebSocket 连接到该路由,以便与 Pusher 服务器建立实时连接。
- 服务器通过
PusherServer
和ApplicationProvider
处理连接逻辑。
-
events(事件推送)
$routes->add('events', Route::post('/apps/{appId}/events', new EventsController));
- 作用: 负责处理客户端推送的事件(消息)。
- 请求类型:
POST
- URL:
/apps/{appId}/events
- 控制器:
EventsController
- 参数:
appId
(应用 ID) - 解析:
- 该路由用于处理客户端推送的事件,并将消息广播到相应的频道或用户。
-
events_batch(批量事件推送)
$routes->add('events_batch', Route::post('/apps/{appId}/batch_events', new EventsBatchController));
- 作用: 允许客户端一次性推送多个事件,提高批量处理效率。
- 请求类型:
POST
- URL:
/apps/{appId}/batch_events
- 控制器:
EventsBatchController
- 解析:
- 该路由支持批量事件推送,可以减少客户端与服务器之间的请求次数,提高性能。
-
connections(查询连接信息)
$routes->add('connections', Route::get('/apps/{appId}/connections', new ConnectionsController));
- 作用: 查询当前应用下的所有活跃连接。
- 请求类型:
GET
- URL:
/apps/{appId}/connections
- 控制器:
ConnectionsController
- 解析:
- 该 API 允许管理员或服务端查询当前应用的 WebSocket 连接信息,比如在线用户数量。
-
channels(查询所有频道信息)
$routes->add('channels', Route::get('/apps/{appId}/channels', new ChannelsController));
- 作用: 获取当前应用的所有活跃频道列表。
- 请求类型:
GET
- URL:
/apps/{appId}/channels
- 控制器:
ChannelsController
- 解析:
- 允许查询当前应用正在使用的所有 WebSocket 频道,常用于管理面板或分析工具。
-
channel(查询单个频道信息)
$routes->add('channel', Route::get('/apps/{appId}/channels/{channel}', new ChannelController));
- 作用: 获取指定频道的详细信息。
- 请求类型:
GET
- URL:
/apps/{appId}/channels/{channel}
- 控制器:
ChannelController
- 参数:
channel
(频道名称) - 解析:
- 用于查询某个具体频道的状态,比如订阅用户数等信息。
-
channel_users(查询频道在线用户列表)
$routes->add('channel_users', Route::get('/apps/{appId}/channels/{channel}/users', new ChannelUsersController));
- 作用: 获取指定频道的在线用户列表(仅适用于 Presence 频道)。
- 请求类型:
GET
- URL:
/apps/{appId}/channels/{channel}/users
- 控制器:
ChannelUsersController
- 解析:
- 适用于 Presence 频道,可以获取该频道所有在线用户的信息。
-
users_terminate(终止用户连接)
$routes->add('users_terminate', Route::post('/apps/{appId}/users/{userId}/terminate_connections', new UsersTerminateController));
- 作用: 断开某个用户的所有连接。
- 请求类型:
POST
- URL:
/apps/{appId}/users/{userId}/terminate_connections
- 控制器:
UsersTerminateController
- 参数:
userId
(用户 ID) - 解析:
- 该 API 用于强制断开某个用户的所有 WebSocket 连接,通常用于管理或安全策略。
-
health_check(健康检查)
$routes->add('health_check', Route::get('/up', new HealthCheckController));
- 作用: 用于检测 Pusher 服务器是否正常运行。
- 请求类型:
GET
- URL:
/up
- 控制器:
HealthCheckController
- 解析:
- 该 API 用于监控 Pusher 服务的健康状态,一般用于运维或监控系统。
总结:
路由名称 | 请求类型 | 作用 |
---|---|---|
sockets | GET |
负责 WebSocket 连接 |
events | POST |
处理单个事件推送 |
events_batch | POST |
处理批量事件推送 |
connections | GET |
查询所有 WebSocket 连接 |
channels | GET |
获取当前应用所有频道 |
channel | GET |
查询单个频道详情 |
channel_users | GET |
查询 Presence 频道在线用户 |
users_terminate | POST |
终止指定用户的所有连接 |
health_check | GET |
检查服务器健康状态 |
总结
考虑到篇幅和阅读感,后面的内容下一节说