Implementierung des Reliable Udp-Protokolls für .Net

Das Internet hat sich längst verändert. Eines der Hauptprotokolle des Internets – UDP – wird von Anwendungen nicht nur zur Übermittlung von Datagrammen und Broadcasts verwendet, sondern auch zur Bereitstellung von „Peer-to-Peer“-Verbindungen zwischen Netzwerkknoten. Aufgrund seines einfachen Designs hat dieses Protokoll viele bisher ungeplante Verwendungsmöglichkeiten, die Mängel des Protokolls, wie z. B. das Fehlen einer garantierten Zustellung, sind jedoch nirgendwo verschwunden. Dieser Artikel beschreibt die Implementierung des garantierten Zustellungsprotokolls über UDP.
Inhalt:Eintrag
Protokollanforderungen
Zuverlässiger UDP-Header
Allgemeine Grundsätze des Protokolls
Zeitüberschreitungen und Protokoll-Timer
Zuverlässiges UDP-Übertragungszustandsdiagramm
Tiefer in den Code. Getriebesteuergerät
Tiefer in den Code. Zustände

Tiefer in den Code. Verbindungen schaffen und etablieren
Tiefer in den Code. Schließen der Verbindung bei Zeitüberschreitung
Tiefer in den Code. Datenübertragung wiederherstellen
Zuverlässige UDP-API
Abschluss
Nützliche Links und Artikel

Eintrag

Die ursprüngliche Architektur des Internets ging von einem homogenen Adressraum aus, in dem jeder Knoten eine globale und eindeutige IP-Adresse hatte und direkt mit anderen Knoten kommunizieren konnte. Nun hat das Internet tatsächlich eine andere Architektur – einen Bereich mit globalen IP-Adressen und viele Bereiche mit privaten Adressen, die hinter NAT-Geräten verborgen sind.In dieser Architektur können nur Geräte im globalen Adressraum problemlos mit jedem im Netzwerk kommunizieren, da sie über eine eindeutige, global routbare IP-Adresse verfügen. Ein Knoten in einem privaten Netzwerk kann sich mit anderen Knoten im selben Netzwerk verbinden und kann sich auch mit anderen bekannten Knoten im globalen Adressraum verbinden. Diese Interaktion wird größtenteils durch den Netzwerkadressübersetzungsmechanismus erreicht. NAT-Geräte wie WLAN-Router erstellen spezielle Übersetzungstabelleneinträge für ausgehende Verbindungen und ändern IP-Adressen und Portnummern in Paketen. Dies ermöglicht ausgehende Verbindungen vom privaten Netzwerk zu Hosts im globalen Adressraum. Gleichzeitig blockieren NAT-Geräte jedoch in der Regel den gesamten eingehenden Datenverkehr, sofern keine separaten Regeln für eingehende Verbindungen festgelegt werden.

Diese Architektur des Internets ist korrekt genug für die Client-Server-Kommunikation, bei der sich Clients in privaten Netzwerken befinden können und Server eine globale Adresse haben. Es bereitet jedoch Schwierigkeiten bei der direkten Verbindung zweier Knoten untereinander anders private Netzwerke. Eine direkte Verbindung zwischen zwei Knoten ist wichtig für Peer-to-Peer-Anwendungen wie Sprachübertragung (Skype), Fernzugriff auf einen Computer (TeamViewer) oder Online-Gaming.

Eine der effektivsten Methoden zum Aufbau einer Peer-to-Peer-Verbindung zwischen Geräten in verschiedenen privaten Netzwerken ist das sogenannte Hole Punching. Diese Technik wird am häufigsten bei Anwendungen verwendet, die auf dem UDP-Protokoll basieren.

Wenn Ihre Anwendung jedoch eine garantierte Übermittlung von Daten erfordert, beispielsweise wenn Sie Dateien zwischen Computern übertragen, wird die Verwendung von UDP viele Schwierigkeiten bereiten, da UDP kein garantiertes Übermittlungsprotokoll ist und im Gegensatz zu TCP keine Paketzustellung in der richtigen Reihenfolge ermöglicht Protokoll.

