تنفيذ بروتوكول Udp الموثوق لـ .Net

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

أعمق في الكود. إنشاء وتأسيس الاتصالات
أعمق في الكود. إغلاق الاتصال في المهلة المحددة
أعمق في الكود. استعادة نقل البيانات
UDP API موثوق
اختتام
روابط ومقالات مفيدة

دخول

افترضت البنية الأصلية للإنترنت مساحة عنوان متجانسة تحتوي كل عقدة فيها على عنوان IP عالمي وفريد ​​ويمكنها التواصل مباشرة مع العقد الأخرى. الآن للإنترنت ، في الواقع ، بنية مختلفة - منطقة واحدة من عناوين IP العالمية والعديد من المناطق ذات العناوين الخاصة المخفية خلف أجهزة NAT.في هذه البنية ، يمكن للأجهزة الموجودة في مساحة العنوان العالمية فقط التواصل بسهولة مع أي شخص على الشبكة لأن لديهم عنوان IP فريدًا وقابل للتوجيه عالميًا. يمكن للعقدة الموجودة على الشبكة الخاصة الاتصال بالعقد الأخرى الموجودة على نفس الشبكة ، ويمكنها أيضًا الاتصال بالعقد الأخرى المعروفة في مساحة العنوان العالمية. يتم تحقيق هذا التفاعل إلى حد كبير بسبب آلية ترجمة عنوان الشبكة. تقوم أجهزة NAT ، مثل أجهزة توجيه Wi-Fi ، بإنشاء إدخالات خاصة لجدول الترجمة للاتصالات الصادرة وتعديل عناوين IP وأرقام المنافذ في الحزم. يسمح هذا بالاتصالات الصادرة من الشبكة الخاصة إلى المضيفين في مساحة العنوان العالمية. ولكن في الوقت نفسه ، عادةً ما تحظر أجهزة NAT جميع حركة المرور الواردة ما لم يتم تعيين قواعد منفصلة للاتصالات الواردة.

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

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

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

في هذه الحالة ، لضمان تسليم الحزمة المضمون ، يلزم تنفيذ بروتوكول طبقة تطبيق يوفر الوظائف الضرورية ويعمل عبر UDP.

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

بالنسبة لبقية هذه المقالة ، سأركز فقط على تنفيذ بروتوكول التسليم المضمون. سيتم وصف تنفيذ تقنية تثقيب الثقب UDP في المقالات التالية.

متطلبات البروتوكول

  1. تسليم حزم موثوق به يتم تنفيذه من خلال آلية ردود فعل إيجابية (ما يسمى بالإقرار الإيجابي)
  2. الحاجة إلى النقل الفعال للبيانات الضخمة ، أي يجب أن يتجنب البروتوكول ترحيل الرزم غير الضروري
  3. يجب أن يكون من الممكن إلغاء آلية تأكيد التسليم (القدرة على العمل كبروتوكول UDP "خالص")
  4. القدرة على تنفيذ وضع الأوامر ، مع تأكيد كل رسالة
  5. يجب أن تكون الوحدة الأساسية لنقل البيانات عبر البروتوكول رسالة

تتوافق هذه المتطلبات إلى حد كبير مع متطلبات بروتوكول البيانات الموثوقة الموصوفة في rfc 908 и rfc 1151، واعتمدت على تلك المعايير عند تطوير هذا البروتوكول.

لفهم هذه المتطلبات ، دعنا نلقي نظرة على توقيت نقل البيانات بين عقدتين شبكيتين باستخدام بروتوكولي TCP و UDP. دعنا في كلتا الحالتين ستفقد حزمة واحدة.
نقل البيانات غير التفاعلية عبر TCP:تنفيذ بروتوكول Udp الموثوق لـ .Net

كما ترى من الرسم التخطيطي ، في حالة فقد الحزمة ، سيكتشف TCP الحزمة المفقودة ويبلغ المرسل بها عن طريق طلب رقم القطعة المفقودة.
نقل البيانات عبر بروتوكول UDP:تنفيذ بروتوكول Udp الموثوق لـ .Net

لا يتخذ UDP أي خطوات للكشف عن الخسارة. تقع مسؤولية التحكم في أخطاء الإرسال في بروتوكول UDP بالكامل على عاتق التطبيق.

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

بالإضافة إلى ذلك ، لتحسين الأداء (أي إرسال أكثر من مقطع واحد دون تلقي إقرار) ، يستخدم بروتوكول TCP ما يسمى بنافذة الإرسال - عدد بايتات البيانات التي يتوقع مرسل المقطع تلقيها.

لمزيد من المعلومات حول بروتوكول TCP ، راجع rfc 793، من UDP إلى rfc 768حيث ، في الواقع ، يتم تعريفها.

مما سبق ، من الواضح أنه من أجل إنشاء بروتوكول موثوق لتسليم الرسائل عبر UDP (المشار إليه فيما يلي باسم موثوق UDP) ، يلزم تنفيذ آليات نقل البيانات المشابهة لبرنامج التعاون الفني. يسمى:

  • حفظ حالة الاتصال
  • استخدم ترقيم المقاطع
  • استخدم حزم التأكيد الخاصة
  • استخدام آلية نوافذ مبسطة لزيادة إنتاجية البروتوكول

بالإضافة إلى ذلك ، أنت بحاجة إلى:

  • يشير إلى بداية رسالة ، لتخصيص موارد للاتصال
  • يشير إلى نهاية الرسالة ، لتمرير الرسالة المستلمة إلى التطبيق الرئيسي وتحرير موارد البروتوكول
  • السماح للبروتوكول الخاص بالاتصال بتعطيل آلية تأكيد التسليم لتعمل كـ UDP "خالص"

رأس UDP موثوق

تذكر أن مخطط بيانات UDP مغلف في مخطط بيانات IP. يتم "تغليف" حزمة UDP الموثوقة بشكل مناسب في مخطط بيانات UDP.
تغليف موثوق لرأس UDP:تنفيذ بروتوكول Udp الموثوق لـ .Net

هيكل رأس UDP الموثوق به بسيط للغاية:

تنفيذ بروتوكول Udp الموثوق لـ .Net

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

الأعلام هي كما يلي:

  • FirstPacket - الحزمة الأولى من الرسالة
  • NoAsk - لا تتطلب الرسالة تمكين آلية إقرار
  • LastPacket - الحزمة الأخيرة من الرسالة
  • RequestForPacket - حزمة تأكيد أو طلب حزمة مفقودة

المبادئ العامة للبروتوكول

نظرًا لأن UDP الموثوق به يركز على نقل الرسائل المضمون بين عقدتين ، فيجب أن يكون قادرًا على إنشاء اتصال بالجانب الآخر. لإنشاء اتصال ، يرسل المرسل حزمة مع علامة FirstPacket ، وستعني الاستجابة التي تم إجراؤها إنشاء الاتصال. جميع حزم الاستجابة ، أو بعبارة أخرى ، حزم إقرار الاستلام ، تقوم دائمًا بتعيين قيمة حقل PacketNumber إلى واحد أكثر من أكبر قيمة PacketNumber للحزم التي تم استلامها بنجاح. حقل الخيارات للحزمة الأولى المرسلة هو حجم الرسالة.

يتم استخدام آلية مماثلة لإنهاء الاتصال. تم تعيين علامة LastPacket على الحزمة الأخيرة من الرسالة. في حزمة الاستجابة ، يُشار إلى رقم الحزمة الأخيرة + 1 ، وهو ما يعني بالنسبة للجانب المستلم تسليم الرسالة بنجاح.
مخطط إنشاء الاتصال والإنهاء:تنفيذ بروتوكول Udp الموثوق لـ .Net

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

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

المهلات وموقتات البروتوكول

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

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

رسم تخطيطي موثوق لحالة نقل UDP

يتم تنفيذ مبادئ البروتوكول في آلة حالة محدودة ، كل حالة منها مسؤولة عن منطق معين لمعالجة الحزم.
رسم تخطيطي موثوق لحالة UDP:

تنفيذ بروتوكول Udp الموثوق لـ .Net

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

FirstPacket إرسال - الحالة الأولية التي يكون فيها الاتصال الصادر عند إرسال الرسالة.

في هذه الحالة ، يتم إرسال الحزمة الأولى للرسائل العادية. بالنسبة للرسائل التي لا تحتوي على تأكيد للإرسال ، فهذه هي الحالة الوحيدة التي يتم فيها إرسال الرسالة بالكامل.

دورة الإرسال - الحالة الأرضية لإرسال رزم الرسائل.

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

تم استلام FirstPacket - الحالة الأولية لمستلم الرسالة.

يتحقق من صحة بداية الإرسال ، ويخلق الهياكل اللازمة ، ويرسل إقرارًا باستلام الحزمة الأولى.

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

تجميع - الحالة الأساسية لاستقبال حزم الرسائل.

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

الطلب مكتمل - إغلاق الاتصال في حالة الاستلام الناجح للرسالة بالكامل.

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

أعمق في الكود. وحدة التحكم في ناقل الحركة

أحد العناصر الرئيسية لـ UDP الموثوق هو كتلة التحكم في الإرسال. تتمثل مهمة هذه الكتلة في تخزين الاتصالات الحالية والعناصر المساعدة ، وتوزيع الحزم الواردة على الاتصالات المقابلة ، وتوفير واجهة لإرسال الحزم إلى اتصال ، وتنفيذ بروتوكول API. تستقبل كتلة التحكم في الإرسال الحزم من طبقة UDP وتعيد توجيهها إلى آلة الحالة للمعالجة. لتلقي الحزم ، يقوم بتنفيذ خادم UDP غير متزامن.
بعض أعضاء فئة TrustedUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

تنفيذ خادم UDP غير متزامن:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

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

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

أعمق في الكود. تنص على

تنفذ الدول آلة الحالة لبروتوكول UDP الموثوق ، حيث تتم المعالجة الرئيسية للحزم. توفر فئة abstractUdpState المجردة واجهة للحالة:

تنفيذ بروتوكول Udp الموثوق لـ .Net

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

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

طريقة DisposeByTimeout

يعد أسلوب DisposeByTimeout مسؤولاً عن تحرير موارد الاتصال بعد انقضاء المهلة والإشارة إلى تسليم رسالة ناجح / غير ناجح.
موثوقة ، حالة.

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

يتم تجاوزه فقط في الدولة الطلب مكتمل.
اكتمل.

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

طريقة حزم العمليات

طريقة ProcessPackets هي المسؤولة عن المعالجة الإضافية للحزمة أو الحزم. تم الاتصال به مباشرة أو عن طريق مؤقت انتظار الحزمة.

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

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

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

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

قادر الطلب مكتمل تقوم الطريقة بإيقاف مؤقت التشغيل وترسل الرسالة إلى المشتركين.
الحزم:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

طريقة ReceivePacket

قادر تم استلام FirstPacket تتمثل المهمة الرئيسية للطريقة في تحديد ما إذا كانت حزمة الرسائل الأولى قد وصلت بالفعل إلى الواجهة ، وكذلك لتجميع رسالة تتكون من حزمة واحدة.
تم استلام FirstPacket.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

قادر دورة الإرسال تم تجاوز هذه الطريقة لقبول إقرارات التسليم وطلبات إعادة الإرسال.
دورة الإرسال.

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

قادر تجميع في طريقة ReceivePacket ، يتم العمل الرئيسي لتجميع رسالة من الحزم الواردة.
تجميع. ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

قادر الطلب مكتمل المهمة الوحيدة للأسلوب هي إرسال إعادة إقرار بالتسليم الناجح للرسالة.
اكتمل. ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

طريقة إرسال الحزمة

قادر FirstPacket إرسال ترسل هذه الطريقة الحزمة الأولى من البيانات ، أو إذا كانت الرسالة لا تتطلب تأكيد التسليم ، فإن الرسالة بأكملها.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

قادر دورة الإرسال في هذه الطريقة ، يتم إرسال كتلة من الحزم.
دورة الإرسال.

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

