Internet ka ka ndryshuar ndjeshĂ«m. NjĂ« nga protokollet kryesore tĂ« Internetit â UDP pĂ«rdoret nga aplikacionet jo vetĂ«m pĂ«r dĂ«rgimin e datagramĂ«ve dhe shpĂ«rndarjet e gjera, por gjithashtu pĂ«r tĂ« siguruar lidhjet «peer-to-peer» midis nyjeve tĂ« rrjetit. Si rezultat i ndĂ«rtimit tĂ« tij tĂ« thjeshtĂ«, ky protokoll ka marrĂ« shumĂ« mĂ«nyra tĂ« paplanifikuara pĂ«rdorimi, megjithatĂ«, mangĂ«sitĂ« e tij, si mungesa e dĂ«rgimit tĂ« garantuar, nuk janĂ« zhdukur. Ky artikull pĂ«rshkruan implementimin e protokollit tĂ« dĂ«rgimit tĂ« garantuar mbi UDP.
Përmbajtja:
Hyrje
Arkitektura fillestare e Internetit parashikon njĂ« hapĂ«sirĂ« tĂ« homogjen tĂ« adresave, ku çdo nyje kishte njĂ« adresĂ« IP globale dhe unike, dhe mund tĂ« komunikonte drejtpĂ«rdrejt me nyjet e tjera. Tani, Interneti nĂ« fakt ka njĂ« arkitekturĂ« tjetĂ«r â njĂ« hapĂ«sirĂ« globale adresash IP dhe shumĂ« hapĂ«sira me adresa private, tĂ« fshehura pas pajisjeve NAT.NĂ« njĂ« arkitekturĂ« tĂ« tillĂ«, vetĂ«m pajisjet qĂ« ndodhen nĂ« hapĂ«sirĂ«n globale tĂ« adresave mund tĂ« ndĂ«rveprojnĂ« lehtĂ«sisht me ndokĂ«nd nĂ« rrjet, pasi kanĂ« njĂ« adresĂ« IP unike dhe globale qĂ« mund tĂ« drejtohet. NjĂ« nyje qĂ« ndodhet nĂ« njĂ« rrjet privat, mund tĂ« bashkohet me nyje tĂ« tjera nĂ« kĂ«tĂ« rrjet, si dhe tĂ« lidhĂ« me nyje tĂ« tjera, tĂ« njohura mirĂ«, nĂ« hapĂ«sirĂ«n globale tĂ« adresave. Ky bashkĂ«punim arrihet kryesisht pĂ«rmes mekanizmit tĂ« pĂ«rkthimit tĂ« adresave rrjetuese. Pajisjet NAT, pĂ«r shembull, routerat Wi-Fi, krijojnĂ« regjistra tĂ« veçantĂ« nĂ« tabelat e pĂ«rkthimit pĂ«r lidhjet dalĂ«se dhe modifikojnĂ« adresat IP dhe numrat e porteve nĂ« paketat. Kjo lejon qĂ« tĂ« vendosĂ« njĂ« lidhje dalĂ«se nga njĂ« rrjet privat me nyje nĂ« hapĂ«sirĂ«n globale tĂ« adresave. Por nĂ« tĂ« njĂ«jtĂ«n kohĂ«, pajisjet NAT zakonisht bllokojnĂ« tĂ« gjithĂ« trafikun hyrĂ«s, pĂ«rveç nĂ«se nuk janĂ« vendosur rregulla tĂ« veçanta pĂ«r lidhjet hyrĂ«se.
Një arkitekturë e tillë e Internetit është mjaft e përshtatshme për ndërveprimin klient-server, kur clientët mund të ndodhen në rrjete private, ndërsa serverët kanë një adresë globale. Por kjo krijon vështirësi për lidhjen direkte midis dy nyjeve në rrjete të ndryshme. shumë rrjete private. Lidhja direkte e dy nyjeve është e rëndësishme për aplikacionet «peer-to-peer», si transmetimi i zërit (Skype), marrja e qasshmërisë të largët (TeamViewer), ose lojrat online.
Një nga metodat më efektive për të vendosur një lidhje peer-to-peer midis pajisjeve që ndodhen në rrjete private të ndryshme quhet «hole punching». Kjo teknikë përdoret më shpesh me aplikacionet që bazohen në protokollin UDP.
Por nëse aplikacioni juaj kërkon që dërgimi i të dhënave të jetë i garantuar, për shembull, kur dërgoni skedarë midis kompjuterëve, atëherë përdorimi i UDP do të sjellë shumë vështirësi, pasi UDP nuk është një protokoll me dërgim të garantuar dhe nuk siguron dërgimin e pakove në rend, ndryshe nga protokolli TCP.
Në këtë rast, për të siguruar dërgimin e garantuar të pakove, është e nevojshme të implementohet një protokoll në nivel të aplikacionit që siguron funksionalitetin e nevojshëm dhe punon mbi UDP.
Dua të theksoj se ekziston një teknikë TCP hole punching, për të vendosur lidhje TCP midis nyjeve në rrjete të ndryshme private, por për shkak të mungesës së mbështetjes nga shumë pajisje NAT, ajo zakonisht nuk merret parasysh si një metodë kryesore për lidhjen e këtyre nyjeve.
Në këtë artikull do të shqyrtoj vetëm implementimin e protokollit të dërgimit të garantuar. Implementimi i teknikës së UDP hole punching do të përshkruhet në artikujt e ardhshëm.
Kërkesat për protokoll
- Dorëzimi i besueshëm i pakove, i realizuar përmes mekanizmit të konfirmimit pozitiv (të ashtuquajturit positive acknowledgment)
- Nevoja për një transmetim efikas të të dhënave të mëdha, dmth. protokolli duhet të shmangë retransmetimet e panevojshme të pakove
- Duhet të ketë mundësinë për të anuluar mekanizmin e konfirmimit të dorëzimit (mundësia për të funksionuar si një protokoll «të pastër» UDP)
- Mundësia për të realizuar një mënyrë komandimi, me konfirmimin e çdo mesazhi
- Njësia baze e dërgimit të të dhënave mbi protokollin duhet të jetë një mesazh
Këto kërkesa shumë kuptojnë me kërkesat e Protokollit të të Dhënave të Besueshme, të përshkruara në dhe , dhe unë u bazova në këto standarde gjatë zhvillimit të këtij protokolli.
Për të kuptuar këto kërkesa, le të shqyrtojmë diagramet e kohës për transmetimin e të dhënave midis dy nyjeve të rrjetit përmes protokolleve TCP dhe UDP. Le të supozojmë se në të dy rastet humbet një paketa.
Transmetimi i të dhënave jo-interaktive përmes TCP:
Siç shihet në diagram, në rastin e humbjes së paketave, TCP do të zbulojë paketën e humbur dhe do t'i raportojë dërguesit, duke kërkuar numrin e segmentit të humbur.
Transmetimi i të dhënave përmes protokollit UDP:
UDP nuk merr asnjë hap për të zbuluar humbjet. Kontrolli i gabimeve të transmetimit në protokollin UDP është në përgjegjësinë e aplikacionit.
Zbulimi i gabimeve në protokollin TCP arrihet përmes krijimit të një lidhjeje me nyjën e fundit, ruajtjes së gjendjes së kësaj lidhjeje, specifikimit të numrit të byte-ve të dërguara në çdo titull pakete dhe njoftimeve për marrjen e tij përmes numrit të konfirmimit "numri i pranuar".
Për më tepër, për të përmirësuar performancën (të dërguarit e më shumë se një segmenti pa marrë konfirmimin), protokolli TCP përdor një dritare transmetimi - numrin e byte-ve të të dhënave që dërguesi i segmentit pret të pranojë.
Për një informacion më të detajuar mbi protokollin TCP, mund të referoheni në , dhe për UDP në , ku ato janë definita.
Nga e gjithë kjo, është e qartë se për të krijuar një protokoll besnik për shpërndarjen e mesazheve mbi UDP (në të ardhmen do ta quajmë Reliable UDP), është e nevojshme të implementohen mekanizma të ngjashëm me ata të TCP për transmetimin e të dhënave. Konkretisht:
- të mbani gjendjen e lidhjes
- të përdorni numërimin e segmenteve
- të përdorni paketa të veçanta konfirmimi
- të përdorni një mekanizëm të thjeshtëzuar window për të rritur kapacitetin e protokollit
Për më tepër, është e nevojshme:
- të sinjalizoni fillimin e mesazhit, për të ndarë burimet për lidhjen
- të sinjalizoni përfundimin e mesazhit, për të përcjellë mesazhin e pranuar në aplikacionin e lartë dhe për të liruar burimet e protokollit
- të lejoni protokollin për lidhje të caktuara të çaktivizoni mekanizmin e konfirmimit të dorëzimit, në mënyrë që të funksionojë si "UDP i pastër"
Kreu i Reliable UDP
Le të kujtojmë se një datagram UDP inkorporohet në një datagram IP. Paketa Reliable UDP përkatësisht "pakuqet" brenda një datagrami UDP.
Inkorporimi i titullit të Reliable UDP:
Struktura e titullit të Reliable UDP është mjaft e thjeshtë:

- Flags â flamujt menaxhues tĂ« paketĂ«s
- MessageType â lloji i mesazhit, pĂ«rdoret nga aplikacionet e larta, pĂ«r tĂ« abonuar nĂ« mesazhe tĂ« caktuara
- TransmissionId â numri i dĂ«rgesĂ«s, sĂ« bashku me adresĂ«n dhe portin e marrĂ«sit identifikon unike lidhjen
- PacketNumber â numri i paketĂ«s
- Options â opsione tĂ« tjera tĂ« protokollit. NĂ« rastin e paketĂ«s sĂ« parĂ« pĂ«rdoret pĂ«r tĂ« specifikuar madhĂ«sinĂ« e mesazhit
Flags mund të jenë si më poshtë:
- FirstPacket â paketa e parĂ« e mesazhit
- NoAsk â mesazhi nuk kĂ«rkon aktivizimin e mekanizmit tĂ« konfirmimit
- LastPacket â paketa e fundit e mesazhit
- RequestForPacket â paketĂ« konfirmimi ose kĂ«rkesĂ« pĂ«r paketĂ« tĂ« humbur
Parimet e përgjithshme të funksionimit të protokollit
Duke qenë se Reliable UDP është orientuar në transmetimin e garantuar të mesazhit midis dy nyjeve, ai duhet të jetë në gjendje të krijojë një lidhje me palën tjetër. Për të krijuar lidhjen, pala dërguese dërgon një paketë me flamurin FirstPacket, përgjigjja e së cilës do të tregojë krijimin e lidhjes. Të gjitha paketat e përgjigjes, ose ndryshe, paketat e konfirmimit, gjithmonë vendosin vlerën e fushës PacketNumber një njësi më shumë se vlera më e madhe e PacketNumber nga paketat që kanë arritur me sukses. Në fushën Options për paketën e parë të dërguar ruhet madhësia e mesazhit.
Për të përfunduar lidhjen përdoret një mekanizëm i ngjashëm. Në paketën e fundit të mesazhit vendoset flamuri LastPacket. Në paketën e përgjigjes është specifikuar numri i paketës së fundit + 1, që për anën e marrëse do të thotë dorëzim i suksesshëm i mesazhit.
Diagrami i krijimit dhe mbylljes së lidhjes:
Kur lidhja Ă«shtĂ« krijuar, fillon transmetimi i tĂ« dhĂ«nave. TĂ« dhĂ«nat dĂ«rgohen nĂ« blloqe paketash. Ădo bllok, pĂ«rveç atij tĂ« fundit, pĂ«rmban njĂ« numĂ«r fikse paketash. Ai Ă«shtĂ« i barabartĂ« me madhĂ«sinĂ« e dritares sĂ« marrjes/transmetimit. Blloku i fundit i tĂ« dhĂ«nave mund tĂ« ketĂ« njĂ« numĂ«r mĂ« tĂ« vogĂ«l paketash. Pas dĂ«rgimit tĂ« çdo blloku, pala dĂ«rguese pret konfirmimin e dorĂ«zimit, ose njĂ« kĂ«rkesĂ« pĂ«r ritransmetimin e paketimeve tĂ« humbura, duke lĂ«nĂ« tĂ« hapur dritaren e marrjes/transmetimit pĂ«r marrjen e pĂ«rgjigjeve. Pas marrjes sĂ« konfirmimit tĂ« dorĂ«zimit tĂ« bllokut, dritarja e pranimit/transmetimit zhvendoset dhe dĂ«rgohet blloku tjetĂ«r i tĂ« dhĂ«nave.
Ankesa pranon paketet. Ădo paketĂ« kontrollohet pĂ«r t'u siguruar qĂ« Ă«shtĂ« brenda dritares sĂ« dĂ«rgesĂ«s. Paketat qĂ« nuk bien brenda dritares dhe duplikatet filtrohen. Duke qenĂ« se madhĂ«sia e dritares Ă«shtĂ« e fixuar dhe e njĂ«jtĂ« pĂ«r marrĂ«sin dhe dĂ«rguesin, nĂ« rastin e shpĂ«rndarjes sĂ« njĂ« blloku paketash pa humbje, dritarja lĂ«viz pĂ«r tĂ« pranuar paketat e bllokut tĂ« ardhshĂ«m tĂ« tĂ« dhĂ«nave dhe dĂ«rgohet njĂ« konfirmim pĂ«r shpĂ«rndarjen. NĂ«se dritarja nuk mbushet brenda periudhĂ«s sĂ« caktuar nga timer-i i punĂ«s, do tĂ« nisĂ« njĂ« kontroll pĂ«r tĂ« parĂ« cilat paketa nuk janĂ« dorĂ«zuar dhe do tĂ« dĂ«rgohen kĂ«rkesa pĂ«r riprorosinĂ« e shpĂ«rndarjes.
Diagrami i riprorosjes:
Koha e skadimit dhe orët e protokollit
Ka disa arsye për të cilat mund të mos vendoset një lidhje. Për shembull, nëse pala e pranimit është jashtë rrjetit. Në këtë rast, kur përpiqeni të vendosni një lidhje, lidhja do të mbyllet për shkak të skadimit të kohës. Në realizimin e Reliable UDP përdoren dy timer-a për të vendosur limite kohore. E para, timer-i i punës, shërben për të pritur një përgjigje nga hosti i largët. Nëse ai skadon nga ana e dërguesit, atëherë do të bëhet riprorosja e paketës së fundit të dërguar. Nëse timer-i skadon nga ana e marrësit, atëherë do të bëhet kontroll për paketat e humbura dhe do të dërgohen kërkesa për riprorosje.
Timer-i i dytĂ« â Ă«shtĂ« i nevojshĂ«m pĂ«r tĂ« mbyllur lidhjen nĂ« rast mungese tĂ« lidhjes midis nyjeve. PĂ«r dĂ«rguesin, ai ndizet menjĂ«herĂ« pas skadimit tĂ« timer-it tĂ« punĂ«s dhe pret pĂ«rgjigje nga nyja e largĂ«t. NĂ« rast mungese pĂ«rgjigjeje brenda periudhĂ«s sĂ« caktuar â lidhja mbyllet dhe burimet lirohen. PĂ«r marrĂ«sin, timer-i i mbylljes sĂ« lidhjes ndizet pas skadimit tĂ« dyfishtĂ« tĂ« timer-it tĂ« punĂ«s. Kjo Ă«shtĂ« e nevojshme si njĂ« masĂ« sigurie pĂ«r tĂ« evituar humbjen e konfirmimit tĂ« paketĂ«s. Kur timer-i aktivizohet, lidhja gjithashtu mbyllet dhe burimet lirohen.
Diagrami i gjendjeve të dërgimit të Reliable UDP
Principet e funksionimit të protokollit zbatohen në një automat të fundme, ku çdo gjendje përgjigjet për një logjikë specifike të përpunimit të paketave.
Diagrami i gjendjeve të Reliable UDP:

Closed në të vërtetë nuk është një gjendje, është një pikë fillestare dhe finale për automatin. Si gjendje Closed pranohet blloku i menaxhimit të dërgesës, i cili, duke realizuar një server asinkron UDP, ridrejton paketat në lidhjet përkatëse dhe nis përpunimin e gjendjeve.
FirstPacketSending gjendja fillestare, në të cilën ndodhet lidhja dalëse kur dërgohet një mesazh.
NĂ« kĂ«tĂ« gjendje dĂ«rgohet paketa e parĂ« pĂ«r mesazhet normale. PĂ«r mesazhet pa konfirmim dĂ«rgese, kjo Ă«shtĂ« gjendja e vetme â nĂ« tĂ« ndodh dĂ«rgimi i tĂ« gjithĂ« mesazhit.
SendingCycle gjendja kryesore për dërgimin e paketave të mesazhit.
Kalimi nĂ« tĂ« nga gjendja FirstPacketSending ndodh pas dĂ«rgimit tĂ« paketĂ«s sĂ« parĂ« tĂ« mesazhit. PikĂ«risht nĂ« kĂ«tĂ« gjendje mbĂ«rrijnĂ« tĂ« gjitha konfirmimet dhe kĂ«rkesat pĂ«r riprorosje. Dalja nga ajo Ă«shtĂ« e mundur nĂ« dy raste â nĂ« rastin e dĂ«rgimit tĂ« suksesshĂ«m tĂ« mesazhit ose pĂ«r shkak tĂ« skadimit tĂ« kohĂ«s.
FirstPacketReceived gjendja fillestare për marrësin e mesazhit.
Në të kontrollohet saktësia e fillimit të dërgesës, krijohen strukturat e nevojshme dhe dërgohet konfirmimi për pranimin e paketës së parë.
PĂ«r njĂ« mesazh qĂ« pĂ«rbĂ«het nga njĂ« paketĂ« tĂ« vetme dhe dĂ«rgohet pa pĂ«rdorimin e konfirmimit tĂ« dĂ«rgesĂ«s â kjo Ă«shtĂ« gjendja e vetme. Pas pĂ«rpunimit tĂ« kĂ«tij mesazhi lidhja mbyllet.
Assembling gjendja kryesore për pranimin e paketave të mesazhit.
NĂ« tĂ« regjistrohen paketat nĂ« njĂ« depo tĂ« pĂ«rkohshme, kontrollohet nĂ«se ka humbje paketa, dĂ«rgohen konfirmime pĂ«r dorĂ«zimin e bllokut tĂ« paketave dhe tĂ« gjithĂ« mesazhit, dhe dĂ«rgohen kĂ«rkesa pĂ«r riprorosjen e paketave tĂ« humbura. NĂ« rastin e marrjes sĂ« suksesshme tĂ« tĂ« gjithĂ« mesazhit â lidhja kalon nĂ« gjendjen PĂ«rfunduarndryshe, pĂ«rfundon pĂ«r shkak tĂ« skadimit tĂ« kohĂ«s.
Përfunduar mbyllja e lidhjes në rastin e marrjes së suksesshëm të të gjithë mesazhit.
Kjo gjendje është e nevojshme për ndërtimin e mesazhit dhe për rastin kur konfirmimi për dorëzimin e mesazhit është humbur në rrugë për dërguesin. Dalja nga kjo gjendje bëhet për shkak të skadimit të kohës, por lidhja konsiderohet e mbyllur me sukses.
Një vështrim më të thellë në kod. Blloku i kontrollit të dërgimit
Një nga elementët kyç të Reliable UDP është blloku i menaxhimit të transmetimit. Detyra e këtij blloku është ruajtja e lidhjeve aktuale dhe elementeve ndihmëse, shpërndarja e paketave të ardhura në lidhjet e përshtatshme, ofrimi i një ndërfaqe për dërgimin e paketave në lidhje dhe realizimi i API të protokollit. Blloku i menaxhimit të transmetimit pranon paketat nga niveli UDP dhe i redirekton ato për përpunim në automatin finit. Për pranimin e paketave, në të është realizuar një server asinkron UDP.
Disa anëtarë të klasës ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock : IDisposable
{
// array bajtësh për çelësin e specifikuar. Përdoret për ndërtimin e mesazheve të ardhura
public ConcurrentDictionary<Tuple, byte[]> IncomingStreams { get; private set;}
// array bajtësh për çelësin e specifikuar. Përdoret për dërgimin e mesazheve të dalura.
public ConcurrentDictionary<Tuple, byte[]> OutcomingStreams { get; private set; }
// regjistri i lidhjes për çelësin e specifikuar.
private readonly ConcurrentDictionary<Tuple, ReliableUdpConnectionRecord> m_listOfHandlers;
// lista e abonentëve për mesazhe.
private readonly List m_subscribers;
// socketi lokal
private Socket m_socketIn;
// porta për mesazhet e ardhura
private int m_port;
// adresa IP lokale
private IPAddress m_ipAddress;
// pika përfundimtare lokale
public IPEndPoint LocalEndpoint { get; private set; }
// koleksioni i gjendjeve të parainicizuara
// të automatit të fundit
public StatesCollection States { get; private set; }
// gjeneratori i numrave të rastësishëm. Përdoret për krijimin e TransmissionId
private readonly RNGCryptoServiceProvider m_randomCrypto;
//...
}
Implementimi i serverit asinkron UDP:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// krijojmë një tampon të ri, për secilin socket.BeginReceiveFrom
byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
// kalojmë tamponin si parametër për metodën asinkrone
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);
// paketa është marrë, e gatshme për të pranuar të ardhmen
Receive();
// pasi mënyra më e thjeshtë për të zgjidhur çështjen me tamponin - të marrim referencën e tij
// nga IAsyncResult.AsyncState
byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
// marrim titullin e paketës
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// erdhi një paketë e pasaktë - e heqim atë
return;
}
// konstrukt pjesën për të përcaktuar regjistrimin e lidhjes për paketën
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// marrim regjistrimin ekzistues të lidhjes ose krijojmë të ri
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// nis paketën për procesimin në automatizmin e fundmë
record.State.ReceivePacket(record, header, bytes);
}
Për çdo dërgesë mesazhi krijohet një strukturë, që përmban informacion për lidhjen. Kjo strukturë quhet connection record.
Disa anëtarë të klasës ReliableUdpConnectionRecord:
klasa e brendshme ReliableUdpConnectionRecord : IDisposable
{
\/\/ lista e byte me mesazhin
public byte[] IncomingStream { get; set; }
\/\/ referenca në gjendjen e automatit përfundimtar
public ReliableUdpState State { get; set; }
\/\/ çifti që përcakton një kuptim të saktë të connection record
\/\/ në bllokun e kontrollit të transferimit
public Tuple Key { get; private set;}
\/\/ kufiri i poshtëm i dritares së marrjes
public int WindowLowerBound;
\/\/ madhësia e dritares së transferimit
public readonly int WindowSize;
\/\/ numri i paketës për t'u dërguar
public int SndNext;
\/\/ numri i paketave për t'u dërguar
public int NumberOfPackets;
\/\/ numri i transferimit (ai është në të vërtetë pjesa e dytë e Tuple)
\/\/ për çdo mesazh është i tij
public readonly Int32 TransmissionId;
\/\/ IP endpoint i largĂ«t â nĂ« thelb marrĂ«si i mesazhit
public readonly IPEndPoint RemoteClient;
\/\/ madhësia e paketës, për të shmangur fragmentimin në nivelin IP
\/\/ nuk duhet tĂ« tejkalojĂ« MTU â (IP.Header + UDP.Header + ReliableUDP.Header)
public readonly int BufferSize;
\/\/ blloku i kontrollit të transferimit
public readonly ReliableUdpConnectionControlBlock Tcb;
\/\/ inkapsulon rezultatet e operacionit asinkron për BeginSendMessage\/EndSendMessage
public readonly AsyncResultSendMessage AsyncResult;
\/\/ mos dërgoni paketat e konfirmimit
public bool IsNoAnswerNeeded;
\/\/ paketa e fundit e marrë në mënyrë korrekte (këtu gjithmonë vendoset në numrin më të madh)
public int RcvCurrent;
\/\/ lista me numrat e paketave të humbura
public int[] LostPackets { get; private set; }
\/\/ a ka ardhur paketa e fundit. Përdoret si bool.
public int IsLastPacketReceived = 0;
\/\/...
}
Një vështrim më të thellë në kod. Gjendjet
Gjendjet realizojnë automatin përfundimtar të protokollit Reliable UDP, ku ndodh përpunimi kryesor i paketave. Klasa abstrakte ReliableUdpState ofron një ndërfaqe për gjendjen:

Të gjithë logjikën e punës së protokollit e përfaqësojnë klasat e paraqitura më lart, së bashku me një klasë ndihmëse që ofron metoda statike, si për shembull, ndërtimin e titullit ReliableUdp nga connection record.
Në vazhdim do të shqyrtojmë në detaje implementimin e metodave të ndërfaqes që përcaktojnë algoritmet kryesore të funksionimit të protokollit.
Metoda DisposeByTimeout
Metoda DisposeByTimeout është përgjegjëse për çlirimin e burimeve të lidhjes pas skadimit të kohës dhe për sinjalizimin e dërgimit të suksesshëm / jo të suksesshëm të mesazhit.
ReliableUdpState.DisposeByTimeout:
protected virtual void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
if (record.AsyncResult != null)
{
connectionRecord.AsyncResult.SetAsCompleted(false);
}
connectionRecord.Dispose();
}
Ai është rishkruar vetëm në gjendjen Përfunduar.
Completed.DisposeByTimeout:
protected override void DisposeByTimeout(object record)
{
ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
// sinjalizojmë marrjen e suksesshme të mesazhit
SetAsCompleted(connectionRecord);
}
Metoda ProcessPackets
Metoda ProcessPackets është përgjegjëse për përpunimin e mëtejshëm të paketave. Ajo thirret drejtpërdrejt ose përmes një timeri që pret paketat.
Në gjendjen Assembling metoda është rishkruar dhe është përgjegjëse për verifikimin e paketave të humbura dhe kalimin në gjendjen Përfunduar, në rastin e marrjes së paketës së fundit dhe kalimit me sukses të verifikimit
Assembling.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
// ka paketa të humbura, dërgojmë kërkesat për to
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
// vendosim timerin për herë të dytë, për përpjekjen për dërgesë
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// nëse pas dy përpjekjeve të aktivizimit të WaitForPacketTimer
// nuk arritëm të marrim paketat - aktivizojmë timerin për mbylljen e lidhjes
StartCloseWaitTimer(connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived != 0)
// verifikimi i suksesshëm
{
// dërgojmë konfirmimin e marrjes së bllokut të dhënave
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
// në vend të realizimit të menjëhershëm të burimeve
// aktivizojmë timerin, për rastin nëse
// nëse konfirmimi i fundit nuk arrin te dërguesi dhe ai kërkon sërish.
// me aktivizimin e timerit - realizojmë burimet
// në gjendjen Completed metoda e timerit është rishkruar
StartCloseWaitTimer(connectionRecord);
}
// ky është rasti kur konfirmimi për bllokun e paketave është humbur
else
{
if (!connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// aktivizojmë timerin për mbylljen e lidhjes
StartCloseWaitTimer(connectionRecord);
}
}
Në gjendjen SendingCycle kjo metodë aktivizohet vetëm nga timeri dhe është përgjegjëse për dërgimin përsëri të mesazhit të fundit, si dhe aktivizimin e timerit për mbylljen e lidhjes.
SendingCycle.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone != 0)
return;
// dërgojmë përsëri paketën e fundit
// (në rast se lidhja rikuperohet, nodi-pranues do dërgojë sërish kërkesat për ato që nuk kanë arritur te ai)
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
// aktivizojmĂ« timerin CloseWait â pĂ«r tĂ« pritur rikuperimin e lidhjes ose pĂ«rfundimin e saj
StartCloseWaitTimer(connectionRecord);
}
Në gjendjen Përfunduar metoda ndalon timerin e punës dhe transferon mesazhin te abonuesit.
Completed.ProcessPackets:
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer != null)
connectionRecord.WaitForPacketsTimer.Dispose();
// mbledhim mesazhin dhe e transferojmë te abonuesit
ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}
Metoda ReceivePacket
Në gjendjen FirstPacketReceived detyra kryesore e metodës është të përcaktojë nëse paketa e parë e mesazhit vërtet ka ardhur në ndërfaqe, si dhe të mbledhë mesazhin përbërë nga një paketë të vetme.
FirstPacketReceived.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// ne e pezullo paketa
return;
// kombinimi i dy flamujve - FirstPacket dhe LastPacket - tregon që kemi një mesazh të vetëm
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))
{
// dërgo paketën e konfirmimit
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
SetAsCompleted(connectionRecord);
return;
}
// me dizajn, të gjitha numrat e paketave fillojnë nga 0;
if (header.PacketNumber != 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// llogarit numrin e paketave që duhet të vijnë
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// regjistro numrin e paketës së fundit të marrë (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// pas kësaj lëvizim dritaren e pranimit njëherë
connectionRecord.WindowLowerBound++;
// ndërruam gjendjen
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// nëse nuk kërkohet mekanizmi i konfirmimit
// aktivizojmë timer-in që do të lirojë të gjitha strukturat
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);
}
}
Në gjendjen SendingCycle ky metodë është ri-përcaktuar për të pranuar konfirmimet për dorëzimin dhe kërkesat për ripërsëritje.
SendingCycle.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
return;
// llogaritja e kufirit përfundimtar të dritares
// merret kufiri i dritares + 1, për të marrë konfirmimet e dorëzimit
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// kontrolli nëse bie brenda dritares
if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// kontrollo për paketën e fundit:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// transmetimi përfundoi
Interlocked.Increment(ref connectionRecord.IsDone);
SetAsCompleted(connectionRecord);
return;
}
// kjo është përgjigje për paketën e parë me konfirmim
if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// pa lëvizur dritaren
SendPacket(connectionRecord);
}
// u mor konfirmimi për marrjen e bllokut të të dhënave
else if (header.PacketNumber == windowHighestBound)
{
// lëvizim dritaren e pranimit/transmetimit
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// rivendosim masivin e kontrollit të transmetimit
connectionRecord.WindowControlArray.Nullify();
// dërgojmë bllokun e paketave
SendPacket(connectionRecord);
}
// kjo Ă«shtĂ« njĂ« kĂ«rkesĂ« pĂ«r ripĂ«rsĂ«ritje â dĂ«rgo paketĂ«n e kĂ«rkuar
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Në gjendjen Assembling në metodën ReceivePacket kryhet puna kryesore për ndërtimin e mesazhit nga paketat e ardhshme.
Assembling.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone != 0)
return;
// përpunimi i pakove pa mekanizmin e konfirmimit të dorëzimit
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
{
// rregullojmë timer-in
connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
// regjistrojmë të dhënat
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// nëse morëm paketën me flamurin e fundit - përfundojmë
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets(connectionRecord);
}
return;
}
// llogaritja e kufirit të fundit të dritares
int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
// éliminimi i pakove jashtë dritares
if (header.PacketNumber (windowHighestBound))
return;
// eliminaciĂłn de dublikatet
if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
return;
// regjistrojmë të dhënat
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// rritim numëruesin e pakove
connectionRecord.PacketCounter++;
// regjistrojmë numrin aktual të paketës në array-n e kontrollit të dritares
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// vendosim paketën më të madhe që ka ardhur
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// rifillojmë timer-in
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// nëse ka ardhur paketa e fundit
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
}
// nëse kemi marrë të gjitha paketat e dritares, atëherë rivendosim numëruesin
// dhe dërgojmë një paketë konfirmimi
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// rivendosim numëruesin.
connectionRecord.PacketCounter = 0;
// zhvendosim dritaren e transmetimit
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// zerojmë array-n e kontrollit të dërgesës
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// nëse paketa e fundit tashmë është marrë
if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
{
// kontrollojmë paketat
ProcessPackets(connectionRecord);
}
}
Në gjendjen Përfunduar detyra e vetme e metodës është dërgimi i një konfirmimi të ripërsëritur për dorëzimin e suksesshëm të mesazhit.
Completed.ReceivePacket:
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// dërgimi i përsëritur i paketës së fundit në lidhje me faktin,
// që ack-i i fundit nuk arriti te dërguesi
if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
}
Metoda SendPacket
Në gjendjen FirstPacketSending kjo metodë kryen dërgimin e paketës së parë të të dhënave, ose, nëse mesazhi nuk kërkon konfirmimin e dorëzimit - të gjithë mesazhin.
FirstPacketSending.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// nëse nuk kërkohet konfirmimi - dërgojmë të gjitha paketat
// dhe lirojmë burimet
if (connectionRecord.IsNoAnswerNeeded)
{
// Këtu ndodh dërgimi As Is
do
{
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// krijojmë header-in e paketës dhe e dërgojmë atë
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// rrisim numëruesin
connectionRecord.SndNext++;
// zhvendosim dritaren
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Nisim timer-in
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Në gjendjen SendingCycle në këtë metodë ndodhi dërgimi i një blloku paketash.
SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// dërgojmë bllokun e paketave
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++;
}
// në rastin e një dritareje të madhe të transmetimit, rifillojmë timer-in pas dërgimit
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
{
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
}
Një vështrim më të thellë në kod. Krijimi dhe vendosja e lidhjeve
Tani që u njoftum me gjendjet dhe metodat kryesore të përdorura për përpunimin e gjendjeve, mund të diskutojmë pak më shumë disa shembuj të punës së protokollit.
Diagrami i transferimit të të dhënave në kushte normale:
Le të shqyrtojmë më në detaje krijimin connection record për lidhjen dhe dërgimin e paketës së parë. Iniciatori i transferimit gjithmonë është aplikacioni që thërret metodën API për dërgimin e mesazhit. Më pas angazhohet metoda StartTransmission e bllokut të kontrollit të dërgesës, e cila fillon dërgimin e të dhënave për një mesazh të ri.
Krijimi i një lidhjeje dalëse:
private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
if (m_isListenerStarted == 0)
{
if (this.LocalEndpoint == null)
{
throw new ArgumentNullException( "", "Duhet të përdorni konstruktorin me parametra ose të nisin dëgjuesin para dërgimit të mesazhit" );
}
// nisni përpunimin e paketimeve hyrëse
StartListener(LocalEndpoint);
}
// krijoni një çelës për fjalorin, bazuar në EndPoint dhe ReliableUdpHeader.TransmissionId
byte[] transmissionId = new byte[4];
// krijoni një numër të rastësishëm transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
// krijoni një rekord të ri për lidhjen dhe kontrolloni,
// nëse një numër i tillë ekziston tashmë në fjalorin tonë
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
{
// nĂ«se ekziston â rindĂ«rtoni njĂ« numĂ«r tĂ« rastĂ«sishĂ«m
m_randomCrypto.GetBytes(transmissionId);
key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0));
if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
// nĂ«se sĂ«rish nuk arrihet â gjeneroni njĂ« pĂ«rjashtim
throw new ArgumentException("Ăifti TransmissionId & EndPoint ekziston tashmĂ« nĂ« fjalor");
}
// nisëm gjendjen për përpunim
m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}
Dërgimi i paketës së parë (gjendja FirstPacketSending):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// ...
// krijoni një ndërfaqe për paketën dhe dërgoni atë
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// rrisni numëruesin
connectionRecord.SndNext++;
// zhvendosni dritaren
connectionRecord.WindowLowerBound++;
// kaloni në gjendjen SendingCycle
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Nisni një timer
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
Pas dĂ«rgimit tĂ« paketĂ«s sĂ« parĂ«, dĂ«rguesi kalon nĂ« gjendjen SendingCycle â duke pritur konfirmimin e pranimit tĂ« paketĂ«s.
Ana e marrësit, me metodën EndReceive, pranon paketën e dërguar, krijon një të re connection record dhe e kalon këtë paketë, me një titull të deshifruar paraprakisht, për përpunim në metodën ReceivePacket të gjendjes FirstPacketReceived
Krijimi i një lidhjeje në anën e marrësit:
private void EndReceive(IAsyncResult ar)
{
// ...
// paketa u mor
// zbërthejmë titullin e paketës
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// erdhi një paketë e pavlefshme - e hedhim atë
return;
}
// ndërtuojmë një çelës për të përcaktuar regjistrin e lidhjes për paketën
Tuple key = new Tuple(connectedClient, header.TransmissionId);
// marrim regjistrin e ekzistues të lidhjes ose krijojmë një të ri
ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
// nisni paketën për përpunim në automatin e fundmë
record.State.ReceivePacket(record, header, bytes);
}
Pranimi i paketës së parë dhe dërgimi i konfirmimit (gjendja FirstPacketReceived):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
// hedhim paketën
return;
// ...
// sipas dizajnit të gjithë numrat e paketave fillojnë nga 0;
if (header.PacketNumber != 0)
return;
// fillojmë një masiv për ruajtjen e pjesëve të mesazhit
ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
// shkruajmë të dhënat e paketës në masiv
ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
// numërojmë numrin e paketave që duhet të vijnë
connectionRecord.NumberOfPackets = (int)Math.Ceiling((double)((double)connectionRecord.IncomingStream.Length / (double)connectionRecord.BufferSize));
// regjistrojmë numrin e paketës së fundit të pranuar (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// pas kësaj e zhvendosim dritaren e pranimit për 1
connectionRecord.WindowLowerBound++;
// ndryshojmë gjendjen
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
if (/*nëse nuk kërkohet mekanizmi i konfirmimit*/)
// ...
else
{
// dërgojme konfirmimin
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
Një vështrim më të thellë në kod. Mbyllja e lidhjes për shkak të skadimit
Përpunimi i kohëve të skadimit është një pjesë e rëndësishme e Reliable UDP. Le të shohim një shembull, ku ndodhi një dështim në nyjën ndërmjetëse dhe dërgimi i të dhënave në të dyja drejtimet u bë i pamundur.
Diagrami i mbylljes së lidhjes për shkak të skadimit:
Siç shihet në diagram, timer-i aktiv i dërguesit ndizet menjëherë pas dërgimit të bllokut të paketave. Kjo ndodh në metodën SendPacket të gjendjes SendingCycle.
Aktivizimi i timer-it aktiv (gjendja SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// dërgojmë një bllok paketash
// ...
// riprogramoni timer-in pas dërgimit
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
}
Periodat e timer-it përcaktohen kur krijohet lidhja. Në mënyrë default, ShortTimerPeriod është 5 sekonda. Në shembullin tanë, ai është vendosur në 1,5 sekonda.
Për një lidhje të ardhshme, timeri aktivizohet pas marrjes së paketës së fundit të dhënash që ka arritur, kjo ndodh në metodën ReceivePacket të gjendjes Assembling
Aktivizimi i timerit të punës (gjendja Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
\/\/ ...
\/\/ rini timerët
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
\/\/ ...
}
Në lidhjen në pritje, gjatë kohës së pritjes së timerit të punës, nuk erdhën më shumë paketa. Timeri u aktivizua dhe thirri metodën ProcessPackets, në të cilën u papërputhën paketa dhe kërkesat për dërgim të përsëritur u dërguan për herë të parë.
Dërgimi i kërkesave për dërgim të përsëritur (gjendja Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
if (/*kontrolli për paketat e humbura */)
{
// dërgojmë kërkesat për dërgim të përsëritur
// vendosim timerin për herë të dytë, për përpjekjen e dërgimit
if (!connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// nëse pas dy përpjekjeve të aktivizimit të WaitForPacketTimer
// nuk arritëm të merrnim paketat - aktivizojmë timerin e përfundimit të lidhjes
StartCloseWaitTimer(connectionRecord);
}
else if (/*erdhën paketa të fundit dhe kontrolli ishte i suksesshëm */)
{
// ...
StartCloseWaitTimer(connectionRecord);
}
// nëse ack për bllokun e paketave është humbur
else
{
if (!connectionRecord.TimerSecondTry)
{
// dërgojmë përsëri ack
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// aktivizojmë timerin e përfundimit të lidhjes
StartCloseWaitTimer(connectionRecord);
}
}
Variabla TimerSecondTry u vendos në e vërtetë. Kjo variabël përgjigjet për ripërsëritjen e aktivizimit të timerit të punës.
Edhe nga ana e dërguesit aktivizohet timeri i punës dhe paketa e fundit e dërguar dërgohet përsëri.
Aktivizimi i timerit të përfundimit të lidhjes (gjendja SendingCycle):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
// ...
// dërgojmë paketën e fundit përsëri
// ...
// aktivizojmĂ« timerin CloseWait â pĂ«r tĂ« pritur rikuperimin e lidhjes ose pĂ«rfundimin e saj
StartCloseWaitTimer(connectionRecord);
}
Pas kësaj, në lidhjen e daljes aktivizohet timeri i përfundimit të lidhjes.
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);
}
Periudha e pritjes së timerit të përfundimit të lidhjes është 30 sekonda siç është e paracaktuar.
Pas një kohe të shkurtër, timeri i punës në anën e marrësit aktivizohet përsëri, dërgohen kërkesa të reja, pas së cilës aktivizohet timeri i përfundimit të lidhjes në lidhjen e ardhshme.
Kur të skadojnë timerët e mbylljes, të gjithë burimet e të dy regjistrave të lidhjes lirohen. Dërguesi njofton aplikacionin e lartë për dështimin e dërgesës (shih API Reliable UDP).
Lirimi i burimeve të regjistrit të lidhjes:
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);
}
}
Një vështrim më të thellë në kod. Rimëkëmbja e dërgimit të të dhënave
Diagrami i rikuperimit të transmetimit të të dhënave në rast të humbjes së paketave:
Siç u diskutua më parë në lidhje me mbylljen e lidhjes për shkak të skadimit, në skadimin e timerit të punës, marrësi do të kontrollojë për paketat e humbura. Në rast se ka humbje paketash, do të përgatitet një listë e numrave të paketave që nuk arritën te marrësi. Këta numra regjistrohen në array-n e LostPackets për lidhjen përkatëse dhe dërgohen kërkesa për ri-dërgim.
Dërgimi i kërkesave për ri-dërgimin e paketave (gjendja Assembling):
public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
\/\/...
if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
{
\/\/ ka humbje paketash, dërgojmë kërkesa për to
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum != 0)
{
ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
}
}
\/\/ ...
}
}
Dërguesi do të pranojë kërkesën për ri-dërgim dhe do të dërgojë paketat e munguar. Vlen të theksohet se në këtë moment dërguesi tashmë ka nisur timerin për mbylljen e lidhjes dhe, kur merr kërkesën, ai ri-nis.
Ri-dërgimi i paketave të humbura (gjendja SendingCycle):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
// ndaloni timer-in e mbylljes së lidhjes
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// ky Ă«shtĂ« njĂ« kĂ«rkesĂ« pĂ«r ripĂ«rsĂ«ritje â dĂ«rgojmĂ« paketĂ«n e kĂ«rkuar
else
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}
Paketë e ripërsëritur (packet#3 në diagram) pranohet nga lidhja hyrëse. Kryhet kontrolli për mbushjen e dritares së pranimit dhe transmetimi normal i të dhënave rinovohet.
Kontrolli për përputhjen në dritaren e pranimit (gjakosja e Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// ...
// rrisim numëruesin e paketimeve
connectionRecord.PacketCounter++;
// regjistroni numrin aktual të paketës në array-n e menaxhimit të dritares
connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
// vendosim paketën më të madhe të arritur
if (header.PacketNumber > connectionRecord.RcvCurrent)
connectionRecord.RcvCurrent = header.PacketNumber;
// rindizni timerat
connectionRecord.TimerSecondTry = false;
connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer != null)
connectionRecord.CloseWaitTimer.Change(-1, -1);
// ...
// nëse kemi pranuar të gjitha paketat e dritares, atëherë e fshijmë numëruesin
// dhe dërgojmë paketën e konfirmimit
else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
{
// fshijmë numëruesin.
connectionRecord.PacketCounter = 0;
// lëvizim dritaren e transmetimit
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// resetimi i array-it të menaxhimit të transmetimit
connectionRecord.WindowControlArray.Nullify();
ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
}
// ...
}
API Reliable UDP
Për të ndërvepruar me protokollin e transmetimit të të dhënave, ekziston një klasë e hapur Reliable Udp, e cila është një mbështjellës mbi blokun e menaxhimit të transmetimit. Ja disa nga anëtarët më të rëndësishëm të klasës:
public sealed class ReliableUdp : IDisposable
{
// merr endpoint-in lokal
public IPEndPoint LocalEndpoint
// krijon një instancë të ReliableUdp dhe fillon
// dëgjimin e paketimeve hyrëse në adresën IP të specifikuar
// dhe porti. Vlera 0 për portin do të thotë përdorim
// porti të alokuar dinamikisht
public ReliableUdp(IPAddress localAddress, int port = 0)
// abonimi për marrjen e mesazheve hyrëse
public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)
// anullimi i abonimit për marrjen e mesazheve
public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
// dërgo mesazh asinkronisht
// Shënim: kompatibiliteti me XP dhe Server 2003 mbetet, pasi përdoret .NET Framework 4.0
public Task SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
// filloni dërgimin asinkron të mesazhit
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
// merrni rezultatin e dërgimit asinkron
public bool EndSendMessage(IAsyncResult asyncResult)
// pastrojë burimet
public void Dispose()
}
Marrja e mesazhit bëhet përmes abonimit. Nënshkrimi i delegatës për metodën e thirrjes:
public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );Mesazhi:
public class ReliableUdpMessage
{
// tipi i mesazhit, enumerim i thjeshtë
public ReliableUdpMessageTypes Type { get; private set; }
// të dhënat e mesazhit
public byte[] Body { get; private set; }
// nĂ«se Ă«shtĂ« e vendosur nĂ« true â mekanizmi i konfirmimit tĂ« dorĂ«zimit do tĂ« anulohet
// për dërgimin e një mesazhi të veçantë
public bool NoAsk { get; private set; }
}
Për t'u abonuar në një lloj të veçantë mesazhesh dhe/ose për një dërgues të veçantë, përdoren dy parametra të opsionalit: ReliableUdpMessageTypes messageType dhe IPEndPoint ipEndPoint.
Llojet e mesazheve:
public enum ReliableUdpMessageTypes : short
{
// Ădo
Any = 0,
// Kërkesë në serverin STUN
StunRequest = 1,
// Përgjigje nga serveri STUN
StunResponse = 2,
// Transferimi i skedarit
FileTransfer =3,
// ...
}
Dërgimi i mesazhit realizohet asinkronisht, për këtë në protokoll është realizuar një model asinkron i programimit:
public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
Rezultati i dĂ«rgimit tĂ« mesazhit do tĂ« jetĂ« true â nĂ«se mesazhi ka arritur me sukses te marrĂ«si dhe false â nĂ«se lidhja Ă«shtĂ« mbyllur pĂ«r shkak tĂ« skadimit tĂ« kohĂ«s:
public bool EndSendMessage(IAsyncResult asyncResult)
Përfundimi
Shumë aspekte nuk janë përshkruar në kuadër të këtij artikulli. Mekanizmat e rregullimit të rrjedhave, trajtimi i përjashtimeve dhe gabimeve, realizimi i metodave asinkrone të dërgimit të mesazheve. Por thelbi i protokollit, përshkrimi i logjikës së trajtimit të paketimeve, vendosja e lidhjes dhe menaxhimi i skadimeve duhet të bëhen të qarta për ju.
Versioni e demonstruar e protokollit të dorëzimit të sigurt është mjaft e qëndrueshme dhe fleksibël dhe përkon me kërkesat e caktuara më parë. Por dua të shtoj se realizimi i përshkruar mund të përmirësohet. Për shembull, për të rritur kapacitetin dhe për të ndryshuar dinamikisht periudhat e timerëve, mund të shtohen mekanizma si dritarja në lëvizje dhe RTT, gjithashtu do të ishte e dobishme implementimi i mekanizmit për të përcaktuar MTU-në midis nyjeve të lidhjes (por vetëm në rastin e dërgimit të mesazheve të mëdha).
Faleminderit për vëmendjen, pres komentet dhe vërejtjet tuaja.
P.S. Për ata që janë të interesuar për detajet ose thjesht duan të testojnë protokollin, linku për projektin në GitHub:
Linke dhe artikuj të dobishëm
- Specifikimi i protokollit TCP: dhe
- Specifikimi i protokollit UDP: dhe
- Diskutimi i protokollit RUDP:
- Protokolli i të dhënave të besueshme: dhe
- Implementim i thjeshtë i konfirmimit të dorëzimit përmes UDP:
- Artikulli që përshkruan mekanizmat për tejkalimin e NAT-eve:
- Implementimi i modelit të programimit asinkron: dhe
- TrashtĂ«sia e modelit tĂ« programimit asinkron nĂ« njĂ« Ćablon asinkron, bazuar nĂ« detyra (APM nĂ« TAP):
Përditësim: Faleminderit dhe për idenë e shtimit të detyrës në ndërfaqe. Kompatibiliteti i bibliotekës me sistemet e vjetra operuese nuk shkelet, pasi që framework-u i katërt mbështet edhe XP-në dhe serverin 2003.
Burimi: habr.com
