WebSocket — сетевой протокол для постоянного двустороннего обмена данными между клиентом и сервером без необходимости создавать новый HTTP-запрос для каждого события. После начального рукопожатия стороны переходят к обмену компактными WebSocket-фреймами и могут отправлять сообщения независимо друг от друга, поэтому технология хорошо подходит для чатов, онлайн-игр, совместного редактирования и интерфейсов с данными в реальном времени.

Что такое WebSocket и какую проблему он решает
Обычный HTTP построен вокруг модели «запрос — ответ»: клиент обращается к серверу, сервер отвечает, после чего конкретная операция считается завершённой. Для загрузки страницы, получения карточки товара или отправки формы такая схема удобна. Но она становится менее естественной, когда серверу нужно самостоятельно и почти мгновенно сообщать клиенту о новых событиях.
Представим чат. Если использовать только классические HTTP-запросы, браузеру пришлось бы регулярно спрашивать сервер:
«Есть новые сообщения? А сейчас? А теперь?»
Так работает polling, или периодический опрос. Чем меньше интервал между запросами, тем ниже задержка, но тем больше лишних запросов, заголовков и нагрузки на сервер.
WebSocket решает задачу иначе. Клиент один раз устанавливает соединение с сервером, после чего этот канал остаётся открытым. Пока соединение существует:
- клиент может отправить данные серверу в любой момент;
- сервер может отправить данные клиенту без предварительного нового запроса;
- по одному соединению передаются как текстовые, так и бинарные сообщения;
- обмен носит событийный характер, а не строится как последовательность отдельных HTTP-запросов.
Такой режим называется full-duplex, то есть полнодуплексным: обе стороны могут передавать данные независимо и одновременно.
Важно не путать WebSocket с «постоянно открытым HTTP-запросом». HTTP действительно участвует в начале соединения, но после успешного рукопожатия WebSocket использует собственный формат кадров — фреймов.
Базовый протокол WebSocket стандартизирован в RFC 6455. Для браузерного API действует также актуальный WHATWG WebSockets Living Standard.
Принцип работы WebSocket по шагам
С точки зрения приложения жизненный цикл WebSocket удобно представить как цепочку:
- клиент определяет адрес сервера;
- устанавливается сетевое соединение;
- выполняется WebSocket handshake;
- соединение переходит в состояние
OPEN; - клиент и сервер обмениваются фреймами;
- при необходимости проверяют жизнеспособность соединения;
- одна из сторон запускает процедуру закрытия;
- транспортное соединение завершается.
Каждый этап решает отдельную задачу.
1. Адрес ws:// или wss://
WebSocket обычно использует два URI-схемы:
ws://— обычное WebSocket-соединение;wss://— WebSocket поверх защищённого TLS-соединения.
По смыслу это похоже на разницу между http:// и https://.
Для публичных веб-приложений практически всегда нужен wss://: передаваемые данные защищаются TLS от чтения и изменения по пути между клиентом и сервером. Браузеры также ограничивают использование небезопасных WebSocket-соединений со страниц, загруженных по HTTPS.
Пример:
wss://example.com/socketАдрес может содержать путь и параметры:
wss://example.com/ws/chat?room=42При этом секретные токены не стоит без необходимости помещать в query string: URL может попасть в журналы сервера, историю диагностики или системы аналитики.
2. Установка транспортного соединения
В классическом варианте WebSocket работает поверх TCP. Если используется wss://, перед передачей WebSocket-данных также устанавливается TLS-сессия.
Упрощённо цепочка выглядит так:
Клиент
│
├─ DNS: определить IP-адрес
│
├─ TCP: установить соединение
│
├─ TLS: создать защищённый канал, если используется wss://
│
└─ WebSocket handshake
↓
Обмен фреймамиRFC 6455 описывает WebSocket как протокол с начальным рукопожатием и последующим фреймированием сообщений поверх TCP.
Современная инфраструктура умеет устанавливать WebSocket и через HTTP/2 или HTTP/3. Для HTTP/2 это определено RFC 8441, а для HTTP/3 — RFC 9220. В этих случаях механизм начальной установки отличается от привычного HTTP/1.1 Upgrade, но прикладная идея остаётся той же: создаётся длительный двусторонний WebSocket-канал.
Handshake — как HTTP превращается в WebSocket
При работе через HTTP/1.1 клиент начинает с обычного HTTP-запроса, но просит сервер переключить протокол.
Упрощённый запрос выглядит так:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13Здесь важны несколько заголовков.
Upgrade: websocket сообщает, что клиент хочет перейти с HTTP на WebSocket.
Connection: Upgrade указывает, что переключение относится к текущему соединению.
Sec-WebSocket-Key содержит случайное значение, сформированное клиентом.
Sec-WebSocket-Version: 13 указывает версию протокола WebSocket, определённую RFC 6455.
Если сервер принимает соединение, для HTTP/1.1 он отвечает кодом:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <вычисленное значение>Код 101 Switching Protocols означает, что сервер согласился переключить протокол.
Для чего нужен Sec-WebSocket-Accept
Сервер не просто копирует Sec-WebSocket-Key. Он добавляет к полученному ключу заданную стандартом строку GUID, вычисляет SHA-1 и кодирует результат в Base64. Полученное значение отправляется в Sec-WebSocket-Accept.
Схематично:
Sec-WebSocket-Key
+
фиксированный GUID
↓
SHA-1
↓
Base64
↓
Sec-WebSocket-AcceptКлиент выполняет такое же вычисление и сравнивает результат с ответом сервера. Если значение не совпадает, WebSocket-соединение не считается установленным.
Этот механизм не шифрует соединение и не аутентифицирует пользователя. Его задача — подтвердить, что сервер действительно обработал запрос как WebSocket handshake, а не случайно ответил на похожий HTTP-запрос.
После успешной проверки рукопожатие заканчивается. Дальнейший поток байтов интерпретируется уже как WebSocket-фреймы.
Что такое WebSocket-фрейм
WebSocket не передаёт сообщения как произвольный непрерывный поток без структуры. Данные упаковываются во фреймы.
У каждого фрейма есть небольшой заголовок и полезная нагрузка.
В упрощённом виде:
┌─────┬────────┬──────┬───────────────┬─────────────┐
│ FIN │ Opcode │ MASK │ Payload length│ Payload data│
└─────┴────────┴──────┴───────────────┴─────────────┘Реальный заголовок содержит и дополнительные зарезервированные биты, но для понимания механики достаточно основных полей.
FIN
Бит FIN показывает, заканчивается ли данным фреймом текущее сообщение.
Если всё сообщение помещается в один фрейм, FIN = 1.
Если сообщение разбито на несколько фрагментов, первый и промежуточные фреймы могут иметь FIN = 0, а последний — FIN = 1.
Opcode
Opcode определяет назначение фрейма.
Основные значения:
| Opcode | Назначение |
0x0 | продолжение фрагментированного сообщения |
0x1 | текстовые данные |
0x2 | бинарные данные |
0x8 | закрытие соединения |
0x9 | Ping |
0xA | Pong |
Поэтому WebSocket изначально умеет различать текстовые и бинарные сообщения на уровне протокола.
Payload length
Поле длины сообщает, сколько данных содержится в полезной нагрузке. Для небольших сообщений длина помещается непосредственно в базовую часть заголовка. Для более крупных используются дополнительные 16 или 64 бита.
Это позволяет отправлять сообщения разного размера, не вводя фиксированный максимальный размер на уровне самого формата фрейма. На практике серверные библиотеки и прокси почти всегда устанавливают собственные ограничения, чтобы защититься от чрезмерного потребления памяти.
Сообщение и фрейм — не одно и то же
Одна из распространённых ошибок — считать, что каждое WebSocket-сообщение всегда соответствует одному сетевому фрейму.
Это не обязательно так.
Например, приложение хочет передать большое сообщение:
"Очень большой набор данных..."Протокол может разделить его:
Фрейм 1: opcode=text, FIN=0
Фрейм 2: opcode=continuation, FIN=0
Фрейм 3: opcode=continuation, FIN=1Получатель собирает фрагменты в одно логическое сообщение.
Фрагментация позволяет не ждать подготовки огромного монолитного кадра и даёт возможность вставлять управляющие фреймы между частями больших сообщений. Например, Ping может быть обработан до того, как закончится передача большого фрагментированного сообщения.
Зачем клиентские фреймы маскируются
В RFC 6455 действует асимметричное правило:
- фреймы от клиента к серверу должны быть маскированы;
- фреймы от сервера к клиенту не маскируются.
Клиент для каждого фрейма создаёт новый 32-битный ключ маскирования. Полезная нагрузка преобразуется операцией XOR с байтами этого ключа.
Упрощённо:
исходные данные XOR masking key = данные в WebSocket-фреймеКлюч передаётся в самом фрейме, поэтому маскирование не является шифрованием и не скрывает данные от наблюдателя сети.
Его назначение связано с защитой сетевой инфраструктуры от определённого класса атак, при которых вредоносный браузерный код мог бы заставить промежуточные устройства воспринимать сформированный поток как другой протокол. Для конфиденциальности нужен TLS, то есть wss://.
Как происходит двусторонний обмен
После установления соединения модель «клиент спросил — сервер ответил» исчезает.
Например:
Клиент Сервер
│ │
│──── "пользователь печатает" ───>│
│ │
│<──── "новое сообщение" ─────────│
│ │
│<──── "статус собеседника" ──────│
│ │
│──── "сообщение прочитано" ─────>│
│ │Серверу не требуется ждать очередного запроса, чтобы отправить клиенту событие.
Именно это свойство делает WebSocket удобным для систем, где состояние меняется часто и обновление должно быстро появляться у клиента.
Типичные сценарии:
- чаты и мессенджеры;
- многопользовательские игры;
- совместное редактирование документов;
- биржевые котировки и торговые терминалы;
- онлайн-мониторинг оборудования;
- панели с оперативной телеметрией;
- уведомления в реальном времени;
- службы поддержки с живым статусом оператора;
- отслеживание положения объектов;
- интерактивные аукционы.
Ping и Pong — проверка живого соединения
Долгое TCP-соединение может фактически перестать работать, хотя приложение ещё не получило явного события закрытия. Например, устройство потеряло сеть, NAT удалил неактивную запись или один из промежуточных узлов прекратил пропускать трафик.
Для проверки соединения WebSocket определяет управляющие фреймы:
Ping— opcode0x9;Pong— opcode0xA.
Если сторона получает Ping, она должна ответить Pong, если соединение ещё не находится в процессе закрытия.
Механизм используется для:
- keepalive;
- проверки доступности второй стороны;
- обнаружения «мёртвых» соединений;
- приблизительного измерения задержки.
При этом стандартный браузерный объект JavaScript WebSocket не предоставляет разработчику методов вроде socket.ping(). Низкоуровневые Ping/Pong существуют в протоколе, но браузерный API их напрямую не открывает. Поэтому веб-приложения нередко вводят собственный heartbeat на прикладном уровне:
{"type":"ping","time":1720000000}и ожидают, например:
{"type":"pong","time":1720000000}Серверные WebSocket-библиотеки обычно позволяют работать и с настоящими protocol-level Ping/Pong.
Как корректно закрывается WebSocket
Просто оборвать TCP-соединение можно, но нормальный WebSocket предусматривает отдельное closing handshake.
Одна сторона отправляет фрейм Close с opcode 0x8. В нём может находиться код причины и короткое текстовое пояснение.
Например:
Клиент Сервер
│ │
│──── Close, code 1000 ───────>│
│<─── Close, code 1000 ────────│
│ │
└──── TCP закрывается ─────────┘Код 1000 означает нормальное закрытие.
Другие коды могут показывать, что:
- клиент покидает страницу;
- сервер завершает работу;
- нарушен формат сообщения;
- получены неподдерживаемые данные;
- сообщение оказалось слишком большим;
- возникла внутренняя ошибка.
После того как обе стороны обменялись Close, транспортное соединение можно завершить корректно.
В браузере состояние WebSocket отражается свойством readyState:
| Состояние | Значение | Смысл |
CONNECTING | 0 | соединение устанавливается |
OPEN | 1 | соединение готово к обмену |
CLOSING | 2 | начата процедура закрытия |
CLOSED | 3 | соединение закрыто или открыть его не удалось |
Простая реализация WebSocket-клиента в браузере
Базовый JavaScript-клиент выглядит компактно:
const socket = new WebSocket("wss://example.com/ws");
socket.addEventListener("open", () => {
console.log("Соединение установлено");
socket.send(JSON.stringify({
type: "hello",
message: "Привет, сервер"
}));
});
socket.addEventListener("message", (event) => {
console.log("Получено:", event.data);
});
socket.addEventListener("error", (event) => {
console.error("Ошибка WebSocket", event);
});
socket.addEventListener("close", (event) => {
console.log(
"Соединение закрыто",
event.code,
event.reason,
event.wasClean
);
});Конструктор new WebSocket() начинает установку соединения асинхронно. Отправлять данные безопасно после события open, когда состояние перешло в OPEN.
Для приложения WebSocket обычно выглядит как поток событий:
open
↓
message
↓
message
↓
message
↓
closeНо сообщения могут одновременно уходить и в обратном направлении через send().
Что WebSocket не делает за разработчика
WebSocket определяет транспортный механизм, но не задаёт бизнес-протокол приложения.
Сам по себе он не знает, что такое:
- пользователь;
- чат;
- комната;
- подписка на канал;
- событие
new_message; - подтверждение доставки;
- авторизация;
- повторная отправка;
- очередь;
- история;
- синхронизация состояния.
Эти правила проектирует само приложение.
Например, поверх WebSocket можно договориться передавать JSON:
{
"type": "message.create",
"requestId": "9f31",
"roomId": 42,
"text": "Привет"
}Сервер может ответить:
{
"type": "message.created",
"requestId": "9f31",
"messageId": 10582,
"status": "ok"
}То есть WebSocket отвечает за доставку сообщений через постоянный двусторонний канал, а смысл этих сообщений определяет прикладной протокол.
Поэтому крупные системы часто используют поверх WebSocket дополнительные протоколы и библиотеки: Socket.IO, STOMP, GraphQL subscriptions или собственную схему событий.
WebSocket, polling, long polling и SSE
WebSocket нужен не всегда. Иногда более простой HTTP-механизм решает задачу лучше.
| Подход | Направление | Постоянное соединение | Задержка | Типичный сценарий |
| Polling | клиент → сервер | нет | зависит от интервала | редкие проверки состояния |
| Long polling | в основном сервер → клиент через удерживаемый запрос | условно | низкая | совместимость со старой инфраструктурой |
| SSE | сервер → клиент | да | низкая | уведомления, ленты событий |
| WebSocket | клиент ↔ сервер | да | низкая | интерактивный двусторонний обмен |
Polling
Клиент каждые несколько секунд делает запрос:
GET /events
GET /events
GET /eventsПлюс — простота.
Минус — значительная часть запросов может возвращать «ничего нового».
Long polling
Сервер не отвечает сразу, а удерживает HTTP-запрос до появления события. После ответа клиент открывает новый запрос.
Это снижает количество пустых обращений, но жизненный цикл всё равно построен вокруг повторяющихся HTTP-запросов.
Server-Sent Events
SSE поддерживает длительный поток событий от сервера к браузеру. Если данные движутся в основном только сервер → клиент, SSE часто проще WebSocket.
Например, для ленты уведомлений или прогресса длительной серверной задачи двусторонний протокол может быть избыточным.
WebSocket
WebSocket особенно полезен, когда обе стороны часто инициируют передачу данных, а задержка должна оставаться низкой.
Чат — классический пример: сервер отправляет новые сообщения, а клиент одновременно отправляет сообщения, статусы прочтения, индикатор набора текста и другие события.
Почему WebSocket обычно эффективнее частого polling
После установки соединения не нужно для каждого маленького события заново отправлять полный набор HTTP-заголовков и выполнять отдельный request/response-цикл.
При частом обмене это даёт несколько практических преимуществ:
- меньше служебного трафика;
- меньше лишних HTTP-запросов;
- сервер может отправить событие сразу после его появления;
- соединение уже установлено, поэтому не требуется создавать новый логический цикл обмена для каждого события;
- бинарные данные можно передавать без текстового кодирования.
Но из этого не следует, что WebSocket «всегда быстрее HTTP». Если приложение делает один запрос раз в несколько минут, постоянное соединение может лишь усложнить архитектуру.
Преимущество WebSocket проявляется прежде всего при частом интерактивном обмене.
Сжатие permessage-deflate
WebSocket поддерживает расширения протокола. Одно из наиболее распространённых — permessage-deflate, определённое RFC 7692.
Клиент и сервер могут договориться о его использовании во время handshake через Sec-WebSocket-Extensions.
Идея проста:
JSON / текст
↓
DEFLATE
↓
WebSocket-фреймСжатие может заметно уменьшать объём больших текстовых сообщений, особенно JSON с повторяющимися именами полей.
Но оно не бесплатно. Компрессия расходует CPU и память, а её параметры требуют аккуратной настройки. Для маленьких сообщений накладные расходы могут свести выгоду к минимуму.
Ограничение классического WebSocket API — backpressure
У WebSocket есть менее очевидная практическая проблема: классический браузерный интерфейс WebSocket не предоставляет полноценный механизм обратного давления, или backpressure.
Представим, что сервер отправляет 100 000 сообщений в секунду, а интерфейс браузера успевает обработать только 10 000. Данные начинают накапливаться в очереди.
Если приложение не контролирует скорость потока, это может привести к:
- росту памяти;
- увеличению задержки;
- зависанию интерфейса;
- высокой загрузке CPU;
- аварийному завершению вкладки.
Поэтому серверная архитектура должна учитывать скорость конкретного клиента: объединять частые обновления, ограничивать очереди, отбрасывать устаревшие события или временно замедлять отправку.
MDN отдельно указывает, что классический WebSocket API не поддерживает backpressure. Экспериментальный WebSocketStream использует Streams API и умеет применять обратное давление, но не обладает такой же широкой совместимостью, как обычный WebSocket.
Что происходит при разрыве сети
WebSocket не гарантирует, что соединение будет существовать бесконечно.
Оно может исчезнуть из-за:
- смены Wi-Fi на мобильную сеть;
- ухода ноутбука в сон;
- перезапуска сервера;
- обновления приложения;
- таймаута прокси;
- NAT;
- временной потери связи;
- аварии балансировщика;
- закрытия вкладки.
Браузерный WebSocket API не выполняет автоматическое переподключение за приложение.
Типичная логика клиента выглядит так:
соединение потеряно
↓
подождать 1 секунду
↓
попытка подключения
↓
если не удалось — подождать 2 секунды
↓
затем 4, 8, 16...Такой подход называется exponential backoff.
После восстановления соединения нередко требуется ещё и восстановить логическое состояние: повторно авторизоваться, подписаться на комнаты, запросить пропущенные события или сверить последнюю известную версию данных.
Это уже задача приложения, а не протокола WebSocket.
Как сервер работает с тысячами WebSocket-соединений
Обычный HTTP-сервер часто думает в терминах коротких операций:
запрос → обработка → ответWebSocket-сервер должен дополнительно хранить множество открытых соединений.
Например:
user_1 → socket A
user_2 → socket B
user_3 → socket C
...
user_50000 → socket NКогда для user_3 появляется событие, сервер должен найти нужное соединение и отправить данные именно туда.
На одном сервере это относительно просто. В распределённой системе возникает дополнительная проблема:
Пользователь A → Server 1
Пользователь B → Server 3Если Server 1 хочет отправить сообщение пользователю B, ему нужно знать, что соединение B находится на Server 3.
Поэтому масштабируемые системы часто добавляют промежуточный слой:
WebSocket Server 1 ─┐
WebSocket Server 2 ─┼─ Redis / NATS / Kafka / другой broker
WebSocket Server 3 ─┘Брокер помогает распространять события между экземплярами приложения.
Сам WebSocket эту архитектуру не определяет. Он предоставляет только двусторонний канал между двумя конечными точками.
Безопасность WebSocket
Постоянное соединение не отменяет обычные требования веб-безопасности.
Использовать wss://
Для рабочих интернет-сервисов WebSocket должен передаваться через TLS. wss:// защищает содержимое соединения от пассивного перехвата и подмены в сети.
Проверять Origin
Браузер отправляет Origin при WebSocket handshake. Серверу стоит проверять его, если соединение предназначено только для доверенных веб-приложений.
Это особенно критично, когда авторизация основана на cookie. Браузер может включать учётные данные в handshake, поэтому сервер не должен считать сам факт наличия cookie достаточным подтверждением того, что соединение открыто с нужного сайта.
Аутентифицировать пользователя
Handshake устанавливает WebSocket-соединение, но не создаёт автоматически модель авторизации.
Сервер должен отдельно определить:
- кто подключился;
- к каким каналам пользователь имеет доступ;
- какие сообщения ему разрешено отправлять;
- какие события ему разрешено получать.
Валидировать сообщения
Любые данные от клиента нужно считать недоверенными.
Полезно ограничивать:
- размер одного сообщения;
- частоту сообщений;
- количество одновременных подписок;
- размер очереди исходящих событий;
- количество соединений на пользователя или IP.
Не путать маскирование с шифрованием
Маскирование клиентских фреймов — часть WebSocket-протокола, но оно не обеспечивает конфиденциальность.
За шифрование отвечает TLS при использовании wss://.
Когда WebSocket подходит, а когда нет
WebSocket стоит рассматривать, если выполняется хотя бы несколько условий:
- сервер должен сам инициировать частые обновления;
- клиент тоже регулярно отправляет события;
- задержка в сотни или тысячи миллисекунд заметна пользователю;
- соединение живёт долго;
- события небольшие, но приходят часто;
- требуется двусторонняя интерактивность.
Хорошие примеры — чат, multiplayer, совместный редактор или панель реального времени.
WebSocket может быть лишним, если приложение:
- делает редкие запросы;
- работает по классической CRUD-модели;
- получает только редкие серверные уведомления;
- не нуждается в двусторонней связи;
- может проще решить задачу SSE или обычным HTTP.
Выбор транспорта должен идти от характера обмена, а не от желания использовать «real-time» технологию.
Полная схема работы WebSocket
Весь процесс можно свести к одной последовательности:
1. Клиент открывает:
wss://example.com/ws
2. Устанавливаются:
TCP → TLS
3. Выполняется handshake:
HTTP-запрос → Upgrade / Extended CONNECT → подтверждение сервера
4. Состояние:
CONNECTING → OPEN
5. Начинается обмен:
client → frame → server
server → frame → client
6. При необходимости:
Ping ↔ Pong
или heartbeat приложения
7. Одна сторона завершает работу:
Close → Close
8. Состояние:
CLOSING → CLOSEDНа прикладном уровне всё это можно представить ещё проще:
HTTP помогает открыть дверь.
WebSocket оставляет дверь открытой.
Фреймы переносят сообщения в обе стороны.
Close аккуратно закрывает дверь.Резюме
WebSocket — это не «ускоренный HTTP» и не готовая система чатов. Это транспортный протокол для длительного полнодуплексного обмена сообщениями.
Его ключевые механизмы состоят из пяти частей:
- Handshake — согласование WebSocket-соединения.
- Фреймы — упаковка текстовых, бинарных и управляющих данных.
- Full-duplex — независимая передача сообщений в обе стороны.
- Ping/Pong и heartbeat — контроль жизнеспособности канала.
- Closing handshake — согласованное завершение соединения.
Для чатов, игр, торговых терминалов, совместного редактирования и других интерактивных систем WebSocket остаётся простым и широко поддерживаемым способом двусторонней связи в браузере. Для редких запросов, однонаправленных уведомлений или обычных CRUD-операций чаще достаточно HTTP или SSE.