🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

كيفية إنشاء تطبيق في الوقت الفعلي باستخدام WebSockets وNode.js في عام 2026

⏱️4 min read  ·  756 words

تتطلب ميزات الوقت الفعلي – الدردشة المباشرة والإشعارات والتحرير التعاوني ولوحات المعلومات المباشرة – اتصالاً مستمرًا بين العميل والخادم. في عام 2026،WebSockets عبر المقبس.io يظل الحل الأكثر عملية والمنتشر على نطاق واسع لمعظم التطبيقات في الوقت الفعلي. يبني هذا البرنامج التعليمي نظامًا كاملاً في الوقت الفعلي مع إمكانية التوسع عبر Redis Pub/Sub.

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 المُدارة فقط إذا أصبح الحمل التشغيلي يمثل مشكلة.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা