A Reliable Udp protokoll megvalósítása a .Net számára

Az internet már régen megváltozott. Az Internet egyik fő protokollja - az UDP-t az alkalmazások nem csak datagramok és adások szállítására használják, hanem "peer-to-peer" kapcsolatok biztosítására is a hálózati csomópontok között. Ennek a protokollnak egyszerű kialakítása miatt számos korábban nem tervezett felhasználási területe van, azonban a protokoll hiányosságai, mint például a garantált kézbesítés hiánya, nem tűntek el sehol. Ez a cikk a garantált kézbesítési protokoll UDP-n keresztüli megvalósítását írja le.
Tartalom:Belépés
Protokollkövetelmények
Megbízható UDP fejléc
A protokoll általános elvei
Időtúllépések és protokoll időzítők
Megbízható UDP átviteli állapot diagram
Mélyebben a kódban. sebességváltó vezérlőegység
Mélyebben a kódban. Államok

Mélyebben a kódban. Kapcsolatok létrehozása és létrehozása
Mélyebben a kódban. A kapcsolat bezárása időtúllépéskor
Mélyebben a kódban. Adatátvitel helyreállítása
Megbízható UDP API
Következtetés
Hasznos linkek és cikkek

Belépés

Az Internet eredeti architektúrája egy homogén címteret feltételezett, amelyben minden csomópont globális és egyedi IP-címmel rendelkezik, és közvetlenül tudott kommunikálni más csomópontokkal. Az internet valójában más architektúrával rendelkezik – egy terület a globális IP-címekkel és sok olyan terület, ahol magáncímek rejtőznek a NAT-eszközök mögött.Ebben az architektúrában csak a globális címtérben lévő eszközök tudnak könnyen kommunikálni bárkivel a hálózaton, mert egyedi, globálisan irányítható IP-címmel rendelkeznek. A magánhálózaton lévő csomópontok csatlakozhatnak ugyanazon a hálózaton lévő többi csomóponthoz, és csatlakozhatnak a globális címtér más jól ismert csomópontjaihoz is. Ez az interakció nagyrészt a hálózati címfordítási mechanizmusnak köszönhető. A NAT-eszközök, például a Wi-Fi útválasztók speciális fordítási táblázat bejegyzéseket hoznak létre a kimenő kapcsolatokhoz, és módosítják az IP-címeket és a portszámokat a csomagokban. Ez lehetővé teszi a kimenő kapcsolatokat a magánhálózatról a globális címtérben lévő gazdagépekhez. Ugyanakkor a NAT-eszközök általában blokkolják az összes bejövő forgalmat, hacsak nem állítanak be külön szabályokat a bejövő kapcsolatokra.

Az Internetnek ez az architektúrája kellően megfelelő a kliens-szerver kommunikációhoz, ahol a kliensek magánhálózatokban lehetnek, és a szervereknek globális címük van. De ez megnehezíti a két csomópont közvetlen összekapcsolását különféle magánhálózatok. A két csomópont közötti közvetlen kapcsolat fontos a peer-to-peer alkalmazásokhoz, például a hangátvitelhez (Skype), a számítógéphez való távoli hozzáféréshez (TeamViewer) vagy az online játékokhoz.

A különböző magánhálózatokon lévő eszközök közötti peer-to-peer kapcsolat létrehozásának egyik leghatékonyabb módszere a lyukasztás. Ezt a technikát leggyakrabban UDP protokollon alapuló alkalmazásoknál használják.

De ha az alkalmazásnak garantált adatszállításra van szüksége, például fájlokat visz át számítógépek között, akkor az UDP használata sok nehézséggel jár, mivel az UDP nem garantált kézbesítési protokoll, és a TCP-vel ellentétben nem biztosítja rendben a csomagok kézbesítését. jegyzőkönyv.

Ebben az esetben a garantált csomagkézbesítés érdekében olyan alkalmazási réteg protokollt kell megvalósítani, amely biztosítja a szükséges funkcionalitást és működik UDP-n keresztül.

