NodeJS 速習チュートリアル

WebSockets

1. WebSocket入門

WebSocketは、クライアントとサーバー間に永続的なコネクションを提供し、リアルタイムで双方向の通信を可能にします。これは、リクエスト・レスポンスモデルに従う従来のHTTPとは根本的に異なります。

1.1 WebSocketの主なメリット

  • リアルタイム更新: データを即座にクライアントへプッシュできます。
  • 効率性: HTTPリクエストを繰り返す必要がありません。
  • 双方向性: クライアントとサーバーの両方からメッセージを送信できます。
  • 低レイテンシ: メッセージが即座に送受信されます。

1.2 WebSocket vs HTTP

リアルタイムアプリケーションを効果的に構築するには、WebSocketとHTTPの違いを理解することが不可欠です。

機能WebSocketHTTP
コネクション永続的(単一のコネクション)リクエストごとに新しいコネクション
通信形態双方向(フルデュプレックス)単方向(リクエスト・レスポンス)
オーバーヘッドハンドシェイク後は最小限すべてのリクエストにヘッダーが含まれる
ユースケースリアルタイムアプリ従来のWebページ、API
チャット、ライブ配信ページ読み込み、フォーム送信

       プロのヒント: WebSocketは、プロトコル(ws:// または wss://)にアップグレードする前に、まずHTTPハンドシェイク(ステータスコード 101)から始まります。

2. WebSocketのセットアップ

2.1 wsモジュールのインストール

まず、プロジェクト用の新しいディレクトリを作成して初期化します。

mkdir websocket-demo
cd websocket-demo
npm init -y

次に、ws パッケージをインストールします。

npm install ws

       注意: ws モジュールは、シンプルで高速、かつ徹底的にテストされたWebSocketのクライアントおよびサーバー実装です。

3. WebSocketサーバーの作成

受信したメッセージをそのまま送り返す(エコー)シンプルなWebSocketサーバーを作成してみましょう。server.js というファイルを作成します。

const WebSocket = require('ws');

// ポート8080でWebSocketサーバーを作成
const wss = new WebSocket.Server({ port: 8080 });

console.log('WebSocketサーバーが ws://localhost:8080 で実行中です');

// 接続イベントのハンドラー
wss.on('connection', (ws) => {
  console.log('新しいクライアントが接続しました');
  
  // クライアントにウェルカムメッセージを送信
  ws.send('WebSocketサーバーへようこそ!');

  // メッセージイベントのハンドラー
  ws.on('message', (message) => {
    console.log(`受信メッセージ: ${message}`);
    // クライアントにメッセージをエコーバック(送り返す)
    ws.send(`サーバーが受信しました: ${message}`);
  });

  // 切断イベントのハンドラー
  ws.on('close', () => {
    console.log('クライアントが切断されました');
  });
});

4. コードの理解

  1. ws モジュールをインポートします。
  2. ポート 8080 で新しいWebSocketサーバーを作成します。
  3. 接続(connection)、メッセージ(message)、切断(close)の各イベントハンドラーを設定します。
  4. 受信したメッセージをすべてクライアントにエコーバックします。

4.1 サーバーの起動方法

  1. 上記のコードを server.js として保存します。
  2. サーバーを実行します:node server.js
  3. サーバーが起動し、ws://localhost:8080 で待機状態になります。

5. WebSocketクライアントの作成

サーバーができたので、接続するクライアントを作成しましょう。Node.js用とブラウザ用の2種類を作成します。

5.1 Node.jsクライアント

client.js というファイルを作成します:

const WebSocket = require('ws');
const readline = require('readline');

// ユーザー入力用のreadlineインターフェースを作成
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

// WebSocketサーバーに接続
const ws = new WebSocket('ws://localhost:8080');

// コネクションが開いた時の処理
ws.on('open', () => {
  console.log('WebSocketサーバーに接続しました');
  promptForMessage();
});

// サーバーからのメッセージをリッスン
ws.on('message', (message) => {
  console.log(`サーバー: ${message}`);
});

// エラーハンドリング
ws.on('error', (error) => {
  console.error('WebSocketエラー:', error);
});

// コネクションが閉じた時の処理
ws.on('close', () => {
  console.log('サーバーから切断されました');
  process.exit(0);
});

// ユーザーにメッセージ入力を促す関数
function promptForMessage() {
  rl.question('メッセージを入力してください(終了するには "exit"): ', (message) => {
    if (message.toLowerCase() === 'exit') {
      ws.close();
      rl.close();
      return;
    }
    ws.send(message);
    promptForMessage();
  });
}

5.2 ブラウザクライアント

HTMLとJavaScriptを使用して、サーバーに接続するシンプルなページを作成します。index.html を作成します:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>WebSocketクライアント</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
    #messages { height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
    .message { margin: 5px 0; }
  </style>
</head>
<body>
  <h1>WebSocketクライアント</h1>
  <div id="status">サーバーに接続中...</div>
  <div id="messages"></div>
  <div>
    <input type="text" id="messageInput" placeholder="メッセージを入力">
    <button onclick="sendMessage()">送信</button>
  </div>

  <script>
    const status = document.getElementById('status');
    const messages = document.getElementById('messages');
    const messageInput = document.getElementById('messageInput');

    // WebSocketサーバーに接続
    const ws = new WebSocket('ws://localhost:8080');

    // コネクションが開いた時
    ws.onopen = () => {
      status.textContent = 'サーバーに接続済み';
      status.style.color = 'green';
    };

    // メッセージを受信した時
    ws.onmessage = (event) => {
      const message = document.createElement('div');
      message.className = 'message';
      message.textContent = event.data;
      messages.appendChild(message);
      messages.scrollTop = messages.scrollHeight;
    };

    // エラー発生時
    ws.onerror = (error) => {
      status.textContent = 'エラーが発生しました';
      status.style.color = 'red';
    };

    // コネクションが閉じた時
    ws.onclose = () => {
      status.textContent = 'サーバーから切断されました';
      status.style.color = 'red';
    };

    // メッセージ送信関数
    function sendMessage() {
      const message = messageInput.value.trim();
      if (message) {
        ws.send(message);
        messageInput.value = '';
      }
    }

    // Enterキーで送信
    messageInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        sendMessage();
      }
    });
  </script>
