Implementacija pouzdanog Udp protokola za .Net

Internet se odavno promijenio. Jedan od glavnih protokola Interneta - UDP se koristi od strane aplikacija ne samo za isporuku datagrama i emitovanja, već i za obezbjeđivanje "peer-to-peer" veza između mrežnih čvorova. Zbog svog jednostavnog dizajna, ovaj protokol ima mnogo ranije neplaniranih upotreba, međutim, nedostaci protokola, kao što je nedostatak zagarantovane isporuke, nisu nigdje nestali. Ovaj članak opisuje implementaciju protokola zagarantovane isporuke preko UDP-a.
Sadržaj:ulazak
Zahtjevi protokola
Pouzdano UDP zaglavlje
Opšti principi protokola
Vremenska ograničenja i tajmeri protokola
Pouzdan dijagram stanja prijenosa UDP-a
Dublje u kod. upravljačka jedinica mjenjača
Dublje u kod. države

Dublje u kod. Stvaranje i uspostavljanje veza
Dublje u kod. Zatvaranje veze nakon isteka
Dublje u kod. Vraćanje prijenosa podataka
Pouzdan UDP API
zaključak
Korisni linkovi i članci

ulazak

Originalna arhitektura Interneta pretpostavljala je homogeni adresni prostor u kojem je svaki čvor imao globalnu i jedinstvenu IP adresu i mogao je komunicirati direktno sa drugim čvorovima. Sada Internet, u stvari, ima drugačiju arhitekturu – jedno područje ​​globalnih IP adresa i mnoga područja sa privatnim adresama skrivenim iza NAT uređaja.U ovoj arhitekturi, samo uređaji u globalnom adresnom prostoru mogu lako komunicirati sa bilo kim na mreži jer imaju jedinstvenu, globalno rutabilnu IP adresu. Čvor u privatnoj mreži može se povezati s drugim čvorovima na istoj mreži, a također se može povezati s drugim dobro poznatim čvorovima u globalnom adresnom prostoru. Ova interakcija se uglavnom postiže zahvaljujući mehanizmu prevođenja mrežnih adresa. NAT uređaji, kao što su Wi-Fi ruteri, kreiraju posebne unose u tablici prijevoda za odlazne veze i modificiraju IP adrese i brojeve portova u paketima. Ovo omogućava odlazne veze iz privatne mreže na hostove u globalnom adresnom prostoru. Ali u isto vrijeme, NAT uređaji obično blokiraju sav dolazni promet osim ako nisu postavljena posebna pravila za dolazne veze.

Ova arhitektura Interneta je dovoljno ispravna za komunikaciju klijent-server, gdje klijenti mogu biti u privatnim mrežama, a serveri imaju globalnu adresu. Ali to stvara poteškoće za direktnu vezu između dva čvora razne privatne mreže. Direktna veza između dva čvora važna je za peer-to-peer aplikacije kao što je prijenos glasa (Skype), dobivanje udaljenog pristupa računaru (TeamViewer) ili igranje na mreži.

Jedna od najefikasnijih metoda za uspostavljanje peer-to-peer veze između uređaja na različitim privatnim mrežama naziva se bušenje rupa. Ova tehnika se najčešće koristi sa aplikacijama zasnovanim na UDP protokolu.

Ali ako vaša aplikacija zahtijeva zagarantovanu isporuku podataka, na primjer, prenosite datoteke između računala, tada će korištenje UDP-a imati mnogo poteškoća zbog činjenice da UDP nije zajamčeni protokol isporuke i ne pruža isporuku paketa po redu, za razliku od TCP-a. protokol.

U ovom slučaju, da bi se osigurala zagarantovana isporuka paketa, potrebno je implementirati protokol sloja aplikacije koji pruža potrebnu funkcionalnost i radi preko UDP-a.

Odmah želim napomenuti da postoji tehnika probijanja TCP rupa za uspostavljanje TCP veza između čvorova u različitim privatnim mrežama, ali zbog nedostatka podrške za to od strane mnogih NAT uređaja, obično se ne smatra glavnim načinom povezivanja. takvi čvorovi.

U nastavku ovog članka fokusirat ću se samo na implementaciju protokola zajamčene isporuke. Implementacija UDP tehnike bušenja rupa bit će opisana u sljedećim člancima.

