Привет! Когда вы общаетесь в мессенджере, торгуете на бирже или смотрите, как меняется счетчик просмотров на стриме, за всем этим стоит одна технология - мгновенный обмен данными между браузером и сервером. HTTP-протокол для этого не подходит: каждый запрос требует нового соединения, а сервер не может сам “протолкнуть” данные клиенту. Тут и появляются WebSockets.
В этой статье разберем, как работает протокол WebSocket, чем он отличается от HTTP и как реализовать real-time обмен данными на Node.js. Если вы пропустили нашу прошлую статью про PostgreSQL или MySQL: какую СУБД выбрать?, рекомендую заглянуть.
Что такое WebSocket
WebSocket - это протокол полнодуплексной связи поверх TCP, описанный в RFC 6455. Он был стандартизирован в декабре 2011 года и с тех пор стал основным инструментом для real-time взаимодействия в браузере. Протокол позволяет серверу отправлять данные клиенту без запроса со стороны клиента, и наоборот - через одно постоянное TCP-соединение.
Главная проблема, которую решает WebSocket: в обычном HTTP сервер не может инициировать отправку данных. Чтобы получить обновления, клиенту приходится постоянно опрашивать сервер (polling), открывать длинные соединения (long polling) или использовать Server-Sent Events. Каждый из этих подходов добавляет задержку и увеличивает нагрузку.
WebSocket дает один канал, через который обе стороны могут отправлять сообщения в любой момент. Никаких повторных запросов, никаких лишних заголовков.
Где это используется
- Чаты и мессенджеры
- Онлайн-игры с множеством игроков
- Стриминг котировок на биржах
- Совместное редактирование документов
- Уведомления в реальном времени
- Live-обновления дашбордов и аналитики
HTTP vs WebSocket: в чем разница
Обычный HTTP работает по модели “запрос-ответ”. Клиент отправляет запрос, сервер отвечает, соединение закрывается. Если клиенту нужны обновления, ему нужно отправлять новый запрос.
WebSocket начинается с обычного HTTP-запроса. Клиент обращается к серверу с просьбой “переключить” протокол (Upgrade). Если сервер согласен, соединение превращается в постоянный двусторонний канал.

Вот ключевые отличия:
- Соединение: HTTP создает новое соединение для каждого запроса (или переиспользует через keep-alive с ограничениями). WebSocket держит одно соединение открытым все время.
- Направление: HTTP - только клиент к серверу. WebSocket - оба направления одновременно.
- Overhead: каждый HTTP-запрос несет полный набор заголовков (сотни байт и больше). После handshake WebSocket отправляет только минимальный фрейм (2-10 байт заголовка).
- Формат данных: HTTP работает с текстом. WebSocket поддерживает текст (UTF-8) и бинарные данные.
Когда WebSocket не нужен
Если вам достаточно односторонней отправки данных от сервера к клиенту (уведомления, логи), подойдут Server-Sent Events (SSE). Они проще в реализации и не требуют специального протокола. А если данные обновляются раз в минуту-пять, обычного polling с setInterval хватит за глаза.
Как работает протокол
Процесс установки WebSocket-соединения состоит из двух фаз: handshake (рукопожатие) и обмен данными через фреймы.
Handshake

Все начинается с обычного HTTP GET-запроса, в котором клиент просит сервер “апгрейднуть” соединение:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13Ключевые заголовки:
Upgrade: websocket- говорит серверу, что клиент хочет переключиться на WebSocketConnection: Upgrade- подтверждает намерение сменить протоколSec-WebSocket-Key- случайный nonce в base64, нужен серверу для подтверждения handshakeSec-WebSocket-Version: 13- версия протокола (13 - текущая и единственная актуальная)
Сервер отвечает статусом 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Заголовок Sec-WebSocket-Accept вычисляется так: берется значение Sec-WebSocket-Key, к нему добавляется GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, от результата берется SHA-1 хеш и кодируется в base64. Это гарантирует, что сервер действительно понял запрос и поддерживает WebSocket, а не просто вернул HTML-страницу.
После этого handshake завершен - HTTP больше не участвует, и начинается обмен фреймами.
Формат фреймов
Данные передаются в виде фреймов - небольших пакетов с заголовком и полезной нагрузкой. Каждый фрейм содержит:
- FIN (1 бит) - является ли этот фрейм последним в сообщении
- Opcode (4 бита) - тип данных:
0x1для текста,0x2для бинарных данных,0x8для закрытия,0x9для ping,0xAдля pong - MASK (1 бит) - замаскированы ли данные (клиент обязан маскировать свои сообщения)
- Payload length (7 бит или больше) - длина полезной нагрузки
- Masking key (32 бита) - ключ для демаскировки (только от клиента к серверу)
- Payload data - сами данные
Маскирование данных от клиента к серверу - это мера безопасности, которая предотвращает атаки подмены данных на прокси-серверах. Сервер отправляет данные без маски.
Ping/Pong: проверка жизни соединения

В любой момент после handshake любая сторона может отправить ping-фрейм (opcode 0x9). Получатель обязан ответить pong-фреймом (opcode 0xA). Это heartbeat-механизм: если pong не приходит, соединение считается разорванным.
Реализация на Node.js: библиотека ws
Самый популярный и легковесный вариант для Node.js - библиотека ws. Она имеет более 22 тысяч звезд на GitHub, стабильна и быстра. Для простых задач это лучший выбор.
Установка и простой сервер
npm install wsСоздаем echo-сервер, который отправляет обратно все полученные сообщения:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
ws.send('Echo: ' + data);
});
ws.send('Connected to WebSocket server');
});
console.log('WebSocket server started on ws://localhost:8080');Разберем, что тут происходит. Мы создаем WebSocket-сервер на порту 8080. При подключении нового клиента срабатывает событие connection, и мы получаем объект ws - это отдельное соединение с клиентом. Через ws.on('message', ...) слушаем входящие сообщения, а через ws.send() отправляем данные обратно.
Broadcast: отправка всем клиентам
Частая задача - отправить сообщение всем подключенным клиентам. Например, в чате каждый новый сообщение должно дойти до всех участников:
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});Здесь wss.clients - это Set всех подключенных клиентов. Мы перебираем их и отправляем сообщение каждому, кроме отправителя. Проверка readyState === WebSocket.OPEN гарантирует, что мы не пытаемся отправить данные в уже закрытое соединение.
Heartbeat: обнаружение разрывов
Соединение может оборваться “тихо” - без явного закрытия. Например, когда пользователь закрывает ноутбук или пропадает Wi-Fi. Чтобы обнаружить такие ситуации, используем ping/pong:
import { WebSocketServer } from 'ws';
function heartbeat() {
this.isAlive = true;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('error', console.error);
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});Каждые 30 секунд сервер отправляет ping всем клиентам. Если клиент не ответил pong, помечаем его как неактивного. На следующей проверке, если pong так и не пришел, соединение принудительно закрывается через terminate().
Клиентская часть в браузере
Браузерный API WebSocket предельно прост. Подключаемся, слушаем события, отправляем данные:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
</head>
<body>
<ul id="messages"></ul>
<form id="form">
<input id="input" autocomplete="off" />
<button>Отправить</button>
</form>
<script>
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', function() {
console.log('Connected to server');
});
socket.addEventListener('message', function(event) {
const item = document.createElement('li');
item.textContent = event.data;
document.getElementById('messages').appendChild(item);
});
socket.addEventListener('close', function() {
console.log('Disconnected from server');
});
document.getElementById('form').addEventListener('submit', function(e) {
e.preventDefault();
const input = document.getElementById('input');
if (input.value) {
socket.send(input.value);
input.value = '';
}
});
</script>
</body>
</html>Браузерная реализация поддерживается всеми современными браузерами с 2015 года. Схемы подключения: ws:// для обычного соединения и wss:// для зашифрованного (аналог http/https). В продакшене всегда используйте wss://.
Socket.IO: продвинутая альтернатива

Библиотека Socket.IO работает поверх WebSocket, но добавляет несколько важных возможностей:
- Автоматическое переподключение при разрыве связи
- Fallback на long-polling, если WebSocket недоступен
- Rooms - группировка клиентов для targeted-рассылки
- Namespaces - разделение логических каналов в одном соединении
- Подтверждение доставки сообщений (acknowledgements)
Пример сервера с rooms
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (room) => {
socket.join(room);
socket.to(room).emit('user-joined', socket.id);
});
socket.on('chat message', (room, msg) => {
io.to(room).emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});Rooms позволяют легко организовать групповой чат, где сообщения отправляются только участникам конкретной комнаты. Метод socket.join(room) добавляет клиента в комнату, io.to(room).emit() отправляет сообщение всем участникам.
Клиент Socket.IO
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('connect', () => {
socket.emit('join-room', 'general');
});
socket.on('chat message', (msg) => {
console.log('Message:', msg);
});
socket.on('user-joined', (userId) => {
console.log('User joined:', userId);
});
</script>ws или Socket.IO?
Выбор зависит от задачи:
- ws - если нужен чистый WebSocket без лишних абстракций, важна производительность и минимальный overhead. Подходит для внутренних сервисов, игр, стриминга.
- Socket.IO - если нужны rooms, auto-reconnect, broadcasting и fallback-механизмы. Удобен для чатов, коллаборативных инструментов, приложений с нестабильным соединением.
Практические советы
Аутентификация при подключении
WebSocket не поддерживает кастомные заголовки из браузера. Токены авторизации обычно передают через query-параметр или в рамках протокола (подпротокол):
// Клиент
const socket = new WebSocket('ws://localhost:8080?token=your-jwt-token');
// Сервер (ws)
wss.on('connection', function connection(ws, req) {
const token = new URL(req.url, 'ws://localhost').searchParams.get('token');
// Проверяем токен...
});Масштабирование
Один WebSocket-сервер не справится с тысячами одновременных подключений. Для горизонтального масштабирования используют sticky sessions (привязка клиента к конкретному серверу через load balancer) и Redis adapter для синхронизации сообщений между инстансами. Об этом мы подробно поговорим в будущей статье про WebSockets масштабирование.
Безопасность
- Всегда используйте
wss://в продакшене - Проверяйте
Originзаголовок при handshake, чтобы избежать CSRF-атак - Ограничивайте размер сообщений, чтобы защититься от DoS
- Валидируйте данные на сервере - никогда не доверяйте клиенту
Заключение
WebSocket - это не замена HTTP, а дополнение к нему. HTTP отлично справляется с запросами документов и API, а WebSocket берет на себя задачи, где нужна мгновенная двусторонняя связь. Протокол прост в понимании, а библиотеки вроде ws и Socket.IO делают реализацию еще проще.
Если вашему приложению нужны real-time обновления - чат, уведомления, live-данные - WebSocket будет правильным выбором. А если данные обновляются редко и направление только сервер-клиент, посмотрите в сторону SSE или обычного polling. Далее оставляю диаграмму по которой проще будет определить какую технологию выбрать для приложения.

📩 Нужна помощь с разработкой? Напишите нам - мы поможем реализовать ваш проект.


Обязательные поля помечены *