Implementarea protocolului Reliable Udp pentru .Net

Internetul s-a schimbat cu mult timp în urmă. Unul dintre principalele protocoale ale Internetului - UDP este folosit de aplicații nu numai pentru a furniza datagrame și transmisii, ci și pentru a oferi conexiuni „peer-to-peer” între nodurile de rețea. Datorită designului său simplu, acest protocol are multe utilizări neplanificate anterior, cu toate acestea, deficiențele protocolului, precum lipsa livrării garantate, nu au dispărut nicăieri. Acest articol descrie implementarea protocolului de livrare garantată prin UDP.
Cuprins:Intrare
Cerințe de protocol
Antet UDP de încredere
Principii generale ale protocolului
Timeout-uri și cronometre de protocol
Diagrama de stare de transmisie UDP fiabilă
Mai adânc în cod. unitate de control al transmisiei
Mai adânc în cod. state

Mai adânc în cod. Crearea și stabilirea conexiunilor
Mai adânc în cod. Închiderea conexiunii la expirarea timpului
Mai adânc în cod. Restabilirea transferului de date
API UDP de încredere
Concluzie
Link-uri si articole utile

Intrare

Arhitectura originală a Internetului presupunea un spațiu de adrese omogen în care fiecare nod avea o adresă IP globală și unică și putea comunica direct cu alte noduri. Acum, Internetul, de fapt, are o arhitectură diferită - o zonă de adrese IP globale și multe zone cu adrese private ascunse în spatele dispozitivelor NAT.În această arhitectură, numai dispozitivele din spațiul global de adrese pot comunica cu ușurință cu oricine din rețea, deoarece au o adresă IP unică, rutabilă la nivel global. Un nod dintr-o rețea privată se poate conecta la alte noduri din aceeași rețea și, de asemenea, se poate conecta la alte noduri binecunoscute din spațiul global de adrese. Această interacțiune este realizată în mare parte datorită mecanismului de traducere a adresei de rețea. Dispozitivele NAT, cum ar fi routerele Wi-Fi, creează intrări speciale în tabelul de traducere pentru conexiunile de ieșire și modifică adresele IP și numerele de porturi în pachete. Acest lucru permite conexiuni de ieșire din rețeaua privată la gazde în spațiul global de adrese. Dar, în același timp, dispozitivele NAT blochează de obicei tot traficul de intrare, cu excepția cazului în care sunt stabilite reguli separate pentru conexiunile de intrare.

Această arhitectură a Internetului este suficient de corectă pentru comunicarea client-server, unde clienții pot fi în rețele private, iar serverele au o adresă globală. Dar creează dificultăți pentru conectarea directă a două noduri între variat rețele private. O conexiune directă între două noduri este importantă pentru aplicațiile peer-to-peer, cum ar fi transmisia vocală (Skype), obținerea accesului de la distanță la un computer (TeamViewer) sau jocurile online.

Una dintre cele mai eficiente metode de stabilire a unei conexiuni peer-to-peer între dispozitive din diferite rețele private se numește perforare. Această tehnică este utilizată cel mai frecvent cu aplicații bazate pe protocolul UDP.

Dar dacă aplicația dvs. necesită livrarea garantată a datelor, de exemplu, transferați fișiere între computere, atunci utilizarea UDP va avea multe dificultăți din cauza faptului că UDP nu este un protocol de livrare garantată și nu oferă livrarea pachetelor în ordine, spre deosebire de TCP protocol.

În acest caz, pentru a asigura livrarea de pachete garantată, este necesară implementarea unui protocol de nivel de aplicație care oferă funcționalitatea necesară și funcționează peste UDP.

Vreau să observ imediat că există o tehnică de perforare TCP pentru stabilirea conexiunilor TCP între noduri din diferite rețele private, dar din cauza lipsei de suport pentru aceasta de către multe dispozitive NAT, de obicei nu este considerată principala modalitate de conectare. astfel de noduri.

Pentru restul acestui articol, mă voi concentra doar pe implementarea protocolului de livrare garantată. Implementarea tehnicii de perforare UDP va fi descrisă în articolele următoare.

Cerințe de protocol

  1. Livrare fiabilă a pachetelor implementată printr-un mecanism de feedback pozitiv (așa-numita confirmare pozitivă)
  2. Necesitatea unui transfer eficient de date mari, de ex. protocolul trebuie să evite retransmiterea inutilă a pachetelor
  3. Ar trebui să fie posibilă anularea mecanismului de confirmare a livrării (capacitatea de a funcționa ca un protocol UDP "pur")
  4. Abilitatea de a implementa modul de comandă, cu confirmarea fiecărui mesaj
  5. Unitatea de bază a transferului de date prin protocol trebuie să fie un mesaj

Aceste cerințe coincid în mare măsură cu cerințele Reliable Data Protocol descrise în 908 и 1151și m-am bazat pe acele standarde când am dezvoltat acest protocol.

Pentru a înțelege aceste cerințe, să ne uităm la momentul transferului de date între două noduri de rețea folosind protocoalele TCP și UDP. În ambele cazuri vom avea un pachet pierdut.
Transfer de date non-interactive prin TCP:Implementarea protocolului Reliable Udp pentru .Net

După cum puteți vedea din diagramă, în caz de pierdere a pachetului, TCP va detecta pachetul pierdut și îl va raporta expeditorului solicitând numărul segmentului pierdut.
Transfer de date prin protocolul UDP:Implementarea protocolului Reliable Udp pentru .Net

UDP nu ia nicio măsură de detectare a pierderilor. Controlul erorilor de transmisie în protocolul UDP este în întregime responsabilitatea aplicației.

Detectarea erorilor în protocolul TCP se realizează prin stabilirea unei conexiuni cu un nod final, stocarea stării acelei conexiuni, indicarea numărului de octeți trimiși în fiecare antet de pachet și notificarea primirilor folosind un număr de confirmare.

În plus, pentru a îmbunătăți performanța (adică trimiterea a mai mult de un segment fără a primi o confirmare), protocolul TCP utilizează așa-numita fereastră de transmisie - numărul de octeți de date pe care expeditorul segmentului se așteaptă să îi primească.

Pentru mai multe informații despre protocolul TCP, consultați 793, de la UDP la 768unde, de fapt, sunt definite.

Din cele de mai sus, este clar că, pentru a crea un protocol de livrare de mesaje fiabil prin UDP (denumit în continuare UDP de încredere), este necesar să se implementeze mecanisme de transfer de date similare cu TCP. Și anume:

  • salvați starea conexiunii
  • utilizați numerotarea segmentelor
  • utilizați pachete speciale de confirmare
  • utilizați un mecanism de ferestre simplificat pentru a crește debitul protocolului

În plus, aveți nevoie de:

  • semnalează începutul unui mesaj, pentru a aloca resurse pentru conexiune
  • semnalează sfârșitul unui mesaj, pentru a transmite mesajul primit aplicației din amonte și a elibera resursele de protocol
  • permite protocolului specific conexiunii să dezactiveze mecanismul de confirmare a livrării să funcționeze ca UDP „pur”.

Antet UDP de încredere

Amintiți-vă că o datagramă UDP este încapsulată într-o datagramă IP. Pachetul Reliable UDP este „împachetat” în mod corespunzător într-o datagramă UDP.
Încapsulare fiabilă antet UDP:Implementarea protocolului Reliable Udp pentru .Net

Structura antetului Reliable UDP este destul de simplă:

Implementarea protocolului Reliable Udp pentru .Net

  • Steaguri - steaguri de control al pachetului
  • MessageType - tip de mesaj utilizat de aplicațiile din amonte pentru a se abona la anumite mesaje
  • TransmissionId - numărul transmisiei, împreună cu adresa și portul destinatarului, identifică în mod unic conexiunea
  • PacketNumber - numărul pachetului
  • Opțiuni - opțiuni suplimentare de protocol. În cazul primului pachet, acesta este folosit pentru a indica dimensiunea mesajului

Steaguri sunt după cum urmează:

  • FirstPacket - primul pachet al mesajului
  • NoAsk - mesajul nu necesită un mecanism de confirmare pentru a fi activat
  • LastPacket - ultimul pachet al mesajului
  • RequestForPacket - pachet de confirmare sau cerere pentru un pachet pierdut

Principii generale ale protocolului

Deoarece Reliable UDP se concentrează pe transmisia garantată a mesajelor între două noduri, trebuie să poată stabili o conexiune cu cealaltă parte. Pentru a stabili o conexiune, expeditorul trimite un pachet cu steag FirstPacket, răspunsul la care va însemna că conexiunea este stabilită. Toate pachetele de răspuns sau, cu alte cuvinte, pachetele de confirmare, stabilesc întotdeauna valoarea câmpului PacketNumber la unul mai mult decât cea mai mare valoare PacketNumber a pachetelor primite cu succes. Câmpul Opțiuni pentru primul pachet trimis este de dimensiunea mesajului.