In diesem Fall ist es zur Gewährleistung einer garantierten Paketzustellung erforderlich, ein Protokoll auf Anwendungsebene zu implementieren, das die erforderliche Funktionalität bereitstellt und über UDP funktioniert.

Ich möchte sofort darauf hinweisen, dass es eine TCP-Hole-Punching-Technik zum Herstellen von TCP-Verbindungen zwischen Knoten in verschiedenen privaten Netzwerken gibt, diese jedoch aufgrund der mangelnden Unterstützung vieler NAT-Geräte normalerweise nicht als Hauptverbindungsmethode angesehen wird solche Knoten.

Für den Rest dieses Artikels werde ich mich nur auf die Implementierung des garantierten Lieferprotokolls konzentrieren. Die Implementierung der UDP-Lochstanztechnik wird in den folgenden Artikeln beschrieben.

Protokollanforderungen

  1. Zuverlässige Paketzustellung durch einen positiven Feedback-Mechanismus (die sogenannte positive Bestätigung)
  2. Die Notwendigkeit einer effizienten Übertragung großer Datenmengen, d. h. Das Protokoll muss unnötige Paketweiterleitung vermeiden
  3. Es sollte möglich sein, den Zustellungsbestätigungsmechanismus (die Fähigkeit, als „reines“ UDP-Protokoll zu funktionieren) abzubrechen.
  4. Möglichkeit zur Implementierung des Befehlsmodus mit Bestätigung jeder Nachricht
  5. Die Grundeinheit der Datenübertragung über das Protokoll muss eine Nachricht sein

Diese Anforderungen stimmen weitgehend mit den Anforderungen des Reliable Data Protocol überein, die in beschrieben sind rfc 908 и rfc 1151, und ich habe mich bei der Entwicklung dieses Protokolls auf diese Standards verlassen.

Um diese Anforderungen zu verstehen, schauen wir uns den Zeitpunkt der Datenübertragung zwischen zwei Netzwerkknoten unter Verwendung der TCP- und UDP-Protokolle an. In beiden Fällen geht ein Paket verloren.
Übertragung nicht interaktiver Daten über TCP:Implementierung des Reliable Udp-Protokolls für .Net

Wie Sie dem Diagramm entnehmen können, erkennt TCP im Falle eines Paketverlusts das verlorene Paket und meldet es dem Absender, indem es nach der Nummer des verlorenen Segments fragt.
Datenübertragung per UDP-Protokoll:Implementierung des Reliable Udp-Protokolls für .Net

UDP unternimmt keine Schritte zur Verlusterkennung. Die Kontrolle von Übertragungsfehlern im UDP-Protokoll liegt ausschließlich in der Verantwortung der Anwendung.

Die Fehlererkennung im TCP-Protokoll erfolgt durch den Aufbau einer Verbindung mit einem Endknoten, die Speicherung des Status dieser Verbindung, die Angabe der Anzahl der gesendeten Bytes in jedem Paket-Header und die Benachrichtigung über Empfangsbestätigungen mithilfe einer Bestätigungsnummer.

Um die Leistung zu verbessern (d. h. mehr als ein Segment zu senden, ohne eine Bestätigung zu erhalten), verwendet das TCP-Protokoll außerdem das sogenannte Übertragungsfenster – die Anzahl der Datenbytes, die der Absender des Segments erwartet.

Weitere Informationen zum TCP-Protokoll finden Sie unter rfc 793, von UDP zu rfc 768wo sie tatsächlich definiert sind.

Aus dem oben Gesagten geht hervor, dass zur Erstellung eines zuverlässigen Nachrichtenübermittlungsprotokolls über UDP (im Folgenden als „ Zuverlässiges UDP) ist es erforderlich, Datenübertragungsmechanismen ähnlich wie TCP zu implementieren. Nämlich:

  • Verbindungsstatus speichern
  • Verwenden Sie die Segmentnummerierung
  • Verwenden Sie spezielle Bestätigungspakete
  • Verwenden Sie einen vereinfachten Fenstermechanismus, um den Protokolldurchsatz zu erhöhen

