Skip to main content
backend26 أكتوبر 202514 دقائق قراءة

البيانات في الوقت الفعلي: WebSockets، SSE، ومتى تحتاجها حقًا

فهم أنماط البيانات في الوقت الفعلي — من WebSockets إلى Server-Sent Events، وقاعدة بيانات Firebase Realtime Database، ومعرفة متى يكون الاستقصاء (Polling) كافيًا.

websocketsrealtimesse
البيانات في الوقت الفعلي: WebSockets، SSE، ومتى تحتاجها حقًا

معظم التطبيقات التي تدعي حاجتها إلى بيانات في الوقت الفعلي لا تحتاج في الواقع إلى بيانات في الوقت الفعلي. إنها تحتاج إلى بيانات تبدو حديثة — يتم تحديثها في غضون ثوانٍ قليلة، وليس في غضون بضعة أجزاء من الثانية. هذا التمييز مهم لأن البنى الحقيقية للوقت الفعلي تُدخل تعقيدًا في إدارة الاتصال، ومزامنة الحالة، والتوسع، ومعالجة الأخطاء، وهي أمور غير ضرورية تمامًا إذا كان المستخدمون سيكونون سعداء تمامًا بتأخير مدته خمس ثوانٍ.

لقد قمت ببناء ميزات حقيقية في الوقت الفعلي — تتبع الطلبات المباشر لمنصة مطاعم، واجهات التحرير التعاوني، لوحات المعلومات المباشرة — وقد قمت أيضًا ببناء ميزات حيث بالغت في البداية في التصميم باستخدام WebSockets ثم استبدلتها لاحقًا بالاستقصاء (polling) الذي يتم تحديثه كل عشر ثوانٍ. لم يلاحظ أحد التغيير. البدء بالنهج الأبسط وإضافة التعقيد فقط عندما يواجه المستخدمون مشكلة فعلية ليس مجرد أمر عملي — بل هو الخيار الهندسي المسؤول.

متى تحتاج حقًا إلى الوقت الفعلي

يُبرر الوقت الفعلي الحقيقي عندما تتدهور قيمة المعلومات بسرعة مع زمن الاستجابة (latency).

الدردشة والمراسلة. يتوقع المستخدمون ظهور الرسائل في غضون ثانية. تأخير مدته خمس ثوانٍ يجعل المحادثة تبدو متقطعة. الوقت الفعلي ليس اختياريًا هنا.

التحرير التعاوني. يتطلب التحرير المتزامن على غرار Google Docs مزامنة في أقل من ثانية لتجنب تعارض التعديلات. يتجاوز التعقيد هنا مجرد النقل — تحتاج إلى تحويل العمليات (operational transformation) أو CRDTs لحل التعارضات.

لوحات المعلومات المباشرة ذات التأثير التشغيلي. لوحة معلومات المراقبة حيث يراقب مهندس الشذوذات أثناء النشر تحتاج إلى تحديثات في الوقت الفعلي. لوحة معلومات تحليلات الأعمال التي تُعرض مرة واحدة يوميًا لا تحتاج إلى ذلك.

الألعاب والتجارب التفاعلية. الألعاب متعددة اللاعبين، المزادات المباشرة، الاستطلاعات في الوقت الفعلي — أي شيء يتفاعل فيه عدة مستخدمين مع حالة مشتركة في وقت واحد.

التداول المالي. خلاصات الأسعار، تحديثات دفتر الأوامر، تغييرات المراكز. الأجزاء من الثانية مهمة هنا، وتعكس البنية ذلك.

متى لا تحتاج إلى الوقت الفعلي

خلاصات وسائل التواصل الاجتماعي. تبدو Twitter و Instagram في الوقت الفعلي، لكنهما تستخدمان مزيجًا من الاستقصاء عند التركيز والإشعارات الفورية. لا يتم تحديث الخلاصة أثناء النظر إليها — بل تسحب للتحديث.

مخزون التجارة الإلكترونية. "تبقى 3 فقط!" لا تحتاج إلى التحديث في الوقت الفعلي. التحقق عند تحميل الصفحة وعند الدفع كافٍ.

عدد الإشعارات. يمكن استقصاء الشارة الحمراء التي تظهر "5 إشعارات جديدة" كل 30 ثانية. لا يلاحظ المستخدمون إذا ظهر إشعار بعد 30 ثانية من إنشائه.

تحديثات المحتوى. منشورات المدونات، قوائم المنتجات، ملفات تعريف المستخدمين — أي بيانات تتغير بشكل غير متكرر. استقصاء عند تحميل الصفحة أو استخدام رؤوس ذاكرة التخزين المؤقت لـ HTTP مع إعادة التحقق.

الاستقصاء (Polling): الخيار الافتراضي الذي لا يُقدر حق قدره

الاستقصاء — إجراء طلبات HTTP دورية للتحقق من وجود بيانات جديدة — هو النهج الأبسط ويعمل لعدد من حالات الاستخدام أكبر مما يفترضه معظم المطورين.

الاستقصاء البسيط (Simple Polling)

function usePolledData<T>(url: string, intervalMs: number = 5000) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let active = true;

    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const result = await response.json();
        if (active) setData(result);
      } catch (e) {
        if (active) setError(e as Error);
      }
    };

    fetchData(); // Initial fetch
    const interval = setInterval(fetchData, intervalMs);

    return () => {
      active = false;
      clearInterval(interval);
    };
  }, [url, intervalMs]);

  return { data, error };
}

الاستقصاء الذكي مع الطلبات الشرطية (Smart Polling with Conditional Requests)

تتيح لك طلبات HTTP الشرطية (If-None-Match مع ETags أو If-Modified-Since) الاستقصاء بكفاءة. يعيد الخادم 304 Not Modified بدون محتوى إذا لم تتغير البيانات، مما يقلل عرض النطاق الترددي إلى ما يقرب من الصفر للموارد غير المتغيرة.

async function pollWithETag(url: string): Promise<{ data: any; changed: boolean }> {
  const cachedETag = etagCache.get(url);

  const headers: HeadersInit = {};
  if (cachedETag) {
    headers['If-None-Match'] = cachedETag;
  }

  const response = await fetch(url, { headers });

  if (response.status === 304) {
    return { data: dataCache.get(url), changed: false };
  }

  const etag = response.headers.get('ETag');
  if (etag) etagCache.set(url, etag);

  const data = await response.json();
  dataCache.set(url, data);

  return { data, changed: true };
}

متى يتعطل الاستقصاء (Polling Breaks Down)

يتوقف الاستقصاء عن كونه قابلاً للتطبيق عندما:

  • تحتاج إلى زمن استجابة أقل من ثانية. الاستقصاء كل 500 مللي ثانية هو في الأساس هجوم حرمان من الخدمة (DoS) على واجهة برمجة التطبيقات الخاصة بك.
  • تتغير البيانات نادرًا ولكن يجب تسليمها فورًا. الاستقصاء كل 5 ثوانٍ لحدث يحدث مرة واحدة في الساعة يهدر 719 طلبًا في الساعة.
  • لديك آلاف العملاء. كل عميل استقصاء هو اتصال مستقل. على نطاق واسع، تتراكم النفقات العامة لـ HTTP من إنشاء الاتصالات، وتحليل الرؤوس، والمصادقة.

Server-Sent Events: الوقت الفعلي الأبسط

تُعد Server-Sent Events (SSE) أكثر تقنيات الوقت الفعلي غير المستخدمة في تطوير الويب. توفر SSE تدفقًا أحادي الاتجاه من الخادم إلى العميل عبر اتصال HTTP قياسي. يتعامل المتصفح مع إعادة الاتصال تلقائيًا، والبروتوكول بسيط للغاية.

تطبيق الخادم (Server Implementation)

