Implementierung des Reliable UDP-Protokolls für .Net

Das Internet hat sich seit langem verändert. Ein grundlegendes Protokoll des Internets – UDP – wird von Anwendungen nicht nur zur Zustellung von Datagrammen und Broadcasts verwendet, sondern auch zur Gewährleistung von „Peer-to-Peer“-Verbindungen zwischen Netzwerknoten. Aufgrund seiner einfachen Struktur hat dieses Protokoll zahlreiche unerwartete Anwendungsmöglichkeiten gefunden, jedoch sind die Protokollnachteile, wie das Fehlen einer garantierten Lieferung, dabei nicht verschwunden. In diesem Artikel wird die Implementierung eines Protokolls zur garantierten Lieferung über UDP beschrieben.
Inhalt:Einleitung
Anforderungen an das Protokoll
Header von Reliable UDP
Allgemeine Funktionsprinzipien des Protokolls
Zeitüberschreitungen und Timer des Protokolls
Zustandsdiagramm der Übertragung von Reliable UDP
Tiefer in den Code. Steuerblock der Übertragung
Tiefer in den Code. Zustände

Tiefer in den Code. Erstellung und Etablierung von Verbindungen
Tiefer in den Code. Schließen der Verbindung aufgrund von Zeitüberschreitung
Tiefer in den Code. Wiederherstellung der Datenübertragung
API Reliable UDP
Fazit
Nützliche Links und Artikel

Einleitung

Die ursprüngliche Architektur des Internets sah einen einheitlichen Adressraum vor, in dem jeder Knoten eine globale und einzigartige IP-Adresse hatte und direkt mit anderen Knoten kommunizieren konnte. Heutzutage hat das Internet in der Tat eine andere Architektur – einen Bereich globaler IP-Adressen und viele Bereiche mit privaten Adressen, die hinter NAT-Geräten verborgen sind.In einer solchen Architektur können nur Geräte, die sich im globalen Adressraum befinden, problemlos mit anderen im Netzwerk interagieren, da sie eine einzigartige, global routierbare IP-Adresse haben. Ein Knoten in einem privaten Netzwerk kann sich mit anderen Knoten in demselben Netzwerk verbinden sowie mit anderen, gut bekannten Knoten im globalen Adressraum. Diese Interaktion wird maßgeblich durch den Mechanismus der Netzwerkadressübersetzung ermöglicht. NAT-Geräte, wie beispielsweise WLAN-Router, erstellen spezielle Einträge in den Übersetzungstabellen für ausgehende Verbindungen und ändern IP-Adressen und Portnummern in den Paketen. Dies ermöglicht die Herstellung einer ausgehenden Verbindung aus dem privaten Netzwerk zu Knoten im globalen Adressraum. Gleichzeitig blockieren NAT-Geräte normalerweise gesamten eingehenden Traffic, wenn keine spezifischen Regeln für eingehende Verbindungen festgelegt sind.

Eine solche Architektur des Internets ist ziemlich gut geeignet für die Client-Server-Interaktion, bei der sich Clients in privaten Netzwerken befinden können, während Server globale Adressen haben. Doch sie schafft Schwierigkeiten für die direkte Verbindung zwischen zwei Knoten in verschiedenen privaten Netzwerken. Privaten Netzwerken. Die direkte Verbindung zwischen zwei Knoten ist für Peer-to-Peer-Anwendungen wichtig, wie z. B. Sprachübertragung (Skype), Fernzugriff auf Computer (TeamViewer) oder Online-Spiele.

Eine der effektivsten Methoden zur Herstellung einer Peer-to-Peer-Verbindung zwischen Geräten in verschiedenen privaten Netzwerken wird „Hole Punching“ genannt. Diese Technik wird häufig mit Anwendungen auf Basis des UDP-Protokolls verwendet.

Wenn Ihre Anwendung jedoch eine garantierte Datenlieferung benötigt, wie zum Beispiel beim Dateiübertrag zwischen Computern, dann treten beim Einsatz von UDP zahlreiche Schwierigkeiten auf, da UDP kein Protokoll für garantierte Lieferung ist und die Paketübertragung nicht in der richtigen Reihenfolge erfolgt, im Gegensatz zum TCP-Protokoll.

In diesem Fall ist es notwendig, ein Anwendungsprotokoll zu implementieren, das die erforderliche Funktionalität für die garantierte Lieferung der Pakete bereitstellt und über UDP arbeitet.

Ich möchte zunächst darauf hinweisen, dass es die Technik des TCP Hole Punching gibt, um TCP-Verbindungen zwischen Knoten in verschiedenen privaten Netzwerken herzustellen. Aufgrund der fehlenden Unterstützung durch viele NAT-Geräte wird sie jedoch normalerweise nicht als primäre Methode zur Verbindung solcher Knoten betrachtet.

Im Folgenden werde ich in diesem Artikel nur die Implementierung des Protokolls für die garantierte Lieferung betrachten. Die Implementierung der Technik des UDP Hole Punching wird in den folgenden Artikeln beschrieben.

Anforderungen an das Protokoll

  1. Zuverlässige Paketlieferung, die durch einen Mechanismus der positiven Rückmeldung (das sogenannte positive acknowledgment) realisiert wird
  2. Die Notwendigkeit einer effektiven Übertragung großer Datenmengen, d.h. das Protokoll sollte überflüssige Paketneusendungen vermeiden
  3. Es sollte die Möglichkeit bestehen, den Bestätigungsmechanismus der Lieferung abzubrechen (die Möglichkeit, als „reines“ UDP-Protokoll zu funktionieren)
  4. Die Möglichkeit der Implementierung eines Betriebmodus mit Bestätigung jeder Nachricht
  5. Die Grundeinheit der Datentransmission im Protokoll sollte eine Nachricht sein

Diese Anforderungen stimmen in vielerlei Hinsicht mit den Anforderungen des Reliable Data Protocols überein, die in rfc 908 und rfc 1151beschrieben sind, und ich habe bei der Entwicklung dieses Protokolls auf diesen Standards basiert.

