Implementace protokolu Reliable UDP pro .Net

Internet se od svého založení výrazně změnil. Jeden z hlavních internetových protokolů – UDP – se používá nejen pro doručování datagramů a širokopásmové vysílání, ale také pro zajištění „peer-to-peer“ spojení mezi uzly sítě. Díky své jednoduché struktuře se u tohoto protokolu vyvinulo mnoho neplánovaných způsobů použití, přičemž jeho nevýhody, jako je absence zaručeného doručení, zůstaly. V tomto článku se popisuje implementace protokolu zaručeného doručení nad UDP.
Obsah:Úvod
Požadavky na protokol
Záhlaví Reliable UDP
Obecné principy fungování protokolu
Časové limity a časovače protokolu
Stavová diagram Reliable UDP
Hlubší do kódu. Blok řízení přenosu
Hlubší do kódu. Stavy

Hlubší do kódu. Vytváření a navazování spojení
Hlubší do kódu. Uzavření spojení po časovém limitu
Hlubší do kódu. Obnova datového přenosu
API Reliable UDP
Závěr
Užitečné odkazy a články

Úvod

Původní architektura Internetu předpokládala homogenní adresní prostor, kde měl každý uzel globální a unikátní IP adresu a mohl přímo komunikovat s jinými uzly. Dnes má Internet de facto jinou architekturu – jednu oblast globálních IP adres a mnoho oblastí s privátními adresami, skrytými za zařízeními NAT.V takové architektuře mohou zařízení v globálním adresním prostoru snadno komunikovat s kýmkoli v síti, protože mají unikátní, globálně směrovatelnou IP adresu. Uzly v soukromé síti se mohou spojovat s jinými uzly v téže síti, stejně jako s jinými, dobře známými uzly v globálním adresním prostoru. Tato interakce je do značné míry umožněna mechanismem překladu síťových adres. Zařízení NAT, například Wi-Fi směrovače, vytvářejí speciální zápisy v tabulkách překladů pro odchozí spojení a modifikují IP adresy a čísla portů v paketech. To umožňuje navázat odchozí spojení ze soukromé sítě s uzly v globálním adresním prostoru. Zároveň však zařízení NAT obvykle blokují veškerý příchozí provoz, pokud nejsou nastaveny zvláštní pravidla pro příchozí spojení.

Taková architektura Internetu je dostatečně správná pro interakci klient-server, kdy mohou klienti být v soukromých sítích a servery mají globální adresu. Ale vytváří obtíže pro přímé spojení dvou uzlů mezi různými soukromými sítěmi. Přímé spojení dvou uzlů je důležité pro aplikace typu peer-to-peer, jako je přenos hlasu (Skype), vzdálený přístup k počítači (TeamViewer) nebo online hry.

Jednou z nejúčinnějších metod pro navázání peer-to-peer spojení mezi zařízeními nacházejícími se v různých soukromých sítích se nazývá „hole punching“. Tato technika se nejčastěji používá s aplikacemi založenými na protokolu UDP.

Pokud však vaše aplikace vyžaduje zaručenou dodávku dat, například když přenášíte soubory mezi počítači, objeví se při používání UDP množství problémů souvisejících s tím, že UDP není protokolem zaručené dodávky a nezajišťuje doručování paketu ve správném pořadí, na rozdíl od protokolu TCP.

V takovém případě je nutné implementovat aplikační protokol, který zajišťuje požadovanou funkcionalitu a funguje na vrcholu UDP, aby byla zaručena dodávka paketů.

Rád bych hned na začátek zmínil, že existuje technika TCP hole punching pro ustanovení TCP spojení mezi uzly v různých soukromých sítích, ale kvůli nedostatečné podpoře ze strany mnoha NAT zařízení, se obvykle nepovažuje za hlavní metodu pro spojení těchto uzlů.

Dále v tomto článku se budu zabývat pouze implementací protokolu zaručené dodávky. Implementace techniky UDP hole punching bude popsána v následujících článcích.

Požadavky na protokol

  1. Spolehlivá dodávka paketů je realizována prostřednictvím mechanismu pozitivní zpětné vazby (tzv. positive acknowledgment)
  2. Je nutné efektivně přenášet velká data, tj. protokol by měl vyhnout se zbytečným retransmisím paketů.
  3. Musí existovat možnost zrušení mechanizmu potvrzení dodávky (možnost fungovat jako „čistý“ UDP protokol)
  4. Možnost realizace režimu s potvrzením každé zprávy.
  5. Základní jednotkou přenosu dat prostřednictvím protokolu by měla být zpráva.

Tyto požadavky se do značné míry shodují s požadavky na Reliable Data Protocol, popsané v rfc 908 a rfc 1151, a při vývoji tohoto protokolu jsem se na tyto standardy opíral.

Abychom pochopili tyto požadavky, podívejme se na časové diagramy přenosu dat mezi dvěma uzly sítě pomocí protokolů TCP a UDP. Předpokládejme, že v obou případech ztratíme jeden paket.
Přenos neinteraktivních dat po TCP:Implementace protokolu Reliable UDP pro .Net

Jak je vidět z diagramu, v případě ztráty paketů TCP zjistí ztracený paket a informuje o tom odesílatele, přičemž požádá o číslo ztraceného segmentu.
Přenos dat pomocí protokolu UDP:Implementace protokolu Reliable UDP pro .Net

UDP nepodniká žádné kroky k detekci ztrát. Kontrola chyb přenosu v UDP protokolu je plně na aplikaci.

Detekce chyb v TCP protokolu je dosažena díky ustanovení spojení s koncovým uzlem, uchování stavu tohoto spojení, uvedení čísla odeslaných bytů v každém hlavičce paketu a notifikacím o přijetí pomocí potvrzovacího čísla „acknowledge number“.

Dodatečně, pro zvýšení výkonu (tj. odeslání více segmentů bez obdržení potvrzení) TCP protokol využívá takzvané okno přenosu — počet bytů dat, které odesílatel segmentu očekává, že obdrží.

Podrobněji se s TCP protokolem můžete seznámit v rfc 793, s UDP v rfc 768, kde jsou vlastně definovány.

Z výše uvedeného je jasné, že pro vytvoření spolehlivého protokolu pro doručování zpráv na bázi UDP (dále nazývaného Reliable UDP), je nutné implementovat podobné mechanismy přenosu dat jako v TCP. A to konkrétně:

  • udržovat stav spojení
  • používat číslování segmentů
  • používat speciální potvrzovací pakety
  • používat zjednodušený mechanismus okna pro zvýšení propustnosti protokolu

Dále je třeba:

  • signalizovat začátek zprávy, aby se vyčlenily prostředky pro spojení
  • signalizovat konec zprávy, aby došlo k předání obdržené zprávy vyššímu aplikačnímu programu a uvolnění prostředků protokolu
  • umožnit protokolu pro konkrétní spojení vypnout mechanismus potvrzení doručení, aby fungoval jako "čistý" UDP

Záhlaví Reliable UDP

Pamatujme, že UDP datagram je zapouzdřen v IP datagramu. Paket Reliable UDP je odpovídajícím způsobem "zabalen" do UDP datagramu.
Zapouzdření hlavičky Reliable UDP:Implementace protokolu Reliable UDP pro .Net

Struktura hlavičky Reliable UDP je dost jednoduchá:

Implementace protokolu Reliable UDP pro .Net

  • Flags – ovládací vlajky paketu
  • MessageType – typ zprávy, používají ji vyšší aplikační programy k odběru určitých zpráv
  • TransmissionId — identifikační číslo přenosu, spolu s adresou a portem příjemce unikátně určuje spojení
  • PacketNumber – číslo paketu
  • Options – dodatečné možnosti protokolu. V případě prvního paketu se používá k určení velikosti zprávy

Vlajky mohou být následující:

  • FirstPacket — první paket zprávy
  • NoAsk — zpráva nevyžaduje zapnutí mechanismu potvrzení
  • LastPacket — poslední paket zprávy
  • RequestForPacket — potvrzovací paket nebo žádost o ztracený paket

Obecné principy fungování protokolu

Jelikož je Reliable UDP zaměřen na zaručené doručení zprávy mezi dvěma uzly, musí být schopno navázat spojení s druhou stranou. Pro navázání spojení strana odesílatele posílá paket s vlajkou FirstPacket, odpověď na který bude znamenat navázání spojení. Všechny odpovědní pakety, nebo jinými slovy, potvrzovací pakety, vždy nastavují hodnotu pole PacketNumber na o jednu vyšší než nejvyšší hodnota PacketNumber úspěšně přijatých paketů. Do pole Options pro první odeslaný paket se zapisuje velikost zprávy.

K ukončení spojení se používá podobný mechanismus. V posledním paketu zprávy se nastavuje příznak LastPacket. V odpovědném paketu je uvedeno číslo posledního paketu + 1, což pro přijímací stranu znamená úspěšné doručení zprávy.
Schéma vytvoření a ukončení spojení:Implementace protokolu Reliable UDP pro .Net

Jakmile je spojení navázáno, začíná přenos dat. Data se přenášejí po blocích paketů. Každý blok, kromě posledního, obsahuje pevný počet paketů. Tento počet odpovídá velikosti přijímacího/přenosového okna. Poslední blok dat může mít menší počet paketů. Po odeslání každého bloku strana odesílatele čeká na potvrzení o doručení nebo žádost o opětovné odeslání ztracených paketů, přičemž okno přijímání/přenosu zůstává otevřené pro přijímání odpovědí. Po přijetí potvrzení o doručení bloku se okno přijímání/přenosu posune a odešle se další blok dat.

Strana příjemce přijímá pakety. Každý paket se kontroluje, zda spadá do přenosového okna. Pakety, které do okna nepatří, a duplikáty se vylučují. Protože velikost okna je pevně stanovena a je stejná pro přijímající i odesílající stranu, při doručení bloku paketů bez ztrát se okno posune pro přijímání paketů dalšího datového bloku a odešle se potvrzení o doručení. Pokud se okno během stanoveného pracovního časového intervalu nenaplní, spustí se kontrola, které pakety nebyly doručeny, a odešlou se žádosti o opětovné doručení.
Schéma opětovného přenosu:Implementace protokolu Reliable UDP pro .Net

Časové limity a časovače protokolu

Existuje několik důvodů, proč nemusí být spojení navázáno. Například pokud je přijímající strana mimo síť. V takovém případě, při pokusu o navázání spojení, dojde k jeho uzavření z důvodu vypršení času. V implementaci Reliable UDP se používají dva časovače k nastavení časových limitů. První, pracovní časovač, slouží k čekání na odpověď od vzdáleného hostitele. Pokud se spustí na straně odesílatele, provede se opětovné odeslání posledního odeslaného paketu. Pokud se však časovač spustí na straně příjemce, provede se kontrola na ztracené pakety a odešlou se žádosti o opětovné doručení.

Druhý časovač je nezbytný k uzavření spojení v případě ztráty komunikace mezi uzly. Na straně odesílatele se spouští ihned po aktivaci pracovního časovače a čeká na odpověď od vzdáleného uzlu. Pokud nedojde k odpovědi v nastaveném období, spojení se ukončí a zdroje se uvolní. Na straně příjemce se časovač pro uzavření spojení spouští po dvojité aktivaci pracovního časovače. To je nutné jako pojistka proti ztrátě potvrzovacího paketu. Při aktivaci časovače se také uzavře spojení a zdroje se uvolní.

Stavová diagram Reliable UDP

Principy fungování protokolu jsou realizovány v konečném automatu, jehož každý stav odpovídá určité logice zpracování paketů.
Stavová diagram Reliable UDP:

Implementace protokolu Reliable UDP pro .Net

Uzavřeno – ve skutečnosti neovládá stav, jedná se o výchozí a koncový bod automatu. Stav Uzavřeno je považován za blok řízení přenosu, který, realizující asynchronní UDP server, přesměrovává pakety do odpovídajících spojení a spouští zpracování stavů.

FirstPacketSending – počáteční stav, v němž se nachází odchozí spojení při odesílání zprávy.

V tomto stavu se odesílá první paket pro běžné zprávy. Pro zprávy bez potvrzení odeslání je to jediný stav – v něm probíhá odeslání celé zprávy.

SendingCycle – hlavní stav pro přenos paketů zprávy.

Přechod do něj ze stavu FirstPacketSending probíhá po odeslání prvního paketu zprávy. Do tohoto stavu přicházejí všechna potvrzení a žádosti o opakování přenosu. Odchod z něj je možný ve dvou případech – v případě úspěšného doručení zprávy nebo vypršení času.

FirstPacketReceived – počáteční stav pro příjemce zprávy.

V něm se kontroluje správnost začátku přenosu, vytvářejí se potřebné struktury a odesílá se potvrzení o přijetí prvního paketu.

Pro zprávu, která se skládá z jediného paketu a je odeslána bez použití potvrzení o doručení – to je jediný stav. Po zpracování takové zprávy se spojení uzavře.

Sestavování – hlavní stav pro přijímání paketů zprávy.

Zde se provádí zápis paketů do dočasného úložiště, kontrola na ztrátu paketů, odesílání potvrzení o dodání balíku paketů a celého zprávy a odesílání požadavků na opětovné dodání ztracených paketů. V případě úspěšného přijetí celé zprávy - se spojení přepne do stavu , a to provedením následujícího příkazu:, jinak dojde k výstupu po uplynutí časového limitu.

, a to provedením následujícího příkazu: – uzavření spojení v případě úspěšného přijetí celé zprávy.

Tento stav je nezbytný pro sestavení zprávy a v případě, že potvrzení o dodání zprávy bylo ztraceno na cestě k odesílateli. Výstup z tohoto stavu se provádí po uplynutí časového limitu, ale spojení se považuje za úspěšně uzavřené.

Hlubší do kódu. Blok řízení přenosu

Jedním z klíčových prvků Reliable UDP je blok řízení přenosu. Úkolem tohoto bloku je ukládání aktuálních spojení a pomocných prvků, rozdělení přijatých paketů podle příslušných spojení, poskytnutí rozhraní pro odesílání paketů spojení a realizace API protokolu. Blok řízení přenosu přijímá pakety z úrovně UDP a přesměrovává je na zpracování do konečného automatu. Pro příjem paketů je v něm implementován asynchronní UDP server.
Některé členy třídy ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // pole bajtů pro daný klíč. Používá se pro sestavení příchozích zpráv    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // pole bajtů pro daný klíč. Používá se pro odesílání odchozích zpráv.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // záznam spojení pro daný klíč.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // seznam odběratelů zpráv.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // místní socket    
  private Socket m_socketIn;
  // port pro příchozí zprávy
  private int m_port;
  // místní IP adresa
  private IPAddress m_ipAddress;    
  // místní koncový bod    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // kolekce předem inicializovaných
  // stavů konečného automatu
  public StatesCollection States { get; private set; }
  // generátor náhodných čísel. Používá se pro vytváření TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implementace asynchronního UDP serveru:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // Vytvoření nového bufferu pro každý socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // Předání bufferu jako parametru pro asynchronní metodu
  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);
  // Paket byl přijat, připraveni přijmout další
  Receive();
  // Nejjednodušší způsob, jak vyřešit problém s bufferem, je získat odkaz na něj
  // z IAsyncResult.AsyncState
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // Získání hlavičky paketu
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // Přišel neplatný paket - zahazujeme ho
    return;
  }
  // Sestavení klíče pro určení connection recordu pro paket
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // Získání existujícího connection recordu nebo vytvoření nového
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // Spuštění paketu do zpracování ve finitním automatu
  record.State.ReceivePacket(record, header, bytes);
}

Pro každé odesílání zprávy se vytváří struktura, která obsahuje informace o připojení. Tato struktura se nazývá connection record.
Některé členy třídy ReliableUdpConnectionRecord:

interní třída ReliableUdpConnectionRecord : IDisposable
{    
  // pole bajtů se zprávou    
  public byte[] IncomingStream { get; set; }
  // odkaz na stav konečného automatu    
  public ReliableUdpState State { get; set; }    
  // pár, který jednoznačně určuje záznam o připojení
  // v řídícím bloku přenosu     
  public Tuple Key { get; private set;}
  // dolní mez příjmového okna    
  public int WindowLowerBound;
  // velikost přenosového okna
  public readonly int WindowSize;     
  // číslo paketu pro odeslání
  public int SndNext;
  // počet paketů k odeslání
  public int NumberOfPackets;
  // číslo přenosu (to je druhá část Tuple)
  // pro každou zprávu své	
  public readonly Int32 TransmissionId;
  // vzdálený IP endpoint – skutečný příjemce zprávy
  public readonly IPEndPoint RemoteClient;
  // velikost paketu, aby se předešlo fragmentaci na úrovni IP
  // neměla by překračovat MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // blok řízení přenosu
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // enkapsuluje výsledky asynchronní operace pro BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // neodesílat pakety potvrzení
  public bool IsNoAnswerNeeded;
  // poslední správně přijatý paket (vždy nastavován na největší číslo)
  public int RcvCurrent;
  // pole s čísly ztracených paketů
  public int[] LostPackets { get; private set; }
  // dorazil poslední paket. Používá se jako bool.
  public int IsLastPacketReceived = 0;
  //...
}

Hlubší do kódu. Stavy

Stavy implementují konečný automat protokolu Reliable UDP, ve kterém probíhá hlavní zpracování paketů. Abstraktní třída ReliableUdpState poskytuje rozhraní pro stav:

Implementace protokolu Reliable UDP pro .Net

Celou logiku fungování protokolu implementují výše uvedené třídy, spolu s pomocnou třídou, která poskytuje statické metody, jako například konstrukce hlavičky ReliableUdp ze záznamu o připojení.

Dále budou podrobně prozkoumány implementace metod rozhraní, které určují hlavní algoritmy fungování protokolu.

Metoda DisposeByTimeout

Metoda DisposeByTimeout je zodpovědná za uvolnění prostředků připojení po uplynutí časového limitu a za signalizaci úspěšného/ neúspěšného doručení zprávy.
ReliableUdpState.DisposeByTimeout:

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

Je přepsán pouze ve stavu , a to provedením následujícího příkazu:.
Completed.DisposeByTimeout:

chráněná metoda void DisposeByTimeout(object záznam) {
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) záznam;
  // informujeme o úspěšném přijetí zprávy
  SetAsCompleted(connectionRecord);
}

Metoda ProcessPackets

Metoda ProcessPackets se stará o další zpracování jednoho nebo více paketů. Je volána přímo, nebo prostřednictvím časovače na čekání na pakety.

Ve stavu Sestavování metoda je přepsána a odpovídá za kontrolu ztracených paketů a přechod do stavu , a to provedením následujícího příkazu:, v případě přijetí posledního paketu a úspěšného ověření
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) {
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0)) {
    // jsou ztracené pakety, posíláme žádosti o ně
    foreach (int seqNum in connectionRecord.LostPackets) {
      if (seqNum != 0) {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // nastavujeme časovač podruhé pro opakovaný pokus o přenos
    if (!connectionRecord.TimerSecondTry) {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // pokud po dvou pokusech o vyvolání WaitForPacketTimer 
    // se nám nepodařilo získat pakety - spouštíme časovač pro uzavření spojení
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // úspěšné ověření 
  {
    // posíláme potvrzení o přijetí bloku dat
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // místo okamžitého uvolnění prostředků
    // spouštíme časovač, pro případ, že
    // pokud poslední ack nedorazí k odesílateli a ten jej znovu vyžádá.
    // po uplynutí časovače - uvolníme prostředky
    // ve stavu Completed je metoda časovače přepsána
    StartCloseWaitTimer(connectionRecord);
  }
  // to je případ, kdy ack na blok paketů byl ztracen
  else {
    if (!connectionRecord.TimerSecondTry) {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // spouštíme časovač pro uzavření spojení
    StartCloseWaitTimer(connectionRecord);
  }
}

Ve stavu SendingCycle tato metoda je volána pouze podle časovače a odpovídá za opětovné odeslání poslední zprávy, stejně jako za aktivaci časovače pro uzavření spojení.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // znovu odesíláme poslední paket 
  // (při obnovení spojení příjemce znovu odešle žádosti, které k němu nedorazily)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // spouštíme časovač CloseWait – pro čekání na obnovení spojení nebo jeho ukončení
  StartCloseWaitTimer(connectionRecord);
}

Ve stavu , a to provedením následujícího příkazu: Tato metoda zastavuje pracovní časovač a předává zprávu odběratelům.
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // sestavujeme zprávu a předáváme ji odběratelům
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

Metoda ReceivePacket

Ve stavu FirstPacketReceived Hlavním úkolem metody je určit, zda první paket zprávy skutečně přišel na rozhraní, a také sestavit zprávu složenou z jediného paketu.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // vyřaďte paket
    return;
  // kombinace dvou příznaků - FirstPacket a LastPacket - říká, že máme jedinou zprávu
  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))
    {
      // odešlete paket potvrzení          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // podle návrhu všechny čísla paketů začínají od 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // spočítejte počet paketů, které mají přijít
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // zapíše číslo posledního přijatého paketu (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // posuneme okno příjmu o 1
  connectionRecord.WindowLowerBound++;
  // přepněte stav
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // pokud není požadován mechanismus potvrzení
  // spustíme časovač, který uvolní všechny struktury         
  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);
  }
}

Ve stavu SendingCycle Tato metoda byla přetížena pro příjem potvrzení o doručení a žádosti o opakování přenosu.
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ýpočet konečné hranice okna
  // je brána hranice okna + 1, pro potvrzení doručení
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // kontrola na zařazení do okna
  if (header.PacketNumber  windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // zkontrolovat na poslední paket:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // předání dokončeno
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // toto je odpověď na první paket s potvrzením
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // bez posunutí okna
    SendPacket(connectionRecord);
  }
  // přišlo potvrzení o přijetí bloku dat
  else if (header.PacketNumber == windowHighestBound)
  {
    // posouváme okno příjmu/předání
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // nulujeme pole pro kontrolu přenosu
    connectionRecord.WindowControlArray.Nullify();
    // odesíláme blok paketů
    SendPacket(connectionRecord);
  }
  // toto je žádost o opakování přenosu – odesíláme požadovaný paket
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Ve stavu Sestavování V metodě ReceivePacket probíhá hlavní práce na sestavení zprávy z přicházejících paketů.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // zpracování paketů bez mechanismu potvrzení dodání
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // resetujeme časovač
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // zapisujeme data
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // pokud jsme obdrželi paket s posledním flagem - dokončujeme
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // výpočet konečné hranice okna
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // zamítáme pakety, které nepatří do okna
  if (header.PacketNumber  (windowHighestBound))
    return;
  // zamítáme duplicity
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // zapisujeme data 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // zvyšujeme počítadlo paketů        
  connectionRecord.PacketCounter++;
  // zapisujeme do pole řízení okna aktuální číslo paketu        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // nastavujeme nejvyšší přijatý paket        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // restartujeme časovače        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // pokud přišel poslední paket
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // pokud jsme obdrželi všechny pakety okna, resetujeme počítadlo
  // a posíláme paket potvrzení
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // resetujeme počítadlo.      
    connectionRecord.PacketCounter = 0;
    // posunuli jsme okno přenosu
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // nulování pole řízení přenosu
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // pokud poslední paket již existuje        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // kontrolujeme pakety          
    ProcessPackets(connectionRecord);
  }
}

Ve stavu , a to provedením následujícího příkazu: Hlavním úkolem metody je odeslat opakované potvrzení o úspěšném doručení zprávy.
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // znovu odeslat poslední paket, protože
  // poslední ack nedošel k odesílateli
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Metoda SendPacket

Ve stavu FirstPacketSending tato metoda provádí odeslání prvního datového paketu, nebo pokud zpráva nevyžaduje potvrzení dodání — celou zprávu.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;
  // pokud není potvrzení potřeba - odešleme všechny pakety
  // a uvolníme zdroje
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Odesílá se v původním stavu
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // vytvoříme hlavičku paketu a odešleme ji
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // zvyšujeme čítač
  connectionRecord.SndNext++;
  // posunujeme okno
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Spouštíme časovač
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Ve stavu SendingCycle v této metodě dochází k odeslání bloku paketů.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // odesíláme blok paketů      
  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++;
  }
  // v případě velkého přenosového okna znovu spouštíme časovač po odeslání
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
  {
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  }
}

Hlubší do kódu. Vytváření a navazování spojení

Nyní, když jsme se seznámili se základními stavy a metodami používanými pro zpracování stavů, můžeme podrobněji probrat několik příkladů fungování protokolu.
Diagram přenosu dat v normálních podmínkách:Implementace protokolu Reliable UDP pro .Net

Podívejme se podrobně na vytvoření connection record pro připojení a odeslání prvního paketu. Iniciátorem přenosu je vždy aplikace, která vyvolává API metodu pro odesílání zprávy. Dále se použije metoda StartTransmission řídícího bloku přenosu, která spouští přenos dat pro novou zprávu.
Vytvoření odchozího spojení:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "Musíte použít konstruktor s parametry nebo spustit posluchač před odesláním zprávy" );
    }
    // spouštíme zpracování příchozích paketů
    StartListener(LocalEndpoint);
  }
  // vytváříme klíč pro slovník, na základě EndPoint a ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // vytváříme náhodné číslo transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // vytváříme nový záznam pro spojení a kontrolujeme, 
  // zda již takové číslo existuje v našich slovnících
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // pokud existuje – opět generujeme náhodné číslo 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // pokud se znovu nepodařilo – generujeme výjimku
      throw new ArgumentException("Pár TransmissionId & EndPoint již existuje ve slovníku");
  }
  // zahájili jsme stav zpracování         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Odesílání prvního paketu (stav FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // vytváříme hlavičku paketu a odesíláme ji 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // zvyšujeme čítač
  connectionRecord.SndNext++;
  // posunujeme okno
  connectionRecord.WindowLowerBound++;
  // přecházíme do stavu SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Spouštíme timer
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Po odeslání prvního paketu odesílatel přechází do stavu SendingCycle – čekat na potvrzení o dodání paketu.
Strana příjemce pomocí metody EndReceive přijímá odeslaný paket, vytváří nový connection record a předává tento paket se předem zpracovanou hlavičkou k metodě ReceivePacket stavu FirstPacketReceived
Vytváření spojení na přijímající straně:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // paket byl přijat
  // parsujeme hlavičku paketu
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {
    // přišel neplatný paket - odmítáme jej
    return;
  }
  // vytváříme klíč k určení connection record pro paket
  Tuple key = new Tuple(connectedClient, header.TransmissionId);
  // získáváme existující connection record nebo vytváříme nový
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // spouštíme paket zpracování ve finite automatu
  record.State.ReceivePacket(record, header, bytes);
}

Přijetí prvního paketu a odeslání potvrzení (stát FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // odmítáme paket
    return;
  // ...
  // podle designu všechny čísla paketů začínají od 0;
  if (header.PacketNumber != 0)
    return;
  // inicializujeme pole pro ukládání částí zprávy
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // zapisujeme data paketu do pole
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // počítáme počet paketů, které mají přijít
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
  // zapisujeme číslo posledního přijatého paketu (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // posunujeme příjmové okno o 1
  connectionRecord.WindowLowerBound++;
  // přepínáme stav
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  if (/* pokud není třeba mechanismus potvrzení */)
  // ...
  else
  {
    // odesíláme potvrzení
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Hlubší do kódu. Uzavření spojení po časovém limitu

Zpracování časových prodlení je důležitou součástí Reliable UDP. Uvažujme příklad, kdy došlo k selhání na prostředním uzlu a doručení dat v obou směrech se stalo nemožným.
Diagram uzavření spojení z důvodu časového prodlení:Implementace protokolu Reliable UDP pro .Net

Jak ukazuje diagram, pracovní časovač odesílatele se spouští okamžitě po odeslání bloku paketů. To se děje v metodě SendPacket stavu SendingCycle.
Spuštění pracovního časovače (stát SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // odesíláme blok paketů   
  // ...   
  // restartujeme časovač po odeslání
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
}

Časové periody časovače se nastavují při vytváření spojení. Výchozí hodnota ShortTimerPeriod je 5 sekund. V příkladu je nastavena na 1,5 sekundy.

U příchozího spojení se časovač spouští po obdržení posledního doručeného paketu dat, což se děje v metodě ReceivePacket stavu. Sestavování
Aktivace pracovního časovače (stav Assembling):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // restartujeme časovače        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

U příchozího spojení přes čas čekání pracovního časovače nedošlo k přijetí více paketů. Časovač se aktivoval a zavolal metodu ProcessPackets, ve které byly zjištěny ztracené pakety a poprvé byly odeslány žádosti o opětovné doručení.
Odeslání žádostí o opětovné doručení (stav Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*kontrola na ztracené pakety */)
  {
    // odesíláme žádosti o opětovné doručení
    // nastavujeme časovač podruhé, pro pokus o opětovné odeslání
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // pokud po dvou pokusech WaitForPacketTimer 
    // se nepodařilo obdržet pakety - spouštíme časovač pro ukončení spojení
    StartCloseWaitTimer(connectionRecord);
  }
  else if (/*došel poslední paket a úspěšná kontrola */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // pokud ack na blok paketů byl ztracen
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // opětovně odesíláme ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // spouštíme časovač pro ukončení spojení
    StartCloseWaitTimer(connectionRecord);
  }
}

Proměnná TimerSecondTry byla nastavena na true. Tato proměnná odpovídá za opětovné spuštění pracovního časovače.

Na straně odesílatele se také aktivuje pracovní časovač a opětovně se odesílá poslední zaslaný paket.
Aktivace časovače pro uzavření spojení (stav SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // znovu odesíláme poslední paket 
  // ...        
  // aktivujeme časovač CloseWait – pro čekání na obnovení spojení nebo jeho ukončení
  StartCloseWaitTimer(connectionRecord);
}

Po aktivaci v odchozím spojení začíná odpočítávání doby uzavření spojení.
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);
}

