Laravel broadcasting如何工作

450 阅读9分钟

今天, 我们将探索Laravel网络框架中广播的概念。它允许你在服务器端发生事情时向客户端发送通知。在这篇文章中, 我们将使用第三方的Pusher库来发送通知到客户端.

如果你曾经想在Laravel的服务器上发生一些事情的时候,从服务器上向客户端发送通知,你要找的就是广播功能。

例如, 让我们假设你已经实现了一个消息传递的应用程序, 允许你的系统的用户互相发送消息.现在, 当用户A向用户B发送消息时, 你想实时通知用户B.你可以显示一个弹出式窗口或一个警报框,通知用户B关于新的信息

这是一个完美的用例, 可以让我们了解Laravel中广播的概念, 这也是我们在这篇文章中要实现的.

如果你想知道服务器是如何向客户端发送通知的, 它是使用套接字来实现的。在我们深入研究实际实现之前,让我们先了解一下套接字的基本流程。

  • 首先,你需要一个支持网络套接字协议的服务器,并允许客户端建立一个网络套接字连接。
  • 你可以实现你自己的服务器或使用第三方服务,如Pusher。在这篇文章中,我们更倾向于后者。
  • 客户端向Web套接字服务器发起一个Web套接字连接,并在连接成功后收到一个唯一的标识符。
  • 一旦连接成功,客户端就会订阅它想接收事件的某些频道。
  • 最后,在订阅的通道下,客户端注册了它想听的事件。
  • 现在在服务器端,当一个特定的事件发生时,我们通过向web-socket服务器提供通道名称和事件名称来通知它。
  • 最后,web-socket服务器将该事件广播给该特定通道上的注册客户。

不要担心这看起来太多了,随着我们在本文中的进展,你会掌握它的技巧。

broadcasting配置文件

接下来, 让我们看一下默认的广播配置文件config/broadcasting.php.

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Broadcaster
    |--------------------------------------------------------------------------
    |
    | This option controls the default broadcaster that will be used by the
    | framework when an event needs to be broadcast. You may set this to
    | any of the connections defined in the "connections" array below.
    |
    | Supported: "pusher", "redis", "log", "null"
    |
    */

    'default' => env('BROADCAST_DRIVER', 'null'),

    /*
    |--------------------------------------------------------------------------
    | Broadcast Connections
    |--------------------------------------------------------------------------
    |
    | Here you may define all of the broadcast connections that will be used
    | to broadcast events to other systems or over websockets. Samples of
    | each available type of connection are provided inside this array.
    |
    */

    'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => true,
            ],
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],

        'log' => [
            'driver' => 'log',
        ],

        'null' => [
            'driver' => 'null',
        ],

    ],

];

默认情况下, Laravel在核心部分支持多个广播适配器.

在这篇文章中, 我们将使用pusher 广播适配器.为了调试的目的, 你也可以使用log 适配器.当然, 如果你使用log 适配器, 客户端将不会收到任何事件通知, 它只会被记录到laravel.log文件中.

从下一节开始, 我们将立即深入到上述用例的实际实现中。

设置先决条件

在广播中, 有不同类型的渠道--公共的, 私人的, 和存在的.当你想公开广播你的事件时,你应该使用公共频道。反之,当你想把事件通知限制在某些私人频道时,就会使用私人频道。

在我们的用例中,我们想在用户收到新消息时通知他们。而要想获得接收广播通知的资格,用户必须登录。因此,在我们的案例中,我们需要使用私人频道。

核心认证功能

首先, 你需要启用默认的Laravel认证系统, 这样注册, 登录等功能就可以开箱即用.如果你不确定如何做,官方文档提供了一个快速的洞察力。

Pusher SDK:安装和配置

由于我们要使用Pusher 第三方服务作为我们的web-socket服务器,你需要在它那里创建一个账户,并确保你的帖子注册时有必要的API凭证。

接下来, 我们需要安装Pusher PHP SDK,这样我们的Laravel应用程序就可以向Pusher web-socket服务器发送广播通知。

在你的Laravel应用根目录下, 运行下面的命令来安装它作为一个composer包.

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

现在, 让我们修改一下**.env**文件,使pusher 适配器成为我们的默认广播驱动程序。

...
...
BROADCAST_DRIVER=pusher

PUSHER_APP_ID={YOUR_APP_ID}
PUSHER_APP_KEY={YOUR_APP_KEY}
PUSHER_APP_SECRET={YOUR_APP_SECRET}
PUSHER_APP_CLUSTER={YOUR_APP_CLUSTER}
...
...

正如你所看到的, 我们已经将默认的广播驱动程序改为pusher 。你还需要配置其他的选项,你应该首先从Pusher账户中得到这些选项。

最后,让我们在config/app.php中启用广播服务,删除下面一行的注释。

App\Providers\BroadcastServiceProvider::class,

到目前为止,我们已经安装了服务器专用库。在下一节,我们将介绍需要安装的客户端库。

Pusher和Laravel Echo库-安装和配置

在广播中, 客户端的责任是订阅频道并监听所需的事件.在引擎盖下, 它是通过打开一个新的连接到web-socket服务器来完成的.

幸运的是, 我们不需要实现任何复杂的JavaScript东西来实现它, 因为Laravel已经提供了一个有用的客户端库, Laravel Echo, 它可以帮助我们在客户端处理socket.此外, 它还支持我们在本文中要使用的Pusher服务.

你可以通过使用NPM包管理器来安装Laravel Echo库。当然, 你需要安装Node和npm,如果你还没有它们的话。剩下的就很简单了, 如下面的片段所示.

$npm install laravel-echo

我们感兴趣的是node_modules/laravel-echo/dist/echo.js文件,你应该复制到public/echo.js

就这样,我们的客户端库的设置就完成了。

后端文件设置

回想一下,我们正在谈论设置一个允许我们的应用程序的用户相互发送消息的应用程序。另一方面,我们将向已登录的用户发送广播通知,当他们收到其他用户的新消息时。

在本节中,我们将创建所需的文件,以实现我们正在寻找的用例。

创建一个模型类

首先,让我们创建Message ,该模型保存用户相互发送的消息。

php artisan make:model Message --migration

我们还需要在我们的messages 表中添加一些字段,如to,from, 和message 。因此,让我们在运行migrate命令之前,修改迁移文件database/migrations/XXXX_XX_XXXX_create_messages_table.php

<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateMessagesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('from', FALSE, TRUE);
            $table->integer('to', FALSE, TRUE);
            $table->text('message');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('messages');
    }
}

现在,让我们运行migrate 命令,在数据库中创建messages 表。

$php artisan migrate

创建一个事件类

每当你想在Laravel中引发一个自定义的事件,你应该为该事件创建一个类。根据事件的类型, Laravel会做出相应的反应并采取必要的行动.

如果该事件是一个普通的事件, Laravel会调用相关的监听器类.另一方面, 如果事件是广播类型的, Laravel会将该事件发送到配置在config/broadcasting.php文件中的web-socket服务器。

因为我们的例子中使用的是Pusher服务, Laravel会将事件发送到Pusher服务器.

让我们使用下面的artisan命令来创建一个自定义事件类:NewMessageNotification.

$php artisan make:event NewMessageNotification

这应该会创建app/Events/NewMessageNotification.php文件。让我们把该文件的内容替换成以下内容。

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use App\Message;

class NewMessageNotification implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;
 
    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Message $message)
    {
        $this->message = $message;
    }
 
    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('user.'.$this->message->to);
    }
}

需要注意的是,NewMessageNotification 类实现了ShouldBroadcastNow 接口。因此, 当我们引发一个事件时, Laravel知道这个事件应该被广播。

事实上, 你也可以实现ShouldBroadcast 接口, Laravel将一个事件加入到事件队列中.当事件队列工作者有机会的时候,它就会被处理。在我们的案例中, 我们想马上广播它, 这就是为什么我们使用了ShouldBroadcastNow 接口.

在我们的案例中,我们想显示用户收到的信息,因此我们在构造函数参数中传递了Message 模型。通过这种方式,数据将与事件一起被传递。

接下来,是broadcastOn 方法,它定义了事件将被广播的频道名称。在我们的例子中,我们使用了私人频道,因为我们想把事件广播限制在登录的用户身上。

$this->message->to 变量指的是事件将被广播的用户的ID。因此,它有效地使频道名称像user.{USER_ID}

创建广播路由

在私有通道的情况下,客户端必须在与web-socket服务器建立连接之前认证自己。它确保在私有通道上广播的事件只发送给经过认证的客户。在我们的案例中, 这意味着只有登录的用户才能订阅我们的频道user.{USER_ID}.

如果你使用Laravel Echo客户端库来订阅频道, 你很幸运!它自动处理了认证部分, 而你只需要定义频道的路由.

让我们继续,在routes/channels.php文件中为我们的私人频道添加一个路由。

<?php
 
/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/
 
Broadcast::channel('App.User.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
});
 
Broadcast::channel('user.{toUserId}', function ($user, $toUserId) {
    return $user->id == $toUserId;
});

正如你所看到的,我们已经为我们的私人频道定义了user.{toUserId} 路线。

channel方法的第二个参数应该是一个关闭函数。Laravel会自动将当前登录的用户作为关闭函数的第一个参数,而第二个参数通常是从频道名称中获取的。

当客户端试图订阅私人频道user.{USER_ID} ,Laravel Echo库在后台使用XMLHttpRequest对象进行必要的认证,更常见的是XHR。

现在我们已经完成了设置, 所以让我们继续测试它.

前端文件设置

在本节中,我们将创建测试我们的用例所需的文件。

创建一个控制器

让我们继续在app/Http/Controllers/MessageController.php创建一个控制器文件,内容如下。

<?php
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Message;
use App\Events\NewMessageNotification;
use Illuminate\Support\Facades\Auth;
 
class MessageController extends Controller
{
    public function __construct() {
        $this->middleware('auth');
    }
 
    public function index()
    {
        $user_id = Auth::user()->id;
        $data = array('user_id' => $user_id);
 
        return view('broadcast', $data);
    }
 
    public function send()
    {
        // ...
         
        // message is being sent
        $message = new Message;
        $message->setAttribute('from', 1);
        $message->setAttribute('to', 2);
        $message->setAttribute('message', 'Demo message from user 1 to user 2');
        $message->save();
         
        // want to broadcast NewMessageNotification event
        event(new NewMessageNotification($message));
         
        // ...
    }
}

创建一个视图

index 方法中,我们使用broadcast 视图,所以让我们也创建resources/views/broadcast.blade.php视图文件。

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
 
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
 
    <title>Test</title>
 
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-default navbar-static-top">
            <div class="container">
                <div class="navbar-header">
 
                    <!-- Collapsed Hamburger -->
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse">
                        <span class="sr-only">Toggle Navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
 
                    <!-- Branding Image -->
                    <a class="navbar-brand123" href="{{ url('/') }}">
                        Test
                    </a>
                </div>
 
                <div class="collapse navbar-collapse" id="app-navbar-collapse">
                    <!-- Left Side Of Navbar -->
                    <ul class="nav navbar-nav">
                        &nbsp;
                    </ul>
 
                    <!-- Right Side Of Navbar -->
                    <ul class="nav navbar-nav navbar-right">
                        <!-- Authentication Links -->
                        @if (Auth::guest())
                            <li><a href="{{ route('login') }}">Login</a></li>
                            <li><a href="{{ route('register') }}">Register</a></li>
                        @else
                            <li class="dropdown">
                                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>
 
                                <ul class="dropdown-menu" role="menu">
                                    <li>
                                        <a href="{{ route('logout') }}"
                                            onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                            Logout
                                        </a>
 
                                        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                            {{ csrf_field() }}
                                        </form>
                                    </li>
                                </ul>
                            </li>
                        @endif
                    </ul>
                </div>
            </div>
        </nav>
 
        <div class="content">
                <div class="m-b-md">
                    New notification will be alerted realtime!
                </div>
        </div>
    </div>
 
    <!-- receive notifications -->
    <script src="{{ asset('js/echo.js') }}"></script>
 
    <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
         
        <script>
          Pusher.logToConsole = true;
         
          window.Echo = new Echo({
            broadcaster: 'pusher',
            key: 'c91c1b7e8c6ece46053b',
            cluster: 'ap2',
            encrypted: true,
            logToConsole: true
          });
         
          Echo.private('user.{{ $user_id }}')
          .listen('NewMessageNotification', (e) => {
              alert(e.message.message);
          });
        </script>
    <!-- receive notifications -->
</body>
</html>

添加路由

最后,我们还需要在routes/web.php文件中添加路由。

Route::get('message/index', 'MessageController@index');
Route::get('message/send', 'MessageController@send');

它是如何工作的

在控制器类的构造方法中,你可以看到我们使用了auth 中间件,以确保控制器方法只被登录的用户访问。

接下来,是index 方法,它渲染了broadcast 视图。让我们把最重要的代码拉到视图文件中。

<!-- receive notifications -->
<script src="{{ asset('js/echo.js') }}"></script>
 
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
     
<script>
    Pusher.logToConsole = true;
 
    window.Echo = new Echo({
        broadcaster: 'pusher',
        key: 'c91c1b7e8c6ece46053b',
        cluster: 'ap2',
        encrypted: true,
        logToConsole: true
    });
 
    Echo.private('user.{{ $user_id }}')
    .listen('NewMessageNotification', (e) => {
        alert(e.message.message);
    });
</script>
<!-- receive notifications -->

首先, 我们加载必要的客户端库, Laravel Echo和Pusher, 允许我们打开与Pusher web-socket服务器的web-socket连接.

接下来, 我们通过提供pusher 作为我们的广播适配器和其他必要的pusher相关信息来创建Echo的实例.

进一步,我们使用Echo的private 方法来订阅私人频道user.{USER_ID} 。正如我们前面所讨论的,客户端必须在订阅私人频道之前对自己进行认证。因此Echo 对象通过在后台发送带有必要参数的XHR来进行必要的认证。最后, Laravel试图找到user.{USER_ID} 路线, 它应该与我们在routes/channels.php文件中定义的路线相匹配.

如果一切顺利, 你应该有一个与Pusher web-socket服务器打开的web-socket连接, 并且在user.{USER_ID} channel上列出事件!从现在开始,我们就可以在这个通道上接收所有传入的事件。

在我们的案例中,我们想要监听NewMessageNotification 事件,因此我们使用了Echo 对象的listen 方法来实现。为了简单起见,我们只提醒我们从Pusher服务器收到的消息。

所以这就是接收来自web-sockets服务器的事件的设置。接下来,我们将通过控制器文件中的send 方法,引发广播事件。

让我们快速拉入send 方法的代码。

public function send()
{
    // ...
     
    // message is being sent
    $message = new Message;
    $message->setAttribute('from', 1);
    $message->setAttribute('to', 2);
    $message->setAttribute('message', 'Demo message from user 1 to user 2');
    $message->save();
     
    // want to broadcast NewMessageNotification event
    event(new NewMessageNotification($message));
     
    // ...
}

在我们的案例中,我们要在收到新消息时通知登录的用户。所以我们已经尝试在send 方法中模仿这种行为。

接下来,我们使用了event 辅助函数来引发NewMessageNotification 事件。由于NewMessageNotification 事件属于ShouldBroadcastNow 类型, Laravel从config/broadcasting.php文件中加载默认的广播配置.最后, 它把NewMessageNotification 事件广播到配置好的web-socket服务器的user.{USER_ID} 通道上.

在我们的例子中, 事件将被广播到Pusher web-socket服务器上的user.{USER_ID} 通道.如果接收用户的ID是1 ,该事件将通过user.1 通道广播。

正如我们前面所讨论的,我们已经有一个设置可以监听这个通道上的事件,所以它应该能够接收这个事件,并将警报框显示给用户!

如何测试我们的设置

让我们继续走下去,看看你应该如何测试我们到目前为止建立的用例。

在你的浏览器中打开URLhttps://your-laravel-site-domain/message/index。如果你还没有登录,你会被重定向到登录界面。一旦你登录,你应该看到我们之前定义的广播视图--还没有什么花哨的东西。

事实上, Laravel已经在后台为你做了相当多的工作.因为我们已经启用了Pusher客户端库提供的Pusher.logToConsole ,它在浏览器控制台中记录了所有的内容,以达到调试的目的。让我们看看当你访问http://your-laravel-site-domain/message/index页面时,有什么被记录到控制台。

Pusher : State changed : initialized -> connecting
 
Pusher : Connecting : {"transport":"ws","url":"wss://ws-ap2.pusher.com:443/app/c91c1b7e8c6ece46053b?protocol=7&client=js&version=4.1.0&flash=false"}
 
Pusher : Connecting : {"transport":"xhr_streaming","url":"https://sockjs-ap2.pusher.com:443/pusher/app/c91c1b7e8c6ece46053b?protocol=7&client=js&version=4.1.0"}
 
Pusher : State changed : connecting -> connected with new socket ID 1386.68660
 
Pusher : Event sent : {"event":"pusher:subscribe","data":{"auth":"c91c1b7e8c6ece46053b:cd8b924580e2cbbd2977fd4ef0d41f1846eb358e9b7c327d89ff6bdc2de9082d","channel":"private-user.2"}}
 
Pusher : Event recd : {"event":"pusher_internal:subscription_succeeded","data":{},"channel":"private-user.2"}
 
Pusher : No callbacks on private-user.2 for pusher:subscription_succeeded

它已经打开了与Pusher web-socket服务器的web-socket连接,并订阅了自己以监听私有通道上的事件。当然,在你的情况下,你可以根据你所登录的用户的ID,有一个不同的通道名称。现在,让我们在测试send 方法时保持这个页面打开。

接下来,让我们在另一个标签页或不同的浏览器中打开http://your-laravel-site-domain/message/sendURL。如果你要使用一个不同的浏览器,你需要登录才能访问该页面。

一旦你打开http://your-laravel-site-domain/message/send页面,你应该能够在另一个标签页中看到一个警报信息,即http://your-laravel-site-domain/message/index。

让我们导航到控制台,看看刚才发生了什么。

Pusher : Event recd : {"event":"App\\Events\\NewMessageNotification","data":{"message":{"id":57,"from":1,"to":2,"message":"Demo message from user 1 to user 2","created_at":"2018-01-13 07:10:10","updated_at":"2018-01-13 07:10:10"}},"channel":"private-user.2"}

正如你所看到的,它告诉你,你刚刚从Pusher web-socket服务器的private-user.2 通道上收到App\Events\NewMessageNotification 事件。

事实上,你也可以看到在Pusher端发生了什么。进入你的Pusher账户并导航到你的应用程序。在Debug Console下,你应该能够看到正在记录的信息。

debug console

这就是本文的结尾!希望这篇文章没有太多的内容,因为我已经尽力将事情简化到最好。

总结

今天, 我们经历了Laravel中最不被讨论的功能之一--广播。它允许你使用网络套接字来发送实时通知。在这篇文章的整个过程中, 我们建立了一个真实世界的例子来证明上述的概念。