Patikimo Udp protokolo įgyvendinimas .Net

Internetas jau seniai pasikeitė. Vienas iš pagrindinių interneto protokolų – UDP yra naudojamas aplikacijų ne tik datagramoms ir transliacijoms pristatyti, bet ir „peer-to-peer“ ryšiams tarp tinklo mazgų užtikrinti. Dėl savo paprastos konstrukcijos šis protokolas turi daug anksčiau neplanuotų panaudojimų, tačiau protokolo trūkumai, tokie kaip garantuoto pristatymo nebuvimas, niekur nedingo. Šiame straipsnyje aprašomas garantuoto pristatymo protokolo įgyvendinimas per UDP.
Turinys:Įrašas
Protokolo reikalavimai
Patikima UDP antraštė
Bendrieji protokolo principai
Laikmatis ir protokolo laikmačiai
Patikima UDP perdavimo būsenos diagrama
Giliau į kodą. transmisijos valdymo blokas
Giliau į kodą. teigia

Giliau į kodą. Ryšių kūrimas ir užmezgimas
Giliau į kodą. Ryšys uždaromas pasibaigus laikui
Giliau į kodą. Duomenų perdavimo atkūrimas
Patikima UDP API
išvada
Naudingos nuorodos ir straipsniai

Įrašas

Pradinė interneto architektūra turėjo vienalytę adresų erdvę, kurioje kiekvienas mazgas turėjo visuotinį ir unikalų IP adresą ir galėjo tiesiogiai bendrauti su kitais mazgais. Dabar internetas iš tikrųjų turi kitokią architektūrą – vieną pasaulinių IP adresų sritį ir daugybę sričių su privačiais adresais, paslėptais už NAT įrenginių.Šioje architektūroje tik pasaulinėje adresų erdvėje esantys įrenginiai gali lengvai susisiekti su bet kuriuo tinklo nariu, nes jie turi unikalų, visuotinai nukreiptą IP adresą. Privataus tinklo mazgas gali prisijungti prie kitų mazgų tame pačiame tinkle, taip pat gali prisijungti prie kitų gerai žinomų mazgų visuotinėje adresų erdvėje. Ši sąveika pasiekiama daugiausia dėl tinklo adresų vertimo mechanizmo. NAT įrenginiai, tokie kaip „Wi-Fi“ maršrutizatoriai, sukuria specialius vertimo lentelės įrašus išeinantiems ryšiams ir modifikuoja IP adresus bei prievadų numerius paketuose. Tai leidžia užmegzti išeinančius ryšius iš privataus tinklo į pagrindinius kompiuterius pasaulinėje adresų erdvėje. Tačiau tuo pačiu metu NAT įrenginiai paprastai blokuoja visą įeinantį srautą, nebent nustatomos atskiros gaunamų ryšių taisyklės.

Tokia interneto architektūra yra pakankamai teisinga kliento ir serverio komunikacijai, kai klientai gali būti privačiuose tinkluose, o serveriai turi globalų adresą. Tačiau tai sukuria sunkumų tiesiogiai sujungti du mazgus įvairūs privačių tinklų. Tiesioginis ryšys tarp dviejų mazgų yra svarbus lygiavertėms programoms, tokioms kaip balso perdavimas (Skype), nuotolinė prieiga prie kompiuterio (TeamViewer) ar internetiniai žaidimai.

Vienas iš efektyviausių būdų užmegzti lygiavertį ryšį tarp įrenginių skirtinguose privačiuose tinkluose vadinamas skylių perforavimu. Ši technika dažniausiai naudojama UDP protokolu pagrįstose programose.

Bet jei jūsų programai reikalingas garantuotas duomenų pristatymas, pavyzdžiui, perkeliate failus iš vieno kompiuterio į kitą, tada naudojant UDP kils daug sunkumų dėl to, kad UDP nėra garantuotas pristatymo protokolas ir nepateikia tvarkingo paketų pristatymo, skirtingai nei TCP. protokolas.

Šiuo atveju, norint užtikrinti garantuotą paketų pristatymą, būtina įdiegti taikomojo lygmens protokolą, kuris suteikia reikiamą funkcionalumą ir veikia per UDP.

Iš karto noriu pastebėti, kad yra TCP skylių perforavimo technika, skirta TCP ryšiams tarp mazgų užmegzti skirtinguose privačiuose tinkluose, tačiau dėl to, kad daugelis NAT įrenginių nepalaiko jo, dažniausiai tai nelaikoma pagrindiniu prisijungimo būdu. tokius mazgus.

Likusioje šio straipsnio dalyje daugiausia dėmesio skirsiu garantuoto pristatymo protokolo įgyvendinimui. UDP skylių išmušimo technikos įgyvendinimas bus aprašytas tolesniuose straipsniuose.

Protokolo reikalavimai

  1. Patikimas paketų pristatymas, įgyvendintas naudojant teigiamo grįžtamojo ryšio mechanizmą (vadinamasis teigiamas patvirtinimas)
  2. Efektyvaus didžiųjų duomenų perdavimo poreikis, t.y. protokolas turi vengti nereikalingo paketų perdavimo
  3. Turėtų būti įmanoma atšaukti pristatymo patvirtinimo mechanizmą (galimybė veikti kaip „grynas“ UDP protokolas)
  4. Galimybė įgyvendinti komandų režimą su kiekvieno pranešimo patvirtinimu
  5. Pagrindinis duomenų perdavimo protokolu vienetas turi būti pranešimas

Šie reikalavimai iš esmės sutampa su Patikimų duomenų protokolo reikalavimais, aprašytais RFK 908 и RFK 1151, ir aš rėmiausi tais standartais kurdamas šį protokolą.

Norėdami suprasti šiuos reikalavimus, pažvelkime į duomenų perdavimo tarp dviejų tinklo mazgų, naudojant TCP ir UDP protokolus, laiką. Tegul abiem atvejais prarasime vieną paketą.
Neinteraktyvių duomenų perkėlimas per TCP:Patikimo Udp protokolo įgyvendinimas .Net

Kaip matote iš diagramos, paketo praradimo atveju TCP aptiks prarastą paketą ir praneš siuntėjui, prašydamas prarasto segmento numerio.
Duomenų perdavimas UDP protokolu:Patikimo Udp protokolo įgyvendinimas .Net

UDP nesiima jokių nuostolių aptikimo veiksmų. UDP protokolo perdavimo klaidų kontrolė yra visiškai programos atsakomybė.

Klaidų aptikimas TCP protokole pasiekiamas užmezgant ryšį su galiniu mazgu, išsaugant to ryšio būseną, nurodant kiekvieno paketo antraštėje išsiųstų baitų skaičių ir pranešant apie kvitus naudojant patvirtinimo numerį.

Be to, siekiant pagerinti našumą (t. y. siųsti daugiau nei vieną segmentą negavus patvirtinimo), TCP protokolas naudoja vadinamąjį perdavimo langą – duomenų baitų skaičių, kurį segmento siuntėjas tikisi gauti.

Norėdami gauti daugiau informacijos apie TCP protokolą, žr RFK 793, nuo UDP iki RFK 768kur iš tikrųjų jie yra apibrėžti.

Iš to, kas išdėstyta aukščiau, aišku, kad siekiant sukurti patikimą pranešimų siuntimo protokolą per UDP (toliau – Patikimas UDP), reikalaujama įdiegti duomenų perdavimo mechanizmus, panašius į TCP. Būtent:

  • išsaugoti ryšio būseną
  • naudoti segmentų numeraciją
  • naudoti specialius patvirtinimo paketus
  • naudokite supaprastintą langų mechanizmą, kad padidintumėte protokolo pralaidumą

Be to, jums reikia:

  • signalizuoja pranešimo pradžią, kad paskirstytų išteklius ryšiui
  • signalizuoja pranešimo pabaigą, kad gautas pranešimas būtų perduotas aukštesniajai programai ir paleistų protokolo išteklius
  • leisti konkrečiam ryšio protokolui išjungti pristatymo patvirtinimo mechanizmą, kad jis veiktų kaip „grynasis“ UDP

Patikima UDP antraštė

Prisiminkite, kad UDP datagrama yra įdėta į IP datagramą. Patikimas UDP paketas yra tinkamai „įvyniotas“ į UDP datagramą.
Patikima UDP antraštės kapsulė:Patikimo Udp protokolo įgyvendinimas .Net

Patikimos UDP antraštės struktūra yra gana paprasta:

Patikimo Udp protokolo įgyvendinimas .Net

  • Flags – paketo kontrolės vėliavėlės
  • MessageType – pranešimo tipas, naudojamas ankstesnėse programose, norint užsiprenumeruoti konkrečius pranešimus
  • TransmissionId – siuntimo numeris kartu su gavėjo adresu ir prievadu, vienareikšmiškai identifikuoja ryšį
  • PacketNumber – paketo numeris
  • Parinktys – papildomos protokolo parinktys. Pirmojo paketo atveju jis naudojamas pranešimo dydžiui nurodyti

Vėliavos yra tokios:

  • FirstPacket – pirmasis žinutės paketas
  • NoAsk – žinutei nereikia įjungti patvirtinimo mechanizmo
  • LastPacket – paskutinis pranešimo paketas
  • RequestForPacket – patvirtinimo paketas arba užklausa dėl prarasto paketo

Bendrieji protokolo principai

Kadangi patikimas UDP yra orientuotas į garantuotą pranešimų perdavimą tarp dviejų mazgų, jis turi sugebėti užmegzti ryšį su kita puse. Norėdamas užmegzti ryšį, siuntėjas siunčia paketą su FirstPacket vėliava, kurio atsakymas reikš, kad ryšys užmegztas. Visuose atsakymų paketuose arba, kitaip tariant, patvirtinimo paketuose, lauko PacketNumber reikšmė visada nustatoma vienu didesnę nei didžiausia sėkmingai gautų paketų PacketNumber reikšmė. Pirmojo išsiųsto paketo parinkčių laukas yra pranešimo dydis.

Panašus mechanizmas naudojamas ryšiui nutraukti. „LastPacket“ vėliavėlė yra nustatyta paskutiniame pranešimo pakete. Atsakymo pakete nurodomas paskutinio paketo numeris + 1, kas gaunančiajai pusei reiškia sėkmingą pranešimo pristatymą.
Ryšio nustatymo ir užbaigimo schema:Patikimo Udp protokolo įgyvendinimas .Net

Užmezgus ryšį, prasideda duomenų perdavimas. Duomenys perduodami paketų blokais. Kiekviename bloke, išskyrus paskutinį, yra nustatytas paketų skaičius. Jis lygus priėmimo/perdavimo lango dydžiui. Paskutiniame duomenų bloke gali būti mažiau paketų. Išsiuntus kiekvieną bloką, siunčiančioji pusė laukia pristatymo patvirtinimo arba prašymo pakartotinai pristatyti prarastus paketus, palikdama atvirą priėmimo/perdavimo langą, kad gautų atsakymus. Gavus bloko pristatymo patvirtinimą, gavimo/perdavimo langas pasislenka ir siunčiamas kitas duomenų blokas.

Priimančioji pusė priima paketus. Kiekvienas paketas patikrinamas, ar jis patenka į perdavimo langą. Paketai ir dublikatai, kurie nepatenka į langą, išfiltruojami. Nes Jei lango dydis yra fiksuotas ir vienodas gavėjui ir siuntėjui, tada, jei paketų blokas pristatomas be nuostolių, langas perkeliamas į kito duomenų bloko paketus ir pateikiamas pristatymo patvirtinimas. išsiųstas. Jei langas neužsipildo per darbo laikmačio nustatytą laikotarpį, bus pradėtas tikrinimas, kurie paketai nebuvo pristatyti ir siunčiami prašymai dėl pristatymo pakartotinai.
Retransliavimo schema:Patikimo Udp protokolo įgyvendinimas .Net

Laikmatis ir protokolo laikmačiai

Yra keletas priežasčių, kodėl nepavyksta užmegzti ryšio. Pavyzdžiui, jei priimančioji šalis yra neprisijungusi. Tokiu atveju, bandant užmegzti ryšį, ryšys bus nutrauktas pasibaigus laikui. Patikimas UDP diegimas naudoja du laikmačius skirtajam laikui nustatyti. Pirmasis, darbo laikmatis, naudojamas laukti atsakymo iš nuotolinio pagrindinio kompiuterio. Jei jis suveikia siuntėjo pusėje, paskutinis išsiųstas paketas siunčiamas iš naujo. Jei laikmatis baigiasi pas gavėją, tada patikrinama, ar nėra prarastų paketų, ir siunčiami pakartotinio pristatymo užklausos.

