Luotettavan Udp-protokollan käyttöönotto .Netille

Internet on muuttunut kauan sitten. Yksi Internetin tärkeimmistä protokollista - UDP:tä käytetään sovelluksissa datagrammien ja lähetysten toimittamiseen, mutta myös "vertais"-yhteyksien tarjoamiseen verkkosolmujen välillä. Yksinkertaisen suunnittelunsa ansiosta tällä protokollalla on monia aiemmin suunnittelemattomia käyttötarkoituksia, mutta protokollan puutteet, kuten toimitustakuun puute, eivät ole kadonneet mihinkään. Tässä artikkelissa kuvataan taatun toimitusprotokollan käyttöönotto UDP:n kautta.
Sisältö:Merkintä
Protokollavaatimukset
Luotettava UDP-otsikko
Protokollan yleiset periaatteet
Aikakatkaisut ja protokollaajastimet
Luotettava UDP-lähetyksen tilakaavio
Syvemmälle koodiin. voimansiirron ohjausyksikkö
Syvemmälle koodiin. valtioita

Syvemmälle koodiin. Yhteyksien luominen ja luominen
Syvemmälle koodiin. Yhteyden sulkeminen aikakatkaisun jälkeen
Syvemmälle koodiin. Tiedonsiirron palauttaminen
Luotettava UDP API
Johtopäätös
Hyödyllisiä linkkejä ja artikkeleita

Merkintä

Internetin alkuperäinen arkkitehtuuri olettaa homogeenista osoiteavaruutta, jossa jokaisella solmulla oli globaali ja ainutlaatuinen IP-osoite ja joka pystyi kommunikoimaan suoraan muiden solmujen kanssa. Nyt Internetillä on itse asiassa erilainen arkkitehtuuri - yksi globaalien IP-osoitteiden alue ja monia alueita, joissa yksityiset osoitteet on piilotettu NAT-laitteiden taakse.Tässä arkkitehtuurissa vain globaalissa osoiteavaruudessa olevat laitteet voivat helposti kommunikoida kenen tahansa verkossa, koska niillä on ainutlaatuinen, maailmanlaajuisesti reititettävissä oleva IP-osoite. Yksityisen verkon solmu voi muodostaa yhteyden muihin saman verkon solmuihin ja myös muihin maailmanlaajuisen osoiteavaruuden tunnettuihin solmuihin. Tämä vuorovaikutus saavutetaan suurelta osin verkko-osoitteen muunnosmekanismin ansiosta. NAT-laitteet, kuten Wi-Fi-reitittimet, luovat erityisiä käännöstaulukkomerkintöjä lähteville yhteyksille ja muokkaavat IP-osoitteita ja porttinumeroita paketeissa. Tämä mahdollistaa lähtevät yhteydet yksityisestä verkosta globaalissa osoiteavaruudessa oleviin isänteihin. Mutta samaan aikaan NAT-laitteet yleensä estävät kaiken saapuvan liikenteen, ellei saapuville yhteyksille ole asetettu erillisiä sääntöjä.

Tämä Internetin arkkitehtuuri on riittävän oikea asiakas-palvelin-viestintään, jossa asiakkaat voivat olla yksityisissä verkoissa ja palvelimilla on globaali osoite. Mutta se vaikeuttaa kahden solmun suoraa yhteyttä toisiinsa eri yksityiset verkot. Suora yhteys kahden solmun välillä on tärkeä peer-to-peer-sovelluksissa, kuten puheensiirrossa (Skype), etäkäytössä tietokoneessa (TeamViewer) tai online-pelaamisessa.

Yksi tehokkaimmista menetelmistä peer-to-peer-yhteyden muodostamiseksi eri yksityisten verkkojen laitteiden välille on nimeltään rei'itys. Tätä tekniikkaa käytetään yleisimmin UDP-protokollaan perustuvissa sovelluksissa.

Mutta jos sovelluksesi tarvitsee taatun tiedon toimituksen, esimerkiksi siirrät tiedostoja tietokoneiden välillä, UDP:n käytössä on monia vaikeuksia, koska UDP ei ole taattu toimitusprotokolla eikä tarjoa pakettien toimitusta järjestyksessä, toisin kuin TCP. protokollaa.

Tässä tapauksessa taatun pakettien toimituksen varmistamiseksi tarvitaan sovelluskerroksen protokolla, joka tarjoaa tarvittavat toiminnot ja toimii UDP:n yli.

Haluan heti huomauttaa, että on olemassa TCP-rei'itystekniikka TCP-yhteyksien muodostamiseen solmujen välille eri yksityisissä verkoissa, mutta koska monet NAT-laitteet eivät tue sitä, sitä ei yleensä pidetä pääasiallisena yhteyden muodostamisena. sellaiset solmut.

Tämän artikkelin loppuosassa keskityn vain taatun toimitusprotokollan toteuttamiseen. UDP-rei'itystekniikan toteutus kuvataan seuraavissa artikkeleissa.

Protokollavaatimukset

  1. Luotettava pakettien toimitus positiivisen palautemekanismin kautta (ns. positiivinen kuittaus)
  2. Suuren datan tehokkaan siirron tarve, ts. protokollan tulee välttää tarpeetonta pakettien välitystä
  3. Toimituksen vahvistusmekanismin (kyky toimia "puhtaan" UDP-protokollana) pitäisi olla mahdollista peruuttaa
  4. Kyky toteuttaa komentotila jokaisen viestin vahvistuksella
  5. Protokollan yli tapahtuvan tiedonsiirron perusyksikön tulee olla viesti

Nämä vaatimukset ovat suurelta osin yhtenevät kohdassa kuvattujen Reliable Data Protocol -vaatimusten kanssa RFK 908 и RFK 1151, ja luotin näihin standardeihin tätä protokollaa kehitettäessä.

Näiden vaatimusten ymmärtämiseksi tarkastellaan tiedonsiirron ajoitusta kahden TCP- ja UDP-protokollia käyttävän verkkosolmun välillä. Olkoon molemmissa tapauksissa yksi paketti kadonnut.
Ei-interaktiivisten tietojen siirto TCP:n kautta:Luotettavan Udp-protokollan käyttöönotto .Netille

Kuten kaaviosta näkyy, pakettien katoamisen tapauksessa TCP havaitsee kadonneen paketin ja raportoi sen lähettäjälle pyytämällä kadonneen segmentin numeroa.
Tiedonsiirto UDP-protokollan kautta:Luotettavan Udp-protokollan käyttöönotto .Netille

UDP ei suorita mitään häviön havaitsemisvaiheita. UDP-protokollan lähetysvirheiden hallinta on täysin sovelluksen vastuulla.

Virheiden havaitseminen TCP-protokollassa saavutetaan muodostamalla yhteys päätesolmuun, tallentamalla kyseisen yhteyden tila, osoittamalla kussakin paketin otsikossa lähetettyjen tavujen lukumäärä ja ilmoittamalla kuittauksista kuittausnumerolla.

Lisäksi suorituskyvyn parantamiseksi (eli useamman kuin yhden segmentin lähettämiseksi vastaanottamatta kuittausta) TCP-protokolla käyttää niin kutsuttua lähetysikkunaa - datatavumäärää, jonka segmentin lähettäjä odottaa vastaanottavansa.

Lisätietoja TCP-protokollasta on kohdassa RFK 793, UDP:stä kohteeseen RFK 768missä ne itse asiassa määritellään.

Edellä olevan perusteella on selvää, että luotettavan viestinvälitysprotokollan luomiseksi UDP:n yli (jäljempänä ns. Luotettava UDP), sen on toteutettava TCP:n kaltaisia ​​tiedonsiirtomekanismeja. Nimittäin:

  • tallenna yhteyden tila
  • käytä segmenttinumerointia
  • käytä erityisiä vahvistuspaketteja
  • Käytä yksinkertaistettua ikkunointimekanismia protokollan suorituskyvyn lisäämiseksi

Lisäksi tarvitset:

  • ilmoittaa viestin alkamisesta resurssien varaamiseksi yhteydelle
  • viestiä viestin lopusta, välittääkseen vastaanotetun viestin ylävirran sovellukselle ja vapauttaakseen protokollaresursseja
  • salli yhteyskohtaisen protokollan estää toimituksen vahvistusmekanismin toimimasta "puhtaan" UDP:nä

Luotettava UDP-otsikko

Muista, että UDP-datagrammi on kapseloitu IP-datagrammiin. Luotettava UDP-paketti "kääritään" asianmukaisesti UDP-datagrammiin.
Luotettava UDP-otsikon kapselointi:Luotettavan Udp-protokollan käyttöönotto .Netille

