Перейти к содержимому

WebSocket / SSE / WebTransport

Три механизма real-time коммуникации браузера с сервером: WebSocket — полнодуплексный TCP-туннель поверх HTTP Upgrade; SSE — односторонний server-push по обычному HTTP; WebTransport — современный API поверх QUIC; выбор между ними — типичный design-вопрос на senior-собеседованиях.

// WebSocket: полнодуплексный канал, обязательно wss:// в production
const ws = new WebSocket('wss://api.example.com/ws');
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
});
ws.addEventListener('message', ({ data }) => {
const msg = JSON.parse(data);
renderUpdate(msg);
});
ws.addEventListener('close', ({ code, reason, wasClean }) => {
// 1000 = нормальное закрытие | 1001 = going away | 1006 = abnormal (network drop)
if (!wasClean) scheduleReconnect();
});
ws.addEventListener('error', e => console.error('WS error', e));
// Exponential backoff reconnect
function scheduleReconnect(attempt = 0) {
const delay = Math.min(500 * 2 ** attempt, 30_000); // 500ms → 1s → 2s → ... → 30s
setTimeout(() => connect(attempt + 1), delay + Math.random() * 500); // jitter
}
// Heartbeat: обнаружение «мёртвых» соединений (NAT timeout ~30-60 сек)
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"ping"}');
}, 25_000);
// SSE (Server-Sent Events): server -> client, автоматический reconnect браузером
const es = new EventSource('/api/stream'); // поверх обычного HTTP
es.addEventListener('message', ({ data }) => { // дефолтное событие
renderChunk(JSON.parse(data));
});
es.addEventListener('price-update', ({ data }) => { // именованное событие
updateTicker(JSON.parse(data));
});
es.addEventListener('error', () => {
// EventSource сам переподключается; readyState: CONNECTING | OPEN | CLOSED
if (es.readyState === EventSource.CLOSED) setStatus('disconnected');
});
es.close(); // явная отписка
// Server (Node.js): формат text/event-stream
// res.writeHead(200, {
// 'Content-Type': 'text/event-stream',
// 'Cache-Control': 'no-cache',
// 'Connection': 'keep-alive',
// 'X-Accel-Buffering': 'no', // отключить буферизацию nginx
// });
// res.write('data: {"price":42}\n\n'); // двойной \n = конец события
// res.write('event: price-update\ndata: {"price":43}\n\n');
// res.write('id: 123\n'); // Last-Event-ID для retry
// WebTransport: QUIC-based, низкая латентность, мультиплексирование (Chrome 97+)
const transport = new WebTransport('https://example.com:4433/wt');
await transport.ready; // QUIC handshake
// Ненадёжные датаграммы (unreliable, как UDP): низкая латентность, без гарантий
const dgramWriter = transport.datagrams.writable.getWriter();
await dgramWriter.write(new TextEncoder().encode('telemetry'));
// Надёжные потоки (reliable streams): отдельный QUIC stream, без head-of-line blocking
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode('command'));
// SSE vs WebSocket vs WebTransport
//
// Направление SSE: server->client WS: duplex WT: duplex
// Протокол HTTP/1.1+ TCP (WS) QUIC (UDP-based)
// Auto-retry ✓ браузер ручной ручной
// Head-of-line да (HTTP/1.1) да (TCP) нет (QUIC streams)
// Поддержка все браузеры все браузеры Chrome 97+, Edge, Firefox 114+
// CPU overhead низкий средний низкий

Итог: SSE — простейший server-push с HTTP и авто-reconnect; WebSocket — полный дуплекс для интерактивности; WebTransport — low-latency QUIC для realtime-приложений нового поколения.