WebSockets: Real-Time Bidirectional Communication
Understanding WebSockets - the protocol that enables full-duplex communication channels over a single TCP connection.
Best viewed on desktop for optimal interactive experience
WebSockets
WebSockets provide a persistent, full-duplex communication channel between client and server. Unlike traditional HTTP, WebSockets maintain an open connection, allowing both parties to send messages at any time - like having an always-open phone line rather than sending letters back and forth.
Interactive Demonstration
Experience real-time bidirectional communication with WebSockets:
WebSocket Controls
WebSocket Connection
Activity Log
WebSocket Advantages
True Real-time
Instant bidirectional communication with ~0ms latency
Persistent Connection
Single TCP connection for entire session
Low Overhead
2-14 bytes per frame vs 700+ bytes HTTP headers
Full Duplex
Client and server can send messages simultaneously
How WebSockets Work
The Handshake Process
WebSocket connections start with an HTTP upgrade handshake:
GET /socket HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Origin: http://example.com HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Connection Lifecycle
Client Server | | |--- HTTP Upgrade Request ----->| | | |<-- 101 Switching Protocols ---| | | |========= WS Connection ========| | | |<------- Message Frame -------->| |<------- Message Frame -------->| |<------- Message Frame -------->| | (bidirectional) | | | |--- Close Frame (1000) -------->| |<-- Close Frame (1000) ---------| | | |========= Disconnected =========|
WebSocket Frame Structure
Frame Format
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
Opcode Types
Opcode | Meaning | Description |
---|---|---|
0x0 | Continuation | Part of fragmented message |
0x1 | Text | UTF-8 text data |
0x2 | Binary | Binary data |
0x8 | Close | Connection close |
0x9 | Ping | Heartbeat request |
0xA | Pong | Heartbeat response |
Implementation
Client-Side WebSocket
class WebSocketClient { constructor(url) { this.url = url; this.ws = null; this.reconnectDelay = 1000; this.maxReconnectDelay = 30000; this.reconnectAttempts = 0; this.messageQueue = []; this.listeners = new Map(); } connect() { return new Promise((resolve, reject) => { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; this.reconnectDelay = 1000; this.flushQueue(); resolve(); }; this.ws.onmessage = (event) => { this.handleMessage(event.data); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); reject(error); }; this.ws.onclose = (event) => { console.log(`WebSocket closed: ${event.code} - ${event.reason}`); this.handleDisconnection(event); }; }); } handleMessage(data) { try { const message = JSON.parse(data); const listeners = this.listeners.get(message.type) || []; listeners.forEach(callback => callback(message)); } catch (error) { console.error('Failed to parse message:', error); } } on(type, callback) { if (!this.listeners.has(type)) { this.listeners.set(type, []); } this.listeners.get(type).push(callback); } send(type, data) { const message = JSON.stringify({ type, data, timestamp: Date.now() }); if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(message); } else { // Queue message for later this.messageQueue.push(message); } } flushQueue() { while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) { const message = this.messageQueue.shift(); this.ws.send(message); } } handleDisconnection(event) { // Clean close (code 1000) - don't reconnect if (event.code === 1000) return; // Attempt reconnection with exponential backoff this.reconnectAttempts++; const delay = Math.min( this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay ); console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { this.connect().catch(error => { console.error('Reconnection failed:', error); }); }, delay); } close() { if (this.ws) { this.ws.close(1000, 'Normal closure'); } } }
Server-Side WebSocket (Node.js)
const WebSocket = require('ws'); class WebSocketServer { constructor(port) { this.wss = new WebSocket.Server({ port }); this.clients = new Map(); this.setupServer(); } setupServer() { this.wss.on('connection', (ws, req) => { const clientId = this.generateClientId(); const client = { id: clientId, ws: ws, ip: req.socket.remoteAddress, connectedAt: Date.now(), isAlive: true }; this.clients.set(clientId, client); console.log(`Client ${clientId} connected`); // Setup ping/pong for connection health ws.on('pong', () => { client.isAlive = true; }); ws.on('message', (data) => { this.handleMessage(clientId, data); }); ws.on('close', (code, reason) => { console.log(`Client ${clientId} disconnected: ${code} - ${reason}`); this.clients.delete(clientId); }); ws.on('error', (error) => { console.error(`Client ${clientId} error:`, error); }); // Send welcome message this.sendToClient(clientId, 'welcome', { clientId, connectedClients: this.clients.size }); }); // Heartbeat interval setInterval(() => { this.wss.clients.forEach((ws) => { const client = [...this.clients.values()].find(c => c.ws === ws); if (client && !client.isAlive) { console.log(`Client ${client.id} failed ping check`); return ws.terminate(); } if (client) { client.isAlive = false; ws.ping(); } }); }, 30000); } handleMessage(clientId, data) { try { const message = JSON.parse(data); console.log(`Message from ${clientId}:`, message); // Echo back to sender this.sendToClient(clientId, 'echo', message); // Broadcast to others if (message.broadcast) { this.broadcast(message, clientId); } } catch (error) { console.error('Failed to parse message:', error); } } sendToClient(clientId, type, data) { const client = this.clients.get(clientId); if (client && client.ws.readyState === WebSocket.OPEN) { client.ws.send(JSON.stringify({ type, data, timestamp: Date.now() })); } } broadcast(message, excludeClientId = null) { this.clients.forEach((client, clientId) => { if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) { client.ws.send(JSON.stringify({ type: 'broadcast', data: message, from: excludeClientId, timestamp: Date.now() })); } }); } generateClientId() { return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }
Performance Characteristics
Overhead Comparison
For 1000 messages:
- HTTP Polling: 1000 × 1KB = 1MB overhead
- WebSocket: 2B + (1000 × 6B) ≈ 6KB overhead
- Improvement: 170× less overhead
Latency Analysis
Metric | HTTP Polling | Long Polling | WebSocket |
---|---|---|---|
Message Latency | 0-interval | ~0ms (waiting) | ~0ms (always) |
Round Trip Time | Full HTTP cycle | Full HTTP cycle | Frame only |
Connection Setup | Every request | Every request | Once |
Bidirectional | No | No | Yes |
Scaling WebSockets
Challenges
- Connection Limits: Each WebSocket maintains a TCP connection
- Memory Usage: ~10-50KB per connection
- Load Balancing: Sticky sessions required
- Horizontal Scaling: Message routing complexity
Solutions
1. Connection Pooling
class WebSocketPool { constructor(maxConnections = 10000) { this.connections = new Map(); this.maxConnections = maxConnections; } canAccept() { return this.connections.size < this.maxConnections; } add(clientId, ws) { if (!this.canAccept()) { ws.close(1013, 'Try again later'); return false; } this.connections.set(clientId, ws); return true; } }
2. Message Broker Architecture
// Using Redis for pub/sub across servers const redis = require('redis'); class ScalableWebSocketServer { constructor() { this.publisher = redis.createClient(); this.subscriber = redis.createClient(); this.localClients = new Map(); this.subscriber.subscribe('global-messages'); this.subscriber.on('message', (channel, message) => { this.handleGlobalMessage(JSON.parse(message)); }); } broadcastGlobal(message) { // Send to local clients this.localClients.forEach(client => { client.send(JSON.stringify(message)); }); // Publish to other servers this.publisher.publish('global-messages', JSON.stringify(message)); } handleGlobalMessage(message) { // Received from another server this.localClients.forEach(client => { client.send(JSON.stringify(message)); }); } }
Security Considerations
1. Authentication
// Token-based auth const ws = new WebSocket('wss://api.example.com/socket', { headers: { 'Authorization': 'Bearer ' + token } }); // Or via query parameter const ws = new WebSocket('wss://api.example.com/socket?token=' + token);
2. Origin Validation
wss.on('connection', (ws, req) => { const origin = req.headers.origin; if (!isValidOrigin(origin)) { ws.close(1008, 'Invalid origin'); return; } // Continue with connection });
3. Rate Limiting
class RateLimiter { constructor(maxMessages = 100, windowMs = 60000) { this.clients = new Map(); this.maxMessages = maxMessages; this.windowMs = windowMs; } check(clientId) { const now = Date.now(); const client = this.clients.get(clientId) || { messages: [], blocked: false }; // Remove old messages client.messages = client.messages.filter(t => t > now - this.windowMs); if (client.messages.length >= this.maxMessages) { client.blocked = true; return false; } client.messages.push(now); this.clients.set(clientId, client); return true; } }
Common Patterns
1. Heartbeat/Keepalive
class HeartbeatWebSocket { constructor(url) { this.url = url; this.pingInterval = 30000; this.pongTimeout = 5000; } startHeartbeat() { this.pingTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); this.pongTimer = setTimeout(() => { console.error('Pong timeout - connection lost'); this.ws.close(); this.reconnect(); }, this.pongTimeout); } }, this.pingInterval); } handleMessage(data) { const message = JSON.parse(data); if (message.type === 'pong') { clearTimeout(this.pongTimer); return; } // Handle other messages } }
2. Message Acknowledgment
class ReliableWebSocket { constructor() { this.messageId = 0; this.pendingAcks = new Map(); this.ackTimeout = 5000; } sendReliable(data) { const messageId = this.messageId++; const message = { id: messageId, data, timestamp: Date.now() }; this.ws.send(JSON.stringify(message)); // Wait for acknowledgment const timer = setTimeout(() => { console.error(`Message ${messageId} not acknowledged`); this.resend(message); }, this.ackTimeout); this.pendingAcks.set(messageId, { message, timer }); } handleAck(messageId) { const pending = this.pendingAcks.get(messageId); if (pending) { clearTimeout(pending.timer); this.pendingAcks.delete(messageId); } } }
Use Cases
Perfect For ✅
-
Real-time Collaboration
- Google Docs
- Figma
- VS Code Live Share
-
Live Updates
- Stock tickers
- Sports scores
- News feeds
-
Gaming
- Multiplayer games
- Turn-based games
- Game state sync
-
Communication
- Chat applications
- Video call signaling
- Presence indicators
Not Ideal For ❌
-
Simple Request-Response
- REST APIs
- Form submissions
- File uploads
-
Cacheable Content
- Static resources
- CDN-friendly data
- Infrequent updates
Debugging WebSockets
Chrome DevTools
- Open Network tab
- Filter by "WS"
- Click on WebSocket connection
- View frames in Messages tab
Logging Strategy
class DebugWebSocket { constructor(url, debug = true) { this.debug = debug; this.ws = new WebSocket(url); this.setupLogging(); } setupLogging() { const original = { send: this.ws.send.bind(this.ws) }; this.ws.send = (data) => { if (this.debug) { console.log('⬆️ Sending:', data); } original.send(data); }; this.ws.onmessage = (event) => { if (this.debug) { console.log('⬇️ Received:', event.data); } this.handleMessage(event); }; } }
Related Concepts
- Short Polling - Simple alternative
- Long Polling - HTTP-based real-time
- Protocol Comparison - Compare all approaches
- Server-Sent Events - One-way server push
Conclusion
WebSockets revolutionize real-time web communication by providing persistent, bidirectional channels with minimal overhead. While they require more complex infrastructure than traditional HTTP, the benefits of instant communication and reduced bandwidth make them indispensable for modern real-time applications. Understanding when and how to implement WebSockets is crucial for building responsive, interactive web experiences.