Implementatie van het Reliable Udp-protocol voor .Net

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
Vereisten voor het protocol
Koptekst Betrouwbaar UDP
Algemene principes van het protocol
Time-outs en timers van het protocol
Statusdiagram voor de overdracht van Betrouwbaar UDP
Dieper in de code. Overdrachtscontrole-eenheid
Dieper in de code. Toestanden

Dieper in de code. Verbindingen maken en instellen
Dieper in de code. Verbinding sluiten bij time-out
Dieper in de code. Herstel van datatransmissie
API Betrouwbaar UDP
Conclusie
Nuttige links en artikelen

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

  1. Zorgvuldige levering van pakketten, gerealiseerd via een mechanisme van positieve terugkoppeling (het zogenaamde positive acknowledgment).
  2. De noodzaak voor effectieve overdracht van grote gegevens, met andere woorden, het protocol moet onnodige hertransmissies van pakketten vermijden.
  3. 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).
  4. De mogelijkheid om een commandomodus te implementeren, met bevestiging van elk bericht.
  5. 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 rfc 908 en rfc 1151, 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:Implementatie van het Reliable Udp-protocol voor .Net

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:Implementatie van het Reliable Udp-protocol voor .Net

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 rfc 793, met UDP in rfc 768, 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:Implementatie van het Reliable Udp-protocol voor .Net

De structuur van de Betrouwbaar UDP-header is vrij eenvoudig:

Implementatie van het Reliable Udp-protocol voor .Net

  • 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:Implementatie van het Reliable Udp-protocol voor .Net

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:Implementatie van het Reliable Udp-protocol voor .Net

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:

Implementatie van het Reliable Udp-protocol voor .Net

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:

Implementatie van het Reliable Udp-protocol voor .Net

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:Implementatie van het Reliable Udp-protocol voor .Net

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:Implementatie van het Reliable Udp-protocol voor .Net

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:Implementatie van het Reliable Udp-protocol voor .Net

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:
Project Reliable UDP

Nuttige links en artikelen

  1. Specificatie van het TCP-protocol: in het Engels en in het Nederlands
  2. Specificatie van het UDP-protocol: in het Engels en in het Nederlands
  3. Discussie over het RUDP-protocol: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 en rfc 1151
  5. Eenvoudige implementatie van bevestiging van levering via UDP: Neem totale controle over uw netwerk met .NET en UDP
  6. Artikel dat mechanismen voor het overwinnen van NAT beschrijft: Peer-to-Peer Communicatie Over Netwerkadresvertalers
  7. Implementatie van een asynchrone programmeermodel: Implementatie van het CLR asynchrone programmeermodel en Hoe het IAsyncResult-ontwerppatroon te implementeren
  8. Overbrengen van het asynchrone programmeermodel naar een asynchroon sjabloon, gebaseerd op taken (APM naar TAP):
    TPL en traditionele .NET asynchrone programmering
    Interop met andere asynchrone patronen en typen

Update: Bedankt mayorovp en sidristij 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

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster