Implementace protokolu Reliable Udp pro .Net

Internet se už dávno změnil. Jeden z hlavních protokolů internetu - UDP je používán aplikacemi nejen k doručování datagramů a vysílání, ale také k poskytování "peer-to-peer" spojení mezi uzly sítě. Tento protokol má díky své jednoduché konstrukci mnoho dříve neplánovaných využití, nicméně nedostatky protokolu, jako je absence garantovaného doručení, nikam nezmizely. Tento článek popisuje implementaci protokolu zaručeného doručení přes UDP.
Obsah:Vstup
Požadavky na protokol
Spolehlivá UDP hlavička
Obecné zásady protokolu
Časové limity a časovače protokolů
Spolehlivý stavový diagram přenosu UDP
Hlouběji do kódu. řídící jednotka převodovky
Hlouběji do kódu. státy

Hlouběji do kódu. Vytváření a navazování spojení
Hlouběji do kódu. Ukončení připojení po vypršení časového limitu
Hlouběji do kódu. Obnovení přenosu dat
Spolehlivé UDP API
Závěr
Užitečné odkazy a články

Vstup

Původní architektura internetu předpokládala homogenní adresní prostor, ve kterém měl každý uzel globální a jedinečnou IP adresu a mohl přímo komunikovat s ostatními uzly. Nyní má internet ve skutečnosti jinou architekturu – jednu oblast globálních IP adres a mnoho oblastí se soukromými adresami skrytými za NAT zařízeními.V této architektuře mohou pouze zařízení v globálním adresovém prostoru snadno komunikovat s kýmkoli v síti, protože mají jedinečnou, globálně směrovatelnou IP adresu. Uzel v privátní síti se může připojit k jiným uzlům ve stejné síti a může se také připojit k jiným dobře známým uzlům v globálním adresním prostoru. Této interakce je dosaženo z velké části díky mechanismu překladu síťových adres. Zařízení NAT, jako jsou směrovače Wi-Fi, vytvářejí speciální položky překladové tabulky pro odchozí připojení a upravují adresy IP a čísla portů v paketech. To umožňuje odchozí připojení z privátní sítě k hostitelům v globálním adresním prostoru. Zároveň však zařízení NAT obvykle blokují veškerý příchozí provoz, pokud nejsou nastavena samostatná pravidla pro příchozí spojení.

Tato architektura Internetu je dostatečně správná pro komunikaci klient-server, kde klienti mohou být v privátních sítích a servery mají globální adresu. To však vytváří potíže pro přímé spojení dvou uzlů mezi nimi rozličný privátní sítě. Přímé spojení mezi dvěma uzly je důležité pro aplikace typu peer-to-peer, jako je přenos hlasu (Skype), získání vzdáleného přístupu k počítači (TeamViewer) nebo online hraní.

Jedna z nejúčinnějších metod pro navázání peer-to-peer spojení mezi zařízeními v různých privátních sítích se nazývá děrování. Tato technika se nejčastěji používá u aplikací založených na protokolu UDP.

Pokud však vaše aplikace vyžaduje zaručené doručení dat, například přenášíte soubory mezi počítači, pak bude mít použití UDP mnoho potíží, protože UDP není zaručeným doručovacím protokolem a nezajišťuje doručování paketů v pořádku, na rozdíl od TCP. protokol.

V tomto případě je pro zajištění garantovaného doručování paketů nutné implementovat protokol aplikační vrstvy, který poskytuje potřebnou funkčnost a funguje přes UDP.

Chci hned poznamenat, že existuje technika děrování TCP pro navazování spojení TCP mezi uzly v různých privátních sítích, ale kvůli nedostatečné podpoře mnoha zařízeními NAT se obvykle nepovažuje za hlavní způsob připojení. takové uzly.

Po zbytek tohoto článku se zaměřím pouze na implementaci protokolu garantovaného doručení. Implementace techniky děrování UDP bude popsána v následujících článcích.

