Standardowy HTTP działa jak wizyta na poczcie – przychodzisz, składasz prośbę, czekasz na odpowiedź i wychodzisz. Świetne do pobierania stron i wysyłania formularzy. Fatalne do czatu, gdzie chcesz widzieć wiadomości w ułamku sekundy. WebSocket zmienia ten model fundamentalnie – otwiera stałe, dwukierunkowe połączenie, przez które serwer może wysyłać dane do klienta w dowolnym momencie, bez czekania na pytanie.
Czym są WebSockety i kiedy ich używać?
WebSocket to protokół komunikacyjny działający na warstwie TCP, który po nawiązaniu połączenia utrzymuje je otwarte przez cały czas sesji. Zarówno klient jak i serwer mogą wysyłać dane w dowolnym momencie – bez konieczności inicjowania każdej wymiany przez klienta.
Połączenie WebSocket zaczyna się od standardowego żądania HTTP z nagłówkiem Upgrade: websocket. Serwer akceptuje upgrade, protokół przełącza się z HTTP na WebSocket i od tej chwili kanał jest otwarty. Ten proces nazywa się WebSocket handshake.
Kiedy WebSocket jest właściwym wyborem:
Czat i komunikatory – każda wiadomość musi dotrzeć natychmiast do wszystkich uczestników bez odpytywania serwera co sekundę. Aplikacje tradingowe i dashboardy finansowe – kursy walut i ceny akcji zmieniają się w milisekundach. Gry wieloosobowe – pozycje graczy, zdarzenia, wynik muszą być synchronizowane w czasie rzeczywistym. Kolaboratywne edytory tekstu jak Google Docs – zmiany jednego użytkownika muszą natychmiast pojawiać się u pozostałych. Powiadomienia push w aplikacjach webowych – serwer informuje klienta o zdarzeniu bez czekania na żądanie.
Kiedy WebSocket nie jest potrzebny:
Jeśli dane zmieniają się rzadko lub klient inicjuje każdą wymianę – klasyczny HTTP jest prostszy i wystarczający. Dla jednostronnego strumienia danych z serwera do klienta (np. aktualizacje statusu, powiadomienia) prostszą alternatywą są Server-Sent Events (SSE) – jednokierunkowy strumień oparty na HTTP, który nie wymaga bibliotek i jest łatwiejszy w implementacji.
Implementacja WebSocket – klient i serwer
Przeglądarka ma wbudowane API WebSocket – nie potrzebujesz żadnych bibliotek po stronie klienta do podstawowej implementacji.
Klient (JavaScript w przeglądarce):
// Nawiązanie połączenia
const ws = new WebSocket('wss://api.example.com/ws')
// Zdarzenia połączenia
ws.addEventListener('open', () => {
console.log('Połączono z serwerem')
ws.send(JSON.stringify({ type: 'auth', token: getAuthToken() }))
})
// Odbieranie wiadomości
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
handleMessage(data)
})
// Obsługa błędów
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error)
})
// Zamknięcie połączenia
ws.addEventListener('close', (event) => {
console.log(`Połączenie zamknięte: ${event.code} ${event.reason}`)
if (!event.wasClean) {
scheduleReconnect() // Automatyczne ponowne połączenie
}
})
// Wysyłanie wiadomości
function sendMessage(content) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'message', content }))
}
}Serwer (Node.js z biblioteką ws)
const WebSocket = require('ws')
const http = require('http')
const server = http.createServer()
const wss = new WebSocket.Server({ server })
// Mapa połączeń z metadanymi
const clients = new Map()
wss.on('connection', (ws, req) => {
const clientId = generateId()
clients.set(ws, { id: clientId, authenticated: false })
console.log(`Nowe połączenie: ${clientId}`)
ws.on('message', (data) => {
try {
const message = JSON.parse(data)
handleMessage(ws, message)
} catch (error) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }))
}
})
ws.on('close', () => {
clients.delete(ws)
console.log(`Połączenie zamknięte: ${clientId}`)
})
ws.on('error', (error) => {
console.error(`Błąd klienta ${clientId}:`, error)
})
})
// Broadcast do wszystkich połączonych klientów
function broadcast(message, excludeClient = null) {
const data = JSON.stringify(message)
wss.clients.forEach((client) => {
if (client !== excludeClient && client.readyState === WebSocket.OPEN) {
client.send(data)
}
})
}
server.listen(3000)Protokół wiadomości – jak strukturyzować dane
WebSocket to tylko kanał transportowy – nie definiuje formatu przesyłanych danych. Musisz sam zaprojektować protokół wiadomości, który będzie używany przez klienta i serwer.
Najpopularniejszym podejściem jest protokół oparty na typach wiadomości – każda wiadomość JSON ma pole type określające jej rodzaj i pole payload z danymi:
// Przykładowe typy wiadomości w aplikacji czatu
const MESSAGE_TYPES = {
AUTH: 'auth',
AUTH_SUCCESS: 'auth_success',
AUTH_ERROR: 'auth_error',
CHAT_MESSAGE: 'chat_message',
USER_JOINED: 'user_joined',
USER_LEFT: 'user_left',
PING: 'ping',
PONG: 'pong'
}
// Przykład obsługi wiadomości po stronie serwera
function handleMessage(ws, message) {
const client = clients.get(ws)
switch (message.type) {
case MESSAGE_TYPES.AUTH:
const user = verifyToken(message.token)
if (user) {
clients.set(ws, { ...client, authenticated: true, user })
ws.send(JSON.stringify({ type: MESSAGE_TYPES.AUTH_SUCCESS, user }))
broadcast({ type: MESSAGE_TYPES.USER_JOINED, user }, ws)
} else {
ws.send(JSON.stringify({ type: MESSAGE_TYPES.AUTH_ERROR }))
ws.close(1008, 'Unauthorized')
}
break
case MESSAGE_TYPES.CHAT_MESSAGE:
if (!client.authenticated) {
ws.close(1008, 'Unauthorized')
return
}
broadcast({
type: MESSAGE_TYPES.CHAT_MESSAGE,
content: sanitize(message.content),
user: client.user,
timestamp: Date.now()
})
break
case MESSAGE_TYPES.PING:
ws.send(JSON.stringify({ type: MESSAGE_TYPES.PONG }))
break
}
}Socket.IO to biblioteka, która buduje warstwę abstrakcji nad WebSocket i dodaje wiele użytecznych funkcji: automatyczne ponowne łączenie, pokoje (rooms), przestrzenie nazw (namespaces), fallback do long-polling gdy WebSocket jest niedostępny i wbudowany system zdarzeń. Warto jej używać w projektach produkcyjnych zamiast surowego WebSocket API:
// Socket.IO - serwer
const io = require('socket.io')(server)
io.on('connection', (socket) => {
socket.on('join-room', (roomId) => {
socket.join(roomId)
socket.to(roomId).emit('user-joined', { userId: socket.id })
})
socket.on('chat-message', ({ roomId, content }) => {
io.to(roomId).emit('chat-message', {
content,
userId: socket.id,
timestamp: Date.now()
})
})
})
// Socket.IO - klient
const socket = io('https://api.example.com')
socket.emit('join-room', 'room-123')
socket.on('chat-message', (message) => renderMessage(message))Kluczowe wyzwania produkcyjne
Surowa implementacja WebSocket działa świetnie w środowisku deweloperskim. Produkcja stawia dodatkowe wymagania, o których warto wiedzieć zanim wdrożysz rozwiązanie na żywo.
Automatyczne ponowne łączenie – połączenia WebSocket zrywają się. Sieć jest zawodna, serwery się restartują, użytkownicy tracą zasięg. Klient musi automatycznie próbować ponownie nawiązać połączenie z wykładniczym opóźnieniem (exponential backoff) żeby nie zalewać serwera żądaniami:
class ReconnectingWebSocket {
constructor(url) {
this.url = url
this.reconnectDelay = 1000
this.maxReconnectDelay = 30000
this.connect()
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.addEventListener('open', () => {
this.reconnectDelay = 1000 // Reset opóźnienia po udanym połączeniu
})
this.ws.addEventListener('close', () => {
setTimeout(() => this.connect(), this.reconnectDelay)
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
)
})
}
}Heartbeat / Ping-Pong – niektóre proxy i load balancery zamykają nieaktywne połączenia po kilkudziesięciu sekundach ciszy. Heartbeat to regularne wysyłanie wiadomości ping/pong (co 30-60 sekund) żeby utrzymać połączenie przy życiu i wykryć zerwane połączenia:
// Serwer - wykrywanie martwych połączeń
function setupHeartbeat(wss) {
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) {
ws.terminate()
return
}
ws.isAlive = false
ws.ping()
})
}, 30000)
wss.on('connection', (ws) => {
ws.isAlive = true
ws.on('pong', () => { ws.isAlive = true })
})
}Skalowanie horyzontalne – WebSocket to stanowe połączenie. Gdy skalujesz aplikację na wiele instancji serwera, klient połączony z instancją A nie może bezpośrednio komunikować się z klientem połączonym z instancją B. Rozwiązaniem jest broker wiadomości – Redis Pub/Sub lub Apache Kafka – przez który wszystkie instancje serwera wymieniają wiadomości:
// Redis Pub/Sub dla WebSocket broadcasting w wielu instancjach
const redis = require('ioredis')
const publisher = new redis()
const subscriber = new redis()
// Subskrypcja do kanału Redis
subscriber.subscribe('websocket-messages')
subscriber.on('message', (channel, message) => {
// Rozgłoś do wszystkich lokalnych klientów WebSocket
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message)
}
})
})
// Publikowanie wiadomości przez Redis (broadcastuje do wszystkich instancji)
function broadcastToAll(message) {
publisher.publish('websocket-messages', JSON.stringify(message))
}Bezpieczeństwo – WebSocket nie ma wbudowanego mechanizmu autoryzacji. Użyj wss:// (WebSocket przez TLS) zamiast ws:// w produkcji. Weryfikuj token JWT lub ciasteczko sesji przy nawiązaniu połączenia. Waliduj i sanityzuj każdą przychodzącą wiadomość. Implementuj rate limiting na poziomie połączeń i wiadomości.
Testowanie i debugowanie
Chrome DevTools ma wbudowaną obsługę WebSocket – w zakładce Network filtruj po „WS” żeby zobaczyć połączenia WebSocket, a po kliknięciu w połączenie – pełną historię wysłanych i odebranych wiadomości z timestampami.
Testowanie serwera przez narzędzie websocat (CLI) lub Postman (obsługuje WebSocket od wersji 8.5):
# websocat - testowanie z linii poleceń
websocat ws://localhost:3000
# Wpisz wiadomość JSON i naciśnij Enter
{"type": "ping"}Testy jednostkowe dla logiki WebSocket najlepiej pisać mockując połączenie:
// Jest + mock WebSocket
const { WebSocket, Server } = require('mock-socket')
test('obsługuje wiadomość auth', async () => {
const server = new Server('ws://localhost:3000')
const client = new WebSocket('ws://localhost:3000')
server.on('connection', (socket) => {
socket.on('message', (data) => {
const msg = JSON.parse(data)
if (msg.type === 'auth') {
socket.send(JSON.stringify({ type: 'auth_success' }))
}
})
})
client.send(JSON.stringify({ type: 'auth', token: 'valid-token' }))
// ... asercje
})WebSockety to technologia, która przy właściwym użyciu otwiera zupełnie nowe możliwości interaktywności aplikacji webowych. Kluczem jest dobry protokół wiadomości, solidna obsługa ponownego łączenia i świadomość wyzwań skalowania zanim trafisz na nie w produkcji. Jeśli masz pytania dotyczące konkretnej implementacji lub architektury czasu rzeczywistego – napisz przez formularz kontaktowy.