Implementazione del protocollo Reliable Udp per .Net

Internet è cambiato molto tempo fa. Uno dei principali protocolli di Internet, UDP, viene utilizzato dalle applicazioni non solo per fornire datagrammi e trasmissioni, ma anche per fornire connessioni "peer-to-peer" tra i nodi di rete. A causa del suo design semplice, questo protocollo ha molti usi precedentemente non pianificati, tuttavia, le carenze del protocollo, come la mancanza di consegna garantita, non sono scomparse da nessuna parte. Questo articolo descrive l'implementazione del protocollo di recapito garantito su UDP.
Contenuto:Iscrizione
Requisiti del protocollo
Intestazione UDP affidabile
Principi generali del protocollo
Timeout e timer di protocollo
Diagramma dello stato di trasmissione UDP affidabile
Più in profondità nel codice. unità di controllo della trasmissione
Più in profondità nel codice. stati

Più in profondità nel codice. Creare e stabilire connessioni
Più in profondità nel codice. Chiusura della connessione al timeout
Più in profondità nel codice. Ripristino del trasferimento dei dati
API UDP affidabile
conclusione
Link e articoli utili

Iscrizione

L'architettura originale di Internet presupponeva uno spazio di indirizzi omogeneo in cui ogni nodo aveva un indirizzo IP globale e univoco e poteva comunicare direttamente con altri nodi. Ora Internet, infatti, ha un'architettura diversa: un'area di indirizzi IP globali e molte aree con indirizzi privati ​​nascosti dietro dispositivi NAT.In questa architettura, solo i dispositivi nello spazio degli indirizzi globale possono comunicare facilmente con chiunque sulla rete perché hanno un indirizzo IP univoco e instradabile a livello globale. Un nodo su una rete privata può connettersi ad altri nodi sulla stessa rete e può anche connettersi ad altri nodi noti nello spazio degli indirizzi globale. Questa interazione è ottenuta in gran parte grazie al meccanismo di traduzione degli indirizzi di rete. I dispositivi NAT, come i router Wi-Fi, creano speciali voci della tabella di traduzione per le connessioni in uscita e modificano gli indirizzi IP e i numeri di porta nei pacchetti. Ciò consente connessioni in uscita dalla rete privata agli host nello spazio degli indirizzi globale. Ma allo stesso tempo, i dispositivi NAT di solito bloccano tutto il traffico in entrata a meno che non vengano impostate regole separate per le connessioni in entrata.

Questa architettura di Internet è sufficientemente corretta per la comunicazione client-server, in cui i client possono trovarsi in reti private e i server hanno un indirizzo globale. Ma crea difficoltà per la connessione diretta di due nodi tra diverso reti private. Una connessione diretta tra due nodi è importante per le applicazioni peer-to-peer come la trasmissione vocale (Skype), l'accesso remoto a un computer (TeamViewer) o il gioco online.

Uno dei metodi più efficaci per stabilire una connessione peer-to-peer tra dispositivi su diverse reti private è chiamato perforazione. Questa tecnica è più comunemente utilizzata con applicazioni basate sul protocollo UDP.

Ma se la tua applicazione richiede la consegna garantita dei dati, ad esempio trasferisci file tra computer, l'utilizzo di UDP avrà molte difficoltà a causa del fatto che UDP non è un protocollo di consegna garantito e non fornisce la consegna dei pacchetti in ordine, a differenza del TCP protocollo.

In questo caso, per garantire la consegna dei pacchetti garantita, è necessario implementare un protocollo a livello di applicazione che fornisca le funzionalità necessarie e funzioni su UDP.

Voglio subito notare che esiste una tecnica di perforazione TCP 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 è considerata il modo principale per connettersi tali nodi.

Per il resto di questo articolo, mi concentrerò solo sull'implementazione del protocollo di consegna garantita. L'implementazione della tecnica di perforazione UDP sarà descritta nei seguenti articoli.

Requisiti del protocollo

  1. Consegna affidabile dei pacchetti implementata attraverso un meccanismo di feedback positivo (il cosiddetto riconoscimento positivo)
  2. La necessità di un trasferimento efficiente di big data, ad es. il protocollo deve evitare l'inoltro di pacchetti non necessari
  3. Dovrebbe essere possibile annullare il meccanismo di conferma della consegna (la capacità di funzionare come protocollo UDP "puro")
  4. Possibilità di implementare la modalità di comando, con conferma di ogni messaggio
  5. L'unità di base del trasferimento dei dati sul protocollo deve essere un messaggio

Questi requisiti coincidono in gran parte con i requisiti Reliable Data Protocol descritti in rfc 908 и rfc 1151e ho fatto affidamento su tali standard durante lo sviluppo di questo protocollo.

Per comprendere questi requisiti, esaminiamo i tempi di trasferimento dei dati tra due nodi di rete utilizzando i protocolli TCP e UDP. Lascia che in entrambi i casi avremo perso un pacchetto.
Trasferimento di dati non interattivi su TCP:Implementazione del protocollo Reliable Udp per .Net

Come puoi vedere dal diagramma, in caso di perdita di pacchetti, TCP rileverà il pacchetto perso e lo riporterà al mittente chiedendo il numero del segmento perso.
Trasferimento dati tramite protocollo UDP:Implementazione del protocollo Reliable Udp per .Net

UDP non esegue alcuna procedura di rilevamento delle perdite. Il controllo degli errori di trasmissione nel protocollo UDP è interamente a carico dell'applicazione.

Il rilevamento degli errori nel protocollo TCP si ottiene stabilendo una connessione con un nodo finale, memorizzando lo stato di tale connessione, indicando il numero di byte inviati in ciascuna intestazione del pacchetto e notificando le ricevute utilizzando un numero di riconoscimento.

Inoltre, per migliorare le prestazioni (ovvero l'invio di più di un segmento senza ricevere un riconoscimento), il protocollo TCP utilizza la cosiddetta finestra di trasmissione, ovvero il numero di byte di dati che il mittente del segmento si aspetta di ricevere.

Per ulteriori informazioni sul protocollo TCP, vedere rfc 793, da UDP a rfc 768dove, infatti, sono definiti.

Da quanto sopra, è chiaro che al fine di creare un affidabile protocollo di consegna dei messaggi su UDP (di seguito denominato UDP affidabile), è necessario implementare meccanismi di trasferimento dei dati simili a TCP. Vale a dire:

  • salvare lo stato della connessione
  • utilizzare la numerazione dei segmenti
  • utilizzare speciali pacchetti di conferma
  • utilizzare un meccanismo a finestre semplificato per aumentare il throughput del protocollo

Inoltre, è necessario:

  • segnalare l'inizio di un messaggio, per allocare risorse per la connessione
  • segnalare la fine di un messaggio, per passare il messaggio ricevuto all'applicazione a monte e rilasciare le risorse del protocollo
  • consentire al protocollo specifico della connessione di disabilitare il meccanismo di conferma della consegna per funzionare come UDP "puro".

Intestazione UDP affidabile

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

La struttura dell'intestazione Reliable UDP è abbastanza semplice:

Implementazione del protocollo Reliable Udp per .Net

  • Flag - flag di controllo dei pacchetti
  • MessageType: tipo di messaggio utilizzato dalle applicazioni upstream per sottoscrivere messaggi specifici
  • TransmissionId - il numero della trasmissione, unitamente all'indirizzo e alla porta del destinatario, identifica univocamente la connessione
  • PacketNumber - numero del pacchetto
  • Opzioni: opzioni di protocollo aggiuntive. Nel caso del primo pacchetto, viene utilizzato per indicare la dimensione del messaggio

I flag sono i seguenti:

  • FirstPacket - il primo pacchetto del messaggio
  • NoAsk: il messaggio non richiede l'abilitazione di un meccanismo di riconoscimento
  • LastPacket - l'ultimo pacchetto del messaggio
  • RequestForPacket - pacchetto di conferma o richiesta di un pacchetto perso

Principi generali del protocollo

Poiché Reliable UDP si concentra sulla trasmissione di messaggi garantita tra due nodi, deve essere in grado di stabilire una connessione con l'altro lato. Per stabilire una connessione, il mittente invia un pacchetto con il flag FirstPacket, la cui risposta significherà che la connessione è stata stabilita. Tutti i pacchetti di risposta, o, in altre parole, i pacchetti di riconoscimento, impostano sempre il valore del campo PacketNumber su uno in più rispetto al valore PacketNumber più grande dei pacchetti ricevuti correttamente. Il campo Opzioni per il primo pacchetto inviato è la dimensione del messaggio.

Un meccanismo simile viene utilizzato per terminare una connessione. Il flag LastPacket è impostato sull'ultimo pacchetto del messaggio. Nel pacchetto di risposta è indicato il numero dell'ultimo pacchetto + 1, che per il lato ricevente indica l'avvenuta consegna del messaggio.
Schema di realizzazione e terminazione del collegamento:Implementazione del protocollo Reliable Udp per .Net

Una volta stabilita la connessione, inizia il trasferimento dei dati. I dati vengono trasmessi in blocchi di pacchetti. Ogni blocco, tranne l'ultimo, contiene un numero fisso di pacchetti. È uguale alla dimensione della finestra di ricezione/trasmissione. L'ultimo blocco di dati può contenere meno pacchetti. Dopo aver inviato ciascun blocco, il lato mittente attende una conferma di consegna o una richiesta di riconsegna dei pacchetti persi, lasciando aperta la finestra di ricezione/trasmissione per ricevere risposte. Dopo aver ricevuto la conferma della consegna del blocco, la finestra di ricezione/trasmissione si sposta e viene inviato il successivo blocco di dati.

Il lato ricevente riceve i pacchetti. Ogni pacchetto viene controllato per vedere se rientra nella finestra di trasmissione. I pacchetti e i duplicati che non rientrano nella finestra vengono filtrati. Perché Se la dimensione della finestra è fissa e uguale per il destinatario e il mittente, allora nel caso di un blocco di pacchetti consegnato senza perdita, la finestra viene spostata per ricevere i pacchetti del successivo blocco di dati e viene inviata una conferma di consegna inviato. Se la finestra non si riempie entro il periodo impostato dal timer di lavoro, verrà avviato un controllo su quali pacchetti non sono stati consegnati e verranno inviate le richieste di riconsegna.
Diagramma di ritrasmissione:Implementazione del protocollo Reliable Udp per .Net

Timeout e timer di protocollo

Esistono diversi motivi per cui non è possibile stabilire una connessione. Ad esempio, se la parte ricevente è offline. In questo caso, quando si tenta di stabilire una connessione, la connessione verrà chiusa per timeout. L'implementazione Reliable UDP utilizza due timer per impostare i timeout. Il primo, il timer di lavoro, viene utilizzato per attendere una risposta dall'host remoto. Se si attiva sul lato mittente, l'ultimo pacchetto inviato viene inviato di nuovo. Se il timer scade al destinatario, viene eseguito un controllo dei pacchetti persi e vengono inviate le richieste di riconsegna.

Il secondo timer è necessario per chiudere la connessione in caso di mancanza di comunicazione tra i nodi. Per il lato mittente, si avvia immediatamente dopo la scadenza del timer di lavoro e attende una risposta dal nodo remoto. Se non vi è alcuna risposta entro il periodo specificato, la connessione viene terminata e le risorse vengono rilasciate. Per il lato ricevente, il timer di chiusura della connessione viene avviato dopo che il timer di lavoro è scaduto due volte. Ciò è necessario per assicurarsi contro la perdita del pacchetto di conferma. Quando il timer scade, anche la connessione viene terminata e le risorse vengono rilasciate.

Diagramma dello stato di trasmissione UDP affidabile

I principi del protocollo sono implementati in una macchina a stati finiti, ogni stato della quale è responsabile di una certa logica di elaborazione dei pacchetti.
Diagramma di stato UDP affidabile:

Implementazione del protocollo Reliable Udp per .Net

Chiuso - non è realmente uno stato, è un punto di inizio e di fine per l'automa. Per stato Chiuso viene ricevuto un blocco di controllo della trasmissione che, implementando un server UDP asincrono, inoltra i pacchetti alle connessioni appropriate e avvia l'elaborazione dello stato.

PrimoPacketSending – lo stato iniziale in cui si trova la connessione in uscita al momento dell'invio del messaggio.

In questo stato viene inviato il primo pacchetto per i messaggi normali. Per i messaggi senza conferma di invio, questo è l'unico stato in cui viene inviato l'intero messaggio.

Ciclo di invio – ground state per la trasmissione di pacchetti di messaggi.

Transizione ad esso dallo stato PrimoPacketSending effettuata dopo l'invio del primo pacchetto del messaggio. È in questo stato che arrivano tutti i riconoscimenti e le richieste di ritrasmissione. Uscire da esso è possibile in due casi: in caso di consegna riuscita del messaggio o per timeout.

Primo pacchetto ricevuto – lo stato iniziale per il destinatario del messaggio.

Controlla la correttezza dell'inizio della trasmissione, crea le strutture necessarie e invia un avviso di ricevimento del primo pacchetto.

Per un messaggio costituito da un singolo pacchetto e inviato senza utilizzare la prova di consegna, questo è l'unico stato. Dopo aver elaborato tale messaggio, la connessione viene chiusa.

assemblaggio – stato di base per la ricezione di pacchetti di messaggi.

Scrive i pacchetti nella memoria temporanea, controlla la perdita di pacchetti, invia conferme di consegna di un blocco di pacchetti e dell'intero messaggio e invia richieste di riconsegna dei pacchetti persi. In caso di ricezione riuscita dell'intero messaggio, la connessione passa allo stato Completato, in caso contrario, termina un timeout.

Completato – chiusura della connessione in caso di avvenuta ricezione dell'intero messaggio.

Questo stato è necessario per l'assemblaggio del messaggio e per il caso in cui la conferma di recapito del messaggio sia stata persa durante il tragitto verso il mittente. Si esce da questo stato dopo un timeout, ma la connessione viene considerata chiusa correttamente.

Più in profondità nel codice. unità di controllo della trasmissione

Uno degli elementi chiave di Reliable UDP è il blocco di controllo della trasmissione. Il compito di questo blocco è memorizzare le connessioni correnti e gli elementi ausiliari, distribuire i pacchetti in entrata alle connessioni corrispondenti, fornire un'interfaccia per l'invio di pacchetti a una connessione e implementare l'API del protocollo. Il blocco di controllo della trasmissione riceve i pacchetti dal livello UDP e li inoltra alla macchina a stati per l'elaborazione. Per ricevere i pacchetti, implementa un server UDP asincrono.
Alcuni membri della classe ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implementazione del server UDP asincrono:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  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);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Per ogni trasferimento di messaggi viene creata una struttura che contiene informazioni sulla connessione. Tale struttura è chiamata record di connessione.
Alcuni membri della classe ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Più in profondità nel codice. stati

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

Implementazione del protocollo Reliable Udp per .Net

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

Successivamente, considereremo in dettaglio l'implementazione dei metodi di interfaccia che determinano gli algoritmi di base del protocollo.

Metodo DisposeByTimeout

Il metodo DisposeByTimeout è responsabile del rilascio delle risorse di connessione dopo un timeout e della segnalazione del recapito del messaggio riuscito/non riuscito.
ReliableUdpState.DisposeByTimeout:

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

È ignorato solo nello stato Completato.
Completato.DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

Metodo ProcessPackets

Il metodo ProcessPackets è responsabile dell'ulteriore elaborazione di uno o più pacchetti. Chiamato direttamente o tramite un timer di attesa pacchetto.

In condizione assemblaggio il metodo viene sovrascritto ed è responsabile del controllo dei pacchetti persi e della transizione allo stato Completato, in caso di ricezione dell'ultimo pacchetto e superamento di un controllo riuscito
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

In condizione Ciclo di invio questo metodo viene chiamato solo su un timer ed è responsabile del rinvio dell'ultimo messaggio, nonché dell'abilitazione del timer di chiusura della connessione.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

In condizione Completato il metodo arresta il timer in esecuzione e invia il messaggio ai sottoscrittori.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Metodo ReceivePacket

In condizione Primo pacchetto ricevuto il compito principale del metodo è determinare se il primo pacchetto di messaggi è effettivamente arrivato all'interfaccia, e anche raccogliere un messaggio costituito da un singolo pacchetto.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  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;
  }
  // by design все packet numbers начинаются с 0;
  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));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  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);
  }
}

In condizione Ciclo di invio questo metodo viene ignorato per accettare le conferme di consegna e le 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;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

In condizione assemblaggio nel metodo ReceivePacket, avviene il lavoro principale di assemblaggio di un messaggio dai pacchetti in arrivo.
Assemblaggio. Pacchetto di ricezione:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

In condizione Completato l'unico compito del metodo è inviare un nuovo riconoscimento dell'avvenuta consegna del messaggio.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Metodo di invio pacchetto

In condizione PrimoPacketSending questo metodo invia il primo pacchetto di dati o, se il messaggio non richiede conferma di consegna, l'intero messaggio.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

In condizione Ciclo di invio in questo metodo viene inviato un blocco di pacchetti.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  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++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Più in profondità nel codice. Creare e stabilire connessioni

Ora che abbiamo visto gli stati di base ei metodi usati per gestirli, analizziamo alcuni esempi di come funziona il protocollo in modo un po' più dettagliato.
Diagramma di trasmissione dati in condizioni normali:Implementazione del protocollo Reliable Udp per .Net

Considera in dettaglio la creazione record di connessione per connettersi e inviare il primo pacchetto. Il trasferimento viene sempre avviato dall'applicazione che chiama l'API di invio del messaggio. Successivamente, viene richiamato 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( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  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;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Dopo aver inviato il primo pacchetto, il mittente entra nello stato Ciclo di invio – attendere la conferma della consegna del pacco.
Il lato ricevente, utilizzando il metodo EndReceive, riceve il pacchetto inviato, ne crea uno nuovo record di connessione e passa questo pacchetto, con un'intestazione già analizzata, al metodo ReceivePacket dello stato per l'elaborazione Primo pacchetto ricevuto
Creazione di una connessione sul lato ricevente:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Ricezione del primo pacchetto e invio di un riconoscimento (stato FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  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));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Più in profondità nel codice. Chiusura della connessione al timeout

La gestione del timeout è una parte importante di Reliable UDP. Considera un esempio in cui un nodo intermedio ha avuto esito negativo e la consegna dei dati in entrambe le direzioni è diventata impossibile.
Diagramma per la chiusura di una connessione per timeout:Implementazione del protocollo Reliable Udp per .Net

Come si può vedere dal diagramma, il timer di lavoro del mittente parte immediatamente dopo l'invio di un blocco di pacchetti. Ciò accade nel metodo SendPacket dello stato Ciclo di invio.
Abilitazione del timer di lavoro (stato SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

I periodi del timer vengono impostati quando viene creata la connessione. Il valore predefinito di ShortTimerPeriod è 5 secondi. Nell'esempio, è impostato su 1,5 secondi.

Per una connessione in entrata, il timer si avvia dopo aver ricevuto l'ultimo pacchetto di dati in entrata, ciò avviene nel metodo ReceivePacket dello stato assemblaggio
Abilitazione del timer di lavoro (stato di assemblaggio):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Non sono più arrivati ​​pacchetti sulla connessione in entrata in attesa del timer di lavoro. Il timer è scattato e ha chiamato il metodo ProcessPackets, in cui sono stati trovati i pacchetti persi e sono state inviate le richieste di riconsegna per la prima volta.
Invio richieste di riconsegna (stato di Assemblaggio):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

La variabile TimerSecondTry è impostata su vero. Questa variabile è responsabile del riavvio del timer di lavoro.

Dal lato del mittente, viene attivato anche il timer di lavoro e l'ultimo pacchetto inviato viene rispedito.
Abilitazione timer chiusura connessione (stato SendCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Successivamente, il timer di chiusura della connessione si avvia nella connessione in uscita.
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 timeout del timer di chiusura della connessione è di 30 secondi per impostazione predefinita.

Dopo un breve periodo di tempo, il timer di lavoro sul lato del destinatario si attiva nuovamente, le richieste vengono nuovamente inviate, dopodiché si avvia il timer di chiusura della connessione per la connessione in entrata

Quando i timer di chiusura si attivano, vengono rilasciate tutte le risorse di entrambi i record di connessione. Il mittente segnala il mancato recapito all'applicazione a monte (vedere API UDP affidabile).
Rilascio 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);
  }
}

Più in profondità nel codice. Ripristino del trasferimento dei dati

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

Come già discusso in chiusura della connessione al timeout, quando scade il timer di lavoro, il ricevitore controllerà i pacchetti persi. In caso di perdita di pacchetti, verrà compilato un elenco del numero di pacchetti che non hanno raggiunto il destinatario. Questi numeri vengono immessi nell'array LostPackets di una connessione specifica e vengono inviate le richieste di riconsegna.
Invio richieste di riconsegna pacchi (stato Assemblaggio):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Il mittente accetterà la richiesta di riconsegna e invierà i pacchetti mancanti. Vale la pena notare che in questo momento il mittente ha già avviato il timer di chiusura della connessione e, quando viene ricevuta una richiesta, viene azzerato.
Nuovo invio di pacchetti persi (stato SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Il pacchetto inviato nuovamente (pacchetto n. 3 nel diagramma) viene ricevuto dalla connessione in entrata. Viene effettuato un controllo per vedere se la finestra di ricezione è piena e viene ripristinata la normale trasmissione dei dati.
Controllo degli hit nella finestra di ricezione (stato di assemblaggio):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API UDP affidabile

Per interagire con il protocollo di trasferimento dei dati, esiste una classe Reliable Udp aperta, che funge da wrapper sul blocco di controllo del trasferimento. Ecco i membri più importanti della classe:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

I messaggi si ricevono in abbonamento. Firma del delegato per il metodo di richiamata:

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

Messaggio:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

Per sottoscrivere un tipo di messaggio specifico e/o un mittente specifico, vengono utilizzati due parametri facoltativi: ReliableUdpMessageTypes messageType e IPEndPoint ipEndPoint.

Tipi di messaggio:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

Il messaggio viene inviato in modo asincrono; per questo, il protocollo implementa un modello di programmazione asincrono:

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

Il risultato dell'invio di un messaggio sarà vero - se il messaggio ha raggiunto correttamente il destinatario e falso - se la connessione è stata chiusa per timeout:

public bool EndSendMessage(IAsyncResult asyncResult)

conclusione

Molto non è stato descritto in questo articolo. Meccanismi di thread matching, gestione di eccezioni ed errori, implementazione di metodi di invio di messaggi asincroni. Ma il nucleo del protocollo, la descrizione della logica per elaborare i pacchetti, stabilire una connessione e gestire i timeout, dovrebbe esserti chiaro.

La versione dimostrata del protocollo di consegna affidabile è sufficientemente robusta e flessibile da soddisfare i requisiti definiti in precedenza. Ma voglio aggiungere che l'implementazione descritta può essere migliorata. Ad esempio, per aumentare il throughput e modificare dinamicamente i periodi del timer, è possibile aggiungere al protocollo meccanismi come la finestra scorrevole e RTT, sarà utile anche implementare un meccanismo per determinare MTU tra i nodi di connessione (ma solo se vengono inviati messaggi di grandi dimensioni) .

Grazie per l'attenzione, attendo con ansia i vostri commenti e commenti.

PS Per chi è interessato ai dettagli o vuole semplicemente testare il protocollo, il link al progetto su GitHube:
Progetto UDP affidabile

Link e articoli utili

  1. Specifica del protocollo TCP: in inglese и на русском
  2. Specifica del protocollo UDP: in inglese и на русском
  3. Discussione del protocollo RUDP: bozza-ietf-sigtran-reliable-udp-00
  4. Protocollo dati affidabile: rfc 908 и rfc 1151
  5. Una semplice implementazione della conferma di consegna su UDP: Assumi il controllo totale della tua rete con .NET e UDP
  6. Articolo che descrive i meccanismi di attraversamento NAT: Comunicazione peer-to-peer attraverso traduttori di indirizzi di rete
  7. Implementazione del modello di programmazione asincrona: Implementazione del modello di programmazione asincrona CLR и Come implementare il modello di progettazione IAsyncResult
  8. Porting del modello di programmazione asincrono al modello asincrono basato su attività (APM in TAP):
    TPL e programmazione asincrona tradizionale .NET
    Interoperabilità con altri modelli e tipi asincroni

Aggiornamento: grazie sindacovp и sidristij per l'idea di aggiungere un'attività all'interfaccia. La compatibilità della libreria con i vecchi sistemi operativi non viene violata, perché Il 4° framework supporta sia XP che 2003 server.

Fonte: habr.com

Aggiungi un commento