Luotettavan UDP-otsikon rakenne on melko yksinkertainen:

Luotettavan Udp-protokollan käyttöönotto .Netille

  • Liput - paketin valvontaliput
  • MessageType - viestityyppi, jota ylävirran sovellukset käyttävät tiettyjen viestien tilaamiseen
  • TransmissionId - lähetyksen numero yhdessä vastaanottajan osoitteen ja portin kanssa, joka yksilöi yhteyden
  • PacketNumber - paketin numero
  • Options - lisäprotokollaasetukset. Ensimmäisen paketin tapauksessa sitä käytetään ilmaisemaan viestin koko

Liput ovat seuraavat:

  • FirstPacket - viestin ensimmäinen paketti
  • NoAsk - viesti ei vaadi kuittausmekanismin ottamista käyttöön
  • LastPacket - viestin viimeinen paketti
  • RequestForPacket - vahvistuspaketti tai pyyntö kadonneesta paketista

Protokollan yleiset periaatteet

Koska Reliable UDP keskittyy taattuun sanomanvälitykseen kahden solmun välillä, sen on kyettävä muodostamaan yhteys toisen puolen kanssa. Yhteyden muodostamiseksi lähettäjä lähettää paketin FirstPacket-lipulla, johon vastaus tarkoittaa, että yhteys on muodostettu. Kaikki vastauspaketit tai toisin sanoen kuittauspaketit asettavat PacketNumber-kentän arvoksi aina yhden suuremman kuin onnistuneesti vastaanotettujen pakettien suurimman PacketNumber-arvon. Ensimmäisen lähetetyn paketin Asetukset-kenttä on viestin koko.

Samanlaista mekanismia käytetään yhteyden katkaisemiseen. LastPacket-lippu asetetaan viestin viimeiseen pakettiin. Vastauspaketissa ilmoitetaan viimeisen paketin numero + 1, mikä tarkoittaa vastaanottavalle puolelle viestin onnistunutta toimittamista.
Kytkentä- ja päätekaavio:Luotettavan Udp-protokollan käyttöönotto .Netille

Kun yhteys on muodostettu, tiedonsiirto alkaa. Tiedot siirretään pakettilohkoina. Jokainen lohko, paitsi viimeinen, sisältää kiinteän määrän paketteja. Se on yhtä suuri kuin vastaanotto-/lähetysikkunan koko. Viimeisessä tietolohkossa voi olla vähemmän paketteja. Kunkin lohkon lähettämisen jälkeen lähettävä puoli odottaa toimitusvahvistusta tai pyyntöä toimittaa kadonneet paketit uudelleen jättäen vastaanotto-/lähetysikkunan auki vastaanottamaan vastauksia. Saatuaan vahvistuksen lohkon toimituksesta vastaanotto-/lähetysikkuna siirtyy ja seuraava datalohko lähetetään.

Vastaanottava puoli vastaanottaa paketit. Jokainen paketti tarkistetaan sen selvittämiseksi, kuuluuko se lähetysikkunaan. Paketit ja kaksoiskappaleet, jotka eivät putoa ikkunaan, suodatetaan pois. Koska Jos ikkunan koko on kiinteä ja sama vastaanottajalle ja lähettäjälle, niin jos pakettilohko toimitetaan ilman häviötä, ikkuna siirtyy vastaanottamaan seuraavan tietolohkon paketteja ja lähetetään toimitusvahvistus. lähetetty. Jos ikkuna ei täyty työajastimen asettamassa ajassa, käynnistetään tarkastus siitä, mitä paketteja ei ole toimitettu ja lähetetään uudelleentoimituspyyntöjä.
Uudelleenlähetyskaavio:Luotettavan Udp-protokollan käyttöönotto .Netille

Aikakatkaisut ja protokollaajastimet

On useita syitä, miksi yhteyttä ei voida muodostaa. Esimerkiksi jos vastaanottava osapuoli on offline-tilassa. Tässä tapauksessa yhteys suljetaan aikakatkaisulla, kun yrität muodostaa yhteyttä. Luotettava UDP-toteutus käyttää kahta ajastinta aikakatkaisujen asettamiseen. Ensimmäistä, työajastinta, käytetään odottamaan vastausta etäisännältä. Jos se laukeaa lähettäjän puolella, viimeksi lähetetty paketti lähetetään uudelleen. Jos ajastin vanhenee vastaanottajan kohdalla, suoritetaan kadonneiden pakettien tarkistus ja lähetetään uudelleentoimituspyynnöt.

