Node.js WebSockets: Complete Theory & Practice Guide
Build real-time, full-duplex applications with ws and Socket.io at production scale.
wsSocket.ioScaling
Table of Contents
1. Theory: WebSocket Fundamentals
WebSockets provide full-duplex, bidirectional communication over a single persistent TCP connection.
// HTTP vs WebSocket Comparison
const comparison = {
http: {
connection: 'Short-lived, request-response',
overhead: 'High (headers per request)',
direction: 'Client → Server only (polling for reverse)',
realtime: '❌ Poor (requires polling/long-polling)',
useCase: 'REST APIs, static content'
},
websocket: {
connection: 'Persistent, single TCP socket',
overhead: 'Low (minimal framing after handshake)',
direction: 'Full bidirectional (client ↔ server)',
realtime: '✅ Excellent (sub-millisecond latency)',
useCase: 'Chat, gaming, live feeds, collaboration'
}
};
WebSocket Handshake Process
// 1. Client sends Upgrade request
const handshakeRequest = {
method: 'GET',
path: '/chat',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'
}
};
// 2. Server responds with 101 Switching Protocols
const handshakeResponse = {
status: 101,
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Accept': 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
}
};
WebSocket Frame Structure
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) |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Concept | Description | Importance |
|---|---|---|
| Opcode | Message type (text, binary, ping, pong, close) | Determines frame purpose |
| FIN bit | Final frame of message | Enables message fragmentation |
| Masking | Client→Server XOR masking | Security requirement |
| Ping/Pong | Heartbeat mechanism | Keep-alive and latency check |
2. Basic: Getting Started with ws
npm install ws
// basic/ws-server.js
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
WebSocket Demo
WebSocket Client
`);
}
});
const wss = new WebSocket.Server({ server });
const clients = new Set();
wss.on('connection', (ws, req) => {
clients.add(ws);
ws.send(JSON.stringify({
type: 'welcome',
message: 'Welcome to WebSocket server!',
clientCount: clients.size,
timestamp: new Date().toISOString()
}));
ws.on('message', (data) => {
const message = data.toString();
ws.send(`Echo: ${message}`);
for (const client of clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'broadcast', message, timestamp: new Date().toISOString() }));
}
}
});
ws.on('close', () => clients.delete(ws));
ws.on('error', () => clients.delete(ws));
});
setInterval(() => {
for (const client of clients) if (client.readyState === WebSocket.OPEN) client.ping();
}, 30000);
server.listen(8080);
// basic/ws-client.js
const WebSocket = require('ws');
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.on('open', () => {
this.reconnectAttempts = 0;
this.send({ type: 'auth', token: 'your-jwt-token', userId: 'user123' });
});
this.ws.on('message', data => console.log('msg:', data.toString()));
this.ws.on('close', () => this.reconnect());
this.ws.on('error', error => console.error('WebSocket error:', error));
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
}
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
setTimeout(() => this.connect(), delay);
}
}
}
new WebSocketClient('ws://localhost:8080').connect();
3. Basic: Socket.io Essentials
npm install socket.io socket.io-client
// basic/socketio-server.js
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: { origin: '*', methods: ['GET', 'POST'], credentials: true },
pingTimeout: 60000,
pingInterval: 25000,
transports: ['websocket', 'polling']
});
io.on('connection', (socket) => {
socket.on('join-room', (room) => socket.join(room));
socket.on('leave-room', (room) => socket.leave(room));
socket.on('room-message', ({ room, message }) => {
io.to(room).emit('room-message', { from: socket.id, message, room, timestamp: new Date() });
});
socket.on('private-message', ({ to, message }) => {
io.to(to).emit('private-message', { from: socket.id, message, timestamp: new Date() });
});
socket.on('disconnect', () => io.emit('user-disconnected', socket.id));
});
<!-- basic/socketio-client.html -->
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script>
const socket = io('http://localhost:3000', {
auth: { token: 'user-jwt-token' },
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
socket.on('connect', () => socket.emit('join-room', 'general'));
socket.on('room-message', (data) => console.log(data));
socket.on('connect_error', (error) => console.log(error.message));
</script>
4. Advanced: Real-time Applications
// advanced/chat-application.js
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const app = express();
const server = http.createServer(app);
const io = socketIO(server);
const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: 6379 });
class ChatApplication {
constructor() {
this.users = new Map();
this.typingUsers = new Map();
this.messageHistory = [];
this.setupMiddleware();
this.setupEvents();
}
setupMiddleware() {
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error('Authentication required'));
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.user = decoded;
socket.userId = decoded.id;
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
}
setupEvents() {
io.on('connection', (socket) => {
this.users.set(socket.userId, socket.id);
this.updateUserPresence(socket.userId, true);
socket.on('join-conversation', async (conversationId) => this.joinConversation(socket, conversationId));
socket.on('send-message', async (data) => this.handleMessage(socket, data));
socket.on('typing', (data) => this.handleTyping(socket, data));
socket.on('mark-read', async (data) => this.markMessagesAsRead(socket, data));
socket.on('disconnect', () => { this.users.delete(socket.userId); this.updateUserPresence(socket.userId, false); });
});
}
async handleMessage(socket, { conversationId, message, replyTo }) {
const messageObj = {
id: this.generateMessageId(),
conversationId,
senderId: socket.userId,
senderName: socket.user.name,
message: this.sanitizeMessage(message),
replyTo,
timestamp: new Date().toISOString(),
status: 'sent',
reactions: {}
};
await this.saveMessage(messageObj);
io.to(`conv:${conversationId}`).emit('new-message', messageObj);
}
sanitizeMessage(message) { return message.replace(/[<>]/g, '').slice(0, 2000); }
generateMessageId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }
async updateUserPresence(userId, isOnline) { const key = `presence:${userId}`; if (isOnline) await redis.set(key, 'online', 'EX', 300); else await redis.del(key); }
async joinConversation() {}
handleTyping() {}
async markMessagesAsRead() {}
async saveMessage(message) { return message; }
}
5. Advanced: Scaling WebSockets
npm install @socket.io/redis-adapter ioredis
// advanced/scaling-redis.js
const { createAdapter } = require('@socket.io/redis-adapter');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createClient } = require('redis');
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', () => cluster.fork());
} else {
(async () => {
const httpServer = createServer();
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
const io = new Server(httpServer, { adapter: createAdapter(pubClient, subClient), cors: { origin: '*' } });
io.on('connection', (socket) => {
socket.on('join-room', room => socket.join(room));
socket.on('room-message', ({ room, message }) => io.to(room).emit('room-message', { from: socket.id, message, timestamp: Date.now() }));
});
httpServer.listen(process.env.PORT || 3000);
})();
}
// HAProxy / Nginx sticky session note:
// Use sticky sessions + Redis adapter for horizontal scaling.
6. Advanced: Security & Authentication
// advanced/security-auth.js
class SecureWebSocketServer {
setupAuthentication() { /* JWT validation + blacklist + duplicate session handling */ }
setupRateLimiting() { /* per-IP and per-event windows */ }
setupSecurityMiddleware() { /* event allowlist + size checks + role permissions */ }
sanitizeData(data) {
if (typeof data === 'string') {
return data
.replace(/