WebSocket、SSE、HTTP ポーリング: リアルタイム通信の実践ガイド
最新の Web アプリケーションでは、リアルタイム通信の必要性がますます高まっています。 インスタント通知、チャット、ライブダッシュボード、価格更新、アクティビティフィード。 しかし、どのテクノロジーを選択すればよいのでしょうか?答えは特定のユースケースによって異なります。 間違ったアプローチは、スケーラビリティの問題や過度の遅延を引き起こす可能性があります または不必要なアーキテクチャの複雑さ。
このガイドでは、3 つの主要なコミュニケーション戦略を詳しく比較します。 ウェブ上でリアルタイム: HTTPポーリング, サーバー送信イベント (SSE) e Webソケット。それぞれについて、その仕組み、長所と短所を見ていきます。 そしてそれを具体的にどのように実装するか 角度のある e Node.js.
何を学ぶか
- HTTP ポーリング、ロング ポーリング、SSE、WebSocket がプロトコル レベルでどのように機能するか
- 具体的な指標を使った各アプローチの長所と短所
- 各テクノロジーのAngularとNode.jsによる実践実装
- 選択に役立つ詳細な比較表
- 適切なテクノロジーを選択するための意思決定の枠組み
- スケーラビリティ、ファイアウォール、インフラストラクチャに関する考慮事項
なぜリアルタイム通信が必要なのでしょうか?
古典的な HTTP プロトコルはモデルベースです リクエストとレスポンス: クライアントが要求し、サーバーが応答すると、接続が閉じます。このモデルは機能します 静的ページや REST API には適していますが、サーバーが必要な場合には不十分になります。 クライアントにプロンプトを表示せずに変更を通知します。
毎秒更新されるメトリクスを表示する必要がある監視ダッシュボードを想像してください。 または、メッセージを即座に表示する必要があるチャット。リクエストレスポンスモデルの場合 純粋に、クライアントは「何かニュースはありますか?」と絶えず尋ねるべきです。トラフィックを生成する 役に立たない、知覚される遅延。
HTTPポーリング
ポーリングは、疑似リアルタイム通信への最も単純かつ最も古いアプローチです。 次の 2 つのバリエーションがあります。 ショートポーリング e ロングポーリング.
ショートポーリング
クライアントは、定期的な間隔 (たとえば、5 秒ごと) で HTTP リクエストを送信します。 サーバーは、利用可能なデータまたは空の応答で即座に応答します。
// server Node.js - endpoint per short polling
import express from 'express';
const app = express();
let latestData = { temperature: 22.5, timestamp: Date.now() };
// Simula aggiornamento dati ogni 3 secondi
setInterval(() => {
latestData = {
temperature: 20 + Math.random() * 10,
timestamp: Date.now()
};
}, 3000);
app.get('/api/sensor', (req, res) => {
res.json(latestData);
});
app.listen(3000);
// Angular service - Short Polling con RxJS
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { interval, switchMap, retry, share } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SensorPollingService {
private http = inject(HttpClient);
// Polling ogni 5 secondi
sensorData$ = interval(5000).pipe(
switchMap(() => this.http.get<SensorData>('/api/sensor')),
retry({ count: 3, delay: 1000 }),
share() // condividi tra più subscriber
);
}
interface SensorData {
temperature: number;
timestamp: number;
}
ショートポーリングの長所と短所
- 長所: 実装が簡単、どこでも機能、特別な依存関係なし
- 長所: あらゆる CDN、プロキシ、ファイアウォールと互換性があります
- に対して: 新しいデータのないリクエストによる帯域幅の無駄
- に対して: ポーリング間隔と等しい最小遅延
- に対して: クライアントの数に比例したサーバーの負荷
ロングポーリング
ロング ポーリングは、接続が完了するまで接続を開いたままにすることで、ショート ポーリングよりも改善されます。 サーバーには送信する新しいデータがあります。クライアントはリクエストを送信し、サーバーはリクエストを「保留」します 更新がある場合 (またはタイムアウトが経過した場合) にのみ応答します。彼は答えを受け取るとすぐに、 クライアントはすぐに新しい接続を開きます。
// Server Node.js - Long Polling
import express from 'express';
import { EventEmitter } from 'events';
const app = express();
const dataEmitter = new EventEmitter();
let lastUpdate = { value: 0, timestamp: Date.now() };
// Simula aggiornamenti casuali
setInterval(() => {
lastUpdate = { value: Math.random() * 100, timestamp: Date.now() };
dataEmitter.emit('update', lastUpdate);
}, Math.random() * 5000 + 2000);
app.get('/api/long-poll', (req, res) => {
const clientTimestamp = parseInt(req.query.since as string) || 0;
// Se ci sono già dati più recenti, rispondi subito
if (lastUpdate.timestamp > clientTimestamp) {
return res.json(lastUpdate);
}
// Altrimenti attendi un aggiornamento o timeout
const timeout = setTimeout(() => {
dataEmitter.removeListener('update', onUpdate);
res.status(204).end(); // Nessun aggiornamento
}, 30000);
const onUpdate = (data: any) => {
clearTimeout(timeout);
res.json(data);
};
dataEmitter.once('update', onUpdate);
// Pulisci se il client si disconnette
req.on('close', () => {
clearTimeout(timeout);
dataEmitter.removeListener('update', onUpdate);
});
});
app.listen(3000);
// Angular service - Long Polling con RxJS
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, switchMap, tap, retry, delay, EMPTY } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class LongPollingService {
private http = inject(HttpClient);
private lastTimestamp = 0;
private data$ = new Subject<any>();
connect(): Observable<any> {
this.poll();
return this.data$.asObservable();
}
private poll(): void {
this.http.get(`/api/long-poll?since=${this.lastTimestamp}`).pipe(
retry({ count: 5, delay: 2000 })
).subscribe({
next: (data: any) => {
if (data) {
this.lastTimestamp = data.timestamp;
this.data$.next(data);
}
// Riconnetti immediatamente per il prossimo aggiornamento
this.poll();
},
error: (err) => {
console.error('Long polling error:', err);
// Riprova dopo un ritardo
setTimeout(() => this.poll(), 5000);
}
});
}
}
サーバー送信イベント (SSE)
SSE は、サーバーがイベントの連続ストリームを送信できるようにする W3C 標準です。 永続的な HTTP 接続を介してクライアントに送信されます。ポーリングとは異なり、接続する 開いたままになり、サーバーはいつでも必要なときにデータを「プッシュ」できます。 WebSocket とは異なり、 SSEは 一方向 (サーバーからクライアントのみ) 標準の HTTP を使用します。
SSE の仕組み
クライアントはヘッダーを使用して HTTP 接続を開きます。 Accept: text/event-stream。
サーバーは次のように応答します Content-Type: text/event-stream そして接続を維持します
オープン、SSE 形式でデータを送信: 各イベントは次のようなフィールドで構成されます。
data:, event:, id: e retry:.
// Server Node.js - SSE endpoint
import express from 'express';
const app = express();
app.get('/api/events', (req, res) => {
// Header SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
// Invia un commento di keep-alive ogni 15 secondi
const keepAlive = setInterval(() => {
res.write(': keep-alive\n\n');
}, 15000);
// Invia dati reali
const sendData = setInterval(() => {
const data = {
cpu: Math.random() * 100,
memory: Math.random() * 100,
timestamp: new Date().toISOString()
};
// Formato SSE: event, id, data
res.write(`event: metrics\n`);
res.write(`id: ${Date.now()}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// Pulizia alla disconnessione del client
req.on('close', () => {
clearInterval(keepAlive);
clearInterval(sendData);
res.end();
});
});
app.listen(3000);
Angular を使用した SSE
ネイティブAPI EventSource ブラウザが自動的に再接続を処理します
受信した最後のイベントの追跡。 Angular では EventSource をラップできます
Observable で RxJS エコシステムと統合します。
import { Injectable, NgZone, inject } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SseService {
private zone = inject(NgZone);
connect(url: string): Observable<MessageEvent> {
return new Observable(observer => {
const eventSource = new EventSource(url);
// Ascolta eventi con nome specifico
eventSource.addEventListener('metrics', (event) => {
// Esegui dentro NgZone per trigger change detection
this.zone.run(() => {
observer.next(event as MessageEvent);
});
});
eventSource.onerror = (error) => {
this.zone.run(() => {
// EventSource si riconnette automaticamente
// ma possiamo loggare l'errore
console.warn('SSE connection error, reconnecting...', error);
});
};
// Cleanup: chiudi la connessione
return () => {
eventSource.close();
};
});
}
}
// Uso nel componente
@Component({
selector: 'app-dashboard',
template: `
<div class="metrics-grid">
<div class="metric-card">
<h3>CPU</h3>
<span>{{ metrics?.cpu | number:'1.1-1' }}%</span>
</div>
<div class="metric-card">
<h3>Memoria</h3>
<span>{{ metrics?.memory | number:'1.1-1' }}%</span>
</div>
</div>
`
})
export class DashboardComponent implements OnInit, OnDestroy {
private sseService = inject(SseService);
private subscription?: Subscription;
metrics: any;
ngOnInit(): void {
this.subscription = this.sseService
.connect('/api/events')
.subscribe(event => {
this.metrics = JSON.parse(event.data);
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
SSE の長所と短所
- 長所: ブラウザによって管理される自動再接続
- 長所: 標準 HTTP を使用し、プロキシおよび CDN と互換性があります
- 長所: 最後のイベント (Last-Event-ID) の自動追跡
- 長所: サーバー側の実装が簡単
- に対して: 一方向のみ (サーバーからクライアント)
- に対して: HTTP/1.1 でのドメインあたりの接続数の制限は 6 つです
- に対して: テキスト形式のみ (ネイティブバイナリなし)
Webソケット
WebSocketは通信プロトコルです 全二重 で動作する 単一の TCP 接続。 HTTP とは異なり、最初のハンドシェイクの後、クライアントとサーバーは オーバーヘッドなしでいつでも、どの方向にでもメッセージを相互に送信できます。 各メッセージの HTTP ヘッダーの数。
ハンドシェイクの仕組み
WebSocket 接続は、HTTP の「アップグレード」リクエストで始まります。クライアントが送信する
ヘッダー付きの GET リクエスト Upgrade: websocket e
Connection: Upgrade。サーバーが受け入れると、ステータスが返されます。
101 Switching Protocols その瞬間から、接続は WebSocket プロトコルに切り替わります。
// Server Node.js - WebSocket con ws
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
import express from 'express';
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// Struttura per le stanze
const rooms = new Map<string, Set<WebSocket>>();
wss.on('connection', (ws: WebSocket) => {
console.log('Nuovo client connesso');
let currentRoom = 'general';
// Aggiungi alla stanza default
joinRoom(ws, currentRoom);
ws.on('message', (raw: Buffer) => {
try {
const message = JSON.parse(raw.toString());
switch (message.type) {
case 'join':
leaveRoom(ws, currentRoom);
currentRoom = message.room;
joinRoom(ws, currentRoom);
ws.send(JSON.stringify({
type: 'system',
text: `Sei entrato nella stanza ${currentRoom}`
}));
break;
case 'chat':
broadcast(currentRoom, {
type: 'chat',
user: message.user,
text: message.text,
timestamp: Date.now()
}, ws);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
}
} catch (err) {
ws.send(JSON.stringify({
type: 'error',
text: 'Formato messaggio non valido'
}));
}
});
ws.on('close', () => {
leaveRoom(ws, currentRoom);
});
});
function joinRoom(ws: WebSocket, room: string): void {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room)!.add(ws);
}
function leaveRoom(ws: WebSocket, room: string): void {
rooms.get(room)?.delete(ws);
if (rooms.get(room)?.size === 0) rooms.delete(room);
}
function broadcast(room: string, data: any, exclude?: WebSocket): void {
const clients = rooms.get(room);
if (!clients) return;
const msg = JSON.stringify(data);
clients.forEach(client => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
}
server.listen(3000);
Angular と RxJS を使用した WebSocket
RxJS には演算子が含まれています webSocket 自動的に管理してくれる
WebSocket 接続を確立し、それを双方向のサブジェクトに変換します。
import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { Observable, Subject, timer, retry, tap, switchMap, EMPTY } from 'rxjs';
export interface ChatMessage {
type: 'chat' | 'system' | 'error' | 'join' | 'ping' | 'pong';
user?: string;
text?: string;
room?: string;
timestamp?: number;
}
@Injectable({ providedIn: 'root' })
export class WebSocketService {
private socket$?: WebSocketSubject<ChatMessage>;
private reconnect$ = new Subject<void>();
connect(url: string): Observable<ChatMessage> {
if (!this.socket$ || this.socket$.closed) {
this.socket$ = webSocket<ChatMessage>({
url,
openObserver: {
next: () => console.log('WebSocket connesso')
},
closeObserver: {
next: () => {
console.log('WebSocket disconnesso');
this.socket$ = undefined;
this.reconnect$.next();
}
}
});
}
return this.socket$.pipe(
retry({
count: 10,
delay: (error, retryCount) => {
// Backoff esponenziale: 1s, 2s, 4s, 8s, max 30s
const delayMs = Math.min(1000 * Math.pow(2, retryCount), 30000);
console.log(`Riconnessione in ${delayMs}ms...`);
return timer(delayMs);
}
})
);
}
send(message: ChatMessage): void {
this.socket$?.next(message);
}
joinRoom(room: string): void {
this.send({ type: 'join', room });
}
sendChat(user: string, text: string): void {
this.send({ type: 'chat', user, text });
}
disconnect(): void {
this.socket$?.complete();
this.socket$ = undefined;
}
}
// Componente Chat Angular
@Component({
selector: 'app-chat',
template: `
<div class="chat-container">
<div class="messages" #messageList>
<div
*ngFor="let msg of messages"
[class]="'message ' + msg.type">
<strong *ngIf="msg.user">{{ msg.user }}:</strong>
{{ msg.text }}
</div>
</div>
<form (submit)="sendMessage($event)">
<input
[(ngModel)]="newMessage"
name="message"
placeholder="Scrivi un messaggio..."
autocomplete="off" />
<button type="submit">Invia</button>
</form>
</div>
`
})
export class ChatComponent implements OnInit, OnDestroy {
private wsService = inject(WebSocketService);
private subscription?: Subscription;
messages: ChatMessage[] = [];
newMessage = '';
username = 'Utente_' + Math.floor(Math.random() * 1000);
ngOnInit(): void {
this.subscription = this.wsService
.connect('ws://localhost:3000')
.subscribe(msg => {
this.messages.push(msg);
});
this.wsService.joinRoom('general');
}
sendMessage(event: Event): void {
event.preventDefault();
if (!this.newMessage.trim()) return;
this.wsService.sendChat(this.username, this.newMessage);
this.messages.push({
type: 'chat',
user: this.username,
text: this.newMessage,
timestamp: Date.now()
});
this.newMessage = '';
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.wsService.disconnect();
}
}
WebSocket の長所と短所
- 長所: 双方向: クライアントとサーバーはいつでもメッセージを送信します
- 長所: 低遅延: 各メッセージの HTTP オーバーヘッドなし
- 長所: ネイティブ バイナリ データ サポート (ArrayBuffer、Blob)
- 長所: ドメインごとの接続制限なし
- に対して: スケーリングがより複雑になる (接続状態)
- に対して: 自動再接続なし (実装する必要があります)
- に対して: 一部の企業プロキシとファイアウォールは WebSocket をブロックします
- に対して: キャッシュ不可、標準 CDN では機能しない
詳細比較表
以下は、複数の側面にわたる 3 つのテクノロジーの直接比較です。
| 特性 | ショートポーリング | ロングポーリング | SSE | Webソケット |
|---|---|---|---|---|
| 方向 | クライアント → サーバー | クライアント → サーバー | サーバー → クライアント | 双方向 |
| プロトコル | HTTP | HTTP | HTTP | WS (TCP 経由) |
| レイテンシ | 高 (ポーリング間隔) | 平均 | 低い | 非常に低い |
| メッセージあたりのオーバーヘッド | 高 (HTTP ヘッダー) | 高い | ベース | 非常に低い (2 ~ 14 バイト) |
| 再接続 | マニュアル | マニュアル | 自動(ブラウザ) | マニュアル |
| バイナリデータ | エンコーディング経由 | エンコーディング経由 | いいえ (テキストのみ) | ネイティブ |
| ドメインによる接続 | 共有 | ストリームごとに 1 つ | 最大 6 (HTTP/1.1) | 制限なし |
| プロキシ/ファイアウォール | 問題ない | まれな問題 | 概ねOK | 考えられるブロック |
| スケーラビリティ | 良好 (ステートレス) | 平均 | 良い | 注意が必要です |
| 複雑 | 非常に低い | 低い | 低い | 中~高 |
| ブラウザのサポート | ユニバーサル | ユニバーサル | すべてモダン | すべてモダン |
意思決定の枠組み: どれを選択するか?
「絶対に最適な」テクノロジーなどというものは存在しません。選択はユースケースによって異なります。 ここでは実際的な意思決定の枠組みを示します。
次の場合にショート ポーリングを選択します。
- データはほとんど変更されません (30 秒以上ごと)
- 遅延は重要ではありません
- 実装を最大限に簡素化したい
- インフラストラクチャが制限されている(共有ホスティング)
- 例: 空席状況、注文状況の確認
次の場合にロングポーリングを選択します。
- インフラストラクチャを変更せずに低遅延が必要な場合
- 更新は不定期ですが、タイムリーにする必要があります
- 技術的な制約のため、SSE または WebSocket は使用できません
- 例: 少量の通知システム
次の場合に SSE を選択します。
- フローは主にサーバーからクライアントです
- 自動再接続をブラウザで管理したい
- プロキシと CDN の互換性が必要です
- データ形式はテキスト形式(JSON、テキスト)です。
- 例: ライブダッシュボード、ニュースフィード、通知、ログストリーミング
次の場合に WebSocket を選択します。
- 真の双方向コミュニケーションが必要です
- 遅延は最小限にする必要があります (50 ミリ秒未満)
- バイナリデータ(音声、ビデオ、ファイル)を送信する必要があります
- メッセージ量が多い (1 秒あたり数十件)
- 例: リアルタイム チャット、マルチプレイヤー ゲーム、共同編集、取引
黄金律
要件を満たす最も単純なソリューションから始めてください。ポーリングが機能する場合 ユースケースでは、WebSocket を使用してアーキテクチャを複雑にしないでください。いつでも進化できる 後で。 多くの場合、SSE が最良の妥協案です: HTTP のようにシンプル、 ただし、リアルタイムプッシュと自動再接続が可能です。
スケーラビリティに関する考慮事項
永続的な接続 (SSE および WebSocket) のスケーリングには別の注意が必要です ステートレス REST API と比較します。
ロードバランシング
永続的な接続では、ロード バランサは自由に分散できません リクエスト。便利です スティッキーセッション またはメッセージング層 (Redis Pub/Sub、NATS、Kafka) を使用して、サーバー インスタンス間でメッセージを同期します。
// Scalare WebSocket con Redis Pub/Sub
import { createClient } from 'redis';
import { WebSocketServer } from 'ws';
const publisher = createClient();
const subscriber = createClient();
await publisher.connect();
await subscriber.connect();
const wss = new WebSocketServer({ port: 3000 });
// Ogni istanza si iscrive al canale Redis
await subscriber.subscribe('chat:general', (message) => {
// Propaga a tutti i client connessi a questa istanza
wss.clients.forEach(client => {
if (client.readyState === 1) {
client.send(message);
}
});
});
wss.on('connection', (ws) => {
ws.on('message', async (data) => {
// Pubblica su Redis per raggiungere tutti i server
await publisher.publish('chat:general', data.toString());
});
});
接続制限
単一の Node.js サーバーで数万の WebSocket 接続を処理できます 同時に接続できますが、接続ごとにメモリが消費されます。使用状況を注意深く監視する メモリの容量を変更し、適切な制限を構成します。 HTTP/1.1 の SSE の制限 HTTP/2 を使用すると、ブラウザ ドメインごとに 6 つの接続をバイパスできます (接続を多重化します)。
ハートビートとタイムアウト
永続的な接続は、プロキシ、NAT によってサイレントに閉じることができます。 またはファイアウォール。必ず仕組みを実装する 心拍数 (ピンポン) により、切断された接続が検出され、すぐに再接続されます。
// Heartbeat per WebSocket
const HEARTBEAT_INTERVAL = 30000; // 30 secondi
wss.on('connection', (ws) => {
let isAlive = true;
ws.on('pong', () => {
isAlive = true;
});
const heartbeat = setInterval(() => {
if (!isAlive) {
console.log('Client non risponde, disconnetto');
return ws.terminate();
}
isAlive = false;
ws.ping();
}, HEARTBEAT_INTERVAL);
ws.on('close', () => {
clearInterval(heartbeat);
});
});
結論
HTTP ポーリング、サーバー送信イベント、WebSocket のいずれを選択するかは問題ではありません。 これは「最良」ですが、特定の問題にはどれが最も適していますか。 ポーリングは単純なシナリオでは引き続き有効です。 SSE は優れたバランスを提供します 単方向フローのシンプルさとリアルタイム能力の間で。 WebSocket は 低遅延の双方向通信には当然の選択です。
Angular では、3 つのテクノロジーすべてが RxJS と自然に統合され、 レスポンシブで構成可能なコードを作成します。重要なのはユースケースから始めることです。 インフラストラクチャの制約を評価し、最も単純なソリューションを選択する 要件を満たしていること。
重要なポイント
- ショートポーリング: シンプルだが非効率で、めったに変更されないデータに適している
- ロングポーリング: 短いポーリングよりも優れた遅延、優れたユニバーサル フォールバック
- SSE: 自動再接続を備えたサーバーからクライアントへのプッシュ、ダッシュボードと通知に最適
- Webソケット: 低遅延の全二重、チャット、ゲーム、コラボレーションに必要
- スケーラビリティ: 永続的な接続には、マルチインスタンス用の Redis Pub/Sub または同様のものが必要です
- RxJS: すべてのテクノロジーは Observable および RxJS オペレーターとシームレスに統合されます
- 経験則: 最も単純なソリューションから始めて、必要な場合にのみ拡張します







