Menu

广播系统

广播系统

简介

在现代的 web 应用程序中, WebSockets 被用来实现实时、即时更新的用户接口。当服务器上的数据更新后,更新信息会通过 WebSocket 连接发送到客户端等待处理。相比于不停地轮询应用程序,这是一种更加可靠和高效的选择。

为了帮助你构建这类应用, Laravel 将通过 WebSocket 连接来使「广播」 事件 变得更加轻松。 广播 Laravel 事件允许你在服务端和客户端 JavaScript 应用程序间共享相同的事件名。

{tip} 在深入了解事件广播之前,请确认你已阅读所有关于 Laravel 事件和监听器 的文档。

配置

所有关于事件广播的配置都保存在 config/broadcasting.php 配置文件中。 Laravel 自带了几个广播驱动: PusherRedis , 和一个用于本地开发与调试的 log 驱动。另外,还有一个 null 驱动允许你完全关闭广播系统。每一个驱动的示例配置都可以在 config/broadcasting.php 配置文件中找到。

广播服务提供者

在对事件进行广播之前,你必须先注册 App\Providers\BroadcastServiceProvider 。对于一个新建的 Laravel 应用程序,你只需要在 config/app.php 配置文件的 providers 数组中取消对该提供者的注释即可。该提供者将允许你注册广播授权路由和回调。

CSRF 令牌

Laravel Echo 需要访问当前会话的 CSRF 令牌。你应当验证你的应用程序的 head HTML 元素是否定义了包含 CSRF 令牌的 meta 标签:

<meta name="csrf-token" content="{{ csrf_token() }}">

对驱动的要求

Pusher

如果你使用 Pusher 来对事件进行广播,请用 Composer 包管理器来安装 Pusher PHP SDK :

composer require pusher/pusher-php-server "~3.0"

然后,你需要在 config/broadcasting.php 配置文件中配置你的 Pusher 证书。该文件中已经包含了一个 Pusher 示例配置,你可以快速地指定你的 Pusher key 、secret 和 application ID。 config/broadcasting.php 文件的 pusher 配置项同时也允许你指定 Pusher 支持的额外 options ,例如 cluster:

'options' => [
    'cluster' => 'eu',
    'encrypted' => true
],

当 Pusher 和 Laravel Echo 一起使用时,你应该在 resources/assets/js/bootstrap.js 文件中实例化 Echo 对象时指定 pusher 作为所需要的 broadcaster :

import Echo from "laravel-echo"

window.Pusher = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key'
});

Redis

如果你使用 Redis 广播器,请安装 Predis 库:

composer require predis/predis

Redis 广播器会使用 Redis 的 发布/订阅 特性来广播消息;尽管如此,你仍需将它与能够从 Redis 接收消息的 WebSocket 服务器配对使用以便将消息广播到你的 WebSocket 频道上去。

当 Redis 广播器发布一个事件的时候,该事件会被发布到它指定的频道上去,传输的数据是一个采用 JSON 编码的字符串。该字符串包含了事件名、 data 数据和生成该事件 socket ID 的用户(如果可用的话)。

Socket.IO

如果你想将 Redis 广播器 和 Socket.IO 服务器进行配对,你需要在你的应用程序中引入 Socket.IO JavaScript 客户端库。你可以通过 NPM 包管理器进行安装:

npm install --save socket.io-client

然后,你需要在实例化 Echo 时指定 socket.io 连接器和 host

import Echo from "laravel-echo"

window.io = require('socket.io-client');

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});

最后,你需要运行一个与 Laravel 兼容的 Socket.IO 服务器。 Laravel 官方并没有内置 Socket.IO 服务器实现;不过,可以选择一个由社区驱动维护的项目 tlaverdure/laravel-echo-server ,目前托管在 GitHub 。

对队列的要求

在开始广播事件之前,你还需要配置和运行 队列监听器 。所有的事件广播都是通过队列任务来完成的,因此应用程序的响应时间不会受到明显影响。

概念综述

Laravel 的事件广播允许你使用基于驱动的 WebSockets 将服务端的 Laravel 事件广播到客户端的 JavaScript 应用程序。当前的 Laravel 自带了 Pusher 和 Redis 驱动。通过使用 Laravel Echo 的 Javascript 包,我们可以很方便地在客户端消费事件。

事件通过「频道」来广播,这些频道可以被指定为公开或私有的。任何访客都可以不经授权或认证订阅一个公开频道;然而,如果想要订阅一个私有频道,那么该用户必须通过认证,并获得该频道的授权。

使用示例程序

在深入了解事件广播的每个组件之前,让我们先用一个电子商务网站作为例子来概览一下。我们不会讨论配置 Pusher 或者 Laravel Echo 的细节,这些会在本文档的其它章节里详细讨论。

在我们的应用程序中,我们假设有一个允许用户查看订单配送状态的页面。有一个 ShippingStatusUpdated 事件会在配送状态更新时被触发:

event(new ShippingStatusUpdated($update));

ShouldBroadcast 接口

当用户在查看自己的订单时,我们不希望他们必须通过刷新页面才能看到状态更新。我们希望一旦有更新时就主动将更新信息广播到客户端。所以,我们必须标记 ShippingStatusUpdated 事件实现 ShouldBroadcast 接口。这会让 Laravel 在事件被触发时广播该事件:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ShippingStatusUpdated implements ShouldBroadcast
{
    /**
     * 有关配送状态更新的信息。
     *
     * @var string
     */
    public $update;
}

ShouldBroadcast 接口要求事件定义一个 broadcastOn 方法。该方法负责指定事件被广播到哪些频道。在(通过 Artisan 命令)生成的事件类中,一个空的 broadcastOn 方法已经被预定义好了,所以我们只需要完成其细节即可。我们希望只有订单的创建者能够看到状态的更新,所以我们要把该事件广播到与这个订单绑定的私有频道上去:

/**
 * 获取事件应该广播的频道。
 *
 * @return array
 */
public function broadcastOn()
{
    return new PrivateChannel('order.'.$this->update->order_id);
}

授权频道

记住,用户只有在被授权之后才能监听私有频道。我们可以在 routes/channels.php 文件中定义频道的授权规则。在本例中,我们需要对视图监听私有 order.1 频道的所有用户进行验证,确保只有订单真正的创建者才能监听:

Broadcast::channel('order.{orderId}', function ($user, $orderId) {
    return $user->id === Order::findOrNew($orderId)->user_id;
});

channel 方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true 或者 false 来表示用户是否被授权监听该频道。

所有的授权回调接收当前被认证的用户作为第一个参数,任何额外的通配符参数作为后续参数。在本例中,我们使用 {orderId} 占位符来表示频道名称的 「ID」 部分是通配符。

对事件广播进行监听

接下来,就只剩下在 JavaScript 应用程序中监听事件了。我们可以通过 Laravel Echo 来实现。首先,我们使用 private 方法来订阅私有频道。然后,使用 listen 方法来监听 ShippingStatusUpdated 事件。默认情况下,事件的所有公有属性会被包括在广播事件中:

Echo.private(`order.${orderId}`)
    .listen('ShippingStatusUpdated', (e) => {
        console.log(e.update);
    });

定义广播事件

要告知 Laravel 一个给定的事件需要广播,只需要在事件类中实现 Illuminate\Contracts\Broadcasting\ShouldBroadcast 接口即可。该接口已被导入到所有由框架生成的事件类中,所以你可以很方便地将它添加到你自己的事件中。

ShouldBroadcast 接口要求你实现一个方法: broadcastOn 。 该方法返回一个频道或者一个频道数组,事件会被广播到这些频道。这些频道必须是 ChannelPrivateChannel 或者 PresenceChannel 的实例。 Channel 代表任何用户都可以订阅的公开频道, 而 PrivateChannelsPresenceChannels 则代表需要 频道授权 的私有频道:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ServerCreated implements ShouldBroadcast
{
    use SerializesModels;

    public $user;

    /**
     * 创建一个新的事件实例。
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     *获得事件广播的频道。
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('user.'.$this->user->id);
    }
}

然后,你只需要像你平时那样 触发事件 。一旦事件被触发,一个 队列任务 会自动广播事件到你指定的广播驱动上。

广播名称

Laravel 默认会使用事件的类名作为广播名称来广播事件。不过,你也可以在事件类中定义一个 broadcastAs 方法来自定义广播名称:

/**
 * 事件的广播名称。
 *
 * @return string
 */
public function broadcastAs()
{
    return 'server.created';
}

如果你使用了 broadcastAs 方法来自定义广播名称,你应当确保在你注册监听器时加上一个 . 的前缀。这将指示 Echo 不要在事件之前添加应用程序的命名空间:

.listen('.server.created', function (e) {
    ....
});

广播数据

当一个事件被广播时,其所有的 public 属性都会自动序列化并作为事件有效载荷进行广播,这允许你在 JavaScript 应用程序中访问到事件所有的公有数据。举个例子,如果你的事件有一个单独的包含了一个 Eloquent 模型的公有 $user 属性,那么事件的广播有效载荷将会是:

{
    "user": {
        "id": 1,
        "name": "Patrick Stewart"
        ...
    }
}

不过,如果你想更细粒度地控制你的广播有效载荷,你可以向你的事件中添加一个 broadcastWith 方法。这个方法会返回一个你想要作为事件有效载荷进行广播的数据数组:

/**
 * 指定广播数据。
 *
 * @return array
 */
public function broadcastWith()
{
    return ['id' => $this->user->id];
}

广播队列

默认情况下,每一个广播事件都会被推送到在 queue.php 配置文件中指定的默认队列连接相应的默认队列中。你可以在事件类中定义一个 broadcastQueue 属性来自定义广播器所使用的队列。该属性需要你指定广播时你想要用的队列名称:

/**
 * 事件被推送到的队列名称。
 *
 * @var string
 */
public $broadcastQueue = 'your-queue-name';

如果你想使用 sync 队列而不是默认队列驱动来广播事件,你可以实现 ShouldBroadcastNow 接口而不是 ShouldBroadcast

<?php

use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;

class ShippingStatusUpdated implements ShouldBroadcastNow
{
    //
}

广播条件

有时,你想在给定条件为 true 的情况下才广播你的事件。你可以通过在事件类中添加一个 broadcastWhen 方法来定义这些条件:

/**
 * 判定事件是否可以广播。
 *
 * @return bool
 */
public function broadcastWhen()
{
    return $this->value > 100;
}

授权频道

对于私有频道,用户只有被授权之后才能监听。实现过程是用户向你的 Laravel 应用程序发起一个携带频道名称的 HTTP 请求,由你的应用程序判断该用户是否能够监听该频道。在使用 Laravel Echo 时,授权订阅私有频道的 HTTP 请求会自动发送;尽管如此,你仍需定义相应的路由来响应这些请求。

定义授权路由

幸运的是,在 Laravel 中我们可以很容易地定义路由来响应频道授权请求。在 Laravel 自带的 BroadcastServiceProvider 中,你可以看到对 Broadcast::routes 方法的调用。该方法会注册 /broadcasting/auth 路由来处理授权请求:

Broadcast::routes();

Broadcast::routes 方法会自动将它的路由置入 web 中间件组中;不过,如果你想自定义指定的属性,你可以向该方法传递一个路由属性数组:

Broadcast::routes($attributes);

定义授权回调

接下来,我们需要定义真正用于处理频道授权的逻辑。该逻辑在应用程序自带的 routes/channels.php 文件中完成。在这个文件中,你可以使用 Broadcast::channel 方法来注册频道授权回调:

Broadcast::channel('order.{orderId}', function ($user, $orderId) {
    return $user->id === Order::findOrNew($orderId)->user_id;
});

channel 方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true 或者 false 来表示用户是否被授权监听该频道。

所有的授权回调接收当前认证用户作为第一个参数,任何额外的通配符参数作为后续参数。在本例中,我们使用 {orderId} 占位符来表示频道名称的 「ID」 部分是通配符。

授权回调模型绑定

就像 HTTP 路由一样,频道路由也可以利用显式或隐式 路由模型绑定 。例如,你可以请求接收一个真正的 Order 模型实例,而不是字符串或数字类型的 order ID:

use App\Order;

Broadcast::channel('order.{order}', function ($user, Order $order) {
    return $user->id === $order->user_id;
});

定义频道类

如果你的应用程序消耗了许多不同的频道,你的 routes/channels.php 文件可能会变得很庞大。所以,你可以使用频道类来代替使用闭包授权频道。要生成一个频道类,请使用 make:channel Artisan 命令。该命令会在 App/Broadcasting 目录中放置一个新的频道类。

php artisan make:channel OrderChannel

接下来,在你的 routes/channels.php 文件中注册你的频道:

use App\Broadcasting\OrderChannel;

Broadcast::channel('order.{order}', OrderChannel::class);

最后,你可以将频道的授权逻辑放入频道类的 join 方法中。该 join 方法将保存你通常放置在频道授权闭包中的相同逻辑。当然,你也可以利用频道模型绑定:

<?php

namespace App\Broadcasting;

use App\User;
use App\Order;

class OrderChannel
{
    /**
     * 创建一个新的频道实例。
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * 认证用户的频道访问权限。
     *
     * @param  \App\User  $user
     * @param  \App\Order  $order
     * @return array|bool
     */
    public function join(User $user, Order $order)
    {
        return $user->id === $order->user_id;
    }
}

{tip} 就像 Laravel 中的很多其它类,频道类会通过 服务容器 自动解析。因此,你可以在频道类的构造函数中对其进行所需依赖项的类型提示。

广播事件

定义好一个事件且将其标记实现 ShouldBroadcast 接口之后,你所要做的仅仅是通过 event 函数来触发该事件。事件分发器会识别出标记了实现 ShouldBroadcast 接口的事件,并将其推送到队列中进行广播:

event(new ShippingStatusUpdated($update));

只广播给他人

当创建一个会用到事件广播的应用程序时,你可以使用 broadcast 函数来代替 event 。和 event 函数一样, broadcast 函数将事件分发到服务端监听器:

broadcast(new ShippingStatusUpdated($update));

不过, broadcast 函数还有一个允许你将当前用户排除在广播接收者之外的 toOthers 方法:

broadcast(new ShippingStatusUpdated($update))->toOthers();

为了更好地理解什么时候使用 toOthers 方法,让我们假设有一个任务列表的应用程序,用户可以通过输入任务名来新建任务。要新建任务,你的应用程序需要发起一个请求到一个 /task 路由,该路由会广播任务的创建,并返回新任务的 JSON 响应。当你的 JavaScript 应用程序从路由收到响应后,它会直接将新任务插入到任务列表中,就像这样:

axios.post('/task', task)
    .then((response) => {
        this.tasks.push(respo
    });

然而,别忘了,我们还广播了任务的创建。如果你的 JavaScript 应用程序正在监听该事件以便添加任务至任务列表,任务列表中将出现重复的任务:一个来自路由响应,另一个来自广播。你可以通过使用 toOthers 方法告知广播器不要将事件广播到当前用户来解决这个问题。

{note} 为了能调用 toOthers 方法,你的事件必须使用 Illuminate\Broadcasting\InteractsWithSockets trait 。

配置

当你初始化 Laravel Echo 实例的时候,一个套接字 ID 会被分配到该连接。如果你使用了 VueAxios ,该套接字 ID 会自动地以 X-Socket-ID 头的方式添加到每一个传出请求中。那么,当你调用 toOthers 方法时,Laravel 会从请求头中取出套接字 ID ,并告知广播器不要广播任何消息到带有这个套接字 ID 的连接上。

你如果你没有使用 Vue 和 Axios ,则需要手动配置 JavaScript 应用程序来发送 X-Socket-ID 请求头。你可以用 Echo.socketId 方法来获取套接字 ID :

var socketId = Echo.socketId();

接收广播

安装 Laravel Echo

Laravel Echo 是一个 JavaScript 库,有了这个库之后,订阅频道监听 Laravel 广播的事件变得非常容易。你可以通过 NPM 包管理器来安装 Echo 。在本例中,因为我们会使用 Pusher 广播器,所以我们也会安装 pusher-js 包:

npm install --save laravel-echo pusher-js

安装好 Echo 之后,你就可以在应用程序的 JavaScript 中创建一个全新的 Echo 实例。做这件事的一个理想的地方是在 Laravel 框架自带的 resources/js/bootstrap.js 文件的底部:

import Echo from "laravel-echo"

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key'
});

当你使用 pusher 连接器来创建一个 Echo 实例的时候,还可以指定 cluster 以及连接是否需要加密:

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key',
    cluster: 'eu',
    encrypted: true
});

对事件进行监听

安装并实例化 Echo 之后, 你就可以开始监听事件广播了。首先,使用 channel 方法获取一个频道实例,然后调用 listen 方法来监听指定的事件:

Echo.channel('orders')
    .listen('OrderShipped', (e) => {
        console.log(e.order.name);
    });

如果你想监听私有频道上的事件,请使用 private 方法。你可以通过链式调用 listen 方法来监听单个频道上的多个事件:

Echo.private('orders')
    .listen(...)
    .listen(...)
    .listen(...);

退出频道

如果想退出频道,可以在你的 Echo 实例上调用 leave 方法:

Echo.leave('orders');

命名空间

你可能已经注意到在上面的例子中,我们并没有为事件类指定完整的命名空间。这是因为 Echo 会默认事件都在 App\Events 命名空间下。不过,你可以在实例化 Echo 时传递一个 namespace 配置项来指定根命名空间:

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key',
    namespace: 'App.Other.Namespace'
});

另外,你可以在使用 Echo 订阅事件的时候为事件类加上 . 前缀。这样就可以指定完全限定名称的类名了:

Echo.channel('orders')
    .listen('.Namespace.Event.Class', (e) => {
        //
    });

Presence 频道

Presence 频道构建在私有频道的安全性基础之上,并提供了额外的特性:获知谁订阅了该频道。这一点使构建强大的,协同的应用变得非常容易,比如一个用户在浏览页面时,通知其他正在浏览相同页面的用户。

授权 Presence 频道

所有的 presence 频道也是私有频道;因此,用户必须被 授权之后才能访问 。不过,在给 presence 频道定义授权回调函数时,如果一个用户已经加入了该频道,那么不应该返回 true ,而应该返回一个关于该用户信息的数组。

由授权回调函数返回的数据能够在你的 JavaScript 应用程序中被 presence 频道事件监听器所使用。如果用户没有被授权加入该 presence 频道,那么你应该返回 false 或者 null

Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

加入 Presence 频道

你可以使用 Echo 的 join 方法来加入 presence 频道。 join 方法会返回一个实现了 PresenceChannel 的对象,通过暴露 listen 方法,允许你订阅 herejoiningleaving 事件。

Echo.join(`chat.${roomId}`)
    .here((users) => {
        //
    })
    .joining((user) => {
        console.log(user.name);
    })
    .leaving((user) => {
        console.log(user.name);
    });

here 回调函数会在你成功加入频道后被立即执行,并接收一个包含其他所有当前订阅该频道的用户的用户信息数组 。 joining 方法会在新用户加入频道时被执行,而 leaving 方法会在用户退出频道时被执行。

广播到 Presence 频道

Presence 频道可以像公开和私有频道一样接收事件。使用一个聊天室的例子,我们可能想把 NewMessage 事件广播到聊天室的 presence 频道。要实现它,我们将从事件的 broadcastOn 方法中返回一个 PresenceChannel 实例:

/**
 * 获得事件广播的频道。
 *
 * @return Channel|array
 */
public function broadcastOn()
{
    return new PresenceChannel('room.'.$this->message->room_id);
}

就像公开或私有事件, presence 频道事件也可以使用 broadcast 函数来广播。同样的,你也可以使用 toOthers 方法将当前用户排除在广播接收者之外:

broadcast(new NewMessage($message));

broadcast(new NewMessage($message))->toOthers();

你可以通过 Echo 的 listen 方法来监听 join 事件:

Echo.join(`chat.${roomId}`)
    .here(...)
    .joining(...)
    .leaving(...)
    .listen('NewMessage', (e) => {
        //
    });

客户端事件

{tip} 使用 Pusher 时,如果要发送客户端事件,你必须在 应用后台 的「应用设置」部分启用「客户端事件」选项。

有时,你可能希望广播一个事件给其它已经连接的客户端,但不通知你的 Laravel 应用程序。这在处理「输入中」这类事情的通知时尤其有用,比如提醒你应用的用户,另一个用户正在给定屏幕上输入信息。

你可以使用 Echo 的 whisper 方法来广播客户端事件:

Echo.private('chat')
    .whisper('typing', {
        name: this.user.name
    });

你可以使用 listenForWhisper 方法来监听客户端事件:

Echo.private('chat')
    .listenForWhisper('typing', (e) => {
        console.log(e.name);
    });

消息通知

通过与将事件广播与 消息通知 配对,你的 JavaScript 应用程序可以在不刷新页面的情况下接收新的消息通知。在此之前,请确保你已经读过了如何使用 广播通知频道 的文档。

配置好使用广播频道的消息通知后,你可以使用 Echo 的 notification 方法来监听广播事件。谨记,频道名称应该和接收消息通知的实体类名相匹配:

Echo.private(`App.User.${userId}`)
    .notification((notification) => {
        console.log(notification.type);
    });

在本例中,所有通过 broadcast 频道发送到 App\User 实例的消息通知都会被回调接收。一个针对 App.User.{id} 频道的授权回调函数已经包含在 Laravel 框架内置的 BroadcastServiceProvider 中了。

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 5
发起讨论


pikalu
请问用 swoole 可以实现吗
0 个点赞 | 0 个回复 | 问答
panco
为什么不直接使用 websocket 框架实现?
0 个点赞 | 1 个回复 | 问答
doderic
翻译时顺便修复 resources 的目录结构
0 个点赞 | 0 个回复 | 分享