Internetul s-a schimbat mult de-a lungul timpului. Unul dintre principalele protocoale ale Internetului, UDP, este folosit de aplicații nu doar pentru livrarea datagramelor și a difuzărilor, ci și pentru a asigura conexiuni „peer-to-peer” între nodurile rețelei. Datorită structurii sale simple, acest protocol a găsit numeroase aplicații neprevăzute anterior, deși dezavantajele protocolului, precum lipsa livrării garantate, nu au dispărut. În acest articol se descrie implementarea unui protocol de livrare garantată pe UDP.
Cuprins:
Introducere
Arhitectura inițială a Internetului presupunea un spațiu de adresare uniform, în care fiecare nod avea o adresă IP globală și unică, putând comunica direct cu alte noduri. Acum, Internetul, de fapt, are o altă arhitectură – o zonă de adrese IP globale și multe zone cu adrese private, ascunse în spatele dispozitivelor NAT.Într-o astfel de arhitectură, doar dispozitivele aflate în spațiul de adrese globale pot interacționa cu ușurință cu cineva din rețea, deoarece au o adresă IP unică, rutabilă global. Un nod aflat într-o rețea privată poate comunica cu alte noduri din aceeași rețea, precum și cu alte noduri bine cunoscute din spațiul de adrese globale. Această interacțiune este realizată în mare măsură datorită mecanismului de traducere a adreselor de rețea. Dispozitivele NAT, de exemplu, routerele Wi-Fi, creează înregistrări speciale în tabelele de traducere pentru conexiunile de ieșire și modifică adresele IP și numerele de porturi din pachete. Acest lucru permite stabilirea unei conexiuni de ieșire dintr-o rețea privată cu noduri din spațiul de adrese globale. Dar, în același timp, dispozitivele NAT blochează, de obicei, tot traficul de intrare, dacă nu sunt stabilite reguli separate pentru conexiunile de intrare.
Această arhitectură a Internetului este suficient de corectă pentru interacțiunea client-server, atunci când clienții pot fi în rețele private, iar serverele au o adresă globală. Însă aceasta creează dificultăți pentru conexiunea directă între două noduri în diverse rețele private. diferite rețele private. Conexiunea directă între două noduri este importantă pentru aplicațiile „peer-to-peer”, cum ar fi transmiterea vocii (Skype), accesarea remote a computerului (TeamViewer) sau jocurile online.
Una dintre cele mai eficiente metode pentru stabilirea unei conexiuni peer-to-peer între dispozitive aflate în diverse rețele private se numește „hole punching”. Această tehnică este utilizată cel mai frecvent cu aplicații bazate pe protocolul UDP.
Dar, dacă aplicația dumneavoastră necesită livrarea garantată a datelor, de exemplu, dacă transmiteți fișiere între computere, utilizarea UDP va aduce multe dificultăți, având în vedere că UDP nu este un protocol de livrare garantată și nu asigură livrarea pachetelor în ordinea corectă, spre deosebire de protocolul TCP.
În acest caz, pentru a asigura livrarea garantată a pachetelor, este necesară implementarea unui protocol la nivelul aplicației, care să ofere funcționalitatea necesară și să funcționeze peste UDP.
Vreau să menționez de la bun început că există tehnica TCP hole punching, pentru stabilirea conexiunilor TCP între noduri din rețele private diferite, dar din cauza lipsei de suport de către multe dispozitive NAT, aceasta nu este de obicei considerată o metodă principală de conectare a acestor noduri.
În continuare, în acest articol, voi analiza doar implementarea protocolului de livrare garantată. Implementarea tehnicii UDP hole punching va fi descrisă în articolele următoare.
Cerințele pentru protocol
- Livrarea fiabilă a pachetelor, realizată prin mecanismul de confirmare pozitivă (așa-numitul positive acknowledgment)
- Necesitatea transferului eficient de date mari, adică protocolul trebuie să evite retransmiterea inutilă a pachetelor
- Trebuie să existe posibilitatea de a anula mecanismul de confirmare a livrării (posibilitatea de a funcționa ca un protocol UDP „curat”)
- Posibilitatea implementării modului de comanda, cu confirmarea fiecărui mesaj
- Unitatea de bază pentru transferul de date prin protocol ar trebui să fie un mesaj
Aceste cerințe se suprapun în mare măsură cu cerințele pentru Protocolul de Date Fiabile, descris în și , și m-am bazat pe aceste standarde în dezvoltarea acestui protocol.
Pentru a înțelege aceste cerințe, să examinăm diagramele temporale pentru transferul de date între două noduri din rețea prin protocoalele TCP și UDP. Să presupunem că în ambele cazuri am pierdut un pachet.
Transferul de date neinteractive prin TCP:
După cum se poate observa din diagramă, în cazul pierderii pachetelor, TCP va detecta pachetul pierdut și va informa expeditorul, cerând numărul segmentului pierdut.
Transferul de date prin protocolul UDP:
UDP nu face nicio încercare de a detecta pierderile. Controlul erorilor în protocolul UDP este complet în sarcina aplicației.
Detectarea erorilor în protocolul TCP se realizează prin stabilirea unei conexiuni cu nodul de destinație, păstrarea stării acestei conexiuni, indicarea numărului de bytes expediat în fiecare antet de pachet, și notificările de recepție prin intermediul numărului de confirmare „acknowledge number”.
În plus, pentru a îmbunătăți performanța (adică trimiterea mai multor segmente fără a primi confirmări), protocolul TCP utilizează așa-numitul fereastră de transfer — numărul de bytes de date pe care expeditorul segmentului se așteaptă să le primească.
Pentru mai multe detalii despre protocolul TCP, se poate consulta , cu UDP în , unde, de fapt, sunt definite.
Din cele de mai sus, este clar că pentru a crea un protocol de livrare a mesajelor fiabil deasupra UDP (pe care îl vom numi în continuare Reliable UDP), este necesar să implementăm mecanisme de transfer de date asemănătoare cu cele din TCP. Anume:
- menținerea stării conexiunii
- utilizarea numerotării segmentelor
- folosirea pachetelor speciale de confirmare
- utilizarea unui mecanism simplificat al feronței, pentru a crește capacitatea de transfer a protocolului
În plus, este necesar:
- să semnalizăm începutul mesajului, pentru a rezerva resurse pentru conexiune
- să semnalizăm sfârșitul mesajului, pentru a trimite mesajul primit aplicației superioare și a elibera resursele protocolului
- să permitem protocolului, pentru conexiuni specifice, să dezactiveze mecanismul de confirmare a livrării, pentru a funcționa ca un UDP „curat”
Antetul Reliable UDP
Să ne amintim că un pachet UDP este încapsulat într-un pachet IP. Pachetul Reliable UDP este, prin urmare, „împachetat” într-un pachet UDP.
Încapsularea antetului Reliable UDP:
Structura antetului Reliable UDP este destul de simplă:

- Flags – steaguri de control ale pachetului
- MessageType – tipul mesajului, utilizat de aplicațiile superioare pentru a se abona la anumite mesaje
- TransmissionId — numărul de transmisie, împreună cu adresa și portul destinatarului, determină în mod unic conexiunea
- PacketNumber – numărul pachetului
- Options – opțiuni suplimentare ale protocolului. În cazul primului pachet, este folosit pentru a indica dimensiunea mesajului
Steagurile sunt următoarele:
- FirstPacket — primul pachet al mesajului
- NoAsk — mesajul nu necesită activarea mecanismului de confirmare
- LastPacket — ultimul pachet al mesajului
- RequestForPacket — pachet de confirmare sau solicitarea unui pachet pierdut
Principiile generale de funcționare ale protocolului
Deoarece Reliable UDP este orientat pe livrarea garantată a mesajului între două noduri, acesta trebuie să fie capabil să stabilească o conexiune cu partea cealaltă. Pentru a stabili conexiunea, partea expeditor trimite un pachet cu steagul FirstPacket, răspunsul la care va semnifica stabilirea conexiunii. Toate pachetele de răspuns, sau, altfel spus, pachetele de confirmare, setează întotdeauna câmpul PacketNumber la o unitate mai mare decât cea mai mare valoare PacketNumber din pachetele care au ajuns cu succes. În câmpul Options pentru primul pachet trimis se înregistrează dimensiunea mesajului.
Pentru a finaliza conexiunea, se folosește un mecanism similar. În ultimul pachet de mesaje se setează un flag LastPacket. În pachetul de răspuns se indică numărul ultimului pachet + 1, ceea ce înseamnă pentru partea receptoare livrarea cu succes a mesajului.
Diagrama stabilirii și încheierii conexiunii:
Odată ce conexiunea este stabilită, începe transferul de date. Datele sunt transmise în blocuri de pachete. Fiecare bloc, cu excepția ultimului, conține un număr fix de pachete. Acesta este egal cu dimensiunea feroniei de recepție/transmisie. Ultimul bloc de date poate avea un număr mai mic de pachete. După trimiterea fiecărui bloc, partea care trimite așteaptă o confirmare a livrării sau o solicitare de retransmitere a pachetelor pierdute, lăsând fereastra deschisă pentru a primi răspunsuri. După primirea confirmării livrării blocului, fereastra de recepție/transmisie se deplasează și se trimite următorul bloc de date.
Partea receptor primește pachetele. Fiecare pachet este verificat pentru a se încadra în fereastra de transmisie. Pachetele care nu se încadrează în fereastră și duplicatele sunt filtrate. Având în vedere că dimensiunea feroniei este strict fixă și identică pentru receptor și expeditor, în cazul livrării unui bloc de pachete fără pierderi, fereastra se deplasează pentru a primi pachetele următorului bloc de date și se trimite o confirmare a livrării. Dacă fereastra nu se umple în perioada stabilită de un timer de lucru, se va lansa o verificare pentru a vedea ce pachete nu au fost livrate și se vor trimite solicitări de retransmitere.
Diagrama retransmiterii:
Timpii de așteptare și temporizatoarele protocolului
Există mai multe motive pentru care conexiunea nu poate fi stabilită. De exemplu, dacă partea receptoare este offline. În acest caz, atunci când se încearcă stabilirea unei conexiuni, aceasta va fi închisă din cauza timeout-ului. În implementarea Reliable UDP se utilizează doi timere pentru stabilirea timeout-urilor. Primul, timerul de lucru, este destinat așteptării unui răspuns de la gazda remote. Dacă acesta expiră de partea expeditorului, se va efectua retransmiterea ultimului pachet trimis. Dacă timerul expiră la receptor, se va efectua o verificare a pachetelor pierdute și se vor trimite solicitări de retransmitere.
Al doilea temporizator este necesar pentru a închide conexiunea în cazul lipsei comunicării între noduri. Pentru partea-emisă, acesta se activează imediat după declanșarea temporizatorului de lucru și așteaptă un răspuns de la nodul îndepărtat. În cazul absenței răspunsului în perioada stabilită, conexiunea se finalizează și resursele sunt eliberate. Pentru partea-recepționistă, temporizatorul de închidere a conexiunii se activează după dublarea declanșării temporizatorului de lucru. Acest lucru este necesar pentru a asigura protecția împotriva pierderii pachetului de confirmare. La declanșarea temporizatorului, conexiunea se finalizează și resursele sunt, de asemenea, eliberate.
Diagrama stărilor de transmisie a Reliable UDP
Principiile de funcționare ale protocolului sunt implementate într-un automat finit, fiecare stare a cărui logică de procesare a pachetelor este specifică.
Diagrama stărilor Reliable UDP:

Închis – în realitate nu este o stare, ci un punct de început și sfârșit pentru automat. Starea Închis este acceptată ca un bloc de control al transmisiei, care, implementând un server UDP asincron, redirecționează pachetele către conexiunile corespunzătoare și inițiază procesarea stărilor.
PrimulPachetTrimis – starea inițială în care se află conexiunea de ieșire la trimiterea unui mesaj.
În această stare se trimite primul pachet pentru mesajele obișnuite. Pentru mesajele fără confirmarea trimiterii, aceasta este singura stare – în care se trimite întregul mesaj.
CiclulDeTransmitere – starea principală pentru transmiterea pachetelor mesajului.
Trecerea în această stare din starea PrimulPachetTrimis se realizează după trimiterea primului pachet al mesajului. Tocmai în această stare ajung toate confirmările și cererile de retransmitere. Ieșirea din ea este posibilă în două cazuri – în cazul livrării cu succes a mesajului sau din cauza expirării timpului.
PrimulPachetRecepționat – starea inițială pentru receptorul mesajului.
În aceasta se verifică corectitudinea începutului transmisiei, se creează structurile necesare și se trimite confirmarea primirii primului pachet.
Pentru un mesaj format dintr-un singur pachet și trimis fără a folosi confirmarea livrării – aceasta este singura stare. După procesarea unui astfel de mesaj, conexiunea se închide.
Asamblare – starea principală pentru primirea pachetelor mesajului.
În aceasta se produce înregistrarea pachetelor într-un depozit temporar, verificarea absenței pierderilor de pachete, trimiterea de confirmări de livrare a blocului de pachete și a mesajului în întregime, și trimiterea de solicitări pentru re-expedierea pachetelor pierdute. În cazul în care întreaga mesaj este primită cu succes, conexiunea trece în starea Completed, altfel se efectuează ieșirea din cauza expirării timpului.
Completed – închiderea conexiunii în cazul în care întreaga mesaj a fost primită cu succes.
Această stare este necesară pentru a construi mesajul și pentru cazul în care confirmarea livrării mesajului a fost pierdută pe parcurs către expeditor. Ieșirea din această stare se face în urma expirării timpului, dar conexiunea este considerată închisă cu succes.
În profunzime în cod. Modul de control al transmisiei
Unul dintre elementele cheie ale Reliable UDP este blocul de control al transmisiei. Sarcina acestui bloc este să păstreze conexiunile curente și elementele auxiliare, să distribuie pachetele primite către conexiunile corespunzătoare, să ofere o interfață pentru trimiterea pachetelor către o conexiune și să implementeze API-ul protocolului. Blocul de control al transmisiei primește pachete de la nivelul UDP și le redirecționează pentru procesare într-o mașină de stări. Pentru a primi pachete, acesta implementează un server UDP asincron.
Unii membri ai clasei ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// array de bytes pentru cheie specificată. Utilizat pentru a construi mesajele primite
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// array de bytes pentru cheie specificată. Utilizat pentru a trimite mesajele de ieșire.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// înregistrare de conexiune pentru cheia specificată.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// listă de abonați la mesaje.
private readonly List m_subscribers;
// socket local
private Socket m_socketIn;
// port pentru mesajele primite
private int m_port;
// adresă IP locală
private IPAddress m_ipAddress;
// punct final local
public IPEndPoint LocalEndpoint { get; private set; }
// colecție de stări pre-initializate
// ale mașinii de stări
public StatesCollection States { get; private set; }
// generator de numere aleatorii. Utilizat pentru a crea TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementarea unui server UDP asincron:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// creăm un nou buffer pentru fiecare socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// transmitem buffer-ul ca parametru pentru metoda asincronă
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);
// pachetul a fost primit, pregătiți-vă să acceptați următorul
Receive();
// deoarece cea mai simplă modalitate de a rezolva problema cu bufferul - este să obțineți o referință la el
// din IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// obținem antetul pachetului
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// a venit un pachet incorect - îl ignorăm
return;
}
// construim cheia pentru a determina record-ul de conexiune pentru pachet
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// obținem un record de conexiune existent sau creăm unul nou
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// lansăm pachetul pentru prelucrare în automatul de stare
record.State.ReceivePacket(record, header, bytes);
}
Pentru fiecare transmitere a unui mesaj se creează o structură care conține informații despre conexiune. Această structură se numește connection record.
Unii membri ai clasei ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord : IDisposable
{
// array of bytes with the 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 boundary of the receiving 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 Tuple)
// one for each message
public readonly Int32 TransmissionId;
// remote IP endpoint – the actual recipient of the message
public readonly IPEndPoint RemoteClient;
// size of the packet 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 largest number)
public int RcvCurrent;
// array of lost packet numbers
public int[] LostPackets { get; private set; }
// has the last packet been received. Used as bool.
public int IsLastPacketReceived = 0;
//...
}
În profunzime în cod. Stările
States implement the finite state machine of the Reliable UDP protocol, where the main packet processing occurs. The abstract class ReliableUdpState provides the interface for state:

All the protocol logic is implemented by the classes presented above, together with a helper class that provides static methods, such as building the ReliableUdp header from the connection record.
Next, the implementations of the interface methods defining the main algorithms of the protocol will be examined in detail.
Metoda DisposeByTimeout
The DisposeByTimeout method is responsible for releasing the connection's resources after the timeout and signaling successful/unsuccessful message delivery.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
It is overridden only in the state Completed.
Completed.DisposeByTimeout:
protejat override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// notificăm despre primirea cu succes a mesajului
SetAsCompleted(connectionRecord);
}
Metoda ProcessPackets
Metoda ProcessPackets se ocupă de procesarea suplimentară a pachetului sau pachetelor. Este apelată direct sau printr-un temporizator de așteptare a pachetelor.
în stare Asamblare metoda este suprascrisă și se ocupă cu verificarea pachetelor pierdute și trecerea în starea Completed, în cazul primirii ultimului pachet și a validării cu succes
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// există pachete pierdute, trimitem solicitări pentru acestea
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// setăm temporizatorul pentru a doua încercare de transmitere
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// dacă după două încercări ale WaitForPacketTimer
// nu am reușit să primim pachetele - lansăm temporizatorul de închidere a conexiunii
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// validare reușită
{
// trimitem confirmarea primirii blocului de date
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// în loc de implementarea imediată a resurselor
// lansăm temporizatorul, în caz că
// dacă ultimul ack nu ajunge la expeditor și acesta îl solicită din nou.
// la activarea temporizatorului - implementăm resursele
// în starea Completed metoda temporizatorului este suprascrisă
StartCloseWaitTimer(connectionRecord);
}
// aceasta este o situație în care ack-ul pentru blocul de pachete a fost pierdut
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// lansăm temporizatorul de închidere a conexiunii
StartCloseWaitTimer(connectionRecord);
}
}
în stare CiclulDeTransmitere această metodă este apelată doar prin temporizator și se ocupă cu retransmiterea ultimului mesaj, precum și cu activarea temporizatorului de închidere a conexiunii.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// retransmit the last packet
// (in case of connection recovery, the receiver node will resend requests that did not reach it)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// start the CloseWait timer – to wait for the connection to be restored or terminated
StartCloseWaitTimer(connectionRecord);
}
în stare Completed the method stops the working timer and sends a message to subscribers.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// gather the message and pass it to subscribers
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Metoda ReceivePacket
în stare PrimulPachetRecepționat the main task of the method is to determine whether the first packet of the message has indeed arrived at the interface, as well as to assemble a message composed of a single packet.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// ignorăm pachetul
return;
// combinația a două steaguri - FirstPacket și LastPacket - indică că avem un singur mesaj
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))
{
// trimitem pachetul de confirmare
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// prin design, toate numerele de pachete încep de la 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// numărăm pachetele care ar trebui să vină
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
// înregistrăm numărul ultimului pachet primit (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// după deplasare o fereastră de primire cu 1
connectionRecord.WindowLowerBound++;
// schimbăm starea
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// dacă nu este necesar mecanismul de confirmare
// pornim un temporizator care va elibera toate structurile
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);
}
}
în stare CiclulDeTransmitere Această metodă este suprascrisă pentru a primi confirmările de livrare și solicitările de retransmisie.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// calculul limitei superioare a feronului
// se ia limita feronului + 1, pentru a primi confirmările de livrare
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// verificare pentru a se încadra în feron
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// verificăm dacă este ultimul pachet:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// transferul s-a încheiat
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// acest răspuns este pentru primul pachet cu confirmare
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// fără deplasarea feronului
SendPacket(connectionRecord);
}
// a venit confirmarea pentru primirea blocului de date
else if (header.PacketNumber == windowHighestBound)
{
// deplasăm feronul de recepție/tranzacție
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// resetăm array-ul de control al transmisiei
connectionRecord.WindowControlArray.Nullify();
// trimitem blocul de pachete
SendPacket(connectionRecord);
}
// aceasta este o cerere pentru retransmisie – trimitem pachetul solicitat
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
în stare Asamblare În metoda ReceivePacket se desfășoară principală activitate de asamblare a mesajului din pachetele primite.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// procesarea pachetelor fără mecanism de confirmare a livrării
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// resetăm temporizatorul
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// scriem datele
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// dacă am primit un pachet cu ultima marcă - finalizăm
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// calculăm limita finală a feronului
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// eliminăm pachetele care nu se încadrează în feron
if (header.PacketNumber (windowHighestBound))
return;
// eliminăm duplicatele
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// scriem datele
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// incrementăm contorul de pachete
connectionRecord.PacketCounter++;
// înregistrăm numărul curent al pachetului în matricea de control a feronului
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// stabilim cel mai mare pachet primit
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// repornim temporizatoarele
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// dacă ultimul pachet a fost primit
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// dacă am primit toate pachetele din feron, resetăm contorul
// și trimitem pachet de confirmare
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resetăm contorul.
connectionRecord.PacketCounter = 0;
// am mutat feronul de transmisie
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// resetarea matricei de control a transmisiei
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// dacă ultimul pachet este deja primit
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// verificăm pachetele
ProcessPackets(connectionRecord);
}
}
în stare Completed scopul unic al metodei este de a trimite o confirmare repetată a livrării cu succes a mesajului.
Finalizat.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// retransmission of the last packet due to the fact that,
// the last ack did not reach the sender
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Metoda SendPacket
în stare PrimulPachetTrimis acest metod se ocupă cu trimiterea primului pachet de date sau, dacă mesajul nu necesită confirmarea livrării - întregul mesaj.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// dacă nu este necesară confirmarea - trimitem toate pachetele
// și eliberăm resursele
if (connectionRecord.IsNoAnswerNeeded)
{
// Aici se efectuează trimiterea As Is
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// creăm header-ul pachetului și îl trimitem
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// incrementăm contorul
connectionRecord.SndNext++;
// deplasăm fereastra
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Pornim temporizatorul
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
în stare CiclulDeTransmitere în acest metod are loc trimiterea unui bloc de pachete.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// trimiterea unui bloc de pachete
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++;
}
// în cazul unei feronțe mari de transfer, repornim temporizatorul după trimitere
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
În profunzime în cod. Crearea și stabilirea conexiunilor
Acum, când ne-am familiarizat cu principalele stări și metode utilizate pentru gestionarea acestora, putem analiza mai detaliat câteva exemple de funcționare a protocolului.
Diagrama transmitere de date în condiții normale:
Să analizăm în detaliu crearea connection record pentru a conecta și a trimite primul pachet. Inițiatorul transferului este întotdeauna aplicația care apelează metodă API pentru trimiterea mesajului. Apoi se folosește metoda StartTransmission din blocul de control al transferului, care lansează transferul de date pentru un nou mesaj.
Crearea unei conexiuni de ieșire:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Trebuie să folosești constructorul cu parametri sau să începi ascultătorul înainte de a trimite un mesaj" );
}
// pornim prelucrarea pachetelor de intrare
StartListener(LocalEndpoint);
}
// creăm o cheie pentru dicționar, bazată pe EndPoint și ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// generăm un număr aleator pentru transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// creăm un nou înregistrare pentru conexiune și verificăm,
// dacă există deja un astfel de număr în dicționarele noastre
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// dacă există – atunci generăm din nou un număr aleator
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// dacă din nou nu a reușit – generăm o excepție
throw new ArgumentException("Perechea TransmissionId & EndPoint există deja în dicționar");
}
// am lansat starea în procesare
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Trimiterea primului pachet (starea FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// creăm antetul pachetului și îl trimitem
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// incrementăm contorul
connectionRecord.SndNext++;
// mutăm fereastra
connectionRecord.WindowLowerBound++;
// trecem la starea SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Pornim timerul
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
După trimiterea primului pachet, expeditorul trece în starea CiclulDeTransmitere – așteptând confirmarea livrării pachetului.
Partea receptor, folosind metoda EndReceive, primește pachetul trimis, creează un nou connection record și transmite acest pachet, cu antetul analizat anterior, în procesare către metoda ReceivePacket a stării PrimulPachetRecepționat
Crearea conexiunii pe partea de primire:
private void EndReceive(IAsyncResult ar)
{
\/\/ ...
\/\/ pachet primit
\/\/ parsăm antetul pachetului
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
\/\/ pachet incorect - îl ignorăm
return;
}
\/\/ construim cheia pentru a determina înregistrarea conexiunii pentru pachet
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
\/\/ obținem înregistrarea conexiunii existente sau creăm una nouă
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
\/\/ lansăm pachetul în procesare în automata finală
record.State.ReceivePacket(record, header, bytes);
}
Primirea primului pachet și trimiterea confirmării (starea FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
\/\/ ignorăm pachetul
return;
\/\/ ...
\/\/ prin design, toate numerele pachetului încep de la 0;
if (header.PacketNumber != 0)
return;
\/\/ inițializăm un array pentru a stoca părțile mesajului
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
\/\/ scriem datele pachetului în array
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
\/\/ numărăm pachetele care ar trebui să vină
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length\/(double) connectionRecord.BufferSize));
\/\/ scriem numărul ultimului pachet primit (0)
connectionRecord.RcvCurrent = header.PacketNumber;
\/\/ după, am mutat fereastra de recepție cu 1
connectionRecord.WindowLowerBound++;
\/\/ comutăm starea
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (\/*dacă nu este necesar mecanismul de confirmare*\)
\/\/ ...
else
{
\/\/ trimitem confirmarea
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
În profunzime în cod. Închiderea conexiunii după expirarea timpului
Gestionarea timeout-urilor este o parte importantă a Reliable UDP. Să luăm un exemplu în care un nod intermediar a eșuat și livrarea datelor în ambele direcții a devenit imposibilă.
Diagrama închiderii conexiunii din cauza timeout-ului:
După cum se poate observa din diagramă, timerul de lucru al expeditorului se activează imediat după trimiterea blocului de pachete. Aceasta se întâmplă în metoda SendPacket în starea CiclulDeTransmitere.
Activarea timer-ului de lucru (starea SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ trimitem blocul de pachete
\/\/ ...
\/\/ restartăm timer-ul după trimitere
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Perioadele temporizatorului sunt stabilite la crearea conexiunii. În mod implicit, ShortTimerPeriod este de 5 secunde. În exemplu, este setat la 1,5 secunde.
Pentru conexiunea de intrare, temporizatorul pornește după primirea ultimei pachete de date, aceasta se întâmplă în metoda ReceivePacket a stării Asamblare
Activarea temporizatorului de lucru (starea Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
\/\/ reinitializăm temporizatoarele
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
}
În conexiunea de intrare, în perioada de așteptare a temporizatorului de lucru nu au sosit alte pachete. Temporizatorul a fost activat și a apelat metoda ProcessPackets, în care au fost identificate pachete pierdute și au fost trimise pentru prima dată cereri de retransmitere.
Trimiterea cererilor de retransmitere (starea Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ ...
if (\/*verificare pentru pachete pierdute *\)
{
\/\/ trimitem cereri de retransmitere
\/\/ stabilim temporizatorul pentru a doua oară, pentru o încercare de retransmitere
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\/\/ dacă după două încercări ale WaitForPacketTimer
\/\/ nu am reușit să primim pachete - activăm temporizatorul de închidere a conexiunii
StartCloseWaitTimer(connectionRecord);
}
else if (\/*ultima pachet a sosit și verificarea a fost de success *\)
{
\/\/ ...
StartCloseWaitTimer(connectionRecord);
}
\/\/ dacă ack pentru blocul de pachete a fost pierdut
else
{
if (!connectionRecord.TimerSecondTry)
{
\/\/ retrimitem ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\/\/ activăm temporizatorul de închidere a conexiunii
StartCloseWaitTimer(connectionRecord);
}
}
Variabila TimerSecondTry a fost setată la true. Această variabilă este responsabilă pentru reluarea temporizatorului de lucru.
Din partea expeditorului, de asemenea, se activează temporizatorul de lucru și se retrimite ultimul pachet trimis.
Activarea temporizatorului de închidere a conexiunii (starea SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ ...
\/\/ retrimitem ultimul pachet
\/\/ ...
\/\/ activăm temporizatorul CloseWait – pentru a aștepta refacerea conexiunii sau închiderea acesteia
StartCloseWaitTimer(connectionRecord);
}
Apoi, un cronometru de închidere a conexiunii este inițiat în conexiunea de ieșire.
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);
}
Perioada de așteptare a cronometru de închidere a conexiunii este de 30 de secunde în mod implicit.
După o scurtă perioadă de timp, cronometru de lucru de pe partea receptorului se declanșează din nou, se fac noi cereri de trimitere, după care se inițiază cronometru de închidere a conexiunii în conexiunea de intrare.
La declanșarea cronometrelor de închidere, toate resursele ambelor înregistrări de conexiune sunt eliberate. Expeditorul notifică despre livrarea eșuată aplicației părinți (vezi API Reliable UDP).
Eliberarea resurselor înregistrării de conexiune:
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);
}
}
În profunzime în cod. Recuperarea transmisiei de date
Diagrama de recuperare a transmisiei de date în caz de pierdere a pachetului:
Așa cum a fost discutat în închiderea conexiunii prin timeout, după expirarea cronometru de lucru, receptorul va verifica pierderile de pachete. Dacă există pierderi de pachete, va fi întocmită o listă cu numerele pachetelor care nu au ajuns la receptor. Aceste numere sunt adăugate în matricea LostPackets a conexiunii specifice și se face trimiterea cererilor pentru retransmitere.
Trimiterea cererilor pentru retransmiterea pachetelor (starea Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
//...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// există pachete pierdute, se trimit cereri pentru ele
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ...
}
}
Expeditorul va primi cererea de retransmitere și va trimite pachetele lipsă. Este de remarcat că în acest moment expeditorul a inițiat deja cronometru de închidere a conexiunii, iar, la primirea cererii, acesta este resetat.
Retransmiterea pachetelor pierdute (starea SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// reset the connection close timer
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// this is a retransmission request – we send the required packet
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Pachetul retransmis (packet#3 pe diagramă) este acceptat de conexiunea de intrare. Se verifică umplerea feronelui de recepție și transferul normal de date este restabilit.
Verificarea feronelui de recepție (starea Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// incrementăm contorul de pachete
connectionRecord.PacketCounter++;
// înregistrăm în tablou controlul feronelui numărul curent al pachetului
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// stabilim cel mai recent pachet recepționat
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// repornim timerele
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// dacă am primit toate pachetele feronelului, resetăm contorul
// și trimitem un pachet de confirmare
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resetăm contorul.
connectionRecord.PacketCounter = 0;
// am mutat feronelul de transmisie
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// resetarea tabloului de control al transferului
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Pentru interacțiunea cu protocolul de transfer de date, există o clasă deschisă Reliable Udp, care este un wrapper peste blocul de control al transferului. Iată cei mai importanți membri ai clasei:
public sealed class ReliableUdp : IDisposable
{
// obține punctul final local
public IPEndPoint LocalEndpoint
// creează o instanță ReliableUdp și pornește
// ascultarea pachetelor primite pe adresa IP specificată
// și port. Valoarea 0 pentru port înseamnă utilizarea
// unui port alocat dinamic
public ReliableUdp(IPAddress localAddress, int port = 0)
// abonare pentru a primi mesaje
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// dezabonare de la primirea mesajelor
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// trimite mesaj în mod asincron
// Nota: compatibilitatea cu XP și Server 2003 nu este afectată, deoarece se folosește .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// începe trimiterea asincronă a mesajului
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// obține rezultatul trimiterii asincrone
public bool EndSendMessage(IAsyncResult asyncResult)
// curăță resursele
public void Dispose()
}
Recepția mesajului se face prin abonament. Semnătura delegatului pentru metoda de apel invers:
public delegate void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Mesaj:
public class ReliableUdpMessage
{
// tipul mesajului, enumerare simplă
public ReliableUdpMessageTypes Type { get; private set; }
// datele mesajului
public byte[] Body { get; private set; }
// dacă este setat pe true – mecanismul de confirmare a livrării va fi dezactivat
// pentru transmiterea unui mesaj specific
public bool NoAsk { get; private set; }
}
Pentru a se abona la un tip specific de mesaje și/sau la un expedient specific, se folosesc două parametri opționali: ReliableUdpMessageTypes messageType și IPEndPoint ipEndPoint.
Tipurile de mesaje:
public enum ReliableUdpMessageTypes : short
{
// Orice
Any = 0,
// Cerere către serverul STUN
StunRequest = 1,
// Răspuns de la serverul STUN
StunResponse = 2,
// Transmitere fișier
FileTransfer =3,
// ...
}
Transmiterea mesajului se realizează în mod asincron, pentru aceasta protocolul implementează un model de programare asincron:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Rezultatul trimiterii mesajului va fi true – dacă mesajul a ajuns cu succes la destinatar și false – dacă conexiunea a fost închisă din cauza expirării timpului:
public bool EndSendMessage(IAsyncResult asyncResult)
Concluzie
Multe nu au fost descrise în cadrul acestui articol. Mecanismele de negociere a fluxurilor, gestionarea excepțiilor și erorilor, implementarea metodelor asincrone de trimitere a mesajelor. Cu toate acestea, nucleul protocolului, descrierea logicii de procesare a pachetelor, stabilirea conexiunii și gestionarea time-out-urilor trebuie să fie clarificate pentru dumneavoastră.
Versiunea demonstrată a protocolului de livrare fiabilă este suficient de stabilă și flexibilă și corespunde cerințelor definite anterior. Dar vreau să adaug că implementarea descrisă poate fi îmbunătățită. De exemplu, pentru a crește lățimea de bandă și a schimba dinamic perioada cronometrelor, protocolul poate include mecanisme precum fereastra glisantă și RTT, iar implementarea unui mecanism de determinare a MTU între nodurile de conexiune va fi, de asemenea, utilă (dar doar în cazul trimiterii mesajelor mari).
Vă mulțumesc pentru atenție, aștept comentariile și observațiile dumneavoastră.
P.S. Pentru cei interesați de detalii sau care doresc să testeze protocolul, aici este linkul către proiectul de pe GitHub:
Linkuri utile și articole
- Specificația protocolului TCP: și
- Specificația protocolului UDP: și
- Discuția protocolului RUDP:
- Reliable Data Protocol: și
- O implementare simplă a confirmării livrării prin UDP:
- Articol care descrie mecanismele de depășire a NAT-urilor:
- Implementarea modelului de programare asincron: și
- Transferul modelului de programare asincron în șablonul asincron bazat pe sarcini (APM în TAP):
Actualizare: Mulțumesc și pentru ideea de a adăuga un task la interfață. Compatibilitatea bibliotecii cu sistemele de operare mai vechi nu este afectată, deoarece cadrul 4 suportă atât XP cât și serverul 2003.
Sursa: habr.com
