Skip to main content

Chapter 12: JavaScript WebSocket - Complete Guide to Real-time Communication

WebSocket is a powerful communication protocol that enables real-time, bidirectional communication between web browsers and servers. Unlike traditional HTTP requests, WebSocket maintains a persistent connection, making it ideal for real-time applications like chat systems, live updates, and interactive games.

Why WebSocket is Essential in JavaScript

WebSocket in JavaScript is crucial because it:

  • Enables Real-time Communication: Provides instant, bidirectional data exchange
  • Reduces Latency: Eliminates the overhead of HTTP request/response cycles
  • Supports Live Applications: Essential for chat, gaming, live feeds, and collaborative tools
  • Improves User Experience: Enables instant updates and interactive features
  • Reduces Server Load: More efficient than polling for real-time data
  • Supports Binary Data: Can handle both text and binary data transmission

Learning Objectives

Through this chapter, you will master:

  • WebSocket connection establishment and management
  • Message sending and receiving
  • Event handling and connection states
  • Error handling and reconnection strategies
  • Real-time application patterns
  • Performance optimization techniques
  • Security considerations and best practices

Basic WebSocket Usage

Creating a WebSocket Connection

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

// Secure WebSocket connection (WSS)
const secureSocket = new WebSocket('wss://example.com/websocket');

// WebSocket with custom protocols
const socketWithProtocol = new WebSocket('ws://localhost:8080', ['chat', 'notification']);

// Connection with authentication
const authenticatedSocket = new WebSocket('ws://localhost:8080?token=your-auth-token');

// WebSocket connection with custom headers (limited browser support)
const socketWithHeaders = new WebSocket('ws://localhost:8080', [], {
headers: {
'Authorization': 'Bearer your-token'
}
});

Connection Event Handling

// WebSocket event handlers
const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.onopen = function(event) {
console.log('WebSocket connection opened');
console.log('Ready state:', socket.readyState);

// Send initial message
socket.send('Hello Server!');
};

// Message received
socket.onmessage = function(event) {
console.log('Message received:', event.data);

// Handle different message types
try {
const data = JSON.parse(event.data);
handleMessage(data);
} catch (error) {
console.log('Received text message:', event.data);
}
};

// Connection closed
socket.onclose = function(event) {
console.log('WebSocket connection closed');
console.log('Close code:', event.code);
console.log('Close reason:', event.reason);
console.log('Was clean:', event.wasClean);
};

// Connection error
socket.onerror = function(error) {
console.error('WebSocket error:', error);
};

// Helper function to handle different message types
function handleMessage(data) {
switch (data.type) {
case 'chat':
displayChatMessage(data.message);
break;
case 'notification':
showNotification(data.message);
break;
case 'user_joined':
updateUserList(data.users);
break;
default:
console.log('Unknown message type:', data.type);
}
}

Connection States

// WebSocket ready states
const socket = new WebSocket('ws://localhost:8080');

function checkConnectionState() {
switch (socket.readyState) {
case WebSocket.CONNECTING:
console.log('Connection is being established');
break;
case WebSocket.OPEN:
console.log('Connection is open and ready to communicate');
break;
case WebSocket.CLOSING:
console.log('Connection is being closed');
break;
case WebSocket.CLOSED:
console.log('Connection is closed or could not be opened');
break;
default:
console.log('Unknown connection state');
}
}

// Check state periodically
setInterval(checkConnectionState, 1000);

// Send message only if connection is open
function sendMessage(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
} else {
console.log('Cannot send message: connection is not open');
}
}

Message Handling

Sending Different Data Types

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

// Send text message
function sendTextMessage(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
}
}

// Send JSON message
function sendJsonMessage(data) {
if (socket.readyState === WebSocket.OPEN) {
const jsonString = JSON.stringify(data);
socket.send(jsonString);
}
}

// Send binary data
function sendBinaryData(data) {
if (socket.readyState === WebSocket.OPEN) {
// Convert to ArrayBuffer
const buffer = new ArrayBuffer(data.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < data.length; i++) {
view[i] = data.charCodeAt(i);
}
socket.send(buffer);
}
}

// Send Blob data
function sendBlobData(blob) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(blob);
}
}

// Usage examples
sendTextMessage('Hello Server!');
sendJsonMessage({ type: 'chat', message: 'Hello World!', user: 'John' });
sendBinaryData('Binary data');

Receiving Different Data Types

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

socket.onmessage = function(event) {
// Check if data is binary
if (event.data instanceof ArrayBuffer) {
handleBinaryData(event.data);
} else if (event.data instanceof Blob) {
handleBlobData(event.data);
} else {
// Text data
handleTextData(event.data);
}
};

function handleTextData(data) {
try {
// Try to parse as JSON
const jsonData = JSON.parse(data);
handleJsonMessage(jsonData);
} catch (error) {
// Handle as plain text
console.log('Received text:', data);
}
}

function handleJsonMessage(data) {
console.log('Received JSON:', data);

// Handle different message types
switch (data.type) {
case 'chat':
displayChatMessage(data);
break;
case 'user_list':
updateUserList(data.users);
break;
case 'system':
showSystemMessage(data.message);
break;
}
}

function handleBinaryData(buffer) {
console.log('Received binary data:', buffer);
const view = new Uint8Array(buffer);
console.log('Binary data as array:', Array.from(view));
}

function handleBlobData(blob) {
console.log('Received blob data:', blob);
// Convert blob to text if needed
blob.text().then(text => {
console.log('Blob as text:', text);
});
}

Advanced WebSocket Patterns

WebSocket Manager Class

class WebSocketManager {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 3000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
...options
};

this.socket = null;
this.reconnectAttempts = 0;
this.heartbeatTimer = null;
this.messageQueue = [];
this.eventListeners = new Map();

this.connect();
}

connect() {
try {
this.socket = new WebSocket(this.url);
this.setupEventHandlers();
} catch (error) {
console.error('Failed to create WebSocket:', error);
this.handleReconnect();
}
}

setupEventHandlers() {
this.socket.onopen = (event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.flushMessageQueue();
this.emit('open', event);
};

this.socket.onmessage = (event) => {
this.handleMessage(event);
this.emit('message', event);
};

this.socket.onclose = (event) => {
console.log('WebSocket closed');
this.stopHeartbeat();
this.emit('close', event);

if (!event.wasClean) {
this.handleReconnect();
}
};

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

handleMessage(event) {
try {
const data = JSON.parse(event.data);

// Handle heartbeat response
if (data.type === 'pong') {
return;
}

// Handle other message types
this.emit('data', data);
} catch (error) {
// Handle non-JSON messages
this.emit('text', event.data);
}
}

send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
this.socket.send(message);
} else {
// Queue message for later
this.messageQueue.push(data);
}
}

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

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

stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}

handleReconnect() {
if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, this.options.reconnectInterval);
} else {
console.error('Max reconnection attempts reached');
this.emit('maxReconnectAttemptsReached');
}
}

// Event system
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}

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

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

close() {
this.stopHeartbeat();
if (this.socket) {
this.socket.close();
}
}
}

// Usage
const wsManager = new WebSocketManager('ws://localhost:8080');

wsManager.on('open', () => {
console.log('Connected to server');
});

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

wsManager.on('error', (error) => {
console.error('WebSocket error:', error);
});

// Send message
wsManager.send({ type: 'chat', message: 'Hello!' });

Real-time Chat Application

class ChatApplication {
constructor(serverUrl) {
this.serverUrl = serverUrl;
this.wsManager = null;
this.currentUser = null;
this.chatMessages = [];
this.onlineUsers = [];

this.initializeUI();
this.connect();
}

initializeUI() {
// Create chat interface
this.chatContainer = document.getElementById('chat-container');
this.messageInput = document.getElementById('message-input');
this.sendButton = document.getElementById('send-button');
this.userList = document.getElementById('user-list');

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

connect() {
this.wsManager = new WebSocketManager(this.serverUrl);

this.wsManager.on('open', () => {
this.showStatus('Connected', 'success');
this.authenticate();
});

this.wsManager.on('data', (data) => {
this.handleServerMessage(data);
});

this.wsManager.on('close', () => {
this.showStatus('Disconnected', 'error');
});

this.wsManager.on('error', (error) => {
this.showStatus('Connection Error', 'error');
});
}

authenticate() {
const username = prompt('Enter your username:');
if (username) {
this.currentUser = username;
this.wsManager.send({
type: 'auth',
username: username
});
}
}

handleServerMessage(data) {
switch (data.type) {
case 'auth_success':
this.showStatus(`Welcome, ${data.username}!`, 'success');
break;

case 'chat_message':
this.addChatMessage(data);
break;

case 'user_joined':
this.addSystemMessage(`${data.username} joined the chat`);
this.updateUserList(data.users);
break;

case 'user_left':
this.addSystemMessage(`${data.username} left the chat`);
this.updateUserList(data.users);
break;

case 'user_list':
this.updateUserList(data.users);
break;

case 'error':
this.showStatus(data.message, 'error');
break;
}
}

sendMessage() {
const message = this.messageInput.value.trim();
if (message && this.wsManager) {
this.wsManager.send({
type: 'chat_message',
message: message,
username: this.currentUser,
timestamp: new Date().toISOString()
});

this.messageInput.value = '';
}
}

addChatMessage(data) {
const messageElement = document.createElement('div');
messageElement.className = 'chat-message';

const isOwnMessage = data.username === this.currentUser;
messageElement.classList.add(isOwnMessage ? 'own-message' : 'other-message');

messageElement.innerHTML = `
<div class="message-header">
<span class="username">${data.username}</span>
<span class="timestamp">${new Date(data.timestamp).toLocaleTimeString()}</span>
</div>
<div class="message-content">${this.escapeHtml(data.message)}</div>
`;

this.chatContainer.appendChild(messageElement);
this.scrollToBottom();
}

addSystemMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'system-message';
messageElement.textContent = message;

this.chatContainer.appendChild(messageElement);
this.scrollToBottom();
}

updateUserList(users) {
this.onlineUsers = users;
this.userList.innerHTML = '';

users.forEach(user => {
const userElement = document.createElement('div');
userElement.className = 'user-item';
userElement.textContent = user;
this.userList.appendChild(userElement);
});
}

showStatus(message, type) {
const statusElement = document.getElementById('status');
statusElement.textContent = message;
statusElement.className = `status ${type}`;
}

scrollToBottom() {
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
}

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

// Initialize chat application
const chatApp = new ChatApplication('ws://localhost:8080/chat');

Error Handling and Reconnection

Robust Error Handling

class RobustWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 1000,
maxReconnectAttempts: Infinity,
reconnectDecay: 1.5,
timeoutInterval: 2000,
...options
};

this.socket = null;
this.reconnectAttempts = 0;
this.reconnectTimer = null;
this.timeoutTimer = null;
this.isManualClose = false;

this.connect();
}

connect() {
try {
this.socket = new WebSocket(this.url);
this.setupEventHandlers();
this.startTimeout();
} catch (error) {
console.error('WebSocket creation failed:', error);
this.handleReconnect();
}
}

setupEventHandlers() {
this.socket.onopen = (event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.clearTimeout();
this.emit('open', event);
};

this.socket.onmessage = (event) => {
this.clearTimeout();
this.emit('message', event);
};

this.socket.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.clearTimeout();
this.emit('close', event);

if (!this.isManualClose) {
this.handleReconnect();
}
};

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

startTimeout() {
this.timeoutTimer = setTimeout(() => {
console.log('Connection timeout');
this.socket.close();
}, this.options.timeoutInterval);
}

clearTimeout() {
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}

handleReconnect() {
if (this.isManualClose) {
return;
}

if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts - 1);

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

this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('Max reconnection attempts reached');
this.emit('maxReconnectAttemptsReached');
}
}

send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
try {
const message = typeof data === 'string' ? data : JSON.stringify(data);
this.socket.send(message);
return true;
} catch (error) {
console.error('Send failed:', error);
this.emit('sendError', error);
return false;
}
} else {
console.warn('Cannot send: connection not open');
return false;
}
}

close() {
this.isManualClose = true;
this.clearTimeout();

if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}

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

// Event system
on(event, callback) {
if (!this.eventListeners) {
this.eventListeners = new Map();
}
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}

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

// Usage
const robustWS = new RobustWebSocket('ws://localhost:8080');

robustWS.on('open', () => {
console.log('Connected successfully');
});

robustWS.on('close', (event) => {
console.log('Connection closed:', event.code, event.reason);
});

robustWS.on('error', (error) => {
console.error('WebSocket error:', error);
});

robustWS.on('maxReconnectAttemptsReached', () => {
console.error('Failed to reconnect after maximum attempts');
});

Performance Optimization

Message Batching

class BatchedWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
batchSize: 10,
batchTimeout: 100, // ms
...options
};

this.socket = null;
this.messageQueue = [];
this.batchTimer = null;

this.connect();
}

connect() {
this.socket = new WebSocket(this.url);
this.setupEventHandlers();
}

setupEventHandlers() {
this.socket.onopen = () => {
console.log('Batched WebSocket connected');
};

this.socket.onmessage = (event) => {
this.handleMessage(event);
};

this.socket.onclose = () => {
console.log('Batched WebSocket closed');
};
}

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

// Send immediately if batch is full
if (this.messageQueue.length >= this.options.batchSize) {
this.flushBatch();
} else {
// Schedule batch send
this.scheduleBatch();
}
}

scheduleBatch() {
if (this.batchTimer) {
clearTimeout(this.batchTimer);
}

this.batchTimer = setTimeout(() => {
this.flushBatch();
}, this.options.batchTimeout);
}

flushBatch() {
if (this.messageQueue.length === 0) {
return;
}

if (this.socket && this.socket.readyState === WebSocket.OPEN) {
const batch = {
type: 'batch',
messages: this.messageQueue.splice(0, this.options.batchSize)
};

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

if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
}

handleMessage(event) {
try {
const data = JSON.parse(event.data);

if (data.type === 'batch') {
// Handle batched messages
data.messages.forEach(message => {
this.processMessage(message);
});
} else {
// Handle single message
this.processMessage(data);
}
} catch (error) {
console.error('Message parsing error:', error);
}
}

processMessage(message) {
console.log('Processing message:', message);
// Handle individual message
}
}

// Usage
const batchedWS = new BatchedWebSocket('ws://localhost:8080');

// Send multiple messages quickly
for (let i = 0; i < 20; i++) {
batchedWS.send({ type: 'update', data: i });
}

Connection Pooling

class WebSocketPool {
constructor(baseUrl, poolSize = 3) {
this.baseUrl = baseUrl;
this.poolSize = poolSize;
this.connections = [];
this.currentIndex = 0;

this.initializePool();
}

initializePool() {
for (let i = 0; i < this.poolSize; i++) {
const ws = new WebSocket(this.baseUrl);
this.connections.push({
socket: ws,
busy: false,
id: i
});
}
}

getConnection() {
// Round-robin selection
for (let i = 0; i < this.poolSize; i++) {
const connection = this.connections[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.poolSize;

if (!connection.busy && connection.socket.readyState === WebSocket.OPEN) {
connection.busy = true;
return connection;
}
}

// All connections busy, return null or wait
return null;
}

releaseConnection(connection) {
connection.busy = false;
}

send(data) {
const connection = this.getConnection();
if (connection) {
try {
connection.socket.send(JSON.stringify(data));
this.releaseConnection(connection);
return true;
} catch (error) {
this.releaseConnection(connection);
return false;
}
}
return false;
}

close() {
this.connections.forEach(conn => {
conn.socket.close();
});
}
}

// Usage
const wsPool = new WebSocketPool('ws://localhost:8080', 5);

// Send message through pool
wsPool.send({ type: 'message', content: 'Hello' });

Security Considerations

Authentication and Authorization

class SecureWebSocket {
constructor(url, authToken) {
this.url = url;
this.authToken = authToken;
this.socket = null;
this.isAuthenticated = false;

this.connect();
}

connect() {
// Add authentication to URL
const authUrl = `${this.url}?token=${this.authToken}`;
this.socket = new WebSocket(authUrl);

this.setupEventHandlers();
}

setupEventHandlers() {
this.socket.onopen = () => {
console.log('Secure WebSocket connected');
this.authenticate();
};

this.socket.onmessage = (event) => {
this.handleMessage(event);
};

this.socket.onclose = (event) => {
if (event.code === 1008) { // Policy violation
console.error('Authentication failed');
this.handleAuthFailure();
}
};
}

authenticate() {
this.send({
type: 'auth',
token: this.authToken,
timestamp: Date.now()
});
}

handleMessage(event) {
try {
const data = JSON.parse(event.data);

if (data.type === 'auth_success') {
this.isAuthenticated = true;
console.log('Authentication successful');
} else if (data.type === 'auth_failure') {
this.handleAuthFailure();
} else if (this.isAuthenticated) {
this.processMessage(data);
}
} catch (error) {
console.error('Message parsing error:', error);
}
}

send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
// Add authentication to all messages
const secureData = {
...data,
auth: this.authToken,
timestamp: Date.now()
};

this.socket.send(JSON.stringify(secureData));
}
}

handleAuthFailure() {
console.error('Authentication failed');
this.socket.close();
// Redirect to login or refresh token
}

processMessage(data) {
console.log('Processing authenticated message:', data);
}
}

// Usage
const secureWS = new SecureWebSocket('ws://localhost:8080', 'your-jwt-token');

Best Practices

1. Connection Management

// Good: Proper connection lifecycle management
class WebSocketService {
constructor() {
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}

connect(url) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.log('Already connected');
return;
}

this.socket = new WebSocket(url);
this.setupEventHandlers();
}

disconnect() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
}

// ... rest of implementation
}

// Avoid: Creating multiple connections without cleanup
function badConnect(url) {
const socket = new WebSocket(url);
// No cleanup, no state management
return socket;
}

2. Error Handling

// Good: Comprehensive error handling
socket.onerror = function(error) {
console.error('WebSocket error:', error);

// Log error for monitoring
logError(error);

// Show user-friendly message
showUserError('Connection error occurred');

// Attempt recovery
attemptReconnection();
};

// Avoid: Ignoring errors
socket.onerror = function(error) {
// Silent failure - bad practice
};

3. Message Validation

// Good: Validate incoming messages
function handleMessage(event) {
try {
const data = JSON.parse(event.data);

// Validate message structure
if (!data.type || !data.payload) {
throw new Error('Invalid message format');
}

// Validate message type
const validTypes = ['chat', 'notification', 'system'];
if (!validTypes.includes(data.type)) {
throw new Error('Invalid message type');
}

// Process valid message
processMessage(data);

} catch (error) {
console.error('Message validation failed:', error);
// Handle invalid message
}
}

// Avoid: Trusting all incoming data
function badHandleMessage(event) {
const data = JSON.parse(event.data);
// No validation - security risk
processMessage(data);
}

Summary

WebSocket is essential for real-time web applications:

  • Connection Management: Establish and maintain persistent connections
  • Message Handling: Send and receive text, JSON, and binary data
  • Event Handling: Manage connection states and errors
  • Reconnection: Implement robust reconnection strategies
  • Performance: Use batching and connection pooling for optimization
  • Security: Implement authentication and message validation
  • Best Practices: Follow proper connection lifecycle and error handling

Mastering WebSocket enables you to build responsive, real-time applications that provide excellent user experiences through instant communication and updates.


This tutorial is part of the JavaScript Mastery series by syscook.dev