Toinen ajastin tarvitaan sulkemaan yhteys, jos solmujen välinen kommunikaatio puuttuu. Lähettäjäpuolella se käynnistyy välittömästi työajastimen umpeutumisen jälkeen ja odottaa vastausta etäsolmulta. Jos vastausta ei saada määritetyn ajan kuluessa, yhteys katkaistaan ​​ja resurssit vapautetaan. Vastaanottavan puolen yhteyden sulkemisajastin käynnistetään sen jälkeen, kun työajastin vanhenee kahdesti. Tämä on tarpeen vahvistuspaketin katoamisen varalta. Kun ajastin umpeutuu, myös yhteys katkeaa ja resurssit vapautuvat.

Luotettava UDP-lähetyksen tilakaavio

Protokollan periaatteet on toteutettu äärellistilakoneessa, jonka jokainen tila vastaa tietystä paketinkäsittelylogiikasta.
Luotettava UDP-tilakaavio:

Luotettavan Udp-protokollan käyttöönotto .Netille

Suljettu - ei todellakaan ole tila, se on automaatin alku- ja loppupiste. Valtion puolesta Suljettu vastaanotetaan lähetyksen ohjauslohko, joka asynkronisen UDP-palvelimen toteuttaen välittää paketit asianmukaisille yhteyksille ja aloittaa tilankäsittelyn.

FirstPacketSending – lähtötila, jossa lähtevä yhteys on, kun viesti lähetetään.

Tässä tilassa lähetetään ensimmäinen paketti normaaleille viesteille. Viesteissä ilman lähetysvahvistusta tämä on ainoa tila, jossa koko viesti lähetetään.

Lähetyssykli – perustila viestipakettien lähettämistä varten.

Siirtyminen siihen osavaltiosta FirstPacketSending suoritetaan sen jälkeen, kun ensimmäinen viestin paketti on lähetetty. Tässä tilassa kaikki kuittaukset ja uudelleenlähetyspyynnöt tulevat. Siitä poistuminen on mahdollista kahdessa tapauksessa - viestin onnistuneen toimituksen yhteydessä tai aikakatkaisulla.

FirstPacketReceived – viestin vastaanottajan alkutila.

Se tarkistaa lähetyksen alun oikeellisuuden, luo tarvittavat rakenteet ja lähettää kuittauksen ensimmäisen paketin vastaanottamisesta.

Viestille, joka koostuu yhdestä paketista ja lähetettiin ilman toimitustodistusta, tämä on ainoa tila. Kun tällainen viesti on käsitelty, yhteys suljetaan.

kokoaminen – perustila viestipakettien vastaanottamiseksi.

Se kirjoittaa paketteja väliaikaiseen tallennustilaan, tarkistaa pakettien katoamisen, lähettää kuittaukset pakettilohkon ja koko viestin toimittamisesta sekä lähettää pyyntöjä kadonneiden pakettien uudelleentoimittamisesta. Jos koko viesti vastaanotetaan onnistuneesti, yhteys siirtyy tilaan Valmistunut, muuten aikakatkaisu päättyy.

Valmistunut – yhteyden sulkeminen, jos koko viesti on vastaanotettu.

Tämä tila on välttämätön viestin kokoamiseksi ja siinä tapauksessa, että viestin toimitusvahvistus katosi matkalla lähettäjälle. Tästä tilasta poistuu aikakatkaisu, mutta yhteys katsotaan onnistuneesti suljetuksi.

Syvemmälle koodiin. voimansiirron ohjausyksikkö

Yksi luotettavan UDP:n avainelementeistä on lähetyksen ohjauslohko. Tämän lohkon tehtävänä on tallentaa nykyiset yhteydet ja apuelementit, jakaa saapuvat paketit vastaaville yhteyksille, tarjota rajapinta pakettien lähettämiseksi yhteyteen sekä toteuttaa protokolla-API. Lähetyksen ohjauslohko vastaanottaa paketit UDP-kerrokselta ja välittää ne edelleen tilakoneelle käsittelyä varten. Pakettien vastaanottamiseksi se toteuttaa asynkronisen UDP-palvelimen.
Jotkut ReliableUdpConnectionControlBlock-luokan jäsenet:

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

Asynkronisen UDP-palvelimen käyttöönotto:

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

Jokaista viestin siirtoa varten luodaan rakenne, joka sisältää tiedot yhteydestä. Tällaista rakennetta kutsutaan yhteystietue.
Jotkut ReliableUdpConnectionRecord-luokan jäsenet:

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

Syvemmälle koodiin. valtioita

Osavaltiot toteuttavat Reliable UDP -protokollan tilakoneen, jossa pakettien pääkäsittely tapahtuu. Abstrakti luokka ReliableUdpState tarjoaa käyttöliittymän tilalle:

Luotettavan Udp-protokollan käyttöönotto .Netille

Protokollan koko logiikka on toteutettu yllä esitetyillä luokilla yhdessä apuluokan kanssa, joka tarjoaa staattisia menetelmiä, kuten esimerkiksi ReliableUdp-otsikon muodostamisen yhteystietueesta.

Seuraavaksi tarkastelemme yksityiskohtaisesti protokollan perusalgoritmit määrittävien liitäntämenetelmien toteutusta.

DisposeByTimeout -menetelmä

DisposeByTimeout-menetelmä vastaa yhteysresurssien vapauttamisesta aikakatkaisun jälkeen ja viestien onnistumisesta/epäonnistumisesta.
ReliableUdpState.DisposeByTimeout:

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

Se on ohitettu vain osavaltiossa Valmistunut.
Valmis.DisposeByTimeout:

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

ProcessPackets-menetelmä

ProcessPackets-menetelmä vastaa paketin tai pakettien lisäkäsittelystä. Soitetaan suoraan tai paketin odotusajastimen kautta.

Kunnossa kokoaminen menetelmä on ohitettu ja vastaa kadonneiden pakettien tarkistamisesta ja siirtymisestä tilaan Valmistunut, jos vastaanotat viimeisen paketin ja läpäisit onnistuneen tarkistuksen
Kokoaminen. Prosessipaketit:

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

Kunnossa Lähetyssykli tätä menetelmää kutsutaan vain ajastimella, ja se vastaa viimeisen viestin uudelleen lähettämisestä sekä yhteyden sulkemisajastimen käyttöönotosta.
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);
}

Kunnossa Valmistunut menetelmä pysäyttää käynnissä olevan ajastimen ja lähettää viestin tilaajille.
Completed.ProcessPackets:

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

ReceivePacket Method

Kunnossa FirstPacketReceived menetelmän päätehtävänä on selvittää, onko ensimmäinen viestipaketti todella saapunut rajapinnalle, ja myös kerätä yhdestä paketista koostuva viesti.
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);
  }
}

Kunnossa Lähetyssykli tämä menetelmä ohitetaan toimituskuittausten ja uudelleenlähetyspyyntöjen hyväksymiseksi.
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));
}

Kunnossa kokoaminen ReceivePacket-menetelmässä pääasiallinen työ viestin kokoamisessa saapuvista paketeista tapahtuu.
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);
  }
}

Kunnossa Valmistunut menetelmän ainoa tehtävä on lähettää kuittaus viestin onnistuneesta toimituksesta.
Valmis.ReceivePacket:

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

Lähetä paketti -menetelmä

Kunnossa FirstPacketSending tämä menetelmä lähettää ensimmäisen tietopaketin tai jos viesti ei vaadi toimitusvahvistusta, koko viestin.
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);
}

Kunnossa Lähetyssykli tässä menetelmässä pakettilohko lähetetään.
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 );
  }
}

Syvemmälle koodiin. Yhteyksien luominen ja luominen

Nyt kun olemme nähneet perustilat ja tilojen käsittelyyn käytetyt menetelmät, puretaan muutama esimerkki protokollan toiminnasta hieman yksityiskohtaisemmin.
Tiedonsiirtokaavio normaaleissa olosuhteissa:Luotettavan Udp-protokollan käyttöönotto .Netille

Harkitse luomista yksityiskohtaisesti yhteystietue muodostaa yhteys ja lähettää ensimmäinen paketti. Siirron aloittaa aina sovellus, joka kutsuu lähetysviestin API:n. Seuraavaksi kutsutaan lähetyksen ohjauslohkon StartTransmission-menetelmää, joka aloittaa tiedonsiirron uutta viestiä varten.
Lähtevän yhteyden luominen:

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

Ensimmäisen paketin lähettäminen (FirstPacketSending-tila):

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

Ensimmäisen paketin lähettämisen jälkeen lähettäjä siirtyy tilaan Lähetyssykli – odota vahvistusta paketin toimituksesta.
Vastaanottava puoli EndReceive-menetelmää käyttäen vastaanottaa lähetetyn paketin, luo uuden yhteystietue ja välittää tämän paketin valmiiksi jäsennellyllä otsikolla tilan ReceivePacket-menetelmälle käsittelyä varten FirstPacketReceived
Yhteyden luominen vastaanottavalla puolella:

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

Ensimmäisen paketin vastaanottaminen ja kuittauksen lähettäminen (FirstPacketReceived-tila):

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

Syvemmälle koodiin. Yhteyden sulkeminen aikakatkaisun jälkeen

Aikakatkaisukäsittely on tärkeä osa luotettavaa UDP:tä. Harkitse esimerkkiä, jossa välisolmu epäonnistui ja tietojen toimittaminen molempiin suuntiin tuli mahdottomaksi.
Kaavio yhteyden sulkemisesta aikakatkaisulla:Luotettavan Udp-protokollan käyttöönotto .Netille

Kuten kaaviosta näkyy, lähettäjän työajastin käynnistyy heti pakettilohkon lähettämisen jälkeen. Tämä tapahtuu tilan SendPacket-menetelmässä Lähetyssykli.
Työajastimen käyttöönotto (SendingCycle-tila):

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

Ajastimen jaksot asetetaan, kun yhteys muodostetaan. Oletusarvoinen ShortTimerPeriod on 5 sekuntia. Esimerkissä se on asetettu 1,5 sekuntiin.

Saapuvan yhteyden ajastin käynnistyy viimeisen saapuvan datapaketin vastaanottamisen jälkeen, tämä tapahtuu tilan ReceivePacket-metodissa kokoaminen
Työajastimen käyttöönotto (kokoamistila):

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

Saapuvalle yhteydelle ei saapunut enempää paketteja odotellessa työajastinta. Ajastin sammui ja kutsui ProcessPackets-menetelmän, jossa kadonneet paketit löydettiin ja uudelleentoimituspyyntöjä lähetettiin ensimmäistä kertaa.
Uudelleentoimituspyyntöjen lähettäminen (kokoamistila):

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

TimerSecondTry-muuttuja on asetettu arvoon totta. Tämä muuttuja on vastuussa työajastimen uudelleenkäynnistämisestä.

Lähettäjän puolella myös työajastin laukeaa ja viimeksi lähetetty paketti lähetetään uudelleen.
Yhteyden sulkemisajastimen ottaminen käyttöön (SendingCycle-tila):

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

Tämän jälkeen yhteyden sulkemisajastin käynnistyy lähtevässä yhteydessä.
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);
}

Yhteyden sulkemisajastimen aikakatkaisuaika on oletusarvoisesti 30 sekuntia.

Hetken kuluttua vastaanottajan puolella oleva työajastin laukeaa uudelleen, pyyntöjä lähetetään uudelleen, minkä jälkeen yhteyden sulkemisajastin käynnistyy tulevalle yhteydelle

Kun sulkemisajastimet käynnistyvät, molempien yhteystietueiden kaikki resurssit vapautetaan. Lähettäjä raportoi toimitushäiriöstä ylävirran sovellukselle (Katso Luotettava UDP API).
Yhteystietueresurssien vapauttaminen:

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

Syvemmälle koodiin. Tiedonsiirron palauttaminen

Tiedonsiirron palautuskaavio pakettien katoamisen yhteydessä:Luotettavan Udp-protokollan käyttöönotto .Netille

Kuten jo mainittiin yhteyden sulkemisessa aikakatkaisun yhteydessä, kun työajastin umpeutuu, vastaanotin tarkistaa kadonneiden pakettien varalta. Pakettien katoamisen sattuessa laaditaan luettelo pakettien määrästä, jotka eivät saapuneet vastaanottajalle. Nämä numerot syötetään tietyn yhteyden LostPackets-taulukkoon ja lähetetään uudelleentoimituspyynnöt.
Pyyntöjen lähettäminen pakettien uudelleentoimittamiseen (kokoamistila):

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

Lähettäjä hyväksyy uudelleentoimituspyynnön ja lähettää puuttuvat paketit. On syytä huomata, että tällä hetkellä lähettäjä on jo käynnistänyt yhteyden sulkemisajastimen ja kun pyyntö vastaanotetaan, se nollataan.
Kadonneiden pakettien uudelleenlähetys (SendingCycle-tila):

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

Uudelleenlähetetty paketti (paketti #3 kaaviossa) vastaanotetaan saapuvan yhteyden kautta. Tarkistetaan, onko vastaanottoikkuna täynnä ja palautuuko normaali tiedonsiirto.
Osumien tarkistaminen vastaanottoikkunassa (kokoamistila):

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

Luotettava UDP API

Tiedonsiirtoprotokollan kanssa vuorovaikutusta varten on olemassa avoin Reliable Udp -luokka, joka on siirron ohjauslohkon kääre. Tässä luokan tärkeimmät jäsenet:

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

Viestit vastaanotetaan tilauksella. Delegaattorin allekirjoitus takaisinsoittomenetelmälle:

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

Viesti:

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

Tietyn viestityypin ja/tai tietyn lähettäjän tilaamiseen käytetään kahta valinnaista parametria: ReliableUdpMessageTypes messageType ja IPEndPoint ipEndPoint.

Viestityypit:

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

Viesti lähetetään asynkronisesti, tätä varten protokolla toteuttaa asynkronisen ohjelmointimallin:

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

Viestin lähettämisen tulos on tosi - jos viesti on saapunut vastaanottajalle ja epätosi - jos yhteys suljettiin aikakatkaisun vuoksi:

public bool EndSendMessage(IAsyncResult asyncResult)

Johtopäätös

Paljon ei ole kuvattu tässä artikkelissa. Säikeen sovitusmekanismit, poikkeus- ja virhekäsittely, asynkronisten viestien lähetysmenetelmien toteutus. Mutta protokollan ytimen, pakettien käsittelyn, yhteyden muodostamisen ja aikakatkaisujen käsittelyn logiikan kuvauksen, pitäisi olla sinulle selvä.

Luotettavan toimitusprotokollan esitelty versio on riittävän vankka ja joustava täyttääkseen aiemmin määritellyt vaatimukset. Mutta haluan lisätä, että kuvattua toteutusta voidaan parantaa. Esimerkiksi suorituskyvyn lisäämiseksi ja ajastimen jaksojen dynaamiseksi muuttamiseksi protokollaan voidaan lisätä mekanismeja, kuten liukuva ikkuna ja RTT, on myös hyödyllistä toteuttaa mekanismi MTU:n määrittämiseksi yhteyssolmujen välillä (mutta vain jos lähetetään suuria viestejä) .

Kiitos huomiostasi, odotan kommenttejasi ja kommenttejasi.

PS Niille, jotka ovat kiinnostuneita yksityiskohdista tai haluavat vain testata protokollaa, linkki projektiin GitHubessa:
Luotettava UDP-projekti

Hyödyllisiä linkkejä ja artikkeleita

  1. TCP-protokollan erittely: englanniksi и на русском
  2. UDP-protokollan erittely: englanniksi и на русском
  3. Keskustelu RUDP-protokollasta: draft-ietf-sigtran-reliable-udp-00
  4. Luotettava dataprotokolla: RFK 908 и RFK 1151
  5. Toimitusvahvistuksen yksinkertainen toteutus UDP:n kautta: Hallitse verkkoasi täysin .NET:n ja UDP:n avulla
  6. Artikkeli, joka kuvaa NAT-läpikulkumekanismeja: Vertaisviestintä verkko-osoitteiden kääntäjien välillä
  7. Asynkronisen ohjelmointimallin toteutus: Asynkronisen CLR-ohjelmointimallin käyttöönotto и IAsyncResult-suunnittelumallin käyttöönotto
  8. Asynkronisen ohjelmointimallin siirtäminen tehtäväpohjaiseen asynkroniseen malliin (APM TAP:ssa):
    TPL ja perinteinen .NET asynkroninen ohjelmointi
    Yhteensopivuus muiden asynkronisten mallien ja tyyppien kanssa

Päivitys: Kiitos Mayorovp и sidristij ideasta tehtävän lisäämisestä käyttöliittymään. Kirjaston yhteensopivuutta vanhojen käyttöjärjestelmien kanssa ei rikota, koska Neljäs kehys tukee sekä XP- että 4-palvelinta.

Lähde: will.com

Lisää kommentti