WebSockets - Pytania Rekrutacyjne i Kompletny Przewodnik 2025

WebSockets to fundament real-time aplikacji - od chatów, przez collaborative editing, po gry multiplayer. Na rozmowach rekrutacyjnych backend/fullstack sprawdzane jest zrozumienie protokołu, różnic względem alternatyw, skalowania i bezpieczeństwa.

Odpowiedź w 30 sekund

Czym jest WebSocket?

WebSocket to protokół full-duplex communication - persystentne połączenie TCP, gdzie obie strony mogą wysyłać dane w dowolnym momencie. Różni się od HTTP tym, że nie wymaga nowego połączenia dla każdej wiadomości. Zaczyna się od HTTP handshake (Upgrade header), potem przechodzi na własny protokół z minimalnym overhead (2-14 bajtów na frame vs ~700 bajtów HTTP headers).

Odpowiedź w 2 minuty

WebSocket rozwiązuje problem real-time komunikacji, który HTTP nie obsługuje natywnie. W HTTP klient musi ciągle pytać serwer o nowe dane (polling), co jest nieefektywne. WebSocket utrzymuje jedno połączenie i pozwala serwerowi natychmiast pushować dane do klienta.

Połączenie WebSocket zaczyna się od standardowego HTTP request z nagłówkiem Upgrade: websocket. Serwer odpowiada 101 Switching Protocols i od tego momentu komunikacja odbywa się przez WebSocket protocol. Dane są pakowane w frames - mogą być tekstowe (UTF-8), binarne, lub kontrolne (ping/pong, close).

Alternatywy dla WebSocket to Server-Sent Events (SSE) i Long Polling. SSE to jednokierunkowy stream od serwera do klienta przez HTTP - prostszy, automatycznie reconnectuje, ale tylko server→client. Long Polling to hack gdzie klient wysyła request, serwer trzyma go otwarty do momentu gdy ma dane - działa wszędzie, ale nieefektywne dla frequent updates.

Socket.IO to popularna biblioteka która buduje na WebSocket, dodając automatyczny reconnect, fallback do long-polling, rooms/namespaces do organizacji klientów, i acknowledgments (potwierdzenia dostarczenia). Jest świetna dla szybkiego startu, ale dodaje overhead - własny protokół na poziomie application layer.

Skalowanie WebSockets wymaga dwóch rzeczy: sticky sessions (żeby klient trafiał do tego samego serwera) i pub/sub system (żeby wiadomość wysłana na jednym serwerze trafiła do klientów na innych serwerach). Typowe rozwiązanie to Redis Pub/Sub lub message broker jak RabbitMQ.

// Serwer WebSocket w Node.js
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  console.log('Client connected from:', req.socket.remoteAddress);

  ws.on('message', (data) => {
    const message = JSON.parse(data);

    // Broadcast do wszystkich klientów
    wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'message',
          payload: message,
          timestamp: Date.now()
        }));
      }
    });
  });

  ws.on('close', (code, reason) => {
    console.log('Client disconnected:', code, reason.toString());
  });

  // Heartbeat ping
  const interval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30000);

  ws.on('close', () => clearInterval(interval));
});

Podstawy protokołu WebSocket

Czym WebSocket różni się od HTTP?

HTTP to protokół request-response - klient wysyła żądanie, serwer odpowiada, połączenie jest zamykane (lub reużywane w HTTP/1.1 keep-alive, ale nadal per-request). WebSocket to protokół full-duplex z persystentnym połączeniem - po nawiązaniu połączenia obie strony mogą wysyłać dane w dowolnym momencie bez czekania na drugą stronę.

HTTP:
Klient → Serwer: GET /data
Klient ← Serwer: Response
(połączenie zamknięte lub idle)

WebSocket:
Klient → Serwer: HTTP Upgrade
Klient ← Serwer: 101 Switching Protocols
(persystentne połączenie)
Klient ↔ Serwer: dowolne wiadomości w obu kierunkach

WebSocket zaczyna się od HTTP handshake ale potem używa własnego protokołu binarnego z minimalnym overhead - 2-14 bajtów na frame vs typowo ~700 bajtów HTTP headers.

Jak wygląda WebSocket handshake?

WebSocket handshake to specjalny HTTP request z nagłówkami Upgrade:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com

Serwer odpowiada 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept to hash z Sec-WebSocket-Key + magic string - zapobiega cache'owaniu proxy i potwierdza że serwer rozumie WebSocket. Po tym handshake protokół przełącza się na WebSocket frames.

Jakie typy frames istnieją w WebSocket?

WebSocket definiuje kilka typów frames (opcode w nagłówku):

  1. Text frame (0x1) - dane tekstowe UTF-8
  2. Binary frame (0x2) - dane binarne
  3. Close frame (0x8) - inicjuje zamknięcie połączenia
  4. Ping frame (0x9) - heartbeat od klienta lub serwera
  5. Pong frame (0xA) - odpowiedź na ping
  6. Continuation frame (0x0) - kontynuacja fragmentowanej wiadomości
// Wysyłanie różnych typów danych
ws.send('Hello'); // text frame
ws.send(Buffer.from([0x01, 0x02, 0x03])); // binary frame
ws.ping(); // ping frame
ws.close(1000, 'Normal closure'); // close frame z kodem

Wyjaśnij strukturę WebSocket frame

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
  • FIN - czy to ostatni fragment
  • Opcode - typ frame
  • MASK - czy payload jest maskowany (wymagane od klienta)
  • Payload length - 7 bitów, lub 16/64 jeśli większy

Co to jest masking i dlaczego jest wymagany od klienta?

Masking to XOR payload z 4-bajtowym kluczem. Jest wymagany od klienta do serwera (nie odwrotnie) z powodów bezpieczeństwa - zapobiega cache poisoning atakom na proxy.

Bez maskingu, złośliwy JavaScript mógłby wysłać przez WebSocket dane które wyglądają jak HTTP response, a proxy mogłoby je cache'ować i serwować innym klientom.

// Demaskowanie (serwer automatycznie robi)
function unmask(payload, mask) {
  const unmasked = Buffer.alloc(payload.length);
  for (let i = 0; i < payload.length; i++) {
    unmasked[i] = payload[i] ^ mask[i % 4];
  }
  return unmasked;
}

WebSocket vs Alternatywy

Kiedy używać WebSocket vs Server-Sent Events (SSE)?

WebSocket gdy potrzebujesz komunikacji dwukierunkowej:

  • Chat / messaging
  • Collaborative editing (Google Docs style)
  • Gry multiplayer
  • Trading platforms (wysyłanie zleceń + otrzymywanie updates)

SSE gdy serwer tylko wysyła dane do klienta:

  • Notifications
  • Live feeds (news, social media)
  • Stock tickers / sports scores
  • Progress updates

Zalety SSE:

  • Prostsze - standardowy HTTP, działa przez proxy
  • Automatyczny reconnect wbudowany w przeglądarkę
  • Text-only (ale możesz base64 encode)
  • Działa na HTTP/2 bez problemu
