JavaScript WebSockets: Real-Time Communication Guide
Master WebSockets in JavaScript for real-time communication. Learn bidirectional messaging, Socket.IO, and building real-time applications.
JavaScript WebSockets: Real-Time Communication Guide
WebSockets provide full-duplex, bidirectional communication between clients and servers, enabling real-time data exchange. This guide covers everything from basic WebSocket usage to building complex real-time applications.
Understanding WebSockets
WebSockets enable persistent connections between the client and server, allowing both parties to send data at any time.
// Basic WebSocket connection
const socket = new WebSocket('ws://localhost:8080');
// Connection events
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
socket.send('Hello Server!');
});
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});
socket.addEventListener('close', (event) => {
console.log('Disconnected from WebSocket server');
console.log('Code:', event.code, 'Reason:', event.reason);
});
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
// Check connection state
console.log('State:', socket.readyState);
// 0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED
WebSocket Client Implementation
Enhanced WebSocket Client
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnect: true,
reconnectInterval: 1000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
...options,
};
this.reconnectAttempts = 0;
this.eventHandlers = new Map();
this.messageQueue = [];
this.isConnected = false;
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection error:', error);
this.handleReconnect();
}
}
setupEventHandlers() {
this.ws.onopen = (event) => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
// Send queued messages
this.flushMessageQueue();
// Start heartbeat
this.startHeartbeat();
// Emit open event
this.emit('open', event);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle different message types
if (data.type === 'pong') {
this.handlePong();
} else {
this.emit('message', data);
// Emit typed events
if (data.type) {
this.emit(data.type, data.payload);
}
}
} catch (error) {
// Handle non-JSON messages
this.emit('message', event.data);
}
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.isConnected = false;
this.stopHeartbeat();
this.emit('close', event);
if (this.options.reconnect && !this.manualClose) {
this.handleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', error);
};
}
handleReconnect() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
this.emit('reconnectFailed');
return;
}
this.reconnectAttempts++;
const delay = this.options.reconnectInterval * this.reconnectAttempts;
console.log(
`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
);
setTimeout(() => {
this.connect();
}, delay);
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
// Queue message if not connected
this.messageQueue.push(message);
}
}
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.ws.send(message);
}
}
// Event emitter pattern
on(event, handler) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, []);
}
this.eventHandlers.get(event).push(handler);
}
off(event, handler) {
const handlers = this.eventHandlers.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
emit(event, data) {
const handlers = this.eventHandlers.get(event);
if (handlers) {
handlers.forEach((handler) => handler(data));
}
}
// Heartbeat mechanism
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.isConnected) {
this.send({ type: 'ping' });
// Set timeout for pong response
this.pongTimeout = setTimeout(() => {
console.warn('Pong timeout - connection may be dead');
this.ws.close();
}, 5000);
}
}, this.options.heartbeatInterval);
}
handlePong() {
clearTimeout(this.pongTimeout);
}
stopHeartbeat() {
clearInterval(this.heartbeatInterval);
clearTimeout(this.pongTimeout);
}
close() {
this.manualClose = true;
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
}
}
}
// Usage
const client = new WebSocketClient('ws://localhost:8080');
client.on('open', () => {
console.log('Connected!');
client.send({ type: 'subscribe', channel: 'updates' });
});
client.on('message', (data) => {
console.log('Received:', data);
});
client.on('update', (payload) => {
console.log('Update:', payload);
});
Binary Data Handling
class BinaryWebSocketClient extends WebSocketClient {
constructor(url, options) {
super(url, options);
this.ws.binaryType = 'arraybuffer'; // or 'blob'
}
sendBinary(data) {
if (data instanceof ArrayBuffer || data instanceof Blob) {
this.ws.send(data);
} else if (data instanceof Uint8Array) {
this.ws.send(data.buffer);
} else {
throw new Error('Invalid binary data type');
}
}
sendFile(file) {
const reader = new FileReader();
reader.onload = (event) => {
const arrayBuffer = event.target.result;
// Send file metadata first
this.send({
type: 'fileMetadata',
name: file.name,
size: file.size,
type: file.type,
});
// Then send the file data
this.sendBinary(arrayBuffer);
};
reader.readAsArrayBuffer(file);
}
setupEventHandlers() {
super.setupEventHandlers();
const originalOnMessage = this.ws.onmessage;
this.ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
this.handleBinaryMessage(event.data);
} else {
originalOnMessage.call(this.ws, event);
}
};
}
handleBinaryMessage(arrayBuffer) {
// Example: Handle image data
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
this.emit('binaryData', {
blob,
url,
size: arrayBuffer.byteLength,
});
}
}
Real-Time Chat Application
Chat Client Implementation
class ChatClient {
constructor(serverUrl, username) {
this.username = username;
this.messages = [];
this.users = new Map();
this.typing = new Map();
this.ws = new WebSocketClient(serverUrl);
this.setupEventHandlers();
}
setupEventHandlers() {
this.ws.on('open', () => {
this.join();
});
this.ws.on('message', (message) => {
this.handleMessage(message);
});
this.ws.on('userJoined', (user) => {
this.users.set(user.id, user);
this.onUserJoined?.(user);
});
this.ws.on('userLeft', (userId) => {
const user = this.users.get(userId);
this.users.delete(userId);
this.typing.delete(userId);
this.onUserLeft?.(user);
});
this.ws.on('chatMessage', (data) => {
this.messages.push(data);
this.onMessageReceived?.(data);
});
this.ws.on('typing', (data) => {
if (data.isTyping) {
this.typing.set(data.userId, data.username);
} else {
this.typing.delete(data.userId);
}
this.onTypingUpdate?.(Array.from(this.typing.values()));
});
this.ws.on('userList', (users) => {
this.users.clear();
users.forEach((user) => this.users.set(user.id, user));
this.onUserListUpdate?.(users);
});
}
join() {
this.ws.send({
type: 'join',
username: this.username,
});
}
sendMessage(text) {
const message = {
type: 'chatMessage',
text,
timestamp: new Date().toISOString(),
};
this.ws.send(message);
}
sendTyping(isTyping) {
this.ws.send({
type: 'typing',
isTyping,
});
}
handleMessage(message) {
switch (message.type) {
case 'history':
this.messages = message.messages;
this.onHistoryLoaded?.(this.messages);
break;
case 'error':
this.onError?.(message.error);
break;
}
}
disconnect() {
this.ws.close();
}
}
// Chat UI Component
class ChatUI {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.setupUI();
this.setupClient();
}
setupUI() {
this.container.innerHTML = `
<div class="chat-container">
<div class="chat-header">
<h3>Chat Room</h3>
<div class="connection-status"></div>
</div>
<div class="chat-main">
<div class="user-list">
<h4>Users</h4>
<ul id="users"></ul>
</div>
<div class="chat-messages">
<div id="messages"></div>
<div id="typing-indicator"></div>
</div>
</div>
<div class="chat-input">
<input type="text" id="message-input" placeholder="Type a message..." />
<button id="send-button">Send</button>
</div>
</div>
`;
this.bindEvents();
}
bindEvents() {
const input = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
let typingTimer;
input.addEventListener('input', () => {
if (!this.isTyping) {
this.isTyping = true;
this.client.sendTyping(true);
}
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
this.isTyping = false;
this.client.sendTyping(false);
}, 1000);
});
sendButton.addEventListener('click', () => {
this.sendMessage();
});
}
setupClient() {
const username = prompt('Enter your username:') || 'Anonymous';
this.client = new ChatClient('ws://localhost:8080', username);
this.client.onMessageReceived = (message) => {
this.addMessage(message);
};
this.client.onUserJoined = (user) => {
this.addSystemMessage(`${user.username} joined the chat`);
this.updateUserList();
};
this.client.onUserLeft = (user) => {
this.addSystemMessage(`${user.username} left the chat`);
this.updateUserList();
};
this.client.onTypingUpdate = (typingUsers) => {
this.updateTypingIndicator(typingUsers);
};
this.client.onUserListUpdate = () => {
this.updateUserList();
};
this.client.onHistoryLoaded = (messages) => {
messages.forEach((msg) => this.addMessage(msg, false));
};
}
sendMessage() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (text) {
this.client.sendMessage(text);
input.value = '';
}
}
addMessage(message, scrollToBottom = true) {
const messagesDiv = document.getElementById('messages');
const messageEl = document.createElement('div');
messageEl.className = 'message';
messageEl.innerHTML = `
<span class="username">${message.username}:</span>
<span class="text">${this.escapeHtml(message.text)}</span>
<span class="timestamp">${this.formatTime(message.timestamp)}</span>
`;
messagesDiv.appendChild(messageEl);
if (scrollToBottom) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
}
addSystemMessage(text) {
const messagesDiv = document.getElementById('messages');
const messageEl = document.createElement('div');
messageEl.className = 'system-message';
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
updateUserList() {
const usersList = document.getElementById('users');
usersList.innerHTML = '';
this.client.users.forEach((user) => {
const li = document.createElement('li');
li.textContent = user.username;
li.className = user.online ? 'online' : 'offline';
usersList.appendChild(li);
});
}
updateTypingIndicator(typingUsers) {
const indicator = document.getElementById('typing-indicator');
if (typingUsers.length === 0) {
indicator.textContent = '';
} else if (typingUsers.length === 1) {
indicator.textContent = `${typingUsers[0]} is typing...`;
} else {
indicator.textContent = `${typingUsers.join(', ')} are typing...`;
}
}
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
Real-Time Collaboration
Collaborative Editor
class CollaborativeEditor {
constructor(editorId, websocketUrl) {
this.editor = document.getElementById(editorId);
this.documentId = this.generateDocumentId();
this.version = 0;
this.pendingOperations = [];
this.collaborators = new Map();
this.ws = new WebSocketClient(websocketUrl);
this.setupEditor();
this.setupWebSocket();
}
setupEditor() {
// Track local changes
this.editor.addEventListener('input', (event) => {
this.handleLocalChange(event);
});
// Track selection changes
document.addEventListener('selectionchange', () => {
const selection = window.getSelection();
if (
selection.rangeCount > 0 &&
this.editor.contains(selection.anchorNode)
) {
this.handleSelectionChange(selection);
}
});
}
setupWebSocket() {
this.ws.on('open', () => {
this.joinDocument();
});
this.ws.on('operation', (data) => {
this.applyRemoteOperation(data);
});
this.ws.on('cursor', (data) => {
this.updateRemoteCursor(data);
});
this.ws.on('collaboratorJoined', (data) => {
this.addCollaborator(data);
});
this.ws.on('collaboratorLeft', (data) => {
this.removeCollaborator(data.userId);
});
this.ws.on('documentState', (data) => {
this.loadDocument(data);
});
}
handleLocalChange(event) {
const operation = this.createOperation(event);
// Send to server
this.ws.send({
type: 'operation',
documentId: this.documentId,
operation: operation,
version: this.version,
});
// Add to pending operations
this.pendingOperations.push(operation);
}
createOperation(event) {
// Simplified operation - in practice, use Operational Transform or CRDT
return {
type: 'insert',
position: event.target.selectionStart,
text: event.data,
timestamp: Date.now(),
userId: this.userId,
};
}
applyRemoteOperation(data) {
const { operation, userId } = data;
// Skip if it's our own operation
if (userId === this.userId) {
this.pendingOperations.shift();
return;
}
// Transform operation against pending operations
const transformedOp = this.transformOperation(operation);
// Apply to editor
this.applyOperation(transformedOp);
// Update version
this.version++;
}
transformOperation(operation) {
// Simplified transformation - implement OT algorithm
let transformed = { ...operation };
this.pendingOperations.forEach((pendingOp) => {
if (pendingOp.position <= transformed.position) {
transformed.position += pendingOp.text.length;
}
});
return transformed;
}
applyOperation(operation) {
const currentValue = this.editor.value;
const before = currentValue.substring(0, operation.position);
const after = currentValue.substring(operation.position);
this.editor.value = before + operation.text + after;
// Restore cursor position
this.editor.setSelectionRange(
operation.position + operation.text.length,
operation.position + operation.text.length
);
}
handleSelectionChange(selection) {
const range = selection.getRangeAt(0);
const position = this.getCaretPosition();
this.ws.send({
type: 'cursor',
documentId: this.documentId,
position: position,
userId: this.userId,
});
}
updateRemoteCursor(data) {
const { userId, position, username, color } = data;
let cursor = this.collaborators.get(userId);
if (!cursor) {
cursor = this.createCursorElement(username, color);
this.collaborators.set(userId, cursor);
}
// Update cursor position
this.positionCursor(cursor, position);
}
createCursorElement(username, color) {
const cursor = document.createElement('div');
cursor.className = 'remote-cursor';
cursor.style.backgroundColor = color;
const label = document.createElement('span');
label.className = 'cursor-label';
label.textContent = username;
label.style.backgroundColor = color;
cursor.appendChild(label);
document.body.appendChild(cursor);
return cursor;
}
positionCursor(cursor, position) {
// Calculate pixel position from character position
const coords = this.getCoordinatesFromPosition(position);
cursor.style.left = coords.x + 'px';
cursor.style.top = coords.y + 'px';
}
getCaretPosition() {
return this.editor.selectionStart;
}
getCoordinatesFromPosition(position) {
// Simplified - in practice, use a more sophisticated method
const text = this.editor.value.substring(0, position);
const lines = text.split('\n');
return {
x: this.editor.offsetLeft + lines[lines.length - 1].length * 8,
y: this.editor.offsetTop + lines.length * 20,
};
}
joinDocument() {
this.ws.send({
type: 'joinDocument',
documentId: this.documentId,
username: this.username,
});
}
loadDocument(data) {
this.editor.value = data.content;
this.version = data.version;
}
generateDocumentId() {
return 'doc_' + Math.random().toString(36).substr(2, 9);
}
}
Socket.IO Integration
Socket.IO Client
// Socket.IO provides additional features over raw WebSockets
class SocketIOApp {
constructor() {
this.socket = io('http://localhost:3000', {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
this.setupEventHandlers();
}
setupEventHandlers() {
// Connection events
this.socket.on('connect', () => {
console.log('Connected to server');
console.log('Socket ID:', this.socket.id);
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
// Custom events
this.socket.on('message', (data) => {
console.log('Received message:', data);
});
// Room events
this.socket.on('userJoinedRoom', (data) => {
console.log(`${data.username} joined room ${data.room}`);
});
this.socket.on('userLeftRoom', (data) => {
console.log(`${data.username} left room ${data.room}`);
});
}
// Emit events
sendMessage(message) {
this.socket.emit('message', message);
}
// Emit with acknowledgment
sendWithAck(event, data) {
return new Promise((resolve) => {
this.socket.emit(event, data, (response) => {
resolve(response);
});
});
}
// Join/leave rooms
joinRoom(roomName) {
this.socket.emit('joinRoom', roomName);
}
leaveRoom(roomName) {
this.socket.emit('leaveRoom', roomName);
}
// Volatile events (can be dropped)
sendVolatile(event, data) {
this.socket.volatile.emit(event, data);
}
// Binary data
sendBinary(data) {
const buffer = new ArrayBuffer(data.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < data.length; i++) {
view[i] = data.charCodeAt(i);
}
this.socket.emit('binaryData', buffer);
}
}
// Advanced Socket.IO patterns
class RealtimeGame {
constructor() {
this.socket = io('/game', {
auth: {
token: this.getAuthToken(),
},
});
this.players = new Map();
this.gameState = {};
this.setupGame();
}
setupGame() {
// Handle initial game state
this.socket.on('gameState', (state) => {
this.gameState = state;
this.render();
});
// Handle player updates
this.socket.on('playerUpdate', (data) => {
this.players.set(data.id, data);
this.renderPlayer(data);
});
// Handle game events with interpolation
this.socket.on('gameUpdate', (updates) => {
this.interpolateGameState(updates);
});
// Latency compensation
this.socket.on('ping', () => {
this.socket.emit('pong');
});
// Start game loop
this.startGameLoop();
}
startGameLoop() {
let lastTime = performance.now();
const gameLoop = (currentTime) => {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Update local game state
this.update(deltaTime);
// Send player input
this.sendInput();
// Render
this.render();
requestAnimationFrame(gameLoop);
};
requestAnimationFrame(gameLoop);
}
sendInput() {
const input = this.getPlayerInput();
if (input.hasChanged) {
// Send input with timestamp for lag compensation
this.socket.emit('playerInput', {
input: input,
timestamp: Date.now(),
sequence: this.inputSequence++,
});
}
}
interpolateGameState(updates) {
// Smooth interpolation between server updates
const interpolationTime = 100; // ms
updates.forEach((update) => {
const player = this.players.get(update.id);
if (player) {
// Store target position
player.targetX = update.x;
player.targetY = update.y;
player.lastUpdate = Date.now();
}
});
}
update(deltaTime) {
// Interpolate player positions
this.players.forEach((player) => {
if (player.targetX !== undefined) {
const t = Math.min(1, (Date.now() - player.lastUpdate) / 100);
player.x = this.lerp(player.x, player.targetX, t);
player.y = this.lerp(player.y, player.targetY, t);
}
});
}
lerp(start, end, t) {
return start + (end - start) * t;
}
}
Performance and Optimization
Message Batching
class BatchedWebSocket {
constructor(url, batchInterval = 50) {
this.ws = new WebSocket(url);
this.batchInterval = batchInterval;
this.messageQueue = [];
this.startBatching();
}
startBatching() {
this.batchTimer = setInterval(() => {
if (this.messageQueue.length > 0) {
this.flushBatch();
}
}, this.batchInterval);
}
send(message) {
this.messageQueue.push(message);
// Flush immediately if queue is getting large
if (this.messageQueue.length >= 100) {
this.flushBatch();
}
}
flushBatch() {
if (this.ws.readyState === WebSocket.OPEN) {
const batch = {
type: 'batch',
messages: this.messageQueue.splice(0),
};
this.ws.send(JSON.stringify(batch));
}
}
close() {
clearInterval(this.batchTimer);
this.flushBatch();
this.ws.close();
}
}
// Message compression
class CompressedWebSocket {
constructor(url) {
this.ws = new WebSocket(url);
this.compressionWorker = new Worker('compression-worker.js');
this.setupWorker();
}
setupWorker() {
this.compressionWorker.onmessage = (e) => {
const { compressed, original } = e.data;
// Only send compressed if it's smaller
if (compressed.length < original.length * 0.9) {
this.ws.send(compressed);
} else {
this.ws.send(original);
}
};
}
send(data) {
const message = JSON.stringify(data);
// Compress large messages
if (message.length > 1024) {
this.compressionWorker.postMessage({
action: 'compress',
data: message,
});
} else {
this.ws.send(message);
}
}
}
Connection Pooling
class WebSocketPool {
constructor(urls, options = {}) {
this.urls = urls;
this.options = {
maxConnections: urls.length,
strategy: 'round-robin', // or 'least-loaded', 'random'
...options,
};
this.connections = [];
this.currentIndex = 0;
this.initializeConnections();
}
initializeConnections() {
this.urls.forEach((url) => {
const connection = {
url,
ws: null,
messageCount: 0,
isConnected: false,
};
this.connect(connection);
this.connections.push(connection);
});
}
connect(connection) {
connection.ws = new WebSocket(connection.url);
connection.ws.onopen = () => {
connection.isConnected = true;
console.log(`Connected to ${connection.url}`);
};
connection.ws.onclose = () => {
connection.isConnected = false;
console.log(`Disconnected from ${connection.url}`);
// Reconnect after delay
setTimeout(() => this.connect(connection), 5000);
};
connection.ws.onerror = (error) => {
console.error(`Error on ${connection.url}:`, error);
};
}
getConnection() {
let selected;
switch (this.options.strategy) {
case 'round-robin':
selected = this.connections[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.connections.length;
break;
case 'least-loaded':
selected = this.connections.reduce((min, conn) => {
if (!conn.isConnected) return min;
return conn.messageCount < min.messageCount ? conn : min;
});
break;
case 'random':
const available = this.connections.filter((c) => c.isConnected);
selected = available[Math.floor(Math.random() * available.length)];
break;
}
return selected;
}
send(message) {
const connection = this.getConnection();
if (connection && connection.isConnected) {
connection.ws.send(JSON.stringify(message));
connection.messageCount++;
} else {
console.error('No available connections');
}
}
broadcast(message) {
this.connections.forEach((connection) => {
if (connection.isConnected) {
connection.ws.send(JSON.stringify(message));
}
});
}
close() {
this.connections.forEach((connection) => {
if (connection.ws) {
connection.ws.close();
}
});
}
}
Security Considerations
class SecureWebSocket {
constructor(url, options = {}) {
this.options = {
maxMessageSize: 1024 * 1024, // 1MB
rateLimitMessages: 100,
rateLimitWindow: 60000, // 1 minute
allowedOrigins: ['https://example.com'],
...options,
};
this.messageCount = 0;
this.rateLimitReset = Date.now() + this.options.rateLimitWindow;
// Use secure WebSocket (wss://)
this.ws = new WebSocket(url.replace(/^ws:/, 'wss:'));
this.setupSecurity();
}
setupSecurity() {
const originalOnMessage = this.ws.onmessage;
this.ws.onmessage = (event) => {
// Validate message size
if (event.data.length > this.options.maxMessageSize) {
console.error('Message too large');
return;
}
// Validate message format
try {
const data = JSON.parse(event.data);
// Validate message structure
if (!this.validateMessage(data)) {
console.error('Invalid message structure');
return;
}
// Call original handler
if (originalOnMessage) {
originalOnMessage.call(this.ws, event);
}
} catch (error) {
console.error('Invalid JSON message');
}
};
}
validateMessage(data) {
// Implement message validation
if (!data.type || typeof data.type !== 'string') {
return false;
}
// Validate against whitelist of allowed message types
const allowedTypes = ['chat', 'update', 'ping', 'pong'];
if (!allowedTypes.includes(data.type)) {
return false;
}
return true;
}
send(message) {
// Rate limiting
if (Date.now() > this.rateLimitReset) {
this.messageCount = 0;
this.rateLimitReset = Date.now() + this.options.rateLimitWindow;
}
if (this.messageCount >= this.options.rateLimitMessages) {
console.error('Rate limit exceeded');
return false;
}
this.messageCount++;
// Sanitize message
const sanitized = this.sanitizeMessage(message);
this.ws.send(JSON.stringify(sanitized));
return true;
}
sanitizeMessage(message) {
// Remove any potentially dangerous content
const sanitized = {};
for (const [key, value] of Object.entries(message)) {
if (typeof value === 'string') {
// Remove script tags and other dangerous content
sanitized[key] = value
.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '');
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}
// Authentication with WebSockets
class AuthenticatedWebSocket {
constructor(url, token) {
this.url = url;
this.token = token;
this.connect();
}
connect() {
// Include auth token in connection
this.ws = new WebSocket(
`${this.url}?token=${encodeURIComponent(this.token)}`
);
this.ws.onopen = () => {
console.log('Authenticated connection established');
};
this.ws.onerror = (error) => {
console.error('Authentication failed:', error);
};
}
// Refresh token before expiry
async refreshToken() {
try {
const response = await fetch('/api/refresh-token', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.token}`,
},
});
const data = await response.json();
this.token = data.token;
// Reconnect with new token
this.ws.close();
this.connect();
} catch (error) {
console.error('Token refresh failed:', error);
}
}
}
Best Practices
-
Handle connection states properly
if (socket.readyState === WebSocket.OPEN) { socket.send(message); } else { // Queue message or handle error }
-
Implement reconnection logic
// Automatic reconnection with exponential backoff let reconnectDelay = 1000; const maxDelay = 30000; function reconnect() { setTimeout(() => { connect(); reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); }, reconnectDelay); }
-
Use binary data for large payloads
// Send binary data instead of base64 strings const buffer = new ArrayBuffer(data.length); socket.send(buffer);
-
Implement heartbeat/keepalive
// Keep connection alive setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'ping' })); } }, 30000);
Conclusion
WebSockets enable powerful real-time features:
- Bidirectional communication for instant updates
- Low latency compared to polling
- Efficient for frequent small messages
- Persistent connections reduce overhead
- Real-time applications like chat, gaming, collaboration
Key takeaways:
- Handle connection lifecycle properly
- Implement reconnection strategies
- Use appropriate data formats
- Consider security implications
- Optimize for performance
- Use Socket.IO for additional features
Master WebSockets to build engaging real-time applications!