Az internet régóta megváltozott. Az internet egyik alapvető protokollja – az UDP – nemcsak a datagramok és a széleskörű terjesztések szállítására, hanem a hálózati csomópontok közötti "peer-to-peer" kapcsolatok biztosítására is használható. Egyszerű felépítése miatt a protokollnak számos előre nem tervezett felhasználási módja alakult ki, de a protokoll hátrányai, mint például a garantált kézbesítés hiánya, továbbra is megmaradtak. Ebben a cikkben a garantált kézbesítést biztosító protokoll UDP feletti megvalósítását ismertetjük.
Tartalom:
Bevezetés
Az internet kezdeti architektúrája homogén címzési teret feltételezett, amelyben minden csomópontnak globális és egyedi IP címe volt, és közvetlenül kommunikálhatott más csomópontokkal. Jelenleg az internet valójában más architektúrával bír – egy globális IP címekből álló területtel és számos, NAT eszközök mögött rejtőző, helyi címekből álló területtel.Ebben az architektúrában csak azok az eszközök, amelyek globális címzési területen helyezkednek el, tudnak könnyedén kapcsolatba lépni bárkivel a hálózaton, mivel egyedi, globálisan elérhető IP címmel rendelkeznek. Egy privát hálózaton található csomópont más csomópontokkal tud kapcsolódni a saját hálózatában, valamint jól ismert csomópontokkal a globális címzési területen. Ezt az interakciót nagymértékben a hálózati címek átalakításának mechanizmusa segíti elő. A NAT eszközök, például a Wi-Fi routerek, speciális bejegyzéseket hoznak létre a fordítótáblákban a kimenő kapcsolatok számára, és módosítják az IP címeket és a portszámokat a csomagokban. Ez lehetővé teszi, hogy egy privát hálózatból kimenő kapcsolatot létesítsenek a globális címzési területen található csomópontokkal. Ugyanakkor a NAT eszközök általában blokkolják az összes bejövő forgalmat, ha külön szabályok nincsenek beállítva a bejövő kapcsolatokra.
Ez az internet architektúra elegendően helyes a kliens-szerver interakcióhoz, amikor a kliensek privát hálózatokban helyezkednek el, míg a szerverek globális címmel rendelkeznek. De ez nehézségeket okoz a két csomópont közvetlen kapcsolatánál különböző privát hálózatokban. Két csomópont közvetlen összekapcsolása fontos a „peer-to-peer” alkalmazások számára, mint például a hangátvitel (Skype), távoli számítógép hozzáférés (TeamViewer) vagy online játékok. Az egyik leghatékonyabb módszer a peer-to-peer kapcsolat létrehozására különböző privát hálózatokban található eszközök között a „hole punching”. Ez a technika legtöbbször UDP protokoll alapú alkalmazások esetén használatos.
De ha az alkalmazásának garantált adatátvitelre van szüksége, például ha fájlokat továbbít a számítógépek között, akkor az UDP használata számos nehézséget okozhat, mivel az UDP nem garantálja az adatátvitelt, és nem biztosítja a csomagok sorrendben való kézbesítését, ellentétben a TCP protokollal.
Ebben az esetben a csomagok garantált kézbesítésének biztosításához egy alkalmazási szintű protokollt kell megvalósítani, amely biztosítja a szükséges funkcionalitást, és az UDP fölött működik.
Egy alkalmazási szintű protokoll megvalósítása szükséges ahhoz, hogy biztosítsa a csomagok garantált kézbesítését.
Először is, szeretném megjegyezni, hogy létezik a TCP hole punching technika, amely TCP kapcsolatok létrehozására szolgál különböző magán hálózatokban található csomópontok között, de mivel sok NAT eszköz nem támogatja, általában nem tekintik fő módszernek az ilyen csomópontok összekapcsolására.
A cikk további részében csak a garantált szállítási protokoll megvalósítását fogom megvizsgálni. Az UDP hole punching technika megvalósítását a következő cikkekben fogom leírni.
A protokoll követelményei
- A csomagok megbízható szállítása a pozitív visszajelzési mechanizmuson keresztül valósul meg (úgynevezett positive acknowledgment).
- Szükség van nagy mennyiségű adat hatékony átvitelére, azaz a protokollnak el kell kerülnie a felesleges csomagújraküldéseket.
- Lehetőség kell, hogy legyen a szállítási megerősítő mechanizmus lemondására (azaz működnie kell mint 'tisztán' UDP protokoll).
- Lehetőség az úgynevezett parancsmódban való működésre, ahol minden üzenetet meg kell erősíteni.
- Az alapvető adatátviteli egység a protokoll által az üzenet kell legyen.
Ezek a követelmények nagymértékben egybevágnak a Reliable Data Protocol követelményeivel, amelyeket a következő helyen írtak le: és , és ezen alapelvek alapján dolgoztam ki ezt a protokollt.
E követelmények megértéséhez nézzük meg az adatátviteli idődiagramokat két csomópont között a TCP és UDP protokollok használatával. Tegyük fel, hogy mindkét esetben egy csomag elveszik.
Nem interaktív adatok átvitele TCP-n:
Ahogyan a diagramon látható, csomagok elvesztése esetén a TCP észleli az elveszett csomagot és értesíti erről a küldőt, kérve az elveszett szegmens számát.
Adatok átvitele UDP protokollon:
Az UDP nem tesz lépéseket az elvesztések észlelésére. Az UDP protokoll hibaellenőrzése teljesen az alkalmazásra hárul.
A TCP protokoll hibáinak észlelését a végponttal való kapcsolat létesítése, az azon állapot megőrzése, a küldött byte-ok számának megadása minden csomag fejlécében, valamint a megérkezésről szóló értesítések küldése az 'acknowledge number' számmal valósítja meg.
Továbbá a teljesítmény növelése érdekében (azaz több mint egy szegmens küldése anélkül, hogy megerősítést kapnánk) a TCP protokoll az úgynevezett küldési ablakot használja - a byte-ok számát, amelyet a szegmens küldője vár elfogadni.
Bővebben a TCP protokollról a következő dokumentumban olvashat: , UDP-vel , ahol ezek valójában meghatározva vannak.
A fentiekből világosan látszik, hogy megbízható üzenetküldési protokoll létrehozásához UDP fölött (továbbiakban hívjuk Reliable UDP), hasonló TCP mechanizmusokat kell megvalósítani. Nevezetesen:
- megőrizni a kapcsolat állapotát
- szegmensek számozását használni
- speciális visszaigazoló csomagokat használni
- egyszerűsített ablakmechanizmust alkalmazni a protokoll átvitelének növelésére
Továbbá szükség van:
- az üzenet kezdetének jelzésére, a források allokálásához a kapcsolathoz
- az üzenet végének jelzésére, a megkapott üzenet továbbításához a magasabb szintű alkalmazásnak, és a protokoll forrásainak felszabadításához
- a protokoll számára lehetővé tenni, hogy bizonyos kapcsolatok esetén kikapcsolja a kézbesítési visszaigazolás mechanizmusát, hogy „tiszta” UDP-ként működjön
Reliable UDP fejléc
Emlékezzünk, hogy az UDP datagram IP datagramba van beágyazva. A Reliable UDP csomag ezért „bekerül” az UDP datagramban.
A Reliable UDP fejlécének beágyazása:
A Reliable UDP fejlécének struktúrája meglehetősen egyszerű:

- Flags – a csomag vezérlő zászlói
- MessageType – az üzenet típusa, amelyet a magasabb szintű alkalmazások használnak meghatározott üzenetek előfizetésére
- TransmissionId — a küldés azonosítója, amely a címzett címével és portjával együtt egyedileg meghatározza a kapcsolatot
- PacketNumber – a csomag száma
- Options – a protokoll kiegészítő opciói. Az első csomag esetén az üzenet méretének meghatározására szolgál
A zászlók a következőek lehetnek:
- FirstPacket — az üzenet első csomagja
- NoAsk — az üzenet nem igényel kézbesítési visszaigazolásának bekapcsolását
- LastPacket — az üzenet utolsó csomagja
- RequestForPacket — visszaigazoló csomag vagy elveszett csomag kérés
A protokoll általános működési elvei
Mivel a Reliable UDP a két csomópont közötti garantált üzenetküldésre orientálódik, képesnek kell lennie a kapcsolat létesítésére a másik féllel. A kapcsolat létesítéséhez a küldő fél egy FirstPacket zászlót tartalmazó csomagot küld, amire a válasz a kapcsolat létesítését jelenti. Minden válaszcsomag, vagy más néven visszaigazoló csomag, mindig egy egységgel nagyobb értéket állít be a PacketNumber mezőben, mint a sikeresen érkezett csomagok legnagyobb PacketNumber értéke. Az első elküldött csomag Options mezőjében az üzenet mérete kerül rögzítésre.
A hasonló mechanizmus használatos a kapcsolat lezárásához. Az utolsó csomagban a LastPacket zászló van beállítva. A válaszcsoportban pedig az utolsó csomag számának +1-es értéke szerepel, ami a fogadó fél számára a sikeres üzenettovábbítást jelenti.
Kapcsolat létrehozásának és lezárásának diagramja:
Amikor a kapcsolat létrejön, megkezdődik az adatok átadása. Az adatok csomagok blokkjai formájában kerülnek továbbításra. Minden blokk, az utolsó kivételével, fix számú csomagot tartalmaz, amely megegyezik a fogadó/átviteli ablak méretével. Az utolsó adatblokk kevesebb csomagot is tartalmazhat. Minden blokk elküldése után az adó fél megerősítést vár a kézbesítésről, vagy kérheti a hiányzó csomagok újraküldését, miközben nyitva hagyja a fogadó/átviteli ablakot a válaszok beérkezésére. A blokk kézbesítésének megerősítése után a fogadó/átviteli ablak eltolódik, és elküldésre kerül a következő adatblokk.
A fogadó fél fogadja a csomagokat. Minden csomagot ellenőriznek, hogy belefér-e a küldési ablakba. Az ablakból ki nem eső csomagok és a duplikátumok kiszűrésre kerülnek. Mivel az ablak mérete szigorúan fix és azonos a fogadónál és a küldőnél, ha egy blokk csomagjait hiba nélkül kézbesítik, az ablak eltolódik a következő adatblokk csomagjainak fogadására, és kézbesítési megerősítés kerül elküldésre. Ha az ablak nem töltődik fel a beállított munkafolyamat időzítőjének időtartama alatt, ellenőrzés indul, hogy mely csomagok nem érkeztek meg, és újraküldési kérelmeket küldenek.
Újraküldési diagram:
A protokoll időtúllépései és időzítői
Több oka is lehet annak, hogy nem lehet létrehozni a kapcsolatot. Például ha a fogadó fél offline van. Ilyen esetben, ha a kapcsolat létesítése során időtúllépés történik, a kapcsolat lezárásra kerül. A megbízható UDP implementálásakor két időzítőt használnak az időtúllépések megállapítására. Az első, a munkafolyamat időzítője, a távoli házigazdától kapott válasz várakozására szolgál. Ha ez a küldő fél oldalán aktiválódik, az utolsó elküldött csomagot újraküldik. Ha az időzítő a fogadónál aktiválódik, akkor a hiányzó csomagok ellenőrzésére kerül sor, és kérjük az újraküldést.
A második időzítő szükséges a kapcsolat lezárásához, ha az egységek között nincs kapcsolat. A küldő fél számára azonnal elindul a munkában lévő időzítő aktiválása után, és vár választ a távoli egységtől. Ha a megadott időn belül nincs válasz, a kapcsolat megszűnik, és az erőforrások felszabadulnak. A fogadó fél számára a kapcsolat lezárási időzítője a munkában lévő időzítő kétszeri aktiválása után indítódik. Erre azért van szükség, hogy megakadályozza a visszaigazoló csomag elvesztését. Az időzítő aktiválásakor a kapcsolat is megszűnik, és az erőforrások felszabadulnak.
A Reliable UDP átviteli állapotdiagramja
A protokoll működési elvei végső automatában valósulnak meg, amelynek minden állapota egy adott csomagfeldolgozási logikáért felelős.
Reliábilis UDP állapotdiagram:

Closed – valójában nem állapot, hanem az automata kezdő és végpontja. Az állapot Closed a továbbítási vezérlő blokk, amely a aszinkron UDP szerver megvalósításával átirányítja a csomagokat a megfelelő kapcsolatokra, és elindítja az állapotok feldolgozását.
FirstPacketSending – az alapállapot, amelyben a kimenő kapcsolat van az üzenet küldésekor.
Ebben az állapotban küldik el az első csomagot a normál üzenetekhez. A megerősítés nélküli üzenetek esetén ez az egyetlen állapot – benne történik az összes üzenet elküldése.
SendingCycle – az alapvető állapot az üzenetcsomagok továbbításához.
Azzal az állapottal vált át, amelyből FirstPacketSending történik az üzenet első csomagjának elküldése után. Ebbe az állapotba érkeznek az összes visszaigazolás és újraküldési kérés. A kijárat két esetben lehetséges – sikeres üzenetátvitel esetén vagy időtúllépéskor.
FirstPacketReceived – az üzenet fogadójának kezdeti állapota.
Ebben ellenőrzik a továbbítás helyességét, létrehozzák a szükséges struktúrákat, és visszaigazolást küldenek az első csomag átvételéről.
Egyetlen csomagból álló és megerősítés nélkül küldött üzenet esetén ez az egyetlen állapot. Ilyen üzenet feldolgozása után a kapcsolat lezárul.
Assembling – az üzenetcsomagok fogadásához szükséges alapállapot.
A csomagokat ideiglenes tárolóba rögzíti, ellenőrzi a csomagok hiányát, visszaigazolásokat küld a csomagblokk és a teljes üzenet kézbesítéséről, és elküldi a hiányzó csomagok újraküldésére vonatkozó kéréseket. Ha az összes üzenet sikeresen megérkezik – a kapcsolat állapota megváltozik Completed, ellenkező esetben időtúllépés miatt megszakad.
Completed – kapcsolat lezárása az összes üzenet sikeres átvitele esetén.
Ez az állapot szükséges az üzenet összegyűjtéséhez, és arra az esetre is, ha az üzenet kézbesítéséről szóló visszaigazolás a küldőhöz vezető úton elveszett. Az állapotból való kilépés időtúllépés miatt történik, de a kapcsolat sikeresen lezártnak számít.
Felület mélyen a kódba. Átviteli vezérlő blokk
A Reliable UDP egyik kulcsfontosságú eleme a szállítási vezérlő blokk. Ennek a blokk feladata a jelenlegi kapcsolatok és a segédelemek tárolása, a beérkezett csomagok elosztása a megfelelő kapcsolatok között, interfész biztosítása a csomagok kapcsolatra történő küldéséhez és a protokoll API megvalósítása. A szállítási vezérlő blokk a UDP szintről csomagokat fogad, és azokat egy automatizmus feldolgozására irányítja. A csomagok fogadására aszinkron UDP szerver lett megvalósítva.
A ReliableUdpConnectionControlBlock osztály néhány tagja:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// bájt tömb a megadott kulcs számára. Bejövő üzenetek összegyűjtésére használják
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// bájt tömb a megadott kulcs számára. Kimenő üzenetek küldésére használják.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// kapcsolati nyilvántartás a megadott kulcs számára.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// üzenetekre feliratkozott előfizetők listája.
private readonly List m_subscribers;
// helyi socket
private Socket m_socketIn;
// port bejövő üzenetekhez
private int m_port;
// helyi IP cím
private IPAddress m_ipAddress;
// helyi végpont
public IPEndPoint LocalEndpoint { get; private set; }
// előre inicializált
// állapotok gyűjteménye
public StatesCollection States { get; private set; }
// véletlenszám-generátor. A TransmissionId létrehozásához használják
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Aszinkron UDP szerver megvalósítása:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// új puffert hozunk létre, minden socket.BeginReceiveFrom-hoz
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// a puffert átadjuk paraméterként az aszinkron metódusnak
this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}
private void EndReceive(IAsyncResult ar)
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
// csomag érkezett, készen állunk a következő fogadására
Receive();
// mivel a legegyszerűbb módja a buffer kezelésének, hogy hivatkozást kapunk rá
// az IAsyncResult.AsyncState-ből
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// a csomag fejléce
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// nem megfelelő csomag érkezett - elvetjük
return;
}
// kulcsot hozunk létre a connection record azonosításához a csomag számára
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// meglévő connection recordot kapunk vagy újat hozunk létre
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// a csomag feldolgozása a végső automatába
record.State.ReceivePacket(record, header, bytes);
}
Minden üzenetátvitelhez létrejön egy struktúra, amely tartalmazza a kapcsolat adatait. Ezt a struktúrát hívják connection record.
A ReliableUdpConnectionRecord osztály néhány tagja:
belső osztály ReliableUdpConnectionRecord : IDisposable
{
// bájt tömb üzenettel
public byte[] IncomingStream { get; set; }
// hivatkozás az állapot végső automatájára
public ReliableUdpState State { get; set; }
// pár, amely egyértelműen azonosítja a connection record-ot
// az adatátviteli vezérlő blokkjában
public Tuple Key { get; private set;}
// az alsó határ a fogadóablak számára
public int WindowLowerBound;
// az adatátviteli ablak mérete
public readonly int WindowSize;
// az elküldendő csomag sorszáma
public int SndNext;
// az elküldendő csomagok száma
public int NumberOfPackets;
// az átvitel sorszáma (ez a Tuple második része)
// minden üzenethez saját
public readonly Int32 TransmissionId;
// távoli IP végpont – maga az üzenet címzettje
public readonly IPEndPoint RemoteClient;
// a csomag mérete, hogy elkerüljük a frakcionálást IP szinten
// nem haladhatja meg az MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
// az adatátviteli vezérlő blokk
public readonly ReliableUdpConnectionControlBlock Tcb;
// a BeginSendMessage/EndSendMessage aszinkron művelet eredményeit kapszulázza
public readonly AsyncResultSendMessage AsyncResult;
// ne küldjünk visszaigazoló csomagokat
public bool IsNoAnswerNeeded;
// az utolsó helyes csomag (mindig a legmagasabb számra állítódik)
public int RcvCurrent;
// a vesztes csomagok sorszámát tartalmazó tömb
public int[] LostPackets { get; private set; }
// megérkezett-e az utolsó csomag. Bool-ként használva.
public int IsLastPacketReceived = 0;
//...
}
Felület mélyen a kódba. Állapotok
Az állapotok a Reliable UDP protokoll végső automatáját valósítják meg, amelyben történik a csomagok fő feldolgozása. Az absztrakt ReliableUdpState osztály felületet biztosít az állapot számára:

A protokoll teljes működési logikáját a fent bemutatott osztályok végzik, együtt egy segédosztállyal, amely statikus módszereket kínál, például a ReliableUdp fejléc megépítése a connection record-ból.
Az interfészhez tartozó módszerek megvalósításait részletesen fogjuk megvizsgálni, amelyek a protokoll alapvető algoritmusait határozzák meg.
DisposeByTimeout metódus
A DisposeByTimeout metódus felelős a csatlakozás erőforrásainak felszabadításáért a timeout lejárta után és az üzenet sikeres/rossz átvitelének jelzéséért.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Csak az állapotban felül van definiálva Completed.
Completed.DisposeByTimeout:
védett felülírású void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// Értesítjük a sikeres üzenetfogadásról
SetAsCompleted(connectionRecord);
}
ProcessPackets metódus
A ProcessPackets metódus a csomag vagy csomagok további feldolgozásáért felelős. Közvetlenül vagy csomagok várakozási ideje által hívják meg.
Állapotban Assembling a metódus felülírva van, és a csomagok elvesztésének ellenőrzéséért, valamint az állapotba való átmenetért felelős Completed, az utolsó csomag megérkezése esetén és a sikeres ellenőrzés teljesítésekor
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// Vannak elveszett csomagok, kérjük őket
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// Beállítjuk a második próbához szükséges időzítőt
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// Ha a két esemény után sem sikerült csomagokat kapnunk
// elindítjuk a kapcsolatzárási időzítőt
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// Sikeres ellenőrzés
{
// Megerősítést küldünk az adathalmasztó csomag megérkezéséről
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// Ahelyett, hogy azonnal végrehajtanánk az erőforrásokat
// elindítjuk az időzítőt, arra az esetre, ha
// az utolsó ack nem jut el a küldőhöz és újra kérné
// Az időzítő aktiválásakor - végrehajtjuk az erőforrásokat
// a Completed állapotban a timer metódus felülírva
StartCloseWaitTimer(connectionRecord);
}
// Ez az eset, amikor az ack a csomagok blokkjához elveszett
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// Elindítjuk a kapcsolatzárási időzítőt
StartCloseWaitTimer(connectionRecord);
}
}
Állapotban SendingCycle Ez a metódus csak időzítők által hívható, és a legutolsó üzenet újraküldéséért, valamint a kapcsolatzárási időzítő aktiválásáért felelős.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// újraküldjük az utolsó csomagot
// (a kapcsolat helyreállítása esetén a fogadó csomópont újra elküldi a kérdéseket, amelyek nem érkeztek meg hozzá)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// elindítjuk a CloseWait időzítőt – a kapcsolat helyreállításának vagy lezárásának várakozására
StartCloseWaitTimer(connectionRecord);
}
Állapotban Completed a módszer leállítja a munkához szükséges időzítőt, és üzenetet küld a feliratkozóknak.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// összegyűjtjük az üzenetet és továbbítjuk a feliratkozóknak
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
ReceivePacket metódus
Állapotban FirstPacketReceived a módszer alapvető feladata, hogy meghatározza, valóban megérkezett-e az üzenet első csomagja a felületre, és összegyűjti a csomagot, amely egyetlen csomagból áll.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// eldobott csomag
return;
// a két zászló kombináció - FirstPacket és LastPacket - azt jelzi, hogy csak egy üzenetünk van
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// megerősítő csomag küldése
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// a dizájn szerint az összes csomagszám 0-val kezdődik;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// kiszámítjuk a beérkező csomagok számát
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// rögzítjük az utolsó megkapott csomag számát (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// eltoljuk a fogadási ablakot 1-gyel
connectionRecord.WindowLowerBound++;
// állapot átváltás
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// ha nincs szükség a visszaigazolás mechanizmusra
// elindítjuk az időzítőt, amely felszabadít minden struktúrát
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
else
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Állapotban SendingCycle Ez a metódus felül van írva a kézbesítési megerősítések és újrakérés kérések fogadására.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// végső ablakhatár kiszámítása
// az ablakhatár + 1 kerül meghatározásra a kézbesítési visszaigazolásokhoz
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// ablakon belüli ellenőrzés
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ellenőrizzük az utolsó csomagot:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// átadás befejezve
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// ez az első csomagra adott válasz a megerősítésével
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// ablak eltolása nélkül
SendPacket(connectionRecord);
}
// megérkezett a blokkok adatait megerősítő csomag
else if (header.PacketNumber == windowHighestBound)
{
// eltoljuk az átviteli/átfogó ablakot
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// töröljük az átviteli kontroll tömböt
connectionRecord.WindowControlArray.Nullify();
// csomagblokkok küldése
SendPacket(connectionRecord);
}
// ez egy újraküldési kérés – küldjük a szükséges csomagot
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Állapotban Assembling A ReceivePacket metódusban zajlik a fő munka, amely az érkező csomagokból állítja össze az üzenetet.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// csomagok feldolgozása kézbesítési megerősítés nélkül
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// időzítő törlése
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// adatok rögzítése
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// ha utolsó csomag érkezett, befejezzük
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// az ablak végső határának kiszámítása
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// a nem ablakba eső csomagok eldobása
if (header.PacketNumber (windowHighestBound))
return;
// duplikátumok eldobása
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// adatok rögzítése
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// csomagok számláló növelése
connectionRecord.PacketCounter++;
// az aktuális csomagszám rögzítése az ablakvezérlő tömbben
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// a legnagyobb érkezett csomagszám beállítása
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// időzítők újraindítása
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ha utolsó csomag érkezett
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// ha megkaptuk az ablak összes csomagját, nullázzuk a számlálót
// és megerősítjük a csomagot
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// számláló nullázása.
connectionRecord.PacketCounter = 0;
// az átviteli ablak eltolása
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// az átviteli vezérlő tömb nullázása
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ha az utolsó csomag már megérkezett
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// csomagok ellenőrzése
ProcessPackets(connectionRecord);
}
}
Állapotban Completed A metódus egyetlen feladata a sikeres üzenetátvitel újraigazolásának elküldése.
Befejezett.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// az utolsó csomag újraküldése, mivel az utolsó ack nem jutott el a küldőhöz
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
SendPacket metódus
Állapotban FirstPacketSending ez a metódus az első adatcsomag küldését végzi, vagy ha az üzenet nem igényel kézbesítési visszaigazolást – az egész üzenetet.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ha nincs szükség visszaigazolásra - küldjük az összes csomagot
// és felszabadítjuk a forrásokat
if (connectionRecord.IsNoAnswerNeeded)
{
// Itt történik az As Is küldés
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// létrehozzuk a csomagfejlécet és elküldjük
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// növeljük a számlálót
connectionRecord.SndNext++;
// eltoljuk az ablakot
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Elindítjuk az időzítőt
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Állapotban SendingCycle ebben a metódusban történik a csomagok blokkjának küldése.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// csomagok blokk küldése
for (connectionRecord.PacketCounter = 0;
connectionRecord.PacketCounter < connectionRecord.WindowSize &&
connectionRecord.SndNext < connectionRecord.NumberOfPackets;
connectionRecord.PacketCounter++)
{
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
connectionRecord.SndNext++;
}
// nagy átviteli ablak esetén az időzítőt újraindítjuk a küldés után
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Felület mélyen a kódba. Kapcsolatok létrehozása és beállítása
Most, hogy megismertük az állapotok és a kezeléshez használt metódusok alapjait, néhány példát részletesebben is áttekinthetünk a protokoll működéséről.
Az adatok átvitelének diagramja normál körülmények között:
Vizsgáljuk meg részletesen a létrehozást connection record az első csomag csatlakoztatásához és elküldéséhez. A küldési folyamat kezdeményezője mindig az az alkalmazás, amely az üzenetküldési API-metódust hívja meg. Ezt követően a küldésvezérlő StartTransmission metódusa kerül aktiválásra, amely elindítja az új üzenet adatainak továbbítását.
Kimeneti kapcsolat létrehozása:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "A paraméterekkel rendelkező konstruktort kell használnia, vagy el kell indítania a figyelőt az üzenet küldése előtt" );
}
// elindítjuk a bejövő csomagok feldolgozását
StartListener(LocalEndpoint);
}
// létrehozzuk a kulcsot a szótárhoz, az EndPoint és a ReliableUdpHeader.TransmissionId alapján
byte[] transmissionId = new byte[4];
// véletlenszám generálása a transmissionId-hez
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// új bejegyzés létrehozása a kapcsolathoz és ellenőrizzük,
// hogy létezik-e már ilyen szám a szótárainkban
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// ha létezik - újra generáljuk a véletlenszámot
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// ha megint nem sikerült - kivételt generálunk
throw new ArgumentException("A Pair TransmissionId & EndPoint már létezik a szótárban");
}
// elindítottuk az állapotot
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Az első csomag elküldése (FirstPacketSending állapot):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// csomag fejléc létrehozása és küldése
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// számláló növelése
connectionRecord.SndNext++;
// ablak eltolása
connectionRecord.WindowLowerBound++;
// állapotváltás a SendingCycle-ra
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Időzítő elindítása
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Az első csomag elküldése után a küldő állapotba lép SendingCycle – várakozni a csomag kézbesítésének megerősítésére.
A fogadó fél az EndReceive metódus segítségével fogadja az elküldött csomagot, létrehoz egy új connection record és a megfelelően elemzett fejléccel együtt továbbítja ezt a csomagot a ReceivePacket metódus feldolgozására. FirstPacketReceived
Kapcsolat létrehozása a fogadó oldalon:
private void EndReceive(IAsyncResult ar)
{
// ...
// csomag megérkezett
// a csomag fejléceinek elemzése
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// érvénytelen csomag érkezett - elvetjük
return;
}
// kulcs létrehozása a csomaghoz tartozó kapcsolat rekord meghatározásához
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// meglévő kapcsolat rekord megszerzése vagy új létrehozása
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
// csomag továbbitása a végső automátába
record.State.ReceivePacket(record, header, bytes);
}
Az első csomag fogadása és a visszaigazolás elküldése (ElsőCsomagFogadva állapot):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// elvetjük a csomagot
return;
// ...
// a tervezés szerint az összes csomagszám nulláról kezdődik;
if (header.PacketNumber != 0)
return;
// tömb inicializálása az üzenet részeinek tárolásához
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// csomag adatok írása a tömbbe
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// az érkezendő csomagok számának meghatározása
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// az utolsó fogadott csomag számának rögzítése (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// ablakoffset növelése 1-el
connectionRecord.WindowLowerBound++;
// állapot váltás
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/* ha nem szükséges a megerősítési mechanizmus */)
// ...
else
{
// válaszcsomag küldése
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Felület mélyen a kódba. Kapcsolat bezárása időtúllépés esetén
A time-outok kezelése fontos része a megbízható UDP-nek. Nézzünk egy példát, ahol egy közbenső csomóponton hiba lépett fel, és az adatok átadása lehetetlenné vált mindkét irányban.
Time-out alapján történő kapcsolat lezárásának diagramja:
A diagram alapján látható, hogy az elküldés előtt a munkát indító időzítő azonnal elindul, közvetlenül a csomagblokk elküldése után. Ez történik a SendPacket metódusban SendingCycle.
A munkát indító időzítő aktiválása (SendingCycle állapot):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// csomagblokk küldése
// ...
// időzítő újraindítása a küldés után
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
A kapcsolódási időzítő periódusai a kapcsolat létrehozásakor kerülnek beállításra. Alapértelmezés szerint a ShortTimerPeriod 5 másodperc, a példában pedig 1,5 másodpercre van beállítva.
A bejövő kapcsolatnál az időzítő az utolsó beérkező adatcsomag megérkezése után indul, ez a ReceivePacket módszerben történik. Assembling
A munkafolyamat időzítőjének aktiválása (Az Assembling állapotban):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// időzítők újraindítása
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
A bejövő kapcsolat esetén az időzítő várakozási ideje alatt nem érkezett több csomag. Az időzítő aktiválódott és a ProcessPackets módszert hívta meg, amelyben elveszett csomagok lettek felfedezve és először kérdések lettek küldve az újraküldésre.
Kérdések küldése az újraküldésre (Az Assembling állapotban):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*elveszett csomagok ellenőrzése*/)
{
// kérdések küldése az újraküldésre
// időzítő beállítása másodszor, az újraküldési kísérlethez
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// ha a WaitForPacketTimer két aktiválás után
// nem sikerült megszerezni a csomagokat - indítsuk el a kapcsolat zárási időzítőjét
StartCloseWaitTimer(connectionRecord);
}
else if (/*megérkezett az utolsó csomag és sikeres ellenőrzés*/)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// ha az ack a csomag blokkra elveszett
else
{
if (!connectionRecord.TimerSecondTry)
{
// az ack újraküldése
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// a kapcsolat zárási időzítőjének aktiválása
StartCloseWaitTimer(connectionRecord);
}
}
A TimerSecondTry változó beállítva: igaz. Ez a változó felelős a munkafolyamat időzítőjének újraindításáért.
A küldő oldalán is aktiválódik a munkafolyamat időzítője, és az utolsó elküldött csomag újraküldésre kerül.
A kapcsolat zárási időzítőjének aktiválása (A SendingCycle állapotban):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// az utolsó csomag újraküldése
// ...
// CloseWait időzítő aktiválása – a kapcsolat helyreállításának vagy lezárásának várakozására
StartCloseWaitTimer(connectionRecord);
}
Ezután a kimenő kapcsolatban elindul a kapcsolat lezárási időzítője.
ReliableUdpState.StartCloseWaitTimer:
protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
else
connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}
A kapcsolat lezárási időzítőjének várakozási időszaka alapértelmezés szerint 30 másodperc.
Rövid idő elteltével a fogadó oldalon ismét aktiválódik a munkához kapcsolódó időzítő, újra kérdéseket küldenek, majd elindul a bejövő kapcsolat lezárási időzítője.
Amikor az időzítők aktiválódnak, mindkét connection record erőforrásait felszabadítják. A küldő értesíti a felettes alkalmazást a sikertelen kézbesítésről (lásd: API Reliable UDP).
A connection record erőforrásainak felszabadítása:
public void Dispose()
{
try
{
System.Threading.Monitor.Enter(this.LockerReceive);
}
finally
{
Interlocked.Increment(ref this.IsDone);
if (WaitForPacketsTimer != null)
{
WaitForPacketsTimer.Dispose();
}
if (CloseWaitTimer != null)
{
CloseWaitTimer.Dispose();
}
byte[] stream;
Tcb.IncomingStreams.TryRemove(Key, out stream);
stream = null;
Tcb.OutcomingStreams.TryRemove(Key, out stream);
stream = null;
System.Threading.Monitor.Exit(this.LockerReceive);
}
}
Felület mélyen a kódba. Az adatok átvitelének helyreállítása
Az adatok átvitelének helyreállítási diagramja csomagvesztés esetén:
Ahogy már korábban tárgyaltuk a kapcsolat időtúllépés általi lezárásakor, a munkához kapcsolódó időzítő lejártakor a fogadónál ellenőrzést végeznek a hiányzó csomagokra. Amennyiben hiányzó csomagokat találnak, egy listát állítanak össze a fogadóhoz el nem jutó csomagszámokkal, amelyeket a konkrét kapcsolat LostPackets tömbjébe helyeznek, majd újrakézbesítési kérelmeket indítanak.
Újrakézbesítési kérelmek küldése csomagokhoz (Assembling állapot):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
\/\/ hiányzó csomagok vannak, elküldjük a kéréseket
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
\/\/ ...
}
}
A küldő megkapja az újrakézbesítési kérelmet és elküldi a hiányzó csomagokat. Érdemes megjegyezni, hogy ezen a ponton a küldőnél már elindult a kapcsolat lezárási időzítője, és a kérés beérkezésekor az újraindul.
Hiányzó csomagok újrakézbesítése (SendingCycle állapot):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// a kapcsolat lezárási időzítőjének visszaállítása
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// ez egy újra küldési kérés – küldjük a szükséges csomagot
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
A újraküldött csomagot (packet#3 a diagramon) egy bejövő kapcsolat fogadja. Ellenőrzés történik a fogadási ablak kitöltésére, és a normál adatátvitel helyreáll.
Ellenőrzés a fogadási ablakba való bekerülésre (Assembling állapot):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// a csomagszámláló növelése
connectionRecord.PacketCounter++;
// a fogadási ablak vezérlő tömbbe a jelenlegi csomagszám rögzítése
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// a legnagyobb beérkezett csomag beállítása
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// időzítők újraindítása
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// ha minden csomag megérkezett az ablakból, akkor visszaállítjuk a számlálót
// és küldjük a visszaigazoló csomagot
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// a számláló visszaállítása.
connectionRecord.PacketCounter = 0;
// az adatátviteli ablak eltolása
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// az adatátviteli vezérlő tömb nullázása
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
Reliable UDP API
Az adatátviteli protokollal való kölcsönhatáshoz egy nyilvános Reliable Udp osztály áll rendelkezésre, amely az adatátviteli vezérlő blokk burkolataként szolgál. Íme a legfontosabb osztálytagok:
publikus sealed class ReliableUdp : IDisposable
{
// megkapja a helyi végpontot
public IPEndPoint LocalEndpoint
// létrehozza a ReliableUdp példányt és elindítja
// a bejövő csomagok figyelését a megadott IP címen
// és porton. A 0 érték a portra dinamikusan kiosztott portot jelent
public ReliableUdp(IPAddress localAddress, int port = 0)
// feliratkozás bejövő üzenetekre
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// leiratkozás az üzenetek fogadásáról
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// aszinkron üzenetküldés
// Megjegyzés: a XP-vel és a Server 2003-zal való kompatibilitás nem sérül, mivel a .NET Framework 4.0-t használja
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// aszinkron üzenetküldés indítása
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// az aszinkron küldés eredményének lekérdezése
public bool EndSendMessage(IAsyncResult asyncResult)
// erőforrások felszabadítása
public void Dispose()
}
Az üzenet fogadása feliratkozás útján történik. A visszahívási metódus delegátumának szignatúrája:
public delegate void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Üzenet:
public class ReliableUdpMessage
{
// az üzenet típusa, egyszerű felsorolás
public ReliableUdpMessageTypes Type { get; private set; }
// az üzenet adatai
public byte[] Body { get; private set; }
// ha true-ra van állítva – a kézbesítési megerősítési mechanizmus le lesz tiltva
// egy konkrét üzenet átküldéséhez
public bool NoAsk { get; private set; }
}
Konkrét üzenet típusokra és/vagy konkrét feladókra való feliratkozáshoz két opcionális paraméter használható: ReliableUdpMessageTypes messageType és IPEndPoint ipEndPoint.
Üzenettípusok:
public enum ReliableUdpMessageTypes : short
{
// Bármilyen
Any = 0,
// Kérés a STUN szerverhez
StunRequest = 1,
// Válasz a STUN szervertől
StunResponse = 2,
// Fájlátvitel
FileTransfer = 3,
// ...
}
Az üzenetküldés aszinkron módon történik, a protokollban aszinkron programozási modellt valósítottak meg:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Az üzenetküldés eredménye true lesz – ha az üzenet sikeresen eljutott a címzetthez és false – ha a kapcsolat időtúllépés miatt záródott:
public bool EndSendMessage(IAsyncResult asyncResult)
Zárás
Sok minden nem volt leírva ebben a cikkben. Az adatfolyamok összehangolásának mechanizmusai, kivételek és hibák kezelése, az aszinkron üzenetküldő módszerek megvalósítása. De a protokoll magja, a csomagok feldolgozási logikájának leírása, a kapcsolatok létrehozása és az időtúllépések kezelése tisztázva kell legyen az Ön számára.
A bemutatott megbízható adatátviteli protokoll verziója elég stabil és rugalmas, megfelel a korábban meghatározott követelményeknek. De szeretnék hozzátenni, hogy a leírt megvalósítás fejleszthető. Például a sávszélesség növelése és a protokoll időzítőinek dinamikus megváltoztatása érdekében olyan mechanizmusokat lehetne hozzáadni, mint a csúszó ablak és az RTT, valamint hasznos lenne a MTU meghatározásának mechanizmusának megvalósítása a kapcsolat csomópontjai között (de csak nagy üzenetek küldése esetén).
Köszönöm a figyelmet, várom a megjegyzéseit és észrevételeit.
P.S. Azok számára, akik érdeklődnek a részletek iránt, vagy csak tesztelni szeretnék a protokollt, íme a projekt linkje a GitHubon:
Hasznos linkek és cikkek
- TCP protokoll specifikáció: és
- UDP protokoll specifikáció: és
- RUDP protokoll megbeszélése:
- Reliable Data Protocol: és
- Egyszerű megvalósítás a UDP-n keresztüli kézbesítés megerősítésére:
- Cikk a NAT-ok leküzdési mechanizmusainak leírásával:
- Az aszinkron programozási modell megvalósítása: és
- Az aszinkron programozási modell átvedlése egy feladat-alapú aszinkron sablonba (APM a TAP-ban):
Frissítés: Köszönöm és az interfészhez való feladat hozzáadásának ötletéért. A könyvtár kompatibilitása nem sérül a régi operációs rendszerekkel, mivel a 4. keret támogatja az XP és a 2003 server-t is.
Forrás: habr.com
