WebSocket入門 Part 2 ~リアルタイムチャットの構築~

はじめに

Part 1では、WebSocketの基本概念とEchoサーバーの実装を通じて、双方向通信の基礎を学んだ。
Part 2では、実践的なアプリケーションとして「リアルタイムチャット」を構築する。

Echoサーバーとチャットサーバーの違いは、「1対1」か「1対多」か という点となる。
今回は、接続している全クライアントにメッセージを配信する「ブロードキャスト」の仕組みと、ユーザー識別の実装方法について整理する。

環境

Part 1と同様の環境を使用する。

Node v14以上
ライブラリ ws
ブラウザ Chrome, Edge, Firefox等

ブロードキャストの実装

Echoサーバーでは、メッセージを送ってきた ws (特定のクライアント) に対してのみ ws.send() を行っていた。
しかし、チャットアプリでは「Aさんの発言を、BさんやCさんにも届ける」必要がある。
これを ブロードキャスト と呼ぶ。

実装のポイント

ws ライブラリでは、wss.clients というプロパティに、現在接続中の全てのクライアント情報が格納されている。
これをループ処理することで、全員への送信が可能となる。

// 接続中の全クライアントに送信する関数
wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
        client.send(message);
    }
});

概念図: ブロードキャスト

Aさんが発言すると、サーバーはそれをコピーして、Aさん自身とBさんの両方に送信する。

Client BServerClient AClient BServerClient Apar[Broadcast]"こんにちは""Aさん: こんにちは""Aさん: こんにちは"

ユーザー識別 (ID管理)

誰が発言したのかを区別するために、各クライアントにIDを割り当てる必要がある。
WebSocketの接続オブジェクト ws は、通常のJavaScriptオブジェクトと同様に、任意のプロパティを追加できる。

これを利用して、接続時にランダムなIDを付与する。

wss.on('connection', (ws) => {
    // UUIDの生成
    ws.userId = crypto.randomUUID();
    
    console.log(`ユーザー ${ws.userId} が接続しました`);
});

切断の検知

チャットアプリでは「誰かが退出した」ことを知ることも重要である。
WebSocketでは close イベントを監視することで、切断を検知できる。

ws.on('close', () => {
    console.log(`ユーザー ${ws.userId} が切断しました`);
    // 必要に応じて、他のユーザーに「退出通知」をブロードキャストする
});

完成コード

これまでの要素を組み合わせた、チャットサーバーの完成コードは以下の通りである。

サーバー側 (server.js)

server.js
const WebSocket = require('ws');
const crypto = require('crypto'); // UUID生成用
const wss = new WebSocket.Server({ port: 8080 });

// 全員にメッセージを送る関数
function broadcast(message) {
    wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
            client.send(message);
        }
    });
}

wss.on('connection', (ws) => {
    // 1. IDの割り当て
    ws.userId = crypto.randomUUID();
    const joinMsg = `システム: ユーザー ${ws.userId} が入室しました`;
    console.log(joinMsg);
    broadcast(joinMsg);

    // 2. メッセージ受信時の処理
    ws.on('message', (message) => {
        const msg = `ユーザー ${ws.userId}: ${message}`;
        console.log(`受信: ${msg}`);
        broadcast(msg);
    });

    // 3. 切断時の処理
    ws.on('close', () => {
        const leaveMsg = `システム: ユーザー ${ws.userId} が退出しました`;
        console.log(leaveMsg);
        broadcast(leaveMsg);
    });
});

console.log('チャットサーバーが起動しました (ws://localhost:8080)');

クライアント側

クライアント側は、HTMLファイル (index.html) と JavaScriptファイル (client.js) に分けて記述する。

UI部分 (index.html)

index.html
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Chat</title>
</head>
<body>
    <h2>WebSocket Chat</h2>
    <input type="text" id="messageInput" placeholder="メッセージを入力">
    <button onclick="sendMessage()">送信</button>
    <div id="chatLog"></div>

    <script src="client.js"></script>
</body>
</html>

ロジック部分 (client.js)

client.js
const socket = new WebSocket('ws://localhost:8080');
const chatLog = document.getElementById('chatLog');

// 接続完了
socket.onopen = () => {
    addLog('システム: サーバーに接続しました');
};

// メッセージ受信
socket.onmessage = (event) => {
    addLog(event.data);
};

// 切断
socket.onclose = () => {
    addLog('システム: サーバーから切断されました');
};

function sendMessage() {
    const input = document.getElementById('messageInput');
    const message = input.value;
    if (message) {
        socket.send(message);
        input.value = '';
    }
}

function addLog(text) {
    const div = document.createElement('div');
    div.textContent = text;
    chatLog.appendChild(div);
}

参考

おわりに

Part 1Part 2を通じて、WebSocketを用いたリアルタイムアプリケーションの基礎を構築した。
意外とシンプルに実装できることが分かったので、これでWebSocketは怖くない!

この基礎があれば、特定のグループだけにメッセージを送る「ルーム機能」や、メッセージをデータベースに保存する「履歴機能」などもこの辺のソースを改造することで実装できるだろう。
余裕があれば、Part 3で 〇×ゲームを作りたい。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。