Laravel 广播系统工做原理

这是一篇译文,译文首发于 Laravel 广播系统工做原理,转载请注明出处。

今天,让咱们深刻研究下 Laravel 的广播系统。广播系统的目的是用于实现当服务端完成某种特定功能后向客户端推送消息的功能。本文咱们将学习如何使用第三方 Pusher 工具向客户端推送消息的功能。php

若是您遇到在 Laravel 中须要实现当服务器处理完成某项工做后向客户端发送消息这类的功能,那么您须要使用到 Laravel 的广播系统。css

好比在一个支持用户互相发送消息的即时通讯应用,当用户 A 给用户 B 发送一条消息时,系统须要实时的将消息推送给用户 B,而且信息以弹出框或提示消息框形式展示给用户 B。html

这种使用场景能够完美诠释 Laravel 广播系统的工做原理。另外,本教程将使用 Laravel 广播系统实现这样一个即时通讯应用。前端

或许您会对服务器是如何将消息及时的推送给客户端的技术原理感兴趣,这是由于在服务端实现这类功能时使用了套接字编程技术。在开始实现即时通讯系统前,先让咱们了解下套接字编程的大体流程:node

  • 首先,服务器须要支持 WebSocket 协议,而且容许客户端创建 WebSocket 链接;
  • 您能够实现本身的 WebSocket 服务,或者使用第三方服务如 Pusher,后文会用到 Pusher 库;
  • 客户端建立一个服务器的 Web Socket 链接,链接成功后客户端会获取惟一标识符;
  • 一旦客户端链接成功,表示该客户端订阅了指定频道,将接收这个频道的消息;
  • 最后,客户端还会注册其所订阅的频道的监听事件;
  • 当服务端完成指定功能后,咱们以指定频道名称和事件名称的信息通知到 WebSocket 服务器;
  • 最终,WebSocket 服务器将这个指定事件已广播的形式推送到全部注册这个频道监听的客户端。

以上所涉及的内容看似不少,但经过本文学习您将掌握个中的诀窍。laravel

接下来,让咱们打开 Laravel 默认广播系统配置文件 config/broadcasting.php 看看里面的配置选项:git

<?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.
    |
    | 该配置选项用于配置项目须要提供广播服务时的默认驱动器。配置链接器可使任意
    | 在 "connections" 节点配置的驱动名称。
    |
    | Supported: "pusher", "redis", "log", "null"
    | 
    | 支持:"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'),
                'encrypted' => true,
            ],
        ],

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

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

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

    ],

];

默认状况下 Laravel 框架提供诸多开箱即用的广播驱动器程序。github

本文将使用 Pusher 做为广播驱动器。但在调试阶段,咱们能够选择使用 log 做为广播驱动。同时若是选用 log 驱动,也就表示客户端将不会接收任何消息,而只是将须要广播的消息写入到 laravel.log 日志文件内。web

在下一节,咱们将进一步讲解如何实现一个即时通讯应用。redis

前期准备

Laravel 广播系统支持 3 中不一样频道类型 - public(公共), private(私有) 和 presence(存在)。当系统须要向所用用户推送信息时,可使用 「public(公共)」 类型的频道。相反,若是仅须要将消息推送给指定的频道,则须要使用 「 private(私有)」 类型的频道。

咱们的示例项目将实现一个仅支持登陆用户才能收到即时信息的消息系统,因此将使用 「 private(私有)」 类型的频道。

开箱即用的认证服务

首先对于新建立的 Laravel 项目,咱们须要安装 Laravel 提供的开箱即用的认证服务组件,默认认证服务功能包括:注册、登陆等功能。若是您不知道如何使用默认认证服务,能够查看 Laravel 的用户认证系统 文档快速入门。

服务端 Pusher SDK 安装配置

这边咱们将使用 Pusher 这个第三方服务做为 WebSocket 服务器,因此还须要建立一个 账号 并确保已获取 API 证书。安装配置遇到任何问题,请在评论区说明。

以后须要使用 Composer 包管理工具安装 Pusher 的 PHP 版本 SDK,这样才能在 Laravel 项目中使用 Pusher 发送广播信息。

如今进入 Laravel 项目的根目录,执行下面这条命令进行安装:

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

安装完成后修改广播配置文件,启用 Pusher 驱动做为广播系统的驱动器。

<?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', 'pusher'),
 
    /*
    |--------------------------------------------------------------------------
    | 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' => 'ap2',
                        'encrypted' => true
            ],
        ],
 
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],
 
        'log' => [
            'driver' => 'log',
        ],
 
        'null' => [
            'driver' => 'null',
        ],
 
    ],
 
];

如你所见,咱们修改了默认驱动器。而且将 connections 节点的 Pusher 配置的 cluster 修改为 ap2

同时还有须要从 .env 配置文件获取的配置选项,因此咱们须要更新 .env 文件,加入以下配置信息:

BROADCAST_DRIVER=pusher
 
PUSHER_APP_ID={YOUR_APP_ID}
PUSHER_APP_KEY={YOUR_APP_KEY}
PUSHER_APP_SECRET={YOUR_APP_SECRET}

接下来,还须要对 Laravel 核心文件稍做修改才能使用最新的 Pusher SDK。不过,我并不提倡修改 Laravel 核心文件,这边因为演示方便因此我修改了其中的代码。

让咱们打开 vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php 文件。将 use Pusher; 替换为 use PusherPusher;

以后打开 vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php 文件,在相似下面的代码中作相同修改:

return new PusherBroadcaster(
  new \Pusher\Pusher($config['key'], $config['secret'],
  $config['app_id'], Arr::get($config, 'options', []))
);

最后,在 config/app.php 配置中开启广播服务提供者配置:

App\Providers\BroadcastServiceProvider::class,

这样 Pusher 库的安装工做就完成了。下一节,咱们将讲解客户端类库的安装。

客户端 Pusher 和 Laravel Echo 类库的安装配置

在广播系统中,客户端接口负责链接 WebSocket 服务器、订阅指定频道和监听事件等功能。

幸运的是 Laravel 已经给咱们提供了一个叫 Laravel Echo 的插件,它实现一个复杂的 JavaScript 客户端程,。而且这个插件内置支持 Pusher 的服务器链接。

能够经过 NPM 包管理器安装 Laravel Echo 模块。若是您尚未安装 Node.js 及 NPM 包管理程序,仍是要先安装 Node.js 才行。

这里我认为您已经安装好了 Node.js,因此安装 Laravel Echo 扩展的命令以下:

npm install laravel-echo

安装完成后咱们直接将 node_modules/laravel-echo/dist/echo.js 文件复制到 public/echo.js 就好了。

仅适用一个 echo.js 文件有点杀鸡用了牛刀的感受,因此您还能够到 Github 直接下载 echo.js 文件。

至此,咱们就完成了客户端组件的安装。

服务端文件设置

回想一下前文提到的内容:首先咱们须要实现一个容许用户互相发送消息的应用;另外,应用会经过广播系统向已登陆系统而且有收到消息的用户推送消息。

这一节咱们将编写服务端代码实现广播系统相关功能。

建立 message 迁移文件

首先,咱们须要建立一个 Message 模型用于存储用户发送的消息,执行以下命令建立一个迁移文件:

php make:model Message --migration

但在执行 migrate 命令前,咱们须要在迁移文件中加入表字段 tofrommessage

<?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 命令运行数据库迁移文件:

php artisan migrate

当须要在 Laravel 执行事件时,咱们首先须要作的是建立一个事件类,Laravel 将基于不一样的事件类型执行不一样的操做。

若是事件为一个普通事件,Laravel 会调用对应的监听类。若是事件类型为广播事件,Laravel 会使用 config/broadcasting.php 配置的驱动器将事件推送到 WebSocket 服务器。

本文使用的是 Pusher 服务,因此 Laravel 将事件推送到 Pusher 服务器。

先使用下面的 artisan 命令建立一个事件类:

php artisan make:event NewMessageNotification

这个命令会建立 app/Events/NewMessageNotification.php 文件,让咱们修改文件内的代码:

<?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\ShouldBroadcastNow;
use App\Message;

class NewMessageNotification implements ShouldBroadcastNow
{
    use 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 接口,这个接口会将事件加入到消息队列中。而后由队列的 Worker 进程依据入队顺序依次执行。因为咱们项目须要当即将消息推送给用户,因此咱们实现 ShouldBroadcastNow 接口更为合适。

还有就是咱们须要显示用户接收的消息信息,因此咱们将 Message 模型做为构造函数的参数,这样消息信息就会同事件一块儿传入到指定频道。

接下来还在 NewMessageNotification 类中建立了一个 broadcastOn 方法,在该方法中定义了广播事件的频道名称,由于只有登陆的用户才能接收消息,因此这里建立了 PrivateChannel 实例做为一个私有频道。

定义频道名称格式相似于 user.{USER_ID} ,其中包含了指向接收信息的用户 ID,用户ID 从 $this->message->to 中获取。

对于客户端程序须要先进行用户身份校验,而后才能惊醒链接 WebSocket 服务器处理;这样才能保证私有频道的消息仅会广播给登陆用户。一样在客户端也仅容许登陆用户才可以订阅 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} 路由,Broadcast::channel 方法的第二个参数接收一个闭包,Laravel 会将登陆用户信息自动注入到闭包的第一个参数,第二个参数会从渠道中解析并获取。

当客户端尝试订阅 user.{USER_ID} 这个私有频道时 Laravel Echo 组件会使用 XMLHttpRequest 以异步请求方式进行用户身份校验处理。

到这里即时通讯全部编码工做就完成了。

建立测试用例

首先,建立一个控制器 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 = new Message;
        $message->setAttribute('from', 1);
        $message->setAttribute('to', 2);
        $message->setAttribute('message', 'Demo message from user 1 to user 2');
        $message->save();

        // 将 NewMessageNotification 加入到事件
        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 路由配置文件定义 HTTP 路由:

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

因为 MessageController 构造函数中使用了 auth 中间件,因此确保了仅有登陆用户才能访问以上路由。

接下来,让咱们分析下 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 -->

视图文件里首先,引入了 echo.jspusher.min.js这两个必要的模块,这样咱们才可以使用 Laravel Echo 去链接 Pusher 的服务器。

接着,建立 Laravel Echo 实例。

以后,经过 Echo 实例的 private 方法订阅 user.{USER_ID} 这个私有频道。以前咱们说过只有登陆用户才能订阅私有频道,因此 Echo 实例会使用 XHR 异步校验用户。而后,Laravel 会尝试查找 user.{USER_ID} 路由,并匹配到已在 routes/channels.php 文件中定义的广播路由。

一切顺利的话,咱们的项目此时即完成了 Pusher 服务器链接,以后就会监听 user.{USER_ID} 频道。这样客户端才能够正常接收指定频道的全部消息。

完成客户端接收 WebSocket 服务器消息接收编码工做后,在服务端须要经过 Message::send 方法发送一个广播消息。

发送的代码以下:

public function send()
    {
        // ...

        // 建立消息
        $message = new Message;
        $message->setAttribute('from', 1);
        $message->setAttribute('to', 2);
        $message->setAttribute('message', 'Demo message from user 1 to user 2');
        $message->save();

        // 将 NewMessageNotification 加入到事件
        event(new NewMessageNotification($message));

        // ...
    }

这段代码先是模拟了登陆用户发送消息的操做。

而后经过 event 辅助函数将 NewMessageNotification 事件类实例加入广播频道。因为 NewMessageNotificationShouldBroadcastNow 类的实例,Laravel 会从 config/broadcasting.php 配置文件中读取广播配置数据,而后将 NewMessageNotification 事件分发到配置文件所配置的 WebSocket 服务器的 user.{USER_ID} 频道。

对于本文示例会将消息广播到 Pusher 服务器的 user.{USER_ID} 频道里。若是订阅者的 ID 是 1,事件所处的广播频道则为 user.1

以前咱们已经在前端代码中完成频道的订阅和监听处理,这里当用户收到消息时会在页面弹出一个消息框提示给用户。

如今如何对以上功能进行测试呢?

在浏览器访问地址 http://your-laravel-site-doma... 。若是您未登陆系统,请先进行登陆处理,登陆后就能够看到广播页面信息了。

虽然如今的 Web 页面看起来什么也没有作,可是 Laravel 已经在后台进行了一系列处理。经过 Pusher 组件的 Pusher.logToConsole 咱们能够开启 Pusher 的调试功能。下面是登陆后的调试信息内容:

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

能够看到咱们完成了 WebSocket 服务器链接和私有频道监听。固然您看到的频道名称获取和个人不同,但内容大体相同。接下来不要关闭这个 Web 页面,而后去访问 send 方法发送消息。

新开一个页面窗口在浏览器访问 http://your-laravel-site-doma... 页面,顺利的话会在 http://your-laravel-site-doma... 页面收到一个提示消息。

同时在 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 服务器的 private-user.2 频道的 AppEventsNewMessageNotification 消息。

固然,咱们还能够经过 Pusher 管理后台的仪表盘看到这个消息内容,它在 Debug Console 标签页,咱们能够看到以下日志信息。

调试日志

这就是今天的所有内容,但愿能给你们带来帮助。

结论

今天,咱们研究了 Laravel 的 广播 这个较少使用的特性。广播可让咱们使用 Web Sockets 发送实时消息。此外咱们还使用广播功能实现了一个简单的实时消息推送项目。本文内容较多,须要一些时间消化,有任何问题能够随时联系我。

原文:How Laravel Broadcasting Works

相关文章
相关标签/搜索