← Назад к вопросам

Какие задачи решал на WebSocket?

1.8 Middle🔥 191 комментариев
#API и сетевые протоколы#Node.js и JavaScript#Soft skills и опыт работы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Задачи, решенные с помощью WebSocket

WebSocket — это технология для двусторонней коммуникации в реальном времени. За годы работы я использовал её для разных сценариев.

1. Real-Time Уведомления в CRM системе

Задача: Сотрудники команды должны видеть изменения в контактах других сотрудников мгновенно, без перезагрузки страницы.

Решение с WebSocket:

// backend/websocket-server.ts
import { Server, Socket } from 'socket.io';
import http from 'http';
import express from 'express';

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: { origin: '*' },
});

// Хранилище активных пользователей
const activeUsers = new Map<string, Set<string>>();

io.on('connection', (socket: Socket) => {
  const userId = socket.handshake.query.userId as string;
  
  // Пользователь подписывается на обновления контакта
  socket.on('subscribe_contact', (contactId: string) => {
    socket.join(`contact:${contactId}`);
  });
  
  // Когда контакт обновлен в БД, отправляем update всем подписанным
  socket.on('update_contact', async (data: { contactId: string; changes: any }) => {
    // Проверка прав доступа
    if (!await hasAccessToContact(userId, data.contactId)) {
      socket.emit('error', 'No access');
      return;
    }
    
    // Сохраняем в БД
    await db.contacts.update(data.contactId, data.changes);
    
    // Отправляем update всем, кто подписан на этот контакт
    io.to(`contact:${data.contactId}`).emit('contact_updated', {
      contactId: data.contactId,
      changes: data.changes,
      updatedBy: userId,
      timestamp: new Date(),
    });
  });
  
  socket.on('disconnect', () => {
    console.log(`User ${userId} disconnected`);
  });
});

server.listen(3001, () => console.log('WebSocket server on 3001'));

Frontend (React):

import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';

function ContactDetails({ contactId }: { contactId: string }) {
  const [contact, setContact] = useState(null);
  const [socket, setSocket] = useState(null);
  
  useEffect(() => {
    const newSocket = io('http://localhost:3001', {
      query: { userId: currentUser.id },
    });
    setSocket(newSocket);
    
    // Подписываемся на обновления контакта
    newSocket.emit('subscribe_contact', contactId);
    
    // Слушаем обновления
    newSocket.on('contact_updated', (data) => {
      setContact(prev => ({ ...prev, ...data.changes }));
      // Показываем уведомление
      toast.info(`Contact updated by ${data.updatedBy}`);
    });
    
    return () => newSocket.disconnect();
  }, [contactId]);
  
  const handleUpdate = (changes) => {
    socket.emit('update_contact', { contactId, changes });
  };
  
  return (
    <div>
      {/* UI для отображения контакта */}
    </div>
  );
}

2. Multiplayer Game — Real-Time Players Position

Задача: В браузер-игре показывать позиции других игроков в реальном времени.

Решение:

// backend/game-server.ts
const TICK_RATE = 60; // 60 обновлений в секунду
const activeSessions = new Map<string, GameSession>();

class GameSession {
  players: Map<string, Player> = new Map();
  gameId: string;
  
  addPlayer(socket: Socket, playerId: string, name: string) {
    const player = new Player(socket, playerId, name);
    this.players.set(playerId, player);
    
    // Отправляем новому игроку всех существующих
    socket.emit('initial_state', {
      players: Array.from(this.players.values()).map(p => ({
        id: p.id,
        x: p.x,
        y: p.y,
        name: p.name,
      })),
    });
    
    // Уведомляем остальных о новом игроке
    socket.broadcast.to(this.gameId).emit('player_joined', {
      id: playerId,
      name: name,
      x: player.x,
      y: player.y,
    });
  }
  
  updatePlayerPosition(playerId: string, x: number, y: number) {
    const player = this.players.get(playerId);
    if (!player) return;
    
    player.x = x;
    player.y = y;
  }
  
  broadcastGameState() {
    const state = Array.from(this.players.values()).map(p => ({
      id: p.id,
      x: p.x,
      y: p.y,
    }));
    
    // Отправляем состояние всем игрокам в сессии
    io.to(this.gameId).emit('game_state', state);
  }
}

// Обновляем состояние 60 раз в секунду
setInterval(() => {
  activeSessions.forEach(session => {
    session.broadcastGameState();
  });
}, 1000 / TICK_RATE);

io.on('connection', (socket: Socket) => {
  const gameId = socket.handshake.query.gameId as string;
  const playerId = socket.handshake.query.playerId as string;
  
  socket.join(gameId);
  
  if (!activeSessions.has(gameId)) {
    activeSessions.set(gameId, new GameSession(gameId));
  }
  
  const session = activeSessions.get(gameId)!;
  session.addPlayer(socket, playerId, socket.handshake.query.playerName);
  
  // Получаем обновления позиции игрока
  socket.on('move', (data: { x: number; y: number }) => {
    session.updatePlayerPosition(playerId, data.x, data.y);
  });
  
  socket.on('disconnect', () => {
    session.players.delete(playerId);
    io.to(gameId).emit('player_left', { id: playerId });
  });
});

3. Live Chat в E-Commerce

Задача: Покупатели и продавцы общаются в реальном времени, с историей сообщений и статусом «онлайн».

Решение:

// backend/chat-server.ts
interface ChatRoom {
  conversationId: string;
  buyerId: string;
  sellerId: string;
  messages: Message[];
}

const chatRooms = new Map<string, ChatRoom>();

io.on('connection', (socket: Socket) => {
  const userId = socket.handshake.query.userId as string;
  
  // Присоединиться к чату
  socket.on('join_chat', async (conversationId: string) => {
    socket.join(`chat:${conversationId}`);
    
    // Загружаем историю сообщений из БД
    const messages = await db.messages.find({ conversationId });
    socket.emit('chat_history', messages);
    
    // Уведомляем, что пользователь онлайн
    io.to(`chat:${conversationId}`).emit('user_status', {
      userId,
      status: 'online',
      timestamp: new Date(),
    });
  });
  
  // Отправка сообщения
  socket.on('send_message', async (data: { conversationId: string; text: string }) => {
    const message = new Message({
      conversationId: data.conversationId,
      senderId: userId,
      text: data.text,
      createdAt: new Date(),
    });
    
    // Сохраняем в БД
    await db.messages.save(message);
    
    // Отправляем всем в комнате
    io.to(`chat:${data.conversationId}`).emit('new_message', {
      id: message.id,
      senderId: userId,
      text: data.text,
      createdAt: message.createdAt,
    });
    
    // Отправляем уведомление другому пользователю
    const room = chatRooms.get(data.conversationId);
    const otherUserId = room.buyerId === userId ? room.sellerId : room.buyerId;
    
    // Если они не в чате, отправляем notification
    const recipientSocket = io.sockets.sockets.get(otherUserId);
    if (!recipientSocket?.rooms.has(`chat:${data.conversationId}`)) {
      await db.notifications.save({
        userId: otherUserId,
        type: 'new_message',
        conversationId: data.conversationId,
        senderName: await getUserName(userId),
      });
    }
  });
  
  // Статус печати
  socket.on('typing', (conversationId: string) => {
    io.to(`chat:${conversationId}`).emit('user_typing', { userId });
  });
  
  socket.on('stop_typing', (conversationId: string) => {
    io.to(`chat:${conversationId}`).emit('user_stop_typing', { userId });
  });
  
  socket.on('disconnect', () => {
    // Уведомляем все комнаты, что пользователь оффлайн
    socket.rooms.forEach(room => {
      if (room.startsWith('chat:')) {
        io.to(room).emit('user_status', {
          userId,
          status: 'offline',
          timestamp: new Date(),
        });
      }
    });
  });
});

4. Live Analytics Dashboard

Задача: Dashboard показывает метрики в реальном времени (views, clicks, conversions).

Решение:

// backend/analytics-server.ts
class AnalyticsStream {
  private subscribers: Map<string, Set<Socket>> = new Map();
  
  subscribe(dashboardId: string, socket: Socket) {
    if (!this.subscribers.has(dashboardId)) {
      this.subscribers.set(dashboardId, new Set());
    }
    this.subscribers.get(dashboardId)!.add(socket);
  }
  
  unsubscribe(dashboardId: string, socket: Socket) {
    this.subscribers.get(dashboardId)?.delete(socket);
  }
  
  // Вызывается каждый раз, когда приходит новое событие (view, click, etc.)
  broadcast(dashboardId: string, event: AnalyticsEvent) {
    const subscribers = this.subscribers.get(dashboardId);
    if (!subscribers) return;
    
    subscribers.forEach(socket => {
      socket.emit('analytics_update', {
        event: event.type, // 'view', 'click', 'conversion'
        value: event.value,
        timestamp: new Date(),
        metric: event.metric,
      });
    });
  }
}

const analyticsStream = new AnalyticsStream();

io.on('connection', (socket: Socket) => {
  socket.on('subscribe_dashboard', (dashboardId: string) => {
    analyticsStream.subscribe(dashboardId, socket);
    
    // Отправляем текущее состояние
    const currentMetrics = db.analytics.getCurrentMetrics(dashboardId);
    socket.emit('current_metrics', currentMetrics);
  });
});

// Когда приходит аналитическое событие (например, пользователь кликнул)
app.post('/api/v1/events', (req, res) => {
  const { dashboardId, eventType, value } = req.body;
  
  // Сохраняем в БД
  db.events.save({ dashboardId, eventType, value, timestamp: new Date() });
  
  // Отправляем всем, кто смотрит этот dashboard
  analyticsStream.broadcast(dashboardId, {
    type: eventType,
    value,
    metric: eventType === 'view' ? 'views' : eventType,
  });
  
  res.json({ ok: true });
});

5. Collaborative Document Editing

Задача: Несколько пользователей редактируют один документ, видя изменения друг друга в реальном времени.

Решение с Operational Transform (OT):

// backend/document-server.ts
class Document {
  content: string = '';
  version: number = 0;
  subscribers: Set<Socket> = new Set();
  
  applyOperation(op: Operation) {
    // Простой пример: insert at position
    const { type, position, text } = op;
    
    if (type === 'insert') {
      this.content = this.content.slice(0, position) + text + this.content.slice(position);
      this.version++;
      
      // Отправляем operation всем (кроме автора)
      this.subscribers.forEach(socket => {
        socket.emit('operation', {
          type: 'insert',
          position,
          text,
          version: this.version,
        });
      });
    }
  }
}

const documents = new Map<string, Document>();

io.on('connection', (socket: Socket) => {
  socket.on('open_document', (docId: string) => {
    if (!documents.has(docId)) {
      documents.set(docId, new Document());
    }
    
    const doc = documents.get(docId)!;
    doc.subscribers.add(socket);
    
    // Отправляем текущее содержимое
    socket.emit('document_loaded', {
      content: doc.content,
      version: doc.version,
    });
  });
  
  socket.on('edit', (data: { docId: string; operation: Operation }) => {
    const doc = documents.get(data.docId);
    if (doc) {
      doc.applyOperation(data.operation);
    }
  });
});

6. Live Notifications System

Задача: Отправить уведомление конкретному пользователю мгновенно, только если он онлайн.

Решение:

// backend/notification-server.ts
const userSockets = new Map<string, Set<Socket>>();

io.on('connection', (socket: Socket) => {
  const userId = socket.handshake.query.userId as string;
  
  if (!userSockets.has(userId)) {
    userSockets.set(userId, new Set());
  }
  userSockets.get(userId)!.add(socket);
  
  socket.on('disconnect', () => {
    const sockets = userSockets.get(userId);
    sockets?.delete(socket);
    if (sockets?.size === 0) {
      userSockets.delete(userId);
    }
  });
});

// Функция отправки уведомления
function notifyUser(userId: string, notification: Notification) {
  const sockets = userSockets.get(userId);
  
  if (sockets && sockets.size > 0) {
    // Пользователь онлайн, отправляем через WebSocket
    sockets.forEach(socket => {
      socket.emit('notification', notification);
    });
  } else {
    // Пользователь оффлайн, сохраняем в БД
    db.notifications.save({
      userId,
      ...notification,
      read: false,
    });
  }
}

Key Technologies Used

Socket.IO — самая популярная библиотека:

  • Автоматический fallback на polling если нет WebSocket
  • Rooms и namespaces для организации соединений
  • Встроенная обработка ошибок и переподключения

Масштабирование (Redis Adapter):

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ host: 'redis', port: 6379 });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

// Теперь события транслируются между всеми инстансами

Best Practices

  1. Масштабирование — используй Redis adapter для нескольких инстансов
  2. Аутентификация — проверяй токен при подключении
  3. Валидация — не доверяй данным от клиента
  4. Error handling — отправляй ошибки клиенту явно
  5. Логирование — трекируй соединения для дебага
  6. Память — очищай старые сокеты и соединения

Заключение

WebSocket идеален для сценариев, требующих низкой задержки и двусторонней коммуникации: игры, чаты, реал-тайм уведомления, совместное редактирование. Критично правильно масштабировать и обрабатывать ошибки для production систем.

Какие задачи решал на WebSocket? | PrepBro