#はじめに

「チャットアプリのメッセージをリアルタイムで受信したい」「株価の変動を即座に表示したい」——従来のHTTPではこれらを効率的に実現できません。

WebSocketとServer-Sent Events(SSE)は、サーバーからクライアントへのリアルタイム通信を実現する技術です。それぞれの特徴を理解し、適切な場面で使い分けることが重要です。

この記事を読むと、以下のことができるようになります:

  • WebSocketとSSEの違いを理解できる
  • 適切なリアルタイム通信方式を選択できる
  • 基本的な実装ができる

#HTTPの限界

#リクエスト-レスポンスモデル

HTTPは基本的に「クライアントがリクエストし、サーバーがレスポンスを返す」というモデルです。

[クライアント] ──リクエスト──> [サーバー]
[クライアント] <──レスポンス── [サーバー]

サーバーから能動的にデータを送ることができません。

#ポーリング

定期的にリクエストを送信して更新を確認する方法です。

// 短いポーリング(非効率)
setInterval(async () => {
  const response = await fetch('/api/messages');
  const messages = await response.json();
  updateUI(messages);
}, 1000);

問題点:

  • 更新がなくてもリクエストが発生
  • サーバーの負荷が高い
  • リアルタイム性が低い

#ロングポーリング

サーバーがレスポンスを保留し、更新があったときに返す方法です。

async function longPoll() {
  const response = await fetch('/api/messages/subscribe');
  const message = await response.json();
  updateUI(message);
  longPoll(); // 再接続
}
longPoll();

ポーリングより効率的ですが、接続の再確立にオーバーヘッドがあります。

#WebSocket

WebSocketは、クライアントとサーバー間で双方向のリアルタイム通信を行うプロトコルです。

[クライアント] <======= WebSocket =======> [サーバー]
               双方向、常時接続

#接続の確立

HTTPからWebSocketへのアップグレードを行います。

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

#クライアント側の実装

// 接続
const ws = new WebSocket('wss://example.com/chat');

// 接続確立時
ws.onopen = () => {
  console.log('接続しました');
  ws.send('Hello, Server!');
};

// メッセージ受信時
ws.onmessage = (event) => {
  console.log('受信:', event.data);
};

// エラー時
ws.onerror = (error) => {
  console.error('エラー:', error);
};

// 切断時
ws.onclose = (event) => {
  console.log('切断しました', event.code, event.reason);
};

// メッセージ送信
ws.send('Hello!');

// JSONを送信
ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }));

// 切断
ws.close();

#サーバー側の実装(Node.js)

// ws ライブラリを使用
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('クライアントが接続しました');

  ws.on('message', (data) => {
    console.log('受信:', data.toString());

    // エコーバック
    ws.send(`Echo: ${data}`);

    // 全クライアントにブロードキャスト
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data.toString());
      }
    });
  });

  ws.on('close', () => {
    console.log('クライアントが切断しました');
  });
});

#再接続の実装

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectInterval = 1000;
    this.maxReconnectInterval = 30000;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('接続しました');
      this.reconnectInterval = 1000; // リセット
    };

    this.ws.onclose = () => {
      console.log(`${this.reconnectInterval}ms後に再接続します`);
      setTimeout(() => this.connect(), this.reconnectInterval);
      // 指数バックオフ
      this.reconnectInterval = Math.min(
        this.reconnectInterval * 2,
        this.maxReconnectInterval
      );
    };

    this.ws.onerror = (error) => {
      console.error('エラー:', error);
      this.ws.close();
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }
}

#Server-Sent Events(SSE)

SSEは、サーバーからクライアントへの一方向のストリーミングを行う技術です。

[クライアント] ──リクエスト──> [サーバー]
[クライアント] <──イベント─── [サーバー]
[クライアント] <──イベント─── [サーバー]
[クライアント] <──イベント─── [サーバー]
              ...

#クライアント側の実装

const eventSource = new EventSource('/api/events');

// メッセージ受信
eventSource.onmessage = (event) => {
  console.log('受信:', event.data);
};

// 名前付きイベント
eventSource.addEventListener('update', (event) => {
  console.log('update:', event.data);
});

eventSource.addEventListener('notification', (event) => {
  console.log('notification:', event.data);
});

// エラー時(自動再接続される)
eventSource.onerror = (error) => {
  console.error('エラー:', error);
};

// 切断
eventSource.close();

#サーバー側の実装(Node.js)

const express = require('express');
const app = express();

app.get('/api/events', (req, res) => {
  // SSE用のヘッダー
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // クライアントにイベントを送信
  const sendEvent = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // 名前付きイベント
  const sendNamedEvent = (event, data) => {
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // 定期的にイベントを送信
  const intervalId = setInterval(() => {
    sendEvent({ time: new Date().toISOString() });
  }, 1000);

  // 切断時のクリーンアップ
  req.on('close', () => {
    clearInterval(intervalId);
  });
});

#SSEのイベント形式

event: update
data: {"message": "Hello"}
id: 12345
retry: 3000
フィールド説明
dataイベントデータ(必須)
eventイベント名(省略時はmessage
idイベントID(再接続時に使用)
retry再接続間隔(ミリ秒)

#再接続とLast-Event-ID

SSEは自動的に再接続し、最後に受信したイベントIDをサーバーに送信します。

app.get('/api/events', (req, res) => {
  const lastEventId = req.headers['last-event-id'];

  if (lastEventId) {
    // 未受信のイベントを送信
    sendMissedEvents(lastEventId, res);
  }

  // 以降のイベントを送信...
});

#WebSocket vs SSE

項目WebSocketSSE
通信方向双方向サーバー→クライアント
プロトコルWebSocket(ws://、wss://)HTTP
再接続手動実装自動
バイナリデータ対応非対応(テキストのみ)
ブラウザ対応良好良好(IE非対応)
プロキシ通過問題あり問題なし
HTTP/2対応別接続多重化可能

#使い分け

WebSocketを選ぶ場合:

  • 双方向通信が必要(チャット、ゲーム)
  • バイナリデータを扱う
  • 高頻度の送受信

SSEを選ぶ場合:

  • サーバーからの一方向通知(ニュース、株価)
  • HTTP/2の恩恵を受けたい
  • シンプルな実装で十分

#Socket.IO

WebSocketを抽象化したライブラリで、フォールバック機能を持ちます。

// サーバー
const { Server } = require('socket.io');
const io = new Server(3000);

io.on('connection', (socket) => {
  console.log('接続:', socket.id);

  socket.on('chat message', (msg) => {
    io.emit('chat message', msg); // 全員にブロードキャスト
  });

  socket.on('disconnect', () => {
    console.log('切断:', socket.id);
  });
});
// クライアント
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

socket.on('connect', () => {
  console.log('接続しました');
});

socket.emit('chat message', 'Hello!');

socket.on('chat message', (msg) => {
  console.log('受信:', msg);
});

#Socket.IOの特徴

  • 自動再接続
  • WebSocket非対応時のフォールバック
  • ルーム機能
  • 名前空間

#HTTP/2 Server Push(廃止)

HTTP/2 Server Pushは、サーバーがリクエストを予測してリソースを送信する機能でしたが、Chromeで廃止されました(2022年)。

# 以前は動作していた
Link: </style.css>; rel=preload; as=style

廃止の理由:

  • 実際の効果が限定的
  • キャッシュとの整合性の問題
  • 103 Early Hintsの登場

#代替: 103 Early Hints

HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style

HTTP/1.1 200 OK
Content-Type: text/html
...

サーバーが最終レスポンスを準備している間に、ヒントを送信できます。

#スケーリングの課題

#WebSocketのスケーリング

WebSocketはステートフル(状態を持つ)なため、スケーリングに課題があります。

問題:
[クライアントA] ←→ [サーバー1]
[クライアントB] ←→ [サーバー2]

AがBにメッセージを送りたいが、異なるサーバーに接続している

#解決策: Pub/Sub

RedisなどのPub/Subを使用してサーバー間でメッセージを共有します。

[クライアントA] ←→ [サーバー1] ←→ [Redis Pub/Sub] ←→ [サーバー2] ←→ [クライアントB]
const Redis = require('ioredis');
const pub = new Redis();
const sub = new Redis();

// メッセージ受信時
sub.subscribe('chat');
sub.on('message', (channel, message) => {
  // 接続中の全クライアントに送信
  wss.clients.forEach((client) => {
    client.send(message);
  });
});

// メッセージ送信時
ws.on('message', (data) => {
  pub.publish('chat', data);
});

#DevToolsでの確認

#Networkタブ

  • WebSocket接続は「WS」フィルターで確認
  • メッセージの送受信をリアルタイムで確認可能

#SSE

  • EventSourceのリクエストは通常のXHR/Fetchとして表示
  • レスポンスがストリーミングされる様子を確認可能

#まとめ

  • HTTP: リクエスト-レスポンスモデル、サーバーからプッシュ不可
  • WebSocket: 双方向リアルタイム通信、チャットやゲーム向け
  • SSE: サーバーからの一方向ストリーミング、通知や更新向け
  • Socket.IO: WebSocketのラッパー、フォールバック機能
  • HTTP/2 Server Push: 廃止、103 Early Hintsが代替
  • スケーリング: Pub/Subパターンでサーバー間連携

#全体のまとめ

これで「Web Platform Fundamentals」のすべてのモジュールを学習しました。HTTPの基礎から始まり、Cookie・認証、セキュリティ、キャッシュ、パフォーマンス、そして発展トピックまで、Webプラットフォームの重要な概念を網羅しました。

これらの知識は日々の開発で活用できます。パフォーマンスの問題に直面したとき、セキュリティを考慮した設計が必要なとき、この記事群を参照してください。

#参考リンク