WebSockets والتطبيقات الفورية
غرف الدردشة والرسائل الخاصة
غرف الدردشة والرسائل الخاصة
توسيع وظائف الدردشة بالغرف يسمح للمستخدمين بتنظيم المحادثات حسب المواضيع، بينما تتيح الرسائل الخاصة التواصل الفردي. هذه الميزات ضرورية لبناء تطبيقات دردشة قابلة للتطوير ومرنة.
فهم غرف Socket.io
الغرف هي قنوات جانب الخادم يمكن للمقابس الانضمام إليها ومغادرتها. تسمح لك ببث الرسائل لمجموعات محددة من المستخدمين:
- الغرف التلقائية: كل مقبس ينضم تلقائيًا إلى غرفة محددة بمعرف المقبس الخاص به
- الغرف المخصصة: يمكنك إنشاء والانضمام إلى أي عدد من الغرف المسماة
- البث: إرسال الرسائل إلى جميع المقابس في غرفة معينة
- الديناميكية: يمكن للمستخدمين الانضمام ومغادرة الغرف في أي وقت
إدارة الغرف من جانب الخادم
نفذ المنطق من جانب الخادم لإنشاء وإدارة غرف الدردشة:
// server.js - خادم غرف الدردشة
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// تخزين معلومات الغرفة
const rooms = new Map(); // roomId => { name, users: Set, messages: [] }
// تخزين معلومات المستخدم
const users = new Map(); // socketId => { id, username, currentRoom }
io.on('connection', (socket) => {
console.log('المستخدم متصل:', socket.id);
// تسجيل المستخدم
users.set(socket.id, {
id: socket.id,
username: socket.handshake.auth.username || `مستخدم${socket.id.substring(0, 4)}`,
currentRoom: null
});
// إرسال الغرف المتاحة
socket.emit('roomsList', Array.from(rooms.entries()).map(([id, room]) => ({
id,
name: room.name,
userCount: room.users.size
})));
// إنشاء غرفة جديدة
socket.on('createRoom', (roomName, callback) => {
const roomId = `room_${Date.now()}`;
rooms.set(roomId, {
name: roomName,
users: new Set(),
messages: [],
createdAt: Date.now(),
createdBy: users.get(socket.id).username
});
console.log(`تم إنشاء الغرفة: ${roomName} (${roomId})`);
// إخطار جميع المستخدمين بالغرفة الجديدة
io.emit('roomCreated', {
id: roomId,
name: roomName,
userCount: 0
});
callback({ success: true, roomId });
});
// الانضمام للغرفة
socket.on('joinRoom', (roomId, callback) => {
const user = users.get(socket.id);
const room = rooms.get(roomId);
if (!room) {
return callback({ success: false, error: 'الغرفة غير موجودة' });
}
// مغادرة الغرفة الحالية إذا كان في واحدة
if (user.currentRoom) {
socket.leave(user.currentRoom);
const oldRoom = rooms.get(user.currentRoom);
if (oldRoom) {
oldRoom.users.delete(socket.id);
// إخطار الغرفة بمغادرة المستخدم
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
}
}
// الانضمام للغرفة الجديدة
socket.join(roomId);
room.users.add(socket.id);
user.currentRoom = roomId;
console.log(`${user.username} انضم للغرفة: ${room.name}`);
// إرسال معلومات الغرفة والسجل للمستخدم
callback({
success: true,
room: {
id: roomId,
name: room.name,
messages: room.messages,
users: Array.from(room.users).map(id => ({
id,
username: users.get(id)?.username
}))
}
});
// إخطار الغرفة بانضمام المستخدم
socket.to(roomId).emit('userJoinedRoom', {
username: user.username,
userId: socket.id,
timestamp: Date.now()
});
// تحديث عدد مستخدمي الغرفة للجميع
io.emit('roomUpdated', {
id: roomId,
userCount: room.users.size
});
});
// إرسال رسالة للغرفة
socket.on('roomMessage', (message, callback) => {
const user = users.get(socket.id);
if (!user.currentRoom) {
return callback({ success: false, error: 'لست في غرفة' });
}
const room = rooms.get(user.currentRoom);
if (!room) {
return callback({ success: false, error: 'الغرفة غير موجودة' });
}
const messageData = {
id: Date.now() + Math.random(),
userId: socket.id,
username: user.username,
message: message,
timestamp: Date.now()
};
// تخزين الرسالة في سجل الغرفة
room.messages.push(messageData);
// بث لجميع المستخدمين في الغرفة
io.to(user.currentRoom).emit('roomMessage', messageData);
callback({ success: true });
});
// مغادرة الغرفة
socket.on('leaveRoom', () => {
const user = users.get(socket.id);
if (user.currentRoom) {
const room = rooms.get(user.currentRoom);
socket.leave(user.currentRoom);
room.users.delete(socket.id);
// إخطار الغرفة
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
// تحديث عدد الغرفة
io.emit('roomUpdated', {
id: user.currentRoom,
userCount: room.users.size
});
user.currentRoom = null;
}
});
// حذف الغرفة (المنشئ فقط يمكنه الحذف)
socket.on('deleteRoom', (roomId, callback) => {
const room = rooms.get(roomId);
if (!room) {
return callback({ success: false, error: 'الغرفة غير موجودة' });
}
const user = users.get(socket.id);
if (room.createdBy !== user.username) {
return callback({ success: false, error: 'منشئ الغرفة فقط يمكنه الحذف' });
}
// طرد جميع المستخدمين من الغرفة
room.users.forEach(userId => {
const userSocket = io.sockets.sockets.get(userId);
if (userSocket) {
userSocket.leave(roomId);
users.get(userId).currentRoom = null;
}
});
// إخطار جميع المستخدمين بحذف الغرفة
io.emit('roomDeleted', { id: roomId, name: room.name });
// حذف الغرفة
rooms.delete(roomId);
callback({ success: true });
});
// معالجة قطع الاتصال
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user && user.currentRoom) {
const room = rooms.get(user.currentRoom);
if (room) {
room.users.delete(socket.id);
socket.to(user.currentRoom).emit('userLeftRoom', {
username: user.username,
userId: socket.id
});
io.emit('roomUpdated', {
id: user.currentRoom,
userCount: room.users.size
});
}
}
users.delete(socket.id);
});
});
const PORT = 3000;
http.listen(PORT, () => {
console.log(`خادم الدردشة يعمل على المنفذ ${PORT}`);
});
ملاحظة: كل مقبس يمكن أن يكون في غرفة واحدة فقط في كل مرة في هذا التطبيق. لدعم غرف متعددة، عدّل كائن المستخدم لتخزين مصفوفة من الغرف.
تطبيق الرسائل الخاصة
تُرسل الرسائل الخاصة مباشرة بين مستخدمين دون البث إلى غرفة:
// إضافة إلى server.js - الرسائل الخاصة
io.on('connection', (socket) => {
// ... الكود السابق ...
// إرسال رسالة خاصة
socket.on('privateMessage', ({ recipientId, message }, callback) => {
const sender = users.get(socket.id);
const recipient = users.get(recipientId);
if (!recipient) {
return callback({ success: false, error: 'المستخدم غير موجود' });
}
const messageData = {
id: Date.now() + Math.random(),
senderId: socket.id,
senderUsername: sender.username,
recipientId: recipientId,
recipientUsername: recipient.username,
message: message,
timestamp: Date.now(),
read: false
};
// إرسال للمستلم
io.to(recipientId).emit('privateMessage', messageData);
// إرسال تأكيد للمرسل
socket.emit('privateMessageSent', messageData);
callback({ success: true, messageId: messageData.id });
});
// وضع علامة على الرسالة الخاصة كمقروءة
socket.on('markMessageRead', (messageId) => {
// في الإنتاج، حدّث قاعدة البيانات
io.emit('messageRead', {
messageId,
readBy: socket.id,
readAt: Date.now()
});
});
// الحصول على المستخدمين المتصلين للرسائل الخاصة
socket.on('getOnlineUsers', (callback) => {
const onlineUsers = Array.from(users.values())
.filter(user => user.id !== socket.id)
.map(user => ({
id: user.id,
username: user.username,
currentRoom: user.currentRoom
}));
callback(onlineUsers);
});
});
واجهة الغرف من جانب العميل
أنشئ واجهة HTML للغرف والرسائل الخاصة:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>غرف الدردشة</title>
<style>
.container {
display: flex;
height: 100vh;
font-family: Arial, sans-serif;
}
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
padding: 20px;
overflow-y: auto;
}
.room-item {
padding: 10px;
margin: 5px 0;
background: #34495e;
border-radius: 5px;
cursor: pointer;
display: flex;
justify-content: space-between;
}
.room-item:hover {
background: #3d5a75;
}
.room-item.active {
background: #3498db;
}
.create-room-btn {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #27ae60;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.main-chat {
flex: 1;
display: flex;
flex-direction: column;
background: #ecf0f1;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.message {
margin: 10px 0;
padding: 10px 15px;
background: white;
border-radius: 8px;
max-width: 60%;
}
.message.own {
margin-right: auto;
background: #3498db;
color: white;
}
.message.private {
background: #9b59b6;
color: white;
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<h3>غرف الدردشة</h3>
<div id="roomsList"></div>
<button class="create-room-btn" onclick="createRoom()">إنشاء غرفة</button>
</div>
<div class="main-chat">
<div class="chat-header">
<h2 id="roomTitle">اختر غرفة</h2>
<button id="leaveBtn" onclick="leaveRoom()">مغادرة الغرفة</button>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="اكتب رسالة...">
<button onclick="sendMessage()">إرسال</button>
</div>
</div>
<div class="users-panel">
<h3>مستخدمو الغرفة</h3>
<div id="roomUsers"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="rooms.js"></script>
</body>
</html>
نصيحة: لتطبيقات الإنتاج، خزن الرسائل الخاصة في قاعدة بيانات ونفذ نظام صندوق وارد/رسائل مناسب مع إيصالات القراءة وسجل الرسائل.
تحذير: تحقق دائمًا من صحة إدخال المستخدم وعقمه من جانب الخادم. لا تثق أبدًا في التحقق من جانب العميل وحده، خاصة لأسماء الغرف والرسائل.
تمرين: حسّن نظام الغرف والرسائل بـ:
- غرف دائمة مخزنة في قاعدة بيانات (MongoDB أو PostgreSQL)
- مسؤولي الغرف بالقدرة على طرد المستخدمين
- سجل الرسائل الخاصة مع واجهة صندوق وارد مخصصة
- شارات الرسائل غير المقروءة تعرض عدد الرسائل الخاصة غير المقروءة
- وظيفة بحث الغرف للعثور على الغرف بالاسم
- غرف خاصة محمية بكلمة مرور
- مشاركة الملفات داخل الغرف (صور، مستندات)
- بدء مكالمات صوتية/فيديو من خلال الرسائل الخاصة
اختبر مع مستخدمين متعددين ينشئون الغرف وينضمون ويغادرون ويتبادلون رسائل الغرفة والرسائل الخاصة.