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

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

19 دقيقة الدرس 9 من 35

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

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

ما هي الغرف؟

الغرف هي قنوات عشوائية يمكن للسوكيتات الانضمام إليها ومغادرتها. تسمح لك ببث الأحداث لمجموعة فرعية من العملاء المتصلين:

// فكر في الغرف مثل قنوات الدردشة // يمكن أن يكون السوكيت في غرف متعددة في وقت واحد // أمثلة: // - room:general (قناة دردشة) // - room:support (قناة دردشة) // - game:123 (مثيل لعبة محدد) // - user:john (غرفة خاصة لمستخدم)
ملاحظة: الغرف هي مفهوم من جانب الخادم فقط. لا يمكن للعملاء الانضمام مباشرة إلى الغرف - يجب عليهم طلب الخادم لإضافتهم إلى غرفة. هذا يعطي الخادم سيطرة كاملة على عضوية الغرفة.

الانضمام والمغادرة من الغرف

يمكن للسوكيتات الانضمام ومغادرة الغرف باستخدام طرق بسيطة:

// الخادم io.on('connection', (socket) => { console.log('مستخدم متصل:', socket.id); // الانضمام إلى غرفة socket.join('general'); console.log(`${socket.id} انضم إلى الغرفة: general`); // الانضمام إلى غرف متعددة socket.join(['general', 'announcements', 'vip']); // مغادرة غرفة socket.leave('general'); // التحقق مما إذا كان السوكيت في غرفة const isInRoom = socket.rooms.has('general'); console.log('في غرفة general؟', isInRoom); // الحصول على جميع الغرف التي يوجد فيها السوكيت console.log('غرف السوكيت:', socket.rooms); // Set { 'socketId', 'general', 'announcements', 'vip' } });

طلب العميل للانضمام للغرفة

عادةً، يطلب العملاء الانضمام إلى غرف محددة:

// العميل socket.emit('joinRoom', 'general'); socket.emit('leaveRoom', 'support'); // الخادم io.on('connection', (socket) => { // معالجة طلب الانضمام للغرفة socket.on('joinRoom', (roomName) => { // التحقق من اسم الغرفة if (!roomName || typeof roomName !== 'string') { socket.emit('error', 'اسم غرفة غير صالح'); return; } // الانضمام إلى الغرفة socket.join(roomName); // إخطار المستخدم socket.emit('roomJoined', roomName); // إخطار الآخرين في الغرفة socket.to(roomName).emit('userJoined', { userId: socket.id, room: roomName }); console.log(`${socket.id} انضم إلى ${roomName}`); }); // معالجة طلب مغادرة الغرفة socket.on('leaveRoom', (roomName) => { socket.leave(roomName); socket.emit('roomLeft', roomName); socket.to(roomName).emit('userLeft', { userId: socket.id, room: roomName }); }); });

البث للغرف

إرسال رسائل لجميع السوكيتات في غرفة محددة:

// البث للجميع في غرفة 'general' io.to('general').emit('message', 'مرحبًا بغرفة general!'); // البث لغرف متعددة io.to('general').to('vip').emit('announcement', 'رسالة خاصة'); // البث للغرفة عدا المرسل socket.to('general').emit('message', 'شخص آخر أرسل هذا'); // البث للغرفة بما في ذلك المرسل io.in('general').emit('message', 'الجميع في general يستقبل هذا'); // ملاحظة: to() و in() متطابقتان - استخدم أيهما تفضل

إدارة الغرف

احصل على معلومات حول الغرف وأعضائها:

// الحصول على جميع السوكيتات في غرفة (Socket.io 4.0+) const socketsInRoom = await io.in('general').fetchSockets(); console.log(`${socketsInRoom.length} مستخدمين في general`); socketsInRoom.forEach(socket => { console.log('معرف السوكيت:', socket.id); console.log('الغرف:', socket.rooms); }); // الحصول على جميع أسماء الغرف const rooms = io.sockets.adapter.rooms; console.log('جميع الغرف:', Array.from(rooms.keys())); // الحصول على عدد السوكيتات في غرفة const roomSize = io.sockets.adapter.rooms.get('general')?.size || 0; console.log('المستخدمون في general:', roomSize); // التحقق من وجود الغرفة const roomExists = io.sockets.adapter.rooms.has('general'); console.log('غرفة general موجودة؟', roomExists);

مثال عملي للغرف: غرف الدردشة

// الخادم const chatRooms = new Map(); // تتبع بيانات وصفية للغرفة io.on('connection', (socket) => { console.log('مستخدم متصل:', socket.id); // الانضمام للغرفة socket.on('joinRoom', async (roomName, username) => { // مغادرة الغرف الحالية (عدا غرفة السوكيت الخاصة) Array.from(socket.rooms).forEach(room => { if (room !== socket.id) { socket.leave(room); } }); // الانضمام لغرفة جديدة socket.join(roomName); // تتبع البيانات الوصفية للغرفة if (!chatRooms.has(roomName)) { chatRooms.set(roomName, { users: [] }); } chatRooms.get(roomName).users.push({ id: socket.id, username }); // إرسال معلومات الغرفة للمستخدم const socketsInRoom = await io.in(roomName).fetchSockets(); socket.emit('roomInfo', { room: roomName, userCount: socketsInRoom.length, users: chatRooms.get(roomName).users }); // إخطار الغرفة socket.to(roomName).emit('userJoined', { userId: socket.id, username: username }); console.log(`${username} انضم إلى ${roomName}`); }); // إرسال رسالة للغرفة socket.on('roomMessage', (roomName, message) => { io.to(roomName).emit('roomMessage', { userId: socket.id, message: message, timestamp: new Date() }); }); // معالجة قطع الاتصال socket.on('disconnect', () => { // الإزالة من جميع البيانات الوصفية للغرف chatRooms.forEach((roomData, roomName) => { const userIndex = roomData.users.findIndex(u => u.id === socket.id); if (userIndex !== -1) { const username = roomData.users[userIndex].username; roomData.users.splice(userIndex, 1); // إخطار الغرفة io.to(roomName).emit('userLeft', { userId: socket.id, username: username }); } }); }); });

ما هي فضاءات الأسماء؟

تسمح لك فضاءات الأسماء بتقسيم منطق تطبيقك عبر قنوات اتصال متعددة على نفس الخادم:

// فكر في فضاءات الأسماء مثل مثيلات Socket.io منفصلة // كل فضاء اسم له: // - معالجات أحداث خاصة به // - غرف خاصة به // - وسيطة خاصة به // - عملاء متصلون خاصون به // أمثلة: // /chat - وظيفة الدردشة // /admin - لوحة الإدارة // /notifications - نظام الإشعارات // /game - منطق اللعبة
نصيحة: استخدم فضاءات الأسماء لميزات التطبيق المختلفة (الدردشة مقابل الإدارة)، واستخدم الغرف داخل فضاء الاسم لتنظيم المستخدمين (chat:general مقابل chat:support).

إنشاء فضاءات الأسماء

حدد فضاءات الأسماء على الخادم:

// الخادم const express = require('express'); const { createServer } = require('http'); const { Server } = require('socket.io'); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); // فضاء الاسم الافتراضي '/' (موجود بالفعل) io.on('connection', (socket) => { console.log('متصل بفضاء الاسم الافتراضي'); }); // إنشاء فضاء اسم /chat const chatNamespace = io.of('/chat'); chatNamespace.on('connection', (socket) => { console.log('متصل بفضاء اسم /chat:', socket.id); socket.on('message', (msg) => { chatNamespace.emit('message', msg); }); }); // إنشاء فضاء اسم /admin const adminNamespace = io.of('/admin'); adminNamespace.on('connection', (socket) => { console.log('مسؤول متصل:', socket.id); socket.on('adminAction', (action) => { console.log('إجراء المسؤول:', action); }); }); // إنشاء فضاء اسم /notifications const notifNamespace = io.of('/notifications'); notifNamespace.on('connection', (socket) => { socket.emit('welcome', 'متصل بالإشعارات'); }); httpServer.listen(3000);

الاتصال بفضاءات الأسماء (العميل)

// العميل // الاتصال بفضاء الاسم الافتراضي const socket = io('http://localhost:3000'); // الاتصال بفضاء اسم /chat const chatSocket = io('http://localhost:3000/chat'); // الاتصال بفضاء اسم /admin const adminSocket = io('http://localhost:3000/admin'); // الاتصال بفضاء اسم /notifications const notifSocket = io('http://localhost:3000/notifications'); // استخدام فضاءات أسماء مختلفة chatSocket.on('connect', () => { console.log('متصل بالدردشة'); chatSocket.emit('message', 'مرحبًا من الدردشة!'); }); adminSocket.on('connect', () => { console.log('متصل بالإدارة'); adminSocket.emit('adminAction', 'view-users'); }); notifSocket.on('welcome', (msg) => { console.log(msg); });

أحداث وغرف خاصة بفضاء الاسم

كل فضاء اسم له غرف وأحداث خاصة به:

// الخادم const chatNamespace = io.of('/chat'); chatNamespace.on('connection', (socket) => { // الانضمام للغرف داخل فضاء اسم /chat socket.join('general'); socket.join('tech'); // البث داخل فضاء الاسم chatNamespace.emit('userCount', chatNamespace.sockets.size); // البث لغرفة داخل فضاء الاسم socket.to('general').emit('userJoined', socket.id); // معالجة الأحداث داخل فضاء الاسم socket.on('chatMessage', (room, message) => { chatNamespace.to(room).emit('chatMessage', { room: room, message: message, sender: socket.id }); }); }); // الغرف في فضاءات أسماء مختلفة مستقلة const adminNamespace = io.of('/admin'); adminNamespace.on('connection', (socket) => { // هذه غرفة 'general' منفصلة عن غرفة 'general' للدردشة socket.join('general'); });

فضاءات الأسماء الديناميكية

أنشئ فضاءات أسماء ديناميكيًا بناءً على أنماط:

// الخادم // إنشاء فضاء اسم لكل مؤسسة io.of(/^\/org-\w+$/).on('connection', (socket) => { const namespace = socket.nsp.name; // مثل '/org-acme' console.log(`متصل بـ ${namespace}`); socket.on('message', (msg) => { // البث لهذه المؤسسة المحددة socket.nsp.emit('message', msg); }); }); // العميل const acmeSocket = io('http://localhost:3000/org-acme'); const xyzSocket = io('http://localhost:3000/org-xyz');

الغرفة مقابل فضاء الاسم: متى تستخدم أيهما؟

// استخدم فضاءات الأسماء لـ: // ✓ فصل ميزات التطبيق المختلفة // ✓ منطق مصادقة/تفويض مختلف // ✓ وسيطة مختلفة لكل ميزة // ✓ معالجات أحداث منفصلة تمامًا // مثال: /chat, /admin, /game // استخدم الغرف لـ: // ✓ تجميع المستخدمين داخل نفس الميزة // ✓ مجموعات مؤقتة (غرف دردشة، ردهات ألعاب) // ✓ البث لمجموعات فرعية من المستخدمين // ✓ نفس معالجات الأحداث، مستقبلون مختلفون // مثال: chat:general, chat:support, game:lobby-1

مثال كامل: الدردشة مع الغرف وفضاءات الأسماء

// الخادم const io = require('socket.io')(3000); // فضاء اسم /chat مع غرف const chat = io.of('/chat'); chat.on('connection', (socket) => { console.log('مستخدم متصل بالدردشة'); // الانضمام للغرفة الافتراضية socket.join('general'); chat.to('general').emit('notification', 'مستخدم جديد انضم إلى general'); // تبديل الغرف socket.on('switchRoom', (newRoom) => { const currentRooms = Array.from(socket.rooms).filter(r => r !== socket.id); currentRooms.forEach(room => { socket.leave(room); chat.to(room).emit('notification', `مستخدم غادر ${room}`); }); socket.join(newRoom); chat.to(newRoom).emit('notification', `مستخدم انضم إلى ${newRoom}`); socket.emit('roomChanged', newRoom); }); // إرسال رسالة للغرف الحالية socket.on('message', async (message) => { const rooms = Array.from(socket.rooms).filter(r => r !== socket.id); rooms.forEach(room => { chat.to(room).emit('message', { room: room, sender: socket.id, message: message }); }); }); socket.on('disconnect', () => { console.log('مستخدم قطع الاتصال من الدردشة'); }); }); // فضاء اسم /admin (لا حاجة لغرف) const admin = io.of('/admin'); admin.on('connection', (socket) => { console.log('مسؤول متصل'); socket.on('getStats', async () => { const chatSockets = await chat.fetchSockets(); socket.emit('stats', { chatUsers: chatSockets.length, adminUsers: admin.sockets.size }); }); });
تمرين: ابنِ نظام دردشة متعدد الغرف:
  • أنشئ فضاء اسم /chat مع ثلاث غرف: general وtech وrandom
  • اسمح للمستخدمين بالانضمام والتبديل بين الغرف
  • بث إشعارات الانضمام/المغادرة لأعضاء الغرفة فقط
  • نفذ أمر لسرد جميع المستخدمين في الغرفة الحالية
  • أضف فضاء اسم /admin يمكنه رؤية إحصائيات جميع الغرف
  • تأكد من إرسال الرسائل فقط للغرفة الصحيحة
  • نظف البيانات الوصفية للغرفة عندما يقطع المستخدمون الاتصال