HTTP vs WebSocket vs SSE

Klasický HTTP je request-response — klient se zeptá, server odpoví. Pro realtime potřebujeme trvalé spojení nebo server push.

HTTP Polling
tradiční
  • Klient se ptá každých N sekund
  • Zbytečné requesty
  • Latence = interval
WebSocket
obousměrný
  • Trvalé TCP spojení
  • Full-duplex, ~1ms latence
  • Chat, hry, kolaborace
SSE
server push
  • Server → klient jednosměrně
  • Auto-reconnect nad HTTP
  • Notifikace, AI streaming
Long Polling
kompromis
  • Request čeká na odpověď
  • Funguje všude
  • Vyšší overhead
// 01 / 09Native WS →

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 };
}
// Chat simulátor (lokální demo)
Připojeno 2 online
Ahoj! Toto je WebSocket chat demo 👋
Bot · 12:00
Napište zprávu a stiskněte Enter
Bot · 12:00

// Kvíz: Co je WebSocket handshake a proč probíhá přes HTTP?

// 02 / 09Socket.io →

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);
});
// 03 / 09Rooms →

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()?

// 04 / 09SSE →

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?

// 07 / 09Cvičení →

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
💡
Doporučené odpovědi: Online count → SSE nebo polling (30s), Nová objednávka → SSE push, Live chat → WebSocket (obousměrný), Tržby → TanStack Query refetchInterval: 60000, Sklad → SSE event z backendu při změně.
🏅

Realtime Engineer

Zvládáte WebSocket, Socket.io, SSE, TanStack Query a Redis škálování.

// 08 / 09Taháček →

// 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: [...] }),
});
// Lekce 8 dokončenaL9: Pokročilý TypeScript →