تتطلب ميزات الوقت الفعلي – الدردشة المباشرة والإشعارات والتحرير التعاوني ولوحات المعلومات المباشرة – اتصالاً مستمرًا بين العميل والخادم. في عام 2026،WebSockets عبر المقبس.io يظل الحل الأكثر عملية والمنتشر على نطاق واسع لمعظم التطبيقات في الوقت الفعلي. يبني هذا البرنامج التعليمي نظامًا كاملاً في الوقت الفعلي مع إمكانية التوسع عبر Redis Pub/Sub.
📋 Table of Contents
WebSockets مقابل SSE مقابل الاقتراع الطويل
| الطريقة | ثنائي الاتجاه | الكمون | الأفضل لـ |
|---|---|---|---|
| ويب سوكيتس | نعم | ~1 مللي ثانية | الدردشة والألعاب والأدوات التعاونية |
| الأحداث المرسلة من الخادم | لا (الخادم → العميل فقط) | ~5 مللي ثانية | لوحات المعلومات والإشعارات المباشرة |
| الاقتراع الطويل | مقلد | 100-500 مللي ثانية | احتياطي للشبكات المقيدة |
إعداد المشروع
mkdir realtime-app && cd realtime-app
npm init -y
npm install express socket.io ioredis cors
# Frontend dependencies
npm install -D vite
الخادم: Express + المقبس.io
// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const Redis = require('ioredis');
const cors = require('cors');
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: "http://localhost:5173", // Vite dev server
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.json());
// Redis Pub/Sub for multi-instance scaling
const pub = new Redis({ host: 'localhost', port: 6379 });
const sub = new Redis({ host: 'localhost', port: 6379 });
// ── Active users tracking ─────────────────────────────────
const activeUsers = new Map(); // socketId → { userId, username, room }
// ── Subscribe to Redis (messages from other server instances) ──
sub.subscribe('chat', 'notifications', (err, count) => {
console.log(`Subscribed to ${count} Redis channels`);
});
sub.on('message', (channel, message) => {
const data = JSON.parse(message);
if (channel === 'chat') {
io.to(data.room).emit('message', data);
}
if (channel === 'notifications') {
io.to(data.userId).emit('notification', data);
}
});
// ── Socket.io connection handler ──────────────────────────
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// ── Authenticate ───────────────────────────────────────
socket.on('authenticate', ({ userId, username }) => {
activeUsers.set(socket.id, { userId, username });
socket.join(userId); // join personal room for direct notifications
socket.emit('authenticated', { userId, socketId: socket.id });
console.log(`User authenticated: ${username} (${userId})`);
});
// ── Join chat room ─────────────────────────────────────
socket.on('join_room', ({ roomId }) => {
const user = activeUsers.get(socket.id);
if (!user) return socket.emit('error', 'Not authenticated');
socket.join(roomId);
activeUsers.set(socket.id, { ...user, room: roomId });
// Notify others in room
socket.to(roomId).emit('user_joined', {
userId: user.userId,
username: user.username,
roomId,
timestamp: Date.now()
});
console.log(`${user.username} joined room: ${roomId}`);
});
// ── Send message ───────────────────────────────────────
socket.on('send_message', async ({ roomId, content }) => {
const user = activeUsers.get(socket.id);
if (!user) return socket.emit('error', 'Not authenticated');
const message = {
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
userId: user.userId,
username: user.username,
content,
room: roomId,
timestamp: Date.now()
};
// Publish to Redis so ALL server instances receive it
pub.publish('chat', JSON.stringify(message));
// TODO: persist to database here
// await db.messages.create({ data: message });
});
// ── Typing indicator ───────────────────────────────────
socket.on('typing', ({ roomId, isTyping }) => {
const user = activeUsers.get(socket.id);
if (!user) return;
socket.to(roomId).emit('user_typing', {
userId: user.userId,
username: user.username,
isTyping
});
});
// ── Disconnect ─────────────────────────────────────────
socket.on('disconnect', () => {
const user = activeUsers.get(socket.id);
if (user?.room) {
socket.to(user.room).emit('user_left', {
userId: user.userId,
username: user.username
});
}
activeUsers.delete(socket.id);
console.log(`Client disconnected: ${socket.id}`);
});
});
// ── REST API: send notification from backend ──────────────
app.post('/notify', (req, res) => {
const { userId, type, message } = req.body;
pub.publish('notifications', JSON.stringify({ userId, type, message, timestamp: Date.now() }));
res.json({ ok: true });
});
server.listen(3001, () => console.log('Server running on port 3001'));
العميل: رد فعل + المقبس.io
// hooks/useSocket.ts
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocket(userId: string, username: string) {
const socket = useRef<Socket | null>(null);
const [connected, setConnected] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
useEffect(() => {
socket.current = io('http://localhost:3001');
socket.current.on('connect', () => {
setConnected(true);
socket.current!.emit('authenticate', { userId, username });
});
socket.current.on('message', (msg: Message) => {
setMessages(prev => [...prev, msg]);
});
socket.current.on('notification', (notif: Notification) => {
setNotifications(prev => [notif, ...prev]);
});
socket.current.on('user_typing', ({ username: typer, isTyping }) => {
setTypingUsers(prev => {
const next = new Set(prev);
isTyping ? next.add(typer) : next.delete(typer);
return next;
});
});
socket.current.on('disconnect', () => setConnected(false));
return () => { socket.current?.disconnect(); };
}, [userId, username]);
const joinRoom = (roomId: string) => socket.current?.emit('join_room', { roomId });
const sendMessage = (roomId: string, content: string) =>
socket.current?.emit('send_message', { roomId, content });
const setTyping = (roomId: string, isTyping: boolean) =>
socket.current?.emit('typing', { roomId, isTyping });
return { connected, messages, notifications, typingUsers, joinRoom, sendMessage, setTyping };
}
القياس باستخدام Redis Pub/Sub
يسمح نمط Redis Pub/Sub في كود الخادم بالتحجيم الأفقي: عندما تصل رسالة على مثيل Node.js واحد، يتم نشرها على Redis. جميع المثيلات الأخرى (المشتركة في Redis) تستقبلها وترسلها إلى عملائها المتصلين. هذا يعني أنه يمكنك تشغيل 10 مثيلات Node.js خلف موازن التحميل وتصل الرسائل إلى جميع المستخدمين بغض النظر عن المثيل المتصل بهم.
# Run multiple instances
PORT=3001 node server.js &
PORT=3002 node server.js &
PORT=3003 node server.js &
# Nginx load balancer with sticky sessions (WebSocket requirement)
upstream websocket {
ip_hash; # sticky sessions - same client hits same server
server localhost:3001;
server localhost:3002;
server localhost:3003;
}
الأسئلة المتداولة
س: المقبس.io مقابل WebSocket API الأصلي؟
ج: يضيف Switch.io: إعادة الاتصال التلقائي، والغرف/مساحات الأسماء، والرجوع إلى الاستقصاء الطويل، وواجهة برمجة التطبيقات المستندة إلى الأحداث. WebSocket الأصلي هو مستوى أقل. استخدم Jack.io للإنتاج — منطق إعادة الاتصال وحده يوفر ساعات من العمل.
س: كم عدد اتصالات WebSocket المتزامنة التي يمكن لـ Node.js التعامل معها؟
ج: على خادم واحد رباعي النواة مزود بذاكرة وصول عشوائي (RAM) سعة 8 جيجابايت، توقع وجود ما بين 10,000 إلى 50,000 اتصال متزامن اعتمادًا على تردد الرسالة. يتسع نطاق Switch.io مع Redis للملايين عبر خوادم متعددة.
س: هل يعمل WebSockets من خلال جدران الحماية والوكلاء؟
ج: تسمح معظم الشبكات الحديثة بـ WebSockets على المنفذ 443 (HTTPS/WSS). يعود المقبس إلى الاستقصاء الطويل إذا تم حظر WebSocket. استخدم WSS (WebSocket الآمن) في الإنتاج.
س: كيف يمكنني الاستمرار في رسائل الدردشة؟
ج: قم بالتخزين في PostgreSQL أو MongoDB. فهرسة حسب (roomId، الطابع الزمني) للاسترجاع السريع. تحميل آخر 50 رسالة عندما ينضم مستخدم إلى غرفة عبر REST API؛ دفق الرسائل الجديدة عبر WebSocket.
س: هل يوجد بديل لخدمة WebSocket المُدارة؟
ج: Pusher وAbly وAWS API Gateway WebSocket هي بدائل مُدارة. تكلفة أعلى ولكن لا توجد إدارة للبنية التحتية. مناسب للاستخدام على نطاق صغير أو عندما يكون وقت المطور هو عنق الزجاجة.
الخلاصة
WebSockets المزودة بـSocket.io وRedis Pub/Sub هي بنية مثبتة وقابلة للتطوير للتطبيقات في الوقت الفعلي في عام 2026. يتعامل كود الخادم في هذا البرنامج التعليمي مع آلاف المستخدمين المتزامنين في مثيل واحد، ويتوسع أفقيًا عبر Redis عندما ينمو الطلب. ابدأ بمثيل واحد، وأضف Redis عندما تحتاج إلى التوسع خارج خادم واحد، والانتقال إلى خدمات WebSocket المُدارة فقط إذا أصبح الحمل التشغيلي يمثل مشكلة.
🔗 Share this article
✍️ Leave a Comment