// Express.js SSE endpoint
app.get('/events/orders/:restaurantId', (req, res) => {
  const { restaurantId } = req.params;

  // SSE headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no', // Disable nginx buffering
  });

  // Send initial data
  const initialOrders = getActiveOrders(restaurantId);
  res.write(`data: ${JSON.stringify(initialOrders)}\n\n`);

  // Subscribe to order changes
  const unsubscribe = orderEmitter.on(`orders:${restaurantId}`, (order) => {
    res.write(`event: orderUpdate\n`);
    res.write(`data: ${JSON.stringify(order)}\n`);
    res.write(`id: ${order.id}-${order.updatedAt}\n\n`);
  });

  // Heartbeat to detect dead connections
  const heartbeat = setInterval(() => {
    res.write(`: heartbeat\n\n`);
  }, 30000);

  // Cleanup on disconnect
  req.on('close', () => {
    clearInterval(heartbeat);
    unsubscribe();
  });
});

تطبيق العميل (Client Implementation)

function useSSE(url: string) {
  const [data, setData] = useState(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    const eventSource = new EventSource(url);

    eventSource.onopen = () => setConnected(true);

    eventSource.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };

    eventSource.addEventListener('orderUpdate', (event) => {
      const order = JSON.parse(event.data);
      setData(prev => updateOrderInList(prev, order));
    });

    eventSource.onerror = () => {
      setConnected(false);
      // EventSource automatically reconnects
    };

    return () => eventSource.close();
  }, [url]);

  return { data, connected };
}

لماذا SSE بدلاً من WebSockets

إعادة الاتصال التلقائي. يتعامل تطبيق EventSource في المتصفح مع إعادة الاتصال مع التراجع الأسي (exponential backoff) تلقائيًا. مع WebSockets، تقوم بتنفيذ هذا بنفسك.

يعمل من خلال الوكلاء وموازنات التحميل. تستخدم SSE بروتوكول HTTP القياسي، لذا فهي تعمل من خلال nginx و CloudFlare ومعظم الوكلاء العكسيين بدون تكوين خاص. تتطلب WebSockets دعمًا صريحًا للوكيل.

مصادقة أبسط. تحمل اتصالات SSE ملفات تعريف الارتباط (cookies) ويمكنها استخدام رؤوس مصادقة HTTP القياسية. تتطلب مصادقة WebSocket إرسال بيانات الاعتماد بعد إنشاء الاتصال، وهو بروتوكول إضافي يجب تنفيذه.

أنواع الأحداث والمعرفات المضمنة. يدعم بروتوكول SSE الأحداث المسماة ومعرفات الرسائل، مما يمكّن العميل من استئناف العمل من حيث توقف بعد إعادة الاتصال.

قيود SSE

  • اتجاه واحد فقط. من الخادم إلى العميل. إذا كنت بحاجة إلى اتصال ثنائي الاتجاه، فلا يمكن لـ SSE القيام بذلك. يمكنك إقران SSE بطلبات HTTP POST العادية لرسائل العميل إلى الخادم، وهو ما يعمل جيدًا للعديد من حالات الاستخدام.
  • حدود اتصال المتصفح. تحد المتصفحات عدد اتصالات HTTP المتزامنة إلى نطاق واحد (عادة ستة). كل اتصال SSE يُحسب ضمن هذا الحد. يقلل تعدد الإرسال في HTTP/2 من هذا، ولكن من الجدير بالوعي به.
  • لا توجد بيانات ثنائية. SSE نصية فقط. إذا كنت بحاجة إلى بث بيانات ثنائية (صوت، فيديو، ملفات)، فاستخدم WebSockets أو آلية منفصلة.

WebSockets: اتصال ثنائي الاتجاه الكامل (Full Duplex Communication)

توفر WebSockets اتصالاً ثنائي الاتجاه الكامل — يمكن لكل من العميل والخادم إرسال الرسائل في أي وقت دون النفقات العامة لدورات طلب/استجابة HTTP. هذا ضروري عندما تحتاج إلى اتصال ثنائي الاتجاه ومنخفض زمن الاستجابة.

تطبيق الخادم باستخدام ws (Server Implementation with ws)

import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';

const server = createServer();
const wss = new WebSocketServer({ server });

// Connection management
const rooms = new Map<string, Set<WebSocket>>();