Zahtjevi protokola

  1. Pouzdana isporuka paketa implementirana kroz mehanizam pozitivne povratne informacije (tzv. pozitivna potvrda)
  2. Potreba za efikasnim prijenosom velikih podataka, tj. protokol mora izbjegavati nepotrebno prenošenje paketa
  3. Trebalo bi biti moguće otkazati mehanizam potvrde isporuke (mogućnost funkcioniranja kao "čisti" UDP protokol)
  4. Mogućnost implementacije komandnog režima, uz potvrdu svake poruke
  5. Osnovna jedinica prenosa podataka preko protokola mora biti poruka

Ovi zahtjevi se u velikoj mjeri poklapaju sa zahtjevima Protokola pouzdanih podataka opisanim u RFC 908 и RFC 1151, i oslanjao sam se na te standarde kada sam razvijao ovaj protokol.

Da bismo razumjeli ove zahtjeve, pogledajmo vrijeme prijenosa podataka između dva mrežna čvora koristeći TCP i UDP protokole. Neka u oba slučaja izgubimo jedan paket.
Prijenos neinteraktivnih podataka preko TCP-a:Implementacija pouzdanog Udp protokola za .Net

Kao što možete vidjeti iz dijagrama, u slučaju gubitka paketa, TCP će otkriti izgubljeni paket i prijaviti ga pošiljaocu tražeći broj izgubljenog segmenta.
Prijenos podataka putem UDP protokola:Implementacija pouzdanog Udp protokola za .Net

UDP ne poduzima nikakve korake za otkrivanje gubitaka. Kontrola grešaka u prijenosu u UDP protokolu je u potpunosti odgovornost aplikacije.

Otkrivanje greške u TCP protokolu se postiže uspostavljanjem veze sa krajnjim čvorom, pohranjivanjem stanja te veze, naznakom broja bajtova poslatih u zaglavlju svakog paketa i obavještavanjem o prijemu pomoću broja potvrde.

Dodatno, za poboljšanje performansi (tj. slanje više od jednog segmenta bez primanja potvrde), TCP protokol koristi takozvani prozor za prijenos - broj bajtova podataka koje pošiljatelj segmenta očekuje da primi.

Za više informacija o TCP protokolu, pogledajte RFC 793, od UDP do RFC 768gde su, u stvari, definisani.

Iz navedenog je jasno da u cilju stvaranja pouzdanog protokola za isporuku poruka preko UDP-a (u daljem tekstu: Pouzdan UDP), potrebno je implementirati mehanizme prijenosa podataka slične TCP-u. naime:

  • sačuvati stanje veze
  • koristite numeraciju segmenata
  • koristite posebne pakete potvrde
  • koristite pojednostavljeni mehanizam prozora za povećanje protoka protokola

Dodatno vam je potrebno:

  • signalizira početak poruke, da dodijeli resurse za vezu
  • signalizirati kraj poruke, da bi primljenu poruku proslijedili uzvodnoj aplikaciji i oslobodili resurse protokola
  • dozvolite protokolu specifičnom za vezu da onemogući mehanizam potvrde isporuke da funkcionira kao "čisti" UDP

Pouzdano UDP zaglavlje

Podsjetimo da je UDP datagram inkapsuliran u IP datagram. Pouzdan UDP paket je na odgovarajući način "umotan" u UDP datagram.
Pouzdana enkapsulacija UDP zaglavlja:Implementacija pouzdanog Udp protokola za .Net

Struktura Pouzdanog UDP zaglavlja je prilično jednostavna:

Implementacija pouzdanog Udp protokola za .Net

  • Zastavice - zastavice za kontrolu paketa
  • MessageType - vrsta poruke koju koriste upstream aplikacije za pretplatu na određene poruke
  • TransmissionId - broj prenosa, zajedno sa adresom i portom primaoca, jedinstveno identifikuje vezu
  • PacketNumber - broj paketa
  • Opcije - dodatne opcije protokola. U slučaju prvog paketa, koristi se za označavanje veličine poruke

Zastave su sljedeće:

  • FirstPacket - prvi paket poruke
  • NoAsk - poruka ne zahtijeva da se omogući mehanizam potvrde
  • LastPacket - posljednji paket poruke
  • RequestForPacket - paket potvrde ili zahtjev za izgubljenim paketom

Opšti principi protokola

Pošto je pouzdan UDP fokusiran na garantovani prenos poruka između dva čvora, mora biti u stanju da uspostavi vezu sa drugom stranom. Da bi uspostavio vezu, pošiljalac šalje paket sa oznakom FirstPacket, čiji će odgovor značiti da je veza uspostavljena. Svi paketi odgovora, ili, drugim riječima, paketi potvrde, uvijek postavljaju vrijednost polja PacketNumber na jednu više od najveće vrijednosti PacketNumber uspješno primljenih paketa. Polje Opcije za prvi poslani paket je veličina poruke.

Sličan mehanizam se koristi za prekid veze. LastPacket zastavica se postavlja na zadnji paket poruke. U paketu odgovora naveden je broj posljednjeg paketa + 1, što za stranu primateljicu znači uspješnu isporuku poruke.
Dijagram uspostavljanja i prekida veze:Implementacija pouzdanog Udp protokola za .Net

Kada se veza uspostavi, počinje prijenos podataka. Podaci se prenose u blokovima paketa. Svaki blok, osim posljednjeg, sadrži fiksni broj paketa. Jednaka je veličini prozora za prijem/prenos. Posljednji blok podataka može imati manje paketa. Nakon slanja svakog bloka, strana pošiljateljica čeka potvrdu isporuke ili zahtjev za ponovnom isporukom izgubljenih paketa, ostavljajući prozor za prijem/prenos otvorenim za primanje odgovora. Nakon prijema potvrde isporuke bloka, prozor za prijem/prenos se pomiče i šalje se sljedeći blok podataka.

Strana koja prima pakete prima pakete. Svaki paket se provjerava da se vidi da li spada u okvir za prijenos. Paketi i duplikati koji ne padaju u prozor se filtriraju. Jer Ako je veličina prozora fiksna i ista za primaoca i pošiljaoca, tada se u slučaju da se blok paketa isporučuje bez gubitka, prozor pomiče na prijem paketa sljedećeg bloka podataka i dobija se potvrda isporuke poslano. Ako se prozor ne popuni u roku koji je zadao radni tajmer, tada će se pokrenuti provjera koji paketi nisu isporučeni i poslat će se zahtjevi za ponovnu dostavu.
Dijagram retransmisije:Implementacija pouzdanog Udp protokola za .Net

Vremenska ograničenja i tajmeri protokola

Postoji nekoliko razloga zašto se veza ne može uspostaviti. Na primjer, ako je primatelj van mreže. U tom slučaju, kada pokušavate uspostaviti vezu, veza će biti zatvorena po isteku vremena. Implementacija Pouzdan UDP koristi dva tajmera za postavljanje vremenskih ograničenja. Prvi, radni tajmer, koristi se za čekanje odgovora udaljenog hosta. Ako se aktivira na strani pošiljaoca, posljednji poslani paket se ponovo šalje. Ako tajmer istekne kod primaoca, tada se vrši provjera izgubljenih paketa i šalju se zahtjevi za ponovnu dostavu.

Drugi tajmer je potreban za zatvaranje veze u slučaju nedostatka komunikacije između čvorova. Za stranu pošiljaoca, počinje odmah nakon što radni tajmer istekne i čeka odgovor od udaljenog čvora. Ako nema odgovora u navedenom periodu, veza se prekida i resursi se oslobađaju. Za prijemnu stranu, tajmer zatvaranja veze se pokreće nakon što radni tajmer istekne dva puta. Ovo je neophodno radi osiguranja od gubitka potvrdnog paketa. Kada tajmer istekne, veza se također prekida i resursi se oslobađaju.

Pouzdan dijagram stanja prijenosa UDP-a

Principi protokola su implementirani u konačnom automatu, čije je svako stanje odgovorno za određenu logiku obrade paketa.
Pouzdan UDP dijagram stanja:

Implementacija pouzdanog Udp protokola za .Net

Zatvoreno - nije zapravo stanje, to je početna i krajnja tačka za automat. Za državu Zatvoreno prima se kontrolni blok prijenosa koji implementirajući asinhroni UDP server prosljeđuje pakete odgovarajućim vezama i započinje obradu stanja.

FirstPacketSending – početno stanje u kojem se nalazi odlazna veza kada je poruka poslana.

U ovom stanju se šalje prvi paket za normalne poruke. Za poruke bez potvrde slanja, ovo je jedino stanje u kojem se šalje cijela poruka.

SendingCycle – osnovno stanje za prijenos paketa poruka.

Prelazak na njega iz države FirstPacketSending izvršeno nakon što je prvi paket poruke poslan. U tom stanju dolaze sva priznanja i zahtjevi za retransmisije. Izlazak iz njega moguć je u dva slučaja - u slučaju uspješne dostave poruke ili po isteku vremena.

FirstPacketReceived – početno stanje za primaoca poruke.

Provjerava ispravnost početka prijenosa, kreira potrebne strukture i šalje potvrdu o prijemu prvog paketa.

Za poruku koja se sastoji od jednog paketa i poslana je bez korištenja dokaza o isporuci, ovo je jedino stanje. Nakon obrade takve poruke, veza se prekida.

Sklapanje – osnovno stanje za prijem paketa poruka.

Zapisuje pakete u privremenu memoriju, provjerava gubitak paketa, šalje potvrde za isporuku bloka paketa i cijele poruke i šalje zahtjeve za ponovnu isporuku izgubljenih paketa. U slučaju uspješnog prijema cijele poruke, veza prelazi u stanje Završeno, u suprotnom, istekne vremensko ograničenje.

Završeno – zatvaranje veze u slučaju uspješnog prijema cijele poruke.

Ovo stanje je neophodno za sklapanje poruke i za slučaj kada je potvrda o dostavi poruke izgubljena na putu do pošiljaoca. Iz ovog stanja se izlazi nakon isteka vremena, ali se veza smatra uspješno zatvorenom.

Dublje u kod. upravljačka jedinica mjenjača

Jedan od ključnih elemenata pouzdanog UDP-a je kontrolni blok prijenosa. Zadatak ovog bloka je pohranjivanje trenutnih veza i pomoćnih elemenata, distribucija dolaznih paketa odgovarajućim vezama, obezbjeđivanje interfejsa za slanje paketa na vezu i implementacija API protokola. Kontrolni blok prijenosa prima pakete od UDP sloja i prosljeđuje ih državnom stroju na obradu. Za primanje paketa, implementira asinhroni UDP server.
Neki članovi klase 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 asinhronog UDP servera:

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 svaki prijenos poruke kreira se struktura koja sadrži informacije o vezi. Takva struktura se zove zapis veze.
Neki članovi klase 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;
  //...
}

Dublje u kod. države

Države implementiraju državnu mašinu pouzdanog UDP protokola, gdje se odvija glavna obrada paketa. Apstraktna klasa ReliableUdpState pruža interfejs za stanje:

Implementacija pouzdanog Udp protokola za .Net

Čitavu logiku protokola implementiraju gore predstavljene klase, zajedno sa pomoćnom klasom koja obezbjeđuje statičke metode, kao što je, na primjer, konstruiranje ReliableUdp zaglavlja iz zapisa veze.

Zatim ćemo detaljno razmotriti implementaciju metoda interfejsa koji određuju osnovne algoritme protokola.

DisposeByTimeout metoda

Metoda DisposeByTimeout je odgovorna za oslobađanje resursa veze nakon isteka vremena i za signaliziranje uspješne/neuspješne isporuke poruke.
ReliableUdpState.DisposeByTimeout:

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

To je samo nadjačano u državi Završeno.
Completed.DisposeByTimeout:

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

ProcessPackets metoda

Metoda ProcessPackets je odgovorna za dodatnu obradu paketa ili paketa. Poziva se direktno ili preko tajmera čekanja paketa.

U stanju Sklapanje metoda je nadjačana i odgovorna je za provjeru izgubljenih paketa i prelazak u stanje Završeno, u slučaju prijema posljednjeg paketa i prolaska uspješne provjere
Assembling.ProcessPackets:

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

U stanju SendingCycle ovaj metod se poziva samo na tajmeru i odgovoran je za ponovno slanje posljednje poruke, kao i za omogućavanje tajmera za zatvaranje veze.
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);
}

U stanju Završeno metoda zaustavlja radni tajmer i šalje poruku pretplatnicima.
Completed.ProcessPackets:

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

ReceivePacket metoda

U stanju FirstPacketReceived glavni zadatak metode je da utvrdi da li je prvi paket poruke zaista stigao na interfejs, kao i da prikupi poruku koja se sastoji od jednog 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);
  }
}

U stanju SendingCycle ovaj metod je poništen za prihvatanje potvrda isporuke i zahtjeva za ponovnim prijenosom.
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));
}