Antrasis laikmatis reikalingas norint uždaryti ryšį, jei trūksta ryšio tarp mazgų. Siuntėjo pusėje jis paleidžiamas iš karto pasibaigus darbo laikmačio galiojimo laikui ir laukia atsakymo iš nuotolinio mazgo. Jei per nurodytą laikotarpį atsakymo negaunama, ryšys nutraukiamas ir ištekliai atleidžiami. Priėmimo pusėje ryšio uždarymo laikmatis paleidžiamas, kai darbo laikmatis baigiasi du kartus. Tai būtina norint apsidrausti nuo patvirtinimo paketo praradimo. Pasibaigus laikmačio laikui, ryšys taip pat nutraukiamas ir ištekliai atleidžiami.

Patikima UDP perdavimo būsenos diagrama

Protokolo principai realizuoti baigtinių būsenų mašinoje, kurios kiekviena būsena yra atsakinga už tam tikrą paketų apdorojimo logiką.
Patikima UDP būsenos diagrama:

Patikimo Udp protokolo įgyvendinimas .Net

Uždaryta - iš tikrųjų nėra būsena, tai yra automato pradžios ir pabaigos taškas. Dėl valstybės Uždaryta gaunamas perdavimo valdymo blokas, kuris, įgyvendindamas asinchroninį UDP serverį, persiunčia paketus atitinkamoms jungtims ir pradeda būsenos apdorojimą.

FirstPacketSending – pradinė būsena, kurioje yra išeinantis ryšys, kai siunčiamas pranešimas.

Šioje būsenoje siunčiamas pirmasis įprastų pranešimų paketas. Pranešimams be siuntimo patvirtinimo tai yra vienintelė būsena, kai siunčiamas visas pranešimas.

Siuntimo ciklas – pranešimų paketų perdavimo pagrindinė būsena.

Perėjimas prie jo iš valstybės FirstPacketSending atliekamas po to, kai buvo išsiųstas pirmasis pranešimo paketas. Būtent tokioje būsenoje ateina visi patvirtinimai ir prašymai persiųsti. Išeiti iš jo galima dviem atvejais – sėkmingo pranešimo pristatymo atveju arba pasibaigus laikui.

FirstPacketReceived – pranešimo gavėjo pradinė būsena.

Ji patikrina perdavimo pradžios teisingumą, sukuria reikiamas struktūras ir išsiunčia patvirtinimą apie pirmojo paketo gavimą.

Pranešimui, kurį sudaro vienas paketas ir kuris buvo išsiųstas nenaudojant pristatymo įrodymo, tai yra vienintelė būsena. Apdorojus tokį pranešimą, ryšys nutraukiamas.

Surinkimas – pagrindinė pranešimų paketų priėmimo būsena.

Jis įrašo paketus į laikiną saugyklą, tikrina, ar paketai neprarado, siunčia patvirtinimus dėl paketų bloko ir viso pranešimo pristatymo bei siunčia prašymus pakartotinai pristatyti prarastus paketus. Sėkmingai gavus visą pranešimą, ryšys pereina į būseną Užbaigtas, kitu atveju baigiasi skirtasis laikas.

Užbaigtas – ryšio nutraukimas sėkmingai gavus visą pranešimą.

Ši būsena reikalinga žinutės surinkimui ir tuo atveju, kai pakeliui pas siuntėją buvo prarastas pranešimo pristatymo patvirtinimas. Iš šios būsenos baigiasi skirtasis laikas, tačiau ryšys laikomas sėkmingai uždarytu.

Giliau į kodą. transmisijos valdymo blokas

Vienas iš pagrindinių patikimo UDP elementų yra perdavimo valdymo blokas. Šio bloko užduotis yra saugoti esamus ryšius ir pagalbinius elementus, paskirstyti gaunamus paketus atitinkamoms jungtims, suteikti sąsają paketų siuntimui į ryšį ir įdiegti protokolo API. Perdavimo valdymo blokas priima paketus iš UDP sluoksnio ir persiunčia juos į būsenos mašiną apdoroti. Norėdami gauti paketus, jis įdiegia asinchroninį UDP serverį.
Kai kurie „ReliableUdpConnectionControlBlock“ klasės nariai:

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

