WebSocket на TypeScript: продвинутые техники real-time приложений

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                

Блоги

Рекомендаций по книгам