Fortgeschrittene Arbeit mit WebSocket in JavaScript: vom Prototyp zur Produktion
\nWebSocket ist ein Protokoll, das eine Vollduplex-Kommunikation zwischen Client und Server über eine einzige permanente TCP-Verbindung ermöglicht. In früheren Artikeln haben wir die Grundlagen behandelt: Verbindungsaufbau, Nachrichtensenden und einfache Chats. Für die Erstellung wirklich zuverlässiger und skalierbarer Echtzeitanwendungen (Online-Spiele, Handelsplattformen, Systeme für kollaboratives Bearbeiten) ist es jedoch notwendig, fortgeschrittene Techniken zu beherrschen. In diesem Material betrachten wir die Architektur eines production-grade WebSocket-Servers auf Node.js, einschließlich Skalierung, Zustandsverwaltung, Autorisierung und Fehlerbehandlung.
\n\n1. Fortgeschrittene Serverarchitektur und Raumverwaltung
\nEin einfacher WebSocket-Server (basierend auf der Bibliothek ws) behandelt alle Clients in einem globalen Objekt. In realen Projekten ist eine logische Isolierung erforderlich – Räume (rooms) und Kanäle.
Betrachten wir ein Beispiel einer Architektur unter Verwendung der Klasse RoomManager:
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; // Speichert den Raum auf dem 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// Verwendung\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});\nDieser Code ermöglicht die Erstellung isolierter Chats oder Spielsitzungen. Beachten Sie die Behandlung der Verbindungsschließung – dies ist entscheidend, um Speicherlecks zu vermeiden.
\n\n2. Skalierung: Clusterbildung und Redis Pub/Sub
\nEin einzelner Node.js-Prozess kann nicht effizient mehr als einen CPU-Kern nutzen. Zur Skalierung verwenden wir das integrierte Modul cluster. Es tritt jedoch ein Problem auf: WebSocket-Verbindungen sind an einen bestimmten Worker gebunden. Wie sendet man eine Nachricht an einen Benutzer, der mit einem anderen Worker verbunden ist?
Die Lösung ist ein externer Nachrichtenbroker, zum Beispiel 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 // Startet Worker entsprechend der Anzahl der Kerne\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(); // Neustart\n });\n} else {\n // Worker\n const server = http.createServer();\n const wss = new Server({ server });\n\n // Verbindung zu Redis herstellen\n const pub = redis.createClient();\n const sub