Advanced Work with WebSocket in JavaScript: From Prototype to Production
\nWebSocket is a protocol that provides full-duplex communication between a client and a server over a single persistent TCP connection. In previous articles, we covered the basics: establishing a connection, sending messages, and simple chats. However, to create truly reliable and scalable real-time applications (online games, trading platforms, collaborative editing systems), it is necessary to master advanced techniques. In this material, we will examine the architecture of a production-grade WebSocket server on Node.js, including scaling, state management, authorization, and error handling.
\n\n1. Advanced Server Architecture and Room Management
\nA simple WebSocket server (based on the ws library) handles all clients in a single global object. In real projects, logical isolation is required — rooms and channels.
Consider an example of an architecture using the RoomManager class:
const WebSocket = require('ws');\n\nclass RoomManager {\n constructor() {\n this.rooms = new Map(); // roomName -> Set<WebSocket>\n }\n\n join(ws, roomName) {\n if (!this.rooms.has(roomName)) {\n this.rooms.set(roomName, new Set());\n }\n this.rooms.get(roomName).add(ws);\n ws.room = roomName; // Save the room on the socket\n }\n\n leave(ws) {\n if (ws.room && this.rooms.has(ws.room)) {\n this.rooms.get(ws.room).delete(ws);\n if (this.rooms.get(ws.room).size === 0) {\n this.rooms.delete(ws.room);\n }\n }\n }\n\n broadcast(roomName, message, excludeWs = null) {\n if (!this.rooms.has(roomName)) return;\n this.rooms.get(roomName).forEach(client => {\n if (client !== excludeWs && client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify(message));\n }\n });\n }\n}\n\n// Usage\nconst wss = new WebSocket.Server({ port: 8080 });\nconst roomManager = new RoomManager();\n\nwss.on('connection', (ws) => {\n ws.on('message', (data) => {\n const msg = JSON.parse(data);\n switch (msg.type) {\n case 'join':\n roomManager.join(ws, msg.room);\n break;\n case 'message':\n roomManager.broadcast(ws.room, { user: msg.user, text: msg.text }, ws);\n break;\n }\n });\n\n ws.on('close', () => roomManager.leave(ws));\n});\nThis code allows you to create isolated chats or game sessions. Pay attention to handling connection closure — this is critical for preventing memory leaks.
\n\n2. Scaling: Clustering and Redis Pub/Sub
\nA single Node.js process cannot efficiently use more than one CPU core. For scaling, we use the built-in cluster module. However, a problem arises: WebSocket connections are tied to a specific worker. How do you send a message to a user connected to a different worker?
The solution is an external message broker, such as Redis Pub/Sub.
\nconst cluster = require('cluster');\nconst http = require('http');\nconst { Server } = require('ws');\nconst redis = require('redis');\n\nconst REDIS_CHANNEL = 'ws:messages';\n\nif (cluster.isMaster) {\n // Launch workers by number of cores\n const numCPUs = require('os').cpus().length;\n for (let i = 0; i < numCPUs; i++) {\n cluster.fork();\n }\n\n cluster.on('exit', (worker) => {\n console.log(`Worker ${worker.process.pid} died`);\n cluster.fork(); // Restart\n });\n} else {\n // Worker\n const server = http.createServer();\n const wss = new Server({ server });\n\n // Connect to Redis\n const pub = redis.createClient();\n const sub