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):
- Text frame (0x1) - dane tekstowe UTF-8
- Binary frame (0x2) - dane binarne
- Close frame (0x8) - inicjuje zamknięcie połączenia
- Ping frame (0x9) - heartbeat od klienta lub serwera
- Pong frame (0xA) - odpowiedź na ping
- 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:
- Automatic fallback - jeśli WebSocket nie działa, używa long-polling
- Automatic reconnection - z exponential backoff
- Rooms i namespaces - logiczne grupowanie klientów
- Acknowledgments - potwierdzenie dostarczenia wiadomości
- Broadcasting - wysyłanie do wszystkich oprócz sendera
- 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:
- Sticky sessions - klient musi trafiać do tego samego serwera
- 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:
- Handshake i połączenie muszą być na tym samym serwerze
- 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:
- Podstawy protokołu - handshake, frames, różnice vs HTTP
- Wybór technologii - WebSocket vs SSE vs Long Polling vs Socket.IO
- Skalowanie - sticky sessions, Redis Pub/Sub, message brokers
- Connection management - heartbeats, reconnection, graceful shutdown
- Bezpieczeństwo - autentykacja, rate limiting, validation
- 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.