Implementacija protokola Reliable Udp za .Net

Internet se je že zdavnaj spremenil. Eden glavnih internetnih protokolov - UDP, uporabljajo aplikacije ne samo za dostavo datagramov in oddaj, temveč tudi za zagotavljanje povezav "enakovrednih" med omrežnimi vozlišči. Zaradi preproste zasnove ima ta protokol številne prej nenačrtovane uporabe, vendar pa pomanjkljivosti protokola, kot je pomanjkanje zajamčene dostave, niso nikamor izginile. Ta članek opisuje izvajanje protokola zajamčene dostave prek UDP.
Vsebina:Začetek
Zahteve protokola
Zanesljiva glava UDP
Splošna načela protokola
Časovne omejitve in časovniki protokola
Zanesljiv diagram stanja prenosa UDP
Globlje v kodo. krmilna enota menjalnika
Globlje v kodo. države

Globlje v kodo. Ustvarjanje in vzpostavljanje povezav
Globlje v kodo. Zapiranje povezave po časovni omejitvi
Globlje v kodo. Obnovitev prenosa podatkov
Zanesljiv UDP API
Zaključek
Uporabne povezave in članki

Začetek

Prvotna arhitektura interneta je predvidevala homogeni naslovni prostor, v katerem je imelo vsako vozlišče globalni in edinstveni naslov IP in je lahko neposredno komuniciralo z drugimi vozlišči. Zdaj ima internet dejansko drugačno arhitekturo - eno področje globalnih naslovov IP in veliko področij z zasebnimi naslovi, ki so skriti za napravami NAT.V tej arhitekturi lahko samo naprave v globalnem naslovnem prostoru enostavno komunicirajo s komer koli v omrežju, ker imajo edinstven naslov IP, ki ga je mogoče globalno usmerjati. Vozlišče v zasebnem omrežju se lahko poveže z drugimi vozlišči v istem omrežju in se lahko poveže tudi z drugimi dobro znanimi vozlišči v globalnem naslovnem prostoru. Ta interakcija je dosežena predvsem zaradi mehanizma prevajanja omrežnih naslovov. Naprave NAT, kot so usmerjevalniki Wi-Fi, ustvarjajo posebne vnose v prevajalski tabeli za odhodne povezave in spreminjajo naslove IP in številke vrat v paketih. To omogoča odhodne povezave iz zasebnega omrežja do gostiteljev v globalnem naslovnem prostoru. Toda hkrati naprave NAT običajno blokirajo ves dohodni promet, razen če so nastavljena posebna pravila za dohodne povezave.

Ta arhitektura interneta je dovolj pravilna za komunikacijo med odjemalcem in strežnikom, kjer so odjemalci lahko v zasebnih omrežjih, strežniki pa imajo globalni naslov. Vendar ustvarja težave pri neposredni povezavi dveh vozlišč med seboj različno zasebna omrežja. Neposredna povezava med dvema vozliščema je pomembna za aplikacije enakovrednih, kot je prenos govora (Skype), pridobivanje oddaljenega dostopa do računalnika (TeamViewer) ali spletne igre.

Ena najučinkovitejših metod za vzpostavitev povezave enakovrednih med napravami v različnih zasebnih omrežjih se imenuje luknjanje. Ta tehnika se najpogosteje uporablja pri aplikacijah, ki temeljijo na protokolu UDP.

Toda če vaša aplikacija potrebuje zajamčeno dostavo podatkov, na primer, če prenašate datoteke med računalniki, bo imela uporaba UDP veliko težav zaradi dejstva, da UDP ni protokol zajamčene dostave in ne zagotavlja dostave paketov po vrstnem redu, za razliko od TCP protokol.

V tem primeru je za zagotovitev zajamčene dostave paketov potrebno implementirati protokol aplikacijskega sloja, ki zagotavlja potrebno funkcionalnost in deluje prek UDP.

Takoj želim opozoriti, da obstaja tehnika luknjanja TCP za vzpostavljanje povezav TCP med vozlišči v različnih zasebnih omrežjih, vendar zaradi pomanjkanja podpore za to v številnih napravah NAT običajno ne velja za glavni način povezovanja takšna vozlišča.

V nadaljevanju tega članka se bom osredotočil le na izvajanje protokola zajamčene dostave. Izvedba tehnike luknjanja UDP bo opisana v naslednjih člankih.

Zahteve protokola

  1. Zanesljiva dostava paketov, ki se izvaja prek mehanizma pozitivne povratne informacije (tako imenovana pozitivna potrditev)
  2. Potreba po učinkovitem prenosu velikih podatkov, tj. protokol se mora izogibati nepotrebnemu posredovanju paketov
  3. Moralo bi biti mogoče preklicati mehanizem za potrditev dostave (zmožnost delovanja kot "čisti" protokol UDP)
  4. Možnost izvajanja ukaznega načina s potrditvijo vsakega sporočila
  5. Osnovna enota prenosa podatkov po protokolu mora biti sporočilo

Te zahteve v veliki meri sovpadajo z zahtevami protokola za zanesljive podatke, opisanimi v RFC 908 и RFC 1151, in pri razvoju tega protokola sem se zanašal na te standarde.

Da bi razumeli te zahteve, si poglejmo časovni razpored prenosa podatkov med dvema omrežnima vozliščema s protokoloma TCP in UDP. Naj bo v obeh primerih en paket izgubljen.
Prenos neinteraktivnih podatkov preko TCP:Implementacija protokola Reliable Udp za .Net

Kot lahko vidite iz diagrama, bo TCP v primeru izgube paketa zaznal izgubljeni paket in o tem poročal pošiljatelju tako, da ga bo vprašal za številko izgubljenega segmenta.
Prenos podatkov preko UDP protokola:Implementacija protokola Reliable Udp za .Net

UDP ne izvaja nobenih korakov za odkrivanje izgube. Za nadzor napak pri prenosu v protokolu UDP je v celoti odgovorna aplikacija.

Zaznavanje napak v protokolu TCP se doseže z vzpostavitvijo povezave s končnim vozliščem, shranjevanjem stanja te povezave, navedbo števila poslanih bajtov v vsaki glavi paketa in obveščanjem o prejemu s potrditveno številko.

Poleg tega za izboljšanje zmogljivosti (tj. pošiljanje več kot enega segmenta brez prejema potrditve) protokol TCP uporablja tako imenovano prenosno okno – število bajtov podatkov, ki jih pošiljatelj segmenta pričakuje, da bo prejel.

Za več informacij o protokolu TCP glejte RFC 793, od UDP do RFC 768kjer so pravzaprav opredeljeni.

Iz zgoraj navedenega je jasno, da za ustvarjanje zanesljivega protokola za dostavo sporočil preko UDP (v nadaljevanju Zanesljiv UDP), potrebno je implementirati mehanizme prenosa podatkov, podobne TCP. namreč:

  • shrani stanje povezave
  • uporabite številčenje segmentov
  • uporabite posebne potrditvene pakete
  • uporabite poenostavljen mehanizem oken za povečanje prepustnosti protokola

Poleg tega potrebujete:

  • signalizira začetek sporočila, da dodeli vire za povezavo
  • signalizira konec sporočila, da posreduje prejeto sporočilo navzgornji aplikaciji in sprosti vire protokola
  • omogoči protokolu, specifičnemu za povezavo, da onemogoči mehanizem za potrditev dostave, da deluje kot "čisti" UDP

Zanesljiva glava UDP

Spomnimo se, da je datagram UDP enkapsuliran v datagram IP. Paket Reliable UDP je ustrezno "zavit" v datagram UDP.
Zanesljivo enkapsulacijo glave UDP:Implementacija protokola Reliable Udp za .Net

Struktura glave Zanesljivo UDP je precej preprosta:

Implementacija protokola Reliable Udp za .Net

  • Zastavice - zastavice za nadzor paketa
  • MessageType – vrsta sporočila, ki jo uporabljajo zgornje aplikacije za naročanje na določena sporočila
  • TransmissionId – številka prenosa, skupaj z naslovom in vrati prejemnika, enolično identificira povezavo
  • PacketNumber - številka paketa
  • Možnosti - dodatne možnosti protokola. V primeru prvega paketa se uporablja za označevanje velikosti sporočila

Zastavice so naslednje:

  • FirstPacket - prvi paket sporočila
  • NoAsk - sporočilo ne zahteva vklopa mehanizma potrditve
  • LastPacket - zadnji paket sporočila
  • RequestForPacket - potrditveni paket ali zahteva za izgubljeni paket

Splošna načela protokola