Výchozí doba čekání časovače uzavření spojení je 30 sekund.

Po krátké době se znovu spustí pracovní časovač na straně příjemce, což vede k odeslání požadavků a poté se spustí časovač uzavření spojení u příchozího spojení.

Po vypršení časovačů uzavření se všechny zdroje obou záznamů spojení uvolní. Odesílatel oznámí neúspěšnou doručení nadřazené aplikaci (viz API Reliable UDP).
Uvolnění zdrojů záznamu spojení:

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

Hlubší do kódu. Obnova datového přenosu

Diagram obnovy přenosu dat při ztrátě paketu:Implementace protokolu Reliable UDP pro .Net

Jak již bylo diskutováno v uzavření spojení z důvodu časového limitu, po uplynutí pracovního časovače příjemce provede kontrolu na ztracené pakety. V případě ztrát bude sestaven seznam čísel paketů, které se nedostaly k příjemci. Tato čísla se zapíší do pole LostPackets konkrétního spojení a vykoná se odeslání požadavků na opětovné doručení.
Odesílání požadavků na opětovné doručení paketů (stav Assembling):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // jsou ztracené pakety, posíláme požadavky na ně
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Odesílatel přijme žádost o opětovné doručení a zašle chybějící pakety. Je třeba poznamenat, že v této době již běží časovač uzavření spojení u odesílatele a při obdržení žádosti se resetuje.
Opětovné odeslání ztracených paketů (stav SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // reset the connection closing timer 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // this is a request for retransmission – sending the required packet          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Znovu odeslaný paket (packet#3 na diagramu) je přijat příchozím připojením. Probíhá kontrola pro naplnění přijímacího okna a běžný přenos dat se obnovuje.
Kontrola zařazení do přijímacího okna (stav: Skládání):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // zvyšujeme čítač paketů        
  connectionRecord.PacketCounter++;
  // zapisujeme do pole řízení okna aktuální číslo paketu        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // nastavujeme největší přijatý paket        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // restartování časovačů        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // pokud jsme obdrželi všechny pakety okna, pak resetujeme čítač
  // a posíláme potvrzující paket
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // resetujeme čítač.      
    connectionRecord.PacketCounter = 0;
    // posunuli jsme okno přenosu
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // nulujeme pole řízení přenosu
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

API Reliable UDP

Pro interakci s přenosovým protokolem existuje otevřená třída Reliable Udp, která funguje jako obálka nad blokem řízení přenosu. Zde jsou nejdůležitější členové třídy:

veřejná uzavřená třída ReliableUdp : IDisposable
{
  // získá lokální koncový bod
  veřejný IPEndPoint LokálníKoncovýBod
  // vytváří instanci ReliableUdp a spouští
  // naslouchání příchozím paketům na uvedené IP adrese
  // a portu. Hodnota 0 pro port znamená použití
  // dynamicky alokovaného portu
  veřejná ReliableUdp(IPAddress lokálníAdresa, int port = 0)
  // přihlášení na příchozí zprávy
  veřejný ReliableUdpSubscribeObject PřihlásitNaZprávy(ReliableUdpMessageCallback zpětnéVolání, ReliableUdpMessageTypes typZprávy = ReliableUdpMessageTypes.Jakýkoli, IPEndPoint ipEndPoint = null)
  // odhlášení od příjmu zpráv
  veřejná void Odhlásit(ReliableUdpSubscribeObject přihlašovacíObjekt)
  // asynchronně odeslat zprávu
  // Poznámka: kompatibilita se systémem XP a Server 2003 zůstává zachována, protože se používá .NET Framework 4.0
  veřejná Task OdeslatZprávuAsync(ReliableUdpMessage spolehliváUdpZpráva, IPEndPoint vzdálenýKoncovýBod, CancellationToken cToken)
  // zahájit asynchronní odesílání zprávy
  veřejná IAsyncResult ZačítOdesláníZprávy(ReliableUdpMessage spolehliváUdpZpráva, IPEndPoint vzdálenýKoncovýBod, AsyncCallback asynchronníZpětnéVolání, Object stav)
  // získat výsledek asynchronního odesílání
  veřejná bool KonecOdesíláníZprávy(IAsyncResult asynchronníVýsledek)
  // uvolnit prostředky
  veřejná void Dispose()
}

Přijímání zprávy se provádí na základě přihlášení. Podpis delegáta pro metodu zpětného volání:

veřejný delegát void ReliableUdpMessageCallback(ReliableUdpMessage spolehliváUdpZpráva, IPEndPoint vzdálenýKlient);

Zpráva:

veřejná třída ReliableUdpMessage
{
  // typ zprávy, jednoduché výčtové
  veřejný ReliableUdpMessageTypes Typ { get; private set; }
  // data zprávy
  veřejné byte[] Tělo { get; private set; }
  // pokud je nastaveno na true – mechanismus potvrzení doručení bude vypnut
  // pro přenos konkrétní zprávy
  veřejný bool BezPtání { get; private set; }
}

Pro přihlášení na konkrétní typ zpráv a/nebo na konkrétního odesílatele se používají dva volitelné parametry: ReliableUdpMessageTypes typZprávy a IPEndPoint ipEndPoint.

Typy zpráv:

veřejný enum ReliableUdpMessageTypes : krátký
{ 
  // Jakýkoli
  Jakýkoli = 0,
  // Dotaz na STUN server
  StunRequest = 1,
  // Odpověď od STUN serveru
  StunResponse = 2,
  // Přenos souboru
  FileTransfer = 3,
  // ...
}

Odesílání zprávy se provádí asynchronně, proto je v protokolu implementován asynchronní model programování:

veřejná IAsyncResult ZačítOdesláníZprávy(ReliableUdpMessage spolehliváUdpZpráva, IPEndPoint vzdálenýKoncovýBod, AsyncCallback asynchronníZpětnéVolání, Object stav)

Výsledek odeslání zprávy bude true – pokud zpráva úspěšně dorazila k příjemci a false – pokud bylo spojení uzavřeno z důvodu časového limitu:

veřejná bool KonecOdesíláníZprávy(IAsyncResult asynchronníVýsledek)

Závěr

Mnoho věcí nebylo v tomto článku popsáno. Mechanismy synchronizace toku, zpracování výjimek a chyb, implementace asynchronních metod odesílání zpráv. Jádro protokolu, popis logiky zpracování paketů, navázání spojení a zpracování časového limitu by mělo být objasněno pro vás.

Předložená verze protokolu pro spolehlivé doručování je dostatečně stabilní a flexibilní a odpovídá dříve stanoveným požadavkům. Chci však dodat, že popsaná implementace může být vylepšena. Například pro zvýšení propustnosti a dynamickou změnu časových intervalů lze do protokolu přidat mechanismy jako sliding window a RTT. Také by bylo užitečné implementovat mechanismus pro určení MTU mezi uzly spojení (ale pouze v případě odesílání velkých zpráv).

Děkuji za pozornost, těším se na vaše komentáře a poznámky.

P.S. Pro ty, kteří se zajímají o podrobnosti nebo si chtějí protokol jednoduše otestovat, odkaz na projekt na GitHubu:
Projekt Reliable UDP

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

  1. Specifikace protokolu TCP: v angličtině a v ruštině
  2. Specifikace protokolu UDP: v angličtině a v ruštině
  3. Diskuze o protokolu RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Reliable Data Protocol: rfc 908 a rfc 1151
  5. Jednoduchá implementace potvrzení doručení pomocí UDP: Získejte úplnou kontrolu nad svou sítí pomocí .NET a UDP
  6. Článek popisující mechanismy překonávání NAT: Peer-to-Peer komunikace přes překladače síťových adres
  7. Implementace asynchronního modelu programování: Implementace asynchronního programovacího modelu CLR a Jak implementovat design pattern IAsyncResult
  8. Přenášení asynchronního modelu programování do asynchronního šablonového vzoru (APM v TAP):
    TPL a tradiční .NET asynchronní programování
    Interop s jinými asynchronními vzory a typy

Aktualizace: Děkuji mayorovp a sidristij za nápad na přidání úkolu do rozhraní. Kompatibilita knihovny se staršími operačními systémy není narušena, protože 4. framework podporuje jak XP, tak 2003 server.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster