Internet on ammu muutunud. Üks peamisi Interneti protokolle — UDP, mida kasutatakse mitte ainult andmegrammide ja levitamise jaoks, vaid ka «peer-to-peer» ühenduste tagamiseks võrgu sõlmede vahel. Tänu oma lihtsale struktuurile on sellel protokollil ilmnenud palju ootamatuid kasutusviise, kuid protokolli puudused, nagu näiteks tagatud kohaletoimetamise puudumine, ei ole kuhugi kadunud. Selles artiklis käsitletakse tagatud kohaletoimetamise protokolli rakendamist UDP peal.
Sisu:
Sissejuhatus
Algne Interneti arhitektuur tähendas ühtset aadressiruumi, kus igal sõlmel oli globaalne ja ainulaadne IP-aadress ning see sai otse suhelda teiste sõlmede vahel. Praegu on Internetil aga tegelikult teine arhitektuur – üks globaalsete IP-aadresside piirkond ja palju privaatsete aadressidega piirkondi, mis on NAT-seadmete taha peidetud.Sellises arhitektuuris saavad ainult need seadmed, mis asuvad globaalsetes aadressiruumi, hõlpsasti suhelda teistega võrgus, kuna neil on ainulaadne, globaalne marsruutitav IP-aadress. Privaatvõrgus asuv sõlm võib ühenduda teiste sama võrgu sõlmedega ja samuti ühenduda teiste hästi tuntud sõlmedega globaalsetes aadressiruumi. Selline suhtlemine saavutatakse suuresti võrguaadresside teisendamise mehhanismi kaudu. NAT-seadmed, nagu Wi-Fi marsruuterid, loovad spetsiaalsed kirjed edastamise tabelites väljaminevate ühenduste jaoks ja muudavad pakettides IP-aadresse ja pordinumbreid. See võimaldab privaatvõrgust luua väljamineva ühenduse globaalsetes aadressiruumi asuvate sõlmedega. Kuid samal ajal blokeerivad NAT-seadmed tavaliselt kogu sissetuleva liikluse, kui sissetulevate ühenduste jaoks pole kehtestatud eraldi reegleid.
Selline Interneti arhitektuur on piisavalt õige kliendi-serveri suhtluse jaoks, kus kliendid võivad asuda privaatsetes võrkudes, kuid serveritel on globaalne aadress. Kuid see tekitab raskusi kahe sõlme vaheline otsese ühenduse loomisega erinevate privaatsete võrkude vahel. erinevate privaatsüsteemide vahel. Otsene ühendus kahe sõlme vahel on oluline "peer-to-peer" rakenduste jaoks, nagu näiteks häälteenus (Skype), kaugarvuti juurde pääsemine (TeamViewer) või online-mängud.
Üks kõige tõhusamaid meetodeid peer-to-peer ühenduse loomisel erinevates privaatvõrkudes asuvate seadmete vahel on nii nimetatud "hole punching". Seda tehnikat kasutatakse kõige sagedamini UDP protokolli põhjal töötavates rakendustes.
Kuid kui teie rakendusele on vajalik andmete garanteeritud kohaletoimetamine, näiteks kui edastate faile arvutite vahel, siis tekivad UDP kasutamisel palju raskusi, kuna UDP ei ole garanteeritud kohaletoimetamise protokoll ega tagasta pakette õiges järjekorras, erinevalt TCP protokollist.
Sellisel juhul on garanteeritud pakettide kohaletoimetamise tagamiseks vajalik rakendada rakendusprotokoll, mis tagab vajalikud funktsioonid ja töötab UDP peal.
Tahaksin kohe märkida, et on olemas TCP hole punching tehnika, et luua TCP ühendusi sõlmede vahel erinevates privaatvõrkudes, kuid paljude NAT-seadmete toe puudumise tõttu ei peeta seda tavaliselt peamiseks viisiks selliste sõlmede ühendamiseks.
Edasi käesolevas artiklis käsitlen ainult garanteeritud kohaletoimetamise protokolli rakendust. UDP hole punching tehnika rakendust kirjeldatakse järgmistes artiklites.
Protokolli nõuded
- Usaldusväärne pakettide kohaletoimetamine, mille on rakendanud positiivne tagasiside mehhanism (nn positiivne tunnustus)
- Suure andmemahu efektiivne edastamine, see tähendab, et protokoll peab vältima tarbetute pakettide edastamisi.
- Peab olema võimalus tühistada kohaletoimetamise kinnitamise mehhanismi (võimalus töötada kui "puhas" UDP protokoll)
- Võime rakendada käsurežiimi, kinnitades iga sõnumi
- Andmete edastamise aluseks protokolli järgi peab olema sõnum
Need nõuded kattuvad suurel määral Reliable Data Protocoli nõuetega, mis on kirjeldatud ja , ja ma lähtusin nende standardite põhjal selle protokolli väljatöötamisel.
Nende nõuete mõistmiseks vaatame ajadiagramme, mis näitavad andmete edastamist kahe võrgu sõlme vahel protokollide TCP ja UDP kaudu. Eeldame, et mõlemas olukorras on kaotatud üks pakett.
Andmete edastamine mitteinteraktiivsete andmete kaudu TCP kaudu:
Nagu diagrammil näha, tuvastab TCP pakettide kaotuse ja teavitab sellest saadetist, paludes kadunud segmentide numbreid.
Andmete edastamine UDP protokolli kaudu:
UDP ei võta kaotuste tuvastamiseks mingeid meetmeid. Veakontroll UDP protokollis lasub täielikult rakendusel.
Veakontroll TCP protokollis saavutatakse tänu ühenduse loomisele lõpp-punktiga, selle ühenduse staatuse säilitamisele, edastatud baitide arvu märkimisele igas paketi päises ning kinnitusnumbrite edastamisele 'acknowledge number' kaudu.
Lisaks kasutab TCP protokoll, et tõsta jõudlust (s.t. saata rohkem kui üks segment ilma kinnitust saamata), nn edastamise akent — andmete baitide arvu, mida segmentide saatja ootab vastuvõetuna.
TCP protokolliga saab lähemalt tutvuda , UDP osas aga , kus need on tegelikult määratletud.
Ülest kirjeldatust on selge, et usaldusväärse sõnumitoimetamise protokolli loomine UDP peal (edaspidi nimetame seda Reliable UDP), nõuab andmete edastamiseks sarnaste mehhanismide rakendamist nagu TCP-l. Nimelt:
- hoida ühenduse olekut
- kasutada segmentide nummerdamist
- kasutada eripakette kinnitamiseks
- kasutada lihtsustatud akmehhanismi, et suurendada protokolli läbilaske võimet
Lisaks on vajalik:
- indikaatorite saatmine sõnumi algusest, et ressursse ühenduse jaoks eraldada
- indikaatorite saatmine sõnumi lõpust, et edastada saadud sõnum ülejäänud rakendusele ja vabastada protokolli ressursid
- lubada protokollil lubada kinnituste mehhanismi väljalülitamist konkreetsete ühenduste jaoks, et toimida nagu "puhas" UDP
Reliable UDP päis
Mäletame, et UDP datagramm on kapseldatud IP datagrammi. Reliable UDP paketid kapseldatakse vastavalt UDP datagrammidesse.
Reliable UDP pealkirja kapseldamine:
Reliable UDP pealkirja struktuur on piisavalt lihtne:

