Das Internet hat sich schon längst verändert. Ein grundlegendes Protokoll des Internets – UDP – wird nicht nur von Anwendungen zur Übermittlung von Datagrammen und Broadcasts verwendet, sondern auch zur Herstellung von "Peer-to-Peer"-Verbindungen zwischen Knoten im Netzwerk. Aufgrund seiner einfachen Struktur hat dieses Protokoll viele ungeplante Anwendungsmöglichkeiten gefunden, wobei die Nachteile des Protokolls, wie das Fehlen einer garantierten Lieferung, jedoch bestehen bleiben. In diesem Artikel wird die Implementierung eines garantierten Lieferprotokolls über UDP beschrieben.
Inhalt:
Einleitung
Die ursprüngliche Architektur des Internets sah ein einheitliches Adressraum vor, in dem jeder Knoten eine globale und einzigartige IP-Adresse hatte und direkt mit anderen Knoten kommunizieren konnte. Heute hat das Internet jedoch eine andere Architektur – einen Bereich globaler IP-Adressen und zahlreiche 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 Netz interagieren, da sie über eine eindeutige, global routbare IP-Adresse verfügen. Ein Knoten in einem privaten Netzwerk kann sich mit anderen Knoten innerhalb desselben Netzwerks verbinden und auch mit anderen bekannten Knoten im globalen Adressraum kommunizieren. Diese Interaktion wird maßgeblich durch das Mechanismus der Adressübersetzung ermöglicht. NAT-Geräte, wie Wi-Fi-Router, erstellen spezielle Einträge in Übersetzungstabellen für ausgehende Verbindungen und modifizieren die IP-Adressen sowie die Portnummern in den Paketen. Dies ermöglicht es, aus einem privaten Netzwerk Verbindung zu Knoten im globalen Adressraum aufzubauen. Gleichzeitig blockieren NAT-Geräte jedoch in der Regel den gesamten eingehenden Verkehr, es sei denn, es sind spezifische Regeln für eingehende Verbindungen festgelegt.
Diese Architektur des Internets ist für die Client-Server-Interaktion durchaus geeignet, da Clients sich in privaten Netzwerken befinden können, während Server eine globale Adresse haben. Allerdings führt sie zu Schwierigkeiten bei der direkten Verbindung zweier Knoten zwischen verschiedenen privaten Netzwerken. Eine direkte Verbindung zwischen zwei Knoten ist wichtig für "Peer-to-Peer"-Anwendungen wie Sprachübertragung (Skype), den Fernzugriff auf Computer (TeamViewer) oder Online-Spiele.
Eine der effektivsten Methoden zur Etablierung einer Peer-to-Peer-Verbindung zwischen Geräten in verschiedenen privaten Netzwerken nennt man "Hole Punching". Diese Technik wird häufig mit Anwendungen, die auf dem UDP-Protokoll basieren, verwendet.
Sollte Ihre Anwendung jedoch eine garantierte Datenlieferung erfordern, beispielsweise beim Übertragen von Dateien zwischen Computern, treten bei der Nutzung von UDP viele Schwierigkeiten auf, da UDP kein Protokoll für die garantierte Lieferung ist und die Pakete im Gegensatz zum TCP-Protokoll nicht in der richtigen Reihenfolge ankommen.
In diesem Fall ist es erforderlich, ein Anwendungsprotokoll zu implementieren, das die notwendige Funktionalität für die garantierte Paketzustellung bietet und auf UDP basiert.
Ich möchte gleich darauf hinweisen, dass es die Technik des TCP Hole Punching gibt, um TCP-Verbindungen zwischen Knoten in verschiedenen privaten Netzwerken herzustellen. Aufgrund der mangelnden Unterstützung durch viele NAT-Geräte wird sie jedoch normalerweise nicht als Hauptmethode zur Verbindung solcher Knoten betrachtet.
In diesem Artikel werde ich mich ausschließlich mit der Implementierung eines Protokolls für die garantierte Paketzustellung beschäftigen. Die Technik des UDP Hole Punching wird in zukünftigen Artikeln beschrieben.
Anforderungen an das Protokoll
- Zuverlässige Paketzustellung, umgesetzt durch einen Mechanismus der positiven Bestätigung (sog. positive acknowledgment).
- Die Notwendigkeit einer effizienten Übertragung großer Datenmengen, d.h. das Protokoll sollte überflüssige Paketweiterleitungen vermeiden.
- Es muss die Möglichkeit bestehen, den Bestätigungsmechanismus zur Lieferung abzubrechen (die Funktion als 'reines' UDP-Protokoll).
- Die Möglichkeit zur Implementierung eines Command-Modus mit Bestätigung jeder Nachricht.
- Die grundlegende Einheit für den Datentransfer nach dem Protokoll muss eine Nachricht sein.
Diese Anforderungen stimmen weitgehend mit den Anforderungen des Reliable Data Protocol überein, die in und , beschrieben sind, und ich habe mich bei der Entwicklung dieses Protokolls auf diese Standards gestützt.
Um diese Anforderungen zu verstehen, betrachten wir die zeitlichen Diagramme für den Datentransfer 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 ist, wird TCP im Falle von Paketverlusten den verlorenen Paket erkennen und den Absender darüber informieren, indem die Nummer des verlorenen Segments angefordert wird.
Datenübertragung über das UDP-Protokoll:
UDP unternimmt keine Schritte zur Verlusterkennung. Die Fehlerkontrolle bei der Übertragung im UDP-Protokoll liegt vollständig in der Verantwortung der Anwendung.
Die Fehlererkennung im TCP-Protokoll erfolgt durch die Herstellung einer Verbindung zum Endknoten, das Beibehalten des Zustands dieser Verbindung, die Angabe der Nummer der gesendeten Bytes in jedem Paket-Header und die Benachrichtigungen über den Empfang mittels der Bestätigungsnummer ‚acknowledge number‘.
Zusätzlich nutzt das TCP-Protokoll zur Steigerung der Leistung (d. h. das Senden von mehr als einem Segment ohne Bestätigung) das sogenannte Übertragungsfenster — die Anzahl der Bytes, die der Segmentsender erwartet zu empfangen.
Eine detailliertere Erklärung des TCP-Protokolls finden Sie in , für UDP in , wo sie tatsächlich definiert sind.
Aus dem Vorhergehenden wird deutlich, dass zur Schaffung eines zuverlässigen Nachrichtenübertragungsprotokolls über UDP (im Folgenden bezeichnen wir es als Reliable UDP) ähnliche Mechanismen zur Datenübertragung wie bei TCP realisiert werden müssen. Nämlich:
- den Verbindungszustand zu speichern
- Segmentnummerierung zu verwenden
- spezielle Bestätigungs-Pakete einzusetzen
- einen vereinfachten Fenstermechanismus zur Erhöhung der Durchsatzkapazität des Protokolls zu verwenden
Zusätzlich ist erforderlich:
- den Beginn der Nachricht zu signalisieren, um Ressourcen für die Verbindung bereitzustellen
- das Ende der Nachricht zu signalisieren, um die erhaltene Nachricht an die übergeordnete Anwendung zu übertragen und Ressourcen des Protokolls freizugeben.
- Ermöglicht es dem Protokoll, für bestimmte Verbindungen die Bestätigungsmechanismen abzuschalten, um als „reines“ UDP zu funktionieren.
Überschrift Reliable UDP
Denken wir daran, dass ein UDP-Datagramm in ein IP-Datagramm gekapselt ist. Das Reliable UDP-Paket wird entsprechend in ein UDP-Datagramm „eingepackt“.
Kapselung des Reliable UDP-Headers:
Die Struktur des Reliable UDP-Headers ist recht einfach:

- Flags – Steuerflags des Pakets
- MessageType – Nachrichtentyp, der von übergeordneten Anwendungen verwendet wird, um sich für bestimmte Nachrichten anzumelden.
- TransmissionId – Übertragungsnummer, die zusammen mit der Adresse und dem Port des Empfängers die Verbindung eindeutig bestimmt.
- PacketNumber – Paketnummer
- Options – zusätzliche Optionen des Protokolls. Im Falle des ersten Pakets wird sie zur Angabe der Nachrichtenlänge verwendet.
Die Flags sind folgende:
- FirstPacket – erstes Paket der Nachricht
- NoAsk – Nachricht erfordert keine Aktivierung des Bestätigungsmechanismus
- LastPacket – letztes Paket der Nachricht
- RequestForPacket – Bestätigungs-Paket oder Anfrage nach einem verlorenen Paket
Allgemeine Funktionsprinzipien des Protokolls
Da Reliable UDP auf die garantierte Übertragung von Nachrichten zwischen zwei Knoten ausgerichtet ist, muss es in der Lage sein, eine Verbindung zur anderen Seite herzustellen. Um die Verbindung herzustellen, sendet die sendende Seite ein Paket mit dem Flag FirstPacket; die Antwort darauf signalisiert die Herstellung der Verbindung. Alle Antwortpakete oder Bestätigungspakete setzen stets den Wert des Feldes PacketNumber um eins höher als den höchsten Wert von PacketNumber der erfolgreich empfangenen Pakete. Im Feld Options des ersten gesendeten Pakets wird die Größe der Nachricht eingetragen.
Für den Verbindungsabschluss wird ein ähnlicher Mechanismus verwendet. Im letzten Paket der Nachricht wird das Flag LastPacket gesetzt. Im Antwortpaket wird die Nummer des letzten Pakets + 1 angegeben, was für die empfangende Seite die erfolgreiche Zustellung der Nachricht bedeutet.
Diagramm zur Herstellung und zum Abschluss der Verbindung:
Sobald die Verbindung hergestellt ist, beginnt die Datenübertragung. Die Daten werden in Paketblöcken übertragen. Jeder Block, mit Ausnahme des letzten, enthält eine feste Anzahl von Paketen, die der Größe des Empfangs- oder Sendefensters entspricht. Der letzte Datenblock kann eine geringere Anzahl von Paketen haben. Nach dem Versand jedes Blocks wartet der Sender auf eine Empfangsbestätigung oder eine Aufforderung zur erneuten Übertragung verlorener Pakete, während das Empfangs- oder Sendefenster offen bleibt, um Antworten zu erhalten. Nach Erhalt der Empfangsbestätigung für den Block wird das Empfangs-/Sendefenster verschoben und der nächste Datenblock gesendet.
Der Empfänger akzeptiert Pakete. Jedes Paket wird auf die Zugehörigkeit zum Übertragungsfenster überprüft. Pakete, die nicht in das Fenster fallen, sowie Duplikate werden aussortiert. Da die Fenstergröße streng festgelegt und bei Empfänger und Sender identisch ist, wird im Falle der verlustfreien Lieferung eines Pakets die Fensterposition verschoben, um die nächsten Datensätze zu empfangen, und eine Lieferbestätigung wird gesendet. Wenn das Fenster innerhalb des festgelegten Arbeitszeitlimits nicht gefüllt wird, erfolgt eine Überprüfung, welche Pakete nicht zugestellt wurden, und es werden Anfragen zur erneuten Zustellung gestartet.
Diagramm der erneuten Übertragung:
Timeouts 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, eine Verbindung herzustellen, aufgrund eines Timeout geschlossen. In der Implementierung von Reliable UDP werden zwei Timer verwendet, um Timeouts festzulegen. Der erste, der Arbeits-Timer, dient dazu, auf eine Antwort vom Remote-Host zu warten. Wenn dieser auf der Senderseite abläuft, wird das letzte gesendete Paket erneut gesendet. Wenn der Timer jedoch auf der Empfängerseite abläuft, erfolgt eine Überprüfung auf verlorene Pakete, gefolgt von Anfragen zur erneuten Zustellung.
Der zweite Timer ist entscheidend für den Verbindungsabbruch, falls die Kommunikation zwischen den Knoten ausfällt. Auf der Senderseite wird er sofort nach dem Auslösen des Arbeitstimers gestartet und wartet auf eine Antwort vom entfernten Knoten. Wenn innerhalb der festgelegten Zeit keine Antwort erfolgt, wird die Verbindung beendet und die Ressourcen freigegeben. Auf der Empfängerseite wird der Verbindungsschließtimer nach dem doppelten Auslösen des Arbeitstimers gestartet. Dies dient als Absicherung gegen den Verlust des Bestätigungs-Pakets. Bei Auslösen des Timers wird ebenfalls die Verbindung beendet und die Ressourcen freigegeben.
Zustandsdiagramm der Reliable UDP-Übertragung
Die Arbeitsprinzipien des Protokolls sind in einem endlichen Automaten implementiert, wobei jeder Zustand für eine bestimmte Logik der Paketverarbeitung verantwortlich ist.
Zustandsdiagramm von Reliable UDP:

Closed ist in Wirklichkeit kein Zustand, sondern der Start- und Endpunkt des Automaten. Der Zustand Closed bezieht sich auf das Steuerblock für die Übertragung, das durch die Implementierung eines asynchronen UDP-Servers die Pakete in die entsprechenden Verbindungen umleitet und die Zustandsbearbeitung initiiert.
FirstPacketSending – der anfängliche Zustand, in dem sich die ausgehende Verbindung beim Senden einer Nachricht befindet.
In diesem Zustand wird das erste Paket für normale Nachrichten gesendet. Bei Nachrichten ohne Zustellbestätigung ist dies der einzige Zustand – hier wird die gesamte Nachricht gesendet.
SendingCycle – der Hauptzustand für die Übertragung von Nachrichtenpaketen.
Der Übergang in diesen Zustand FirstPacketSending erfolgt nach dem Senden des ersten Nachrichtenpakets. In diesen Zustand kommen alle Bestätigungen und Wiederübertragungsanfragen. Ein Austritt aus diesem Zustand ist in zwei Fällen möglich – bei erfolgreicher Zustellung der Nachricht oder nach Ablauf der Zeit.
FirstPacketReceived – der Anfangszustand für den Empfänger der Nachricht.
In diesem Zustand wird die Korrektheit des Übertragungsbeginns überprüft, notwendige Strukturen werden erstellt und eine Bestätigung des Empfangs des ersten Pakets wird gesendet.
Für eine Nachricht, die aus einem einzigen Paket besteht und ohne Nutzung der Zustellbestätigung gesendet wurde – ist dies der einzige Zustand. Nach der Verarbeitung einer solchen Nachricht wird die Verbindung geschlossen.
Assembling – der Hauptzustand für den Empfang von Nachrichtenpaketen.
In diesem Zustand werden die Pakete im Zwischenspeicher aufgezeichnet, auf Paketverluste überprüft, Bestätigungen über die Zustellung von Paketblöcken und gesamten Nachrichten gesendet sowie Anfragen zur erneuten Zustellung verlorener Pakete verschickt. Bei erfolgreichem Empfang der gesamten Nachricht wechselt die Verbindung in den Zustand Abgeschlossen, andernfalls erfolgt ein Timeout.
Abgeschlossen – Schließen der Verbindung im Falle des erfolgreichen Empfangs der gesamten Nachricht.
Dieser Zustand ist notwendig für den Zusammenbau der Nachricht und für den Fall, dass die Zustellbestätigung der Nachricht auf dem Weg zum Sender verloren gegangen ist. Der Austritt aus diesem Zustand erfolgt nach einem Timeout, wobei die Verbindung als erfolgreich geschlossen betrachtet wird.
Tiefer in den Code. Block zur Steuerung der Übertragung
Eines der Schlüsselsysteme von Reliable UDP ist der Übertragungsmanagementblock. Seine Aufgabe besteht darin, die aktuellen Verbindungen und Hilfselemente zu speichern, ankommende Pakete den entsprechenden Verbindungen zuzuordnen, eine Schnittstelle für das Senden von Paketen an die Verbindung bereitzustellen und die API des Protokolls zu implementieren. Der Übertragungsmanagementblock empfängt Pakete von der UDP-Ebene und leitet sie zur Verarbeitung an den Endzustandsautomaten weiter. Zur Annahme von Paketen wurde ein asynchroner UDP-Server implementiert.
Einige Mitglieder der Klasse ReliableUdpConnectionControlBlock:
interne Klasse ReliableUdpConnectionControlBlock : IDisposable
{
// Byte-Array für den angegebenen Schlüssel. Wird zur Zusammenstellung eingehender Nachrichten verwendet
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> EingehendeStreams { get; private set;}
// Byte-Array für den angegebenen Schlüssel. Wird zum Senden ausgehender Nachrichten verwendet.
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> AusgehendeStreams { get; private set; }
// Verbindungsdatensatz für den angegebenen Schlüssel.
private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listeDerHandler;
// Liste der Abonnenten für Nachrichten.
private readonly List<ReliableUdpSubscribeObject> m_abonnenten;
// Lokaler Socket
private Socket m_socketIn;
// Port für eingehende Nachrichten
private int m_port;
// Lokale IP-Adresse
private IPAddress m_ipAdresse;
// Lokaler Endpunkt
public IPEndPoint LokalerEndpunkt { get; private set; }
// Sammlung von vorab initialisierten
// Zuständen des Automaten
public StatesCollection Zustände { get; private set; }
// Zufallszahlengenerator. Wird zur Erstellung von TransmissionId verwendet
private readonly RNGCryptoServiceProvider m_zufallsCrypto;
//...
}
Implementierung eines asynchronen UDP-Servers:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// einen neuen Puffer für jedes socket.BeginReceiveFrom erstellen
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// Puffer als Parameter für die asynchrone Methode übergeben
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 das nächste zu akzeptieren
Receive();
// der einfachste Weg, um mit dem Puffer umzugehen - eine Referenz darauf aus IAsyncResult.AsyncState erhalten
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// Header des Pakets erhalten
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// ungültiges Paket empfangen - verwerfen
return;
}
// Schlüssel für die Bestimmung des Connection Record für das Paket konstruieren
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// vorhandenen Connection Record erhalten oder neuen erstellen
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// Paket zur Verarbeitung im Zustandsautomat starten
record.State.ReceivePacket(record, header, bytes);
}
Für jede Übertragung einer Nachricht wird eine Struktur erstellt, die Informationen über die Verbindung enthält. Diese Struktur wird genannt Verbindungsprotokoll.
Einige Mitglieder der Klasse ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// Byte-Array mit der Nachricht
public byte[] IncomingStream { get; set; }
// Referenz auf den Zustand des endlichen Automaten
public ReliableUdpState State { get; set; }
// Paar, das den Verbindungsdatensatz eindeutig definiert
// im Übertragungssteuerblock
public Tuple Key { get; private set;}
// Untergrenze des Empfangsfensters
public int WindowLowerBound;
// Größe des Übertragungsfensters
public readonly int WindowSize;
// Paketnummer für den Versand
public int SndNext;
// Anzahl der zu sendenden Pakete
public int NumberOfPackets;
// Übertragungsnummer (diese ist der zweite Teil des Tuples)
// für jede Nachricht die eigene
public readonly Int32 TransmissionId;
// Remote-IP-Endpunkt – der Empfänger der Nachricht
public readonly IPEndPoint RemoteClient;
// Paketgröße, um Fragmentierung auf IP-Ebene zu vermeiden
// sollte MTU – (IP.Header + UDP.Header + RelaibleUDP.Header) nicht überschreiten
public readonly int BufferSize;
// Übertragungssteuerblock
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;
// letztes korrekt empfangenes Paket (wird immer auf die höchste Nummer gesetzt)
public int RcvCurrent;
// Array mit der Nummer verlorener Pakete
public int[] LostPackets { get; private set; }
// Wurde das letzte Paket empfangen? Verwendet als bool.
public int IsLastPacketReceived = 0;
//...
}
Tiefer in den Code. Zustände
Zustände implementieren den Endzustandsautomaten des Protokolls Reliable UDP, in dem die Hauptverarbeitung von Paketen stattfindet. Die abstrakte Klasse ReliableUdpState bietet ein Interface für den Zustand:

