Reliable UDP protokoll megvalósítása .Net környezetben

Az internet régóta megváltozott. Az internet egyik alapvető protokollja – az UDP – nemcsak a datagramok és a széleskörű terjesztések szállítására, hanem a hálózati csomópontok közötti "peer-to-peer" kapcsolatok biztosítására is használható. Egyszerű felépítése miatt a protokollnak számos előre nem tervezett felhasználási módja alakult ki, de a protokoll hátrányai, mint például a garantált kézbesítés hiánya, továbbra is megmaradtak. Ebben a cikkben a garantált kézbesítést biztosító protokoll UDP feletti megvalósítását ismertetjük.
Tartalom:Bevezetés
A protokoll követelményei
Reliable UDP fejléc
A protokoll általános működési elvei
A protokoll időtúllépései és időzítői
A Reliable UDP átviteli állapotdiagramja
Felület mélyen a kódba. Átviteli vezérlő blokk
Felület mélyen a kódba. Állapotok

Felület mélyen a kódba. Kapcsolatok létrehozása és beállítása
Felület mélyen a kódba. Kapcsolat bezárása időtúllépés esetén
Felület mélyen a kódba. Az adatok átvitelének helyreállítása
Reliable UDP API
Zárás
Hasznos linkek és cikkek

Bevezetés

Az internet kezdeti architektúrája homogén címzési teret feltételezett, amelyben minden csomópontnak globális és egyedi IP címe volt, és közvetlenül kommunikálhatott más csomópontokkal. Jelenleg az internet valójában más architektúrával bír – egy globális IP címekből álló területtel és számos, NAT eszközök mögött rejtőző, helyi címekből álló területtel.Ebben az architektúrában csak azok az eszközök, amelyek globális címzési területen helyezkednek el, tudnak könnyedén kapcsolatba lépni bárkivel a hálózaton, mivel egyedi, globálisan elérhető IP címmel rendelkeznek. Egy privát hálózaton található csomópont más csomópontokkal tud kapcsolódni a saját hálózatában, valamint jól ismert csomópontokkal a globális címzési területen. Ezt az interakciót nagymértékben a hálózati címek átalakításának mechanizmusa segíti elő. A NAT eszközök, például a Wi-Fi routerek, speciális bejegyzéseket hoznak létre a fordítótáblákban a kimenő kapcsolatok számára, és módosítják az IP címeket és a portszámokat a csomagokban. Ez lehetővé teszi, hogy egy privát hálózatból kimenő kapcsolatot létesítsenek a globális címzési területen található csomópontokkal. Ugyanakkor a NAT eszközök általában blokkolják az összes bejövő forgalmat, ha külön szabályok nincsenek beállítva a bejövő kapcsolatokra.

Ez az internet architektúra elegendően helyes a kliens-szerver interakcióhoz, amikor a kliensek privát hálózatokban helyezkednek el, míg a szerverek globális címmel rendelkeznek. De ez nehézségeket okoz a két csomópont közvetlen kapcsolatánál különböző privát hálózatokban. Két csomópont közvetlen összekapcsolása fontos a „peer-to-peer” alkalmazások számára, mint például a hangátvitel (Skype), távoli számítógép hozzáférés (TeamViewer) vagy online játékok. Az egyik leghatékonyabb módszer a peer-to-peer kapcsolat létrehozására különböző privát hálózatokban található eszközök között a „hole punching”. Ez a technika legtöbbször UDP protokoll alapú alkalmazások esetén használatos.

De ha az alkalmazásának garantált adatátvitelre van szüksége, például ha fájlokat továbbít a számítógépek között, akkor az UDP használata számos nehézséget okozhat, mivel az UDP nem garantálja az adatátvitelt, és nem biztosítja a csomagok sorrendben való kézbesítését, ellentétben a TCP protokollal.

Ebben az esetben a csomagok garantált kézbesítésének biztosításához egy alkalmazási szintű protokollt kell megvalósítani, amely biztosítja a szükséges funkcionalitást, és az UDP fölött működik.

Egy alkalmazási szintű protokoll megvalósítása szükséges ahhoz, hogy biztosítsa a csomagok garantált kézbesítését.

Először is, szeretném megjegyezni, hogy létezik a TCP hole punching technika, amely TCP kapcsolatok létrehozására szolgál különböző magán hálózatokban található csomópontok között, de mivel sok NAT eszköz nem támogatja, általában nem tekintik fő módszernek az ilyen csomópontok összekapcsolására.

A cikk további részében csak a garantált szállítási protokoll megvalósítását fogom megvizsgálni. Az UDP hole punching technika megvalósítását a következő cikkekben fogom leírni.

A protokoll követelményei

  1. A csomagok megbízható szállítása a pozitív visszajelzési mechanizmuson keresztül valósul meg (úgynevezett positive acknowledgment).
  2. Szükség van nagy mennyiségű adat hatékony átvitelére, azaz a protokollnak el kell kerülnie a felesleges csomagújraküldéseket.
  3. Lehetőség kell, hogy legyen a szállítási megerősítő mechanizmus lemondására (azaz működnie kell mint 'tisztán' UDP protokoll).
  4. Lehetőség az úgynevezett parancsmódban való működésre, ahol minden üzenetet meg kell erősíteni.
  5. Az alapvető adatátviteli egység a protokoll által az üzenet kell legyen.