Požadavky na protokol

  1. Spolehlivé doručování paketů realizované prostřednictvím mechanismu pozitivní zpětné vazby (tzv. pozitivního potvrzení)
  2. Potřeba efektivního přenosu velkých dat, tzn. protokol musí zabránit zbytečnému předávání paketů
  3. Mělo by být možné zrušit mechanismus potvrzení doručení (schopnost fungovat jako „čistý“ protokol UDP)
  4. Schopnost implementovat příkazový režim s potvrzením každé zprávy
  5. Základní jednotkou přenosu dat protokolem musí být zpráva

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

Abychom těmto požadavkům porozuměli, podívejme se na načasování přenosu dat mezi dvěma síťovými uzly pomocí protokolů TCP a UDP. Nechť v obou případech ztratíme jeden paket.
Přenos neinteraktivních dat přes TCP:Implementace protokolu Reliable Udp pro .Net

Jak můžete vidět z diagramu, v případě ztráty paketu TCP detekuje ztracený paket a oznámí jej odesílateli dotazem na číslo ztraceného segmentu.
Přenos dat protokolem UDP:Implementace protokolu Reliable Udp pro .Net

UDP neprovádí žádné kroky k detekci ztrát. Kontrola chyb přenosu v protokolu UDP je plně v odpovědnosti aplikace.

Detekce chyb v protokolu TCP je dosažena navázáním spojení s koncovým uzlem, uložením stavu tohoto spojení, uvedením počtu odeslaných bajtů v každé hlavičce paketu a upozorněním na přijetí pomocí čísla potvrzení.

Navíc pro zlepšení výkonu (tj. odeslání více než jednoho segmentu bez přijetí potvrzení) používá protokol TCP tzv. přenosové okno – počet bajtů dat, které odesílatel segmentu očekává, že obdrží.

Další informace o protokolu TCP viz rfc 793, z UDP do rfc 768kde jsou ve skutečnosti definovány.

Z výše uvedeného je zřejmé, že pro vytvoření spolehlivého protokolu pro doručování zpráv přes UDP (dále jen tzv Spolehlivý UDP), je nutné implementovat mechanismy přenosu dat podobné TCP. A to:

  • uložit stav připojení
  • použít číslování segmentů
  • použijte speciální potvrzovací balíčky
  • použijte zjednodušený mechanismus oken ke zvýšení propustnosti protokolu

Kromě toho potřebujete:

  • signalizovat začátek zprávy, alokovat zdroje pro připojení
  • signalizovat konec zprávy, předat přijatou zprávu nadřazené aplikaci a uvolnit prostředky protokolu
  • umožnit protokolu specifickému pro připojení deaktivovat mechanismus potvrzení doručení, aby fungoval jako "čistý" UDP

Spolehlivá UDP hlavička

Připomeňme, že datagram UDP je zapouzdřen v datagramu IP. Reliable UDP paket je vhodně "zabalen" do UDP datagramu.
Spolehlivé zapouzdření hlavičky UDP:Implementace protokolu Reliable Udp pro .Net

Struktura hlavičky Reliable UDP je poměrně jednoduchá:

Implementace protokolu Reliable Udp pro .Net

  • Flags - příznaky kontroly balíku
  • MessageType – typ zprávy používaný upstreamovými aplikacemi k odběru konkrétních zpráv
  • TransmissionId - číslo přenosu spolu s adresou a portem příjemce jednoznačně identifikuje připojení
  • PacketNumber - číslo paketu
  • Možnosti - další možnosti protokolu. V případě prvního paketu se používá k označení velikosti zprávy

Příznaky jsou následující:

  • FirstPacket – první paket zprávy
  • NoAsk – zpráva nevyžaduje aktivaci potvrzovacího mechanismu
  • LastPacket - poslední paket zprávy
  • RequestForPacket - potvrzovací paket nebo požadavek na ztracený paket

Obecné zásady protokolu

Protože se Reliable UDP zaměřuje na zaručený přenos zpráv mezi dvěma uzly, musí být schopen navázat spojení s druhou stranou. Pro navázání spojení odešle odesílatel paket s příznakem FirstPacket, jehož odpověď bude znamenat navázání spojení. Všechny pakety s odpovědí, nebo jinými slovy potvrzovací pakety, vždy nastavují hodnotu pole PacketNumber o jednu vyšší, než je největší hodnota PacketNumber z úspěšně přijatých paketů. Pole Možnosti pro první odeslaný paket představuje velikost zprávy.

Podobný mechanismus se používá k ukončení spojení. Příznak LastPacket je nastaven na poslední paket zprávy. V paketu odpovědi je uvedeno číslo posledního paketu + 1, což pro přijímající stranu znamená úspěšné doručení zprávy.
Schéma navázání a ukončení připojení:Implementace protokolu Reliable Udp pro .Net

Po navázání spojení začne přenos dat. Data jsou přenášena v blocích paketů. Každý blok, kromě posledního, obsahuje pevný počet paketů. Je rovna velikosti přijímacího/vysílacího okna. Poslední blok dat může mít méně paketů. Po odeslání každého bloku odesílající strana čeká na potvrzení doručení nebo požadavek na opětovné doručení ztracených paketů, přičemž okno pro příjem/odeslání zůstává otevřené pro příjem odpovědí. Po obdržení potvrzení o doručení bloku se okno příjmu/odeslání posune a je odeslán další blok dat.

Přijímající strana přijímá pakety. Každý paket je zkontrolován, zda spadá do přenosového okna. Pakety a duplikáty, které nespadají do okna, jsou odfiltrovány. Protože Pokud je velikost okna pevná a stejná pro příjemce i odesílatele, pak v případě bezztrátového doručení bloku paketů se okno posune na příjem paketů dalšího bloku dat a dojde k potvrzení doručení. odesláno. Pokud se okno nezaplní do doby nastavené pracovním časovačem, bude zahájena kontrola, které pakety nebyly doručeny a budou odeslány požadavky na opětovné doručení.
Schéma opětovného přenosu:Implementace protokolu Reliable Udp pro .Net

Časové limity a časovače protokolů

Existuje několik důvodů, proč nelze navázat spojení. Například pokud je příjemce offline. V tomto případě při pokusu o navázání spojení bude spojení ukončeno časovým limitem. Implementace Reliable UDP používá k nastavení časových limitů dva časovače. První, pracovní časovač, se používá k čekání na odpověď od vzdáleného hostitele. Pokud se spustí na straně odesílatele, pak je poslední odeslaný paket odeslán znovu. Pokud časovač vyprší u příjemce, je provedena kontrola ztracených paketů a jsou odeslány požadavky na opětovné doručení.

Druhý časovač je potřebný k uzavření spojení v případě nedostatečné komunikace mezi uzly. Na straně odesílatele se spustí okamžitě po vypršení pracovního časovače a čeká na odpověď vzdáleného uzlu. Pokud během zadaného období nedojde k žádné odpovědi, připojení se ukončí a prostředky se uvolní. Na přijímací straně se časovač uzavření spojení spustí poté, co pracovní časovač vyprší dvakrát. To je nutné k zajištění proti ztrátě potvrzovacího paketu. Když časovač vyprší, připojení je také ukončeno a zdroje jsou uvolněny.

Spolehlivý stavový diagram přenosu UDP

Principy protokolu jsou implementovány v konečném automatu, jehož každý stav je zodpovědný za určitou logiku zpracování paketů.
Spolehlivý stavový diagram UDP:

Implementace protokolu Reliable Udp pro .Net

Zavřeno - není ve skutečnosti stav, je to počáteční a koncový bod pro automat. Pro stát Zavřeno je přijat řídicí blok přenosu, který implementuje asynchronní UDP server, předává pakety příslušným spojením a zahajuje zpracování stavu.

FirstPacketSending – počáteční stav, ve kterém je odchozí spojení při odeslání zprávy.

V tomto stavu je odeslán první paket pro normální zprávy. U zpráv bez potvrzení odeslání je to jediný stav, kdy je odeslána celá zpráva.

Cyklus odesílání – základní stav pro přenos paketů zpráv.

