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

أنماط البث والمراسلة

20 دقيقة الدرس 10 من 35

أنماط البث والمراسلة

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

أنماط الإرسال الأساسية

إليك الطرق الأساسية لإرسال الأحداث في Socket.io:

// الخادم io.on('connection', (socket) => { // 1. الإرسال لهذا السوكيت فقط socket.emit('message', 'فقط أنت تستقبل هذا'); // 2. الإرسال لجميع السوكيتات (بما في ذلك المرسل) io.emit('message', 'الجميع يستقبل هذا'); // 3. الإرسال لجميع السوكيتات عدا المرسل socket.broadcast.emit('message', 'الجميع عدا المرسل'); // 4. الإرسال لغرفة محددة io.to('roomName').emit('message', 'أعضاء الغرفة فقط'); // 5. الإرسال للغرفة عدا المرسل socket.to('roomName').emit('message', 'أعضاء الغرفة عدا المرسل'); // 6. الإرسال لغرف متعددة io.to('room1').to('room2').emit('message', 'غرف متعددة'); // 7. الإرسال لسوكيت بمعرفه io.to(socketId).emit('message', 'سوكيت محدد بالمعرف'); });
ملاحظة: socket.emit() ترسل لعميل واحد، io.emit() ترسل لجميع العملاء، و socket.broadcast.emit() ترسل للجميع عدا المرسل.

البث لجميع العملاء

إرسال رسالة لكل عميل متصل:

// الخادم io.on('connection', (socket) => { // بث إعلان الخادم للجميع io.emit('announcement', { type: 'info', message: 'صيانة الخادم خلال 5 دقائق', timestamp: new Date() }); // تحديث عدد المستخدمين للجميع const updateUserCount = () => { io.emit('userCount', io.engine.clientsCount); }; updateUserCount(); // الإرسال عندما يتصل شخص ما socket.on('disconnect', () => { updateUserCount(); // الإرسال عندما يقطع شخص ما الاتصال }); }); // العميل socket.on('announcement', (data) => { console.log(`[${data.type}] ${data.message}`); alert(data.message); }); socket.on('userCount', (count) => { document.getElementById('user-count').textContent = count; });

البث للجميع عدا المرسل

إخطار المستخدمين الآخرين دون إخطار نفسك:

// الخادم io.on('connection', (socket) => { // إخطار الآخرين عند انضمام المستخدم socket.broadcast.emit('userJoined', { userId: socket.id, timestamp: new Date() }); // مؤشر الكتابة socket.on('typing', (isTyping) => { socket.broadcast.emit('userTyping', { userId: socket.id, isTyping: isTyping }); }); // إشعار إجراء المستخدم socket.on('userAction', (action) => { socket.broadcast.emit('notification', { userId: socket.id, action: action, timestamp: new Date() }); }); }); // العميل socket.on('userJoined', (data) => { console.log(`المستخدم ${data.userId} انضم`); }); socket.on('userTyping', (data) => { if (data.isTyping) { showTypingIndicator(data.userId); } else { hideTypingIndicator(data.userId); } });

البث لغرف محددة

إرسال رسائل للمستخدمين في غرفة محددة أو غرف متعددة:

// الخادم io.on('connection', (socket) => { // الانضمام لغرفة socket.on('joinRoom', (roomName) => { socket.join(roomName); // الإرسال لهذا المستخدم socket.emit('joinedRoom', roomName); // الإرسال للجميع في الغرفة (بما في ذلك المرسل) io.to(roomName).emit('roomUpdate', { message: `المستخدم انضم إلى ${roomName}`, userCount: io.sockets.adapter.rooms.get(roomName)?.size || 0 }); // الإرسال للجميع في الغرفة عدا المرسل socket.to(roomName).emit('notification', { message: 'مستخدم جديد انضم للغرفة' }); }); // إرسال رسالة لغرفة محددة socket.on('roomMessage', (roomName, message) => { // البث للجميع في الغرفة (بما في ذلك المرسل) io.to(roomName).emit('roomMessage', { userId: socket.id, room: roomName, message: message, timestamp: new Date() }); }); // الإرسال لغرف متعددة socket.on('multiRoomMessage', (rooms, message) => { let emitter = io; rooms.forEach(room => { emitter = emitter.to(room); }); emitter.emit('message', message); }); });

المراسلة الخاصة (الرسائل المباشرة)

إرسال رسالة لمستخدم محدد بمعرف السوكيت الخاص به:

// الخادم const users = new Map(); // تتبع معرف السوكيت إلى تعيين اسم المستخدم io.on('connection', (socket) => { // تسجيل المستخدم socket.on('register', (username) => { users.set(socket.id, username); socket.username = username; }); // رسالة خاصة بمعرف السوكيت socket.on('privateMessage', (targetSocketId, message) => { // الإرسال للمستقبل io.to(targetSocketId).emit('privateMessage', { from: socket.id, fromUsername: socket.username, message: message, timestamp: new Date() }); // إرسال تأكيد للمرسل socket.emit('messageSent', { to: targetSocketId, message: message, timestamp: new Date() }); }); // رسالة خاصة باسم المستخدم socket.on('sendToUser', (username, message) => { // البحث عن معرف السوكيت باسم المستخدم let targetSocketId = null; for (const [socketId, user] of users.entries()) { if (user === username) { targetSocketId = socketId; break; } } if (targetSocketId) { io.to(targetSocketId).emit('privateMessage', { from: socket.id, fromUsername: socket.username, message: message }); } else { socket.emit('error', `المستخدم ${username} غير موجود`); } }); socket.on('disconnect', () => { users.delete(socket.id); }); }); // العميل socket.emit('register', 'أحمد'); // إرسال رسالة خاصة socket.emit('privateMessage', 'target-socket-id', 'مرحبًا بشكل خاص!'); // استقبال رسالة خاصة socket.on('privateMessage', (data) => { console.log(`رسالة خاصة من ${data.fromUsername}: ${data.message}`); });

ورقة الغش للإرسال

مرجع سريع لجميع أنماط الإرسال:

// === الأنماط الأساسية === socket.emit('event', data) // → للمرسل فقط io.emit('event', data) // → لجميع العملاء socket.broadcast.emit('event', data) // → للجميع عدا المرسل // === أنماط الغرف === io.to('room').emit('event', data) // → للجميع في الغرفة socket.to('room').emit('event', data) // → للغرفة عدا المرسل io.in('room').emit('event', data) // → للجميع في الغرفة (نفس .to()) // === غرف متعددة === io.to('room1').to('room2').emit('event', data) // → لـ room1 و room2 // === رسالة خاصة === io.to(socketId).emit('event', data) // → لسوكيت محدد // === فضاء اسم === io.of('/namespace').emit('event', data) // → للجميع في فضاء الاسم // === متطاير (قد يضيع) === socket.volatile.emit('event', data) // → قد يسقط إذا لم يكن جاهزًا // === مع إقرار === socket.emit('event', data, (response) => {}) // → مع رد استدعاء
نصيحة: احفظ ورقة الغش هذه كتعليق في الكود الخاص بك. من السهل نسيان نمط الإرسال الذي يجب استخدامه في سيناريوهات مختلفة!

أنماط إقرار الرسائل

اطلب تأكيدًا بأن الرسالة قد استلمت:

// الخادم io.on('connection', (socket) => { // النمط 1: العميل يطلب، الخادم يقر socket.on('sendMessage', (message, callback) => { // التحقق من الرسالة if (!message || message.length === 0) { callback({ success: false, error: 'رسالة فارغة' }); return; } // بث الرسالة io.emit('message', { from: socket.id, message: message, timestamp: new Date() }); // إقرار النجاح callback({ success: true, messageId: Date.now() }); }); // النمط 2: الخادم يطلب، العميل يقر socket.emit('serverQuestion', 'هل ما زلت هناك؟', (response) => { console.log('العميل رد:', response); }); // النمط 3: مهلة للإقرار socket.timeout(5000).emit('requestData', (err, response) => { if (err) { console.log('العميل لم يستجب في الوقت المحدد'); } else { console.log('العميل رد:', response); } }); }); // العميل // الاستجابة للنمط 1 socket.emit('sendMessage', 'مرحبًا!', (response) => { if (response.success) { console.log('تم إرسال الرسالة، المعرف:', response.messageId); } else { console.error('فشل:', response.error); } }); // الاستجابة للنمط 2 socket.on('serverQuestion', (question, callback) => { console.log('الخادم سأل:', question); callback('نعم، أنا هنا!'); });

أعلام ومعدلات البث

التحكم في سلوك البث بالأعلام:

// الخادم io.on('connection', (socket) => { // متطاير: قد يضيع إذا لم يكن العميل جاهزًا (جيد للتحديثات عالية التردد) socket.volatile.emit('cursorPosition', { x: 100, y: 200 }); // بث متطاير socket.volatile.broadcast.emit('gameState', gameData); // ثنائي: تحسين للبيانات الثنائية socket.binary(true).emit('image', buffer); // ضغط: تمكين الضغط لكل رسالة (Socket.io 4.0+) socket.compress(true).emit('largeData', bigObject); // محلي: الإرسال فقط على هذا الخادم (ليس للخوادم الأخرى في المجموعة) socket.local.emit('localEvent', data); // دمج الأعلام socket.volatile.compress(true).to('room').emit('optimizedData', data); });

مثال بث كامل

// الخادم - تطبيق دردشة كامل const io = require('socket.io')(3000); const users = new Map(); const rooms = new Map(); io.on('connection', (socket) => { console.log('مستخدم متصل:', socket.id); // تسجيل المستخدم socket.on('register', (username) => { users.set(socket.id, username); socket.username = username; // إخطار الجميع io.emit('userJoined', { userId: socket.id, username: username, totalUsers: users.size }); }); // الانضمام لغرفة socket.on('joinRoom', (roomName) => { socket.join(roomName); // تتبع عضوية الغرفة if (!rooms.has(roomName)) { rooms.set(roomName, new Set()); } rooms.get(roomName).add(socket.id); // التأكيد للمستخدم socket.emit('roomJoined', { room: roomName, userCount: rooms.get(roomName).size }); // إخطار أعضاء الغرفة (عدا المرسل) socket.to(roomName).emit('userJoinedRoom', { username: socket.username, room: roomName }); }); // رسالة غرفة عامة socket.on('roomMessage', (roomName, message) => { io.to(roomName).emit('roomMessage', { from: socket.id, username: socket.username, room: roomName, message: message, timestamp: new Date() }); }); // رسالة خاصة socket.on('privateMessage', (targetUsername, message) => { // البحث عن السوكيت المستهدف let targetSocketId = null; for (const [socketId, username] of users.entries()) { if (username === targetUsername) { targetSocketId = socketId; break; } } if (targetSocketId) { // الإرسال للمستقبل io.to(targetSocketId).emit('privateMessage', { from: socket.id, fromUsername: socket.username, message: message, timestamp: new Date() }); // التأكيد للمرسل socket.emit('privateMessageSent', { to: targetUsername, message: message }); } else { socket.emit('error', `المستخدم ${targetUsername} غير موجود`); } }); // مؤشر الكتابة (متطاير، خاص بالغرفة) socket.on('typing', (roomName, isTyping) => { socket.volatile.to(roomName).emit('userTyping', { userId: socket.id, username: socket.username, isTyping: isTyping }); }); // إعلان عام (للمسؤولين فقط) socket.on('announcement', (message, adminKey) => { if (adminKey === 'secret-admin-key') { io.emit('announcement', { message: message, timestamp: new Date() }); } }); // قطع الاتصال socket.on('disconnect', () => { const username = users.get(socket.id); users.delete(socket.id); // الإزالة من تتبع الغرفة rooms.forEach((members, roomName) => { if (members.has(socket.id)) { members.delete(socket.id); io.to(roomName).emit('userLeftRoom', { username: username, room: roomName }); } }); // إخطار الجميع io.emit('userLeft', { userId: socket.id, username: username, totalUsers: users.size }); }); }); console.log('خادم الدردشة يعمل على المنفذ 3000');

أفضل ممارسات البث

// ✅ جيد - استخدام الأنماط المناسبة socket.emit('welcome', 'مرحبًا!'); // الإرسال لهذا المستخدم socket.broadcast.emit('newUser', userId); // إخطار الآخرين // ✅ جيد - استخدام متطاير للتحديثات عالية التردد socket.volatile.emit('cursor', { x, y }); // موافق لفقدان بعض التحديثات // ✅ جيد - استخدام الغرف للبث الموجه io.to('game123').emit('gameUpdate', data); // المستخدمون المعنيون فقط // ❌ سيء - لا ترسل للجميع عندما يجب استخدام الغرف io.emit('gameUpdate', data); // الجميع يحصل عليه! // ❌ سيء - لا ترسل بيانات كبيرة بشكل متكرر io.emit('heavyData', largeObject); // يبطئ الخادم // ✅ جيد - خنق أو ضغط البيانات الكبيرة socket.compress(true).emit('data', large); // الضغط أولاً
تمرين: ابنِ نظام مراسلة شامل:
  • نفذ رسائل بث عامة (جميع المستخدمين)
  • نفذ بث قائم على الغرف (قنوات الدردشة)
  • نفذ مراسلة خاصة بين المستخدمين (رسائل مباشرة)
  • أضف إقرارات الرسائل مع ردود استدعاء النجاح/الخطأ
  • نفذ أحداث متطايرة لمؤشرات الكتابة
  • أضف أمرًا للبث لغرف متعددة في وقت واحد
  • تتبع وعرض عدد المستخدمين المتصلين لكل غرفة
  • نفذ أمر مسؤول يبث لجميع فضاءات الأسماء