Die gesamte Logik des Protokolls wird von den oben dargestellten Klassen zusammen mit einer Hilfsklasse realisiert, die statische Methoden bereitstellt, wie beispielsweise den Aufbau des ReliableUdp-Headers aus dem Verbindungsprotokoll.
Im Folgenden werden die Implementierungen der Interface-Methoden, die die grundlegenden Algorithmen des Protokolls definieren, im Detail betrachtet.
Methode DisposeByTimeout
Die Methode DisposeByTimeout ist für die Freigabe der Verbindungsressourcen nach Ablauf des Zeitlimits verantwortlich und signalisiert, ob die Nachricht erfolgreich oder nicht erfolgreich zugestellt wurde.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Er wird nur im Zustand überschrieben Abgeschlossen.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// Signalisiere, dass die Nachricht erfolgreich empfangen wurde
SetAsCompleted(connectionRecord);
}
Methode ProcessPackets
Die Methode ProcessPackets ist für die zusätzliche Verarbeitung eines oder mehrerer Pakete verantwortlich. Sie wird direkt aufgerufen oder über einen Paketwartezeit-Timer.
Im Zustand Assembling wurde die Methode überschrieben und ist für die Überprüfung verlorener Pakete und den Übergang in den Zustand Abgeschlossen, falls das letzte Paket empfangen wurde und die Überprüfung erfolgreich war
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
\\ es gibt verlorene Pakete, senden Sie Anfragen dafür
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
\\ setzen Sie den Timer zum zweiten Mal für den Übertragungswiederholungsversuch
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\\ falls nach zwei Versuchen von WaitForPacketTimer
\\ die Pakete nicht erhalten wurden - starten wir den Timer zum Schließen der Verbindung
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
\\ erfolgreiche Überprüfung
{
\\ senden Sie die Bestätigung für den Empfang des Datenblocks
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
\\ anstelle der sofortigen Bereitstellung von Ressourcen
\\ starten wir den Timer, falls
\\ der letzte Ack nicht beim Sender ankommt und er ihn erneut anfordert.
\\ beim Auslösen des Timers - implementieren wir die Ressourcen
\\ im Zustand Completed ist die Timer-Methode überschrieben
StartCloseWaitTimer(connectionRecord);
}
\\ dies ist der Fall, wenn der Ack für den Datenblock verloren ging
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\\ starten wir den Timer zum Schließen der Verbindung
StartCloseWaitTimer(connectionRecord);
}
}
Im Zustand SendingCycle Diese Methode wird nur durch den Timer aufgerufen und ist für das erneute Senden der letzten Nachricht sowie für die Aktivierung des Schließtimers der Verbindung zuständig.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// Senden Sie das letzte Paket erneut
// (Im Falle einer Wiederherstellung der Verbindung wird der Empfängerknoten 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 eine Wiederherstellung oder Beendigung der Verbindung zu warten
StartCloseWaitTimer(connectionRecord);
}
Im Zustand Abgeschlossen Die Methode stoppt den aktiven Timer und überträgt die Nachricht an die Abonnenten.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// Nachricht sammeln und an die Abonnenten weitergeben
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Methode ReceivePacket
Im Zustand FirstPacketReceived Die Hauptaufgabe der Methode besteht darin, zu bestimmen, ob wirklich das erste Paket der Nachricht an das Interface angekommen ist, sowie die Nachricht zu sammeln, die aus einem einzigen Paket besteht.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// Paket verwerfen
return;
// Kombination aus zwei Flags - FirstPacket und LastPacket - zeigt an, dass wir eine einzelne 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ätigungspaket senden
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// Aus Design beginnen alle Packetnummern bei 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Anzahl der Pakete berechnen, die kommen sollten
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
// Nummer des letzten empfangenen Pakets aufzeichnen (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// danach das Empfangsfenster um 1 verschieben
connectionRecord.WindowLowerBound++;
// Zustand wechseln
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// wenn 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 SendingCycle Diese Methode wurde überschrieben, um Bestätigungen über die Zustellung und Anfragen zur Wiederü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 Bestätigungen der Zustellung zu erhalten
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// Überprüfung, ob die Nummer 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, ob es das letzte Paket ist:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// Übertragung abgeschlossen
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// Dies ist die Antwort auf das erste Paket mit Bestätigung
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// ohne Fensterverschiebung
SendPacket(connectionRecord);
}
// Es kam eine Bestätigung für den Erhalt des Datenblocks
else if (header.PacketNumber == windowHighestBound)
{
// Fenster für Empfang/Übertragung verschieben
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// Kontrollarray für die Übertragung zurücksetzen
connectionRecord.WindowControlArray.Nullify();
// Block von Paketen senden
SendPacket(connectionRecord);
}
// Dies ist eine Anfrage für eine erneute Übertragung – erforderliches Paket senden
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Im Zustand Assembling In der Methode ReceivePacket erfolgt die Hauptarbeit beim Zusammenstellen der Nachricht aus den eingehenden Paketen.
Zusammenstellen.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 aufzeichnen
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Wenn wir ein Paket mit dem letzten Flagge erhalten haben, beenden wir
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// Berechnung der endgültigen Fenstergrenze
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// Pakete außerhalb des Fensters verwerfen
if (header.PacketNumber (windowHighestBound))
return;
// Duplikate verwerfen
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// Daten aufzeichnen
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Paketanzahl erhöhen
connectionRecord.PacketCounter++;
// aktuellen Paketnummer im Fensterverwaltungsspeicher aufzeichnen
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// Die höchste empfangene Paketnummer festlegen
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 angekommen ist
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// Wenn wir alle Pakete im Fenster erhalten haben, setzen wir den Zähler zurück
// und senden das Bestätigungspaket
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// Zähler zurücksetzen.
connectionRecord.PacketCounter = 0;
// Übertragungsschiebefenster verschieben
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// Übertragungssteuerungsarray zurücksetzen
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// Wenn das letzte Paket bereits vorhanden ist
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 über die erfolgreiche Zustellung der Nachricht zu senden.
Abgeschlossen. ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// erneutes Senden des letzten Pakets, da
// die letzte Bestätigung nicht beim Absender angekommen ist
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Methode SendPacket
Im Zustand FirstPacketSending Diese Methode sendet das erste Datenpaket oder, wenn die Nachricht keine Zustellbestätigung erfordert, die gesamte Nachricht.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// wenn keine Bestätigung erforderlich ist - senden wir alle Pakete
// und geben Ressourcen frei
if (connectionRecord.IsNoAnswerNeeded)
{
// Hier erfolgt das Senden 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));
// erhöhen Sie den Zähler
connectionRecord.SndNext++;
// verschieben Sie das Fenster
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Timer starten
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Im Zustand SendingCycle In dieser Methode erfolgt das Senden eines Pakets.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// Sende Paketblock
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++;
}
// Falls das Übertragungsfenster groß ist, den Timer nach dem Senden zurücksetzen
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
{
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
}
Tiefer in den Code. Verbindungen erstellen und herstellen
Jetzt, da wir mit den grundlegenden Zuständen und Methoden vertraut sind, die zur Verarbeitung von Zuständen verwendet werden, können wir einige Beispiele für die Funktionsweise des Protokolls etwas genauer betrachten.
Datenübertragungsdiagramm unter normalen Bedingungen:
Lassen Sie uns die Erstellung im Detail betrachten Verbindungsprotokoll für die Verbindung und das Senden des ersten Pakets. Der Initiator der Übertragung ist immer die Anwendung, die die API-Methode zum Senden einer Nachricht aufruft. Danach wird die Methode StartTransmission des Übertragungssteuerblocks aktiviert, die die Datenübertragung für die neue Nachricht startet.
Erstellung 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 der Verarbeitung eingehender Pakete
StartListener(LocalEndpoint);
}
// Erstellen eines Schlüssels für das Wörterbuch, basierend auf EndPoint und ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// Erstellen einer zufälligen Nummer transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// Erstellen eines neuen Eintrags für die Verbindung und Überprüfen,
// ob eine solche Nummer bereits in unseren Wörterbüchern existiert
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// Wenn es existiert – dann generieren wir die zufällige Nummer 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 gelang – werfen wir 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 in den 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 SendingCycle – auf die Bestätigung des Paketempfangs warten.
Die empfangende Partei nimmt das gesendete Paket mit der Methode EndReceive an, erstellt einen neuen Verbindungsprotokoll und übergibt dieses Paket mit dem zuvor analysierten Header zur Verarbeitung an die Methode ReceivePacket im Zustand FirstPacketReceived
Einrichten der Verbindung auf der Empfangsseite:
private void EndReceive(IAsyncResult ar)
{
// ...
// Paket empfangen
// Header des Pakets parsen
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// Ungültiges Paket empfangen - wird verworfen
return;
}
// Schlüssel zur Bestimmung des Connection Records für das Paket konstruieren
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
// Bestehenden Connection Record abrufen oder neuen erstellen
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// Paket zur Verarbeitung im endlichen Automaten starten
record.State.ReceivePacket(record, header, bytes);
}
Empfang des ersten Pakets und Versand der Bestätigung (Zustand FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// Paket verwerfen
return;
// ...
// Aus design beginnen alle Paketnummern bei 0;
if (header.PacketNumber != 0)
return;
// Initialisieren des Arrays zur Speicherung der Nachrichtenbestandteile
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// Speichern der Paketdaten im Array
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Berechnen der Anzahl der Pakete, die eintreffen sollten
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// Speichern der letzten empfangenen Paketnummer (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// Nach der Verschiebung verschieben wir das Empfangsfenster um 1
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. Verbindung aufgrund von Timeout schließen
Die Verarbeitung von Zeitüberschreitungen ist ein wichtiger Bestandteil von Reliable UDP. Betrachten wir ein Beispiel, in dem ein Ausfall an einem Zwischenknoten aufgetreten ist und die Datenlieferung in beiden Richtungen nicht mehr möglich ist.
Diagramm zur Schließung der Verbindung aufgrund eines Timeouts:
Wie aus dem Diagramm ersichtlich, wird der Arbeits-Timer des Senders sofort nach dem Senden des Paketblocks aktiviert. Dies geschieht in der Methode SendPacket im Zustand SendingCycle.
Aktivierung des Arbeits-Timers (Zustand SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// Paketblock senden
// ...
// Timer nach dem Senden neu starten
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Die Timer-Perioden werden bei der Erstellung der Verbindung festgelegt. Standardmäßig beträgt ShortTimerPeriod 5 Sekunden. Im Beispiel ist er auf 1,5 Sekunden gesetzt.
Für eingehende Verbindungen wird der Timer nach dem Erhalt des letzten erfolgreich zugestellten Datenpakets aktiviert, dies geschieht in der Methode ReceivePacket im Zustand Assembling
Aktivierung des Arbeits-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);
// ...
}
Im eingehenden Verbindungsversuch wurden im Wartezeitraum des Arbeits-Timers keine weiteren Pakete empfangen. Der Timer ist abgelaufen und rief die Methode ProcessPackets auf, in der verlorene Pakete erkannt und zum ersten Mal Wiederholungsanforderungen gesendet wurden.
Versende Wiederholungsanforderungen (Status: Zusammenstellung):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*Prüfung auf verlorene Pakete */)
{
// Wiederholungsanforderungen senden
// Timer zum zweiten Mal einstellen, um einen erneuten Übertragungsversuch zu starten
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// Wenn nach zwei Versuchen des WaitForPacketTimers
// keine Pakete empfangen wurden - starte den Schließwartetimer
StartCloseWaitTimer(connectionRecord);
}
else if (/*letztes Paket empfangen und erfolgreiche Prüfung */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// falls ack für den Paketblock verloren ging
else
{
if (!connectionRecord.TimerSecondTry)
{
// ack erneut senden
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// starte den Schließwartetimer
StartCloseWaitTimer(connectionRecord);
}
}
Die Variable TimerSecondTry wurde auf truegesetzt. Diese Variable ist für den erneuten Neustart des aktiven Timers verantwortlich.
Auch auf der Senderseite wird der aktive Timer aktiviert und das zuletzt gesendete Paket wird erneut gesendet.
Aktivierung des Schließtimers für die Verbindung (Zustand SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ ...
\/\/ senden Sie das letzte Paket erneut
\/\/ ...
\/\/ aktivieren Sie den CloseWait-Timer – um auf die Wiederherstellung oder den Abbruch der Verbindung zu warten
StartCloseWaitTimer(connectionRecord);
}
Daraufhin wird im ausgehenden Verbindung der Schließtimer aktiviert.
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ßtimers ist standardmäßig auf 30 Sekunden eingestellt.
Nach kurzer Zeit wird der aktive Timer auf der Empfängerseite erneut aktiviert, Anfragen werden erneut gesendet, woraufhin der Schließtimer für die eingehende Verbindung gestartet wird.
Bei Auslösung der Schließtimer werden alle Ressourcen beider Verbindungsdatensätze freigegeben. Der Absender meldet dem übergeordneten Anwendungssystem die fehlgeschlagene Zustellung (siehe API Reliable UDP).
Freigabe der Ressourcen des Verbindungsdatensatzes:
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 bei der Verbindungsschließung aufgrund von Timeouts besprochen, erfolgt beim Ablauf des Arbeitszeitmessers beim Empfänger eine Überprüfung auf verlorene Pakete. Im Falle von Paketverlusten wird eine Liste mit den Nummern der Pakete erstellt, die den Empfänger nicht erreicht haben. Diese Nummern werden in das Array LostPackets des jeweiligen Verbindungsdatensatzes eingetragen, und es werden Wiederholungsanfragen zum Versand veranlasst.
Versand von Wiederholungsanfragen für Pakete (Status: Zusammenstellung):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
\/\/ Es gibt verlorene Pakete, wir senden Anfragen für diese
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
\/\/ ...
}
}
Der Sender wird die Anfrage zur erneuten Zustellung annehmen und die fehlenden Pakete zusenden. Es ist zu beachten, dass zu diesem Zeitpunkt bereits ein Timer zur Schließung der Verbindung beim Sender läuft und beim Empfang der Anfrage zurückgesetzt wird.
Erneute Sendung verlorener Pakete (Zustand SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
\/\/ Zurücksetzen des Timers zur Schließung der Verbindung
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
\/\/ Dies ist eine Anfrage zur erneuten Übertragung – das erforderliche Paket wird gesendet
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Das erneut gesendete Paket (Paket#3 in der Grafik) wird von der eingehenden Verbindung angenommen. Es erfolgt eine Überprüfung auf das Füllen des Empfangsfensters, und die normale Datenübertragung wird wiederhergestellt.
Überprüfung auf das Füllen des Empfangsfensters (Zustand Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// Erhöhen des Paketzählers
connectionRecord.PacketCounter++;
// Aktuelle Paketnummer im Fenstersteuerungsarray speichern
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// Höchste empfangene Paketnummer festlegen
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 im Fenster empfangen wurden, setzen wir den Zähler zurück
// und senden ein Bestätigungspaket
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// Zähler zurücksetzen.
connectionRecord.PacketCounter = 0;
// Übertragungsfenster verschieben
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// Übertragungssteuerungsarray zurücksetzen
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Für die Interaktion mit dem Datenübertragungsprotokoll gibt es die öffentliche Klasse Reliable Udp, die eine Wrapper-Struktur über dem Übertragungssteuerblock darstellt. 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 eingehender Pakete an der angegebenen IP-Adresse
// und dem Port. Ein Wert von 0 für den Port bedeutet, dass
// ein dynamisch zugewiesener Port verwendet wird
public ReliableUdp(IPAddress localAddress, int port = 0)
// Abonnement für eingehende Nachrichten
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// Abbestellen von Nachrichtenempfang
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// Asynchrones Senden einer Nachricht
// Hinweis: Die Kompatibilität mit XP und Server 2003 bleibt bestehen, da .NET Framework 4.0 verwendet wird
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// Beginnt das asynchrone Senden einer Nachricht
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// Erhält das Ergebnis des asynchronen Sendens
public bool EndSendMessage(IAsyncResult asyncResult)
// Ressourcen bereinigen
public void Dispose()
}
Das Empfangen von Nachrichten erfolgt durch ein Abonnement. Die Signatur des Delegaten für die Callback-Methode:
Öffentlicher Delegat void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Nachricht:
Öffentliche Klasse ReliableUdpMessage
{
// Typ der Nachricht, einfache Aufzählung
öffentliche ReliableUdpMessageTypes Typ { get; private set; }
// Daten der Nachricht
öffentliche byte[] Body { get; private set; }
// Wenn auf true gesetzt, wird der Bestätigungsmechanismus deaktiviert
// für die Übertragung einer bestimmten Nachricht
öffentliche bool NoAsk { get; private set; }
}
Um sich für einen bestimmten Nachrichtentyp und/oder einen bestimmten Absender anzumelden, verwenden Sie zwei optionale Parameter: ReliableUdpMessageTypes messageType und IPEndPoint ipEndPoint.
Nachrichtentypen:
Öffentliche Aufzählung ReliableUdpMessageTypes : kurz
{
// Beliebig
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; hierfür ist im Protokoll ein asynchrones Programmiermodell implementiert:
öffentliche IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Das Ergebnis des Sendens einer Nachricht ist true – wenn die Nachricht erfolgreich an den Empfänger zugestellt wurde, und false – wenn die Verbindung aufgrund eines Timeouts geschlossen wurde:
öffentliche bool EndSendMessage(IAsyncResult asyncResult)
Fazit
In diesem Artikel wurde vieles nicht behandelt. Die Mechanismen zur Flusskontrolle, die Fehler- und Ausnahmebehandlung sowie die Implementierung asynchroner Nachrichtenversände müssen für Sie klar sein. Das Kernprotokoll, die Beschreibung der Paketverarbeitung, die Verbindungsherstellung und das Timeout-Management sollten Ihnen jedoch deutlich werden.
Die demonstrierte Version des zuverlässigen Lieferprotokolls ist ausreichend stabil und flexibel und erfüllt die zuvor festgelegten Anforderungen. Ich möchte jedoch hinzufügen, dass die beschriebene Implementierung verbessert werden kann. Zum Beispiel könnten Mechanismen wie das Sliding Window und RTT hinzugefügt werden, um die Durchsatzkapazität zu erhöhen und die Timer-Intervalle dynamisch anzupassen. Zudem wäre die Implementierung eines Mechanismus zur Bestimmung der MTU zwischen den Verbindungsknoten nützlich (aber nur bei der Übertragung großer Nachrichten).
Vielen Dank für Ihre Aufmerksamkeit, ich freue mich auf Ihre Kommentare und Anmerkungen.
P.S. Für diejenigen, die sich für Details interessieren oder einfach nur 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 über das RUDP-Protokoll:
- Zuverlässiges Datenprotokoll: und
- Einfache Implementierung der Zustellbestätigung über UDP:
- Artikel, der Mechanismen zur Überwindung von NATs beschreibt:
- Implementierung eines asynchronen Programmiermodells: und
- Übertragung des asynchronen Programmiermodells auf ein auf Aufgaben basierendes asynchrones Muster (APM in TAP):
Update: Danke und für die Idee, die Aufgabe zum Interface hinzuzufügen. Die Kompatibilität der Bibliothek mit älteren Betriebssystemen wird nicht beeinträchtigt, da das .NET Framework 4 sowohl Windows XP als auch Windows Server 2003 unterstützt.
Quelle: habr.com
