Перейти к содержимому

20. WebSockets (ws)

Иллюстрация к уроку

WebSockets — протокол двусторонней связи между клиентом и сервером в реальном времени. В отличие от HTTP, соединение постоянное.

HTTP (request-response):
Клиент → Запрос → Сервер
Клиент ← Ответ ← Сервер
(соединение закрывается)
WebSocket (двусторонний):
Клиент ←→ Соединение ←→ Сервер
(постоянное, обе стороны могут отправлять в любой момент)

Когда использовать:

  • Чаты и мессенджеры
  • Игры реального времени
  • Биржевые котировки, трекинг
  • Уведомления в реальном времени
  • Совместное редактирование
Окно терминала
npm install ws
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');
});
// Отправить сообщение всем подключённым клиентам
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 });
});
});
// Простая реализация комнат
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));
});
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...');
// 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: 'Привет!' });
  1. Создай базовый WebSocket сервер с echo (ответ тем же сообщением)
  2. Реализуй broadcast — отправка всем подключённым клиентам
  3. Добавь комнаты: join, leave, chat внутри комнаты
  4. Подключи JWT аутентификацию через query параметр token
  5. Реализуй heartbeat ping/pong для обнаружения отвалившихся соединений