Zusätzlich benötigen Sie:

  • Signalisieren Sie den Beginn einer Nachricht, um Ressourcen für die Verbindung zuzuweisen
  • Signalisieren Sie das Ende einer Nachricht, um die empfangene Nachricht an die Upstream-Anwendung weiterzuleiten und Protokollressourcen freizugeben
  • Erlauben Sie dem verbindungsspezifischen Protokoll, den Übermittlungsbestätigungsmechanismus zu deaktivieren, um als „reines“ UDP zu funktionieren

Zuverlässiger UDP-Header

Denken Sie daran, dass ein UDP-Datagramm in einem IP-Datagramm gekapselt ist. Das Reliable UDP-Paket wird entsprechend in ein UDP-Datagramm „verpackt“.
Zuverlässige UDP-Header-Kapselung:Implementierung des Reliable Udp-Protokolls für .Net

Die Struktur des Reliable UDP-Headers ist recht einfach:

Implementierung des Reliable Udp-Protokolls für .Net

  • Flags – Paketkontrollflags
  • MessageType – Nachrichtentyp, der von Upstream-Anwendungen zum Abonnieren bestimmter Nachrichten verwendet wird
  • TransmissionId – die Nummer der Übertragung identifiziert zusammen mit der Adresse und dem Port des Empfängers die Verbindung eindeutig
  • PacketNumber – Paketnummer
  • Optionen – zusätzliche Protokolloptionen. Im Fall des ersten Pakets wird es verwendet, um die Größe der Nachricht anzugeben

Die Flaggen lauten wie folgt:

  • FirstPacket – das erste Paket der Nachricht
  • NoAsk – Für die Nachricht muss kein Bestätigungsmechanismus aktiviert werden
  • LastPacket – das letzte Paket der Nachricht
  • RequestForPacket – Bestätigungspaket oder Anfrage für ein verlorenes Paket

Allgemeine Grundsätze des Protokolls

Da sich Reliable UDP auf die garantierte Nachrichtenübertragung zwischen zwei Knoten konzentriert, muss es in der Lage sein, eine Verbindung mit der anderen Seite herzustellen. Um eine Verbindung herzustellen, sendet der Absender ein Paket mit dem FirstPacket-Flag, dessen Antwort bedeutet, dass die Verbindung hergestellt wurde. Alle Antwortpakete, oder mit anderen Worten Bestätigungspakete, legen den Wert des Felds „PacketNumber“ immer auf einen Wert fest, der um eins größer ist als der größte PacketNumber-Wert erfolgreich empfangener Pakete. Das Optionsfeld für das erste gesendete Paket gibt die Größe der Nachricht an.

Ein ähnlicher Mechanismus wird zum Beenden einer Verbindung verwendet. Das LastPacket-Flag wird auf das letzte Paket der Nachricht gesetzt. Im Antwortpaket wird die Nummer des letzten Pakets + 1 angegeben, was für die Empfängerseite eine erfolgreiche Zustellung der Nachricht bedeutet.
Verbindungsaufbau- und -abschlussdiagramm:Implementierung des Reliable Udp-Protokolls für .Net

Sobald die Verbindung hergestellt ist, beginnt die Datenübertragung. Daten werden in Paketblöcken übertragen. Jeder Block, außer dem letzten, enthält eine feste Anzahl von Paketen. Sie entspricht der Größe des Empfangs-/Sendefensters. Der letzte Datenblock enthält möglicherweise weniger Pakete. Nach dem Senden jedes Blocks wartet die sendende Seite auf eine Zustellungsbestätigung oder eine Aufforderung zur erneuten Zustellung verlorener Pakete und lässt das Empfangs-/Sendefenster für den Empfang von Antworten geöffnet. Nach Erhalt der Bestätigung der Blockzustellung verschiebt sich das Empfangs-/Sendefenster und der nächste Datenblock wird gesendet.

