Internet sa už dávno zmenil. Jeden z hlavných protokolov internetu – UDP sa používa aplikáciami nielen na doručovanie datagramov a širokopásmových rozosielaní, ale aj na zabezpečenie „peer-to-peer“ spojení medzi uzlami siete. Vzhľadom na svoju jednoduchú štruktúru sa u tohto protokolu objavilo množstvo neplánovaných spôsobov využitia, avšak nedostatky protokolu, ako napríklad nedostatok garantovaného doručenia, nikam nezmizli. V tomto článku sa opisuje implementácia protokolu garantovaného doručenia nad UDP.
Obsah:
Úvod
Pôvodná architektúra internetu predpokladala homogénny adresný priestor, v ktorom mal každý uzol globálnu a jedinečnú IP adresu a mohol priamo komunikovať s ostatnými uzlami. Teraz internet v skutočnosti má inú architektúru – jednu oblasť globálnych IP adries a množstvo oblastí s privátnymi adresami, skrytými za zariadeniami NAT.V takej architektúre môžu zariadenia nachádzajúce sa v globálnom adresnom priestore ľahko interagovať s niekým v sieti, pretože majú jedinečnú, globálnu smerovateľnú IP adresu. Uzol nachádzajúci sa v súkromnej sieti sa môže pripojiť k iným uzlom v tej istej sieti, ako aj k iným dobre známym uzlom v globálnom adresnom priestore. Takáto interakcia sa dosahuje predovšetkým prostredníctvom mechanizmu prekladania sieťových adries. NAT zariadenia, napríklad Wi-Fi smerovače, vytvárajú špeciálne záznamy v prekladačských tabuľkách pre odchádzajúce pripojenia a modifikujú IP adresy a porty v paketoch. To umožňuje vybudovať odchádzajúce pripojenie zo súkromnej siete k uzlom v globálnom adresnom priestore. Ale súčasne, NAT zariadenia obvykle blokujú celý prichádzajúci prenos, pokiaľ nie sú nastavené samostatné pravidlá pre prichádzajúce pripojenia.
Takáto architektúra internetu je dostatočne správna pre klient-server interakciu, kde klienti môžu byť v súkromných sieťach a servery majú globálnu adresu. Ale vytvára to ťažkosti pre priame pripojenie dvoch uzlov medzi rôznymi súkromnými sieťami. Priame pripojenie dvoch uzlov je dôležité pre aplikácie typu „peer-to-peer“, ako je prenos hlasu (Skype), vzdialený prístup k počítaču (TeamViewer) alebo online hry.
Jeden z najefektívnejších spôsobov na nadviazanie peer-to-peer pripojenia medzi zariadeniami nachádzajúcimi sa v rôznych súkromných sieťach sa nazýva „hole punching“. Táto technika sa najčastejšie používa s aplikáciami založenými na protokole UDP.
Ale ak vaša aplikácia vyžaduje garantované doručenie dát, napríklad pri prenose súborov medzi počítačmi, objaví sa množstvo problémov, pretože UDP nie je protokolom garantovaného doručenia a nezaručuje doručenie paketov v poradí, na rozdiel od protokolu TCP.
V takom prípade, aby sa zabezpečilo garantované doručenie paketov, je potrebné implementovať protokol aplikačnej úrovne, ktorý poskytuje potrebnú funkčnosť a pracuje nad UDP.
Hneď chcem poznamenať, že existuje technika TCP hole punching na nadväzovanie TCP pripojení medzi uzlami v rôznych súkromných sieťach, ale kvôli nedostatočnej podpore zo strany mnohých NAT zariadení sa zvyčajne neopravuje ako hlavný spôsob spájania takýchto uzlov.
Ďalej v tomto článku sa budem venovať iba implementácii protokolu zaručenej dodávky. Implementácia techniky UDP hole punching bude popísaná v ďalších článkoch.
Požiadavky na protokol
- Spoľahlivá dodávka paketov je realizovaná prostredníctvom mechanizmu pozitívnej spätnej väzby (takzvané positive acknowledgment)
- Existuje potreba efektívne prenášať veľké dáta, t.j. protokol by sa mal vyhnúť zbytočným retransláciám paketov.
- Musí existovať možnosť zrušiť mechanizmus potvrdenia dodávky (možnosť fungovať ako "čistý" UDP protokol).
- Možnosť realizovať režim príkazov s potvrdením každého správy.
- Základnou jednotkou prenosu dát v protokole by mala byť správa.
Tieto požiadavky sa do veľkej miery zhodujú s požiadavkami na Reliable Data Protocol, ktoré sú popísané v a , a na týchto štandardoch som založil túto implementáciu protokolu.
Pre porozumenie týchto požiadaviek sa pozrime na časové diagramy prenosu dát medzi dvoma uzlami v sieti pomocou protokolov TCP a UDP. Predpokladajme, že v oboch prípadoch stratíme jeden paket.
Príjem neinteraktívnych dát cez TCP:
Z diagramu je vidieť, že v prípade straty paketov TCP zistí stratený paket a oznámi to odosielateľovi, pričom požaduje číslo strateného segmentu.
Prenos dát pomocou protokolu UDP:
UDP neprijíma žiadne opatrenia na zisťovanie strát. Kontrola chýb prenosu v protokole UDP je plne na aplikácii.
Zistenie chýb v protokole TCP sa dosahuje pomocou nadväzovania pripojenia s cieľovým uzlom, uchovávaním stavu tohto pripojenia, uvádzaním čísla odoslaných bajtov v každom hlavičke paketu a notifikáciami o prijatí pomocou čísla potvrdenia "acknowledge number".
Okrem toho, na zvýšenie efektivity (t.j. na odosielanie viacerých segmentov bez prijímania potvrdenia) protokol TCP využíva takzvané prenosové okno — počet bajtov dát, ktoré odosielateľ segmentu očakáva prijať.
S protokolom TCP sa môžete podrobnejšie oboznámiť v , s UDP v , kde sú vlastne definované.
Z vyššie opísaného je jasné, že na vytvorenie spoľahlivého protokolu doručenia správ nad UDP (ďalej budeme nazývať Reliable UDP), je potrebné implementovať podobné mechanizmy prenášania údajov ako v TCP. A to konkrétne:
- udržiavať stav pripojenia
- používať číslovanie segmentov
- využívať špeciálne potvrdzovacie pakety
- použiť zjednodušený mechanizmus okna na zvýšenie priepustnosti protokolu
Naviac je potrebné:
- signalizovať začiatok správy, aby sa vyčlenili zdroje na pripojenie
- signalizovať skončenie správy, aby sa odovzdala prijatá správa vyššej aplikácii a uvoľnili sa zdroje protokolu
- umožniť protokolu pre konkrétne pripojenia deaktivovať mechanizmus potvrdzovania doručenia, aby fungoval ako "čistý" UDP
Hlavička Reliable UDP
Pripomeňme, že UDP datagram je enkapsulovaný v IP datagrame. Paket Reliable UDP je teda "zabalený" v UDP datagrame.
Enkapsulácia hlavičky Reliable UDP:
Štruktúra hlavičky Reliable UDP je pomerne jednoduchá:

- Flags – ovládacie vlajky paketu
- MessageType – typ správy, používaný vyššími aplikáciami na prihlásenie sa na konkrétne správy
- TransmissionId — číslo prenosu, spolu s adresou a portom príjemcu unikátne určuje pripojenie
- PacketNumber – číslo paketu
- Options – dodatočné možnosti protokolu. V prípade prvého paketu sa používa na určenie veľkosti správy
Vlajky môžu byť nasledujúce:
- FirstPacket — prvý paket správy
- NoAsk — správa nevyžaduje zapnutie mechanizmu potvrdzovania
- LastPacket — posledný paket správy
- RequestForPacket — potvrdzovací paket alebo žiadosť o stratený paket
Základné princípy fungovania protokolu
Keďže Reliable UDP je zameraný na garantované doručenie správy medzi dvoma uzlami, musí byť schopný nadviazať pripojenie s druhou stranou. Na nadviazanie pripojenia strana odosielateľa posiela paket s vlajkou FirstPacket, odpoveď na ktorý bude znamenať nadviazanie pripojenia. Všetky odpovedajúce pakety, alebo inak, potvrdzovacie pakety, vždy nastavujú hodnotu poľa PacketNumber na jednotku vyššie ako najvyššia hodnota PacketNumber úspešne prijatých paketov. V poli Options pre prvý odoslaný paket sa zaznamenáva veľkosť správy.
Na ukončenie pripojenia sa používa podobný mechanizmus. V poslednom balíku správ sa nastaví príznak LastPacket. V odpoveďovom balíku sa uvádza číslo posledného balíka + 1, čo pre prijímajúcu stranu znamená úspešné doručenie správy.
Schéma nadväzovania a ukončenia pripojenia:
Keď sa pripojenie nadviaže, začína sa prenos údajov. Údaje sa prenášajú v blokoch balíkov. Každý blok, okrem posledného, obsahuje pevný počet balíkov. Tieto predpokladajú veľkosť prijímacej/vysielacej slučky. Posledný blok údajov môže mať menší počet balíkov. Po odoslaní každého bloku strana odosielateľa čaká na potvrdenie o doručení, alebo požiadavku na opätovné odoslanie stratených balíkov, pričom sa ponecháva otvorená slučka pre prijatie odpovedí. Po prijatí potvrdenia o doručení bloku sa slučka prijímania/vysielania posunie a odosiela sa ďalší blok údajov.
Strana prijímajúca balíky. Každý balík sa kontroluje, či sa nachádza v prenosovej slučke. Balíky, ktoré sa do slučky nezmestia, a duplikáty sú filtrované. Keďže veľkosť slučky je striktne pevná a rovnaká pre prijímača a odosielateľa, ak sa blok balíkov dodá bez strát, slučka sa posunie na prijatie balíkov ďalšieho bloku údajov a odosiela sa potvrdenie o doručení. Ak sa slučka počas nastaveného pracovného časového intervalu nevyplní, spustí sa kontrola, ktoré balíky neboli doručené a odošlú sa požiadavky na opätovné doručenie.
Schéma opätovného prenosu:
Časové obmedzenia a časovače protokolu
Existuje niekoľko dôvodov, prečo nemusí byť pripojenie nadviazané. Napríklad ak je prijímajúca strana mimo siete. V takom prípade, pri pokuse o nadviazanie pripojenia, sa pripojenie uzavrie z dôvodu časového limitu. V implementácii Reliable UDP sa používajú dva časovače na nastavenie časových limitov. Prvý, pracovný časovač, slúži na čakanie na odpoveď od vzdialeného hostiteľa. Ak sa aktivuje na strane odosielateľa, vykoná sa opätovné odoslanie posledného odoslaného balíka. Ak sa však časovač aktivuje u prijímateľa, vykoná sa kontrola na stratené balíky a odošlú sa požiadavky na opätovné doručenie.
Druhý časovač je potrebný na uzavretie spojenia v prípade absencie komunikácie medzi uzlami. Pre odosielajúcu stranu sa spúšťa okamžite po aktivácii pracovného časovača a čaká na odpoveď od vzdialeného uzla. V prípade absencie odpovede počas stanoveného obdobia sa spojenie ukončuje a zdroje sa uvoľňujú. Pre prijímajúcu stranu sa časovač na uzavretie spojenia spúšťa po dvojitom aktivovaní pracovného časovača. To je potrebné ako ochrana pred stratou potvrdenia paketov. Po aktivácii časovača sa taktiež ukončuje spojenie a zdroje sa uvoľňujú.
Stavová diagram spojeného Reliable UDP
Princípy fungovania protokolu sú implementované v konečnom automate, pričom každý stav zodpovedá určitej logike spracovania paketov.
Diagram stavov Reliable UDP:

Uzavreté – v skutočnosti nie je stavom, to je štartovný a konečný bod pre automat. Za stav Uzavreté sa považuje riadiaci blok prenosu, ktorý, implementujúc asynchrónny UDP server, presmerúva pakety do príslušných spojení a spúšťa spracovanie stavov.
PrvéOdosielaniePaketu – počiatočný stav, v ktorom sa nachádza odchádzajúce spojenie pri odosielaní správy.
V tomto stave sa odosiela prvý paket pre bežné správy. Pre správy bez potvrdenia o odoslaní je to jediné stanovené stavy – v ňom sa odosiela celá správa.
CyklusOdosielania – hlavný stav na prenos paketov správy.
Prechod do neho zo stavu PrvéOdosielaniePaketu sa uskutočňuje po odoslaní prvého paketu správy. Práve do tohto stavu prichádzajú všetky potvrdenia a žiadosti o opakované prenosy. Východ z neho je možný v dvoch prípadoch – pri úspešnom doručení správy alebo z dôvodu timeoutu.
PrvýPacketPrijatý – počiatočný stav pre prijímateľov správy.
V ňom sa kontroluje správnosť začiatku prenosu, vytvárajú sa potrebné štruktúry a odosiela sa potvrdenie o prijatí prvého paketu.
Pre správu pozostávajúcu z jediného paketu a odoslaného bez použitia potvrdenia o doručení – je to jediné stanovené stavy. Po spracovaní takejto správy sa spojenie uzavrie.
Zostavovanie – hlavný stav na prijímanie paketov správy.
V ňom sa vykonáva záznam paketov do dočasného úložiska, kontrola na neprítomnosť strát paketov, odosielanie potvrdení o doručení bloku paketov a správ celých, a odosielanie požiadaviek na opätovné doručenie stratenej paketov. V prípade úspešného prijatia celej správy – spojenie prejde do stavu Completed, inak sa vykonáva výstup podľa časového limitu.
Completed – zatvorenie spojenia v prípade úspešného prijatia celej správy.
Tento stav je potrebný na zostavenie správy a pre prípad, keď bolo potvrdenie o doručení správy stratené na ceste k odosielateľovi. Výstup z tohto stavu sa vykonáva podľa časového limitu, ale spojenie sa považuje za úspešne uzavreté.
Hlbšie do kódu. Blok riadenia prenosu
Jedným z kľúčových prvkov Reliable UDP je blok riadenia prenosu. Úlohou tohto bloku je uchovávať aktuálne spojenia a pomocné prvky, rozdeľovať prichádzajúce pakety do príslušných spojení, poskytovať rozhranie na odosielanie paketov do spojenia a realizovať API protokolu. Blok riadenia prenosu prijíma pakety z úrovne UDP a presmerováva ich na spracovanie do konečného automatu. Na prijímanie paketov v ňom je implementovaný asynchrónny UDP server.
Niektoré členy triedy ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// pole bajtov pre určený kľúč. Používa sa na zostavenie prichádzajúcich správ
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// pole bajtov pre určený kľúč. Používa sa na odoslanie odchádzajúcich správ.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// záznam spojenia pre určený kľúč.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// zoznam odberateľov správ.
private readonly List m_subscribers;
// lokálny socket
private Socket m_socketIn;
// port pre prichádzajúce správy
private int m_port;
// lokálna IP adresa
private IPAddress m_ipAddress;
// lokálny koncový bod
public IPEndPoint LocalEndpoint { get; private set; }
// kolekcia predbežne inicializovaných
// stavov konečného automatu
public StatesCollection States { get; private set; }
// generátor náhodných čísel. Používa sa na vytvorenie TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementácia asynchrónneho UDP servera:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// vytvárame nový buffer pre každý socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// odovzdávame buffer ako parameter pre asynchrónnu metódu
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 bol prijatý, sme pripravení prijímať ďalší
Receive();
// najjednoduchší spôsob, ako vyriešiť problém s bufferom - získať odkaz naň
// z IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// získavame hlavičku paketu
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// prišiel nesprávny paket - zahodíme ho
return;
}
// konštruujeme kľúč na určenie connection record pre paket
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// získavame existujúci connection record alebo vytvárame nový
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// spúšťame paket na spracovanie vo fínom automate
record.State.ReceivePacket(record, header, bytes);
}
Pre každé odoslanie správy sa vytvára štruktúra, ktorá obsahuje informácie o pripojení. Takáto štruktúra sa nazýva connection record.
Niektoré členy triedy ReliableUdpConnectionRecord:
interná trieda ReliableUdpConnectionRecord : IDisposable
{
// pole bajtov so správou
public byte[] IncomingStream { get; set; }
// odkaz na stav konečného automatu
public ReliableUdpState State { get; set; }
// pár, ktorý jednoznačne určuje záznam o pripojení
// v bloku riadenia prenosu
public Tuple Key { get; private set;}
// dolná hranica prijímacieho okna
public int WindowLowerBound;
// veľkosť prenosového okna
public readonly int WindowSize;
// číslo paketu na odoslanie
public int SndNext;
// počet paketov na odoslanie
public int NumberOfPackets;
// číslo prenosu (presne to je druhá časť Tuple)
// pre každú správu vlastné
public readonly Int32 TransmissionId;
// vzdialený IP endpoint – vlastne príjemca správy
public readonly IPEndPoint RemoteClient;
// veľkosť paketu, aby sa predišlo fragmentácii na úrovni IP
// nemala by presiahnuť MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
// blok riadenia prenosu
public readonly ReliableUdpConnectionControlBlock Tcb;
// enkapsuluje výsledky asynchrónnej operácie pre BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// neodosielať pakety potvrdenia
public bool IsNoAnswerNeeded;
// posledný správne prijatý paket (vždy sa nastaví na najväčšie číslo)
public int RcvCurrent;
// pole s číslami stratených paketov
public int[] LostPackets { get; private set; }
// prišiel posledný paket. Používa sa ako bool.
public int IsLastPacketReceived = 0;
//...
}
Hlbšie do kódu. Stavy
Stavy implementujú konečný automat protokolu Reliable UDP, v ktorom prebieha základné spracovanie paketov. Abstraktá trieda ReliableUdpState poskytuje rozhranie pre stav:

Celú logiku práce protokolu implementujú vyššie uvedené triedy v spolupráci s pomocnou triedou, ktorá poskytuje statické metódy, ako napríklad vytváranie hlavičky ReliableUdp z záznamu o pripojení.
Ďalej budú podrobne preskúmané implementácie metód rozhrania, ktoré určujú základné algoritmy práce protokolu.
Metóda DisposeByTimeout
Metóda DisposeByTimeout zodpovedá za uvoľnenie zdrojov pripojenia po uplynutí časového limitu a na signálovanie o úspešnej/neosúhlasenej dodávke správy.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Je predefinovaný iba v stave Completed.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// Oznamujeme o úspešnom prijatí správy
SetAsCompleted(connectionRecord);
}
Metóda ProcessPackets
Metóda ProcessPackets je zodpovedná za dodatočné spracovanie paketu alebo pakete. Je volaná priamo, alebo cez časovač čakania na pakety.
V stave Zostavovanie metóda je predefinovaná a zodpovedá za kontrolu stratených paketov a prechod do stavu Completed, v prípade prijatia posledného paketu a úspešnej kontroly
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// sú stratené pakety, posielame na ne požiadavky
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// nastavíme časovač po druhýkrát, na opakovanú snahu o prenos
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// ak po dvoch pokusoch o WaitForPacketTimer
// sa nepodarilo získať pakety - aktivujeme časovač na uzavretie spojenia
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// úspešná kontrola
{
// posielame potvrdenie o prijatí bloku dát
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// namiesto okamžitej implementácie zdrojov
// uvádzame časovač, pre prípad, ak
// ak posledné ack nedôjde k odosielateľovi a on ho vyžiada znovu.
// po aktivácii časovača - implementujeme zdroje
// v stave Completed je metóda časovača predefinovaná
StartCloseWaitTimer(connectionRecord);
}
// to je situácia, keď ack na blok paketov bol stratený
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// aktivujeme časovač na uzavretie spojenia
StartCloseWaitTimer(connectionRecord);
}
}
V stave CyklusOdosielania táto metóda je volaná iba po časovači a zodpovedá za opakované odosielanie poslednej správy, ako aj za spustenie časovača uzavretia spojenia.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// opätovne posielame posledný paket
// (v prípade obnovenia spojenia uzol-prijímač znova pošle požiadavky, ktoré k nemu nedorazili)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// zapneme časovač CloseWait – na čakaniu na obnovenie spojenia alebo jeho ukončenie
StartCloseWaitTimer(connectionRecord);
}
V stave Completed metóda zastavuje pracovný časovač a posiela správu odberateľom.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// zhromažďujeme správu a odosielame ju odberateľom
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Metóda ReceivePacket
V stave PrvýPacketPrijatý hlavná úloha metódy — určiť, či prvý paket správy skutočne prišiel na rozhranie, ako aj zhromaždiť správu pozostávajúcu z jediného paketu.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
return; // discard packet
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); // send acknowledgment packet
}
SetAsCompleted(connectionRecord);
return;
}
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));
connectionRecord.RcvCurrent = header.PacketNumber;
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);
}
}
V stave CyklusOdosielania Táto metóda je predefinovaná na prijímanie potvrdení o doručení a žiadostí o opätovné odoslanie.
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čnej hranice okna
// berie sa hranica okna + 1, pre získanie potvrdení dodania
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// kontrola na zasahovanie do okna
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// skontrolovať na posledný paket:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// prenos dokončený
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// toto je odpoveď na prvý paket s potvrdením
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// bez posunu okna
SendPacket(connectionRecord);
}
// prišlo potvrdenie o prijatí bloku dát
else if (header.PacketNumber == windowHighestBound)
{
// posúvame okno príjmu/odovzdávania
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// nulujeme pole kontroly odovzdávania
connectionRecord.WindowControlArray.Nullify();
// odosielame blok paketov
SendPacket(connectionRecord);
}
// toto je požiadavka na opätovný prenos – odosielame požadovaný paket
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
V stave Zostavovanie V metóde ReceivePacket prebieha hlavná práca pri skladaní správy z prichádzajúcich paketov.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// spracovanie paketov s vypnutým mechanizmom potvrdenia doručenia
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// resetujeme časovač
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// zapisujeme dáta
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// ak sme dostali paket s posledným flagom - ukončíme
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// výpočet konečnej hranice okna
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// odmietame pakety, ktoré sa nehodia do okna
if (header.PacketNumber (windowHighestBound))
return;
// odmietame duplicity
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// zapisujeme dáta
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// zvyšujeme počítadlo paketov
connectionRecord.PacketCounter++;
// zapisujeme do poľa riadenia okna aktuálne číslo paketu
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// nastavujeme najvyšší prichádzajúci paket
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// reštartujeme časovače
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ak prišiel posledný paket
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// ak sme dostali všetky pakety okna, resetujeme počítadlo
// a posielame potvrdenie
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resetujeme počítadlo.
connectionRecord.PacketCounter = 0;
// posunuli sme okno prenosu
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// nulovanie poľa riadenia prenosu
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ak posledný paket už je
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// kontrolujeme pakety
ProcessPackets(connectionRecord);
}
}
V stave Completed Jednoduchou úlohou metódy je odoslať opakované potvrdenie o úspešnom doručení správy.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// opakované odoslanie posledného paketu kvôli tomu,
// že posledné ack nedošlo k odosielateľovi
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Metóda SendPacket
V stave PrvéOdosielaniePaketu táto metóda uskutočňuje odoslanie prvého dátového paketu, alebo, ak správa nevyžaduje potvrdenie dodania – celú správu.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ak nie je potrebné potvrdenie - odosielame všetky pakety
// a oslobodzujeme prostriedky
if (connectionRecord.IsNoAnswerNeeded)
{
// Tu prebieha odoslanie tak, ako je
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// vytvárame hlavičku paketu a odosielame ju
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// zvyšujeme čítač
connectionRecord.SndNext++;
// posúvame okno
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Spúšťame časovač
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
V stave CyklusOdosielania v tejto metóde prebieha odoslanie bloku paketov.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// posielame blok paketov
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 prípade veľkého okna prenosu, reštartujeme časovač po odoslaní
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Hlbšie do kódu. Vytvorenie a nadviazanie spojení
Teraz, keď sme sa zoznámili so základnými stavmi a metódami používanými na spracovanie stavov, môžeme sa podrobnejšie pozrieť na niekoľko príkladov fungovania protokolu.
Diagram prenosu údajov za normálnych podmienok:
Pozrime sa podrobne na vytvorenie connection record na pripojenie a odoslanie prvej správy. Iniciátorom prenosu je vždy aplikácia, ktorá vyvoláva API metódu na odoslanie správy. Potom sa aktivuje metóda StartTransmission v bloku riadenia prenosu, ktorá spúšťa prenos údajov pre novú správu.
Vytvorenie odchádzajúceho spojenia:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Musíte použiť konštruktor s parametrami alebo spustiť poslucháča pred odoslaním správy" );
}
// spúšťame spracovanie prichádzajúcich paketov
StartListener(LocalEndpoint);
}
// vytvárame kľúč pre slovník, na základe EndPoint a ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// vytvárame náhodné číslo transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// vytvárame nový záznam pre spojenie a kontrolujeme,
// či už takýto číslo existuje v našich slovníkoch
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// ak existuje – opäť 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)))
// ak znovu neúspešne – generujeme výnimku
throw new ArgumentException("Pár TransmissionId & EndPoint už existuje v slovníku");
}
// spustili sme stav do spracovania
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Odoslanie prvej správy (stav FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// vytvárame hlavičku paketu a odosielame ju
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// zvyšujeme čítač
connectionRecord.SndNext++;
// posúvame okno
connectionRecord.WindowLowerBound++;
// prechádzame do stavu SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Spúšťame časovač
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Po odoslaní prvej správy odosielateľ prechádza do stavu CyklusOdosielania – čakať na potvrdenie o doručení správy.
Strana prijímača, pomocou metódy EndReceive, prijíma odoslaný paket, vytvára novú connection record a odovzdáva tento paket, s predtým spracovanou hlavičkou, na spracovanie metóde ReceivePacket stavu PrvýPacketPrijatý
Vytvorenie pripojenia na príjmovej strane:
private void EndReceive(IAsyncResult ar)
{
\/\/ ...
\/\/ paket bol prijatý
\/\/ parsujeme hlavičku paketu
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
\/\/ prišiel nesprávny paket - odmietame ho
return;
}
\/\/ konštruujeme kľúč na určenie záznamu pripojenia pre paket
Tuple key = new Tuple(connectedClient, header.TransmissionId);
\/\/ získavame existujúci záznam pripojenia alebo vytvárame nový
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
\/\/ spúšťame paket na spracovanie vo finálnom automate
record.State.ReceivePacket(record, header, bytes);
}
Prijímanie prvého paketu a odoslanie potvrdenia (stav FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
\/\/ odmietame paket
return;
\/\/ ...
\/\/ podľa dizajnu všetky čísla paketov začínajú od 0;
if (header.PacketNumber != 0)
return;
\/\/ inicializujeme pole na uchovávanie častí správy
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
\/\/ zapisujeme dáta paket do poľa
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
\/\/ počítame počet paketov, ktoré by mali prísť
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
\/\/ zapisujeme číslo posledného prijatého paketu (0)
connectionRecord.RcvCurrent = header.PacketNumber;
\/\/ po posunutí okna prijatia na 1
connectionRecord.WindowLowerBound++;
\/\/ prepíname stav
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*ak nie je potrebný mechanizmus potvrdenia*/)
\/\/ ...
else
{
\/\/ odosielame potvrdenie
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Hlbšie do kódu. Ukončenie spojenia po časovom obmedzení
Spracovanie časových úzkych je dôležitou súčasťou Reliable UDP. Pozrime sa na príklad, kde došlo k poruche na medzuzle a doručovanie dát v oboch smeroch sa stalo nemožným.
Diagram uzavretia pripojenia na základe časového úzku:
Z diagramu je zrejmé, že pracovný časovač odosielateľa sa spúšťa ihneď po odoslaní bloku paketu. To sa deje v metóde SendPacket. CyklusOdosielania.
Aktivácia pracovného časovača (stav SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ odosielame blok paketu
\/\/ ...
\/\/ reštartujeme časovač po odoslaní
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Doba časovača sa nastavuje prilikom uspostavljanja veze. Po defaultu, ShortTimerPeriod iznosi 5 sekundi. U ovom primeru, postavljen je na 1,5 sekundi.
Za dolaznu vezu, tajmer se pokreće nakon primanja poslednjeg paketa podataka, što se dešava u metodi ReceivePacket stanja. Zostavovanie
Aktivacija radnog tajmera (stanje Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// resetujemo tajmere
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
Za dolaznu vezu, tokom perioda čekanja radnog tajmera nije stiglo više paketa. Tajmer se aktivirao i pozvao metodu ProcessPackets, u kojoj su otkriveni izgubljeni paketi i prvi put poslati zahtevi za ponovnu isporuku.
Slanje zahteva za ponovnu isporuku (stanje Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*provera na izgubljene pakete */)
{
// šaljemo zahteve za ponovnu isporuku
// postavljamo tajmer po drugi put, za ponovni pokušaj prenosa
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// ako nakon dva pokušaja aktuelnog WaitForPacketTimer
// nije uspelo da se dobiju paketi - pokrećemo tajmer za zatvaranje veze
StartCloseWaitTimer(connectionRecord);
}
else if (/*stigao poslednji paket i uspešna provera */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// ako je ack na blok paketa izgubljen
else
{
if (!connectionRecord.TimerSecondTry)
{
// ponovo šaljemo ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// pokrećemo tajmer za zatvaranje veze
StartCloseWaitTimer(connectionRecord);
}
}
Promenljiva TimerSecondTry postavila se na true. Ova promenljiva odgovara za ponovni restart radnog tajmera.
S druge strane pošiljaoca takođe se aktivira radni tajmer i ponovo se šalje poslednji poslati paket.
Aktivacija tajmera za zatvaranje veze (stanje SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// ponovo šaljemo poslednji paket
// ...
// aktiviramo tajmer CloseWait – za čekanje oporavka veze ili njenog zatvaranja
StartCloseWaitTimer(connectionRecord);
}
Potom sa spustí časovač na uzavretie spojenia v odchádzajúcom pripojení.
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ý úsek časovača na uzavretie spojenia je predvolene nastavený na 30 sekúnd.
O chvíľu sa znovu spustí pracovný časovač na strane prijímateľa, znovu sa pošlú požiadavky, po čom sa spustí časovač uzavretia spojenia v prichádzajúcom pripojení.
Po aktivácii časovačov na uzavretie sa všetky zdroje oboch záznamov pripojenia uvoľnia. Odosielateľ informuje nadradenú aplikáciu o neúspešnom doručení (pozri API Reliable UDP).
Uvoľnenie zdrojov záznamu pripojenia:
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);
}
}
Hlbšie do kódu. Obnovenie prenosu dát
Diagram prenosu dát pri strate paketu:
Ako už bolo diskutované pri uzatváraní spojenia z dôvodu časového outu, po uplynutí pracovného časovača na strane prijímateľa sa vykoná kontrola na stratu paketov. V prípade, že došlo k stratám paketov, vytvorí sa zoznam čísel paketov, ktoré sa nedostali k prijímateľovi. Tieto čísla sa zapíšu do poľa LostPackets konkrétneho spojenia a vykonajú sa požiadavky na opätovné doručenie.
Odosielanie požiadaviek na opätovné doručenie paketov (stav Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
\/\/ existujú stratené pakety, posielame požiadavky na ne
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
\/\/ ...
}
}
Odosielateľ prijme požiadavku na opätovné doručenie a pošle chýbajúce pakety. Je dôležité poznamenať, že v tomto momente už má odosielateľ spustený časovač na uzavretie spojenia a pri obdržaní požiadavky sa resetuje.
Opätovné odosielanie stratených paketov (stav SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// Obnovenie časovača uzavretia pripojenia
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// ide o požiadavku na opätovné odoslanie – odosielame požadovaný paket
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Opätovne odoslaný paket (packet#3 na diagrame) je prijatý vstupným spojením. Kontroluje sa naplnenie prijímacej okna a bežný prenos dát sa obnovuje.
Kontrola, či sa nachádza v prijímacej okne (stav Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// zvyšujeme počet paketov
connectionRecord.PacketCounter++;
// zapisujeme do poľa správy okna aktuálne číslo paketu
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// nastavujeme najvyšší prichádzajúci paket
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// reštartujeme časovače
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// ak nám prišli všetky pakety okna, tak resetujeme počet
// a posielame potvrdzovací paket
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// resetujeme počet.
connectionRecord.PacketCounter = 0;
// posúvame prijímací okno
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// nulovanie poľa kontroly prenosu
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Na interakciu s protokolom prenosu údajov existuje otvorená trieda Reliable Udp, ktorá je obalom nad blokom správy prenosu. Tu sú najdôležitejšie členy triedy:
verejná sealed trieda ReliableUdp : IDisposable
{
// získa lokálny koncový bod
verejný IPEndPoint LocalEndpoint
// vytvorí inštanciu ReliableUdp a spustí
// počúvanie prichádzajúcich paketov na zadaní IP adrese
// a porte. Hodnota 0 pre port znamená použitie
// dynamicky prideleného portu
verejný ReliableUdp(IPAddress lokálnaAdresa, int port = 0)
// predplatné prijímania prichádzajúcich správ
verejný ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// odhlásenie z prijímania správ
verejný void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// asynchrónne odoslať správu
// Poznámka: kompatibilita so systémom XP a Server 2003 nie je stratená, keďže sa používa .NET Framework 4.0
verejný Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// začať asynchrónne odosielanie správy
verejný IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// získať výsledok asynchrónneho odosielania
verejný bool EndSendMessage(IAsyncResult asyncResult)
// uvoľniť prostriedky
verejný void Dispose()
}
Prijímanie správ sa vykonáva na základe predplatného. Podpis delegáta pre metódu spätného volania:
verejný delegát void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Správa:
verejná trieda ReliableUdpMessage
{
// typ správy, jednoduché vymenovanie
verejný ReliableUdpMessageTypes Type { získať; súkromné; nastaviť; }
// dáta správy
verejné byte[] Body { získať; súkromné; nastaviť; }
// ak je nastavené na true – mechanizmus potvrdenia doručenia bude vypnutý
// pre odoslanie konkrétnej správy
verejný bool NoAsk { získať; súkromné; nastaviť; }
}
Na predplatné pre konkrétny typ správ a/alebo konkrétneho odosielateľa sa používajú dva voliteľné parametre: ReliableUdpMessageTypes messageType a IPEndPoint ipEndPoint.
Typy správ:
verejný enum ReliableUdpMessageTypes : short
{
// Ľubovoľné
Any = 0,
// Požiadavka na STUN server
StunRequest = 1,
// Odpoveď zo STUN servera
StunResponse = 2,
// Prenos súboru
FileTransfer = 3,
// ...
}
Odosielanie správ sa vykonáva asynchrónne, na to je v protokole implementovaný asynchrónny model programovania:
verejný IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Výsledok odoslania správy bude true – ak správa úspešne došla k prijímateľovi a false – ak bolo spojenie uzavreté pre časový limit:
verejný bool EndSendMessage(IAsyncResult asyncResult)
Záver
Mnohé nebolo popísané v rámci tohto článku. Mechanizmy zlaďovania tokov, spracovanie výnimiek a chýb, implementácia asynchrónnych metód odosielania správ. Ale jadro protokolu, popis logiky správy paketov, nadväzovanie spojenia a spracovanie časových limitov by sa mali objasniť pre Vás.
Demonštrovaná verzia protokolu spoľahlivej dodávky je dostatočne odolná a flexibilná, a spĺňa určité predtým definované požiadavky. Ale chcem pridať, že popísaná implementácia môže byť vylepšená. Napríklad, pre zvýšenie priepustnosti a dynamické zmeny intervalov časovačov by do protokolu mohli byť pridané mechanizmy ako sliding window a RTT, tiež by bolo užitočné implementovať mechanizmus na určenie MTU medzi uzlami spojenia (ale iba v prípade odosielania veľkých správ).
Ďakujem za pozornosť, teším sa na Vaše komentáre a pripomienky.
P.S. Pre tých, ktorí majú záujem o podrobnosti alebo si chcú jednoducho otestovať protokol, odkaz na projekt na GitHube:
Užitočné odkazy a články
- Špecifikácia protokolu TCP: a
- Špecifikácia protokolu UDP: a
- Diskusia o RUDP protokole:
- Reliable Data Protocol: a
- Jednoduchá implementácia potvrdenia dodávky cez UDP:
- Článok, ktorý popisuje mechanizmy prekonávania NAT:
- Implementácia asynchrónneho modelu programovania: a
- Presun asynchrónneho modelu programovania do asynchrónneho vzoru založeného na úlohách (APM v TAP):
Aktualizácia: Ďakujem a za návrh pridať úlohu k rozhraniu. Kompatibilita knižnice so starými operačnými systémami nie je narušená, pretože štvrtý rámec podporuje aj XP a 2003 server.
Zdroj: habr.com
