コンテンツにスキップ

Top

Laravel + Vue.js で WebSocket を使って Push通知 (Broadcast) する方法

Laravel で サーバからブラウザへPush通知したかったのだけどいまいちやり方がわからなかったり、Pusherを使った方法が紹介されていたりでうーんという感じだった。

ので、Pusher を使わない、 Laravel の WebSocket Server と Laravel Echo を使った Push 通知 の方法を調べたので以下に述べる。
(WebPushというのがLaravelに用意されているが、httpsじゃないとダメ、とか、クライアント側が拒否したらダメ、とかいろいろあるので今回は調べなかった)

サーバとクライアントの双方向通信の実現方法について

実はWebSocketを使わなくてもサーバとブラウザで双方向通信(もどき)はできます。

ものすごく簡単なのはポーリングですね。ブラウザ側から定期的にサーバにアクセスする、という方法です。

でも、たくさんのクライアントがポーリングしまくったらネットワーク的に大変なのは容易にわかります。
Ajax + Cometといった技術も生まれますが、これはサーバからのレスポンスを遅らせてるだけなので、ネットワークの通信負荷は減ったところでサーバリソースを占有する問題は残ります。

そこでそういった問題がないWebSocketでの双方向通信が検討されました。

まぁ双方向通信といってもサーバからブラウザへのPushが主ですが。なぜならブラウザからサーバへはすでに普通にやっていることなので。

Pusher について

PusherはWebSocketサーバーです。
アカウントを登録することで基本無料で使用することができますが、接続数や商用利用などに何か制限があるかもしれません(私自身Pusherを使ったことがないので知りませんし、今回の記事においいてもPusherそのものの知識は一切不要です)

これから説明するLaravelでのWebSocketによる双方向通信は、実はPusherを使った双方向通信のPusher部分をLaravel WebSocker Serverに置き換えることで実現しています。

Laravel WebSocket Server は最近(というほどでもないがPusherより後に)できた機能で、これができるまではLaravelはPusherを使ってブロードキャストなどを行っていました。

そしてそのPusherをLaravelのWebSocket Serverにまるっと置き換えることにより、Pusherを使わずに双方向通信できるようにしたわけです。

そのため、これから以下の文章にPusherがたくさん出てきますが、Pusherを使うわけではないので勘違いしないでください。

必要なモジュールを入れる

Broadcastに必要となるモジュールを入れる。

Pusherとlaravel-echoとlaravel-websocketsが主なインストール物。

なお、バージョンが新しすぎても古すぎても問題が起きるので、以下のバージョンで問題が起きた場合が都度うまくいくバージョンに変更しながら頑張って。

例えば自分の環境では、pusher/pusher-php-server をバージョン指定しないで入れたところ、7.2.4がインストールされたのだが、その状態で進めようとしたところ、途中でエラーになって困った。
ググったら7.0系にすれば回避できた的なことが書いてあったのでその方法で回避した。
それぞれの環境で異なると思うので、いろいろ試して頑張って。

composer require pusher/pusher-php-server "^7.0" 
npm install --save laravel-echo pusher-js
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
php artisan migrate
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

自分の環境では、pusher/pusher-php-server をバージョン指定しないで入れたところ、7.2.4がインストールされ問題になったが、同じようになって困っている人は以下のように remove して すれば7.0系を入れることができる。

composer remove pusher/pusher-php-server
composer require pusher/pusher-php-server "^7.0" -W

設定ファイルの修正

次に、設定ファイルを変更しましょう。以下のファイルの変更が必要となります。

・config/app.php
・config/broadcasting.php
・resources/js/bootstrap.js

また、config/websockets.php が生成されていることを確認しましょう。
できていない場合、以下のコマンドが実行されていない可能性があります。

php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

config/app.php の変更

デフォルトではブロードキャスト機能は無効化されているので有効にしましょう。

         App\Providers\AppServiceProvider::class,
         App\Providers\AuthServiceProvider::class,
         // App\Providers\BroadcastServiceProvider::class,
         App\Providers\EventServiceProvider::class,
         App\Providers\RouteServiceProvider::class,

の、App\Providers\BroadcastServiceProvider::class のコメントアウトを外す。

         App\Providers\AppServiceProvider::class,
         App\Providers\AuthServiceProvider::class,
         App\Providers\BroadcastServiceProvider::class,
         App\Providers\EventServiceProvider::class,
         App\Providers\RouteServiceProvider::class,

config/broadcasting.php の変更

        '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' => false,
                'encrypted' => true,
            ],
        ],
        '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' => false,
                'encrypted' => true,
                'host'      => env('PUSHER_HOST'),
                'port'      => env('PUSHER_PORT'),
                'scheme'    => 'http',
            ],
        ],
に。

hostとかの追加だけでなく、useTLSがfalseになっていることにも注意しましょう。

resources/js/bootstrap.js の変更

以下のコメントアウトを外しましょう。

//import Echo from 'laravel-echo';

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

//window.Echo = new Echo({
//    broadcaster: 'pusher',
//    key: process.env.MIX_PUSHER_APP_KEY,
//    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
//    forceTLS: false,
//    wsHost: process.env.MIX_PUSHER_HOST,
//    wsPort: process.env.MIX_PUSHER_PORT,
//});
import Echo from 'laravel-echo';

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

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: false,
    wsHost: process.env.MIX_PUSHER_HOST,
    wsPort: process.env.MIX_PUSHER_PORT,
});
に。

今回はSSLは使わないので、 forceTLS は falseにしましょう。

.envに追加の設定を行う

.envファイルに以下を追加しましょう。
(すでに値はある場合がありますが、その場合は変更してください)

なんかPUSHERという文字が大量に出てきますが、pusher自体は一切使いません。
laravel WebSocket の機能がPusherとの接続部分をそのまま乗っ取るからです。
よって、pusherにアカウント登録とかは不要です。

BROADCAST_DRIVER=pusher
PUSHER_HOST=hogefuga.com
PUSHER_PORT=6001
PUSHER_SCHEME=http

# 実際にPusherを使うわけではないので、以下の値は何でもよい
PUSHER_APP_ID=id
PUSHER_APP_KEY=key
PUSHER_APP_SECRET=secret
PUSHER_APP_CLUSTER=cluster

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_HOST="${PUSHER_HOST}"
MIX_PUSHER_PORT="${PUSHER_PORT}"
MIX_PUSHER_SCHEME="${PUSHER_SCHEME}"

LARAVEL_WEBSOCKETS_HOST="${PUSHER_HOST}"
LARAVEL_WEBSOCKETS_PORT="${PUSHER_PORT}"

PUSHER_HOST は 外からアクセスする場合も当然あるので、ドメイン名かアクセスできるIPアドレスにしないといけません。
ただ、とりあえず自分のPCのみで試すなら localhost や 127.0.0.1でもかまいませんが。

ここまで済んだらとりあえずnpm runしましょう。

php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
npm run prod

Laravel WebSocket Server の起動

以下のコマンドで起動できます。

PS > php artisan websockets:serve
Starting the WebSocket server on port 6001...

ポート番号はデフォルトで6001ですが、変えたいなら引数で指定できます。

php artisan websockets:serve --port=3030

Laravel WebSocket Server でエラーが!

Laravel WebSocket Server を立ち上げたところ、一定周期で謎のエラーがで続けて困った。

PS > php artisan websockets:serve
Starting the WebSocket server on port 6001...

Unhandled promise rejection with TypeError: React\Http\Io\ClientRequestStream::closeError(): Argument #1 ($error) must be of type Exception, Error given, called in C:\xampp\htdocs\hoge\vendor\react\promise\src\Internal\RejectedPromise.php on line 73 in :\xampp\htdocs\hoge\vendor\react\http\src\Io\ClientRequestStream.php:238
Stack trace:
#0 C:\xampp\htdocs\hoge\vendor\react\promise\src\Internal\RejectedPromise.php(73): React\Http\Io\ClientRequestStream->closeError(Object(Error))
#1 C:\xampp\htdocs\hoge\vendor\react\promise\src\Promise.php(47): React\Promise\Internal\RejectedPromise->then(Object(Closure), Array)
#2 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\ClientRequestStream.php(100): React\Promise\Promise->then(Object(Closure), Array)
#3 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\ClientRequestStream.php(122): React\Http\Io\ClientRequestStream->writeHead()
#4 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\ClientRequestStream.php(135): React\Http\Io\ClientRequestStream->write('{"app_id":"1","...')
#5 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\Sender.php(152): React\Http\Io\ClientRequestStream->end('{"app_id":"1","...')
#6 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\Transaction.php(146): React\Http\Io\Sender->send(Object(React\Http\Message\Request))
#7 C:\xampp\htdocs\hoge\vendor\react\http\src\Io\Transaction.php(83): React\Http\Io\Transaction->next(Object(React\Http\Message\Request), Object(React\Promise\Deferred), Object(React\Http\Io\ClientRequestState))

原因はreact/promiseのバージョンのせいとのことなので、バージョンを戻そうとしたところ、

PS >composer require react/promise:^2.3
エラーになってうまくいかない。
どうも自分の環境では2.3を入れることができないようだ。
仕方ないので、composer.lockファイルを開き、

"react/promise": "^3.0 || ^2.7 || ^1.2.1"

とかが書かれている箇所を全部 以下の様に 2.3 にして、

"react/promise": "^3.0 || ^2.3 || ^1.2.1"

