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 | 
检查服务器健康状态 | 
总结
考虑到篇幅和阅读感,后面的内容下一节说

                
            
        
浙公网安备 33010602011771号