⚡ WebSockets

Real-Time Bidirectional Communication

What are WebSockets?

WebSockets provide full-duplex communication channels over a single TCP connection. Unlike HTTP, WebSockets allow the server to push data to the client without the client requesting it first.

🔌 WebSocket vs HTTP

// HTTP (Request-Response)
// Client must initiate every interaction
// 1. Client → Server: Request
// 2. Server → Client: Response
// Overhead: Headers sent with every request

fetch('/api/messages')  // Client must poll repeatedly

// WebSocket (Full-Duplex)
// Persistent connection, both can send anytime
// 1. Client ↔ Server: Handshake (upgrade from HTTP)
// 2. Client ↔ Server: Bidirectional messages
// Low overhead: Only data frames after connection

let ws = new WebSocket('ws://localhost:3000');
ws.onmessage = e => console.log(e.data);  // Server can push

// Use cases for WebSockets:
// ✅ Chat applications
// ✅ Live notifications
// ✅ Real-time dashboards
// ✅ Collaborative editing
// ✅ Gaming
// ✅ Live sports scores
// ✅ Stock tickers

// Use HTTP when:
// ❌ One-way data flow
// ❌ Infrequent updates
// ❌ RESTful operations
// ❌ Large file transfers

🚀 Creating a WebSocket Connection

// Create connection
let ws = new WebSocket('ws://localhost:3000');

// Secure WebSocket (WSS) - like HTTPS
let wss = new WebSocket('wss://example.com/socket');

// With subprotocols
let ws = new WebSocket('ws://localhost:3000', 'chat');
let ws = new WebSocket('ws://localhost:3000', ['chat', 'json']);

// Connection states
console.log(ws.readyState);
// WebSocket.CONNECTING (0) - connecting
// WebSocket.OPEN (1) - connection open
// WebSocket.CLOSING (2) - connection closing
// WebSocket.CLOSED (3) - connection closed

// Check if open
if (ws.readyState === WebSocket.OPEN) {
    ws.send('Hello');
}

// Connection lifecycle
let ws = new WebSocket('ws://localhost:3000');

ws.onopen = event => {
    console.log('Connected');
    ws.send('Hello Server!');
};

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

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

ws.onclose = event => {
    console.log('Disconnected');
    console.log('Code:', event.code);
    console.log('Reason:', event.reason);
    console.log('Clean:', event.wasClean);
};

📤 Sending Messages

// Send text
ws.send('Hello Server');

// Send JSON
let data = { type: 'message', content: 'Hello' };
ws.send(JSON.stringify(data));

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

// Send ArrayBuffer
let buffer = new ArrayBuffer(8);
let view = new Uint8Array(buffer);
view[0] = 72;  // 'H'
view[1] = 105; // 'i'
ws.send(buffer);

// Check buffer amount before sending
console.log(ws.bufferedAmount);  // Bytes queued but not sent

// Wait for buffer to clear
function sendWhenReady(ws, data) {
    if (ws.bufferedAmount === 0) {
        ws.send(data);
    } else {
        setTimeout(() => sendWhenReady(ws, data), 100);
    }
}

// Safe send function
function safeSend(ws, data) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(data);
        return true;
    }
    return false;
}

📥 Receiving Messages

// Receive text
ws.onmessage = event => {
    console.log('Message:', event.data);
};

// Receive JSON
ws.onmessage = event => {
    try {
        let data = JSON.parse(event.data);
        console.log('Type:', data.type);
        console.log('Content:', data.content);
    } catch (error) {
        console.error('Invalid JSON:', event.data);
    }
};

// Handle different message types
ws.onmessage = event => {
    let message = JSON.parse(event.data);
    
    switch (message.type) {
        case 'chat':
            displayChatMessage(message.content);
            break;
        case 'notification':
            showNotification(message.content);
            break;
        case 'update':
            updateData(message.content);
            break;
        default:
            console.warn('Unknown message type:', message.type);
    }
};

// Receive Blob
ws.onmessage = event => {
    if (event.data instanceof Blob) {
        event.data.text().then(text => {
            console.log('Blob content:', text);
        });
    }
};

// Receive ArrayBuffer
ws.binaryType = 'arraybuffer';  // Default is 'blob'

ws.onmessage = event => {
    if (event.data instanceof ArrayBuffer) {
        let view = new Uint8Array(event.data);
        console.log('Binary data:', view);
    }
};

🔒 Closing Connection

// Close connection
ws.close();

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

// Close event codes
// 1000 - Normal closure
// 1001 - Going away (page unload)
// 1002 - Protocol error
// 1003 - Unsupported data
// 1006 - Abnormal closure (no close frame)
// 1007 - Invalid data
// 1008 - Policy violation
// 1009 - Message too big
// 1011 - Server error
// 3000-3999 - Application-specific
// 4000-4999 - Private use

ws.onclose = event => {
    console.log('Close code:', event.code);
    console.log('Reason:', event.reason);
    console.log('Was clean:', event.wasClean);
    
    if (event.code === 1000) {
        console.log('Normal close');
    } else if (event.code === 1006) {
        console.log('Connection lost');
        reconnect();
    }
};

