Laravel 廣播系統工作原理

liuqing_hu發表於2018-05-23

這是一篇譯文,譯文首發於 Laravel 廣播系統工作原理,轉載請註明出處。

今天,讓我們深入研究下 Laravel 的廣播系統。廣播系統的目的是用於實現當服務端完成某種特定功能後向客戶端推送訊息的功能。本文我們將學習如何使用第三方 Pusher 工具向客戶端推送訊息的功能。

如果您遇到在 Laravel 中需要實現當伺服器處理完成某項工作後向客戶端傳送訊息這類的功能,那麼您需要使用到 Laravel 的廣播系統。

比如在一個支援使用者互相傳送訊息的即時通訊應用,當使用者 A 給使用者 B 傳送一條訊息時,系統需要實時的將訊息推送給使用者 B,並且資訊以彈出框或提示訊息框形式展現給使用者 B。

這種使用場景可以完美詮釋 Laravel 廣播系統的工作原理。另外,本教程將使用 Laravel 廣播系統實現這樣一個即時通訊應用。

或許您會對伺服器是如何將訊息及時的推送給客戶端的技術原理感興趣,這是因為在服務端實現這類功能時使用了套接字程式設計技術。在開始實現即時通訊系統前,先讓我們瞭解下套接字程式設計的大致流程:

  • 首先,伺服器需要支援 WebSocket 協議,並且允許客戶端建立 WebSocket 連線;
  • 您可以實現自己的 WebSocket 服務,或者使用第三方服務如 Pusher,後文會用到 Pusher 庫;
  • 客戶端建立一個伺服器的 Web Socket 連線,連線成功後客戶端會獲取唯一識別符號;
  • 一旦客戶端連線成功,表示該客戶端訂閱了指定頻道,將接收這個頻道的訊息;
  • 最後,客戶端還會註冊其所訂閱的頻道的監聽事件;
  • 當服務端完成指定功能後,我們以指定頻道名稱和事件名稱的資訊通知到 WebSocket 伺服器;
  • 最終,WebSocket 伺服器將這個指定事件已廣播的形式推送到所有註冊這個頻道監聽的客戶端。

以上所涉及的內容看似很多,但通過本文學習您將掌握箇中的訣竅。

接下來,讓我們開啟 Laravel 預設廣播系統配置檔案 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.
    |
    | 該配置選項用於配置專案需要提供廣播服務時的預設驅動器。配置聯結器可以使任意
    | 在 "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 框架提供諸多開箱即用的廣播驅動器程式。

本文將使用 Pusher 作為廣播驅動器。但在除錯階段,我們可以選擇使用 log 作為廣播驅動。同時如果選用 log 驅動,也就表示客戶端將不會接收任何訊息,而只是將需要廣播的訊息寫入到 laravel.log 日誌檔案內。

在下一節,我們將進一步講解如何實現一個即時通訊應用。

前期準備

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 Pusher\Pusher;

之後開啟 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">
                         
                    </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-domain/message/index 。如果您未登入系統,請先進行登入處理,登入後就可以看到廣播頁面資訊了。

雖然現在的 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-domain/message/send 頁面,順利的話會在 http://your-laravel-site-domain/message/index 頁面收到一個提示訊息。

同時在 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 頻道的 App\Events\NewMessageNotification 訊息。

當然,我們還可以通過 Pusher 管理後臺的儀表盤看到這個訊息內容,它在 Debug Console 標籤頁,我們可以看到如下日誌資訊。

除錯日誌

這就是今天的全部內容,希望能給大家帶來幫助。

結論

今天,我們研究了 Laravel 的 廣播 這個較少使用的特性。廣播可以讓我們使用 Web Sockets 傳送實時訊息。此外我們還使用廣播功能實現了一個簡單的實時訊息推送專案。本文內容較多,需要一些時間消化,有任何問題可以隨時聯絡我。

原文:How Laravel Broadcasting Works

相關文章