Розробка WebSocket API для веб-додатку
WebSocket — протокол, що забезпечує двонаправлений канал зв'язку між клієнтом і сервером по одному TCP-з'єднанню. Підходить для: real-time-чату, 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 проти нативного WebSocket
Socket.io — надстройка над WebSocket з fallback на long polling, автоматичним переподключенням, кімнатами, namespace й acks:
io.to(roomId).emit('message', { text: 'Hello' }); // замість ручного broadcast
Нативний 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 тижні.







