WebSocket: как устроен и когда он не нужен

✓ Все примеры выполнены на Node.js v22.23.1 (LTS), июль 2026 — сервер поднят на голом node:http

Содержание

Про WebSocket обычно рассказывают на уровне «постоянное соединение, сервер может слать сам». Это правда, но за ней прячется главное непонимание: откуда это соединение берётся. Разберём на живом сервере, поднятом на голом node:http — без единого пакета.

Сначала это обычный HTTP

Пока никто не попросил upgrade, сервер отвечает как всегда:

обычный GET -> 200 обычный HTTP-ответ

Никакого «отдельного протокола» ещё нет. WebSocket не открывает своё соединение — он переиспользует уже установленное HTTP-соединение и меняет на нём протокол.

Рукопожатие: GET, который меняет протокол

Клиент отправляет обычный GET с особыми заголовками. Вот что реально пришло на сервер:

запрос клиента:
  метод                 : GET <- обычный GET!
  Upgrade               : websocket
  Connection            : upgrade
  Sec-WebSocket-Version : 13
  Sec-WebSocket-Key     : O6Q2h/D2QQ9lbNIRpRf8GA==
ответ сервера:
  статус                : 101 Switching Protocols
  Sec-WebSocket-Accept  : qcgqQinETMzEmZQuJ5y55SVlgbo=
Как HTTP превращается в WebSocket
  1. 1 Клиент: обычный GETПлюс заголовки Upgrade: websocket и случайный Sec-WebSocket-Key
  2. 2 Сервер: событие upgradeNode не отдаёт такой запрос обработчику request — для него отдельное событие
  3. 3 Сервер считает Acceptbase64(sha1(ключ + GUID)) — детерминированно, без всякой криптозащиты
  4. 4 Ответ 101 Switching ProtocolsНе 200. Тот же TCP-сокет, новый протокол
  5. 5 HTTP закончилсяДальше по сокету идут фреймы WebSocket, обе стороны шлют когда хотят

Вся суть — в шаге с Sec-WebSocket-Accept. Сервер берёт ключ клиента, приклеивает к нему константу 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 из RFC 6455, считает SHA-1 и кодирует в base64:

const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const accept = createHash('sha1').update(key + GUID).digest('base64');

Зачем это, если GUID известен всем и никакой тайны тут нет? Это не защита, а проверка понимания. Ответ доказывает, что на том конце действительно WebSocket-сервер, а не кэширующий прокси, который просто повторил запрос. Ключ клиента случайный — значит, заранее заготовленный ответ не подойдёт.

Отдельно про Node: такой запрос не попадёт в обработчик createServer. Для него есть отдельное событие:

server.on('upgrade', (req, socket) => {
  const accept = createHash('sha1').update(key + GUID).digest('base64');
  socket.write(
    'HTTP/1.1 101 Switching Protocols\r\n' +
    'Upgrade: websocket\r\n' +
    'Connection: Upgrade\r\n' +
    `Sec-WebSocket-Accept: ${accept}\r\n\r\n`
  );
  // дальше socket — обычный TCP-сокет, HTTP на нём кончился
});

Главное: теперь сервер шлёт сам

Ради этого всё и затевалось. После рукопожатия сервер отправил два сообщения — клиент не запрашивал ничего:

клиент получил без единого запроса: [ 'привет от сервера', 'сервер пишет сам, клиент не спрашивал' ]

Это и есть разница с AJAX, которую не обойти никаким polling-ом: там инициатива всегда у клиента, здесь — у обоих.

Клиент обязан маскировать, сервер — нет

Асимметрия, о которой почти не пишут, а она в стандарте:

сервер получил: "привет от клиента" | клиент маскировал: true

Каждый фрейм от клиента к серверу обязан быть замаскирован — XOR со случайным 4-байтовым ключом, который едет в том же фрейме. Обратно — нет, сервер отправляет как есть.

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

Практический вывод: если пишете разбор фреймов руками — не забудьте, что входящие от клиента всегда маскированы. Это классическая причина «данные приходят мусором».

Состояния: close() не закрывает мгновенно

readyState в работе            : 1 (OPEN)
readyState сразу после close() : 2 (CLOSING — идёт закрывающее рукопожатие)

close() не рвёт соединение — он начинает закрывающее рукопожатие. Сокет уходит в CLOSING и станет CLOSED только после ответа второй стороны.

Значение Состояние
0 CONNECTING — рукопожатие идёт
1 OPEN — можно слать
2 CLOSING — закрытие начато
3 CLOSED — всё

Отсюда типичная ошибка: вызвать close() и сразу считать соединение закрытым. Ждать нужно события close.

WebSocket, AJAX или SSE

Вопрос «что лучше — сокеты или AJAX» поставлен неверно: это разные инструменты. Правильный вопрос — в какую сторону идут данные.

Три способа получить данные с сервера

AJAX / fetch

Клиент спросил — сервер ответил

  • Инициатива только у клиента
  • Обычный HTTP, кэшируется, простой
  • Сервер сам ничего не пришлёт
  • Берите по умолчанию

SSE (Server-Sent Events)

Сервер шлёт, клиент слушает

  • Только сервер → клиент
  • Поверх обычного HTTP
  • Переподключается сам
  • Ленты, уведомления, прогресс задач
  • Часто это и есть правильный ответ

WebSocket

Обе стороны шлют когда хотят

  • Двусторонний канал
  • Свой протокол после 101
  • Переподключение пишете сами
  • Прокси должен пропускать Upgrade
  • Чаты, игры, совместное редактирование

Практическое правило: если данные идут только от сервера к клиенту — берите SSE. Он проще, работает поверх обычного HTTP, переподключается сам и не требует настройки прокси. WebSocket нужен, когда клиент тоже должен слать часто — чат, игра, совместное редактирование.

Самая частая ошибка — взять сокеты ради ленты уведомлений и потом полгода чинить переподключения и прокси.

Нужен ли пакет ws

Разделим:

  • Клиент — не нужен. WebSocket встроен в глобальные объекты Node начиная с 22-й версии. Проверено: typeof WebSocket === 'function'.
  • Сервер — нужен. Встроенного WebSocket-сервера в Node нет. Всё, что вы видели выше, — ручной разбор фреймов, и это годится для понимания, а не для продакшена: там ещё пинги, фрагментация, сжатие и обработка обрывов.

Для сервера берут ws (минимальный) либо Socket.io — он поверх сокетов добавляет переподключение, комнаты и откат на polling.

Что было на этой странице в 2017 году

Первая версия вышла 30 июля 2017 года и называлась «Что такое WebSocket. Что лучше — Веб-сокеты или AJAX?». Сама постановка вопроса — примета времени: тогда WebSocket подавали как замену AJAX, новую технологию вместо старой.

Сегодня видно, что это ложная дихотомия. AJAX никуда не делся и остался выбором по умолчанию; WebSocket занял свою узкую нишу — двустороннюю. А между ними появился третий вариант, которого в статье не было: SSE для случая «только сервер шлёт», и именно он закрывает половину задач, ради которых в 2017-м тянули сокеты.

Изменилось и практическое: тогда клиентский WebSocket в Node требовал пакета, теперь он встроен. А вот сервер как требовал ws, так и требует — встроенного WebSocket-сервера в платформе нет до сих пор.


Смежные темы: чат на Socket.io — что даёт обёртка поверх сокетов; HTTP-сервер — откуда берётся событие upgrade; события — на чём построен сокет. Полный список — в справочнике по Node.js.

Частые вопросы

WebSocket — это отдельный протокол или надстройка над HTTP?
И то, и другое. Соединение начинается обычным HTTP-запросом GET с заголовком Upgrade: websocket. После ответа 101 тот же TCP-сокет переключается на протокол WebSocket, и HTTP там больше нет.
Что лучше — WebSocket или AJAX?
Вопрос поставлен неверно: они решают разные задачи. AJAX — запрос-ответ по инициативе клиента. WebSocket — постоянный канал, где сервер шлёт сам. Для «получить данные по клику» сокет избыточен.
Когда WebSocket избыточен?
Когда данные идут только в одну сторону — от сервера к клиенту. Для лент обновлений, уведомлений и прогресса задач есть SSE: он проще, работает поверх обычного HTTP и переподключается сам.
Нужен ли пакет `ws` в 2026?
Для клиента — нет: WebSocket встроен в Node начиная с 22-й версии. Для сервера пакет по-прежнему нужен: встроенного WebSocket-сервера в Node нет, а писать разбор фреймов руками в продакшене не стоит.
Почему WebSocket не работает через мой прокси?
Прокси должен уметь пропускать Upgrade. Старые конфигурации nginx рвут такие соединения — нужно явно прописать proxy_set_header Upgrade и Connection "upgrade".