// Serwer SSE
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const sendEvent = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // Wysyłaj updates
  const interval = setInterval(() => {
    sendEvent({ time: Date.now() });
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

// Klient SSE (przeglądarka)
const events = new EventSource('/events');
events.onmessage = (e) => console.log(JSON.parse(e.data));

Czym jest Long Polling i kiedy go używać?

Long Polling to technika gdzie klient wysyła request, a serwer trzyma go otwarty (nie odpowiada) dopóki nie ma nowych danych lub nie minie timeout. Gdy klient dostanie odpowiedź, natychmiast wysyła kolejny request.

// Serwer Long Polling
app.get('/poll', async (req, res) => {
  const lastEventId = req.query.lastEventId;

  // Czekaj na nowe dane (max 30 sekund)
  const data = await waitForNewData(lastEventId, 30000);

  if (data) {
    res.json(data);
  } else {
    res.status(204).end(); // No Content - reconnect
  }
});

// Klient Long Polling
async function poll(lastEventId = 0) {
  try {
    const res = await fetch(`/poll?lastEventId=${lastEventId}`);
    if (res.ok) {
      const data = await res.json();
      handleData(data);
      poll(data.id); // Następny poll
    } else {
      setTimeout(() => poll(lastEventId), 1000); // Retry
    }
  } catch (err) {
    setTimeout(() => poll(lastEventId), 5000); // Backoff
  }
}

Używaj Long Polling gdy:

  • WebSocket/SSE nie działają (stare proxy, firewalle)
  • Fallback dla WebSocket
  • Rzadkie updates (minuty/godziny między eventami)

WebSocket vs HTTP/2 Server Push - jakie różnice?

HTTP/2 Server Push pozwala serwerowi wysłać zasoby zanim klient o nie poprosi (np. push CSS gdy klient żąda HTML). Jednak:

  • Push tylko zasoby związane z request
  • Brak prawdziwego real-time - nadal request-response model
  • Klient nie może wysyłać danych poza requests
  • Zostało w praktyce porzucone (Chrome usunął wsparcie)

WebSocket to zupełnie inny use case - persystentne połączenie dla dowolnych danych w obu kierunkach, niezależnie od HTTP requests.

Porównaj WebSocket, SSE i Long Polling

Cecha WebSocket SSE Long Polling
Kierunek Bidirectional Server→Client Symulowany bidirectional
Protokół WebSocket HTTP HTTP
Binary data ❌ (text only)
Reconnect Manual Automatic Manual
HTTP/2 compatible Osobne połączenie Tak Tak
Proxy friendly Może być problem Tak Tak
Overhead Niski (2-14B/frame) Średni Wysoki (headers każdy poll)
Skalowanie Trudniejsze Łatwiejsze Łatwiejsze

Socket.IO

Czym jest Socket.IO i czym różni się od natywnych WebSockets?

Socket.IO to biblioteka budująca na WebSocket z dodatkowymi featurami:

  1. Automatic fallback - jeśli WebSocket nie działa, używa long-polling
  2. Automatic reconnection - z exponential backoff
  3. Rooms i namespaces - logiczne grupowanie klientów
  4. Acknowledgments - potwierdzenie dostarczenia wiadomości
  5. Broadcasting - wysyłanie do wszystkich oprócz sendera
  6. Multiplexing - wiele kanałów na jednym połączeniu

Socket.IO używa własnego protokołu na poziomie application layer - nie jest kompatybilny z raw WebSocket clients.

// Socket.IO serwer
import { Server } from 'socket.io';

const io = new Server(3000);

io.on('connection', (socket) => {
  // Rooms
  socket.join('room1');

  // Emit z acknowledgment
  socket.emit('message', 'Hello', (response) => {
    console.log('Client acknowledged:', response);
  });

  // Broadcast do room (oprócz sendera)
  socket.to('room1').emit('notification', 'New user joined');

  // Broadcast do wszystkich (oprócz sendera)
  socket.broadcast.emit('userConnected', socket.id);
});

// Namespaces
const chat = io.of('/chat');
chat.on('connection', (socket) => {
  // Oddzielna logika dla /chat namespace
});

Kiedy używać Socket.IO vs natywne WebSockets?

Używaj Socket.IO gdy:

  • Potrzebujesz szybkiego startu z batteries-included
  • Rooms/namespaces są przydatne dla twojego use case
  • Zależy ci na fallback do long-polling
  • Klienci to przeglądarki z Socket.IO client

Używaj natywnych WebSockets gdy:

  • Potrzebujesz minimalnego overhead
  • Klienci to nie przeglądarki (IoT, mobile apps, inne serwery)
  • Masz własne wymagania protokołu
  • Nie potrzebujesz featurów Socket.IO
// Natywne WebSockets - mniej overhead
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    // Ręczne parsowanie, routing, rooms...
  });
});

Jak działają rooms i namespaces w Socket.IO?

Rooms to arbitralne kanały do których sockety mogą dołączać/opuszczać. Używane do grupowania klientów (np. pokoje czatu, game lobbies):