Azonnal szeretném megjegyezni, hogy létezik egy TCP-lyukasztási technika a különböző magánhálózatok csomópontjai közötti TCP-kapcsolatok létrehozására, de mivel sok NAT-eszköz nem támogatja ezt, általában nem ezt tekintik a csatlakozás fő módjának. ilyen csomópontok.

A cikk további részében csak a garantált kézbesítési protokoll megvalósítására összpontosítok. Az UDP lyukasztási technika megvalósítását a következő cikkek ismertetik.

Protokollkövetelmények

  1. Megbízható csomagküldés pozitív visszacsatolási mechanizmuson keresztül (ún. pozitív nyugtázás)
  2. A big data hatékony átvitelének igénye, pl. a protokollnak kerülnie kell a szükségtelen csomagtovábbítást
  3. Lehetővé kell tenni a kézbesítés-megerősítő mechanizmus törlését (a „tiszta” UDP-protokollként való működés képessége)
  4. Parancsmód megvalósításának lehetősége, minden üzenet megerősítésével
  5. A protokollon keresztüli adatátvitel alapegysége egy üzenet kell, hogy legyen

Ezek a követelmények nagyrészt egybeesnek a Reliable Data Protocol követelményeivel, amelyeket leírtunk Rfc 908 и Rfc 1151, és ezekre a szabványokra támaszkodtam a protokoll kidolgozásakor.

E követelmények megértéséhez nézzük meg a TCP és UDP protokollokat használó két hálózati csomópont közötti adatátvitel időzítését. Legyen mindkét esetben egy csomag elveszik.
Nem interaktív adatok átvitele TCP-n keresztül:A Reliable Udp protokoll megvalósítása a .Net számára

Ahogy az ábrán látható, csomagvesztés esetén a TCP észleli az elveszett csomagot, és az elveszett szegmens számának kérésével jelenti azt a feladónak.
Adatátvitel UDP protokollon keresztül:A Reliable Udp protokoll megvalósítása a .Net számára

Az UDP nem végez veszteségészlelési lépéseket. Az UDP protokoll átviteli hibáinak ellenőrzése teljes mértékben az alkalmazás felelőssége.

A TCP-protokollban a hibaészlelést úgy érik el, hogy kapcsolatot létesítenek egy végcsomóponttal, eltárolják a kapcsolat állapotát, jelzik az egyes csomagfejlécekben elküldött bájtok számát, és a nyugtákat egy nyugtázási számmal értesítik.

Ezenkívül a teljesítmény javítása érdekében (azaz egynél több szegmens küldése nyugtázás nélkül) a TCP-protokoll az úgynevezett átviteli ablakot használja - az adatbájtok számát, amelyet a szegmens küldője vár.

A TCP protokollról további információért lásd: Rfc 793, UDP-től ig Rfc 768ahol valójában meghatározzák.

A fentiekből kitűnik, hogy egy megbízható üzenetküldési protokoll létrehozása érdekében UDP-n keresztül (a továbbiakban: Megbízható UDP), a TCP-hez hasonló adatátviteli mechanizmusok megvalósítása szükséges. Ugyanis:

  • mentse a kapcsolat állapotát
  • szegmensszámozást használjon
  • használjon speciális visszaigazoló csomagokat
  • egyszerűsített ablakozási mechanizmust használjon a protokoll átviteli sebességének növelésére

Ezenkívül szüksége van:

  • jelzi az üzenet kezdetét, hogy erőforrásokat foglaljon le a kapcsolathoz
  • jelzi az üzenet végét, hogy a kapott üzenetet továbbítsa az upstream alkalmazásnak, és felszabadítsa a protokoll erőforrásait
  • lehetővé teszi a kapcsolat-specifikus protokoll számára, hogy letiltja a kézbesítés-megerősítő mechanizmust, hogy „tiszta” UDP-ként működjön

Megbízható UDP fejléc

Emlékezzünk vissza, hogy egy UDP-datagram egy IP-datagramba van beágyazva. A Megbízható UDP-csomag megfelelően "csomagolódik" egy UDP-datagramba.
Megbízható UDP-fejléc-beágyazás:A Reliable Udp protokoll megvalósítása a .Net számára

A Reliable UDP fejléc felépítése meglehetősen egyszerű:

A Reliable Udp protokoll megvalósítása a .Net számára

  • Flags - csomagvezérlő zászlók
  • MessageType – üzenettípus, amelyet az upstream alkalmazások használnak bizonyos üzenetekre való feliratkozáshoz
  • TransmissionId - az átvitel száma, a címzett címével és portjával együtt egyedileg azonosítja a kapcsolatot
  • PacketNumber – csomagszám
  • Opciók – további protokollopciók. Az első csomag esetében az üzenet méretének jelzésére szolgál

A zászlók a következők:

  • FirstPacket – az üzenet első csomagja
  • NoAsk – az üzenethez nincs szükség nyugtázási mechanizmus engedélyezésére
  • LastPacket – az üzenet utolsó csomagja
  • RequestForPacket – megerősítő csomag vagy kérés elveszett csomagra vonatkozóan

A protokoll általános elvei

Mivel a megbízható UDP a két csomópont közötti garantált üzenetátvitelre összpontosít, képesnek kell lennie arra, hogy kapcsolatot létesítsen a másik oldallal. A kapcsolat létrehozásához a feladó egy csomagot küld a FirstPacket jelzővel, amelyre adott válasz a kapcsolat létrejöttét jelenti. Minden válaszcsomag vagy más szóval nyugtázási csomag mindig eggyel többre állítja a PacketNumber mező értékét, mint a sikeresen fogadott csomagok legnagyobb PacketNumber értéke. Az elsőként elküldött csomag Opciók mezője az üzenet mérete.

Hasonló mechanizmust használnak a kapcsolat megszüntetésére. A LastPacket jelző az üzenet utolsó csomagján van beállítva. A válaszcsomagban az utolsó csomag száma + 1 van feltüntetve, ami a fogadó oldal számára az üzenet sikeres kézbesítését jelenti.
Csatlakoztatási és lezárási diagram:A Reliable Udp protokoll megvalósítása a .Net számára

A kapcsolat létrejötte után megkezdődik az adatátvitel. Az adatok továbbítása csomagok blokkjaiban történik. Az utolsó kivételével minden blokk meghatározott számú csomagot tartalmaz. Ez megegyezik a vételi/átviteli ablak méretével. Az utolsó adatblokk kevesebb csomagot tartalmazhat. Az egyes blokkok elküldése után a küldő oldal a kézbesítés visszaigazolására vagy az elveszett csomagok újbóli kézbesítésére vonatkozó kérésre vár, nyitva hagyva a fogadási/küldési ablakot a válaszok fogadására. A blokk kézbesítésének visszaigazolása után a fogadási/küldési ablak eltolódik, és a következő adatblokk kerül elküldésre.

A fogadó oldal fogadja a csomagokat. Minden egyes csomagot ellenőriznek, hogy beleesnek-e az átviteli ablakba. A program kiszűri azokat a csomagokat és másolatokat, amelyek nem esnek az ablakba. Mert Ha az ablak mérete fix és azonos a címzettnek és a feladónak, akkor egy csomagblokk veszteségmentes kézbesítése esetén az ablak eltolódik a következő adatblokk csomagjainak fogadására és a kézbesítés visszaigazolása küldött. Ha az ablak nem töltődik be a munkaidő-számláló által beállított időtartamon belül, akkor elindul a nem kézbesített csomagok ellenőrzése és az újrakézbesítési kérelmek elküldése.
Újraadási diagram:A Reliable Udp protokoll megvalósítása a .Net számára

Időtúllépések és protokoll időzítők

Számos oka lehet annak, ha nem lehet kapcsolatot létesíteni. Például, ha a fogadó fél offline állapotban van. Ebben az esetben a kapcsolat létrehozása során a kapcsolat időtúllépéssel lezárja. A megbízható UDP megvalósítás két időzítőt használ az időtúllépések beállításához. Az első, a munkaidőzítő arra szolgál, hogy várja a választ a távoli gazdagéptől. Ha a küldő oldalon aktiválódik, akkor az utoljára elküldött csomagot küldi újra. Ha az időzítő lejár a címzettnél, akkor a rendszer ellenőrzi az elveszett csomagokat, és elküldi az újrakézbesítési kérelmeket.

A második időzítő szükséges a kapcsolat lezárásához a csomópontok közötti kommunikáció hiánya esetén. A küldő oldalon a működési időzítő lejárta után azonnal elindul, és a távoli csomópont válaszára vár. Ha a megadott időtartamon belül nem érkezik válasz, a kapcsolat megszakad, és az erőforrások felszabadulnak. A fogadó oldalon a kapcsolatzárási időzítő a munkaidőzítő kétszeri lejárta után indul el. Erre azért van szükség, hogy biztosítsuk a visszaigazoló csomag elvesztését. Amikor az időzítő lejár, a kapcsolat is megszakad, és az erőforrások felszabadulnak.

Megbízható UDP átviteli állapot diagram

A protokoll alapelveit egy véges állapotú gépben valósítják meg, amelynek minden egyes állapota a csomagfeldolgozás bizonyos logikájáért felelős.
Megbízható UDP állapotdiagram:

A Reliable Udp protokoll megvalósítása a .Net számára

Zárva - valójában nem állapot, hanem az automata kezdő- és végpontja. Az államnak Zárva átvitelvezérlő blokk érkezik, amely egy aszinkron UDP szervert megvalósítva csomagokat továbbít a megfelelő kapcsolatokhoz és elindítja az állapotfeldolgozást.

FirstPacketSending – a kimenő kapcsolat kezdeti állapota az üzenet elküldésekor.

Ebben az állapotban a rendszer elküldi az első csomagot a normál üzenetekhez. Az elküldés megerősítése nélküli üzeneteknél ez az egyetlen állapot, ahol a teljes üzenet elküldésre kerül.

Küldésiciklus – alapállapot az üzenetcsomagok továbbításához.

Áttérés rá az államból FirstPacketSending az üzenet első csomagjának elküldése után történik. Ebben az állapotban érkezik minden visszaigazolás és újraküldési kérelem. Ebből két esetben lehet kilépni - sikeres üzenet kézbesítés esetén vagy időtúllépéssel.

FirstPacketReceived – az üzenet címzettjének kezdeti állapota.

Ellenőrzi az átvitel kezdetének helyességét, létrehozza a szükséges struktúrákat, és visszaigazolást küld az első csomag átvételéről.

Az egyetlen csomagból álló üzenet esetében, amelyet kézbesítési igazolás nélkül küldtek el, ez az egyetlen állapot. Az ilyen üzenet feldolgozása után a kapcsolat megszakad.

Összeszerelés – üzenetcsomagok fogadásának alapállapota.

Csomagokat ír az ideiglenes tárhelyre, ellenőrzi a csomagvesztést, nyugtázza a csomagok blokkjának és a teljes üzenet kézbesítését, és kéri az elveszett csomagok újrakézbesítését. A teljes üzenet sikeres fogadása esetén a kapcsolat állapotba kerül Befejezett, ellenkező esetben az időkorlát kilép.

Befejezett – a kapcsolat megszakítása a teljes üzenet sikeres beérkezése esetén.

Ez az állapot szükséges az üzenet összeállításához és arra az esetre, ha az üzenet kézbesítési visszaigazolása elveszett a feladóhoz vezető úton. Ebből az állapotból időtúllépés lép ki, de a kapcsolat sikeresen lezártnak tekinthető.

Mélyebben a kódban. sebességváltó vezérlőegység

A megbízható UDP egyik kulcseleme az átvitelvezérlő blokk. Ennek a blokknak az a feladata, hogy tárolja az aktuális kapcsolatokat és a segédelemeket, elosztja a bejövő csomagokat a megfelelő kapcsolatokhoz, interfészt biztosít a csomagok kapcsolathoz történő küldéséhez, valamint a protokoll API megvalósítása. Az átvitelvezérlő blokk fogadja a csomagokat az UDP rétegtől, és továbbítja azokat az állapotgépnek feldolgozásra. A csomagok fogadásához aszinkron UDP-kiszolgálót valósít meg.
A ReliableUdpConnectionControlBlock osztály néhány tagja:

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

Aszinkron UDP szerver megvalósítása:

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

Minden üzenetátvitelhez létrejön egy struktúra, amely információkat tartalmaz a kapcsolatról. Az ilyen szerkezetet ún kapcsolati rekord.
A ReliableUdpConnectionRecord osztály néhány tagja:

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

Mélyebben a kódban. Államok

Az államok a Reliable UDP protokoll állapotgépét valósítják meg, ahol a csomagok fő feldolgozása történik. A ReliableUdpState absztrakt osztály interfészt biztosít az állapothoz:

A Reliable Udp protokoll megvalósítása a .Net számára

A protokoll teljes logikáját a fent bemutatott osztályok valósítják meg, egy olyan segédosztállyal együtt, amely statikus módszereket biztosít, mint például a ReliableUdp fejléc összeállítása a kapcsolati rekordból.

Ezután részletesen megvizsgáljuk a protokoll alapvető algoritmusait meghatározó interfész metódusok megvalósítását.

DisposeByTimeout módszer

A DisposeByTimeout metódus felelős a kapcsolati erőforrások időkorlát utáni felszabadításáért és a sikeres/sikertelen üzenetkézbesítés jelzéséért.
ReliableUdpState.DisposeByTimeout:

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

Csak az államban van felülírva Befejezett.
Befejezve.DisposeByTimeout:

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

ProcessPackets módszer

A ProcessPackets metódus a csomag vagy csomagok további feldolgozásáért felelős. Közvetlenül vagy csomagvárakozó időzítőn keresztül hívható.

Állapotban Összeszerelés a metódus felül van írva, és felelős az elveszett csomagok ellenőrzéséért és az állapotba váltásért Befejezett, az utolsó csomag fogadása és sikeres ellenőrzés esetén
Összeállítás. ProcessPackets:

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

Állapotban Küldésiciklus ezt a metódust csak egy időzítő hívja meg, és felelős az utolsó üzenet újraküldéséért, valamint a kapcsolatzáró időzítő engedélyezéséért.
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);
}

Állapotban Befejezett a metódus leállítja a futó időzítőt és elküldi az üzenetet az előfizetőknek.
Completed.ProcessPackets:

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

ReceivePacket módszer

Állapotban FirstPacketReceived a metódus fő feladata annak megállapítása, hogy valóban megérkezett-e az első üzenetcsomag az interfészre, valamint összegyűjti az egyetlen csomagból álló üzenetet.
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);
  }
}

Állapotban Küldésiciklus ez a módszer felül van írva a kézbesítési visszaigazolások és az újraküldési kérések elfogadásához.
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));
}

Állapotban Összeszerelés a ReceivePacket metódusban a bejövő csomagokból üzenet összeállításának fő munkája zajlik.
Összeállítás.Csomag fogadása:

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

Állapotban Befejezett a metódus egyetlen feladata, hogy az üzenet sikeres kézbesítéséről ismételten visszaigazolást küldjön.
Befejezve.Csomag fogadása:

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

Csomagküldési mód

Állapotban FirstPacketSending ez a módszer elküldi az első adatcsomagot, vagy ha az üzenet nem igényel kézbesítési visszaigazolást, akkor a teljes üzenetet.
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);
}

Állapotban Küldésiciklus ennél a módszernél csomagblokkot küldenek.
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 );
  }
}

Mélyebben a kódban. Kapcsolatok létrehozása és létrehozása

Most, hogy láttuk az alapállapotokat és az állapotok kezelésére használt módszereket, részletezzünk néhány példát a protokoll működésére.
Adatátviteli diagram normál körülmények között:A Reliable Udp protokoll megvalósítása a .Net számára

Fontolja meg részletesen a létrehozást kapcsolati rekord a csatlakozáshoz és az első csomag elküldéséhez. Az átvitelt mindig az az alkalmazás kezdeményezi, amely meghívja az üzenetküldési API-t. Ezután az átvitelvezérlő blokk StartTransmission metódusa kerül meghívásra, amely elindítja az új üzenet adatátvitelét.
Kimenő kapcsolat létrehozása:

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

Az első csomag küldése (FirstPacketSending állapot):

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

Az első csomag elküldése után a feladó állapotba lép Küldésiciklus – várja meg a csomag kézbesítésének visszaigazolását.
A fogadó oldal az EndReceive metódussal fogadja az elküldött csomagot, újat hoz létre kapcsolati rekord és átadja ezt a csomagot egy előre elemzett fejléccel az állapot ReceivePacket metódusának feldolgozásra FirstPacketReceived
Kapcsolat létrehozása a fogadó oldalon:

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

Az első csomag fogadása és visszaigazolás küldése (FirstPacketReceived állapot):

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

Mélyebben a kódban. A kapcsolat bezárása időtúllépéskor

Az időtúllépés kezelése a megbízható UDP fontos része. Tekintsünk egy példát, amelyben egy köztes csomópont meghibásodott, és az adattovábbítás mindkét irányban lehetetlenné vált.
A kapcsolat időtúllépéssel történő lezárásának diagramja:A Reliable Udp protokoll megvalósítása a .Net számára

Amint az ábrán látható, a küldő munkaidőzítője egy csomagblokk elküldése után azonnal elindul. Ez az állapot SendPacket metódusában történik Küldésiciklus.
A munkaidőzítő engedélyezése (SendingCycle állapot):

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

Az időzítő periódusait a kapcsolat létrehozásakor állítja be. Az alapértelmezett ShortTimerPeriod 5 másodperc. A példában 1,5 másodpercre van beállítva.

Bejövő kapcsolat esetén az időzítő az utolsó bejövő adatcsomag vétele után indul el, ez az állapot ReceivePacket metódusában történik Összeszerelés
Munkaidőzítő engedélyezése (összeállítási állapot):

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

Nem érkezett több csomag a bejövő kapcsolatra, miközben a munkaidőzítőre várt. Az időzítő kikapcsolt, és meghívta a ProcessPackets metódust, ahol megtalálták az elveszett csomagokat, és első alkalommal küldték el az újrakézbesítési kérelmeket.
Újbóli kézbesítési kérelmek küldése (összeállítás állapota):

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

A TimerSecondTry változó értéke igaz. Ez a változó felelős a munkaidőzítő újraindításáért.

A küldő oldalon a munkaidőzítő is aktiválódik, és az utoljára elküldött csomag újraküldésre kerül.
Kapcsolatbezárási időzítő engedélyezése (SendingCycle állapot):

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

Ezt követően a kimenő kapcsolaton elindul a kapcsolatzárási időzítő.
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);
}

A kapcsolat bezárásának időkorlátja alapértelmezés szerint 30 másodperc.

Rövid idő elteltével a fogadó oldalon lévő munkaidőzítő újra bekapcsol, a kérések újra elküldésre kerülnek, majd a bejövő kapcsolatra elindul a kapcsolatzárási időzítő.

Amikor a bezárási időzítők aktiválódnak, mindkét kapcsolati rekord összes erőforrása felszabadul. A küldő jelenti a kézbesítési hibáról az upstream alkalmazásnak (Lásd: Megbízható UDP API).
Kapcsolati rekord erőforrások felszabadítása:

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

Mélyebben a kódban. Adatátvitel helyreállítása

Adatátvitel helyreállítási diagram csomagvesztés esetén:A Reliable Udp protokoll megvalósítása a .Net számára

Amint azt az időkorlátnál már említettük, amikor a munkaidőzítő lejár, a vevő ellenőrzi az elveszett csomagokat. Csomagvesztés esetén összeállítják a címzetthez nem jutott csomagok számát. Ezek a számok bekerülnek egy adott kapcsolat LostPackets tömbjébe, és elküldik az újrakézbesítési kérelmeket.
Kérelmek küldése csomagok újrakézbesítésére (összeállítási állapot):

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

A feladó elfogadja az újrakézbesítési kérelmet, és elküldi a hiányzó csomagokat. Érdemes megjegyezni, hogy ebben a pillanatban a küldő már elindította a kapcsolatzárási időzítőt, és kérés érkezésekor az visszaáll.
Elveszett csomagok újraküldése (SendingCycle állapot):

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

Az újraküldött csomagot (3. csomag a diagramon) a bejövő kapcsolat fogadja. Ellenőrzik, hogy a fogadási ablak megtelt-e, és visszaáll-e a normál adatátvitel.
Találatok ellenőrzése a fogadó ablakban (összeállítási állapot):

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

Megbízható UDP API

Az adatátviteli protokollal való interakcióhoz van egy nyitott Reliable Udp osztály, amely az átvitelvezérlő blokk feletti burkoló. Íme az osztály legfontosabb tagjai:

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

Az üzenetek fogadása előfizetéssel történik. Delegált aláírás visszahívási módhoz:

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

Üzenet:

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

Egy adott üzenettípusra és/vagy egy adott feladóra való feliratkozáshoz két választható paraméter használható: ReliableUdpMessageTypes messageType és IPEndPoint ipEndPoint.

Üzenet típusok:

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

Az üzenet aszinkron módon kerül elküldésre, ehhez a protokoll egy aszinkron programozási modellt valósít meg:

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

Az üzenet küldésének eredménye igaz - ha az üzenet sikeresen eljutott a címzetthez és hamis - ha a kapcsolat időtúllépés miatt megszakadt:

public bool EndSendMessage(IAsyncResult asyncResult)

Következtetés

Ebben a cikkben sok mindent nem írtak le. Szálillesztési mechanizmusok, kivétel- és hibakezelés, aszinkron üzenetküldési módszerek megvalósítása. De a protokoll magja, a csomagok feldolgozásának logikájának leírása, a kapcsolat létrehozása és az időtúllépések kezelése egyértelmű kell, hogy legyen.

A megbízható kézbesítési protokoll bemutatott verziója elég robusztus és rugalmas ahhoz, hogy megfeleljen a korábban meghatározott követelményeknek. De szeretném hozzátenni, hogy a leírt megvalósítás javítható. Például az átviteli sebesség növelése és az időzítési periódusok dinamikus megváltoztatása érdekében olyan mechanizmusok adhatók a protokollhoz, mint a csúszóablak és az RTT, valamint hasznos lesz egy olyan mechanizmus megvalósítása is, amely meghatározza az MTU-t a kapcsolati csomópontok között (de csak akkor, ha nagy üzeneteket küldenek). .

Köszönöm a figyelmet, várom észrevételeiket, észrevételeiket.

PS Akit érdekelnek a részletek, vagy csak szeretnének tesztelni a protokollt, annak a projekt linkje a GitHube-on:
Megbízható UDP projekt

Hasznos linkek és cikkek

  1. TCP protokoll specifikáció: на английском и на русском
  2. UDP protokoll specifikáció: на английском и на русском
  3. Az RUDP protokoll megbeszélése: draft-ietf-sigtran-reliable-udp-00
  4. Megbízható adatprotokoll: Rfc 908 и Rfc 1151
  5. A kézbesítés visszaigazolásának egyszerű megvalósítása UDP-n keresztül: Vegye át teljes mértékben a hálózatkezelést a .NET és az UDP segítségével
  6. A NAT bejárási mechanizmusait leíró cikk: Peer-to-peer kommunikáció hálózati címfordítókon keresztül
  7. Az aszinkron programozási modell megvalósítása: A CLR aszinkron programozási modell megvalósítása и Az IAsyncResult tervezési minta megvalósítása
  8. Az aszinkron programozási modell portolása a feladatalapú aszinkron mintára (APM a TAP-ban):
    TPL és hagyományos .NET aszinkron programozás
    Együttműködés más aszinkron mintákkal és típusokkal

Frissítés: Köszönöm Mayorovp и sidristij egy feladat hozzáadásának ötletéért a felülethez. A könyvtár kompatibilitása a régi operációs rendszerekkel nem sérül, mert A 4. keretrendszer támogatja az XP-t és a 2003-as szervert is.

Forrás: will.com

Hozzászólás