もう一度、

composer require react/promise:^2.3
したらOK牧場。

サーバを立ち上げなおしても変なエラーがでなくなった。
バージョン問題めんどい。

php artisan queue:listen の起動

ブロードキャストのイベントはqueueに入るので、php artisan queue:listenまたはphp artisan queue:workをしてキューの受付をしないといけない。
意外に忘れがちなので注意。
これを立ち上げておかないと、ブロードキャストのイベントを生成しても受け取ることはできない。

PS > php artisan queue:listen

ブロードキャストするイベントの作成

Laravel-echo でブロードキャストするイベントを作成しないといけません。
まず以下のコマンドでイベントを作成しましょう。

php artisan make:event BroadcastMessageEvent
これで app/Events/BroadcastMessageEvent.php が生成されているはずです。

じつはこれはそのまま使えず、 implements ShouldBroadcast をつけてあげないといけません。

class BroadcastMessageEvent

→

class BroadcastMessageEvent implements ShouldBroadcast

そして broadcastOn() の中が

return new PrivateChannel('channel-name');
と、ProvateChannelになっていますが、これは認証とかが必要なときに使うので、今回は認証とかはないのでChannelに置き換えます。
return new Channel('channel-name');

これでとりあえずイベントは遅れるのですが、何もデータを送らないのもあれなので、引数で渡されたデータを渡すようにします。

    public $message;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(string $_msg)
    {
        $this->message = $_msg;
    }
これにより、イベント発火時に引数に設定した文字列も一緒にブロードキャストされ、ブラウザ側で受け取れるようになります。

テスト用のプログラムの作成

では、実際に動作確認しないと何が何だかわからんと思うので、簡単にサンプルプログラムを作りましょう。

動作確認において、別のPCがあるとよいのだが、なければ1台のPCで複数ブラウザを立ち上げて確認をすればよい。

ここはWebSocketの本質とはなんも関係ないのでさくっと進める。

Laravel + Vue3.jsの設定は済んでいるものとして、Broadcast.vue と Broadcast.php を作り、ブロードキャストボタンを押したらブロードキャストされる、というサンプルを作る。

ここらへんはLaravel + Vue3.jsの勉強ではないので、やれ routes/web.php の設定なり、resources/js/router.js なりの説明とかは一切しない。

http://localhost/broadcast にアクセスしたらresorces/js/components/Broadcast.vueの画面が表示され、そこでブロードキャストボタンで /broadcastをaxiosで投げたら app/HttpControllers/Broadcast.php 内のbroadcast関数が呼び出されるようにroutes/web.phpの設定をしておくこと。

resorces/js/components/Broadcast.vue の作成

上のエリアに文字を書いてブロードキャストボタンを押したらbroadcast()が実施され、最終的にブロードキャストから受け取ったメッセージを下のエリアに表示するVue。

<template>
    <div>
        <div>
            <textarea v-model="this.send_message"></textarea>
        </div>
        <div>
            <button type="button" @click="broadcast()">ブロードキャスト</button>
        </div>
        <div>
            <textarea v-model="this.recv_message"></textarea>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            send_message: '',
            recv_message: ''
        };
    },
    mounted() {
        Echo.channel('channel-name')
        .listen('BroadcastMessageEvent', (data) => {
            this.recv_message = data.message;
        });
    },
    unmounted () {
        Echo.channel('channel-name').stopListening('BroadcastMessageEvent');
    },
    methods: {
        broadcast: async function() {
            const url = '/broadcast';
            const params = { message: this.send_message };
            await axios.post(url, params)
            .then((response) => {
                this.send_message = '';
            });
        },
    },
};
</script>
unmounted()の中で.stopListeningをコールしているが、これを呼ばないとどの画面にいてもずっとリスニングし続けるので注意。

app/HttpControllers/Broadcast.php の作成

broadcast 関数が呼ばれたらブロードキャストのイベントを発火する。

    public function broadcast(Request $req, Response $res)
    {
        $msg = $req->message;

        BroadcastMessageEvent::dispatch($msg); // このdispatchでブロードキャストが発行される
    }

これでブロードキャストボタンを押したら、相手側(自分も含めて)にブロードキャストメッセージが送られたことがわかるはず。

Laravel-Websockets

Laravel Websocket Server が立ち上がっているときに、

http://127.0.0.1/laravel-websockets

にアクセスすると、イベントをPushできる画面が表示される。

Event Creator の
Channel のところにChannel名を、
Event のところにイベントクラスのクラス名(ちゃんとApp\Events\BroadcastMessageEventのようにApp\Event\から書く)を、
Data のところに「JSON形式」のデータを入れてSend eventボタンを押下するとイベントが投げられる。

DataをJSON形式にしないと、「Error sending event」と表示されて失敗するので注意!

以上!