WebSocketとNode.jsでリアルタイムアプリを構築:チャットアプリから10,000 CCUのダッシュボードまで

Development tutorial - IT technology blog
Development tutorial - IT technology blog

コードを書いてみよう:5分で作るリアルタイムアプリ

難しい理論はさておき、まずはWebSocketのパワーを体験してみましょう。今回はSocket.ioを使用します。これは自動再接続機能やブラウザ間の互換性に優れており、Node.jsにおける「定番」のライブラリです。

1. プロジェクトの初期化

mkdir socket-demo && cd socket-demo
npm init -y
npm install express socket.io

2. サーバー側 (server.js)

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', (socket) => {
  console.log('ユーザーが接続しました:', socket.id);
  
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg); // 全クライアントにメッセージを送信
  });
});

server.listen(3000, () => {
  console.log('サーバーが起動しました: http://localhost:3000');
});

3. クライアント側 (index.html)

<!DOCTYPE html>
<html>
<body>
  <ul id="messages"></ul>
  <input id="input" /><button onclick="send()">送信</button>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();
    function send() {
      const input = document.getElementById('input');
      socket.emit('chat message', input.value);
      input.value = '';
    }
    socket.on('chat message', (msg) => {
      const item = document.createElement('li');
      item.textContent = msg;
      document.getElementById('messages').appendChild(item);
    });
  </script>
</body>
</html>

ブラウザで2つのタブを開いてみてください。メッセージが両方の画面に即座に表示されるはずです。これが双方向通信(Full-duplex)の力です。

WebSocketは従来のHTTPと何が違うのか?

以前は、データを更新するためにショートポーリング(Short Polling)がよく使われていました。クライアントが5秒ごとにサーバーへ問い合わせる方式です。しかし、この方法はリクエストのたびにヘッダーやCookieを送信し、ハンドシェイクをやり直すため、リソースを大幅に消費します。

WebSocketはこの問題を根本的に解決します。HTTPで一度だけ「挨拶(ハンドシェイク)」を行い、その後は持続的なTCPプロトコルにアップグレードします。これにより、極めて低いオーバーヘッドでデータをやり取りできるようになります。メッセージごとに1KBのヘッダーを送る代わりに、接続を維持するための数バイトだけで済みます。

プロ仕様の監視ダッシュボードへのアップグレード

チャットアプリから監視ダッシュボードに移行するには、考え方を切り替える必要があります。ユーザーがメッセージを送るのを待つのではなく、サーバーが実際のイベントに基づいてシステムデータを能動的にプッシュします。

リアルタイムダッシュボードの構成

サーバーのCPU使用率をリアルタイムで監視する場合を考えてみましょう。setIntervalを使用するか、Redisからのイベントをリッスンしてフロントエンドにプッシュします。

// 2秒ごとにシステム統計をプッシュ
setInterval(() => {
  const stats = {
    cpu: (Math.random() * 100).toFixed(2),
    memory: (Math.random() * 100).toFixed(2),
    time: new Date().toLocaleTimeString()
  };
  io.emit('dashboard-update', stats);
}, 2000);

フロントエンドにChart.jsを組み合わせれば、F5キーを押さなくてもグラフが動き続けるダッシュボードが完成します。

陥りやすい罠と実戦での教訓

システムを数万件の同時接続(CCU)までスケールさせると、デモコードのように単純にはいきません。ここでは、私が徹夜でバグ修正を繰り返して学んだ教訓を紹介します。

1. クリーンアップ忘れによるメモリリーク

稼働からわずか数時間でメモリリークに遭遇したことがあります。原因は、ループ内でリスナーを登録していたり、Reactコンポーネントがアンマウントされたときにリスナーを解除していなかったりしたことでした。

解決策: 不要になったら必ず socket.off()socket.removeAllListeners() を使用してください。

2. 入り口での認証(Authentication)

接続が完了してからチャットイベントでトークンを送る人が多いですが、それは避けましょう。認証(Authentication)のミドルウェアを使用してハンドシェイクの段階でブロックし、不正な接続にサーバーリソースを割かないようにします。

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  isValid(token) ? next() : next(new Error("アクセスが拒否されました"));
});

3. Redis Adapterによる水平スケーリング

単一のNode.jsサーバーでは限界があります。ロードバランサーを使用して3つのサーバーインスタンスを実行すると、サーバーAのユーザーはサーバーBのユーザーからのメッセージを受け取れません。

ここで救世主となるのがRedis Adapterです。Redisがサーバー間のメッセージ調整役(ハブ)となります。数行の設定を追加するだけで、システムを無限にスケールさせることが可能になります。

4. 不安定なネットワーク状態への対応

カフェのWi-Fiや4G回線は頻繁に不安定になります。Socket.ioには自動再接続の仕組みがありますが、UI側での工夫も必要です。ローディングバーや「再接続中…」といった通知を表示し、ユーザーがアプリがフリーズしたと感じないようにしましょう。

パフォーマンス最適化의 ヒント

  • バイナリデータ: 画像やファイルはBuffer/Binary形式で送信しましょう。従来のJSON文字列よりも帯域を大幅に節約できます。
  • Namespace & Room: 必要がない限り、全員にブロードキャストしてはいけません。ユーザーを room(例:socket.join('order-id-123'))にグループ化し、メッセージの流れを最適化しましょう。
  • ハートビート(Heartbeat): Ping/Pongの間隔を適切に調整します。クライアントが長時間応答しない場合は、すぐに接続を切断してサーバーのRAMを解放しましょう。

リアルタイム通信の実装は、単なるメッセージのやり取りではなく、接続状態を管理する「技術」です。初心者の方は、まずSocket.ioとRedisを導入してみてください。ユーザーが急増した際のトラブルを未然に防ぐことができます。

CORSエラーや頻繁な切断に悩まされていませんか?コメント欄で教えてください。私の実戦経験をもとに、一緒に原因を突き止めましょう。

Share: