Какие задачи решал на WebSocket?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Задачи, решенные с помощью 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
- Масштабирование — используй Redis adapter для нескольких инстансов
- Аутентификация — проверяй токен при подключении
- Валидация — не доверяй данным от клиента
- Error handling — отправляй ошибки клиенту явно
- Логирование — трекируй соединения для дебага
- Память — очищай старые сокеты и соединения
Заключение
WebSocket идеален для сценариев, требующих низкой задержки и двусторонней коммуникации: игры, чаты, реал-тайм уведомления, совместное редактирование. Критично правильно масштабировать и обрабатывать ошибки для production систем.