U stanju Sklapanje u metodi ReceivePacket odvija se glavni posao sastavljanja poruke od dolaznih paketa.
Assembling.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);
  }
}

U stanju Završeno jedini zadatak metode je da pošalje ponovnu potvrdu uspješne isporuke poruke.
Completed.ReceivePacket:

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

Metoda slanja paketa

U stanju FirstPacketSending ovaj metod šalje prvi paket podataka, ili, ako poruka ne zahtijeva potvrdu isporuke, cijelu poruku.
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);
}

U stanju SendingCycle u ovoj metodi se šalje blok paketa.
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 );
  }
}

Dublje u kod. Stvaranje i uspostavljanje veza

Sada kada smo vidjeli osnovna stanja i metode koje se koriste za rukovanje stanjima, razložimo nekoliko primjera kako protokol radi malo detaljnije.
Dijagram prijenosa podataka u normalnim uvjetima:Implementacija pouzdanog Udp protokola za .Net

Razmotrite detaljno stvaranje zapis veze za povezivanje i slanje prvog paketa. Prijenos uvijek inicira aplikacija koja poziva API za slanje poruke. Zatim se poziva metoda StartTransmission kontrolnog bloka prijenosa, koja započinje prijenos podataka za novu poruku.
Kreiranje odlazne veze:

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

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

Nakon slanja prvog paketa, pošiljalac ulazi u stanje SendingCycle – sačekajte potvrdu isporuke paketa.
Strana koja prima, koristeći metodu EndReceive, prima poslani paket, kreira novi zapis veze i prosljeđuje ovaj paket, s unaprijed raščlanjenim zaglavljem, metodi ReceivePacket stanja na obradu FirstPacketReceived
Kreiranje veze na prijemnoj 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);
}

Primanje prvog paketa i slanje potvrde (FirstPacketReceived stanje):

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

Dublje u kod. Zatvaranje veze nakon isteka

Rukovanje timeout-om je važan dio pouzdanog UDP-a. Razmotrimo primjer u kojem je posredni čvor pokvario i isporuka podataka u oba smjera postala je nemoguća.
Dijagram za zatvaranje veze po vremenskom ograničenju:Implementacija pouzdanog Udp protokola za .Net

Kao što se vidi iz dijagrama, pošiljateljev radni tajmer počinje odmah nakon slanja bloka paketa. Ovo se dešava u metodi SendPacket stanja SendingCycle.
Omogućavanje radnog tajmera (SendingCycle stanje):

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

Vremenski periodi se postavljaju kada se veza uspostavi. Zadani ShortTimerPeriod je 5 sekundi. U primjeru je postavljeno na 1,5 sekunde.

Za dolaznu vezu, tajmer počinje nakon prijema posljednjeg dolaznog paketa podataka, to se događa u metodi ReceivePacket stanja Sklapanje
Omogućavanje radnog tajmera (stanje sklapanja):

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

Više paketa nije stizalo na dolaznu vezu dok se čeka radni tajmer. Tajmer se isključio i pozvao metodu ProcessPackets, gdje su izgubljeni paketi pronađeni i zahtjevi za ponovnu isporuku poslani po prvi put.
Slanje zahtjeva za ponovnu isporuku (stanje sklapanja):

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

Varijabla TimerSecondTry je postavljena na istinski. Ova varijabla je odgovorna za ponovno pokretanje radnog tajmera.

Na strani pošiljaoca se takođe aktivira radni tajmer i ponovo se šalje poslednji poslani paket.
Omogućavanje tajmera zatvaranja veze (SendingCycle stanje):

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

Nakon toga, tajmer zatvaranja veze počinje u odlaznoj vezi.
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);
}

Vremensko ograničenje tajmera za zatvaranje veze je podrazumevano 30 sekundi.

Nakon kratkog vremena ponovo se aktivira radni tajmer na strani primaoca, ponovo se šalju zahtjevi, nakon čega počinje tajmer zatvaranja veze za dolaznu vezu

Kada se aktiviraju tajmeri zatvaranja, svi resursi oba zapisa veze se oslobađaju. Pošiljalac prijavljuje neuspjeh isporuke uzvodnoj aplikaciji (pogledajte Pouzdan UDP API).
Otpuštanje resursa zapisa veze:

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

Dublje u kod. Vraćanje prijenosa podataka

Dijagram oporavka prijenosa podataka u slučaju gubitka paketa:Implementacija pouzdanog Udp protokola za .Net

Kao što je već rečeno u zatvaranju veze na vremenskom ograničenju, kada radni tajmer istekne, primalac će proveriti da li postoje izgubljeni paketi. U slučaju gubitka paketa, sastavlja se lista broja paketa koji nisu stigli do primaoca. Ovi brojevi se unose u niz LostPackets određene veze i šalju se zahtjevi za ponovnu isporuku.
Slanje zahtjeva za ponovnu isporuku paketa (stanje sklapanja):

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šiljalac će prihvatiti zahtjev za ponovnu dostavu i poslati pakete koji nedostaju. Vrijedi napomenuti da je u ovom trenutku pošiljatelj već pokrenuo tajmer zatvaranja veze i, kada se primi zahtjev, on se resetuje.
Ponovno slanje izgubljenih paketa (SendingCycle stanje):

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

Ponovo poslani paket (paket #3 na dijagramu) prima dolazna veza. Provjerava se da li je prozor za prijem pun i da li se normalan prijenos podataka vraća.
Provjera pogodaka u prozoru za prijem (stanje sklapanja):

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

Pouzdan UDP API

Za interakciju s protokolom prijenosa podataka, postoji otvorena klasa Pouzdani Udp, koja je omotač preko kontrolnog bloka prijenosa. Evo najvažnijih članova klase:

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

Poruke se primaju putem pretplate. Potpis delegata za metodu povratnog poziva:

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

Poruka:

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

Za pretplatu na određeni tip poruke i/ili određenog pošiljatelja, koriste se dva opciona parametra: ReliableUdpMessageTypes messageType i IPEndPoint ipEndPoint.

Vrste poruka:

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

Poruka se šalje asinhrono; za to protokol implementira model asinhronog programiranja:

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

Rezultat slanja poruke bit će istinit - ako je poruka uspješno stigla do primaoca i lažan - ako je veza prekinuta do isteka vremena:

public bool EndSendMessage(IAsyncResult asyncResult)

zaključak

Mnogo toga nije opisano u ovom članku. Mehanizmi uparivanja niti, rukovanje izuzetcima i greškama, implementacija asinhronih metoda slanja poruka. Ali srž protokola, opis logike za obradu paketa, uspostavljanje veze i rukovanje vremenskim ograničenjima, trebao bi vam biti jasan.

Demonstrirana verzija pouzdanog protokola isporuke je dovoljno robusna i fleksibilna da ispuni prethodno definirane zahtjeve. Ali želim da dodam da se opisana implementacija može poboljšati. Na primjer, da bi se povećala propusnost i dinamički mijenjali periodi tajmera, mehanizmi kao što su klizni prozor i RTT mogu se dodati u protokol, također će biti korisno implementirati mehanizam za određivanje MTU između čvorova veze (ali samo ako se šalju velike poruke) .

Hvala vam na pažnji, radujem se vašim komentarima i komentarima.

PS Za one koje zanimaju detalji ili samo žele da testiraju protokol, link ka projektu na GitHubeu:
Pouzdan UDP projekat

Korisni linkovi i članci

  1. Specifikacija TCP protokola: na engleskom и na ruskom
  2. Specifikacija UDP protokola: na engleskom и na ruskom
  3. Diskusija o RUDP protokolu: draft-ietf-sigtran-pouzdan-udp-00
  4. Pouzdan protokol podataka: RFC 908 и RFC 1151
  5. Jednostavna implementacija potvrde isporuke preko UDP-a: Preuzmite potpunu kontrolu nad svojim umrežavanjem uz .NET i UDP
  6. Članak koji opisuje NAT mehanizme prelaska: Peer-to-Peer komunikacija preko mrežnih prevodilaca adresa
  7. Implementacija modela asinhronog programiranja: Implementacija CLR modela asinhronog programiranja и Kako implementirati IAsyncResult obrazac dizajna
  8. Portiranje modela asinhronog programiranja na asinhroni obrazac zasnovan na zadatku (APM u TAP):
    TPL i tradicionalno .NET asinkrono programiranje
    Interoperacija s drugim asinkronim obrascima i tipovima

Ažuriranje: Hvala mayorovp и sidristij za ideju dodavanja zadatka u interfejs. Kompatibilnost biblioteke sa starim operativnim sistemima nije narušena, jer Četvrti okvir podržava i XP i 4 server.

izvor: www.habr.com

Dodajte komentar