Раздел 11 · Network

Реальное время: polling, SSE, WebSocket, chunked

middle~45 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Реальное время: polling, SSE, WebSocket, chunked

Базовый HTTP это один запрос, один ответ: клиент спросил, сервер ответил, соединение свободно. Но половина современного веба хочет наоборот, чтобы сервер слал данные сам: новое сообщение в чате, тик котировки, строчка лога, прогресс загрузки. Этот урок про четыре способа продавить одностороннюю и двустороннюю доставку поверх HTTP, от костыля до честного дуплекса.

Эволюция тут историческая, и пройти её по порядку полезно: каждый следующий приём чинит боль предыдущего.

Проблема: сервер хочет говорить первым

В классической модели сервер нем, пока его не спросили. Чтобы узнать про новое событие, клиент вынужден спрашивать снова и снова. Отсюда первый, наивный приём.

Short-polling: спрашиваем по таймеру

// каждые 3 секунды дёргаем сервер: есть новое?
setInterval(async () => {
  const res = await fetch('/api/messages?since=' + lastId);
  const items = await res.json();
  render(items);
}, 3000);

Просто и работает везде, но цена высокая: пустые запросы жгут трафик и батарею, а задержка до события в среднем половина интервала. Это отправная точка: проще и переносимее уже не сделать, и дальше каждый способ будет бороться именно с этими двумя минусами, лишним трафиком и задержкой.

Long-polling: держим запрос открытым

Long-polling переворачивает идею: клиент спрашивает, а сервер молчит, пока не появится событие (или не истечёт таймаут). Получил ответ, сразу спросил снова.

async function poll() {
  try {
    const res = await fetch('/api/updates'); // сервер держит до события
    const data = await res.json();
    handle(data);
  } catch {}
  poll(); // сразу новый цикл
}
poll();

Задержка почти нулевая, лишних запросов нет. Но каждое событие это полный HTTP-цикл с новыми заголовками, а сервер держит кучу открытых соединений. Исторически этот класс приёмов звали Comet. В 2026 long-polling это fallback (например, в socket.io, когда WebSocket не проходит через прокси), а не первый выбор.

Server-Sent Events: честный односторонний стрим

SSE это первый честный push: одно долгое HTTP-соединение, по которому сервер бесконечно дописывает события. Формат текстовый и до смешного простой.

data: привет
data: ещё строка

event: price
id: 42
retry: 5000
data: {"ticker":"AAPL","px":231.5}

Поля: data: полезная нагрузка, event: имя события, id: идентификатор (браузер вернёт его в Last-Event-ID при переподключении), retry: интервал reconnect в миллисекундах. Пустая строка завершает событие.

Клиент это одна строчка, и переподключение встроено:

const es = new EventSource('/api/stream');
es.addEventListener('price', (e) => render(JSON.parse(e.data)));
es.onmessage = (e) => console.log('default event:', e.data);
// при разрыве браузер сам переподключится и пришлёт Last-Event-ID

Сервер на Node держит соединение открытым и пишет в него:

function stream(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  const timer = setInterval(() => {
    res.write(`data: ${new Date().toISOString()}\n\n`);
  }, 1000);
  req.on('close', () => clearInterval(timer));
}

SSE идеален, когда поток нужен только в одну сторону: лента уведомлений, прогресс задачи, серверные логи, токены LLM. Минусы: только текст, только сервер-клиент, и в HTTP/1.1 действует лимит на число одновременных соединений к домену (в HTTP/2 эта боль уходит, мультиплексирование снимает лимит).

WebSocket: полный дуплекс

Когда обе стороны должны говорить когда угодно (чат, игра, совместный редактор), нужен WebSocket.

Хитрость в том, как он начинается: обычный HTTP-запрос с заголовком Upgrade, на который сервер отвечает 101 Switching Protocols. После этого по тому же TCP-соединению летят фреймы в обе стороны, без заголовков HTTP на каждое сообщение.

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
// клиент
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello' }));
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.onclose = () => reconnectWithBackoff();

ws:// поверх обычного TCP, wss:// поверх TLS (всегда wss в проде). Важные нюансы: переподключение и heartbeat ты реализуешь сам (в отличие от SSE, где reconnect встроен), а ещё нужно помнить, что прокси и балансировщики любят рвать долгие соединения. Библиотеки вроде socket.io добавляют поверх reconnection, rooms и fallback на long-polling.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.