Разработка WebSocket API для веб-приложения
WebSocket — протокол, обеспечивающий двунаправленный канал связи между клиентом и сервером по одному TCP-соединению. Подходит для: realtime-чата, live-уведомлений, совместного редактирования, биржевых котировок, онлайн-игр.
Когда нужен WebSocket
| Задача | WebSocket | Polling | SSE |
|---|---|---|---|
| Чат, совместная работа | ✓ | Плохо | Нет (однонаправленный) |
| Уведомления | ✓ | Приемлемо | ✓ |
| Котировки/аналитика | ✓ | Плохо | ✓ |
| Загрузка файла с прогрессом | Нет нужды | Нет нужды | ✓ |
| REST CRUD | Нет нужды | — | — |
Базовая реализация (Node.js + ws)
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
const server = createServer(app);
const wss = new WebSocketServer({ server });
// Хранение соединений: комната → Set<WebSocket>
const rooms = new Map<string, Set<WebSocket>>();
wss.on('connection', (ws, req) => {
const roomId = new URL(req.url!, 'http://x').searchParams.get('room');
if (!roomId) return ws.close(4000, 'Missing room');
// Добавляем в комнату
if (!rooms.has(roomId)) rooms.set(roomId, new Set());
rooms.get(roomId)!.add(ws);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
// Broadcast в комнату
rooms.get(roomId)?.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
});
ws.on('close', () => {
rooms.get(roomId)?.delete(ws);
});
});
Протокол сообщений
Структурированный JSON-протокол вместо raw текста:
// Типы сообщений
type WSMessage =
| { type: 'join_room'; roomId: string; userId: string }
| { type: 'message'; roomId: string; text: string; timestamp: number }
| { type: 'typing'; roomId: string; userId: string }
| { type: 'error'; code: string; message: string };
Аутентификация
WebSocket не поддерживает кастомные заголовки при handshake. Варианты:
-
Token в query string —
ws://api.com/ws?token=eyJ...(видно в логах, менее безопасно) -
Cookie — при ws:// на том же домене,
withCredentials: true - First message auth — первое сообщение после подключения — аутентификация
ws.on('connection', (socket) => {
let authenticated = false;
const authTimeout = setTimeout(() => {
if (!authenticated) socket.close(4001, 'Auth timeout');
}, 5000);
socket.once('message', (data) => {
const { type, token } = JSON.parse(data.toString());
if (type === 'auth' && validateToken(token)) {
authenticated = true;
clearTimeout(authTimeout);
socket.send(JSON.stringify({ type: 'auth_success' }));
} else {
socket.close(4001, 'Invalid token');
}
});
});
Горизонтальное масштабирование
Проблема: при нескольких серверах клиент A подключён к серверу 1, клиент B — к серверу 2. Сообщение от A не дойдёт до B.
Решение — Redis Pub/Sub для межсерверной коммуникации:
import { createClient } from 'redis';
const pub = createClient();
const sub = createClient();
// Сервер получает сообщение от клиента
ws.on('message', async (data) => {
await pub.publish(`room:${roomId}`, data.toString());
});
// Все серверы подписаны и доставляют своим клиентам
sub.subscribe(`room:${roomId}`, (message) => {
rooms.get(roomId)?.forEach(client => {
if (client.readyState === WebSocket.OPEN) client.send(message);
});
});
Socket.io vs Native WebSocket
Socket.io — надстройка над WebSocket с fallback на long polling, автоматическим переподключением, комнатами, namespace и acks:
io.to(roomId).emit('message', { text: 'Hello' }); // вместо ручного broadcast
Native WebSocket — меньше overhead, полный контроль, нет лишних абстракций. Рекомендуется при > 10K соединений или когда важна задержка.
Heartbeat и reconnect
Браузеры и прокси закрывают idle-соединения. Ping/pong каждые 30 секунд:
wss.on('connection', (ws) => {
let alive = true;
ws.on('pong', () => { alive = true; });
const interval = setInterval(() => {
if (!alive) return ws.terminate();
alive = false;
ws.ping();
}, 30000);
ws.on('close', () => clearInterval(interval));
});
Сроки
WebSocket-сервер с комнатами, аутентификацией, Redis Pub/Sub: 1–2 недели. С Socket.io, протоколом сообщений, обработкой reconnect и тестами: 2–3 недели.