أعمق في الكود. إنشاء وتأسيس الاتصالات

الآن بعد أن رأينا الحالات الأساسية والأساليب المستخدمة للتعامل مع الحالات ، فلنقم بتفصيل بعض الأمثلة عن كيفية عمل البروتوكول بمزيد من التفاصيل.
مخطط نقل البيانات في ظل الظروف العادية:تنفيذ بروتوكول Udp الموثوق لـ .Net

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

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

إرسال الحزمة الأولى (حالة إرسال FirstPacket):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

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

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

استلام الحزمة الأولى وإرسال إقرار (حالة FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

أعمق في الكود. إغلاق الاتصال في المهلة المحددة

يعد التعامل مع المهلة جزءًا مهمًا من UDP الموثوق به. ضع في اعتبارك مثالاً فشلت فيه عقدة وسيطة وأصبح تسليم البيانات في كلا الاتجاهين مستحيلاً.
رسم تخطيطي لإغلاق اتصال قبل انتهاء المهلة:تنفيذ بروتوكول Udp الموثوق لـ .Net

كما يتضح من الرسم التخطيطي ، يبدأ مؤقت عمل المرسل فورًا بعد إرسال كتلة من الحزم. يحدث هذا في طريقة SendPacket للدولة دورة الإرسال.
تمكين مؤقت العمل (حالة دورة الإرسال):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

يتم تعيين فترات المؤقت عند إنشاء الاتصال. الافتراضي ShortTimerPeriod 5 ثوان. في المثال ، تم ضبطه على 1,5 ثانية.

بالنسبة للاتصال الوارد ، يبدأ المؤقت بعد تلقي آخر حزمة بيانات واردة ، وهذا يحدث في طريقة ReceivePacket للحالة تجميع
تمكين مؤقت العمل (حالة التجميع):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

لم يتم وصول المزيد من الحزم على الاتصال الوارد أثناء انتظار مؤقت العمل. انطلق الموقت واستدعى طريقة ProcessPackets ، حيث تم العثور على الحزم المفقودة وتم إرسال طلبات إعادة التسليم لأول مرة.
إرسال طلبات إعادة التسليم (حالة التجميع):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

تم تعيين متغير TimerSecondTry إلى صحيح. هذا المتغير مسؤول عن إعادة تشغيل مؤقت العمل.

من جانب المرسل ، يتم أيضًا تشغيل مؤقت العمل واستياء آخر حزمة تم إرسالها.
تمكين مؤقت إغلاق الاتصال (حالة دورة الإرسال):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

بعد ذلك ، يبدأ مؤقت إغلاق الاتصال في الاتصال الصادر.
موثوقةUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

تكون فترة مهلة مؤقت إغلاق الاتصال 30 ثانية بشكل افتراضي.

بعد وقت قصير ، يعمل مؤقت العمل على جانب المستلم مرة أخرى ، ويتم إرسال الطلبات مرة أخرى ، وبعد ذلك يبدأ مؤقت إغلاق الاتصال للاتصال الوارد

عند إطلاق مؤقتات الإغلاق ، يتم تحرير جميع موارد كلا سجلي الاتصال. يبلغ المرسل عن فشل التسليم إلى التطبيق الرئيسي (انظر UDP API الموثوق).
تحرير موارد سجل الاتصال:

public void Dispose()
{
  try
  {
    System.Threading.Monitor.Enter(this.LockerReceive);
  }
  finally
  {
    Interlocked.Increment(ref this.IsDone);
    if (WaitForPacketsTimer != null)
    {
      WaitForPacketsTimer.Dispose();
    }
    if (CloseWaitTimer != null)
    {
      CloseWaitTimer.Dispose();
    }
    byte[] stream;
    Tcb.IncomingStreams.TryRemove(Key, out stream);
    stream = null;
    Tcb.OutcomingStreams.TryRemove(Key, out stream);
    stream = null;
    System.Threading.Monitor.Exit(this.LockerReceive);
  }
}

أعمق في الكود. استعادة نقل البيانات

مخطط استعادة نقل البيانات في حالة فقدان الحزم:تنفيذ بروتوكول Udp الموثوق لـ .Net

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

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

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

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

يتم استلام الحزمة المستاءة (الحزمة رقم 3 في الرسم التخطيطي) عن طريق الاتصال الوارد. يتم إجراء فحص لمعرفة ما إذا كانت نافذة الاستلام ممتلئة ويتم استعادة نقل البيانات العادي.
التحقق من النتائج في نافذة الاستلام (حالة التجميع):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

UDP API موثوق

للتفاعل مع بروتوكول نقل البيانات ، توجد فئة Udp موثوقة مفتوحة ، وهي عبارة عن غلاف فوق كتلة التحكم في النقل. فيما يلي أهم أعضاء الفصل:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

يتم استلام الرسائل عن طريق الاشتراك. تفويض التوقيع لأسلوب رد الاتصال:

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

الرسالة:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

للاشتراك في نوع رسالة معين و / أو مرسل محدد ، يتم استخدام معلمتين اختياريتين: نوع الرسالة الموثوق بها و / أو إيبندبوينت.

أنواع الرسائل:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

يتم إرسال الرسالة بشكل غير متزامن ؛ لهذا ، يقوم البروتوكول بتنفيذ نموذج برمجة غير متزامن:

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

ستكون نتيجة إرسال الرسالة صحيحة - إذا وصلت الرسالة إلى المستلم بنجاح وخطأ - إذا تم إغلاق الاتصال قبل انتهاء المهلة:

public bool EndSendMessage(IAsyncResult asyncResult)

اختتام

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

النسخة الموضحة من بروتوكول التسليم الموثوق بها قوية ومرنة بما يكفي لتلبية المتطلبات المحددة مسبقًا. لكني أريد أن أضيف أنه يمكن تحسين التنفيذ الموصوف. على سبيل المثال ، لزيادة الإنتاجية وتغيير فترات المؤقت ديناميكيًا ، يمكن إضافة آليات مثل النافذة المنزلقة و RTT إلى البروتوكول ، وسيكون من المفيد أيضًا تنفيذ آلية لتحديد MTU بين عقد الاتصال (ولكن فقط في حالة إرسال رسائل كبيرة) .

أشكركم على اهتمامكم ، وأتطلع إلى تعليقاتكم وتعليقاتكم.

ملاحظة لأولئك الذين يهتمون بالتفاصيل أو يريدون فقط اختبار البروتوكول ، رابط المشروع على GitHube:
مشروع UDP موثوق

روابط ومقالات مفيدة

  1. مواصفات بروتوكول TCP: باللغة الإنجليزية и на русском
  2. مواصفات بروتوكول UDP: باللغة الإنجليزية и на русском
  3. مناقشة بروتوكول RUDP: مشروع ietf-sigtran-موثوقة- udp-00
  4. بروتوكول بيانات موثوق: rfc 908 и rfc 1151
  5. تنفيذ بسيط لتأكيد التسليم عبر UDP: تمتع بالتحكم الكامل في الشبكات الخاصة بك باستخدام .NET و UDP
  6. مقالة تصف آليات اجتياز NAT: التواصل من نظير إلى نظير عبر مترجمي عناوين الشبكة
  7. تنفيذ نموذج البرمجة غير المتزامن: تنفيذ نموذج البرمجة غير المتزامنة CLR и كيفية تنفيذ نمط تصميم IAsyncResult
  8. نقل نموذج البرمجة غير المتزامن إلى النمط غير المتزامن المستند إلى المهام (APM في TAP):
    TPL والبرمجة التقليدية غير المتزامنة .NET
    التفاعل مع الأنماط والأنواع غير المتزامنة الأخرى

تحديث: شكرا لك مايوروف и سيدريستيج لفكرة إضافة مهمة إلى الواجهة. لا يتم انتهاك توافق المكتبة مع أنظمة التشغيل القديمة ، لأن يدعم الإطار الرابع كلاً من خادم XP و 4.

المصدر: www.habr.com

إضافة تعليق