Ker je Reliable UDP osredotočen na zajamčen prenos sporočil med dvema vozliščema, mora biti sposoben vzpostaviti povezavo z drugo stranjo. Za vzpostavitev povezave pošiljatelj pošlje paket z zastavico FirstPacket, odgovor na katerega bo pomenil, da je povezava vzpostavljena. Vsi odzivni paketi ali z drugimi besedami potrditveni paketi vedno nastavijo vrednost polja PacketNumber na eno večjo vrednost od največje vrednosti PacketNumber uspešno prejetih paketov. Polje Možnosti za prvi poslani paket je velikost sporočila.

Podoben mehanizem se uporablja za prekinitev povezave. Zastavica LastPacket je nastavljena na zadnjem paketu sporočila. V odgovornem paketu je navedena številka zadnjega paketa + 1, kar za sprejemno stran pomeni uspešno dostavo sporočila.
Diagram vzpostavitve in prekinitve povezave:Implementacija protokola Reliable Udp za .Net

Ko je povezava vzpostavljena, se začne prenos podatkov. Podatki se prenašajo v blokih paketov. Vsak blok, razen zadnjega, vsebuje določeno število paketov. Je enaka velikosti sprejemno/oddajnega okna. Zadnji blok podatkov ima morda manj paketov. Po pošiljanju vsakega bloka pošiljateljska stran čaka na potrditev dostave ali zahtevo za ponovno dostavo izgubljenih paketov, okno za sprejem/pošiljanje pa pusti odprto za sprejem odgovorov. Po prejemu potrditve dostave bloka se okno za sprejem/oddajanje premakne in pošlje se naslednji blok podatkov.

Prejemna stran sprejme pakete. Vsak paket se preveri, ali spada v okvir prenosa. Paketi in dvojniki, ki ne padejo v okno, so izločeni. Ker Če je velikost okna fiksna in enaka za prejemnika in pošiljatelja, potem se v primeru, da je blok paketov dostavljen brez izgube, okno premakne za sprejem paketov naslednjega bloka podatkov in potrdi dostavo. poslano. Če se okno ne zapolni v roku, ki ga nastavi delovni časovnik, se bo začelo preverjanje, kateri paketi niso bili dostavljeni, in poslane bodo zahteve za ponovno dostavo.
Diagram ponovnega prenosa:Implementacija protokola Reliable Udp za .Net

Časovne omejitve in časovniki protokola

Obstaja več razlogov, zakaj povezave ni mogoče vzpostaviti. Na primer, če je prejemnik brez povezave. V tem primeru, ko poskušate vzpostaviti povezavo, bo povezava prekinjena s časovno omejitvijo. Izvedba zanesljivega UDP uporablja dva časovnika za nastavitev časovnih omejitev. Prvi, delovni časovnik, se uporablja za čakanje na odgovor oddaljenega gostitelja. Če se sproži na strani pošiljatelja, se ponovno pošlje zadnji poslani paket. Če časovnik poteče pri prejemniku, se izvede preverjanje izgubljenih paketov in pošljejo zahteve za ponovno dostavo.

Drugi časovnik je potreben za prekinitev povezave v primeru pomanjkanja komunikacije med vozlišči. Za stran pošiljatelja se začne takoj po izteku delovnega časovnika in čaka na odgovor oddaljenega vozlišča. Če v določenem roku ni odgovora, se povezava prekine in sredstva se sprostijo. Za prejemno stran se časovnik za prekinitev povezave zažene po dvakratnem izteku delovnega časovnika. To je potrebno za zavarovanje pred izgubo potrditvenega paketa. Ko se časovnik izteče, se prekine tudi povezava in sredstva se sprostijo.

Zanesljiv diagram stanja prenosa UDP

Načela protokola se izvajajo v končnem avtomatu, katerega vsako stanje je odgovorno za določeno logiko obdelave paketov.
Zanesljiv diagram stanja UDP:

Implementacija protokola Reliable Udp za .Net

zaprto - v resnici ni stanje, je začetna in končna točka za avtomat. Za državo zaprto prejet je nadzorni blok prenosa, ki z implementacijo asinhronega UDP strežnika posreduje pakete ustreznim povezavam in začne procesiranje stanja.

FirstPacketSending – začetno stanje, v katerem je odhodna povezava, ko je sporočilo poslano.

V tem stanju je poslan prvi paket za običajna sporočila. Za sporočila brez potrditve pošiljanja je to edino stanje, v katerem je poslano celotno sporočilo.

SendingCycle – osnovno stanje za prenos sporočilnih paketov.

Prehod nanj iz države FirstPacketSending izvede po tem, ko je bil poslan prvi paket sporočila. V tem stanju prihajajo vse potrditve in zahteve za ponovni prenos. Izhod iz njega je možen v dveh primerih - v primeru uspešne dostave sporočila ali po časovni omejitvi.

FirstPacketReceived – začetno stanje za prejemnika sporočila.

Preveri pravilnost začetka prenosa, ustvari potrebne strukture in pošlje potrdilo o prejemu prvega paketa.

Za sporočilo, ki je sestavljeno iz enega paketa in je bilo poslano brez uporabe dokazila o dostavi, je to edino stanje. Po obdelavi takšnega sporočila se povezava prekine.

Sestavljanje – osnovno stanje za sprejem paketov sporočil.

Zapisuje pakete v začasno shrambo, preverja izgubo paketov, pošilja potrditve za dostavo bloka paketov in celotnega sporočila ter pošilja zahteve za ponovno dostavo izgubljenih paketov. V primeru uspešnega prejema celotnega sporočila povezava preide v stanje Končana, sicer se izteče časovna omejitev.

Končana – prekinitev povezave v primeru uspešnega prejema celotnega sporočila.

To stanje je potrebno za sestavo sporočila in za primer, ko se je potrdilo o dostavi sporočila izgubilo na poti do pošiljatelja. Iz tega stanja se prekine časovna omejitev, vendar se povezava šteje za uspešno zaprto.

Globlje v kodo. krmilna enota menjalnika

Eden od ključnih elementov zanesljivega UDP je blok za nadzor prenosa. Naloga tega bloka je shranjevanje trenutnih povezav in pomožnih elementov, distribucija dohodnih paketov na ustrezne povezave, zagotavljanje vmesnika za pošiljanje paketov v povezavo in implementacija protokola API. Blok za nadzor prenosa sprejema pakete iz plasti UDP in jih posreduje stroju stanja v obdelavo. Za sprejemanje paketov implementira asinhroni strežnik UDP.
Nekateri člani razreda 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;    	
  //...
}

Implementacija asinhronega strežnika UDP:

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

Za vsak prenos sporočila se ustvari struktura, ki vsebuje podatke o povezavi. Takšna struktura se imenuje zapis povezave.
Nekateri člani razreda 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;
  //...
}

Globlje v kodo. države

Države implementirajo državni stroj protokola Reliable UDP, kjer poteka glavna obdelava paketov. Abstraktni razred ReliableUdpState nudi vmesnik za stanje:

Implementacija protokola Reliable Udp za .Net

Celotno logiko protokola izvajajo zgoraj predstavljeni razredi, skupaj s pomožnim razredom, ki zagotavlja statične metode, kot je na primer izdelava glave ReliableUdp iz zapisa povezave.

Nato bomo podrobno preučili izvedbo vmesniških metod, ki določajo osnovne algoritme protokola.

Metoda DisposeByTimeout

Metoda DisposeByTimeout je odgovorna za sprostitev virov povezave po časovni omejitvi in ​​za signalizacijo uspešne/neuspešne dostave sporočila.
ReliableUdpState.DisposeByTimeout:

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

Preglasi se samo v državi Končana.
Completed.DisposeByTimeout:

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

Metoda ProcessPackets

Metoda ProcessPackets je odgovorna za dodatno obdelavo paketa ali paketov. Kliče se neposredno ali prek časovnika čakanja na paket.

V stanju Sestavljanje metoda je preglasena in je odgovorna za preverjanje izgubljenih paketov in prehod v stanje Končana, v primeru prejema zadnjega paketa in uspešnega preverjanja
Sestavljanje.ProcessPackets:

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

V stanju SendingCycle ta metoda se kliče samo na časovniku in je odgovorna za ponovno pošiljanje zadnjega sporočila ter omogočanje časovnika za prekinitev povezave.
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);
}

V stanju Končana metoda ustavi delujoči časovnik in pošlje sporočilo naročnikom.
Končano.ProcessPackets:

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

Metoda ReceivePacket

V stanju FirstPacketReceived Glavna naloga metode je ugotoviti, ali je prvi paket sporočil dejansko prispel na vmesnik, in tudi zbrati sporočilo, sestavljeno iz enega samega paketa.
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);
  }
}

V stanju SendingCycle ta metoda je preglasena za sprejemanje potrditev dostave in zahtev za ponovno pošiljanje.
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));
}

V stanju Sestavljanje v metodi ReceivePacket poteka glavno delo sestavljanja sporočila iz dohodnih paketov.
Sestavljanje.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);
  }
}

V stanju Končana edina naloga metode je poslati ponovno potrditev uspešne dostave sporočila.
Končano. ReceivePacket:

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

Metoda pošiljanja paketa

V stanju FirstPacketSending ta metoda pošlje prvi paket podatkov ali, če sporočilo ne zahteva potrditve dostave, celotno sporočilo.
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);
}

V stanju SendingCycle pri tej metodi se pošlje blok paketov.
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 );
  }
}

Globlje v kodo. Ustvarjanje in vzpostavljanje povezav

Zdaj, ko smo videli osnovna stanja in metode, ki se uporabljajo za obravnavanje stanj, razčlenimo nekaj primerov delovanja protokola nekoliko podrobneje.
Diagram prenosa podatkov v normalnih pogojih:Implementacija protokola Reliable Udp za .Net

Podrobno razmislite o ustvarjanju zapis povezave za povezavo in pošiljanje prvega paketa. Prenos vedno sproži aplikacija, ki kliče API za pošiljanje sporočil. Nato se prikliče metoda StartTransmission nadzornega bloka prenosa, ki začne prenos podatkov za novo sporočilo.
Ustvarjanje odhodne povezave:

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

Pošiljanje prvega paketa (stanje 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);
}

Po pošiljanju prvega paketa pošiljatelj preide v stanje SendingCycle – počakajte na potrditev dostave paketa.
Prejemna stran z metodo EndReceive sprejme poslani paket, ustvari novega zapis povezave in ta paket s predhodno razčlenjeno glavo posreduje metodi ReceivePacket stanja za obdelavo FirstPacketReceived
Ustvarjanje povezave na sprejemni strani:

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

Prejem prvega paketa in pošiljanje potrditve (stanje 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);
  }
}

Globlje v kodo. Zapiranje povezave po časovni omejitvi

Obravnava časovne omejitve je pomemben del zanesljivega UDP. Razmislite o primeru, v katerem je vmesno vozlišče odpovedalo in je dostava podatkov v obe smeri postala nemogoča.
Diagram za prekinitev povezave po časovni omejitvi:Implementacija protokola Reliable Udp za .Net

Kot je razvidno iz diagrama, se delovni časovnik pošiljatelja začne takoj po pošiljanju bloka paketov. To se zgodi v metodi stanja SendPacket SendingCycle.
Omogočanje delovnega časovnika (stanje SendingCycle):

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

Obdobja časovnika se nastavijo, ko se vzpostavi povezava. Privzeti ShortTimerPeriod je 5 sekund. V primeru je nastavljen na 1,5 sekunde.

Za dohodno povezavo se časovnik začne po prejemu zadnjega dohodnega podatkovnega paketa, to se zgodi v metodi ReceivePacket stanja Sestavljanje
Omogočanje delovnega časovnika (stanje sestavljanja):

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

Med čakanjem na delovni časovnik na dohodno povezavo ni prispel noben več paketov. Časovnik se je izklopil in poklical metodo ProcessPackets, kjer so bili najdeni izgubljeni paketi in prvič poslane zahteve za ponovno dostavo.
Pošiljanje zahtev za ponovno dostavo (stanje sestavljanja):

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

Spremenljivka TimerSecondTry je nastavljena na Res. Ta spremenljivka je odgovorna za ponovni zagon delovnega časovnika.

Na strani pošiljatelja se sproži tudi delujoči časovnik in ponovno pošlje zadnji poslani paket.
Omogočanje časovnika za zaprtje povezave (stanje SendingCycle):

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

Po tem se v odhodni povezavi zažene časovnik za zaprtje povezave.
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);
}

Časovna omejitev časovnika za prekinitev povezave je privzeto 30 sekund.

Po kratkem času se delovni časovnik na strani prejemnika ponovno sproži, zahteve se ponovno pošljejo, nato pa se za dohodno povezavo zažene časovnik za zaprtje povezave

Ko se sprožijo časovniki za zapiranje, se sprostijo vsi viri obeh zapisov povezave. Pošiljatelj sporoči neuspelo dostavo zgornji aplikaciji (glejte Zanesljiv API UDP).
Sprostitev virov zapisa povezave:

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

Globlje v kodo. Obnovitev prenosa podatkov

Diagram obnovitve prenosa podatkov v primeru izgube paketa:Implementacija protokola Reliable Udp za .Net

Kot smo že omenili pri zapiranju povezave ob časovni omejitvi, bo sprejemnik, ko poteče delovni časovnik, preveril izgubljene pakete. V primeru izgube paketa se sestavi seznam števila paketov, ki niso prispeli do prejemnika. Te številke se vnesejo v matriko LostPackets določene povezave in pošljejo se zahteve za ponovno dostavo.
Pošiljanje zahtev za ponovno dostavo paketov (stanje sestavljanja):

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

Pošiljatelj bo sprejel zahtevo za ponovno dostavo in poslal manjkajoče pakete. Omeniti velja, da je pošiljatelj v tem trenutku že zagnal časovnik za zaprtje povezave in se ponastavi, ko prejme zahtevo.
Ponovno pošiljanje izgubljenih paketov (stanje 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));
}

Ponovno poslani paket (paket št. 3 v diagramu) prejme vhodna povezava. Preveri se, ali je sprejemno okno polno in je normalen prenos podatkov ponovno vzpostavljen.
Preverjanje zadetkov v prejemnem oknu (stanje sestavljanja):

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

Zanesljiv UDP API

Za interakcijo s protokolom za prenos podatkov obstaja odprt razred Reliable Udp, ki je ovoj nad blokom za nadzor prenosa. Tukaj so najpomembnejši člani razreda:

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

Sporočila prejemamo po naročnini. Podpis pooblaščenca za metodo povratnega klica:

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

Sporočilo:

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

Za naročanje na določeno vrsto sporočila in/ali določenega pošiljatelja se uporabita dva neobvezna parametra: ReliableUdpMessageTypes messageType in IPEndPoint ipEndPoint.

Vrste sporočil:

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

Sporočilo je poslano asinhrono; za to protokol izvaja model asinhronega programiranja:

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

Rezultat pošiljanja sporočila bo resničen - če je sporočilo uspešno prispelo do prejemnika in false - če je bila povezava prekinjena zaradi časovne omejitve:

public bool EndSendMessage(IAsyncResult asyncResult)

Zaključek

Marsikaj v tem članku ni bilo opisanega. Mehanizmi ujemanja niti, obravnavanje izjem in napak, implementacija metod asinhronega pošiljanja sporočil. Toda jedro protokola, opis logike za obdelavo paketov, vzpostavljanje povezave in obravnavanje časovnih omejitev, bi vam moralo biti jasno.

Predstavljena različica zanesljivega protokola dostave je dovolj robustna in prilagodljiva, da izpolnjuje predhodno opredeljene zahteve. Vendar želim dodati, da je opisano izvedbo mogoče izboljšati. Na primer, za povečanje prepustnosti in dinamično spreminjanje časovnih obdobij je mogoče protokolu dodati mehanizme, kot sta drsno okno in RTT, prav tako bo koristno implementirati mehanizem za določanje MTU med povezovalnimi vozlišči (vendar le, če so poslana velika sporočila) .

Hvala za vašo pozornost, veselim se vaših komentarjev in komentarjev.

PS Za tiste, ki jih zanimajo podrobnosti ali želite samo preizkusiti protokol, povezava do projekta na GitHube:
Zanesljiv projekt UDP

Uporabne povezave in članki

  1. Specifikacija protokola TCP: v angleščini и v ruščini
  2. Specifikacija protokola UDP: v angleščini и v ruščini
  3. Razprava o protokolu RUDP: osnutek-ietf-sigtran-zanesljiv-udp-00
  4. Zanesljiv podatkovni protokol: RFC 908 и RFC 1151
  5. Preprosta izvedba potrditve dostave preko UDP: Prevzemite popoln nadzor nad svojim omrežjem z .NET in UDP
  6. Članek, ki opisuje mehanizme prečkanja NAT: Komunikacija enakovrednih med prevajalniki omrežnih naslovov
  7. Izvedba modela asinhronega programiranja: Implementacija modela asinhronega programiranja CLR и Kako implementirati načrtovalni vzorec IAsyncResult
  8. Prenos modela asinhronega programiranja v asinhroni vzorec, ki temelji na opravilih (APM v TAP):
    TPL in tradicionalno asinhrono programiranje .NET
    Interop z drugimi asinhronimi vzorci in vrstami

Posodobitev: Hvala županovp и sidristij za idejo o dodajanju naloge v vmesnik. Združljivost knjižnice s starimi operacijskimi sistemi ni kršena, ker Četrti okvir podpira tako XP kot strežnik 4.

Vir: www.habr.com

Dodaj komentar