20. WebSockets (ws)

WebSockets — протокол двусторонней связи между клиентом и сервером в реальном времени. В отличие от HTTP, соединение постоянное.
HTTP vs WebSocket
Заголовок раздела «HTTP vs WebSocket»HTTP (request-response):Клиент → Запрос → СерверКлиент ← Ответ ← Сервер(соединение закрывается)
WebSocket (двусторонний):Клиент ←→ Соединение ←→ Сервер(постоянное, обе стороны могут отправлять в любой момент)Когда использовать:
- Чаты и мессенджеры
- Игры реального времени
- Биржевые котировки, трекинг
- Уведомления в реальном времени
- Совместное редактирование
Установка
Заголовок раздела «Установка»npm install wsБазовый WebSocket сервер
Заголовок раздела «Базовый WebSocket сервер»const { WebSocketServer } = require('ws');const http = require('http');const express = require('express');
const app = express();const server = http.createServer(app);
// WebSocket сервер привязан к HTTP серверуconst wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => { const ip = req.socket.remoteAddress; console.log(`Новое подключение: ${ip}`);
// Отправить сообщение клиенту ws.send(JSON.stringify({ type: 'welcome', message: 'Подключено!' }));
// Получить сообщение от клиента ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); console.log('Получено:', message);
// Ответить этому клиенту ws.send(JSON.stringify({ type: 'echo', data: message })); } catch (err) { ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); } });
// Клиент отключился ws.on('close', (code, reason) => { console.log(`Отключён: ${code} ${reason}`); });
// Ошибка соединения ws.on('error', (err) => { console.error('WS ошибка:', err.message); });});
server.listen(3000, () => { console.log('HTTP + WS сервер на порту 3000');});Broadcast — отправка всем
Заголовок раздела «Broadcast — отправка всем»// Отправить сообщение всем подключённым клиентамfunction broadcast(data, excludeWs = null) { const message = JSON.stringify(data); wss.clients.forEach((client) => { if (client !== excludeWs && client.readyState === WebSocket.OPEN) { client.send(message); } });}
wss.on('connection', (ws) => { // Уведомить остальных о новом подключении broadcast({ type: 'user_joined', count: wss.clients.size }, ws);
ws.on('message', (data) => { const msg = JSON.parse(data.toString());
if (msg.type === 'chat') { // Отправить всем (включая отправителя) broadcast({ type: 'chat', user: msg.user, text: msg.text, timestamp: Date.now(), }); } });
ws.on('close', () => { broadcast({ type: 'user_left', count: wss.clients.size }); });});Комнаты (rooms)
Заголовок раздела «Комнаты (rooms)»// Простая реализация комнатconst rooms = new Map(); // roomId → Set<ws>
function joinRoom(ws, roomId) { if (!rooms.has(roomId)) rooms.set(roomId, new Set()); rooms.get(roomId).add(ws); ws.roomId = roomId;}
function leaveRoom(ws) { const room = rooms.get(ws.roomId); if (room) { room.delete(ws); if (room.size === 0) rooms.delete(ws.roomId); }}
function broadcastToRoom(roomId, data, excludeWs = null) { const room = rooms.get(roomId); if (!room) return; const message = JSON.stringify(data); room.forEach((client) => { if (client !== excludeWs && client.readyState === WebSocket.OPEN) { client.send(message); } });}
wss.on('connection', (ws) => { ws.on('message', (raw) => { const msg = JSON.parse(raw.toString());
switch (msg.type) { case 'join': leaveRoom(ws); joinRoom(ws, msg.roomId); broadcastToRoom(msg.roomId, { type: 'system', text: `${msg.username} присоединился`, }); break;
case 'chat': broadcastToRoom(ws.roomId, { type: 'chat', user: msg.user, text: msg.text, timestamp: Date.now(), }); break;
case 'leave': broadcastToRoom(ws.roomId, { type: 'system', text: `${msg.username} вышел`, }, ws); leaveRoom(ws); break; } });
ws.on('close', () => leaveRoom(ws));});Аутентификация WebSocket
Заголовок раздела «Аутентификация WebSocket»const jwt = require('jsonwebtoken');const url = require('url');
const wss = new WebSocketServer({ server, // Проверяем токен до установки соединения verifyClient: (info, callback) => { const params = new URL(info.req.url, 'http://localhost').searchParams; const token = params.get('token');
if (!token) { callback(false, 401, 'Токен не предоставлен'); return; }
try { const payload = jwt.verify(token, process.env.JWT_SECRET); info.req.user = payload; // сохраняем данные юзера callback(true); } catch { callback(false, 401, 'Неверный токен'); } },});
wss.on('connection', (ws, req) => { console.log(`Пользователь ${req.user.userId} подключился`); ws.userId = req.user.userId;});
// Клиент подключается так:// new WebSocket('ws://localhost:3000?token=eyJhbGci...');Heartbeat — проверка живых соединений
Заголовок раздела «Heartbeat — проверка живых соединений»// Ping-pong для обнаружения отвалившихся клиентовfunction heartbeat() { this.isAlive = true;}
wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', heartbeat); // клиент ответил на ping});
// Каждые 30 сек проверяемconst interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) { console.log('Клиент не отвечает — отключаем'); return ws.terminate(); } ws.isAlive = false; ws.ping(); // отправляем ping });}, 30000);
wss.on('close', () => clearInterval(interval));Клиент (браузер)
Заголовок раздела «Клиент (браузер)»// Подключение с автоматическим переподключениемclass WebSocketClient { constructor(url) { this.url = url; this.handlers = new Map(); this.connect(); }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { console.log('WebSocket подключён'); };
this.ws.onmessage = (event) => { const data = JSON.parse(event.data); const handler = this.handlers.get(data.type); if (handler) handler(data); };
this.ws.onclose = () => { console.log('Переподключение через 3 сек...'); setTimeout(() => this.connect(), 3000); };
this.ws.onerror = (err) => { console.error('WS ошибка:', err); }; }
on(type, handler) { this.handlers.set(type, handler); }
send(type, data = {}) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, ...data })); } }}
// Использованиеconst client = new WebSocketClient('ws://localhost:3000?token=...');client.on('chat', (msg) => console.log(`${msg.user}: ${msg.text}`));client.send('chat', { user: 'Яша', text: 'Привет!' });Практика
Заголовок раздела «Практика»- Создай базовый WebSocket сервер с echo (ответ тем же сообщением)
- Реализуй broadcast — отправка всем подключённым клиентам
- Добавь комнаты: join, leave, chat внутри комнаты
- Подключи JWT аутентификацию через query параметр token
- Реализуй heartbeat ping/pong для обнаружения отвалившихся соединений