متى يكون WebSocket أكثر من اللازم، أو لماذا عادت SSE إلى الموضة
\n\nتخيل: تحتاج إلى إرسال بيانات من الخادم إلى العميل في الوقت الفعلي. أسعار العملات، إشعارات الرسائل الجديدة، سجلات الخادم، تقدم معالجة الملفات. أول ما يتبادر إلى الذهن هو WebSocket. ولكن ماذا لو لم تكن بحاجة إلى قناة ثنائية الاتجاه؟ ماذا لو كان العميل يستمع فقط، والخادم يتحدث فقط؟
\n\nهنا تظهر الأحداث المرسلة من الخادم (SSE) — بروتوكول يسمح للخادم بإرسال البيانات إلى العميل عبر اتصال HTTP عادي. بدون مصافحة إضافية، بدون بروتوكولات معقدة، بدون صداع مع الوكلاء وموازنات التحميل. ونعم، في Rust يعمل هذا بشكل رائع.
\n\nفي هذه المقالة، سنشرح كيفية إعداد SSE بلغة Rust من الصفر باستخدام الأطر الشائعة. سترى أمثلة عملية، وستتعرف على المخاطر الخفية، وستفهم متى يكون SSE هو الخيار الأفضل لك.
\n\nما هي الأحداث المرسلة من الخادم (SSE) ولماذا Rust؟
\n\nSSE هو معيار W3C (واجهة برمجة تطبيقات EventSource) يسمح للخادم بإرسال بيانات متدفقة إلى العميل عبر HTTP. يتصل العميل بنقطة نهاية، ويبدأ الخادم في إرسال رسائل بتنسيق text/event-stream. يعمل كل شيء عبر HTTP/1.1 أو HTTP/2 العادي، ولا يتطلب وكلاء خاصين، وهو مدعوم من جميع المتصفحات الحديثة.
لماذا Rust؟ لأن Rust يمنحنا:
\n- \n
- عدم التزامن بدون مفاجآت — Tokio و async-std يسمحان بالحفاظ على آلاف اتصالات SSE على خيط واحد \n
- أمان الذاكرة — لا تسريبات، لا حالات سباق في التدفق متعدد الخيوط \n
- استهلاك ضئيل للموارد — اتصال SSE واحد يستهلك كيلوبايتات قليلة \n
- النظام البيئي — Actix Web، Rocket، Axum — جميعها تدعم SSE بشكل مدمج أو مع إضافات بسيطة \n
كيف يعمل SSE: تحليل البروتوكول
\n\nيقوم العميل بإنشاء EventSource في المتصفح، مع تحديد URL. يستجيب الخادم برأس Content-Type: text/event-stream ويبدأ في إرسال أسطر بالتنسيق:
data: مرحباً بالعالم!\\n\\n\n\nكل رسالة هي كتلة حيث data: تحتوي على الحمولة. يتم فصل الرسائل بسطرين جديدين. يمكن إرسال JSON، أرقام، نصوص — أي شيء. بالإضافة إلى ذلك، هناك حقول id و event و retry.
إليك مثال بسيط لرسالة SSE:
\n\nid: 1\nevent: update\ndata: {"status": "processing", "progress": 42}\n\n\n\nيتلقى العميل بلغة JavaScript هذا كحدث update مع حقل data يحتوي على JSON.
كتابة خادم SSE بلغة Rust باستخدام Actix Web
\n\nلنبدأ مع الإطار الأكثر شيوعًا — Actix Web. سنقوم بتثبيت التبعيات:
\n\n[dependencies]\nactix-web = "4"\nactix-rt = "2"\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n\nالآن سنكتب نقطة نهاية بسيطة ترسل الوقت الحالي كل ثانيتين:
\n\nuse actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, middleware};\nuse actix_web::rt::time;\nuse futures::stream::StreamExt;\nuse std::time::Duration;\n\n#[get("/events")]\nasync fn stream_events(req: HttpRequest) -> HttpResponse {\n let mut rx = tokio::sync::broadcast::channel::<String>(100).1;\n\n let stream = async_stream::stream! {\n loop {\n let message = format!("data: {}\\n\\n", chrono::Local::now());\n yield Ok::<_, actix_web::Error>(\n web::Bytes::from(message)\n );\n time::sleep(Duration::from_secs(2)).await;\n }\n };\n\n HttpResponse::Ok()\n .insert_header(("Content-Type", "text/event-stream"))\n .insert_header(("Cache-Control", "no-cache"))\n .insert_header(("Connection"