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
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
- Zuverlässige Paketlieferung, die durch einen Mechanismus der positiven Rückmeldung (das sogenannte positive acknowledgment) realisiert wird
- Die Notwendigkeit einer effektiven Übertragung großer Datenmengen, d.h. das Protokoll sollte überflüssige Paketneusendungen vermeiden
- Es sollte die Möglichkeit bestehen, den Bestätigungsmechanismus der Lieferung abzubrechen (die Möglichkeit, als „reines“ UDP-Protokoll zu funktionieren)
- Die Möglichkeit der Implementierung eines Betriebmodus mit Bestätigung jeder Nachricht
- 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 und beschrieben 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:
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:
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 , mit UDP in , 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:
Die Struktur des Zuverlässiges UDP-Headers ist ziemlich einfach:

- 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:
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:
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:

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:

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:
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:
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:
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:
Nützliche Links und Artikel
- Spezifikation des TCP-Protokolls: und
- Spezifikation des UDP-Protokolls: und
- Diskussion des RUDP-Protokolls:
- Reliable Data Protocol: und
- Einfache Implementierung der Zustellbestätigung über UDP:
- Artikel, der Mechanismen zur Überwindung von NATs beschreibt:
- Implementierung des asynchronen Programmiermodells: und
- Übertragung des asynchronen Programmiermodells in ein auf Aufgaben basierendes asynchrones Muster (APM in TAP):
Update: Danke und 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
