WebSockets in JavaScript

WebSockets provide a persistent connection between a client and server, allowing for real-time, bidirectional communication. Unlike HTTP, which is stateless, WebSockets maintain an open connection, making them ideal for applications that require live updates like chat apps, multiplayer games, and collaborative tools.

Introduction to WebSockets

Traditional web communication uses HTTP, where the client sends a request and the server responds. This model isn't efficient for real-time applications because:

  • Each request requires a new connection
  • Headers add overhead to each request
  • Clients must poll the server for updates

WebSockets solve these problems by:

  • Establishing a persistent connection
  • Enabling bidirectional communication
  • Reducing overhead after the initial handshake
  • Allowing real-time data transfer

How WebSockets Work

WebSockets operate through a standardized protocol that starts with an HTTP handshake and then upgrades to a WebSocket connection:

  1. Handshake: The client sends an HTTP request with headers indicating a desire to upgrade to the WebSocket protocol
  2. Upgrade: If the server supports WebSockets, it responds with an acknowledgment
  3. Connection: The HTTP connection is replaced with a WebSocket connection using the same underlying TCP/IP connection
  4. Data Transfer: Both client and server can send messages to each other at any time
  5. Closure: Either side can close the connection

Note: WebSocket URLs use the ws:// or wss:// (secure) protocol instead of http:// or https://.

Creating a WebSocket Connection

In JavaScript, you can create a WebSocket connection using the WebSocket API:

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

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

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

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

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

WebSocket Events

The WebSocket API provides several events to handle different aspects of the connection:

  • open: Fired when the connection is established
  • message: Fired when data is received from the server
  • close: Fired when the connection is closed
  • error: Fired when an error occurs
// Alternative syntax using on* properties
socket.onopen = (event) => {
  console.log('Connection established');
};

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

socket.onclose = (event) => {
  console.log('Connection closed', event.code, event.reason);
};

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

Sending Data

You can send data to the server using the send() method:

// Send a string
socket.send('Hello Server!');

// Send JSON data
const data = {
  type: 'message',
  content: 'Hello Server!',
  timestamp: Date.now()
};
socket.send(JSON.stringify(data));

// Send binary data
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
for (let i = 0; i < view.length; i++) {
  view[i] = i;
}
socket.send(buffer);

Note: WebSockets can send text or binary data. When sending objects, you need to convert them to strings using JSON.stringify().

Receiving Data

Data from the server is received through the message event:

socket.addEventListener('message', (event) => {
  // Check if the data is text or binary
  if (typeof event.data === 'string') {
    console.log('Received text:', event.data);
    
    // If the data is JSON, parse it
    try {
      const jsonData = JSON.parse(event.data);
      console.log('Received JSON:', jsonData);
      
      // Handle different message types
      if (jsonData.type === 'chat') {
        displayChatMessage(jsonData);
      } else if (jsonData.type === 'notification') {
        showNotification(jsonData);
      }
    } catch (e) {
      // Not JSON, handle as plain text
      console.log('Received plain text:', event.data);
    }
  } else if (event.data instanceof Blob) {
    // Handle binary data (e.g., image)
    const reader = new FileReader();
    reader.onload = () => {
      const arrayBuffer = reader.result;
      processArrayBuffer(arrayBuffer);
    };
    reader.readAsArrayBuffer(event.data);
  } else if (event.data instanceof ArrayBuffer) {
    // Handle binary data directly
    processArrayBuffer(event.data);
  }
});

Closing a Connection

You can close a WebSocket connection using the close() method:

// Close normally
socket.close();

// Close with a code and reason
socket.close(1000, 'Normal closure');

// Common close codes:
// 1000: Normal closure
// 1001: Going away (e.g., page navigation)
// 1002: Protocol error
// 1003: Unsupported data
// 1008: Policy violation
// 1011: Server error

Handling Connection Issues

