Docs

16.6-WebSockets-Real-Time

16.6 WebSockets and Real-Time Communication

Overview

WebSockets provide a persistent, full-duplex communication channel between client and server over a single TCP connection. Unlike HTTP's request-response pattern, WebSockets enable real-time bidirectional data transfer, making them ideal for live applications like chat, gaming, and live feeds.

Learning Objectives

  • Understand WebSocket protocol basics
  • Create and manage WebSocket connections
  • Handle messages, errors, and connection states
  • Implement reconnection strategies
  • Build real-time features with WebSockets

WebSocket Basics

Creating a Connection

// Create WebSocket connection
const socket = new WebSocket('wss://example.com/socket');

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

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

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

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

WebSocket States

const socket = new WebSocket('wss://example.com/socket');

// Check connection state
console.log(socket.readyState);

// ReadyState values:
// 0 - CONNECTING: Connection not yet open
// 1 - OPEN: Connection is open and ready
// 2 - CLOSING: Connection is closing
// 3 - CLOSED: Connection is closed

// Helper function to check state
function isConnected(socket) {
  return socket.readyState === WebSocket.OPEN;
}

Sending Data

const socket = new WebSocket('wss://example.com/socket');

socket.onopen = () => {
  // Send string
  socket.send('Hello');

  // Send JSON
  socket.send(JSON.stringify({ type: 'message', text: 'Hello' }));

  // Send ArrayBuffer
  const buffer = new ArrayBuffer(16);
  socket.send(buffer);

  // Send Blob
  const blob = new Blob(['Hello'], { type: 'text/plain' });
  socket.send(blob);
};

// Set binary type for received data
socket.binaryType = 'arraybuffer'; // or 'blob'

Receiving Data

socket.onmessage = (event) => {
  if (typeof event.data === 'string') {
    // Text message
    const data = JSON.parse(event.data);
    console.log('Received:', data);
  } else if (event.data instanceof ArrayBuffer) {
    // Binary data
    const view = new DataView(event.data);
    console.log('Binary data received');
  } else if (event.data instanceof Blob) {
    // Blob data
    event.data.text().then((text) => console.log(text));
  }
};

Closing Connection

// Close with default code (1000)
socket.close();

// Close with code and reason
socket.close(1000, 'User logged out');

// Handle close event
socket.onclose = (event) => {
  console.log(`Closed: ${event.code} - ${event.reason}`);
  console.log('Clean close:', event.wasClean);
};

WebSocket Wrapper Class

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

    this.socket = null;
    this.reconnectAttempts = 0;
    this.listeners = new Map();
    this.messageQueue = [];
  }

  connect() {
    return new Promise((resolve, reject) => {
      this.socket = new WebSocket(this.url);

      this.socket.onopen = () => {
        console.log('Connected');
        this.reconnectAttempts = 0;
        this.flushQueue();
        this.emit('connect');
        resolve(this);
      };

      this.socket.onclose = (event) => {
        console.log('Disconnected');
        this.emit('disconnect', event);

        if (this.options.reconnect && !event.wasClean) {
          this.attemptReconnect();
        }
      };

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

      this.socket.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          this.emit('message', data);

          // Emit specific event type if present
          if (data.type) {
            this.emit(data.type, data.payload);
          }
        } catch (e) {
          this.emit('message', event.data);
        }
      };
    });
  }

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

    if (this.isConnected()) {
      this.socket.send(message);
    } else {
      this.messageQueue.push(message);
    }
  }

  flushQueue() {
    while (this.messageQueue.length > 0) {
      this.socket.send(this.messageQueue.shift());
    }
  }

  isConnected() {
    return this.socket && this.socket.readyState === WebSocket.OPEN;
  }

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

    this.reconnectAttempts++;
    console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);

    setTimeout(() => {
      this.connect().catch(() => {});
    }, this.options.reconnectInterval * this.reconnectAttempts);
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
    return this;
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      const callbacks = this.listeners.get(event);
      const index = callbacks.indexOf(callback);
      if (index !== -1) {
        callbacks.splice(index, 1);
      }
    }
    return this;
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach((callback) => callback(data));
    }
  }

  close(code = 1000, reason = '') {
    if (this.socket) {
      this.options.reconnect = false;
      this.socket.close(code, reason);
    }
  }
}

// Usage
const client = new WebSocketClient('wss://example.com/socket');

client.on('connect', () => console.log('Connected!'));
client.on('message', (data) => console.log('Received:', data));
client.on('chat', (message) => console.log('Chat:', message));

