Laravel+WebSocketを触ってみる。

今回は、WebSocketの勉強として、複数のブラウザでリアルタイムに同期されるチャット機能を実装してみます。

WebSocketとは?

サーバとクライアント間で相互通信を行うためのプロトコル

Socket.IOって何?

Getting Started with Socket.IO, Node.js and Express

Socket.IOはWebSocketで通信を行うためのライブラリ。(厳密にはWebSocketの実装ではないらしいが)
ブラウザ用のJavaScriptライブラリと、サーバサイド用のNode.jsライブラリがある。

LaravelでWebSocketを使うには?

ブロードキャスト(Broadcast)という機能を利用する
この仕組みにより、Laravelでイベントがトリガーされるとフロントにもイベントが通知される様になるとの事。
ブロードキャストドライバとして、PusherというSaaSサービスを使う方式RedisのPub/Sub機能を使用する方式がある。

Laravel-Echoって何?

Laravelのブロードキャストをブラウザで受け取るためのJavaScriptのライブラリ
フロントのライブラリまで提供してくれるLaravelコミュニティの厚さがやばい。

Laravel-Echo-Serverって何?

Laravelのイベントをブロードキャストするミドルウェア(NodeJSサーバ)。
Socket.IOを用いて作成されている。
laravel-echo-server startで起動して6001ポートでWebSocket接続を待ち受け、Laravelアプリケーションで発生したイベントをブラウザにブロードキャストする。

どんな構成になるのか?

今回はRedisを用いた方式でやってみる。
ブラウザのPOST送信からイベントブロードキャスト受信までを図にするとこんな感じになる。

Laravel + Redis + Laravel Echo Serverの構成図

ちなみにPusherを使う方式だとこうなる。
RedisサーバとLaravel-Echo-Serverが不要になる。

Laravel + Pusherの構成図

実装していく。

基本的には公式ドキュメントに従って進める。

まずは画面を実装しておく。
現状はこんな感じ。

フロント側でsocket.io-clientlaravel-echoを読み込む必要がある
フロントのコードはこんな感じになった。

import Echo from 'laravel-echo';

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

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

window.Echo.channel('messages')
    .listen('MessageSend', (e) => {
        console.log(e.order.name);
    });

Laravel Echo Serverを用意する

Laravel Echo Serverの設定ファイルをプロジェクトルートに配置する。
laravel-echo-server initコマンドでlaravel-echo-server.jsonが生成されるので、これをプロジェクトルートに配置する。

laravel-echo-server initを実行している図
生成されたlaravel-echo-server.json

Laravel Echo ServerのREADMEを参考にDockerイメージを作っていく
こんな感じのDockerfileを作った。

FROM node:14.2.0-alpine3.10
RUN npm install -g laravel-echo-server
CMD [ "laravel-echo-server", "start" ]

で、docker-composeにも追加して、コンテナを起動する。
ポート6001を外から接続できる様にするのを忘れずに。

  socket.io:
    container_name: ${COMPOSE_PROJECT_NAME}-socket.io
    build: socket.io
    working_dir: "/var/www/app"
    volumes:
      - ${SRC_DIR}:/var/www/app
    ports:
      - "6001:6001"
    restart: always

コンテナのログを確認して以下の様に表示されていればOK。

project-name-socket.io | L A R A V E L  E C H O  S E R V E R
project-name-socket.io | 
project-name-socket.io | version 1.6.2
project-name-socket.io | 
project-name-socket.io | ⚠ Starting server in DEV mode...
project-name-socket.io | 
project-name-socket.io | ✔  Running at localhost on port 6001
project-name-socket.io | ✔  Channels are ready.
project-name-socket.io | ✔  Listening for http events...
project-name-socket.io | ✔  Listening for redis events...
project-name-socket.io | 
project-name-socket.io | Server ready!

メッセージ送信処理を作る(構成図の①)