Ezek a követelmények nagymértékben egybevágnak a Reliable Data Protocol követelményeivel, amelyeket a következő helyen írtak le: rfc 908 és rfc 1151, és ezen alapelvek alapján dolgoztam ki ezt a protokollt.

E követelmények megértéséhez nézzük meg az adatátviteli idődiagramokat két csomópont között a TCP és UDP protokollok használatával. Tegyük fel, hogy mindkét esetben egy csomag elveszik.
Nem interaktív adatok átvitele TCP-n:Reliable UDP protokoll megvalósítása .Net környezetben

Ahogyan a diagramon látható, csomagok elvesztése esetén a TCP észleli az elveszett csomagot és értesíti erről a küldőt, kérve az elveszett szegmens számát.
Adatok átvitele UDP protokollon:Reliable UDP protokoll megvalósítása .Net környezetben

Az UDP nem tesz lépéseket az elvesztések észlelésére. Az UDP protokoll hibaellenőrzése teljesen az alkalmazásra hárul.

A TCP protokoll hibáinak észlelését a végponttal való kapcsolat létesítése, az azon állapot megőrzése, a küldött byte-ok számának megadása minden csomag fejlécében, valamint a megérkezésről szóló értesítések küldése az 'acknowledge number' számmal valósítja meg.

Továbbá a teljesítmény növelése érdekében (azaz több mint egy szegmens küldése anélkül, hogy megerősítést kapnánk) a TCP protokoll az úgynevezett küldési ablakot használja - a byte-ok számát, amelyet a szegmens küldője vár elfogadni.

Bővebben a TCP protokollról a következő dokumentumban olvashat: rfc 793, UDP-vel rfc 768, ahol ezek valójában meghatározva vannak.

A fentiekből világosan látszik, hogy megbízható üzenetküldési protokoll létrehozásához UDP fölött (továbbiakban hívjuk Reliable UDP), hasonló TCP mechanizmusokat kell megvalósítani. Nevezetesen:

  • megőrizni a kapcsolat állapotát
  • szegmensek számozását használni
  • speciális visszaigazoló csomagokat használni
  • egyszerűsített ablakmechanizmust alkalmazni a protokoll átvitelének növelésére

Továbbá szükség van:

  • az üzenet kezdetének jelzésére, a források allokálásához a kapcsolathoz
  • az üzenet végének jelzésére, a megkapott üzenet továbbításához a magasabb szintű alkalmazásnak, és a protokoll forrásainak felszabadításához
  • a protokoll számára lehetővé tenni, hogy bizonyos kapcsolatok esetén kikapcsolja a kézbesítési visszaigazolás mechanizmusát, hogy „tiszta” UDP-ként működjön

Reliable UDP fejléc

Emlékezzünk, hogy az UDP datagram IP datagramba van beágyazva. A Reliable UDP csomag ezért „bekerül” az UDP datagramban.
A Reliable UDP fejlécének beágyazása:Reliable UDP protokoll megvalósítása .Net környezetben

A Reliable UDP fejlécének struktúrája meglehetősen egyszerű:

Reliable UDP protokoll megvalósítása .Net környezetben

  • Flags – a csomag vezérlő zászlói
  • MessageType – az üzenet típusa, amelyet a magasabb szintű alkalmazások használnak meghatározott üzenetek előfizetésére
  • TransmissionId — a küldés azonosítója, amely a címzett címével és portjával együtt egyedileg meghatározza a kapcsolatot
  • PacketNumber – a csomag száma
  • Options – a protokoll kiegészítő opciói. Az első csomag esetén az üzenet méretének meghatározására szolgál

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

  • FirstPacket — az üzenet első csomagja
  • NoAsk — az üzenet nem igényel kézbesítési visszaigazolásának bekapcsolását
  • LastPacket — az üzenet utolsó csomagja
  • RequestForPacket — visszaigazoló csomag vagy elveszett csomag kérés

A protokoll általános működési elvei

Mivel a Reliable UDP a két csomópont közötti garantált üzenetküldésre orientálódik, képesnek kell lennie a kapcsolat létesítésére a másik féllel. A kapcsolat létesítéséhez a küldő fél egy FirstPacket zászlót tartalmazó csomagot küld, amire a válasz a kapcsolat létesítését jelenti. Minden válaszcsomag, vagy más néven visszaigazoló csomag, mindig egy egységgel nagyobb értéket állít be a PacketNumber mezőben, mint a sikeresen érkezett csomagok legnagyobb PacketNumber értéke. Az első elküldött csomag Options mezőjében az üzenet mérete kerül rögzítésre.

A hasonló mechanizmus használatos a kapcsolat lezárásához. Az utolsó csomagban a LastPacket zászló van beállítva. A válaszcsoportban pedig az utolsó csomag számának +1-es értéke szerepel, ami a fogadó fél számára a sikeres üzenettovábbítást jelenti.
Kapcsolat létrehozásának és lezárásának diagramja:Reliable UDP protokoll megvalósítása .Net környezetben

Amikor a kapcsolat létrejön, megkezdődik az adatok átadása. Az adatok csomagok blokkjai formájában kerülnek továbbításra. Minden blokk, az utolsó kivételével, fix számú csomagot tartalmaz, amely megegyezik a fogadó/átviteli ablak méretével. Az utolsó adatblokk kevesebb csomagot is tartalmazhat. Minden blokk elküldése után az adó fél megerősítést vár a kézbesítésről, vagy kérheti a hiányzó csomagok újraküldését, miközben nyitva hagyja a fogadó/átviteli ablakot a válaszok beérkezésére. A blokk kézbesítésének megerősítése után a fogadó/átviteli ablak eltolódik, és elküldésre kerül a következő adatblokk.

A fogadó fél fogadja a csomagokat. Minden csomagot ellenőriznek, hogy belefér-e a küldési ablakba. Az ablakból ki nem eső csomagok és a duplikátumok kiszűrésre kerülnek. Mivel az ablak mérete szigorúan fix és azonos a fogadónál és a küldőnél, ha egy blokk csomagjait hiba nélkül kézbesítik, az ablak eltolódik a következő adatblokk csomagjainak fogadására, és kézbesítési megerősítés kerül elküldésre. Ha az ablak nem töltődik fel a beállított munkafolyamat időzítőjének időtartama alatt, ellenőrzés indul, hogy mely csomagok nem érkeztek meg, és újraküldési kérelmeket küldenek.
Újraküldési diagram:Reliable UDP protokoll megvalósítása .Net környezetben

A protokoll időtúllépései és időzítői

Több oka is lehet annak, hogy nem lehet létrehozni a kapcsolatot. Például ha a fogadó fél offline van. Ilyen esetben, ha a kapcsolat létesítése során időtúllépés történik, a kapcsolat lezárásra kerül. A megbízható UDP implementálásakor két időzítőt használnak az időtúllépések megállapítására. Az első, a munkafolyamat időzítője, a távoli házigazdától kapott válasz várakozására szolgál. Ha ez a küldő fél oldalán aktiválódik, az utolsó elküldött csomagot újraküldik. Ha az időzítő a fogadónál aktiválódik, akkor a hiányzó csomagok ellenőrzésére kerül sor, és kérjük az újraküldést.

A második időzítő szükséges a kapcsolat lezárásához, ha az egységek között nincs kapcsolat. A küldő fél számára azonnal elindul a munkában lévő időzítő aktiválása után, és vár választ a távoli egységtől. Ha a megadott időn belül nincs válasz, a kapcsolat megszűnik, és az erőforrások felszabadulnak. A fogadó fél számára a kapcsolat lezárási időzítője a munkában lévő időzítő kétszeri aktiválása után indítódik. Erre azért van szükség, hogy megakadályozza a visszaigazoló csomag elvesztését. Az időzítő aktiválásakor a kapcsolat is megszűnik, és az erőforrások felszabadulnak.

A Reliable UDP átviteli állapotdiagramja

A protokoll működési elvei végső automatában valósulnak meg, amelynek minden állapota egy adott csomagfeldolgozási logikáért felelős.
Reliábilis UDP állapotdiagram:

Reliable UDP protokoll megvalósítása .Net környezetben

Closed – valójában nem állapot, hanem az automata kezdő és végpontja. Az állapot Closed a továbbítási vezérlő blokk, amely a aszinkron UDP szerver megvalósításával átirányítja a csomagokat a megfelelő kapcsolatokra, és elindítja az állapotok feldolgozását.

FirstPacketSending – az alapállapot, amelyben a kimenő kapcsolat van az üzenet küldésekor.

Ebben az állapotban küldik el az első csomagot a normál üzenetekhez. A megerősítés nélküli üzenetek esetén ez az egyetlen állapot – benne történik az összes üzenet elküldése.

SendingCycle – az alapvető állapot az üzenetcsomagok továbbításához.

Azzal az állapottal vált át, amelyből FirstPacketSending történik az üzenet első csomagjának elküldése után. Ebbe az állapotba érkeznek az összes visszaigazolás és újraküldési kérés. A kijárat két esetben lehetséges – sikeres üzenetátvitel esetén vagy időtúllépéskor.

FirstPacketReceived – az üzenet fogadójának kezdeti állapota.

Ebben ellenőrzik a továbbítás helyességét, létrehozzák a szükséges struktúrákat, és visszaigazolást küldenek az első csomag átvételéről.

Egyetlen csomagból álló és megerősítés nélkül küldött üzenet esetén ez az egyetlen állapot. Ilyen üzenet feldolgozása után a kapcsolat lezárul.

Assembling – az üzenetcsomagok fogadásához szükséges alapállapot.

A csomagokat ideiglenes tárolóba rögzíti, ellenőrzi a csomagok hiányát, visszaigazolásokat küld a csomagblokk és a teljes üzenet kézbesítéséről, és elküldi a hiányzó csomagok újraküldésére vonatkozó kéréseket. Ha az összes üzenet sikeresen megérkezik – a kapcsolat állapota megváltozik Completed, ellenkező esetben időtúllépés miatt megszakad.

Completed – kapcsolat lezárása az összes üzenet sikeres átvitele esetén.

Ez az állapot szükséges az üzenet összegyűjtéséhez, és arra az esetre is, ha az üzenet kézbesítéséről szóló visszaigazolás a küldőhöz vezető úton elveszett. Az állapotból való kilépés időtúllépés miatt történik, de a kapcsolat sikeresen lezártnak számít.

Felület mélyen a kódba. Átviteli vezérlő blokk

A Reliable UDP egyik kulcsfontosságú eleme a szállítási vezérlő blokk. Ennek a blokk feladata a jelenlegi kapcsolatok és a segédelemek tárolása, a beérkezett csomagok elosztása a megfelelő kapcsolatok között, interfész biztosítása a csomagok kapcsolatra történő küldéséhez és a protokoll API megvalósítása. A szállítási vezérlő blokk a UDP szintről csomagokat fogad, és azokat egy automatizmus feldolgozására irányítja. A csomagok fogadására aszinkron UDP szerver lett megvalósítva.
A ReliableUdpConnectionControlBlock osztály néhány tagja:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // bájt tömb a megadott kulcs számára. Bejövő üzenetek összegyűjtésére használják
  public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
  // bájt tömb a megadott kulcs számára. Kimenő üzenetek küldésére használják.
  public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
  // kapcsolati nyilvántartás a megadott kulcs számára.
  private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
  // üzenetekre feliratkozott előfizetők listája.
  private readonly List m_subscribers;
  // helyi socket
  private Socket m_socketIn;
  // port bejövő üzenetekhez
  private int m_port;
  // helyi IP cím
  private IPAddress m_ipAddress;
  // helyi végpont
  public IPEndPoint LocalEndpoint { get; private set; }
  // előre inicializált
  // állapotok gyűjteménye
  public StatesCollection States { get; private set; }
  // véletlenszám-generátor. A TransmissionId létrehozásához használják
  private readonly RNGCryptoServiceProvider m_randomCrypto;
  //...
}

Aszinkron UDP szerver megvalósítása:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // új puffert hozunk létre, minden socket.BeginReceiveFrom-hoz
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // a puffert átadjuk paraméterként az aszinkron metódusnak
  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);
  // csomag érkezett, készen állunk a következő fogadására
  Receive();
  // mivel a legegyszerűbb módja a buffer kezelésének, hogy hivatkozást kapunk rá
  // az IAsyncResult.AsyncState-ből
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // a csomag fejléce
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {
    // nem megfelelő csomag érkezett - elvetjük
    return;
  }
  // kulcsot hozunk létre a connection record azonosításához a csomag számára
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // meglévő connection recordot kapunk vagy újat hozunk létre
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // a csomag feldolgozása a végső automatába
  record.State.ReceivePacket(record, header, bytes);
}

Minden üzenetátvitelhez létrejön egy struktúra, amely tartalmazza a kapcsolat adatait. Ezt a struktúrát hívják connection record.
A ReliableUdpConnectionRecord osztály néhány tagja:

belső osztály ReliableUdpConnectionRecord : IDisposable
{    
  // bájt tömb üzenettel    
  public byte[] IncomingStream { get; set; }
  // hivatkozás az állapot végső automatájára    
  public ReliableUdpState State { get; set; }    
  // pár, amely egyértelműen azonosítja a connection record-ot
  // az adatátviteli vezérlő blokkjában     
  public Tuple Key { get; private set;}
  // az alsó határ a fogadóablak számára    
  public int WindowLowerBound;
  // az adatátviteli ablak mérete
  public readonly int WindowSize;     
  // az elküldendő csomag sorszáma
  public int SndNext;
  // az elküldendő csomagok száma
  public int NumberOfPackets;
  // az átvitel sorszáma (ez a Tuple második része)
  // minden üzenethez saját	
  public readonly Int32 TransmissionId;
  // távoli IP végpont – maga az üzenet címzettje
  public readonly IPEndPoint RemoteClient;
  // a csomag mérete, hogy elkerüljük a frakcionálást IP szinten
  // nem haladhatja meg az MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // az adatátviteli vezérlő blokk
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // a BeginSendMessage/EndSendMessage aszinkron művelet eredményeit kapszulázza
  public readonly AsyncResultSendMessage AsyncResult;
  // ne küldjünk visszaigazoló csomagokat
  public bool IsNoAnswerNeeded;
  // az utolsó helyes csomag (mindig a legmagasabb számra állítódik)
  public int RcvCurrent;
  // a vesztes csomagok sorszámát tartalmazó tömb
  public int[] LostPackets { get; private set; }
  // megérkezett-e az utolsó csomag. Bool-ként használva.
  public int IsLastPacketReceived = 0;
  //...
}

Felület mélyen a kódba. Állapotok

Az állapotok a Reliable UDP protokoll végső automatáját valósítják meg, amelyben történik a csomagok fő feldolgozása. Az absztrakt ReliableUdpState osztály felületet biztosít az állapot számára:

Reliable UDP protokoll megvalósítása .Net környezetben

A protokoll teljes működési logikáját a fent bemutatott osztályok végzik, együtt egy segédosztállyal, amely statikus módszereket kínál, például a ReliableUdp fejléc megépítése a connection record-ból.

Az interfészhez tartozó módszerek megvalósításait részletesen fogjuk megvizsgálni, amelyek a protokoll alapvető algoritmusait határozzák meg.

DisposeByTimeout metódus

A DisposeByTimeout metódus felelős a csatlakozás erőforrásainak felszabadításáért a timeout lejárta után és az üzenet sikeres/rossz átvitelének 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 állapotban felül van definiálva Completed.
Completed.DisposeByTimeout:

védett felülírású void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  
  // Értesítjük a sikeres üzenetfogadásról
  SetAsCompleted(connectionRecord);        
}

ProcessPackets metódus

A ProcessPackets metódus a csomag vagy csomagok további feldolgozásáért felelős. Közvetlenül vagy csomagok várakozási ideje által hívják meg.

Állapotban Assembling a metódus felülírva van, és a csomagok elvesztésének ellenőrzéséért, valamint az állapotba való átmenetért felelős Completed, az utolsó csomag megérkezése esetén és a sikeres ellenőrzés teljesítésekor
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // Vannak elveszett csomagok, kérjük őket
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // Beállítjuk a második próbához szükséges időzítőt
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // Ha a két esemény után sem sikerült csomagokat kapnunk
    // elindítjuk a kapcsolatzárási időzítőt
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // Sikeres ellenőrzés
  {
    // Megerősítést küldünk az adathalmasztó csomag megérkezéséről
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // Ahelyett, hogy azonnal végrehajtanánk az erőforrásokat
    // elindítjuk az időzítőt, arra az esetre, ha
    // az utolsó ack nem jut el a küldőhöz és újra kérné
    // Az időzítő aktiválásakor - végrehajtjuk az erőforrásokat
    // a Completed állapotban a timer metódus felülírva
    StartCloseWaitTimer(connectionRecord);
  }
  // Ez az eset, amikor az ack a csomagok blokkjához elveszett
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // Elindítjuk a kapcsolatzárási időzítőt
    StartCloseWaitTimer(connectionRecord);
  }
}

Állapotban SendingCycle Ez a metódus csak időzítők által hívható, és a legutolsó üzenet újraküldéséért, valamint a kapcsolatzárási időzítő aktiválásáért felelős.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  // újraküldjük az utolsó csomagot
  // (a kapcsolat helyreállítása esetén a fogadó csomópont újra elküldi a kérdéseket, amelyek nem érkeztek meg hozzá)
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // elindítjuk a CloseWait időzítőt – a kapcsolat helyreállításának vagy lezárásának várakozására
  StartCloseWaitTimer(connectionRecord);
}

Állapotban Completed a módszer leállítja a munkához szükséges időzítőt, és üzenetet küld a feliratkozóknak.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // összegyűjtjük az üzenetet és továbbítjuk a feliratkozóknak
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

ReceivePacket metódus

Állapotban FirstPacketReceived a módszer alapvető feladata, hogy meghatározza, valóban megérkezett-e az üzenet első csomagja a felületre, és összegyűjti a csomagot, amely egyetlen csomagból áll.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // eldobott csomag
    return;
  // a két zászló kombináció - FirstPacket és LastPacket - azt jelzi, hogy csak egy üzenetünk van
  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))
    {
      // megerősítő csomag küldése
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // a dizájn szerint az összes csomagszám 0-val kezdődik;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // kiszámítjuk a beérkező csomagok számát
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // rögzítjük az utolsó megkapott csomag számát (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // eltoljuk a fogadási ablakot 1-gyel
  connectionRecord.WindowLowerBound++;
  // állapot átváltás
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // ha nincs szükség a visszaigazolás mechanizmusra
  // elindítjuk az időzítőt, amely felszabadít minden struktúrát
  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 SendingCycle Ez a metódus felül van írva a kézbesítési megerősítések és újrakérés kérések fogadására.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // végső ablakhatár kiszámítása
  // az ablakhatár + 1 kerül meghatározásra a kézbesítési visszaigazolásokhoz
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // ablakon belüli ellenőrzés        
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ellenőrizzük az utolsó csomagot:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // átadás befejezve
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // ez az első csomagra adott válasz a megerősítésével         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // ablak eltolása nélkül
    SendPacket(connectionRecord);
  }
  // megérkezett a blokkok adatait megerősítő csomag
  else if (header.PacketNumber == windowHighestBound)
  {
    // eltoljuk az átviteli/átfogó ablakot
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // töröljük az átviteli kontroll tömböt
    connectionRecord.WindowControlArray.Nullify();
    // csomagblokkok küldése
    SendPacket(connectionRecord);
  }
  // ez egy újraküldési kérés – küldjük a szükséges csomagot          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Állapotban Assembling A ReceivePacket metódusban zajlik a fő munka, amely az érkező csomagokból állítja össze az üzenetet.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // csomagok feldolgozása kézbesítési megerősítés nélkül
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // időzítő törlése
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // adatok rögzítése
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // ha utolsó csomag érkezett, befejezzük
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }
  // az ablak végső határának kiszámítása
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // a nem ablakba eső csomagok eldobása
  if (header.PacketNumber  (windowHighestBound))
    return;
  // duplikátumok eldobása
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // adatok rögzítése
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // csomagok számláló növelése
  connectionRecord.PacketCounter++;
  // az aktuális csomagszám rögzítése az ablakvezérlő tömbben
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // a legnagyobb érkezett csomagszám beállítása
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // időzítők újraindítása
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ha utolsó csomag érkezett
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // ha megkaptuk az ablak összes csomagját, nullázzuk a számlálót
  // és megerősítjük a csomagot
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // számláló nullázása.
    connectionRecord.PacketCounter = 0;
    // az átviteli ablak eltolása
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // az átviteli vezérlő tömb nullázása
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ha az utolsó csomag már megérkezett
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // csomagok ellenőrzése
    ProcessPackets(connectionRecord);
  }
}

Állapotban Completed A metódus egyetlen feladata a sikeres üzenetátvitel újraigazolásának elküldése.
Befejezett.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // az utolsó csomag újraküldése, mivel az utolsó ack nem jutott el a küldőhöz
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

SendPacket metódus

Állapotban FirstPacketSending ez a metódus az első adatcsomag küldését végzi, vagy ha az üzenet nem igényel kézbesítési visszaigazolást – az egész üzenetet.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ha nincs szükség visszaigazolásra - küldjük az összes csomagot
  // és felszabadítjuk a forrásokat
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Itt történik az As Is küldés
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // létrehozzuk a csomagfejlécet és elküldjük
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // növeljük a számlálót
  connectionRecord.SndNext++;
  // eltoljuk az ablakot
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Elindítjuk az időzítőt
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Állapotban SendingCycle ebben a metódusban történik a csomagok blokkjának küldése.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // csomagok blokk küldése      
  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++;
  }
  // nagy átviteli ablak esetén az időzítőt újraindítjuk a küldés után
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
  {
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  }
}

Felület mélyen a kódba. Kapcsolatok létrehozása és beállítása

Most, hogy megismertük az állapotok és a kezeléshez használt metódusok alapjait, néhány példát részletesebben is áttekinthetünk a protokoll működéséről.
Az adatok átvitelének diagramja normál körülmények között:Reliable UDP protokoll megvalósítása .Net környezetben

Vizsgáljuk meg részletesen a létrehozást connection record az első csomag csatlakoztatásához és elküldéséhez. A küldési folyamat kezdeményezője mindig az az alkalmazás, amely az üzenetküldési API-metódust hívja meg. Ezt követően a küldésvezérlő StartTransmission metódusa kerül aktiválásra, amely elindítja az új üzenet adatainak továbbítását.
Kimeneti kapcsolat létrehozása:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "A paraméterekkel rendelkező konstruktort kell használnia, vagy el kell indítania a figyelőt az üzenet küldése előtt" );
    }
    // elindítjuk a bejövő csomagok feldolgozását
    StartListener(LocalEndpoint);
  }
  // létrehozzuk a kulcsot a szótárhoz, az EndPoint és a ReliableUdpHeader.TransmissionId alapján
  byte[] transmissionId = new byte[4];
  // véletlenszám generálása a transmissionId-hez
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // új bejegyzés létrehozása a kapcsolathoz és ellenőrizzük, 
  // hogy létezik-e már ilyen szám a szótárainkban
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // ha létezik - újra generáljuk a véletlenszámot
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // ha megint nem sikerült - kivételt generálunk
      throw new ArgumentException("A Pair TransmissionId & EndPoint már létezik a szótárban");
  }
  // elindítottuk az állapotot
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

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

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // csomag fejléc létrehozása és küldése
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // számláló növelése
  connectionRecord.SndNext++;
  // ablak eltolása
  connectionRecord.WindowLowerBound++;
  // állapotváltás a SendingCycle-ra
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Időzítő elindítása
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Az első csomag elküldése után a küldő állapotba lép SendingCycle – várakozni a csomag kézbesítésének megerősítésére.
A fogadó fél az EndReceive metódus segítségével fogadja az elküldött csomagot, létrehoz egy új connection record és a megfelelően elemzett fejléccel együtt továbbítja ezt a csomagot a ReceivePacket metódus feldolgozására. FirstPacketReceived
Kapcsolat létrehozása a fogadó oldalon:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // csomag megérkezett
  // a csomag fejléceinek elemzése        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // érvénytelen csomag érkezett - elvetjük
    return;
  }
  // kulcs létrehozása a csomaghoz tartozó kapcsolat rekord meghatározásához
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // meglévő kapcsolat rekord megszerzése vagy új létrehozása
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // csomag továbbitása a végső automátába
  record.State.ReceivePacket(record, header, bytes);
}

Az első csomag fogadása és a visszaigazolás elküldése (ElsőCsomagFogadva állapot):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // elvetjük a csomagot
    return;
  // ...
  // a tervezés szerint az összes csomagszám nulláról kezdődik;
  if (header.PacketNumber != 0)          
    return;
  // tömb inicializálása az üzenet részeinek tárolásához
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // csomag adatok írása a tömbbe
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // az érkezendő csomagok számának meghatározása
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // az utolsó fogadott csomag számának rögzítése (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // ablakoffset növelése 1-el
  connectionRecord.WindowLowerBound++;
  // állapot váltás
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/* ha nem szükséges a megerősítési mechanizmus */)
  // ...
  else
  {
    // válaszcsomag küldése
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Felület mélyen a kódba. Kapcsolat bezárása időtúllépés esetén

A time-outok kezelése fontos része a megbízható UDP-nek. Nézzünk egy példát, ahol egy közbenső csomóponton hiba lépett fel, és az adatok átadása lehetetlenné vált mindkét irányban.
Time-out alapján történő kapcsolat lezárásának diagramja:Reliable UDP protokoll megvalósítása .Net környezetben

A diagram alapján látható, hogy az elküldés előtt a munkát indító időzítő azonnal elindul, közvetlenül a csomagblokk elküldése után. Ez történik a SendPacket metódusban SendingCycle.
A munkát indító időzítő aktiválása (SendingCycle állapot):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // csomagblokk küldése   
  // ...   
  // időzítő újraindítása a küldés után
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

A kapcsolódási időzítő periódusai a kapcsolat létrehozásakor kerülnek beállításra. Alapértelmezés szerint a ShortTimerPeriod 5 másodperc, a példában pedig 1,5 másodpercre van beállítva.

A bejövő kapcsolatnál az időzítő az utolsó beérkező adatcsomag megérkezése után indul, ez a ReceivePacket módszerben történik. Assembling
A munkafolyamat időzítőjének aktiválása (Az Assembling állapotban):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // időzítők újraindítása
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

A bejövő kapcsolat esetén az időzítő várakozási ideje alatt nem érkezett több csomag. Az időzítő aktiválódott és a ProcessPackets módszert hívta meg, amelyben elveszett csomagok lettek felfedezve és először kérdések lettek küldve az újraküldésre.
Kérdések küldése az újraküldésre (Az Assembling állapotban):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*elveszett csomagok ellenőrzése*/)
  {
    // kérdések küldése az újraküldésre
    // időzítő beállítása másodszor, az újraküldési kísérlethez
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // ha a WaitForPacketTimer két aktiválás után
    // nem sikerült megszerezni a csomagokat - indítsuk el a kapcsolat zárási időzítőjét
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/*megérkezett az utolsó csomag és sikeres ellenőrzés*/)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // ha az ack a csomag blokkra elveszett
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // az ack újraküldése
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // a kapcsolat zárási időzítőjének aktiválása
    StartCloseWaitTimer(connectionRecord);
  }
}

A TimerSecondTry változó beállítva: igaz. Ez a változó felelős a munkafolyamat időzítőjének újraindításáért.

A küldő oldalán is aktiválódik a munkafolyamat időzítője, és az utolsó elküldött csomag újraküldésre kerül.
A kapcsolat zárási időzítőjének aktiválása (A SendingCycle állapotban):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // az utolsó csomag újraküldése
  // ...        
  // CloseWait időzítő aktiválása – a kapcsolat helyreállításának vagy lezárásának várakozására
  StartCloseWaitTimer(connectionRecord);
}

Ezután a kimenő kapcsolatban elindul a kapcsolat lezárási időzítője.
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 lezárási időzítőjének várakozási időszaka alapértelmezés szerint 30 másodperc.

Rövid idő elteltével a fogadó oldalon ismét aktiválódik a munkához kapcsolódó időzítő, újra kérdéseket küldenek, majd elindul a bejövő kapcsolat lezárási időzítője.

Amikor az időzítők aktiválódnak, mindkét connection record erőforrásait felszabadítják. A küldő értesíti a felettes alkalmazást a sikertelen kézbesítésről (lásd: API Reliable UDP).
A connection record erőforrásainak 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);
  }
}

Felület mélyen a kódba. Az adatok átvitelének helyreállítása

Az adatok átvitelének helyreállítási diagramja csomagvesztés esetén:Reliable UDP protokoll megvalósítása .Net környezetben

Ahogy már korábban tárgyaltuk a kapcsolat időtúllépés általi lezárásakor, a munkához kapcsolódó időzítő lejártakor a fogadónál ellenőrzést végeznek a hiányzó csomagokra. Amennyiben hiányzó csomagokat találnak, egy listát állítanak össze a fogadóhoz el nem jutó csomagszámokkal, amelyeket a konkrét kapcsolat LostPackets tömbjébe helyeznek, majd újrakézbesítési kérelmeket indítanak.
Újrakézbesítési kérelmek küldése csomagokhoz (Assembling állapot):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  \/\/...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    \/\/ hiányzó csomagok vannak, elküldjük a kéréseket
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    \/\/ ...
  }
}

A küldő megkapja az újrakézbesítési kérelmet és elküldi a hiányzó csomagokat. Érdemes megjegyezni, hogy ezen a ponton a küldőnél már elindult a kapcsolat lezárási időzítője, és a kérés beérkezésekor az újraindul.
Hiányzó csomagok újrakézbesítése (SendingCycle állapot):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // a kapcsolat lezárási időzítőjének visszaállítása
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // ez egy újra küldési kérés – küldjük a szükséges csomagot          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

A újraküldött csomagot (packet#3 a diagramon) egy bejövő kapcsolat fogadja. Ellenőrzés történik a fogadási ablak kitöltésére, és a normál adatátvitel helyreáll.
Ellenőrzés a fogadási ablakba való bekerülésre (Assembling állapot):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // a csomagszámláló növelése        
  connectionRecord.PacketCounter++;
  // a fogadási ablak vezérlő tömbbe a jelenlegi csomagszám rögzítése        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // a legnagyobb beérkezett csomag beállítása        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // időzítők újraindítása        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // ha minden csomag megérkezett az ablakból, akkor visszaállítjuk a számlálót
  // és küldjük a visszaigazoló csomagot
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // a számláló visszaállítása.      
    connectionRecord.PacketCounter = 0;
    // az adatátviteli ablak eltolása
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // az adatátviteli vezérlő tömb nullázása
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

Reliable UDP API

Az adatátviteli protokollal való kölcsönhatáshoz egy nyilvános Reliable Udp osztály áll rendelkezésre, amely az adatátviteli vezérlő blokk burkolataként szolgál. Íme a legfontosabb osztálytagok:

publikus sealed class ReliableUdp : IDisposable
{
  // megkapja a helyi végpontot
  public IPEndPoint LocalEndpoint    
  // létrehozza a ReliableUdp példányt és elindítja
  // a bejövő csomagok figyelését a megadott IP címen
  // és porton. A 0 érték a portra dinamikusan kiosztott portot jelent
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // feliratkozás bejövő üzenetekre
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // leiratkozás az üzenetek fogadásáról
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // aszinkron üzenetküldés
  // Megjegyzés: a XP-vel és a Server 2003-zal való kompatibilitás nem sérül, mivel a .NET Framework 4.0-t használja
  public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // aszinkron üzenetküldés indítása
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // az aszinkron küldés eredményének lekérdezése
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // erőforrások felszabadítása
  public void Dispose()    
}

Az üzenet fogadása feliratkozás útján történik. A visszahívási metódus delegátumának szignatúrája:

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

Üzenet:

public class ReliableUdpMessage
{
  // az üzenet típusa, egyszerű felsorolás
  public ReliableUdpMessageTypes Type { get; private set; }
  // az üzenet adatai
  public byte[] Body { get; private set; }
  // ha true-ra van állítva – a kézbesítési megerősítési mechanizmus le lesz tiltva
  // egy konkrét üzenet átküldéséhez
  public bool NoAsk { get; private set; }
}

Konkrét üzenet típusokra és/vagy konkrét feladókra való feliratkozáshoz két opcionális paraméter használható: ReliableUdpMessageTypes messageType és IPEndPoint ipEndPoint.

Üzenettípusok:

public enum ReliableUdpMessageTypes : short
{ 
  // Bármilyen
  Any = 0,
  // Kérés a STUN szerverhez
  StunRequest = 1,
  // Válasz a STUN szervertől
  StunResponse = 2,
  // Fájlátvitel
  FileTransfer = 3,
  // ...
}

Az üzenetküldés aszinkron módon történik, a protokollban aszinkron programozási modellt valósítottak meg:

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

Az üzenetküldés eredménye true lesz – ha az üzenet sikeresen eljutott a címzetthez és false – ha a kapcsolat időtúllépés miatt záródott:

public bool EndSendMessage(IAsyncResult asyncResult)

Zárás

Sok minden nem volt leírva ebben a cikkben. Az adatfolyamok összehangolásának mechanizmusai, kivételek és hibák kezelése, az aszinkron üzenetküldő módszerek megvalósítása. De a protokoll magja, a csomagok feldolgozási logikájának leírása, a kapcsolatok létrehozása és az időtúllépések kezelése tisztázva kell legyen az Ön számára.

A bemutatott megbízható adatátviteli protokoll verziója elég stabil és rugalmas, megfelel a korábban meghatározott követelményeknek. De szeretnék hozzátenni, hogy a leírt megvalósítás fejleszthető. Például a sávszélesség növelése és a protokoll időzítőinek dinamikus megváltoztatása érdekében olyan mechanizmusokat lehetne hozzáadni, mint a csúszó ablak és az RTT, valamint hasznos lenne a MTU meghatározásának mechanizmusának megvalósítása a kapcsolat csomópontjai között (de csak nagy üzenetek küldése esetén).

Köszönöm a figyelmet, várom a megjegyzéseit és észrevételeit.

P.S. Azok számára, akik érdeklődnek a részletek iránt, vagy csak tesztelni szeretnék a protokollt, íme a projekt linkje a GitHubon:
Reliable UDP projekt

Hasznos linkek és cikkek

  1. TCP protokoll specifikáció: angol nyelven és oroszul
  2. UDP protokoll specifikáció: angol nyelven és oroszul
  3. RUDP protokoll megbeszélése: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 és rfc 1151
  5. Egyszerű megvalósítás a UDP-n keresztüli kézbesítés megerősítésére: Vegye át a teljes ellenőrzést hálózatával a .NET és UDP segítségével
  6. Cikk a NAT-ok leküzdési mechanizmusainak leírásával: Peer-to-Peer kommunikáció hálózati címfordítók között
  7. Az aszinkron programozási modell megvalósítása: A CLR aszinkron programozási modelljének megvalósítása és Hogyan valósítsuk meg az IAsyncResult tervezési mintát
  8. Az aszinkron programozási modell átvedlése egy feladat-alapú aszinkron sablonba (APM a TAP-ban):
    TPL és a hagyományos .NET aszinkron programozás
    Interop más aszinkron mintákkal és típusokkal

Frissítés: Köszönöm mayorovp és sidristij az interfészhez való feladat hozzáadásának ötletéért. A könyvtár kompatibilitása nem sérül a régi operációs rendszerekkel, mivel a 4. keret támogatja az XP és a 2003 server-t is.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster