Implementazione del protocollo Reliable Udp per .Net

Internet è cambiato da tempo. Uno dei protocolli principali di Internet, UDP, viene utilizzato dalle applicazioni non solo per la consegna di datagrammi e per le trasmissioni broadcast, ma anche per garantire connessioni peer-to-peer tra i nodi della rete. Data la sua semplice struttura, questo protocollo ha trovato numerosi modi di utilizzo non previsti in precedenza; tuttavia, i difetti del protocollo, come la mancanza di una consegna garantita, non sono scomparsi. Questo articolo descrive l'implementazione di un protocollo di consegna garantita sopra UDP.
Contenuto:Introduzione
Requisiti del protocollo
Intestazione Reliable UDP
Principi generali di funzionamento del protocollo
Timeout e timer del protocollo
Diagramma degli stati della trasmissione Reliable UDP
Approfondimento sul codice. Blocco di controllo della trasmissione
Approfondimento sul codice. Stati

Approfondimento sul codice. Creazione e stabilizzazione delle connessioni
Approfondimento sul codice. Chiusura della connessione per timeout
Approfondimento sul codice. Ripristino della trasmissione dei dati
API Reliable UDP
Conclusione
Link e articoli utili

Introduzione

L'architettura iniziale di Internet prevedeva uno spazio di indirizzamento omogeneo, in cui ogni nodo possedeva un indirizzo IP globale e unico, e poteva comunicare direttamente con altri nodi. Oggi, tuttavia, Internet ha di fatto un'architettura diversa: un'area di indirizzi IP globali e molte aree con indirizzi privati, nascoste dietro dispositivi NAT.In un'architettura del genere, solo i dispositivi che si trovano nello spazio degli indirizzi globale possono facilmente interagire con chiunque nella rete, poiché dispongono di un indirizzo IP unico e instradabile a livello globale. Un nodo all'interno di una rete privata può connettersi ad altri nodi nella stessa rete, così come a nodi ben noti nello spazio degli indirizzi globale. Tale interazione è raggiunta in gran parte grazie al meccanismo di traduzione degli indirizzi di rete. I dispositivi NAT, come i router Wi-Fi, creano registrazioni speciali nelle tabelle di traduzione per le connessioni in uscita e modificano gli indirizzi IP e i numeri di porta nei pacchetti. Questo consente di stabilire una connessione in uscita da una rete privata verso nodi nello spazio degli indirizzi globale. Tuttavia, allo stesso tempo, i dispositivi NAT di solito bloccano tutto il traffico in ingresso, a meno che non siano impostate regole specifiche per le connessioni in ingresso.

Questa architettura di Internet è abbastanza corretta per l'interazione client-server, quando i client possono trovarsi in reti private e i server hanno un indirizzo globale. Tuttavia, crea difficoltà per la connessione diretta di due nodi tra diverse reti private. La connessione diretta di due nodi è importante per le applicazioni "peer-to-peer", come la trasmissione vocale (Skype), l'accesso remoto al computer (TeamViewer) o i giochi online.

Uno dei metodi più efficaci per stabilire una connessione peer-to-peer tra dispositivi presenti in diverse reti private si chiama "hole punching". Questa tecnica è utilizzata più comunemente con applicazioni basate sul protocollo UDP.

Tuttavia, se la tua applicazione richiede una consegna garantita dei dati, ad esempio se stai trasferendo file tra computer, l'uso dell'UDP comporterà molte difficoltà, poiché l'UDP non è un protocollo di consegna garantita e non garantisce l'ordine di consegna dei pacchetti, a differenza del protocollo TCP.

In tal caso, per garantire la consegna garantita dei pacchetti, è necessario implementare un protocollo di livello applicativo che fornisca la funzionalità necessaria e operi sopra l'UDP.

Voglio subito notare che esiste una tecnica di TCP hole punching per stabilire connessioni TCP tra nodi in diverse reti private, ma a causa della mancanza di supporto da parte di molti dispositivi NAT, di solito non viene considerata come il metodo principale per connettere tali nodi.

In questo articolo, considera solo l'implementazione del protocollo di consegna garantita. L'implementazione della tecnica UDP hole punching sarà descritta negli articoli successivi.

Requisiti del protocollo

  1. Consegna affidabile dei pacchetti realizzata attraverso un meccanismo di riconoscimento positivo (cosiddetto positive acknowledgment)
  2. Necessità di una trasmissione efficace di grandi quantità di dati, cioè il protocollo deve evitare trasmissioni superflue di pacchetti
  3. Deve esserci la possibilità di annullare il meccanismo di conferma di consegna (possibilità di funzionare come un protocollo UDP "pulito")
  4. Possibilità di implementare una modalità di comando, con conferma di ogni messaggio
  5. L'unità di base per la trasmissione dei dati attraverso il protocollo deve essere un messaggio

Questi requisiti coincidono in gran parte con i requisiti per il Reliable Data Protocol, descritti in rfc 908 e rfc 1151, e mi sono basato su questi standard nello sviluppo di questo protocollo.

Per comprendere questi requisiti, diamo un'occhiata agli diagrammi temporali di trasmissione dei dati tra due nodi di rete attraverso i protocolli TCP e UDP. Supponiamo che in entrambi i casi si perda un pacchetto.
Trasmissione di dati non interattivi tramite TCP:Implementazione del protocollo Reliable Udp per .Net

Come mostrato nel diagramma, nel caso di perdita di pacchetti, TCP rileverà il pacchetto perso e ne informerà il mittente, richiedendo il numero del segmento perso.
Trasmissione di dati tramite il protocollo UDP:Implementazione del protocollo Reliable Udp per .Net

UDP non compie alcun passo per rilevare le perdite. Il controllo degli errori nella trasmissione nel protocollo UDP è completamente a carico dell'applicazione.

La rilevazione degli errori nel protocollo TCP è raggiunta grazie alla creazione di una connessione con il nodo finale, mantenendo lo stato di questa connessione, indicando il numero di byte inviati in ogni intestazione di pacchetto e notificando le ricezioni tramite il numero di conferma 'acknowledge number'.

Inoltre, per migliorare le prestazioni (cioè inviare più di un segmento senza ricevere conferma), il protocollo TCP utilizza il cosiddetto finestra di trasmissione: il numero di byte di dati che il mittente del segmento si aspetta di ricevere.

Per maggiori dettagli sul protocollo TCP, è possibile consultare rfc 793, con UDP in rfc 768, dove sono, in realtà, definiti.

Da quanto sopra, è chiaro che per creare un protocollo di consegna dei messaggi affidabile sopra UDP (d’ora in poi chiamato Reliable UDP), è necessario implementare meccanismi di trasmissione dati simili a quelli di TCP. In particolare:

  • mantenere lo stato della connessione
  • utilizzare la numerazione dei segmenti
  • utilizzare pacchetti di conferma speciali
  • utilizzare un meccanismo di finestra semplificato per aumentare la capacità del protocollo

Inoltre, è necessario:

  • segnalare l'inizio del messaggio, per allocare risorse per la connessione
  • segnalare la fine del messaggio, per trasmettere il messaggio ricevuto all'applicazione sovrastante e liberare le risorse del protocollo
  • permettere al protocollo di disabilitare il meccanismo di conferma per determinate connessioni, affinché funzioni come un UDP "puro"

Intestazione Reliable UDP

Ricordiamo che un datagramma UDP è incapsulato in un datagramma IP. Il pacchetto Reliable UDP viene quindi "avvolto" in un datagramma UDP.
Incapsulamento dell'intestazione Reliable UDP:Implementazione del protocollo Reliable Udp per .Net

La struttura dell'intestazione Reliable UDP è piuttosto semplice:

Implementazione del protocollo Reliable Udp per .Net

  • Flags – bandiere di controllo del pacchetto
  • MessageType – tipo di messaggio, utilizzato dalle applicazioni sovrastanti per abbonarsi a messaggi specifici
  • TransmissionId — numero di trasmissione, insieme all'indirizzo e alla porta del destinatario definisce univocamente la connessione
  • PacketNumber – numero del pacchetto
  • Options – opzioni aggiuntive del protocollo. Nel caso del primo pacchetto, viene utilizzato per indicare la dimensione del messaggio

Le bandiere possono essere le seguenti:

  • FirstPacket — primo pacchetto del messaggio
  • NoAsk — il messaggio non richiede l'attivazione del meccanismo di conferma
  • LastPacket — ultimo pacchetto del messaggio
  • RequestForPacket — pacchetto di conferma o richiesta del pacchetto perso

Principi generali di funzionamento del protocollo

Poiché Reliable UDP è orientato alla consegna garantita dei messaggi tra due nodi, deve essere in grado di stabilire una connessione con l'altra parte. Per stabilire una connessione, la parte mittente invia un pacchetto con il flag FirstPacket, la risposta a questo pacchetto significherà l'instaurazione della connessione. Tutti i pacchetti di risposta, o, in alternativa, i pacchetti di conferma, impostano sempre il valore del campo PacketNumber a uno in più del valore massimo di PacketNumber dei pacchetti ricevuti con successo. Nel campo Options del primo pacchetto inviato viene registrata la dimensione del messaggio.

Per completare la connessione, viene utilizzato un meccanismo simile. Nell'ultimo pacchetto di messaggi viene impostato il flag LastPacket. Nel pacchetto di risposta viene indicato il numero dell'ultimo pacchetto + 1, il che per il lato ricevente significa consegna avvenuta con successo del messaggio.
Diagramma per l'instaurazione e la chiusura della connessione:Implementazione del protocollo Reliable Udp per .Net

Quando la connessione è stabilita, inizia la trasmissione dei dati. I dati vengono trasmessi in blocchi di pacchetti. Ogni blocco, ad eccezione dell'ultimo, contiene un numero fisso di pacchetti. Questo numero corrisponde alla dimensione della finestra di ricezione/trasmissione. L'ultimo blocco di dati può contenere un numero inferiore di pacchetti. Dopo l'invio di ogni blocco, la parte mittente attende una conferma di consegna o una richiesta di rinvio dei pacchetti persi, mantenendo aperta la finestra di ricezione/trasmissione per ricevere le risposte. Una volta ricevuta la conferma di consegna del blocco, la finestra di ricezione/trasmissione viene spostata e viene inviato il successivo blocco di dati.

La parte ricevente accetta i pacchetti. Ogni pacchetto viene verificato per la corrispondenza con la finestra di trasmissione. I pacchetti che non rientrano nella finestra e i duplicati vengono scartati. Poiché la dimensione della finestra è rigorosamente fissa e identica sia per il ricevente che per il mittente, in caso di consegna di un blocco di pacchetti senza perdite, la finestra viene spostata per ricevere i pacchetti del successivo blocco di dati e viene inviata una conferma di consegna. Se la finestra non si riempie entro un periodo stabilito dal timer di lavoro, verrà avviato un controllo per verificare quali pacchetti non sono stati consegnati e verranno inviati richieste di rinvio.
Diagramma per la retransmissione:Implementazione del protocollo Reliable Udp per .Net

Timeout e timer del protocollo

Esistono diverse ragioni per cui potrebbe non essere possibile stabilire una connessione. Ad esempio, se la parte ricevente è offline. In tal caso, se si tenta di stabilire una connessione, questa verrà chiusa per timeout. Nell'implementazione di Reliable UDP vengono utilizzati due timer per definire i timeout. Il primo, il timer di lavoro, serve ad attendere una risposta dall'host remoto. Se scatta sul lato del mittente, viene eseguita una nuova trasmissione dell'ultimo pacchetto inviato. Se il timer scatta presso il ricevente, viene eseguita una verifica dei pacchetti persi e vengono inviate richieste di rinvio.

Il secondo timer è necessario per chiudere la connessione in caso di assenza di comunicazione tra i nodi. Per il lato mittente, viene avviato subito dopo l'attivazione del timer di lavoro e attende una risposta dal nodo remoto. In assenza di risposta entro il periodo stabilito, la connessione viene chiusa e le risorse vengono liberate. Per il lato ricevente, il timer di chiusura connessione viene avviato dopo la doppia attivazione del timer di lavoro. Questo è necessario come garanzia contro la perdita del pacchetto di conferma. Quando il timer scatta, la connessione viene anch'essa chiusa e le risorse vengono liberate.

Diagramma degli stati della trasmissione Reliable UDP

I principi di funzionamento del protocollo sono implementati in un automa a stati, ogni stato del quale è responsabile di una specifica logica di elaborazione dei pacchetti.
Diagramma degli stati UDP Affidabile:

Implementazione del protocollo Reliable Udp per .Net

Chiuso – non è effettivamente uno stato, ma è il punto di partenza e di arrivo per l'automa. Lo stato Chiuso viene considerato come un blocco di controllo della trasmissione, che, implementando un server UDP asincrono, reindirizza i pacchetti verso le connessioni appropriate e avvia l'elaborazione degli stati.

InvioPrimoPacchetto – stato iniziale in cui si trova la connessione in uscita durante l'invio di un messaggio.

In questo stato viene inviato il primo pacchetto per i messaggi normali. Per i messaggi inviati senza conferma di ricezione, questo è l'unico stato: in esso avviene l'invio dell'intero messaggio.

CicloDiInvio – stato principale per la trasmissione dei pacchetti di messaggio.

Il passaggio ad esso dallo stato InvioPrimoPacchetto avviene dopo l'invio del primo pacchetto di messaggio. È in questo stato che arrivano tutte le conferme e le richieste di ritrasmissione. L'uscita è possibile in due casi: in caso di consegna riuscita del messaggio o per timeout.

PrimoPacchettoRicevuto – stato iniziale per il ricevente del messaggio.

In esso vengono verificate la correttezza dell'inizio della trasmissione, create le strutture necessarie e inviata una conferma di ricezione del primo pacchetto.

Per un messaggio composto da un singolo pacchetto e inviato senza conferma di consegna, questo è l'unico stato. Dopo l'elaborazione di tale messaggio la connessione viene chiusa.

Assemblaggio – stato principale per la ricezione dei pacchetti di messaggio.

In esso vengono registrati i pacchetti in uno storage temporaneo, vengono controllati per l'assenza di perdite di pacchetti, vengono inviati conferme di consegna del blocco di pacchetti e del messaggio intero, e vengono inviate richieste di riespedizione dei pacchetti persi. In caso di ricezione riuscita dell'intero messaggio – la connessione passa allo stato Completed, altrimenti viene eseguito un timeout.

Completed – chiusura della connessione in caso di ricezione riuscita dell'intero messaggio.

Questo stato è necessario per l'assemblaggio del messaggio e per i casi in cui la conferma di consegna del messaggio è stata persa durante il tragitto verso il mittente. L'uscita da questo stato avviene tramite timeout, ma la connessione è considerata chiusa con successo.

Approfondimento sul codice. Blocco di controllo della trasmissione

