Budowanie aplikacji real-time z WebSockets w Node.js

W erze aplikacji webowych wymagających natychmiastowej interakcji, takich jak czaty, gry online czy systemy monitoringu, WebSockets stały się kluczową technologią. Node.js, dzięki swojej asynchronicznej naturze i lekkości, jest idealnym środowiskiem do budowania takich rozwiązań. W tym artykule omówimy podstawy WebSockets, porównamy popularne biblioteki, pokażemy przykłady kodu oraz najlepsze praktyki. Bazujemy na aktualnych rekomendacjach z 2024 i 2025 roku, w tym na wbudowanym wsparciu Node.js od wersji 21.

Czym są WebSockets i dlaczego warto je używać w Node.js?

WebSockets to protokół komunikacji, który umożliwia dwukierunkową, pełnodupleksową wymianę danych między klientem a serwerem poprzez pojedyncze, trwałe połączenie. W przeciwieństwie do HTTP, gdzie każde żądanie wymaga nowego połączenia, WebSockets pozwalają na ciągły strumień danych z niskim opóźnieniem, co jest kluczowe dla aplikacji real-time.

Node.js doskonale nadaje się do tego celu dzięki modelowi event-driven i non-blocking I/O. Od wersji 21 Node.js oferuje wbudowane wsparcie dla klienta WebSocket (stabilne od v22.4.0), ale do serwera nadal polecane są biblioteki zewnętrzne. Popularne to:

  • ws: Lekka, szybka biblioteka implementująca czysty protokół WebSocket. Idealna do prostych zastosowań, bez dodatkowych overheadów.
  • Socket.IO: Rozszerza WebSockets o funkcje jak fallback na HTTP long-polling, automatyczne ponowne połączenia, acknowledgements, broadcasting i multiplexing (namespaces). Dodaje nieco overheadu (kilka bajtów na wiadomość), ale ułatwia rozwój złożonych aplikacji. Nie jest kompatybilna z czystymi WebSockets.

Wybór zależy od potrzeb: ws dla wydajności, Socket.IO dla wygody i skalowalności.

Podstawowa konfiguracja serwera WebSocket

Zacznijmy od instalacji: npm install ws lub npm install socket.io.

Przykład z biblioteką ws

Prosty serwer broadcastujący wiadomości do wszystkich klientów:

JavaScript
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  ws.send('Witaj na serwerze WebSocket!');
  ws.on('message', (message) => {
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`Serwer otrzymał: ${message}`);
      }
    });
  });
  ws.on('close', () => console.log('Klient rozłączony'));
});

Klient w przeglądarce:

JavaScript
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => ws.send('Cześć, serwerze!');
ws.onmessage = (event) => console.log(`Wiadomość z serwera: ${event.data}`);
ws.onclose = () => console.log('Połączenie zamknięte');

Ten kod umożliwia broadcasting i obsługę rozłączeń.

Przykład z Socket.IO

Serwer:

JavaScript
const { Server } = require("socket.io");
const io = new Server(8000);

io.on("connection", (socket) => {
  console.log("Użytkownik połączony");
  socket.on("wiadomosc", (data) => {
    io.emit("wiadomosc", data); // Broadcasting do wszystkich
  });
});

Klient:

JavaScript
const io = require("socket.io-client");
const socket = io("http://localhost:8000");

socket.on("connect", () => {
  console.log("Połączono");
  socket.emit("wiadomosc", "Cześć!");
});
socket.on("wiadomosc", (data) => console.log(data));

Socket.IO dodaje funkcje jak acknowledgements: socket.emit("wiadomosc", data, (ack) => { /* potwierdzenie */ });.

Przykładowa aplikacja: Czat real-time

Budujemy prosty czat z wieloma klientami. Użyjemy ws dla serwera i HTML/JS dla klienta.

Serwer (server.js):

JavaScript
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const port = 6969;
const server = http.createServer(express);
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });
});

server.listen(port, () => console.log(`Serwer nasłuchuje na porcie ${port}!`));

Klient (index.html):

HTML
<h1>Czat Real-Time</h1>
<pre id="messages" style="height: 400px; overflow: scroll"></pre>
<input type="text" id="messageBox" placeholder="Wpisz wiadomość" style="display: block; width: 100%; margin-bottom: 10px; padding: 10px;" />
<button id="send" style="width: 100%; height: 30px;">Wyślij</button>
<script>
  const sendBtn = document.querySelector('#send');
  const messages = document.querySelector('#messages');
  const messageBox = document.querySelector('#messageBox');
  let ws;
  function showMessage(message) {
    messages.textContent += `\n\n${message}`;
    messages.scrollTop = messages.scrollHeight;
    messageBox.value = '';
  }
  function init() {
    ws = new WebSocket('ws://localhost:6969');
    ws.onopen = () => console.log('Połączono!');
    ws.onmessage = ({ data }) => showMessage(data);
    ws.onclose = () => ws = null;
  }
  sendBtn.onclick = () => {
    if (!ws) return showMessage("Brak połączenia :(");
    ws.send(messageBox.value);
    showMessage(messageBox.value);
  }
  init();
</script>

Ten przykład umożliwia wysyłanie wiadomości między wieloma klientami w czasie rzeczywistym.

Najlepsze praktyki

  • Bezpieczeństwo: Używaj WSS (z SSL) zamiast WS. Implementuj autentykację JWT w nagłówkach. Odświeżaj połączenia co godzinę dla odnowienia tokenów.
  • Skalowalność: Monitoruj liczbę otwartych połączeń, nie tylko CPU. Używaj brokerów wiadomości (Redis, Kafka) z Pub/Sub dla multi-node. Dostosuj ustawienia OS dla wielu połączeń.
  • Monitorowanie: Implementuj heartbeat (ping/pong co 30s) do wykrywania zerwanych połączeń. Używaj APM jak Elastic czy New Relic z custom transactions.
  • Inne: Obsługuj błędy, optymalizuj rozmiar wiadomości, używaj kompresji. Dla automatycznego reconnection – Socket.IO ma to wbudowane, dla ws implementuj ręcznie.

Wniosek

WebSockets w Node.js otwierają drzwi do dynamicznych aplikacji real-time. Zaczynając od prostych przykładów, możesz skalować do złożonych systemów. Pamiętaj o aktualizacjach Node.js i testowaniu w produkcji. Śledź dokumentację i community dla nowych trendów.

Citations: