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

توسيع تطبيقات WebSocket

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

مقدمة في توسيع WebSocket

مع نمو تطبيقك في الوقت الفعلي، لن يتمكن خادم واحد من التعامل مع جميع الاتصالات المتزامنة. يستكشف هذا الدرس كيفية توسيع تطبيقات WebSocket أفقياً عبر خوادم متعددة مع الحفاظ على الوظائف في الوقت الفعلي.

تحديات التوسع

تواجه تطبيقات WebSocket تحديات توسع فريدة:

  • الاتصالات ذات الحالة: يحتفظ كل عميل باتصال دائم بخادم معين
  • بث الرسائل: يجب أن تصل الأحداث إلى العملاء المتصلين بخوادم مختلفة
  • ارتباط الجلسة: يجب أن يعيد العملاء الاتصال بنفس مثيل الخادم
  • الحالة المشتركة: يجب مزامنة حالة التطبيق عبر المثيلات
مفهوم أساسي: على عكس تطبيقات HTTP عديمة الحالة، تحتفظ خوادم WebSocket بالاتصالات النشطة، مما يجعل التوسع الأفقي أكثر تعقيداً.

بنية التوسع الأفقي

تتضمن بنية WebSocket الموسعة النموذجية:

┌─────────┐ │ العملاء │ └────┬────┘ │ ┌────▼──────────┐ │ موزع الحمل │ (جلسات لاصقة) └────┬──────────┘ │ ┌────▼────┬──────────┬──────────┐ │ خادم1 │ خادم2 │ خادم3 │ └────┬────┴─────┬────┴─────┬────┘ │ │ │ └──────┬───┴──────┬───┘ │ │ ┌────▼──────────▼────┐ │ Redis Pub/Sub │ (ناقل الرسائل) └─────────────────────┘

الجلسات اللاصقة (ارتباط الجلسة)

تضمن الجلسات اللاصقة إعادة اتصال العملاء بنفس الخادم:

// تكوين Nginx للجلسات اللاصقة upstream websocket_servers { ip_hash; # التوجيه بناءً على IP العميل server 192.168.1.10:3000; server 192.168.1.11:3000; server 192.168.1.12:3000; } server { location /socket.io/ { proxy_pass http://websocket_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } }
مهم: قد لا تعمل الجلسات اللاصقة المستندة إلى IP بشكل مثالي مع العملاء خلف NAT أو باستخدام شبكات الهاتف المحمول مع IP متغير.

محول Redis لـ Socket.io

يمكّن محول Redis لـ Socket.io من بث الرسائل عبر خوادم متعددة:

// تثبيت التبعيات // npm install @socket.io/redis-adapter redis const { Server } = require('socket.io'); const { createAdapter } = require('@socket.io/redis-adapter'); const { createClient } = require('redis'); const io = new Server(3000); // إنشاء عملاء Redis (pub و sub) const pubClient = createClient({ host: 'localhost', port: 6379 }); const subClient = pubClient.duplicate(); // الاتصال بعملاء Redis Promise.all([pubClient.connect(), subClient.connect()]).then(() => { // إرفاق محول Redis بـ Socket.io io.adapter(createAdapter(pubClient, subClient)); console.log('تم الاتصال بمحول Redis'); }); // سيصل هذا البث إلى جميع الخوادم io.emit('message', 'مرحباً من الخادم 1');

كيف يعمل محول Redis

يستخدم المحول Redis Pub/Sub لمزامنة الرسائل:

  1. يتلقى الخادم 1 رسالة ويستدعي io.emit()
  2. ينشر المحول الرسالة إلى Redis
  3. تشترك جميع مثيلات الخادم (بما في ذلك الخادم 1) في قنوات Redis
  4. يتلقى كل خادم الرسالة ويبثها إلى عملائه المحليين
  5. يتلقى جميع العملاء عبر جميع الخوادم الرسالة
أفضل ممارسة: استخدم Redis لمزامنة الرسائل وقاعدة بيانات منفصلة (مثل PostgreSQL أو MongoDB) لتخزين البيانات الدائمة.

مثال كامل متعدد الخوادم

// server.js - قم بتشغيل مثيلات متعددة على منافذ مختلفة const express = require('express'); const { Server } = require('socket.io'); const { createAdapter } = require('@socket.io/redis-adapter'); const { createClient } = require('redis'); const app = express(); const PORT = process.env.PORT || 3000; const server = app.listen(PORT); const io = new Server(server, { cors: { origin: '*' } }); // إعداد Redis const pubClient = createClient({ url: 'redis://localhost:6379' }); const subClient = pubClient.duplicate(); Promise.all([pubClient.connect(), subClient.connect()]).then(() => { io.adapter(createAdapter(pubClient, subClient)); console.log(`الخادم يعمل على المنفذ ${PORT} مع محول Redis`); }); // تتبع المستخدمين المتصلين عبر جميع الخوادم const connectedUsers = new Map(); io.on('connection', (socket) => { console.log(`تم اتصال العميل بالخادم على المنفذ ${PORT}`); socket.on('register', (username) => { socket.username = username; connectedUsers.set(socket.id, username); // البث إلى جميع الخوادم io.emit('user_joined', { username, serverPort: PORT, totalUsers: io.engine.clientsCount }); }); socket.on('message', (data) => { // يصل هذا إلى العملاء على جميع الخوادم io.emit('message', { username: socket.username, message: data.message, serverPort: PORT, timestamp: new Date() }); }); socket.on('disconnect', () => { const username = connectedUsers.get(socket.id); connectedUsers.delete(socket.id); io.emit('user_left', { username, serverPort: PORT }); }); });

تشغيل مثيلات خادم متعددة

# الطرفية 1 PORT=3000 node server.js # الطرفية 2 PORT=3001 node server.js # الطرفية 3 PORT=3002 node server.js

إدارة الحالة المشتركة

للحالة المشتركة عبر الخوادم، استخدم Redis للوصول السريع:

const redis = require('redis'); const stateClient = redis.createClient({ url: 'redis://localhost:6379' }); await stateClient.connect(); // تخزين حالة المستخدم عبر الإنترنت io.on('connection', async (socket) => { const userId = socket.handshake.auth.userId; // تعيين المستخدم كمتصل في Redis await stateClient.hSet('users:online', userId, JSON.stringify({ socketId: socket.id, serverPort: PORT, connectedAt: new Date() })); // الحصول على جميع المستخدمين المتصلين const onlineUsers = await stateClient.hGetAll('users:online'); socket.emit('online_users', Object.values(onlineUsers).map(JSON.parse)); socket.on('disconnect', async () => { await stateClient.hDel('users:online', userId); }); });

اعتبارات موازنة الحمل

متطلبات موزع الحمل:
  • يجب أن يدعم بروتوكول WebSocket (ترقية HTTP/1.1)
  • يجب تنفيذ الجلسات اللاصقة (تجزئة IP أو استناداً إلى ملفات تعريف الارتباط)
  • فحوصات صحة نقاط نهاية WebSocket
  • تكوين مهلة مناسب (WebSockets طويلة الأمد)

وضع العنقود PM2

استخدم PM2 لإدارة مثيلات Node.js متعددة:

// ecosystem.config.js module.exports = { apps: [{ name: 'websocket-app', script: './server.js', instances: 4, // 4 مثيلات exec_mode: 'cluster', env: { NODE_ENV: 'production' } }] }; // البدء مع PM2 // pm2 start ecosystem.config.js
تحذير: وضع العنقود PM2 وحده لا يتعامل مع توسيع WebSocket. لا يزال تحتاج إلى محول Redis للاتصال عبر المثيلات.

مراقبة التطبيقات الموسعة

// إضافة المراقبة إلى كل مثيل خادم const os = require('os'); setInterval(() => { const metrics = { serverPort: PORT, connectedClients: io.engine.clientsCount, memoryUsage: process.memoryUsage(), cpuUsage: os.loadavg(), uptime: process.uptime() }; // إرسال إلى خدمة المراقبة أو السجل console.log('مقاييس الخادم:', metrics); // اختيارياً النشر إلى Redis للمراقبة المركزية pubClient.publish('server:metrics', JSON.stringify(metrics)); }, 30000); // كل 30 ثانية
تمرين:
  1. قم بإعداد Redis على جهازك المحلي
  2. أنشئ خادم Socket.io مع محول Redis
  3. قم بتشغيل 3 مثيلات من الخادم على منافذ مختلفة (3000، 3001، 3002)
  4. أنشئ عميل دردشة بسيط يتصل بأي مثيل
  5. تحقق من أن الرسائل المرسلة إلى خادم واحد تصل إلى العملاء على خوادم أخرى
  6. راقب الخادم الذي يتصل به كل عميل

ملخص

يتطلب توسيع تطبيقات WebSocket:

  • الجلسات اللاصقة للحفاظ على ارتباط العميل-الخادم
  • محول Redis لبث الرسائل عبر الخوادم
  • إدارة الحالة المشتركة باستخدام Redis أو ما شابه
  • تكوين موزع حمل مناسب
  • مديري العمليات مثل PM2 لإدارة المثيلات
  • المراقبة وفحوصات الصحة