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=
-
1
Клиент: обычный
GETПлюс заголовкиUpgrade: websocketи случайныйSec-WebSocket-Key -
2
Сервер: событие
upgradeNode не отдаёт такой запрос обработчикуrequest— для него отдельное событие -
3
Сервер считает
Acceptbase64(sha1(ключ + GUID))— детерминированно, без всякой криптозащиты -
4
Ответ
101 Switching ProtocolsНе 200. Тот же TCP-сокет, новый протокол - 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?
Upgrade: websocket. После ответа 101 тот же TCP-сокет переключается на протокол WebSocket, и HTTP там больше нет.Что лучше — WebSocket или AJAX?
Когда WebSocket избыточен?
Нужен ли пакет `ws` в 2026?
WebSocket встроен в Node начиная с 22-й версии. Для сервера пакет по-прежнему нужен: встроенного WebSocket-сервера в Node нет, а писать разбор фреймов руками в продакшене не стоит.Почему WebSocket не работает через мой прокси?
Upgrade. Старые конфигурации nginx рвут такие соединения — нужно явно прописать proxy_set_header Upgrade и Connection "upgrade".