Um diese Anforderungen zu verstehen, betrachten wir die zeitlichen Diagramme der Datenübertragung zwischen zwei Knoten im Netzwerk über die Protokolle TCP und UDP. Angenommen, in beiden Fällen geht ein Paket verloren.
Übertragung nicht-interaktiver Daten über TCP:Implementierung des Reliable UDP-Protokolls für .Net

Wie aus dem Diagramm ersichtlich, wird im Falle von Paketverlusten TCP das verlorene Paket erkennen und den Absender darüber informieren, indem es um die Nummer des verlorenen Segments bittet.
Übertragung von Daten über das UDP-Protokoll:Implementierung des Reliable UDP-Protokolls für .Net

UDP unternimmt keine Schritte zur Erkennung von Verlusten. Die Fehlerkontrolle bei der Übertragung im UDP-Protokoll liegt vollständig in der Verantwortung der Anwendung.

Die Fehlersuche im TCP-Protokoll erfolgt durch die Einrichtung einer Verbindung zum Endknoten, das Beibehalten des Zustands dieser Verbindung, die Angabe der gesendeten Byte-Nummer in jeder Paketkopfdaten und die Benachrichtigungen über den Erhalt mithilfe der Bestätigungsnummer „acknowledge number“.

Zusätzlich verwendet das TCP-Protokoll zur Leistungssteigerung (d.h. zum Senden von mehr als einem Segment ohne Erhalt einer Bestätigung) das sogenannte Übertragungsfenster – die Anzahl der Byte-Daten, die der Sender des Segments erwartet zu erhalten.

Detailliertere Informationen zum TCP-Protokoll finden Sie in rfc 793, mit UDP in rfc 768, wo sie eigentlich definiert sind.

Aus dem Obigen wird klar, dass zur Erstellung eines zuverlässigen Nachrichtenübertragungsprotokolls über UDP (im Folgenden genannt Zuverlässiges UDP), Mechanismen zur Datenübertragung ähnlich denen von TCP implementiert werden müssen. Konkret:

  • den Verbindungsstatus beizubehalten
  • die Segmentnummerierung zu verwenden
  • spezielle Bestätigungspakete zu verwenden
  • einen vereinfachten Fenstermechanismus zu verwenden, um die Durchsatzkapazität des Protokolls zu erhöhen

Zusätzlich ist erforderlich:

  • das Signal zu senden, wenn eine Nachricht beginnt, um Ressourcen für die Verbindung bereitzustellen
  • das Signal zu senden, wenn eine Nachricht endet, um die empfangene Nachricht an die übergeordnete Anwendung zu übertragen und die Ressourcen des Protokolls freizugeben
  • dem Protokoll zu ermöglichen, für bestimmte Verbindungen den Bestätigungsmechanismus zu deaktivieren, um als 'reines' UDP zu funktionieren

Header von Reliable UDP

Erinnern wir uns, dass ein UDP-Datagramm in ein IP-Datagramm eingekapselt wird. Das Paket Zuverlässiges UDP wird entsprechend in ein UDP-Datagramm 'verpackt'.
Einkapselung des Zuverlässiges UDP-Headers:Implementierung des Reliable UDP-Protokolls für .Net

Die Struktur des Zuverlässiges UDP-Headers ist ziemlich einfach:

Implementierung des Reliable UDP-Protokolls für .Net

  • Flags – Steuerflags des Pakets
  • MessageType – Nachrichtentyp, wird von übergeordneten Anwendungen verwendet, um sich für bestimmte Nachrichten anzumelden
  • TransmissionId — Übertragungsnummer, zusammen mit der Adresse und dem Port des Empfängers eindeutig die Verbindung definiert
  • PacketNumber – Paketnummer
  • Options – zusätzliche Optionen des Protokolls. Im Fall des ersten Pakets wird es verwendet, um die Größe der Nachricht anzugeben

Die Flags sind wie folgt:

  • FirstPacket — erstes Paket der Nachricht
  • NoAsk — die Nachricht erfordert keine Aktivierung des Bestätigungsmechanismus
  • LastPacket — letztes Paket der Nachricht
  • RequestForPacket — Bestätigungspaket oder Anfrage nach einem verlorenen Paket

Allgemeine Funktionsprinzipien des Protokolls

Da Zuverlässiges UDP auf die garantierte Übertragung einer Nachricht zwischen zwei Knoten ausgelegt ist, muss es in der Lage sein, eine Verbindung zur anderen Seite herzustellen. Um eine Verbindung herzustellen, sendet die Senderseite ein Paket mit dem Flag FirstPacket, auf welches die Verbindungsherstellung signalisiert wird. Alle Antwortpakete oder anders gesagt, Bestätigungspakete setzen immer den Wert des Feldes PacketNumber um eins höher als den höchsten Wert von PacketNumber bei erfolgreich empfangenen Paketen. Im Feld Options wird für das erste gesendete Paket die Größe der Nachricht eingetragen.

Ein ähnlicher Mechanismus wird zum Abschluss der Verbindung verwendet. Im letzten Paket wird das LastPacket-Flag gesetzt. Im Antwortpaket wird die Nummer des letzten Pakets + 1 angegeben, was für die empfangende Seite die erfolgreiche Lieferung der Nachricht bedeutet.
Diagramm zur Einrichtung und Beendigung der Verbindung:Implementierung des Reliable UDP-Protokolls für .Net

Wenn die Verbindung hergestellt ist, beginnt die Datenübertragung. Die Daten werden in Paketen übertragen. Jeder Block, außer dem letzten, enthält eine feste Anzahl von Paketen. Dies entspricht der Größe des Empfangs-/Übertragungsfensters. Der letzte Datenblock kann weniger Pakete enthalten. Nach dem Versand jedes Blocks wartet die sendende Seite auf eine Empfangsbestätigung oder eine Anfrage für die erneute Zustellung verlorener Pakete und hält das Empfangs-/Übertragungsfenster für Antworten offen. Nach Erhalt der Empfangsbestätigung für den Block wird das Empfangs-/Übertragungsfenster verschoben und der nächste Datenblock wird gesendet.

Die empfangende Seite nimmt die Pakete an. Jedes Paket wird auf den Eingang im Übertragungsfenster überprüft. Pakete, die nicht ins Fenster passen, sowie Duplikate werden herausgefiltert. Da die Fenstergröße strikt festgelegt und sowohl beim Empfänger als auch beim Sender gleich ist, wird das Fenster bei der Lieferung eines Paketblocks ohne Verluste verschoben, um die Pakete des nächsten Datenblocks zu empfangen, und eine Empfangsbestätigung wird gesendet. Wenn das Fenster innerhalb des festgelegten Zeitraums des Arbeitszeitgebers nicht gefüllt wird, wird eine Überprüfung durchgeführt, welche Pakete nicht zugestellt wurden, und Rücksendungsanforderungen werden gesendet.
Diagramm der erneuten Übertragung:Implementierung des Reliable UDP-Protokolls für .Net

Zeitüberschreitungen und Timer des Protokolls

Es gibt mehrere Gründe, warum keine Verbindung hergestellt werden kann. Zum Beispiel, wenn die empfangende Seite offline ist. In diesem Fall wird die Verbindung beim Versuch, sie herzustellen, aufgrund eines Timeout geschlossen. In der Implementierung von Reliable UDP werden zwei Timer zum Festlegen von Timeouts verwendet. Der erste, der Arbeitstimer, dient zum Warten auf eine Antwort vom entfernten Host. Wenn er auf der sendenden Seite abläuft, wird das zuletzt gesendete Paket erneut gesendet. Wenn der Timer jedoch auf der Empfängerseite abläuft, wird überprüft, welche Pakete verloren gegangen sind, und es werden Rücksendungsanfragen versendet.

Der zweite Timer ist erforderlich, um die Verbindung im Falle eines Verbindungsabbruchs zwischen den Knoten zu schließen. Für die Senderseite wird er sofort nach dem Auslösen des Arbeitstimers gestartet und wartet auf eine Antwort vom entfernten Knoten. Wenn innerhalb des festgelegten Zeitraums keine Antwort eintrifft, wird die Verbindung beendet und die Ressourcen freigegeben. Für die Empfängerseite wird der Verbindungsschließtimer gestartet, nachdem der Arbeitstimer zweimal ausgelöst wurde. Dies dient als Absicherung gegen den Verlust von Bestätigungs-Paketen. Bei Auslösen des Timers wird ebenfalls die Verbindung beendet und die Ressourcen freigegeben.

Zustandsdiagramm der Übertragung von Reliable UDP

Die Funktionsprinzipien des Protokolls sind in einem endlichen Automaten implementiert, wobei jeder Zustand für eine bestimmte Logik zur Verarbeitung von Paketen verantwortlich ist.
Zustandsdiagramm von Reliable UDP:

Implementierung des Reliable UDP-Protokolls für .Net

Geschlossen – ist tatsächlich kein Zustand, sondern der Start- und Endpunkt für den Automaten. Als Zustand Geschlossen wird der Übertragungssteuerblock betrachtet, der, indem er einen asynchronen UDP-Server implementiert, Pakete an die entsprechenden Verbindungen umleitet und die Zustandsverarbeitung startet.

ErstesPaketSenden – der Ausgangszustand, in dem sich die ausgehende Verbindung beim Senden einer Nachricht befindet.

In diesem Zustand wird das erste Paket für normale Nachrichten gesendet. Für Nachrichten, die ohne Empfangsbestätigung gesendet werden, ist dies der einzige Zustand – hier wird die gesamte Nachricht gesendet.

Sendezyklus – der Hauptzustand für die Übertragung von Nachrichtenpaketen.

Der Übergang in diesen Zustand von ErstesPaketSenden geschieht nach dem Senden des ersten Nachrichtenpakets. In diesen Zustand kommen alle Bestätigungen und Nachforderungsanfragen. Der Ausstieg aus ihm ist in zwei Fällen möglich – entweder bei erfolgreicher Zustellung der Nachricht oder bei Zeitüberschreitung.

ErstesPaketEmpfangen – der Anfangszustand für den Empfänger der Nachricht.

In ihm wird die Korrektheit des Beginns der Übertragung überprüft, notwendige Strukturen werden erstellt, und eine Bestätigung über den Empfang des ersten Pakets wird gesendet.

Für eine Nachricht, die aus einem einzigen Paket besteht und ohne Verwendung einer Empfangsbestätigung gesendet wird – ist dies der einzige Zustand. Nach der Verarbeitung einer solchen Nachricht wird die Verbindung geschlossen.

Zusammenstellen – der Hauptzustand zum Empfang von Nachrichtenpaketen.

In ihm werden Pakete in einem temporären Speicher aufgezeichnet, auf Verluste überprüft, Bestätigungen über die Zustellung von Paketblöcken und der gesamten Nachricht gesendet sowie Anfragen für die erneute Zustellung verlorener Pakete versendet. Im Falle des erfolgreichen Empfangs der gesamten Nachricht wechselt die Verbindung in den Zustand Abgeschlossen, andernfalls wird ein Timeout durchgeführt.

Abgeschlossen – Schließen der Verbindung im Falle des erfolgreichen Empfangs der gesamten Nachricht.

Dieser Zustand ist notwendig, um die Nachricht zusammenzustellen und für den Fall, dass die Zustellbestätigung der Nachricht auf dem Weg zum Absender verloren ging. Der Austritt aus diesem Zustand erfolgt durch ein Timeout, jedoch wird die Verbindung als erfolgreich geschlossen betrachtet.

Tiefer in den Code. Steuerblock der Übertragung

