Implementácia protokolu Reliable Udp pre .Net

Internet sa už dávno zmenil. Jeden z hlavných protokolov internetu – UDP, používajú aplikácie nielen na doručovanie datagramov a vysielania, ale aj na poskytovanie „peer-to-peer“ spojení medzi sieťovými uzlami. Vďaka svojmu jednoduchému dizajnu má tento protokol mnoho predtým neplánovaných použití, avšak nedostatky protokolu, ako napríklad absencia zaručeného doručenia, nikde nezmizli. Tento článok popisuje implementáciu protokolu zaručeného doručenia cez UDP.
Obsah:Vstup
Požiadavky na protokol
Spoľahlivá hlavička UDP
Všeobecné zásady protokolu
Časové limity a časovače protokolov
Spoľahlivý diagram stavu prenosu UDP
Hlbšie do kódu. riadiaca jednotka prevodovky
Hlbšie do kódu. štátov

Hlbšie do kódu. Vytváranie a nadväzovanie spojení
Hlbšie do kódu. Zatvorenie spojenia po uplynutí časového limitu
Hlbšie do kódu. Obnovuje sa prenos dát
Spoľahlivé UDP API
Záver
Užitočné odkazy a články

Vstup

Pôvodná architektúra internetu predpokladala homogénny adresný priestor, v ktorom mal každý uzol globálnu a jedinečnú IP adresu a mohol priamo komunikovať s ostatnými uzlami. Teraz má internet v skutočnosti inú architektúru – jednu oblasť globálnych IP adries a mnoho oblastí so súkromnými adresami skrytými za zariadeniami NAT.V tejto architektúre môžu iba zariadenia v globálnom adresnom priestore jednoducho komunikovať s kýmkoľvek v sieti, pretože majú jedinečnú, globálne smerovateľnú IP adresu. Uzol v súkromnej sieti sa môže pripojiť k iným uzlom v rovnakej sieti a tiež sa môže pripojiť k iným dobre známym uzlom v globálnom adresnom priestore. Táto interakcia je dosiahnutá prevažne vďaka mechanizmu prekladu sieťových adries. Zariadenia NAT, ako napríklad smerovače Wi-Fi, vytvárajú špeciálne položky prekladovej tabuľky pre odchádzajúce pripojenia a upravujú adresy IP a čísla portov v paketoch. To umožňuje odchádzajúce pripojenia zo súkromnej siete k hostiteľom v globálnom adresnom priestore. Zároveň však zariadenia NAT zvyčajne blokujú všetku prichádzajúcu komunikáciu, pokiaľ nie sú nastavené osobitné pravidlá pre prichádzajúce pripojenia.

Táto architektúra internetu je dostatočne správna pre komunikáciu klient-server, kde klienti môžu byť v privátnych sieťach a servery majú globálnu adresu. To však vytvára ťažkosti pre priame spojenie dvoch uzlov medzi nimi rôzne súkromné ​​siete. Priame spojenie medzi dvoma uzlami je dôležité pre aplikácie typu peer-to-peer, ako je prenos hlasu (Skype), získanie vzdialeného prístupu k počítaču (TeamViewer) alebo online hranie.

Jedna z najefektívnejších metód na vytvorenie spojenia typu peer-to-peer medzi zariadeniami v rôznych súkromných sieťach sa nazýva dierovanie. Táto technika sa najčastejšie používa pri aplikáciách založených na protokole UDP.

Ak však vaša aplikácia vyžaduje zaručené doručovanie údajov, napríklad prenášate súbory medzi počítačmi, potom bude mať použitie UDP veľa problémov, pretože UDP nie je zaručeným protokolom doručovania a nezabezpečuje doručovanie paketov v poriadku, na rozdiel od TCP. protokol.

V tomto prípade je na zabezpečenie garantovaného doručovania paketov potrebné implementovať protokol aplikačnej vrstvy, ktorý poskytuje potrebnú funkcionalitu a funguje cez UDP.

Okamžite chcem poznamenať, že existuje technika dierovania TCP na nadviazanie spojenia TCP medzi uzlami v rôznych súkromných sieťach, ale kvôli nedostatočnej podpore mnohých zariadení NAT sa zvyčajne nepovažuje za hlavný spôsob pripojenia. takéto uzly.

Po zvyšok tohto článku sa budem venovať iba implementácii protokolu o zaručenom doručení. Implementácia techniky dierovania UDP bude popísaná v nasledujúcich článkoch.

Požiadavky na protokol

  1. Spoľahlivé doručovanie paketov realizované prostredníctvom mechanizmu pozitívnej spätnej väzby (tzv. pozitívneho potvrdenia)
  2. Potreba efektívneho prenosu veľkých dát, t.j. protokol sa musí vyhýbať zbytočnému prenosu paketov
  3. Malo by byť možné zrušiť mechanizmus potvrdenia doručenia (schopnosť fungovať ako „čistý“ protokol UDP)
  4. Schopnosť implementovať príkazový režim s potvrdením každej správy
  5. Základnou jednotkou prenosu dát cez protokol musí byť správa

Tieto požiadavky sa vo veľkej miere zhodujú s požiadavkami protokolu Reliable Data Protocol opísanými v rfc908 и rfc1151a pri vývoji tohto protokolu som sa spoliehal na tieto štandardy.

Aby sme pochopili tieto požiadavky, pozrime sa na načasovanie prenosu dát medzi dvoma sieťovými uzlami pomocou protokolov TCP a UDP. Nech v oboch prípadoch stratíme jeden paket.
Prenos neinteraktívnych údajov cez TCP:Implementácia protokolu Reliable Udp pre .Net

Ako môžete vidieť z diagramu, v prípade straty paketu TCP deteguje stratený paket a nahlási ho odosielateľovi požiadaním o číslo strateného segmentu.
Prenos dát cez protokol UDP:Implementácia protokolu Reliable Udp pre .Net

UDP nevykonáva žiadne kroky na zistenie straty. Kontrola prenosových chýb v protokole UDP je plne v kompetencii aplikácie.

Detekcia chýb v protokole TCP sa dosiahne vytvorením spojenia s koncovým uzlom, uložením stavu tohto spojenia, uvedením počtu bajtov odoslaných v hlavičke každého paketu a upozornením na príjem pomocou čísla potvrdenia.

Okrem toho na zlepšenie výkonu (t. j. odoslanie viac ako jedného segmentu bez prijatia potvrdenia) protokol TCP používa takzvané prenosové okno – počet bajtov údajov, ktoré odosielateľ segmentu očakáva, že ich prijme.

Ďalšie informácie o protokole TCP nájdete v časti rfc793, z UDP do rfc768kde sú v skutočnosti definované.

Z uvedeného je zrejmé, že pre vytvorenie spoľahlivého protokolu doručovania správ cez UDP (ďalej len tzv Spoľahlivý UDP), je potrebné implementovať mechanizmy prenosu údajov podobné TCP. menovite:

  • uložiť stav pripojenia
  • použite číslovanie segmentov
  • použite špeciálne potvrdzovacie balíčky
  • použite zjednodušený mechanizmus okien na zvýšenie priepustnosti protokolu

Okrem toho potrebujete:

  • signalizovať začiatok správy, alokovať zdroje pre pripojenie
  • signalizovať koniec správy, odovzdať prijatú správu nadradenej aplikácii a uvoľniť prostriedky protokolu
  • umožniť protokolu špecifickému pre pripojenie deaktivovať mechanizmus potvrdenia doručenia, aby fungoval ako "čistý" UDP

Spoľahlivá hlavička UDP

Pripomeňme, že datagram UDP je zapuzdrený v datagrame IP. Reliable UDP paket je vhodne "zabalený" do UDP datagramu.
Spoľahlivé zapuzdrenie hlavičky UDP:Implementácia protokolu Reliable Udp pre .Net

Štruktúra hlavičky Reliable UDP je pomerne jednoduchá:

Implementácia protokolu Reliable Udp pre .Net

  • Príznaky - príznaky kontroly balíkov
  • MessageType – typ správy, ktorý používajú upstream aplikácie na odber konkrétnych správ
  • TransmissionId - číslo prenosu spolu s adresou a portom príjemcu jednoznačne identifikuje spojenie
  • PacketNumber – číslo paketu
  • Možnosti - ďalšie možnosti protokolu. V prípade prvého paketu sa používa na označenie veľkosti správy

Vlajky sú nasledovné:

  • FirstPacket – prvý paket správy
  • NoAsk – správa nevyžaduje aktiváciu mechanizmu potvrdenia
  • LastPacket – posledný paket správy
  • RequestForPacket - potvrdzovací paket alebo požiadavka na stratený paket

Všeobecné zásady protokolu

Keďže Reliable UDP je zameraný na garantovaný prenos správ medzi dvoma uzlami, musí byť schopný nadviazať spojenie s druhou stranou. Na vytvorenie spojenia odosielateľ odošle paket s príznakom FirstPacket, ktorého odpoveď bude znamenať, že spojenie je nadviazané. Všetky pakety s odpoveďou, alebo inými slovami, potvrdzovacie pakety, vždy nastavujú hodnotu poľa PacketNumber o jednu vyššiu, než je najväčšia hodnota PacketNumber úspešne prijatých paketov. Pole Možnosti pre prvý odoslaný paket predstavuje veľkosť správy.

Podobný mechanizmus sa používa na ukončenie spojenia. Príznak LastPacket je nastavený na posledný paket správy. V pakete odpovede je uvedené číslo posledného paketu + 1, čo pre prijímajúcu stranu znamená úspešné doručenie správy.
Schéma vytvorenia a ukončenia pripojenia:Implementácia protokolu Reliable Udp pre .Net

Po nadviazaní spojenia sa spustí prenos dát. Dáta sa prenášajú v blokoch paketov. Každý blok, okrem posledného, ​​obsahuje pevný počet paketov. Rovná sa veľkosti prijímacieho/vysielacieho okna. Posledný blok údajov môže mať menej paketov. Po odoslaní každého bloku čaká odosielajúca strana na potvrdenie doručenia alebo požiadavku na opätovné doručenie stratených paketov, pričom okno prijatia/prenosu zostáva otvorené na prijímanie odpovedí. Po prijatí potvrdenia o doručení bloku sa okno príjmu/prenosu posunie a odošle sa ďalší blok dát.

Prijímajúca strana prijíma pakety. Každý paket sa kontroluje, či spadá do prenosového okna. Pakety a duplikáty, ktoré nespadajú do okna, sú odfiltrované. Pretože Ak je veľkosť okna pevná a rovnaká pre príjemcu aj odosielateľa, tak v prípade bezstratového doručenia bloku paketov sa okno posunie na príjem paketov ďalšieho bloku dát a potvrdí sa doručenie. odoslaná. Ak sa okno nezaplní v lehote nastavenej pracovným časovačom, spustí sa kontrola, ktoré pakety neboli doručené a budú odoslané žiadosti o opätovné doručenie.
Schéma retransmisie:Implementácia protokolu Reliable Udp pre .Net

Časové limity a časovače protokolov

Existuje niekoľko dôvodov, prečo nie je možné nadviazať spojenie. Napríklad, ak je príjemca offline. V tomto prípade pri pokuse o nadviazanie spojenia bude spojenie ukončené časovým limitom. Implementácia Reliable UDP používa dva časovače na nastavenie časových limitov. Prvý, pracovný časovač, sa používa na čakanie na odpoveď od vzdialeného hostiteľa. Ak sa spustí na strane odosielateľa, posledný odoslaný paket sa odošle znova. Ak časovač vyprší u príjemcu, vykoná sa kontrola stratených paketov a odošle sa žiadosť o opätovné doručenie.

