لماذا تُعدّ البيانات الآنية ضرورة للشركات السورية؟
يُحدِّث موزّع جملة في دمشق جداول بياناته كلّ 15 دقيقة لمراقبة مخزون المستودع. ومشغّل لوجستي في حلب يتصل بالسائقين هاتفياً لمعرفة مواقعهم. ومراقب مالي في اللاذقية ينتظر تقارير نهاية اليوم لاكتشاف مشكلات التدفق النقدي.
كلّ هذه الأساليب تحمل تكلفة: قرارات متأخرة، وفرص إعادة تخزين فائتة، ومشكلات لا تُكتشف في وقتها. المشكلة الجوهرية ليست في غياب البيانات؛ إذ تتوفر البيانات عادةً، بل في زمن التسليم.
تُغلق لوحات التحكم الآنية المبنية على WebSockets هذه الفجوة؛ فبدلاً من استطلاع واجهة برمجية كلّ ثوانٍ، تُنشئ قناة ثنائية الاتجاه دائمة تُرسل التحديثات فور حدوثها، لتعكس لوحات التحكم مستويات المخزون الحية، ومواقع السائقين، وتأكيدات الدفع، وحالة خطوط الإنتاج — لحظةً بلحظة.
يتناول هذا الدليل البنية التقنية والتطبيق العملي، مع اعتبارات خاصة ببيئة الشبكات السورية.
---
1. المقارنة: HTTP Polling مقابل WebSockets مقابل SSE
قبل اللجوء إلى WebSockets، يستحق الأمر فهم متى يناسب كلّ أسلوب.
الاستطلاع الدوري (Short Polling)
الأبسط: يُرسل المتصفح طلب HTTP جديداً كلّ ثوانٍ.
// مثال توضيحي — غير جاهز للإنتاج
setInterval(async () => {
const res = await fetch('/api/dashboard/metrics');
const data = await res.json();
renderMetrics(data);
}, 5000);
المقايضات: سهل التطبيق، لكنه يُهدر حزمة البيانات وقدرة الخادم حتى حين لا تتغير البيانات. كل طلب يحمل ترويسات HTTP بحجم 500–800 بايت على اتصال موبايل محدود.
الأحداث المُرسَلة من الخادم (SSE)
بثّ أحادي الاتجاه من الخادم إلى المتصفح.
// مثال توضيحي — غير جاهز للإنتاج
const es = new EventSource('/api/stream/metrics');
es.addEventListener('metrics_update', (e) => {
renderMetrics(JSON.parse(e.data));
});
المقايضات: بسيط ويُعيد الاتصال تلقائياً، لكنه يقتصر على الاتجاه من الخادم إلى العميل.
WebSockets
قناة TCP ثنائية الاتجاه تُنشأ عبر HTTP Upgrade. بعد المصافحة، تكون حِزم الإطارات بحجم 2–10 بايت فقط، مما يُمكّن التواصل عالي التردد في الاتجاهين.
اختر WebSockets حين: تحتاج إلى مراسلة ثنائية الاتجاه، أو تحديثات بتردد عالٍ (أكثر من تحديث في الثانية)، أو تصفية مُدفوعة من الخادم.
---
2. نظرة عامة على البنية
تجمع لوحة التحكم الآنية في بيئة الإنتاج بين ثلاث طبقات:
┌────────────────────────────────────────────────────────────┐
│ المتصفح / التطبيق │
│ عميل WebSocket ←─────────────────────────────────────► │
└───────────────────────┬────────────────────────────────────┘
│ wss://
┌───────────────────────▼────────────────────────────────────┐
│ بوابة WebSocket (Node.js / Go / Python) │
│ - التحقق من الهوية وإدارة الجلسات │
│ - توجيه الاشتراكات │
│ - توزيع البثّ على العملاء المشتركين │
└───────────────────────┬────────────────────────────────────┘
│ Pub/Sub
┌───────────────────────▼────────────────────────────────────┐
│ وسيط الرسائل (Redis Pub/Sub أو NATS) │
│ - يفصل منتجي الأحداث عن بوابة WS │
│ - يُمكّن التوسع الأفقي لعُقد WS │
└───────────────────────┬────────────────────────────────────┘
│
┌───────────────────────▼────────────────────────────────────┐
│ خدمات العمل (مخزون، طلبات، لوجستيك، مدفوعات) │
│ تنشر أحداثاً عند تغيير الحالة │
└────────────────────────────────────────────────────────────┘
الفكرة الجوهرية: بوابة WebSocket لا تتصل مباشرةً بقاعدة البيانات عند كل إرسال. تُكتب تغييرات الحالة في قاعدة البيانات وتُنشر رسالة حدث خفيفة إلى الوسيط، ثم تُوزَّع على العملاء المشتركين.
---
3. تطبيق الخادم (Node.js + ws)
مكتبة ws هي خادم WebSocket خفيف وعالي الأداء لـ Node.js. الأمثلة التالية توضيحية وغير جاهزة للإنتاج — تفتقر إلى تقييد معدل الطلبات، وتعقيم المدخلات، ومعالجة الأخطاء الكاملة.
3.1 تثبيت التبعيات
npm install ws ioredis
3.2 بوابة WebSocket مع اشتراك Redis
// server.js — توضيحي، غير جاهز للإنتاج
import { WebSocketServer, WebSocket } from 'ws';
import { createClient } from 'redis';
import http from 'http';
const httpServer = http.createServer();
const wss = new WebSocketServer({ server: httpServer });
const subscriptions = new Map(); // channel → Set<WebSocket>
const subscriber = createClient({ url: process.env.REDIS_URL });
await subscriber.connect();
await subscriber.pSubscribe('dashboard:*', (message, channel) => {
const clients = subscriptions.get(channel) ?? new Set();
const frame = JSON.stringify({ channel, data: JSON.parse(message) });
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(frame);
}
}
});
wss.on('connection', (ws, req) => {
// يجب التحقق من رمز المصادقة قبل المتابعة
const clientChannels = new Set();
ws.on('message', (raw) => {
let msg;
try { msg = JSON.parse(raw); } catch { return; }
if (msg.type === 'subscribe' && typeof msg.channel === 'string') {
if (!subscriptions.has(msg.channel)) {
subscriptions.set(msg.channel, new Set());
}
subscriptions.get(msg.channel).add(ws);
clientChannels.add(msg.channel);
}
if (msg.type === 'unsubscribe' && clientChannels.has(msg.channel)) {
subscriptions.get(msg.channel)?.delete(ws);
clientChannels.delete(msg.channel);
}
});
ws.on('close', () => {
for (const ch of clientChannels) {
subscriptions.get(ch)?.delete(ws);
}
});
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// إنهاء الاتصالات المتوقفة كل 30 ثانية
setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30_000);
httpServer.listen(3001);
3.3 النشر من خدمة عمل
// inventory-service.js — توضيحي
await publisher.publish(
`dashboard:warehouse:${warehouseId}`,
JSON.stringify({
type: 'stock_update',
productId,
newQty: updatedQty,
ts: Date.now(),
})
);
---
4. تطبيق عميل المتصفح
// dashboard.js — توضيحي، غير جاهز للإنتاج
class RealtimeDashboard {
constructor(wsUrl, authToken) {
this.wsUrl = wsUrl;
this.authToken = authToken;
this.ws = null;
this.handlers = new Map();
this.reconnectDelay = 1000;
}
connect() {
this.ws = new WebSocket(`${this.wsUrl}?token=${this.authToken}`);
this.ws.onopen = () => {
this.reconnectDelay = 1000;
// إعادة الاشتراك في جميع القنوات بعد إعادة الاتصال
for (const channel of this.handlers.keys()) {
this._send({ type: 'subscribe', channel });
}
};
this.ws.onmessage = (event) => {
const { channel, data } = JSON.parse(event.data);
this.handlers.get(channel)?.(data);
};
this.ws.onclose = () => {
// تراجع أسّي للاتصال — ضروري للشبكات غير المستقرة
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
};
}
subscribe(channel, handler) {
this.handlers.set(channel, handler);
if (this.ws?.readyState === WebSocket.OPEN) {
this._send({ type: 'subscribe', channel });
}
}
_send(msg) { this.ws.send(JSON.stringify(msg)); }
}
---
5. التعامل مع واقع الشبكات في سوريا
تواجه اتصالات WebSocket المستمرة تحديات خاصة على البنية التحتية السورية: انقطاعات ADSL المتكررة، واختناقات 3G/4G، والبروكسيات الشفافة التي تُغلق الاتصالات الخاملة.
5.1 نبضات القلب (Heartbeat)
نبضة القلب من جهة الخادم (ws.ping()) تُحقق هدفين: اكتشاف الاتصالات المنقطعة، وإبقاء جلسة TCP حية خلال بوابات NAT والبروكسيات. فاصل زمني بين 25 و30 ثانية خيار عملي — أقصر من مهلة الخمود النموذجية للبروكسي (60 ثانية).
5.2 التراجع الأسّي للعميل
تُضاعف فئة RealtimeDashboard فترة انتظار إعادة الاتصال عند كل انقطاع حتى سقف 30 ثانية. هذا يمنع موجة إعادة اتصال جماعية حين تؤثر موجة انقطاع الشبكة على مستخدمين كثيرين في آنٍ واحد.
5.3 حالة ابتدائية هجينة (HTTP + WebSocket)
اجلب لقطة البيانات الأولية عبر HTTP فور تحميل الصفحة، ثم استخدم WebSocket للتحديثات الآنية:
// لقطة أولية عبر HTTP
const snapshot = await fetch('/api/warehouse/WH-01/snapshot').then(r => r.json());
renderInitialState(snapshot);
// الاتصال بـ WebSocket للتغييرات اللاحقة
dash.connect();
هذا النهج يمنع ظهور لوحة تحكم فارغة أثناء الاتصال.
5.4 إلزامية TLS / WSS
استخدم دائماً wss://. البروكسيات الشفافة في بعض بيئات مزودي الخدمة تتدخل في حركة ws:// غير المشفّرة وتُخلّ بالإطارات أو تُسقطها بصمت.
5.5 إعداد Nginx لـ WebSocket
# nginx.conf — توضيحي
server {
listen 443 ssl;
server_name api.example.sy;
location /ws {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
}
}
---
6. المصادقة والتفويض
لا تُعرِّض نقطة WebSocket بدون مصادقة. التحقق من رمز المصادقة يجب أن يتم فور الاتصال، وقبل قبول أيّ اشتراك في قناة. على مستوى القناة، تحقق من صلاحية المستخدم للوصول إلى البيانات المطلوبة قبل إضافته إلى مجموعة المشتركين.
---
7. التوسع لأكثر من عُقدة واحدة
تستطيع عملية Node.js الواحدة استيعاب عشرات الآلاف من اتصالات WebSocket المتزامنة، لكن عند الحاجة إلى عُقد متعددة للتكرار أو توازن الحمل، فإن نمط Redis Pub/Sub المبيَّن في القسم الثالث يحلّ هذا تلقائياً: كل عُقدة بوابة تشترك في Redis، وعندما تنشر خدمة عمل حدثاً، تستلمه جميع العُقد وتُوزّعه على عملائها المحليين.
---
8. اعتبارات واجهة لوحة التحكم
تقليص عمليات العرض: إذا تحدّثت مستويات المخزون 20 مرة في الثانية، أعد الرسم بشكل دوري (كل 60 مللي ثانية) بدلاً من كل رسالة.
مؤشر توقف البيانات: حين ينقطع WebSocket، اعرض شريط "جاري إعادة الاتصال..." لتنبيه المستخدم أن البيانات قد لا تكون محدّثة.
إبراز التغييرات: لوّن الخلايا التي تغيرت قيمها لحظياً (أخضر للزيادة، أحمر للنقصان) لتوجيه انتباه المستخدم دون الحاجة لمراقبة كل صف.
---
9. قائمة التحقق قبل الإطلاق
- [ ] فرض
wss://— لا مساراتws://غير مشفّرة في الإنتاج - [ ] التحقق من رمز المصادقة عند الاتصال وعند كل اشتراك في قناة
- [ ] نبضة قلب من الخادم لاكتشاف الاتصالات المتوقفة وإنهائها
- [ ] تطبيق التراجع الأسّي للاتصال على العميل
- [ ] إعداد Nginx بترويسات Upgrade ومهلة قراءة طويلة
- [ ] نقطة HTTP للحالة الأولية قبل اتصال WebSocket
- [ ] Redis Pub/Sub لبيئات متعددة العُقد
- [ ] التحقق من أسماء قنوات الاشتراك من جهة الخادم
---
الخلاصة
لوحات التحكم الآنية المبنية على WebSockets ليست بنية تحتية معقدة — بل هي ترقية عملية تستطيع الشركات السورية التي تُدير مستودعات أو أسطول شحن أو خطوط إنتاج أو عمليات مالية تطبيقها اليوم. البنية التقنية راسخة ومفهومة: طبقة Redis Pub/Sub تربط خدمات العمل القائمة ببوابة WebSocket خفيفة، دون أيّ تغيير في نموذج البيانات.
هل أنت مستعد لبناء لوحة تحكم عمليات آنية لشركتك؟ تواصل مع دراجون سوفت (Dragonfly Soft) لمناقشة متطلباتك مع فريقنا الهندسي.