Зачем нужен real-time
Современные приложения требуют мгновенной доставки данных: чаты, уведомления, live-обновления, совместное редактирование. HTTP не подходит для этого — нужен WebSocket или SSE.
WebSocket vs HTTP
WebSocket — протокол двусторонней связи поверх TCP. В отличие от HTTP, соединение остаётся открытым:
- HTTP — запрос-ответ, каждый раз новое соединение
- WebSocket — постоянное соединение, двусторонний обмен
- SSE (Server-Sent Events) — односторонний поток от сервера
Socket.io — библиотека для real-time
Socket.io упрощает работу с WebSocket и добавляет fallback на polling:
// server.ts
import { Server } from 'socket.io';
import { createServer } from 'http';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
// Подключение клиента
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Присоединение к комнате
socket.on('join-room', (roomId: string) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', { id: socket.id });
});
// Получение сообщения
socket.on('message', (data: { roomId: string; text: string }) => {
io.to(data.roomId).emit('message', {
id: Date.now(),
text: data.text,
userId: socket.id,
timestamp: new Date()
});
});
// Typing индикатор
socket.on('typing', (roomId: string) => {
socket.to(roomId).emit('user-typing', socket.id);
});
// Отключение
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
httpServer.listen(4000);
Клиент Socket.io
// hooks/useSocket.ts
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocket(roomId: string) {
const [socket, setSocket] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const newSocket = io('http://localhost:3000');
newSocket.on('connect', () => {
setIsConnected(true);
newSocket.emit('join-room', roomId);
});
newSocket.on('message', (message: Message) => {
setMessages(prev => [...prev, message]);
});
newSocket.on('disconnect', () => {
setIsConnected(false);
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, [roomId]);
const sendMessage = (text: string) => {
socket?.emit('message', { roomId, text });
};
return { messages, sendMessage, isConnected };
}
// components/Chat.tsx
function Chat({ roomId }: { roomId: string }) {
const { messages, sendMessage, isConnected } = useSocket(roomId);
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (input.trim()) {
sendMessage(input);
setInput('');
}
};
return (
<div className="chat">
<div className="status">
{isConnected ? '🟢 Online' : '🔴 Offline'}
</div>
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className="message">
{msg.text}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Сообщение..."
/>
<button type="submit">Отправить</button>
</form>
</div>
);
}
Уведомления в реальном времени
// Серверная часть
io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
// Присоединяем к персональной комнате
socket.join(`user:${userId}`);
// Отправка уведомления конкретному пользователю
function sendNotification(targetUserId: string, notification: Notification) {
io.to(`user:${targetUserId}`).emit('notification', notification);
}
});
// При создании заказа
async function createOrder(data: OrderData) {
const order = await prisma.order.create({ data });
// Уведомляем админов
io.to('admins').emit('new-order', order);
// Уведомляем пользователя
io.to(`user:${order.userId}`).emit('notification', {
type: 'order',
title: 'Заказ создан',
message: `Заказ #${order.id} успешно создан`
});
return order;
}
Server-Sent Events (SSE)
SSE проще WebSocket, когда нужен только поток от сервера:
// API route для SSE
// app/api/events/route.ts
export async function GET(request: Request) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
// Отправляем событие каждые 5 секунд
const interval = setInterval(() => {
const data = JSON.stringify({
time: new Date().toISOString(),
value: Math.random()
});
controller.enqueue(
encoder.encode(`data: ${data}\n\n`)
);
}, 5000);
request.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
// Клиент
function useLiveData() {
const [data, setData] = useState(null);
useEffect(() => {
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data));
};
return () => eventSource.close();
}, []);
return data;
}
Масштабирование WebSocket
// Используем Redis для синхронизации между серверами
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Заключение
WebSocket и Socket.io — мощные инструменты для real-time функций. Используйте их для чатов, уведомлений, live-обновлений. SSE подходит для простых сценариев с односторонним потоком.
Real-time делает приложение живым. Но помните о масштабировании — WebSocket соединения требуют ресурсов.