Přechod na něj ze stavu FirstPacketSending provedené po odeslání prvního paketu zprávy. V tomto stavu přicházejí všechna potvrzení a žádosti o opakované přenosy. Výstup z něj je možný ve dvou případech - v případě úspěšného doručení zprávy nebo timeoutem.

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

Zkontroluje správnost začátku přenosu, vytvoří potřebné struktury a odešle potvrzení o přijetí prvního paketu.

U zprávy, která se skládá z jednoho paketu a byla odeslána bez použití potvrzení o doručení, je to jediný stav. Po zpracování takové zprávy je spojení ukončeno.

Montáž – základní stav pro příjem paketů zpráv.

Zapisuje pakety do dočasného úložiště, kontroluje ztrátu paketů, odesílá potvrzení o doručení bloku paketů a celé zprávy a odesílá požadavky na opětovné doručení ztracených paketů. V případě úspěšného přijetí celé zprávy přejde spojení do stavu Dokončeno, jinak vyprší časový limit.

Dokončeno – ukončení spojení v případě úspěšného přijetí celé zprávy.

Tento stav je nutný pro sestavení zprávy a pro případ, kdy se potvrzení o doručení zprávy ztratilo na cestě k odesílateli. Tento stav je ukončen po vypršení časového limitu, ale připojení je považováno za úspěšně uzavřené.

Hlouběji do kódu. řídící jednotka převodovky

Jedním z klíčových prvků Reliable UDP je blok řízení přenosu. Úkolem tohoto bloku je ukládat aktuální spojení a pomocné prvky, distribuovat příchozí pakety na odpovídající spojení, poskytovat rozhraní pro odesílání paketů do spojení a implementovat protokol API. Blok řízení přenosu přijímá pakety z vrstvy UDP a předává je ke zpracování do stavového stroje. Pro příjem paketů implementuje asynchronní UDP server.
Někteří členové třídy ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Implementace asynchronního UDP serveru:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Pro každý přenos zprávy je vytvořena struktura, která obsahuje informace o spojení. Taková struktura se nazývá záznam o připojení.
Někteří členové třídy ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Hlouběji do kódu. státy

Státy implementují stavový automat protokolu Reliable UDP, kde probíhá hlavní zpracování paketů. Abstraktní třída ReliableUdpState poskytuje rozhraní pro stav:

Implementace protokolu Reliable Udp pro .Net

Celá logika protokolu je implementována výše uvedenými třídami spolu s pomocnou třídou, která poskytuje statické metody, jako je například sestavení hlavičky ReliableUdp ze záznamu připojení.

Dále se budeme podrobně zabývat implementací metod rozhraní, které určují základní algoritmy protokolu.

Metoda DisposeByTimeout

Metoda DisposeByTimeout je zodpovědná za uvolnění prostředků připojení po vypršení č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();
}

Přepsáno je pouze ve státě Dokončeno.
Completed.DisposeByTimeout:

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

Metoda ProcessPackets

Metoda ProcessPackets je zodpovědná za dodatečné zpracování balíčku nebo balíčků. Volání přímo nebo prostřednictvím časovače čekání na pakety.

Ve stavu Montáž metoda je přepsána a je zodpovědná za kontrolu ztracených paketů a přechod do stavu Dokončeno, v případě přijetí posledního paketu a úspěšného provedení kontroly
Assembling.ProcessPackets:

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

Ve stavu Cyklus odesílání tato metoda se volá pouze na časovači a je zodpovědná za opětovné odeslání poslední zprávy a také za povolení časovače uzavření připojení.
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Ve stavu Dokončeno metoda zastaví běžící časovač a odešle zprávu předplatitelům.
Completed.ProcessPackets:

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

Metoda ReceivePacket

Ve stavu FirstPacketReceived hlavním úkolem metody je zjistit, zda první paket zprávy skutečně dorazil na rozhraní, a také shromáždit zprávu sestávající z jediného paketu.
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Ve stavu Cyklus odesílání tato metoda je přepsána pro přijímání potvrzení o doručení a požadavků na opakovaný přenos.
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Ve stavu Montáž v metodě ReceivePacket probíhá hlavní práce sestavení zprávy z příchozích paketů.
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

