はじめに
現代のWebアプリケーションでは、チャット機能や株価情報の更新、通知機能など、リアルタイム性が求められる場面が増えています。サーバーからクライアントへ最新情報を届ける方法として、主に3つの通信方式があります。
- ポーリング(Polling): クライアントが定期的にサーバーへ問い合わせる
- WebSocket: クライアントとサーバー間で双方向の永続的な接続を確立する
- SSE(Server-Sent Events): サーバーからクライアントへ一方向にデータを送信する
それぞれに特徴があり、用途によって使い分ける必要があります。この記事では、各通信方式の仕組みと、どのような場面で使うべきかを解説します。
ポーリング(Polling)
ポーリングとは
ポーリングは、クライアントが一定間隔でサーバーに対してHTTPリクエストを送り、新しいデータがあるか確認する方式です。最もシンプルで理解しやすい仕組みですね。
仕組みと動作フロー
クライアントは設定した間隔(例えば5秒ごと)でサーバーにリクエストを送り、新しいデータの有無を確認します。サーバーは毎回HTTPレスポンスを返し、接続は都度切断されます。
メリットとデメリット
メリット
- 実装がシンプルで、既存のHTTP通信の知識で対応できる
- サーバー側の実装が容易で、特別なプロトコルやライブラリが不要
- ファイアウォールやプロキシの影響を受けにくい
デメリット
- 更新がない場合も無駄なリクエストが発生し、サーバーとネットワークに負荷がかかる
- ポーリング間隔によってはリアルタイム性が低い(5秒間隔なら最大5秒の遅延)
- 同時接続数が多い場合、サーバーリソースを圧迫する
適している場面と実装例
適している場面
- リアルタイム性がそれほど求められない場合(数秒〜数十秒の遅延が許容される)
- 更新頻度が低いデータ(ダッシュボードの定期更新など)
- シンプルな実装で済ませたい小規模なアプリケーション
基本的な実装例
// クライアント側(JavaScript)
async function pollData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('ポーリングエラー:', error);
}
}
// 5秒ごとにポーリング
setInterval(pollData, 5000);
// サーバー側(Node.js/Express)
app.get('/api/data', (req, res) => {
const latestData = getLatestData(); // 最新データを取得
res.json(latestData);
});
WebSocket
WebSocketとは
WebSocketは、クライアントとサーバー間で双方向の永続的な接続を確立するプロトコルです。一度接続すれば、どちらからでも自由にデータを送信できます。
仕組みと動作フロー
最初にHTTPプロトコルで接続を開始し、その後WebSocketプロトコルにアップグレードします。接続が確立されると、クライアントとサーバーは対等な関係となり、いつでもメッセージを送信できます。
メリットとデメリット
メリット
- 双方向のリアルタイム通信が可能で、遅延がほとんどない
- 接続を維持するため、リクエストのオーバーヘッドがなく効率的
- サーバーから能動的にデータをプッシュできる
デメリット
- 実装が複雑で、接続管理や再接続処理が必要
- サーバー側で接続を維持するため、多数のクライアントがいる場合はリソース消費が大きい
- 一部のプロキシやファイアウォールで接続が遮断される可能性がある
- スケーリングが難しい(ロードバランサーの設定など)
適している場面と実装例
適している場面
- チャットアプリケーションやオンラインゲームなど、リアルタイム性が重要
- 双方向通信が必要な場合(クライアントからも頻繁にデータを送る)
- データの更新頻度が高く、常に最新の状態を保ちたい
基本的な実装例
// クライアント側(JavaScript)
const socket = new WebSocket('ws://localhost:3000');
// 接続が開いたとき
socket.addEventListener('open', (event) => {
console.log('WebSocket接続が確立しました');
socket.send('Hello Server!');
});
// メッセージを受信したとき
socket.addEventListener('message', (event) => {
console.log('サーバーからのメッセージ:', event.data);
updateUI(event.data);
});
// エラー発生時
socket.addEventListener('error', (error) => {
console.error('WebSocketエラー:', error);
});
// サーバー側(Node.js/ws)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', (ws) => {
console.log('クライアントが接続しました');
// クライアントからメッセージを受信
ws.on('message', (message) => {
console.log('受信:', message);
// 全クライアントにブロードキャスト
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
// 定期的にデータを送信
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ timestamp: Date.now() }));
}
}, 1000);
ws.on('close', () => {
clearInterval(interval);
console.log('クライアントが切断しました');
});
});
SSE(Server-Sent Events)
SSEとは
SSE(Server-Sent Events)は、サーバーからクライアントへ一方向にデータをストリーミングする技術です。HTTPプロトコルを使用しつつ、サーバーからのプッシュ通知を実現します。
仕組みと動作フロー
SSEは通常のHTTP接続を使用しますが、レスポンスを閉じずにストリームとして保持します。サーバーは必要に応じてイベントデータを送信し、クライアントはそれを受信し続けます。
メリットとデメリット
メリット
- 実装がWebSocketより簡単で、通常のHTTPを使用する
- 自動的に再接続する機能が標準で備わっている
- サーバーからのプッシュ通知に特化しており、用途がシンプル
- プロキシやファイアウォールとの互換性が高い
デメリット
- サーバーからクライアントへの一方向通信のみ(クライアントからはHTTPリクエストが必要)
- Internet Explorerではサポートされていない
- 同時接続数の制限がある(ブラウザごとに6〜8接続程度)
適している場面と実装例
適している場面
- サーバーからのプッシュ通知が主な用途(ニュースフィード、株価更新など)
- クライアントからの送信は少なく、主にサーバーからの情報を受け取る
- 実装をシンプルに保ちたいが、ポーリングより効率的にしたい
基本的な実装例
// クライアント側(JavaScript)
const eventSource = new EventSource('/api/events');
// メッセージを受信
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('受信データ:', data);
updateUI(data);
});
// カスタムイベントを受信
eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('更新イベント:', data);
});
// エラー処理
eventSource.addEventListener('error', (error) => {
console.error('SSEエラー:', error);
// 自動的に再接続を試みる
});
// 接続を閉じる
// eventSource.close();
// サーバー側(Node.js/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 interval = setInterval(() => {
const data = {
timestamp: Date.now(),
message: 'サーバーからの更新'
};
// デフォルトのmessageイベント
res.write(`data: ${JSON.stringify(data)}\n\n`);
// カスタムイベント
res.write(`event: update\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 2000);
// クライアントが切断したときの処理
req.on('close', () => {
clearInterval(interval);
console.log('クライアントが切断しました');
});
});
3つの通信方式の比較
比較表
| 項目 | ポーリング | WebSocket | SSE |
|---|---|---|---|
| 通信方向 | リクエスト/レスポンス | 双方向 | サーバー→クライアント |
| プロトコル | HTTP | WebSocket | HTTP |
| 接続 | 都度確立・切断 | 永続的 | 永続的 |
| リアルタイム性 | 低〜中 | 高 | 高 |
| 実装の複雑さ | 簡単 | やや複雑 | 簡単 |
| サーバー負荷 | 高い(頻繁なリクエスト) | 中(接続維持) | 中(接続維持) |
| ブラウザ対応 | すべて | ほぼすべて | IE以外 |
| 再接続 | 不要(毎回新規接続) | 手動実装が必要 | 自動 |
| ファイアウォール | 問題なし | 問題が起こる可能性 | 問題なし |
選択基準とユースケース
各通信方式を選択する基準を、具体的なユースケースとともに見ていきましょう。
ポーリングを選ぶ場合
- ダッシュボードの定期更新(1分ごとの売上データ更新など)
- メールの新着確認
- ジョブの実行状態確認
- シンプルな通知機能
WebSocketを選ぶ場合
- チャットアプリケーション
- オンラインゲーム
- 共同編集ツール(Google Docsのようなリアルタイム編集)
- リアルタイムコラボレーションツール
- トレーディングプラットフォーム(双方向の注文処理)
SSEを選ぶ場合
- ニュースフィードやSNSのタイムライン更新
- 株価や為替レートの配信
- サーバーログのストリーミング表示
- 進捗状況の通知(ファイルアップロードの進行状況など)
- 通知センター
パフォーマンスとコストの観点
サーバー負荷の観点
- ポーリング: クライアント数 × ポーリング頻度 分のリクエスト処理が必要
- WebSocket: 接続数分のメモリとCPUリソースを常時消費
- SSE: WebSocketと同様だが、一方向のため若干軽量
開発コストの観点
- ポーリング: 最も低い(既存のHTTP APIの延長)
- SSE: 低い(標準APIが使いやすい)
- WebSocket: 高い(接続管理、再接続、エラーハンドリングなど)
運用コストの観点
- ポーリング: スケールアウトが容易、ロードバランサーの設定が不要
- SSE: スケールアウトが比較的容易
- WebSocket: Sticky Sessionの設定やRedisなどの共有ストレージが必要になる場合がある
まとめ
この記事では、バックエンドAPIの3つの通信方式について解説しました。
それぞれの特徴
- ポーリング: シンプルで実装が容易だが、無駄なリクエストが発生する
- WebSocket: 双方向のリアルタイム通信が可能だが、実装とスケーリングが複雑
- SSE: サーバーからのプッシュに特化し、実装がシンプルで自動再接続機能を持つ
実際のプロジェクトでの選び方
- まず、リアルタイム性がどの程度必要かを見極める
- 通信が一方向か双方向かを確認する
- 開発リソースと運用コストを考慮する
- ブラウザの対応状況を確認する
多くの場合、最初はシンプルなポーリングから始めて、必要に応じてSSEやWebSocketに移行するアプローチが現実的です。過度に複雑な実装は避け、要件に合った最適な方式を選択しましょう。