Protocolul Udp. Care este diferența dintre TCP și UDP, în termeni simpli. Mai adânc în cod. Închiderea unei conexiuni din cauza expirării timpului

O suită de protocoale de rețea pentru Internet. Cu UDP, aplicațiile computerizate pot trimite mesaje (în acest caz numite datagrame) către alte gazde printr-o rețea IP fără a fi nevoie de o comunicare prealabilă pentru a stabili canale speciale de transmisie sau căi de date. Protocolul a fost dezvoltat de David P. Reed în 1980 și definit oficial în RFC 768.

UDP utilizează un model de transmisie simplu, fără strângeri implicite de mână pentru a asigura fiabilitatea, ordonarea sau integritatea datelor. Astfel, UDP oferă un serviciu nesigur, iar datagramele pot să apară neregulate, să fie duplicate sau să dispară fără urmă. UDP implică faptul că verificarea și corectarea erorilor fie nu sunt necesare, fie trebuie efectuate în cadrul aplicației. Aplicațiile sensibile la timp folosesc adesea UDP, deoarece este de preferat să aruncați pachetele decât să așteptați pachetele întârziate, ceea ce ar putea să nu fie posibil pe sistemele în timp real. Dacă este necesară corectarea erorilor la nivelul interfeței de rețea, aplicația poate utiliza TCP sau SCTP concepute în acest scop.

Natura UDP ca protocol fără stat este, de asemenea, utilă pentru serverele care răspund la solicitări mici de la un număr mare de clienți, cum ar fi DNS și aplicații media de streaming precum IPTV, Voice over IP, protocoale de tunel IP și multe jocuri online.

YouTube enciclopedic

    1 / 5

    ✪ Porturi și redirecționare/deschidere de porturi. Instrucțiuni și explicații la îndemâna ta!

  • Subtitrări

Porturi de serviciu

UDP nu oferă nicio garanție de livrare a mesajelor protocolului superior și nu stochează starea mesajelor trimise. Din acest motiv, UDP este uneori numit Protocolul de datagramă nesigură.

Verificați suma

Câmpul sumă de control este utilizat pentru a verifica antetul și datele pentru erori. Dacă cantitatea nu este generată de transmițător, atunci câmpul este umplut cu zerouri. Câmpul este opțional pentru IPv4.

Calculul sumei de control

Metoda de calcul a sumei de control este definită în RFC 1071.

Înainte de a calcula suma de control, dacă lungimea mesajului UDP în octeți este impară, atunci mesajul UDP este completat la sfârșit cu un octet nul (pseudo-antetul și octetul nul de umplutură nu sunt trimise împreună cu mesajul, sunt folosite doar la calcularea sumei de control). Câmpul sumei de control din antetul UDP se presupune a fi zero în timpul calculului sumei de control.

Pentru a calcula suma de control, pseudo-antetul și mesajul UDP sunt împărțite în cuvinte pe doi octeți. Apoi, suma tuturor cuvintelor este calculată în aritmetica codului invers (adică un cod în care se obține un număr negativ dintr-un număr pozitiv prin inversarea tuturor cifrelor numărului și există două zerouri: 0x0000 (notat + 0) și 0xffff (notat −0)). Rezultatul este scris în câmpul corespunzător din antetul UDP.

Valoarea sumei de control egală cu 0x0000 (+0 în codul invers) este rezervată și înseamnă că suma de control nu a fost calculată pentru trimitere. Dacă suma de control a fost calculată și s-a dovedit a fi egală cu 0x0000, atunci valoarea 0xffff (-0 în codul invers) este introdusă în câmpul sumă de control.

Când un mesaj este primit, destinatarul calculează din nou suma de control (ținând cont de câmpul sumei de control), iar dacă rezultatul este −0 (adică 0xffff), atunci suma de control este considerată a fi convergent. Dacă suma nu converge (datele au fost deteriorate în timpul transmiterii sau suma de control a fost calculată incorect pe partea expeditoare), atunci decizia privind acțiunile ulterioare este luată de partea care primește. De regulă, majoritatea dispozitivelor moderne care funcționează cu pachete UDP/IP au setări care le permit fie să ignore astfel de pachete, fie să le omite pentru procesarea ulterioară, indiferent de incorectitudinea sumei de control.

Exemplu de calcul al sumei de control

De exemplu, să calculăm suma de control a mai multor cuvinte pe 16 biți: 0x398a, 0xf802, 0x14b2, 0xc281.

Pentru a face acest lucru, puteți mai întâi să adăugați numere în perechi, tratându-le ca numere fără semn pe 16 biți, urmate de reducerea la codul de complement a doi prin adăugarea unuia la rezultat, dacă în timpul adunării a avut loc un transfer la cea mai mare cifră (a 17-a) (adică, de facto, această operație transformăm un număr negativ din complementul său în codul său reciproc). Sau, ceea ce este echivalent, putem considera că transportul este adăugat la cifra de ordin inferioară a numărului.

0x398a + 0xf802 = 0x1318c → 0x318d (transfer la ordine superioară) 0x318d + 0x14b2 = 0x0463f → 0x463f (număr pozitiv) 0x463f + 0xc281 = 0x108c0 → 108c0

La sfârșit, toți biții din numărul rezultat sunt inversați

0x08c1 = 0000 1000 1100 0001 → 1111 0111 0011 1110 = 0xf73e sau, în caz contrar - 0xffff − 0x08c1 = 0xf73e . Aceasta este suma de control dorită.

La calcularea sumei de control, se folosește din nou un pseudo-antet, simulând un antet IPv6 real:

Biți 0 - 7 8 - 15 16 - 23 24 - 31
0 Sursa adresei
32
64
96
128 Adresa destinatarului
160
192
224
256 Lungimea UDP
288 Zerouri Următorul titlu
320 Port sursă Portul de destinație
352 Lungime Verificați suma
384+
Date

Adresa sursă este aceeași ca și în antetul IPv6. Adresa destinatarului - destinatar final; dacă pachetul IPv6 nu conține un antet de rutare, atunci aceasta va fi adresa de destinație din antetul IPv6, în caz contrar, pe nodul de pornire, va fi adresa ultimului element al antetului de rutare, iar pe nodul receptor, adresa de destinație din antetul IPv6. Valoarea Next Header este egală cu valoarea protocolului - 17 pentru UDP. Lungimea UDP - lungimea antetului UDP și a datelor.

Fiabilitate și soluții pentru problemele de supraîncărcare

Datorită lipsei de fiabilitate, aplicațiile UDP trebuie pregătite pentru pierderi, erori și dublari. Unele dintre ele (de exemplu, TFTP) pot adăuga opțional mecanisme elementare de fiabilitate la nivel de aplicație.

Dar, de cele mai multe ori, astfel de mecanisme nu sunt folosite de aplicațiile UDP și chiar interferează cu ele. Streaming media, jocurile multiplayer în timp real și VoIP sunt exemple de aplicații care folosesc adesea protocolul UDP. În aceste aplicații speciale, pierderea pachetelor nu este de obicei o problemă mare. Dacă aplicația necesită un nivel ridicat de fiabilitate, atunci puteți utiliza un alt protocol (TCP) sau puteți utiliza metode de codare rezistente la erori (cod de ștergere). ru ro).

O problemă potențială mai serioasă este că, spre deosebire de TCP, aplicațiile bazate pe UDP nu au neapărat mecanisme bune de control și evitare a congestionării. Aplicațiile UDP sensibile la congestie care consumă o parte semnificativă din lățimea de bandă disponibilă pot compromite stabilitatea Internetului.

Mecanismele de rețea au fost concepute pentru a minimiza efecte posibile de la suprasarcini sub sarcini necontrolate, de mare viteză. Elementele de rețea, cum ar fi routerele care utilizează cozi de pachete și tehnici de eliminare sunt adesea singurele instrumente disponibile pentru a încetini traficul excesiv UDP. DCCP (Datagram Congestion Control Protocol) a fost dezvoltat ca o soluție parțială la acest lucru potenţială problemă prin adăugarea de mecanisme la gazda finală pentru a monitoriza congestionarea fluxurilor UDP de mare viteză, cum ar fi streaming media.

Aplicații

Numeroase aplicații cheie de Internet utilizează UDP, inclusiv DNS (unde cererile trebuie să fie rapide și constau dintr-o singură cerere urmată de un singur pachet de răspuns), Protocolul simplu de gestionare a rețelei (SNMP), Protocolul de informații de rutare (RIP), Configurația dinamică a gazdei (DHCP) .

Traficul de voce și video este efectuat de obicei folosind UDP. Protocoalele de streaming video și audio în direct sunt concepute pentru a gestiona pierderile aleatorii de pachete, astfel încât calitatea să fie doar ușor degradată în loc să întâmpine întârzieri mari atunci când pachetele pierdute sunt retransmise. Deoarece atât TCP, cât și UDP funcționează în aceeași rețea, multe companii au observat că recenta creștere a traficului UDP din aceste aplicații în timp real împiedică performanța aplicațiilor TCP precum bazele de date sau sistemele de contabilitate. Deoarece atât aplicațiile de afaceri, cât și aplicațiile în timp real sunt importante pentru companii, dezvoltarea de soluții de calitate la probleme este văzută de unii ca o prioritate de top.

Comparație între UDP și TCP

TCP este un protocol orientat spre conexiune, ceea ce înseamnă că este necesară o „strângere de mână” pentru a stabili o conexiune între două gazde. Odată stabilită conexiunea, utilizatorii pot trimite date în ambele direcții.

  • Fiabilitate- TCP gestionează confirmarea mesajelor, retransmiterea și expirarea timpului. Se fac numeroase încercări de a transmite mesajul. Dacă se pierde pe parcurs, serverul va solicita din nou partea pierdută. Cu TCP nu lipsesc date sau (în cazul timeout-urilor multiple) conexiuni întrerupte.
  • Ordine- dacă două mesaje sunt trimise secvenţial, primul mesaj va ajunge mai întâi la aplicaţia destinatară. Dacă bucăți de date ajung în ordine greșită, TCP trimite datele nerespectate într-un buffer până când toate datele pot fi comandate și trimise aplicației.
  • Greutatea- TCP necesită trei pachete pentru a stabili o conexiune socket înainte de a trimite date. TCP monitorizează fiabilitatea și congestia.
  • Filetat- datele sunt citite ca un flux de octeți, nu sunt transmise desemnări speciale pentru limitele sau segmentele mesajelor.

UDP este un protocol mai simplu, bazat pe mesaje, fără conexiune. Aceste tipuri de protocoale nu stabilesc o conexiune dedicată între două gazde. Comunicarea se realizează prin transmiterea de informații într-o singură direcție de la o sursă la un destinatar fără a verifica disponibilitatea sau starea destinatarului. În aplicațiile Voice over IP (TCP/IP), UDP are un avantaj față de TCP, unde orice strângere de mână ar împiedica o bună comunicare vocală. În VoIP, utilizatorii finali trebuie să ofere orice confirmare necesară de primire a mesajului în timp real.

  • Nesigur- atunci când un mesaj este trimis, nu se știe dacă va ajunge la destinație - se poate pierde pe parcurs. Nu există concepte precum confirmare, retransmitere, expirare.
  • Tulburare- dacă două mesaje sunt trimise aceluiași destinatar, atunci nu poate fi prevăzută ordinea în care ating scopul.
  • Lejeritate- fără comandarea mesajelor, fără urmărirea conexiunii etc. Este un strat mic de transport proiectat în IP.
  • Datagrame- pachetele sunt trimise individual și verificate pentru integritate doar dacă ajung. Pachetele au anumite limite care sunt respectate odată primite, ceea ce înseamnă că o operație de citire pe soclul de primire va produce mesajul așa cum a fost trimis inițial.
  • Fără control de suprasarcină- UDP în sine nu evită aglomerația. Este posibil ca aplicațiile cu lățime de bandă mare să provoace colapsul congestiei, cu excepția cazului în care implementează controale la nivel de aplicație.

Protocolul de datagramă utilizator - UDP

Protocolul UDP este unul dintre cele două protocoale de nivel de transport utilizate în stiva de protocoale TCP/IP. UDP permite unui program de aplicație să-și transmită mesajele printr-o rețea cu o suprasarcină minimă asociată cu conversia protocoalelor de nivel de aplicație în IP. Totuși, programul de aplicație însuși trebuie să aibă grijă să confirme că mesajul a fost livrat la destinație. Antetul datagramei UDP (mesaj) arată ca cel prezentat în Figura 2.10.

Orez. 2.10. Structura antetului mesajului UDP

Unitatea de date a protocolului UDP se numește pachet UDP sau datagramă utilizator. Un pachet UDP constă dintr-un antet și un câmp de date care conține pachetul stratului de aplicație. Antetul are un format simplu și este format din patru câmpuri de doi octeți:

    Port sursă UDP - numărul portului procesului de trimitere,

    Port destinație UDP - numărul portului procesului destinatar,

    Lungimea mesajului UDP - lungimea pachetului UDP în octeți,

    Suma de control UDP - suma de control a pachetului UDP

Nu toate câmpurile unui pachet UDP trebuie completate. Dacă datagrama trimisă nu așteaptă un răspuns, atunci pot fi plasate zerouri în locul adresei expeditorului. De asemenea, puteți refuza să calculați suma de control, dar vă rugăm să rețineți că protocolul IP calculează suma de control numai pentru antetul pachetului IP, ignorând câmpul de date

Porturile din antet definesc protocolul UDP ca un multiplexor care permite colectarea și trimiterea mesajelor din aplicații către nivelul de protocol. În acest caz, aplicația folosește un anumit port. Aplicațiile care comunică prin rețea pot utiliza porturi diferite, ceea ce se reflectă în antetul pachetului. În total, pot fi definite 216 porturi diferite. Primele 256 de porturi sunt alocate așa-numitelor „servicii binecunoscute”, care includ, de exemplu, portul UDP 53, care este atribuit serviciului DNS.

Camp Lungime determină lungimea totală a mesajului. Camp Sumă de control servește la controlul integrității datelor. O aplicație care utilizează protocolul UDP trebuie să aibă grijă de integritatea datelor analizând câmpurile Checksum și Length. În plus, la schimbul de date prin UDP, programul de aplicație însuși trebuie să se ocupe de monitorizarea livrării datelor către destinatar. Acest lucru se realizează de obicei prin schimbul de confirmări de livrare între programele de aplicație.

Cele mai cunoscute servicii bazate pe UDP sunt BIND Domain Name Service și NFS Distributed File System. Revenind la exemplul traceroute, acest program folosește și transportul UDP. De fapt, mesajul UDP este trimis în rețea, dar folosește un port care nu are serviciu, motiv pentru care este generat un pachet ICMP, care detectează lipsa serviciului pe mașina de recepție atunci când pachetul ajunge în sfârșit la mașină de destinație.

Protocol de control al transferului - TCP

Dacă monitorizarea calității transmisiei de date prin rețea este importantă pentru o aplicație, atunci în acest caz se utilizează protocolul TCP. Acest protocol se mai numește și un protocol de încredere, orientat spre conexiune și orientat spre flux. Înainte de a discuta aceste proprietăți ale protocolului, să luăm în considerare formatul datagramei transmise prin rețea (Figura 2.11). Conform acestei structuri, TCP, ca și UDP, are porturi. Primele 256 de porturi sunt alocate WKS, porturile de la 256 la 1024 sunt alocate serviciilor Unix, iar restul pot fi folosite la discreția dvs. În câmp Număr de secvență numărul pachetului este definit în secvența de pachete care alcătuiește întregul mesaj, urmat de un câmp de confirmare Numărul de cunoștințeși alte informații de control.

Orez. 2.11. Structura pachetelor TCP

    Portul sursă (SOURS PORT) ocupă 2 octeți, identifică procesul de trimitere;

    Portul de destinație (PORTUL DE DESTINARE) ocupă 2 octeți, identifică procesul destinatar;

    Numărul de secvență (SEQUENCE NUMBER) ocupă 4 octeți, indică numărul de octeți, care determină offset-ul segmentului în raport cu fluxul de date trimise;

    Numărul confirmat (NUMĂR DE CUNOAȘTERE) ocupă 4 octeți, conține numărul maxim de octeți din segmentul primit, mărit cu unu; această valoare este folosită ca chitanță;

    Lungimea antetului (HLEN) este de 4 biți și indică lungimea antetului segmentului TCP, măsurată în cuvinte de 32 de biți. Lungimea antetului nu este fixă ​​și poate varia în funcție de valorile setate în câmpul Opțiuni;

    Reserve (RESERVED) ocupă 6 biți, câmpul este rezervat pentru utilizare ulterioară;

    Biții de cod (CODE BITS) ocupă 6 biți și conțin informații de serviciu despre tipul unui anumit segment, specificate prin setarea biților corespunzători acestui câmp la unul:

    URG - mesaj urgent;

    ACK - chitanță pentru segmentul primit;

    PSH - cerere de a trimite un mesaj fără a aștepta umplerea bufferului;

    RST - cerere de restabilire a conexiunii;

    SYN - mesaj folosit pentru sincronizarea contoarelor datelor transmise la stabilirea unei conexiuni;

    FIN este un semn că partea de transmisie a atins ultimul octet din fluxul de date transmis.

    O fereastră (WINDOW) ocupă 2 octeți, conține valoarea declarată a dimensiunii ferestrei în octeți;

    Suma de control (CHECKSUM) are 2 octeți și este calculată pe segment;

    Pointerul urgent (URGENT POINTER) ocupă 2 octeți, este utilizat împreună cu bitul codului URG, indică sfârșitul datelor care trebuie primite urgent, în ciuda depășirii bufferului;

    OPȚIUNI - acest câmp are o lungime variabilă și poate lipsi cu totul, dimensiunea maximă a câmpului este de 3 octeți; folosit pentru a rezolva probleme auxiliare, de exemplu, la alegerea dimensiunii maxime a segmentului;

    PADDING, o umplutură cu lungime variabilă, este un câmp inactiv folosit pentru a aduce dimensiunea antetului la un număr întreg de cuvinte de 32 de biți.

