رحلة مع الـ Server-Sent Events (SSE): كيفاش تبني Real-time Apps بلا تعقيدات

مروان بوفروج
مروان بوفروجفي Backend · March 25, 2026 · ١٥ دقيقة

فاش كنهضرو على الـ Real-time، غالباً أول حاجة كتجينا فبالنا هي الـ WebSocket. ولكن من بعد ما بحثت، لقيت بلي كين بزاف ديال الطرق، وكل وحدة عندها الـ Use case ديالها (واش الداتا غادة فجهة وحدة ولا فجوج جهات … إلخ إلخ ).

فبزاف ديال الحالات، الـ WebSocket كيكون غير Overhead زايد فـ الـ Infrastructure؛ كايخصك تخلص الثمن ديال الـ Stateful connections، ومشاكل الـ Load balancers (خاصك Sticky Sessions)، وزيد عليها الصداع ديال الـ Heartbeats باش الـ Connection ما تموتش.

فهاد المقال، غادي نركزو على الـ SSE (Server-Sent Events)، وعلاش فـ بزاف ديال الـ Scenarios كيكون هو الخيار الأذكى والأسهل، خصوصاً فاش كيكون الغرض هو غير Streaming ديال الـ Data من الـ Server لـ Client.

هاد المقال مستوحى من العرض الرائع اللي قدمو Azim Pulat (Ex-Google) في DevFest Warsaw 24. يلا بغيتي تعمق كتر، كنصحك بشدة تشوف التالك ديالو من هنا

علاش الـ Streaming؟

فاش كتسول Claude أو ChatGPT شي سؤال، كتلاحظ بلي الجواب كيبان كلمة بكلمة.
السؤال المطروح : علاش السيرفر ما كيتسناش حتى يكمل الجواب كامل عاد يصيفطو في HTTP Response وحدة؟

الجواب باختصار: تجربة المستخدم (UX). تخيل user كيتسنا 10 ثواني قدام Loading Spinner بلا ما يعرف واش السيستم خدام ولا لا. هادي تجربة كارثية. الحل هو نصيفطو كل جزء (Chunk) من الجواب فاش اجهز. التقنية اللي كتخلي هاد الـ Streaming ممكن وبأقل تكلفة هندسية (يعني سهلة) هي Server-Sent Events (SSE).


شنو هو Server-Sent Events (SSE)؟

الـ SSE هو اتصال HTTP عادي كيبقى مفتوح. باش البراوزر يفهم بلي هادا ماشي Response عادي، كاينين شروط تقنية فـ الـ Protocol Specification:

1. الـ Headers الضرورية

السيرفر خاصو يصيفط هاد الـ Headers باش ينجح الـ Stream:

  • Content-Type: text/event-stream: هادي هي اللي كتقول للبراوزر "بقا راد البال، الداتا جاية دقة دقة".

  • Cache-Control: no-cache: باش نمنعو أي Proxy فـ الطريق (بحال Nginx) أنه يخزن الداتا ويصيفطها دقة وحدة.

  • Connection: keep-alive: باش نحافظو على الـ TCP Connection مفتوحة.

2. شكل الداتا (Data Format)

الـ SSE هو Text-based protocol. الداتا خاصها تحترم فورما معينة، وإلا الـ EventSource فـ البراوزر ما غاديش يقراها:

  • كل رسالة خاصها تبدا بـ :data متبوعة بـ Payload.

  • كل رسالة خاصها تسالي بجوج سطور خاويين \n\n. بلا بيهم، البراوزر غايبقى يتسنى التتمة وما غاديش يعرض الداتا.

data: first word\n\n data: second word\n\n data: third word\n\n

التطبيق العملي: بناء SSE بـ Go و React

باش نفهمو هادشي عملياً، بنينا Demo صغير: سيرفر بـ Go كيقلّد AI Model كيولّد Tokens, one by one، و Frontend كيعرضهم في الحين.

1. السيرفر (Go)

باش تصاوب SSE Endpoint في Go، كتحتاج تضبط 3 ديال العناصر الأساسية:

// handler for "/event" func event(w http.ResponseWriter, _ *http.Request) { // إخبار البراوزر بالنوع ديال الستريم w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") tokens := []string{"hey", "how", "is", "it", "going"} for _, token := range tokens { // الفورما المعيارية: data: <content>\n\n fmt.Fprintf(w, "data: %s\n\n", token) // Flush() كتفرض على السيرفر يخرج الداتا من الـ Buffer للشبكة فورا w.(http.Flusher).Flush() time.Sleep(time.Millisecond * 500) } // إرسال Custom Event باش نعلمو الكليان بلي سالينا fmt.Fprint(w, "event: done\ndata: end_of_stream\n\n") w.(http.Flusher).Flush() }

شرح الميكانيزم:

  • text/event-stream: كتقول للبراوزر يخلي الـ Connection مفتوحة ويقرا الداتا بشكل متواصل.

  • Flush(): هادي هي الساروت. Go (وبزاف ديال اللغات) كيخزن الداتا في Buffer.
    Flush() كتفرض على السيرفر يصيفط داكشي اللي فالـ Buffer للكليان فوراً بلا ما يتسنا الـ Loop تسالي.

2. مشكل الـ Client-Side: علاش البراوزر مكيبينش الداتا؟

علاش وخا دير هادشي، مغديش إبنلك Real-time effect.

يلا جربتي الـ Endpoint مباشرة فـ الـ Address Bar، غالباً غتلاحظ بلي الـ Tokens كيبانو كاملين دقة وحدة فـ دقة وحدة. المشكل ماشي فـ السيرفر، وإنما فـ الـ Buffering Strategy ديال الـ Client:

  • Response-oriented (Browsers/Postman): كيتعامل مع الـ HTTP بمنطق الـ Full Payload. كيتسنى الـ Request يسالي كامل عاد كايدير ليه Render.

  • Stream-oriented (cURL/EventSource): كيقرا الـ Chunks بمجرد ما كيوصلو للـ Network Interface بلا ما يتسنى الـ Connection تسد.

باش تأكد أن الـ Server-side خدام، جرب هاد الكوماند فـ التيرمينال:

curl -N http://localhost:8080/event

(الـ Flag -N كيطفي الـ buffering فـ cURL)

3. بناء الكليان (React)

في الـ JavaScript، كنتعاملو مع الـ SSE عن طريق EventSource API:

useEffect(() => { const sse = new EventSource("http://localhost:8080/event"); // استقبال الميساجات العادية sse.onmessage = (e) => { setTokens((prev) => [...prev, e.data]); }; // استقبال حدث النهاية اللي صيفطنا من السيرفر sse.addEventListener("done", () => { sse.close(); }); // ضروري نسدو الكونيكسيون فاش كيموت الكومبوننت return () => sse.close(); }, []);

علاش sse.close مصيرية؟ الـ EventSource مصمم فأساسه لـ Live Feeds اللي ما كيساليوش. يلا السيرفر قطع الاتصال من جيهتو، البراوزر غيفترض أن وقع شي مشكل فالشبكة (Network Error) وغيحاول يدير Auto-reconnect أوتوماتيكياً ويطلب الداتا من جديد. باش نتفاداو هاد الـ Loop، السيرفر كيصيفط Custom Event (event: done)، والكليان كيستعمل close() باش يقتل الاتصال بطريقة نظيفة.

علاش SSE ماشي WebSockets؟ (The Practical Choice)

في حالة الـ AI Streaming، الـ SSE هو الخيار الأذكى (The Pragmatic Choice) لعدة أسباب هندسية:

  1. Over HTTP: الـ SSE خدام فوق HTTP عادي. ما كتحتاجش تدير "Upgrade" للبروتوكول بحال WebSockets.

  2. Unidirectional by design: في الـ Chatbot، الكليان كيصيفط Request وحدة (السؤال)، والسيرفر هو اللي كيبقى يصيفط بزاف ديال Responses (الكلمات). هاد "الاتجاه الواحد" هو بالضبط الاختصاص ديال SSE.

  3. Automatic Reconnection: البراوزر فيه ميكانيزم داخلي كيعاود الاتصال بوحدو يلا طاح، بلا ما تكتب تال سطر ديال الكود.

  4. Lightweight: ما كاينش Overhead ديال الـ Handshake.

WebSockets والـ WebRTC

الـ SSE مزيان لتطبيقات الـ LLM، ولكن عندو حدود واضحة: الاتجاه الواحد. السيرفر كيهضر والكليان كيسمع. فاش كنبغيو نبنيو تطبيق تفاعلي (Chat, Multiplayer Games)، كيتطلب منا الأمر ننتقلو لبروتوكولات أخرى.

WebSockets: ملي كتحتاج اتجاه ثنائي (Bi-directional)

الـ WebSocket كيبدا كاتصال HTTP عادي (Handshake)، ومن بعد كيدير Upgrade لبروتوكول مستقل ws:// أو wss://. هادشي كيخليه:

  • يدعم إرسال الداتا في الاتجاهين في نفس الوقت.

  • يدعم الـ Binary Data (ماشي غير النصوص).

WebRTC: ملي الـ Latency كيولي حياة أو موت

فاش كنبغيو نديرو Video/Audio Call بحال Google Meet، حتى WebSockets ما كينفعوش. مرور الداتا من السيرفر كيزيد من الـ Latency. هنا كيدخل WebRTC اللي كيخلي البراوزرات يتواصلو مع بعضياتهم مباشرة (Peer-to-Peer) عبر بروتوكول UDP عوض TCP، باش يتفاداو الـ التأخير اللي كيوقع فاش كتضيع شي Packet (Head-of-line blocking).

فمقال أخر نحاول ندويو و نفهمو كثر على WebSocket & webRTC

مقارنة سريعة

SSEWebSocketWebRTC
اتجاه الداتاسيرفر ← كليان (Uni-directional)ثنائي الاتجاه (Bi-directional)من كليان لـ كليان (Peer-to-Peer)
نوع الداتاText-basedText & BinaryVideo, Audio, & Arbitrary Data
أفضل استخدامLLM Streaming, Live News, NotificationsChat Apps, Gaming, Collaborative ToolsVideo Calls, Live P2P Streaming

الخلاصة

في HTTP/1.1، البراوزر مسموح ليه بـ 6 ديال الـ TCP Connections فقط لنفس الـ Domain. يلا بغيتي الـ App ديالك يسكايلي، تأكد أنك خدام بـ HTTP/2، اللي كيحل هاد المشكل عن طريق الـ Multiplexing (بزاف ديال الـ SSE Streams فـ كونيكسيون وحدة).

اختيار الطريقة المناسبة كيعتمد على المشكل اللي باغي تحل.

Don't over-engineer. ابدأ بـ SSE، ويلا احتاجيتي الـ Client يهضر مع السيرفر فـ نفس الوقت، عاد فكر فـ WebSockets

(الكود ديال الـ Demo متاح على GitHub).