前回作成した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でのセキュリティ、つまりプライベートチャンネルの仕組みを説明する予定です。
メルマガ購読の申し込みはこちらから。