Asinchroninio UDP serverio diegimas:

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

Kiekvienam pranešimo perdavimui sukuriama struktūra, kurioje yra informacija apie ryšį. Tokia struktūra vadinama ryšio įrašas.
Kai kurie „ReliableUdpConnectionRecord“ klasės nariai:

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

Giliau į kodą. teigia

Valstybės įdiegia Patikimo UDP protokolo būsenos mašiną, kurioje vyksta pagrindinis paketų apdorojimas. Abstrakčioji klasė ReliableUdpState suteikia būsenos sąsają:

Patikimo Udp protokolo įgyvendinimas .Net

Visą protokolo logiką įgyvendina aukščiau pateiktos klasės kartu su pagalbine klase, teikiančia statinius metodus, tokius kaip, pavyzdžiui, ReliableUdp antraštės sukūrimas iš ryšio įrašo.

Toliau mes išsamiai apsvarstysime sąsajos metodų, kurie nustato pagrindinius protokolo algoritmus, įgyvendinimą.

DisposeByTimeout metodas

Metodas „DisposeByTimeout“ yra atsakingas už ryšio išteklių atleidimą pasibaigus skirtajam laikui ir signalizaciją apie sėkmingą / nesėkmingą pranešimų pristatymą.
ReliableUdpState.DisposeByTimeout:

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

Tai nepaisoma tik valstybėje Užbaigtas.
Baigta. DisposeByTimeout:

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

ProcessPackets metodas

ProcessPackets metodas yra atsakingas už papildomą paketo ar paketų apdorojimą. Skambinama tiesiogiai arba per paketo laukimo laikmatį.

Gali Surinkimas metodas yra nepaisomas ir yra atsakingas už prarastų paketų patikrinimą ir perėjimą į būseną Užbaigtas, gavus paskutinį paketą ir sėkmingai išlaikius patikrinimą
Surinkimas. Proceso paketai:

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

Gali Siuntimo ciklas Šis metodas iškviečiamas tik laikmačiu ir yra atsakingas už paskutinio pranešimo pakartotinį siuntimą, taip pat už ryšio uždarymo laikmačio įjungimą.
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);
}

Gali Užbaigtas metodas sustabdo veikiantį laikmatį ir siunčia pranešimą abonentams.
Užbaigta. ProcessPackets:

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

Gauti paketą metodas

Gali FirstPacketReceived pagrindinė metodo užduotis yra nustatyti, ar pirmasis pranešimų paketas iš tikrųjų atkeliavo į sąsają, taip pat surinkti pranešimą, susidedantį iš vieno paketo.
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);
  }
}

Gali Siuntimo ciklas Šis metodas yra nepaisomas, kad būtų priimti pristatymo patvirtinimai ir pakartotinio siuntimo užklausos.
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));
}

Gali Surinkimas taikant ReceivePacket metodą, vyksta pagrindinis žinutės surinkimo iš gaunamų paketų darbas.
Surinkimas.Gauti paketą:

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

Gali Užbaigtas vienintelė metodo užduotis – išsiųsti pakartotinį sėkmingo pranešimo pristatymo patvirtinimą.
Užbaigta.Gauti paketą:

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

Paketo siuntimo metodas

Gali FirstPacketSending šiuo būdu išsiunčiamas pirmasis duomenų paketas arba, jei žinutei nereikia patvirtinti pristatymo, visas pranešimas.
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);
}

Gali Siuntimo ciklas šiuo metodu siunčiamas paketų blokas.
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 );
  }
}

Giliau į kodą. Ryšių kūrimas ir užmezgimas

Dabar, kai pamatėme pagrindines būsenas ir metodus, naudojamus būsenoms tvarkyti, panagrinėkime kelis protokolo veikimo pavyzdžius šiek tiek išsamiau.
Duomenų perdavimo schema normaliomis sąlygomis:Patikimo Udp protokolo įgyvendinimas .Net

Išsamiai apsvarstykite kūrimą ryšio įrašas prisijungti ir išsiųsti pirmąjį paketą. Perkėlimą visada inicijuoja programa, kuri iškviečia siuntimo pranešimo API. Toliau iškviečiamas perdavimo valdymo bloko metodas StartTransmission, kuris pradeda naujo pranešimo duomenų perdavimą.
Išeinančio ryšio sukūrimas:

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

