前回作成したWebsocketを使用した会員チャット。Laravelのお陰でプログラムはシンプルなのですが、裏で何か起こっているかいまひとつ。ということで、仕組みに関して私なりの理解で説明します。

デモの会員のチャットプログラムのインストール手順はこちらから。

HttpとWebsocketの違い

まず、チャットで使用されている通信のプロトコルに関して。

Websocketは、Httpと同様にインターネットのアプリレイヤーのプロトコルの1つですが、以下のような違いがあります。

Httpがリクエストとレスポンスで通信が完了するのと違い、Websocketは一度接続が確立するとクローズ(サーバーあるいはクライアントが)するまで双方向の通信が可能となり、とても高速の通信が可能となります。

メッセージを送信すると

前回開発した会員チャットのデモにおいては、これらの2つの通信プロトコルがどう使用されているか見てみましょう。

クライアントのブラウザからメッセージを送信したときの流れの図を作成してみました。クライアント(ブラウザー)とサーバー(ウェブサーバーとPusherのサーバー)を結ぶ線において、通信のプロトコルが異なることに注意してください。黒線はHttpで、赤線はWebSocketです。

Http

まず、クライアントとウェブサーバーとPushサーバー間のHttp/Httpsの通信を見てみます。

上の図では、チャットの画面(太郎のブラウザなど)で、メッセージを入力して「送信」ボタンをクリックすると、同じページ上に含まれるchat.jsの以下のaddMessage()が実行されます。

...
createApp({
    setup () {
...
        function addMessage(message) {
            messages.value.push(message);

            axios.post('/messages', message).then(response => {
                // success
            });
        }
...

そこでは、入力した自分のメッセージを画面に反映させるとともに、ajaxで/messageにデータをPOSTします。
つまり、以下のChatController.phpのsendMessage()が実行されます。

...
class ChatsController extends Controller
{
...
    public function sendMessage(Request $request)
    {
        $user = Auth::user();

        // DBにレコードを作成
        $message = $user->messages()->create([
            'message' => $request->message,
        ]);

        // Pusherにブロードキャストに必要な情報を送信
        broadcast(new MessageSent($user, $message))->toOthers();

        return ['status' => 'Message Sent!'];
    }
...

上のメソッドでは、受信したデータをDBに記録して、ヘルパーのbroadcast()をコールして、Pusherのサーバーにブロードキャストするデータを送ります。データは、MessageSentのイベントインスタンスとして渡し、toOthers()で、にブロードキャストの対象には自分が含まれないことを指定します。ここまででは、まだHttpのプロトコルを通信の枠内です。

MessageSentイベントの定義を見てみましょう。

...
class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $user;
    public $message;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(User $user, Message $message)
    {
        $this->user = $user;
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('chat');
    }
}

このイベントはLaravelで通常に使われるイベントと同じですが、ShouldBroadcastをインターフェースとしているところが違います。それゆえに、broadcastOn()のメソッドが定義されています。そのメソッドにおいて、chatという名前のプライベートチャンネルが指定されていることに注意してください。

Websocket

今度は、クライアントとPusherサーバー間のWebsocketの通信について見てみます。

まず、クライアントとPusherサーバーのWebsocketの接続は、以下のbootstrap.jsのコードのnew Echo()の初期化の実行で確立します。

...
import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
    cluster:import.meta.env.VITE_PUSHER_APP_CLUSTER,
});

次に、chat.jsの以下でvueのアプリが初期化され、setup()がコールされます。そこでは、window.Echo.private('chat').listenにおいて、

  • クライアントとPusherサーバーでchatチャンネルの購読を確立する(最初の1回のみ)
  • そのチャンネルにおいて、PushサーバーからMessageSentイベントを受信したらクライアントの画面に表示する

作業を行います。ここで使用するチャンネル名(chat)とイベント名(MessageSent)は、Http側の通信で使用されているものと同じであることが必要です。

...
createApp({
    setup () {
        const messages = ref([]);

        fetchMessages();

        window.Echo.private('chat')
            .listen('MessageSent', (e) => {
                messages.value.push({
                    message: e.message.message,
                    user: e.user
                });
            });
...

例えば、花子さんのクライアントでPusherのサーバーとチャンネルの購読が確立すると、太郎さんが送信したメッセージ(つまり、MessageSentイベント)は、PusherサーバーからWebsocketを通じて送られてきます。そして、それがMessageSentイベントなら、花子さんのブラウザにリアルタイムで太郎さんのメッセージが表示されます。

Pusherのデバッグ画面

Pusherのサイトの管理画面では、上で説明したWebsocketの一連のイベントを、以下のDebug Consoleのメニューにおいてすべて閲覧することができます。

そこでは、太郎さんの接続と購読、花子さんの接続と購読、そして太郎さんが送信したメッセージの受信などのWebsocketイベントが最新なものからリスト表示されます。ちなみに、太郎さんが送信してPusherが受信したイベントでは、チャンネル名が、private-chatと表示されていますが、これはLaravel Echoが、MessageSentで指定したchatがプライベートチャンネルゆえに、private-をプレフィックスしているからです。

次回は

HttpとWebsocketを使用したチャットの仕組みを説明をしましたが、次回はクライアントとPusherサーバー間でのWebsocketでのセキュリティ、つまりプライベートチャンネルの仕組みを説明する予定です。

By khino