WebSockets vs SSE vs HTTP Polling: Praktyczny przewodnik po komunikacji w czasie rzeczywistym
Nowoczesne aplikacje internetowe coraz częściej wymagają komunikacji w czasie rzeczywistym: natychmiastowe powiadomienia, czat, pulpit nawigacyjny na żywo, aktualizacje cen, kanał aktywności. Ale jaką technologię wybrać? Odpowiedź zależy od konkretnego przypadku użycia i wybierz niewłaściwe podejście może oznaczać problemy ze skalowalnością, nadmierne opóźnienia lub niepotrzebną złożoność architektoniczną.
W tym przewodniku szczegółowo porównamy trzy główne strategie komunikacji w czasie rzeczywistym w Internecie: Odpytywanie HTTP, Zdarzenia wysyłane przez serwer (SSE) e WebSockety. Dla każdego zobaczymy, jak to działa, zalety i wady, i jak to konkretnie wdrożyć za pomocą Kątowy e Node.js.
Czego się dowiesz
- Jak HTTP Polling, Long Polling, SSE i WebSockets działają na poziomie protokołu
- Plusy i minusy każdego podejścia z konkretnymi wskaźnikami
- Praktyczne wdrożenie z Angularem i Node.js dla każdej technologii
- Szczegółowa tabela porównawcza ułatwiająca dokonanie wyboru
- Ramy decyzyjne dotyczące wyboru właściwej technologii
- Kwestie dotyczące skalowalności, zapory sieciowej i infrastruktury
Dlaczego potrzebna jest komunikacja w czasie rzeczywistym?
Klasyczny protokół HTTP opiera się na modelu żądanie-odpowiedź: klient pyta, serwer odpowiada, połączenie zostaje zamknięte. Ten model działa dobry w przypadku stron statycznych i interfejsów API REST, ale staje się niewystarczający, gdy serwer musi powiadomić klienta o zmianie bez pytania.
Wyobraź sobie pulpit monitorujący, który musi wyświetlać metryki aktualizowane co sekundę, lub czat, na którym wiadomości muszą pojawiać się natychmiast. Z modelem żądanie-odpowiedź pure, klient powinien stale pytać „czy są jakieś nowości?” generując ruch bezużyteczne i postrzegane opóźnienie.
Odpytywanie HTTP
Polling to najprostsza i najstarsza metoda komunikacji w czasie pseudorzeczywistym. Istnieją dwa warianty: Krótka ankieta e Długie głosowanie.
Krótka ankieta
Klient wysyła żądania HTTP w regularnych odstępach czasu (na przykład co 5 sekund). Serwer odpowiada natychmiast, podając dostępne dane lub pustą odpowiedź.
// 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;
}
Plusy i minusy krótkiej ankiety
- Plusy: Prosty w implementacji, działa wszędzie, bez specjalnych zależności
- Plusy: Kompatybilny z dowolnym CDN, proxy i zaporą ogniową
- Przeciwko: Marnowanie przepustowości w przypadku żądań bez nowych danych
- Przeciwko: Minimalne opóźnienie równe interwałowi odpytywania
- Przeciwko: Obciążenie serwera proporcjonalne do liczby klientów
Długie głosowanie
Długie odpytywanie poprawia działanie krótkiego odpytywania, utrzymując połączenie otwarte do serwer ma nowe dane do wysłania. Klient wysyła żądanie, serwer „wstrzymuje” żądanie i odpowiada tylko wtedy, gdy nastąpi aktualizacja (lub upłynie limit czasu). Gdy tylko otrzyma odpowiedź, klient natychmiast otwiera nowe połączenie.
// 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);
}
});
}
}
Zdarzenia wysyłane przez serwer (SSE)
SSE to standard W3C, który umożliwia serwerowi wysyłanie ciągłego strumienia zdarzeń do klienta poprzez stałe połączenie HTTP. W przeciwieństwie do odpytywania, łączenie pozostaje otwarty, a serwer może „wypychać” dane, kiedy tylko chce. W przeciwieństwie do WebSocket, SSE jest jednokierunkowy (tylko serwer do klienta) i wykorzystuje standardowy protokół HTTP.
Jak działa SSE
Klient otwiera połączenie HTTP z nagłówkiem Accept: text/event-stream.
Serwer odpowiada Content-Type: text/event-stream i utrzymuje połączenie
open, wysyłanie danych w formacie SSE: każde zdarzenie składa się z pól takich jak
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 z Angularem
Natywne API EventSource przeglądarka automatycznie obsługuje ponowne połączenie
i śledzenie ostatniego otrzymanego zdarzenia. W Angularze możemy zawinąć EventSource
w Observable do integracji z ekosystemem 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();
}
}
Plusy i minusy SSE
- Plusy: Automatyczne ponowne połączenie zarządzane przez przeglądarkę
- Plusy: Używa standardowego protokołu HTTP, kompatybilnego z serwerami proxy i sieciami CDN
- Plusy: Automatyczne śledzenie ostatniego zdarzenia (Last-Event-ID)
- Plusy: Proste do wdrożenia po stronie serwera
- Przeciwko: Tylko w jedną stronę (serwer do klienta)
- Przeciwko: Limit 6 połączeń na domenę w HTTP/1.1
- Przeciwko: Tylko format tekstowy (bez natywnego pliku binarnego)
WebSockety
WebSocket to protokół komunikacyjny pełny dupleks na którym działa pojedynczego połączenia TCP. W przeciwieństwie do protokołu HTTP, po wstępnym uzgadnianiu klient i serwer mogą wysyłać sobie wiadomości w dowolnym czasie i kierunku, bez dodatkowych kosztów nagłówków HTTP dla każdej wiadomości.
Jak działa uścisk dłoni
Połączenie WebSocket rozpoczyna się od żądania „uaktualnienia” HTTP. Klient wysyła
żądanie GET z nagłówkami Upgrade: websocket e
Connection: Upgrade. Jeśli serwer zaakceptuje, odpowiada statusem
101 Switching Protocols i od tego momentu połączenie przełącza się na protokół 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 z Angularem i RxJS
RxJS zawiera operatora webSocket który zarządza automatycznie
połączenie WebSocket i zamienia je w podmiot dwukierunkowy.
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();
}
}
Plusy i minusy WebSocketów
- Plusy: Dwukierunkowy: klient i serwer wysyłają wiadomości w dowolnym momencie
- Plusy: Niskie opóźnienia: brak narzutu HTTP dla każdej wiadomości
- Plusy: Natywna obsługa danych binarnych (ArrayBuffer, Blob)
- Plusy: Brak limitów połączeń na domenę
- Przeciwko: Bardziej złożone skalowanie (stan połączenia)
- Przeciwko: Brak automatycznego ponownego połączenia (należy wdrożyć)
- Przeciwko: Niektóre korporacyjne serwery proxy i zapory sieciowe blokują WebSockety
- Przeciwko: Nie można buforować, nie działa ze standardową siecią CDN
Szczegółowa tabela porównawcza
Oto bezpośrednie porównanie trzech technologii w wielu wymiarach:
| Charakterystyczny | Krótka ankieta | Długie głosowanie | SSE | WebSockety |
|---|---|---|---|---|
| Kierunek | Klient → Serwer | Klient → Serwer | Serwer → Klient | Dwukierunkowy |
| Protokół | HTTP | HTTP | HTTP | WS (przez TCP) |
| Utajenie | Wysoki (interwał odpytywania) | Przeciętny | Niski | Bardzo niski |
| Narzut na wiadomość | Wysoki (nagłówek HTTP) | Wysoki | Bas | Bardzo niski (2-14 bajtów) |
| Ponowne połączenie | Podręcznik | Podręcznik | Automatycznie (przeglądarka) | Podręcznik |
| Dane binarne | Poprzez kodowanie | Poprzez kodowanie | Nie (tylko SMS) | Rodzinny |
| Połączenie według domeny | Wspólny | 1 na strumień | Maks. 6 (HTTP/1.1) | Żadnych ograniczeń |
| Serwer proxy/zapora sieciowa | Bez problemu | Rzadkie problemy | Ogólnie OK | Możliwe bloki |
| Skalowalność | Dobry (bezpaństwowy) | Przeciętny | Dobry | Wymaga uwagi |
| Złożoność | Bardzo niski | Niski | Niski | Średnio-wysoki |
| Obsługa przeglądarki | Uniwersalny | Uniwersalny | Wszystko nowoczesne | Wszystko nowoczesne |
Ramy decyzyjne: który wybrać?
Nie ma czegoś takiego jak „absolutnie najlepsza” technologia. Wybór zależy od przypadku użycia. Oto praktyczne ramy decyzyjne.
Wybierz opcję Krótkie odpytywanie, gdy:
- Dane zmieniają się rzadko (co ponad 30 sekund)
- Opóźnienie nie jest krytyczne
- Chcesz maksymalnej prostoty wdrożenia
- Infrastruktura jest ograniczona (hosting współdzielony)
- Przykład: sprawdź dostępność miejsc, status zamówienia
Wybierz opcję Długie odpytywanie, gdy:
- Potrzebujesz małych opóźnień bez zmiany infrastruktury
- Aktualizacje są sporadyczne, ale muszą być wprowadzane na czas
- Nie można używać SSE ani WebSocket ze względu na ograniczenia techniczne
- Przykład: system powiadomień o małej głośności
Wybierz SSE, gdy:
- Przepływ odbywa się głównie z serwera do klienta
- Chcesz automatycznego ponownego połączenia zarządzanego przez przeglądarkę
- Potrzebujesz zgodności z serwerem proxy i CDN
- Format danych jest tekstowy (JSON, tekst)
- Przykład: pulpit nawigacyjny na żywo, kanał aktualności, powiadomienia, przesyłanie strumieniowe dzienników
Wybierz WebSocket, gdy:
- Potrzebujesz prawdziwej dwustronnej komunikacji
- Opóźnienie musi być minimalne (<50 ms)
- Musisz przesłać dane binarne (audio, wideo, pliki)
- Głośność wiadomości jest wysoka (dziesiątki na sekundę)
- Przykład: czat w czasie rzeczywistym, gry wieloosobowe, wspólne edytowanie, handel
Złota zasada
Zacznij od najprostszego rozwiązania, które spełni Twoje wymagania. Jeśli odpytywanie działa w Twoim przypadku nie komplikuj architektury za pomocą protokołu WebSocket. Zawsze możesz ewoluować później. SSE jest często najlepszym kompromisem: proste jak HTTP, ale z funkcją push w czasie rzeczywistym i automatycznym ponownym połączeniem.
Rozważania dotyczące skalowalności
Skalowanie trwałych połączeń (SSE i WebSocket) wymaga innej uwagi w porównaniu do bezstanowych interfejsów API REST.
Równoważenie obciążenia
W przypadku trwałych połączeń moduł równoważenia obciążenia nie może swobodnie dystrybuować prośby. To przydatne lepkie sesje lub warstwę komunikatów (Redis Pub/Sub, NATS, Kafka) do synchronizacji wiadomości pomiędzy instancjami serwerów.
// 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 połączeń
Pojedynczy serwer Node.js może obsłużyć dziesiątki tysięcy połączeń WebSocket jednoczesne, ale każde połączenie zużywa pamięć. Uważnie monitoruj użycie pamięci i skonfiguruj odpowiednie limity. W przypadku SSE w HTTP/1.1 limit przy użyciu protokołu HTTP/2 można ominąć 6 połączeń na domenę przeglądarki (który multipleksuje połączenia).
Bicie serca i przekroczenie limitu czasu
Stałe połączenia można zamykać w trybie cichym za pomocą serwerów proxy i NAT lub zaporę sieciową. Zawsze wdrażaj mechanizm bicie serca (ping/pong), aby wykryć martwe połączenia i szybko połączyć się ponownie.
// 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);
});
});
Wniosek
Wybór pomiędzy odpytywaniem HTTP, zdarzeniami wysyłanymi przez serwer i protokołem WebSocket nie jest kwestią przypadku który jest „najlepszy”, ale który jest najbardziej odpowiedni dla konkretnego problemu. Sondowanie pozostaje ważne w przypadku prostych scenariuszy. SSE oferuje doskonałą równowagę pomiędzy prostotą a możliwością działania w czasie rzeczywistym dla przepływów jednokierunkowych. WebSocket jest oczywisty wybór w przypadku komunikacji dwukierunkowej o niskim opóźnieniu.
W Angular wszystkie trzy technologie integrują się naturalnie z RxJS, umożliwiając napisać responsywny i komponowalny kod. Kluczem jest zacząć od przypadku użycia, ocenić ograniczenia infrastrukturalne i wybrać najprostsze rozwiązanie który spełnia wymagania.
Kluczowe punkty
- Krótka ankieta: proste, ale nieefektywne, odpowiednie dla danych, które rzadko się zmieniają
- Długie odpytywanie: lepsze opóźnienie niż krótkie odpytywanie, dobre uniwersalne rozwiązanie awaryjne
- SSE: Push między serwerem a klientem z automatycznym ponownym połączeniem, idealny do pulpitów nawigacyjnych i powiadomień
- WebSockety: Pełny dupleks o niskim opóźnieniu, niezbędny do rozmów, gier i współpracy
- Skalowalność: trwałe połączenia wymagają Redis Pub/Sub lub podobnego rozwiązania dla wielu instancji
- RxJS: wszystkie technologie płynnie integrują się z operatorami Observable i RxJS
- Praktyczna zasada: Zacznij od najprostszego rozwiązania i skaluj tylko w razie potrzeby