wss.on('connection', (ws, req) => {
  // Authenticate the connection
  const token = new URL(req.url!, `http://${req.headers.host}`).searchParams.get('token');
  const user = verifyToken(token);

  if (!user) {
    ws.close(4001, 'Unauthorized');
    return;
  }

  // Attach user data
  (ws as any).userId = user.id;

  ws.on('message', (raw) => {
    try {
      const message = JSON.parse(raw.toString());
      handleMessage(ws, user, message);
    } catch (e) {
      ws.send(JSON.stringify({ error: 'Invalid message format' }));
    }
  });

  ws.on('close', () => {
    // Remove from all rooms
    rooms.forEach((clients, roomId) => {
      clients.delete(ws);
      if (clients.size === 0) rooms.delete(roomId);
    });
  });

  // Heartbeat
  (ws as any).isAlive = true;
  ws.on('pong', () => { (ws as any).isAlive = true; });
});

// Dead connection detection
const heartbeatInterval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!(ws as any).isAlive) {
      ws.terminate();
      return;
    }
    (ws as any).isAlive = false;
    ws.ping();
  });
}, 30000);

function handleMessage(ws: WebSocket, user: any, message: any) {
  switch (message.type) {
    case 'join_room':
      joinRoom(ws, message.roomId);
      break;
    case 'leave_room':
      leaveRoom(ws, message.roomId);
      break;
    case 'broadcast':
      broadcastToRoom(message.roomId, {
        type: 'message',
        userId: user.id,
        content: message.content,
        timestamp: Date.now(),
      }, ws);
      break;
  }
}

function joinRoom(ws: WebSocket, roomId: string) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId)!.add(ws);
}

function broadcastToRoom(roomId: string, data: any, exclude?: WebSocket) {
  const clients = rooms.get(roomId);
  if (!clients) return;

  const payload = JSON.stringify(data);
  clients.forEach((client) => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(payload);
    }
  });
}

server.listen(8080);

تطبيق العميل مع إعادة الاتصال (Client Implementation with Reconnection)

class WebSocketClient {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private handlers = new Map<string, Set<(data: any) => void>>();

  constructor(url: string) {
    this.url = url;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.emit('connected', null);
    };

    this.ws.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data);
        this.emit(message.type, message);
      } catch (e) {
        console.error('Failed to parse WebSocket message:', e);
      }
    };

    this.ws.onclose = (event) => {
      this.emit('disconnected', { code: event.code, reason: event.reason });

      if (event.code !== 4001 && this.reconnectAttempts < this.maxReconnectAttempts) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = () => {
      // Error is followed by close event, handle reconnection there
    };
  }

  private scheduleReconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;

    setTimeout(() => {
      this.emit('reconnecting', { attempt: this.reconnectAttempts });
      this.connect();
    }, delay);
  }

  send(type: string, data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }));
    }
  }

  on(event: string, handler: (data: any) => void) {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event)!.add(handler);
    return () => this.handlers.get(event)?.delete(handler);
  }

  private emit(event: string, data: any) {
    this.handlers.get(event)?.forEach((handler) => handler(data));
  }

  disconnect() {
    this.maxReconnectAttempts = 0; // Prevent reconnection
    this.ws?.close();
  }
}

استراتيجية إعادة الاتصال (Reconnection Strategy)

تستحق منطق إعادة الاتصال الاهتمام لأنه يؤثر بشكل مباشر على تجربة المستخدم وحمل الخادم.

التراجع الأسي (Exponential backoff) يمنع مشاكل "القطيع الهادر" (thundering herd problems). إذا أعيد تشغيل الخادم الخاص بك وقام 10,000 عميل بإعادة الاتصال في وقت واحد، فسيتعطل الخادم مرة أخرى. بإضافة التذبذب (jitter) والتأخيرات الأسية، تنتشر الاتصالات بمرور الوقت:

function getReconnectDelay(attempt: number): number {
  const baseDelay = 1000;
  const maxDelay = 30000;
  const exponentialDelay = baseDelay * Math.pow(2, attempt);
  const jitter = Math.random() * 1000; // Random jitter up to 1 second
  return Math.min(exponentialDelay + jitter, maxDelay);
}

