Docs
16.6-WebSockets-Real-Time
16.6 WebSockets and Real-Time Communication
Overview
WebSockets provide a persistent, full-duplex communication channel between client and server over a single TCP connection. Unlike HTTP's request-response pattern, WebSockets enable real-time bidirectional data transfer, making them ideal for live applications like chat, gaming, and live feeds.
Learning Objectives
- •Understand WebSocket protocol basics
- •Create and manage WebSocket connections
- •Handle messages, errors, and connection states
- •Implement reconnection strategies
- •Build real-time features with WebSockets
WebSocket Basics
Creating a Connection
// Create WebSocket connection
const socket = new WebSocket('wss://example.com/socket');
// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to server');
socket.send('Hello Server!');
});
// Listen for messages
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});
// Handle errors
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
// Connection closed
socket.addEventListener('close', (event) => {
console.log('Disconnected:', event.code, event.reason);
});
WebSocket States
const socket = new WebSocket('wss://example.com/socket');
// Check connection state
console.log(socket.readyState);
// ReadyState values:
// 0 - CONNECTING: Connection not yet open
// 1 - OPEN: Connection is open and ready
// 2 - CLOSING: Connection is closing
// 3 - CLOSED: Connection is closed
// Helper function to check state
function isConnected(socket) {
return socket.readyState === WebSocket.OPEN;
}
Sending Data
const socket = new WebSocket('wss://example.com/socket');
socket.onopen = () => {
// Send string
socket.send('Hello');
// Send JSON
socket.send(JSON.stringify({ type: 'message', text: 'Hello' }));
// Send ArrayBuffer
const buffer = new ArrayBuffer(16);
socket.send(buffer);
// Send Blob
const blob = new Blob(['Hello'], { type: 'text/plain' });
socket.send(blob);
};
// Set binary type for received data
socket.binaryType = 'arraybuffer'; // or 'blob'
Receiving Data
socket.onmessage = (event) => {
if (typeof event.data === 'string') {
// Text message
const data = JSON.parse(event.data);
console.log('Received:', data);
} else if (event.data instanceof ArrayBuffer) {
// Binary data
const view = new DataView(event.data);
console.log('Binary data received');
} else if (event.data instanceof Blob) {
// Blob data
event.data.text().then((text) => console.log(text));
}
};
Closing Connection
// Close with default code (1000)
socket.close();
// Close with code and reason
socket.close(1000, 'User logged out');
// Handle close event
socket.onclose = (event) => {
console.log(`Closed: ${event.code} - ${event.reason}`);
console.log('Clean close:', event.wasClean);
};
WebSocket Wrapper Class
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnect: true,
reconnectInterval: 1000,
maxReconnectAttempts: 5,
...options,
};
this.socket = null;
this.reconnectAttempts = 0;
this.listeners = new Map();
this.messageQueue = [];
}
connect() {
return new Promise((resolve, reject) => {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
this.flushQueue();
this.emit('connect');
resolve(this);
};
this.socket.onclose = (event) => {
console.log('Disconnected');
this.emit('disconnect', event);
if (this.options.reconnect && !event.wasClean) {
this.attemptReconnect();
}
};
this.socket.onerror = (error) => {
this.emit('error', error);
reject(error);
};
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit('message', data);
// Emit specific event type if present
if (data.type) {
this.emit(data.type, data.payload);
}
} catch (e) {
this.emit('message', event.data);
}
};
});
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.isConnected()) {
this.socket.send(message);
} else {
this.messageQueue.push(message);
}
}
flushQueue() {
while (this.messageQueue.length > 0) {
this.socket.send(this.messageQueue.shift());
}
}
isConnected() {
return this.socket && this.socket.readyState === WebSocket.OPEN;
}
attemptReconnect() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
console.log('Max reconnection attempts reached');
this.emit('reconnectFailed');
return;
}
this.reconnectAttempts++;
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);
setTimeout(() => {
this.connect().catch(() => {});
}, this.options.reconnectInterval * this.reconnectAttempts);
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this;
}
off(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
return this;
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach((callback) => callback(data));
}
}
close(code = 1000, reason = '') {
if (this.socket) {
this.options.reconnect = false;
this.socket.close(code, reason);
}
}
}
// Usage
const client = new WebSocketClient('wss://example.com/socket');
client.on('connect', () => console.log('Connected!'));
client.on('message', (data) => console.log('Received:', data));
client.on('chat', (message) => console.log('Chat:', message));
await client.connect();
client.send({ type: 'join', room: 'general' });
Common Patterns
Heartbeat/Ping-Pong
class WebSocketWithHeartbeat extends WebSocketClient {
constructor(url, options = {}) {
super(url, {
heartbeatInterval: 30000,
...options,
});
this.heartbeatTimer = null;
}
connect() {
return super.connect().then((client) => {
this.startHeartbeat();
return client;
});
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected()) {
this.send({ type: 'ping', timestamp: Date.now() });
}
}, this.options.heartbeatInterval);
this.on('pong', (data) => {
const latency = Date.now() - data.timestamp;
console.log(`Latency: ${latency}ms`);
});
}
close(code, reason) {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
super.close(code, reason);
}
}
Request-Response Pattern
class WebSocketRPC extends WebSocketClient {
constructor(url, options = {}) {
super(url, options);
this.pendingRequests = new Map();
this.requestId = 0;
this.on('response', (data) => {
const { id, result, error } = data;
const pending = this.pendingRequests.get(id);
if (pending) {
this.pendingRequests.delete(id);
if (error) {
pending.reject(new Error(error));
} else {
pending.resolve(result);
}
}
});
}
request(method, params, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = ++this.requestId;
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error('Request timeout'));
}, timeout);
this.pendingRequests.set(id, {
resolve: (result) => {
clearTimeout(timer);
resolve(result);
},
reject: (error) => {
clearTimeout(timer);
reject(error);
},
});
this.send({ type: 'request', id, method, params });
});
}
}
// Usage
const rpc = new WebSocketRPC('wss://api.example.com');
await rpc.connect();
const user = await rpc.request('getUser', { id: 123 });
const posts = await rpc.request('getPosts', { userId: 123 });
Room-Based Chat
class ChatClient extends WebSocketClient {
constructor(url) {
super(url);
this.currentRoom = null;
}
join(room) {
this.currentRoom = room;
this.send({ type: 'join', room });
}
leave() {
if (this.currentRoom) {
this.send({ type: 'leave', room: this.currentRoom });
this.currentRoom = null;
}
}
sendMessage(text) {
if (this.currentRoom) {
this.send({
type: 'message',
room: this.currentRoom,
text,
timestamp: Date.now(),
});
}
}
sendTyping(isTyping) {
if (this.currentRoom) {
this.send({
type: 'typing',
room: this.currentRoom,
isTyping,
});
}
}
}
// Usage
const chat = new ChatClient('wss://chat.example.com');
chat.on('message', (data) => {
console.log(`${data.user}: ${data.text}`);
});
chat.on('userJoined', (data) => {
console.log(`${data.user} joined the room`);
});
chat.on('typing', (data) => {
console.log(`${data.user} is typing...`);
});
await chat.connect();
chat.join('general');
chat.sendMessage('Hello everyone!');
Server-Sent Events (SSE)
For one-way server-to-client streaming, SSE is simpler:
// SSE is simpler for one-way server-to-client
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
console.log('Data:', event.data);
};
eventSource.addEventListener('update', (event) => {
console.log('Update:', JSON.parse(event.data));
});
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
eventSource.close();
};
WebSocket vs SSE vs Polling
| Feature | WebSocket | SSE | Polling |
|---|---|---|---|
| Direction | Bidirectional | Server → Client | Client → Server |
| Connection | Persistent | Persistent | New per request |
| Protocol | WS/WSS | HTTP | HTTP |
| Complexity | Higher | Lower | Lowest |
| Use Case | Chat, Games | Notifications | Legacy systems |
Best Practices
- •Always handle reconnection - Networks are unreliable
- •Use heartbeats - Detect dead connections
- •Queue messages - Buffer during disconnection
- •Handle errors gracefully - Show user feedback
- •Use WSS - Always use secure WebSocket in production
- •Implement backoff - Exponential backoff for reconnection
Summary
| Concept | Description |
|---|---|
| WebSocket | Full-duplex communication channel |
| readyState | Connection state (0-3) |
| send() | Send data to server |
| close() | Close connection |
| Events | open, message, error, close |
Resources
- •MDN: WebSocket
- •WebSocket Protocol
- •Socket.io - Popular WebSocket library