Die empfangende Seite empfängt die Pakete. Jedes Paket wird daraufhin überprüft, ob es in das Übertragungsfenster fällt. Pakete und Duplikate, die nicht in das Fenster fallen, werden herausgefiltert. Weil Wenn die Größe des Fensters fest und für den Empfänger und den Absender gleich ist, wird das Fenster im Falle einer verlustfreien Zustellung eines Paketblocks verschoben, um Pakete des nächsten Datenblocks zu empfangen und eine Zustellungsbestätigung zu erhalten gesendet. Wenn sich das Fenster nicht innerhalb des vom Arbeitstimer festgelegten Zeitraums füllt, wird eine Überprüfung gestartet, welche Pakete nicht zugestellt wurden, und es werden Anfragen zur erneuten Zustellung gesendet.
Neuübertragungsdiagramm:Implementierung des Reliable Udp-Protokolls für .Net

Zeitüberschreitungen und Protokoll-Timer

Es gibt mehrere Gründe, warum eine Verbindung nicht hergestellt werden kann. Zum Beispiel, wenn die empfangende Partei offline ist. In diesem Fall wird beim Versuch, eine Verbindung herzustellen, die Verbindung aufgrund einer Zeitüberschreitung geschlossen. Die Reliable UDP-Implementierung verwendet zwei Timer, um Zeitüberschreitungen festzulegen. Der erste, der Arbeitstimer, wird verwendet, um auf eine Antwort vom Remote-Host zu warten. Wenn es auf der Absenderseite ausgelöst wird, wird das zuletzt gesendete Paket erneut gesendet. Wenn der Timer beim Empfänger abläuft, wird eine Prüfung auf verlorene Pakete durchgeführt und Anfragen zur erneuten Zustellung werden gesendet.

Der zweite Timer wird benötigt, um die Verbindung zu schließen, falls die Kommunikation zwischen den Knoten ausfällt. Auf der Senderseite startet es unmittelbar nach Ablauf des Arbeitstimers und wartet auf eine Antwort vom Remote-Knoten. Erfolgt innerhalb des angegebenen Zeitraums keine Antwort, wird die Verbindung abgebaut und Ressourcen freigegeben. Auf der Empfangsseite wird der Timer für das Schließen der Verbindung gestartet, nachdem der Arbeitstimer zweimal abgelaufen ist. Dies ist notwendig, um den Verlust des Bestätigungspakets abzusichern. Wenn der Timer abläuft, wird auch die Verbindung beendet und Ressourcen werden freigegeben.

Zuverlässiges UDP-Übertragungszustandsdiagramm

Die Prinzipien des Protokolls werden in einer endlichen Zustandsmaschine implementiert, deren jeder Zustand für eine bestimmte Logik der Paketverarbeitung verantwortlich ist.
Zuverlässiges UDP-Zustandsdiagramm:

Implementierung des Reliable Udp-Protokolls für .Net

Geschlossen - ist eigentlich kein Zustand, sondern ein Start- und Endpunkt für den Automaten. Für den Staat Geschlossen Es wird ein Übertragungskontrollblock empfangen, der unter Implementierung eines asynchronen UDP-Servers Pakete an die entsprechenden Verbindungen weiterleitet und die Statusverarbeitung startet.

FirstPacketSending – der Ausgangszustand, in dem sich die ausgehende Verbindung zum Zeitpunkt des Versendens der Nachricht befindet.

In diesem Zustand wird das erste Paket für normale Nachrichten gesendet. Bei Nachrichten ohne Sendebestätigung ist dies der einzige Zustand, in dem die gesamte Nachricht gesendet wird.

Sendezyklus – Grundzustand für die Übertragung von Nachrichtenpaketen.

Übergang vom Staat dorthin FirstPacketSending wird ausgeführt, nachdem das erste Paket der Nachricht gesendet wurde. In diesem Zustand kommen alle Bestätigungen und Anfragen nach erneuten Übertragungen. Das Verlassen ist in zwei Fällen möglich – bei erfolgreicher Zustellung der Nachricht oder bei Zeitüberschreitung.

FirstPacketReceived – der Ausgangszustand für den Empfänger der Nachricht.

Es prüft den korrekten Beginn der Übertragung, erstellt die notwendigen Strukturen und sendet eine Empfangsbestätigung des ersten Pakets.

Für eine Nachricht, die aus einem einzelnen Paket besteht und ohne Zustellnachweis gesendet wurde, ist dies der einzige Status. Nach der Verarbeitung einer solchen Nachricht wird die Verbindung geschlossen.

Zusammenbauen – Grundzustand für den Empfang von Nachrichtenpaketen.

Es schreibt Pakete in den temporären Speicher, prüft auf Paketverlust, sendet Bestätigungen für die Zustellung eines Paketblocks und der gesamten Nachricht und sendet Anfragen zur erneuten Zustellung verlorener Pakete. Bei erfolgreichem Empfang der gesamten Nachricht geht die Verbindung in den Zustand über Abgeschlossene Verkäufe, andernfalls kommt es zu einer Zeitüberschreitung.

Abgeschlossene Verkäufe – Schließen der Verbindung bei erfolgreichem Empfang der gesamten Nachricht.

Dieser Zustand ist für den Aufbau der Nachricht und für den Fall erforderlich, dass die Zustellbestätigung der Nachricht auf dem Weg zum Absender verloren gegangen ist. Dieser Zustand wird durch einen Timeout verlassen, die Verbindung gilt jedoch als erfolgreich geschlossen.

Tiefer in den Code. Getriebesteuergerät

Eines der Schlüsselelemente von Reliable UDP ist der Übertragungskontrollblock. Die Aufgabe dieses Blocks besteht darin, aktuelle Verbindungen und Hilfselemente zu speichern, eingehende Pakete auf die entsprechenden Verbindungen zu verteilen, eine Schnittstelle zum Senden von Paketen an eine Verbindung bereitzustellen und die Protokoll-API zu implementieren. Der Übertragungskontrollblock empfängt Pakete von der UDP-Schicht und leitet sie zur Verarbeitung an die Zustandsmaschine weiter. Um Pakete zu empfangen, implementiert es einen asynchronen UDP-Server.
Einige Mitglieder der ReliableUdpConnectionControlBlock-Klasse:

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;    	
  //...
}

Implementierung eines asynchronen UDP-Servers:

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);
}

Für jede Nachrichtenübertragung wird eine Struktur erstellt, die Informationen über die Verbindung enthält. Eine solche Struktur heißt Verbindungsdatensatz.
Einige Mitglieder der ReliableUdpConnectionRecord-Klasse:

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;
  //...
}

Tiefer in den Code. Zustände

Staaten implementieren die Zustandsmaschine des Reliable UDP-Protokolls, in der die Hauptverarbeitung von Paketen stattfindet. Die abstrakte Klasse ReliableUdpState stellt eine Schnittstelle für den Zustand bereit:

Implementierung des Reliable Udp-Protokolls für .Net

Die gesamte Logik des Protokolls wird durch die oben vorgestellten Klassen implementiert, zusammen mit einer Hilfsklasse, die statische Methoden bereitstellt, wie zum Beispiel die Konstruktion des ReliableUdp-Headers aus dem Verbindungsdatensatz.

Als nächstes betrachten wir im Detail die Implementierung der Schnittstellenmethoden, die die grundlegenden Algorithmen des Protokolls bestimmen.

DisposeByTimeout-Methode

Die DisposeByTimeout-Methode ist für die Freigabe von Verbindungsressourcen nach einem Timeout und für die Signalisierung einer erfolgreichen/fehlgeschlagenen Nachrichtenzustellung verantwortlich.
ReliableUdpState.DisposeByTimeout:

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

Es wird nur im Bundesstaat außer Kraft gesetzt Abgeschlossene Verkäufe.
Completed.DisposeByTimeout:

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

ProcessPackets-Methode

Die ProcessPackets-Methode ist für die zusätzliche Verarbeitung eines oder mehrerer Pakete verantwortlich. Wird direkt oder über einen Paket-Wartetimer angerufen.

