🛰️

WebSocketの最小実装からRedis連携まで:リアルタイム通信の基礎とスケールアウト対応

に公開

はじめに

リアルタイム通信を学ぶとき、いきなりRedisやKafkaを導入すると「なぜ必要なのか」が見えづらくなります。
この記事ではまず 最小限のWebSocketサーバー を動かして感覚を掴み、
その後に スケールアウト時の課題とRedisによる解決 を体験する流れで整理します。

Step 1. 最小構成でWebSocketを動かしてみる

構成図

[Browser] ⇄ (WebSocket) ⇄ [Node.js Server]

サーバー

server.js
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 3001 });

wss.on("connection", (ws) => {
  console.log("🔌 Client connected");

  ws.on("message", (msg) => {
    console.log("📨 Received:", msg.toString());
    // 全クライアントに送信(同じプロセス内)
    wss.clients.forEach((client) => {
      if (client.readyState === 1) {
        client.send(msg.toString());
      }
    });
  });
});

console.log("🚀 WebSocket server on ws://localhost:3001");

クライアント

client.html
<!DOCTYPE html>
<html>
  <body>
    <h2>リアルタイムカウンター</h2>
    <p>カウント: <span id="count">0</span></p>
    <button id="inc">+1</button>

    <script>
      const socket = new WebSocket("ws://localhost:3001");
      const countEl = document.getElementById("count");

      socket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        countEl.textContent = data.count;
      };

      document.getElementById("inc").onclick = () => {
        const newCount = parseInt(countEl.textContent) + 1;
        socket.send(JSON.stringify({ count: newCount }));
      };
    </script>
  </body>
</html>

実行手順

bash

node server.js
# そして client.html をブラウザで開く

→ 2タブ開いて一方で「+1」すると、両方の画面が同時に更新されます 🎉

⚠️ Step 2. スケールアウトすると破綻する

ECSなどでWebSocketサーバーをスケールさせると、ALBによって接続が複数タスクに分散されます。

[ALB]
 ├──▶ [Server #1] (clients A, B)
 └──▶ [Server #2] (clients C, D)

各サーバーはメモリ内に自分のクライアントだけを保持しているため、
Server #1で発生したイベントをServer #2のクライアントに送ることができません。

Step 3. Redis Pub/Subでスケール対応

構成図

          ┌──────────┐
          │   Redis   │  ← Pub/Sub Broker
          └────┬─────┘
               │
   ┌───────────┴───────────┐
   ▼                       ▼
[Server #1]           [Server #2]
 (clients A,B)         (clients C,D)

修正版サーバー

server.js
import { WebSocketServer } from "ws";
import { createClient } from "redis";

const wss = new WebSocketServer({ port: process.env.PORT || 3001 });
const redisSubscriber = createClient({ url: "redis://localhost:6379" });
const redisPublisher = createClient({ url: "redis://localhost:6379" });

await redisSubscriber.connect();
await redisPublisher.connect();

await redisSubscriber.subscribe("counterUpdated", (message) => {
  wss.clients.forEach((client) => {
    if (client.readyState === 1) client.send(message);
  });
});

wss.on("connection", (ws) => {
  ws.on("message", async (msg) => {
    await redisPublisher.publish("counterUpdated", msg.toString());
  });
});

Redisをブローカーとして利用することで、
どのサーバーでイベントが発生しても全サーバーに配信できるようになります。

Redis導入の効果まとめ

項目 導入前(単体) 導入後(Redis連携)
通信範囲 同一サーバー内のみ 複数サーバー間で共有
ECSスケール対応 ❌ 不可 ✅ 可
状態共有 メモリのみ Redis経由
冗長構成 不可 可能

Redisは「イベントブローカー」として、
どのサーバーからでも、全クライアントに同じイベントを届ける 役割を果たします。

Discussion