Internet è cambiato da tempo. Uno dei protocolli fondamentali di Internet, UDP, è utilizzato dalle applicazioni non solo per la consegna di datagrammi e invii broadcast, ma anche per garantire connessioni 'peer-to-peer' tra nodi della rete. A causa della sua semplice struttura, questo protocollo ha trovato molteplici utilizzi non previsti in precedenza, sebbene i suoi svantaggi, come la mancanza di garanzia di consegna, non siano scomparsi. In questo articolo viene descritta l'implementazione di un protocollo di consegna garantita sopra UDP.
Contenuto:
Introduzione
L'architettura iniziale di Internet prevedeva uno spazio indirizzi omogeneo, in cui ogni nodo aveva un indirizzo IP globale e unico, potendo comunicare direttamente con altri nodi. Oggi, infatti, Internet ha un'architettura diversa: una rete di indirizzi IP globali e molte aree con indirizzi privati, nascoste dietro dispositivi NAT.In un'architettura di questo tipo, solo i dispositivi che si trovano nello spazio degli indirizzi globale possono interagire facilmente con chiunque in rete, poiché possiedono un indirizzo IP unico e globalmente instradabile. Un nodo che si trova in una rete privata può connettersi ad altri nodi all'interno della stessa rete, nonché collegarsi ad altri nodi noti nello spazio degli indirizzi globale. Questa interazione è in gran parte possibile grazie al meccanismo di traduzione degli indirizzi di rete. I dispositivi NAT, come ad esempio i router Wi-Fi, creano registrazioni speciali nelle tabelle di traduzione per le connessioni in uscita e modificano gli indirizzi IP e i numeri delle porte nei pacchetti. Questo consente di stabilire una connessione in uscita dalla rete privata a nodi nello spazio degli indirizzi globale. Tuttavia, allo stesso tempo, i dispositivi NAT bloccano generalmente tutto il traffico in ingresso, a meno che non siano impostate regole specifiche per le connessioni in ingresso.
Questa architettura di Internet è piuttosto adatta per l'interazione tra client e server, dove i client possono trovarsi in reti private e i server hanno un indirizzo globale. Tuttavia, presenta difficoltà nel connessione diretta di due nodi tra diverse reti private. La connessione diretta tra due nodi è cruciale per le applicazioni «peer-to-peer», come la trasmissione vocale (Skype), l'accesso remoto a computer (TeamViewer) o i giochi online.
Uno dei metodi più efficaci per stabilire una connessione peer-to-peer tra dispositivi situati in diverse reti private è chiamato «hole punching». Questa tecnica è più comunemente utilizzata con applicazioni basate sul protocollo UDP.
Tuttavia, se la tua applicazione richiede una consegna garantita dei dati, ad esempio, quando trasferisci file tra computer, ci saranno molte difficoltà con l'uso di UDP, poiché non è un protocollo di consegna garantita e non assicura la consegna dei pacchetti in ordine, a differenza del protocollo TCP.
In questo caso, per garantire la consegna sicura dei pacchetti, è necessario implementare un protocollo di livello applicativo che fornisca le funzionalità richieste e che operi sopra l'UDP.
Vorrei subito sottolineare che esiste una tecnica di TCP hole punching per stabilire connessioni TCP tra nodi in diverse reti private, ma poiché non è supportata da molti dispositivi NAT, di solito non viene considerata come il principale metodo di collegamento tra tali nodi.
In questo articolo discuterò solo l'implementazione di un protocollo di consegna garantita. L'implementazione della tecnica di UDP hole punching sarà descritta in articoli successivi.
Requisiti del protocollo
- Consegna affidabile dei pacchetti, realizzata tramite un meccanismo di conferma positiva (cosiddetto positive acknowledgment)
- Necessità di una trasmissione efficiente di grandi quantità di dati, quindi il protocollo deve evitare retransmissione non necessaria dei pacchetti
- Deve esserci la possibilità di disattivare il meccanismo di conferma della consegna (possibilità di operare come protocollo UDP 'puro')
- Possibilità di implementare una modalità di comando, con conferma di ciascun messaggio
- L'unità di base per la trasmissione dei dati tramite il protocollo deve essere un messaggio
Questi requisiti coincidono in gran parte con quelli del Reliable Data Protocol, descritti in e , e ho basato questo protocollo su tali standard.
Per comprendere questi requisiti, esaminiamo i diagrammi temporali della trasmissione dei dati tra due nodi di rete utilizzando i protocolli TCP e UDP. Supponiamo che in entrambi i casi si perda un pacchetto.
Trasmissione di dati non interattivi tramite TCP:
Come si può vedere dal diagramma, nel caso di perdita di pacchetti, TCP rileverà il pacchetto mancante e ne informerà il mittente, richiedendo il numero del segmento perso.
Trasmissione dei dati tramite il protocollo UDP:
UDP non adotta alcuna misura per rilevare le perdite. Il controllo degli errori nella trasmissione del protocollo UDP è interamente responsabilità dell'applicazione.
Il rilevamento degli errori nel protocollo TCP avviene grazie all'instaurazione di una connessione con il nodo finale, mantenendo lo stato di questa connessione, specificando il numero dei byte inviati in ciascuno header di pacchetto e alle notifiche di ricezione 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 la cosiddetta finestra di trasmissione — il numero di byte di dati che il mittente del segmento si aspetta di ricevere.
Maggiore approfondimento sul protocollo TCP può essere trovato in , e sul UDP in , dove vengono definiti.
Da quanto sopra, è chiaro che per creare un protocollo di consegna messaggi affidabile sopra UDP (da qui in avanti chiamato Reliable UDP), è necessario implementare meccanismi di trasmissione dati simili a 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 larghezza di banda del protocollo
Inoltre, è necessario:
- segnalare l'inizio del messaggio, per allocare risorse per la connessione
- segnalare la fine del messaggio, per inviare il messaggio ricevuto all'applicazione superiore e liberare le risorse del protocollo
- consentire al protocollo di disabilitare il meccanismo di conferma della consegna per determinate connessioni, funzionando quindi come un UDP "puro"
Intestazione Reliable UDP
Ricordiamo che un datagramma UDP è incapsulato in un datagramma IP. Il pacchetto Reliable UDP è quindi "avvolto" in un datagramma UDP.
Incapsulazione dell'intestazione Reliable UDP:
La struttura dell'intestazione Reliable UDP è abbastanza semplice:

- Flags – flag di controllo del pacchetto
- MessageType – tipo di messaggio, utilizzato dalle applicazioni superiori per iscriversi a determinati messaggi
- TransmissionId — numero di trasmissione, insieme all'indirizzo e alla porta del destinatario definisce in modo univoco la connessione
- PacketNumber – numero del pacchetto
- Options – opzioni aggiuntive del protocollo. Nel caso del primo pacchetto, serve per specificare la dimensione del messaggio
I flag possono essere i 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 per un pacchetto mancante
Principi generali di funzionamento del protocollo
Poiché Reliable UDP è orientato a garantire la consegna del messaggio tra due nodi, deve essere in grado di stabilire una connessione con l'altra parte. Per stabilire la connessione, il lato mittente invia un pacchetto con il flag FirstPacket, la cui risposta indicherà l'instaurazione della connessione. Tutti i pacchetti di risposta, o in altre parole, i pacchetti di conferma, impostano sempre il valore del campo PacketNumber a uno in più rispetto al valore più alto di PacketNumber dei pacchetti ricevuti con successo. Nel campo Options del primo pacchetto inviato viene registrata la dimensione del messaggio.
Per terminare la connessione viene utilizzato un meccanismo simile. Nell'ultimo pacchetto del messaggio viene impostato il flag LastPacket. Nel pacchetto di risposta viene indicato il numero dell'ultimo pacchetto + 1, il che per il lato ricevente significa la consegna riuscita del messaggio.
Diagramma di stabilimento e terminazione della connessione:
Una volta stabilita la connessione, inizia il trasferimento dei dati. I dati sono trasferiti in blocchi di pacchetti. Ogni blocco, tranne l'ultimo, contiene un numero fisso di pacchetti, che corrisponde alla dimensione della finestra di ricezione/trasmittente. L'ultimo blocco di dati può contenere un numero inferiore di pacchetti. Dopo l'invio di ciascun blocco, il mittente attende una conferma di consegna, oppure una richiesta di reinvio dei pacchetti persi, lasciando aperta la finestra di ricezione/trasmittente per le risposte. Una volta ricevuta la conferma di consegna del blocco, la finestra di ricezione/trasmittente viene spostata e viene inviato il successivo blocco di dati.
Il destinatario riceve i pacchetti. Ogni pacchetto viene controllato per rientrare nella finestra di trasmissione. I pacchetti che non rientrano nella finestra e i duplicati vengono filtrati. Poiché la dimensione della finestra è rigorosamente fissa e identica per il destinatario e il mittente, in caso di consegna di un blocco di pacchetti senza perdite, la finestra si sposta per ricevere i pacchetti del blocco successivo e viene inviata una conferma di consegna. Se la finestra non si riempie entro il periodo stabilito dal timer di lavoro, verrà avviato un controllo per determinare quali pacchetti non sono stati consegnati e verranno inviati richieste di reinvio.
Diagramma di reinvio:
Timeout e timer del protocollo
Esistono diverse ragioni per cui potrebbe non essere stabilita una connessione. Ad esempio, se il lato ricevente è offline. In questo caso, durante un tentativo di connessione, la connessione verrà chiusa per timeout. Nella realizzazione del Reliable UDP vengono utilizzati due timer per impostare i timeout. Il primo, il timer di lavoro, serve ad attendere una risposta dall'host remoto. Se scade sul lato del mittente, viene effettuata una ritrasmissione dell'ultimo pacchetto inviato. Se invece il timer scade sul lato del ricevente, viene effettuato un controllo sui pacchetti persi e vengono inviati richieste per la ritrasmissione.
Il secondo timer è necessario per chiudere la connessione in caso di mancanza di comunicazione tra i nodi. Per la parte mittente, viene attivato subito dopo l'attivazione del timer di lavoro e attende una risposta dal nodo remoto. Se non arriva risposta entro il periodo stabilito, la connessione viene chiusa e le risorse vengono liberate. Per la parte ricevente, il timer di chiusura della connessione si attiva dopo il doppio scatto del timer di lavoro. Questo è necessario per evitare la perdita del pacchetto di conferma. Alla scadenza del timer, la connessione viene chiusa e le risorse vengono liberate.
Diagramma di stati per la trasmissione di Reliable UDP
I principi di funzionamento del protocollo sono implementati in un automa finito, ogni stato del quale è responsabile di una logica specifica di elaborazione dei pacchetti.
Diagramma degli stati Reliable UDP:

Chiuso – in realtà non è uno stato, ma il punto di partenza e di arrivo per l'automa. Lo stato Chiuso è rappresentato da 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 senza conferma di invio, questo è l'unico stato: in esso avviene l'invio dell'intero messaggio.
SendingCycle – stato principale per la trasmissione dei pacchetti di messaggi.
Il passaggio a questo stato avviene da InvioPrimoPacchetto dopo l'invio del primo pacchetto di messaggio. È in questo stato che arrivano tutte le conferme e le richieste di retransmissione. L'uscita è possibile in due casi: nel caso di consegna riuscita del messaggio o per timeout.
FirstPacketReceived – stato iniziale per il destinatario del messaggio.
In questo stato viene verificata la correttezza dell'inizio della trasmissione, create le strutture necessarie e inviata la conferma del ricevimento del primo pacchetto.
Per un messaggio composto da un unico pacchetto e inviato senza utilizzo della conferma di ricezione – questo è l'unico stato. Dopo l'elaborazione di questo messaggio la connessione viene chiusa.
Assembling – stato principale per la ricezione dei pacchetti di messaggio.
Qui registra i pacchetti in una memoria temporanea, verifica l'assenza di perdite di pacchetti, invia conferme di ricezione del blocco di pacchetti e del messaggio intero, e invia richieste di ri-invio dei pacchetti persi. In caso di ricezione corretta dell'intero messaggio, la connessione passa allo stato Completato, altrimenti si esegue un'uscita per timeout.
Completato – chiusura della connessione in caso di ricezione corretta dell'intero messaggio.
Questo stato è necessario per la costruzione del messaggio e per il caso in cui la conferma di ricezione del messaggio sia stata persa nel tragitto verso il mittente. L'uscita da questo stato avviene per timeout, ma la connessione è considerata chiusa con successo.
Più in profondità nel codice. Blocco di controllo della trasmissione
Uno degli elementi chiave di Reliable UDP è il blocco di controllo delle trasmissioni. 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 delle trasmissioni 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:
class interna ReliableUdpConnectionControlBlock : IDisposable
{
// array di byte per la chiave specificata. Utilizzato per assemblare i messaggi in arrivo
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set; }
// array di byte per la chiave specificata. Utilizzato per inviare messaggi in uscita.
public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
// record di connessione per la chiave specificata.
private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
// lista degli iscritti ai messaggi.
private readonly List<ReliableUdpSubscribeObject> m_subscribers;
// socket locale
private Socket m_socketIn;
// porta per i messaggi in arrivo
private int m_port;
// indirizzo IP locale
private IPAddress m_ipAddress;
// punto finale locale
public IPEndPoint LocalEndpoint { get; private set; }
// collezione di stati precedentemente inizializzati
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 Receive()
{
EndPoint connectedClient = 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 connectedClient, EndReceive, buffer);
}
private void EndReceive(IAsyncResult ar)
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
// pacchetto ricevuto, pronti per ricevere il successivo
Receive();
// poiché il modo più semplice per gestire il 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 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 l'elaborazione del pacchetto nella macchina a stati
record.State.ReceivePacket(record, header, bytes);
}
Per ogni trasmissione di messaggi viene creata una struttura contenente le informazioni sulla connessione. Questa struttura è chiamata connection record.
Alcuni membri della classe ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// array of bytes with message
public byte[] IncomingStream { get; set; }
// reference to the state of the finite state machine
public ReliableUdpState State { get; set; }
// pair that uniquely identifies the connection record
// in the transmission control block
public Tuple Key { get; private set;}
// lower bound of the receive window
public int WindowLowerBound;
// size of the transmission window
public readonly int WindowSize;
// packet number to send
public int SndNext;
// number of packets to send
public int NumberOfPackets;
// transmission number (this is the second part of the Tuple)
// a different one for each message
public readonly Int32 TransmissionId;
// remote IP endpoint – the actual recipient of the message
public readonly IPEndPoint RemoteClient;
// packet size to avoid fragmentation at the IP level
// must not exceed MTU – (IP.Header + UDP.Header + ReliableUDP.Header)
public readonly int BufferSize;
// transmission control block
public readonly ReliableUdpConnectionControlBlock Tcb;
// encapsulates the results of the asynchronous operation for BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// do not send acknowledgment packets
public bool IsNoAnswerNeeded;
// last correctly received packet (always set to the highest number)
public int RcvCurrent;
// array with the numbers of lost packets
public int[] LostPackets { get; private set; }
// has the last packet arrived. Used as bool.
public int IsLastPacketReceived = 0;
//...
}
Più in profondità nel codice. Stati
Gli stati implementano una macchina a stati del protocollo Reliable UDP, in cui avviene la gestione principale dei pacchetti. La classe astratta ReliableUdpState fornisce un'interfaccia per lo stato:

Tutta la logica del protocollo è implementata dalle classi sopra menzionate, insieme a una classe ausiliaria che fornisce metodi statici, come ad esempio la costruzione dell'intestazione ReliableUdp da un record di connessione.
Di seguito verranno esaminate in dettaglio le implementazioni dei metodi dell'interfaccia che definiscono gli algoritmi principali del funzionamento del protocollo.
Metodo DisposeByTimeout
Il metodo DisposeByTimeout è responsabile del rilascio delle risorse della connessione al termine del timeout e per segnalare l'esito positivo/negativo della consegna del messaggio.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
È sovrascritto solo nello stato Completato.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// segnaliamo il ricevimento del messaggio con successo
SetAsCompleted(connectionRecord);
}
Metodo ProcessPackets
Il metodo ProcessPackets gestisce l'elaborazione aggiuntiva di un pacchetto o di più pacchetti. Viene chiamato direttamente o tramite un timer di attesa dei pacchetti.
Stato Assembling il metodo è stato sovrascritto e gestisce il controllo dei pacchetti perduti e il passaggio allo stato Completato, in caso di ricezione dell'ultimo pacchetto e superamento del controllo
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 il tentativo di invio
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// se dopo due tentativi di WaitForPacketTimer
// non sono riuscito a ricevere i pacchetti - avviamo il timer di chiusura della connessione
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// verifica riuscita
{
// inviamo conferma di ricezione del blocco di dati
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// invece di implementare istantaneamente le risorse
// avviamo il timer, nel caso in cui
// se l'ultimo ack non arrivi al mittente e lui lo richieda di nuovo.
// al suono del timer - implementiamo risorse
// nello stato di Completato il metodo del timer è sovrascritto
StartCloseWaitTimer(connectionRecord);
}
// questo è il caso in cui l'ack per un 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 SendingCycle Questo metodo viene chiamato solo dal timer e si occupa di ritrasmettere l'ultimo messaggio oltre a attivare il timer di chiusura della connessione.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
\/\/ ritrasmettiamo l'ultimo pacchetto
\/\/ (in caso di ripristino della connessione, il nodo ricevente re-invierà le richieste che non sono arrivate)
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 Completato 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 FirstPacketReceived L'obiettivo principale del metodo è determinare se il primo pacchetto del messaggio è realmente arrivato all'interfaccia e raccogliere il messaggio costituito da un singolo pacchetto.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// scartare il pacchetto
return;
// la combinazione di entrambe le bandiere - FirstPacket e LastPacket - indica che abbiamo un solo messaggio
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))
{
// inviare il pacchetto di conferma
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// per design tutti i numeri dei pacchetti iniziano da 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// calcolare il numero di pacchetti che dovrebbero arrivare
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// registrare 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;
// se non è necessario il meccanismo di conferma
// avviamo il timer che libererà tutte le strutture
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 SendingCycle 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 ricezione
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// verifica di inclusione nella finestra
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// controlla se è l'ultimo pacchetto:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// trasmissione completata
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// questo è la risposta al primo pacchetto con conferma
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// senza spostamento della 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);
}
// questo è una richiesta di ritrasmissione – inviamo il pacchetto richiesto
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Stato Assembling Nel metodo ReceivePacket avviene il lavoro principale di assemblaggio del messaggio dai pacchetti ricevuti.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// trattamento dei pacchetti con meccanismo di conferma disabilitato
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// reset del timer
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// registriamo i dati
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// se abbiamo ricevuto un pacchetto con l'ultimo flag - completare
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));
// scartare i pacchetti che non rientrano nella finestra
if (header.PacketNumber (windowHighestBound))
return;
// scartare i duplicati
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// registriamo i dati
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// incrementare il contatore dei pacchetti
connectionRecord.PacketCounter++;
// registriamo l'attuale numero di pacchetto nell'array di controllo della finestra
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// impostiamo il pacchetto più grande 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 ci sono arrivati tutti i pacchetti della finestra, resettiamo il contatore
// e inviamo il pacchetto di conferma
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resettiamo il contatore.
connectionRecord.PacketCounter = 0;
// abbiamo spostato la finestra di trasmissione
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// reset 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 Completato L'unico compito del metodo è inviare una conferma di ricezione del messaggio con esito positivo.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// Ritrasmissione 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 oppure, se il messaggio non richiede conferma di ricezione, invia 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 SendingCycle In questo metodo avviene l'invio di un blocco di pacchetti.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// inviamo il 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 );
}
}
Più in profondità nel codice. Creazione e stabilimento di connessioni
Ora che ci siamo familiarizzati con gli stati e i metodi principali utilizzati per gestire gli stati, possiamo esaminare più da vicino alcuni esempi di funzionamento del protocollo.
Diagramma di trasmissione dei dati in condizioni normali:
Esaminiamo in dettaglio la creazione connection record per la connessione e l'invio del primo pacchetto. L'iniziatore della trasmissione è sempre l'applicazione che chiama il metodo API per l'invio del messaggio. Successivamente, si utilizza il metodo StartTransmission del blocco di controllo della trasmissione, che avvia la trasmissione dei dati per un 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 il costruttore con parametri o avviare il listener prima di inviare il messaggio");
}
// avviamo la gestione dei pacchetti in arrivo
StartListener(LocalEndpoint);
}
// creiamo una chiave per il dizionario, basata su EndPoint e ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// generiamo un numero casuale transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// creiamo una nuova запись 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 – generiamo 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 di nuovo non è andato a buon fine – generiamo un'eccezione
throw new ArgumentException("La coppia TransmissionId & EndPoint esiste già nel dizionario");
}
// avviato lo stato per la gestione
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));
// Incrementiamo 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 SendingCycle – attendere la conferma della consegna del pacchetto.
La parte ricevente, utilizzando il metodo EndReceive, riceve il pacchetto inviato, crea un nuovo connection record e passa il suddetto pacchetto, con l'intestazione precedentemente analizzata, al metodo ReceivePacket nello stato FirstPacketReceived
Creazione della connessione sul lato ricevente:
private void EndReceive(IAsyncResult ar)
{
// ...
// pacchetto ricevuto
// stiamo analizzando l'intestazione del pacchetto
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// pacchetto non valido ricevuto - lo scartiamo
return;
}
// costruiamo la chiave per determinare il record di connessione per il pacchetto
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(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 nell'elaborazione nell'automa 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))
// ignoriamo il pacchetto
return;
// ...
// per design tutti i numeri di pacchetto iniziano da 0;
if (header.PacketNumber != 0)
return;
// inizializziamo l'array per memorizzare le parti del messaggio
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// scriviamo 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);
}
}
Più in profondità nel 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 su un nodo intermedio, rendendo impossibile la consegna dei dati in entrambe le direzioni.
Diagramma di chiusura della connessione per timeout:
Come si può notare dal diagramma, il timer di lavoro del mittente si attiva immediatamente dopo l'invio del blocco di pacchetti. Questo avviene nel metodo SendPacket dello stato SendingCycle.
Attivazione del timer di lavoro (stato SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// inviamo il 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 sono impostati al momento della creazione della connessione. Di default, ShortTimerPeriod è di 5 secondi. Nell'esempio è impostato a 1,5 secondi.
Nel caso di una connessione in entrata, il timer si attiva dopo la ricezione dell'ultimo pacchetto di dati arrivato, ciò avviene nel metodo ReceivePacket dello stato Assembling
Attivazione del timer di lavoro (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);
// ...
}
Nel collegamento in entrata non sono stati ricevuti più pacchetti durante il tempo di attesa del timer di lavoro. Il timer è scattato e ha richiamato il metodo ProcessPackets, nel quale sono stati rilevati pacchetti mancanti e sono state inviate le richieste di ripetizione per la prima volta.
Invio di richieste di ripetizione (stato Assemblaggio):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*controllo dei pacchetti mancanti */)
{
// inviamo richieste di ripetizione
// impostiamo il timer per la seconda volta, per un nuovo tentativo di trasferimento
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 pacchetti - avviamo il timer di chiusura della connessione
StartCloseWaitTimer(connectionRecord);
}
else if (/*è arrivato l'ultimo pacchetto e verifica riuscita */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// se ack per il blocco di pacchetti è andato perso
else
{
if (!connectionRecord.TimerSecondTry)
{
// inoltriamo 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 della riaccensione del timer di lavoro.
Anche il lato del mittente attiva il timer di lavoro e invia nuovamente l'ultimo pacchetto inviato.
Attivazione del timer di chiusura della connessione (stato SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// inviamo nuovamente l'ultimo pacchetto
// ...
// attiviamo il timer CloseWait – per attendere il ripristino o la chiusura della connessione
StartCloseWaitTimer(connectionRecord);
}
Dopo di che, nel collegamento in uscita viene avviato il timer di chiusura della connessione.
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 predefinito è di 30 secondi.
Dopo un breve momento, il timer di lavoro si attiva nuovamente sul lato del ricevente, vengono nuovamente inviati i pacchetti e successivamente si attiva il timer di chiusura della connessione sul collegamento in arrivo.
Quando si attivano i timer di chiusura, tutte le risorse di entrambi i record di connessione vengono liberate. Il mittente comunica il fallimento della consegna all'applicazione 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);
}
}
Più in profondità nel codice. Ripristino della trasmissione dati
Diagramma di recupero dei dati in caso di perdita di pacchetti:
Come già discusso nella chiusura della connessione per timeout, al termine del timer di lavoro, il destinatario eseguirà un controllo per verificare se ci sono pacchetti persi. In caso di perdita di pacchetti, verrà creato un elenco dei numeri dei pacchetti non consegnati al destinatario. Questi numeri vengono registrati nell'array LostPackets di connessione specifica e vengono inviati per richiedere una nuova consegna.
Invio di richieste per la riconsegna dei pacchetti (stato Assemblaggio):
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 consegna ripetuta e invierà i pacchetti mancanti. È importante notare che in quel momento il mittente ha già avviato un timer di chiusura della connessione e, al ricevimento della richiesta, questo viene azzerato.
Invio ripetuto dei pacchetti persi (stato SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// resetta il timer per la chiusura della connessione
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// questa è una richiesta per la ritrasmissione – inviamo il pacchetto richiesto
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Il pacchetto reinviato (packet#3 nel diagramma) viene accettato dalla connessione in entrata. Viene effettuato il controllo per il riempimento della finestra di ricezione e il normale trasferimento dei dati viene ripristinato.
Controllo per rientrare nella finestra di ricezione (stato In Assemblaggio):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// incrementiamo il contatore dei pacchetti
connectionRecord.PacketCounter++;
// registriamo nel 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 nella finestra, resettiamo il contatore
// e inviamo un pacchetto di conferma
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resettiamo il contatore.
connectionRecord.PacketCounter = 0;
// spostiamo la finestra di trasmissione
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// azzeriamo l'array di controllo della trasmissione
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Per interagire con il protocollo di trasferimento dati, è presente una classe pubblica Reliable Udp, che funge da involucro sopra il blocco di controllo della trasmissione. Ecco i membri più importanti della classe:
public sealed class ReliableUdp : IDisposable
{
// Ottiene il punto finale locale
public IPEndPoint LocalEndpoint
// Crea un'istanza di ReliableUdp e avvia
// l'ascolto dei pacchetti in entrata all'indirizzo IP specificato
// e sulla porta. Un valore di 0 per la porta significa utilizzare
// una porta assegnata 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 dei messaggi
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// Inviare un messaggio in modo asincrono
// Nota: la compatibilità con XP e Server 2003 non viene compromessa poiché viene utilizzato il .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// Inizia l'invio asincrono di un messaggio
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// Ottiene il risultato dell'invio asincrono
public bool EndSendMessage(IAsyncResult asyncResult)
// Pulisce le risorse
public void Dispose()
}
La ricezione dei messaggi 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à disattivato
// per l'invio 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
{
// Qualsiasi
Any = 0,
// Richiesta a STUN server
StunRequest = 1,
// Risposta da STUN server
StunResponse = 2,
// Trasferimento file
FileTransfer = 3,
// ...
}
L'invio del messaggio avviene in modo asincrono, per questo nel protocollo è implementato un modello di programmazione asincrono:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Il risultato dell'invio del messaggio sarà true – se il messaggio è stato consegnato con successo al 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 accordo dei flussi, la gestione delle eccezioni e degli errori, e l'implementazione dei metodi asincroni per l'invio dei messaggi. Tuttavia, il nucleo del protocollo, la logica di gestione dei pacchetti, l'instaurazione della connessione e la gestione dei timeout devono essere chiari per voi.
La versione del protocollo di consegna affidabile dimostrata è sufficientemente stabilizzante e flessibile, rispondendo ai requisiti precedentemente stabiliti. Vorrei aggiungere che l'implementazione descritta può essere migliorata. Ad esempio, per aumentare la larghezza di banda e modificare dinamicamente i periodi di timer nel protocollo, potrebbero essere aggiunti meccanismi come la finestra scorrevole e il RTT; sarebbe utile anche implementare un meccanismo per determinare l'MTU tra i nodi della connessione (ma solo nel caso di invio di grandi messaggi).
Grazie per l'attenzione, attendo i vostri 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:
Link utili e articoli
- Specifiche del protocollo TCP: e
- Specifiche del protocollo UDP: e
- Discussione sul protocollo RUDP:
- Protocollo di Dati Affidabile: e
- Una semplice implementazione della conferma di consegna tramite UDP:
- Articolo che descrive i meccanismi per superare i NAT:
- Implementazione di un modello di programmazione asincrona: e
- Trasferire il modello di programmazione asincrona in un pattern asincrono basato su task (APM in TAP):
Aggiornamento: Grazie e per l'idea di aggiungere un task all'interfaccia. La compatibilità della libreria con i sistemi operativi precedenti non è compromessa, poiché il framework 4 supporta sia XP che il server 2003.
Fonte: habr.com
