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
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
- Spolehlivá dodávka paketů je realizována prostřednictvím mechanismu pozitivní zpětné vazby (tzv. positive acknowledgment)
- Je nutné efektivně přenášet velká data, tj. protokol by měl vyhnout se zbytečným retransmisím paketů.
- Musí existovat možnost zrušení mechanizmu potvrzení dodávky (možnost fungovat jako „čistý“ UDP protokol)
- Možnost realizace režimu s potvrzením každé zprávy.
- 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 a , 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:
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:
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 , s UDP v , 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:
Struktura hlavičky Reliable UDP je dost jednoduchá:

- 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í:
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:
Č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:

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:

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:
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í:
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:
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:
Užitečné odkazy a články
- Specifikace protokolu TCP: a
- Specifikace protokolu UDP: a
- Diskuze o protokolu RUDP:
- Reliable Data Protocol: a
- Jednoduchá implementace potvrzení doručení pomocí UDP:
- Článek popisující mechanismy překonávání NAT:
- Implementace asynchronního modelu programování: a
- Přenášení asynchronního modelu programování do asynchronního šablonového vzoru (APM v TAP):
Aktualizace: Děkuji a 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
