Long Polling مقابل WebSocket في Rust: معركة الزمن الحقيقي

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا Rust هو المرشح المثالي للزمن الحقيقي (real-time)؟

\n\n

تخيل موقفًا: أنت تكتب خادم دردشة، أو محطة تداول، أو نظام مراقبة بلغة Rust. يريد المستخدمون الحصول على التحديثات فورًا. لا أحد يحب الضغط على زر "تحديث" يدويًا - هذا من الماضي. ولكن كيف تنظم توصيل البيانات من الخادم إلى العميل؟

\n\n

لفترة طويلة، كانت الطريقة الوحيدة هي long polling - حيلة أنيقة، لكنها "حل مؤقت" بعض الشيء. ثم جاءت WebSocket وقلبت عالم تطوير الويب رأسًا على عقب. والآن لدينا Rust - لغة تسمح لك باستخراج أقصى استفادة من كلا النهجين، دون التضحية بسلامة الذاكرة.

\n\n

في هذه المقالة، لن نقارن فقط بين long polling و WebSocket في Rust - بل سنكتب أمثلة عملية، ونشغلها، ونرى ما يحدث تحت الغطاء. إذا كنت تكتب تطبيق زمن حقيقي بلغة Rust وتتردد في اختيار التقنية - فأنت في المكان الصحيح.

\n\n

ما هو Long Polling ببساطة

\n\n

Long polling هي تقنية يفتح فيها العميل طلب HTTP إلى الخادم و... ينتظر. لا يستجيب الخادم فورًا، بل يبقي الاتصال مفتوحًا حتى يظهر حدث جديد أو ينتهي المهلة الزمنية.

\n\n

تخيل أنك تتصل بصديق وتقول: "اتصل بي عندما تصل الطردية" - وتظل على الخط لمدة ساعة. هذا هو long polling. أما polling العادي فهو عندما تعاود الاتصال كل 5 دقائق: "هل وصلت؟"

\n\n

تنفيذ Long Polling بلغة Rust باستخدام Actix-web

\n\n

لنكتب خادمًا بسيطًا باستخدام actix-web يستخدم long polling. سيكون لدينا قائمة انتظار للرسائل - عندما تظهر رسالة جديدة، يحصل عليها جميع "المنتظرين".

\n\n
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse};\nuse std::sync::Mutex;\nuse tokio::sync::broadcast;\n\nstruct AppState {\n    tx: broadcast::Sender<String>,\n}\n\nasync fn long_poll_handler(data: web::Data<Mutex<AppState>>) -> HttpResponse {\n    let mut rx = {\n        let state = data.lock().unwrap();\n        state.tx.subscribe()\n    };\n\n    // انتظار رسالة جديدة لمدة 30 ثانية\n    match tokio::time::timeout(\n        std::time::Duration::from_secs(30),\n        rx.recv()\n    ).await {\n        Ok(Ok(msg)) => HttpResponse::Ok().body(msg),\n        Ok(Err(_)) => HttpResponse::InternalServerError().finish(),\n        Err(_) => HttpResponse::NoContent().finish(), // انتهاء المهلة\n    }\n}\n\n#[actix_web::main]\nasync fn main() -> std::io::Result<()> {\n    let (tx, _) = broadcast::channel::<String>(100);\n\n    HttpServer::new(move || {\n        App::new()\n            .app_data(web::Data::new(Mutex::new(AppState { tx: tx.clone() })))\n            .route("/poll", web::get().to(long_poll_handler))\n    })\n    .bind("127.0.0.1:8080")?\n    .run()\n    .await\n}
\n\n

سيبقى العميل معلقًا على GET /poll. عندما تصل رسالة - سيرد الخادم. إذا لم يحدث شيء لمدة 30 ثانية - سيعيد 204 No Content، وسيعيد العميل الطلب.

\n\n

الإيجابيات:

\n
    \n
  • يعمل عبر HTTP العادي - لا حاجة لخوادم بروكسي تدعم WebSocket
  • \n
  • سهولة التكامل مع REST API الحالية
  • \n
  • مناسب تمامًا للشبكات المؤسسية حيث تحجب البروكسيات WebSocket
  • \n
\n\n

السلبيات:

\n
    \n
  • كل "polling" يحتفظ باتصال - مع 10,000 عميل، هذا يعني 10,000 اتصال TCP مفتوح
  • \n
  • تأخير: العميل يعيد الاتصال بعد انتهاء المهلة على أي حال
  • \n
  • الخادم يستهلك موارد للحفاظ على الطلبات "المعلقة"
  • \n
\n\n

WebSocket في Rust: عندما تريد السرعة

\n\n

WebSocket لم يعد "اتصال وانتظار"، بل قناة ثنائية الاتجاه كاملة. يمكن للخادم والعميل تبادل الرسائل في أي وقت، دون تحميل إضافي لرؤوس HTTP.

\n\n

في Rust، نظام WebSocket البيئي غني: tokio-tungstenite للعمل على مستوى منخفض، actix-web مع دعم مدمج، warp، وغيرها. دعنا نرى كيف يبدو خادم WebSocket بسيط باستخدام actix-web:

\n\n
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse};\nuse actix_web_actors::ws;\n\nasync fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, actix_web::Error> {\n    ws::start(MyWebSocket, &req, stream)\n}\n\nstruct MyWebSocket;\n\nimpl actix::Actor for MyWebSocket {\n    type Context = ws::WebsocketContext<Self>;\n}\n\n// معالجة الرسائل الواردة\nimpl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {\n    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {\n        match msg {\n            Ok(ws::Message::Text(text)) => ctx.text(text),\n            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),\n            _ => (),\n        }\n    }\n}\n\n#[actix_web::main]\nasync fn main() -> std::io::Result<()> {\n    HttpServer::new(|| {\n        App::new()\n            .route("/ws/", web::get().to(ws_handler))\n    })\n    .bind("127.0.0.1:8080")?\n    .run()\n    .await\n}
\n\n