Eines der Schlüsselmomente von Reliable UDP ist der Übertragungssteuerblock. Die Aufgabe dieses Blocks besteht darin, die aktuellen Verbindungen und Hilfselemente zu speichern, die eingehenden Pakete den entsprechenden Verbindungen zuzuordnen, eine Schnittstelle zum Senden von Paketen an die Verbindung bereitzustellen und die API des Protokolls umzusetzen. Der Übertragungssteuerblock akzeptiert Pakete von der UDP-Ebene und leitet sie zur Verarbeitung an den Endzustandsautomaten weiter. Für den Empfang von Paketen verfügt er über einen asynchronen UDP-Server.
Einige Mitglieder der Klasse ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // Byte-Array für den angegebenen Schlüssel. Wird zur Zusammenstellung eingehender Nachrichten verwendet.
  public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set; }
  // Byte-Array für den angegebenen Schlüssel. Wird zum Senden ausgehender Nachrichten verwendet.
  public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
  // Verbindungsaufzeichnung für den angegebenen Schlüssel.
  private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
  // Liste der Abonnenten für Nachrichten.
  private readonly List m_subscribers;
  // Lokaler Socket
  private Socket m_socketIn;
  // Port für eingehende Nachrichten
  private int m_port;
  // Lokale IP-Adresse
  private IPAddress m_ipAddress;
  // Lokale Endpunkt
  public IPEndPoint LocalEndpoint { get; private set; }
  // Sammlung der vorinitialisierten
  // Zustände des Endzustandsautomaten
  public StatesCollection States { get; private set; }
  // Zufallszahlengenerator. Wird zur Erstellung von TransmissionId verwendet
  private readonly RNGCryptoServiceProvider m_randomCrypto;
  //...
}

Implementierung eines asynchronen UDP-Servers:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // erstellen Sie einen neuen Puffer für jeden socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // übergeben Sie den Puffer als Parameter für die asynchrone Methode
  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);
  // Paket empfangen, bereit zum Empfangen des nächsten        
  Receive();
  // da die einfachste Möglichkeit, das Problem mit dem Puffer zu lösen, darin besteht, eine Referenz darauf zu erhalten 
  // aus IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // erhalten Sie den Paketkopf        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // ein ungültiges Paket empfangen - ignorieren Sie es
    return;
  }
  // erstellen Sie einen Schlüssel zur Bestimmung des connection record für das Paket
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // erhalten Sie den vorhandenen connection record oder erstellen Sie einen neuen
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // starten Sie das Paket zur Verarbeitung im endlichen Automaten
  record.State.ReceivePacket(record, header, bytes);
}

Für jede Nachricht wird eine Struktur erstellt, die Informationen über die Verbindung enthält. Eine solche Struktur wird genannt connection record.
Einige Mitglieder der Klasse ReliableUdpConnectionRecord:

interne Klasse ReliableUdpConnectionRecord : IDisposable
{    
  // Byte-Array mit der Nachricht    
  public byte[] IncomingStream { get; set; }
  // Verweis auf den Zustand des Automaten    
  public ReliableUdpState State { get; set; }    
  // Paar, das den Verbindungseintrag eindeutig zuordnet
  // im Steuerblock der Übertragung     
  public Tuple Key { get; private set;}
  // untere Grenze des Empfangsfensters    
  public int WindowLowerBound;
  // Größe des Übertragungsfensters
  public readonly int WindowSize;     
  // Nummer des zu sendenden Pakets
  public int SndNext;
  // Anzahl der zu sendenden Pakete
  public int NumberOfPackets;
  // Übertragungsnummer (dies ist der zweite Teil des Tupels)
  // für jede Nachricht eindeutig	
  public readonly Int32 TransmissionId;
  // Remote-IP-Endpunkt – der eigentliche Empfänger der Nachricht
  public readonly IPEndPoint RemoteClient;
  // Größe des Pakets, um Fragmentierung auf IP-Ebene zu vermeiden
  // darf MTU – (IP.Header + UDP.Header + ReliableUDP.Header) nicht überschreiten
  public readonly int BufferSize;
  // Steuerblock der Übertragung
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // kapselt die Ergebnisse der asynchronen Operation für BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // keine Bestätigungs-Pakete senden
  public bool IsNoAnswerNeeded;
  // zuletzt korrekt empfangenes Paket (immer auf die größte Nummer gesetzt)
  public int RcvCurrent;
  // Array mit den Nummern der verlorenen Pakete
  public int[] LostPackets { get; private set; }
  // wurde das letzte Paket empfangen? Wird als bool verwendet.
  public int IsLastPacketReceived = 0;
  //...
}

Tiefer in den Code. Zustände

Die Zustände implementieren den Automaten des Reliable UDP-Protokolls, in dem die Hauptverarbeitung der Pakete stattfindet. Die abstrakte Klasse ReliableUdpState bietet ein Interface für den Zustand:

Implementierung des Reliable UDP-Protokolls für .Net

Die gesamte Logik des Protokolls wird von den oben genannten Klassen zusammen mit einer Hilfsklasse umgesetzt, die statische Methoden bereitstellt, wie beispielsweise den Aufbau des ReliableUdp-Headers aus dem Verbindungseintrag.

Im Folgenden werden die Implementierungen der Schnittstellenmethoden, die die grundlegenden Algorithmen des Protokolls definieren, im Detail behandelt.

Methode DisposeByTimeout

Die Methode DisposeByTimeout ist dafür verantwortlich, die Ressourcen der Verbindung nach Ablauf des Timers freizugeben und um über die erfolgreiche/nicht erfolgreiche Zustellung der Nachricht zu signalisieren.
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 Zustand überschrieben Abgeschlossen.
Completed.DisposeByTimeout:

geschützt überschreiben void DisposeByTimeout(object Aufzeichnung)
{
  ReliableUdpConnectionRecord verbindungsAufzeichnung = (ReliableUdpConnectionRecord) Aufzeichnung;
  // informieren über den erfolgreichen Erhalt der Nachricht
  SetAsCompleted(verbindungsAufzeichnung);
}

