Задача
HTTP устроен как «клиент спросил, сервер ответил». А чату, бирже, уведомлениям нужно, чтобы сервер сам сообщил о новом событии. Есть четыре подхода.
Short polling
Клиент опрашивает сервер по таймеру:
setInterval(async () => {
const res = await fetch('/api/messages?after=' + lastId);
render(await res.json());
}, 5000);
Просто и работает везде, но задержка до интервала и куча пустых запросов.
Long polling
Клиент шлёт запрос, сервер держит его открытым, пока не появится событие (или таймаут ~30 с), отвечает, клиент сразу шлёт новый. Задержка маленькая, но на каждое событие новый HTTP-запрос с заголовками.
Server-Sent Events (SSE)
Однонаправленный поток от сервера к клиенту поверх обычного HTTP. Сервер отвечает Content-Type: text/event-stream и не закрывает ответ:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 17
event: price
data: {"ticker":"SBER","price":301.5}
Каждое событие заканчивается пустой строкой. На клиенте:
const es = new EventSource('/api/stream');
es.addEventListener('price', (e) => console.log(JSON.parse(e.data)));
Сильные стороны SSE:
- встроенный автореконнект; после переподключения браузер шлёт
Last-Event-ID, и сервер может дослать пропущенное; - работает через обычные прокси и HTTP/2;
- куки шлются как в обычном запросе.
Ограничения: только текст, только сервер → клиент. EventSource не умеет ставить свои заголовки (например Authorization), поэтому авторизуют кукой. На HTTP/1.1 браузер держит не больше 6 соединений на домен, и каждая вкладка с SSE съедает одно. На HTTP/2 потоки мультиплексируются, лимит не мешает.
WebSocket
Двунаправленный канал поверх одного TCP-соединения. Начинается с HTTP-запроса на upgrade:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Сервер отвечает 101 Switching Protocols, и дальше по соединению ходят фреймы (текст или бинарные) в обе стороны. Схемы ws:// и wss:// (через TLS, в проде только она).
const ws = new WebSocket('wss://app.ru/chat');
ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 1 }));
ws.onmessage = (e) => console.log(e.data);
ws.onclose = (e) => scheduleReconnect(e.code);
Что надо сделать руками:
- реконнект с экспоненциальной задержкой и джиттером;
- heartbeat (ping/pong), чтобы заметить мёртвое соединение и не дать прокси закрыть его по простою;
- досылку пропущенного после переподключения;
- проверку
Originпри handshake: CORS на WebSocket не распространяется, и чужой сайт может открыть соединение с куками пользователя (атака CSWSH).
Масштабирование
Соединения долгоживущие и привязаны к конкретному инстансу. Если пользователь A подключён к серверу 1, а B к серверу 2, сообщение надо переслать между ними: обычно через Redis pub/sub или брокер. Балансировщику нужна поддержка upgrade и большие таймауты.
Что выбрать
| Задача | Выбор |
|---|---|
| уведомления, лента, прогресс задачи, стриминг ответа | SSE |
| чат, совместное редактирование, игры | WebSocket |
| редкие обновления, простота важнее | polling |
Что спросят на собесе
- Чем WebSocket отличается от SSE и long polling?
- Как устанавливается WebSocket-соединение?
- Что делать при обрыве соединения?
- Как масштабировать WebSocket на несколько серверов?
- Почему для уведомлений часто хватает SSE?