DevToolBoxGRATIS
Blog

WebSocket-tutorial: Gids voor realtime communicatie

15 min lezenby DevToolBox

What Are WebSockets?

WebSockets provide a persistent, full-duplex communication channel between a client (browser) and a server over a single TCP connection. Unlike HTTP, where the client must initiate every request, WebSockets allow both the server and client to send data at any time. This makes WebSockets ideal for real-time applications like chat, live dashboards, collaborative editing, multiplayer games, and stock tickers.

This tutorial covers the WebSocket protocol, client-side and server-side implementation in JavaScript, error handling, reconnection strategies, and production best practices.

WebSocket vs HTTP: Key Differences

FeatureHTTPWebSocket
ConnectionNew connection per requestPersistent connection
DirectionClient-to-server (request/response)Bidirectional (full-duplex)
OverheadHeaders sent with every requestMinimal framing after handshake
LatencyHigher (connection setup each time)Lower (connection stays open)
Protocolhttp:// or https://ws:// or wss://
Use CaseREST APIs, page loadsReal-time data, live updates
Server PushNot native (use SSE or polling)Native server push

How WebSocket Handshake Works

A WebSocket connection starts as an HTTP request that gets upgraded to the WebSocket protocol. The client sends an upgrade request, and the server responds with a 101 Switching Protocols status.

# Client Request (HTTP Upgrade)
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

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

# After this handshake, the connection is upgraded
# and both sides can send WebSocket frames

Client-Side WebSocket API

The browser provides a native WebSocket API that is straightforward to use. Here is a complete client implementation with all event handlers.

// Basic WebSocket connection
const ws = new WebSocket('wss://echo.websocket.org');

// Connection opened
ws.addEventListener('open', (event) => {
  console.log('Connected to WebSocket server');
  ws.send('Hello Server!');
});

// Listen for messages
ws.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);

  // Handle different data types
  if (event.data instanceof Blob) {
    // Binary data
    const reader = new FileReader();
    reader.onload = () => console.log('Binary:', reader.result);
    reader.readAsArrayBuffer(event.data);
  } else {
    // Text data (usually JSON)
    try {
      const data = JSON.parse(event.data);
      console.log('Parsed:', data);
    } catch {
      console.log('Text:', event.data);
    }
  }
});

// Connection closed
ws.addEventListener('close', (event) => {
  console.log('Disconnected:', event.code, event.reason);
  console.log('Clean close:', event.wasClean);
});

// Error occurred
ws.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

// Send different data types
ws.send('Plain text message');
ws.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
ws.send(new Blob(['binary data']));
ws.send(new ArrayBuffer(8));

// Check connection state
console.log(ws.readyState);
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSING
// 3 = CLOSED

// Close connection gracefully
ws.close(1000, 'Normal closure');

Server-Side Implementation with Node.js

The most popular Node.js WebSocket library is ws. Here is a complete server implementation.

// server.ts - WebSocket server with Node.js
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

// Create HTTP server
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket server is running');
});

// Create WebSocket server
const wss = new WebSocketServer({ server });

// Track connected clients
const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, req) => {
  const clientId = generateId();
  clients.set(clientId, ws);
  console.log(`Client ${clientId} connected from ${req.socket.remoteAddress}`);

  // Send welcome message
  ws.send(JSON.stringify({
    type: 'welcome',
    clientId,
    message: 'Connected to WebSocket server',
  }));

  // Handle incoming messages
  ws.on('message', (data, isBinary) => {
    try {
      const message = JSON.parse(data.toString());
      console.log(`Received from ${clientId}:`, message);

      switch (message.type) {
        case 'chat':
          // Broadcast to all other clients
          broadcast({
            type: 'chat',
            from: clientId,
            text: message.text,
            timestamp: Date.now(),
          }, ws);
          break;

        case 'ping':
          ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
          break;

        default:
          ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' }));
      }
    } catch (err) {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
    }
  });

  // Handle client disconnect
  ws.on('close', (code, reason) => {
    clients.delete(clientId);
    console.log(`Client ${clientId} disconnected: ${code} ${reason}`);
    broadcast({
      type: 'system',
      message: `User ${clientId} left the chat`,
    });
  });

  // Handle errors
  ws.on('error', (error) => {
    console.error(`Client ${clientId} error:`, error);
    clients.delete(clientId);
  });

  // Heartbeat: detect dead connections
  (ws as any).isAlive = true;
  ws.on('pong', () => { (ws as any).isAlive = true; });
});

// Broadcast message to all clients except sender
function broadcast(data: object, exclude?: WebSocket) {
  const message = JSON.stringify(data);
  wss.clients.forEach(client => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Heartbeat interval to detect dead connections
const heartbeat = setInterval(() => {
  wss.clients.forEach(ws => {
    if ((ws as any).isAlive === false) {
      return ws.terminate();
    }
    (ws as any).isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => clearInterval(heartbeat));

// Start server
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

function generateId(): string {
  return Math.random().toString(36).substring(2, 9);
}

Robust Client with Auto-Reconnection

// WebSocket client with reconnection and message queue
class ReconnectingWebSocket {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 1000;
  private maxDelay = 30000;
  private messageQueue: string[] = [];
  private listeners = new Map<string, Set<Function>>();

  constructor(url: string) {
    this.url = url;
    this.connect();
  }

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

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;

      // Flush queued messages
      while (this.messageQueue.length > 0) {
        const msg = this.messageQueue.shift()!;
        this.ws!.send(msg);
      }

      this.emit('connected', {});
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.emit(data.type, data);
        this.emit('message', data);
      } catch {
        this.emit('message', { raw: event.data });
      }
    };

    this.ws.onclose = (event) => {
      console.log(`WebSocket closed: ${event.code}`);
      this.emit('disconnected', { code: event.code, reason: event.reason });

      if (event.code !== 1000) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = () => {
      this.emit('error', {});
    };
  }

  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.log('Max reconnection attempts reached');
      this.emit('maxRetriesReached', {});
      return;
    }

    const delay = Math.min(
      this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
      this.maxDelay
    );

    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);

    setTimeout(() => {
      this.reconnectAttempts++;
      this.connect();
    }, delay);
  }

  send(type: string, payload: any = {}) {
    const message = JSON.stringify({ type, ...payload });

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

  on(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
  }

  off(event: string, callback: Function) {
    this.listeners.get(event)?.delete(callback);
  }

  private emit(event: string, data: any) {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }

  close() {
    this.maxReconnectAttempts = 0; // Prevent reconnection
    this.ws?.close(1000, 'Client closing');
  }

  get state(): number {
    return this.ws?.readyState ?? WebSocket.CLOSED;
  }
}

// Usage
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');

ws.on('connected', () => console.log('Online'));
ws.on('disconnected', () => console.log('Offline'));
ws.on('chat', (data: any) => console.log('Chat:', data.text));

ws.send('chat', { text: 'Hello, World!' });

React Hook for WebSocket

// useWebSocket.ts - Custom React Hook
import { useEffect, useRef, useState, useCallback } from 'react';

type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';

interface UseWebSocketOptions {
  onMessage?: (data: any) => void;
  onOpen?: () => void;
  onClose?: (event: CloseEvent) => void;
  onError?: (event: Event) => void;
  reconnect?: boolean;
  reconnectInterval?: number;
  maxRetries?: number;
}

export function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
  const {
    onMessage,
    onOpen,
    onClose,
    onError,
    reconnect = true,
    reconnectInterval = 3000,
    maxRetries = 5,
  } = options;

  const [status, setStatus] = useState<WebSocketStatus>('connecting');
  const [lastMessage, setLastMessage] = useState<any>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const retriesRef = useRef(0);

  const connect = useCallback(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
    setStatus('connecting');

    ws.onopen = () => {
      setStatus('connected');
      retriesRef.current = 0;
      onOpen?.();
    };

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        setLastMessage(data);
        onMessage?.(data);
      } catch {
        setLastMessage(event.data);
        onMessage?.(event.data);
      }
    };

    ws.onclose = (event) => {
      setStatus('disconnected');
      onClose?.(event);

      if (reconnect && event.code !== 1000 && retriesRef.current < maxRetries) {
        retriesRef.current++;
        setTimeout(connect, reconnectInterval);
      }
    };

    ws.onerror = (event) => {
      setStatus('error');
      onError?.(event);
    };
  }, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval, maxRetries]);

  useEffect(() => {
    connect();
    return () => {
      wsRef.current?.close(1000);
    };
  }, [connect]);

  const send = useCallback((data: any) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(
        typeof data === 'string' ? data : JSON.stringify(data)
      );
    }
  }, []);

  return { status, lastMessage, send };
}

// Usage in a React component
function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<any[]>([]);

  const { status, send } = useWebSocket(
    `wss://api.example.com/chat/${roomId}`,
    {
      onMessage: (data) => {
        if (data.type === 'chat') {
          setMessages(prev => [...prev, data]);
        }
      },
    }
  );

  const handleSend = (text: string) => {
    send({ type: 'chat', text });
  };

  return (
    <div>
      <div>Status: {status}</div>
      <div>
        {messages.map((msg, i) => (
          <p key={i}>{msg.text}</p>
        ))}
      </div>
      <input
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            handleSend(e.currentTarget.value);
            e.currentTarget.value = '';
          }
        }}
        placeholder="Type a message..."
        disabled={status !== 'connected'}
      />
    </div>
  );
}

WebSocket with Socket.IO

// Socket.IO provides additional features on top of WebSockets:
// - Automatic reconnection
// - Room/namespace support
// - Fallback to HTTP long polling
// - Built-in acknowledgments

// Server (socket.io)
import { Server } from 'socket.io';

const io = new Server(3001, {
  cors: { origin: 'http://localhost:3000' },
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  // Join a room
  socket.on('join-room', (room: string) => {
    socket.join(room);
    io.to(room).emit('system', `${socket.id} joined ${room}`);
  });

  // Handle chat message
  socket.on('chat', (data, callback) => {
    io.to(data.room).emit('chat', {
      from: socket.id,
      text: data.text,
      timestamp: Date.now(),
    });
    // Acknowledge receipt
    callback({ status: 'delivered' });
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

// Client (socket.io-client)
import { io } from 'socket.io-client';

const socket = io('http://localhost:3001');

socket.on('connect', () => {
  socket.emit('join-room', 'general');
});

socket.on('chat', (data) => {
  console.log(`${data.from}: ${data.text}`);
});

// Send with acknowledgment
socket.emit('chat', { room: 'general', text: 'Hello!' }, (response: any) => {
  console.log('Message status:', response.status);
});

WebSocket Security Best Practices

  • Always use WSS (WebSocket Secure): Use wss:// in production, never ws://
  • Authenticate on connection: Validate JWT tokens or session cookies during the handshake
  • Rate limit messages: Prevent clients from flooding the server with messages
  • Validate all messages: Never trust client-sent data -- validate types, lengths, and content
  • Set message size limits: Configure maximum payload size to prevent memory attacks
  • Implement heartbeats: Use ping/pong frames to detect dead connections
  • Handle CORS properly: Restrict allowed origins in production
  • Use rooms for authorization: Only allow users to join rooms they have access to

Authentication Example

// Server-side authentication during WebSocket handshake
import { WebSocketServer } from 'ws';
import jwt from 'jsonwebtoken';

const wss = new WebSocketServer({
  server,
  verifyClient: (info, callback) => {
    const token = new URL(info.req.url!, 'http://localhost')
      .searchParams.get('token');

    if (!token) {
      callback(false, 401, 'Unauthorized');
      return;
    }

    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!);
      (info.req as any).user = decoded;
      callback(true);
    } catch {
      callback(false, 403, 'Invalid token');
    }
  },
});

// Client-side: pass token in URL
const token = localStorage.getItem('authToken');
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`);

WebSocket vs Server-Sent Events vs Long Polling

FeatureWebSocketSSELong Polling
DirectionBidirectionalServer to clientSimulated bidirectional
Protocolws:// / wss://HTTPHTTP
ConnectionPersistentPersistentRepeated requests
Browser SupportAll modernAll modernAll browsers
Binary DataYesNo (text only)Yes
Auto ReconnectManualBuilt-inManual
Proxy FriendlySometimes issuesYesYes
Best ForChat, gamingNotifications, feedsLegacy compatibility

Production Deployment Tips

  • Use a load balancer with sticky sessions: WebSocket connections must stay with the same server
  • Implement horizontal scaling: Use Redis pub/sub or similar to broadcast across multiple server instances
  • Monitor connection counts: Track active connections and set alerts for unusual spikes
  • Set connection limits per IP: Prevent a single client from opening too many connections
  • Configure Nginx for WebSocket: Add proper upgrade headers in your reverse proxy configuration
  • Graceful shutdown: Close all connections with proper close codes before server restart

Nginx WebSocket Proxy Configuration

# Nginx configuration for WebSocket proxy
upstream websocket_backend {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Timeout settings
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Conclusion

WebSockets are essential for any application requiring real-time, bidirectional communication. Start with the native browser WebSocket API for simple use cases, and consider Socket.IO or a custom reconnecting wrapper for production applications. Always implement proper error handling, reconnection logic, authentication, and message validation.

For simpler server-to-client streaming, consider Server-Sent Events (SSE) instead. For infrequent updates, regular HTTP polling may be sufficient. Choose the technology that matches your real-time requirements.

Test your WebSocket payloads with our JSON Formatter and debug JWT tokens in your authentication flow with the JWT Decoder.

𝕏 Twitterin LinkedIn
Was dit nuttig?

Blijf op de hoogte

Ontvang wekelijkse dev-tips en nieuwe tools.

Geen spam. Altijd opzegbaar.

Try These Related Tools

JWTJWT Decoder{ }JSON Formatter🔗URL ParserB64Base64 Encoder/Decoder

Related Articles

REST API Best Practices: De Complete Gids voor 2026

Leer REST API design best practices: naamconventies, foutafhandeling, authenticatie en beveiliging.

API-authenticatie: OAuth 2.0 vs JWT vs API Key

Vergelijk API-authenticatiemethoden: OAuth 2.0, JWT Bearer tokens en API keys.

CORS-fouten oplossen: Volledige troubleshooting gids

Los CORS-fouten stap voor stap op.