Uno dei componenti chiave di Reliable UDP è il blocco di controllo della trasmissione. Il compito di questo blocco è conservare le connessioni attuali e gli elementi ausiliari, distribuire i pacchetti ricevuti alle connessioni corrispondenti, fornire un'interfaccia per l'invio di pacchetti alla connessione e implementare l'API del protocollo. Il blocco di controllo della trasmissione riceve pacchetti dal livello UDP e li indirizza per l'elaborazione all'automa finale. Per la ricezione dei pacchetti, è implementato un server UDP asincrono.
Alcuni membri della classe ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // array di byte per la chiave specificata. Utilizzato per assemblare i messaggi in ingresso    
  public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
  // array di byte per la chiave specificata. Utilizzato per inviare messaggi in uscita.
  public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
  // record di connessione per la chiave specificata.
  private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
  // lista di sottoscrittori ai messaggi.
  private readonly List m_subscribers;    
  // socket locale    
  private Socket m_socketIn;
  // porta per messaggi in arrivo
  private int m_port;
  // indirizzo IP locale
  private IPAddress m_ipAddress;    
  // endpoint locale    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // collezione di stati inizializzati
  // dell'automa finale
  public StatesCollection States { get; private set; }
  // generatore di numeri casuali. Utilizzato per creare TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implementazione di un server UDP asincrono:

private void Ricevi()
{
  EndPoint clientConnesso = new IPEndPoint(IPAddress.Any, 0);
  // creiamo un nuovo buffer, per ogni socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // passiamo il buffer come parametro per il metodo asincrono
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref clientConnesso, FineRicezione, buffer);
}   

private void FineRicezione(IAsyncResult ar)
{
  EndPoint clientConnesso = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref clientConnesso);
  // pacchetto ricevuto, pronti per ricevere il successivo        
  Ricevi();
  // poiché il modo più semplice per risolvere il problema del buffer è ottenere un riferimento ad esso 
  // da IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // otteniamo l'intestazione del pacchetto        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // è arrivato un pacchetto non valido - lo scartiamo
    return;
  }
  // costruiamo una chiave per identificare il record di connessione per il pacchetto
  Tuple chiave = new Tuple(clientConnesso, header.TransmissionId);
  // otteniamo il record di connessione esistente o ne creiamo uno nuovo
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(chiave, new ReliableUdpConnectionRecord(chiave, this, header.ReliableUdpMessageType));
  // avviamo il pacchetto nel processo nella macchina a stati
  record.State.ReceivePacket(record, header, bytes);
}

Per ogni trasmissione di messaggio viene creata una struttura che contiene informazioni sulla connessione. Questa struttura è chiamata record di connessione.
Alcuni membri della classe ReliableUdpConnectionRecord:

classe interna ReliableUdpConnectionRecord : IDisposable
{
  	// array di byte con il messaggio
  public byte[] IncomingStream { get; set; }
  	// riferimento allo stato della macchina a stati
  public ReliableUdpState State { get; set; }
  	// coppia che definisce univocamente il record di connessione
  	// nel blocco di controllo della trasmissione
  public Tuple Key { get; private set; }
  	// limite inferiore della finestra di ricezione
  public int WindowLowerBound;
  	// dimensione della finestra di trasmissione
  public readonly int WindowSize;
  	// numero del pacchetto da inviare
  public int SndNext;
  	// numero di pacchetti da inviare
  public int NumberOfPackets;
  	// numero di trasmissione (questa è la seconda parte del Tuple)
  	// per ogni messaggio il suo
  public readonly Int32 TransmissionId;
  	// endpoint IP remoto – effettivamente il destinatario del messaggio
  public readonly IPEndPoint RemoteClient;
  	// dimensione del pacchetto, per evitare la frammentazione a livello IP
  	// non deve superare l'MTU – (IP.Header + UDP.Header + ReliableUDP.Header)
  public readonly int BufferSize;
  	// blocco di controllo della trasmissione
  public readonly ReliableUdpConnectionControlBlock Tcb;
  	// incapsula i risultati dell'operazione asincrona per BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  	// non inviare pacchetti di conferma
  public bool IsNoAnswerNeeded;
  	// ultimo pacchetto ricevuto correttamente (è sempre impostato sul numero più alto)
  public int RcvCurrent;
  	// array con i numeri dei pacchetti persi
  public int[] LostPackets { get; private set; }
  	// è arrivato l'ultimo pacchetto. Usato come bool.
  public int IsLastPacketReceived = 0;
  	//...
}

Approfondimento sul codice. Stati

Gli stati implementano la macchina a stati del protocollo Reliable UDP, nella quale avviene l'elaborazione principale dei pacchetti. La classe astratta ReliableUdpState fornisce un'interfaccia per lo stato:

Implementazione del protocollo Reliable Udp per .Net

Tutta la logica operativa del protocollo è implementata dalle classi sopra presentate, insieme a una classe di supporto che fornisce metodi statici, come ad esempio la costruzione dell'intestazione ReliableUdp dal record di connessione.

Di seguito verranno esaminate in dettaglio le implementazioni dei metodi dell'interfaccia che definiscono i principali algoritmi di funzionamento del protocollo.

Metodo DisposeByTimeout

Il metodo DisposeByTimeout è responsabile del rilascio delle risorse di connessione al termine del timeout e per la segnalazione della consegna del messaggio avvenuta con successo/non riuscita.
ReliableUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

È stato ridefinito solo nello stato Completed.
Completed.DisposeByTimeout:

protegido sovrascrivere void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // comunichiamo il ricevimento riuscito del messaggio
  SetAsCompleted(connectionRecord);        
}

Metodo ProcessPackets

Il metodo ProcessPackets è responsabile dell'elaborazione aggiuntiva di uno o più pacchetti. Viene chiamato direttamente o tramite un timer di attesa dei pacchetti.

Stato Assemblaggio il metodo è sovrascritto e ha il compito di controllare i pacchetti persi e passare allo stato Completed, nel caso in cui venga ricevuto l'ultimo pacchetto e venga superato il controllo di successo
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // ci sono pacchetti persi, inviamo richieste per essi
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // impostiamo il timer per la seconda volta, per un nuovo tentativo di invio
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // se dopo due tentativi di attivazione del WaitForPacketTimer
    // non siamo riusciti a ricevere i pacchetti - avviamo il timer di chiusura della connessione
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // controllo di successo 
  {
    // inviamo una conferma di ricezione del blocco di dati
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // al posto dell'implementazione istantanea delle risorse
    // avviamo un timer, nel caso in cui
    // se l'ultimo ack non raggiunge il mittente e questo lo richiede di nuovo.
    // al termine del timer - implementiamo le risorse
    // nello stato Completed il metodo del timer è sovrascritto
    StartCloseWaitTimer(connectionRecord);
  }
  // questo è il caso in cui l'ack per il blocco di pacchetti è andato perso
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // avviamo il timer di chiusura della connessione
    StartCloseWaitTimer(connectionRecord);
  }
}

Stato CicloDiInvio questo metodo viene chiamato solo tramite timer ed è responsabile del reinvio dell'ultimo messaggio, oltre che dell'attivazione del timer di chiusura della connessione.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  // inviamo nuovamente l'ultimo pacchetto 
  // (in caso di ripristino della connessione, il nodo ricevente invierà di nuovo le richieste che non sono arrivate a lui)
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // attiviamo il timer CloseWait – per attendere il ripristino della connessione o la sua conclusione
  StartCloseWaitTimer(connectionRecord);
}

Stato Completed il metodo ferma il timer di lavoro e invia un messaggio agli abbonati.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // raccogliamo il messaggio e lo inviamo agli abbonati
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Metodo ReceivePacket

Stato PrimoPacchettoRicevuto l'obiettivo principale del metodo è determinare se il primo pacchetto del messaggio è effettivamente arrivato all'interfaccia, oltre a raccogliere un messaggio costituito da un unico pacchetto.
FirstPacketReceived.ReceivePacket:

public override void RiceviPacchetto(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    
    return;
  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))
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  connectionRecord.RcvCurrent = header.PacketNumber;
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  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);
  }
}

Stato CicloDiInvio Questo metodo è stato sovrascritto per ricevere conferme di consegna e richieste di ritrasmissione.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // calcolo del limite finale della finestra
  // si prende il limite della finestra + 1, per ottenere conferme di consegna
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // controllo se rientra nella finestra        
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // verificare se è l'ultimo pacchetto:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // trasmissione completata
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // questa è una risposta al primo pacchetto con conferma         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // senza spostare la finestra
    SendPacket(connectionRecord);
  }
  // è arrivata la conferma di ricezione del blocco dati
  else if (header.PacketNumber == windowHighestBound)
  {
    // spostiamo la finestra di ricezione/trasmissione
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // azzeriamo l'array di controllo della trasmissione
    connectionRecord.WindowControlArray.Nullify();
    // inviamo il blocco di pacchetti
    SendPacket(connectionRecord);
  }
  // è una richiesta di ritrasmissione – inviamo il pacchetto richiesto          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Stato Assemblaggio Nel metodo ReceivePacket avviene il lavoro principale di assemblaggio del messaggio dai pacchetti in arrivo.
Assemblaggio.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // elaborazione dei pacchetti con il meccanismo di conferma della consegna disattivato
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // ripristino del timer
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // registrazione dei dati
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // se abbiamo ricevuto un pacchetto con il flag finale - concludiamo
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // calcolo del limite superiore della finestra
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // scartiamo i pacchetti non compresi nella finestra
  if (header.PacketNumber  (windowHighestBound))
    return;
  // scartiamo i duplicati
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // registriamo i dati 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // incrementiamo il contatore dei pacchetti        
  connectionRecord.PacketCounter++;
  // registriamo nell'array di controllo della finestra il numero corrente del pacchetto        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // impostiamo il pacchetto più alto ricevuto        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // riavviamo i timer        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // se è arrivato l'ultimo pacchetto
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // se abbiamo ricevuto tutti i pacchetti della finestra, ripristiniamo il contatore
  // e inviamo un pacchetto di conferma
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // ripristiniamo il contatore.      
    connectionRecord.PacketCounter = 0;
    // abbiamo spostato la finestra di trasmissione
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // azzeramento dell'array di controllo della trasmissione
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // se l'ultimo pacchetto è già stato ricevuto        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // controlliamo i pacchetti          
    ProcessPackets(connectionRecord);
  }
}

Stato Completed L'unico compito del metodo è inviare nuovamente una conferma della consegna riuscita del messaggio.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // reinvio dell'ultimo pacchetto a causa del fatto che,
  // l'ultimo ack non è arrivato al mittente
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Metodo SendPacket

Stato InvioPrimoPacchetto questo metodo invia il primo pacchetto di dati, o, se il messaggio non richiede conferma di ricezione, l'intero messaggio.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // se non è necessaria la conferma - inviamo tutti i pacchetti
  // e liberiamo le risorse
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Qui avviene l'invio As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // creiamo l'intestazione del pacchetto e la inviamo 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // incrementiamo il contatore
  connectionRecord.SndNext++;
  // spostiamo la finestra
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Avviamo il timer
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Stato CicloDiInvio in questo metodo avviene l'invio di un blocco di pacchetti.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // inviamo un blocco di pacchetti      
  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 caso di una finestra di trasmissione grande, riavviamo il timer dopo l'invio
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
  {
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  }
}

Approfondimento sul codice. Creazione e stabilizzazione delle connessioni

Ora che ci siamo familiarizzati con gli stati e i metodi principali utilizzati per gestire gli stati, possiamo esaminare più in dettaglio alcuni esempi di funzionamento del protocollo.
Diagramma di trasmissione dei dati in condizioni normali:Implementazione del protocollo Reliable Udp per .Net

Esaminiamo in dettaglio la creazione record di connessione per stabilire una connessione e inviare il primo pacchetto. L'iniziatore della trasmissione è sempre l'applicazione che chiama il metodo API per l'invio del messaggio. Successivamente, viene utilizzato il metodo StartTransmission del blocco di controllo della trasmissione, che avvia la trasmissione dei dati per il nuovo messaggio.
Creazione di una connessione in uscita:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException("", "Devi utilizzare un costruttore con parametri o avviare il listener prima di inviare il messaggio");
    }
    // avviamo l'elaborazione dei pacchetti in entrata
    StartListener(LocalEndpoint);
  }
  // creiamo una chiave per il dizionario, basata su EndPoint e ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // creiamo un numero casuale transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // creiamo una nuova registrazione per la connessione e verifichiamo, 
  // se esiste già un numero simile nei nostri dizionari
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // se esiste - rigeneriamo nuovamente un numero casuale 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // se non ci siamo riusciti di nuovo - generiamo un'eccezione
      throw new ArgumentException("La coppia TransmissionId & Endpoint esiste già nel dizionario");
  }
  // abbiamo avviato lo stato per l'elaborazione         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Invio del primo pacchetto (stato FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // creiamo l'intestazione del pacchetto e la inviamo 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // aumentiamo il contatore
  connectionRecord.SndNext++;
  // spostiamo la finestra
  connectionRecord.WindowLowerBound++;
  // passiamo allo stato SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Avviamo il timer
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Dopo l'invio del primo pacchetto, il mittente passa allo stato CicloDiInvio – in attesa di conferma di ricezione del pacchetto.
La parte ricevente, mediante il metodo EndReceive, riceve il pacchetto inviato, crea un nuovo record di connessione e passa questo pacchetto, con l'intestazione precedentemente analizzata, al metodo ReceivePacket per l'elaborazione. PrimoPacchettoRicevuto
Creazione della connessione lato ricevente:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // pacchetto ricevuto
  // analizziamo l'intestazione del pacchetto
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // è arrivato un pacchetto non valido - scartalo
    return;
  }
  // costruiamo la chiave per determinare il record di connessione per il pacchetto
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // otteniamo il record di connessione esistente o ne creiamo uno nuovo
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // avviamo il pacchetto nel processore a stato finale
  record.State.ReceivePacket(record, header, bytes);
}