// Close on page unload
window.addEventListener('beforeunload', () => {
    ws.close(1001, 'Page closing');
});

🔄 Automatic Reconnection

// Simple reconnect
function connect(url) {
    let ws = new WebSocket(url);
    
    ws.onopen = () => {
        console.log('Connected');
    };
    
    ws.onclose = event => {
        console.log('Disconnected, reconnecting...');
        setTimeout(() => connect(url), 1000);
    };
    
    return ws;
}

let ws = connect('ws://localhost:3000');

// Exponential backoff
class ReconnectingWebSocket {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.attempts = 0;
        this.maxAttempts = 10;
        this.connect();
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('Connected');
            this.attempts = 0;
            this.onopen && this.onopen();
        };
        
        this.ws.onmessage = event => {
            this.onmessage && this.onmessage(event);
        };
        
        this.ws.onerror = error => {
            this.onerror && this.onerror(error);
        };
        
        this.ws.onclose = event => {
            this.onclose && this.onclose(event);
            
            if (this.attempts < this.maxAttempts) {
                let delay = Math.min(1000 * Math.pow(2, this.attempts), 30000);
                console.log(`Reconnecting in ${delay}ms...`);
                
                setTimeout(() => {
                    this.attempts++;
                    this.connect();
                }, delay);
            } else {
                console.error('Max reconnection attempts reached');
            }
        };
    }
    
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        } else {
            console.warn('WebSocket not open');
        }
    }
    
    close() {
        this.attempts = this.maxAttempts;  // Prevent reconnect
        this.ws.close();
    }
}

// Usage
let ws = new ReconnectingWebSocket('ws://localhost:3000');

ws.onopen = () => console.log('Connected');
ws.onmessage = event => console.log('Message:', event.data);
ws.send('Hello');

💬 Chat Application Example

class ChatClient {
    constructor(url, username) {
        this.url = url;
        this.username = username;
        this.ws = null;
        this.messageHandlers = [];
        this.connect();
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('Connected to chat');
            this.send({
                type: 'join',
                username: this.username
            });
        };
        
        this.ws.onmessage = event => {
            let message = JSON.parse(event.data);
            this.handleMessage(message);
        };
        
        this.ws.onerror = error => {
            console.error('Chat error:', error);
        };
        
        this.ws.onclose = () => {
            console.log('Disconnected from chat');
            setTimeout(() => this.connect(), 3000);
        };
    }
    
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        }
    }
    
    sendMessage(text) {
        this.send({
            type: 'message',
            username: this.username,
            text: text,
            timestamp: Date.now()
        });
    }
    
    sendTyping() {
        this.send({
            type: 'typing',
            username: this.username
        });
    }
    
    handleMessage(message) {
        switch (message.type) {
            case 'message':
                this.notifyHandlers('message', message);
                break;
            case 'user-joined':
                this.notifyHandlers('userJoined', message);
                break;
            case 'user-left':
                this.notifyHandlers('userLeft', message);
                break;
            case 'typing':
                this.notifyHandlers('typing', message);
                break;
        }
    }
    
    on(event, handler) {
        this.messageHandlers.push({ event, handler });
    }
    
    notifyHandlers(event, data) {
        this.messageHandlers
            .filter(h => h.event === event)
            .forEach(h => h.handler(data));
    }
    
    disconnect() {
        this.send({ type: 'leave' });
        this.ws.close();
    }
}

// Usage
let chat = new ChatClient('ws://localhost:3000/chat', 'John');

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

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

chat.on('typing', data => {
    showTypingIndicator(data.username);
});

// Send message
let input = document.querySelector('#messageInput');
let sendBtn = document.querySelector('#sendBtn');

sendBtn.addEventListener('click', () => {
    chat.sendMessage(input.value);
    input.value = '';
});

// Typing indicator
let typingTimeout;
input.addEventListener('input', () => {
    clearTimeout(typingTimeout);
    chat.sendTyping();
    typingTimeout = setTimeout(() => {
        // Stop typing indicator
    }, 3000);
});

📊 Real-Time Dashboard Example

class Dashboard {
    constructor(url) {
        this.ws = new WebSocket(url);
        this.subscribers = new Map();
        this.connect();
    }
    
    connect() {
        this.ws.onopen = () => {
            console.log('Dashboard connected');
            // Subscribe to data streams
            this.subscribe(['users', 'sales', 'traffic']);
        };
        
        this.ws.onmessage = event => {
            let data = JSON.parse(event.data);
            this.handleUpdate(data);
        };
        
        this.ws.onclose = () => {
            console.log('Dashboard disconnected');
            setTimeout(() => this.connect(), 5000);
        };
    }
    
    subscribe(topics) {
        this.ws.send(JSON.stringify({
            action: 'subscribe',
            topics: topics
        }));
    }
    
    handleUpdate(data) {
        let { topic, value } = data;
        
        // Notify subscribers
        if (this.subscribers.has(topic)) {
            this.subscribers.get(topic).forEach(callback => {
                callback(value);
            });
        }
    }
    
    on(topic, callback) {
        if (!this.subscribers.has(topic)) {
            this.subscribers.set(topic, []);
        }
        this.subscribers.get(topic).push(callback);
    }
}

// Usage
let dashboard = new Dashboard('ws://localhost:3000/dashboard');

dashboard.on('users', count => {
    document.querySelector('#userCount').textContent = count;
});

dashboard.on('sales', data => {
    updateSalesChart(data);
});

dashboard.on('traffic', data => {
    updateTrafficGraph(data);
});

🎮 Multiplayer Game Example

class GameClient {
    constructor(url, playerId) {
        this.url = url;
        this.playerId = playerId;
        this.ws = new WebSocket(url);
        this.players = new Map();
        this.setup();
    }
    
    setup() {
        this.ws.onopen = () => {
            console.log('Connected to game');
            this.send({
                type: 'join',
                playerId: this.playerId
            });
        };
        
        this.ws.onmessage = event => {
            let message = JSON.parse(event.data);
            this.handleMessage(message);
        };
    }
    
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        }
    }
    
    handleMessage(message) {
        switch (message.type) {
            case 'player-joined':
                this.players.set(message.playerId, message.player);
                this.onPlayerJoined(message.player);
                break;
                
            case 'player-left':
                this.players.delete(message.playerId);
                this.onPlayerLeft(message.playerId);
                break;
                
            case 'player-moved':
                let player = this.players.get(message.playerId);
                if (player) {
                    player.x = message.x;
                    player.y = message.y;
                    this.onPlayerMoved(message.playerId, message.x, message.y);
                }
                break;
                
            case 'game-state':
                this.onGameState(message.state);
                break;
        }
    }
    
    move(x, y) {
        this.send({
            type: 'move',
            playerId: this.playerId,
            x: x,
            y: y,
            timestamp: Date.now()
        });
    }
    
    // Throttle position updates
    startSendingPosition() {
        setInterval(() => {
            let { x, y } = this.getPlayerPosition();
            this.move(x, y);
        }, 50);  // 20 updates per second
    }
    
    // Override these methods
    onPlayerJoined(player) {}
    onPlayerLeft(playerId) {}
    onPlayerMoved(playerId, x, y) {}
    onGameState(state) {}
    getPlayerPosition() { return { x: 0, y: 0 }; }
}

// Usage
let game = new GameClient('ws://localhost:3000/game', 'player-123');

game.onPlayerJoined = player => {
    console.log('Player joined:', player);
    addPlayerToScene(player);
};

game.onPlayerMoved = (playerId, x, y) => {
    updatePlayerPosition(playerId, x, y);
};

game.startSendingPosition();

💡 Best Practices

// 1. Always handle reconnection
// Network can be unstable

// 2. Heartbeat/Ping-Pong
// Keep connection alive, detect dead connections
setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'ping' }));
    }
}, 30000);  // Every 30 seconds

ws.onmessage = event => {
    let message = JSON.parse(event.data);
    if (message.type === 'pong') {
        lastPong = Date.now();
    }
};

// 3. Message queue for offline messages
class QueuedWebSocket {
    constructor(url) {
        this.url = url;
        this.queue = [];
        this.connect();
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            // Send queued messages
            while (this.queue.length > 0) {
                this.ws.send(this.queue.shift());
            }
        };
    }
    
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        } else {
            this.queue.push(data);
        }
    }
}

// 4. Compression for large messages
// Use gzip or deflate if supported

// 5. Authentication
// Send token after connection
ws.onopen = () => {
    ws.send(JSON.stringify({
        type: 'auth',
        token: localStorage.getItem('token')
    }));
};

// 6. Error handling
ws.onerror = error => {
    console.error('WebSocket error:', error);
    notifyUser('Connection error');
};

// 7. Close gracefully
window.addEventListener('beforeunload', () => {
    ws.close(1000, 'Page closing');
});

// 8. Rate limiting
// Don't spam server with messages
let lastSend = 0;
const MIN_INTERVAL = 100;  // ms

function rateLimitedSend(data) {
    let now = Date.now();
    if (now - lastSend >= MIN_INTERVAL) {
        ws.send(data);
        lastSend = now;
    }
}

🔧 Debugging WebSockets

// Browser DevTools
// 1. Network tab → WS filter
// 2. Shows all WebSocket connections
// 3. Click connection to see messages
// 4. Can see frames sent/received

// Log all events
let ws = new WebSocket('ws://localhost:3000');

ws.addEventListener('open', event => {
    console.log('OPEN:', event);
});

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

ws.addEventListener('error', event => {
    console.log('ERROR:', event);
});

ws.addEventListener('close', event => {
    console.log('CLOSE:', event.code, event.reason);
});

// Wrapper for logging
class LoggedWebSocket extends WebSocket {
    send(data) {
        console.log('→ SEND:', data);
        super.send(data);
    }
}

// Monitor connection quality
let messagesSent = 0;
let messagesReceived = 0;
let latency = 0;

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

ws.onmessage = event => {
    let message = JSON.parse(event.data);
    if (message.type === 'pong') {
        latency = Date.now() - message.timestamp;
        console.log('Latency:', latency, 'ms');
    }
};

🎯 Key Takeaways