Fiabilitatea TCP constă în faptul că sursa de date repetă trimiterea, cu excepția cazului în care primește confirmarea de la destinatar într-o anumită perioadă de timp că a fost primită cu succes. Acest mecanism se numește Conștientizare pozitivă cu retransmisie (PAR). După cum am definit anterior, unitatea de transmisie (pachet de date, mesaj etc.) în termeni TCP se numește segment. Există un câmp de sumă de control în antetul TCP. Dacă datele sunt deteriorate în timpul transmisiei, atunci modulul care separă segmentele TCP de pachetele IP poate determina acest lucru folosind suma de control. Pachetul deteriorat este distrus și nimic nu este trimis la sursă. Dacă datele nu au fost corupte, acestea sunt transmise ansamblului de mesaje ale aplicației și o confirmare este trimisă sursei.

Orientarea conexiunii este determinată de faptul că înainte de a trimite un segment de date, modulele TCP sursă și destinație fac schimb de informații de control. Acest schimb se numește strângere de mână(literal „strângere de mână”). TCP folosește o strângere de mână în trei faze:

    Sursa stabilește o conexiune cu destinația trimițându-i un pachet cu indicatorul Synchronize Sequence Numbers (SYN). Numărul din secvență identifică numărul pachetului din mesajul aplicației. Nu trebuie să fie 0 sau unu. Dar toate celelalte numere îl vor folosi ca bază, ceea ce va permite colectarea pachetelor în ordinea corectă;

    Destinatarul răspunde cu un număr în câmpul de confirmare a primirii SYN care se potrivește cu numărul setat de sursă. În plus, câmpul „număr în ordine” poate indica și numărul care a fost solicitat de sursă;

    Sursa confirmă că a acceptat segmentul de destinație și trimite prima bucată de date.

Grafic acest proces este prezentat în Figura 2.12.

Orez. 2.12. Stabilirea unei conexiuni TCP

După ce conexiunea este stabilită, sursa trimite date destinatarului și așteaptă confirmarea de la acesta că au fost primite, apoi trimite din nou datele etc., până când mesajul se termină. Mesajul se termină când bitul FIN este setat în câmpul steagurilor, ceea ce înseamnă „nu mai sunt date”.

Natura de streaming a protocolului este determinată de faptul că SYN determină numărul de pornire pentru numărarea octeților transmisi, nu a pachetelor. Aceasta înseamnă că dacă SYN a fost setat la 0 și s-au transmis 200 de octeți, atunci numărul setat în următorul pachet va fi 201, nu 2.

Este clar că natura de streaming a protocolului și cerința de a confirma primirea datelor dau naștere problemei vitezei de transfer al datelor. Pentru a rezolva această problemă, utilizați o „fereastră” - un câmp - fereastră. Ideea de a folosi fereastra este destul de simplă: transmiteți date fără a aștepta confirmarea primirii acestora. Aceasta înseamnă că sursa transmite o anumită cantitate de date egală cu fereastră fără a aștepta confirmarea primirii acesteia, iar după aceea oprește transmiterea și așteaptă confirmarea. Dacă primește confirmare doar pentru o parte din datele transmise, va începe să transmită o nouă porțiune din numărul care urmează celui confirmat. Acest lucru este prezentat grafic în Figura 2.13.

Orez. 2.13. Mecanismul de transmitere a datelor TCP

În acest exemplu, fereastra este setată la 250 de octeți lățime. Aceasta înseamnă că segmentul curent este un segment cu un offset SYN de 250 de octeți. Cu toate acestea, după transmiterea întregii ferestre, modulul TCP sursă a primit o confirmare pentru a primi doar primii 100 de octeți. Prin urmare, transferul va începe de la 101 de octeți, și nu de la 251.

Astfel, am examinat toate proprietățile de bază ale protocolului TCP. Rămâne doar să numim cele mai cunoscute aplicații pe care TCP le folosește pentru schimbul de date. Acestea sunt în primul rând TELNET și FTP, precum și protocolul HTTP, care este inima World Wide Web.

Să întrerupem puțin conversația despre protocoale și să ne îndreptăm atenția către o componentă atât de importantă a întregului sistem TCP/IP precum adresele IP.

Introducere

UDP este un protocol de nivel de transport simplu orientat pe datagramă: un proces emite o datagramă UDP la un moment dat, rezultând o datagramă IP care este transmisă. Acest lucru este în contrast cu protocoalele orientate spre flux, cum ar fi TCP, în care cantitatea de date care este scoasă de aplicație nu are practic nicio relație cu numărul de datagrame IP trimise.

Figura 11.1 prezintă încapsularea unei datagrame UDP într-o datagramă IP.

Figura 11.1 Încapsularea UDP.

Specificația UDP oficială este dată în RFC 768 [Postel 1980].

UDP este un protocol nesigur: trimite datagrame pe care o aplicație le scrie la nivelul IP, dar nu există nicio garanție că vor ajunge la destinația finală. Din punct de vedere al fiabilității, ați putea fi tentat să evitați utilizarea UDP și să utilizați întotdeauna protocoale de încredere, cum ar fi TCP. După ce TCP este tratat în , vom reveni la acest subiect și vom analiza ce tipuri de aplicații pot folosi UDP.

Aplicațiile nu trebuie să-și facă griji cu privire la dimensiunea datagramei IP rezultate. Dacă este mai mare ca dimensiune decât MTU pentru o anumită rețea (vezi Capitolul 2, secțiunea), datagrama IP va fi fragmentată. Acest lucru se aplică fiecărei rețele prin care va trece o datagramă în drumul său de la sursă la destinație, cu excepția primei rețele la care este conectată gazda care trimite. (Am definit conceptul de MTU de transport într-o secțiune din Capitolul 2.) Fragmentarea IP va fi discutată mai detaliat într-o secțiune a acestui capitol.

Antet UDP

Figura 11.2 prezintă câmpurile prezente în antetul UDP.

Figura 11.2 Antet UDP.

Numerele portului indică procesele de trimitere și primire. Acesta arată că TCP și UDP folosesc numărul portului de destinație pentru a demultiplexa datele care provin de la IP. Deoarece IP demultiplexează datagramele IP de intrare pentru TCP și UDP (folosind valoarea protocolului din antetul IP), TCP se uită la numerele de porturi TCP, iar UDP se uită la numerele de porturi UDP. Numerele de porturi TCP sunt independente de numerele de porturi UDP.

În ciuda acestei independențe, dacă un serviciu rezervat este furnizat atât de TCP, cât și de UDP, același număr de port este de obicei ales pentru ambele straturi de transport. Acest lucru se face pentru comoditate și nu ca o cerință de protocol.

Câmpul de lungime UDP conține lungimea în octeți a antetului UDP și a datelor UDP. Valoarea minimă pentru acest câmp este de 8 octeți. (Nimic rău nu se va întâmpla dacă este trimisă o datagramă UDP cu lungime de date zero.) Parametrul de lungime UDP este redundant. O datagramă IP conține lungimea sa totală în octeți, astfel încât lungimea unei datagrame UDP este lungimea totală minus lungimea antetului IP (care este specificată în câmpul lungime antet de pe ).

Sumă de control UDP

Suma de verificare UDP acoperă antetul UDP și datele UDP. Amintiți-vă că suma de control din antetul IP acoperă doar antetul IP - nu acoperă datele conținute în datagrama IP. Atât UDP, cât și TCP conțin sume de control în anteturile lor, care acoperă atât antetul, cât și datele. Pentru UDP, suma de control este opțională, dar pentru TCP este necesară.

Suma de control UDP este calculată exact în același mod ca (Capitolul 3, secțiunea) suma de control pentru antetul IP (suma cuvintelor de 16 biți cu depășire), deși există diferențe. În primul rând, o datagramă UDP poate consta dintr-un număr impar de octeți, în timp ce calculul sumei de control implică adăugarea de cuvinte de 16 biți. Acest lucru adaugă zero octeți de completare la sfârșitul datagramei, dacă este necesar pentru a calcula suma de control. (octeții de completare nu sunt transmisi.)

În UDP și TCP, există pseudo-anteturi de 12 octeți (în datagramele UDP și segmentele TCP) numai în scopul calculării sumei de control. Pseudo-anteturile conțin anumite câmpuri din antetul IP. Toate acestea sunt făcute pentru a verifica dacă datele au ajuns la destinația dorită (IP-ul nu va accepta datagrame care nu sunt adresate acelei gazde și nu va putea transmite datagrame UDP destinate unui alt strat superior). Figura 11.3 prezintă pseudo-antetul și datagrama UDP.

Figura 11.3 Câmpurile utilizate pentru a calcula suma de control UDP.

În această figură, am arătat în mod specific o datagramă cu o lungime impară, caz în care este necesar un octet suplimentar pentru a calcula suma de control. Rețineți că lungimea datagramei UDP apare de două ori la calcularea sumei de control.

Dacă suma de control calculată este 0, aceasta este stocată ca toți biții (65535), aceste valori fiind echivalente în aritmetica unică-complement. Dacă suma de control transmisă este 0, înseamnă că expeditorul nu a calculat suma de control.

Dacă expeditorul calculează suma de control și destinatarul stabilește că există o eroare, datagrama UDP este distrusă în tăcere și nu este generat niciun mesaj de eroare. (Același lucru se întâmplă dacă stratul IP detectează o eroare în suma de verificare a antetului IP.)

Suma de control UDP este calculată de expeditor și verificată de destinatar. Vă permite să determinați orice modificări ale antetului UDP sau ale datelor care au avut loc de-a lungul căii dintre expeditor și destinatar.

Deși suma de control UDP este un parametru opțional, acesta trebuie întotdeauna calculat. La sfârșitul anilor 1980, unii producători de computere au început să dezactiveze calculul sumei de control UDP în mod implicit pentru a crește viteza sistemului de fișiere de rețea (NFS), care utilizează UDP. Acest lucru poate fi acceptabil pe o singură rețea LAN în care CRC este calculat pentru cadre la nivelul de legătură de date (cadre Ethernet sau Token Ring), care poate fi folosit pentru a determina corupția cadrelor pe măsură ce datagrama trece prin routere. Credeți sau nu, există routere care au erori în software-ul sau hardware-ul lor care schimbă biții în datagramele pe care le direcționează. Aceste erori nu pot fi detectate în datagramele UDP dacă opțiunea sumă de control este dezactivată. De asemenea, trebuie remarcat faptul că unele protocoale de nivel de legătură (de exemplu, SLIP) nu au nicio formă de calcul a sumei de control pentru datele din legătură.

RFC privind cerințele gazdei necesită ca calculul sumei de control UDP să fie activat în mod implicit. De asemenea, solicită ca suma de control primită să fie verificată dacă a fost calculată de către expeditor (în cazul în care suma de control primită nu este zero). Unele implementări ignoră acest lucru și verifică suma de control primită dacă opțiunea de calculare a sumei de control de ieșire este activată.

Ieșirea comenzii tcpdump

Este destul de dificil de determinat dacă opțiunea de calcul al sumei de control UDP este activată pe un anumit sistem. De obicei, aplicația nu are acces la câmpul sumă de control al antetului UDP primit. Pentru a rezolva această problemă, autorul a adăugat o altă opțiune la programul tcpdump, după care a început să scoată sumele de control UDP primite. Dacă valoarea primită este 0, înseamnă că expeditorul nu a calculat suma de control.

Figura 11.4 arată ieșirea către și de la trei sisteme diferite din rețeaua noastră. Am rulat programul sock(), trimițând o singură datagramă UDP cu 9 octeți de date către un server echo standard.

>

1 0.0 sun.1900 > gemini.echo: udp 9 (UDP cksum=6e90)
2 0.303755 (0.3038) gemini.echo > sun.1900: udp 9 (UDP cksum=0)

3 17.392480 (17.0887) sun.1904 > aix.echo: udp 9 (UDP cksum=6e3b)
4 17.614371 (0.2219) aix.echo > sun.1904: udp 9 (UDP cksum=6e3b)

5 32.092454 (14.4781) sun.1907 > solaris.echo: udp 9 (UDP cksum=6e74)
6 32.314378 (0.2219) solaris.echo > sun.1907: udp 9 (UDP cksum=6e74)

Figura 11.4 Ieșire de la tcpdump, care poate fi utilizată pentru a determina dacă opțiunea sumă de control UDP este activată pe o anumită gazdă.

De aici putem observa că două dintre cele trei sisteme au opțiunea sumă de control UDP activată.

De asemenea, rețineți că datagrama de ieșire are aceeași sumă de control ca datagrama de intrare (liniile 3 și 4, 5 și 6). În Figura 11.3, veți observa că cele două adrese IP sunt schimbate, la fel ca și cele două numere de port. Alte câmpuri din pseudo-antetul și antetul UDP au rămas aceleași, deoarece datele au fost repetate. Acest lucru confirmă faptul că suma de control UDP (și într-adevăr toate sumele de control din familia de protocoale TCP/IP) este o sumă simplă de 16 biți. Cu ajutorul acestuia, este imposibil să detectați o eroare, care constă în schimbarea locurilor a două valori de 16 biți.

Câteva statistici

[Mogul 1992] oferă câteva informații statistice despre apariția erorilor de sumă de control pe un server NFS destul de ocupat care rulează de 40 de zile. Figura 11.5 prezintă date statistice.

Numărul de erori în sumele de control

Număr aproximativ de pachete

Ethernet
IP
UDP
TCP

Figura 11.5 Statistica pachetelor corupte identificate folosind sumele de verificare.

Ultima coloană arată numărul aproximativ de pachete, deoarece alte protocoale utilizează stratul Ethernet și IP. De exemplu, nu toate cadrele Ethernet sunt folosite de datagramele IP; ARP folosește și Ethernet. Nu toate datagramele IP sunt folosite de UDP sau TCP, deoarece ICMP folosește și IP.

Rețineți că au fost detectate semnificativ mai multe erori de sumă de control TCP decât erori de sumă de control UDP. Acest lucru se datorează cel mai probabil faptului că TCP stabilește de obicei conexiuni „la distanță lungă” (care trec prin multe routere, poduri etc.), în timp ce traficul UDP este de obicei local.

Prin urmare, erorile enumerate în linia de jos nu sunt întotdeauna legate de stratul de legătură de date (Ethernet, Token ring). Când transferați date, ar trebui să activați întotdeauna opțiunea sumă de control la punctele finale. Cu toate acestea, dacă datele transmise sunt de o oarecare valoare, nu ar trebui să aveți încredere completă în sumele de control UDP sau TCP, deoarece acestea sunt simple sume de control și nu sunt garantate pentru a proteja datele de toate erorile posibile.

Exemplu simplu

Vom folosi programul sock pentru a genera niște datagrame UDP, pe care le vom analiza folosind tcpdump:

>bsdi % sock -v -u -i -n4 svr4 arunca
conectat pe 140.252.13.35.1108 la 140.252.13.34.9

bsdi % sock -v -u -i -n4 -w0 svr4 arunca
conectat pe 140.252.13.35.1110 la 140.252.13.34.9

În primul caz de pornire a programului, modul de depanare este setat ( -v), în timp ce puteți vizualiza numerele de porturi alocate dinamic, UDP este specificat ( -u) în loc de TCP în mod implicit, iar modul sursă este setat, opțiunea ( -i), aceasta înseamnă că vom trimite date, mai degrabă decât să citim de la intrarea standard sau să scriem la ieșirea standard. Opțiunea -n4 îi spune să scoată 4 datagrame (în loc de valoarea implicită de 1024) către gazda destinație svr4. Serviciul de eliminare este descris în secțiunea din Capitolul 1. Folosim o dimensiune implicită de ieșire de 1024 de octeți per înregistrare.

A doua oară când am rulat programul, specificând -w0, aceasta va produce datagrame de lungime zero. Figura 11.6 arată rezultatul comenzii tcpdump pentru două exemple.

>

1 0.0 bsdi.1108 > svr4.discard: udp 1024
2 0.002424 (0.0024) bsdi.1108 > svr4.discard: udp 1024
3 0.006210 (0.0038) bsdi.1108 > svr4.discard: udp 1024
4 0.010276 (0.0041) bsdi.1108 > svr4.discard: udp 1024

5 41.720114 (41.7098) bsdi.1110 > svr4.discard: udp 0
6 41.721072 (0.0010) bsdi.1110 > svr4.discard: udp 0
7 41.722094 (0.0010) bsdi.1110 > svr4.discard: udp 0
8 41.723070 (0.0010) bsdi.1110 > svr4.discard: udp 0

Figura 11.6 Ieșirea comenzii tcpdump atunci când datagramele UDP sunt trimise într-o singură direcție.

Ieșirea arată patru datagrame de 1024 de octeți urmate de patru datagrame de lungime zero. Fiecare datagramă urmează pe cea anterioară cu un interval de câteva milisecunde. (A durat 41 de secunde pentru a introduce a doua comandă.)

Înainte ca prima datagramă să fie trimisă, nu exista nicio legătură între expeditor și destinatar. (În discuția noastră despre TCP, arătăm că o conexiune trebuie stabilită înainte ca primul octet de date să fie trimis.) Este important de reținut că receptorul nu emite o confirmare când primește datele. Expeditorul, în acest exemplu, nu are idee dacă datele au fost primite la capătul de la distanță.

În cele din urmă, rețineți că numărul portului sursă UDP se modifică de fiecare dată când rulați programul. Portul a fost mai întâi 1108, apoi 1110. În secțiunea Capitolului 1, am arătat că numerele de porturi alocate dinamic utilizate de clienți variază de obicei între 1024 și 5000.

Fragmentarea IP

Figura 11.9 arată formatul de eroare inaccesibil ICMP pentru acest caz. Diferă de formatul afișat în, deoarece biții 16-31 din al doilea cuvânt de 32 de biți pot conține următorul hop MTU în loc să fie setați la 0.

Figura 11.9 Eroare de inaccesibil ICMP atunci când este necesară fragmentarea, dar bitul „nu fragmentare” este setat.

Dacă routerul nu acceptă acest nou format de eroare ICMP, următorul hop MTU este setat la 0.

Noul RFC privind cerințele routerului [Almquist 1993] specifică că un router trebuie să genereze acest nou formular atunci când emite un mesaj ICMP inaccesibil.

Problema pe care o vom discuta a apărut atunci când am primit o eroare ICMP în timp ce încercam să determinăm MTU-ul unei legături dial-up SLIP între routerul netb și soarele gazdă. Cunoaștem MTU-ul acestei legături de la sun la netb pentru că, în primul rând, acest lucru este specificat la configurarea SLIP pe gazda sun și, în al doilea rând, am văzut MTU-ul când am rulat comanda netstat în secțiunea din capitolul 3. Acum dorim să determinăm MTU-ul în altă direcție. (Acest lucru explică modul de determinare a MTU folosind SNMP.) Pentru legăturile punct la punct, MTU nu trebuie să fie același în ambele direcții.

Pentru determinare a fost utilizată următoarea tehnică. Am rulat un ping de la gazda solaris la gazda bsdi, mărind dimensiunea pachetului de date până când fragmentarea a fost aplicată pachetelor. Procesul este prezentat în Figura 11.10.

Figura 11.10 Sisteme care au fost utilizate pentru a determina MTU a legăturii SLIP dintre netb și sun.

Programul tcpdump a fost lansat pe gazda solară, ceea ce ne-a permis să vedem cum se realizează fragmentarea în canalul SLIP. Fragmentarea nu a apărut și totul a fost în regulă până când dimensiunea datelor din pachetul ping a crescut de la 500 de octeți la 600. Solicitările ecou primite erau vizibile (ca și cum nu ar exista fragmentare), dar răspunsurile ecou au dispărut.

Pentru a înțelege mai bine ce se întâmplă, tcpdump a fost rulat și pe bsdi, după care a devenit clar ce se trimite și ce se primește. Figura 11.11 arată rezultatul.

>

1 0.0 solaris >
2 0,000000 (0,0000) bsdi >
3 0,000000 (0,0000) soare >
trebuie să fragezi, mtu = 0 (DF)

4 0,738400 (0,7384) solaris > bsdi: icmp: cerere ecou (DF)
5 0,748800 (0,0104) bsdi > solaris: icmp: echo reply (DF)
6 0,748800 (0,0000) soare > bsdi: icmp: solaris inaccesibil -
trebuie să fragezi, mtu = 0 (DF)

Figura 11.11 Ieșirea programului tcpdump de la ping la bsdi de la Solaris cu o datagramă IP de 600 de octeți.

În primul rând, expresia (DF) de pe fiecare linie înseamnă că bitul „nu fragmentați” din antetul IP este setat la unu. Aceasta înseamnă că Solaris 2.2 setează în mod normal acest bit la unu ca parte a mecanismului de determinare a MTU de transport.

Linia 1 indică faptul că ping-ul trece prin netb la sun fără fragmentare și cu bitul DF setat, deci putem concluziona că dimensiunea critică MTU pentru netb gazdă SLIP nu a fost încă atinsă.

De asemenea, observați din rândul numărul 2 că flag-ul DF este copiat în fiecare răspuns ecou. Acesta este exact ceea ce a cauzat problema. Răspunsul ecou are aceeași dimensiune ca și cererea ecou (puțin peste 600 de octeți), dar MTU-ul interfeței SLIP de ieșire a gazdei Sun este 552. Răspunsul ecou trebuie să fie fragmentat, dar steag-ul DF este setat. Acest lucru face ca sun să genereze o eroare ICMP inaccesabilă și să o trimită către bsdi (unde este distrusă).

Acesta este motivul pentru care nu am văzut ecouri de la Solaris. Răspunsurile nu treceau prin soare. Figura 11.12 arată calea pe care o parcurg pachetele.

Figura 11.12 Schimb de pachete pentru acest exemplu.

În cele din urmă, rețineți că expresia mtu=0 din liniile 3 și 6 din Figura 11.11 indică faptul că soarele nu returnează MTU pentru interfața de ieșire în mesajul ICMP inaccesibil, așa cum se arată în Figura 11.9. (În capitolul 25, vom rezolva această problemă folosind SNMP și ne vom asigura că SLIP MTU al interfeței netb este 1500.)

Determinarea MTU de transport folosind Traceroute

Deoarece majoritatea sistemelor nu acceptă funcția de determinare a MTU de transport, vom modifica programul traceroute() astfel încât să poată determina MTU de transport. Vom trimite un pachet cu bitul de nu fragmentare setat. Mărimea primului pachet trimis va fi egală cu MTU-ul interfeței de ieșire. Când este returnată eroarea ICMP „nu se poate fragmenta”, vom reduce pachetul mărimea. Dacă routerul care a trimis eroarea ICMP acceptă o nouă versiune care include interfața de ieșire MTU în mesajul ICMP, vom folosi valoarea rezultată; altfel vom încerca următorul MTU mai mic. Deoarece RFC 1191 [Mogul și Deering 1990] afirmă că există un număr limitat de valori MTU, programul nostru are un tabel cu valori posibile și pur și simplu se va trece la următoarea valoare mai mică.

Să încercăm un algoritm similar de la soarele gazdă la alunecarea gazdei, știind că canalul SLIP are un MTU de 296:

>

soare % traceroute.pmtu slip

MTU de ieșire = 1500
1 bsdi (140.252.13.35) 15 ms 6 ms 6 ms
2 bsdi (140.252.13.35) 6 ms
este necesară fragmentarea și DF setat, încercând un nou MTU = 1492
este necesară fragmentarea și DF setat, încercând un nou MTU = 1006
este necesară fragmentarea și DF setat, încercând un nou MTU = 576
este necesară fragmentarea și DF setat, încercând un nou MTU = 552
este necesară fragmentarea și DF setat, încercând un nou MTU = 544
este necesară fragmentarea și DF setat, încercând un nou MTU = 512
este necesară fragmentarea și DF setat, încercând un nou MTU = 508
este necesară fragmentarea și DF setat, încercând un nou MTU = 296
2 alunecare (140.252.13.65) 377 ms 377 ms 377 ms

În acest exemplu, routerul bsdi nu a returnat interfața MTU de ieșire în mesajul ICMP, așa că vom cădea la următoarea valoare MTU inferioară. Prima linie de ieșire pentru un TTL de 2 raportează numele de gazdă bsdi, dar acest lucru se datorează faptului că a fost returnat de router într-o eroare ICMP. Ultima linie de ieșire pentru un TTL de 2 este exact ceea ce ne așteptam.

Este ușor să modificați codul ICMP pe bsdi pentru a obține MTU-ul interfeței de ieșire. Și dacă facem asta și ne întoarcem la programul nostru, vom obține următoarea ieșire:

>

soare % traceroute.pmtu slip
traceroute pentru a aluneca (140.252.13.65), 30 de hamei max
MTU de ieșire = 1500
1 bsdi (140.252.13.35) 53 ms 6 ms 6 ms
2 bsdi (140.252.13.35) 6 ms
este necesară fragmentarea și DF setat, următorul hop MTU = 296
2 alunecare (140.252.13.65) 377 ms 378 ms 377 ms

Nu are rost să încerci aici opt valori MTU diferite; routerul a raportat valoarea dorită.

Internetul mondial

O versiune modificată a traceroute a fost rulată de mai multe ori pe diferite gazde din întreaga lume. A ajuns în 15 țări (inclusiv în Antarctica), folosind diverse canale transatlantice și transpacific. Cu toate acestea, înainte de a face acest lucru, am mărit MTU-ul conexiunii SLIP dial-up dintre subrețeaua noastră și routerul netb (Figura 11.12) la 1500, la fel ca în Ethernet.

Din cele 18 ori în care a fost rulat programul, în doar două cazuri transportul MTU a fost mai mic de 1500. O legătură transatlantică avea un MTU de 572 (o valoare ciudată care nici măcar nu este listată în RFC 1191), iar routerul nu a returnat un Eroare ICMP pe noul format. O altă legătură între două routere din Japonia nu a procesat cadre de 1500 de octeți, iar routerul nu a returnat o eroare ICMP în noul format. După ce MTU a fost redus la 1006, totul a funcționat.

Concluzia pe care o putem trage din aceste experimente este că majoritatea (dar nu toate) rețelele de suprafață extinsă pot gestiona în prezent pachete mai mari de 512 octeți. Utilizarea caracteristicii de căutare MTU de transport permite aplicațiilor să funcționeze mult mai eficient folosind MTU-uri mai mari.

Determinarea MTU de transport la utilizarea UDP

Să ne uităm la interacțiunea dintre o aplicație care utilizează UDP și mecanismul de descoperire a transportului MTU. Trebuie să vedem ce se întâmplă atunci când o aplicație trimite o datagramă care este prea mare pentru un canal intermediar.

Deoarece singurul sistem care acceptă un mecanism de descoperire a transportului MTU este Solaris 2.x, îl folosim ca gazdă sursă pentru a trimite o datagramă de 650 de octeți pentru a slip. Deoarece gazda slip se află în spatele unei legături SLIP cu un MTU de 296, orice datagramă UDP mai mare de 268 de octeți (296 - 20 - 8) cu setul de biți „nu fragmentați” ar trebui să provoace o eroare ICMP „nu se poate fragmenta” din bsdi. router. Figura 11.13 prezintă topologia și MTU ale canalelor.

Figura 11.13 Sisteme utilizate pentru determinarea MTU de transport folosind UDP.

Următoarea comandă generează zece datagrame UDP de 650 de octeți la intervale de 5 secunde:

> solare % sock -u -i -n10 -w650 -p5 slip arunca

Figura 11.14 arată rezultatul comenzii tcpdump. Când a fost rulat acest exemplu, routerul bsdi a fost configurat să nu returneze MTU-ul următor hop ca parte a unei erori ICMP „nu se poate fragmenta”.

Prima datagramă trimisă cu setul de biți DF (linia 1) generează eroarea așteptată de la routerul bsdi (linia 2). Interesant este că următoarea datagramă, trimisă și cu setul de biți DF (linia 3), generează aceeași eroare ICMP (linia 4). Ne așteptam ca această datagramă să fie trimisă cu bitul DF dezactivat.

La linia 5, IP a realizat în cele din urmă că datagramele către această destinație nu ar trebui trimise cu biții DF setat și apoi a început să fragmenteze datagramele la gazda sursă. Acest comportament este diferit de ceea ce a fost arătat în exemplele anterioare, unde IP a trimis datagramele pe care le-a primit de la UDP, în timp ce ruterelor cu MTU-uri mai mici (bsdi în acest caz) li s-a permis să se fragmenteze.

>

1 0.0 solaris.38196 > slip.discard: udp 650 (DF)
2 0,004218 (0,0042) bsdi > solaris: icmp:

3 4.980528 (4.9763) solaris.38196 > slip.discard: udp 650 (DF)
4 4,984503 (0,0040) bsdi > solaris: icmp:
alunecare inaccesibil - trebuie să fragmentați, mtu = 0 (DF)

5 9.870407 (4.8859) solaris.38196 > slip.discard: udp 650 (frag 47942:552@0+)
6 9,960056 (0,0896) solaris > slip: (frag 47942:106@552)

7 14.940338 (4.9803) solaris.38196 > slip.discard: udp 650 (DF)
8 14,944466 (0,0041) bsdi > solaris: icmp:
alunecare inaccesibil - trebuie să fragmentați, mtu = 0 (DF)

9 19.890015 (4.9455) solaris.38196 > slip.discard: udp 650 (frag 47944:552@0+)
10 19,950463 (0,0604) solaris > slip: (frag 47944:106@552)

11 24.870401 (4.9199) solaris.38196 > slip.discard: udp 650 (frag 47945:552@0+)
12 24,960038 (0,0896) solaris > slip: (frag 47945:106@552)

13 29.880182 (4.9201) solaris.38196 > slip.discard: udp 650 (frag 47946:552@0+)
14 29,940498 (0,0603) solaris > slip: (frag 47946:106@552)

15 34.860607 (4.9201) solaris.38196 > slip.discard: udp 650 (frag 47947:552@0+)
16 34,950051 (0,0894) solaris > slip: (frag 47947:106@552)

17 39.870216 (4.9202) solaris.38196 > slip.discard: udp 650 (frag 47948:552@0+)
18 39,930443 (0,0602) solaris > slip: (frag 47948:106@552)

19 44.940485 (5.0100) solaris.38196 > slip.discard: udp 650 (DF)
20 44,944432 (0,0039) bsdi > solaris: icmp:
alunecare inaccesibil - trebuie să fragmentați, mtu = 0 (DF)

Figura 11.14 Determinarea MTU de transport folosind UDP.

Deoarece mesajul ICMP „nu se poate fragmenta” nu conține următorul hop MTU, aceasta înseamnă că IP a decis că toată lumea este mulțumită cu un MTU de 576. Primul fragment (linia 5) conține 544 de octeți de date UDP, 8 octeți de antet UDP și 20 de octeți de antet IP, dimensiunea totală a unei datagrame IP este de 572 de octeți. Al doilea fragment (linia 6) conține cei 106 octeți rămași de date UDP și antetul IP de 20 de octeți.

Din păcate, următoarea datagramă, linia 7, are bitul DF setat, deci este eliminată de bsdi și este returnată o eroare ICMP. Ce s-a întâmplat aici este că temporizatorul IP a expirat, care i-a spus IP să verifice dacă MTU-ul de transport a crescut prin resetarea bitului DF. Vom vedea că acest lucru se întâmplă din nou în liniile 19 și 20. Comparând timpii din liniile 7 și 19 în care bitul DF este setat la unu, vedem că MTU de transport este verificat să crească la fiecare 30 de secunde.

Acest cronometru de 30 de secunde este prea scurt. RFC 1191 recomandă setarea temporizatorului la 10 minute. Valoarea temporizatorului poate fi modificată utilizând parametrul ip_ire_pathmtu_interval (Anexa E, secțiunea). În Solaris 2.2, nu există nicio modalitate de a dezactiva detectarea MTU de transport pentru o singură aplicație UDP sau pentru toate aplicațiile UDP. Poate fi activat sau dezactivat numai pentru întregul sistem în ansamblu prin modificarea parametrului ip_path_mtu_discovery. După cum putem vedea din acest exemplu, activarea caracteristicii de detectare a transportului MTU atunci când aplicațiile UDP trimit datagrame care probabil vor fi fragmentate va duce la eliminarea datagrama.

Dimensiunea maximă a datagramei acceptată de stratul IP pe solaris (576 de octeți) este incorectă. Am văzut în Figura 11.13 că MTU-ul real este de 296 de octeți. Aceasta înseamnă că fragmentele generate de solaris sunt fragmentate din nou pe bsdi. Figura 11.15 arată ieșirea tcpdump primită la gazda de destinație (slip) pentru prima datagramă care sosește (liniile 5 și 6 din Figura 11.14).

>

1 0.0 solaris.38196 > slip.discard: udp 650 (frag 47942:272@0+)
2 0,304513 (0,3045) solaris > slip: (frag 47942:272@272+)
3 0,334651 (0,0301) solaris > slip: (frag 47942:8@544+)
4 0,466642 (0,1320) solaris > slip: (frag 47942:106@552)

Figura 11.15 Prima datagramă care ajunge la gazda slip de la Solaris.

În acest exemplu, gazda solaris nu ar trebui să fragmenteze datagramele de ieșire, ci ar trebui să dezactiveze bitul DF și să permită routerului cu MTU mai mic să efectueze fragmentarea.

Acum vom rula același exemplu, dar vom schimba comportamentul routerului bsdi, astfel încât să returneze următorul hop MTU în mesajul ICMP „nu se poate fragmenta”. Figura 11.16 prezintă primele șase linii de ieșire tcpdump.

>

1 0.0 solaris.37974 > slip.discard: udp 650 (DF)
2 0,004199 (0,0042) bsdi > solaris: icmp:

3 4.950193 (4.9460) solaris.37974 > slip.discard: udp 650 (DF)
4 4,954325 (0,0041) bsdi > solaris: icmp:
alunecare inaccesibil - trebuie să fragmentați, mtu = 296 (DF)

5 9.779855 (4.8255) solaris.37974 > slip.discard: udp 650 (frag 35278:272@0+)
6 9,930018 (0,1502) solaris > slip: (frag 35278:272@272+)
7 9,990170 (0,0602) solaris > slip: (frag 35278:114@544)

Figura 11.16 Determinarea MTU de transport folosind UDP.

Încă o dată vedem că primele două datagrame au fost trimise cu bitul DF setat și ambele au primit erori ICMP. În prezent, eroarea ICMP indică următorul MTU înainte, care este 296.

În rândurile 5, 6 și 7, vedem că gazda sursă efectuează fragmentare, ca în Figura 11.14. Dacă MTU-ul următor hop este cunoscut, sunt generate doar trei fragmente, în comparație cu cele patru fragmente care sunt generate de routerul bsdi din Figura 11.15.

Interacțiunea dintre UDP și ARP

Folosind UDP, putem privi o interacțiune foarte interesantă între UDP și o implementare tipică ARP.

Folosim programul sock pentru a genera o datagramă UDP cu 8192 de octeți de date. Ne așteptăm ca în acest caz să fie generate șase fragmente Ethernet (vezi Capitolul 11). De asemenea, înainte de a rula programul, ne vom asigura că memoria cache ARP este goală, astfel încât înainte de trimiterea primului fragment, trebuie schimbată o cerere și un răspuns ARP.

>

bsdi % arp -a verificați dacă memoria cache ARP este goală
bsdi % sock -u -i -n1 -w8192 svr4 aruncați

Ne așteptăm ca prima datagramă să provoace trimiterea unei cereri ARP. Următoarele cinci fragmente care sunt generate de IP pun două întrebări la care putem răspunde doar utilizând tcpdump: fragmentele rămase vor fi gata pentru a fi trimise înainte ca răspunsul ARP să fie primit, dacă da, ce va face ARP cu aceste câteva pachete direcționate? la o anumită destinație în timp ce așteptați un răspuns ARP? Figura 11.17 arată rezultatul programului tcpdump.

>

1 0.0 arp cine-are svr4 spune bsdi
2 0,001234 (0,0012) arp cine-are svr4 spune bsdi
3 0,001941 (0,0007) arp cine-are svr4 spune bsdi
4 0,002775 (0,0008) arp cine-are svr4 spune bsdi
5 0,003495 (0,0007) arp cine-are svr4 spune bsdi
6 0,004319 (0,0008) arp cine-are svr4 spune bsdi
7 0,008772 (0,0045) răspuns arp svr4 este la 0:0:c0:c2:9b:26
8 0,009911 (0,0011) răspuns arp svr4 este la 0:0:c0:c2:9b:26
9 0,011127 (0,0012) bsdi > svr4: (frag 10863:800@7400)
10 0,011255 (0,0001) răspuns arp svr4 este la 0:0:c0:c2:9b:26
11 0,012562 (0,0013) răspuns arp svr4 este la 0:0:c0:c2:9b:26
12 0,013458 (0,0009) răspuns arp svr4 este la 0:0:c0:c2:9b:26
13 0,014526 (0,0011) răspuns arp svr4 este la 0:0:c0:c2:9b:26
14 0,015583 (0,0011) răspuns arp svr4 este la 0:0:c0:c2:9b:26

Figura 11.17 Schimb de pachete la trimiterea unei datagrame UDP de 8192 de octeți prin Ethernet.

Această concluzie este destul de neașteptată. În primul rând, înainte de primirea primului răspuns ARP, sunt generate șase solicitări ARP. După cum ați putea ghici, IP generează rapid șase fragmente și este trimisă o solicitare ARP pentru fiecare.

Apoi, când este primit primul răspuns ARP (linia 7), este trimis doar ultimul fragment (linia 9)! Aceasta înseamnă că primele cinci fragmente au fost aruncate. În realitate, acesta este un exemplu de funcționare normală ARP. Majoritatea implementărilor rețin doar ultimul pachet care trebuie trimis către gazda destinație în timp ce așteaptă un răspuns ARP.

RFC privind cerințele gazdei necesită implementări pentru a preveni inundarea ARP (trimiterea în mod repetat a cererilor ARP pentru aceeași adresă IP cu frecvență înaltă). Frecvența maximă recomandată este o dată pe secundă. Aici vedem șase solicitări ARP în 4,3 milisecunde. RFC privind cerințele gazdei necesită ca ARP să stocheze cel puțin un pachet și trebuie să fie cel mai recent pachet. Este exact ceea ce am văzut aici.

Următoarea anomalie inexplicabilă este că svr4 a trimis înapoi șapte răspunsuri ARP, nu șase.

Un ultim lucru de remarcat este că tcpdump a rulat încă 5 minute după ce ultimul răspuns ARP a revenit, așteptând să vadă svr4 să trimită înapoi o eroare ICMP „timp depășit în timpul reasamblarii”. Mesajul ICMP nu a apărut niciodată. (Am arătat formatul acestui mesaj în . Câmpul de cod setat la 1 indică faptul că timpul a trecut în timp ce datagrama este reasamblată.)

Stratul IP trebuie să pornească un temporizator când apare primul fragment de datagramă. Aici, „primul” înseamnă primul fragment care sosește pentru o datagramă dată, nu doar primul fragment (cu un decalaj al fragmentului de 0). O valoare tipică de timeout variază între 30 și 60 de secunde. Dacă toate fragmentele pentru această datagramă nu au ajuns înainte de expirarea temporizatorului, toate fragmentele sunt aruncate. Dacă acest lucru nu se face, fragmentele care nu ajung niciodată (cum am văzut în acest exemplu) pot cauza depășirea tamponului de recepție.

Există două motive pentru care nu am văzut mesajul ICMP. În primul rând, majoritatea implementărilor Berkeley nu generează niciodată această eroare! Aceste implementări setează un cronometru și elimină toate fragmentele atunci când cronometrul expiră, dar nu este generată nicio eroare ICMP. În al doilea rând, primul fragment - fragmentul cu offset egal cu 0, care conține antetul UDP, nu a fost primit. (Acesta a fost primul dintre cele cinci pachete abandonate de ARP.) Implementarea nu necesită generarea unei erori ICMP dacă primul fragment nu este primit. Motivul este că receptorul de erori ICMP nu poate spune ce proces utilizator a trimis datagrama care a fost eliminată deoarece antetul stratului de transport nu era disponibil. Iar nivelul superior (fie o aplicație TCP, fie o aplicație UDP) va expira și va repeta transmisia.

În această secțiune, am folosit fragmentarea IP pentru a vedea cum funcționează comunicarea dintre UDP și ARP. Această interacțiune poate fi văzută dacă expeditorul trimite mai multe datagrame UDP în succesiune rapidă. Am folosit fragmentarea, deoarece pachetele sunt generate rapid folosind IP, ceea ce este mult mai rapid decât ca un utilizator să genereze mai multe pachete.

Dimensiunea maximă a datagramei UDP

Teoretic, dimensiunea maximă a unei datagrame IP poate fi de 65535 de octeți, care este limitată de câmpul de lungime completă de 16 biți din antetul IP (vezi). Cu o lungime a antetului IP de 20 de octeți și o lungime a antetului UDP de 8 octeți, datagrama UDP lasă un maxim de 65507 octeți pentru datele utilizatorului. Cu toate acestea, majoritatea implementărilor folosesc datagrame semnificativ mai mici.

Două restricții intră de obicei în joc. În primul rând, programul de aplicație poate fi limitat de interfața sa de programare. API-ul Sockets (Capitolul 1, secțiunea) oferă o funcție care poate fi apelată de o aplicație pentru a seta dimensiunea bufferelor de intrare și de ieșire. Pentru un socket UDP, această dimensiune este direct legată de dimensiunea maximă a unei datagrame UDP care poate fi citită și scrisă de UDP. În prezent, majoritatea sistemelor oferă o dimensiune maximă implicită a unei datagrame UDP care poate fi citită sau scrisă, care este de 8192 de octeți. (Această valoare este setată la 8192, deoarece acesta este ceea ce citește și scrie NFS în mod implicit.)

Următoarea limitare este determinată de implementarea nucleului TCP/IP. Pot exista caracteristici de implementare (sau erori) care limitează dimensiunea unei datagrame UDP la mai puțin de 65535 de octeți.

Autorul a experimentat cu diferite dimensiuni de datagramă UDP folosind programul sock. Folosind interfața loopback sub SunOS 4.1.3, dimensiunea maximă a datagramei UDP a fost de 32767 octeți. Nu a fost posibilă utilizarea unei valori mai mari. La transferul prin Ethernet de la BSD/386 la SunOS 4.1.3, dimensiunea maximă a unei datagrame IP pe care Sun a putut-o accepta era de 32786 (cu 32758 de octeți de date utilizator). Folosind interfața loopback din Solaris 2.2, dimensiunea maximă a unei datagrame IP care putea fi trimisă și primită a fost de 65535 octeți. La transferul de la Solaris 2.2 la AIX 3.2.2, a fost posibil să se transfere o datagramă IP cu o dimensiune maximă de 65535 octeți.

Trunchierea datagramei

Doar pentru că IP poate trimite și primi datagrame de o anumită dimensiune nu înseamnă că aplicația care primește este pregătită să citească datagrame de acea dimensiune. API-ul UDP permite aplicațiilor să specifice numărul maxim de octeți care trebuie procesați la un moment dat. Ce se întâmplă dacă datagrama primită este mai mare decât datagrama pe care aplicația este dispusă să o accepte?

Din păcate, răspunsul depinde de interfața de programare și de implementare.

Versiunile tradiționale ale API-ului socket Berkeley trunchiază datagramele, eliminând orice date care nu se potrivesc. Dacă aplicația este notificată depinde de versiune. (4.3 BSD Reno și versiunile ulterioare pot notifica aplicația că o datagramă a fost trunchiată.) Socket-urile API sub SVR4 (inclusiv Solaris 2.x) nu trunchiază datagramele. Orice date care nu se potrivesc sunt citite secvenţial. Aplicația nu este notificată cu mai multe cicluri de citire și i se va trimite o singură datagramă UDP. API-urile TLI nu aruncă date. În schimb, este returnat un indicator care indică faptul că există mai multe date decât pot fi citite simultan, astfel încât aplicația începe să citească datagrama rămasă secvenţial.

Când discutăm despre TCP, vom vedea că acest protocol oferă aplicației fluxuri secvențiale de octeți fără nicio restricție. TCP furnizează aplicației orice dimensiune de date pe care le solicită aplicația – și nicio dată nu se pierde pe acea interfață.

Eroare de suprimare a sursei ICMP

Folosind UDP, puteți genera o eroare de stingere a sursei ICMP. Această eroare poate fi generată de un sistem (router sau gazdă) atunci când primește datagramele mai repede decât pot fi procesate acele datagrame. Rețineți expresia „poate fi”. Sistemul nu necesită suprimarea sursei de trimitere, chiar dacă tampoanele sunt pline și datagramele sunt aruncate.

Figura 11.18 arată formatul erorii de suprimare a sursei ICMP. Suntem într-o poziție ideală pentru a genera o eroare similară în rețeaua noastră de testare. Putem trimite datagrame de la bsdi la router sun prin Ethernet, iar aceste datagrame trebuie direcționate printr-un canal SLIP. Deoarece canalul SLIP este de aproximativ o mie de ori mai lent decât Ethernet, putem depăși cu ușurință tamponul. Următoarea comandă trimite datagrame de 100 de 1024 de octeți de la gazda bsdi prin router sun la solaris. Trimitem datagrame către serviciul standard de eliminare, unde vor fi ignorate:

>bsdi % sock -u -i -w1024 -n100 solaris arunca

Figura 11.18 Eroare de suprimare a sursei ICMP.

Figura 11.19 arată ieşirea tcpdump corespunzătoare acestei comenzi.

>

1 0.0 bsdi.1403 > solaris.discard: udp 1024
26 de linii nu sunt afișate
27 0.10 (0.00) bsdi.1403 > solaris.discard: udp 1024
28 0,11 (0,01) sun > bsdi: icmp: source quench

29 0.11 (0.00) bsdi.1403 > solaris.discard: udp 1024
30 0,11 (0,00) soare > bsdi: icmp: stingere sursă
142 de linii nu sunt afișate
173 0,71 (0,06) bsdi.1403 > solaris.discard: udp 1024
174 0,71 (0,00) soare > bsdi: icmp: stingere sursă

Figura 11.19 Suprimarea sursei ICMP de la soarele routerului.

Am eliminat multe linii din această ieșire. Primele 26 de datagrame au fost primite fără erori: am afișat doar ieșirea pentru prima. Începând de la a 27-a datagramă, de fiecare dată când este trimisă o datagramă primim o eroare de suprimare a sursei. Au fost 26+(74x2)=174 linii de ieșire în total.

Din calculul nostru de lățime de bandă a liniei seriale din capitolul 2, putem vedea că ar dura mai mult de o secundă pentru a transmite o datagramă de 1024 de octeți la 9600 bps. (În exemplul nostru, acest lucru va dura mai mult deoarece o datagramă de 20+8+1024 de octeți va fi fragmentată deoarece MTU-ul legăturii SLIP de la sun la netb este de 552 de octeți.) Cu toate acestea, din timpii prezentati în Figura 11.19, vedem acel router soare a primit toate cele 100 de datagrame în mai puțin de o secundă înainte ca prima să fie trimisă pe canalul SLIP. Este clar că am folosit multe dintre tampoanele sale.

Deși RFC 1009 necesită ca un router să genereze erori de suprimare a sursei atunci când tampoanele sale devin pline, noul RFC privind cerințele routerului [Almquist 1993] modifică acest lucru și spune că un router nu trebuie să genereze erori de suprimare a sursei.

Următorul punct de observat în exemplu este că programul sock nu a primit niciodată notificări că sursa a fost suprimată sau, dacă a făcut-o, le-a ignorat. Acest lucru sugerează că implementările BSD ignoră de obicei mesajele de suprimare a sursei primite atunci când se utilizează protocolul UDP. (În cazul TCP, atunci când se primește o notificare, transmisia de date pe o conexiune pentru care este generată suprimarea sursei este încetinită. Vom discuta acest lucru în secțiunea Capitolului 21.) Problema este că procesul care a generat datele care, la rândul său, a cauzat sursa de suprimare poate să se fi finalizat deja când mesajul de suprimare a sursei este primit. Într-adevăr, dacă folosim programul de timp Unix pentru a estima cât timp rulează programul sock, constatăm că a rulat doar aproximativ 0,5 secunde. Totuși, am văzut în Figura 11.19 că unele mesaje de suprimare a sursei au fost primite la 0,71 secunde după ce prima datagramă a fost trimisă, adică după ce procesul a încetat să ruleze. Ce se întâmplă dacă programul nostru produce 100 de datagrame și iese. Nu toate cele 100 de datagrame vor fi trimise - unele dintre ele vor fi în coada de ieșire.

Acest exemplu demonstrează că UDP este un protocol nesigur și arată importanța controlului fluxului. Chiar dacă programul de șosete a trimis cu succes 100 de datagrame în rețea, doar 26 au ajuns la destinație. Cele 74 rămase au fost cel mai probabil aruncate de routerul intermediar. Dacă aplicația nu acceptă o anumită formă de notificări, expeditorul nu știe dacă destinatarul a acceptat datele.

Server UDP

Există mai multe caracteristici ale utilizării UDP care afectează proiectarea și implementarea serverului. Proiectarea și implementarea clienților este de obicei mai ușoară decât implementarea serverelor. Acesta este motivul pentru care vom discuta aici despre dezvoltarea serverului, mai degrabă decât dezvoltarea clientului. Serverele interacționează de obicei cu sistemul de operare și majoritatea serverelor necesită să existe o modalitate de a gestiona cererile de la mai mulți clienți în același timp.

De obicei, atunci când un client pornește, stabilește imediat o conexiune la un server. Serverele, pe de altă parte, pornesc și apoi intră în somn în timp ce așteaptă să sosească o solicitare de la un client. În cazul UDP, serverul „se trezește” atunci când o datagramă sosește de la client, această datagramă poate conține o solicitare într-o anumită formă.

Nu vom lua în considerare aspectele de programare ale clienților și serverelor (el descrie totul mai detaliat), dar vom lua în considerare caracteristicile protocolului UDP care afectează proiectarea și implementarea unui server care utilizează UDP. (Vom discuta detaliile serverului TCP într-o secțiune din Capitolul 18.) Vom discuta câteva caracteristici în funcție de implementările UDP care vor fi utilizate și ne vom uita, de asemenea, la caracteristicile care sunt comune majorității implementărilor.

Adresa IP și numărul portului clientului

O datagramă UDP sosește de la client. Antetul IP conține adresele IP sursă și destinație, iar antetul UDP conține numerele portului UDP sursă și destinație. Când o aplicație primește o datagramă UDP, sistemul de operare trebuie să îi spună cine a trimis mesajul - adresa IP sursă și numărul portului.

Această caracteristică permite unui server UDP să gestioneze mai mulți clienți. Fiecare răspuns este trimis clientului care a trimis cererea.

Destinatia adresei IP

Unele aplicații trebuie să știe cui este destinată datagrama, adică adresa IP de destinație. De exemplu, RFC privind cerințele gazdei specifică faptul că un server TFTP trebuie să ignore datagramele primite care sunt trimise la o adresă de difuzare. (Vom descrie adresarea difuzării în , iar TFTP în .)

Aceasta înseamnă că sistemul de operare trebuie să transmită adresa IP de destinație din datagrama UDP primită către aplicație. Din păcate, nu toate implementările oferă această caracteristică.

API-ul sockets oferă această capacitate folosind opțiunea IP_RECVDSTADDR. Dintre sistemele acoperite în text, numai BSD/386, 4.4BSD și AIX 3.2.2 acceptă această opțiune. SVR4, SunOS 4.x și Solaris 2.x nu sunt acceptate.

Coada de intrare UDP

În secțiunea Capitolului 1, am spus că majoritatea serverelor UDP pot servi toate cererile către clienți folosind un singur port UDP (porturi de server precunoscute).

De obicei, dimensiunea cozii de intrare asociată fiecărui port UDP care este utilizat de aplicație este limitată. Aceasta înseamnă că cererile care sosesc în același timp de la clienți diferiți sunt puse automat într-o coadă UDP. Datagramele UDP primite sunt trimise aplicației (când solicită următoarea datagramă) în ordinea în care au fost primite.

Cu toate acestea, există posibilitatea, dacă coada este plină, ca modulul UDP din nucleu să elimine datagramele primite. Putem observa acest lucru cu următorul experiment. Începem programul nostru de șosete pe gazda bsdi, pornind astfel serverul UDP:

>

bsdi % ciorap -s -u -v -E -R256 -r256 -P30 6666
de la 140.252.13.33, la 140.252.13.63: 1111111111 de la soare la adresa de difuzare
de la 140.252.13.34, la 140.252.13.35: 4444444444444 de la svr4 la adresa personala

Am folosit următoarele steaguri: -s, rulează programul ca server, -u pentru UDP, -v, tipărește adresa IP a clientului și -E imprimă adresa IP de destinație (în acest caz sistemul o permite). În plus, am setat tamponul de primire UDP pentru acest port la 256 de octeți (-R), împreună cu dimensiunea care poate fi citită de fiecare aplicație (-r). Indicatorul -P30 vă spune să așteptați 30 de secunde după ștergerea portului UDP înainte de a citi prima datagramă. Acest lucru ne oferă timp să pornim clienții pe alte două gazde, să trimitem niște datagrame și să vedem cum funcționează coada de primire.

După ce serverul a pornit și a trecut o pauză de 30 de secunde, pornim un client pe soarele gazdă și trimitem trei datagrame:

>

soare % ciorap -u -v 140.252.13.63 6666 la adresa de difuzare Ethernet
conectat la 140.252.13.33.1252 la 140.252.13.63.6666
1111111111 11 octeți de date (cu linie nouă)
222222222 10 octeți de date (cu linie nouă)
33333333333 12 octeți de date (cu linie nouă)

Adresa de destinație este adresa de difuzare (140.252.13.63). Apoi am pornit un alt client pe gazda svr4 și am trimis încă trei datagrame:

>

svr4% ciorap -u -v bsdi 6666
conectat pe 0.0.0.0.1042 la 140.252.13.35.6666
4444444444444 14 octeți de date (cu linie nouă)
555555555555555 16 octeți de date (cu linie nouă)
66666666 9 octeți de date (cu linie nouă)

Primul lucru de observat în ieșirea interactivă afișată mai devreme pe bsdi este că numai două datagrame au fost primite de aplicație: prima de la sun, care a fost toate, și prima de la svr4, care a fost toate patru. Restul de patru datagrame au fost aruncate.

Ieșirea comenzii tcpdump din Figura 11.20 arată că toate cele șase datagrame au fost livrate gazdei de destinație. Datagramele au sosit de la doi clienți în ordine inversă: mai întâi de la soare, apoi de la svr4 și așa mai departe. Putem observa, de asemenea, că toate cele șase au fost livrate în aproximativ 12 secunde, în perioada de 30 de secunde în timp ce serverul dormea.

>

1 0.0 sun.1252 > 140.252.13.63.6666: udp 11
2 2.499184 (2.4992) svr4.1042 > bsdi.6666: udp 14
3 4.959166 (2.4600) sun.1252 > 140.252.13.63.6666: udp 10
4 7.607149 (2.6480) svr4.1042 > bsdi.6666: udp 16
5 10.079059 (2.4719) sun.1252 > 140.252.13.63.6666: udp 12
6 12.415943 (2.3369) svr4.1042 > bsdi.6666: udp 9

Figura 11.20 Ieșirea tcpdump pentru datagramele UDP trimise de doi clienți.

De asemenea, trebuie remarcat faptul că cu opțiunea -E, serverul poate afla adresa IP de destinație a fiecărei datagrame. Serverul poate alege ce să facă cu prima datagramă primită care a fost trimisă la adresa de difuzare.

În acest exemplu, este necesar să acordați atenție altor caracteristici. În primul rând, aplicația nu a raportat când coada de intrare a fost plină. Datagramele UDP suplimentare au fost pur și simplu aruncate. Vedem, de asemenea, în ieșirea tcpdump că nimic nu a fost trimis înapoi către client pentru a-l informa că datagramele au fost aruncate. Nu a fost trimis nimic asemănător cu un mesaj de suprimare a sursei ICMP, absolut nimic. În cele din urmă, dorim să subliniem că coada de intrare UDP operează pe o bază FIFO (primul intrat, primul ieşit), în timp ce, după cum am văzut în secţiunea acestui capitol, coada de intrare ARP operează pe un LIFO (ultimul în, primul ieşit). primul ieşit) bază.

Limitarea adresei IP locale

Majoritatea serverelor UDP folosesc metacaractere pentru adresele lor IP atunci când creează puncte finale UDP. Aceasta înseamnă că o datagramă UDP de intrare destinată portului serverului va fi acceptată de orice interfață locală. De exemplu, putem porni un server UDP pe portul 7777:

>soare % ciorap -u -s 7777

Vom folosi apoi comanda netstat pentru a vedea starea acestui punct final:

>

soare % netstat -a -n -f inet
Conexiuni la internet active (inclusiv servere)
Proto Recv-Q Send-Q Adresă locală Adresă străină (stat)
udp 0 0 *.7777 *.*

În această ieșire, am eliminat multe linii și am lăsat doar pe cele care ne sunt interesante. Indicatorul -a raportează toate punctele finale ale rețelei. Indicatorul -n tipărește adresele IP în notație zecimală în loc să utilizeze DNS și să convertească adresele în nume și tipărește numere de porturi în loc de nume de servicii. Opțiunea -f inet raportează numai punctele finale TCP și UDP.

Adresa locală este tipărită ca *.7777, unde asteriscul înseamnă că orice adresă poate fi înlocuită ca adresă IP locală.

Când serverul își creează punctul final, poate specifica una dintre adresele IP locale ale gazdei, inclusiv una dintre adresele sale de difuzare, ca adresă IP locală a punctului final. În acest caz, datagrama UDP de intrare va fi transmisă la punctul final numai dacă adresa IP de destinație se potrivește cu adresa locală specificată. Folosind programul sock, puteți specifica o adresă IP înainte de numărul portului, iar această adresă IP devine adresa IP locală pentru punctul final. De exemplu,

>soare % ciorap -u -s 140.252.1.29 7777

din datagramele care sosesc pe interfața SLIP, selectează datagramele cu adresa 140.252.1.29. Ieșirea comenzii netstat va arăta astfel:

>


udp 0 0 140.252.1.29.7777 *.*

Dacă încercăm să trimitem o datagramă către acest server de la gazda bsdi, a cărei adresă este 140.252.13.35, prin Ethernet, va fi returnată o eroare ICMP despre indisponibilitatea portului. Serverul nu va vedea niciodată această datagramă. Figura 11.21 arată acest lucru mai detaliat.

>

1 0.0 bsdi.1723 > sun.7777: udp 13
2 0,000822 (0,0008) sun > bsdi: icmp: sun udp port 7777 inaccessible

Figura 11.21 Eșec de procesare a datagramei UDP cauzată de o nepotrivire a adresei locale a serverului.

Este posibil să rulați alte servere pe același port, fiecare cu propria sa adresă IP locală. Cu toate acestea, aplicația trebuie să permită sistemului să refolosească același număr de port.

Opțiunea socket trebuie specificată în API-ul SO_REUSEADDR. Acest lucru este realizat de programul nostru de șosete folosind opțiunea -A.

Pe gazda sun putem porni cinci servere diferite pe același port UDP (8888):

>

soare % ciorap -u -s 140.252.1.29 8888 pentru canalul SLIP
soare % ciorap -u -s -A 140.252.13.33 8888 pentru Ethernet
soare % ciorap -u -s -A 127.0.0.1 8888 pentru interfață loopback
soare % ciorap -u -s -A 140.252.13.63 8888 pentru cererile de transmisie Ethernet
soare % ciorap -u -s -A 8888 pentru orice altceva (metacaractere în adresa IP)

Primul dintre servere era de așteptat să fie pornit cu indicatorul -A, care spune sistemului că același număr de port poate fi reutilizat. Ieșirea comenzii netstat arată următoarele cinci servere:

>

Proto Recv-Q Send-Q Adresă locală Adresă străină (stat)
udp 0 0 *.8888 *.*
udp 0 0 140.252.13.63.8888 *.*
udp 0 0 127.0.0.1 8888 *.*
udp 0 0 140.252.13.33 8888 *.*
udp 0 0 140.252.1.29 8888 *.*

În acest scenariu, numai datagramele destinate adresei 140.252.1.255 vor lovi serverul cu caractere wildcard folosite ca adresă IP locală, deoarece celelalte patru servere acoperă toate adresele posibile.

Când utilizați înlocuirea adresei IP, este utilizat un sistem de prioritate. Un punct final cu o adresă IP specificată care se potrivește cu adresa IP de destinație va fi întotdeauna ales înaintea unei adrese wildcard. Punctul final cu wildcard este utilizat numai atunci când nu se găsește nicio potrivire pentru adresa specificată.

Limitarea adreselor IP externe

În toate rezultatele comenzii netstat pe care le-am arătat mai devreme, adresele IP de la distanță și numerele de porturi la distanță sunt afișate ca *.*. Aceasta înseamnă că punctul final va accepta datagramele UDP primite de la orice adresă IP și orice număr de port. Majoritatea implementărilor permit punctelor finale UDP să restricționeze adresele de la distanță.

Cu alte cuvinte, punctul final poate accepta numai datagrame UDP de la adresa IP și numărul de port specificate. Programul nostru sock folosește opțiunea -f pentru a specifica adresa IP la distanță și numărul portului:

>soare % ciorap -u -s -f 140.252.13.35.4444 5555

În acest caz, adresa IP la distanță este setată la 140.252.13.35 (gazda noastră bsdi), iar numărul portului la distanță la 4444. Portul serverului precunoscut este 5555. Dacă rulăm netstat, vom vedea că adresa IP locală este de asemenea setat, chiar dacă nu l-am specificat direct:

>

Proto Recv-Q Send-Q Adresă locală Adresă străină (stat)
udp 0 0 140.252.13.33.5555 140.252.13.35.4444

Acest prin efect Specificarea unei adrese IP la distanță și a unui număr de port la distanță pe sistemele Berkeley: dacă nu a fost selectată o adresă IP locală la setarea adresei de la distanță, adresa locală este selectată automat. Adresa IP locală este setată la adresa IP a interfeței care este selectată utilizând rutarea IP pentru a ajunge la adresa IP de la distanță specificată. Într-adevăr, în acest exemplu, adresa IP a Sun pentru Ethernet-ul care este conectat la adresa de la distanță este 140.252.13.33.

Figura 11.22 prezintă cele trei tipuri de adrese și porturi pe care un server UDP le poate seta.

Figura 11.22 Specificarea adreselor IP locale și la distanță și a numărului de port pentru serverul UDP.

În toate cazurile, lport este portul precunoscut al serverului, iar localIP trebuie să fie adresa IP a interfeței locale. Ordinea în care cele trei linii sunt aranjate în Figura 11.22 arată ordinea în care modulul UDP încearcă să determine care punct final local a primit o datagramă. Cea mai restrictivă asociere adresa-port (prima linie) este selectată prima, iar cea mai puțin restrictivă (ultima linie, unde atât adresa IP, cât și numărul portului sunt specificate ca metacaractere) este selectată ultima.

Recepție multiplă per port

Deși nu sunt specificate în RFC, cele mai multe implementări permit ca o singură aplicație la un moment dat să fie asociată cu o adresă locală și un număr de port UDP. Când o datagramă UDP ajunge la gazda de destinație la adresa sa IP și numărul de port, o copie este livrată la un singur punct final. Adresa IP a punctului final poate fi reprezentată ca metacaractere, așa cum se arată mai devreme.

De exemplu, în SunOS 4.1.3 am pornit un server pe portul 9999 cu o adresă IP locală sub formă de wildcards:

>soare % ciorap -u -s 9999

Dacă încercăm apoi să pornim un alt server cu aceeași adresă wildcard locală și același port, nu va funcționa, chiar dacă specificăm opțiunea -A:

>

soare % ciorap -u -s 9999 nu ar trebui să se întâmple așa
nu se poate lega adresa locală: adresă deja utilizată

soare % ciorap -u -s -A 9999 de aceea am specificat steagul -A
nu se poate lega adresa locală: adresă deja utilizată

Pentru sistemele care acceptă multicast (vezi), lucrurile stau diferit. Mai multe puncte finale pot folosi aceeași adresă locală și aceeași număr de port UDP, dar aplicația trebuie să spună API-ului că acest lucru este permis (steagul -A utilizează opțiunea socket SO_REUSEADDR).

4.4BSD, care acceptă multicasting, necesită ca aplicația să seteze o opțiune de socket diferită (SO_REUSEPORT) pentru a permite mai multor puncte finale să partajeze același port. Mai mult, fiecare punct final trebuie să specifice această opțiune, inclusiv primul punct final care utilizează acest port.

Când o datagramă UDP ajunge la adresa sa IP de destinație, care este o adresă de difuzare sau multicast și există mai multe puncte finale asociate cu acea adresă IP și numărul de port, o copie a datagramei de intrare este trimisă la fiecare punct final. (Adresa IP locală a punctului final poate fi specificată ca wildcards și se va potrivi cu orice adresă IP de destinație.) Cu toate acestea, dacă datagrama IP care sosește are o adresă IP de destinație care este o adresă privată, doar o copie a datagramei este livrată la un punct final. Care punct final primește datagrama adresei personale este dependent de implementare.

Scurte concluzii

UDP este un protocol simplu. Specificația oficială RFC 768 [Postel 1980] are doar trei pagini. Serviciile pe care le oferă proceselor utilizatorilor de deasupra și din spatele IP sunt numere de porturi și sume de verificare opționale. Am folosit UDP pentru a revizui calculele sumelor de control și a vedea cum funcționează fragmentarea.

Apoi ne-am uitat la eroarea ICMP de neatins, care face parte din noua caracteristică de determinare a MTU de transport (vezi Capitolul 2, secțiunea ). Ne-am uitat la determinarea MTU de transport folosind Traceroute și UDP. Se discută și procesul de interacțiune dintre UDP și ARP.

Am verificat că o eroare de suprimare a sursei ICMP poate fi trimisă de un sistem care primește datagrame IP mai repede decât poate procesa. Este posibil să generați cu ușurință aceste erori ICMP folosind UDP.

Exerciții

  1. În secțiunea acestui capitol, am provocat fragmentarea în Ethernet prin scrierea unei datagrame UDP cu o dimensiune a datelor utilizator de 1473 de octeți. Care este cea mai mică dimensiune a datelor utilizator care poate provoca fragmentarea pe Ethernet dacă se utilizează încapsularea IEEE 802 (Capitolul 2, secțiunea)?
  2. Citiți RFC 791 [Postel 1981a] și spuneți-mi de ce toate fragmentele, cu excepția ultimului, trebuie să aibă o lungime de un multiplu de 8 octeți.
  3. Imaginați-vă o datagramă Ethernet și UDP cu 8192 de octeți de date utilizator. Câte fragmente vor fi transferate și care va fi lungimea offset-ului pentru fiecare fragment?
  4. Continuând cu exemplul anterior, imaginați-vă că aceste datagrame sunt apoi transferate pe un canal SLIP cu un MTU de 552. Trebuie să vă amintiți că cantitatea de date din fiecare fragment (totul cu excepția antetului IP) trebuie să fie un multiplu de 8 octeți. Câte fragmente sunt transmise și care este decalajul și lungimea fiecărui fragment?
  5. O aplicație care utilizează UDP trimite o datagramă care este fragmentată în 4 părți. Imaginați-vă că fragmentele 1 și 2 au ajuns la destinație, în timp ce fragmentele 3 și 4 s-au pierdut. Aplicația expiră și apoi, după 10 secunde, retransmite datagrama UDP. Această datagramă este fragmentată exact în același mod ca prima transmisie (același offset și aceeași lungime). Acum imaginați-vă că fragmentele 1 și 2 se pierd, dar fragmentele 3 și 4 ajung la destinație. Temporizatorul de reasamblare de pe gazda receptor este setat la 60 de secunde, astfel încât, atunci când fragmentele 3 și 4 au ajuns la destinația lor finală, fragmentele 1 și 2 din prima transmisie nu fuseseră încă aruncate. Poate destinatarul să asambla o datagramă IP din aceste patru fragmente?
  6. De unde puteți ști că fragmentele din Figura 11.15 corespund de fapt rândurilor 5 și 6 din Figura 11.14?
  7. ), rutarea surselor hard și loose (Capitolul 8, secțiunea). Cum credeți că sunt gestionate aceste opțiuni în timpul fragmentării? Comparați răspunsul dvs. cu RFC 791.
  8. Am spus că datagramele IP primite sunt demultiplexate pe baza numărului portului UDP de destinație. Este corect?

Internetul s-a schimbat de mult. Unul dintre principalele protocoale de internet, UDP este folosit de aplicații nu numai pentru a furniza datagrame și transmisii, ci și pentru a oferi conexiuni peer-to-peer între nodurile de rețea. Datorită designului său simplu, acest protocol are multe utilizări neplanificate anterior; cu toate acestea, deficiențele protocolului, cum ar fi lipsa livrării garantate, nu au dispărut. Acest articol descrie o implementare a Protocolului de livrare garantată prin UDP.

Introducere

Arhitectura originală a Internetului prevedea un spațiu de adrese uniform în care fiecare nod avea o adresă IP globală și unică și putea comunica direct cu alte noduri. Acum, Internetul, de fapt, are o arhitectură diferită - o zonă de adrese IP globale și multe zone cu adrese private ascunse în spatele dispozitivelor NAT. În această arhitectură, numai dispozitivele din spațiul global de adrese pot comunica cu ușurință cu oricine din rețea, deoarece au o adresă IP unică, rutabilă la nivel global. Un nod situat într-o rețea privată se poate conecta la alte noduri din aceeași rețea, precum și la alte noduri binecunoscute din spațiul global de adrese. Această interacțiune este realizată în mare parte datorită mecanismului de traducere a adresei de rețea. Dispozitivele NAT, cum ar fi routerele Wi-Fi, creează intrări speciale în tabelul de traducere pentru conexiunile de ieșire și modifică adresele IP și numerele de porturi în pachete. Acest lucru permite realizarea conexiunilor de ieșire dintr-o rețea privată la gazde în spațiul global de adrese. Dar, în același timp, dispozitivele NAT blochează de obicei tot traficul de intrare, cu excepția cazului în care sunt stabilite reguli separate pentru conexiunile de intrare.

Această arhitectură Internet este suficient de corectă pentru interacțiunea client-server, când clienții pot fi localizați în rețele private, iar serverele au o adresă globală. Dar creează dificultăți pentru conectarea directă a două noduri între ele variat rețele private. Conexiunea directă a două noduri este importantă pentru aplicațiile peer-to-peer, cum ar fi transmisia vocală (Skype), accesul la computer la distanță (TeamViewer) sau jocurile online.

Una dintre cele mai metode eficiente a stabili conexiuni peer-to-peer între dispozitive situate pe diferite rețele private se numește „hole punching”. Această tehnică este folosită cel mai adesea cu aplicații bazate pe protocolul UDP.

Dar dacă aplicația dvs. necesită livrarea garantată a datelor, de exemplu, transferați fișiere între computere, atunci utilizarea UDP va prezenta multe dificultăți datorită faptului că UDP nu este un protocol de livrare garantată și nu asigură livrarea pachetelor în ordine, spre deosebire de protocolul TCP.

În acest caz, pentru a asigura livrarea garantată a pachetelor, este necesar să se implementeze un protocol la nivel de aplicație care să ofere funcționalitatea necesară și să funcționeze pe deasupra UDP.

Aș dori să notez imediat că există o tehnică de perforare TCP pentru stabilirea conexiunilor TCP între noduri din diferite rețele private, dar din cauza lipsei de suport pentru aceasta de către multe dispozitive NAT, de obicei nu este considerată metoda principală de conectarea unor astfel de noduri.

Cerințe de protocol

  1. Livrare fiabilă a pachetelor implementată printr-un mecanism de feedback pozitiv (așa-numita confirmare pozitivă)
  2. Necesitatea unui transfer eficient de date mari, de ex. protocolul trebuie să evite retransmisiile inutile de pachete
  3. Ar trebui să fie posibilă anularea mecanismului de confirmare a livrării (abilitatea de a funcționa ca un protocol UDP „pur”)
  4. Posibilitatea implementării modului de comandă, cu confirmarea fiecărui mesaj
  5. Unitatea de bază a transmisiei de date conform protocolului trebuie să fie un mesaj
Aceste cerințe sunt în mare parte aceleași cu cerințele Reliable Data Protocol descrise în RFC 908 și RFC 1151 și am bazat dezvoltarea acestui protocol pe aceste standarde.

Pentru a înțelege aceste cerințe, să ne uităm la diagramele de timp ale transferului de date între două noduri de rețea folosind protocoalele TCP și UDP. Să pierdem un pachet în ambele cazuri.

Transmiterea datelor non-interactive prin TCP:


După cum puteți vedea din diagramă, în caz de pierdere a pachetului, TCP va detecta pachetul pierdut și îl va raporta expeditorului solicitând numărul de segment pierdut.

Transfer de date prin protocolul UDP:



UDP nu ia nicio măsură pentru a detecta pierderile. Controlul erorilor de transmisie în protocolul UDP este în întregime responsabilitatea aplicației.

Detectarea erorilor în protocolul TCP se realizează prin stabilirea unei conexiuni cu un nod final, stocarea stării acestei conexiuni, indicarea numărului de octeți trimiși în fiecare antet de pachet și notificarea primirilor folosind un număr de confirmare.

În plus, pentru a îmbunătăți performanța (adică, trimiterea a mai mult de un segment fără a primi o confirmare), protocolul TCP utilizează o așa-numită fereastră de transmisie - numărul de octeți de date pe care expeditorul segmentului se așteaptă să îi primească.

Mai multe detalii despre protocolul TCP pot fi găsite în rfc 793, cu UDP în rfc 768, unde, de fapt, sunt definite.

Din cele de mai sus, este clar că pentru a crea un protocol de livrare de mesaje fiabil prin UDP (în continuare vom apela UDP de încredere), este necesar să se implementeze mecanisme de transfer de date similare cu TCP. Și anume:

  • salvați starea conexiunii
  • utilizați numerotarea segmentelor
  • utilizați pachete speciale de confirmare
  • utilizați un mecanism de fereastră simplificat pentru a crește debitul protocolului
În plus, este necesar:
  • semnalează începutul unui mesaj pentru a aloca resurse pentru conexiune
  • semnalează sfârșitul unui mesaj, pentru a transmite mesajul primit unei aplicații din amonte și pentru a elibera resurse de protocol
  • permite unui protocol specific conexiunii să dezactiveze mecanismul de confirmare a livrării să funcționeze ca UDP „pur”

Antet UDP de încredere

Amintiți-vă că o datagramă UDP este încapsulată într-o datagramă IP. Pachetul Reliable UDP este în consecință „înfășurat” într-o datagramă UDP.

Încapsulare fiabilă antet UDP:



Structura antetului UDP de încredere este destul de simplă:

  • Steaguri – steaguri de control al pachetului
  • MessageType – tip de mesaj, utilizat de aplicațiile din amonte pentru a se abona la anumite mesaje
  • TransmissionId - numărul de transmisie, împreună cu adresa și portul destinatarului, identifică în mod unic conexiunea
  • PacketNumber – numărul pachetului
  • Opțiuni – opțiuni suplimentare de protocol. În cazul primului pachet, acesta este folosit pentru a indica dimensiunea mesajului
Steagurile sunt după cum urmează:
  • FirstPacket - primul pachet al mesajului
  • NoAsk - mesajul nu necesită activarea mecanismului de confirmare
  • LastPacket - ultimul pachet de mesaje
  • RequestForPacket - pachet de confirmare sau cerere pentru un pachet pierdut

Principii generale ale protocolului

Deoarece Reliable UDP se concentrează pe garantarea transmiterii unui mesaj între două noduri, trebuie să poată stabili o conexiune cu cealaltă parte. Pentru a stabili o conexiune, partea care trimite trimite un pachet cu steag FirstPacket, răspunsul la care va însemna că conexiunea a fost stabilită. Toate pachetele de răspuns sau, cu alte cuvinte, pachetele de confirmare, stabilesc întotdeauna valoarea câmpului PacketNumber la o valoare mai mare decât cea mai mare valoare PacketNumber a pachetelor sosite cu succes. Câmpul Opțiuni pentru primul pachet trimis înregistrează dimensiunea mesajului.

Un mecanism similar este utilizat pentru a finaliza conexiunea. Flagul LastPacket este setat în ultimul pachet de mesaje. Pachetul de răspuns indică numărul ultimului pachet + 1, ceea ce pentru partea de primire înseamnă livrarea cu succes a mesajului.

Diagrama stabilirii si terminarii conexiunii:



Când conexiunea este stabilită, începe transferul de date. Datele sunt transmise în blocuri de pachete. Fiecare bloc, cu excepția ultimului, conține un număr fix de pachete. Este egală cu dimensiunea ferestrei de primire/transmitere. Ultimul bloc de date poate avea mai puține pachete. După trimiterea fiecărui bloc, partea expeditoare așteaptă confirmarea livrării sau o cerere de relivrare a pachetelor pierdute, lăsând deschisă fereastra de primire/transmitere pentru a primi răspunsuri. După primirea confirmării livrării blocului, fereastra de recepție/transmisie se schimbă și următorul bloc de date este trimis.

Partea de primire primește pachetele. Fiecare pachet este verificat pentru a vedea dacă se încadrează în fereastra de transmisie. Pachetele și duplicatele care nu intră în fereastră sunt eliminate. Deoarece Deoarece dimensiunea ferestrei este fixă ​​și aceeași pentru destinatar și expeditor, atunci, în cazul livrării unui bloc de pachete fără pierderi, fereastra este deplasată pentru a primi pachete din următorul bloc de date și se trimite o confirmare de livrare. . Dacă fereastra nu se umple în perioada stabilită de cronometrul de lucru, se va lansa o verificare pentru a vedea ce pachete nu au fost livrate și vor fi trimise cereri de relivrare.

Diagrama de retransmisie:


Timeout-uri și cronometre ale protocolului

Există mai multe motive pentru care o conexiune nu poate fi stabilită. De exemplu, dacă partea care primește este offline. În acest caz, când încercați să stabiliți o conexiune, conexiunea va fi închisă din cauza unui timeout. Implementarea Reliable UDP folosește două temporizatoare pentru a seta timeout-uri. Primul, temporizatorul de lucru, este folosit pentru a aștepta un răspuns de la gazda la distanță. Dacă este declanșat din partea care trimite, atunci ultimul pachet trimis este retrimis. Dacă temporizatorul este declanșat la destinatar, atunci se efectuează o verificare pentru pachetele pierdute și se trimit cereri de re-livrare.

Al doilea cronometru este necesar pentru a închide conexiunea dacă nu există nicio comunicare între noduri. Pentru partea de trimitere, începe imediat după expirarea temporizatorului de lucru și așteaptă un răspuns de la nodul de la distanță. Dacă nu există niciun răspuns în perioada specificată, conexiunea este întreruptă și resursele sunt eliberate. Pentru partea destinatarului, temporizatorul de închidere a conexiunii începe după ce temporizatorul de lucru se declanșează de două ori. Acest lucru este necesar pentru a vă asigura împotriva pierderii pachetului de confirmare. Când temporizatorul se declanșează, conexiunea este, de asemenea, întreruptă și resursele sunt eliberate.

Diagrama stării transmisiei UDP de încredere

Principiile de funcționare ale protocolului sunt implementate într-o mașină cu stări finite, fiecare stare a cărei stare este responsabilă pentru o logică specifică de procesare a pachetelor.
Diagrama de stare UDP fiabilă:

Închis– nu este de fapt o stare, este punctul de pornire și de sfârșit pentru mașină. Pentru stat Închis este primit un bloc de control al transmisiei care, implementând un server UDP asincron, redirecționează pachetele către conexiunile corespunzătoare și începe procesarea stării.

PrimulPachetTrimitere– starea inițială în care se află conexiunea de ieșire la trimiterea unui mesaj.

În această stare, este trimis primul pachet pentru mesajele obișnuite. Pentru mesajele fără confirmarea trimiterii, aceasta este singura stare - întregul mesaj este trimis în ea.

SendingCycle– stare de bază pentru transmiterea pachetelor de mesaje.

Trecerea la ea de la stat PrimulPachetTrimitere efectuată după trimiterea primului pachet de mesaje. În această stare vin toate confirmările și cererile de retransmisii. Ieșirea din acesta este posibilă în două cazuri - în cazul livrării cu succes a mesajului sau din cauza unui timeout.

Primul pachet primit– starea inițială pentru destinatarul mesajului.

Verifică corectitudinea începerii transmisiei, creează structurile necesare și trimite o confirmare de primire a primului pachet.

Pentru un mesaj format dintr-un singur pachet și trimis fără a utiliza o confirmare de livrare, aceasta este singura stare. După procesarea unui astfel de mesaj, conexiunea este închisă.

Asamblare– stare de bază pentru primirea pachetelor de mesaje.

Acesta scrie pachete în stocarea temporară, verifică dacă există pierderi de pachete, trimite confirmări de livrare a unui bloc de pachete și a întregului mesaj și trimite cereri de re-livrare a pachetelor pierdute. Dacă întregul mesaj este primit cu succes, conexiunea intră în Efectuat, în caz contrar, apare o ieșire de timeout.

Efectuat– închiderea conexiunii dacă întregul mesaj este primit cu succes.

Această stare este necesară pentru asamblarea mesajului și pentru cazul în care confirmarea livrării mesajului a fost pierdută în drumul către expeditor. Această stare este ieșită de un timeout, dar conexiunea este considerată închisă cu succes.

Mai adânc în cod. Unitate de control transmisie

Unul dintre elementele cheie ale Reliable UDP este blocul de control al transmisiei. Scopul acestui bloc este de a stoca conexiunile curente și elementele auxiliare, de a distribui pachetele primite la conexiunile corespunzătoare, de a oferi o interfață pentru trimiterea de pachete la conexiune și de a implementa protocolul API. Blocul de control al transmisiei primește pachete de la nivelul UDP și le redirecționează către mașina de stare pentru procesare. Pentru a primi pachete, implementează un server UDP asincron.

Unii membri ai clasei ReliableUdpConnectionControlBlock:

clasa internă ReliableUdpConnectionControlBlock: IDisposable ( // matrice de octeți pentru cheia specificată. Folosit pentru a asambla mesajele primite public ConcurrentDictionary , byte> IncomingStreams (get; private set;) // matrice de octeți pentru cheia specificată. Folosit pentru a trimite mesaje de ieșire. public ConcurrentDictionary , byte> OutcomingStreams ( get; private set; ) // înregistrarea conexiunii pentru cheia specificată. private readonly ConcurrentDictionary , ReliableUdpConnectionRecord> m_listOfHandlers; // lista abonaților la mesaje. listă privată numai pentru citire m_abonați; // socket local privat Socket m_socketIn; // port pentru mesajele primite private int m_port; // adresa IP locală Adresă IP privată m_ipAddress; // punct final local IPEndPoint public LocalEndpoint ( get; set privat; ) // colecție de // stări pre-inițializate ale mașinii de stat public StatesCollection States ( get; set privat; ) // generator de numere aleatorii. Folosit pentru a crea TransmissionId private readonly RNGCryptoServiceProvider m_randomCrypto; //... )


Implementarea unui server UDP asincron:

private void Receive() ( EndPoint connectedClient = nou IPEndPoint(IPAddress.Any, 0); // creează un nou buffer pentru fiecare socket.BeginReceiveFrom byte buffer = octet nou; // trece tamponul ca parametru metodei asincrone aceasta. m_socketIn. BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer); ) private void EndReceive(IAsyncResult ar) ( EndPoint connectedClient = nou IPEndPoint(IPAddress =.int bytes, Citire0); .m_socketIn .EndReceiveFrom(ar, ref connectedClient); //pachet primit, gata să primească următoarea Receive(); // deoarece cel mai simplu mod rezolvați problema cu bufferul - obțineți un link către acesta // de la IAsyncResult.AsyncState byte bytes = ((byte) ar.AsyncState).Slice(0, bytesRead); // obțineți antetul pachetului antet ReliableUdpHeader; if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header)) ( // a sosit un pachet incorect - aruncați-l înapoi; ) // construiți o cheie pentru a determina înregistrarea conexiunii pentru pachetul Tuple cheie = tuplu nou (connectedClient, header.TransmissionId); // obțineți o înregistrare de conexiune existentă sau creați una nouă înregistrare ReliableUdpConnectionRecord = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType)); // lansează pachetul pentru procesare în mașina de stări record.State.ReceivePacket(record, header, bytes); )


Pentru fiecare transmisie de mesaj, este creată o structură care conține informații despre conexiune. Această structură se numește înregistrarea conexiunii.

Unii membri ai clasei ReliableUdpConnectionRecord:

clasa internă ReliableUdpConnectionRecord: IDisposable ( // matrice de octeți cu mesajul octet public IncomingStream ( get; set; ) // referință la starea mașinii cu stări finite public ReliableUdpState State ( get; set; ) // o pereche care identifică în mod unic înregistrarea conexiunii // în blocul de control transferând Tuple public Cheie (get; private set;) // limita inferioară a ferestrei de primire public int WindowLowerBound; // transfer dimensiunea ferestrei public readonly int WindowSize; // numărul pachetului de trimis public int SndNext; // numărul de pachete de trimis public int NumberOfPackets; // numărul de transmisie (aceasta este a doua parte a tuplului) // fiecare mesaj are propriul său public, numai citire, Int32 TransmissionId; // punct final IP la distanță – destinatarul real al mesajului public numai pentru citire IPEndPoint RemoteClient; // dimensiunea pachetului, pentru a evita fragmentarea la nivel IP // nu trebuie să depășească MTU – (IP.Header + UDP.Header + RelaibleUDP.Header) public readonly int BufferSize; // bloc de control al transmisiei public readonly ReliableUdpConnectionControlBlock Tcb; // încapsulează rezultatele unei operații asincrone pentru BeginSendMessage/EndSendMessage public readonly AsyncResultSendMessage AsyncResult; // nu trimite pachete de confirmare public bool IsNoAnswerNeeded; // ultimul pachet primit corect (setat întotdeauna la cel mai mare număr) public int RcvCurrent; // matrice cu numărul de pachete pierdute public int LostPackets ( get; private set; ) // dacă ultimul pachet a sosit. Folosit ca bool. public int IsLastPacketReceived = 0; //... )

Mai adânc în cod. state

Statele implementează mașina de stări a protocolului Reliable UDP, în care are loc procesarea principală a pachetelor. Clasa abstractă ReliableUdpState oferă o interfață pentru stare:

Întreaga logică a protocolului este implementată de clasele prezentate mai sus, împreună cu o clasă auxiliară care oferă metode statice, cum ar fi, de exemplu, construirea antetului ReliableUdp din înregistrarea conexiunii.

Metoda DisposeByTimeout

Metoda DisposeByTimeout este responsabilă pentru eliberarea resurselor de conexiune atunci când expiră un timeout și pentru semnalarea reușită/eșecul livrării mesajului.

ReliableUdpState.DisposeByTimeout:

vid virtual protejat DisposeByTimeout(înregistrare obiect) ( ReliableUdpConnectionRecord connectionRecord = înregistrare (ReliableUdpConnectionRecord); dacă (record.AsyncResult != null) ( connectionRecord.AsyncResult.SetAsCompleted(false); ) connectionRecord).Dispose();


Este anulat doar în stat Efectuat.

Completed.DisposeByTimeout:

protected override void DisposeByTimeout(înregistrare obiect) ( ReliableUdpConnectionRecord connectionRecord = înregistrare (ReliableUdpConnectionRecord); // raportează primirea cu succes a mesajului SetAsCompleted(connectionRecord); )

Metoda ProcessPackets

Metoda ProcessPackets este responsabilă pentru procesarea suplimentară a pachetului sau pachetelor. Apelat direct sau printr-un temporizator de așteptare de pachete.

Capabil Asamblare metoda este suprascrisă și este responsabilă pentru verificarea pachetelor pierdute și trecerea la stare Efectuat, în cazul primirii ultimului pachet și trecerii cu succes a verificării

Asamblare.Pachete de proces:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( if (connectionRecord.IsDone != 0) return; if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0)) (pachete pentru trimitere, sunt pierderi de solicitare pentru ele, //se pierde ( int seqNum în connectionRecord.LostPackets) ( if (seqNum != 0) ( ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum); ) ) ) // setați cronometrul a doua oară pentru a reîncerca transferul dacă (!connectionRecord.TimerSecondTry.) WaitForPacketsTimer .Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; ) // dacă după două încercări de declanșare a WaitForPacketTimer // nu a fost posibilă primirea pachetelor, porniți cronometrul de finalizare a conexiunii StartCloseWaitTimer(connectionRecord); ) else if (connectionRecord. IsLastPacketReceived != 0) // verificare cu succes ( // trimite confirmarea primirii blocului de date ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.State = connectionRecord.Tcb.States.Completed; connectionRecord.State.ProcessPackets(connectionRecord); // în loc de implementarea instantanee a resurselor // pornim un cronometru în cazul în care // ultimul ack nu ajunge la expeditor și acesta îl solicită din nou. // când se declanșează temporizatorul - implementăm resurse // în starea Completed, metoda timer-ului este suprascrisă StartCloseWaitTimer(connectionRecord); ) // acesta este cazul când confirmarea unui bloc de pachete a fost pierdut altfel ( if (!connectionRecord.TimerSecondTry) ( ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimer); connectionRecord.ShortTimer); TimerSecondTry = true; return; ) // pornește temporizatorul de finalizare a conexiunii StartCloseWaitTimer(connectionRecord); ) )


Capabil SendingCycle această metodă este apelată numai pe un cronometru și este responsabilă pentru retrimiterea ultimului mesaj, precum și pentru pornirea temporizatorului de închidere a conexiunii.

SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( dacă (connectionRecord.IsDone != 0) return; // retrimite ultimul pachet // (dacă conexiunea este restabilită, nodul receptor va retrimite cererile care nu au ajuns la el) ReliableUdpStateTools.SendPacket ( connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1)); // activați cronometrul CloseWait - pentru a aștepta ca conexiunea să fie restaurată sau finalizarea acesteia StartCloseWaitTimer(connectionRecord); )


Capabil Efectuat Metoda oprește cronometrul de rulare și transmite un mesaj către abonați.

Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( if (connectionRecord.WaitForPacketsTimer != null) connectionRecord.WaitForPacketsTimer.Dispose(); // colectează mesajul și trimite-l abonaților ReliableUdpStateTools.CreateMessage;(connectionMecordry.CreateMessage);

Metoda ReceivePacket

Capabil Primul pachet primit Sarcina principală a metodei este de a determina dacă primul pachet de mesaj a ajuns de fapt pe interfață și, de asemenea, de a asambla un mesaj format dintr-un singur pachet.

FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, încărcare utilă de octeți) ( if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))) // aruncați pachetul returnat; // - combinația de pachete returnate și Ultimul pachet - spune primul pachet și ultimul pachet. avem singurul mesaj dacă (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) & header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) ( ReliableUdpStateTools.CreatePacket. der.Lungime, sarcină utilă. Lungime) ); if ( !header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk)) ( // trimite un pachet de confirmare ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); ) SetAsCompleted(connectionRecord) //; întoarce de la toate numărul de pachete; if (header.PacketNumber != 0) return; ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, antet); ReliableUdpStateTools.WritePacketData(connectionRecord, antet, sarcină utilă); // numără numărul de pachete care ar trebui să ajungă connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize)); // înregistrează numărul ultimului pachet primit (0) connectionRecord.RcvCurrent = header.PacketNumber; // după aceea am mutat fereastra de primire cu 1 connectionRecord.WindowLowerBound++; // comută starea connectionRecord.State = connectionRecord.Tcb.States.Assembling; // dacă nu este necesar un mecanism de confirmare // pornește un temporizator care va elibera toate structurile dacă (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk)) ( connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1TimerPeriod) ); ) else ( ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer = timer nou (CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); ) )


Capabil SendingCycle această metodă este înlocuită pentru a accepta confirmările de livrare și cererile de retransmisie.

SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, încărcare utilă octet) ( if (connectionRecord.IsDone != 0) return; if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.Flags.ReliableUdpHeaderFlags.Request) returnează calcularea finală a ferestrei/borderului; // chenarul ferestrei + 1 este luat pentru a primi confirmările de livrare int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets)); // verificați dacă ați accesat fereastra dacă (header.PacketNumber< connectionRecord.WindowLowerBound || header.PacketNumber >windowHighestBound) return; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // verificați ultimul pachet: if (header.PacketNumber == connectionRecord.NumberOfPackets) ( // transmisie finalizată Interlocked.Increment(ref connectionRecord.IsDone); SetAsCompleted(connectionRecord); return; ) // acesta este răspunsul la primul pachet cu confirmare dacă ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1)) ( // fără schimbare de fereastră SendPacket(connectionRecord); ) // confirmarea primirii blocului de date a sosit altfel dacă (header.PacketNumber == windowHighestBound) ( // deplasează fereastra de primire/transmitere connectionRecord.WindowLowerBound += connectionRecord.WindowSize; // resetează matricea de control al transmisiei connectionRecord.WindowControlArray.Nullify(); // trimite un bloc de pachete SendPacket( connectionRecord); ) // aceasta este o cerere de repetare a transmisiei - trimiteți pachetul necesar altfel ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber)); )


Capabil Asamblareîn metoda ReceivePacket, are loc principala activitate de asamblare a mesajului din pachetele primite.

Asamblare.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, byte payload) ( if (connectionRecord.IsDone != 0) return; // procesarea pachetelor cu mecanismul de confirmare a livrării dezactivat if (header.Flags.HasFlags.HasFlags) // resetați cronometrul connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1); // scrieți datele ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); // dacă am primit un pachet cu ultimul steag - faceți-o, completează dacă (header.Flags.HasFlag (ReliableUdpHeaderFlags.LastPacket)) ( connectionRecord.State = connectionRecord.Tcb.States.Completed; connectionRecord.State.ProcessPackets(connectionRecord); ) return; ) // calculul celei mai mari ferestre Bound int = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1)); // aruncați pachetele care nu intră în fereastră dacă (header.PacketNumber< connectionRecord.WindowLowerBound || header.PacketNumber >(windowHighestBound)) return; // eliminați duplicatele dacă (connectionRecord.WindowControlArray.Contains(header.PacketNumber)) return; // scrieți datele ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload); //creșterea contorului de pachete connectionRecord.PacketCounter++; // scrie numărul curent al pachetului în matricea de control al ferestrei connectionRecord.WindowControlArray = header.PacketNumber; // setați cel mai mare pachet de intrare dacă (header.PacketNumber > connectionRecord.RcvCurrent) connectionRecord.RcvCurrent = header.PacketNumber; // reporniți temporizatoarele connectionRecord.TimerSecondTry = false; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // dacă ultimul pachet a sosit dacă (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket)) ( Interlocked.Increment(ref connectionRecord.IsLastPacketReceived); ) // dacă am primit toate pachetele ferestrei, apoi resetați contorul // și trimiteți un pachet de confirmare altfel dacă (connectionRecord.PacketCounter == connectionRecord.WindowSize) ( // resetați contorul. connectionRecord.PacketCounter = 0; // a mutat fereastra de transmisie connectionRecord.WindowLowerBound += connectionRecord.WindowSize; // resetați transmisia control array connectionRecord.WindowControlArray.Nullify() ; ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); ) // dacă ultimul pachet este deja disponibil dacă (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) (ref. connectionRecord.IsLastPacketReceived) != 0 pachete de conectare(PacheteRecord) //verificarea procesării ); ))


Capabil Efectuat Singura sarcină a metodei este de a trimite o confirmare repetată a livrării cu succes a mesajului.

Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, byte payload) ( // retrimiterea ultimului pachet din cauza // faptului că ultima confirmare nu a ajuns la expeditor dacă (header.Flags.HasFlag(ReliableUdpHeaderFlags.Lader) ) ( ReliableUdpStateTools .SendAcknowledgePacket(connectionRecord); ) )

Metoda SendPacket

Capabil PrimulPachetTrimitere Această metodă trimite primul pachet de date sau, dacă mesajul nu necesită confirmare de livrare, întregul mesaj.

FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) ( connectionRecord.PacketCounter = 0; connectionRecord.SndNext = 0; connectionRecord.WindowLowerBound = 0; // dacă nu este necesară confirmarea, trimiteți toate pachetele // și eliberați resurse dacă (connectionRecord.IsedNoAns) // Aici trimiterea are loc așa cum se întâmplă ( ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools. CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord))); connectionRecord.SndtNex.< connectionRecord.NumberOfPackets); SetAsCompleted(connectionRecord); return; } // создаем заголовок пакета и отправляем его ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord); ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header)); // увеличиваем счетчик connectionRecord.SndNext++; // сдвигаем окно connectionRecord.WindowLowerBound++; connectionRecord.State = connectionRecord.Tcb.States.SendingCycle; // Запускаем таймер connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); }


Capabil SendingCycleîn această metodă, este trimis un bloc de pachete.

SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) ( // trimite un bloc de pachete pentru (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++; } // на случай большого окна передачи, перезапускаем таймер после отправки connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) { connectionRecord.CloseWaitTimer.Change(-1, -1); } }

Mai adânc în cod. Crearea și stabilirea conexiunilor

Acum că suntem familiarizați cu stările și metodele de bază utilizate pentru procesarea stărilor, putem privi mai detaliat câteva exemple de funcționare a protocolului.

Diagrama transferului de date în condiții normale:



Să aruncăm o privire mai atentă asupra creației înregistrarea conexiunii pentru a vă conecta și a trimite primul pachet. Transferul este întotdeauna inițiat de aplicația care apelează metoda API pentru trimiterea mesajului. În continuare, este utilizată metoda StartTransmission a blocului de control al transmisiei, începând transmisia de date pentru noul mesaj.

Crearea unei conexiuni de ieșire:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult) ( if (m_isListenerStarted == 0) ( if (this.LocalEndpoint == null) (aruncă un nou ArgumentNullException("", trebuie să folosești un parametrul de pornire sau „auditor” înainte de a începe trimiterea mesajului"); ) // începe procesarea pachetelor primite StartListener(LocalEndpoint); ) // creează o cheie pentru dicționar bazată pe EndPoint și ReliableUdpHeader.TransmissionId byte transmissionId = octet nou; // creează un număr aleatoriu transmissionId m_randomCrypto.GetBytes( transmisieId); Tuplu cheie = tuplu nou (endPoint, BitConverter.ToInt32(transmissionId, 0)); // creăm o înregistrare nouă pentru conexiune și verificăm // dacă un astfel de număr există deja în dicționarele noastre dacă (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult))) ( // dacă există , apoi repetați generați un număr aleatoriu m_randomCrypto.GetBytes(transmissionId); key = new Tuple (endPoint, BitConverter.ToInt32(transmissionId, 0)); if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))) // dacă nu reușește din nou, aruncați o excepție throw new ArgumentException(„Perechea ID de transmisie și punctul final există deja în dicționar”); ) // a început starea de procesare a m_listOfHandlers.State.SendPacket(m_listOfHandlers); )


Trimiterea primului pachet (starea FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) ( connectionRecord.PacketCounter = 0; connectionRecord.SndNext = 0; connectionRecord.WindowLowerBound = 0; // ... // creați antetul pachetului și trimiteți-l ReliableUdpHeader header = ReliableToolRecorder. ); ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, antet)); // mărește contorul connectionRecord.SndNext++; // deplasează fereastra connectionRecord.WindowLowerBound++; // trece la starea conexiunii la înregistrarea trimiterii. .States.SendingCycle; // Porniți cronometrul connectionRecord.WaitForPacketsTimer = timer nou (CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); )


După trimiterea primului pachet, expeditorul intră în stare SendingCycle– așteptați confirmarea livrării coletului.
Partea de primire, folosind metoda EndReceive, acceptă pachetul trimis și creează un nou înregistrarea conexiuniiși transmite acest pachet, cu un antet analizat în prealabil, metodei de procesare ReceivePacket de stat Primul pachet primit

Crearea unei conexiuni pe partea de recepție:

private void EndReceive(IAsyncResult ar) ( // ... // pachetul primit // analiza antetul pachetului antetul ReliableUdpHeader; if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header)) ( // a sosit un pachet incorect - aruncați-l înapoi ; ) // construiește o cheie pentru a determina înregistrarea conexiunii pentru pachetul Tuple cheie = tuplu nou (connectedClient, header.TransmissionId); // obțineți o înregistrare de conexiune existentă sau creați una nouă înregistrare ReliableUdpConnectionRecord = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType)); // lansează pachetul pentru procesare în mașina de stări record.State.ReceivePacket(record, header, bytes); )


Primirea primului pachet și trimiterea unei confirmări (starea FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, încărcare utilă de octeți) ( dacă (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket)) // renunțați la returnarea pachetului prin proiectare; // ... // 0 ; if (header.PacketNumber != 0) return; // inițializați o matrice pentru a stoca părți ale mesajului ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header); // scrieți pachetul de date în tabloul ReliableUdpStateTools.WritePacketData,header, sarcină utilă); // numără numărul de pachete care ar trebui să ajungă connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize)); // înregistrează numărul a ultimului pachet primit (0 ) connectionRecord.RcvCurrent = header.PacketNumber; // după aceea fereastra de recepție este deplasată cu 1 connectionRecord.WindowLowerBound++; // comută starea connectionRecord.State = connectionRecord.Tcb.States.Assembling; if (/*dacă nu este necesar mecanismul de confirmare*/) // ... else ( // trimite confirmarea ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); connectionRecord.WaitForPacketsTimer = timer nou (CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1); ) )

Mai adânc în cod. Închiderea unei conexiuni din cauza expirării timpului

Gestionarea timeout-urilor este o parte importantă a Reliable UDP. Luați în considerare un exemplu în care un nod intermediar eșuează și livrarea datelor în ambele direcții devine imposibilă.

Diagrama închiderii unei conexiuni prin expirare:



După cum se poate observa din diagramă, temporizatorul de lucru al expeditorului începe imediat după trimiterea unui bloc de pachete. Acest lucru se întâmplă în metoda SendPacket a statului SendingCycle.

Activarea temporizatorului de lucru (starea SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) ( // trimite un bloc de pachete // ... // repornește cronometrul după trimiterea connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != nullaWaitTimer != ) connectionRecord. CloseWaitTimer.Change(-1, -1); )


Perioadele temporizatorului sunt setate la crearea conexiunii. În mod implicit, ShortTimerPeriod este de 5 secunde. În exemplu, acesta este setat la 1,5 secunde.

Pentru o conexiune de intrare, cronometrul pornește după primirea ultimului pachet de date primit; acest lucru se întâmplă în metoda ReceivePacket a statului Asamblare

Activarea temporizatorului de lucru (starea de asamblare):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, byte payload) ( // ... // reporniți temporizatoarele connectionRecord.TimerSecondTry = fals; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimer.C; = null) connectionRecord.CloseWaitTimer.Change(-1, -1); // ... )


Conexiunea de intrare nu a primit mai multe pachete în timpul de așteptare pentru cronometrul de lucru. Cronometrul s-a declanșat și a apelat metoda ProcessPackets, care a detectat pachetele pierdute și a trimis cereri de relivrare pentru prima dată.

Trimiterea cererilor de relivrare (starea asamblare):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( // ... dacă (/*verifică pachetele pierdute */) ( // trimite cereri de re-livrare // setează cronometrul a doua oară pentru a reîncerca transmisia dacă (!connectionRecord. TimerSecondTry) ( connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; ) // dacă după două încercări de a declanșa WaitForPacketTimer // nu a fost posibil să primiți pachete, porniți temporizatorul de finalizare a conexiunii StartCloseWaitTimer(connectionRecord); ) else if (/*ultimul pachet a sosit și verificarea cu succes */) ( // ... StartCloseWaitTimer(connectionRecord); ) // dacă ack-ul către blocul de pachete a fost pierdut altfel ( dacă (! connectionRecord.TimerSecondTry) ( // retrimite ack connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); connectionRecord.TimerSecondTry = true; return; ) // pornește cronometrul de finalizare a conexiunii StartCloseWaitTimer(connectionRecord); ) )


Variabila TimerSecondTry este setată la Adevărat. Această variabilă este responsabilă pentru repornirea temporizatorului de lucru.

Din partea expeditorului, este declanșat și un temporizator de lucru și ultimul pachet trimis este retrimis.

Activarea temporizatorului de închidere a conexiunii (starea SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( // ... // retrimite ultimul pachet // ... // activați cronometrul CloseWait - pentru a aștepta ca conexiunea să fie restaurată sau finalizarea acesteia StartCloseWaitTimer(connectionRecord); )


După aceea, temporizatorul de închidere a conexiunii este pornit în conexiunea de ieșire.

ReliableUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord) ( dacă (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1); else connectionRecord.CloseWaitTimer = noua conexiuneRecord.CloseWaitTimer, connectionRecord. ;)


Perioada de expirare implicită pentru temporizatorul de închidere a conexiunii este de 30 de secunde.

După un timp scurt, temporizatorul de lucru pe partea de recepție este declanșat din nou, cererile sunt trimise din nou, după care este pornit temporizatorul de închidere a conexiunii pentru conexiunea de intrare.

Când se declanșează temporizatoarele de închidere, toate resursele ambelor înregistrări de conexiune sunt eliberate. Expeditorul raportează eșecul de livrare către aplicația din amonte ( consultați API Reliable UDP).

Eliberarea înregistrării conexiunii"a resurse:

public void Dispose() ( încercați ( System.Threading.Monitor.Enter(this.LockerReceive); ) în cele din urmă ( Interlocked.Increment(ref this.IsDone); if (WaitForPacketsTimer != null) ( WaitForPacketsTimer.Dispose(); ) dacă (CloseWaitTimer != null) ( CloseWaitTimer.Dispose(); ) flux de octeți; Tcb.IncomingStreams.TryRemove(Key, out stream); stream = null; Tcb.OutcomingStreams.TryRemove(Key, out stream); stream = null; System .Threading.Monitor.Exit(this.LockerReceive); ) )

Mai adânc în cod. Recuperare transfer de date

Diagrama de recuperare a transmisiei de date în caz de pierdere a pachetului:



După cum sa discutat deja în închiderea unei conexiuni după expirarea timpului, când expiră temporizatorul de lucru, receptorul va verifica dacă există pachete pierdute. Dacă există pierderi de pachete, va fi compilată o listă cu numărul de pachete care nu au ajuns la destinatar. Aceste numere sunt introduse în matricea LostPackets a unei anumite conexiuni și sunt trimise cereri de re-livrare.

Trimiterea cererilor de re-livrare a pachetelor (starea de asamblare):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord) ( //... if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0)) ( // sunt pachete pierdute, trimiteți cereri de conectare pentru ele sec.Nr. LostPackets ) ( dacă (seqNum != 0) ( ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum); ) ) // ... ) )


Expeditorul va accepta cererea de relivrare și va retrimite coletele lipsă. Este de remarcat faptul că în acest moment expeditorul a pornit deja un temporizator de închidere a conexiunii și, la primirea unei cereri, acesta este resetat.

Retrimiterea pachetelor pierdute (starea SendingCycle):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, byte payload) ( // ... connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); // resetați cronometrul de închidere a conexiunii dacă !=lose nuWaitRecordTimerC. connectionRecord .CloseWaitTimer.Change(-1, -1); // ... // aceasta este o cerere de retransmisie - trimiteți pachetul necesar altfel ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionPacketRecord, header)); )


Pachetul retrimis (pachetul #3 din diagramă) este primit de conexiunea de intrare. Se face o verificare pentru a se asigura că fereastra de primire este plină și că transmisia normală de date este restabilită.

Verificarea hit-urilor în fereastra de primire (starea de asamblare):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, antet ReliableUdpHeader, byte payload) ( // ... // crește numărul de pachete connectionRecord.PacketCounter++; // scrie numărul curent al pachetului în matricea de control al ferestrei connectionRecord.WindowControlArray = header.PacketCounter; // setează cel mai mare pachet primit dacă (header.PacketNumber > connectionRecord.RcvCurrent) connectionRecord.RcvCurrent = header.PacketNumber; // repornește temporizatoarele connectionRecord.TimerSecondTry = false; connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPerio); connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); // ... // dacă am primit toate pachetele ferestre, apoi resetați contorul // și trimiteți un pachet de confirmare altfel dacă (connectionRecord .PacketCounter == connectionRecord. WindowSize) ( // resetează contorul. connectionRecord.PacketCounter = 0; // a mutat fereastra de transmisie connectionRecord.WindowLowerBound += connectionRecord.WindowSize; //anularea matricei de control al transmisiei connectionRecord.WindowControlArray.Nullify(); ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord); ) // ... )

API UDP de încredere

Pentru a interacționa cu protocolul de transfer de date, există o clasă deschisă Reliable Udp, care este un înveliș peste blocul de control al transmisiei. Iată cei mai importanți membri ai clasei:
clasă publică sigilată ReliableUdp: IDisposable ( // obține un punct final local public IPEndPoint LocalEndpoint // creează o instanță de ReliableUdp și începe // să asculte pachetele primite pe adresa IP // și portul specificate. O valoare de 0 pentru port înseamnă utilizarea / / un port alocat dinamic public ReliableUdp (IPAddress localAddress, int port = 0) // abonați-vă pentru a primi mesajele primite public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessagePointMessage = ReliableUdpMessage. scribe de la primirea mesajelor public nud Dezabonare(ReliableUdpSubscribeObject subscribeObject) // trimite un mesaj asincron // Notă: compatibilitatea cu XP și Server 2003 nu este pierdută, deoarece este utilizată sarcina publică .NET Framework 4.0 SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken) // începe trimiterea mesajului asincron public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, ca rezultat, ca rezultat, ca obiectyncCallback, ca rezultat) trimiterea publicului bool EndSendMessage(IAsyncResult asyncResult) // resurse clare public void Dispose() )
Primirea mesajelor se face prin abonament. Semnătură delegată pentru metoda de apel invers:
delegat public void ReliableUdpMessageCallback(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient);
Mesaj:
clasă publică ReliableUdpMessage ( // tip mesaj, enumerare simplă public ReliableUdpMessageTypes Tip ( get; private set; ) // date mesaj octet public Body ( get; private set; ) // dacă se setează la adevărat, mecanismul de confirmare a livrării va fi dezactivat / / pentru transmiterea unui anumit mesaj public bool NoAsk (get; private set; ) )
Pentru a vă abona la un anumit tip de mesaj și/sau un anumit expeditor, sunt utilizați doi parametri opționali: ReliableUdpMessageTypes messageType și IPEndPoint ipEndPoint.

Tipuri de mesaje:
enumerare publică ReliableUdpMessageTypes: scurt ( // Oricare = 0, // Solicitare către serverul STUN StunRequest = 1, // Răspuns de la serverul STUN StunResponse = 2, // Transfer fișier FileTransfer =3, // ... )

Mesajul este transmis asincron; în acest scop, protocolul implementează un model de programare asincronă:
public IAsyncResult BeginSendMessage (ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, stare obiect)
Rezultatul trimiterii unui mesaj va fi adevărat - dacă mesajul a ajuns cu succes la destinatar și fals - dacă conexiunea a fost închisă din cauza unui timeout:
public bool EndSendMessage(IAsyncResult asyncResult)

Concluzie

Multe nu au fost descrise în scopul acestui articol. Mecanisme de coordonare a firelor, tratarea excepțiilor și erorilor, implementarea metodelor de trimitere a mesajelor asincrone. Dar nucleul protocolului, o descriere a logicii pentru procesarea pachetelor, stabilirea unei conexiuni și stabilirea timeout-urilor, ar trebui să devină mai clară pentru tine.

Versiunea demonstrată a protocolului de livrare fiabil este destul de robustă și flexibilă și îndeplinește cerințele definite anterior. Dar vreau să adaug că implementarea descrisă poate fi îmbunătățită. De exemplu, pentru a crește debitul și a schimba dinamic perioadele de temporizare, puteți adăuga mecanisme precum fereastra glisante și RTT la protocol; va fi, de asemenea, util să implementați un mecanism pentru determinarea MTU între nodurile de conexiune (dar numai în cazul trimiterii). mesaje mari).

Vă mulțumesc pentru atenție, aștept comentariile și comentariile voastre.
Implementarea modelului de programare asincronă CLR și cum se implementează modelul de proiectare IAsyncResult

  • Mutarea modelului de programare asincronă la modelul asincron bazat pe sarcini (APM la TAP):
    Programare asincronă TPL și .NET tradițională
    Interoperabilitate cu alte modele și tipuri asincrone
  • Actualizare: Mulțumim lui mayorovp și sidristij pentru ideea de a adăuga o sarcină la interfață. Compatibilitatea bibliotecii cu sistemul de operare mai vechi nu este afectată, deoarece al 4-lea cadru acceptă atât serverul XP, cât și 2003.

    UDP (User Datagram Protocol) este un protocol de transport fără conexiune cu livrare de date nesigură. Acestea. nu oferă confirmarea livrării pachetelor, nu păstrează ordinea pachetelor primite și poate pierde sau duplica pachete. UDP funcționează similar cu IP, cu excepția introducerii conceptului de porturi. UDP este, în general, mai rapid decât TCP datorită supraîncărcării mai mici. Este folosit de aplicații care nu necesită livrare fiabilă sau le implementează singure. De exemplu, Servere de nume, serviciu TFTP (Trivial File Transfer Protocol), SNMP (Protocol simplu de gestionare a rețelei), sisteme de autentificare. Identificator de protocol UDP în teren Protocol Antet IP – numărul 17.

    Orice aplicație care utilizează UDP ca serviciu de nivel de transport trebuie să furnizeze ea însăși mecanisme de confirmare și un sistem de numerotare secvențială pentru a se asigura că pachetele sunt livrate în aceeași ordine în care au fost trimise.

    Portul de destinație

    Orez. Formatul antetului pachetului UDP

    Scopul câmpurilor pachetului udp:

    Numărul portului expeditorului -Sursă Port(16 biți) – conține numărul portului din care a fost trimis pachetul, atunci când acest lucru este relevant (de exemplu, expeditorul așteaptă un răspuns). Dacă acest câmp nu este utilizat, acesta este umplut cu zerouri.

    Numărul portului de destinație –Destinaţie Port(16 biți) – conține numărul portului către care va fi livrat pachetul.

    Lungime -Lungime(16 biți) – Conține lungimea acestei datagrame în octeți, inclusiv antetul și datele.

    Câmp sumă de control –Sumă de control(16 biți) – reprezintă complementul pe biți al unei sume de 16 biți de cuvinte de 16 biți. Suma implicată în calcularea cantității este: pachet de date cu câmpuri de aliniere a limitei de 16 biți (zero), antet pachet UDP, pseudo-header (informații din protocolul IP).

    protocolul tcp

    TCP (Transmission Control Protocol) este un protocol de transport orientat spre conexiune cu livrare fiabilă a datelor. Prin urmare, are algoritmi stringenți de detectare a erorilor menționați să asigure integritatea datelor transmise.

    Numerotarea și confirmarea consecutivă sunt utilizate pentru a asigura o livrare fiabilă. Folosind numerotarea secvenţială, se determină ordinea datelor în pachete şi se identifică pachetele lipsă. Numerotarea consecutivă cu confirmare vă permite să organizați o comunicare fiabilă, care este numită full duplex(full duplex). Fiecare parte a conexiunii oferă propria sa numerotare pentru cealaltă parte.

    TCP este un protocol serial de octeți. Spre deosebire de protocoalele seriale de pachete, acesta atribuie un număr de serie fiecărui octet al pachetului transmis, mai degrabă decât fiecărui pachet individual.

    Portul de destinație

    Numărul de confirmare

    Orez. Formatul antetului pachetului TCP