Ve stavu Dokončeno jediným úkolem metody je odeslat opětovné potvrzení o úspěšném doručení zprávy.
Completed.ReceivePacket:

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

Metoda odesílání paketů

Ve stavu FirstPacketSending tato metoda odešle první paket dat, nebo pokud zpráva nevyžaduje potvrzení doručení, celou zprávu.
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Ve stavu Cyklus odesílání při této metodě je odeslán blok paketů.
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Hlouběji do kódu. Vytváření a navazování spojení

Nyní, když jsme viděli základní stavy a metody používané ke zpracování stavů, pojďme si rozebrat pár příkladů fungování protokolu trochu podrobněji.
Diagram přenosu dat za normálních podmínek:Implementace protokolu Reliable Udp pro .Net

Zvažte podrobně tvorbu záznam o připojení pro připojení a odeslání prvního paketu. Přenos vždy iniciuje aplikace, která volá rozhraní API pro odesílání zpráv. Dále je vyvolána metoda StartTransmission řídicího bloku přenosu, která spustí přenos dat pro novou zprávu.
Vytvoření odchozího připojení:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Odeslání prvního paketu (stav FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Po odeslání prvního paketu se odesílatel dostane do stavu Cyklus odesílání – vyčkejte na potvrzení doručení balíku.
Přijímající strana pomocí metody EndReceive přijme odeslaný paket, vytvoří nový záznam o připojení a předá tento paket s předem analyzovanou hlavičkou do metody ReceivePacket daného stavu ke zpracování FirstPacketReceived
Vytvoření připojení na přijímací straně:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

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

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Hlouběji do kódu. Ukončení připojení po vypršení časového limitu

Zpracování časového limitu je důležitou součástí Reliable UDP. Vezměme si příklad, kdy mezilehlý uzel selhal a doručení dat v obou směrech se stalo nemožným.
Diagram pro uzavření spojení časovým limitem:Implementace protokolu Reliable Udp pro .Net

Jak je vidět z diagramu, pracovní časovač odesílatele se spustí ihned po odeslání bloku paketů. To se děje v metodě SendPacket státu Cyklus odesílání.
Povolení pracovního časovače (stav SendingCycle):

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

Časové intervaly se nastavují při vytvoření připojení. Výchozí ShortTimerPeriod je 5 sekund. V příkladu je nastavena na 1,5 sekundy.

U příchozího připojení se časovač spustí po přijetí posledního příchozího datového paketu, to se děje v metodě ReceivePacket státu Montáž
Povolení časovače práce (stav sestavování):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Během čekání na pracovní časovač nepřišly na příchozí spojení žádné další pakety. Časovač se vypnul a zavolal metodu ProcessPackets, kde byly nalezeny ztracené pakety a byly poprvé odeslány požadavky na opětovné doručení.
Odesílání žádostí o opětovné doručení (stav sestavování):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Proměnná TimerSecondTry je nastavena na pravdivý. Tato proměnná je zodpovědná za restartování pracovního časovače.

Na straně odesílatele se také spustí pracovní časovač a znovu se odešle poslední odeslaný paket.
Povolení časovače uzavření připojení (stav SendingCycle):

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

Poté se v odchozím připojení spustí časovač uzavření připojení.
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);
}

Časový limit časovače uzavření připojení je ve výchozím nastavení 30 sekund.

Po krátké době se opět spustí pracovní časovač na straně příjemce, znovu jsou odeslány požadavky, načež se spustí časovač uzavření spojení pro příchozí spojení

Při aktivaci časovačů zavření jsou uvolněny všechny prostředky obou záznamů připojení. Odesílatel ohlásí selhání doručení nadřazené aplikaci (viz Spolehlivé rozhraní UDP API).
Uvolnění zdrojů záznamu připojení:

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

Hlouběji do kódu. Obnovení přenosu dat

Diagram obnovy přenosu dat v případě ztráty paketu:Implementace protokolu Reliable Udp pro .Net

