Implementacija Reliable Udp protokola za .Net

Internet se davno promijenio. Jedan od glavnih internetskih protokola - UDP koriste aplikacije ne samo za isporuku datagrama i emitiranja, već i za pružanje "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 zajamčene isporuke, nisu nigdje nestali. Ovaj članak opisuje implementaciju protokola zajamčene isporuke preko UDP-a.
Sadržaj:Ulazak
Zahtjevi protokola
Pouzdano UDP zaglavlje
Opća načela protokola
Istek vremena i mjerači vremena protokola
Pouzdan UDP dijagram stanja prijenosa
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 vremena
Dublje u kod. Obnavljanje prijenosa podataka
Pouzdan UDP API
Zaključak
Korisni linkovi i članci

Ulazak

Izvorna arhitektura Interneta pretpostavljala je homogeni adresni prostor u kojem je svaki čvor imao globalnu i jedinstvenu IP adresu i mogao izravno komunicirati s drugim čvorovima. Sada Internet, zapravo, ima drugačiju arhitekturu - jedno područje ​globalnih IP adresa i mnogo područja s privatnim adresama skrivenim iza NAT uređaja.U ovoj arhitekturi samo uređaji u globalnom adresnom prostoru mogu lako komunicirati s bilo kim na mreži jer imaju jedinstvenu IP adresu koja se globalno može usmjeravati. Čvor na 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žne adrese. NAT uređaji, poput Wi-Fi usmjerivača, stvaraju posebne unose tablice prijevoda za odlazne veze i mijenjaju IP adrese i brojeve portova u paketima. To omogućuje odlazne veze s 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.

Ovakva arhitektura Interneta dovoljno je ispravna za komunikaciju klijent-poslužitelj, pri čemu klijenti mogu biti u privatnim mrežama, a poslužitelji imaju globalnu adresu. Ali to stvara poteškoće za izravnu vezu između dvaju čvorova razne privatne mreže. Izravna veza između dva čvora važna je za peer-to-peer aplikacije kao što je prijenos glasa (Skype), dobivanje udaljenog pristupa računalu (TeamViewer) ili igranje na mreži.

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

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

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

Želim odmah napomenuti da postoji tehnika bušenja TCP rupa za uspostavljanje TCP veza između čvorova u različitim privatnim mrežama, ali zbog nedostatka podrške za nju na mnogim NAT uređajima, obično se ne smatra glavnim načinom povezivanja takvi čvorovi.

U ostatku ovog članka usredotočit ć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 dostava paketa implementirana putem mehanizma pozitivne povratne sprege (tzv. pozitivna potvrda)
  2. Potreba za učinkovitim prijenosom velikih podataka, tj. protokol bi trebao izbjegavati nepotrebne prijenose paketa
  3. Trebalo bi biti moguće poništiti mehanizam potvrde isporuke (mogućnost funkcioniranja kao "čisti" UDP protokol)
  4. Mogućnost implementacije naredbenog načina, uz potvrdu svake poruke
  5. Osnovna jedinica prijenosa podataka preko protokola mora biti poruka

Ovi zahtjevi uvelike se podudaraju sa zahtjevima Reliable Data Protocola opisanim u RFC 908 и RFC 1151, i oslonio sam se na te standarde kada sam razvijao ovaj protokol.

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

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

UDP ne poduzima nikakve korake otkrivanja gubitka. Kontrola grešaka u prijenosu u UDP protokolu u potpunosti je odgovornost aplikacije.

Otkrivanje pogrešaka u TCP protokolu postiže se uspostavljanjem veze s krajnjim čvorom, pohranjivanjem stanja te veze, naznakom broja poslanih bajtova u svakom zaglavlju paketa i obavještavanjem o primitku pomoću broja potvrde.

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

Za više informacija o TCP protokolu pogledajte RFC 793, od UDP do RFC 768gdje su zapravo definirani.

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

  • spremi stanje veze
  • koristiti numeriranje segmenata
  • koristiti posebne pakete potvrde
  • koristite pojednostavljeni mehanizam otvaranja prozora za povećanje protoka protokola

Dodatno, trebate:

  • signalizirati početak poruke, za dodjelu resursa za vezu
  • signalizira kraj poruke, kako bi proslijedio primljenu poruku uzvodnoj aplikaciji i oslobodio resurse protokola
  • dopustite protokolu specifičnom za vezu da onemogući mehanizam potvrde isporuke da funkcionira kao "čisti" UDP

Pouzdano UDP zaglavlje

Podsjetimo se da je UDP datagram enkapsuliran u IP datagram. Pouzdan UDP paket je na odgovarajući način "zamotan" u UDP datagram.
Pouzdana enkapsulacija UDP zaglavlja:Implementacija Reliable Udp protokola za .Net

Struktura Pouzdanog UDP zaglavlja prilično je jednostavna:

Implementacija Reliable Udp protokola za .Net

  • Zastavice - zastavice za kontrolu paketa
  • MessageType - vrsta poruke koju koriste uzvodne aplikacije za pretplatu na određene poruke
  • TransmissionId - broj prijenosa, zajedno s adresom i portom primatelja, jedinstveno identificira 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 uključen mehanizam potvrde
  • LastPacket - posljednji paket poruke
  • RequestForPacket - potvrdni paket ili zahtjev za izgubljeni paket

Opća načela protokola

Budući da je Reliable UDP fokusiran na zajamčeni prijenos poruka između dva čvora, mora biti u mogućnosti uspostaviti vezu s drugom stranom. Da bi uspostavio vezu, pošiljatelj šalje paket s 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 jedan više od najveće vrijednosti PacketNumber uspješno primljenih paketa. Polje Opcije za prvi poslani paket je veličina poruke.

Sličan mehanizam koristi se za prekid veze. Oznaka LastPacket postavljena je na zadnji paket poruke. U paketu odgovora naznačen je broj zadnjeg paketa + 1, što za primateljsku stranu znači uspješnu isporuku poruke.
Dijagram uspostavljanja i prekida veze:Implementacija Reliable Udp protokola za .Net

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

Primajuća strana prima pakete. Svaki paket se provjerava da bi se vidjelo da li ulazi u okvir prijenosa. Paketi i duplikati koji ne uđu u prozor se filtriraju. Jer Ako je veličina prozora fiksna i ista za primatelja i pošiljatelja, tada se u slučaju da se blok paketa isporučuje bez gubitka, prozor pomiče za primanje paketa sljedećeg bloka podataka i potvrda isporuke je poslao. Ako se prozor ne popuni u roku koji je postavio mjerač vremena, pokrenut će se provjera koji paketi nisu isporučeni i poslat će se zahtjevi za ponovnom dostavom.
Dijagram retransmisije:Implementacija Reliable Udp protokola za .Net

Istek vremena i mjerači vremena protokola

Postoji nekoliko razloga zašto se veza ne može uspostaviti. Na primjer, ako je primatelj izvan mreže. U tom slučaju, prilikom pokušaja uspostavljanja veze, veza će se zatvoriti istekom vremena. Implementacija pouzdanog UDP-a koristi dva mjerača vremena za postavljanje vremenskih ograničenja. Prvi, radni mjerač vremena, koristi se za čekanje odgovora od udaljenog računala. Ako se aktivira na strani pošiljatelja, tada se ponovno šalje posljednji poslani paket. Ako timer istekne kod primatelja, tada se vrši provjera izgubljenih paketa i šalju se zahtjevi za ponovnom dostavom.

Drugi mjerač vremena je potreban za zatvaranje veze u slučaju nedostatka komunikacije između čvorova. Za stranu pošiljatelja, počinje odmah nakon isteka radnog vremena i čeka odgovor od udaljenog čvora. Ako nema odgovora u navedenom roku, veza se prekida i resursi se oslobađaju. Za prijemnu stranu, mjerač vremena zatvaranja veze pokreće se nakon što radno vrijeme istekne dva puta. Ovo je neophodno kako bi se osigurao gubitak potvrdnog paketa. Kada timer istekne, veza se također prekida i resursi se oslobađaju.

Pouzdan UDP dijagram stanja prijenosa

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

Implementacija Reliable Udp protokola za .Net

Zatvoreno - zapravo nije stanje, to je početna i krajnja točka za automat. Za državu Zatvoreno prima se kontrolni blok prijenosa koji, implementirajući asinkroni UDP poslužitelj, prosljeđuje pakete na odgovarajuće veze i započinje obradu stanja.

FirstPacketSending – početno stanje u kojem je 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.

Ciklus slanja – osnovno stanje za prijenos paketa poruka.

Prijelaz na njega iz države FirstPacketSending provodi se nakon što je poslan prvi paket poruke. U tom stanju dolaze sve potvrde i zahtjevi za ponovni prijenos. Izlaz iz njega moguć je u dva slučaja - u slučaju uspješne dostave poruke ili po isteku vremena.

FirstPacketReceived – početno stanje za primatelja poruke.

Provjerava ispravnost početka prijenosa, kreira potrebne strukture i šalje potvrdu o primitku 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.

sastavljanje – osnovno stanje za primanje paketa poruka.

Zapisuje pakete u privremenu pohranu, provjerava gubitak paketa, šalje potvrde za isporuku bloka paketa i cijele poruke te šalje zahtjeve za ponovnu isporuku izgubljenih paketa. U slučaju uspješnog prijema cijele poruke, veza prelazi u stanje Dovršen, u protivnom, isteklo je vrijeme čekanja.

Dovršen – prekid veze u slučaju uspješnog prijema cijele poruke.

Ovo stanje je neophodno za sastavljanje poruke i za slučaj kada je potvrda o isporuci poruke izgubljena na putu do pošiljatelja. Ovo stanje se izlazi nakon vremenskog ograničenja, 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 pohraniti trenutne veze i pomoćne elemente, distribuirati dolazne pakete odgovarajućim vezama, osigurati sučelje za slanje paketa na vezu i implementirati API protokola. Kontrolni blok prijenosa prima pakete s UDP sloja i prosljeđuje ih stroju stanja na obradu. Za primanje paketa implementira asinkroni UDP poslužitelj.
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 asinkronog UDP poslužitelja:

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 podatke o vezi. Takva se struktura naziva 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 State Machine Reliable UDP protokola, gdje se odvija glavna obrada paketa. Apstraktna klasa ReliableUdpState pruža sučelje za stanje:

Implementacija Reliable Udp protokola za .Net

Cjelokupna logika protokola implementirana je gore predstavljenim klasama, zajedno s pomoćnom klasom koja pruža statičke metode, kao što je, na primjer, konstruiranje ReliableUdp zaglavlja iz zapisa veze.

Zatim ćemo detaljno razmotriti implementaciju metoda sučelja koje određuju osnovne algoritme protokola.

Metoda DisposeByTimeout

Metoda DisposeByTimeout odgovorna je za oslobađanje resursa veze nakon vremenskog ograničenja 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();
}

Nadilazi se samo u državi Dovršen.
Dovršeno. DisposeByTimeout:

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

Metoda ProcessPackets

Metoda ProcessPackets odgovorna je za dodatnu obradu paketa ili paketa. Poziva se izravno ili putem mjerača vremena čekanja paketa.

U stanju sastavljanje metoda je nadjačana i odgovorna je za provjeru izgubljenih paketa i prijelaz u stanje Dovršen, u slučaju primitka zadnjeg paketa i uspješnog prolaska provjere
Sastavljanje.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 Ciklus slanja ova se metoda poziva samo na mjeraču vremena i odgovorna je za ponovno slanje zadnje poruke, kao i za omogućavanje mjerača vremena zatvaranja 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 Dovršen metoda zaustavlja aktivni mjerač vremena i šalje poruku pretplatnicima.
Dovršeno.ProcessPackets:

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

Metoda ReceivePacket

U stanju FirstPacketReceived glavni zadatak metode je utvrditi je li prvi paket poruke stvarno stigao na sučelje, te također prikupiti 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 Ciklus slanja ova metoda je nadjačana za prihvaćanje potvrda isporuke i zahtjeva za ponovnim slanjem.
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 sastavljanje u metodi ReceivePacket odvija se glavni posao sastavljanja poruke od pristiglih paketa.
Sastavljanje.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 Dovršen jedina zadaća metode je slanje ponovne potvrde o uspješnoj isporuci poruke.
Završeno. Primi paket:

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 ova metoda š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 Ciklus slanja u ovoj metodi šalje se 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 korištene za rukovanje stanjima, raščlanimo nekoliko primjera kako protokol radi s malo više detalja.
Dijagram prijenosa podataka u normalnim uvjetima:Implementacija Reliable Udp protokola za .Net

Razmotrite detaljno stvaranje zapis veze za povezivanje i slanje prvog paketa. Prijenos uvijek pokreće aplikacija koja poziva API za slanje poruka. Zatim se poziva metoda StartTransmission kontrolnog bloka prijenosa, koja započinje prijenos podataka za novu poruku.
Stvorite odlaznu vezu:

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šiljatelj ulazi u stanje Ciklus slanja – pričekajte potvrdu isporuke paketa.
Primateljska strana metodom EndReceive prima poslani paket, kreira novi zapis veze i prosljeđuje ovaj paket, s unaprijed raščlanjenim zaglavljem, metodi ReceivePacket stanja za obradu FirstPacketReceived
Stvaranje 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 (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);
  }
}

Dublje u kod. Zatvaranje veze nakon isteka vremena

Rukovanje vremenskim ograničenjem važan je dio pouzdanog UDP-a. Razmotrite primjer u kojem je međučvor zatajio i isporuka podataka u oba smjera postala je nemoguća.
Dijagram zatvaranja veze prema isteku vremena:Implementacija Reliable Udp protokola za .Net

Kao što se može vidjeti iz dijagrama, tajmer rada pošiljatelja počinje odmah nakon slanja bloka paketa. To se događa u SendPacket metodi stanja Ciklus slanja.
Omogućavanje radnog mjerača vremena (Stanje ciklusa slanja):

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

Vremenska razdoblja se postavljaju kada se uspostavi veza. Zadani ShortTimerPeriod je 5 sekundi. U primjeru je postavljeno na 1,5 sekundi.

Za dolaznu vezu, mjerač vremena počinje nakon primitka zadnjeg dolaznog paketa podataka, to se događa u metodi ReceivePacket stanja sastavljanje
Omogućavanje radnog vremena (stanje sastavljanja):

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 nije stigao nijedan paket na dolaznu vezu dok se čeka radni mjerač vremena. Tajmer se ugasio i pozvao metodu ProcessPackets, gdje su pronađeni izgubljeni paketi i prvi put poslani zahtjevi za ponovnom isporukom.
Slanje zahtjeva za ponovnom dostavom (stanje sastavljanja):

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 postavljena je na istinski. Ova varijabla je odgovorna za ponovno pokretanje radnog mjerača vremena.

Na strani pošiljatelja također se aktivira radni mjerač vremena i ponovno se šalje posljednji poslani paket.
Omogućavanje mjerača vremena zatvaranja veze (stanje Ciklusa slanja):

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

Nakon toga, mjerač vremena 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);
}

Razdoblje vremenskog ograničenja zatvaranja veze je prema zadanim postavkama 30 sekundi.

Nakon kratkog vremena radni mjerač vremena na primateljevoj strani ponovno se aktivira, ponovno se šalju zahtjevi, nakon čega za dolaznu vezu počinje mjerač vremena zatvaranja veze

Kada se pokreću mjerači vremena zatvaranja, oslobađaju se svi resursi oba zapisa veze. Pošiljatelj prijavljuje neuspjeh isporuke uzlaznoj aplikaciji (pogledajte Pouzdan UDP API).
Oslobađanje 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. Obnavljanje prijenosa podataka

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

Kao što je već objašnjeno u zatvaranju veze nakon isteka vremena, kada istekne radni mjerač vremena, primatelj će provjeriti ima li izgubljenih paketa. U slučaju gubitka paketa, sastavlja se popis broja paketa koji nisu stigli do primatelja. Ti se brojevi unose u polje LostPackets određene veze i šalju se zahtjevi za ponovnom isporukom.
Slanje zahtjeva za ponovnu isporuku paketa (stanje sastavljanja):

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 će prihvatiti zahtjev za ponovnom dostavom i poslati pakete koji nedostaju. Vrijedno je napomenuti da je u ovom trenutku pošiljatelj već pokrenuo mjerač vremena zatvaranja veze i, kada se primi zahtjev, on se resetira.
Ponovno slanje izgubljenih paketa (Stanje ciklusa slanja):

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 #3 u dijagramu) prima dolazna veza. Provjerava se da li je prozor za primanje pun i da li je uspostavljen normalan prijenos podataka.
Provjera pogodaka u prozoru primanja (stanje sastavljanja):

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 Reliable Udp, koja je omotač nad blokom kontrole 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đenu vrstu poruke i/ili određenog pošiljatelja koriste se dva opcijska 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 asinkrono; za to protokol implementira model asinkronog programiranja:

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

Rezultat slanja poruke bit će true - ako je poruka uspješno stigla do primatelja i false - ako je veza prekinuta timeoutom:

public bool EndSendMessage(IAsyncResult asyncResult)

Zaključak

Mnogo toga nije opisano u ovom članku. Mehanizmi povezivanja niti, rukovanje iznimkama i pogreškama, implementacija metoda asinkronog slanja poruka. Ali srž protokola, opis logike za obradu paketa, uspostavljanje veze i rukovanje vremenskim ograničenjima, trebali bi vam biti jasni.

Demonstrirana verzija pouzdanog protokola isporuke dovoljno je robusna i fleksibilna da ispuni prethodno definirane zahtjeve. Ali želim dodati da se opisana implementacija može poboljšati. Na primjer, za povećanje propusnosti i dinamičku promjenu vremenskih razdoblja, mehanizmi kao što su klizni prozor i RTT mogu se dodati protokolu, 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, veselim se vašim komentarima i komentarima.

PS Za one koje zanimaju detalji ili samo žele testirati protokol, poveznica na projekt na GitHube:
Pouzdan UDP projekt

Korisni linkovi i članci

  1. Specifikacija TCP protokola: na engleskom jeziku и na ruskom
  2. Specifikacija UDP protokola: na engleskom jeziku и na ruskom
  3. Rasprava o RUDP protokolu: nacrt-ietf-sigtran-pouzdan-udp-00
  4. Pouzdan podatkovni protokol: 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 traverzalne mehanizme: Peer-to-peer komunikacija preko mrežnih prevoditelja adresa
  7. Implementacija modela asinkronog programiranja: Implementacija modela CLR asinkronog programiranja и Kako implementirati uzorak dizajna IAsyncResult
  8. Prijenos modela asinkronog programiranja na asinkroni obrazac temeljen na zadatku (APM u TAP):
    TPL i tradicionalno .NET asinkrono programiranje
    Interakcija s drugim asinkronim obrascima i vrstama

Ažuriranje: Hvala gradonačelnikovp и sidristij za ideju dodavanja zadatka sučelju. Kompatibilnost biblioteke sa starim operativnim sustavima nije narušena, jer Četvrti okvir podržava i XP i 4 poslužitelj.

Izvor: www.habr.com

Dodajte komentar