WebSocket на TypeScript: продвинутые техники для real-time приложений
WebSocket предоставляет постоянное двунаправленное соединение между клиентом и сервером, что делает его идеальным для real-time приложений: чатов, уведомлений, совместного редактирования и онлайн-игр. В этой статье мы рассмотрим продвинутые техники работы с WebSocket на TypeScript, выходящие за рамки простого echo-сервера.
1. Архитектура с управлением комнатами (Rooms)
В реальных приложениях редко используется глобальная рассылка всем клиентам. Гораздо чаще требуется логическое разделение клиентов по комнатам (например, чат-комнаты или игровые сессии). Рассмотрим реализацию на TypeScript с использованием библиотеки ws.
import { WebSocketServer, WebSocket } from 'ws';
interface Room {
id: string;
clients: Set<WebSocket>;
}
class RoomManager {
private rooms: Map<string, Room> = new Map();
createRoom(roomId: string): void {
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, { id: roomId, clients: new Set() });
}
}
joinRoom(roomId: string, client: WebSocket): void {
this.createRoom(roomId);
this.rooms.get(roomId)!.clients.add(client);
client.send(JSON.stringify({ type: 'room_joined', roomId }));
}
leaveRoom(roomId: string, client: WebSocket): void {
const room = this.rooms.get(roomId);
if (room) {
room.clients.delete(client);
if (room.clients.size === 0) {
this.rooms.delete(roomId);
}
}
}
broadcastToRoom(roomId: string, message: object, sender?: WebSocket): void {
const room = this.rooms.get(roomId);
if (room) {
const data = JSON.stringify(message);
room.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN && client !== sender) {
client.send(data);
}
});
}
}
}
const wss = new WebSocketServer({ port: 8080 });
const roomManager = new RoomManager();
wss.on('connection', (ws: WebSocket) => {
ws.on('message', (raw: Buffer) => {
try {
const msg = JSON.parse(raw.toString());
switch (msg.type) {
case 'join':
roomManager.joinRoom(msg.roomId, ws);
break;
case 'leave':
roomManager.leaveRoom(msg.roomId, ws);
break;
case 'message':
roomManager.broadcastToRoom(msg.roomId,
{ type: 'chat', user: msg.user, text: msg.text }, ws);
break;
}
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
}
});
ws.on('close', () => {
// Очистка: удаляем клиента из всех комнат
roomManager.leaveRoom('*', ws);
});
});
Этот код демонстрирует безопасную работу с комнатами: создание по требованию, автоматическую очистку пустых комнат и исключение отправителя из рассылки.
2. Аутентификация и управление состоянием
WebSocket соединения должны быть аутентифицированы. Продвинутый подход — проверка токена при установке соединения и поддержание сессии.
import { IncomingMessage } from 'http';
import { WebSocketServer, WebSocket } from 'ws';
interface AuthenticatedSocket extends WebSocket {
userId?: string;
username?: string;
}
const wss = new WebSocketServer({
port: 8080,
verifyClient: (info: { req: IncomingMessage }, callback: (res: boolean) => void) => {
const token = extractToken(info.req.url || '');
if (token && validateToken(token)) {
callback(true);
} else {
callback(false);
}
}
});
function extractToken(url: string): string | null {
const params = new URLSearchParams(url.split('?')[1] || '');
return params.get('token');
}
function validateToken(token: string): boolean {
// Здесь должна быть реальная проверка JWT или другого токена
return token.length > 10;
}
wss.on('connection', (ws: AuthenticatedSocket, req: IncomingMessage) => {
const token = extractToken(req.url || '');
const payload = decodeToken(token!);
ws.userId = payload.userId;
ws.username = payload.username;
ws.on('message', (data: Buffer) => {
const msg = JSON.parse(data.toString());
// Все сообщения теперь содержат информацию о пользователе
handleMessage(ws, msg);
});
});
function decodeToken(token: string): { userId: string; username: string } {
// Декодирование JWT или другого токена
return { userId: '123', username: 'user' };
}
Важные моменты:
- Токен передается в query-параметрах URL при установке соединения
- Функция
verifyClientпозволяет отклонить соединение до его установки - Расширение интерфейса
WebSocketдля хранения данных сессии
3. Обработка разрывов и реконнект
Стабильность real-time приложения критична. Реализуем механизм автоматического восстановления соединения на клиенте и сервере.
// Серверная часть: сохранение состояния для восстановления
class SessionStore {
private sessions: Map<string, { userId: string; rooms: string[] }> = new Map();
saveSession(socketId: string, data: { userId: string; rooms: string[] }): void {
this.sessions.set(socketId, data);
}
restoreSession(socketId: string): { userId: string; rooms: string[] } | null {
return this.sessions.get(socketId) || null;
}
removeSession(socketId: string): void {
this.sessions.delete(socketId);
}
}
// Клиентская часть на TypeScript
class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private token: string;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000; // ms
constructor(url: string, token: string) {
this.url = url;
this.token = token;
}
connect(): void {
this.ws = new WebSocket(`${this.url}?token=${this.token}`);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
};
this.ws.onclose = (event: CloseEvent) => {
if (event.code !== 1000) { // Не штатное закрытие
this.scheduleReconnect();
}
};
this.ws.onerror = (error: Event) => {
console.error('WebSocket error:', error);
this.ws?.close();
};
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnect