Что такое WebSocket и когда использовать вместо HTTP polling?
Что такое WebSocket
WebSocket (RFC 6455) — это прикладной протокол поверх TCP, который начинается с HTTP-запроса с заголовком Upgrade: websocket и после успешного handshake переходит в двунаправленный (full-duplex) бинарный/текстовый канал. После апгрейда HTTP больше не используется: обе стороны шлют кадры (frames) в любой момент, без запроса-ответа.
Ключевые свойства:
- одно долгоживущее TCP-соединение вместо потока коротких HTTP-запросов;
- сервер может инициировать отправку (server push) без запроса клиента;
- маленький оверхед на сообщение — кадр от 2 байт против сотен байт HTTP-заголовков;
- работает поверх 80/443, дружит с прокси и TLS (
wss://).
HTTP polling и его варианты
- Short polling — клиент по таймеру шлёт
GET /messages. Простой, но создаёт RPS-нагрузку и задержку до интервала опроса. - Long polling — сервер держит запрос открытым, пока не появятся данные, потом отвечает. Лучше по задержке, но всё равно одно сообщение на один HTTP-цикл и постоянные реконнекты.
- SSE (Server-Sent Events) — однонаправленный push сервер → клиент поверх HTTP. Хорош для лент новостей, но не даёт канал клиент → сервер.
Когда WebSocket выигрывает
Брать WebSocket, когда выполняется хотя бы одно:
- нужна низкая задержка двусторонняя связь (чаты, игры, совместное редактирование, WebRTC-сигналинг);
- сервер часто инициирует сообщения, а интервал опроса пришлось бы делать <1–2 с;
- много мелких частых сообщений — оверхед HTTP-заголовков становится дороже полезной нагрузки;
- нужны подписки на несколько потоков по одному соединению (биржевые тикеры, телеметрия).
Когда WebSocket не нужен
- Запрос-ответ без push — обычный REST/HTTP/2 проще и кэшируется.
- Только сервер → клиент, редкие события — SSE дешевле и переживает обрывы из коробки.
- Запросы раз в минуту и реже — short polling достаточен и не требует stateful-инфраструктуры.
Цена WebSocket в Node.js
- Соединение stateful: балансировщик должен делать sticky sessions или использовать общий брокер (Redis pub/sub, NATS) для масштабирования между инстансами.
- Нужны heartbeat (ping/pong) и автоматический reconnect на клиенте — TCP сам обрыв не всегда замечает.
- Авторизация делается на handshake (cookie/
Sec-WebSocket-Protocol/токен в первом сообщении), потому что заголовки потом не приходят. - На каждое соединение тратится память и файловый дескриптор — десятки тысяч сокетов требуют тюнинга
ulimitи backpressure.
Короткое правило выбора
Если задержка важнее простоты и поток сообщений двусторонний — WebSocket. Если поток односторонний от сервера — SSE. Если события редкие или нужен кэш/CDN — обычный HTTP с long polling в худшем случае.
Что хочет услышать интервьюер
Понимание, что WebSocket — full-duplex поверх TCP с HTTP-handshake и Upgrade
Сравнение short polling, long polling, SSE и WebSocket по задержке и оверхеду
Конкретные кейсы применения: чаты, игры, тикеры, совместное редактирование
Осознание stateful-природы и проблем масштабирования (sticky sessions, pub/sub)
Знание про heartbeat, reconnect и авторизацию на handshake
Пример: WebSocket-сервер на ws с heartbeat
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
// Heartbeat: помечаем живые соединения и выбрасываем мёртвые
function heartbeat(this: WebSocket & { isAlive?: boolean }) {
this.isAlive = true;
}
wss.on('connection', (ws: WebSocket & { isAlive?: boolean }, req) => {
// Авторизация на handshake — токен в query или заголовке
const token = new URL(req.url ?? '/', 'http://x').searchParams.get('token');
if (!token) return ws.close(4001, 'unauthorized');
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', (data) => {
// Эхо, в реальном коде — роутинг по типу события
ws.send(data);
});
});
// Раз в 30 секунд проверяем живость и пингуем
const interval = setInterval(() => {
for (const client of wss.clients as Set<WebSocket & { isAlive?: boolean }>) {
if (client.isAlive === false) {
client.terminate();
continue;
}
client.isAlive = false;
client.ping();
}
}, 30_000);
wss.on('close', () => clearInterval(interval));
Пример: Клиент с reconnect и экспоненциальным backoff
function connect(url: string) {
let attempt = 0;
let ws: WebSocket;
const open = () => {
ws = new WebSocket(url);
ws.onopen = () => {
attempt = 0; // Сброс backoff после успешного коннекта
};
ws.onmessage = (e) => {
// Обработка сообщения от сервера
console.log('msg', e.data);
};
ws.onclose = () => {
// Экспоненциальный backoff с потолком 30 секунд
const delay = Math.min(30_000, 1000 * 2 ** attempt++);
setTimeout(open, delay);
};
ws.onerror = () => ws.close();
};
open();
return () => ws?.close();
}
const stop = connect('wss://example.com/feed?token=abc');
Пример: Long polling для сравнения
// Клиентский long polling — каждый ответ закрывает HTTP и открывает новый
async function longPoll(cursor = 0): Promise<void> {
try {
const res = await fetch(`/api/messages?since=${cursor}`);
const { messages, nextCursor } = await res.json();
for (const m of messages) handle(m);
return longPoll(nextCursor);
} catch {
// На ошибке — пауза и повтор, чтобы не положить сервер
await new Promise((r) => setTimeout(r, 2000));
return longPoll(cursor);
}
}
function handle(_m: unknown) {}
Типичные ошибки
Считать WebSocket «улучшенным HTTP» и тянуть его на любой запрос-ответ
Игнорировать SSE и предлагать WebSocket там, где нужен только server → client поток
Забывать про sticky sessions/общий брокер при горизонтальном масштабировании
Не реализовывать ping/pong и reconnect, надеясь, что TCP сам отловит обрыв
Передавать авторизацию в каждом сообщении вместо проверки на handshake