Druhý časovač je potrebný na uzavretie spojenia v prípade nedostatočnej komunikácie medzi uzlami. Na strane odosielateľa sa spustí ihneď po uplynutí pracovného časovača a čaká na odpoveď vzdialeného uzla. Ak nedôjde k žiadnej odozve počas určeného obdobia, pripojenie sa ukončí a zdroje sa uvoľnia. Na prijímacej strane sa časovač zatvorenia spojenia spustí po tom, ako dvakrát vyprší pracovný časovač. Je to potrebné na zabezpečenie proti strate potvrdzovacieho paketu. Po uplynutí časovača sa pripojenie tiež ukončí a zdroje sa uvoľnia.

Spoľahlivý diagram stavu prenosu UDP

Princípy protokolu sú implementované v konečnom automate, ktorého každý stav je zodpovedný za určitú logiku spracovania paketov.
Spoľahlivý stavový diagram UDP:

Implementácia protokolu Reliable Udp pre .Net

Zatvorené - nie je v skutočnosti stav, je to počiatočný a konečný bod pre automat. Pre štát Zatvorené je prijatý riadiaci blok prenosu, ktorý implementuje asynchrónny UDP server, posiela pakety na príslušné spojenia a spúšťa spracovanie stavu.

FirstPacketSending – počiatočný stav, v ktorom je odchádzajúce spojenie pri odoslaní správy.

V tomto stave sa odošle prvý paket pre normálne správy. Pre správy bez potvrdenia odoslania je to jediný stav, kedy sa odošle celá správa.

Cyklus odosielania – základný stav pre prenos paketov správ.

Prechod do nej zo štátu FirstPacketSending po odoslaní prvého paketu správy. V tomto stave prichádzajú všetky potvrdenia a žiadosti o retransmisie. Výstup z nej je možný v dvoch prípadoch – v prípade úspešného doručenia správy alebo timeoutom.

FirstPacketReceived – počiatočný stav pre príjemcu správy.

Skontroluje správnosť začiatku prenosu, vytvorí potrebné štruktúry a odošle potvrdenie o prijatí prvého paketu.

V prípade správy, ktorá pozostáva z jedného paketu a bola odoslaná bez použitia potvrdenia o doručení, je to jediný stav. Po spracovaní takejto správy sa spojenie uzavrie.

zostavenie – základný stav pre príjem paketov správ.

Zapisuje pakety do dočasného úložiska, kontroluje stratu paketov, posiela potvrdenia o doručení bloku paketov a celej správy a posiela požiadavky na opätovné doručenie stratených paketov. V prípade úspešného prijatia celej správy prejde spojenie do stavu Dokončené, v opačnom prípade vyprší časový limit.

Dokončené – ukončenie spojenia v prípade úspešného prijatia celej správy.

Tento stav je potrebný pre zostavenie správy a pre prípad, keď sa potvrdenie o doručení správy stratilo na ceste k odosielateľovi. Tento stav sa ukončí po uplynutí časového limitu, ale spojenie sa považuje za úspešne uzavreté.

Hlbšie do kódu. riadiaca jednotka prevodovky

Jedným z kľúčových prvkov spoľahlivého UDP je blok riadenia prenosu. Úlohou tohto bloku je ukladať aktuálne spojenia a pomocné prvky, distribuovať prichádzajúce pakety do príslušných spojení, poskytovať rozhranie na odosielanie paketov do spojenia a implementovať protokol API. Blok riadenia prenosu prijíma pakety z vrstvy UDP a posiela ich do stavového automatu na spracovanie. Na prijímanie paketov implementuje asynchrónny UDP server.
Niektorí členovia triedy 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;    	
  //...
}

Implementácia asynchrónneho 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);
}

Pre každý prenos správy sa vytvorí štruktúra, ktorá obsahuje informácie o spojení. Takáto štruktúra je tzv záznam o pripojení.
Niektorí členovia triedy 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;
  //...
}

Hlbšie do kódu. štátov

Štáty implementujú stavový automat protokolu Reliable UDP, kde prebieha hlavné spracovanie paketov. Abstraktná trieda ReliableUdpState poskytuje rozhranie pre stav:

Implementácia protokolu Reliable Udp pre .Net

Celá logika protokolu je implementovaná vyššie uvedenými triedami spolu s pomocnou triedou, ktorá poskytuje statické metódy, ako je napríklad zostavenie hlavičky ReliableUdp zo záznamu pripojenia.

Ďalej podrobne zvážime implementáciu metód rozhrania, ktoré určujú základné algoritmy protokolu.

Metóda DisposeByTimeout

Metóda DisposeByTimeout je zodpovedná za uvoľnenie prostriedkov pripojenia po uplynutí časového limitu a za signalizáciu úspešného/neúspešného doručenia správy.
ReliableUdpState.DisposeByTimeout:

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

Je to prepísané iba v štáte Dokončené.
Completed.DisposeByTimeout:

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

Metóda ProcessPackets

Metóda ProcessPackets je zodpovedná za dodatočné spracovanie balíka alebo balíkov. Volané priamo alebo prostredníctvom časovača čakania na pakety.

V stave zostavenie metóda je prepísaná a je zodpovedná za kontrolu stratených paketov a prechod do stavu Dokončené, v prípade prijatia posledného paketu a úspešnej kontroly
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);
  }
}

V stave Cyklus odosielania táto metóda sa volá iba na časovači a je zodpovedná za opätovné odoslanie poslednej správy, ako aj za aktiváciu časovača zatvorenia spojenia.
SendingCycle.ProcessPackets:

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

V stave Dokončené metóda zastaví bežiaci časovač a odošle správu predplatiteľom.
Completed.ProcessPackets:

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

ReceivePacket metóda

V stave FirstPacketReceived hlavnou úlohou metódy je zistiť, či prvý paket správ skutočne dorazil na rozhranie, a tiež zhromaždiť správu pozostávajúcu z jedného paketu.
FirstPacketReceived.ReceivePacket:

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

V stave Cyklus odosielania táto metóda je prepísaná, aby sa prijímali potvrdenia o doručení a žiadosti o opakovaný prenos.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

V stave zostavenie v metóde ReceivePacket prebieha hlavná práca pri zostavovaní správy z prichádzajúcich paketov.
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);
  }
}

V stave Dokončené jedinou úlohou metódy je poslať opätovné potvrdenie o úspešnom doručení správy.
Completed.ReceivePacket:

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

Metóda odosielania paketov

V stave FirstPacketSending táto metóda odošle prvý paket dát alebo, ak správa nevyžaduje potvrdenie doručenia, celú správu.
FirstPacketSending.SendPacket:

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

V stave Cyklus odosielania pri tejto metóde sa odosiela blok paketov.
SendingCycle.SendPacket:

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

Hlbšie do kódu. Vytváranie a nadväzovanie spojení

Teraz, keď sme videli základné stavy a metódy používané na spracovanie stavov, poďme si rozobrať niekoľko príkladov fungovania protokolu trochu podrobnejšie.
Diagram prenosu údajov za normálnych podmienok:Implementácia protokolu Reliable Udp pre .Net

Zvážte podrobne stvorenie záznam o pripojení pripojiť a odoslať prvý paket. Prenos vždy iniciuje aplikácia, ktorá volá rozhranie API na odosielanie správ. Ďalej je vyvolaná metóda StartTransmission riadiaceho bloku prenosu, ktorá spustí prenos dát pre novú správu.
Vytvorenie odchádzajúceho pripojenia:

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

Odosielanie prvého paketu (stav FirstPacketSending):

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

Po odoslaní prvého paketu sa odosielateľ dostane do stavu Cyklus odosielania - počkajte na potvrdenie doručenia balíka.
Prijímajúca strana pomocou metódy EndReceive prijme odoslaný paket, vytvorí nový záznam o pripojení a odovzdá tento paket s vopred analyzovanou hlavičkou metóde ReceivePacket štátu na spracovanie FirstPacketReceived
Vytvorenie spojenia na prijímacej strane:

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

Prijatie prvého paketu a odoslanie potvrdenia (stav 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);
  }
}

Hlbšie do kódu. Zatvorenie spojenia po uplynutí časového limitu

Spracovanie časového limitu je dôležitou súčasťou spoľahlivého UDP. Uvažujme o príklade, v ktorom zlyhal medziľahlý uzol a doručovanie údajov v oboch smeroch sa stalo nemožným.
Schéma ukončenia spojenia časovým limitom:Implementácia protokolu Reliable Udp pre .Net

Ako je zrejmé z diagramu, pracovný časovač odosielateľa sa spustí ihneď po odoslaní bloku paketov. To sa deje v metóde SendPacket štátu Cyklus odosielania.
Povolenie pracovného časovača (stav SendingCycle):

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

Časové intervaly sa nastavia pri vytvorení spojenia. Predvolená hodnota ShortTimerPeriod je 5 sekúnd. V príklade je nastavená na 1,5 sekundy.

Pre prichádzajúce spojenie sa časovač spustí po prijatí posledného prichádzajúceho dátového paketu, to sa deje v metóde ReceivePacket štátu zostavenie
Povolenie pracovného časovača (stav zostavy):

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

Počas čakania na pracovný časovač neprišli na prichádzajúce spojenie žiadne ďalšie pakety. Časovač sa spustil a zavolal metódu ProcessPackets, kde boli stratené pakety nájdené a prvýkrát boli odoslané požiadavky na opätovné doručenie.
Odosielanie žiadostí o opätovné doručenie (stav zostavovania):

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

Premenná TimerSecondTry je nastavená na pravdivý. Táto premenná je zodpovedná za reštartovanie pracovného časovača.

Na strane odosielateľa sa tiež spustí pracovný časovač a znova sa odošle posledný odoslaný paket.
Povolenie časovača zatvorenia pripojenia (stav SendingCycle):

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

Potom sa v odchádzajúcom pripojení spustí časovač ukončenia spojenia.
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);
}

Časový limit časovača zatvorenia pripojenia je predvolene 30 sekúnd.

Po krátkom čase sa opäť spustí pracovný časovač na strane príjemcu, znova sa odošlú požiadavky, po ktorých sa spustí časovač ukončenia spojenia pre prichádzajúce spojenie

Keď sa spustia zatváracie časovače, uvoľnia sa všetky zdroje oboch záznamov pripojenia. Odosielateľ nahlási zlyhanie doručenia nadradenej aplikácii (pozri Spoľahlivé UDP API).
Uvoľňujú sa zdroje záznamu pripojenia:

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

Hlbšie do kódu. Obnovuje sa prenos dát

Diagram obnovy prenosu dát v prípade straty paketov:Implementácia protokolu Reliable Udp pre .Net

Ako už bolo uvedené pri zatváraní spojenia po uplynutí časového limitu, keď pracovný časovač vyprší, prijímač skontroluje stratené pakety. V prípade straty paketu sa zostaví zoznam počtu paketov, ktoré sa nedostali k príjemcovi. Tieto čísla sa zadajú do poľa LostPackets konkrétneho pripojenia a odošlú sa žiadosti o opätovné doručenie.
Odosielanie žiadostí o opätovné doručenie balíkov (stav zostavovania):

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

Odosielateľ prijme požiadavku na opätovné doručenie a odošle chýbajúce pakety. Stojí za zmienku, že v tomto momente už odosielateľ spustil časovač uzatvárania spojenia a po prijatí požiadavky sa resetuje.
Opätovné odosielanie stratených paketov (stav SendingCycle):

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

Opätovne odoslaný paket (paket č. 3 v diagrame) je prijatý prichádzajúcim spojením. Skontroluje sa, či je prijímacie okno plné a či je obnovený normálny prenos dát.
Kontrola zhôd v prijímacom okne (stav zostavovania):

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

Spoľahlivé UDP API

Na interakciu s protokolom prenosu údajov existuje otvorená trieda Reliable Udp, ktorá je obalom nad blokom riadenia prenosu. Tu sú najdôležitejší členovia triedy:

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

Správy sa prijímajú na základe predplatného. Delegovať podpis pre metódu spätného volania:

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

Správa:

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

Na predplatenie špecifického typu správy a/alebo konkrétneho odosielateľa sa používajú dva voliteľné parametre: ReliableUdpMessageTypes messageType a IPEndPoint ipEndPoint.

Typy správ:

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

Správa sa odosiela asynchrónne; na tento účel protokol implementuje asynchrónny programovací model:

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

Výsledok odoslania správy bude pravdivý - ak sa správa úspešne dostala k príjemcovi a nepravda - ak bolo spojenie ukončené časovým limitom:

public bool EndSendMessage(IAsyncResult asyncResult)

Záver

V tomto článku sa toho veľa nepopísalo. Mechanizmy zhody vlákien, spracovanie výnimiek a chýb, implementácia asynchrónnych metód odosielania správ. Ale jadro protokolu, popis logiky spracovania paketov, nadviazania spojenia a spracovania časových limitov, by vám malo byť jasné.

Predvedená verzia spoľahlivého doručovacieho protokolu je dostatočne robustná a flexibilná, aby splnila predtým definované požiadavky. Chcem však dodať, že opísaná implementácia sa dá vylepšiť. Napríklad na zvýšenie priepustnosti a dynamickú zmenu periód časovača môžu byť do protokolu pridané mechanizmy ako posuvné okno a RTT, bude tiež užitočné implementovať mechanizmus na určovanie MTU medzi uzlami pripojenia (ale iba ak sa odosielajú veľké správy ).

Ďakujem za pozornosť, teším sa na vaše komentáre a komentáre.

PS Pre tých, ktorých zaujímajú podrobnosti alebo si chcú protokol len otestovať, odkaz na projekt na GitHube:
Spoľahlivý projekt UDP

Užitočné odkazy a články

  1. Špecifikácia protokolu TCP: v angličtine и На русском
  2. Špecifikácia protokolu UDP: v angličtine и На русском
  3. Diskusia k protokolu RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Spoľahlivý dátový protokol: rfc908 и rfc1151
  5. Jednoduchá implementácia potvrdenia o doručení cez UDP: Prevezmite úplnú kontrolu nad sieťou pomocou .NET a UDP
  6. Článok popisujúci mechanizmy prechodu NAT: Peer-to-Peer komunikácia medzi sieťovými prekladačmi adries
  7. Implementácia modelu asynchrónneho programovania: Implementácia modelu asynchrónneho programovania CLR и Ako implementovať návrhový vzor IAsyncResult
  8. Portovanie asynchrónneho programovacieho modelu na asynchrónny vzor založený na úlohách (APM v TAP):
    TPL a tradičné .NET asynchrónne programovanie
    Interoperabilita s inými asynchrónnymi vzormi a typmi

Aktualizácia: Ďakujem starostaovp и sidristij za myšlienku pridania úlohy do rozhrania. Kompatibilita knižnice so starými operačnými systémami nie je narušená, pretože 4. framework podporuje server XP aj 2003.

Zdroj: hab.com

Pridať komentár