Methode ProcessPackets

Die Methode ProcessPackets ist für die zusätzliche Verarbeitung eines oder mehrerer Pakete verantwortlich. Sie wird direkt oder über einen Paketwartetimer aufgerufen.

Im Zustand Zusammenstellen Die Methode wurde überschrieben und ist für die Überprüfung verlorener Pakete und den Übergang in den Zustand verantwortlich Abgeschlossen, im Falle des Erhalts des letzten Pakets und einer erfolgreichen Überprüfung
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord verbindungsAufzeichnung)
{
  if (verbindungsAufzeichnung.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(verbindungsAufzeichnung, verbindungsAufzeichnung.IsLastPacketReceived != 0))
  {
    // es gibt verlorene Pakete, wir senden Anfragen nach ihnen
    foreach (int seqNum in verbindungsAufzeichnung.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(verbindungsAufzeichnung, seqNum);
      }
    }
    // Timer zum zweiten Mal setzen, für einen erneuten Versuch der Übertragung
    if (!verbindungsAufzeichnung.TimerSecondTry)
    {
      verbindungsAufzeichnung.WaitForPacketsTimer.Change(verbindungsAufzeichnung.ShortTimerPeriod, -1);
      verbindungsAufzeichnung.TimerSecondTry = true;
      return;
    }
    // wenn nach zwei Versuchen des WaitForPacketTimer 
    // die Pakete nicht empfangen wurden - Timer für den Verbindungsabbruch starten
    StartCloseWaitTimer(verbindungsAufzeichnung);
  }
  else if (verbindungsAufzeichnung.IsLastPacketReceived != 0)
  // erfolgreiche Überprüfung 
  {
    // wir senden eine Bestätigung über den Erhalt des Datenblocks
    ReliableUdpStateTools.SendAcknowledgePacket(verbindungsAufzeichnung);
    verbindungsAufzeichnung.State = verbindungsAufzeichnung.Tcb.States.Completed;
    verbindungsAufzeichnung.State.ProcessPackets(verbindungsAufzeichnung);
    // anstelle der sofortigen Implementierung von Ressourcen
    // starten wir den Timer, falls
    // das letzte Ack nicht beim Sender ankommt und er es erneut anfordert.
    // beim Auslösen des Timers - Ressourcen implementieren
    // im Zustand Completed wird die Timer-Methode überschrieben
    StartCloseWaitTimer(verbindungsAufzeichnung);
  }
  // dies ist der Fall, wenn das Ack für den Block von Paketen verloren ging
  else
  {
    if (!verbindungsAufzeichnung.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(verbindungsAufzeichnung);
      verbindungsAufzeichnung.WaitForPacketsTimer.Change(verbindungsAufzeichnung.ShortTimerPeriod, -1);
      verbindungsAufzeichnung.TimerSecondTry = true;
      return;
    }
    // Timer für den Verbindungsabbruch starten
    StartCloseWaitTimer(verbindungsAufzeichnung);
  }
}

Im Zustand Sendezyklus Diese Methode wird nur durch den Timer aufgerufen und ist verantwortlich für die erneute Sendung der letzten Nachricht sowie für das Einschalten des Verbindungsschließ-Timers.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  // Senden Sie das letzte Paket erneut
  // (Im Falle der Wiederherstellung der Verbindung wird der Empfangs-Knoten die Anfragen, die ihn nicht erreicht haben, erneut senden)
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // Starten Sie den CloseWait-Timer – um auf die Wiederherstellung oder Beendigung der Verbindung zu warten
  StartCloseWaitTimer(connectionRecord);
}

Im Zustand Abgeschlossen Die Methode stoppt den Arbeits-Timer und überträgt eine Nachricht an die Abonnenten.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // Sammeln Sie die Nachricht und übergeben Sie sie an die Abonnenten
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Methode ReceivePacket

Im Zustand ErstesPaketEmpfangen Die Hauptaufgabe der Methode besteht darin, zu bestimmen, ob das erste Paket der Nachricht tatsächlich am Interface angekommen ist, sowie die Nachricht, die aus einem einzigen Paket besteht, zu sammeln.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // Paket verwerfen
    return;
  // die Kombination zweier Flags - FirstPacket und LastPacket - sagt uns, dass wir eine einzige Nachricht haben
  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))
    {
      // Bestätigungs-Paket senden
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // von Design beginnen alle Paketnummern mit 0;
  if (header.PacketNumber != 0)
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // Anzahl der erwarteten Pakete zählen
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
  // die Nummer des zuletzt empfangenen Pakets (0) speichern
  connectionRecord.RcvCurrent = header.PacketNumber;
  // Fenster des Empfangs um 1 verschieben
  connectionRecord.WindowLowerBound++;
  // Zustand wechseln
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // falls kein Bestätigungsmechanismus erforderlich ist
  // Timer starten, der alle Strukturen freigibt
  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);
  }
}