- Flags – paketi juhtflaggid
- MessageType – sõnumi tüüp, mida kasutatakse kõrgematel rakendustel, et tellida teatud sõnumeid
- TransmissionId — edastuse number, mis koos adressaadi ja sadama aadressiga unikaalselt määratleb ühenduse
- PacketNumber – paketi number
- Options – protokolli täiendavad valikud. Esimese paketi puhul kasutatakse sõnumi suuruse näitamiseks
Flags võivad olla järgmised:
- FirstPacket — sõnumi esimene pakett
- NoAsk — sõnum ei nõua kinnituse mehhanismi kasutamist
- LastPacket — sõnumi viimane pakett
- RequestForPacket — kinnituse pakett või kadunud paketi taotlus
Protokolli üldpõhimõtted
Kuna Reliable UDP on suunatud sõnumi garanteeritud edastamisele kahe sõlme vahel, peab see suutma luua ühenduse teise poolega. Ühenduse loomiseks saadab saatja esimese paketi, millel on FirstPacket lipp, millele vastus tähendab ühenduse loomist. Kõik vastuspaketid, või teisiti öeldes, kinnituspaketid, seadistavad alati PacketNumber välja väärtuse, mis on üks suurem kui suurim PacketNumber väärtus edukalt saabunud pakettide seas. Esimese saadetud paketi Options väljal kajastatakse sõnumi suurust.
Ühenduse lõpetamiseks kasutatakse sarnast mehhanismi. Viimases sõnumipaketis on seatud LastPacket lipp. Vastuspaketis osutatakse viimase paketi numbrile + 1, mis tähendab vastuvõtja jaoks sõnumi edukat kohaletoimetamist.
Diagramm ühenduse loomise ja lõpetamise kohta:
Kui ühendus on loodud, algab andmete edastamine. Andmed edastatakse paketikaubikute kaupa. Iga kaubik, välja arvatud viimane, sisaldab fikseeritud arvu pakette. See arv on vastuvõtu/edastuseaken. Viimasel andmekaubikul võib olla vähem pakette. Iga kaubiku saatmise järel ootab saatja kinnitusest, et see on kätte toimetatud, või allalaaditud kaotatud paketid, avades vastuseid eelneva vastuvõtu/edastuse jaoks. Pärast kaubiku kätte toimetamise kinnitamise saamist nihkub vastuvõtu/edastuseaken ja saadetakse järgmine andmekaubik.
Saaja aktsepteerib pakette. Igal pakett kontrollitakse, kas see sobib edastamisaknaa. Akna ulatusest välja jäävad paketid ja duplikaad filtreeritakse välja. Kuna akna suurus on rangelt fikseeritud ja sama nii saajal kui ka saatjal, siis juhul, kui blokikapid saadetakse ilma kaotuseta, nihkub aken järgmise andmeploki vastuvõtmiseks ja saadetakse kinnitus saadetuse kohta. Kui aken ei täitu seadistatud tööajastumise ajal, käivitatakse kontroll, millised paketid ei ole kätte saadud ja saadetakse taotlused uuesti saadetamiseks.
Edastamise kordusdiagramm:
Protokolli aegumised ja taimerid
Ühenduse loomise võimalike takistuste seas on mitu põhjust. Näiteks võib vastuvõtja olla võrgu väljas. Sellisel juhul, kui proovite ühendust luua, katkestatakse ühendus ajaülevaate tõttu. Reliable UDP rakenduses kasutatakse kahe ajastuse määramiseks kahte taimerit. Esimene, töötaimer, ootab vastust kaugserverilt. Kui see aktiveerub saatja poolel, saadetakse viimane saatmise pakett uuesti. Kui aga taimer aktiveerub vastuvõtja poolel, kontrollitakse paketikaotusi ja saadetakse kordusnõudmisi.
Teine taimer on vajalik ühenduse sulgemiseks, kui sõlmede vahel puudub side. Saatja pool käivitab selle kohe pärast töötaimeri aktiveerimist ja ootab vastust kaug-sõlme poolt. Kui vastust ei tule sätestatud aja jooksul, siis ühendus lõpetatakse ja ressursid vabastatakse. Vastuvõtja poolel käivitub ühenduse sulgemise taimer pärast töötaimeri topeltaktiveerimist. See on vajalik kinnituse pakettide kadu vältimiseks. Taimeri aktiveerimisel lõpetatakse samuti ühendus ja ressursid vabastatakse.
Reliable UDP edastamise olekute diagramm
Protokolli tööpõhimõtted on ellu viidud lõppautomaatides, mille iga olek vastutab teatud loogika eest pakettide töötlemisel.
Reliable UDP olekudiagramm:

Closed – ei ole tegelikult olek, see on automaatide algus- ja lõpupunkt. Oleku Closed mõistetakse edastuse juhtimise plokina, mis, rakendades asünkroonset UDP serverit, suunab paketid vastavatesse ühendustesse ja käivitab olekute töötlemise.
FirstPacketSending – algne olek, milles on väljaminev ühendus sõnumi saatmise ajal.
Selle oleku puhul saadetakse esimene pakett tavaliste sõnumite jaoks. Kinnitamise mittesaatmise sõnumite puhul on see ainus olek – kogu sõnum saadetakse just selles seisundis.
SendingCycle – põhiseisund sõnumipakettide edastamiseks.
Üleminek sellesse olekusse FirstPacketSending toimub pärast esimese sõnumipaketi saatmist. Just sellesse olekusse jõuavad kõik kinnitused ja taotlused edastuste kordamiseks. Väljuda sellest on võimalik kahel juhul – sõnumi eduka kohaletoimetamise korral või ajutise katkestamise tõttu.
FirstPacketReceived – algne olek sõnumi saajale.
Selles kontrollitakse edastamise alguse õigust, luuakse vajalikud struktuurid ja saadetakse kinnitamine esimese paketi vastuvõtmise kohta.
Üheainsa paketiga sõnumi puhul, mis saadetakse ilma kohaletoimetamise kinnituseta – see on ainus olek. Pärast sellise sõnumi töötlemist suletakse ühendus.
Assembling – põhiseisund sõnumipakettide vastuvõtmiseks.
Selles salvestatakse pakette ajutisse hoidlasse, kontrollitakse kadude olemasolu, saadetakse kinnitusi pakettide kohaletoimetamise kohta ja kogu sõnumi kohta ning saadetakse taotlusi kadunud pakettide uuesti kohaletoimetamiseks. Kui kogu sõnum on edukalt vastu võetud, läheb ühendus järgmisse olekusse Täidetud, vastasel juhul toimub välja minek ajaületuse tõttu.
Täidetud – ühenduse sulgemine kogu sõnumi eduka saamise korral.
See olek on vajalik sõnumi koostamiseks ja olukorra jaoks, kus sõnumi kohaletoimetamise kinnitus on teel saatjale kaduma läinud. Sellest olekust väljumine toimub ajaületuse tõttu, kuid ühendus loetakse edukalt suletuks.
Sügavam koodi. Edastuse juhtimisplokk
Üks Reliable UDP peamisi komponente on edastusjuhtimisplokk. Selle ploki ülesanne on hoida käimasolevaid ühendusi ja abielemente, jaotada saabunud pakette vastavatesse ühendustesse, pakkuda liidest pakettide saatmiseks ühendusse ning teostada protokollide API-d. Edastusjuhtimisplokk võtab pakette UDP tasemelt ja suunab need töötlemiseks lõppautomaatidesse. Pakettide vastuvõtmiseks on selles rakendatud asünkroonne UDP server.
Mõned ReliableUdpConnectionControlBlock klassi liikmed:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// byte array for the specified key. Used for assembling incoming messages
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// byte array for the specified key. Used for sending outgoing messages.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// connection record for the specified key.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// list of subscribers for messages.
private readonly List m_subscribers;
// local socket
private Socket m_socketIn;
// port for incoming messages
private int m_port;
// local IP address
private IPAddress m_ipAddress;
// local endpoint
public IPEndPoint LocalEndpoint { get; private set; }
// collection of pre-initialized
// states of the finite automaton
public StatesCollection States { get; private set; }
// random number generator. Used for creating TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Asynchronous UDP server implementation:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// loome uus puhvr, iga socket.BeginReceiveFrom jaoks
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// edastame puhvri parameetrina asünkroonse meetodi jaoks
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);
// pakett on saadud, järgmise vastuvõtmiseks valmis
Receive();
// kuna lihtsaim viis lahendada puhvriga seotud küsimus on saada viide sellele
// IAsyncResult.AsyncState-st
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// saame paketi pealkirja
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// ebaõige pakett - lükkame tagasi
return;
}
// loome võtme, et määrata paketi ühenduse kirje
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// saame olemasoleva ühenduse kirje või loome uue
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// käivitame paketi töötlemise lõppautomaatides
record.State.ReceivePacket(record, header, bytes);
}
Iga sõnumi edastamine loob struktuuri, mis sisaldab teavet ühenduse kohta. Sellist struktuuri nimetatakse connection record.
Mõned ReliableUdpConnectionRecord klassi liikmed:
internal class ReliableUdpConnectionRecord : IDisposable
{
// sissetulevate sõnumite baitide massiiv
public byte[] IncomingStream { get; set; }
// viide automaatseisundi objekti
public ReliableUdpState State { get; set; }
// paar, mis määratleb selgelt connection record'i
// edastamise juhtimissektoris
public Tuple Key { get; private set;}
// vastuvõtuvaba alumine piir
public int WindowLowerBound;
// edastamise akna suurus
public readonly int WindowSize;
// edastatav paketi number
public int SndNext;
// edastatavate pakettide hulk
public int NumberOfPackets;
// edastamise number (see on Tuple'i teine osa)
// iga sõnumi jaoks eraldi
public readonly Int32 TransmissionId;
// kaugserver – sõnumi tegelik vastuvõtja
public readonly IPEndPoint RemoteClient;
// paketi suurus, et vältida IP tasemel fragmenteerimist
// ei tohi ületada MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
public readonly int BufferSize;
// edastamise juhtimise blok
public readonly ReliableUdpConnectionControlBlock Tcb;
// kapseldab asünkroonsete toimingute tulemusi BeginSendMessage/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
// ei saada kinnitavate pakettide
public bool IsNoAnswerNeeded;
// viimane korrektselt vastuvõetud pakett (alati seatud suurimale numbrile)
public int RcvCurrent;
// massiiv kadunud pakettide numbritega
public int[] LostPackets { get; private set; }
// kas viimane pakett on saadud. Kasutatakse boolina.
public int IsLastPacketReceived = 0;
//...
}
Sügavam koodi. Oleku muutujad
Seisundid rakendavad Reliable UDP protokolli lõppautomaat, kus toimub pakettide põhikäsitlus. Abstraktne klass ReliableUdpState pakub oleku liidest:

Kogu protokolli tööloogika on realiseeritud ülaltoodud klasside poolt, koos abiklassiga, mis pakub staatilisi meetodeid, nagu näiteks ReliableUdp päise koostamine connection record'ist.
Edasi käsitletakse detailides liidese meetodite realiseerimist, mis määravad protokolli põhialgoritmid.
Meetod DisposeByTimeout
Meetod DisposeByTimeout vastutab ühenduse ressursside vabastamise eest ajaülevaate möödumisel ja edastamise eduka/ebaõnnestunud signaalimise eest.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Seda on üle määratud ainult olekus Täidetud.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// teavitame eduka sõnumi saamisest
SetAsCompleted(connectionRecord);
}
Meetod ProcessPackets
ProcessPackets meetod vastutab pakettide või pakettide täiendava töötlemise eest. Seda kutsutakse otse või pakettide ootamisaja taimeri kaudu.
olekus Assembling meetod on üle kirjutatud ja vastutab kadunud pakettide kontrollimise ja olekusse ülemineku eest Täidetud, viimase paketi saamisel ja eduka kontrolli läbimisel
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// on kaotatud pakette, saadame nende kohta päringud
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// seadistame taimeri teist korda, et proovida edastust uuesti
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// kui kahe proovimise järel WaitForPacketTimer ei
// õnnestunud pakette saada - alustame ühenduse sulgemise taimerit
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// edukas kontroll
{
// saadame kinnituse andmepaki vastuvõtmiseks
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// собираем ресурсы для момента слушания
// käivitame taimeri, juhuks kui
// kui viimane kinnitamine ei jõua saatjale ja ta küsib selle uuesti.
// taimeri löömisel - realiseerime ressursid
// olekus Completed on taimeri meetod ümber määratud
StartCloseWaitTimer(connectionRecord);
}
// see on olukord, kui andmepaki kinnitamine on kaduma läinud
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// alustame ühenduse sulgemise taimerit
StartCloseWaitTimer(connectionRecord);
}
}
olekus SendingCycle see meetod kutsutakse esmakordselt ainult ajastamise järgi ning see vastutab viimase sõnumi saatmise taastamise ning ühenduse sulgemise taimeri aktiveerimise eest.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
\/\/ saadame viimase paketi uuesti
\/\/ (ühenduse taastamisel saadetakse saajale uuesti päringud, mis temani ei jõudnud)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
\/\/ aktiveerime CloseWait taimeri – ootamiseks ühenduse taastamise või lõpetamise oletamiseks
StartCloseWaitTimer(connectionRecord);
}
olekus Täidetud meetod peatab töötava taimeri ja edastab sõnumi tellijatele.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
\/\/ kogume sõnumi ja edastame selle tellijatele
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Meetod ReceivePacket
olekus FirstPacketReceived meetodi peamine ülesanne on määrata, kas esimene sõnumipakett tõepoolest jõudis liidesele ning koguda ühekordne paketist koosnev sõnum.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// pakett kõrvaldame
return;
// kahe lipu kombinatsioon - FirstPacket ja LastPacket - ütleb, et meil on ainus sõnum
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))
{
// saadame kinnituse paketi
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// disaini järgi kõik paketi numbrid algavad 0-st;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// loeme pakettide arvu, mis peaksid saama
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// salvestame viimase saadud paketi numbri (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// seejärel nihutame vastuvõtu akna 1 võrra
connectionRecord.WindowLowerBound++;
// vahetame olekuid
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// kui kinnituse mehhanism ei ole vajalik
// käivitame taimeri, mis vabastab kõik struktuurid
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);
}
}
olekus SendingCycle see meetodi on ülekirjutatud kohaletoimetamise kinnituste ja edastamise taotluste vastuvõtmiseks.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// akna lõpp-punkti arvestamine
// akna piir + 1, et saada kinnitus edastamise kohta
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// kontroll akna piiridele
if (header.PacketNumber windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// kontroll viimasel paketil:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// edastus on lõpetatud
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// see on esimese paketi kinnitus
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// ilma akna liigutamiseta
SendPacket(connectionRecord);
}
// saadud andmeploki kinnitus
else if (header.PacketNumber == windowHighestBound)
{
// liigutame vastuvõtu/edastamise akna
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// lähtestame edastuse kontrollimise massiivi
connectionRecord.WindowControlArray.Nullify();
// saadame pakettide ploki
SendPacket(connectionRecord);
}
// see on taotlus uuesti edastamiseks – saadame vajaliku paketi
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
olekus Assembling ReceivePacket meetodis toimub peamine töö saabuvate pakkide sõnumi kokkupanemiseks.
Kogumine.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// Pakettide töötlemine, kui kinnitusmehhanism on välja lülitatud
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// Taimeri lähtestamine
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// Andmete salvestamine
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Kui saime viimase flagiga paketi - lõpetame
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// Aknapiiri lõpparvutamine
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// Eemaldame aknasse mitte mahuvad paketid
if (header.PacketNumber (windowHighestBound))
return;
// Eemaldame duplikaadid
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// Andmete salvestamine
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// Pakettide arvu suurendamine
connectionRecord.PacketCounter++;
// Salvestame juhise alla praeguse paketi numbri
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// Määrame suurima saabuva paketi
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// Taimerite taaskäivitamine
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// Kui viimane pakett on saabunud
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// Kui kõik akna paketid on saabunud, siis lähtestame arvu
// ja saadame kinnituspaketi
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// Arvu lähtestamine.
connectionRecord.PacketCounter = 0;
// Edastusakna liigutamine
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// Edastusjuhtimise massiivi nullimine
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// Kui viimane pakett on juba olemas
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// Kontrollime pakette
ProcessPackets(connectionRecord);
}
}
olekus Täidetud meetodi ainus eesmärk on saata korduv kinnitus sõnumi edastamise õnnestumisest.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// viimane pakett saadetakse uuesti,
// kuna viimane ack ei jõudnud saatjani
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Meetod SendPacket
olekus FirstPacketSending see meetod saadab esimese andmepaketi või, kui sõnum ei vaja kohaletoimetamise kinnitust — kogu sõnumi.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// kui kinnitus ei ole vajalik - saadame kõik paketid
// ja vabastame ressursid
if (connectionRecord.IsNoAnswerNeeded)
{
// Siin toimub saatmine As Is
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// loome paketi pea ja saadame selle
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// suurendame loendurit
connectionRecord.SndNext++;
// nihutame akent
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Käivitame taimeri
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
olekus SendingCycle Selles meetodis toimub pakettide bloki saatmine.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// saadame paketibloki
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++;
}
// juhul kui edastamise aken on suur, taaskäivitame taimeri peale saatmist
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
{
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
}
Sügavam koodi. Ühenduste loomine ja seadistamine
Nüüd, kui oleme tutvunud põhiseisundite ja meetoditega, mida kasutatakse olekute töötlemiseks, saame kaaluda veidi põhjalikumalt mõningaid protokolli tööd.
Andmete edastamise diagramm normaalsetes tingimustes:
Kaalume põhjalikult loomist connection record ühenduse jaoks ja esimese paketi saatmist. Edastamise algatajana toimib alati rakendus, mis kutsub välja sõnumi saatmise API-meetodi. Järgmisena kaasatakse edastamise juhtimise ploki StartTransmission meetod, mis käivitab uue sõnumi andmete edastamise.
Väljamineva ühenduse loomine:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException("", "Peate kasutama parameetritega konstruktorit või käivitama kuulaja messagi saatmise eel.");
}
// Käivitame sissetulevate pakettide töötlemise
StartListener(LocalEndpoint);
}
// Loome võtme sõnastikule, tuginedes EndPoint ja ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// Loome juhusliku numbri transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// Loome uue kirje ühenduse jaoks ja kontrollime,
// kas selline number juba meie sõnastikes eksisteerib
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// Kui eksisteerib – genereerime juhusliku numbri uuesti
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// Kui jälle ei õnnestunud – genereerime erandi
throw new ArgumentException("Paari TransmissionId & EndPoint' olemasolek sõnastikus");
}
// Oleme seisundi töötlemise käivitanud
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Esimese paketi saatmine (olek FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// loome paketi pealkiri ja saatke see
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// suurendame loendurit
connectionRecord.SndNext++;
// nihutame akna
connectionRecord.WindowLowerBound++;
// läheme SendingCycle olekusse
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Käivitage ajastaja
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Pärast esimese paketi saatmist liigub saatja olekusse SendingCycle – oodata kinnitust paketi kohaletoimetamisest.
Saaja pool, kasutades meetodit EndReceive, aktsepteerib saadetud paketti, loob uue connection record ja edastab antud paketi, eelnevalt parsitud pealkirjaga, ReceivePacket meetodisse olekust FirstPacketReceived
Ühenduse loomine vastuvõtupoolselt:
private void EndReceive(IAsyncResult ar)
{
// ...
// pakett on saadud
// paketi päise analüüsimine
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// vale pakett - loobume sellest
return;
}
// konstrueerime võtme, et määrata kindlaks ühenduse rekord paketi jaoks
Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
// saame olemasoleva ühenduse rekordi või loome uue
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// käivitame paketi töötlemise lõppstaadiumis
record.State.ReceivePacket(record, header, bytes);
}
Esimese paketi vastuvõtmine ja kinnituse saatmine (olekus FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// kuvatakse paketti
return;
// ...
// disaini tõttu algavad kõik pakettide numbrid 0-st;
if (header.PacketNumber != 0)
return;
// initsialiseerime massiivi sõnumi osade hoidmiseks
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// salvestame andmed paketis massiivi
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// arvutame, kui palju pakette peaks saama
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// salvestame viimase saadud paketi numbri (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// edastame vastuvõtuvahendi akna ühe võrra
connectionRecord.WindowLowerBound++;
// vahetame olekut
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*kui tõendusse mõeldud mehhanismi ei nõuta*/)
// ...
else
{
// saadame tõenduse
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Sügavam koodi. Ühenduse sulgemine aegumise põhjal
Ajutiste katkestuste käsitlemine on oluline osa Reliable UDP-st. Vaatame näidet, kus vahepealses sõlmes toimus rike ja andmete edastamine mõlemal suunal muutus võimatuks.
Ühenduse sulgemise ajagraafik:
Nagu diagrammil näha, käivitub saatja tööajanäit kohe pärast paketibloki saatmist. See toimub meetodis SendPacket olekus. SendingCycle.
Tööajanäidu sisselülitamine (olek SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// saadame paketibloki
// ...
// taaskäivitame taimeri pärast saatmist
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
Taimeri perioodid määratakse ühenduse loomisel. Vaikimisi on ShortTimerPeriod 5 sekundit. Näites on see seatud 1,5 sekundiks.
Siseneva ühenduse taimer käivitub pärast viimasest saabunud andmepakist, see toimub meetodis ReceivePacket olekus. Assembling
Tööajanäidu sisselülitamine (olek Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// taimerite taaskäivitamine
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
}
Sissetulevas ühenduses ei tulnud tööajal ooteaega jooksul enam pakette. Taimer aktiveerus ja kutsus välja meetodi ProcessPackets, kus tuvastati kaotatud paketid ja esmakordselt saadeti kordusnõuded.
Kordusnõuete saatmine (seisund: Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/ ...
if (\/*kaotatud paketid kontrollimine *\/)
{
\/\/ saadame kordusnõuded
\/\/ seadistame taimeri teist korda, edastamise proovimiseks
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\/\/ kui kahel korral WaitForPacketTimer säilitamisel
\/\/ ei õnnestunud pakette saada - käivitame ühenduse lõpufaasi taimeri
StartCloseWaitTimer(connectionRecord);
}
else if (\/*viimane pakk oli saadetud ja edukas kontroll *\/)
{
\/\/ ...
StartCloseWaitTimer(connectionRecord);
}
\/\/ kui akn pakkide blokk oli kadunud
else
{
if (!connectionRecord.TimerSecondTry)
{
\/\/ saadame akn uuesti
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
\/\/ käivitame ühenduse lõpufaasi taimeri
StartCloseWaitTimer(connectionRecord);
}
}
Muutuja TimerSecondTry seadistati true. See muutuja vastutab tööaegade taaskäivitamise eest.
Saatja poolel aktiveeritakse ka tööaeg ja saadetakse uuesti viimane saadetud pakett.
Ühendi sulgemise taimeri aktiveerimine (seisund SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// saadame uuesti viimase paketi
// ...
// lülitame sisse CloseWait taimeri – et oodata ühenduse taastumist või lõpetamist
StartCloseWaitTimer(connectionRecord);
}
Pärast seda käivitatakse väljuvas ühenduses sulgemise taimer.
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);
}
Ühendi sulgemise taimeri ooteperiood on vaikimisi 30 sekundit.
Lühikese aja pärast aktiveeritakse uuesti tööaeg saaja poolel, saadetakse uuesti päringud, seejärel käivitatakse sissetuleva ühenduse sulgemise taimer.
Sulgedes ühenduse eelregistreerimise taimerite tegevuse, vabastatakse kõigi ressursid. Saaja teatab ebaõnnestunud edastamisest ülemuurivale rakendusele (vt API Reliable UDP).
Ühenduse eelregistreerimise ressursside vabastamine:
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);
}
}
Sügavam koodi. Andmevoogude taastamine
Andmete edastamise taastamise diagramm paketi kaotuse korral:
Kuidas juba arutatud ühenduse sulgemise ajal ajaülesande lõppemise korral, toimub vastuvõtja juures kontroll kaotatud pakettide järgi. Kui pakette on kaotatud, koostatakse kaotatud paketide numbrite loetelu, mis kantakse ühenduse konkreetse LostPackets massiivi. Need numbrid saadetakse uuesti edastamise taotlemiseks.
Uuesti edastamisnõuete saatmine (kogumise seisund):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
//...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// kadunud paketid, saadame nende taotlused
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// ...
}
}
Saatja aktsepteerib taotlust uue kohaletoimetamise jaoks ja saadab puuduvad paketid. Tuleb märkida, et saatjal on sel hetkel juba käivitatud ühenduse sulgemise taimer ja taotluse saamisel see nullitakse.
Kadunud pakettide kordussaatmine (olek SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// nullita ühenduse sulgemise taimer
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// see on kordustransportimise taotlus – saadame vajaliku paketi
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Uuesti saadetud pakett (packet#3 diagrammil) võetakse vastu siseneva ühenduse kaudu. Kontrollitakse vastuvõtu akna täitmist ja tavaline andmeedastus taastatakse.
Kontroll vastuvõtu aknasse (seisund Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// suurendame pakettide loenduri
connectionRecord.PacketCounter++;
// salvestame akna juhtimise massiivi praeguse paketi numbri
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// seadistame suurima saabunud paketi
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// taaskäivitame taimerid
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// kui kõik akna paketid on saabunud, siis lähtestame loenduri
// ja saadame kinnituse paketi
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// lähtestame loenduri.
connectionRecord.PacketCounter = 0;
// oleme liikunud edastamise akent
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// lähtestame edastamise juhtimise massiivi
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
Reliable UDP API
Andmete edastamise protokolli suhtlemiseks on olemas avatud klass Reliable Udp, mis on wrapper edastamise juhtimise ploki üle. Siin on klassi kõige olulisemad liikmed:
avalik suletud klass ReliableUdp : IDisposable
{
// saab kohaliku lõpp-punkti
public IPEndPoint LocalEndpoint
// loob ReliableUdp eksemplari ja käivitab
// sisenevate pakkide kuulamise antud IP aadressil
// ja sadamal. Port 0 tähendab dünaamiliselt eraldatud
// pordi kasutamist
public ReliableUdp(IPAddress localAddress, int port = 0)
// tellimine sisenevate sõnumite saamiseks
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// tellimisest tagasiside saamiseks loobumine
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// asünkroonne sõnumi saatmine
// Märkus: ühilduvus XP ja Server 2003-ga ei kaandu, kuna kasutatakse .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// alusta asünkroonset sõnumi saatmist
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// saada asünkroonse saatmise tulemus
public bool EndSendMessage(IAsyncResult asyncResult)
// vabasta ressursid
public void Dispose()
}
Sõnumi vastuvõtt toimub tellimise kaudu. Tagasikutsumise meetodi delegeeri allkiri:
aval public delegate void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);Sõnum:
public class ReliableUdpMessage
{
// sõnumi tüüp, lihtne loend
public ReliableUdpMessageTypes Type { get; private set; }
// sõnumi andmed
public byte[] Body { get; private set; }
// kui true, siis kinnitusmehhanism tarnimiseks on keelatud
// konkreetse sõnumi edastamiseks
public bool NoAsk { get; private set; }
}
Konkreetse sõnumi tüübi ja/või konkreetse saatja tellimiseks kasutatakse kahte valikulist parameetrit: ReliableUdpMessageTypes messageType ja IPEndPoint ipEndPoint.
Sõnumi tüübid:
public enum ReliableUdpMessageTypes : short
{
// Igaüks
Any = 0,
// Kirjutamine STUN serverisse
StunRequest = 1,
// Vastus STUN serverilt
StunResponse = 2,
// Faili edastamine
FileTransfer = 3,
// ...
}
Sõnumi edastamine toimub asünkroonselt, selleks on protokollis rakendatud asünkroonne programmeerimis mudel:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Sõnumi edastamise tulemus on true – kui sõnum jõudis saajani edu korral ja false – kui ühendus suleti ajaülesande tõttu:
public bool EndSendMessage(IAsyncResult asyncResult)
Kokkuvõte
Käesolevas artiklis ei ole palju kirjeldatud. Voogude sünkroniseerimise mehhanismid, erandite ja vigade töötlemine ning asünkroonsete sõnumite saatmise meetodite rakendamine. Kuid protokolli tuum, pakettide töötlemise loogika, ühenduse loomine ja ajutiste kontaktide töötlemine peaksid Teile selgemaks saama.
Käideldav usaldusliku kohaletoimetamise protokoll on piisavalt stabiilne ja paindlik ning vastab eelnevalt määratletud nõuetele. Soovin aga lisada, et kirjeldatud rakendust on võimalik täiustada. Näiteks võiksite suurendada läbilaskevõimet ja dünaamiliselt muuta ajastuse perioode ning protokolli võiks lisada mehhanisme nagu liugukse mehhanism ja RTT; samuti oleks kasulik rakendada MTU määramise mehhanismi ühenduse sõlmede vahel (kuid ainult suuremate sõnumite saatmise korral).
Aitäh tähelepanu eest, ootan Teie kommentaare ja märkusi.
P.S. Neile, kes huvituvad üksikasjadest või lihtsalt tahavad protokolli testida, on siin projekt GitHubis:
Kasulikud lingid ja artiklid
- TCP protokolli spetsifikatsioon: ja
- UDP protokolli spetsifikatsioon: ja
- RUDP protokolli arutelu:
- Usaldusväärne andmeprotokoll: ja
- Lihtne rakendus UDP kohaletoimetamise kinnitamiseks:
- Artikkel, mis käsitleb NAT-de ületamise mehhanisme:
- Asünkroonse programmeerimise mudeli rakendamine: ja
- Asünkroonse programmeerimise mudeli viimine ülesannete põhisesse asünkroonse mustrisse (APM-st TAP-ks):
Uuendus: Aitäh ja ideede lisamise ülesande rakendamiseks. Raamatukogu ühilduvus vanade opsüsteemidega ei muutu, kuna 4. raamistik toetab nii XP-d kui ka 2003 serverit.
Allikas: habr.com