Un mecanism similar este utilizat pentru a termina o conexiune. Steagul LastPacket este setat pe ultimul pachet al mesajului. În pachetul de răspuns este indicat numărul ultimului pachet + 1, ceea ce pentru partea de primire înseamnă livrarea cu succes a mesajului.
Diagrama de stabilire a conexiunii și de terminare:Implementarea protocolului Reliable Udp pentru .Net

Când 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. Este egală cu dimensiunea ferestrei de primire/transmitere. Ultimul bloc de date poate avea mai puține pachete. După trimiterea fiecărui bloc, partea expeditoare așteaptă o confirmare de livrare sau o cerere de re-livrare a pachetelor pierdute, lăsând deschisă fereastra de primire/transmitere pentru a primi răspunsuri. După primirea confirmării livrării blocului, fereastra de primire/transmitere se schimbă și următorul bloc de date este trimis.

Partea de primire primește pachetele. Fiecare pachet este verificat pentru a vedea dacă se încadrează în fereastra de transmisie. Pachetele și duplicatele care nu intră în fereastră sunt filtrate. Deoarece Dacă dimensiunea ferestrei este fixă ​​și aceeași pentru destinatar și expeditor, atunci în cazul în care un bloc de pachete este livrat fără pierderi, fereastra este deplasată pentru a primi pachete din următorul bloc de date și o confirmare de livrare este trimis. În cazul în care fereastra nu se umple în perioada stabilită de cronometrul de lucru, atunci va fi începută o verificare a pachetelor care nu au fost livrate și vor fi trimise cereri de relivrare.
Diagrama de retransmisie:Implementarea protocolului Reliable Udp pentru .Net

Timeout-uri și cronometre de protocol

Există mai multe motive pentru care o conexiune nu poate fi stabilită. De exemplu, dacă partea care primește este offline. În acest caz, când încercați să stabiliți o conexiune, conexiunea va fi închisă până la expirarea timpului. Implementarea Reliable UDP folosește două temporizatoare pentru a seta timeout-uri. Primul, temporizatorul de lucru, este folosit pentru a aștepta un răspuns de la gazda la distanță. Dacă se declanșează din partea expeditorului, atunci ultimul pachet trimis este retrimis. Dacă cronometrul expiră la destinatar, atunci se efectuează o verificare a pachetelor pierdute și se trimit cereri de re-livrare.

Al doilea cronometru este necesar pentru a închide conexiunea în cazul unei lipse de comunicare între noduri. Pentru partea expeditorului, începe imediat după expirarea temporizatorului de lucru și așteaptă un răspuns de la nodul de la distanță. Dacă nu există niciun răspuns în perioada specificată, conexiunea este întreruptă și resursele sunt eliberate. Pentru partea de recepție, temporizatorul de închidere a conexiunii este pornit după ce temporizatorul de lucru expiră de două ori. Acest lucru este necesar pentru a vă asigura împotriva pierderii pachetului de confirmare. Când cronometrul expiră, conexiunea este, de asemenea, întreruptă și resursele sunt eliberate.

Diagrama de stare de transmisie UDP fiabilă

Principiile protocolului sunt implementate într-o mașină cu stări finite, fiecare stare a cărei stare este responsabilă pentru o anumită logică a procesării pachetelor.
Diagrama de stare UDP fiabilă:

Implementarea protocolului Reliable Udp pentru .Net

Operații Închise - nu este cu adevărat o stare, este un punct de început și de sfârșit pentru automat. Pentru stat Operații Închise este primit un bloc de control al transmisiei care, implementând un server UDP asincron, transmite pachetele către conexiunile corespunzătoare și începe procesarea stării.

PrimulPachetTrimitere – starea inițială în care se află conexiunea de ieșire atunci când mesajul este trimis.

În această stare, este trimis primul pachet pentru mesajele normale. Pentru mesajele fără confirmare de trimitere, aceasta este singura stare în care este trimis întregul mesaj.

SendingCycle – starea fundamentală pentru transmiterea pachetelor de mesaje.

Trecerea la ea de la stat PrimulPachetTrimitere efectuată după ce a fost trimis primul pachet de mesaj. În această stare vin toate confirmările și cererile de retransmisii. Ieșirea din acesta este posibilă în două cazuri - în cazul livrării cu succes a mesajului sau prin timeout.

Primul pachet primit – starea inițială pentru destinatarul mesajului.

Verifică corectitudinea începutului transmisiei, creează structurile necesare și trimite o confirmare de primire a primului pachet.

Pentru un mesaj care constă dintr-un singur pachet și a fost trimis fără a utiliza dovada livrării, aceasta este singura stare. După procesarea unui astfel de mesaj, conexiunea este închisă.

Asamblare – stare de bază pentru primirea pachetelor de mesaje.

Scrie pachete în stocarea temporară, verifică dacă există pierderi de pachete, trimite confirmări pentru livrarea unui bloc de pachete și a întregului mesaj și trimite cereri de re-livrare a pachetelor pierdute. În cazul primirii cu succes a întregului mesaj, conexiunea intră în stare Terminat, în caz contrar, expiră un timeout.

Terminat – închiderea conexiunii în cazul primirii cu succes a întregului mesaj.

Această stare este necesară pentru asamblarea mesajului și pentru cazul în care confirmarea de livrare a mesajului s-a pierdut în drumul către expeditor. Această stare este ieșită de un timeout, dar conexiunea este considerată închisă cu succes.

Mai adânc în cod. unitate de control al transmisiei

Unul dintre elementele cheie ale Reliable UDP este blocul de control al transmisiei. Sarcina acestui bloc este de a stoca conexiunile curente și elementele auxiliare, de a distribui pachetele primite la conexiunile corespunzătoare, de a oferi o interfață pentru trimiterea pachetelor la o conexiune și de a implementa protocolul API. Blocul de control al transmisiei primește pachete de la nivelul UDP și le transmite către mașina de stare pentru procesare. Pentru a primi pachete, implementează un server UDP asincron.
Unii membri ai clasei ReliableUdpConnectionControlBlock:

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

Implementarea serverului UDP asincron:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

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

Pentru fiecare transfer de mesaj, se creează o structură care conține informații despre conexiune. O astfel de structură se numește înregistrarea conexiunii.
Unii membri ai clasei ReliableUdpConnectionRecord:

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

Mai adânc în cod. state

Statele implementează mașina de stări a protocolului Reliable UDP, unde are loc procesarea principală a pachetelor. Clasa abstractă ReliableUdpState oferă o interfață pentru starea:

Implementarea protocolului Reliable Udp pentru .Net

Întreaga logică a protocolului este implementată de clasele prezentate mai sus, împreună cu o clasă auxiliară care oferă metode statice, cum ar fi, de exemplu, construirea antetului ReliableUdp din înregistrarea conexiunii.

În continuare, vom analiza în detaliu implementarea metodelor de interfață care determină algoritmii de bază ai protocolului.

Metoda DisposeByTimeout

Metoda DisposeByTimeout este responsabilă pentru eliberarea resurselor de conexiune după expirarea timpului și pentru semnalizarea livrării cu succes/nereușite a mesajelor.
ReliableUdpState.DisposeByTimeout:

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

Este anulat doar în stat Terminat.
Completed.DisposeByTimeout:

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

Metoda ProcessPackets

Metoda ProcessPackets este responsabilă pentru procesarea suplimentară a unui pachet sau pachete. Apelat direct sau printr-un temporizator de așteptare de pachete.

In conditie Asamblare metoda este suprascrisă și este responsabilă pentru verificarea pachetelor pierdute și trecerea la stare Terminat, în cazul primirii ultimului pachet și trecerii cu succes a verificării
Asamblare.Pachete de proces:

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

In conditie SendingCycle această metodă este apelată numai pe un cronometru și este responsabilă pentru retrimiterea ultimului mesaj, precum și pentru activarea cronometrului de închidere a conexiunii.
SendingCycle.ProcessPackets:

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

In conditie Terminat metoda oprește cronometrul de rulare și trimite mesajul către abonați.
Completed.ProcessPackets:

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

Metoda ReceivePacket

In conditie Primul pachet primit sarcina principală a metodei este de a determina dacă primul pachet de mesaj a ajuns efectiv la interfață și, de asemenea, de a colecta un mesaj format dintr-un singur pachet.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

In conditie SendingCycle această metodă este înlocuită pentru a accepta confirmările de livrare și cererile 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;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

In conditie Asamblare în metoda ReceivePacket are loc principala activitate de asamblare a unui mesaj din pachetele primite.
Asamblare.ReceivePacket:

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

In conditie Terminat singura sarcină a metodei este să trimită o re-confirmare a livrării cu succes a mesajului.
Completed.ReceivePacket:

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

Metoda de trimitere a pachetului

In conditie PrimulPachetTrimitere această metodă trimite primul pachet de date, sau dacă mesajul nu necesită confirmare de livrare, întregul mesaj.
FirstPacketSending.SendPacket:

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

In conditie SendingCycle în această metodă, este trimis un bloc de pachete.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Mai adânc în cod. Crearea și stabilirea conexiunilor

Acum că am văzut stările de bază și metodele folosite pentru a gestiona stările, haideți să dezvăluim câteva exemple despre modul în care funcționează protocolul mai detaliat.
Diagrama transmisiei datelor în condiții normale:Implementarea protocolului Reliable Udp pentru .Net

Luați în considerare în detaliu creația înregistrarea conexiunii pentru a vă conecta și a trimite primul pachet. Transferul este întotdeauna inițiat de aplicația care apelează API-ul de trimitere a mesajului. În continuare, este invocată metoda StartTransmission a blocului de control al transmisiei, care începe transmiterea datelor pentru noul 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( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Trimiterea primului pachet (starea FirstPacketSending):

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

După trimiterea primului pachet, expeditorul intră în stare SendingCycle – așteptați confirmarea livrării coletului.
Partea de primire, folosind metoda EndReceive, primește pachetul trimis, creează un nou înregistrarea conexiunii și transmite acest pachet, cu un antet analizat în prealabil, metodei ReceivePacket a stării pentru procesare Primul pachet primit
Crearea unei conexiuni pe partea de recepție:

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

Primirea primului pachet și trimiterea unei confirmări (starea FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Mai adânc în cod. Închiderea conexiunii la expirarea timpului

Gestionarea timeout-ului este o parte importantă a Reliable UDP. Luați în considerare un exemplu în care un nod intermediar a eșuat și livrarea datelor în ambele direcții a devenit imposibilă.
Diagrama pentru închiderea unei conexiuni prin timeout:Implementarea protocolului Reliable Udp pentru .Net

După cum se poate observa din diagramă, cronometrul de lucru al expeditorului începe imediat după trimiterea unui bloc de pachete. Acest lucru se întâmplă în metoda SendPacket a statului SendingCycle.
Activarea temporizatorului de lucru (starea SendingCycle):

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

Perioadele temporizatorului sunt setate la crearea conexiunii. ShortTimerPeriod implicit este de 5 secunde. În exemplu, este setat la 1,5 secunde.

Pentru o conexiune de intrare, cronometrul pornește după primirea ultimului pachet de date primit, acest lucru se întâmplă în metoda ReceivePacket a statului Asamblare
Activarea temporizatorului de lucru (starea de asamblare):

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

Nu mai au mai sosit pachete pe conexiunea de intrare în timp ce aștepta cronometrul de lucru. Cronometrul s-a oprit și a apelat metoda ProcessPackets, unde pachetele pierdute au fost găsite și au fost trimise pentru prima dată cererile de relivrare.
Trimiterea cererilor de relivrare (starea asamblare):

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

Variabila TimerSecondTry este setată la adevărat. Această variabilă este responsabilă pentru repornirea temporizatorului de lucru.

Din partea expeditorului, se declanșează și cronometrul de lucru, iar ultimul pachet trimis este retrimis.
Activarea temporizatorului de închidere a conexiunii (starea SendingCycle):

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

După aceea, temporizatorul de închidere a conexiunii începe î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 expirare a temporizatorului de închidere a conexiunii este implicit de 30 de secunde.

După un timp scurt, cronometrul de lucru din partea destinatarului se declanșează din nou, solicitările sunt trimise din nou, după care pornește temporizatorul de închidere a conexiunii pentru conexiunea de intrare.

Când se declanșează cronometrele de închidere, toate resursele ambelor înregistrări de conexiune sunt eliberate. Expeditorul raportează eșecul de livrare către aplicația din amonte (consultați API-ul UDP de încredere).
Eliberarea resurselor de înregistrare a conexiunii:

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

Mai adânc în cod. Restabilirea transferului de date

Diagrama de recuperare a transmisiei de date în caz de pierdere a pachetului:Implementarea protocolului Reliable Udp pentru .Net

După cum sa discutat deja în închiderea conexiunii la expirarea timpului, când expiră temporizatorul de lucru, receptorul va verifica dacă există pachete pierdute. În cazul pierderii pachetelor, se va întocmi o listă cu numărul de pachete care nu au ajuns la destinatar. Aceste numere sunt introduse în matricea LostPackets a unei anumite conexiuni și sunt trimise cereri de relivrare.
Trimiterea cererilor de relivrare a pachetelor (starea de asamblare):

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

Expeditorul va accepta cererea de relivrare și va trimite pachetele lipsă. Este de remarcat faptul că în acest moment expeditorul a pornit deja cronometrul de închidere a conexiunii și, atunci când se primește o solicitare, acesta este resetat.
Retrimiterea pachetelor pierdute (starea SendingCycle):

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

Pachetul retrimis (pachetul #3 din diagramă) este primit de conexiunea de intrare. Se face o verificare pentru a vedea dacă fereastra de primire este plină și transmisia normală a datelor este restabilită.
Verificarea rezultatelor în fereastra de primire (starea de asamblare):

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

API UDP de încredere

Pentru a interacționa cu protocolul de transfer de date, există o clasă deschisă Reliable Udp, care este un înveliș peste blocul de control al transferului. Iată cei mai importanți membri ai clasei:

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

Mesajele se primesc prin abonament. Semnătură delegată pentru metoda de apel invers:

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

Mesaj:

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

Pentru a vă abona la un anumit tip de mesaj și/sau un anumit expeditor, sunt utilizați doi parametri opționali: ReliableUdpMessageTypes messageType și IPEndPoint ipEndPoint.

Tipuri de mesaje:

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

Mesajul este trimis asincron; pentru aceasta, protocolul implementează un model de programare asincron:

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

Rezultatul trimiterii unui mesaj va fi adevărat - dacă mesajul a ajuns cu succes la destinatar și fals - dacă conexiunea a fost închisă până la expirare:

public bool EndSendMessage(IAsyncResult asyncResult)

Concluzie

Nu au fost descrise multe în acest articol. Mecanisme de potrivire a firelor, gestionarea excepțiilor și erorilor, implementarea metodelor de trimitere a mesajelor asincrone. Dar nucleul protocolului, descrierea logicii pentru procesarea pachetelor, stabilirea unei conexiuni și gestionarea timeout-urilor, ar trebui să vă fie clare.

Versiunea demonstrată a protocolului de livrare fiabil este suficient de robustă și flexibilă pentru a îndeplini cerințele definite anterior. Dar vreau să adaug că implementarea descrisă poate fi îmbunătățită. De exemplu, pentru a crește debitul și a schimba dinamic perioadele de cronometru, mecanisme precum fereastra glisante și RTT pot fi adăugate la protocol, va fi, de asemenea, utilă implementarea unui mecanism pentru determinarea MTU între nodurile de conexiune (dar numai dacă sunt trimise mesaje mari). ).

Vă mulțumesc pentru atenție, aștept comentariile și comentariile voastre.

PS Pentru cei care sunt interesați de detalii sau doresc doar să testeze protocolul, linkul către proiect pe GitHube:
Proiect UDP de încredere

Link-uri si articole utile

  1. Specificația protocolului TCP: на английском и in rusa
  2. Specificația protocolului UDP: на английском и in rusa
  3. Discuție despre protocolul RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protocol de date de încredere: 908 и 1151
  5. O implementare simplă a confirmării livrării prin UDP: Preluați controlul total asupra rețelei dvs. cu .NET și UDP
  6. Articolul care descrie mecanismele de traversare a NAT: Comunicare peer-to-peer prin traducătorii de adrese de rețea
  7. Implementarea modelului de programare asincronă: Implementarea modelului de programare asincronă CLR и Cum se implementează modelul de proiectare IAsyncResult
  8. Portarea modelului de programare asincronă la modelul asincron bazat pe sarcini (APM în TAP):
    Programare asincronă TPL și .NET tradițională
    Interoperabilitate cu alte modele și tipuri asincrone

Actualizare: Mulțumesc mayorovp и sidristij pentru ideea de a adăuga o sarcină la interfață. Compatibilitatea bibliotecii cu vechile sisteme de operare nu este încălcată, deoarece Al 4-lea cadru acceptă atât serverul XP, cât și 2003.

Sursa: www.habr.com

Adauga un comentariu