画面からWebサーバにメッセージをPOSTする処理を作る
(この辺はWebSocketに直接関係ないので省略。DBにメッセージを保存したりイベントを発行したりしてます。)
で、POSTしたら以下のエラーが発生した。
redisのPHP拡張がインストールされていないとの事。
ドキュメントにはcomposer require predis/predisしろとしか書かれていないが・・・?

Please make sure the PHP Redis extension is installed and enabled.

predisではなくphpredisに変えてみる
phpのDockerfileに以下を追記して再ビルドする。

RUN pecl install redis
RUN docker-php-ext-enable redis

改めてPOSTしてみるとエラーが変わった
.envREDIS_HOSTを設定していなかったので設定したら解決した

Connection refused", exception: "RedisException"

これでメッセージが送信できる様になった

ブラウザでイベントが受信できない(構成図の④)

一通り必要な物を用意して、POST(イベントをトリガー)しても、laravel-echoのlistenが呼び出されない。

window.Echo.channel('messages')
    .listen('MessageSend', (e) => {
        console.log('receive MSG', e); // ここが呼ばれない
    });

調査のため、redisでsubscribeしてみる
redis-cliで接続してsubscribe [チャンネル名]する。
イベント待ちになるのでブラウザからPOSTを送ってみるも反応なし。

127.0.0.1:6379> subscribe messages
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "messages"
3) (integer) 1

逆にpublishしてみる
ブラウザに通知は来ない(console.logに出力なし)

127.0.0.1:6379> publish messages aasdf
(integer) 1

LaravelのEventListenerが呼ばれているのか確認してみる
ブレークポイントを掛けてPOSTしてみると、ブレークが掛かるので、問題なく呼ばれている。

EventListenerが呼ばれたタイミングでブレークを掛けている図

原因が分からないのでBroadcastの実装を追いかける
Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.phpformatChannelsでチャンネル名を組み立てているのだが、ここでprefixが付与されている

    /**
     * Format the channel array into an array of strings.
     *
     * @param  array  $channels
     * @return array
     */
    protected function formatChannels(array $channels)
    {
        return array_map(function ($channel) {
            return $this->prefix.$channel;
        }, parent::formatChannels($channels));
    }

prefixはconfig/database.phpで定義されていてデフォルトはlaravel_database_になっている

config/database.phpのprefixが設定されている箇所

あらためてredis-cliからprefix付きでsubscribeしてPOSTしてみるとメッセージが出力された
redisまでは到達していることがわかった。

redis-cliでsubscribeしている図

Laravel Echo Serverのログを確認する
docker-compose logs socket.ioで確認できる。
以下の様なエラーが発生している。
Laravel Echo Serverからredisに接続できていない模様。

project-name-socket.io | [ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
project-name-socket.io |     at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1142:16)

laravel-echo-server.jsondatabaseConfigにホスト名やprefixを設定する

  "database": "redis",
  "databaseConfig": {
    "redis": {
        "host": "kvs",
        "keyPrefix": "laravel_database_"
    }
  },

改めてLaravel Echo Serverのログを確認すると接続できているっぽいログが表示された

project-name-socket.io | [3:40:13 PM] - XXXX left channel: messages (transport close)
project-name-socket.io | [3:40:14 PM] - XXXX joined channel: laravel_database_messages
project-name-socket.io | Channel: laravel_database_messages
project-name-socket.io | Event: App\Events\MessageSend

一応chromeの開発者ツールのネットワークタブでも確認してみると、こんな感じで表示された

chrome開発者ツールのネットワークタブでwebsocketの通信を確認している図

送受信の詳細も確認できる。

chrome開発者ツールのネットワークタブでwebsocketの通信の詳細を確認している図

こんな感じでイベントの受信もできる様になった

完成!

左右に2つのブラウザを立ち上げて、お互いにメッセージを送り合える事を確認して完成!

完成したチャット機能

所感

Pusherの料金表を見ると最安で月額約5000円〜との事なので、Laravel Echo ServerやRedisをメンテするコストを考えるとPusherを使ったほうが安そうだなぁって思いました・・・。

コメントする