await client.connect();
client.send({ type: 'join', room: 'general' });

Common Patterns

Heartbeat/Ping-Pong

class WebSocketWithHeartbeat extends WebSocketClient {
  constructor(url, options = {}) {
    super(url, {
      heartbeatInterval: 30000,
      ...options,
    });

    this.heartbeatTimer = null;
  }

  connect() {
    return super.connect().then((client) => {
      this.startHeartbeat();
      return client;
    });
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.isConnected()) {
        this.send({ type: 'ping', timestamp: Date.now() });
      }
    }, this.options.heartbeatInterval);

    this.on('pong', (data) => {
      const latency = Date.now() - data.timestamp;
      console.log(`Latency: ${latency}ms`);
    });
  }

  close(code, reason) {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
    }
    super.close(code, reason);
  }
}

Request-Response Pattern

class WebSocketRPC extends WebSocketClient {
  constructor(url, options = {}) {
    super(url, options);
    this.pendingRequests = new Map();
    this.requestId = 0;

    this.on('response', (data) => {
      const { id, result, error } = data;
      const pending = this.pendingRequests.get(id);
      if (pending) {
        this.pendingRequests.delete(id);
        if (error) {
          pending.reject(new Error(error));
        } else {
          pending.resolve(result);
        }
      }
    });
  }

  request(method, params, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const id = ++this.requestId;

      const timer = setTimeout(() => {
        this.pendingRequests.delete(id);
        reject(new Error('Request timeout'));
      }, timeout);

      this.pendingRequests.set(id, {
        resolve: (result) => {
          clearTimeout(timer);
          resolve(result);
        },
        reject: (error) => {
          clearTimeout(timer);
          reject(error);
        },
      });

      this.send({ type: 'request', id, method, params });
    });
  }
}

// Usage
const rpc = new WebSocketRPC('wss://api.example.com');
await rpc.connect();

const user = await rpc.request('getUser', { id: 123 });
const posts = await rpc.request('getPosts', { userId: 123 });

Room-Based Chat

class ChatClient extends WebSocketClient {
  constructor(url) {
    super(url);
    this.currentRoom = null;
  }

  join(room) {
    this.currentRoom = room;
    this.send({ type: 'join', room });
  }

  leave() {
    if (this.currentRoom) {
      this.send({ type: 'leave', room: this.currentRoom });
      this.currentRoom = null;
    }
  }

  sendMessage(text) {
    if (this.currentRoom) {
      this.send({
        type: 'message',
        room: this.currentRoom,
        text,
        timestamp: Date.now(),
      });
    }
  }

  sendTyping(isTyping) {
    if (this.currentRoom) {
      this.send({
        type: 'typing',
        room: this.currentRoom,
        isTyping,
      });
    }
  }
}

// Usage
const chat = new ChatClient('wss://chat.example.com');

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

chat.on('userJoined', (data) => {
  console.log(`${data.user} joined the room`);
});

chat.on('typing', (data) => {
  console.log(`${data.user} is typing...`);
});

await chat.connect();
chat.join('general');
chat.sendMessage('Hello everyone!');

Server-Sent Events (SSE)

For one-way server-to-client streaming, SSE is simpler:

// SSE is simpler for one-way server-to-client
const eventSource = new EventSource('/api/events');

eventSource.onmessage = (event) => {
  console.log('Data:', event.data);
};

eventSource.addEventListener('update', (event) => {
  console.log('Update:', JSON.parse(event.data));
});

eventSource.onerror = (error) => {
  console.error('SSE Error:', error);
  eventSource.close();
};

WebSocket vs SSE vs Polling

FeatureWebSocketSSEPolling
DirectionBidirectionalServer → ClientClient → Server
ConnectionPersistentPersistentNew per request
ProtocolWS/WSSHTTPHTTP
ComplexityHigherLowerLowest
Use CaseChat, GamesNotificationsLegacy systems

Best Practices

  1. Always handle reconnection - Networks are unreliable
  2. Use heartbeats - Detect dead connections
  3. Queue messages - Buffer during disconnection
  4. Handle errors gracefully - Show user feedback
  5. Use WSS - Always use secure WebSocket in production
  6. Implement backoff - Exponential backoff for reconnection

Summary

ConceptDescription
WebSocketFull-duplex communication channel
readyStateConnection state (0-3)
send()Send data to server
close()Close connection
Eventsopen, message, error, close

Resources

.6 WebSockets Real Time - JavaScript Tutorial | DeepML