io.on('connection', (socket) => {
  // Dołącz do room
  socket.join('game:123');

  // Opuść room
  socket.leave('game:123');

  // Emit do room
  io.to('game:123').emit('gameUpdate', data);

  // Emit do wielu rooms
  io.to('room1').to('room2').emit('message', data);

  // Każdy socket jest automatycznie w room = socket.id
  io.to(socket.id).emit('private', 'Only for you');
});

Namespaces to oddzielne endpointy na tym samym połączeniu (multiplexing):

// Różne namespaces dla różnych części aplikacji
const mainNsp = io.of('/');
const chatNsp = io.of('/chat');
const adminNsp = io.of('/admin');

// Namespace może mieć własny middleware
adminNsp.use((socket, next) => {
  if (isAdmin(socket.handshake.auth.token)) {
    next();
  } else {
    next(new Error('Unauthorized'));
  }
});

adminNsp.on('connection', (socket) => {
  // Tylko admin sockety
});

Jak działa automatic reconnection w Socket.IO?

Socket.IO automatycznie próbuje reconnect z exponential backoff:

// Klient
const socket = io('http://localhost:3000', {
  reconnection: true,
  reconnectionAttempts: Infinity,
  reconnectionDelay: 1000,      // Początkowy delay
  reconnectionDelayMax: 5000,   // Maksymalny delay
  randomizationFactor: 0.5      // Jitter
});

socket.on('connect', () => {
  console.log('Connected');
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
  // 'io server disconnect' - serwer zamknął
  // 'io client disconnect' - klient zamknął
  // 'transport close' - połączenie zerwane
  // 'ping timeout' - serwer nie odpowiada
});

socket.on('reconnect', (attemptNumber) => {
  console.log('Reconnected after', attemptNumber, 'attempts');
});

socket.on('reconnect_error', (error) => {
  console.log('Reconnect failed:', error);
});

Skalowanie WebSockets

Jak skalować WebSockets na wiele serwerów?

Dwa kluczowe problemy do rozwiązania:

  1. Sticky sessions - klient musi trafiać do tego samego serwera
  2. Message broadcasting - wiadomość na serwerze A musi dotrzeć do klientów na serwerze B
                  Load Balancer (sticky sessions)
                 /          |          \
            Server A    Server B    Server C
                 \          |          /
                  Redis Pub/Sub lub Message Broker

Rozwiązania:

1. Redis Adapter (Socket.IO):

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));

// Teraz io.emit() automatycznie broadcastuje przez Redis
io.emit('message', 'Hello all servers!');

2. Custom Pub/Sub (natywne WebSockets):

import Redis from 'ioredis';

const redis = new Redis();
const sub = new Redis();

// Subskrybuj kanał
sub.subscribe('broadcast');
sub.on('message', (channel, message) => {
  // Wyślij do wszystkich lokalnych klientów
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

// Przy wysyłaniu - publikuj do Redis
function broadcast(message) {
  redis.publish('broadcast', JSON.stringify(message));
}

Jak działają sticky sessions dla WebSocket?

Sticky sessions (session affinity) zapewniają że wszystkie requesty od klienta trafiają do tego samego serwera. Ważne dla WebSocket bo:

  1. Handshake i połączenie muszą być na tym samym serwerze
  2. Stan połączenia (rooms, auth) jest w pamięci serwera

Implementacja na load balancerze:

Nginx:

upstream websocket {
    ip_hash;  # Sticky sessions bazowane na IP
    server server1:3000;
    server server2:3000;
    server server3:3000;
}

server {
    location /socket.io/ {
        proxy_pass http://websocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

AWS ALB:

Target Group → Attributes → Stickiness: Enabled

Cookie-based (Socket.IO): Socket.IO może użyć cookie do sticky sessions zamiast IP hash - lepsze gdy klienci są za NAT.

Jak obsłużyć tysiące równoczesnych połączeń WebSocket?

// 1. Zwiększ limity systemowe
// /etc/sysctl.conf
// net.core.somaxconn = 65535
// fs.file-max = 1000000

// 2. Zwiększ limit file descriptors
// ulimit -n 1000000

// 3. Użyj efektywnej biblioteki (ws jest szybszy niż socket.io)
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: false, // Wyłącz kompresję dla performance
  maxPayload: 1024 * 1024   // Limit wielkości wiadomości
});

// 4. Unikaj memory leaks - czyszczenie przy disconnect
wss.on('connection', (ws) => {
  const subscriptions = new Set();

  ws.on('close', () => {
    // Cleanup
    subscriptions.forEach(sub => sub.unsubscribe());
    subscriptions.clear();
  });
});

// 5. Monitoring połączeń
setInterval(() => {
  console.log('Active connections:', wss.clients.size);
}, 10000);

Jak używać message brokerów do skalowania WebSockets?

Zamiast Redis Pub/Sub możesz użyć pełnego message brokera dla większej niezawodności:

RabbitMQ z fanout exchange:

import amqp from 'amqplib';

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

// Exchange fanout - broadcast do wszystkich queue
await channel.assertExchange('websocket.broadcast', 'fanout');

// Każdy serwer ma własną queue
const { queue } = await channel.assertQueue('', { exclusive: true });
await channel.bindQueue(queue, 'websocket.broadcast', '');

// Odbieranie wiadomości
channel.consume(queue, (msg) => {
  const data = JSON.parse(msg.content.toString());
  broadcastToLocalClients(data);
});

// Wysyłanie broadcastu
function broadcast(message) {
  channel.publish(
    'websocket.broadcast',
    '',
    Buffer.from(JSON.stringify(message))
  );
}

Kafka dla dużego scale:

import { Kafka } from 'kafkajs';

const kafka = new Kafka({ brokers: ['localhost:9092'] });
const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: `server-${process.pid}` });

await producer.connect();
await consumer.connect();
await consumer.subscribe({ topic: 'websocket-events' });

// Każdy serwer jest w innym consumer group = każdy dostaje wszystkie wiadomości
await consumer.run({
  eachMessage: async ({ message }) => {
    broadcastToLocalClients(JSON.parse(message.value.toString()));
  }
});

Heartbeats i Connection Management

Jak implementować heartbeat/ping-pong?

Heartbeat wykrywa martwe połączenia i zapobiega timeoutom proxy/firewalle:

// Serwer - ping klientów
const wss = new WebSocketServer({ port: 8080 });

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', heartbeat); // Odpowiedź na ping

  ws.on('error', console.error);
});

// Sprawdzaj co 30 sekund
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate(); // Martwe połączenie
    }

    ws.isAlive = false;
    ws.ping(); // Wyślij ping
  });
}, 30000);

wss.on('close', () => {
  clearInterval(interval);
});

Jak obsługiwać reconnection na kliencie?

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000;
    this.maxReconnectDelay = 30000;
    this.messageQueue = [];
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('Connected');
      this.reconnectDelay = 1000; // Reset delay

      // Wyślij kolejkowane wiadomości
      while (this.messageQueue.length > 0) {
        this.ws.send(this.messageQueue.shift());
      }
    };

    this.ws.onclose = (event) => {
      console.log('Disconnected, reconnecting in', this.reconnectDelay, 'ms');

      setTimeout(() => {
        this.reconnectDelay = Math.min(
          this.reconnectDelay * 2,
          this.maxReconnectDelay
        );
        this.connect();
      }, this.reconnectDelay);
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  }

  send(message) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      // Queue message for when connection is restored
      this.messageQueue.push(message);
    }
  }
}

Jak obsługiwać graceful shutdown serwera?

const wss = new WebSocketServer({ port: 8080 });

async function gracefulShutdown() {
  console.log('Shutting down gracefully...');

  // 1. Przestań przyjmować nowe połączenia
  wss.close();

  // 2. Powiadom klientów
  wss.clients.forEach((ws) => {
    ws.send(JSON.stringify({
      type: 'shutdown',
      message: 'Server is restarting'
    }));
    ws.close(1001, 'Server shutdown');
  });

  // 3. Daj czas na wysłanie close frames
  await new Promise(resolve => setTimeout(resolve, 5000));

  // 4. Zamknij pozostałe połączenia
  wss.clients.forEach((ws) => ws.terminate());

  process.exit(0);
}

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

Autentykacja i Bezpieczeństwo

Jak autentykować połączenia WebSocket?

WebSocket nie obsługuje custom headers po handshake, więc autentykacja musi być w:

1. Query string (najprostsze, ale token w logach):

// Klient
const ws = new WebSocket('wss://example.com/ws?token=eyJhbG...');

// Serwer
wss.on('connection', (ws, req) => {
  const url = new URL(req.url, 'wss://example.com');
  const token = url.searchParams.get('token');

  try {
    const user = jwt.verify(token, SECRET);
    ws.user = user;
  } catch (err) {
    ws.close(4001, 'Unauthorized');
  }
});

2. Cookie (automatycznie wysyłane):

// Klient - cookie jest automatycznie załączone
const ws = new WebSocket('wss://example.com/ws');

// Serwer
wss.on('connection', (ws, req) => {
  const cookies = parseCookies(req.headers.cookie);
  const sessionId = cookies.session;

  const user = await getSessionUser(sessionId);
  if (!user) {
    ws.close(4001, 'Unauthorized');
  }
});

3. Pierwszy message (najczystsze):

// Klient
ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', token: 'eyJhbG...' }));
};

// Serwer
wss.on('connection', (ws) => {
  ws.isAuthenticated = false;

  ws.on('message', (data) => {
    const message = JSON.parse(data);

    if (!ws.isAuthenticated) {
      if (message.type === 'auth') {
        try {
          ws.user = jwt.verify(message.token, SECRET);
          ws.isAuthenticated = true;
          ws.send(JSON.stringify({ type: 'auth_success' }));
        } catch {
          ws.close(4001, 'Invalid token');
        }
      } else {
        ws.close(4001, 'Authentication required');
      }
      return;
    }

    // Normal message handling
  });
});

Jak zabezpieczyć WebSocket przed atakami?

1. Zawsze używaj WSS (WebSocket Secure):

// Serwer z TLS
import { createServer } from 'https';
import { readFileSync } from 'fs';

const server = createServer({
  cert: readFileSync('cert.pem'),
  key: readFileSync('key.pem')
});

const wss = new WebSocketServer({ server });
server.listen(443);

2. Waliduj Origin:

wss.on('headers', (headers, req) => {
  const origin = req.headers.origin;
  const allowedOrigins = ['https://example.com', 'https://app.example.com'];

  if (!allowedOrigins.includes(origin)) {
    // Reject connection
    req.destroy();
  }
});

3. Rate limiting:

const connectionCounts = new Map();

wss.on('connection', (ws, req) => {
  const ip = req.socket.remoteAddress;
  const count = connectionCounts.get(ip) || 0;

  if (count >= 10) { // Max 10 połączeń z jednego IP
    ws.close(4029, 'Too many connections');
    return;
  }

  connectionCounts.set(ip, count + 1);

  ws.on('close', () => {
    connectionCounts.set(ip, connectionCounts.get(ip) - 1);
  });
});

// Message rate limiting
const messageRates = new Map();

ws.on('message', (data) => {
  const now = Date.now();
  const rate = messageRates.get(ws) || { count: 0, resetAt: now + 1000 };

  if (now > rate.resetAt) {
    rate.count = 0;
    rate.resetAt = now + 1000;
  }

  rate.count++;
  messageRates.set(ws, rate);

  if (rate.count > 100) { // Max 100 msg/sec
    ws.close(4029, 'Rate limit exceeded');
    return;
  }

  // Process message
});

4. Waliduj i sanityzuj wiadomości:

import Joi from 'joi';

const messageSchema = Joi.object({
  type: Joi.string().valid('chat', 'action', 'ping').required(),
  payload: Joi.any(),
  timestamp: Joi.number()
});

ws.on('message', (data) => {
  let message;
  try {
    message = JSON.parse(data);
  } catch {
    ws.close(4000, 'Invalid JSON');
    return;
  }

  const { error, value } = messageSchema.validate(message);
  if (error) {
    ws.send(JSON.stringify({ error: 'Invalid message format' }));
    return;
  }

  // Safe to process
});

Jakie są typowe close codes i ich znaczenie?

// Standard close codes (RFC 6455)
1000 // Normal Closure - wszystko OK
1001 // Going Away - serwer/klient się wyłącza
1002 // Protocol Error - błąd protokołu WebSocket
1003 // Unsupported Data - nieobsługiwany typ danych
1005 // No Status Received - nie podano kodu (reserved)
1006 // Abnormal Closure - połączenie zerwane (reserved)
1007 // Invalid Payload Data - np. nieprawidłowe UTF-8
1008 // Policy Violation - naruszenie polityki
1009 // Message Too Big - za duża wiadomość
1010 // Mandatory Extension - brak wymaganego extension
1011 // Internal Error - niespodziewany błąd serwera
1012 // Service Restart - serwer się restartuje
1013 // Try Again Later - temporary condition
1014 // Bad Gateway - proxy/gateway error
1015 // TLS Handshake - błąd TLS (reserved)

// Custom codes (4000-4999)
4001 // Unauthorized
4029 // Rate Limit Exceeded
4000 // Bad Request

Zaawansowane Wzorce

Jak implementować pub/sub pattern z WebSockets?

class PubSub {
  constructor() {
    this.channels = new Map(); // channel -> Set<WebSocket>
  }

  subscribe(ws, channel) {
    if (!this.channels.has(channel)) {
      this.channels.set(channel, new Set());
    }
    this.channels.get(channel).add(ws);

    ws.on('close', () => this.unsubscribe(ws, channel));
  }

  unsubscribe(ws, channel) {
    const subscribers = this.channels.get(channel);
    if (subscribers) {
      subscribers.delete(ws);
      if (subscribers.size === 0) {
        this.channels.delete(channel);
      }
    }
  }

  publish(channel, message) {
    const subscribers = this.channels.get(channel);
    if (!subscribers) return;

    const data = JSON.stringify(message);
    subscribers.forEach(ws => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(data);
      }
    });
  }
}

const pubsub = new PubSub();

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const { type, channel, message } = JSON.parse(data);

    switch (type) {
      case 'subscribe':
        pubsub.subscribe(ws, channel);
        break;
      case 'unsubscribe':
        pubsub.unsubscribe(ws, channel);
        break;
      case 'publish':
        pubsub.publish(channel, message);
        break;
    }
  });
});

Jak implementować presence (kto jest online)?

const presence = new Map(); // odwrotna mapa: ws -> userData
const userSockets = new Map(); // userId -> Set<WebSocket>

function broadcastPresence() {
  const users = Array.from(presence.values());
  const message = JSON.stringify({ type: 'presence', users });

  wss.clients.forEach(ws => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
  });
}

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const { type, user } = JSON.parse(data);

    if (type === 'join') {
      presence.set(ws, {
        id: user.id,
        name: user.name,
        status: 'online',
        joinedAt: Date.now()
      });

      if (!userSockets.has(user.id)) {
        userSockets.set(user.id, new Set());
      }
      userSockets.get(user.id).add(ws);

      broadcastPresence();
    }
  });

  ws.on('close', () => {
    const user = presence.get(ws);
    if (user) {
      presence.delete(ws);

      const sockets = userSockets.get(user.id);
      if (sockets) {
        sockets.delete(ws);
        if (sockets.size === 0) {
          userSockets.delete(user.id);
        }
      }

      broadcastPresence();
    }
  });
});

Jak obsłużyć message ordering i delivery guarantees?

WebSocket gwarantuje ordering w ramach jednego połączenia, ale nie gwarantuje delivery (połączenie może się zerwać):

// Klient z acknowledgments i retry
class ReliableSocket {
  constructor(url) {
    this.pending = new Map(); // messageId -> { message, timeout, retries }
    this.sequence = 0;
    this.connect(url);
  }

  send(payload) {
    const messageId = ++this.sequence;
    const message = { id: messageId, payload };

    return new Promise((resolve, reject) => {
      this.pending.set(messageId, {
        message,
        resolve,
        reject,
        retries: 0
      });

      this.trySend(messageId);
    });
  }

  trySend(messageId) {
    const entry = this.pending.get(messageId);
    if (!entry) return;

    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(entry.message));

