WebSockets vs SSE vs HTTP Polling: Praktický průvodce komunikací v reálném čase
Moderní webové aplikace stále více vyžadují komunikaci v reálném čase: okamžitá upozornění, chat, živý panel, aktualizace cen, zdroj aktivity. Ale jakou technologii zvolit? Odpověď závisí na konkrétním případu použití a vyberte si špatný přístup může znamenat problémy se škálovatelností, nadměrnou latenci nebo zbytečné architektonické složitosti.
V této příručce do hloubky porovnáme tři hlavní komunikační strategie v reálném čase na webu: HTTP Polling, Události odeslané serverem (SSE) e WebSockets. U každého uvidíme, jak to funguje, výhody a nevýhody, a jak jej konkrétně realizovat pomocí Hranatý e Node.js.
Co se naučíte
- Jak HTTP Polling, Long Polling, SSE a WebSockets fungují na úrovni protokolu
- Klady a zápory každého přístupu s konkrétními metrikami
- Praktická implementace s Angular a Node.js pro každou technologii
- Podrobná srovnávací tabulka vám pomůže při výběru
- Rozhodovací rámec pro výběr správné technologie
- Škálovatelnost, firewall a aspekty infrastruktury
Proč je potřeba komunikace v reálném čase?
Klasický protokol HTTP je založen na modelu žádost-odpověď: klient se zeptá, server odpoví, spojení se uzavře. Tento model funguje v pořádku pro statické stránky a REST API, ale stane se nedostatečným, když server musí upozornit klienta na změnu bez vyzvání.
Představte si monitorovací panel, který potřebuje zobrazovat metriky aktualizované každou sekundu, nebo chat, kde se zprávy musí objevit okamžitě. S modelem žádost-odpověď čistý, klient by se měl neustále ptát "jsou nějaké novinky?" generování provozu zbytečná a vnímaná latence.
HTTP Polling
Dotazování je nejjednodušší a nejstarší přístup ke komunikaci v pseudo-reálném čase. Existují dvě varianty: Krátké hlasování e Dlouhé hlasování.
Krátké hlasování
Klient odesílá HTTP požadavky v pravidelných intervalech (například každých 5 sekund). Server odpoví okamžitě dostupnými daty nebo prázdnou odpovědí.
// 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;
}
Výhody a nevýhody krátkého hlasování
- Pro: Jednoduchá implementace, funguje všude, žádné zvláštní závislosti
- Pro: Kompatibilní s jakýmkoliv CDN, proxy a firewallem
- Proti: Plýtvání šířkou pásma pro požadavky bez nových dat
- Proti: Minimální latence rovna intervalu dotazování
- Proti: Zatížení serveru úměrně počtu klientů
Dlouhé hlasování
Dlouhé dotazování vylepšuje krátké dotazování tím, že udržuje připojení otevřené do server má nová data k odeslání. Klient odešle požadavek, server požadavek "podrží". a odpoví pouze tehdy, když dojde k aktualizaci (nebo vyprší časový limit). Jakmile dostane odpověď, klient okamžitě otevře nové připojení.
// 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);
}
});
}
}
Události odeslané serverem (SSE)
SSE je standard W3C, který umožňuje serveru odesílat nepřetržitý proud událostí ke klientovi prostřednictvím trvalého připojení HTTP. Na rozdíl od dotazování, připojení zůstává otevřená a server může „tlačit“ data, kdykoli chce. Na rozdíl od WebSockets, SSE je jednosměrný (pouze server-klient) a používá standardní HTTP.
Jak SSE funguje
Klient otevře HTTP připojení s hlavičkou Accept: text/event-stream.
Server odpoví s Content-Type: text/event-stream a udržuje spojení
open, odesílání dat ve formátu SSE: každá událost je složena z polí jako např
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);
SSE s Angular
Nativní API EventSource prohlížeč automaticky zpracovává opětovné připojení
a sledování poslední přijaté události. V Angular můžeme zabalit EventSource
v Observable pro integraci s ekosystémem 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();
}
}
Klady a zápory SSE
- Pro: Automatické opětovné připojení spravované prohlížečem
- Pro: Používá standardní HTTP, kompatibilní s proxy a CDN
- Pro: Automatické sledování poslední události (Last-Event-ID)
- Pro: Jednoduchá implementace na straně serveru
- Proti: Pouze jednosměrný (od serveru ke klientovi)
- Proti: Limit 6 připojení na doménu v HTTP/1.1
- Proti: Pouze textový formát (žádný nativní binární kód)
WebSockets
WebSocket je komunikační protokol plně duplexní která působí na jediné TCP spojení. Na rozdíl od HTTP po počátečním handshake klient a server mohou si navzájem posílat zprávy v kteroukoli dobu a směrem, bez režie HTTP hlaviček pro každou zprávu.
Jak funguje podání ruky
Připojení WebSocket začíná požadavkem HTTP na "upgrade". Klient odešle
požadavek GET s hlavičkami Upgrade: websocket e
Connection: Upgrade. Pokud server přijme, odpoví stavem
101 Switching Protocols a od té chvíle se připojení přepne na protokol 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);
WebSocket s Angular a RxJS
RxJS obsahuje operátora webSocket který se řídí automaticky
připojení WebSocket a změní jej na obousměrný Předmět.
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();
}
}
Výhody a nevýhody WebSockets
- Pro: Obousměrné: Klient a server posílají zprávy kdykoli
- Pro: Nízká latence: Žádná režie HTTP pro každou zprávu
- Pro: Nativní podpora binárních dat (ArrayBuffer, Blob)
- Pro: Žádné omezení připojení na doménu
- Proti: Složitější na měřítko (stav připojení)
- Proti: Žádné automatické opětovné připojení (musí být implementováno)
- Proti: Některé podnikové servery proxy a firewally blokují WebSockets
- Proti: Nelze uložit do mezipaměti, nefunguje se standardním CDN
Podrobná srovnávací tabulka
Zde je přímé srovnání těchto tří technologií napříč více dimenzemi:
| Charakteristický | Krátké hlasování | Dlouhé hlasování | SSE | WebSockets |
|---|---|---|---|---|
| Směr | Klient → Server | Klient → Server | Server → Klient | Obousměrný |
| Protokol | HTTP | HTTP | HTTP | WS (přes TCP) |
| Latence | Vysoká (interval dotazování) | Průměrný | Nízký | Velmi nízké |
| Režie na zprávu | Vysoká (hlavička HTTP) | Vysoký | Bas | Velmi nízká (2–14 bajtů) |
| Opětovné připojení | Manuál | Manuál | Automaticky (prohlížeč) | Manuál |
| Binární data | Prostřednictvím kódování | Prostřednictvím kódování | Ne (pouze text) | Rodák |
| Připojení podle domény | Sdíleno | 1 za stream | Max 6 (HTTP/1.1) | Žádné limity |
| Proxy/Firewall | Žádný problém | Vzácné problémy | Obecně OK | Možné bloky |
| Škálovatelnost | Dobrý (bez státní příslušnosti) | Průměrný | Dobrý | Vyžaduje to pozornost |
| Složitost | Velmi nízké | Nízký | Nízký | Středně vysoká |
| Podpora prohlížeče | Univerzální | Univerzální | Vše moderní | Vše moderní |
Rámec rozhodování: Který si vybrat?
Nic jako „absolutně nejlepší“ technologie neexistuje. Výběr závisí na případu použití. Zde je praktický rámec rozhodování.
Vyberte Krátké hlasování, když:
- Data se mění zřídka (každých 30 a více sekund)
- Latence není kritická
- Chcete maximální jednoduchost implementace
- Infrastruktura je omezená (sdílený hosting)
- Příklad: zkontrolovat dostupnost míst, stav objednávky
Zvolte Dlouhé hlasování, když:
- Potřebujete nízkou latenci bez změny infrastruktury
- Aktualizace jsou sporadické, ale musí být včasné
- SSE nebo WebSocket nemůžete používat kvůli technickým omezením
- Příklad: oznamovací systém s nízkými hlasitostmi
Vyberte SSE, když:
- Tok probíhá primárně mezi servery a klienty
- Chcete automatické opětovné připojení spravované prohlížečem
- Potřebujete kompatibilitu proxy a CDN
- Formát dat je textový (JSON, text)
- Příklad: živý řídicí panel, zpravodajský kanál, upozornění, streamování protokolu
Vyberte WebSocket, když:
- Potřebujete skutečnou obousměrnou komunikaci
- Latence musí být minimální (<50 ms)
- Potřebujete přenášet binární data (audio, video, soubory)
- Hlasitost zpráv je vysoká (desítky za sekundu)
- Příklad: chat v reálném čase, hry pro více hráčů, společné úpravy, obchodování
Zlaté pravidlo
Začněte s nejjednodušším řešením, které splní vaše požadavky. Pokud hlasování funguje pro váš případ použití nekomplikujte architekturu pomocí WebSocket. Vždy se můžete vyvíjet později. SSE je často nejlepší kompromis: jednoduché jako HTTP, ale s push v reálném čase a automatickým opětovným připojením.
Úvahy o škálovatelnosti
Škálování trvalých připojení (SSE a WebSocket) vyžaduje jinou pozornost ve srovnání s bezstavovými REST API.
Vyrovnávání zátěže
S trvalými připojeními se nástroj pro vyrovnávání zatížení nemůže volně distribuovat žádosti. Je to užitečné lepkavé relace nebo vrstva zpráv (Redis Pub/Sub, NATS, Kafka) pro synchronizaci zpráv mezi instancemi serveru.
// 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());
});
});
Limity připojení
Jediný server Node.js zvládne desítky tisíc připojení WebSocket současně, ale každé připojení spotřebovává paměť. Pečlivě sledujte používání paměti a nakonfigurujte příslušné limity. Pro SSE v HTTP/1.1 je limit 6 připojení na doménu prohlížeče lze obejít pomocí HTTP/2 (který multiplexuje spojení).
Tlukot srdce a časový limit
Trvalá připojení mohou být tiše uzavřena pomocí proxy, NAT nebo firewall. Vždy implementujte mechanismus tlukot srdce (ping/pong) pro detekci mrtvých spojení a okamžité opětovné připojení.
// 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);
});
});
Závěr
Volba mezi HTTP Polling, Server-Sent Events a WebSocket není otázkou který je "nejlepší", ale který je pro konkrétní problém nejvhodnější. Dotazování zůstává platné pro jednoduché scénáře. SSE nabízí vynikající rovnováhu mezi jednoduchostí a kapacitou v reálném čase pro jednosměrné toky. WebSocket je jasná volba pro obousměrnou komunikaci s nízkou latencí.
V Angular se všechny tři technologie přirozeně integrují s RxJS, což umožňuje psát citlivý a složitelný kód. Klíčem je začít případem použití, vyhodnotit infrastrukturní omezení a zvolit nejjednodušší řešení který splňuje požadavky.
Klíčové body
- Krátké hlasování: jednoduché, ale neefektivní, vhodné pro data, která se jen zřídka mění
- Dlouhé hlasování: lepší latence než krátké dotazování, dobrý univerzální záložní zdroj
- SSE: push server-to-client s automatickým opětovným připojením, ideální pro řídicí panely a oznámení
- WebSockets: Full-duplex s nízkou latencí, nezbytný pro chatování, hraní her a spolupráci
- Škálovatelnost: trvalá připojení vyžadují Redis Pub/Sub nebo podobný pro více instancí
- RxJS: všechny technologie se hladce integrují s operátory Observable a RxJS
- Základní pravidlo: Začněte s nejjednodušším řešením a škálujte pouze v případě potřeby