WebSocket connections can fail or disconnect for various reasons. It's important to implement reconnection logic:

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.socket = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectInterval = options.reconnectInterval || 1000;
    this.maxReconnectInterval = options.maxReconnectInterval || 30000;
    this.listeners = {
      open: [],
      message: [],
      close: [],
      error: []
    };
    
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = (event) => {
      console.log('Connection established');
      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.listeners.open.forEach(listener => listener(event));
    };
    
    this.socket.onmessage = (event) => {
      this.listeners.message.forEach(listener => listener(event));
    };
    
    this.socket.onclose = (event) => {
      this.isConnected = false;
      this.listeners.close.forEach(listener => listener(event));
      
      if (event.code !== 1000) {
        this.reconnect();
      }
    };
    
    this.socket.onerror = (event) => {
      this.listeners.error.forEach(listener => listener(event));
    };
  }
  
  reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.log('Max reconnect attempts reached');
      return;
    }
    
    this.reconnectAttempts++;
    const delay = Math.min(
      this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
      this.maxReconnectInterval
    );
    
    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
    
    setTimeout(() => {
      console.log('Attempting to reconnect...');
      this.connect();
    }, delay);
  }
  
  addEventListener(type, listener) {
    if (this.listeners[type]) {
      this.listeners[type].push(listener);
    }
  }
  
  removeEventListener(type, listener) {
    if (this.listeners[type]) {
      this.listeners[type] = this.listeners[type].filter(l => l !== listener);
    }
  }
  
  send(data) {
    if (this.isConnected) {
      this.socket.send(data);
    } else {
      console.error('Cannot send: WebSocket is not connected');
    }
  }
  
  close(code, reason) {
    this.socket.close(code, reason);
  }
}

// Usage
const socket = new ReconnectingWebSocket('wss://example.com/socketserver', {
  maxReconnectAttempts: 10,
  reconnectInterval: 2000
});

socket.addEventListener('open', (event) => {
  console.log('Connected!');
});

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

WebSocket Protocols

You can specify sub-protocols when creating a WebSocket connection:

// Specify one protocol
const socket = new WebSocket('wss://example.com/socketserver', 'chat-protocol');

// Specify multiple protocols (server will pick one)
const socket = new WebSocket('wss://example.com/socketserver', ['chat-protocol', 'v2.chat-protocol']);

// Check which protocol was selected
socket.addEventListener('open', (event) => {
  console.log('Connected using protocol:', socket.protocol);
});

Building a Simple Chat Application

Let's build a simple chat application using WebSockets:

// Client-side code
const username = prompt('Enter your username:') || 'Anonymous';
const chatSocket = new WebSocket('wss://chat-server-example.com/chat');

// DOM elements
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const chatMessages = document.getElementById('chatMessages');
const connectionStatus = document.getElementById('connectionStatus');

// Connection opened
chatSocket.addEventListener('open', (event) => {
  connectionStatus.textContent = 'Connected';
  connectionStatus.className = 'connected';
  
  // Send a join message
  const joinMessage = {
    type: 'join',
    username: username,
    timestamp: Date.now()
  };
  chatSocket.send(JSON.stringify(joinMessage));
});

// Connection closed
chatSocket.addEventListener('close', (event) => {
  connectionStatus.textContent = 'Disconnected';
  connectionStatus.className = 'disconnected';
});

// Error handling
chatSocket.addEventListener('error', (event) => {
  connectionStatus.textContent = 'Error';
  connectionStatus.className = 'error';
  console.error('WebSocket error:', event);
});

// Listen for messages
chatSocket.addEventListener('message', (event) => {
  try {
    const message = JSON.parse(event.data);
    
    // Create message element
    const messageElement = document.createElement('div');
    messageElement.className = 'message';
    
    // Format timestamp
    const date = new Date(message.timestamp);
    const timeString = date.toLocaleTimeString();
    
    // Handle different message types
    switch (message.type) {
      case 'chat':
        messageElement.innerHTML = `
          ${timeString}
          ${message.username}:
          ${message.content}
        `;
        break;
      case 'join':
        messageElement.className = 'message system';
        messageElement.innerHTML = `
          ${timeString}
          ${message.username} joined the chat
        `;
        break;
      case 'leave':
        messageElement.className = 'message system';
        messageElement.innerHTML = `
          ${timeString}
          ${message.username} left the chat
        `;
        break;
    }
    
    // Add message to chat
    chatMessages.appendChild(messageElement);
    
    // Scroll to bottom
    chatMessages.scrollTop = chatMessages.scrollHeight;
  } catch (e) {
    console.error('Error parsing message:', e);
  }
});

// Send message
function sendMessage() {
  const content = messageInput.value.trim();
  
  if (content && chatSocket.readyState === WebSocket.OPEN) {
    const chatMessage = {
      type: 'chat',
      username: username,
      content: content,
      timestamp: Date.now()
    };
    
    chatSocket.send(JSON.stringify(chatMessage));
    messageInput.value = '';
  }
}

// Event listeners
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (event) => {
  if (event.key === 'Enter') {
    sendMessage();
  }
});

// Handle page unload
window.addEventListener('beforeunload', () => {
  if (chatSocket.readyState === WebSocket.OPEN) {
    const leaveMessage = {
      type: 'leave',
      username: username,
      timestamp: Date.now()
    };
    
    // Using sendBeacon for reliability during page unload
    navigator.sendBeacon(
      'https://chat-server-example.com/leave',
      JSON.stringify(leaveMessage)
    );
    
    chatSocket.close();
  }
});

WebSocket Libraries

While the native WebSocket API is powerful, several libraries can simplify WebSocket development:

Socket.IO

Socket.IO is one of the most popular libraries, offering features like automatic reconnection, fallbacks to HTTP long-polling, and room-based broadcasting.

// Client-side Socket.IO
import { io } from 'socket.io-client';

const socket = io('https://example.com', {
  reconnectionAttempts: 5,
  reconnectionDelay: 1000
});

socket.on('connect', () => {
  console.log('Connected to server');
  socket.emit('join', { username: 'John' });
});

socket.on('chat message', (msg) => {
  console.log('Received message:', msg);
});

socket.on('disconnect', () => {
  console.log('Disconnected from server');
});

// Send a message
socket.emit('chat message', { text: 'Hello everyone!' });

// Join a room
socket.emit('join room', 'javascript-developers');

// Leave a room
socket.emit('leave room', 'javascript-developers');

SockJS

SockJS provides a WebSocket-like object that falls back to non-WebSocket alternatives when WebSockets aren't available.

// Client-side SockJS
import SockJS from 'sockjs-client';

const socket = new SockJS('https://example.com/sockjs');

socket.onopen = () => {
  console.log('Connection opened');
};

socket.onmessage = (e) => {
  console.log('Received:', e.data);
};

socket.onclose = () => {
  console.log('Connection closed');
};

// Send a message
socket.send('Hello SockJS!');

WebSockets vs. Other Technologies

Let's compare WebSockets with other real-time communication technologies:

WebSockets vs. HTTP Polling

  • HTTP Polling: Client repeatedly requests updates from the server at regular intervals
  • WebSockets: Persistent connection with real-time updates
  • Advantage: WebSockets have lower latency and less overhead for frequent updates

WebSockets vs. Server-Sent Events (SSE)

  • SSE: Server can push updates to clients, but communication is one-way (server to client only)
  • WebSockets: Bidirectional communication (both server to client and client to server)
  • Advantage: WebSockets support both directions and binary data

WebSockets vs. Long Polling

  • Long Polling: Client requests information, server holds the request open until new data is available
  • WebSockets: Persistent connection without repeated requests
  • Advantage: WebSockets have lower latency and less overhead

Security Considerations

When implementing WebSockets, consider these security best practices:

  • Use WSS (WebSocket Secure): Always use encrypted connections (wss://) in production
  • Validate Input: Validate all messages received from clients
  • Implement Authentication: Authenticate users before establishing WebSocket connections
  • Rate Limiting: Implement rate limiting to prevent abuse
  • Handle Reconnections Safely: Verify user authentication when reconnecting
  • Cross-Origin Protection: Implement proper CORS policies for WebSocket connections

Best Practices

  • Handle Connection States: Always check the connection state before sending messages
  • Implement Reconnection Logic: Automatically reconnect if the connection is lost
  • Use Heartbeats: Implement ping/pong messages to detect dead connections
  • Structure Your Messages: Use a consistent message format (e.g., JSON with a type field)
  • Close Connections Properly: Always close connections when they're no longer needed
  • Handle Errors: Implement proper error handling for all WebSocket operations
  • Consider Scalability: For production applications, consider using a WebSocket library that supports clustering

Browser Support

WebSockets are supported in all modern browsers:

  • Chrome 4+
  • Firefox 4+
  • Safari 5+
  • Edge 12+
  • Opera 10.7+
  • iOS Safari 4.2+
  • Android Browser 4.4+

Next Steps

Now that you understand WebSockets, you might want to explore:

  • Building real-time dashboards and monitoring systems
  • Creating multiplayer games with WebSockets
  • Implementing collaborative editing features
  • Exploring WebRTC for peer-to-peer communication
  • Learning about WebSocket server implementations (Node.js, Spring, Django Channels, etc.)