Im Zustand Sendezyklus Diese Methode wurde überschrieben, um Bestätigungen über die Zustellung und Anfragen zur erneuten Übertragung zu empfangen.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // Berechnung der oberen Fenstergrenze
  // Es wird die Fenstergrenze + 1 genommen, um die Lieferbestätigungen zu erhalten
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // Überprüfung, ob es im Fenster liegt
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // Überprüfen auf das letzte Paket:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // Übertragung beendet
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // Dies ist eine Antwort auf das erste Paket mit Bestätigung
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // Ohne Fensterverschiebung
    SendPacket(connectionRecord);
  }
  // Bestätigung über den Erhalt des Datenblocks ist eingegangen
  else if (header.PacketNumber == windowHighestBound)
  {
    // Fenster für Empfang/Übertragung verschieben
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // Array für die Übertragungsüberwachung zurücksetzen
    connectionRecord.WindowControlArray.Nullify();
    // Block von Paketen senden
    SendPacket(connectionRecord);
  }
  // Dies ist eine Anfrage zur Wiederübertragung – das erforderliche Paket senden
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Im Zustand Zusammenstellen Im Methode ReceivePacket findet die Hauptarbeit der Zusammenstellung der Nachricht aus den eingehenden Paketen statt.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // Verarbeitung von Paketen ohne Bestätigungsmechanismus
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // Timer zurücksetzen
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // Daten schreiben
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // Wenn das letzte Paket empfangen wurde, beenden
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }
  // Berechnung der oberen Fenstergrenze
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // Pakete, die nicht ins Fenster passen, verwerfen
  if (header.PacketNumber  (windowHighestBound))
    return;
  // Duplikate verwerfen
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // Daten schreiben
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // Paketanzahl erhöhen
  connectionRecord.PacketCounter++;
  // Aktuelle Paketnummer im Fenstersteuerungsarray speichern
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // Höchste empfangene Paketnummer setzen
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // Timer zurücksetzen
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // Wenn das letzte Paket empfangen wurde
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // Wenn alle Pakete im Fenster empfangen wurden, den Zähler zurücksetzen
  // und Bestätigungspaket senden
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // Zähler zurücksetzen.
    connectionRecord.PacketCounter = 0;
    // Übertragungsfenster verschieben
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // Fenstersteuerungsarray zurücksetzen
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // Wenn das letzte Paket bereits empfangen wurde
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // Pakete überprüfen
    ProcessPackets(connectionRecord);
  }
}

Im Zustand Abgeschlossen Die einzige Aufgabe der Methode besteht darin, eine erneute Bestätigung des erfolgreichen Empfangs der Nachricht zu senden.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // erneute Sendung des letzten Pakets, weil
  // die letzte Bestätigung nicht beim Absender angekommen ist
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Methode SendPacket

Im Zustand ErstesPaketSenden Diese Methode sendet das erste Datenpaket oder, wenn die Nachricht keine Bestätigung der Zustellung erfordert, die gesamte Nachricht.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;
  // Wenn keine Bestätigungen erforderlich sind - senden wir alle Pakete
  // und geben Ressourcen frei
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Hier erfolgt die Sendung As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // erstellen Sie den Paketheader und senden Sie ihn
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // den Zähler erhöhen
  connectionRecord.SndNext++;
  // Fenster verschieben
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Timer starten
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Im Zustand Sendezyklus In dieser Methode wird ein Block von Paketen gesendet.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // Senden eines Blocks von Paketen      
  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++;
  }
  // Für den Fall eines großen Übertragungsfensters, setzen Sie den Timer nach dem Senden zurück
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
  {
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  }
}

Tiefer in den Code. Erstellung und Etablierung von Verbindungen

Jetzt, da wir mit den grundlegenden Zuständen und Methoden vertraut sind, die zur Verarbeitung der Zustände verwendet werden, können wir einige Beispiele für das Protokoll genauer betrachten.
Diagramm der Datenübertragung unter normalen Bedingungen:Implementierung des Reliable UDP-Protokolls für .Net

Lassen Sie uns die Erstellung näher betrachten connection record zur Verbindung und zum Versand des ersten Pakets. Der Initiator der Übertragung ist immer die Anwendung, die die API-Methode zum Versand der Nachricht aufruft. Danach wird die Methode StartTransmission des Übertragungssteuerblocks aktiviert, die die Datenübertragung für die neue Nachricht startet.
Herstellen einer ausgehenden Verbindung:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException("", "Sie müssen den Konstruktor mit Parametern verwenden oder den Listener starten, bevor Sie die Nachricht senden.");
    }
    // starten Sie die Verarbeitung eingehender Pakete
    StartListener(LocalEndpoint);
  }
  // erstellen Sie einen Schlüssel für das Wörterbuch auf Basis von EndPoint und ReliableUdpHeader.TransmissionId
  byte[] transmissionId = new byte[4];
  // erstellen Sie eine Zufallsnummer für transmissionId
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // erstellen Sie einen neuen Eintrag für die Verbindung und überprüfen Sie,
  // ob eine solche Nummer bereits in unseren Wörterbüchern vorhanden ist
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // falls vorhanden – generieren Sie die Zufallsnummer erneut
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // wenn es wieder nicht klappt – werfen Sie eine Ausnahme
      throw new ArgumentException("Das Paar TransmissionId & EndPoint existiert bereits im Wörterbuch.");
  }
  // haben den Zustand zur Verarbeitung gestartet
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Versand des ersten Pakets (Zustand FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;
  // ... 
  // erstellen Sie den Paketheader und senden Sie ihn
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // erhöhen Sie den Zähler
  connectionRecord.SndNext++;
  // verschieben Sie das Fenster
  connectionRecord.WindowLowerBound++;
  // wechseln Sie zum Zustand SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // starten Sie den Timer
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Nach dem Versand des ersten Pakets wechselt der Sender in den Zustand Sendezyklus – auf die Bestätigung des Paketempfangs warten.
Die Empfangsseite akzeptiert das gesendete Paket mithilfe der Methode EndReceive, erstellt ein neues connection record und übergibt dieses Paket mit dem zuvor geparsten Header zur Verarbeitung an die Methode ReceivePacket im Zustand. ErstesPaketEmpfangen
Erstellung der Verbindung auf der Empfängerseite:

private void EndReceive(IAsyncResult ar)
{
   ...
   Paket empfangen
   Header des Pakets parsen        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
     Ungültiges Paket erhalten - verwerfen
    return;
  }
   Schlüssel zur Bestimmung des Connection Records für das Paket erstellen
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
   Existierendes Connection Record abrufen oder ein neues erstellen
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
   Paket zur Verarbeitung im Endzustandsautomat starten
  record.State.ReceivePacket(record, header, bytes);
}