      // Retry jeśli brak ACK w 5 sekund
      entry.timeout = setTimeout(() => {
        entry.retries++;
        if (entry.retries < 3) {
          this.trySend(messageId);
        } else {
          entry.reject(new Error('Message delivery failed'));
          this.pending.delete(messageId);
        }
      }, 5000);
    }
  }

  handleAck(messageId) {
    const entry = this.pending.get(messageId);
    if (entry) {
      clearTimeout(entry.timeout);
      entry.resolve();
      this.pending.delete(messageId);
    }
  }
}

// Serwer
ws.on('message', (data) => {
  const { id, payload } = JSON.parse(data);

  // Wyślij ACK
  ws.send(JSON.stringify({ type: 'ack', id }));

  // Process message
  processMessage(payload);
});

Jak streamować duże dane przez WebSocket?

// Serwer - chunked streaming
async function streamLargeFile(ws, filePath) {
  const stream = createReadStream(filePath, { highWaterMark: 64 * 1024 }); // 64KB chunks
  let chunkIndex = 0;
  const totalChunks = Math.ceil(statSync(filePath).size / (64 * 1024));

  ws.send(JSON.stringify({
    type: 'stream_start',
    totalChunks,
    fileName: path.basename(filePath)
  }));

  for await (const chunk of stream) {
    ws.send(JSON.stringify({
      type: 'stream_chunk',
      index: chunkIndex++,
      data: chunk.toString('base64')
    }));

    // Backpressure - czekaj jeśli bufor pełny
    if (ws.bufferedAmount > 1024 * 1024) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }

  ws.send(JSON.stringify({ type: 'stream_end' }));
}

// Klient - odbieranie chunków
const chunks = [];
let expectedChunks = 0;

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'stream_start':
      expectedChunks = msg.totalChunks;
      chunks.length = 0;
      break;

    case 'stream_chunk':
      chunks[msg.index] = atob(msg.data);
      updateProgress(chunks.length / expectedChunks);
      break;

    case 'stream_end':
      const file = chunks.join('');
      downloadFile(file);
      break;
  }
};

Zadania Praktyczne

1. Zaimplementuj chat room z rooms i presence

Wymagania:

  • Użytkownik może dołączyć do wielu rooms
  • Lista użytkowników online w każdym room
  • Historia ostatnich 50 wiadomości per room
  • Typing indicator

2. Stwórz system notyfikacji z prioritetami

Wymagania:

  • Notyfikacje z priority: low, medium, high, urgent
  • Urgent = natychmiastowa dostawa + retry
  • Low = batch co 30 sekund
  • Persistence - notyfikacje czekają na reconnect

3. Zaimplementuj collaborative editing (simple)

Wymagania:

  • Wiele osób edytuje ten sam tekst
  • Sync kursora (gdzie jest każdy użytkownik)
  • Conflict resolution (last-write-wins lub operational transform)

4. Zaprojektuj skalowalne rozwiązanie

Scenariusz: 100k równoczesnych użytkowników, 10 serwerów, rooms do 1000 osób.

Opisz:

  • Architekturę (load balancer, serwery, message broker)
  • Jak obsłużyć sticky sessions
  • Jak broadcastować do room rozłożonego na wiele serwerów
  • Jak obsłużyć failure jednego serwera

Podsumowanie

WebSockets to fundament real-time komunikacji w web aplikacjach. Na rozmowach rekrutacyjnych skup się na:

  1. Podstawy protokołu - handshake, frames, różnice vs HTTP
  2. Wybór technologii - WebSocket vs SSE vs Long Polling vs Socket.IO
  3. Skalowanie - sticky sessions, Redis Pub/Sub, message brokers
  4. Connection management - heartbeats, reconnection, graceful shutdown
  5. Bezpieczeństwo - autentykacja, rate limiting, validation
  6. Wzorce - pub/sub, rooms, presence, reliable delivery

WebSockets wymagają więcej infrastruktury niż REST API - pamiętaj o tym przy projektowaniu architektury. Nie każda aplikacja potrzebuje real-time - polling co 30 sekund często wystarczy.

Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.