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 实例,并将最大请求大小传入。
    接下来,我们重点定位到:makePusherRouterpusherRoutes,我们一个个来分析:

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() 方法中定义的每个路由的作用解析:

  1. 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 服务器建立实时连接。
      • 服务器通过 PusherServerApplicationProvider 处理连接逻辑。
  2. events(事件推送)

    $routes->add('events', Route::post('/apps/{appId}/events', new EventsController));
    
    • 作用: 负责处理客户端推送的事件(消息)。
    • 请求类型: POST
    • URL: /apps/{appId}/events
    • 控制器: EventsController
    • 参数: appId(应用 ID)
    • 解析:
      • 该路由用于处理客户端推送的事件,并将消息广播到相应的频道或用户。
  3. events_batch(批量事件推送)

    $routes->add('events_batch', Route::post('/apps/{appId}/batch_events', new EventsBatchController));
    
    • 作用: 允许客户端一次性推送多个事件,提高批量处理效率。
    • 请求类型: POST
    • URL: /apps/{appId}/batch_events
    • 控制器: EventsBatchController
    • 解析:
      • 该路由支持批量事件推送,可以减少客户端与服务器之间的请求次数,提高性能。
  4. connections(查询连接信息)

    $routes->add('connections', Route::get('/apps/{appId}/connections', new ConnectionsController));
    
    • 作用: 查询当前应用下的所有活跃连接。
    • 请求类型: GET
    • URL: /apps/{appId}/connections
    • 控制器: ConnectionsController
    • 解析:
      • 该 API 允许管理员或服务端查询当前应用的 WebSocket 连接信息,比如在线用户数量。
  5. channels(查询所有频道信息)

    $routes->add('channels', Route::get('/apps/{appId}/channels', new ChannelsController));
    
    • 作用: 获取当前应用的所有活跃频道列表。
    • 请求类型: GET
    • URL: /apps/{appId}/channels
    • 控制器: ChannelsController
    • 解析:
      • 允许查询当前应用正在使用的所有 WebSocket 频道,常用于管理面板或分析工具。
  6. channel(查询单个频道信息)

    $routes->add('channel', Route::get('/apps/{appId}/channels/{channel}', new ChannelController));
    
    • 作用: 获取指定频道的详细信息。
    • 请求类型: GET
    • URL: /apps/{appId}/channels/{channel}
    • 控制器: ChannelController
    • 参数: channel(频道名称)
    • 解析:
      • 用于查询某个具体频道的状态,比如订阅用户数等信息。
  7. 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 频道,可以获取该频道所有在线用户的信息。
  8. 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 连接,通常用于管理或安全策略。
  9. 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 检查服务器健康状态

总结

考虑到篇幅和阅读感,后面的内容下一节说

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