إدارة اتصال SignalR الفعالة

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

مقدمة


ما هو SignalR - إنها نوع من الواجهة عبر تقنيات WebSockets ، والاستقصاء الطويل ، وأحداث إرسال الخادم . بفضل هذه الواجهة ، يمكنك العمل بشكل موحد مع أي من هذه التقنيات ولا تقلق بشأن التفاصيل. بالإضافة إلى ذلك ، بفضل تقنية الاقتراع الطويل ، يمكنك دعم العملاء الذين ، لسبب ما ، لا يستطيعون العمل على مآخذ الويب ، مثل IE-8. يتم تمثيل الواجهة بواجهة برمجة تطبيقات عالية المستوى تعتمد على RPC . بالإضافة إلى ذلك ، تقدم SignalR بناء الاتصالات وفقًا لمبدأ "الناشر-المشترك" ، والذي يطلق عليه في مصطلحات API المجموعات. وسيتم مناقشة هذا أبعد من ذلك.

التحديات


ولعل الشيء الأكثر إثارة للاهتمام في البرمجة هو القدرة على حل المشاكل غير القياسية. واليوم سنقوم بتعيين واحدة من هذه المهام وسننظر في حلها.

في عصر تطور أفكار التوسع ، وخاصةً الأفقي ، يتمثل التحدي الرئيسي في الحاجة إلى وجود أكثر من خادم واحد. وقد تعامل مطورو المكتبة المشار إليها بالفعل مع هذه المكالمة ، ويمكن العثور على وصف للحل على MSDN . باختصار ، يُقترح ، باستخدام مبدأ الناشر-المشترك ، لمزامنة المكالمات بين الخوادم. يشترك كل خادم في حافلة مشتركة ويتم إرسال جميع الأوامر المرسلة من هذا الخادم أولاً إلى الناقل. علاوة على ذلك ، ينطبق الأمر على جميع الخوادم وعندها فقط على العملاء:

صورة

من المهم ملاحظة أن كل عميل متصل بالخادم لديه معرف اتصال فريد خاص به - ConnectionId - ويتم معالجة جميع الرسائل في النهاية باستخدام هذا المعرف. لذلك ، يخزن كل خادم هذه الاتصالات.

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

لماذا نحتاج للاتصال


كما ذكر سابقًا ، تقدم SignalR نموذجًا للناشر والمشترك. هنا ، وحدة توجيه الرسائل ليست ConnectionId بل مجموعة. المجموعة هي مجموعة من الاتصالات. عن طريق إرسال رسالة إلى مجموعة ، نرسل رسالة إلى جميع ConnectionId الموجودة في هذه المجموعة. من السهل إنشاء مجموعات - عند توصيل عميل بالخادم ، ندعو ببساطة إلى طريقة AddToGroupAsync API:

public override async Task OnConnectedAsync() { foreach (var chat in _options.Chats) await Groups.AddToGroupAsync(ConnectionId, chat); await Groups.AddToGroupAsync(ConnectionId, Client); } 

وكيف تترك المجموعة؟ يقدم المطورون طريقة API RemoveFromGroupAsync :

 public override async Task OnDisconnectedAsync(Exception exception) { foreach (var chat in _options.Chats) await Groups.RemoveFromGroupAsync(ConnectionId, chat); await Groups.RemoveFromGroupAsync(ConnectionId, Client); } 

لاحظ أن وحدة البيانات هي ConnectionId. ومع ذلك ، من وجهة نظر نموذج المجال ، ConnectionId غير موجودة ، ولكن هناك عملاء. في هذا الصدد ، يتم تعيين تنظيم العميل لتعيين ConnectionId والعكس بالعكس لمستخدمي المكتبة المحددة.

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

طرق لتعيين العملاء للاتصالات


قسم كامل على MSDN مخصص لهذه المشكلة. الطرق التالية مقترحة للنظر فيها:

  • تخزين في الذاكرة
  • "مجموعة المستخدمين"
  • التخزين الخارجي الدائم

كيفية تتبع الاتصالات؟
يمكنك تتبع الاتصالات باستخدام أساليب محور OnConnectedAsync و OnDisconnectedAsync .

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

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

ولكن ماذا عن رديس؟
بالمناسبة ، يعد استخدام Redis لتخزين الاتصالات خيارًا سيئًا أيضًا. هناك مشكلة حادة في تنظيم البيانات في الذاكرة. من ناحية ، المفتاح هو العميل ، من ناحية أخرى ، المجموعة.

"مجموعة المستخدمين"


ما هي "مجموعة المستخدمين"؟ هذه مجموعة في مصطلحات SignalR حيث يمكن أن يكون عميل واحد فقط هو العميل - هو نفسه. هذا يضمن 2 الأشياء:

  1. سيتم تسليم الرسائل لشخص واحد فقط
  2. سيتم تسليم الرسائل إلى جميع الأجهزة البشرية

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

"مجموعة المستخدمين" هي الخطوة الأولى نحو حل هذه المشكلة. والخطوة الثانية هي بناء "مرآة" على العميل. نعم ، نعم ، المرايا.

المرآة


مصدر الأوامر المرسلة من العميل إلى الخادم هي إجراءات المستخدم. نشر رسالة - إرسال أمر إلى الخادم:

 this.state.hubConnection .invoke('post', {message, group, nick}) .catch(err => console.error(err)); 

ونقوم بإخطار جميع عملاء المجموعة بالوظيفة الجديدة:

 public async Task PostMessage(PostMessage message) { await Clients.Group(message.Group).SendAsync("message", new { Message = message.Message, Group = message.Group, Nick = ClientNick }); } 

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

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

فيما يلي مثال على أمر إلغاء الاشتراك في محادثة الخادم:

 public async Task LeaveChat(LeaveChatMessage message) { await Clients.OthersInGroup(message.Group).SendAsync("lost", new ClientCommand { Group = message.Group, Nick = Client }); await Clients.Group(Client).SendAsync("mirror", new MirrorChatCommand { Method = "unsubscribe", Payload = new UnsubscribeChatMessage { Group = message.Group } }); } 

 public async Task Unsubscribe(UnsubscribeChatMessage message) { await Groups.RemoveFromGroupAsync(ConnectionId, message.Group); } 

وهنا هو رمز العميل:

 connection.on('mirror', (message) => { connection .invoke(message.method, message.payload) .catch(err => console.error(err)); }); 

دعنا ندرس بمزيد من التفصيل ما يحدث هنا:

  1. يبدأ العميل إلغاء الاشتراك - يرسل أمر "الإجازة" إلى الخادم
  2. يرسل الخادم أمر "إلغاء الاشتراك" إلى "مجموعة المستخدمين" على "النسخة المتطابقة"
  3. يتم تسليم الرسالة إلى جميع الأجهزة العميلة.
  4. يتم إرسال رسالة على العميل إلى الخادم باستخدام الطريقة المحددة من قبل الخادم
  5. على كل خادم ، العميل غير مشترك

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

فلماذا نحتاج للاتصال؟


وجود "مجموعة مستخدمين" و "مرآة" على العميل يلغي الحاجة إلى العمل مع الاتصالات. ما رأيك ، أيها القراء الأعزاء ، حول هذا الموضوع؟ شارك برأيك في التعليقات.

شفرة المصدر للحصول على أمثلة:

github.com/aesamson/signalr-server
github.com/aesamson/signalr-client

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


All Articles