واجهة مستخدم حالة الاتصال مهمة. يحتاج المستخدمون إلى معرفة متى تم قطع اتصالهم ومتى يحاول التطبيق إعادة الاتصال. لافتة صغيرة تقول "إعادة الاتصال..." أفضل من إظهار بيانات قديمة بصمت.

Socket.IO: الراحة مقابل التحكم

Socket.IO هي مكتبة WebSockets الأكثر شيوعًا، وتوفر راحة كبيرة: إعادة الاتصال التلقائي، إدارة الغرف، الإقرارات، والعودة إلى الاستقصاء عندما تكون WebSockets غير متاحة. كما أنها تضيف نفقات عامة كبيرة وتُدخل طبقة من التجريد يمكن أن تخفي المشاكل.

متى تستخدم Socket.IO

  • النماذج الأولية السريعة. واجهة برمجة تطبيقات Socket.IO أبسط من WebSockets الخام، والميزات المضمنة توفر وقت التطوير.
  • عندما تحتاج إلى العودة إلى الاستقصاء. بعض شبكات الشركات تحظر اتصالات WebSocket. تعود Socket.IO تلقائيًا إلى HTTP long polling.
  • إدارة الغرف والمساحات الاسمية. إذا كانت ميزة الوقت الفعلي الخاصة بك تتوافق بشكل طبيعي مع الغرف (غرف الدردشة، ردهات الألعاب، المستندات التعاونية)، فإن تجريدات الغرف في Socket.IO توفر الكثير من التعليمات البرمجية المتكررة.

متى تتجنب Socket.IO

  • تطبيقات عالية الأداء. يضيف بروتوكول Socket.IO نفقات عامة لكل رسالة (تأطير، ترميز، بيانات وصفية). بالنسبة للتطبيقات ذات الإنتاجية العالية، تكون WebSockets الخام أسرع بشكل ملحوظ.
  • عندما تحتاج إلى قابلية التشغيل البيني. Socket.IO ليست WebSocket قياسية — لا يمكن لعميل WebSocket عادي الاتصال بخادم Socket.IO. إذا كان عملاؤك يشملون بيئات غير JavaScript (تطبيقات الجوال، أجهزة إنترنت الأشياء)، فإن Socket.IO تتطلب مكتبة عميل لكل منصة.
  • عندما تريد فهم ما يحدث. تجرد Socket.IO إدارة الاتصال، وهو أمر مريح حتى يحدث خطأ ما. غالبًا ما يعني تصحيح مشكلة اتصال Socket.IO فهم طبقة التجريد فوق المشكلة الفعلية.

Firebase Realtime Database و Firestore

توفر Firebase إمكانيات الوقت الفعلي دون إدارة أي بنية تحتية لـ WebSocket. هذه هي قيمتها الأساسية — أنت تتخلى عن التحكم مقابل الراحة.

Firebase Realtime Database

import { getDatabase, ref, onValue, set } from 'firebase/database';

const db = getDatabase();

// Listen for real-time updates
const ordersRef = ref(db, `restaurants/${restaurantId}/orders`);
onValue(ordersRef, (snapshot) => {
  const orders = snapshot.val();
  updateUI(orders);
});

// Write data (triggers listeners on all connected clients)
await set(ref(db, `restaurants/${restaurantId}/orders/${orderId}`), {
  status: 'preparing',
  updatedAt: Date.now(),
});

قاعدة بيانات Realtime Database هي شجرة JSON. كل عملية كتابة إلى أي عقدة تنتشر فورًا إلى كل عميل يستمع إلى تلك العقدة أو أي عقدة أصل. هذا قوي ولكنه خطير — يستقبل المستمع على عقدة عالية المستوى تحديثات لكل تغيير في الشجرة الفرعية بأكملها.

Firestore Real-Time Listeners

import { getFirestore, collection, onSnapshot, query, where } from 'firebase/firestore';

const db = getFirestore();

// Listen for active orders
const q = query(
  collection(db, 'orders'),
  where('restaurantId', '==', restaurantId),
  where('status', 'in', ['pending', 'preparing', 'ready'])
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === 'added') addOrder(change.doc.data());
    if (change.type === 'modified') updateOrder(change.doc.data());
    if (change.type === 'removed') removeOrder(change.doc.id);
  });
});

مستمعو الوقت الفعلي في Firestore أكثر دقة من Realtime Database — يمكنك الاستماع إلى استعلامات محددة، وليس فقط المسارات. تخبرك طريقة docChanges() بالضبط ما الذي تغير، مما يجعل تحديثات واجهة المستخدم الفعالة ممكنة.

Supabase Realtime

تقدم Supabase إمكانيات الوقت الفعلي فوق PostgreSQL، مما يمنحك قوة الاستعلام في SQL مع تحديثات الوقت الفعلي. تستخدم ميزات النسخ المتماثل في PostgreSQL (النسخ المتماثل المنطقي و WAL) لاكتشاف التغييرات وبثها عبر قنوات WebSocket.

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(url, key);

// Listen for changes to the orders table
const channel = supabase
  .channel('orders')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'orders',
      filter: `restaurant_id=eq.${restaurantId}`,
    },
    (payload) => {
      console.log('Change type:', payload.eventType);
      console.log('New data:', payload.new);
      console.log('Old data:', payload.old);
    }
  )
  .subscribe();

الميزة على Firebase واضحة: بياناتك تعيش في PostgreSQL مع إمكانيات استعلام SQL الكاملة، ومعاملات ACID، والأدوات القياسية. طبقة الوقت الفعلي هي إضافة، وليست بديلاً لمحرك الاستعلام الخاص بك. المقايضة هي أن Supabase Realtime أقل اختبارًا على نطاق واسع من Firebase، ويعتمد أداء الوقت الفعلي على إنتاجية النسخ المتماثل في PostgreSQL.

توسيع نطاق اتصالات WebSocket (Scaling WebSocket Connections)

يمكن لخادم واحد التعامل مع عشرات الآلاف من اتصالات WebSocket، ولكن التوسع إلى ما بعد خادم واحد يُدخل مشكلة تنسيق: رسالة منشورة على الخادم A تحتاج إلى الوصول إلى العملاء المتصلين بالخادم B.

نمط الناشر/المشترك (The Pub/Sub Pattern)

Redis pub/sub هو الحل القياسي:

import { createClient } from 'redis';

const publisher = createClient();
const subscriber = createClient();

await publisher.connect();
await subscriber.connect();

// When a message comes in on any server, publish to Redis
function publishToRoom(roomId: string, message: any) {
  publisher.publish(`room:${roomId}`, JSON.stringify(message));
}

// Each server subscribes and forwards to its local connections
await subscriber.subscribe(`room:${roomId}`, (message) => {
  const data = JSON.parse(message);
  broadcastToLocalClients(roomId, data);
});

يتيح لك هذا النمط تشغيل خوادم WebSocket متعددة خلف موازن تحميل. يتعامل كل خادم مع اتصالاته الخاصة، وينسق Redis الاتصال عبر الخوادم.

الجلسات الثابتة (Sticky Sessions)

اتصالات WebSocket ذات حالة — بمجرد إنشائها، يجب أن تظل على نفس الخادم. تحتاج موازنات التحميل إلى جلسات ثابتة (تسمى أيضًا تقارب الجلسة) لتوجيه اتصال WebSocket للعميل إلى نفس الخادم الذي تعامل مع ترقية HTTP الأولية.

مع nginx:

upstream websocket_servers {
    ip_hash;  # Sticky sessions based on client IP
    server ws1.example.com:8080;
    server ws2.example.com:8080;
}

server {
    location /ws {
        proxy_pass http://websocket_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400;  # 24 hours
    }
}

الوقت الفعلي مقابل الوقت شبه الفعلي (Real-Time vs Near-Real-Time)

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

