Антон Ларичев

Введение
WebSocket — это полнодуплексный протокол поверх TCP, который позволяет серверу и клиенту обмениваться сообщениями в реальном времени через одно постоянное соединение. В отличие от HTTP-запросов и long-polling, WebSocket экономит трафик и снижает задержку до миллисекунд, что делает его незаменимым для чатов, игровых лобби, биржевых тикеров и систем уведомлений.
В статье разберём, как поднять WebSocket-сервер на Node.js с помощью библиотеки ws, написать минимальный чат с комнатами, добавить heartbeat-проверку живых соединений и реализовать broadcast-уведомления. Все примеры рассчитаны на Node.js 20+ и работают без дополнительных фреймворков.
Как устроен WebSocket
Клиент инициирует обычный HTTP-запрос с заголовком Upgrade: websocket. Если сервер согласен, он отвечает кодом 101 Switching Protocols, после чего TCP-соединение переключается в режим обмена бинарными фреймами. Дальше обе стороны могут отправлять данные в любой момент — никакого polling.
Важные моменты, о которых часто забывают:
- WebSocket не имеет встроенного авто-реконнекта — это задача клиента.
- Прокси и балансировщики могут резать idle-соединения через 60 секунд.
- Браузерный
WebSocketне передаёт кастомные заголовки, поэтому токен авторизации обычно прокидывают через query-параметр или subprotocol.
Базовый сервер на ws
Установим библиотеку: npm install ws. Затем создадим простой сервер, который принимает соединения и отвечает эхо-сообщением.
import { WebSocketServer } from 'ws';
// Создаём WebSocket-сервер на порту 8080
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
// Логируем IP подключившегося клиента
console.log('Новое подключение:', req.socket.remoteAddress);
ws.on('message', (data) => {
// data приходит как Buffer, приводим к строке
const text = data.toString();
console.log('Получено:', text);
ws.send(`echo: ${text}`);
});
ws.on('close', () => {
console.log('Клиент отключился');
});
});
Это рабочий сервер, но он держит соединения вечно и не умеет рассылать сообщения нескольким клиентам. Перейдём к чату.
Чат с комнатами
Реализуем простой чат, в котором клиент при подключении указывает комнату через query-параметр, а сервер раздаёт сообщения только участникам этой комнаты.
import { WebSocketServer } from 'ws';
import { parse } from 'url';
const wss = new WebSocketServer({ port: 8080 });
// Храним соединения по комнатам: Map<roomId, Set<WebSocket>>
const rooms = new Map();
wss.on('connection', (ws, req) => {
const { query } = parse(req.url, true);
const room = query.room || 'general';
// Регистрируем клиента в комнате
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(ws);
ws.room = room;
ws.on('message', (raw) => {
const message = raw.toString();
// Рассылаем сообщение всем, кроме отправителя
for (const client of rooms.get(room)) {
if (client !== ws && client.readyState === ws.OPEN) {
client.send(JSON.stringify({ room, message }));
}
}
});
ws.on('close', () => {
// Убираем клиента и чистим пустую комнату
rooms.get(room).delete(ws);
if (rooms.get(room).size === 0) rooms.delete(room);
});
});
Клиент подключается строкой new WebSocket('ws://localhost:8080?room=js-news'). Такой подход масштабируется на тысячи соединений в одном Node.js-процессе, а для горизонтального масштабирования между инстансами обычно подключают Redis Pub/Sub.
Heartbeat и обнаружение мёртвых соединений
TCP-соединение может «зависнуть» — клиент потерял сеть, но сервер ещё не знает об этом. Решается это ping/pong-фреймами на уровне протокола.
function heartbeat() {
// Помечаем соединение как живое при получении pong
this.isAlive = true;
}
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', heartbeat);
});
// Каждые 30 секунд проверяем все соединения
const interval = setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
// Клиент не ответил на прошлый ping — закрываем
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30000);
wss.on('close', () => clearInterval(interval));
Этот паттерн — стандарт де-факто из документации ws. Без него у вас постепенно накопятся «зомби»-соединения, и сервер начнёт жрать память.
Real-time уведомления
Для уведомлений нужен broadcast по конкретному пользователю, а не по комнате. Привяжем сокет к userId после авторизации и сделаем функцию отправки.
const userSockets = new Map(); // userId -> Set<WebSocket>
wss.on('connection', (ws, req) => {
const { query } = parse(req.url, true);
// Проверяем JWT-токен, выдаём userId
const userId = verifyToken(query.token);
if (!userId) return ws.close(4001, 'Unauthorized');
if (!userSockets.has(userId)) userSockets.set(userId, new Set());
userSockets.get(userId).add(ws);
ws.on('close', () => userSockets.get(userId).delete(ws));
});
// Вызываем из любого места приложения
export function notify(userId, payload) {
const sockets = userSockets.get(userId);
if (!sockets) return;
const data = JSON.stringify(payload);
for (const ws of sockets) {
if (ws.readyState === ws.OPEN) ws.send(data);
}
}
Теперь любой сервис (создание заказа, новое сообщение, изменение статуса) может позвать notify(userId, {...}) и пользователь моментально увидит событие в UI.
Частые ошибки
- Передача токена в заголовке. Браузер не умеет добавлять
Authorizationк WebSocket-запросу. Используйте query-параметр или subprotocol и сразу закрывайте соединение при невалидном токене. - Отсутствие heartbeat. Без ping/pong зависшие клиенты копятся часами и съедают память.
- Broadcast без проверки
readyState. Если отправить в закрывающийся сокет, получите исключение и уроните весь цикл рассылки. - Хранение state только в памяти процесса. При перезапуске или нескольких инстансах за балансировщиком сообщения теряются. Решение — Redis Pub/Sub или отдельный сервис состояния.
- Игнорирование размера сообщения. По умолчанию
wsпринимает фреймы до 100 МБ. Установите лимит через опциюmaxPayload, иначе один клиент уронит сервер.
Заключение
WebSocket на Node.js закрывает почти весь спектр real-time задач: от чатов до биржевых лент. Библиотека ws даёт минималистичный, но достаточный API, а связка с heartbeat и Redis Pub/Sub позволяет дорасти до продакшена с десятками тысяч одновременных соединений. Начните с простого эхо-сервера, добавьте комнаты, прикрутите авторизацию и мониторинг живых соединений — и у вас будет полноценная real-time платформа без тяжёлых фреймворков.






Комментарии
0