WebSocketによるリアルタイム通知基盤の構築:双方向メッセージ配信の実装

アーキテクチャ概要と役割設計

Spring BootとSTOMP over WebSocketを組み合わせたリアルタイム通信システムを構築します。画面は「管理者用(送信権限)」と「一般閲覧用(購読のみ)」に分離し、サーバー側でメッセージのブロードキャストを担う構成を採用します。WebJarsによりフロントエンド依存ライブラリのバージョン管理を一元化し、STOMPプロトコルによる標準的なメッセージフレームングを実現します。

クライアント側実装(HTML/JavaScript)

役割に応じたUI構成を定義し、通信ロジックは共通モジュールで管理します。以下の例は管理者用画面の構造を示しています。閲覧用画面は送信エリアのHTMLブロックをコメントアウトまたは削除するだけで対応可能です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>リアルタイム通知ダッシュボード</title>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/js/notification-client.js"></script>
</head>
<body>
<div class="container mt-4" id="app-root">
    <div class="row mb-3">
        <div class="col-md-6">
            <div class="btn-group" role="group">
                <button type="button" id="btn-link" class="btn btn-primary">接続開始</button>
                <button type="button" id="btn-cut" class="btn btn-secondary" disabled>切断</button>
            </div>
        </div>
        <div class="col-md-6 admin-only">
            <div class="input-group">
                <input type="text" id="msg-input" class="form-control" placeholder="配信メッセージを入力">
                <button type="button" id="btn-publish" class="btn btn-success">配信</button>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <div class="table-responsive" id="feed-panel" style="display:none;">
                <table class="table table-bordered">
                    <thead class="table-dark">
                        <tr><th>通知内容</th><th>受信時刻</th></tr>
                    </thead>
                    <tbody id="msg-list"></tbody>
                </table>
            </div>
        </div>
    </div>
</div>
</body>
</html>

コア通信モジュール(JavaScript)

STOMPクライアントのライフサイクル管理とデータレンダリングを関数化します。変数名と制御フローを最適化し、接続状態に応じたUI切り替えとXSS対策を標準的に実装しています。

let wsSession = null;

const UI = {
    btnLink: $('#btn-link'),
    btnCut: $('#btn-cut'),
    msgList: $('#msg-list'),
    feedPanel: $('#feed-panel'),
    toggleState(isConnected) {
        this.btnLink.prop('disabled', isConnected);
        this.btnCut.prop('disabled', !isConnected);
        this.feedPanel.toggle(isConnected);
        this.msgList.empty();
    }
};

function establishConnection() {
    const transport = new SockJS('/ws/hub');
    wsSession = Stomp.over(transport);

    wsSession.connect({}, (frame) => {
        console.log('[STOMP] セッション確立:', frame);
        UI.toggleState(true);

        // トピック購読登録
        wsSession.subscribe('/topic/system_alerts', (payload) => {
            const data = JSON.parse(payload.body);
            renderFeedEntry(data);
        });
    });
}

function terminateConnection() {
    if (wsSession) {
        wsSession.disconnect();
    }
    UI.toggleState(false);
    console.log('[STOMP] セッション切断完了');
}

function broadcastMessage() {
    const inputVal = $('#msg-input').val().trim();
    if (!inputVal) return;

    wsSession.send('/app/notify', {}, JSON.stringify({
        text: inputVal,
        timestamp: Date.now()
    }));
    $('#msg-input').val('');
}

function renderFeedEntry(data) {
    const dateStr = new Date(data.timestamp).toLocaleString('ja-JP');
    const row = `<tr>
        <td class="align-middle">${escapeHtml(data.text)}</td>
        <td class="align-middle text-muted">${dateStr}</td>
    </tr>`;
    UI.msgList.prepend(row);
}

function escapeHtml(unsafe) {
    return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

$(document).ready(() => {
    $('#btn-link').on('click', establishConnection);
    $('#btn-cut').on('click', terminateConnection);
    $('#btn-publish').on('click', broadcastMessage);
});

サーバー側実装(Spring Boot)

クライアントから到着したSTOMPフレームをインターセプトし、指定した宛先URLへメッセージを再送します。SpringのWebSocketサポートにより、スレッドセーフなブロードキャスト配信が保証されます。

package com.example.websocket.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.example.websocket.model.RequestPayload;
import com.example.websocket.model.ResponsePayload;

@Controller
public class NotificationController {

    /**
     * クライアント送信エンドポイントとブロードキャスト宛先のマッピング
     */
    @MessageMapping("/notify")
    @SendTo("/topic/system_alerts")
    public ResponsePayload handleBroadcast(RequestPayload payload) {
        return new ResponsePayload(payload.getText(), System.currentTimeMillis());
    }
}

// モデル定義例(Java 14+ record推奨)
// public record RequestPayload(String text) {}
// public record ResponsePayload(String text, long timestamp) {}

データフローと動作検証

  1. 接続フェーズ:クライアントは/ws/hub経由でSockJSトランスポートを確立し、STOMPオーバーレイを初期化します。
  2. 購読フェーズconnectコールバック内で/topic/system_alertsへのサブスクライブフレームをサーバーへ送信。これ以降、サーバーからのプッシュ配信を待ち受け状態に入ります。
  3. 送信フェーズ:管理者画面で配信ボタンを押下すると、/app/notify宛てにJSONペイロードを格納したSTOMPフレームが送信されます。
  4. ブロードキャストフェーズ:Spring Controllerがメッセージを処理し、@SendToで指定されたトピックへ再送。これにより、サブスクライブ中の全クライアントのコールバック関数が同期的にトリガーされます。
  5. レンダリングフェーズ:クライアント側で受信データをDOM操作し、UIへ反映。HTMLエスケープ処理を施すことでセキュリティリスクを排除します。

タグ: websocket STOMP Spring Boot javascript Real-Time Messaging

7月13日 00:15 投稿