Empfang des ersten Pakets und Senden der Bestätigung (Zustand FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
     Paket verwerfen
    return;
   ...
   Bezeichner für alle Packetnummern beginnt mit 0;
  if (header.PacketNumber != 0)          
    return;
   Array zur Speicherung der Nachrichtenteile initialisieren
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
   Daten des Pakets ins Array schreiben
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
   Anzahl der erwartet eintreffenden Pakete zählen
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
   Nummer des letzten empfangenen Pakets (0) speichern
  connectionRecord.RcvCurrent = header.PacketNumber;
   Fenster für den Empfang um 1 verschieben
  connectionRecord.WindowLowerBound++;
   Zustand umschalten
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if ( Wenn kein Bestätigungsmechanismus erforderlich ist)
   ...
  else
  {
     Bestätigung senden
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

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

Die Behandlung von Zeitüberschreitungen ist ein wichtiger Teil von Reliable UDP. Betrachten wir ein Beispiel, in dem an einem Zwischenknoten ein Fehler aufgetreten ist und die Datenlieferung in beiden Richtungen unmöglich geworden ist.
Diagramm zur Schließung der Verbindung aufgrund von Zeitüberschreitung:Implementierung des Reliable UDP-Protokolls für .Net

Wie aus dem Diagramm ersichtlich ist, wird der aktive Timer des Senders sofort nach dem Senden des Paketblocks aktiviert. Dies geschieht im SendPacket-Zustand. Sendezyklus.
Aktivierung des Arbeits-Timers (Zustand SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
   Block von Paketen senden   
   ...   
   Timer nach dem Senden zurücksetzen
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
}

Die Timerperioden werden beim Erstellen der Verbindung festgelegt. Standardmäßig beträgt ShortTimerPeriod 5 Sekunden. Im Beispiel ist er auf 1,5 Sekunden eingestellt.

Der Timer für eingehende Verbindungen wird nach dem Empfang des letzten ankommenden Datenpakets gestartet, was in der Methode ReceivePacket des Zustands geschieht. Zusammenstellen
Aktivierung des Arbeiters Timers (Zustand Assembling):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // Timer neu starten        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

In der eingehenden Verbindung sind während der Wartezeit des Arbeiters Timers keine weiteren Pakete angekommen. Der Timer wurde ausgelöst und die Methode ProcessPackets aufgerufen, in der verlorene Pakete festgestellt und zum ersten Mal Anfragen für die erneute Zustellung gesendet wurden.
Sendung von Anfragen zur erneuten Zustellung (Zustand Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*Überprüfung auf verlorene Pakete */)
  {
    // Anfragen zur erneuten Zustellung senden
    // Timer zum zweiten Mal setzen, für einen erneuten Versuch der Übertragung
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // wenn nach zwei Auslösungen von WaitForPacketTimer 
    // keine Pakete empfangen werden konnten - den Timer zum Schließen der Verbindung starten
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/*der letzte Paket kam an und die Prüfung war erfolgreich */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // wenn die ack für einen Paketblock verloren ging
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // ack erneut senden
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // Timer zum Schließen der Verbindung starten
    StartCloseWaitTimer(connectionRecord);
  }
}

Die Variable TimerSecondTry wurde gesetzt auf true. Diese Variable ist für den erneuten Neustart des Arbeiters Timers verantwortlich.

Auf Seiten des Senders wird ebenfalls der Arbeiters Timer ausgelöst und das zuletzt gesendete Paket erneut gesendet.
Aktivierung des Verbindungsschließtimers (Zustand SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // zuletzt gesendetes Paket erneut senden 
  // ...        
  // CloseWait Timer aktivieren – um auf eine Wiederherstellung der Verbindung oder deren Abschluss zu warten
  StartCloseWaitTimer(connectionRecord);
}

Nach dem wird ein Timer zum Schließen der Verbindung im ausgehenden Anschluss gestartet.
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);
}

Die Wartezeit des Schließen-Timers beträgt standardmäßig 30 Sekunden.

Nach kurzer Zeit wird der Arbeits-Timer auf der Empfängerseite erneut aktiviert, es werden erneut Anfragen gesendet, wonach der Schließen-Timer der eingehenden Verbindung gestartet wird.

Bei Auslösung der Schließen-Timer werden alle Ressourcen beider Verbindungsprotokolle freigegeben. Der Sender informiert die übergeordnete Anwendung über die fehlgeschlagene Zustellung (siehe API Reliable UDP).
Freigabe der Ressourcen des Verbindungsprotokolls:

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. Wiederherstellung der Datenübertragung

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

Wie bereits im Zusammenhang mit dem Schließen der Verbindung aufgrund eines Timeouts besprochen, wird beim Ablaufen des Arbeitstimers beim Empfänger eine Überprüfung auf verlorene Pakete durchgeführt. Im Falle von Paketverlusten wird eine Liste der Paketnummern erstellt, die den Empfänger nicht erreicht haben. Diese Nummern werden im Array LostPackets der spezifischen Verbindung gespeichert, und es erfolgt das Versenden von Anfragen für eine erneute Zustellung.
Versenden von Anfragen zur erneuten Zustellung von Paketen (Zustand Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // es gibt verlorene Pakete, Anfragen werden auf diese geschickt
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Der Sender wird die Anfrage zur erneuten Zustellung akzeptieren und die fehlenden Pakete versenden. Es ist zu beachten, dass zu diesem Zeitpunkt bereits der Timer zum Schließen der Verbindung beim Sender gestartet wurde und bei Erhalt der Anfrage zurückgesetzt wird.
Erneute Zustellung verlorener Pakete (Zustand SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // Timer für das Schließen der Verbindung zurücksetzen 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // dies ist eine Anfrage zur Wiederübertragung – senden Sie das erforderliche Paket          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Das erneut gesendete Paket (packet#3 im Diagramm) wird von der eingehenden Verbindung empfangen. Es erfolgt eine Überprüfung auf die Überfüllung des Empfangsfensters, und die normale Datenübertragung wird wiederhergestellt.
Überprüfung auf den Empfang im Fenster (Zustand Assembling):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // erhöhen Sie den Paketzähler        
  connectionRecord.PacketCounter++;
  // Speichern der aktuellen Paketnummer im Fenstersteuerungsarray        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // Festlegen des höchsten empfangenen Pakets        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // Timer neu starten        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // wenn alle Pakete des Fensters empfangen wurden, zurücksetzen des Zählers
  // und Senden des Bestätigungspakets
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // Zähler zurücksetzen.      
    connectionRecord.PacketCounter = 0;
    // Fenster verschieben
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // Rücksetzen des Übertragungssteuerungsarrays
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API Reliable UDP

Für die Interaktion mit dem Datenübertragungsprotokoll gibt es eine öffentliche Klasse Reliable Udp, die eine Anwendung über den Übertragungssteuerblock ist. Hier sind die wichtigsten Mitglieder der Klasse:

public sealed class ReliableUdp : IDisposable
{
  // erhält den lokalen Endpunkt
  public IPEndPoint LocalEndpoint    
  // erstellt eine Instanz von ReliableUdp und startet
  // das Abhören von eingehenden Paketen an der angegebenen IP-Adresse
  // und dem Port. Der Wert 0 für den Port bedeutet die Verwendung
  // eines dynamisch zugewiesenen Ports
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // Abonnieren des Empfangs von eingehenden Nachrichten
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // Abbestellen des Empfangs von Nachrichten
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // asynchron eine Nachricht senden 
  // Hinweis: Die Kompatibilität zu XP und Server 2003 bleibt erhalten, da .NET Framework 4.0 verwendet wird
  public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // Beginn der asynchronen Nachrichtenübermittlung
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // Ergebnis der asynchronen Übermittlung erhalten
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // Ressourcen freigeben
  public void Dispose()    
}

Der Empfang von Nachrichten erfolgt durch Abonnements. Die Signatur des Delegaten für die Rückruffunktion lautet:

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

Nachricht:

public class ReliableUdpMessage
{
  // Typ der Nachricht, einfache Enumeration
  public ReliableUdpMessageTypes Type { get; private set; }
  // Daten der Nachricht
  public byte[] Body { get; private set; }
  // wenn auf true gesetzt – wird der Bestätigungsmechanismus deaktiviert
  // für die Übertragung einer bestimmten Nachricht
  public bool NoAsk { get; private set; }
}

Für das Abonnieren eines bestimmten Nachrichtentyps und/oder eines bestimmten Absenders werden zwei optionale Parameter verwendet: ReliableUdpMessageTypes messageType und IPEndPoint ipEndPoint.

Nachrichtentypen:

public enum ReliableUdpMessageTypes : short
{ 
  // Alle
  Any = 0,
  // Anfrage an den STUN-Server 
  StunRequest = 1,
  // Antwort vom STUN-Server
  StunResponse = 2,
  // Dateiübertragung
  FileTransfer =3,
  // ...
}

Das Senden von Nachrichten erfolgt asynchron, dafür wird im Protokoll ein asynchrones Programmiermodell implementiert:

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

Das Ergebnis der Nachrichtenübermittlung wird true sein – wenn die Nachricht erfolgreich beim Empfänger angekommen ist und false – wenn die Verbindung aufgrund eines Zeitlimits geschlossen wurde:

public bool EndSendMessage(IAsyncResult asyncResult)

Fazit

Vieles wurde nicht im Rahmen dieses Artikels beschrieben. Die Mechanismen zur Abstimmung von Streams, die Verarbeitung von Ausnahmen und Fehlern sowie die Implementierung asynchroner Methoden zum Versenden von Nachrichten. Aber das Protokollkern, die Beschreibung der Paketverarbeitung, die Verbindungseinrichtung und die Behandlung von Timeouts sollten für Sie klarer werden.

Die demonstrierte Version des zuverlässigen Zustellprotokolls ist ausreichend stabil und flexibel und entspricht bestimmten zuvor festgelegten Anforderungen. Ich möchte jedoch hinzufügen, dass die beschriebene Implementierung verbessert werden kann. Zum Beispiel könnten Mechanismen wie Sliding Window und RTT hinzugefügt werden, um die Bandbreite zu erhöhen und die Timer-Intervalle dynamisch zu ändern. Außerdem wäre die Implementierung eines Mechanismus zur Bestimmung der MTU zwischen den Verbindungsknoten nützlich (aber nur, wenn große Nachrichten gesendet werden).

Danke für Ihre Aufmerksamkeit, ich warte auf Ihre Kommentare und Anmerkungen.

P.S. Für diejenigen, die an Details interessiert sind oder einfach das Protokoll testen möchten, hier ist der Link zum Projekt auf GitHub:
Projekt Reliable UDP

Nützliche Links und Artikel

  1. Spezifikation des TCP-Protokolls: in Englisch und in Russisch
  2. Spezifikation des UDP-Protokolls: in Englisch und in Russisch
  3. Diskussion des RUDP-Protokolls: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 und rfc 1151
  5. Einfache Implementierung der Zustellbestätigung über UDP: Übernehmen Sie die vollständige Kontrolle über Ihr Netzwerk mit .NET und UDP
  6. Artikel, der Mechanismen zur Überwindung von NATs beschreibt: Peer-to-Peer-Kommunikation über Netzwerkadressübersetzer
  7. Implementierung des asynchronen Programmiermodells: Implementierung des CLR-Asynchronen Programmiermodells und Wie man das IAsyncResult-Designmuster implementiert
  8. Übertragung des asynchronen Programmiermodells in ein auf Aufgaben basierendes asynchrones Muster (APM in TAP):
    TPL und traditionelles .NET asynchrones Programmieren
    Interop mit anderen asynchronen Mustern und Typen

Update: Danke mayorovp und sidristij für den Vorschlag, die Aufgabe zur Schnittstelle hinzuzufügen. Die Kompatibilität der Bibliothek mit alten Betriebssystemen wird nicht beeinträchtigt, da das .NET Framework 4 sowohl XP als auch Windows Server 2003 unterstützt.

Quelle: habr.com

60GB SSD 8Gb DDR4