SSEGWSW: بوابة الأحداث المرسلة بواسطة الخادم بواسطة عمال الخدمة

تحية!

اسمي ساشا وأعمل مهندسًا معماريًا في Tinkoff Business.

في هذه المقالة ، أريد أن أتحدث عن كيفية التغلب على حد المستعرض لعدد اتصالات HTTP المفتوحة طويلة العمر داخل نفس المجال باستخدام عامل الخدمة.

إذا كنت تريد ، فلا تتردد في تخطي الخلفية ووصف المشكلة والبحث عن حل والمتابعة الفورية إلى النتيجة.

SSEGWSW

قبل التاريخ


ذات مرة في Tinkoff Business ، كانت هناك محادثة عملت على Websocket .

بعد مرور بعض الوقت ، توقف عن تصميم حسابه الشخصي ، وكان يطلب بشكل عام كتابة طويلة من الزاوي 1.6 إلى الزاوي 2+. قررت أن الوقت قد حان للبدء في تحديثه. اكتشف أحد زملاء الخلفية أن واجهة الدردشة ستتغير ، واقترح في الوقت نفسه إعادة واجهة برمجة التطبيقات ، على وجه الخصوص - تغيير النقل من طبقة الويب إلى طبقة المقابس الآمنة (الأحداث التي يرسلها الخادم) . اقترح هذا لأنه عند تحديث التكوين NGINX تم قطع جميع الاتصالات ، والتي كانت مؤلمة لاستعادتها.

ناقشنا بنية الحل الجديد وتوصلنا إلى استنتاج مفاده أننا سوف نستقبل ونرسل البيانات باستخدام طلبات HTTP المعتادة. على سبيل المثال ، أرسل رسالة POST: / api / send-message ، واحصل على قائمة بمربعات حوار GET: / api / conversations-list ، وما إلى ذلك. وسيتم إرسال الأحداث غير المتزامنة مثل "رسالة جديدة من المحاور" عبر SSE. وبالتالي ، سنزيد من التسامح مع الخطأ في التطبيق: إذا تعطل اتصال SSE ، ستظل المحادثة تعمل ، ولن تتلقى إعلامات في الوقت الفعلي.

بالإضافة إلى الدردشة في websocket ، كنا نطارد الأحداث لمكون "الإعلامات الرفيعة". يتيح لك هذا المكون إرسال إشعارات متعددة إلى الحساب الشخصي للمستخدم ، على سبيل المثال ، أن عملية استيراد الحسابات ، والتي قد تستغرق عدة دقائق ، قد تم إكمالها بنجاح. للتخلي عن websocket بالكامل ، نقلنا هذا المكون إلى اتصال SSE منفصل.

المشكلة


عند فتح علامة تبويب متصفح واحدة ، يتم إنشاء اتصالين SSE: واحد للدردشة والآخر للإعلامات الدقيقة. حسنًا ، دعهم يبدعون. اسف ام ماذا لا نشعر بالأسف ، ولكن المتصفحات تشعر بالأسف! لديهم حد لعدد الاتصالات المستمرة المتزامنة للمجال . تخمين كم هو في كروم؟ صحيح ، ستة! لقد فتحت ثلاث علامات تبويب - لقد سجلت مجموعة الاتصال بالكامل ولم يعد بإمكانك تقديم طلبات HTTP. هذا صحيح بالنسبة لبروتوكول HTTP / 1.x. في HTTP / 2 لا توجد مشكلة من هذا القبيل بسبب تعدد الإرسال.

هناك طريقتان لحل هذه المشكلة على مستوى البنية التحتية:

  1. تقاسم المجال.
  2. HTTP / 2.

كلتا الطريقتين بدت باهظة الثمن ، لأن الكثير من البنية التحتية يجب أن تتأثر.

لذلك ، بالنسبة للمبتدئين ، حاولنا حل المشكلة من جانب المتصفح. كانت الفكرة الأولى هي إجراء نوع من النقل بين علامات التبويب ، على سبيل المثال ، من خلال واجهة برمجة تطبيقات LocalStorage أو Broadcast Channel .

المعنى هو: فتح اتصالات SSE في علامة تبويب واحدة فقط وإرسال البيانات إلى الباقي. لم يكن هذا الحل مثاليًا أيضًا ، حيث سيتطلب إصدار 50 SPA ، والتي تشكل حساب Tinkoff Business الشخصي. يعد إصدار 50 تطبيقًا مكلفًا أيضًا ، لذلك تابعت البحث عن طرق أخرى.

قرار


لقد عملت مؤخرًا مع عمال الخدمة وفكرت: هل من الممكن تطبيقها في هذه الحالة؟

للإجابة على هذا السؤال ، عليك أولاً أن تفهم ما الذي يقوم به عمال الخدمة عمومًا؟ يمكنهم تقديم طلبات بالوكالة ، يبدو كما يلي:

self.addEventListener('fetch', event => { const response = self.caches.open('example') .then(caches => caches.match(event.request)) .then(response => response || fetch(event.request)); event.respondWith(response); }); 

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

حسنًا ، دعنا نحاول اعتراض اتصال SSE والإجابة عليه:

 self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } event.respondWith(new Response('Hello!')); }); 

في طلبات الشبكة ، نرى الصورة التالية:

صورة

وفي وحدة التحكم ، هذا:

صورة

بالفعل ليست سيئة. تم اعتراض الطلب ، لكن SSE لا يريد استجابة في شكل نص / عادي ، لكنه يريد دفق النص / الحدث . كيفية إنشاء تيار الآن؟ لكن هل يمكنني حتى الرد على دفق من عامل خدمة؟ حسنًا لنرى:

صورة

! ممتاز تأخذ فئة الاستجابة كهيئة ReadableStream . بعد قراءة الوثائق ، يمكنك معرفة أن ReadableStream لديها وحدة تحكم بها طريقة enqueue () - بمساعدتها ، يمكنك دفق البيانات. مناسبة ، خذها!

 self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const responseText = 'Hello!'; const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream); event.respondWith(response); }); 

صورة

لا يوجد خطأ ، الاتصال معلقة في حالة معلقة ولا توجد بيانات تصل من جانب العميل. بمقارنة طلبي مع طلب خادم حقيقي ، أدركت أن الإجابة كانت في الرؤوس. لطلبات SSE ، يجب تحديد الرؤوس التالية:

 const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', }; 

عند إضافة هذه الرؤوس ، سيتم فتح الاتصال بنجاح ، ولكن لن يتم استلام البيانات من جانب العميل. هذا واضح ، لأنه لا يمكنك فقط إرسال نص عشوائي - يجب أن يكون هناك بعض التنسيق.

في javascript.info ، يتم وصف تنسيق البيانات الذي تريد إرسال البيانات به من الخادم بشكل جيد . يمكن وصفها بسهولة بوظيفة واحدة:

 const sseChunkData = (data: string, event?: string, retry?: number, id?: number): string => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n'; 

للامتثال لتنسيق SSE ، يجب أن يرسل الخادم رسائل مفصولة بفواصل الأسطر المزدوجة \ n \ n .

تتكون الرسالة من الحقول التالية:

  • البيانات - نص الرسالة ، يتم تفسير العديد من البيانات في صف واحد كرسالة واحدة ، مفصولة بفواصل الأسطر \ n ؛
  • id - بتحديث خاصية lastEventId المرسلة في رأس Last-Event-ID عند إعادة الاتصال ؛
  • إعادة المحاولة - التأخير الموصى به قبل إعادة الاتصال بالمللي ثانية ، لا يمكن تعيينه باستخدام JavaScript ؛
  • الحدث - اسم حدث المستخدم ، المشار إليه قبل البيانات.

أضف الرؤوس اللازمة ، وقم بتغيير الإجابة إلى التنسيق المطلوب وشاهد ما يحدث:

 self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const sseChunkData = (data, event, retry, id) => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n'; const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', }; const responseText = sseChunkData('Hello!'); const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream, {headers: sseHeaders}); event.respondWith(response); }); 

صورة

يا عالمي نعم ، لقد أجريت اتصال SSE بدون خادم!

يؤدي


الآن يمكننا اعتراض طلب SSE والرد عليه دون تجاوز المستعرض بنجاح.

في البداية ، كانت الفكرة هي إقامة اتصال مع الخادم ، ولكن هناك شيئًا واحدًا - ومنه لإرسال البيانات إلى علامات التبويب. لنقم بذلك!

 self.addEventListener('fetch', event => { const {headers, url} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; //   SSE- if (!isSSERequest) { return; } //    SSE const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', }; // ,    SSE const sseChunkData = (data, event, retry, id) => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n'; //    ,   — url,  — EventSource const serverConnections = {}; //   url             const getServerConnection = url => { if (!serverConnections[url]) serverConnections[url] = new EventSource(url); return serverConnections[url]; }; //          const onServerMessage = (controller, {data, type, retry, lastEventId}) => { const responseText = sseChunkData(data, type, retry, lastEventId); const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); controller.enqueue(responseData); }; const stream = new ReadableStream({ start: controller => getServerConnection(url).onmessage = onServerMessage.bind(null, controller) }); const response = new Response(stream, {headers: sseHeaders}); event.respondWith(response); }); 

نفس الرمز على جيثب.
حصلت على حل بسيط جدًا لمهمة غير تافهة. ولكن ، بالطبع ، لا يزال هناك الكثير من الفروق الدقيقة. على سبيل المثال ، تحتاج إلى إغلاق الاتصال بالخادم عند إغلاق جميع علامات التبويب ، ودعم بروتوكول SSE بشكل كامل ، وما إلى ذلك.

لقد قررنا كل هذا بنجاح - أنا متأكد من أنه لن يكون من الصعب عليك!

Source: https://habr.com/ru/post/ar471718/


All Articles