In gutem Zustand Zusammenbauen Die Methode wird überschrieben und ist für die Überprüfung auf verlorene Pakete und den Übergang in den Status verantwortlich Abgeschlossene Verkäufe, falls das letzte Paket empfangen und eine erfolgreiche Prüfung bestanden wurde
Assembling.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);
  }
}

In gutem Zustand Sendezyklus Diese Methode wird nur nach einem Timer aufgerufen und ist für das erneute Senden der letzten Nachricht sowie für die Aktivierung des Timers für das Schließen der Verbindung verantwortlich.
SendingCycle.ProcessPackets:

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

In gutem Zustand Abgeschlossene Verkäufe Die Methode stoppt den laufenden Timer und sendet die Nachricht an die Abonnenten.
Abgeschlossene.Prozesspakete:

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

ReceivePacket-Methode

In gutem Zustand FirstPacketReceived Die Hauptaufgabe des Verfahrens besteht darin, festzustellen, ob das erste Nachrichtenpaket tatsächlich an der Schnittstelle angekommen ist, und außerdem eine Nachricht zu sammeln, die aus einem einzelnen Paket besteht.
FirstPacketReceived.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);
  }
}

In gutem Zustand Sendezyklus Diese Methode wird außer Kraft gesetzt, um Zustellungsbestätigungen und Neuübertragungsanfragen zu akzeptieren.
SendingCycle.ReceivePacket:

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));
}

In gutem Zustand Zusammenbauen In der Methode „ReceivePacket“ findet die Hauptarbeit des Zusammenstellens einer Nachricht aus eingehenden Paketen statt.
Assembling.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);
  }
}

In gutem Zustand Abgeschlossene Verkäufe Die einzige Aufgabe der Methode besteht darin, eine erneute Bestätigung der erfolgreichen Zustellung der Nachricht zu senden.
Abgeschlossen.ReceivePacket:

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

Paketmethode senden

In gutem Zustand FirstPacketSending Diese Methode sendet das erste Datenpaket oder, wenn für die Nachricht keine Zustellungsbestätigung erforderlich ist, die gesamte Nachricht.
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);
}

In gutem Zustand Sendezyklus Bei dieser Methode wird ein Paketblock gesendet.
SendingCycle.SendPacket:

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 );
  }
}

Tiefer in den Code. Verbindungen schaffen und etablieren

Nachdem wir nun die Grundzustände und die Methoden zum Umgang mit Zuständen kennengelernt haben, wollen wir einige Beispiele für die Funktionsweise des Protokolls etwas detaillierter aufschlüsseln.
Datenübertragungsdiagramm unter normalen Bedingungen:Implementierung des Reliable Udp-Protokolls für .Net

Betrachten Sie die Schöpfung im Detail Verbindungsdatensatz um eine Verbindung herzustellen und das erste Paket zu senden. Die Übertragung wird immer von der Anwendung initiiert, die die API zum Senden von Nachrichten aufruft. Als nächstes wird die StartTransmission-Methode des Übertragungskontrollblocks aufgerufen, die die Übertragung der Daten für die neue Nachricht startet.
Ausgehende Verbindung erstellen:

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]);
}

Senden des ersten Pakets (FirstPacketSending-Status):

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);
}

Nach dem Senden des ersten Pakets tritt der Absender in den Status ein Sendezyklus – Warten Sie auf die Bestätigung der Paketzustellung.
Die empfangende Seite empfängt mithilfe der EndReceive-Methode das gesendete Paket und erstellt ein neues Verbindungsdatensatz und übergibt dieses Paket mit einem vorab analysierten Header zur Verarbeitung an die Methode „ReceivePacket“ des Status FirstPacketReceived
Verbindung auf der Empfangsseite herstellen:

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);
}

Empfangen des ersten Pakets und Senden einer Bestätigung (Status 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);
  }
}

Tiefer in den Code. Schließen der Verbindung bei Zeitüberschreitung

Die Behandlung von Zeitüberschreitungen ist ein wichtiger Bestandteil von Reliable UDP. Stellen Sie sich ein Beispiel vor, bei dem ein Zwischenknoten ausfiel und die Datenübermittlung in beide Richtungen unmöglich wurde.
Diagramm zum Schließen einer Verbindung bei Timeout:Implementierung des Reliable Udp-Protokolls für .Net

Wie aus dem Diagramm ersichtlich ist, startet der Arbeitstimer des Absenders unmittelbar nach dem Senden eines Paketblocks. Dies geschieht in der SendPacket-Methode des Staates Sendezyklus.
Aktivieren des Arbeitszeit-Timers (SendingCycle-Status):

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

Die Timer-Zeiträume werden beim Verbindungsaufbau eingestellt. Die Standardeinstellung für ShortTimerPeriod beträgt 5 Sekunden. Im Beispiel ist sie auf 1,5 Sekunden eingestellt.

Bei einer eingehenden Verbindung startet der Timer nach dem Empfang des letzten eingehenden Datenpakets, dies geschieht in der Methode „ReceivePacket“ des Status Zusammenbauen
Aktivierung des Arbeitszeit-Timers (Montagezustand):

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);
  // ...
}

Während des Wartens auf den Arbeitstimer sind auf der eingehenden Verbindung keine weiteren Pakete angekommen. Der Timer lief und rief die ProcessPackets-Methode auf, wo die verlorenen Pakete gefunden und erneute Zustellungsanfragen zum ersten Mal gesendet wurden.
Senden von Anfragen zur erneuten Zustellung (Montagestatus):

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);
  }
}

Die Variable TimerSecondTry ist auf gesetzt was immer dies auch sein sollte.. Diese Variable ist für den Neustart des Arbeitstimers verantwortlich.

Auf der Seite des Absenders wird außerdem der Arbeitstimer ausgelöst und das zuletzt gesendete Paket erneut gesendet.
Timer zum Schließen der Verbindung wird aktiviert (SendingCycle-Status):

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

Danach startet der Timer für das Schließen der Verbindung in der ausgehenden Verbindung.
ReliableUdpState.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);
}

Der Timeout-Zeitraum für das Schließen der Verbindung beträgt standardmäßig 30 Sekunden.

Nach kurzer Zeit feuert der Arbeits-Timer auf Empfängerseite erneut, Anfragen werden erneut gesendet, danach startet der Verbindungs-Schließ-Timer für die eingehende Verbindung

Wenn die Schließtimer ausgelöst werden, werden alle Ressourcen beider Verbindungsdatensätze freigegeben. Der Absender meldet den Zustellungsfehler an die Upstream-Anwendung (siehe Zuverlässige UDP-API).
Verbindungsdatensatzressourcen freigeben:

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);
  }
}

Tiefer in den Code. Datenübertragung wiederherstellen

Diagramm zur Wiederherstellung der Datenübertragung bei Paketverlust:Implementierung des Reliable Udp-Protokolls für .Net

Wie bereits beim Schließen der Verbindung bei Zeitüberschreitung erläutert, prüft der Empfänger nach Ablauf des Arbeitstimers, ob Pakete verloren gegangen sind. Im Falle eines Paketverlusts wird eine Liste mit der Anzahl der Pakete erstellt, die den Empfänger nicht erreicht haben. Diese Nummern werden in das LostPackets-Array einer bestimmten Verbindung eingegeben und Anfragen zur erneuten Zustellung werden gesendet.
Senden von Anfragen zur erneuten Zustellung von Paketen (Montagestatus):

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);
      }
    }
    // ...
  }
}

Der Absender akzeptiert die erneute Zustellanforderung und sendet die fehlenden Pakete. Es ist zu beachten, dass der Absender zu diesem Zeitpunkt den Timer für das Schließen der Verbindung bereits gestartet hat und dieser bei Eingang einer Anfrage zurückgesetzt wird.
Verlorene Pakete erneut senden (SendingCycle-Status):

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));
}

Das erneut gesendete Paket (Paket Nr. 3 im Diagramm) wird von der eingehenden Verbindung empfangen. Es wird geprüft, ob das Empfangsfenster voll ist und die normale Datenübertragung wiederhergestellt wird.
Überprüfung auf Treffer im Empfangsfenster (Assemblierungsstatus):

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);
  }
  // ...
}

Zuverlässige UDP-API

Für die Interaktion mit dem Datenübertragungsprotokoll gibt es eine offene Reliable Udp-Klasse, die einen Wrapper für den Übertragungskontrollblock darstellt. Hier sind die wichtigsten Mitglieder der Klasse:

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()    
}

Nachrichten werden per Abonnement empfangen. Signatur für die Callback-Methode delegieren:

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

Nachricht:

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

Um einen bestimmten Nachrichtentyp und/oder einen bestimmten Absender zu abonnieren, werden zwei optionale Parameter verwendet: ReliableUdpMessageTypes messageType und IPEndPoint ipEndPoint.

Nachrichtentypen:

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

Der Versand der Nachricht erfolgt asynchron; dazu implementiert das Protokoll ein asynchrones Programmiermodell:

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

Das Ergebnis des Sendens einer Nachricht ist wahr, wenn die Nachricht den Empfänger erfolgreich erreicht hat, und falsch, wenn die Verbindung aufgrund einer Zeitüberschreitung geschlossen wurde:

public bool EndSendMessage(IAsyncResult asyncResult)

Abschluss

Vieles wurde in diesem Artikel nicht beschrieben. Thread-Matching-Mechanismen, Ausnahme- und Fehlerbehandlung, Implementierung asynchroner Methoden zum Senden von Nachrichten. Aber der Kern des Protokolls, die Beschreibung der Logik für die Verarbeitung von Paketen, den Verbindungsaufbau und den Umgang mit Zeitüberschreitungen, sollte Ihnen klar sein.

Die demonstrierte Version des zuverlässigen Lieferprotokolls ist robust und flexibel genug, um die zuvor definierten Anforderungen zu erfüllen. Ich möchte aber hinzufügen, dass die beschriebene Implementierung verbessert werden kann. Um beispielsweise den Durchsatz zu erhöhen und Timer-Perioden dynamisch zu ändern, können dem Protokoll Mechanismen wie Schiebefenster und RTT hinzugefügt werden. Außerdem ist es nützlich, einen Mechanismus zur Bestimmung der MTU zwischen Verbindungsknoten zu implementieren (jedoch nur, wenn große Nachrichten gesendet werden). .

Vielen Dank für Ihre Aufmerksamkeit, ich freue mich auf Ihre Kommentare und Kommentare.

PS Für alle, die sich für die Details interessieren oder einfach nur das Protokoll testen möchten, der Link zum Projekt auf GitHube:
Zuverlässiges UDP-Projekt

Nützliche Links und Artikel

  1. TCP-Protokollspezifikation: auf Englisch и in Englisch
  2. UDP-Protokollspezifikation: auf Englisch и in Englisch
  3. Diskussion des RUDP-Protokolls: Draft-ietf-sigtran-reliable-udp-00
  4. Zuverlässiges Datenprotokoll: rfc 908 и rfc 1151
  5. Eine einfache Implementierung der Lieferbestätigung über UDP: Übernehmen Sie mit .NET und UDP die vollständige Kontrolle über Ihr Netzwerk
  6. Artikel, der NAT-Traversal-Mechanismen beschreibt: Peer-to-Peer-Kommunikation über Network Address Translators
  7. Implementierung des asynchronen Programmiermodells: Implementierung des asynchronen CLR-Programmiermodells и So implementieren Sie das IAsyncResult-Entwurfsmuster
  8. Portierung des asynchronen Programmiermodells auf das aufgabenbasierte asynchrone Muster (APM in TAP):
    TPL und traditionelle asynchrone .NET-Programmierung
    Interop mit anderen asynchronen Mustern und Typen

Update: Vielen Dank Bürgermeister и Sidristij für die Idee, der Schnittstelle eine Aufgabe hinzuzufügen. Die Kompatibilität der Bibliothek mit alten Betriebssystemen wird nicht verletzt, denn Das 4. Framework unterstützt sowohl XP- als auch 2003-Server.

Source: habr.com

Kommentar hinzufügen