Het internet is in de loop der tijd enorm veranderd. Een van de belangrijkste protocollen van het internet, UDP, wordt gebruikt door toepassingen, niet alleen voor het verzenden van datagrammen en broadcast, maar ook voor het mogelijk maken van peer-to-peer verbindingen tussen netwerkknopen. Gezien de eenvoudige structuur van dit protocol zijn er tal van onvoorziene manieren van toepassing ontstaan, maar de nadelen van het protocol, zoals het ontbreken van gegarandeerde levering, zijn nog steeds aanwezig. Dit artikel beschrijft de implementatie van gegarandeerde levering bovenop UDP.
Inhoud:
Inleiding
De oorspronkelijke architectuur van het internet ging uit van een homogeen adresruimte waarin elke knoop een wereldwijd en uniek IP-adres had en rechtstreeks met andere knopen kon communiceren. Tegenwoordig heeft het internet echter een andere architectuur: ƩƩn gebied van wereldwijde IP-adressen en tal van gebieden met particuliere adressen, verborgen achter NAT-apparaten.In een dergelijke architectuur kunnen alleen apparaten die zich in het globale adresruimte bevinden gemakkelijk met anderen in het netwerk communiceren, omdat ze een uniek, globaal routerbaar IP-adres hebben. Een knooppunt in een privƩnetwerk kan verbinding maken met andere knooppunten in hetzelfde netwerk en kan ook verbinding maken met andere, goed bekende knooppunten in de globale adresruimte. Deze interactie wordt in hoge mate bereikt dankzij het mechanisme voor netwerkadresvertaling. NAT-apparaten, zoals Wi-Fi-routers, maken speciale vermeldingen in vertalingstabellen voor uitgaande verbindingen en wijzigen IP-adressen en poortnummers in pakketten. Dit maakt het mogelijk om vanuit een privƩnetwerk een uitgaande verbinding tot stand te brengen met knooppunten in de globale adresruimte. Maar tegelijkertijd blokkeren NAT-apparaten meestal al het binnenkomende verkeer, tenzij er afzonderlijke regels zijn ingesteld voor binnenkomende verbindingen.
Een dergelijke architectuur van het internet is redelijk voor client-serverinteractie, waarbij clients zich in privƩnetwerken kunnen bevinden en servers een globaal adres hebben. Maar het creƫert moeilijkheden voor de directe verbinding tussen twee knooppunten. verschillende privƩnetwerken. Een directe verbinding tussen twee knooppunten is belangrijk voor 'peer-to-peer'-applicaties, zoals spraakoverdracht (Skype), toegang op afstand tot een computer (TeamViewer), of online gaming.
Een van de meest effectieve methoden voor het tot stand brengen van een peer-to-peer verbinding tussen apparaten in verschillende privƩnetwerken wordt 'hole punching' genoemd. Deze techniek wordt meestal gebruikt met applicaties op basis van het UDP-protocol.
Maar als uw applicatie een gegarandeerde levering van gegevens vereist, bijvoorbeeld bij het overdragen van bestanden tussen computers, dan zullen er veel moeilijkheden optreden bij het gebruik van UDP, aangezien UDP geen gegarandeerd leveringsprotocol is en geen levering van pakketten in volgorde waarborgt, in tegenstelling tot het TCP-protocol.
In dat geval is het nodig om een applicatielaagprotocol te implementeren dat de vereiste functionaliteit biedt en bovenop UDP werkt.
Ik wil meteen opmerken dat er een techniek is genaamd TCP hole punching, voor het tot stand brengen van TCP-verbindingen tussen knooppunten in verschillende particuliere netwerken, maar vanwege het gebrek aan ondersteuning door veel NAT-apparaten wordt het meestal niet als de primaire manier om dergelijke knooppunten te verbinden beschouwd.
In dit artikel zal ik alleen de implementatie van het protocol voor gegarandeerde levering bespreken. De implementatie van de techniek van UDP hole punching wordt in volgende artikelen beschreven.
Vereisten voor het protocol
- Zorgvuldige levering van pakketten, gerealiseerd via een mechanisme van positieve terugkoppeling (het zogenaamde positive acknowledgment).
- De noodzaak voor effectieve overdracht van grote gegevens, met andere woorden, het protocol moet onnodige hertransmissies van pakketten vermijden.
- Er moet de mogelijkheid zijn om het mechanisme van bevestiging van levering ongedaan te maken (de mogelijkheid om te functioneren als een 'pure' UDP-protocol).
- De mogelijkheid om een commandomodus te implementeren, met bevestiging van elk bericht.
- De basiseenheid voor gegevensoverdracht via het protocol moet een bericht zijn.
Deze vereisten komen in veel opzichten overeen met de vereisten voor het Reliable Data Protocol, zoals beschreven in en , en ik heb me bij de ontwikkeling van dit protocol op deze standaarden gebaseerd.
Om deze vereisten te begrijpen, laten we de tijdsdiagrammen voor gegevensoverdracht tussen twee knooppunten in het netwerk door TCP- en UDP-protocollen bekijken. Laten we in beide gevallen aannemen dat er ƩƩn pakket verloren gaat.
Overdracht van niet-interactieve gegevens via TCP:
Zoals te zien is in het diagram, zal TCP in het geval van pakketverlies het verloren pakket detecteren en dit aan de verzender melden, waarbij het nummer van het verloren segment wordt opgevraagd.
Gegevensoverdracht via het UDP-protocol:
UDP neemt geen stappen om verliezen te detecteren. Foutcontrole bij overdracht in het UDP-protocol ligt volledig bij de applicatie.
Foutdetectie in het TCP-protocol wordt bereikt door een verbinding met de eindknoop op te zetten, de status van deze verbinding te behouden, het nummer van verzonden bytes in elke pakketheader aan te geven, en meldingen van ontvangst uit te voeren met behulp van het bevestigingsnummer 'acknowledge number'.
Daarnaast gebruikt het TCP-protocol om de prestaties te verhogen (dat wil zeggen, om meer dan ƩƩn segment te verzenden zonder bevestiging te ontvangen) het zogenaamde verzendvenster ā het aantal bytes aan gegevens dat de verzender van het segment verwacht te ontvangen.
Voor meer informatie over het TCP-protocol kan men kijken naar , met UDP in , waar ze eigenlijk worden gedefinieerd.
Uit het bovenstaande blijkt dat voor het creƫren van een betrouwbaar berichtleveringsprotocol bovenop UDP (hierna te noemen Betrouwbaar UDP), het nodig is om mechanismen voor gegevensoverdracht vergelijkbaar met TCP te implementeren. Namelijk:
- de verbindingstoestand behouden
- segmentnummers gebruiken
- speciale bevestigingspakketten gebruiken
- een vereenvoudigd venstermodus gebruiken om de doorvoersnelheid van het protocol te verhogen
Daarnaast is het nodig om:
- te signaleren dat een bericht begint, om middelen voor de verbinding vrij te maken
- te signaleren dat een bericht eindigt, om het ontvangen bericht naar de bovenliggende toepassing te verzenden en de middelen van het protocol vrij te geven
- het protocol voor specifieke verbindingen de bevestigingsmechanismen uit te schakelen, zodat het functioneert als 'puur' UDP
Koptekst Betrouwbaar UDP
Laten we niet vergeten dat een UDP-datagram in een IP-datagram is ingekapseld. Een Betrouwbaar UDP-pakket wordt dus 'ingepakt' in een UDP-datagram.
Inpakking van de Betrouwbaar UDP-header:
De structuur van de Betrouwbaar UDP-header is vrij eenvoudig:

