Browser APIsFeatured

JavaScript WebSockets: Real-Time Communication Guide

Master WebSockets in JavaScript for real-time communication. Learn bidirectional messaging, Socket.IO, and building real-time applications.

By JavaScriptDoc Team
websocketsreal-timejavascriptsocket.iocommunication

JavaScript WebSockets: Real-Time Communication Guide

WebSockets provide full-duplex, bidirectional communication between clients and servers, enabling real-time data exchange. This guide covers everything from basic WebSocket usage to building complex real-time applications.

Understanding WebSockets

WebSockets enable persistent connections between the client and server, allowing both parties to send data at any time.

// Basic WebSocket connection
const socket = new WebSocket('ws://localhost:8080');

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

socket.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);
});

socket.addEventListener('close', (event) => {
  console.log('Disconnected from WebSocket server');
  console.log('Code:', event.code, 'Reason:', event.reason);
});

socket.addEventListener('error', (error) => {
  console.error('WebSocket error:', error);
});

// Check connection state
console.log('State:', socket.readyState);
// 0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED

WebSocket Client Implementation

Enhanced WebSocket Client

class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = {
      reconnect: true,
      reconnectInterval: 1000,
      maxReconnectAttempts: 5,
      heartbeatInterval: 30000,
      ...options,
    };

    this.reconnectAttempts = 0;
    this.eventHandlers = new Map();
    this.messageQueue = [];
    this.isConnected = false;

    this.connect();
  }

  connect() {
    try {
      this.ws = new WebSocket(this.url);
      this.setupEventHandlers();
    } catch (error) {
      console.error('WebSocket connection error:', error);
      this.handleReconnect();
    }
  }

  setupEventHandlers() {
    this.ws.onopen = (event) => {
      console.log('WebSocket connected');
      this.isConnected = true;
      this.reconnectAttempts = 0;

      // Send queued messages
      this.flushMessageQueue();

      // Start heartbeat
      this.startHeartbeat();

      // Emit open event
      this.emit('open', event);
    };

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

        // Handle different message types
        if (data.type === 'pong') {
          this.handlePong();
        } else {
          this.emit('message', data);

          // Emit typed events
          if (data.type) {
            this.emit(data.type, data.payload);
          }
        }
      } catch (error) {
        // Handle non-JSON messages
        this.emit('message', event.data);
      }
    };

    this.ws.onclose = (event) => {
      console.log('WebSocket closed:', event.code, event.reason);
      this.isConnected = false;
      this.stopHeartbeat();

      this.emit('close', event);

      if (this.options.reconnect && !this.manualClose) {
        this.handleReconnect();
      }
    };

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

  handleReconnect() {
    if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      this.emit('reconnectFailed');
      return;
    }

    this.reconnectAttempts++;
    const delay = this.options.reconnectInterval * this.reconnectAttempts;

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

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

  send(data) {
    const message = typeof data === 'string' ? data : JSON.stringify(data);

    if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      // Queue message if not connected
      this.messageQueue.push(message);
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.ws.send(message);
    }
  }

  // Event emitter pattern
  on(event, handler) {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, []);
    }
    this.eventHandlers.get(event).push(handler);
  }

  off(event, handler) {
    const handlers = this.eventHandlers.get(event);
    if (handlers) {
      const index = handlers.indexOf(handler);
      if (index !== -1) {
        handlers.splice(index, 1);
      }
    }
  }

  emit(event, data) {
    const handlers = this.eventHandlers.get(event);
    if (handlers) {
      handlers.forEach((handler) => handler(data));
    }
  }

  // Heartbeat mechanism
  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      if (this.isConnected) {
        this.send({ type: 'ping' });

        // Set timeout for pong response
        this.pongTimeout = setTimeout(() => {
          console.warn('Pong timeout - connection may be dead');
          this.ws.close();
        }, 5000);
      }
    }, this.options.heartbeatInterval);
  }

  handlePong() {
    clearTimeout(this.pongTimeout);
  }

  stopHeartbeat() {
    clearInterval(this.heartbeatInterval);
    clearTimeout(this.pongTimeout);
  }

  close() {
    this.manualClose = true;
    this.stopHeartbeat();

    if (this.ws) {
      this.ws.close();
    }
  }
}

// Usage
const client = new WebSocketClient('ws://localhost:8080');

client.on('open', () => {
  console.log('Connected!');
  client.send({ type: 'subscribe', channel: 'updates' });
});

client.on('message', (data) => {
  console.log('Received:', data);
});

client.on('update', (payload) => {
  console.log('Update:', payload);
});

Binary Data Handling

class BinaryWebSocketClient extends WebSocketClient {
  constructor(url, options) {
    super(url, options);
    this.ws.binaryType = 'arraybuffer'; // or 'blob'
  }

  sendBinary(data) {
    if (data instanceof ArrayBuffer || data instanceof Blob) {
      this.ws.send(data);
    } else if (data instanceof Uint8Array) {
      this.ws.send(data.buffer);
    } else {
      throw new Error('Invalid binary data type');
    }
  }

  sendFile(file) {
    const reader = new FileReader();

    reader.onload = (event) => {
      const arrayBuffer = event.target.result;

      // Send file metadata first
      this.send({
        type: 'fileMetadata',
        name: file.name,
        size: file.size,
        type: file.type,
      });

      // Then send the file data
      this.sendBinary(arrayBuffer);
    };

    reader.readAsArrayBuffer(file);
  }

  setupEventHandlers() {
    super.setupEventHandlers();

    const originalOnMessage = this.ws.onmessage;

    this.ws.onmessage = (event) => {
      if (event.data instanceof ArrayBuffer) {
        this.handleBinaryMessage(event.data);
      } else {
        originalOnMessage.call(this.ws, event);
      }
    };
  }

  handleBinaryMessage(arrayBuffer) {
    // Example: Handle image data
    const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
    const url = URL.createObjectURL(blob);

    this.emit('binaryData', {
      blob,
      url,
      size: arrayBuffer.byteLength,
    });
  }
}

Real-Time Chat Application

Chat Client Implementation

class ChatClient {
  constructor(serverUrl, username) {
    this.username = username;
    this.messages = [];
    this.users = new Map();
    this.typing = new Map();

    this.ws = new WebSocketClient(serverUrl);
    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.ws.on('open', () => {
      this.join();
    });

    this.ws.on('message', (message) => {
      this.handleMessage(message);
    });

    this.ws.on('userJoined', (user) => {
      this.users.set(user.id, user);
      this.onUserJoined?.(user);
    });

    this.ws.on('userLeft', (userId) => {
      const user = this.users.get(userId);
      this.users.delete(userId);
      this.typing.delete(userId);
      this.onUserLeft?.(user);
    });

    this.ws.on('chatMessage', (data) => {
      this.messages.push(data);
      this.onMessageReceived?.(data);
    });

    this.ws.on('typing', (data) => {
      if (data.isTyping) {
        this.typing.set(data.userId, data.username);
      } else {
        this.typing.delete(data.userId);
      }
      this.onTypingUpdate?.(Array.from(this.typing.values()));
    });

    this.ws.on('userList', (users) => {
      this.users.clear();
      users.forEach((user) => this.users.set(user.id, user));
      this.onUserListUpdate?.(users);
    });
  }

  join() {
    this.ws.send({
      type: 'join',
      username: this.username,
    });
  }

  sendMessage(text) {
    const message = {
      type: 'chatMessage',
      text,
      timestamp: new Date().toISOString(),
    };

    this.ws.send(message);
  }

  sendTyping(isTyping) {
    this.ws.send({
      type: 'typing',
      isTyping,
    });
  }

  handleMessage(message) {
    switch (message.type) {
      case 'history':
        this.messages = message.messages;
        this.onHistoryLoaded?.(this.messages);
        break;

      case 'error':
        this.onError?.(message.error);
        break;
    }
  }

  disconnect() {
    this.ws.close();
  }
}

// Chat UI Component
class ChatUI {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.setupUI();
    this.setupClient();
  }

  setupUI() {
    this.container.innerHTML = `
      <div class="chat-container">
        <div class="chat-header">
          <h3>Chat Room</h3>
          <div class="connection-status"></div>
        </div>
        
        <div class="chat-main">
          <div class="user-list">
            <h4>Users</h4>
            <ul id="users"></ul>
          </div>
          
          <div class="chat-messages">
            <div id="messages"></div>
            <div id="typing-indicator"></div>
          </div>
        </div>
        
        <div class="chat-input">
          <input type="text" id="message-input" placeholder="Type a message..." />
          <button id="send-button">Send</button>
        </div>
      </div>
    `;

    this.bindEvents();
  }

  bindEvents() {
    const input = document.getElementById('message-input');
    const sendButton = document.getElementById('send-button');

    input.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        this.sendMessage();
      }
    });

    let typingTimer;
    input.addEventListener('input', () => {
      if (!this.isTyping) {
        this.isTyping = true;
        this.client.sendTyping(true);
      }

      clearTimeout(typingTimer);
      typingTimer = setTimeout(() => {
        this.isTyping = false;
        this.client.sendTyping(false);
      }, 1000);
    });

    sendButton.addEventListener('click', () => {
      this.sendMessage();
    });
  }

  setupClient() {
    const username = prompt('Enter your username:') || 'Anonymous';
    this.client = new ChatClient('ws://localhost:8080', username);

    this.client.onMessageReceived = (message) => {
      this.addMessage(message);
    };

    this.client.onUserJoined = (user) => {
      this.addSystemMessage(`${user.username} joined the chat`);
      this.updateUserList();
    };

    this.client.onUserLeft = (user) => {
      this.addSystemMessage(`${user.username} left the chat`);
      this.updateUserList();
    };

    this.client.onTypingUpdate = (typingUsers) => {
      this.updateTypingIndicator(typingUsers);
    };

    this.client.onUserListUpdate = () => {
      this.updateUserList();
    };

    this.client.onHistoryLoaded = (messages) => {
      messages.forEach((msg) => this.addMessage(msg, false));
    };
  }

  sendMessage() {
    const input = document.getElementById('message-input');
    const text = input.value.trim();

    if (text) {
      this.client.sendMessage(text);
      input.value = '';
    }
  }

  addMessage(message, scrollToBottom = true) {
    const messagesDiv = document.getElementById('messages');
    const messageEl = document.createElement('div');
    messageEl.className = 'message';

    messageEl.innerHTML = `
      <span class="username">${message.username}:</span>
      <span class="text">${this.escapeHtml(message.text)}</span>
      <span class="timestamp">${this.formatTime(message.timestamp)}</span>
    `;

    messagesDiv.appendChild(messageEl);

    if (scrollToBottom) {
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }
  }

  addSystemMessage(text) {
    const messagesDiv = document.getElementById('messages');
    const messageEl = document.createElement('div');
    messageEl.className = 'system-message';
    messageEl.textContent = text;

    messagesDiv.appendChild(messageEl);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  }

  updateUserList() {
    const usersList = document.getElementById('users');
    usersList.innerHTML = '';

    this.client.users.forEach((user) => {
      const li = document.createElement('li');
      li.textContent = user.username;
      li.className = user.online ? 'online' : 'offline';
      usersList.appendChild(li);
    });
  }

  updateTypingIndicator(typingUsers) {
    const indicator = document.getElementById('typing-indicator');

    if (typingUsers.length === 0) {
      indicator.textContent = '';
    } else if (typingUsers.length === 1) {
      indicator.textContent = `${typingUsers[0]} is typing...`;
    } else {
      indicator.textContent = `${typingUsers.join(', ')} are typing...`;
    }
  }

  formatTime(timestamp) {
    const date = new Date(timestamp);
    return date.toLocaleTimeString();
  }

  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
}

Real-Time Collaboration

Collaborative Editor

class CollaborativeEditor {
  constructor(editorId, websocketUrl) {
    this.editor = document.getElementById(editorId);
    this.documentId = this.generateDocumentId();
    this.version = 0;
    this.pendingOperations = [];
    this.collaborators = new Map();

    this.ws = new WebSocketClient(websocketUrl);
    this.setupEditor();
    this.setupWebSocket();
  }

  setupEditor() {
    // Track local changes
    this.editor.addEventListener('input', (event) => {
      this.handleLocalChange(event);
    });

    // Track selection changes
    document.addEventListener('selectionchange', () => {
      const selection = window.getSelection();
      if (
        selection.rangeCount > 0 &&
        this.editor.contains(selection.anchorNode)
      ) {
        this.handleSelectionChange(selection);
      }
    });
  }

