Node.js و Express

الاتصال في الوقت الفعلي باستخدام Socket.io

45 دقيقة الدرس 21 من 40

الاتصال في الوقت الفعلي باستخدام Socket.io

Socket.io هي مكتبة قوية تتيح الاتصال في الوقت الفعلي ثنائي الاتجاه والقائم على الأحداث بين المتصفح والخادم. تبني على WebSockets وتوفر آليات احتياطية للمتصفحات القديمة، مما يجعلها الحل الأمثل لميزات الوقت الفعلي مثل تطبيقات الدردشة والإشعارات المباشرة والتحرير التعاوني ولوحات المعلومات في الوقت الفعلي.

فهم WebSockets

قبل الغوص في Socket.io، دعنا نفهم WebSockets:

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

فوائد WebSocket:

  • اتصال ثنائي الاتجاه في الوقت الفعلي
  • زمن وصول أقل مقارنة بالاستطلاع
  • تقليل حمل الخادم (لا يوجد مصافحات HTTP متكررة)
  • مثالية للدردشة والألعاب والتحديثات المباشرة والأدوات التعاونية

تثبيت وإعداد Socket.io

قم بتثبيت Socket.io في تطبيق Express الخاص بك:

npm install socket.io

خادم Express + Socket.io الأساسي:

// server.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "http://localhost:3000", methods: ["GET", "POST"] } }); // تقديم الملفات الثابتة app.use(express.static('public')); // حدث اتصال Socket.io io.on('connection', (socket) => { console.log('عميل جديد متصل:', socket.id); // الاستماع للأحداث المخصصة socket.on('message', (data) => { console.log('رسالة مستلمة:', data); // بث لجميع العملاء io.emit('message', data); }); // معالجة قطع الاتصال socket.on('disconnect', () => { console.log('عميل منفصل:', socket.id); }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`الخادم يعمل على المنفذ ${PORT}`); });
مهم: استخدم http.createServer(app) ومرره إلى Socket.io. لا تستخدم app.listen() مباشرة عند دمج Socket.io مع Express.

إعداد Socket.io من جانب العميل

أنشئ عميل HTML أساسي:

<!-- public/index.html --> <!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>دردشة Socket.io</title> <style> #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 8px; margin-bottom: 5px; background: #f0f0f0; } </style> </head> <body> <h1>دردشة في الوقت الفعلي</h1> <ul id="messages"></ul> <form id="messageForm"> <input id="messageInput" autocomplete="off" /> <button>إرسال</button> </form> <script src="/socket.io/socket.io.js"></script> <script> const socket = io(); // الاستماع للرسائل من الخادم socket.on('message', (data) => { const item = document.createElement('li'); item.textContent = data; document.getElementById('messages').appendChild(item); window.scrollTo(0, document.body.scrollHeight); }); // إرسال رسالة عند إرسال النموذج document.getElementById('messageForm').addEventListener('submit', (e) => { e.preventDefault(); const input = document.getElementById('messageInput'); if (input.value) { socket.emit('message', input.value); input.value = ''; } }); </script> </body> </html>

أحداث Socket.io

يستخدم Socket.io أحداثاً مخصصة للاتصال. يمكنك إصدار والاستماع لأي اسم حدث:

// جانب الخادم io.on('connection', (socket) => { // الاستماع للأحداث المخصصة socket.on('chat message', (msg) => { console.log('رسالة دردشة:', msg); }); socket.on('user typing', (username) => { socket.broadcast.emit('user typing', username); }); socket.on('user stopped typing', (username) => { socket.broadcast.emit('user stopped typing', username); }); // إرسال حدث لعميل محدد socket.emit('welcome', 'مرحباً بك في الدردشة!'); // إرسال لجميع العملاء بما في ذلك المرسل io.emit('user joined', 'انضم مستخدم'); // إرسال لجميع العملاء باستثناء المرسل socket.broadcast.emit('user joined', 'انضم مستخدم'); });

معالجة الأحداث من جانب العميل:

// جانب العميل const socket = io(); // إصدار أحداث socket.emit('chat message', 'مرحباً بالعالم!'); socket.emit('user typing', 'أحمد'); // الاستماع للأحداث socket.on('welcome', (message) => { console.log(message); }); socket.on('user joined', (message) => { console.log(message); }); socket.on('user typing', (username) => { console.log(`${username} يكتب...`); });

الغرف والمساحات الاسمية

الغرف تسمح لك بتجميع المقابس معاً وبث الرسائل إلى مجموعات محددة:

// الانضمام إلى غرفة socket.join('room1'); // مغادرة غرفة socket.leave('room1'); // إرسال لجميع العملاء في غرفة io.to('room1').emit('message', 'مرحباً room1!'); // إرسال لغرف متعددة io.to('room1').to('room2').emit('message', 'مرحباً لكلا الغرفتين!'); // الحصول على جميع الغرف التي يوجد بها المقبس console.log(socket.rooms); // مثال: تنفيذ غرفة دردشة io.on('connection', (socket) => { socket.on('join room', (roomName) => { socket.join(roomName); socket.to(roomName).emit('user joined', `انضم المستخدم ${socket.id} إلى ${roomName}`); }); socket.on('leave room', (roomName) => { socket.leave(roomName); socket.to(roomName).emit('user left', `غادر المستخدم ${socket.id} من ${roomName}`); }); socket.on('room message', ({ room, message }) => { io.to(room).emit('room message', { user: socket.id, message: message, timestamp: Date.now() }); }); });
نصيحة: كل مقبس ينضم تلقائياً إلى غرفة محددة بـ socket.id. يمكنك استخدام io.to(socket.id).emit() لإرسال رسائل إلى مقبس محدد.

المساحات الاسمية تسمح لك بتقسيم منطق التطبيق عبر نقاط نهاية مختلفة:

// جانب الخادم - إنشاء مساحات اسمية const chatNamespace = io.of('/chat'); const adminNamespace = io.of('/admin'); chatNamespace.on('connection', (socket) => { console.log('اتصل المستخدم بمساحة الدردشة الاسمية'); socket.on('chat message', (msg) => { chatNamespace.emit('chat message', msg); }); }); adminNamespace.on('connection', (socket) => { console.log('اتصل المسؤول بمساحة الإدارة الاسمية'); socket.on('admin command', (command) => { adminNamespace.emit('command result', `تم التنفيذ: ${command}`); }); }); // جانب العميل - الاتصال بمساحة اسمية محددة const chatSocket = io('http://localhost:3000/chat'); const adminSocket = io('http://localhost:3000/admin'); chatSocket.on('chat message', (msg) => { console.log('دردشة:', msg); }); adminSocket.on('command result', (result) => { console.log('إدارة:', result); });

بث الرسائل

يوفر Socket.io طرقاً متعددة لبث الرسائل:

io.on('connection', (socket) => { // إرسال للمقبس الحالي فقط socket.emit('message', 'فقط لك'); // إرسال لجميع العملاء بما في ذلك المرسل io.emit('message', 'للجميع'); // إرسال لجميع العملاء باستثناء المرسل socket.broadcast.emit('message', 'للجميع باستثناء المرسل'); // إرسال لجميع العملاء في room1 باستثناء المرسل socket.to('room1').emit('message', 'لـ room1 باستثناء المرسل'); // إرسال لجميع العملاء في room1 بما في ذلك المرسل io.to('room1').emit('message', 'لـ room1 بما في ذلك المرسل'); // إرسال لجميع العملاء في المساحة الاسمية io.of('/chat').emit('message', 'لجميع في مساحة /chat الاسمية'); // إرسال لمقبس محدد بالمعرف io.to(socketId).emit('message', 'لمقبس محدد'); // ربط غرف متعددة socket.to('room1').to('room2').to('room3').emit('message', 'لغرف متعددة'); });

بناء تطبيق دردشة كامل

إليك تطبيق دردشة جاهز للإنتاج مع الغرف ومؤشرات الكتابة وإدارة المستخدمين:

// chat-server.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server); app.use(express.static('public')); // تخزين المستخدمين النشطين const users = new Map(); const rooms = new Map(); io.on('connection', (socket) => { console.log('اتصال جديد:', socket.id); // معالجة انضمام المستخدم socket.on('join', ({ username, room }) => { // تخزين معلومات المستخدم users.set(socket.id, { username, room }); socket.join(room); // تتبع أعضاء الغرفة if (!rooms.has(room)) { rooms.set(room, new Set()); } rooms.get(room).add(socket.id); // رسالة ترحيب للمستخدم socket.emit('message', { user: 'النظام', text: `مرحباً بك في ${room}، ${username}!`, timestamp: Date.now() }); // إخطار الغرفة عن المستخدم الجديد socket.to(room).emit('message', { user: 'النظام', text: `انضم ${username} إلى الغرفة`, timestamp: Date.now() }); // إرسال قائمة المستخدمين المحدثة io.to(room).emit('room users', { room, users: Array.from(rooms.get(room)).map(id => users.get(id)?.username) }); }); // معالجة رسائل الدردشة socket.on('chat message', (message) => { const user = users.get(socket.id); if (user) { io.to(user.room).emit('message', { user: user.username, text: message, timestamp: Date.now() }); } }); // معالجة مؤشر الكتابة socket.on('typing', () => { const user = users.get(socket.id); if (user) { socket.to(user.room).emit('user typing', user.username); } }); socket.on('stop typing', () => { const user = users.get(socket.id); if (user) { socket.to(user.room).emit('user stopped typing', user.username); } }); // معالجة قطع الاتصال socket.on('disconnect', () => { const user = users.get(socket.id); if (user) { const { username, room } = user; // الإزالة من الغرفة if (rooms.has(room)) { rooms.get(room).delete(socket.id); if (rooms.get(room).size === 0) { rooms.delete(room); } } // إزالة المستخدم users.delete(socket.id); // إخطار الغرفة io.to(room).emit('message', { user: 'النظام', text: `غادر ${username} الغرفة`, timestamp: Date.now() }); // تحديث قائمة المستخدمين if (rooms.has(room)) { io.to(room).emit('room users', { room, users: Array.from(rooms.get(room)).map(id => users.get(id)?.username) }); } } }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`خادم الدردشة يعمل على المنفذ ${PORT}`); });

البرمجيات الوسيطة Socket.io

استخدم البرمجيات الوسيطة للمصادقة والتفويض:

// برمجية وسيطة للمصادقة io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) { return next(new Error('المصادقة مطلوبة')); } // التحقق من الرمز (مثال) try { const decoded = jwt.verify(token, process.env.JWT_SECRET); socket.user = decoded; next(); } catch (err) { next(new Error('رمز غير صالح')); } }); // برمجية وسيطة للتسجيل io.use((socket, next) => { console.log('محاولة اتصال المقبس:', { id: socket.id, ip: socket.handshake.address, timestamp: new Date() }); next(); }); // جانب العميل مع المصادقة const socket = io({ auth: { token: 'your-jwt-token' } }); // معالجة أخطاء الاتصال socket.on('connect_error', (err) => { console.error('فشل الاتصال:', err.message); });

تمرين: بناء تطبيق رسم تعاوني في الوقت الفعلي

  1. أنشئ خادم Express مع Socket.io
  2. نفذ لوحة رسم من جانب العميل
  3. بث إحداثيات الرسم في الوقت الفعلي
  4. أضف غرفاً حتى تتمكن مجموعات متعددة من الرسم بشكل منفصل
  5. نفذ ميزة "مسح اللوحة"
  6. أضف اختيار اللون وعناصر التحكم في حجم الفرشاة
  7. اعرض المستخدمين النشطين في كل غرفة

الأداء وأفضل الممارسات

أفضل الممارسات:
  • استخدم الغرف لتحديد نطاق بث الرسائل
  • نفذ إقرارات للرسائل الحرجة
  • قم بإعداد آليات نبض القلب/الاختبار لمراقبة الاتصال
  • استخدم البيانات الثنائية لإرسال الصور/الملفات
  • نفذ تحديد المعدل لمنع فيضان الرسائل
  • قم بتنظيف مستمعي الأحداث عند قطع الاتصال
  • استخدم المساحات الاسمية لفصل اهتمامات التطبيق
// إقرارات للتسليم الموثوق socket.emit('important message', data, (response) => { console.log('أقر الخادم:', response); }); // إقرار جانب الخادم socket.on('important message', (data, callback) => { // معالجة البيانات callback({ status: 'received', timestamp: Date.now() }); }); // مثال تحديد المعدل const messageRates = new Map(); socket.on('chat message', (msg) => { const now = Date.now(); const userRate = messageRates.get(socket.id) || { count: 0, resetTime: now + 60000 }; if (now > userRate.resetTime) { userRate.count = 0; userRate.resetTime = now + 60000; } if (userRate.count >= 10) { socket.emit('rate limit', 'رسائل كثيرة جداً. يرجى الإبطاء.'); return; } userRate.count++; messageRates.set(socket.id, userRate); // معالجة الرسالة io.emit('chat message', msg); });

تجعل Socket.io بناء ميزات الوقت الفعلي واضحاً وموثوقاً. تتعامل مع إعادة الاتصال والاحتياطات والتوافق عبر المتصفحات تلقائياً، مما يسمح لك بالتركيز على بناء تجارب رائعة في الوقت الفعلي.