WebSockets والتطبيقات الفورية

تحسين الأداء

18 دقيقة الدرس 28 من 35

تحسين الأداء

تحسين أداء التطبيقات في الوقت الفعلي أمر بالغ الأهمية لقابلية التوسع وتجربة المستخدم. في هذا الدرس، سنستكشف تقنيات لزيادة الكفاءة في تطبيقات WebSocket و Socket.io.

نقل البيانات الثنائية

نقل البيانات الثنائية أكثر كفاءة بكثير من JSON لأنواع معينة من البيانات، مما يقلل من استخدام النطاق الترددي ويحسن السرعة.

// إرسال البيانات الثنائية مع WebSocket const ws = new WebSocket('ws://localhost:8080'); // إرسال ArrayBuffer const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setInt32(0, 12345); view.setFloat32(4, 67.89); ws.send(buffer); // إرسال Blob (للملفات/الصور) fetch('image.jpg') .then(response => response.blob()) .then(blob => { ws.send(blob); }); // استقبال البيانات الثنائية ws.binaryType = 'arraybuffer'; // أو 'blob' ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { const view = new DataView(event.data); const number = view.getInt32(0); const float = view.getFloat32(4); console.log('مستلم:', number, float); } }; // Socket.io مع البيانات الثنائية const socket = io(); // إرسال ثنائي const uint8Array = new Uint8Array([1, 2, 3, 4, 5]); socket.emit('binary-data', uint8Array); // استقبال ثنائي socket.on('image-data', (buffer) => { const blob = new Blob([buffer], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); document.getElementById('img').src = url; });
نصيحة الأداء: البيانات الثنائية أصغر بنسبة 30-50٪ من JSON المكافئ للبيانات الرقمية. استخدمها لحالة اللعبة، وبيانات المستشعر، والصور، والصوت، وتدفقات الفيديو.

ضغط الرسائل (perMessageDeflate)

يقلل ضغط الرسائل من استخدام النطاق الترددي عن طريق ضغط إطارات WebSocket تلقائيًا.

// خادم WebSocket مع الضغط const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080, perMessageDeflate: { zlibDeflateOptions: { chunkSize: 1024, memLevel: 7, level: 3 // مستوى الضغط (0-9) }, zlibInflateOptions: { chunkSize: 10 * 1024 }, clientNoContextTakeover: true, serverNoContextTakeover: true, serverMaxWindowBits: 10, concurrencyLimit: 10, threshold: 1024 // ضغط الرسائل فقط > 1KB } }); // Socket.io مع الضغط const io = require('socket.io')(3000, { perMessageDeflate: { threshold: 1024, zlibDeflateOptions: { chunkSize: 8 * 1024, level: 6 } } }); // تعطيل الضغط لرسائل محددة socket.compress(false).emit('small-message', 'hello'); socket.compress(true).emit('large-message', largeDataObject);
// معيار: فعالية الضغط const originalData = { users: Array(1000).fill(null).map((_, i) => ({ id: i, name: `User ${i}`, email: `user${i}@example.com`, timestamp: Date.now() })) }; const jsonSize = JSON.stringify(originalData).length; console.log(`حجم JSON الأصلي: ${(jsonSize / 1024).toFixed(2)} KB`); // مع الضغط، التخفيض النموذجي: 60-80٪ // الحجم المضغوط: ~8-16 KB (من ~40 KB)

تجميع الاتصالات

يدير تجميع الاتصالات اتصالات WebSocket بكفاءة، وإعادة استخدام الاتصالات ومنع استنفاد الموارد.

// مجمع اتصال WebSocket class WebSocketPool { constructor(url, poolSize = 5) { this.url = url; this.poolSize = poolSize; this.connections = []; this.availableConnections = []; this.pendingRequests = []; this.initialize(); } initialize() { for (let i = 0; i < this.poolSize; i++) { this.createConnection(); } } createConnection() { const ws = new WebSocket(this.url); ws.onopen = () => { this.availableConnections.push(ws); this.processQueue(); }; ws.onclose = () => { // إزالة من الاتصالات المتاحة const index = this.availableConnections.indexOf(ws); if (index > -1) { this.availableConnections.splice(index, 1); } // إعادة إنشاء الاتصال setTimeout(() => this.createConnection(), 1000); }; this.connections.push(ws); } async send(data) { return new Promise((resolve, reject) => { const request = { data, resolve, reject }; this.pendingRequests.push(request); this.processQueue(); }); } processQueue() { while (this.pendingRequests.length > 0 && this.availableConnections.length > 0) { const request = this.pendingRequests.shift(); const ws = this.availableConnections.shift(); ws.send(JSON.stringify(request.data)); // إرجاع الاتصال إلى المجمع بعد تأخير setTimeout(() => { this.availableConnections.push(ws); request.resolve(); }, 100); } } close() { this.connections.forEach(ws => ws.close()); } } // الاستخدام const pool = new WebSocketPool('ws://localhost:8080', 10); for (let i = 0; i < 100; i++) { pool.send({ message: `Message ${i}` }); }

إدارة الذاكرة

تمنع إدارة الذاكرة المناسبة تسرب الذاكرة وتضمن اتصالات طويلة المدى مستقرة.

// معالجة الرسائل بكفاءة في استخدام الذاكرة class OptimizedSocketServer { constructor() { this.connections = new Map(); // استخدم Map لعمليات O(1) this.messageBuffer = new Map(); this.MAX_BUFFER_SIZE = 1000; this.BUFFER_CLEANUP_INTERVAL = 60000; // دقيقة واحدة // تنظيف دوري setInterval(() => this.cleanupBuffers(), this.BUFFER_CLEANUP_INTERVAL); } addConnection(socket) { this.connections.set(socket.id, { socket, joinedAt: Date.now(), messageCount: 0, lastActivity: Date.now() }); // إعداد المستمعين مع التنظيف المناسب const messageHandler = this.handleMessage.bind(this, socket.id); const disconnectHandler = this.handleDisconnect.bind(this, socket.id); socket.on('message', messageHandler); socket.on('disconnect', disconnectHandler); // تخزين المعالجات للتنظيف socket._handlers = { messageHandler, disconnectHandler }; } handleMessage(socketId, data) { const connection = this.connections.get(socketId); if (!connection) return; connection.messageCount++; connection.lastActivity = Date.now(); // استخدم مخزن دائري لتاريخ الرسائل let buffer = this.messageBuffer.get(socketId); if (!buffer) { buffer = []; this.messageBuffer.set(socketId, buffer); } buffer.push({ data, timestamp: Date.now() }); // احتفظ فقط بالرسائل الحديثة if (buffer.length > this.MAX_BUFFER_SIZE) { buffer.shift(); } } handleDisconnect(socketId) { const connection = this.connections.get(socketId); if (!connection) return; // إزالة مستمعي الأحداث const socket = connection.socket; if (socket._handlers) { socket.removeListener('message', socket._handlers.messageHandler); socket.removeListener('disconnect', socket._handlers.disconnectHandler); delete socket._handlers; } // تنظيف هياكل البيانات this.connections.delete(socketId); this.messageBuffer.delete(socketId); } cleanupBuffers() { const now = Date.now(); const INACTIVE_THRESHOLD = 300000; // 5 دقائق for (const [socketId, connection] of this.connections.entries()) { if (now - connection.lastActivity > INACTIVE_THRESHOLD) { // مسح المخازن غير النشطة this.messageBuffer.delete(socketId); } } // تلميح جمع القمامة القسري if (global.gc) { global.gc(); } } getMemoryUsage() { const usage = process.memoryUsage(); return { heapUsed: (usage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', heapTotal: (usage.heapTotal / 1024 / 1024).toFixed(2) + ' MB', connections: this.connections.size, bufferedMessages: Array.from(this.messageBuffer.values()) .reduce((sum, buf) => sum + buf.length, 0) }; } }
نصائح تحسين الذاكرة:
  • استخدم WeakMap للتخزين المؤقت عندما يكون ذلك ممكنًا
  • نفذ المخازن الدائرية لتاريخ الرسائل
  • أزل مستمعي الأحداث عند قطع الاتصال
  • حدد حدودًا للبيانات المخزنة لكل اتصال
  • استخدم البث المباشر لنقل البيانات الكبيرة
  • راقب استخدام الذاكرة مع process.memoryUsage()

التسلسل الفعال

اختيار تنسيق التسلسل الصحيح يؤثر بشكل كبير على الأداء.

// مقارنة التسلسل const data = { userId: 12345, timestamp: Date.now(), position: { x: 100.5, y: 200.3, z: 50.1 }, health: 85, inventory: [1, 2, 3, 4, 5] }; // 1. JSON (خط الأساس) const jsonStr = JSON.stringify(data); console.log('حجم JSON:', jsonStr.length); // ~120 بايت // 2. MessagePack (JSON ثنائي) const msgpack = require('msgpack-lite'); const msgpackBuffer = msgpack.encode(data); console.log('حجم MessagePack:', msgpackBuffer.length); // ~60 بايت (أصغر بنسبة 50٪) // 3. Protocol Buffers (تحديد المخطط) const protobuf = require('protobufjs'); // تحميل مخطط .proto const root = protobuf.loadSync('game.proto'); const GameState = root.lookupType('GameState'); // الترميز const message = GameState.create(data); const buffer = GameState.encode(message).finish(); console.log('حجم Protobuf:', buffer.length); // ~40 بايت (أصغر بنسبة 67٪) // 4. بروتوكول ثنائي مخصص (الأكثر كفاءة لحالات استخدام محددة) function encodeGameState(state) { const buffer = new ArrayBuffer(28); const view = new DataView(buffer); view.setUint32(0, state.userId); view.setFloat64(4, state.timestamp); view.setFloat32(12, state.position.x); view.setFloat32(16, state.position.y); view.setFloat32(20, state.position.z); view.setUint8(24, state.health); // ... ترميز المخزون return buffer; } const customBuffer = encodeGameState(data); console.log('حجم الثنائي المخصص:', customBuffer.byteLength); // ~28 بايت
// استخدام MessagePack مع Socket.io const io = require('socket.io')(3000); const msgpack = require('msgpack-lite'); // جانب الخادم io.on('connection', (socket) => { socket.on('game-update', (buffer) => { const data = msgpack.decode(new Uint8Array(buffer)); // معالجة تحديث اللعبة // البث للآخرين const encoded = msgpack.encode(data); socket.broadcast.emit('game-update', encoded); }); }); // جانب العميل const socket = io(); function sendGameUpdate(state) { const encoded = msgpack.encode(state); socket.emit('game-update', encoded.buffer); } socket.on('game-update', (buffer) => { const state = msgpack.decode(new Uint8Array(buffer)); updateGameState(state); });

مراقبة عدد الاتصالات

تتبع وتحسين مقاييس الاتصال لإدارة أفضل للموارد.

// مراقبة الاتصال الشاملة class ConnectionMonitor { constructor(io) { this.io = io; this.metrics = { totalConnections: 0, activeConnections: 0, peakConnections: 0, connectionsByRoom: new Map(), bandwidthUsage: { sent: 0, received: 0 }, messageCount: 0, errorCount: 0 }; this.setupMonitoring(); this.startReporting(); } setupMonitoring() { this.io.on('connection', (socket) => { this.metrics.totalConnections++; this.metrics.activeConnections++; if (this.metrics.activeConnections > this.metrics.peakConnections) { this.metrics.peakConnections = this.metrics.activeConnections; } // مراقبة الرسائل socket.use((packet, next) => { this.metrics.messageCount++; // تقدير النطاق الترددي const size = JSON.stringify(packet).length; this.metrics.bandwidthUsage.received += size; next(); }); // تتبع عضويات الغرفة socket.on('join-room', (roomId) => { const count = this.metrics.connectionsByRoom.get(roomId) || 0; this.metrics.connectionsByRoom.set(roomId, count + 1); }); socket.on('leave-room', (roomId) => { const count = this.metrics.connectionsByRoom.get(roomId) || 0; this.metrics.connectionsByRoom.set(roomId, Math.max(0, count - 1)); }); socket.on('error', () => { this.metrics.errorCount++; }); socket.on('disconnect', () => { this.metrics.activeConnections--; }); }); } startReporting() { setInterval(() => { const report = this.generateReport(); console.log('=== مقاييس WebSocket ===\"); console.log(report); // إعادة تعيين العدادات this.metrics.messageCount = 0; this.metrics.bandwidthUsage = { sent: 0, received: 0 }; }, 60000); // كل دقيقة } generateReport() { const memUsage = process.memoryUsage(); return { connections: { active: this.metrics.activeConnections, total: this.metrics.totalConnections, peak: this.metrics.peakConnections }, rooms: Object.fromEntries(this.metrics.connectionsByRoom), performance: { messagesPerMinute: this.metrics.messageCount, bandwidthKBps: { sent: (this.metrics.bandwidthUsage.sent / 1024 / 60).toFixed(2), received: (this.metrics.bandwidthUsage.received / 1024 / 60).toFixed(2) }, errors: this.metrics.errorCount }, memory: { heapUsed: (memUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', heapTotal: (memUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB' } }; } getMetrics() { return this.metrics; } } // الاستخدام const monitor = new ConnectionMonitor(io); // عرض نقطة نهاية المقاييس app.get('/metrics', (req, res) => { res.json(monitor.generateReport()); });

معالجة الضغط الخلفي

تعامل مع الضغط الخلفي عند إرسال البيانات بشكل أسرع مما يمكن للعملاء استلامه.

// إرسال واعٍ بالضغط الخلفي class BackpressureSocket { constructor(socket) { this.socket = socket; this.queue = []; this.sending = false; this.maxQueueSize = 100; } async send(data) { return new Promise((resolve, reject) => { if (this.queue.length >= this.maxQueueSize) { reject(new Error('قائمة انتظار الإرسال ممتلئة')); return; } this.queue.push({ data, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.sending || this.queue.length === 0) { return; } this.sending = true; while (this.queue.length > 0) { const item = this.queue.shift(); try { // التحقق مما إذا كان مخزن المقبس ممتلئًا if (this.socket.bufferedAmount > 1024 * 1024) { // 1MB // انتظر حتى يستنزف المخزن await this.waitForDrain(); } this.socket.send(JSON.stringify(item.data)); item.resolve(); } catch (error) { item.reject(error); } } this.sending = false; } waitForDrain() { return new Promise((resolve) => { const checkBuffer = () => { if (this.socket.bufferedAmount < 512 * 1024) { // 512KB resolve(); } else { setTimeout(checkBuffer, 100); } }; checkBuffer(); }); } } // الاستخدام const bpSocket = new BackpressureSocket(ws); for (let i = 0; i < 1000; i++) { try { await bpSocket.send({ message: `Data ${i}` }); } catch (error) { console.error('فشل الإرسال:', error); break; } }
تمرين تطبيقي:
  1. نفذ نقل البيانات الثنائية للعبة ترسل مواقع اللاعبين 60 مرة في الثانية
  2. أنشئ مجمع اتصالات يدير بكفاءة أكثر من 100 اتصال WebSocket متزامن
  3. ابنِ لوحة تحكم مراقبة تعرض مقاييس الاتصال في الوقت الفعلي واستخدام النطاق الترددي
  4. حسّن تطبيق دردشة عن طريق تنفيذ تسلسل MessagePack والضغط
  5. نفذ معالجة الضغط الخلفي لتطبيق بث البيانات