  setupWebSocket() {
    this.ws.on('open', () => {
      this.joinDocument();
    });

    this.ws.on('operation', (data) => {
      this.applyRemoteOperation(data);
    });

    this.ws.on('cursor', (data) => {
      this.updateRemoteCursor(data);
    });

    this.ws.on('collaboratorJoined', (data) => {
      this.addCollaborator(data);
    });

    this.ws.on('collaboratorLeft', (data) => {
      this.removeCollaborator(data.userId);
    });

    this.ws.on('documentState', (data) => {
      this.loadDocument(data);
    });
  }

  handleLocalChange(event) {
    const operation = this.createOperation(event);

    // Send to server
    this.ws.send({
      type: 'operation',
      documentId: this.documentId,
      operation: operation,
      version: this.version,
    });

    // Add to pending operations
    this.pendingOperations.push(operation);
  }

  createOperation(event) {
    // Simplified operation - in practice, use Operational Transform or CRDT
    return {
      type: 'insert',
      position: event.target.selectionStart,
      text: event.data,
      timestamp: Date.now(),
      userId: this.userId,
    };
  }

  applyRemoteOperation(data) {
    const { operation, userId } = data;

    // Skip if it's our own operation
    if (userId === this.userId) {
      this.pendingOperations.shift();
      return;
    }

    // Transform operation against pending operations
    const transformedOp = this.transformOperation(operation);

    // Apply to editor
    this.applyOperation(transformedOp);

    // Update version
    this.version++;
  }

  transformOperation(operation) {
    // Simplified transformation - implement OT algorithm
    let transformed = { ...operation };

    this.pendingOperations.forEach((pendingOp) => {
      if (pendingOp.position <= transformed.position) {
        transformed.position += pendingOp.text.length;
      }
    });

    return transformed;
  }

  applyOperation(operation) {
    const currentValue = this.editor.value;
    const before = currentValue.substring(0, operation.position);
    const after = currentValue.substring(operation.position);

    this.editor.value = before + operation.text + after;

    // Restore cursor position
    this.editor.setSelectionRange(
      operation.position + operation.text.length,
      operation.position + operation.text.length
    );
  }

  handleSelectionChange(selection) {
    const range = selection.getRangeAt(0);
    const position = this.getCaretPosition();

    this.ws.send({
      type: 'cursor',
      documentId: this.documentId,
      position: position,
      userId: this.userId,
    });
  }

  updateRemoteCursor(data) {
    const { userId, position, username, color } = data;

    let cursor = this.collaborators.get(userId);
    if (!cursor) {
      cursor = this.createCursorElement(username, color);
      this.collaborators.set(userId, cursor);
    }

    // Update cursor position
    this.positionCursor(cursor, position);
  }

  createCursorElement(username, color) {
    const cursor = document.createElement('div');
    cursor.className = 'remote-cursor';
    cursor.style.backgroundColor = color;

    const label = document.createElement('span');
    label.className = 'cursor-label';
    label.textContent = username;
    label.style.backgroundColor = color;

    cursor.appendChild(label);
    document.body.appendChild(cursor);

    return cursor;
  }

  positionCursor(cursor, position) {
    // Calculate pixel position from character position
    const coords = this.getCoordinatesFromPosition(position);

    cursor.style.left = coords.x + 'px';
    cursor.style.top = coords.y + 'px';
  }

  getCaretPosition() {
    return this.editor.selectionStart;
  }

  getCoordinatesFromPosition(position) {
    // Simplified - in practice, use a more sophisticated method
    const text = this.editor.value.substring(0, position);
    const lines = text.split('\n');

    return {
      x: this.editor.offsetLeft + lines[lines.length - 1].length * 8,
      y: this.editor.offsetTop + lines.length * 20,
    };
  }

  joinDocument() {
    this.ws.send({
      type: 'joinDocument',
      documentId: this.documentId,
      username: this.username,
    });
  }

  loadDocument(data) {
    this.editor.value = data.content;
    this.version = data.version;
  }

  generateDocumentId() {
    return 'doc_' + Math.random().toString(36).substr(2, 9);
  }
}

Socket.IO Integration

Socket.IO Client

// Socket.IO provides additional features over raw WebSockets
class SocketIOApp {
  constructor() {
    this.socket = io('http://localhost:3000', {
      transports: ['websocket'],
      reconnection: true,
      reconnectionAttempts: 5,
      reconnectionDelay: 1000,
    });

    this.setupEventHandlers();
  }

  setupEventHandlers() {
    // Connection events
    this.socket.on('connect', () => {
      console.log('Connected to server');
      console.log('Socket ID:', this.socket.id);
    });

    this.socket.on('disconnect', (reason) => {
      console.log('Disconnected:', reason);
    });

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

    // Custom events
    this.socket.on('message', (data) => {
      console.log('Received message:', data);
    });

    // Room events
    this.socket.on('userJoinedRoom', (data) => {
      console.log(`${data.username} joined room ${data.room}`);
    });

    this.socket.on('userLeftRoom', (data) => {
      console.log(`${data.username} left room ${data.room}`);
    });
  }

  // Emit events
  sendMessage(message) {
    this.socket.emit('message', message);
  }

  // Emit with acknowledgment
  sendWithAck(event, data) {
    return new Promise((resolve) => {
      this.socket.emit(event, data, (response) => {
        resolve(response);
      });
    });
  }

  // Join/leave rooms
  joinRoom(roomName) {
    this.socket.emit('joinRoom', roomName);
  }

  leaveRoom(roomName) {
    this.socket.emit('leaveRoom', roomName);
  }

  // Volatile events (can be dropped)
  sendVolatile(event, data) {
    this.socket.volatile.emit(event, data);
  }

  // Binary data
  sendBinary(data) {
    const buffer = new ArrayBuffer(data.length);
    const view = new Uint8Array(buffer);

    for (let i = 0; i < data.length; i++) {
      view[i] = data.charCodeAt(i);
    }

    this.socket.emit('binaryData', buffer);
  }
}

// Advanced Socket.IO patterns
class RealtimeGame {
  constructor() {
    this.socket = io('/game', {
      auth: {
        token: this.getAuthToken(),
      },
    });

    this.players = new Map();
    this.gameState = {};

    this.setupGame();
  }

  setupGame() {
    // Handle initial game state
    this.socket.on('gameState', (state) => {
      this.gameState = state;
      this.render();
    });

    // Handle player updates
    this.socket.on('playerUpdate', (data) => {
      this.players.set(data.id, data);
      this.renderPlayer(data);
    });

    // Handle game events with interpolation
    this.socket.on('gameUpdate', (updates) => {
      this.interpolateGameState(updates);
    });

    // Latency compensation
    this.socket.on('ping', () => {
      this.socket.emit('pong');
    });

    // Start game loop
    this.startGameLoop();
  }

  startGameLoop() {
    let lastTime = performance.now();

    const gameLoop = (currentTime) => {
      const deltaTime = currentTime - lastTime;
      lastTime = currentTime;

      // Update local game state
      this.update(deltaTime);

      // Send player input
      this.sendInput();

      // Render
      this.render();

      requestAnimationFrame(gameLoop);
    };

    requestAnimationFrame(gameLoop);
  }

  sendInput() {
    const input = this.getPlayerInput();

    if (input.hasChanged) {
      // Send input with timestamp for lag compensation
      this.socket.emit('playerInput', {
        input: input,
        timestamp: Date.now(),
        sequence: this.inputSequence++,
      });
    }
  }

  interpolateGameState(updates) {
    // Smooth interpolation between server updates
    const interpolationTime = 100; // ms

    updates.forEach((update) => {
      const player = this.players.get(update.id);

      if (player) {
        // Store target position
        player.targetX = update.x;
        player.targetY = update.y;
        player.lastUpdate = Date.now();
      }
    });
  }

  update(deltaTime) {
    // Interpolate player positions
    this.players.forEach((player) => {
      if (player.targetX !== undefined) {
        const t = Math.min(1, (Date.now() - player.lastUpdate) / 100);

        player.x = this.lerp(player.x, player.targetX, t);
        player.y = this.lerp(player.y, player.targetY, t);
      }
    });
  }

  lerp(start, end, t) {
    return start + (end - start) * t;
  }
}

Performance and Optimization

Message Batching

class BatchedWebSocket {
  constructor(url, batchInterval = 50) {
    this.ws = new WebSocket(url);
    this.batchInterval = batchInterval;
    this.messageQueue = [];
    this.startBatching();
  }

  startBatching() {
    this.batchTimer = setInterval(() => {
      if (this.messageQueue.length > 0) {
        this.flushBatch();
      }
    }, this.batchInterval);
  }

  send(message) {
    this.messageQueue.push(message);

    // Flush immediately if queue is getting large
    if (this.messageQueue.length >= 100) {
      this.flushBatch();
    }
  }

  flushBatch() {
    if (this.ws.readyState === WebSocket.OPEN) {
      const batch = {
        type: 'batch',
        messages: this.messageQueue.splice(0),
      };

      this.ws.send(JSON.stringify(batch));
    }
  }

  close() {
    clearInterval(this.batchTimer);
    this.flushBatch();
    this.ws.close();
  }
}

// Message compression
class CompressedWebSocket {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.compressionWorker = new Worker('compression-worker.js');

    this.setupWorker();
  }

  setupWorker() {
    this.compressionWorker.onmessage = (e) => {
      const { compressed, original } = e.data;

      // Only send compressed if it's smaller
      if (compressed.length < original.length * 0.9) {
        this.ws.send(compressed);
      } else {
        this.ws.send(original);
      }
    };
  }

  send(data) {
    const message = JSON.stringify(data);

    // Compress large messages
    if (message.length > 1024) {
      this.compressionWorker.postMessage({
        action: 'compress',
        data: message,
      });
    } else {
      this.ws.send(message);
    }
  }
}

Connection Pooling

class WebSocketPool {
  constructor(urls, options = {}) {
    this.urls = urls;
    this.options = {
      maxConnections: urls.length,
      strategy: 'round-robin', // or 'least-loaded', 'random'
      ...options,
    };

    this.connections = [];
    this.currentIndex = 0;

    this.initializeConnections();
  }

  initializeConnections() {
    this.urls.forEach((url) => {
      const connection = {
        url,
        ws: null,
        messageCount: 0,
        isConnected: false,
      };

      this.connect(connection);
      this.connections.push(connection);
    });
  }

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

    connection.ws.onopen = () => {
      connection.isConnected = true;
      console.log(`Connected to ${connection.url}`);
    };

    connection.ws.onclose = () => {
      connection.isConnected = false;
      console.log(`Disconnected from ${connection.url}`);

      // Reconnect after delay
      setTimeout(() => this.connect(connection), 5000);
    };

    connection.ws.onerror = (error) => {
      console.error(`Error on ${connection.url}:`, error);
    };
  }

  getConnection() {
    let selected;

    switch (this.options.strategy) {
      case 'round-robin':
        selected = this.connections[this.currentIndex];
        this.currentIndex = (this.currentIndex + 1) % this.connections.length;
        break;

      case 'least-loaded':
        selected = this.connections.reduce((min, conn) => {
          if (!conn.isConnected) return min;
          return conn.messageCount < min.messageCount ? conn : min;
        });
        break;

      case 'random':
        const available = this.connections.filter((c) => c.isConnected);
        selected = available[Math.floor(Math.random() * available.length)];
        break;
    }

    return selected;
  }

  send(message) {
    const connection = this.getConnection();

    if (connection && connection.isConnected) {
      connection.ws.send(JSON.stringify(message));
      connection.messageCount++;
    } else {
      console.error('No available connections');
    }
  }

  broadcast(message) {
    this.connections.forEach((connection) => {
      if (connection.isConnected) {
        connection.ws.send(JSON.stringify(message));
      }
    });
  }

  close() {
    this.connections.forEach((connection) => {
      if (connection.ws) {
        connection.ws.close();
      }
    });
  }
}

Security Considerations

class SecureWebSocket {
  constructor(url, options = {}) {
    this.options = {
      maxMessageSize: 1024 * 1024, // 1MB
      rateLimitMessages: 100,
      rateLimitWindow: 60000, // 1 minute
      allowedOrigins: ['https://example.com'],
      ...options,
    };

    this.messageCount = 0;
    this.rateLimitReset = Date.now() + this.options.rateLimitWindow;

    // Use secure WebSocket (wss://)
    this.ws = new WebSocket(url.replace(/^ws:/, 'wss:'));
    this.setupSecurity();
  }

  setupSecurity() {
    const originalOnMessage = this.ws.onmessage;

    this.ws.onmessage = (event) => {
      // Validate message size
      if (event.data.length > this.options.maxMessageSize) {
        console.error('Message too large');
        return;
      }

      // Validate message format
      try {
        const data = JSON.parse(event.data);

        // Validate message structure
        if (!this.validateMessage(data)) {
          console.error('Invalid message structure');
          return;
        }

        // Call original handler
        if (originalOnMessage) {
          originalOnMessage.call(this.ws, event);
        }
      } catch (error) {
        console.error('Invalid JSON message');
      }
    };
  }

  validateMessage(data) {
    // Implement message validation
    if (!data.type || typeof data.type !== 'string') {
      return false;
    }

    // Validate against whitelist of allowed message types
    const allowedTypes = ['chat', 'update', 'ping', 'pong'];
    if (!allowedTypes.includes(data.type)) {
      return false;
    }

    return true;
  }

  send(message) {
    // Rate limiting
    if (Date.now() > this.rateLimitReset) {
      this.messageCount = 0;
      this.rateLimitReset = Date.now() + this.options.rateLimitWindow;
    }

    if (this.messageCount >= this.options.rateLimitMessages) {
      console.error('Rate limit exceeded');
      return false;
    }

    this.messageCount++;

    // Sanitize message
    const sanitized = this.sanitizeMessage(message);

    this.ws.send(JSON.stringify(sanitized));
    return true;
  }

  sanitizeMessage(message) {
    // Remove any potentially dangerous content
    const sanitized = {};

    for (const [key, value] of Object.entries(message)) {
      if (typeof value === 'string') {
        // Remove script tags and other dangerous content
        sanitized[key] = value
          .replace(/<script[^>]*>.*?<\/script>/gi, '')
          .replace(/<iframe[^>]*>.*?<\/iframe>/gi, '');
      } else {
        sanitized[key] = value;
      }
    }

    return sanitized;
  }
}

// Authentication with WebSockets
class AuthenticatedWebSocket {
  constructor(url, token) {
    this.url = url;
    this.token = token;
    this.connect();
  }

  connect() {
    // Include auth token in connection
    this.ws = new WebSocket(
      `${this.url}?token=${encodeURIComponent(this.token)}`
    );

    this.ws.onopen = () => {
      console.log('Authenticated connection established');
    };

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

  // Refresh token before expiry
  async refreshToken() {
    try {
      const response = await fetch('/api/refresh-token', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.token}`,
        },
      });

      const data = await response.json();
      this.token = data.token;

      // Reconnect with new token
      this.ws.close();
      this.connect();
    } catch (error) {
      console.error('Token refresh failed:', error);
    }
  }
}

Best Practices

  1. Handle connection states properly

    if (socket.readyState === WebSocket.OPEN) {
      socket.send(message);
    } else {
      // Queue message or handle error
    }
    
  2. Implement reconnection logic

    // Automatic reconnection with exponential backoff
    let reconnectDelay = 1000;
    const maxDelay = 30000;
    
    function reconnect() {
      setTimeout(() => {
        connect();
        reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
      }, reconnectDelay);
    }
    
  3. Use binary data for large payloads

    // Send binary data instead of base64 strings
    const buffer = new ArrayBuffer(data.length);
    socket.send(buffer);
    
  4. Implement heartbeat/keepalive

    // Keep connection alive
    setInterval(() => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'ping' }));
      }
    }, 30000);
    

Conclusion

WebSockets enable powerful real-time features:

  • Bidirectional communication for instant updates
  • Low latency compared to polling
  • Efficient for frequent small messages
  • Persistent connections reduce overhead
  • Real-time applications like chat, gaming, collaboration

Key takeaways:

  • Handle connection lifecycle properly
  • Implement reconnection strategies
  • Use appropriate data formats
  • Consider security implications
  • Optimize for performance
  • Use Socket.IO for additional features

Master WebSockets to build engaging real-time applications!