</body>
</html>

       注意: ブラウザのセキュリティ制限により、このHTMLファイルはWebサーバー(http-serverlive-server など)経由で提供する必要があります。

6. アプリケーションのテスト

  1. WebSocketサーバーを起動します:node server.js
  2. クライアントのHTMLページを複数のブラウザウィンドウで開きます。
  3. 異なるクライアントからメッセージを送信し、リアルタイムに反映されるか確認します。
  4. ブラウザクライアントと並行してNode.jsクライアントも実行できます。

7. 実装の理解

  • サーバーは、接続されているすべてのクライアントのセットを保持します。
  • あるクライアントからメッセージを受信すると、それを他へブロードキャスト(配信)できます。
  • クライアントは接続、切断、およびエラーイベントを処理します。
  • メッセージは受信した瞬間にリアルタイムで表示されます。

8. WebSocketイベント

WebSocketはイベント駆動型モデルを採用しています。主なイベントは以下の通りです:

イベント説明
connection (サーバー)クライアントがサーバーに接続したときに発生
open (クライアント)コネクションが確立されたときに発生
messageメッセージを受信したときに発生
errorエラーが発生したときに発生
closeコネクションが閉じられたときに発生

9. 実用的なユースケース

WebSocketは、多種多様なアプリケーションで使用されています:

  • チャットアプリ: メッセージの即時配信
  • ライブダッシュボード: メトリクスやデータのリアルタイム更新
  • 共同編集ツール: 複数ユーザーによる同一ドキュメントの同時編集
  • ゲーム: 高速なインタラクションが必要なオンラインマルチプレイヤーゲーム
  • 金融プラットフォーム: リアルタイムの株価ティッカーや取引プラットフォーム
  • IoTアプリケーション: 接続されたデバイスの監視と制御

10. 高度なWebSocket機能

10.1 バイナリデータの転送

WebSocketはバイナリデータの送信をサポートしており、特定の種類のデータではより効率的です。

// バイナリデータの送信(サーバー側)
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // バイナリ形式の 'Hello'
ws.send(buffer, { binary: true });

// バイナリデータの受信(クライアント側)
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new Uint8Array(event.data);
    console.log('バイナリデータを受信しました:', view);
  }
};

10.2 ハートビートと接続監視

切断を検知して処理するために、ハートビートを実装します。

// サーバー側ハートビート
function setupHeartbeat(ws) {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
}

// 30秒ごとに全クライアントへPingを送信
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

// サーバー停止時のクリーンアップ
wss.on('close', () => {
  clearInterval(interval);
});

11. セキュリティの考慮事項

11.1 認証(Authentication)

WebSocket接続は常に認証を行うべきです。以下はJWTを用いた例です。

const http = require('http');
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');

const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });

// アップグレードリクエスト時の認証処理
server.on('upgrade', (request, socket, head) => {
  try {
    const token = request.url.split('token=')[1];
    if (!token) throw new Error('トークンが提供されていません');
    
    // JWTトークンの検証
    jwt.verify(token, 'your-secret-key', (err, decoded) => {
      if (err) {
        socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
        socket.destroy();
        return;
      }
      
      // WebSocketハンドシェイクの続行
      wss.handleUpgrade(request, socket, head, (ws) => {
        ws.user = decoded; // ユーザーデータをWebSocketに添付
        wss.emit('connection', ws, request);
      });
    });
  } catch (error) {
    socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
    socket.destroy();
  }
});

11.2 レート制限(Rate Limiting)

不正利用を防ぐためにレート制限を導入します。

const rateLimit = require('ws-rate-limit');

// 1接続あたり1分間に100メッセージまで
const limiter = rateLimit({
  windowMs: 60 * 1000, // 1分
  max: 100,
  message: 'メッセージ送信が多すぎます。少し時間を置いてください。',
});

wss.on('connection', (ws) => {
  limiter(ws);
  // ... その他の処理
});

12. パフォーマンス最適化

12.1 圧縮(Compression)

per-message deflate を有効にすることで、帯域幅の使用量を削減できます。

const WebSocket = require('ws');

const wss = new WebSocket.Server({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: {
      chunkSize: 1024,
      memLevel: 7,
      level: 3
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    concurrencyLimit: 10,
  }
});

ベストプラクティス: 本番環境のアプリケーションでは、WebSocketをサポートしていないブラウザへのフォールバック機能などを提供する Socket.IO のようなライブラリの使用も検討してください。