Pirmojo paketo siuntimas (FirstPacketSending būsena):

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

Išsiuntus pirmąjį paketą, siuntėjas patenka į būseną Siuntimo ciklas – laukti siuntos pristatymo patvirtinimo.
Priimančioji pusė, naudodama EndReceive metodą, priima išsiųstą paketą, sukuria naują ryšio įrašas ir perduoda šį paketą su iš anksto išanalizuota antrašte apdorojimo būsenos metodui ReceivePacket FirstPacketReceived
Ryšio sukūrimas priimančiojoje pusėje:

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

Pirmojo paketo gavimas ir patvirtinimo siuntimas (FirstPacketReceived būsena):

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

Giliau į kodą. Ryšys uždaromas pasibaigus laikui

Skirtojo laiko tvarkymas yra svarbi patikimo UDP dalis. Apsvarstykite pavyzdį, kai tarpinis mazgas nepavyko ir duomenų pristatymas abiem kryptimis tapo neįmanomas.
Ryšio uždarymo pasibaigus laikui diagrama:Patikimo Udp protokolo įgyvendinimas .Net

Kaip matyti iš diagramos, siuntėjo darbo laikmatis pradeda veikti iškart po paketų bloko išsiuntimo. Tai atsitinka naudojant būsenos SendPacket metodą Siuntimo ciklas.
Darbo laikmačio įjungimas (SendingCycle būsena):

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

Laikmačio laikotarpiai nustatomi užmezgant ryšį. Numatytasis „ShortTimerPeriod“ yra 5 sekundės. Pavyzdyje jis nustatytas į 1,5 sekundės.

Įeinančiam ryšiui laikmatis paleidžiamas gavus paskutinį įeinantį duomenų paketą, tai atsitinka būsenos ReceivePacket metodu Surinkimas
Darbo laikmačio įjungimas (surinkimo būsena):

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

Laukiant darbo laikmačio į gaunamą ryšį nebegavo jokių paketų. Laikmatis išsijungė ir iškvietė ProcessPackets metodą, kur buvo rasti prarasti paketai ir pirmą kartą išsiųstos pakartotinio pristatymo užklausos.
Pakartotinio pristatymo užklausų siuntimas (surinkimo būsena):

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

Kintamasis TimerSecondTry nustatytas į tiesa. Šis kintamasis yra atsakingas už darbo laikmačio paleidimą iš naujo.

Siuntėjo pusėje taip pat suveikia darbo laikmatis ir iš naujo siunčiamas paskutinis išsiųstas paketas.
Ryšio uždarymo laikmačio įjungimas (SendingCycle būsena):

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

Po to ryšio uždarymo laikmatis pradeda veikti išeinančiame ryšyje.
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);
}

Pagal numatytuosius nustatymus ryšio uždarymo laikmačio skirtasis laikas yra 30 sekundžių.

Po trumpo laiko vėl įsijungia darbinis laikmatis gavėjo pusėje, vėl siunčiamos užklausos, po kurių pradedamas įeinančio ryšio uždarymo laikmatis

Kai užsidaro uždarymo laikmačiai, išleidžiami visi abiejų ryšio įrašų ištekliai. Siuntėjas praneša apie pristatymo sutrikimą ankstesnei programai (žr. Patikimas UDP API).
Ryšio įrašo išteklių atleidimas:

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

Giliau į kodą. Duomenų perdavimo atkūrimas

Duomenų perdavimo atkūrimo diagrama praradus paketus:Patikimo Udp protokolo įgyvendinimas .Net

Kaip jau buvo aptarta nutraukiant ryšį pasibaigus laikui, kai baigiasi darbo laikmačio laikas, imtuvas patikrins, ar nėra prarastų paketų. Paketų praradimo atveju bus sudarytas gavėjo nepasiekusių paketų sąrašas. Šie skaičiai įvedami į konkretaus ryšio LostPackets masyvą ir siunčiami pakartotinio pristatymo užklausos.
Užklausų siuntimas pakartotinai pristatyti paketus (surinkimo būsena):

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

Siuntėjas priims pakartotinio pristatymo užklausą ir išsiųs trūkstamus paketus. Verta paminėti, kad šiuo metu siuntėjas jau įjungė ryšio uždarymo laikmatį ir, gavus užklausą, atstatomas.
Pamestų paketų pakartotinis siuntimas (SendingCycle būsena):

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

Persiunčiamas paketas (paketas Nr. 3 diagramoje) gaunamas įeinančio ryšio. Patikrinama, ar pilnas priėmimo langas ir atkurtas įprastas duomenų perdavimas.
Patikimų tikrinimas gavimo lange (surinkimo būsena):

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

Patikima UDP API

Norint sąveikauti su duomenų perdavimo protokolu, yra atvira Patikimo Udp klasė, kuri yra perdavimo valdymo bloko įvyniojimas. Čia yra svarbiausi klasės nariai:

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

Žinutės gaunamos prenumeruojant. Atšaukimo metodo atstovo parašas:

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

Pranešimas:

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

Norint užsiprenumeruoti konkretų pranešimo tipą ir (arba) konkretų siuntėją, naudojami du pasirenkami parametrai: ReliableUdpMessageTypes messageType ir IPEndPoint ipEndPoint.

Pranešimų tipai:

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

Pranešimas siunčiamas asinchroniškai; tam protokolas įgyvendina asinchroninio programavimo modelį:

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

Pranešimo išsiuntimo rezultatas bus teisingas - jei pranešimas sėkmingai pasiekė gavėją ir klaidingas - jei ryšys buvo nutrauktas pasibaigus laikui:

public bool EndSendMessage(IAsyncResult asyncResult)

išvada

Šiame straipsnyje daug kas nebuvo aprašyta. Gijų suderinimo mechanizmai, išimčių ir klaidų tvarkymas, asinchroninių pranešimų siuntimo metodų įgyvendinimas. Bet protokolo esmė, paketų apdorojimo, ryšio užmezgimo ir skirtojo laiko apdorojimo logikos aprašymas turėtų būti jums aiškus.

Parodyta patikimo pristatymo protokolo versija yra pakankamai tvirta ir lanksti, kad atitiktų anksčiau nustatytus reikalavimus. Tačiau noriu pridurti, kad aprašytą įgyvendinimą galima patobulinti. Pavyzdžiui, norint padidinti pralaidumą ir dinamiškai keisti laikmačio periodus, prie protokolo galima pridėti tokius mechanizmus kaip stumdomas langas ir RTT, taip pat bus naudinga įdiegti MTU tarp ryšio mazgų nustatymo mechanizmą (bet tik tuo atveju, jei siunčiami dideli pranešimai). .

Dėkoju už dėmesį, laukiu jūsų komentarų ir komentarų.

PS Tiems, kurie domisi detalėmis ar tiesiog nori išbandyti protokolą, nuoroda į projektą GitHube:
Patikimas UDP projektas

Naudingos nuorodos ir straipsniai

  1. TCP protokolo specifikacija: anglų kalba и rusiškai
  2. UDP protokolo specifikacija: anglų kalba и rusiškai
  3. RUDP protokolo aptarimas: draft-ietf-sigtran-reliable-udp-00
  4. Patikimas duomenų protokolas: RFK 908 и RFK 1151
  5. Paprastas pristatymo patvirtinimo per UDP įgyvendinimas: Visiškai valdykite savo tinklą naudodami .NET ir UDP
  6. Straipsnis, kuriame aprašomi NAT perėjimo mechanizmai: Lygiavertis ryšys tarp tinklo adresų vertėjų
  7. Asinchroninio programavimo modelio įgyvendinimas: CLR asinchroninio programavimo modelio įgyvendinimas и Kaip įdiegti IAsyncResult dizaino modelį
  8. Asinchroninio programavimo modelio perkėlimas į užduotimis pagrįstą asinchroninį modelį (APM TAP):
    TPL ir tradicinis .NET asinchroninis programavimas
    Sąveika su kitais asinchroniniais modeliais ir tipais

Atnaujinimas: Ačiū Mayorovp и sidristij už idėją pridėti užduotį prie sąsajos. Bibliotekos suderinamumas su senomis operacinėmis sistemomis nepažeidžiamas, nes 4-oji sistema palaiko ir XP, ir 2003 serverį.

Šaltinis: www.habr.com

Добавить комментарий