WebSocket на TypeScript: Строим продвинутые real-time приложения
Real-time приложения стали неотъемлемой частью современного веба. От чатов и уведомлений до онлайн-игр и торговых платформ — технология WebSocket обеспечивает постоянное двустороннее соединение между клиентом и сервером. На продвинутом уровне разработка таких систем требует не только понимания протокола, но и умения строить отказоустойчивые, масштабируемые архитектуры. В этой статье мы разберем, как создавать production-grade WebSocket приложения на TypeScript, используя лучшие практики и паттерны.
1. Архитектура и кластеризация WebSocket сервера
Одиночный сервер WebSocket не выдержит нагрузки реального проекта. Ключевая задача — организовать кластеризацию и обмен сообщениями между инстансами. Рассмотрим архитектуру с использованием ws библиотеки и шины событий Redis.
Установим зависимости:
npm install ws redis ioredis @types/ws @types/ioredis
Создадим базовый кластеризованный сервер:
import { WebSocketServer, WebSocket } from 'ws';
import Redis from 'ioredis';
const redisPub = new Redis();
const redisSub = new Redis();
const wss = new WebSocketServer({ port: 8080 });
// Канал для межсерверной коммуникации
const CHANNEL = 'ws:messages';
redisSub.subscribe(CHANNEL, (err, count) => {
if (err) console.error('Ошибка подписки:', err);
console.log(`Подписан на ${count} каналов`);
});
// Получение сообщений из других инстансов
redisSub.on('message', (channel, message) => {
const parsed = JSON.parse(message);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(parsed.data);
}
});
});
wss.on('connection', (ws) => {
console.log('Новое соединение');
ws.on('message', (data) => {
const message = data.toString();
// Публикуем в Redis для всех инстансов
redisPub.publish(CHANNEL, JSON.stringify({ data: message }));
});
});
Преимущества:
- Горизонтальное масштабирование — добавляйте сколько угодно серверов
- Отказоустойчивость — при падении одного инстанса другие продолжают работу
- Единая шина сообщений через Redis Pub/Sub
2. Аутентификация и управление сессиями
В production-приложениях нельзя доверять неаутентифицированным соединениям. Реализуем проверку токенов через JWT при установке соединения.
Установим jsonwebtoken:
npm install jsonwebtoken @types/jsonwebtoken
Создадим middleware для аутентификации WebSocket:
import { IncomingMessage } from 'http';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
interface AuthenticatedSocket extends WebSocket {
userId?: string;
}
function authenticate(ws: AuthenticatedSocket, req: IncomingMessage): boolean {
const url = new URL(req.url || '', 'http://localhost');
const token = url.searchParams.get('token');
if (!token) {
ws.close(4001, 'Токен не предоставлен');
return false;
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
ws.userId = decoded.userId;
console.log(`Пользователь ${decoded.userId} аутентифицирован`);
return true;
} catch (err) {
ws.close(4002, 'Невалидный токен');
return false;
}
}
// Использование
wss.on('connection', (ws: AuthenticatedSocket, req) => {
if (!authenticate(ws, req)) return;
ws.on('message', (data) => {
// Теперь мы знаем userId
console.log(`Сообщение от ${ws.userId}: ${data}`);
});
});
Рекомендации:
- Передавайте токен через query-параметр или заголовки при установке соединения
- Используйте короткоживущие access-токены и refresh-токены
- Храните сессии в Redis для быстрого доступа
3. Продвинутая обработка сообщений и комнаты
Для чатов, уведомлений и коллаборативных инструментов необходима поддержка комнат (каналов). Реализуем систему подписки на каналы.
import { WebSocket } from 'ws';
interface RoomSocket extends WebSocket {
rooms: Set<string>;
userId: string;
}
class RoomManager {
private rooms: Map<string, Set<RoomSocket>> = new Map();
private userRooms: Map<string, Set<string>> = new Map();
joinRoom(ws: RoomSocket, roomName: string): void {
ws.rooms.add(roomName);
if (!this.rooms.has(roomName)) {
this.rooms.set(roomName, new Set());
}
this.rooms.get(roomName)!.add(ws);
if (!this.userRooms.has(ws.userId)) {
this.userRooms.set(ws.userId, new Set());
}
this.userRooms.get(ws.userId)!.add(roomName);
ws.send(JSON.stringify({ type: 'joined', room: roomName }));
}
leaveRoom(ws: RoomSocket, roomName: string): void {
ws.rooms.delete(roomName);
this.rooms.get(roomName)?.delete(ws);
this.userRooms.get(ws.userId)?.delete(roomName);
}
broadcast(roomName: string, message: string, exclude?: RoomSocket): void {
const room = this.rooms.get(roomName);
if (!room) return;
room.forEach((client) => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
removeUser(ws: RoomSocket): void {
ws.rooms.forEach((room) => this.leaveRoom(ws, room));
}
}
const roomManager = new RoomManager();
wss.on('connection', (ws: RoomSocket) => {
ws.rooms = new Set();
ws.on('message', (data) => {
const parsed = JSON.parse(data.toString());
switch (parsed.action) {
case 'join':
roomManager.joinRoom(ws, parsed.room);
break;
case 'leave':
roomManager.leaveRoom(ws, parsed.room);
break;
case 'message':
roomManager.broadcast(parsed.room, data, ws);
break;
}
});
ws.on('close', () => {
roomManager.removeUser(ws);
});
});
Ключевые возможности:
- Динамическое создание и удаление комнат
- Таргетированная рассылка сообщений
- Автоматическая очистка при отключении клиента