النهج زمن الاستجابة التعقيد الأفضل لـ
تحديث الصفحة يبدأه المستخدم لا شيء المحتوى الثابت، التغييرات غير المتكررة
الاستقصاء (30 ثانية) حتى 30 ثانية منخفض عدد الإشعارات، تحديثات لوحة المعلومات
الاستقصاء (5 ثوانٍ) حتى 5 ثوانٍ منخفض خلاصات الأنشطة، حالة الطلب
الاستقصاء الطويل أقل من ثانية متوسط الأنظمة القديمة، البيئات المعادية للوكيل
SSE أقل من ثانية متوسط الخلاصات المباشرة، الإشعارات، التدفقات أحادية الاتجاه
WebSockets أقل من ثانية عالٍ الدردشة، التعاون، الوقت الفعلي ثنائي الاتجاه
WebRTC أجزاء من الثانية عالٍ جدًا الفيديو/الصوت، من نظير إلى نظير

النهج الصحيح هو الأبسط الذي يلبي متطلبات زمن الاستجابة لديك. ابدأ بالاستقصاء. إذا اشتكى المستخدمون من عدم حداثة البيانات، فقم بالترقية إلى SSE. إذا كنت بحاجة إلى اتصال ثنائي الاتجاه، فاستخدم WebSockets. يجب تبرير كل خطوة في سلم التعقيد بحاجة مستخدم ملموسة، وليس بمتطلب مستقبلي افتراضي.

توصيات عملية

بعد بناء ميزات الوقت الفعلي عبر عدة مشاريع، هذه هي الأنماط التي أعود إليها باستمرار:

الافتراضي هو SSE للوقت الفعلي أحادي الاتجاه. معظم ميزات الوقت الفعلي هي من الخادم إلى العميل: تحديثات الطلبات، تدفقات الإشعارات، خلاصات البيانات المباشرة. تتعامل SSE مع كل هذه الأمور بتعقيد أقل من WebSockets.

استخدم WebSockets فقط للاحتياجات ثنائية الاتجاه. الدردشة، التحرير التعاوني، التفاعلات متعددة اللاعبين — هذه تحتاج حقًا إلى WebSockets. إذا كان عميلك يتلقى البيانات فقط، فإن SSE أبسط.

نفذ دائمًا إعادة الاتصال مع التراجع (backoff). تنقطع الاتصالات. تتغير الشبكات. تعاد تشغيل الخوادم. يحتاج عميلك إلى التعامل مع هذا بأناقة، ويحتاج خادمك إلى النجاة من عاصفة إعادة الاتصال.

أرسل الفروقات (diffs)، وليس الحالة الكاملة. بدلاً من إرسال قائمة الطلبات بأكملها في كل مرة يتغير فيها طلب واحد، أرسل الطلب الذي تغير فقط. يطبق العميل الفرق على حالته المحلية. هذا يقلل من عرض النطاق الترددي والمعالجة على كلا الجانبين.

امتلك خطة احتياطية. إذا فشل اتصال WebSocket الخاص بك ولم يتمكن من إعادة الاتصال، يجب أن يظل التطبيق يعمل. مؤشر "آخر تحديث قبل 30 ثانية" مع تحديث يدوي أفضل من شاشة فارغة.

راقب عدد الاتصالات في الإنتاج. تستهلك اتصالات WebSocket موارد الخادم (الذاكرة، واصفات الملفات). يمكن أن يؤدي الارتفاع المفاجئ في الاتصالات — من خطأ في العميل يتسبب في إعادة الاتصال السريع، على سبيل المثال — إلى تعطيل الخادم الخاص بك. قم بتنبيه عند وجود شذوذ في عدد الاتصالات.

الهدف ليس امتلاك بنية الوقت الفعلي الأكثر تعقيدًا. الهدف هو تسليم البيانات للمستخدمين بسرعة كافية ليشعر التطبيق بالاستجابة، بأقل قدر من تعقيد البنية التحتية الذي يحقق هذا الشعور.

DU

Danil Ulmashev

Full Stack Developer

مهتم بالعمل معًا؟