WebSockets والتطبيقات الفورية
تحسين الأداء
تحسين الأداء
تحسين أداء التطبيقات في الوقت الفعلي أمر بالغ الأهمية لقابلية التوسع وتجربة المستخدم. في هذا الدرس، سنستكشف تقنيات لزيادة الكفاءة في تطبيقات 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;
}
}
تمرين تطبيقي:
- نفذ نقل البيانات الثنائية للعبة ترسل مواقع اللاعبين 60 مرة في الثانية
- أنشئ مجمع اتصالات يدير بكفاءة أكثر من 100 اتصال WebSocket متزامن
- ابنِ لوحة تحكم مراقبة تعرض مقاييس الاتصال في الوقت الفعلي واستخدام النطاق الترددي
- حسّن تطبيق دردشة عن طريق تنفيذ تسلسل MessagePack والضغط
- نفذ معالجة الضغط الخلفي لتطبيق بث البيانات