- Flags ā bestuurlijke vlaggen van het pakket
- MessageType ā type bericht, gebruikt door bovenliggende toepassingen, om zich voor bepaalde berichten in te schrijven
- TransmissionId ā transmissienummer, samen met het adres en poort van de ontvanger, bepaalt uniek de verbinding
- PacketNumber ā pakketnummer
- Options ā extra opties van het protocol. In het geval van het eerste pakket gebruikt om de grootte van het bericht aan te geven
De vlaggen zijn als volgt:
- FirstPacket ā het eerste pakket van het bericht
- NoAsk ā bericht vereist geen bevestigingsmechanisme
- LastPacket ā het laatste pakket van het bericht
- RequestForPacket ā bevestigingspakket of verzoek om een verloren pakket
Algemene principes van het protocol
Aangezien Betrouwbaar UDP gericht is op gegarandeerde berichtoverdracht tussen twee knooppunten, moet het in staat zijn verbinding te maken met de andere kant. Om een verbinding tot stand te brengen, stuurt de zender een pakket met de vlag FirstPacket, waar de reactie op zal betekenen dat de verbinding is tot stand gebracht. Alle antwoordpakketten, of anders gezegd, bevestigingspakketten, verhogen altijd de waarde van het veld PacketNumber met ƩƩn boven het hoogste PacketNumber van de succesvol ontvangen pakketten. In het veld Options voor het eerste verzonden pakket wordt de grootte van het bericht vastgelegd.
Voor het beƫindigen van de verbinding wordt een vergelijkbaar mechanisme gebruikt. In het laatste pakketbericht wordt de LastPacket-vlag ingesteld. In het antwoordpakket wordt het nummer van het laatste pakket + 1 gegeven, wat voor de ontvangende kant betekent dat het bericht succesvol is afgeleverd.
Diagram van het tot stand brengen en beƫindigen van de verbinding:
Wanneer de verbinding tot stand is gebracht, begint de gegevensoverdracht. Gegevens worden verzonden in blokken van pakketten. Elk blok, behalve het laatste, bevat een vast aantal pakketten. Dit aantal is gelijk aan de grootte van het ontvangst-/verzendvenster. Het laatste gegevensblok kan minder pakketten bevatten. Na het verzenden van elk blok wacht de verzendende kant op een bevestiging van levering of een verzoek om herverzending van verloren pakketten, terwijl het ontvangst-/verzendvenster open blijft voor het ontvangen van antwoorden. Na ontvangst van de bevestiging van de levering van het blok, wordt het ontvangst-/verzendvenster verschoven en wordt het volgende gegevensblok verzonden.
De ontvangende kant ontvangt de pakketten. Elk pakket wordt gecontroleerd op binnenkomst in het verzendvenster. Pakketten die niet in het venster passen en duplicaten worden gefilterd. Aangezien de grootte van het venster strikt vastligt en gelijk is voor zowel de ontvanger als de zender, wordt, in het geval dat een blok pakketten zonder verliezen wordt afgeleverd, het venster verschoven om pakketten van de volgende gegevensblok te ontvangen en wordt een bevestiging van levering verzonden. Als het venster niet is gevuld binnen de periode die door de werkbare timer is ingesteld, wordt er een controle uitgevoerd op welke pakketten niet zijn afgeleverd en worden er verzoeken voor herverzending verzonden.
Diagram voor herverzending:
Time-outs en timers van het protocol
Er zijn verschillende redenen waarom een verbinding niet tot stand kan worden gebracht. Bijvoorbeeld als de ontvangende kant offline is. In dat geval zal de verbinding bij een poging om de verbinding tot stand te brengen worden gesloten vanwege een time-out. In de implementatie van Reliable UDP worden twee timers gebruikt voor het instellen van time-outs. De eerste, de werkbare timer, is bedoeld om te wachten op een antwoord van de externe host. Als deze op de zenderzijde afgaat, wordt het laatst verzonden pakket opnieuw verzonden. Als de timer aan de ontvangerzijde afgaat, wordt er gecontroleerd op verloren pakketten en worden verzoeken voor herverzending verzonden.
De tweede timer is nodig om de verbinding te sluiten als er geen communicatie is tussen de knooppunten. Aan de kant van de verzender wordt deze gestart onmiddellijk na het afgaan van de werk-timer en wacht op een antwoord van het externe knooppunt. Als er binnen de vastgestelde periode geen antwoord komt, wordt de verbinding beƫindigd en worden de middelen vrijgegeven. Aan de kant van de ontvanger wordt de sluit-timer gestart na twee keer afgaan van de werk-timer. Dit is nodig om verlies van de bevestigingspakket te voorkomen. Bij het afgaan van de timer wordt de verbinding ook beƫindigd en worden de middelen vrijgegeven.
Statusdiagram voor de overdracht van Betrouwbaar UDP
De werkprincipes van het protocol zijn geĆÆmplementeerd in een eindautomaat, waarbij elke toestand verantwoordelijk is voor specifieke logica in de verwerking van pakketten.
Statusdiagram van Reliable UDP:

Gesloten is in feite geen toestand, het is een start- en eindpunt voor de automaat. De toestand Gesloten wordt aangenomen door de controle-eenheid van de overdracht, die, door een asynchrone UDP-server te implementeren, pakketten doorstuurt naar de juiste verbindingen en de verwerking van toestanden start.
EerstePakketVerzenden is de initiƫle toestand waarin de uitgaande verbinding zich bevindt bij het verzenden van een bericht.
In deze toestand wordt het eerste pakket verzonden voor reguliere berichten. Voor berichten zonder verzendbevestiging is dit de enige toestand - hierin wordt het gehele bericht verzonden.
VerzendCyclus is de hoofdtoestand voor het verzenden van berichtenpakketten.
De overgang naar deze toestand vanuit de toestand EerstePakketVerzenden gebeurt na het verzenden van het eerste pakket van het bericht. Alle bevestigingen en verzoeken voor herverzending komen in deze toestand binnen. Verlaat uit deze toestand kan in twee gevallen plaatsvinden - bij succesvolle aflevering van het bericht of na een time-out.
EerstePakketOntvangen is de initiƫle toestand voor de ontvanger van het bericht.
In deze toestand wordt de juistheid van het begin van de overdracht gecontroleerd, worden de nodige structuren aangemaakt en wordt er een bevestiging van ontvangst van het eerste pakket verzonden.
Voor een bericht dat bestaat uit een enkel pakket en zonder gebruik van afleverbevestiging is dit de enige toestand. Na verwerking van zo'n bericht wordt de verbinding gesloten.
Samenvoegen is de hoofdtoestand voor het ontvangen van berichtenpakketten.
Het slaat pakketten op in tijdelijke opslag, controleert op het ontbreken van verloren pakketten, verzendt bevestigingen van de levering van blokken pakketten en berichten als geheel, en verzendt verzoeken voor herlevering van verloren pakketten. In het geval dat het gehele bericht succesvol is ontvangen, gaat de verbinding naar de status Completed, anders wordt er een time-out uitgevoerd.
Completed ā de verbinding wordt gesloten in het geval van succesvolle ontvangst van het gehele bericht.
Deze status is noodzakelijk voor het samenstellen van het bericht en in gevallen waarin de bevestiging van de levering van het bericht onderweg naar de verzender verloren is gegaan. Het verlaten van deze status gebeurt via een time-out, maar de verbinding wordt als succesvol gesloten beschouwd.
Dieper in de code. Overdrachtscontrole-eenheid
Een van de belangrijkste elementen van Reliable UDP is het beheersblok voor de overdracht. De taak van dit blok is het opslaan van de huidige verbindingen en bijbehorende elementen, het verdelen van binnenkomende pakketten over de juiste verbindingen, het bieden van een interface voor het verzenden van pakketten naar de verbinding en de implementatie van de API van het protocol. Het beheersblok voor de overdracht ontvangt pakketten van het UDP-niveau en leidt ze door naar de eindautomaat voor verwerking. Voor het ontvangen van pakketten is er een asynchrone UDP-server geĆÆmplementeerd.
Enkele leden van de klasse ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// byte-array voor de opgegeven sleutel. Wordt gebruikt voor het samenstellen van binnenkomende berichten
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// byte-array voor de opgegeven sleutel. Wordt gebruikt voor het verzenden van uitgaande berichten.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// connectierecord voor de opgegeven sleutel.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// lijst van abonnees op berichten.
private readonly List m_subscribers;
// lokale socket
private Socket m_socketIn;
// poort voor binnenkomende berichten
private int m_port;
// lokaal IP-adres
private IPAddress m_ipAddress;
// lokale eindpunt
public IPEndPoint LocalEndpoint { get; private set; }
// collectie vooraf geĆÆnitialiseerde
// toestanden van de eindautomaat
public StatesCollection States { get; private set; }
// randomnummergenerator. Wordt gebruikt voor het creƫren van TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementatie van een asynchrone UDP-server:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// creƫer een nieuwe buffer voor elke socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// geef de buffer door als parameter voor de 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);
// pakket ontvangen, klaar om de volgende te ontvangen
Receive();
// omdat de eenvoudigste oplossing voor de buffer is om een referentie naar deze te krijgen
// uit IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// ontvang de header van het pakket
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// onjuist pakket ontvangen - negeren
return;
}
// construeren van een sleutel voor het bepalen van het connection record voor het pakket
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// verkrijg het bestaande connection record of creƫer een nieuwe
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// stuur het pakket naar verwerking in de eindautomaat
record.State.ReceivePacket(record, header, bytes);
}
Voor elke boodschapoverdracht wordt een structuur gemaakt die informatie over de verbinding bevat. Zo'n structuur wordt genoemd connection record.
Sommige leden van de klasse ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// array of bytes with the message
public byte[] IncomingStream { get; set; }
// reference to the state of the finite automaton
public ReliableUdpState State { get; set; }
// pair that uniquely identifies the connection record
// in the transmission control block
public Tuple Key { get; private set;}
// lower bound of the receive window
public int WindowLowerBound;
// size of the transmission window
public readonly int WindowSize;
// packet number to send
public int SndNext;
// number of packets to send
public int NumberOfPackets;
// transmission number (this is the second part of the Tuple)
// each message has its own
public readonly Int32 TransmissionId;
// remote IP endpoint ā the actual recipient of the message
public readonly IPEndPoint RemoteClient;
// packet size to avoid fragmentation at the IP level
// must not exceed MTU ā (IP.Header + UDP.Header + ReliableUDP.Header)
public readonly int BufferSize;
// transmission control block
public readonly ReliableUdpConnectionControlBlock Tcb;
// encapsulates the results of the asynchronous operation for BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// do not send acknowledgment packets
public bool IsNoAnswerNeeded;
// the last correctly received packet (always set to the highest number)
public int RcvCurrent;
// array with the numbers of lost packets
public int[] LostPackets { get; private set; }
// whether the last packet has arrived. Used as a bool.
public int IsLastPacketReceived = 0;
//...
}
Dieper in de code. Toestanden
De toestanden implementeren de eindige automaat van het Reliable UDP-protocol, waarin de belangrijkste verwerking van pakketten plaatsvindt. De abstracte klasse ReliableUdpState biedt een interface voor de toestand:

De hele logica van het protocol wordt gerealiseerd door de bovenstaande klassen, samen met een hulpklas die statische methoden biedt, zoals bijvoorbeeld het opbouwen van de ReliableUdp-header uit de connection record.
Verdere details van de implementatie van de interface-methoden, die de belangrijkste algoritmen van het protocol definiƫren, worden hierna besproken.
Methode DisposeByTimeout
De Methode DisposeByTimeout is verantwoordelijk voor het vrijgeven van de middelen van de verbinding na afloop van de time-out en voor het signaleren van succesvolle/niet-succesvolle aflevering van het bericht.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Het is alleen overschreven in de toestand Completed.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// geeft aan dat het bericht succesvol is ontvangen
SetAsCompleted(connectionRecord);
}
Methode ProcessPackets
De methode ProcessPackets is verantwoordelijk voor de extra verwerking van ƩƩn of meerdere pakketten. Deze wordt rechtstreeks of via een pakketwachttimer aangeroepen.
In staat Samenvoegen de methode is overschreven en is verantwoordelijk voor het controleren van verloren pakketten en het overgaan naar de status Completed, in het geval van ontvangst van het laatste pakket en het succesvol doorlopen van de controle
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// er zijn verloren pakketten, stuur verzoeken voor deze
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// stel de timer voor de tweede keer in voor een hertransmissiepoging
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// als na twee pogingen met WaitForPacketTimer
// de pakketten niet zijn ontvangen - start de timer voor het sluiten van de verbinding
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// succesvolle controle
{
// bevestiging van ontvangst van het gegevensblok verzenden
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// in plaats van directe implementatie van bronnen
// starten we een timer, voor het geval dat
// als de laatste ack niet bij de verzender aankomt en deze opnieuw aanvraagt.
// bij het afgaan van de timer - implementeren we de bronnen
// de methode van de timer is overschreven in de staat Completed
StartCloseWaitTimer(connectionRecord);
}
// dit is het geval waarin de ack voor het pakketblok verloren is gegaan
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// start de timer voor het sluiten van de verbinding
StartCloseWaitTimer(connectionRecord);
}
}
In staat VerzendCyclus deze methode wordt alleen via de timer aangeroepen en is verantwoordelijk voor het opnieuw verzenden van het laatste bericht, evenals voor het inschakelen van de sluitingtimer.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// stuur opnieuw het laatste pakket
// (in het geval van herstel van de verbinding zal de ontvangende knooppunt opnieuw verzoeken verzenden die niet zijn aangekomen)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// start de CloseWait-timer ā om te wachten op herverbinding of beĆ«indiging ervan
StartCloseWaitTimer(connectionRecord);
}
In staat Completed de methode stopt de werktimer en stuurt een bericht naar de abonnees.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// verzamel het bericht en stuur het naar de abonnees
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Methode ReceivePacket
In staat EerstePakketOntvangen de belangrijkste taak van de methode is om vast te stellen of het eerste pakket van het bericht daadwerkelijk op de interface is aangekomen, en om het bericht te verzamelen dat uit een enkel pakket bestaat.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// negeer het pakket
return;
// de combinatie van twee vlaggen - FirstPacket en LastPacket - geeft aan dat we een enkel bericht hebben
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))
{
// verzend bevestigingspakket
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// bij ontwerp beginnen alle packetnummers met 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// bereken het aantal pakketten dat moet komen
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// registreer het nummer van het laatst ontvangen pakket (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// schuif het ontvangstvenster met 1 op
connectionRecord.WindowLowerBound++;
// schakelen naar de staat
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// als bevestigingsmechanisme niet vereist is
// start een timer die alle structuren vrijgeeft
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
else
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
In staat VerzendCyclus Deze methode is overschreven om bevestigingen van levering en verzoeken voor herverzending te ontvangen.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// berekening van de uiteindelijke grens van het venster
// de grens van het venster + 1, om bevestigingen van levering te krijgen
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// controleer of het binnen het venster valt
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// controleren op het laatste pakket:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// overdracht voltooid
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// dit is een antwoord op het eerste pakket met bevestiging
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// zonder vensterverplaatsing
SendPacket(connectionRecord);
}
// bevestiging ontvangen van blok gegevens
else if (header.PacketNumber == windowHighestBound)
{
// verschuiven van het ontvangst/overdracht venster
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// reset de transmissiecontrolearray
connectionRecord.WindowControlArray.Nullify();
// verzend het blok van pakketten
SendPacket(connectionRecord);
}
// dit is een verzoek om hertransmissie ā verzend het vereiste pakket
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
In staat Samenvoegen In de methode ReceivePacket vindt het belangrijkste werk plaats voor het samenstellen van een bericht uit binnenkomende pakketten.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// verwerking van pakketten zonder bevestigingsmechanisme
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// reset de timer
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// schrijf gegevens
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// als we een pakket met de laatste vlag hebben gekregen - beƫindigen
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// berekening van de uiteindelijke vensterrand
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// negeren van pakketten die niet in het venster passen
if (header.PacketNumber (windowHighestBound))
return;
// negeren van duplicaten
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// schrijf gegevens
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// verhoog de pakketten teller
connectionRecord.PacketCounter++;
// schrijf het huidige pakketnummer in het venstervijzelingsarray
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// stel het hoogste ontvangen pakket in
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// herstart timers
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// als het laatste pakket is aangekomen
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// als we alle pakketten in het venster hebben ontvangen, reset dan de teller
// en stuur een bevestigingspakket
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// reset de teller.
connectionRecord.PacketCounter = 0;
// verschoof het verzendvenster
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// reset het transmissiecontrolearray
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// als het laatste pakket al is ontvangen
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// controleer de pakketten
ProcessPackets(connectionRecord);
}
}
In staat Completed De enige taak van de methode is om een herbevestiging van de succesvolle levering van het bericht te versturen.
Voltooid.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// herhaling van het laatst verzonden pakket vanwege
// het feit dat de laatste ack de verzender niet heeft bereikt
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Methode SendPacket
In staat EerstePakketVerzenden deze methode verzendt het eerste datapakket of, als bevestiging van levering niet vereist is, het hele bericht.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// als bevestiging niet nodig is - verzenden we alle pakketten
// en vrijgeven van middelen
if (connectionRecord.IsNoAnswerNeeded)
{
// Hier vindt de verzending As Is plaats
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// pakketheader creƫren en verzenden
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// teller verhogen
connectionRecord.SndNext++;
// venster verschuiven
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Timer starten
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
In staat VerzendCyclus in deze methode vindt de verzending van een pakketblok plaats.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// een pakketblok verzenden
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++;
}
// in het geval van een groot verzendvenster, timer opnieuw starten na verzending
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Dieper in de code. Verbindingen maken en instellen
Nu we bekend zijn met de belangrijkste staten en methoden die worden gebruikt voor het verwerken van statussen, kunnen we enkele voorbeelden van protocollogica iets nader bekijken.
Gegevensoverdrachtsdiagram onder normale omstandigheden:
Laten we de creatie nader bekijken connection record voor het verbinden en het verzenden van het eerste pakket. De initiator van de overdracht is altijd de applicatie die de API-methode voor het verzenden van een bericht aanroept. Vervolgens wordt de StartTransmission-methode van de transmissiecontrolestructuur ingeschakeld, die de gegevensoverdracht voor het nieuwe bericht start.
Het opzetten van een uitgaand verbinding:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "U moet een constructor met parameters gebruiken of de listener starten voordat u een bericht verzendt" );
}
// start de verwerking van binnenkomende pakketten
StartListener(LocalEndpoint);
}
// creƫer een sleutel voor de woordenlijst, gebaseerd op EndPoint en ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// genereer een willekeurig nummer voor transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// creƫer een nieuwe vermelding voor de verbinding en controleer,
// of dit nummer al bestaat in onze woordenlijsten
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// als het bestaat - genereer dan opnieuw een willekeurig nummer
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// als het opnieuw niet gelukt is - gooi een uitzondering
throw new ArgumentException("De combinatie TransmissionId & EndPoint bestaat al in de woordenlijst");
}
// we hebben de staat in verwerking gestart
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Het verzenden van het eerste pakket (status FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// creƫer de pakketheader en verzend deze
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// verhoog de teller
connectionRecord.SndNext++;
// verschuif het venster
connectionRecord.WindowLowerBound++;
// ga naar de status SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Start de timer
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Na het verzenden van het eerste pakket gaat de afzender over naar de staat VerzendCyclus ā wachten op bevestiging van de ontvangst van het pakket.
De ontvangende zijde accepteert het verzonden pakket met de methode EndReceive, maakt een nieuwe connection record en geeft dit pakket, met de vooraf geparsed header, door aan de methode ReceivePacket in de status. EerstePakketOntvangen
Verbinden aan de ontvangende kant:
private void EndReceive(IAsyncResult ar)
{
// ...
// pakket ontvangen
// de pakketheader parseren
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// onjuiste pakket ontvangen - dit negeren
return;
}
// constructie van de sleutel voor het bepalen van het verbindingrecord voor het pakket
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// bestaande verbindingrecord ophalen of nieuwe aanmaken
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// pakket in de eindautomaat verwerken
record.State.ReceivePacket(record, header, bytes);
}
Ontvangen van het eerste pakket en bevestiging verzenden (status FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// pakket negeren
return;
// ...
// bij ontwerp beginnen alle pakketnummers met 0;
if (header.PacketNumber != 0)
return;
// initialiseer array voor opslaan van berichtenfragmenten
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// schrijf pakketdata naar de array
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// aantal pakketten die moeten komen berekenen
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length/(double)connectionRecord.BufferSize));
// schrijf nummer van het laatst ontvangen pakket (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// schuif het ontvangstvenster met 1 op
connectionRecord.WindowLowerBound++;
// schakel status over
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*als bevestigingsmechanisme niet vereist is*/)
// ...
else
{
// bevestiging verzenden
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Dieper in de code. Verbinding sluiten bij time-out
Het afhandelen van time-outs is een belangrijk onderdeel van Reliable UDP. Laten we een voorbeeld bekijken waarin er een storing op een tussenknooppunt is opgetreden en de gegevenslevering in beide richtingen onmogelijk is geworden.
Diagram van het sluiten van de verbinding door time-out:
Zoals uit het diagram blijkt, wordt de werktimer van de verzender geactiveerd direct na het verzenden van een pakketblok. Dit gebeurt in de methode SendPacket van de status VerzendCyclus.
Activatie van de werktimer (status SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// verzenden van pakketblok
// ...
// timer opnieuw starten na verzenden
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
De timerperioden worden ingesteld bij het maken van een verbinding. Standaard is ShortTimerPeriod 5 seconden. In dit voorbeeld is het ingesteld op 1,5 seconde.
Bij een binnenkomende verbinding start de timer nadat het laatste ontvangen datapakket is aangekomen, dit gebeurt in de ReceivePacket-methode van de status. Samenvoegen
Inschakelen van de werkende timer (status Assemblage):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// reset de timers
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
Bij de binnenkomende verbinding zijn er tijdens de wachtperiode van de werkende timer geen extra pakketten ontvangen. De timer is geactiveerd en heeft de ProcessPackets-methode aangeroepen, waarin verloren pakketten zijn gedetecteerd en voor het eerst verzoeken om herlevering zijn verzonden.
Verzenden van verzoeken om herlevering (status Assemblage):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*controle op verloren pakketten */)
{
// verzenden verzoeken om herlevering
// stel de timer opnieuw in voor een tweede pogingen tot verzending
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// als na twee pogingen van WaitForPacketTimer
// de pakketten niet zijn ontvangen - start de timer voor het sluiten van de verbinding
StartCloseWaitTimer(connectionRecord);
}
else if (/*de laatste pakket is aangekomen en succesvol gecontroleerd */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// als ack voor het pakket blok is verloren
else
{
if (!connectionRecord.TimerSecondTry)
{
// stuur de ack opnieuw
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// start de timer voor het sluiten van de verbinding
StartCloseWaitTimer(connectionRecord);
}
}
De variabele TimerSecondTry is ingesteld op true. Deze variabele is verantwoordelijk voor het opnieuw starten van de werkende timer.
Aan de verzendzijde wordt ook de werkende timer geactiveerd en wordt het laatst verzonden pakket opnieuw verzonden.
Inschakelen van de timer voor het sluiten van de verbinding (status SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// verzend het laatst verzonden pakket opnieuw
// ...
// schakel de CloseWait-timer in ā voor het wachten op herstel of beĆ«indiging van de verbinding
StartCloseWaitTimer(connectionRecord);
}
Na de initiƫle verbinding wordt de timer voor het afsluiten van de verbinding gestart.
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);
}
De standaard wachttijd voor de afsluit-timer van de verbinding is 30 seconden.
Na korte tijd wordt de werkende timer aan de ontvangende kant opnieuw geactiveerd, worden er opnieuw verzoeken verzonden, waarna de afsluit-timer van de binnenkomende verbinding wordt gestart.
Bij het afgaan van de afsluit-timers worden alle middelen van beide connection records vrijgegeven. De verzender meldt de mislukte aflevering aan de bovenliggende applicatie (zie API Reliable UDP).
Het vrijgeven van middelen van de connection record:
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);
}
}
Dieper in de code. Herstel van datatransmissie
Diagram van het herstel van gegevensoverdracht bij pakketverlies:
Zoals al besproken bij het afsluiten van de verbinding door tijdsoverschrijding, zal bij het verstrijken van de werkende timer aan de ontvangende kant een controle op verloren pakketten plaatsvinden. In het geval van pakketverlies wordt een lijst samengesteld met de pakketnummers die de ontvanger niet heeft bereikt. Deze nummers worden opgenomen in de array LostPackets van de specifieke verbinding en er wordt een verzoek tot herlevering verzonden.
Verzoek om herlevering van pakketten verzenden (toestand Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
//...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// verloren pakketten, verzoeken verzenden
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ...
}
}
De verzender ontvangt het verzoek om herlevering en stuurt de ontbrekende pakketten opnieuw. Het is vermeldenswaard dat op dit moment de timer voor het afsluiten van de verbinding aan de verzender al is gestart en, bij ontvangst van het verzoek, deze wordt gereset.
Herzending van verloren pakketten (toestand SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
\/\/ timer voor het sluiten van de verbinding resetten
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
\/\/ dit is een verzoek om heroverdracht ā verstuur het vereiste pakket
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Het opnieuw verzonden pakket (packet#3 in het diagram) wordt ontvangen door de inkomende verbinding. Er wordt gecontroleerd op invulling van de ontvangstvenster en de normale gegevensoverdracht wordt hersteld.
Controle op de ontvangst in het venster (status Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
\/\/ verhoog de pakket teller
connectionRecord.PacketCounter++;
\/\/ schrijf het huidige pakketnummer in de venster beheersarray
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
\/\/ stel het hoogste ontvangen pakket in
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
\/\/ reset de timers
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
\/\/ als we alle pakketten van het venster hebben ontvangen, reset de teller
\/\/ en verstuur het bevestigingspakket
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
\/\/ reset de teller.
connectionRecord.PacketCounter = 0;
\/\/ verplaatsen van het verzendvenster
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
\/\/ reset de beheersarray van verzending
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
\/\/ ...
}
API Betrouwbaar UDP
Voor interactie met het dataverzendingprotocol is er een openbare klasse Reliable Udp, die een wrapper is rond de verzendbeheerlaag. Dit zijn de belangrijkste leden van de klasse:
public sealed class ReliableUdp : IDisposable
{
// ontvangt het lokale eindpunt
public IPEndPoint LocalEndpoint
// creƫert een exemplaar van ReliableUdp en start
// het luisteren naar binnenkomende pakketten op het opgegeven IP-adres
// en poort. Waarde 0 voor de poort betekent het gebruik van
// dynamisch toegewezen poort
public ReliableUdp(IPAddress localAddress, int port = 0)
// abonnement op het ontvangen van binnenkomende berichten
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// afmelden voor het ontvangen van berichten
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// asynchroon een bericht verzenden
// Opmerking: de compatibiliteit met XP en Server 2003 blijft behouden, omdat .NET Framework 4.0 wordt gebruikt
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// start asynchrone verzending van een bericht
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// verkrijg het resultaat van de asynchrone verzending
public bool EndSendMessage(IAsyncResult asyncResult)
// vrijgeven van resources
public void Dispose()
}
Het ontvangen van een bericht is gebaseerd op een abonnement. De handtekening van de delegate voor de callbackmethode is:
public delegate void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Bericht:
public class ReliableUdpMessage
{
// type van het bericht, eenvoudige enumeratie
public ReliableUdpMessageTypes Type { get; private set; }
// gegevens van het bericht
public byte[] Body { get; private set; }
// als ingesteld op true - de bevestigingsmechanisme wordt uitgeschakeld
// voor het verzenden van een specifiek bericht
public bool NoAsk { get; private set; }
}
Voor het abonneren op een specifiek type berichten en/of een specifieke afzender worden twee optionele parameters gebruikt: ReliableUdpMessageTypes messageType en IPEndPoint ipEndPoint.
Typen berichten:
public enum ReliableUdpMessageTypes : short
{
// Ieder
Any = 0,
// Verzoek aan de STUN-server
StunRequest = 1,
// Antwoord van de STUN-server
StunResponse = 2,
// Bestanden overdragen
FileTransfer = 3,
// ...
}
Het verzenden van berichten gebeurt asynchroon, daarvoor is er in het protocol een asynchrone programmeermodel geĆÆmplementeerd:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Het resultaat van het verzenden van een bericht zal true zijn ā als het bericht succesvol bij de ontvanger is aangekomen en false ā als de verbinding door een time-out is gesloten:
public bool EndSendMessage(IAsyncResult asyncResult)
Conclusie
Veel is niet beschreven in dit artikel. Mechanismen voor het afstemmen van datastromen, het verwerken van uitzonderingen en fouten, en de implementatie van asynchrone methoden voor het verzenden van berichten. Maar de kern van het protocol, de beschrijving van de logica voor het verwerken van pakketten, het opzetten van verbindingen en het afhandelen van time-outs, moet duidelijk zijn voor u.
De gedemonstreerde versie van het protocol voor betrouwbare levering is behoorlijk robuust en flexibel, en voldoet aan eerder gestelde eisen. Maar ik wil toevoegen dat de beschreven implementatie kan worden verbeterd. Bijvoorbeeld, om de doorvoersnelheid te verhogen en dynamische wijzigingen van timerintervallen mogelijk te maken, kunnen mechanismen zoals sliding window en RTT aan het protocol worden toegevoegd; ook zou het nuttig zijn om een mechanisme voor het bepalen van MTU tussen verbindingsknooppunten te implementeren (maar alleen bij het verzenden van grote berichten).
Bedankt voor uw aandacht, ik kijk uit naar uw opmerkingen en suggesties.
P.S. Voor degenen die geĆÆnteresseerd zijn in details of gewoon het protocol willen testen, hier is de link naar het project op GitHub:
Nuttige links en artikelen
- Specificatie van het TCP-protocol: en
- Specificatie van het UDP-protocol: en
- Discussie over het RUDP-protocol:
- Reliable Data Protocol: en
- Eenvoudige implementatie van bevestiging van levering via UDP:
- Artikel dat mechanismen voor het overwinnen van NAT beschrijft:
- Implementatie van een asynchrone programmeermodel: en
- Overbrengen van het asynchrone programmeermodel naar een asynchroon sjabloon, gebaseerd op taken (APM naar TAP):
Update: Bedankt en voor het idee om een taak aan de interface toe te voegen. De compatibiliteit van de bibliotheek met oudere besturingssystemen blijft gewaarborgd, omdat het 4e framework zowel XP als 2003 server ondersteunt.
Bron: habr.com