Jak již bylo uvedeno při uzavírání spojení po vypršení časového limitu, když pracovní časovač vyprší, přijímač zkontroluje ztracené pakety. V případě ztráty paketu bude sestaven seznam počtu paketů, které se nedostaly k příjemci. Tato čísla jsou zadána do pole LostPackets konkrétního připojení a jsou odeslány požadavky na opětovné doručení.
Odesílání požadavků na opětovné doručení balíčků (stav sestavování):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Odesílatel přijme požadavek na opětovné doručení a odešle chybějící pakety. Stojí za zmínku, že v tuto chvíli odesílatel již spustil časovač uzavření spojení a po přijetí požadavku 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);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Znovu odeslaný paket (paket č. 3 ve schématu) je přijat příchozím spojením. Je provedena kontrola, zda je přijímací okno plné a zda je obnoven normální přenos dat.
Kontrola shody v přijímacím okně (stav sestavování):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

Spolehlivé UDP API

Pro interakci s protokolem přenosu dat existuje otevřená třída Reliable Udp, což je obal nad blokem řízení přenosu. Zde jsou nejdůležitější členové třídy:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

Zprávy jsou přijímány prostřednictvím předplatného. Delegování podpisu pro metodu zpětného volání:

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

Zpráva:

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

Pro přihlášení k odběru určitého typu zprávy a/nebo konkrétního odesílatele se používají dva volitelné parametry: ReliableUdpMessageTypes messageType a IPEndPoint ipEndPoint.

Typy zpráv:

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

Zpráva je odeslána asynchronně, proto protokol implementuje asynchronní programovací model:

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

Výsledek odeslání zprávy bude pravdivý - pokud zpráva úspěšně dorazila k příjemci a nepravda - pokud bylo spojení ukončeno časovým limitem:

public bool EndSendMessage(IAsyncResult asyncResult)

Závěr

V tomto článku toho nebylo mnoho popsáno. Mechanismy porovnávání vláken, zpracování výjimek a chyb, implementace asynchronních metod odesílání zpráv. Ale jádro protokolu, popis logiky pro zpracování paketů, navazování spojení a zpracování timeoutů, by vám mělo být jasné.

Předvedená verze protokolu spolehlivého doručení je dostatečně robustní a flexibilní, aby splnila dříve definované požadavky. Chci ale dodat, že popsanou implementaci lze vylepšit. Například pro zvýšení propustnosti a dynamickou změnu period časovače lze do protokolu přidat mechanismy jako posuvné okno a RTT, užitečné bude také implementovat mechanismus pro určování MTU mezi uzly připojení (ale pouze v případě, že jsou odesílány velké zprávy) .

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

PS Pro ty, které zajímají podrobnosti nebo si chtějí protokol jen vyzkoušet, odkaz na projekt na GitHube:
Spolehlivý projekt UDP

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

  1. Specifikace protokolu TCP: v angličtině и v ruštině
  2. Specifikace protokolu UDP: v angličtině и v ruštině
  3. Diskuse o protokolu RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Spolehlivý datový protokol: rfc 908 и rfc 1151
  5. Jednoduchá implementace potvrzení doručení přes UDP: Převezměte úplnou kontrolu nad svými sítěmi pomocí .NET a UDP
  6. Článek popisující mechanismy procházení NAT: Peer-to-Peer komunikace napříč síťovými překladači adres
  7. Implementace modelu asynchronního programování: Implementace modelu asynchronního programování CLR и Jak implementovat návrhový vzor IAsyncResult
  8. Portování asynchronního programovacího modelu na asynchronní vzor založený na úloze (APM v TAP):
    TPL a tradiční asynchronní programování .NET
    Interoperabilita s jinými asynchronními vzory a typy

Aktualizace: Děkuji starostaovp и sidristij pro myšlenku přidání úkolu do rozhraní. Kompatibilita knihovny se starými operačními systémy není narušena, protože 4. framework podporuje server XP i 2003.

Zdroj: www.habr.com

Přidat komentář