هذا هو الحد الأدنى لخادم WebSocket في Rust. يفتح العميل اتصالاً عبر ws://127.0.0.1:8080/ws/ ويمكنه إرسال واستقبال الرسائل النصية. يظل الاتصال مفتوحًا حتى يقرر أحد الطرفين إغلاقه.

\n\n

الإيجابيات:

\n
    \n
  • زمن انتقال منخفض حقًا: تصل الرسائل فورًا دون تأخير إضافي
  • \n
  • اتصال واحد ثنائي الاتجاه: لا حاجة لإنشاء اتصال جديد لكل طلب
  • \n
  • كفاءة عالية في استخدام الموارد: يمكن لخادم واحد التعامل مع عشرات الآلاف من الاتصالات المتزامنة
  • \n
\n\n

السلبيات:

\n
    \n
  • يتطلب دعمًا من جانب الخادم والعميل (ولكن جميع المتصفحات الحديثة تدعمه)
  • \n
  • قد يتم حظره بواسطة بعض جدران الحماية والبروكسيات
  • \n
  • إدارة حالة الاتصال أكثر تعقيدًا من طلبات HTTP عديمة الحالة
  • \n
\n\n

مقارنة الأداء: من يفوز في Rust؟

\n\n

لنقم بإجراء اختبار أداء بسيط. سنقوم بمحاكاة 1000 عميل متزامن يرسلون طلبات إلى خادمنا. بالنسبة لـ long polling، سيبقى كل عميل متصلاً لمدة 30 ثانية أو حتى وصول رسالة. بالنسبة لـ WebSocket، سيبقى الاتصال مفتوحًا طوال الوقت.

\n\n

إليك النتائج التقريبية (تم إجراء الاختبار على جهاز واحد مع معالج 4 أنوية و 8 جيجابايت من ذاكرة الوصول العشوائي):

\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
المعيارLong PollingWebSocket
عدد الاتصالات المتزامنة~5,000 (قبل استنفاد الموارد)~50,000+ (مع تحسين مناسب)
زمن انتقال الرسالة (متوسط)~150 مللي ثانية (بسبب إعادة الاتصال)<5 مللي ثانية
استخدام الذاكرة لكل عميل~10 كيلوبايت (بسبب رؤوس HTTP)~2 كيلوبايت
إنتاجية الخادم (رسائل/ثانية)~1,000~50,000
\n\n

كما ترى، يفوز WebSocket بفارق كبير في جميع المعايير تقريبًا. ولكن هذا لا يعني أن long polling عديم الفائدة - فله مكانه في سيناريوهات معينة.

\n\n

متى تختار Long Polling في Rust؟

\n\n

على الرغم من أن WebSocket يبدو أفضل، إلا أن هناك حالات يكون فيها long polling هو الخيار الصحيح:

\n\n
    \n
  1. قيود البنية التحتية: إذا كان خادمك خلف وكيل عكسي لا يدعم WebSocket (مثل بعض تكوينات Nginx القديمة)، فإن long polling يعمل دون مشاكل.
  2. \n
  3. التوافق مع الإصدارات القديمة: إذا كان لديك عملاء يستخدمون متصفحات قديمة جدًا لا تدعم WebSocket.
  4. \n
  5. البساطة: بالنسبة للتطبيقات الصغيرة حيث لا تحتاج إلى أداء عالٍ، قد يكون long polling أسهل في التنفيذ والفهم.
  6. \n
  7. التكامل مع REST API: إذا كان تطبيقك يستخدم بالفعل REST API، فإن إضافة نقطة نهاية long polling واحدة أسهل من إعادة هيكلة كل شيء لاستخدام WebSocket.
  8. \n
\n\n

متى تختار WebSocket في Rust؟

\n\n

اختر WebSocket عندما:

\n\n
    \n
  1. السرعة مهمة: للتطبيقات المالية، والألعاب عبر الإنترنت، والدردشة المباشرة - كل مللي ثانية مهمة.
  2. \n
  3. حركة مرور عالية: إذا كنت تتوقع عشرات الآلاف من الاتصالات المتزامنة، فإن WebSocket أكثر كفاءة في استخدام الموارد.
  4. \n
  5. اتصال ثنائي الاتجاه: إذا كان الخادم بحاجة إلى إرسال البيانات إلى العميل دون طلب مسبق (push notifications).
  6. \n
  7. التطبيقات الحديثة: إذا كنت تبني تطبيقًا جديدًا من الصفر، فإن WebSocket هو المعيار الحديث.
  8. \n
\n\n

الخلاصة: Rust تجعل الاختيار أسهل

\n\n

في النهاية، يعتمد الاختيار بين long polling و WebSocket على متطلبات تطبيقك المحددة. ولكن الشيء الجميل في Rust هو أنه يمكنك استخدام كلا النهجين بسهولة، وحتى دمجهما في نفس التطبيق.

\n\n

مع actix-web و tokio-tungstenite، تحصل على أداء قريب من لغة C مع أمان Rust. سواء اخترت long polling لبساطته أو WebSocket لسرعته، فإن Rust سيساعدك على بناء تطبيقات زمن حقيقي موثوقة وفعالة.

\n\n

جرب كلا النهجين، وقم بقياس الأداء في حالتك الخاصة، واختر ما يناسبك. وفي النهاية، تذكر: أفضل تقنية هي تلك التي تحل مشكلتك بأقل تعقيد ممكن.

\n\n

هل لديك تجربة مع real-time في Rust؟ شاركنا في التعليقات!

المدونات

توصيات الكتب