استضافة تطبيقات الوقت الحقيقي: WebSockets على سيرفرك خطوة بخطوة

WebSocket يفتح قناة دائمة بين المتصفح والسيرفر برسائل بحجم بايتات بدل طلبات HTTP كاملة متكررة، فيخفض التأخير والحمل بشكل كبير. الإعداد يحتاج خادماً عكسياً يمرّر wss، وحدّ الاتصالات المتزامنة يتحدد بالرام.
لاستضافة تطبيق وقت حقيقي (محادثة، إشعارات فورية، لوحة مباشرة، تعاون على مستند) تحتاج WebSocket، وهو بروتوكول يفتح قناة اتصال دائمة ثنائية الاتجاه بين المتصفح والسيرفر عبر اتصال TCP واحد يبقى مفتوحاً. بدل أن يسأل المتصفح السيرفر كل بضع ثوانٍ (polling)، يدفع السيرفر البيانات فور توفّرها. الفرق بالأرقام حاسم: طلب polling كل ثانية على 1000 مستخدم يعني 1000 طلب HTTP كامل في الثانية بترويسات تتجاوز 500 بايت لكل طلب، بينما WebSocket يبقي 1000 اتصال مفتوحاً ويرسل إطاراً بحجم 2 إلى 6 بايتات فوق الحمولة فقط عند وجود جديد.
الإعداد على سيرفرك ثلاث خطوات: تشغيل خادم WebSocket (مثل Laravel Reverb أو Soketi)، ثم ضبط خادم عكسي (Nginx) ليمرّر ترقية الاتصال إلى wss المشفّر، ثم فتح المنفذ في الجدار الناري. حدّ الاتصالات المتزامنة يتحدد أساساً بالرام: كل اتصال مفتوح يستهلك من عشرات إلى مئات الكيلوبايتات، فباقة بـ4GB رام تحمل عشرات الآلاف من الاتصالات الخاملة بسهولة. زمن الاستجابة نحو 8ms من الرياض على خادم سحابي سعودي يجعل الإحساس بالفورية حقيقياً لمستخدمك المحلي، بعكس سيرفر بعيد بتأخير 120ms. نفصّل الأرقام والإعداد كاملاً.
WebSocket مقابل Polling بالأرقام
الفرق ليس في الأناقة فقط، بل في الحمل والتأخير. لنقارن سيناريو 1000 مستخدم متصل يتوقع تحديثات فورية:
| المعيار | Polling كل ثانية | WebSocket |
|---|---|---|
| الطلبات في الثانية | 1000 طلب HTTP كامل | 0 (قناة مفتوحة) |
| حجم كل رسالة | ترويسات 500+ بايت لكل طلب | إطار 2 إلى 6 بايتات فوق الحمولة |
| التأخير قبل ظهور التحديث | حتى ثانية كاملة (فترة السؤال) | لحظي عند حدوث الحدث |
| حمل المعالج | عالٍ (بدء ومعالجة كل طلب) | منخفض (اتصال قائم) |
| استهلاك الرام لكل مستخدم | لا شيء بين الطلبات | عشرات إلى مئات الكيلوبايتات دائماً |
polling يهدر المعالج والنطاق في طلبات فارغة أغلب الوقت، ويضيف تأخيراً يساوي فترة السؤال. WebSocket يقلب المعادلة: حمل معالج منخفض وتأخير شبه معدوم، مقابل ذاكرة محجوزة للاتصالات المفتوحة. هذه المقايضة في صالح WebSocket لأي تطبيق فيه تحديثات متكررة. احسبها ببساطة: 1000 مستخدم بـpolling كل ثانية يعني 60 ألف طلب في الدقيقة يبدأ كل منها عملية PHP كاملة ويستعلم قاعدة البيانات، أغلبها يرجع فارغاً لأن لا جديد. نفس الألف على WebSocket لا يولّدون أي طلب حتى يقع حدث فعلي، فتنخفض دورات المعالج والاستعلامات إلى جزء يسير مما كانت عليه، وهذا وحده قد يؤجّل حاجتك لترقية موارد سيرفرك.
الخطوة الأولى: خادم WebSocket
لا يخدم PHP-FPM التقليدي WebSocket لأنه يعالج طلباً ثم ينتهي، بينما WebSocket يحتاج عملية طويلة العمر تبقي الاتصالات حية. الخياران العمليان في نظام Laravel:
- Laravel Reverb: خادم رسمي مكتوب بـPHP، يندمج مباشرة مع بث الأحداث في Laravel، سهل التشغيل بأمر artisan واحد.
- Soketi: خادم مفتوح المصدر متوافق مع بروتوكول Pusher، مبني على Node، خفيف وسريع.
كلاهما عملية دائمة تشغّلها تحت مشرف عمليات مثل supervisor أو systemd حتى تعيد تشغيلها تلقائياً عند أي سقوط. تستمع العملية على منفذ داخلي (مثل 8080)، ولا تعرّضه مباشرة للإنترنت، بل تمرّره عبر الخادم العكسي. الفرق العملي بين الخيارين أن Reverb يبقيك داخل نظام Laravel بالكامل فتبثّ الأحداث بنفس صياغة بثّ الأحداث التي تعرفها، بينما Soketi أخفّ وزناً ويناسبك لو أردت فصل خادم الاتصالات عن تطبيقك أو خدمة عدة تطبيقات من خادم اتصالات واحد. كلاهما يعمل جيداً على خادم سحابي بموارد متواضعة للبداية، والترقية لاحقة عند نمو الاتصالات.
الخطوة الثانية: ضبط Nginx لتمرير wss
WebSocket يبدأ بطلب HTTP عادي فيه ترويسة ترقية (Upgrade)، فلابد أن يمرّر Nginx هذه الترقية بدل إغلاقها. المفتاح ثلاث ترويسات في كتلة الموقع: تمرير Upgrade وConnection ورفع البروتوكول إلى HTTP/1.1. إضافة إلى ذلك، ارفع proxy_read_timeout إلى قيمة عالية (مثل 3600 ثانية) لأن الاتصال يبقى مفتوحاً طويلاً، وإلا يقطعه Nginx بعد المهلة الافتراضية القصيرة. مع شهادة SSL يصبح المسار wss المشفّر، وهو ما تطلبه المتصفحات على المواقع الآمنة، وشهادة Let's Encrypt المجانية تكفي.
خطأ شائع: مهلة القراءة القصيرة
لو نسيت رفع proxy_read_timeout، تنقطع الاتصالات كل 60 ثانية والعميل يعيد الاتصال باستمرار، فيبدو التطبيق متقطعاً. اضبط المهلة عالية، وفعّل رسائل ping/pong الدورية من الخادم للحفاظ على حياة الاتصال عبر أي وسيط.
حدّ الاتصالات المتزامنة حسب الرام
السؤال العملي: كم اتصالاً يحمل سيرفري؟ الجواب يعتمد على الرام أساساً، إضافة إلى سقف واصفات الملفات في النظام. كل اتصال WebSocket خامل يستهلك من 30 إلى 250 كيلوبايت حسب الخادم وحجم المخازن المؤقتة. تقدير تقريبي على باقات الخادم السحابي بافتراض توفّر نصف الرام للاتصالات ومتوسط 100 كيلوبايت للاتصال:
| الباقة | الرام | اتصالات خاملة متزامنة (تقريبي) |
|---|---|---|
| أساس | 2 GB | ~10,000 |
| نمو | 4 GB | ~20,000 |
| أعمال | 8 GB | ~40,000 |
| مؤسسة | 16 GB | ~80,000 |
هذه أرقام اتصالات خاملة، والاتصالات النشطة التي تتبادل رسائل كثيرة تستهلك أكثر بسبب المخازن المؤقتة ومعالجة المعالج. ارفع سقف واصفات الملفات في النظام (ulimit وnofile) لأن الافتراضي غالباً 1024 وهو أقل بكثير مما تحتاجه، فكل اتصال مفتوح يحجز واصف ملف واحداً على الأقل، والسقف المنخفض يوقف اتصالات جديدة قبل أن تنفد الرام أصلاً. لو تجاوزت آلاف الاتصالات النشطة، أضف رام: 2GB بـ600 ريال سنوياً ترفع السقف بنحو 10,000 اتصال خامل إضافي، وراقب استهلاك المعالج بالتوازي لأن الرسائل الكثيفة قد تُشبع نواة قبل أن تُشبع الذاكرة.
التوسّع الأفقي: أكثر من عملية WebSocket
عملية WebSocket واحدة محدودة بنواة معالج واحدة غالباً، فعند نمو الحركة تشغّل عدة عمليات وتوزّع الاتصالات بينها. المشكلة أن الاتصالات موزّعة على عمليات مختلفة، فرسالة تصل لعملية لابد أن تصل لمشتركين على عملية أخرى. الحل قناة نشر مشتركة عبر Redis Pub/Sub: كل عملية تنشر أحداثها على Redis، وكل العمليات تستمع، فتصل الرسالة لكل المشتركين مهما كانت العملية التي يتصلون بها. هذا النمط يسمح لسيرفر واحد قوي بخدمة عشرات الآلاف من الاتصالات النشطة موزّعة على عدة نوى، ثم بالتوسّع لعدة سيرفرات لاحقاً بنفس آلية Redis.
لماذا التأخير المحلي يفرق
في تطبيق وقت حقيقي، التأخير هو التجربة نفسها. رسالة محادثة أو تحديث لوحة يجب أن يظهر فوراً. سيرفر بعيد بتأخير 120ms ذهاباً وإياباً يعني ربع ثانية محسوسة في كل تفاعل، بينما 8ms من الرياض على خادم سعودي يجعل التحديث يبدو لحظياً. مع WebSocket، هذا التأخير يتكرر مع كل رسالة، فالفارق يتراكم في المحادثات الكثيفة والألعاب واللوحات المباشرة. الاستضافة القريبة من مستخدمك ليست رفاهية في تطبيق حي، بل شرط في جودة التجربة. أضف إلى ذلك أن البيانات السعودية تبقى داخل المملكة، وهو مطلب للجهات التي تشترط سيادة البيانات على قنواتها الحيّة لا صفحاتها فقط.
احتياطي عند سقوط الاتصال
الاتصالات الحيّة تنقطع أحياناً لأسباب خارج سيطرتك (شبكة الجوال، وسيط في الطريق، إعادة تشغيل الخادم). صمّم عميلك ليعيد الاتصال تلقائياً بفاصل متزايد (exponential backoff) بدل محاولات فورية متلاحقة تُغرق السيرفر عند عودته. عند إعادة الاتصال، أعد جلب الحالة الأخيرة عبر طلب HTTP عادي حتى لا يفوت المستخدم أي حدث وقع أثناء الانقطاع، فالجمع بين WebSocket للفورية وHTTP للاسترجاع يعطي تجربة متينة لا تنكسر عند أول تعثّر شبكة.
خطوات الإطلاق
- شغّل Laravel Reverb أو Soketi كعملية دائمة تحت supervisor أو systemd.
- اضبط Nginx لتمرير ترويسات الترقية ورفع proxy_read_timeout إلى 3600 ثانية.
- فعّل SSL ليعمل wss المشفّر، شهادة Let's Encrypt مجانية.
- افتح المنفذ الداخلي في الجدار الناري للخادم العكسي فقط لا للإنترنت.
- ارفع سقف واصفات الملفات (nofile) فوق 1024 بكثير حسب اتصالاتك المتوقعة.
- فعّل ping/pong الدورية لإبقاء الاتصالات حية ومراقبة استهلاك الرام.
بهذا الإعداد يخدم سيرفر واحد آلاف المستخدمين المتزامنين بتأخير محلي منخفض. ابدأ بباقة نمو للتطبيقات المتوسطة، وراقب الرام وواصفات الملفات، ورقِّ عند نمو الاتصالات النشطة. الفورية الحقيقية تبدأ من WebSocket صحيح الإعداد على سيرفر قريب من مستخدمك.


