Implementimi i protokollit Reliable Udp për .Net

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
Kërkesat për protokoll
Kreu i Reliable UDP
Parimet e përgjithshme të funksionimit të protokollit
Koha e skadimit dhe orët e protokollit
Diagrami i gjendjeve të dërgimit të Reliable UDP
Një vështrim më të thellë në kod. Blloku i kontrollit të dërgimit
Një vështrim më të thellë në kod. Gjendjet

Një vështrim më të thellë në kod. Krijimi dhe vendosja e lidhjeve
Një vështrim më të thellë në kod. Mbyllja e lidhjes për shkak të skadimit
Një vështrim më të thellë në kod. Rimëkëmbja e dërgimit të të dhënave
API Reliable UDP
Përfundimi
Linke dhe artikuj të dobishëm

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

  1. Dorëzimi i besueshëm i pakove, i realizuar përmes mekanizmit të konfirmimit pozitiv (të ashtuquajturit positive acknowledgment)
  2. Nevoja për një transmetim efikas të të dhënave të mëdha, dmth. protokolli duhet të shmangë retransmetimet e panevojshme të pakove
  3. 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)
  4. Mundësia për të realizuar një mënyrë komandimi, me konfirmimin e çdo mesazhi
  5. 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ë rfc 908 dhe rfc 1151, 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:Implementimi i protokollit Reliable Udp për .Net

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:Implementimi i protokollit Reliable Udp për .Net

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ë rfc 793, dhe për UDP në rfc 768, 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:Implementimi i protokollit Reliable Udp për .Net

Struktura e titullit të Reliable UDP është mjaft e thjeshtë:

Implementimi i protokollit Reliable Udp për .Net

  • 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:Implementimi i protokollit Reliable Udp për .Net

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:Implementimi i protokollit Reliable Udp për .Net

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:

Implementimi i protokollit Reliable Udp për .Net

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:

Implementimi i protokollit Reliable Udp për .Net

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:Implementimi i protokollit Reliable Udp për .Net

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:Implementimi i protokollit Reliable Udp për .Net

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:Implementimi i protokollit Reliable Udp për .Net

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:
Projekti Reliable UDP

Linke dhe artikuj të dobishëm

  1. Specifikimi i protokollit TCP: në anglisht dhe në rusisht
  2. Specifikimi i protokollit UDP: në anglisht dhe në rusisht
  3. Diskutimi i protokollit RUDP: draft-ietf-sigtran-reliable-udp-00
  4. Protokolli i të dhënave të besueshme: rfc 908 dhe rfc 1151
  5. Implementim i thjeshtë i konfirmimit të dorëzimit përmes UDP: Merrni Kontroll të Plotë mbi Rrjetin tuaj me .NET dhe UDP
  6. Artikulli që përshkruan mekanizmat për tejkalimin e NAT-eve: Komunikimi Peer-to-Peer Nëpërmjet Translatorëve të Adresave të Rrjetit
  7. Implementimi i modelit të programimit asinkron: Implementimi i Modelit Asinkron të CLR dhe Si të implementoni modelin e dizajnit IAsyncResult
  8. TrashtĂ«sia e modelit tĂ« programimit asinkron nĂ« njĂ« ßablon asinkron, bazuar nĂ« detyra (APM nĂ« TAP):
    TPL dhe Programimi Asinkron Tradicional i .NET
    Interop me Modelet dhe Llojet e Tjera Asinkrone

Përditësim: Faleminderit mayorovp dhe sidristij 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

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster