コードを書いてみよう: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エラーや頻繁な切断に悩まされていませんか?コメント欄で教えてください。私の実戦経験をもとに、一緒に原因を突き止めましょう。