Ricezione del primo pacchetto e invio della conferma (stato FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // scartiamo il pacchetto
    return;
  // ...
  // per design tutti i numeri dei pacchetti iniziano da 0;
  if (header.PacketNumber != 0)          
    return;
  // inizializziamo l'array per memorizzare le parti del messaggio
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // registriamo i dati del pacchetto nell'array
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // calcoliamo il numero di pacchetti che devono arrivare
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length/(double)connectionRecord.BufferSize));
  // registriamo il numero dell'ultimo pacchetto ricevuto (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // dopo spostiamo la finestra di ricezione di 1
  connectionRecord.WindowLowerBound++;
  // cambiamo stato
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*se non è necessario il meccanismo di conferma*/)
  // ...
  else
  {
    // inviamo la conferma
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Approfondimento sul codice. Chiusura della connessione per timeout

La gestione dei timeout è una parte importante del Reliable UDP. Consideriamo un esempio in cui si è verificato un guasto in un nodo intermedio e la consegna dei dati in entrambe le direzioni è diventata impossibile.
Diagramma di chiusura della connessione per timeout:Implementazione del protocollo Reliable Udp per .Net

Come si può vedere dal diagramma, il timer di lavoro sul mittente inizia subito dopo l'invio di un blocco di pacchetti. Questo avviene nel metodo SendPacket nello stato CicloDiInvio.
Attivazione del timer di lavoro (stato SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // inviamo un blocco di pacchetti   
  // ...   
  // riavviamo il timer dopo l'invio
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
}

I periodi del timer vengono impostati al momento della creazione della connessione. Per impostazione predefinita, ShortTimerPeriod è pari a 5 secondi. Nell'esempio, è impostato a 1,5 secondi.

Nel caso di una connessione in entrata, il timer si avvia dopo aver ricevuto l'ultimo pacchetto di dati, ciò avviene nel metodo ReceivePacket nello stato Assemblaggio
Attivazione del timer operativo (stato Assembling):

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

Nella connessione in entrata, durante il tempo di attesa del timer operativo non sono arrivati ulteriori pacchetti. Il timer è scattato e ha chiamato il metodo ProcessPackets, nel quale sono stati rilevati pacchetti persi e sono state inviate per la prima volta richieste di consegna ripetuta.
Invio di richieste di consegna ripetuta (stato Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*controllo dei pacchetti persi */)
  {
    // inviamo richieste di consegna ripetuta
    // impostiamo il timer per la seconda volta, per il tentativo di trasmissione
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // se dopo due attivazioni di WaitForPacketTimer 
    // non siamo riusciti a ricevere pacchetti - avviamo il timer di chiusura della connessione
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/*è arrivato l'ultimo pacchetto e il controllo ha avuto successo */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // se ack sul blocco dei pacchetti è andato perso
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // rimandiamo nuovamente l'ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // avviamo il timer di chiusura della connessione
    StartCloseWaitTimer(connectionRecord);
  }
}

La variabile TimerSecondTry è stata impostata a true. Questa variabile è responsabile del riavvio del timer operativo.

Anche dal lato del mittente si attiva il timer operativo e viene rimandato l'ultimo pacchetto inviato.
Attivazione del timer di chiusura della connessione (stato SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // rimandiamo nuovamente l'ultimo pacchetto 
  // ...        
  // attiviamo il timer CloseWait – per attendere il ripristino della connessione o la sua chiusura
  StartCloseWaitTimer(connectionRecord);
}

Dopo di che, viene avviato un timer di chiusura della connessione nella connessione uscente.
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);
}

Il periodo di attesa del timer di chiusura della connessione è di 30 secondi per impostazione predefinita.

Dopo breve tempo, il timer di lavoro lato destinatario scatta nuovamente, vengono inviati nuovi richieste e, successivamente, viene avviato il timer di chiusura della connessione nella connessione entrante.

Al verificarsi dei timer di chiusura, tutte le risorse di entrambi i record di connessione vengono liberate. Il mittente comunica un errore di consegna all'applicazione di livello superiore (vedi API Reliable UDP).
Liberazione delle risorse del record di connessione:

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);
  }
}

Approfondimento sul codice. Ripristino della trasmissione dei dati

Diagramma di recupero della trasmissione dei dati in caso di perdita di pacchetto:Implementazione del protocollo Reliable Udp per .Net

Come già discusso nella chiusura della connessione per timeout, al termine del timer di lavoro, il destinatario effettuerà un controllo delle perdite di pacchetti. In caso di perdite di pacchetti, verrà creato un elenco dei numeri dei pacchetti non ricevuti dal destinatario. Questi numeri vengono inseriti nell'array LostPackets della connessione specifica e vengono inviati richieste per la riconsegna.
Invio di richieste di riconsegna dei pacchetti (stato Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // ci sono pacchetti persi, inviamo richieste per essi
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Il mittente accetterà la richiesta di riconsegna e invierà nuovamente i pacchetti mancanti. Vale la pena notare che in quel momento il mittente ha già avviato il timer di chiusura della connessione e, una volta ricevuta la richiesta, esso viene azzerato.
Riconsegna dei pacchetti persi (stato SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // reset del timer di chiusura della connessione 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // questa è una richiesta di ritrasmissione – inviamo il pacchetto richiesto          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Il pacchetto ritrasmesso (packet#3 nel diagramma) viene ricevuto dalla connessione in ingresso. Viene effettuato un controllo del riempimento della finestra di ricezione e la normale trasmissione dei dati viene ripristinata.
Controllo per il rispetto della finestra di ricezione (stato Assembling):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // incrementiamo il contatore dei pacchetti        
  connectionRecord.PacketCounter++;
  // registriamo nell'array di controllo della finestra il numero attuale del pacchetto        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // impostiamo il pacchetto più alto ricevuto        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // riavviamo i timer        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // se abbiamo ricevuto tutti i pacchetti della finestra, quindi ripristiniamo il contatore
  // e inviamo il pacchetto di conferma
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // ripristiniamo il contatore.      
    connectionRecord.PacketCounter = 0;
    // abbiamo spostato la finestra di trasmissione
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // azzeramento dell'array di controllo della trasmissione
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API Reliable UDP

Per interagire con il protocollo di trasmissione dati, è disponibile una classe pubblica Reliable Udp, che funge da involucro per il blocco di controllo della trasmissione. Ecco i membri più importanti della classe:

classe pubblica sigillata ReliableUdp : IDisposable
{
  
  // ottiene l'endpoint locale
  public IPEndPoint LocalEndpoint    
  // crea un'istanza di ReliableUdp e avvia
  // l'ascolto dei pacchetti in arrivo sull'indirizzo IP specificato
  // e sulla porta. Un valore di 0 per la porta significa utilizzare
  // una porta allocata dinamicamente
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // iscrizione per ricevere messaggi in arrivo
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // disiscrizione dalla ricezione di messaggi
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // invia un messaggio in modo asincrono
  // Nota: non si perde la compatibilità con XP e Server 2003, poiché si utilizza .NET Framework 4.0
  public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // inizia a inviare un messaggio in modo asincrono
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // ottiene il risultato dell'invio asincrono
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // libera le risorse
  public void Dispose()    
}

La ricezione del messaggio avviene tramite iscrizione. La firma del delegato per il metodo di callback:

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

Messaggio:

public class ReliableUdpMessage
{
  // tipo di messaggio, semplice enumerazione
  public ReliableUdpMessageTypes Type { get; private set; }
  // dati del messaggio
  public byte[] Body { get; private set; }
  // se impostato su true - il meccanismo di conferma della consegna sarà disabilitato
  // per la trasmissione di un messaggio specifico
  public bool NoAsk { get; private set; }
}

Per iscriversi a un tipo specifico di messaggi e/o a un mittente specifico si utilizzano due parametri opzionali: ReliableUdpMessageTypes messageType e IPEndPoint ipEndPoint.

Tipi di messaggi:

public enum ReliableUdpMessageTypes : short
{ 
  // Qualunque
  Any = 0,
  // Richiesta al server STUN 
  StunRequest = 1,
  // Risposta dal server STUN
  StunResponse = 2,
  // Trasferimento file
  FileTransfer = 3,
  // ...
}

L'invio dei messaggi avviene in modo asincrono; a tal fine, il protocollo implementa un modello di programmazione asincrona:

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

Il risultato dell'invio del messaggio sarà true – se il messaggio è stato ricevuto con successo dal destinatario e false – se la connessione è stata chiusa per timeout:

public bool EndSendMessage(IAsyncResult asyncResult)

Conclusione

Molti aspetti non sono stati descritti in questo articolo. I meccanismi di coordinamento dei flussi, la gestione delle eccezioni e degli errori, l'implementazione di metodi di invio messaggi asincroni. Tuttavia, il nucleo del protocollo, la descrizione della logica di elaborazione dei pacchetti, la configurazione della connessione e la gestione dei timeout devono essere chiariti per te.

La versione del protocollo di consegna affidabile presentata è abbastanza stabile e flessibile e soddisfa requisiti precedentemente definiti. Tuttavia, voglio aggiungere che l'implementazione descritta può essere migliorata. Ad esempio, per aumentare la larghezza di banda e modificare dinamicamente i periodi dei timer, nel protocollo possono essere aggiunti meccanismi come la finestra mobile e il RTT; sarebbe inoltre utile implementare un meccanismo per determinare l'MTU tra i nodi della connessione (ma solo nel caso di invio di messaggi grandi).

Grazie per l'attenzione, attendo i tuoi commenti e osservazioni.

P.S. Per coloro che sono interessati ai dettagli o vogliono semplicemente testare il protocollo, ecco il link al progetto su GitHub:
Progetto Reliable UDP

Link e articoli utili

  1. Specifiche del protocollo TCP: in inglese e in russo
  2. Specifiche del protocollo UDP: in inglese e in russo
  3. Discussione sul protocollo RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 e rfc 1151
  5. Implementazione semplice della conferma di consegna tramite UDP: Prendi il pieno controllo della tua rete con .NET e UDP
  6. Articolo che descrive i meccanismi per superare i NAT: Comunicazione peer-to-peer attraverso i traduttori di indirizzi di rete
  7. Implementazione del modello di programmazione asincrona: Implementazione del modello di programmazione asincrona CLR e Come implementare il pattern di design IAsyncResult
  8. Trasferimento del modello di programmazione asincrona in un modello asincrono basato su attività (APM in TAP):
    TPL e programmazione asincrona tradizionale .NET
    Interop con altri pattern e tipi asincroni

Aggiornamento: Grazie mayorovp e sidristij per l'idea di aggiungere un task all'interfaccia. La compatibilità della libreria con i sistemi operativi più vecchi non viene compromessa, poiché il quarto framework supporta sia XP che Server 2003.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster