#はじめに
「チャットアプリのメッセージをリアルタイムで受信したい」「株価の変動を即座に表示したい」——従来の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
| 項目 | WebSocket | SSE |
|---|---|---|
| 通信方向 | 双方向 | サーバー→クライアント |
| プロトコル | 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プラットフォームの重要な概念を網羅しました。
これらの知識は日々の開発で活用できます。パフォーマンスの問題に直面したとき、セキュリティを考慮した設計が必要なとき、この記事群を参照してください。