HTTP vs WebSocket vs SSE
Klasický HTTP je request-response — klient se zeptá, server odpoví. Pro realtime potřebujeme trvalé spojení nebo server push.
- Klient se ptá každých N sekund
- Zbytečné requesty
- Latence = interval
- Trvalé TCP spojení
- Full-duplex, ~1ms latence
- Chat, hry, kolaborace
- Server → klient jednosměrně
- Auto-reconnect nad HTTP
- Notifikace, AI streaming
- Request čeká na odpověď
- Funguje všude
- Vyšší overhead
Native WebSocket API
// SERVER (Node.js + 'ws')
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const clients = new Map();
wss.on('connection', (ws) => {
const id = crypto.randomUUID();
clients.set(ws, { id });
ws.send(JSON.stringify({ type: 'WELCOME', id }));
broadcast({ type: 'USER_JOINED', id }, ws);
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === 'CHAT') {
broadcast({ type: 'CHAT', from: id, text: msg.text });
}
});
ws.on('close', () => {
clients.delete(ws);
broadcast({ type: 'USER_LEFT', id });
});
});
function broadcast(data, exclude) {
const str = JSON.stringify(data);
wss.clients.forEach(c => {
if (c !== exclude && c.readyState === WebSocket.OPEN)
c.send(str);
});
}
// CLIENT — React hook
function useWebSocket(url) {
const ws = useRef(null);
const [msgs, setMsgs] = useState([]);
const [online, setOnline] = useState(false);
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onopen = () => setOnline(true);
ws.current.onclose = () => setOnline(false);
ws.current.onmessage = e => {
const data = JSON.parse(e.data);
if (data.type === 'CHAT')
setMsgs(prev => [...prev, data]);
};
return () => ws.current?.close();
}, [url]);
const send = msg => ws.current?.readyState === 1
&& ws.current.send(JSON.stringify(msg));
return { msgs, online, send };
}
// Kvíz: Co je WebSocket handshake a proč probíhá přes HTTP?
Socket.io — produkční realtime
Socket.io přidává nad WebSocket: automatický fallback, rooms, namespaces, reconnection, acknowledgements a broadcasting API.
// npm install socket.io socket.io-client
// SERVER
import { createServer } from 'http';
import { Server } from 'socket.io';
import express from 'express';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: 'http://localhost:3000', credentials: true }
});
io.on('connection', (socket) => {
console.log('Připojeno:', socket.id);
socket.on('room:join', async (roomId) => {
await socket.join(roomId);
// Informovat ostatní (bez odesílatele)
socket.to(roomId).emit('user:joined', { id: socket.id });
// Zaslat seznam uživatelů novému příchozímu
const sockets = await io.in(roomId).fetchSockets();
socket.emit('room:users', sockets.map(s => s.id));
});
// Zpráva s acknowledgement (potvrzení doručení)
socket.on('chat:send', async (text, callback) => {
const msg = {
id: Date.now(), from: socket.id,
text, timestamp: Date.now()
};
io.to(socket.data.room).emit('chat:message', msg);
callback({ ok: true, messageId: msg.id }); // potvrzení
});
socket.on('typing:start', () =>
socket.to(socket.data.room).emit('typing', socket.id)
);
socket.on('disconnect', () =>
io.to(socket.data.room).emit('user:left', socket.id)
);
});
// CLIENT
import { io } from 'socket.io-client';
const socket = io('http://localhost:4000', {
auth: { token: getToken() },
reconnectionAttempts: 5,
});
socket.emit('chat:send', 'Ahoj!', ({ ok, messageId }) => {
console.log('Doručeno:', ok, messageId);
});
Rooms & Namespaces
// ROOMS — logické skupiny socketů
socket.join('room:42'); // vstup
socket.leave('room:42'); // odchod
socket.to('room:42').emit(ev, data); // ostatní v místnosti
io.in('room:42').emit(ev, data); // VŠICHNI v místnosti
io.to(socketId).emit('private', data); // soukromá zpráva
socket.broadcast.emit(ev, data); // všem KROMĚ mě
// NAMESPACES — oddělené skupiny s vlastním middleware
const adminNs = io.of('/admin');
adminNs.use(adminAuthMiddleware);
adminNs.on('connection', socket => { /* ... */ });
// Klient
const adminSocket = io('/admin', { auth: { token } });
// Kvíz: Jaký je rozdíl mezi socket.to('room').emit() a io.in('room').emit()?
Server-Sent Events (SSE)
SSE je jednodušší alternativa pro server → klient push. Funguje nad HTTP, má automatický reconnect — ideální pro notifikace, live dashboardy a AI streaming.
// SERVER — Express SSE endpoint
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const sendEvent = (event, data) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Heartbeat — zabrání timeout
const hb = setInterval(() => res.write(': ping\n\n'), 15000);
// Naslouchat událostem
const handler = (notification) => sendEvent('notification', notification);
emitter.on('new', handler);
req.on('close', () => {
clearInterval(hb);
emitter.off('new', handler);
res.end();
});
});
// AI Streaming (OpenAI)
app.post('/api/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.flushHeaders();
const stream = await openai.chat.completions.create({
model: 'gpt-4', messages: req.body.messages, stream: true
});
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content ?? '';
if (token) res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
});
// CLIENT — React hook
function useSSE(url) {
const [events, setEvents] = useState([]);
useEffect(() => {
const es = new EventSource(url);
es.addEventListener('notification', e =>
setEvents(prev => [...prev, JSON.parse(e.data)])
);
es.onerror = () => console.log('SSE reconnecting...');
return () => es.close();
}, [url]);
return events;
}
TanStack Query — server state
TanStack Query řeší server state — caching, loading/error stavy, background refetch, optimistic updates. Nahrazuje ruční useEffect pro data fetching.
// npm install @tanstack/react-query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 min čerstvá data
gcTime: 10 * 60 * 1000, // 10 min v cache
retry: 3,
},
},
});
// Čtení dat
function ProductList() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['products', { page: 1 }],
queryFn: () => api.get('/products?page=1'),
});
if (isLoading) return ;
if (isError) return ;
return data.map(p => );
}
// Mutace s optimistic update
const mutation = useMutation({
mutationFn: (data) => api.post('/products', data),
onMutate: async (newProduct) => {
await qc.cancelQueries({ queryKey: ['products'] });
const prev = qc.getQueryData(['products']);
qc.setQueryData(['products'], old =>
[...old, { ...newProduct, id: 'temp-' + Date.now() }]
);
return { prev };
},
onError: (err, _, ctx) =>
qc.setQueryData(['products'], ctx?.prev),
onSettled: () =>
qc.invalidateQueries({ queryKey: ['products'] }),
});
Škálování s Redis
Problém: více instancí serveru nemůže sdílet WS spojení. Řešení: Redis pub/sub jako message broker — všechny instance posílají a přijímají přes Redis.
// Socket.io Redis adapter
// npm install @socket.io/redis-adapter ioredis
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Teď všechny instance sdílejí rooms a broadcasts přes Redis!
// Redis cache — snižuje DB zátěž
const redis = new Redis(process.env.REDIS_URL);
async function getProductsCached(filter) {
const key = `products:${filter}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await db.product.findMany({ where: { kategorie: filter } });
await redis.setex(key, 300, JSON.stringify(data)); // TTL 5 min
return data;
}
// Rate limiting s Redis
async function checkRateLimit(ip) {
const key = `rl:${ip}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
return count <= 100; // max 100 req/min
}
// Kvíz: Proč SSE funguje automaticky za HTTP proxy, ale WebSocket může mít problémy?
Cvičení — Realtime dashboard architektura
Navrhněte realtime dashboard pro e-shop. Pro každý prvek rozhodněte mezi WebSocket, SSE a polling:
- Live počet online uživatelů
- Notifikace "Nová objednávka!" (okamžitě)
- Live chat se zákazníky
- Tržby za den (graf, každou minutu)
- Upozornění na nízký sklad
Realtime Engineer
Zvládáte WebSocket, Socket.io, SSE, TanStack Query a Redis škálování.
// Taháček
// Socket.io server
io.on('connection', socket => {
socket.join('room');
socket.to('room').emit(ev, data); // ostatní
io.in('room').emit(ev, data); // všichni
socket.on(ev, (data, cb) => cb({ ok: true }));
});
// SSE endpoint
res.setHeader('Content-Type', 'text/event-stream');
res.flushHeaders();
res.write(`event: name\ndata: ${JSON.stringify(data)}\n\n`);
// SSE klient
const es = new EventSource('/api/events');
es.addEventListener('name', e => JSON.parse(e.data));
// TanStack Query
const { data } = useQuery({ queryKey: [...], queryFn: ... });
const mut = useMutation({
mutationFn: fn,
onSettled: () => qc.invalidateQueries({ queryKey: [...] }),
});