Comunicarea Prin Socluri
=== Comunicarea prin socluri – forma finala ===
Rețele de calculatoare
1
Există invenții în istoria omenirii care pot fi considerate „trepte” ale dezvoltării umanității. Apariția focului, a roții, a prafului de pușcă, a curentului electric, a mașinii cu aburi, a telefonului au delimitat începerea unor etape noi în viața omului. Pentru secolul XX nu se mai poate vorbi despre o invenție, ci despre o serie de invenții. Nu se mai poate spune ce descoperire a fost mai importantă: mașina, telefonul, avionul sau calculatorul. Un lucru este însă cert: omenirea actuală se caracterizează prin nevoia de comunicare. Iar comunicarea s-a dezvoltat foarte mult prin apariția rețelelor de calculatoare.
Încă de la început au existat calculatoare mari (mainframe-uri), care aveau conectate mai multe terminale și astfel mai mulți oameni puteau conlucra la un proiect sau schimba informații. Dar aceste calculatoare erau inaccesibile micilor firme, oamenilor în general.
După apariția calculatoarelor personale, cantitatea de informație a crescut uimitor. Pentru schimbul de informație, ușor și ieftin, se impunea comunicarea între calculatoare. Astfel a apărut rețeaua de calculatoare: mai multe calculatoare autonome capabile să transfere date între ele (interconectate).
Avantajele interconectării calculatoarelor, adică a folosirii rețelelor de calculatoare sunt numeroase:
Pe lângă multiplele avantaje ale rețelelor de calculatoare, există și probleme datorate apariției acestora. Soluționarea lor nu este simplă în multe cazuri:
Deși au apărut noi probleme o dată cu rețelele de calculatoarele, nu se poate nega utilitatea lor. La ora actuală rețelele de calculatoare sunt atât de răspândite încât nu se mai poate pune problema interzicerii sau diminuării numărului lor. După dezvoltarea într-un ritm alert a Internetului nu rămâne decât să se încerce o rezolvare a problemelor legate de rețelele de calculatoare și dezvoltarea unui cadru legislativ corespunzător.
Tipuri de rețele
Tehnologia conectării rețelelor este foarte diversificată. De aceea există mai multe tipuri de rețele. Clasificarea rețelelor se face după mai multe criterii: după mărimea rețelei, după modalitatea de partajare a resurselor, după hardware. Din punct de vedere al modalității de partajare a resurselor există două tipuri de rețele:
Mărimea unei rețele nu se referă la numărul de calculatoare interconectate, ci mai degrabă la aria geografică acoperită și la modul de transmisie a datelor. Cu toate acestea există o strânsă legatură între mărimea rețelei și numărul de calculatoare din rețea. După acest criteriu, rețelele de calculatoare se împart în:
În sfârșit, după tehnologia de transmisie, rețelele se impart în două tipuri:
Modele de referință
În cercetare, pentru rezolvarea unei probleme practice se asociază, în general, un model teoretic. Pe baza acestui model se dezvoltă o teorie ce conține descrierea problemei, regulile ce o caracterizează, teorie care conduce la anumite rezultate ce pot fi puse în practică ulterior. Pentru cazul în care problema practică este rețeaua de calculatoare partea de teorie și cea de practică nu sunt clar delimitate. Nici ordinea de dezvoltare nu a fost foarte strictă: mai întâi cercetare teoretică și apoi punere în practică.
Pentru a reduce din complexitatea proiectării, majoritatea rețelelor sunt organizate sub forma unei serii de straturi sau niveluri, fiecare din ele construit peste cel de dedesubt. Scopul fiecărui nivel este să ofere anumite servicii nivelurilor superioare, protejându-le totodată de detaliile privitoare la implementarea efectivă a serviciilor oferite. Un nivel n de pe o mașină conversează cu nivelul n de pe altă mașină. Această organizare pe straturi a rețelelor este o modelare teoretică, acceptată pe scară largă și care constituie baza teoriei rețelelor de calculatoare.
Terminologie
Regulile și convențiile utilizate în conversația dintre două niveluri de pe mașini diferite se numește protocol. În general, un protocol reprezintă o înțelegere între părțile care comunică asupra modului de realizare a comunicării.
Între două niveluri adiacente există o interfață. Interfața definește ce operații și servicii primitive oferă nivelul de jos către nivelul de sus.
Elementele active din fiecare nivel se numesc entități. O entitate poate fi software (un proces) sau hardware (cip cu intrare/ieșire). Entitățile corespunzătoare aceluiași nivel, dar aflate pe mașini diferite, se numesc entități egale. Entitățile de la nivelul n implementează un serviciu utilizat de nivelu n+1. Nivelul n se numește furnizor de servicii, iar nivelul n+1 se numește utilizator de servicii.
Serviciile sunt disponibile în SAP-uri (Service Acces Point – puncte de acces la servicii). SAP-urile nivelului n sunt locurile unde nivelul n+1 poate avea acces la serviciile oferite. Fiecare SAP are o adresă care îl identifică în mod unic.
Pentru ca două niveluri să schimbe între ele informație, trebuie să fie convenit un set de reguli referitoare la interfață.
În figura de mai sus este ilustrat transferul de date între două niveluri. Entitatea nivelului superior transmite, prin intermediul SAP-ului, un IDU (unitate de date de interfață) . Acest IDU constă dintr-un SDU (unitate de date de serviciu) și un ICI (informație de control a interfeței). SDU reprezintă informația transmisă prin rețea de entitate către entitatea pereche. Pentru a fi trimis, SDU-ul va fi fragmentat în bucăți (PDU – unitate de date protocol) care sunt trimise ca pachete separate.
Un serviciu este o colecție de funcții (primitive de serviciu) puse la dispoziție de un nivel prin intermediul interfeței. Nivelurile pot oferi nivelurilor de deasupra două tipuri de servicii: orientate pe conexiuni și fără conexiuni.
Fiecare serviciu se caracterizează prin QoS (Quality of Service – calitatea serviciului). Unele servicii sunt mai lente și sigure (datele ajung nealterate și în aceeași ordine), pe când altele sunt mai rapide, dar nesigure. Pentru trasmisia unui fișier, de exemplu, este necesar un serviciu sigur. În schimb, pentru vizualizarea unui film prin rețea nu se poate admite o întârziere a datelor, în schimb se acceptă posibiltatea ca unii pixeli să fie diferiți.
În funcție de calitatea serviciului, tipurile de servicii de mai sus se împart în alte categorii de servicii.
O primitivă de serviciu este o operație din cadrul unui serviciu care cere acestuia executare a anumitor intstrucțiuni sau îl interoghează asupra acțiunilor executate de o entitate pereche.
Deși sunt adesea confundate, serviciile și protocoalele reprezintă concepte distincte. Un serviciu este un set de primitive (operații) pe care un nivel le furnizează nivelului de deasupra sa. Serviciul definește ce operații este pregătit să îndeplinească pentru utilizatorii săi, dar nu spune nimic despre cum sunt implementate aceste operații.
Prin contrast, un protocol este un set de reguli care guvernează formatul și semnificația cadrelor, pachetelor sau mesajelor schimbate între ele de entitățile pereche dintr-un nivel. Entitățile folosesc protocoale pentru a implementa definițiile serviciului lor. Ele sunt libere să își schimbe protocoalele după cum doresc, atât timp cât nu modifică serviciul oferit prin interfață. În acest fel, serviciul și protocolul sunt complet decuplate.
Prin analogie cu un limbajul C++: un serviciu reprezintă declarația unei clase (cu membri și atribute), pe când un protocol reprezintă implementarea acestei clase (codul funcțiilor).
Modelul OSI
Acest model se bazează pe o propunere dezvoltată de către Organizația Internațională de Standardizare (ISO – International Standards Organization) ca un prim pas către standardizarea internațională a protocoalelor folosite pe diferite niveluri. Modelul se numește ISO OSI (Open Systems Interconnection), pentru că el se ocupă de sisteme deschise comunicării cu alte sisteme.
Modelul OSI cuprinde șapte niveluri. Principiile aplicate pentru a se ajunge la cele șapte niveluri sunt următoarele:
Un nivel trebuie creat atunci când este nevoie de un nivel de abstractizare diferit.
Fiecare nivel trebuie să îndeplinească un rol bine definit.
Funcția fiecărui nivel trebuie aleasă acordându-se atenție definirii de protocoale standardizate pe plan internațional.
Delimitarea nivelurilor trebuie făcută astfel încât să se minimizeze fluxul de informații prin interfețe.
Numărul de niveluri trebuie să fie suficient de mare pentru a nu fi nevoie să se introducă în același nivel funcții diferite și suficient de mic pentru ca arhitectura să rămână funcțională.
Modelul OSI nu reprezintă în sine o arhitectură de rețea pentru că nu specifică serviciile și protocoalele utilizate la fiecare nivel. Modelul spune ce ar trebui să facă fiecare nivel. Pentru fiecare nivel există standarde care au fost publicate separat. În figura de mai jos sunt prezentate nivelurile acestui model:
În conformitate cu modelul de referință OSI, fiecare nivel îndeplinește anumite funcții bine delimitate:
Modalitatea de transport a datelor folosind modelul OSI este următoarea: emițătorul furnizează datele nivelului aplicație. Acesta le atașează un antet (antetul aplicație, care poate fi și vid) și transmite obiectul rezultat, mai departe, nivelului prezentare. Fiecare nivel până la nivelul fizic se comportă identic. Nivelul fizic va transmite efectiv datele. Pe mașina receptoare antetele sunt eliminate succesiv pe măsură ce mesajul trece de la un nivel la altul în sus.
De observat este că, în conformitate cu arhitectura de rețea pe niveluri, fiecare nivel din acest model de referință se comportă ca și cum transmiterea datelor ar fi orizontală (între mașini) și nu verticală (între niveluri).
Modelul TCP/IP
Spre deosebire de modelul OSI care este abstract și mai puțin implementat, modelul TCP/IP nu este, în esență, un model de referință. Luându-și numele după cele două protocoale importante care sunt implementate pe scară largă în lumea rețelelor, acest model încearcă o abstractizare a tehnologiei deja existente.
Acest model se bazează pe o stivă de protocoale, dispus în patru niveluri. În figura de mai jos se observă relația dintre nivelurile modelului OSI și cele ale modelului TCP/IP.
Nefiind bine gândit dinainte, ci creat „din mers”, în modelul TCP/IP nu se face o distincție foarte clară între interfețe, servicii și protocoale. Cu toate acestea se poate vorbi despre funcții specifice fiecărui nivel:
Modelul TCP/IP nu este un model în sine, ci o descriere a implementării deja existente. Pentru exemplificare, în figura de mai jos sunt prezentate câteva protocoale utilizate și dispunerea lor pe nivelurile acestui model. Spre deosebire de modelul OSI care poate caracteriza aproape orice rețea de calculatoare, acest model nu poate descrie alte tipuri de rețea în afară de cea de tip TCP/IP. Pe de altă parte, acest model oferă foarte multe protocoale, utilizate pe scară largă, pe când modelul OSI nu descrie nici un protocol.
Dacă inițial nu s-a acordat prea mare importanță bazei teoretice, în prezent se încearcă o corectare a proiectării rețelelor de calculatoare. Modelul TCP/IP este dovada că între teorie și practică, pentru cazul rețelelor de calculatoare, nu există o linie clară, dar este și soluția pentru corectarea acestei deficiențe. Acest model este suficient de general (suficient de teoretic) pentru a explica funcționarea rețelelor TCP/IP (în particular a Internetului), dar insuficient pentru dezvoltarea unei teorii. Cu alte cuvinte este porțiunea teoretică necesară unui programator (utilizator) pentru ca acesta să poată scrie programe structurate în sensul teoriei modelelor de referință pentru rețele de calculatoare.
TCP/IP
Stiva de protocoale descrisă generic prin denumirea de TCP/IP este cea mai larg utilizată la ora actuală. Comunicarea în Internet se realizează pe baza acestei stive. Protocoalele TCP și IP au fost create inițial ca un proiect al Departamentului de Apărare al Statelor Unite (DoD – Department of Defence) pentru a conecta diferite rețele diferite într-o „rețea de rețele”. Proiectul a fost încununat de succes datorită funcțiilor de bază de care dispunea (transfer de fișiere, poștă electronică și conectare la distanță) și a fost preluat de către Agenția de Cercetare pentru Proiecte Avansate (ARPA – Advanced Research Projects Agency). Bolt, Beranek și Bewman (BBN) au primit în decembrie 1968 un contract pentru proiectarea și construirea unei rețele cu comutare de pachete. Proiectul se numea ARPANET și a început prin patru noduri la sfârșitul lui 1969.
Protocolul inițial de conectare în ARPANET a fost Protoclul de Control în Rețea (NCP – Network Control Protocol). Protocoalele TCP și IP au fost propuse și implementate în 1974 ca o variantă îmbunătățită de comunicație, după ce NCP s-a dovedit a nu fi compatibil într-o rețea cu trafic încărcat. În 1983 DoD acceptă ca toate calculatoarele sale să fie echipate cu noul pachet de protocoale în vederea comunicării la distanță.
Tot în 1983, popularitatea TCP/IP-ul a crescut simțitor prin includerea sa în kernelul de comunicație al sistemului BSD UNIX versiunea 4.2 (Universitatea California). Deși în august 1990 DoD dispune ca toate sistemele sale să folosească protocoale OSI, dezvoltarea TCP/IP a continuat. Unul din motive este că în iunie 1987 cel puțin 130 de companii aveau produse care suportau TCP/IP și mii de rețele de diferite tipuri foloseau acest pachet de protocoale.
Dintre modificările pe care le-au suferit protocoalele TCP/IP, cea mai importantă este dezvoltarea standardului IPv6 în decembrie 1995. În prezent orice sistem de operare include majoritatea protocoalelor TCP/IP pentru interconectare.
Apărut inițial pe sisteme UNIX, setul de protocoale TCP/IP respectă ideea sistemelor deschise (open systems). Noțiunea de sistem deschis este aplicabilă atât pentru software, cât și pentru hardware și denotă faptul că arhitectura sa este publică. Mai exact, pentru sistemul UNIX, de exemplu, codul sursă este public și gratuit. Astfel oricine poate folosi acest sistem și poate modifica sau adăuga cod pentru diferite necesități. TCP/IP-ul însuși a fost dezvoltat ca o extindere a acestui sistem. Chiar mai mult, setul de protocoale TCP/IP se încadrează în ideea de sistem de rețea deschis (open system networking), adică o rețea ce se bazează pe un set de protocoale bine cunoscute și inteligibile. Ca și la sistemele deschise, folosirea unui astfel de sistem de rețea deschis duce la scriere de programe care „merg” pe orice mașină pe care este implementat setul de protocoale TCP/IP.
Spre deosebire de modelele de referință prezentate anterior (care sugerează doar ce ar trebui să facă fiecare nivel), un sistem deschis este o noțiune „palpabilă” și atât interfața, cât și implementarea sa sunt publice.
Lista protocoalelor care fac parte din setul TCP/IP nu este standard. Toate lucrările care descriu acest model sunt de acord cu (și/sau descriu) câteva protocoale de bază. Numărul și existența lor însă diferă la un sistem la altul, adică de la o implementare la alta. În tabelul de mai jos sunt câteva din cele mai populare protocoale care fac parte din setul TCP/IP , grupate după funcțiile lor.
Interconectarea rețelelor
2
În modelul de referință TCP/IP, sub nivelul internet se află necunoscutul. Mai exact, se află suportul fizic de transmitere a datelor, suport fizic care asigură că datele ajung de la un calculator la destinație, într-un mod transparent nivelurilor superioare. Pentru un programator nu este interesantă partea cum ajung datele la destinație, este suficient că ele ajung. În cazul în care rețeaua funcționează bine și calculatoarele sunt corect configurate să folosească această rețea nu apare problema interconectării.
Să presupunem însă că rețeaua nu funcționează perfect, datele unui program nu ajung la programul corespondent sau pur și simplu calculatorul folosit nu este configurat corect pentru a folosi rețeaua la care este legat. În mod clasic se spune ca în acest caz să se apeleze la administratorul de rețea… Pe de altă parte, prin simpla folosire a unei rețele și a unor programe de rețea se întâlnesc termeni ca poartă (gateway), punte (bridge), ruter, zid de protecție (firewall), etc. În oricare din aceste cazuri chiar și un simplu utilizator are nevoie să cunoască noțiunile și mai ales diferența dintre elementele de interconectare a rețelelor.
Rețele Ethernet
În cadrul rețelelor locale (LAN), cea mai răspândită modalitate de conectare a calculatoarelor este sistemul Ethernet. Sistemul Ethernet pentru rețele LAN a fost standardizat de către firmele Xerox, DEC și Intel sub numele de IEEE 802.3 și funcționează la viteze de la 10Mbps până la 100Mbps (1Mbps = 1.000.000 biți/secundă).
În sistemul Ethernet există noțiunile de multicast (trimitere multiplă) și broadcast (difuzare). Datele trimise în modul multicast vor fi interpretate doar de către calculatoarele cărora le sunt adresate (poate fi doar unul), pe cînd cele trimise prin broadcast vor fi interpretate de toate calculatoarele din rețea.
Acest sistem presupune două tipuri de conectare: de tip BNC sau UTP. Conexiunea de tip BNC presupune un singur cablu coaxial (10Base5, de maxim 500m, sau 10Base2, de maxim 200m) pe care sunt dispuse toate calculatoarele. Mai evoluată este conexiune UTP în care fiecare calculator este conectat la un concentrator (hub) printr-un cablu torsadat (10Base-T, de maxim 100m, sau 10Base-F, de maxim 2000m). Hub-urile pot fi conectate între ele și pot conecta și subrețele de tip BNC. Într-o astfel de rețea transmisia datelor se face prin difuzare (broadcast), iar fiecare calculator are o adresă unică în rețea numită adresă MAC (Medium Access Control – controlul accesului la mediu). În figura următoare este ilustrată o rețea complexă de tip Ethernet.
Tot la nivelul fizic (din modelul OSI) mai există niște dispozitive de transmitere a datelor numite repetoare (repetear). Acestea au rolul de a retransmite datele, fără a interpreta în nici un fel conținutul lor. Spre deosebire de hub-uri, repetoarele sunt folosite atât pe linii UTP, cât și pe cele BNC și au principalul rol de amplificare a semnalului. O altă funcție importantă a repetoarelor este că pot face conversia mediului de transmisie (de exemplu de pe fibră optică pe cablu). Atât hub-urile cât și repetoarele lucrează cu biți simpli (tensiune mai mare sau mai mică) care nu sunt grupați în nici un fel.
În transmiterea datelor, pentru cazul în care numărul de calculatoare interconectate este mai mare, apare problema coliziunii pachetelor și a încărcării rețelei. Pentru prevenirea unor astfel de probleme, există mai multe tipuri de hub-uri:
Soluția oferită prin folosirea unor hub-uri mai performante nu este, de multe ori, suficientă.
Punți (bridge) și comutatoare (switch)
Pentru îmbunătățirea performanțelor unei rețele (evitarea coliziunilor) a apărut ideea divizării rețelei în subrețele. O punte (bridge) face legătura între mai multe rețele sau, după caz, împarte o rețea în mai multe subrețele. Aceasta se află la nivelul legătură de date din modelul OSI. O punte transmite date dintr-o rețea în alta pe baza adresei MAC, adică indiferent dacă pachetul este IP, IPX sau OSI. Apărute inițial în anii 1980, punțile conectau rețele de același tip, dar mai recent ele pot funcționa între rețele diferite (de exemplu între rețele Ethernet și Token Ring).
Deoarece se găsesc la nivelul legătură de date, punțile lucrează cu grupări de biți, cadre. Ele conțin o tabelă de rutare și retransmit cadrele într-o anumită rețea sau în toate rețelele în cazul în care destinația nu este cunoscută. Este de remarcat faptul că punțile sunt dispozitive care fac retransmiterea cadrelor software, putând la nevoie filtra datele. După modalitatea de funcționare, există mai multe tipuri de punți:
Dintre funcțiile și avantajele punților pentru care sunt folosite în interconectarea rețelelor, cele mai importante sunt conectarea la distanță (remote bridge) și translaterea cadrelor (translational bridge).
O metodă mai evoluată de interconectare a hub-urilor și calculatoarelor dintr-un LAN este oferită de comutatoare (switch). Apărute pe la mijocul anilor 1990, comutatoarele sunt dispozitive destinate împărțirii unei rețele în mai multe subrețele. Mai exact, comutatoarele sunt destinate conectării segementelor de rețea Ethernet.
Între comutatoare și punți există destul de multe asemănări, dar și deosebiri. La fel ca și punțile, comutatoarele funcționează la nivelul legătură de date și transferă/separă datele între două sau mai multe rețele. Spre deosebire de punți, însă, comutatoarele funcționează doar în rețele de același tip (respectiv Ethernet) și de obicei comutatoarele au mai multe porturi. Mare deosebire este însă în modul în care cele două dispozitive retransmit pachetele. Comutatoarele funcționează la nivel hardware, au integrate cipuri de transfer și sortare a pachetelor. De aceea viteza lor în comparație cu a punților este mult mai mare. De asemenea comutatoarele transferă datele între interfețele conectate la viteza maximă admisă de amândouă. De exemplu, dacă la un switch sunt conectate interfețe de 10Mbps și de 100Mbps, două interfețe de 100Mbps vor comunica între ele la viteza lor maximă și doar la 10Mbps cu cele care nu suportă această viteză. Spre deosebire, toate interfețele conectate la o punte funcționează la o viteză maximă fixă, stabilită în funcție de vitezele interfețelor și a punții. O altă modalitate de îmbunătățire a performanțelor pe care o introduc comutatoarele este funcționarea paralelă: oricare două interfețe beneficiază de o cale dedicată de comunicare.
Există două tipuri de comutatoare:
În general comutatoarele sunt considerate niște punți mai avansate, iar la ora actuală sunt din ce în ce mai mult folosite. Însăși principiul de funcționare pentru găsirea destinației unui cadru, respectiv folosirea unor tabele de rutare refăcute dinamic, apropie comutatoarele de punți.
Rutere (router) și porți (gateway)
Un ruter este un dispozitiv special de interconectare a rețelelor ce se încadrează la nivelul rețea din modelul de referință OSI. Pe de altă parte, termenul de ruter este folosit în mod generic pentru a desemna o modalitate de interconectare a rețelelor. Spre deosebire de punți și comutatoare care pot fi privite atât ca elemente de separare a unei rețele cât și ca legare de rețele, ruterele sunt în mod explicit dispozitive de legare a rețelelor. Mai mult, ruterele formează o subrețea într-o rețea de rețele. Ruterele folosesc algoritmi mult mai complecși de transmitere/dirijare a pachetelor de la sursă la destinație. Inițial ruterele au fost specifice protocolului IP, dar în timp au apărut rutere multiprotocol.
O poartă este considerată ca fiind termenul ce desemnează interconectarea rețelelor la nivel mai înalt decât ruterele. Există o diferențiere între porți, după nivelul la care aceastea operează asupra pachetelor:
În general funcția unei porți este de a interpreta conținutul unui pachet și/sau de a-l converti pentru un alt protocol. Diferența dintre poartă și ruter nu este strictă. De exemplu, protocoalele de la nivelul rețea (nivel internet) din modelul TCP/IP de dirijare și control pentru rutere au denumiri cum ar fi protocolul de poartă interioară sau protocolul de poartă exterioară. În funcție de nivelul la care este referit (nivelul rețea sau un nivel superior) un astfel de conector este denumit ruter sau poartă.
Deoarece se află la nivelul rețea, ruterele folosesc algoritmi de dirijare îndelung elaborați și care asigură optimalitatea pentru drumul parcurs de pachete până la destinație. Mai mult, la nivelul transport ruterele pot alege drumuri diferite pentru pachete cu un conținut diferit, în funcție de reguli. De exemplu în Canada, conform legilor, un pachet destinat aceleași țări nu are voie să depășească granița, chiar dacă ar ajunge mai repede.
Un zid de protecție (firewall) este o modalitate de a asigura o protecție a datelor care intrâ și ies dintr-o rețea. Zidul de protecție este format din două rutere și o poartă. Fiecare dintre rutere filtrează pachetele la intrare sau ieșire având în vedere doar adresa destinație/sursă. Poarta se ocupă de conținutul pachetelor. Configurarea unui zid de protecție se face software, selectându-se care pachete au voie să treacă și care nu.
Noțiunea de intermediar (proxy) denotă o aplicație de tip server care preia cereri dintr-o rețea locală, le rezolvă într-o rețea externă și după o eventuală filtrare a datelor, întoarce rezultatele. De exemplu pentru conectarea unei rețele de mai multe calculatoare care au o singură adresă externă se poate folosi un server dedicat pentru navigare pe internet.
Nivelul internet
3
Nivelul internet din modelul de referință TCP/IP nu este identic cu noțiunea de Internet folosită în mod curent. Internetul poate fi văzut ca o colecție de subrețele care sunt conectate împreună. El reprezintă denumirea dată suportului fizic de transmitere a datelor între calculatoare aparținând acestor subrețele. Nivelul internet, în schimb, descrie și implementează protocoalele folosite pentru transferul datelor între subrețele. Prin corespondență nivelului internet din modelul TCP/IP îi corespunde nivelul rețea în modelul OSI.
La acest nivel se implementează protocolul internet (IP) care este responsabil de conectarea rețelelor. Aceste rețele poartă denumirea de sisteme autonome( AS – Autonomous Systems).
Protocolul internet (IP)
O datagramă IP, ce reprezintă structura de transmitere a datelor la acest nivel, constă dintr-o parte antet și o parte date efective. Transmiterea octeților antetului se face în forma big endian, adică primul octet este cel mai semnificativ. Pe unele sisteme , de exemplu Pentium, forma de transmitere a datelor este little indian, fiind necesară o conversie.
Teoretic, o datagrama poate avea până la 64 Kocteți. În practică deseori o datagramă este fragmentată în unități mai mici, care pot să parcurgă drumuri diferite. De obicei, fragmentele care circulă pe internet au în jurul a 1500 de octeți.
Câmpurile antetului datagramelor IP au fost destul de bine gândite, nefiind necesară până acum modificarea lor.
Adrese IP
Fiecare gazdă și ruter din Internet are o adresă IP, care codifică adresa sa de rețea și adresa de gazdă. Această adresă este unică pe tot Internetul. Un calculator care este conectat la mai multe rețele are câte o adresă pentru fiecare rețea. Adresele IP sunt cuvinte de 32 de biți, dar de obicei sunt reprezentate ca patru numere zecimale despărțite prin punct. Nu toate adresele sunt disponibile pentru folosire de către gazde, unele având un scop special. Adresele IP se împart în cinci clase, după mărime rețelelor care le folosesc:
Primele trei clase A, B și C sunt destinate folosirii de către rețelele particulare, adică rețele ale unor firme sau organizații. Astfel, cele din clasa A sunt destinate unor firme sau organizații foarte mari. Cele din clasa B sunt destinate firmelor și organizațiilor de mărime mijlocie, iar cele din clasa C sunt destinate folosirii de către mici firme, școli și universităț,etc. Adresele din clasa D sunt rezervate pentru trimiterea multiplă și se numesc adrese de grupuri multicast. Clasa E este rezervată și nefolosită având scop experimental.
Numărul rețelelor conectate în prezent la Internet este de ordinul zecilor de mii, iar numărul crește exponențial. De aceea, adresele de rețea sunt distribuite de către Centrul de Informații de Rețea (NIC – Network Information Center).
Adresele din clasa D au fost create în scopul de trimitere multiplă, iar cele din clasa E sunt rezervate pentru folosire ulterioară. Dintre adresele speciale, 0.0.0.0 specifică gazda locală, 127.x.z.y sunt adresate buclei locale (loopback) și folosite pentru teste, iar 255.255.255.255 este adresă de difuzare în rețeaua locală.
O adresă IP se compune dintr-o adresă de rețea și o adresă de gazdă. De exemplu, pentru o rețea din clasa C și o gazdă cu adresa de IP 192.168.1.13, porțiunea de rețea este 192.168.1 iar porțiunea de gazdă este 13. Adresa unei rețele se specifică folosind 0 ca număr al gazdei, de exemplu 192.168.1.0.
Pe lângă adresa de IP în practică se mai folosește masca de rețea. O astfel de masca, tot de 32 de biți, are 1 acolo unde ar trebui să fie adresa de rețea și 0 în rest. Masca de rețea este folosită de rutere: Fiecare ruter are o tabelă de adrese IP de forma (rețea, 0) care indică cum se ajunge la diferite rețele exterioare. Pe lângă acestea mai există în această tabelă adrese complete IP specificând modul de acces al gazdelor din rețeaua locală. Când un pachet ajunge la un ruter, asupra adresei IP de destinație se face o operație de și logic cu masca rețelei locale. Dacă adresa rețelei este aceeași, ruterul trimite pachetul gazdei. Dacă nu, în mod analog, se verifică și celelalte adrese de rețea din tabela ruterului, fiecare având masca sa proprie. Aproape toate ruterele au în tabela lor o adresă de rețea 0.0.0.0 cu masca 0.0.0.0 care specifică ce se întâmplă cu un pachet care nu a fost adresat nici unei din adresele cunoscute (un și logic când unul din biți este 0 dă rezultatul 0).
Această modalitate de folosire a tabelelor de rutare permite împărțirea unei rețele în două sau mai multe subrețele. Împărțirea unei rețele în subrețele constă în aduăgare de biți de 1 în masca rețelei, biți „împrumutați” de la adresa de gazdă. Din exterior, rețeaua rămâne un tot unitar.
Fie de exemplu rețeaua 192.168.1.0. Masca acestei rețele este 255.255.255.0. Această rețea se împarte în două subrețele, fiecare cu maxim 127 de calculatoare, prin folosirea măștii 255.255.255.128. Un pachet extern care vine la ruterul rețelei indică adresa de rețea locală. Ruterul nu mai face însă și logic cu masca inițială, ci cu cea nouă. Astfel pachetul este dirijat spre gazdă sau, după caz, spre un ruter intern care gestioneză pachetele din rețeaua internă. Termenul de subrețea folosit în acest caz este impropriu în contextul noțiunilor prezentate până acum, dar este deja încetățenit și, în practică, folosirea lui nu produce confuzii.
Această impărțire în subrețele prin folosirea măștii reduce mărimea tabelei unui ruter și implicit munca acestuia; ruterul inițial nu mai este nevoit să cunoască calea spre toate gazdele rețelei. De asemenea se evită supraîncărcarea rețelei cu pachete: dacă o gazdă trimite un pachet spre o altă gazdă din aceeași subrețea acesta nu va ajunge în cealalte subrețele. O subrețea se mai notează în practică prin adresă_subrețea/biți_de_gazdă. De exemplu 192.168.1.128/127 reprezintă o subrețea în care toate gazdele au adrese IP cuprinse între 192.168.1.129 și 192.168.1.254 cu masca de subrețea 255.255.255.128.
Adresele IP din clasa D sunt folosite pentru trimitere multiplă. Fiecare adresă din această clasă identifică un grup de utilizatori. Un grup de utilizator poate fi permanent sau temporar. Un grup permanent nu trebuie configurat și are o adresă de grup permanentă. Dintre cele mai folosite astfel de grupuri permanente fac parte:
În folosirea adreselor IP sunt implicate mai multe protocoale, la nivel internet, în afară de protocolul IP. Dintre acestea protocoalele ARP și RARP se ocupă de relația dintre adresele de IP și adresele LAN( de exemplu Ethernet). Mai există protocoale de dirijare: IGMP (Internet Group Management Protocol – Protocol de gestiune a grupurilor Internet) care este responsabil de gestiunea adreselor IP din clasa D, adică de trimitere multiplă.
Extinderi ale protocolului IP
Deși un calculator este de obicei fix și are o adresă de IP specifică, cel puțin în viziunea de la începutul implementării TCP/IP-ului, în prezent există multe cazuri în care calculatoarele și adresele IP „se mișcă”. Au apărut astfel noțiunile de ip mobil și ip dinamic. Noțiunile sunt adeseori confundate cu toate că desemnează lucruri complet diferite:
În gama problemelor apărute în timp în gestiunea adreselor IP cea mai importantă o reprezintă lipsa adreselor de gazdă și mai ales de rețea. Pentru multe firme soluția de rețea o reprezintă o rețea de clasă B, adică cu mai mult de 254 de calculatoare. Numărul acestor rețelele este de 16.384, care este insuficient. Pentru moment s-a adoptat o soluție de compromis numită dirijarea fără clase între domenii (CIDR – Classless InterDomain Routing) care funcționează pe ideea că dacă o rețea are nevoie de un număr de 2000 adrese, de exemplu, să i se acorde 2048 rezultate din opt rețele de clasă C succesive. De asemenea soluția mai impune impărțire a spațiului de adrese de clasă C în patru porțiuni asociate la patru zone geografice (Europa, America de Nord, America Centrală și de Sud, Asia și Pacific).
Soluția mai normală, pe termen lung și care cu siguranță se va impune (deja este foarte populară) se numește IPv6 (Internet Protocol version 6). Acest protocol îmbunătățește într-o foarte mare măsură actualul protocol IP (numit și IPv4), dar nu este, în general, compatibil cu acesta. Principalele caracteristici ale noului protocol sunt:
Deoarece IPv6 este o versiune mai nouă este normal ca orice implementare ulterioară să suporte IPv4, adică orice aplicație existentă la ora actuală pentru rețelele TCP/IP. Până a fi utilizat la scară largă, la ora actuală, ca și ip-ul mobil, pachetele IPv6 circulă prin internet ca printr-un tunel.
Sistemul numelor de domeniu (DNS)
Deși acest protocol (Domain Name System) se află, în modelul de referință TCP/IP, pe nivelul aplicație, legătura strânsă între adresele IP și numele de domeniu impune prezentarea acestuia alături de protocolul internet. Mai mult, dacă protocoale ca FTP sau TELNET sunt folosite mai mult sau mai puțin în funcție de necesitatea utilizatorilor, DNS-ul este configurat o dată cu adresa/adresele de IP și tabelele de rutare și folosit în mod implicit în mai toate operațiile de rețea.
Deoarece operația de adresare a unei gazde printr-un număr este greoaie, fiecare calculator conectat la internet poate avea un nume, care să-l facă mai ușor de identificat. De asemenea, rețeaua din care face parte are un nume ce indică structura gazdelor ce o compun. Rețeaua poate face parte dintr-o altă rețea care la rândul ei are un nume sugestiv, relativ la rețelele care o compun. Cu alte cuvinte, Internetul se împarte în rețele cu un anumit specific care pot fi, în mod natural, denumite domenii.
Astfel o gazdă capătă un nume pe Internet, format din numele său, numele subdomeniului, numele domeniului, etc. De exemplu www.yahoo.com specifică gazda care oferă servicii de „web” din cadrul firmei Yahoo, care se încadrează în domeniul comercial.
Marile domenii ale Internetului se împart în două categorii: generice și nume de țări. Printre cele generice se regăsesc .com (domeniu comercial), .edu (educațional), .org (organizații nonprofit), .mil (militar), .net (alte rețele non-profit). Domeniile pentru țări sunt formate din două litere care identifică țara respectivă, la fel ca în sistemul de poștă: .ro (România), .uk (Marea Britanie), .de (Germania), .fr (Franta), etc.
La fel ca gestionarea pachetelor IP, care presupune că fiecare rețea are unul sau mai multe rutere specializate pentru acest lucru, pentru găsirea unui nume de gazdă (adică legătura dintre numele de domeniu și adresa IP) fiecare rețea are un rezolvitor (resolver) de nume de domeniu. Un astfel de rezolvitor cunoaște numele tuturor gazdelor din rețeaua locală, pe de o parte, și mai știe pe cine să întrebe despre numele de domenii pe care nu le cunoaște.
DNS este un sistem ierarhic, structurat sub formă de arbore. Baza acestui arbore se notează cu ‘.’ și se numește rădăcină (root). Sub . se regăsesc domeniile internetului, denumite domenii de prim rang (TLD – Top Level Domains), care sunt ramuri ale arborelui derivate din rădăcină. Când se face căutarea unei gazde, interogarea se face recursiv în ierarhie, pornindu-se de la vârf. De exemplu, pentru a afla adresa pentru prep.ai.mit.edu trebuie mai întâi gasit un server de nume care se ocupă de TLD-ul edu. Majoritatea serverelor de nume cunosc serverele pentru rădăcină, care sunt specificate într-un fișier numit root.hints. În continuare se parcurge arborele căutându-se serverul de nume pentru mit.edu și așa mai departe. Dacă prep.ai.mit.edu este o gazdă și nu un domeniu, el poate fi privit ca o frunză a arborelui. În consecință, un subdomeniu trebuie să spună rezolvitorului domeniului de care aparține (tatăl din arborele descris mai sus) cum se numește și cine se ocupă de rezolvarea numelor în subdomeniul respectiv.
Deoarece regăsirea numelui de domeniu a unui calculator este aproape la fel de importantă ca folosirea adresei de IP a acestuia, este util de știut cum se configurează un server de nume de domeniu. Exemplul folosit este de pe un sistem UNIX, dar majoritatea sistemelor au adoptat aceeași formă de structurare a fișierelor de definiție a zonelor de domenii.
Fișierul de configurare (/etc/named.conf):
options {
directory "/etc/named.db"; // use current directory
forward first;
forwarders {
192.129.4.1
193.226.102.10;
};
};
zone "grozav.unibuc.ro" {
type master;
file "grozav.unibuc.ro";
notify yes;
};
zone "1.168.192.IN-ADDR.ARPA"{
type master;
file "1.168.192.in-addr.arpa";
notify yes;
};
zone "10.168.192.IN-ADDR.ARPA"{
type master;
file "10.168.192.in-addr.arpa";
notify yes;
};
zone "." {
type hint; // used to be specified w/ "cache"
file "cache.db";
};
Serverul de nume rulează un demon (daemon) de nume, numit ndc. Acesta preia cereri pentru rezolvare de nume, în mod normal pe portul 53, și pe baza opțiunilor specificate în fișierul de configurare. Fișierul de configurare conține o parte de opțiuni și o parte de definiții de zone.
În exemplul de mai sus în opțiuni se specifică două alte servere de nume care pot răspunde cererilor, pentru a nu se mai apela la parcurgerea întregului arbore. Definiția de zone cuprinde pe de o parte zona ., necesară, și pe de altă parte celelalte nume de subdomenii gestionate. Un server de nume poate să fie de tip master (principal) sau slave (ajutător) pentru o zonă.
Dacă un server de nume primește o cerere el vede mai întâi dacă domeniul gazdei cerute se află într-una din zonele definite (mai puțin .). Dacă da, folosește fișierul de zonă respectiv și răspunde cererii. Dacă nu, trimite cererea unui dintre „apropiați” (forwarders). Dacă nici aceștia nu reușesc să o rezolve, cererea este trimisă unuia dintre serverele din zona rădăcină.
Fișierul de zonă (/etc/named.db/grozav.unibuc.ro):
@ IN SOA grozd.unibuc.ro. root.grozd.unibuc.ro. (
2001020900 ; serial
3600 ; refresh
900 ; retry
1209600 ; expire
43200 ; default_ttl
)
IN NS grozav.unibuc.ro.
IN MX 10 grozav.unibuc.ro.
IN A 192.129.3.11
grozav IN A 192.168.1.1
Server IN A 192.168.1.1
www IN CNAME Server
Swan IN A 192.168.1.2
Dracula IN A 192.168.1.3
Ics IN A 192.168.1.4
Sorin IN A 192.168.1.5
Geo IN A 192.168.1.6
Tec IN A 192.168.1.7
June IN A 192.168.1.8
Strut IN A 192.168.1.9
Wild IN A 192.168.1.10
Strumph IN A 192.168.1.11
Danezu IN A 192.168.1.12
Rococo IN A 192.168.1.13
Un fișier de zonă cuprinde mai multe înregistrări de resurse (RR – resource records). O înregistrare de resurse este de forma:
Fișierul de nume inverse (/etc/named.db/1.168.192.in-addr.arpa):
@ IN SOA grozav.unibuc.ro. root.grozav.uniuc.ro. (
2001020900
10800
3600
3600000
86400 )
IN NS grozav.unibuc.ro.
1 IN PTR Server.grozav.unibuc.ro.
2 IN PTR Swan.grozav.unibuc.ro.
3 IN PTR Dracula.grozav.unibuc.ro.
4 IN PTR Ics.grozav.unibuc.ro.
5 IN PTR Sorin.grozav.unibuc.ro.
6 IN PTR Geo.grozav.unibuc.ro.
7 IN PTR Tec.grozav.unibuc.ro.
8 IN PTR June.grozav.unibuc.ro.
9 IN PTR Strut.grozav.unibuc.ro.
10 IN PTR Wild.grozav.unibuc.ro.
11 IN PTR Strumph.grozav.unibuc.ro.
12 IN PTR Danezu.grozav.unibuc.ro.
13 IN PTR Rococo.grozav.unibuc.ro.
În practică este necesară și aflarea numelui de domeniu știindu-se adresa de IP. Acest lucru este posibil prin definirea unor zone speciale, care se termină cu .in-addr.arpa. Procedeul este simplu: protocolul DNS tratează adresa de IP ca și cum ar fi un nume de domeniu și încearcă să-l rezolve. Sistemul ierarhic DNS se face pe un nume de domeniu de la dreapta la stânga, în timp ce împărțirea în clase și în subrețele (ierarhizarea) adreselor IP se de la stânga la dreapta. Pentru consecvență, adresele IP tratate ca nume de domeniu se notează invers, ca în exemplul de mai sus. O zonă de nume inverse conține doar înregistrări de tipul PTR (exceptând SOA și eventual NS). Valoarea câmpurilor nume domeniu se compune la fel (de exemplu 1 înseamnă de fapt 1.1.168.192.in-addr.arpa. care este o convenție pentru 192.168.1.1), iar în câmpul valoare trebuie specificate nume de domeniu complete.
Observație:
Între numele de domeniu, respectiv stuctura ierarhică a acestora, și adresele de IP, respectiv împărțirea acestora în clase și subrețele, nu există nici o legătură. De exemplu un provider de Internet, care are un nume de domeniu comercial (.com), poate furniza servicii unei organizații non-profit, punându-i la dispoziție o subrețea. Numele de domeniu al acestei organizații poate fi .org sau .edu fără nici o restricție din partea firmei furnizoare de internet. Rețeaua grozav.unibuc.ro este o subrețea a rețelei roedu.net.
Nivelul transport
4
Nivelul transport este poate cel mai important din structura unei rețele, atât în modelul de referință OSI, dar în mod special în modelul TCP/IP. Specificațiile OSI spun că nivelul rețea trebuie să ofere servicii orientate pe conexiune și servicii de tip datagramă. În modelul TCP/IP nivelul rețea, respectiv Internet, oferă doar servicii de tip datagramă.
Nivelul de transport trebuie să ofere ambele tipuri de servicii. În plus, acest nivel trebuie să ofere servicii sigure, care nu pierd date. Cu alte cuvinte, nivelul transport trebuie să îmbunătățească calitatea serviciilor.
O altă sarcină este de a separa nivelurile dependente de rețea (de hardware-ul și tehnologia acesteia) de nivelurile independente de suportul fizic, care implementează protocoale/aplicații ce pot fi folosite pe o mare varietate de rețele. Din această cauză se face o distincție între nivelurile de sub nivelul transport, numite furnizoare de transport, și cele de deasupra, numite utilizatoare de transport.
Calitatea serviciului (QoS – Quality of Service)
Este ideal ca un serviciu să fie rapid, ieftin și sigur atât din punct de vedere al integrității datelor cât și a securității informației. Acest lucru nu este posibil în cazul rețelelor: dacă, de exemplu, se insistă asupra integrității datelor protocolul de verificare și eventual corectare a erorilor rezultă o transmitere mai lentă a datelor.
Parametri care pot varia prin folosirea diferitelor protocoale constituie calitatea serviciului. Dacă până la nivelul transport restul nivelurilor „se străduiau” să ofere o calitate cât mai bună din toate punctele de vedere, fără prea mari compromisuri, la acest nivel se pune accentul pe oferirea de servicii cât mai apropiate de necesitățile utilizatorile de transport. De abia la acest nivel apare noțiunea de QoS în sensul terminologiei rețelelor.
Nivelul transport permite utilizatorului specificarea unor valori minime, acceptabil și preferabile pentru parametrii de calitate ai serviciului oferit. Entitatea transport decide dacă poate furniza servicii care să respecte acești parametri sau nu. Puține protocoale folosesc toți parametrii de calitate a serviciului posibili, rezumându-se la a încerca să reducă rata reziduală a erorilor.
Observație:
Chiar dacă parametri ai calității serviciului sunt aplicabili și pentru serviciile neorientate pe conexiuni, în esență QoS se referă la serviciile sigure, orientate pe conexiune.
Primitive de servicii și protocoale de transport
La nivelul transport există două tipuri de servicii: servicii sigure, orientate pe conexiune, și servicii nesigure, de tip datagramă. Pentru TCP/IP serviciul sigur este oferit de protocolul TCP, iar cel datagramă este oferit de protocolul UDP. Atât pentru cazul practic (UDP), cât și în cazul general pentru nivelul protocol serviciile de tip datagramă nu sunt interesante, ele oferind doar o îmbunătățire a serviciilor de la nivelul rețea.
În schimb, serviciile sigure folosesc protocoale destul de complicate pentru a asigura conexiunea. Aceste protocoale trebuie să rezolve probleme ca stabilirea conexiunii, eliberarea acesteia sau controlul fluxului (care s-au dovedit a nu fi simple). Pe de altă parte serviciile pe care le pun la dispoziție sunt foarte utilizate și de aceea trebuie să fie ușor de folosit.
În cazul clasic al celui mai simplu protocol de transport, primitivele serviciului sunt următoarele:
Pentru a desemna forma de transimisie a datelor între entitățile de transport pereche se folosește termenul de unitate de date a protocolului de transport (TPDU – Transport Protocol Data Unit).
Pentru acest serviciu simplu se presupune că există un server și un număr oarecare de clienți care doresc să comunice. Pentru început serverul apelează primitiva LISTEN și blochează entitatea de transport (serverul se blochează) până se primește o cerere de conectare. Un apel al primitivei CONNECT pe unul din clienți blochează entitatea de transport de pe client, după ce aceasta trimite un TPDU ce conține CONNECTION REQUEST. La primirea acestui TPDU, entitatea server se deblochează și trimite înapoi entității client un TPDU de tip CONNECTION ACCEPTED, deblocând clientul.
Din acest moment conexiunea este stabilită și cele două capete pot schimba informație folosind primitivele SEND și RECEIVE. În cel mai simplu scenariu, una din părți apelează primitiva RECEIVE și blochează entitatea, în timp ce cealaltă parte apelează primitiva SEND. Cea de-a doua entitate trimite un TPDU cu date către prima și o deblochează. După aceasta procesul se poate desfășura invers sau se poate trimite o confirmare a pachetelor.
Pentru a se elibera conexiunea se poate opta pentru două variante: deconectarea simetrică sau asimetrică. În varianta asimetrică oricare dintre utilizatori poate apela primitiva DISCONNECT, ca urmare entitatea de transport va trimite un TPDU cu DISCONNECT REQUEST după care va închide conexiunea. La recepționarea TPDU-ului, entitatea pereche va închide la rândul ei conexiunea. În cea dea doua variantă, conexiunea este eliberată numai după ce pentru amândouă entități a fost apelată primitiva DISCONNECT. Când unul dintre capete apelează această primitivă, entitatea trimite TPDU ce conține DISCONNECT REQUEST. Prin acest TPDU el informează entitatea pereche că nu mai are date de trimis, dar poate în continuare să primească date. Când și la celălalt capăt se apelează primitiva DISCONNECT, se trimite TPDU-ul corespunzător și se eliberează conexiunea.
Acesta este cel mai simplu model de servicii de transport. În practică serviciile sunt ceva mai complicate, iar primitivele mai numeroase, dar principiul de funcționare este același.
Utilizatorului care folosește serviciile de transport îi este transparent modul în care protocolul de transport stabilește conexiunea, controlează erorile din pachetele de date sau eliberează o conexiune.
Atunci când un proces aplicație dorește să stabilească o conexiune cu un proces aflat la distanță el trebuie să specifice cu care proces dorește să se conecteze. Metoda folosită în mod normal este de a defini adrese de transport la care procesele pot să aștepte cereri de conexiune. Serviciile fiecărui nivel sunt accesibile prin punctele de access la serviciu SAP, În cazul nivelului de transport acestea se numesc puncte de acces la serviciile de transport (TSAP – Transport Services Acces Point). Analog pentru rețea ele sunt NSAP (Network Services Acces Point). De exemplu, pentru TCP/IP, NSAP-urile sunt adresele IP, iar TSAP-urile sunt porturile.
În general un calculator are un singur NSAP și mai multe TSAP-uri. La nivel transport sunt conectate două entități după TSAP-urile care le identifică. Pentru a afla TSAP-ul unui proces de pe o mașină oarecare există două variante. Prima variantă ar fi ca anumite procese să ruleze la niște TSAP-uri specifice, cunoscute de toată lumea și rezervate doar acestui tip de procese. Pe lângă acestea, procesul trebuie să ruleze în permanență pentru a accepta cereri de conectare.
Cealaltă variantă este cunoscută sub numele de protocolul de conectare inițială. Un server care oferă servicii rulează o aplicație de tip server de procese, care așteaptă conexiuni pe mai multe TSAP-uri. Atunci când un client încearcă să se conecteze la un proces folosește un TSAP oarecare, unde va răspunde serverul de procese. Acesta va lansa procesul dorit și va transfera conexiunea noului proces. Pe lângă serverul de procese mai există și posibilitatea folosirii unui server de nume, care să cunoască TSAP-ul mai multor procese după denumirea lor. Un client se conectează la serverul de nume și îi cere TSAP-ul pentru un anumit proces. Aceste două servere rezolvă atât problema numărului de procese care trebuie să fie active cât și a folosirii de TSAP diferite de către un proces.
Paradoxal, în implementarea unui protocol de transport ce oferă servicii orientate pe conexiuni cea mai dificilă problemă este stabilirea unei conexiuni. Această problemă se datorează faptului că rețeaua poate pierde, memora sau duplica pachete. În mod normal entitatea de transport trimite un TPDU de tip CONNECTION REQUEST(CR) și așteaptă primirea unui TPDU de tip CONNECTION ACCEPTED(ACK) de la gazda a doua. Scenarii posibile în funcționarea defectuasă a stabilirii unei conexiuni sunt: TPDU-ul CR este duplicat de către rețea și ca urmare gazda a doua va deschide două conexiuni; TPDU-ul ACK este pierdut și prima gazdă așteaptă nedefinit să se deblocheze. Soluția cea mai răspândită folosește o durată de viață a pachetelor în rețea (TTL – Time To Live) și un număr de secvență pentru TPDU. Procedura în trei pași este următoarea: gazda1 trimite CR(x), gazda2 răspunde cu ACK(x, y) iar în final gazda1 confirmă alegerea lui z în primul mesaj de date DATA(x, y).
Pentru eliberarea unei conexiuni principala problemă este pierderea pachetelor. Dacă gazda1 trimite un TPDU DISCONNECT REQUEST (DR), acesta poate să nu ajungă la gazda2. Astfel, gazda2 va menține conexiunea deschisă și va continua să trimită date. Dacă gazda1 dorește o confirmare pentru DR aceasta poate să nu ajungă și de data aceasta celălalt capăt va menține deschisă o conexiune invalidă. Rezolvarea acestei probleme se face, în general cu un ceas (pentru fiecare entitate). La expirarea unui anumit interval de timp în care nu s-a primit confirmarea se eliberează automat conexiunea și/sau se trimite un TPDU de tip DR.
Protocolul de transport mai trebuie să rezolve și alte probleme, legate de fluxul de date. Dintre acestea cele mai importante sunt confirmarea pachetelor, spațiul de memorie tampon (buffer) necesară stocării pachetelor primite, inundarea unui receptor prea lent cu date, etc.
Protocolul de control al transmisiei (TCP)
Protocolul de control al transmisiei (TCP – Transmission Control Protocol) a fost proiectat explicit pentru a asigura un flux sigur de octeți de la un capăt la celălalt al conexiunii într-o inter-rețea nesigură. A fost definit în mod oficial în RFC 793, iar extensiile ulterioare au fost expuse în RFC 1323.
Entitatea de TCP este un proces sau o parte a nucleului unei mașini și gestionează fluxuri de date și interfața către nivelul IP. Entitatea TCP acceptă fluxuri de date utilizator pe care le împarte în fragmente ce nu depășesc 64k (de obicei 1500). Aceste fragmente sunt trimise ca datagrame IP separate. La destinație fragmentele sunt verificate și reansamblate într-un flux de octeți accesibil utilizatorului.
Serviciul TCP este o conexiune duplex integral, punct-la-punct care oferă un flux sigur de octeți. Duplex integral înseamnă că traficul se poate desfășura simultan în ambele direcții. Punct-la-punct desemnează o conexiune cu exact două puncte finale, fără posibilitate de difuzare parțială sau totală. Fluxul de octeți, spre deosebire de fluxul de mesaje, nu păstrează mărimea mesajelor de la un capăt la celălalt: de exemplu un mesaj de 2048 octeți poate ajunge ca două de 1024 octeți și invers. Serviciul TCP este obținut prin crearea atât de către emițător, cât și de către receptor, a unor puncte finale, definite în mod unic prin NSAP (respectiv adresă IP) și TSAP (respectiv un număr de 16 biți numit port). Conexiunea TCP se poate indentifica prin cele două puncte finale.
Observație:
Inițial doar porturile mai mici de 256 erau considerate rezervate pentru aplicații dedicate (de exemplu 21 – FTP, 80 – HTTP, 23 – TELNET, etc), dar în prezent este recomandabilă folosirea porturilor mai mari de 1024. Aceste porturi se numesc porturi general cunoscute și sunt prezentate în RFC 1700.
Protocolul TCP este compus din specificația de antet TCP, PDU numit segment de date, controlul conexiunii și gestionarea datelor. Antetul TCP are o dimensiune fixă de 20 octeți. Un segment de date conține un antet TCP și 0 sau mai muți octeți. Controlul conexiunii reprezintă modalitatea de stabilire și eliberare a conexiunii, precum și rezolvarea problemelor legate de acestea. Gestionarea datelor se referă la confirmarea pachetelor, controlul congestiilor și detectarea erorilor.
Antetul TCP are 20 de octeți și este inclus în orice segment de date. După antet pot urma mai multe opțiuni și date până la o dimensiune de 65515 octeți (40 de octeți fiind folosiți de antetul TCP și antetul IP). Segmente care nu conțin date după antet sunt des folosite pentru confirmări și mesaje de control. Antetul este format din zece câmpuri:
În administrarea conexiunii, TCP folosește „înțelegerea în trei pași”. Pentru a stabili o conexiune serverul așteaptă în mod pasiv o cerere de conexiune prin execuția primitivelor LISTEN și ACCEPT. Clientul execută primitiva CONNECT, care trimite serverului un segment cu SYN=1 și ACK=0. Entitatea TCP de pe serverul verifică dacă există un proces care așteaptă la portul cerut de client, în caz contrar trimițând un packet cu RST=1. Dacă procesul există, cererea de conexiune îi este pasată. Procesul poate refuza sau accepta conexiunea. Dacă o acceptă va trimite un segment de confirmare, cu SYN=1 și ACK=1.
Pentru eliberarea unei conexiuni, una din părți trimite un segment cu FIN=1. La confirmarea acestui segment, legătura rămâne deschisă doar într-un sens. Dacă și cealaltă parte trimite FIN=1, el este confirmat cu ACK=1 și conexiunea este eliberată. În plus, la emiterea unui segment de terminare este pornit un cronometru. Dacă segmentul nu este confirmat într-un anumit interval conexiunea este automat eliberată.
Funcționarea algoritmului de stabilire și eliberare a conexiunii poate fi descrisă printr-un automat finit determinist. Stările automatului sunt următoarele:
În figura de mai jos sunt prezentate tranzițiile automatului. Linia groasă reprezintă cazul comun al unui client conectându-se la un server. Linia groasă întreruptă reprezintă pașii serverului în mod normal în stabilirea și eliberarea conexiunii. Liniile subțiri sunt evenimente posibile, dar mai puțin obișnuite. Liniile sunt etichetate prin eveniment/acțiune. Evenimentul poate fi apelarea unei primitive de către utilizator, recepționarea unui segment sau expirarea unui interval egal cu dublul duratei de viață a unui pachet. Acțiunea reprezintă transmiterea unui segment de control sau „nici o acțiune”, specificată cu –.
Protocolul de bază utilizat de către entitățile TCP este protocolul cu fereastră glisantă, de dimensiune variabilă. (Protocolul cu fereastră glisantă: Atât emițătorul cât și receptorul au câte o fereastră de numere de secvență pe care le acceptă. Dacă emițătorul are de trimis un pachet, el va crea un nou număr de secvență pentru acesta și va adăuga noul număr la fereastra de transmisie. Dacă emițătorul primește un pachet de confirmare pentru un anumit număr de secvență, el va elimina acest număr din fereastra de transmisie. De cealaltă parte, dacă receptorul primește un pachet care are un număr de secvență ce se încadrează în fereastra de recepție el va confirma acest pachet și va elimina numărul său din fereastră. Atât receptorul, cât și emițătorul ignoră pachetele cu număr de secvență ce nu se încadrează în secvență. )
Atunci când un emițător transmite un segment, el pornește un cronometru. Atunci când un segment ajunge la destinație, entitatea TCP receptoare trimite înapoi un segment (cu informație utilă, dacă aceasta există, sau fără informație) de confirmare și care mai conține și numărul de secvență următor pe care aceasta se așteaptă să-l recepționeze. Dacă cronometrul emițătorului depășește o anumită valoare înaintea primirii confirmării, emițătorul retransmite segmentul neconfirmat.
De observat este că fiecare octet are un număr de secvență. De exemplu, dacă sunt trimiși 1024 de octeți de date într-un segment ce are numărul de secvență din antet 1024, numărul de secvență al ultimului octet este 2048. În segmentul de confirmare trimis înapoi, numărul de confirmare va fi 2049 și semnifică numărul de secvență al următorului octet ce se așteaptă a fi primit.
Entitatea TCP memorează datele în zone tampon de transmisie și de recepție. Dimensiunea maximă a ferestrei în TCP este dimensiunea zonei tampon corespunzătoare de care entitatea TCP dispune. Entitatea TCP nu transmite date imediat ce sunt primite, nici către entitatea pereche, nici către aplicație. De exemplu, dacă utilizatorul dorește să transmită 1024 de octeți, iar tamponul de transmisie este de 2048, entitatea TCP așteaptă până acesta se umple și apoi trimite datele. Invers, dacă entitatea TCP primește 512 octeți de la entitatea pereche, îi depozitează în zona tampon de recepție care are 1024 de octeți și trimite înapoi un segment de confirmare în care indică dimensiunea ferestrei de 512 octeți.
Mai mult, confirmarea poate fi și ea întârziată un anumit interval de timp, în speranța umplerii zonei tampon sau a apariției unor date.
Excepțiile pe care le permite protocolul TCP sunt informațiile urgente și segmentele de un octet pentru cazul în care fereastra de recepție are dimensiunea 0. Informațiile urgente sunt trimise imediat de către entitatea TCP, chiar dacă zona tampon nu este plină sau dimensiunea ferestrei este 0. Segmentele de un octet sunt trimise pentru a forța receptorul să reanunțe următorul octet așteptat și dimensiunea ferestrei. Acestea previn interblocarea în cazul în care anunțarea unei ferestre este pierdută.
Congestia datelor într-o conexiune poate apărea atât din cauza capacității rețelei cât și a capacității receptorului. Pentru prevenirea congestiei emițătorul menține două ferestre: fereastra aceptată de receptor și fereastra de congestie. Numărul octeților care pot fi trimiși este minimul dintre cele două ferestre.
La stabilirea conexiunii, emițătorul inițializează fereastra de congestie la dimensiunea celui mai mare segment utilizat în conexiune. El trimite apoi un segment de această dimensiune. Dacă este confirmat, va dubla dimensiunea ferestrei de congestie și va încerca să transmită un segment de această dimensiune. Acest procedeu continuă până când segmentul de test nu este confirmat.
Protocolul de datagrame utilizator (UDP)
Setul de protocoale Internet suportă de asemenea un protocol de transport fără conexiune. UDP (User Datagram Protocol) oferă aplicațiilor o modalitate de a trimite datagrame IP neprelucrate încapsulate. La fel ca la TCP, TSAP-ul în acest protocol este numit port și are aceeași semnificație. Spre deosebire de TCP, UDP permite transmiterea multiplă prin folosirea adreselor IP de clasă D. Transmiterea datelor se face într-un singur sens și este folosită în aplicații de tip client-server. Antetul segmentelor UDP au dimensiunea de 8 octeți și conțin 4 câmpuri:
Pentru ca segmentele transmise să fie recepționate este necesar ca pe mașina destinație să existe un proces UDP care să aștepte pasiv recepția datagramelor pe portul specificat în câmpul Port destinație.
Socluri Berkeley (Berkeley Sockets)
5
În capitolele anterioare au fost prezentate noțiuni de arhitectură a rețelelelor de calculatoare. Această arhitectură se bazează pe niveluri. Fiecare nivel se compune dintr-o serie de servicii puse la dispoziția nivelului superior printr-o interfață și din mai multe protocoale care descriu aceste servicii. Fiecare serviciu este accesibil prin puncte de acces la serviciu.
În modelul TCP/IP nivelul transport pune la dispoziție două tipuri de servicii: un serviciu orientat pe conexiune și unul fără conexiune. Punctele de acces la aceste servicii se numesc porturi. Identificarea unei mașini din rețea se face prin adresa IP. Două perechi (adresă IP, port) identifică o conexiune, în cazul serviciului orientat pe conexiune.
Ce sunt soclurile Berkeley
Un soclu Berkeley este o pereche (adresă IP, port) și desemnează un punct final de comunicație în modelul TCP/IP. Deoarece serviciile de la nivelul transport se identifică prin punctele sale finale, soclurile Berkeley se pot identifica cu serviciile acestui nivel. Mai exact, primitivele pentru socluri coincid cu primitivele serviciilor de transport TCP/IP.
Observație:
În cazul UDP există posibilitatea de trimitere multiplă. Acest serviciu poate fi identificat de două socluri Berkeley prin folosirea unei adrese IP de clasă D. Se obține astfel o singură pereche dublă (adresă IP, port) care identifică serviciul.
La nivelul aplicație soclurile pot fi privite ca o formă de comunicație între procese (IPC – Inter Process Communication). Spre deosebire de alte IPC-uri, soclurile permit comunicația între platforme.
Soclurile Berkeley au fost preluate, sau implementate, și pentru alte servicii de transport decât TCP/IP și chiar pentru scopuri locale de tip IPC. Astfel soclurile au devenit un standard de interfață de tip punct final de comunicație, ce se caracterizează prin „familia” căreia aparțin și tipul serviciului oferit.
Soclurile pentru serviciile de transport TCP/IP se încadrează în familia INET și furnizează servicii de tip STREAM (flux de octeți) și DATAGRAM (datagramă). Alte familii posibile mai sunt IPX, X25, APPLETALK, etc și desemnează nivelul transport din modelul/suita de protocoale corespunzătoare. Tipul serviciilor oferite mai poate avea valori ca SEQPACKET (secvență de pachete), RDM (datagrame garantate dar neordonate), RAW (acces direct la serviciile nivelului rețea).
Distincția între serviciile desemnate se face prin specificarea tipului de soclu. Această modalitate de selecție a serviciului este în sensul arhitecturii structurate pentru rețele și poate fi privită ca cea mai simplă formă de specificare a calității serviciului (QoS). Mai mult, primitivele soclurilor permit specificare mai multor parametri pentru serviciul oferit, dependente sau independente de protocolul utilizat. Acest lucru face ca soclurile să fie o interfață puternică de nivel transport.
Termenul de socket se traduce prin priză. Se poate face o analogie între socluri și priza ISDN: aceasta oferă posibilitatea de conectare a unui telefon, televizor sau calculator la rețeaua ISDN, fiecare folosind un serviciu și un protocol specific.
De exemplu, pentru telefon utilizatorul ridică receptorul, formează numărul, vorbește și închide. La fel, o aplicație conectată la un soclu deschide o conexiune, trimite date și, în final, închide conexiunea.
Un post de televiziune transmite în permanență un program pe rețeaua ISDN. Utilizatorul deschide televizorul, selectează programul și îl poate închide fără a anunța pe nimeni. Analog, un server trimite datagrame la un grup de adrese, pe un anumit port. O aplicație client creează un soclu pe acest port și „aderă” la grupul de adrese al serverului. După un timp în care primește datagrame de la server, clientul poate părăsi grupul.
Denumirea de soclu Berkeley vine de la prima implementare a soclurilor, apărută în distribuția BSD – UNIX 4.2, distribuție dezvoltată la universitatea Berkeley, California. În continuare prezentarea soclurilor se va rezuma la a celor din familia INET, adică cele folosite în Internet și care descriu nivelul transport din TCP/IP. Acestea sunt, de altfel, și cele mai răspândite.
Primitivele soclurilor Berkeley
Primitivele soclurilor Berkeley descriu serviciile oferite de către TCP și UDP. Deși numărul acestora este mai mare, iar definiția lor poate varia de la un sistem la altul, un număr de opt primitive sunt de bază. Primele patru primitive sunt executate, în această ordine, de server. Primitiva SOCKET creează un nou capăt al conexiunii și alocă spațiu pentru el în tabelele entității de transport. În apelarea acestei primitive se specifică formatul de adresă utilizat, tipul de serviciu dorit și protocolul. Un apel SOCKET reușit întoarce un descriptor de fișier (similar unui apel OPEN de fișiere) care va fi folosit ulterior în apelurile celorlalte primitive.
Soclurile nou create nu au încă nici o adresă. Atașarea unei adrese se face utilizând primitiva BIND. Odată ce un server a atașat o adresă unui soclu, clienții se pot conecta la el.Motivul pentru care apelul SOCKET nu creează adresa direct este că unele aplicații se îngrijesc de adresa lor (de exemplu, unele folosesc aceeași adresă de ani de zile și oricine cunoaște această adresă), în timp ce altele nu.
Urmează apelul LISTEN, care alocă spațiu pentru a reține apelurile primite în cazul când mai mulți clienți încearcă să se conecteze în același timp. Pentru a se bloca și a aștepta un apel, serverul execută o primitivă ACCEPT. Atunci când sosește un TPDU care cere o conexiune, entitatea de transport creează un nou soclu cu aceleași proprietăți ca cel inițial și întoarce un descriptor de fișier pentru acesta. Serverul poate atunci să creeze un nou proces sau fir de execuție care va gestiona conexiunea de pe noul soclu și să aștepte în continuare cereri de conexiune pe soclul inițial.
Din punct de vedere al clientului soclul trebuie creat folosind primitiva SOCKET. Primitiva BIND nu mai este necesară, deoarece adresa folosită nu mai este importantă pentru server. Primitiva CONNECT blochează apelantul și demarează procesul de conectare. Când acesta s-a terminat (adică atunci când TPDU-ul corespunzător a fost primit de la server), procesul client este deblocat și conexiunea este stabilită. Atât clientul cât și serverul pot utiliza acum primitivele SEND și RECEIVE pentru a transmite sau recepționa date folosind o conexiune duplex integral.
Eliberarea conexiunii este simetrică. Atunci când ambele părți au executat primitiva CLOSE conexiunea este eliberată.
Observație: Soclurile TCP pot fi împărțite în două categorii: socluri-server și socluri-client. Un soclu-server ascultă și acceptă conexiuni pentru care creează socluri-client. Conexiunea efectivă este dată de două socluri-client care pot interschimba datele aplicației între ele. Un server are un soclu-server și mai multe socluri-client. Un client are un soclu-client.
Pentru soclurile UDP (datagramă) nu sunt folosite toate primitivele. Pentru a trimite o datagramă un server trebuie mai întâi să creeze un soclu folosind primitiva SOCKET, specificând că acesta este de tip datagramă. Apelul primitivei BIND este opțional. Trimiterea unei datagrame se face prin apelul unei primitive SENDTO, în care se specifică soclul destinație (adică adresă IP și port). Terminarea folosirii serviciului se face prin distrugerea soclului. Clientul, după crearea soclului, trebuie să apeleze primitiva BIND pentru a stabili o adresă și un port. Pentu recepționarea unei datagrame clientul apelează primitiva RECEIVEFROM, care blochează procesul până la recepționarea unei datagrame.
Proprietăți generale ale soclurilor
Unele apeluri de primitive ale soclurilor sunt „blocante”, adică blochează procesul care le-a executat până primesc date de la entitatea pereche. De exemplu primitivele RECEIVE și ACCEPT blochează procesul curent până se primesc octeți de la soclul pereche, respectiv până când apare o cerere de conexiune. Un soclu pentru care admite execuția de operații care blochează procesul se numesc socluri blocante (blocking sockets). Folosirea soclurilor blocante într-o aplicație este corelată, în majoritatea cazurilor, cu programarea multi-proces.
Soclurile permit și evitarea acestor blocări ale aplicației. Dacă se creează un soclu de tip non-blocant, apelul unei primitive blocante în condițiile în care zona de date primite (tampon) este goală duce la generarea unei erori de tipul EWOULDBLOCK. În general, soclurile care nu admite execuția de operații care ar blocha procesul curent se numesc socluri non-blocante (non-blocking sockets). Aceast tip de socluri face posibilă folosirea lor în sisteme care nu sunt multi-proces (de exemplu Windows 3.1).
Observație:
Un soclu non-blocant nu implică eliminarea unor primitive (chiar apar unele în plus, de obicei). În loc să blocheze procesul, primitivele respective întorc o eroare.
La implementarea unei aplicații trebuie avut în vedere dacă crearea unui proces (fir de execuție) pentru fiecare soclu este mai avantajoasă decât folosirea soclurilor de tip non-blocant. Să luăm următorul exemplu: o aplicație de transfer de fișiere. Dacă aplicația citește de pe soclu câte 1024 de octeți și fișierul de transmis are câteva sute de megaocteți o aplicație cu socluri non-blocante ar trebui să verifice de sute de mii de ori dacă soclul este disponibil pentru citire, ceea ce ar avea un impact dezastruos asupra performanței sistemului. Pe de altă parte, folosirea unui soclu blocant ar opri execuția programului pe o perioadă de câteva zile… Soluția: soclul care se ocupă cu recepționarea fișierului este lansat într-un nou proces (fir de execuție) care lucrează în paralel cu aplicația. Când s-a terminat recepționarea fișierului, acest nou proces este oprit și aplicația este informată de starea operației.
Un alt exemplu: o aplicație de tip server de discuție online. Să presupunem că ordinul utilizatorilor acestui server este de câteva mii. Dacă, în acest caz, s-ar folosi socluri blocante serverul ar trebui să aștepte după un anumit utilizator și să-și blocheze execuția, adică deservirea celorlalți, până cel așteptat trimite un mesaj. Dacă serverul ar lansa un fir de execuție pentru fiecare utilizator s-ar ajunge (teoretic) la mii de procese simultane, lucru care pur și simplu „omoară” sistemul gazdă. Singura soluție viabilă în acest caz o reprezintă soclurile non-blocante.
Calitatea serviciului accesat prin intermediul soclurilor este influențată de tipul soclului și alte opțiuni adiționale. Tipul soclului descrie tipul de serviciu care va fi folosit de către aplicație. Cele mai folosite sunt SOCK_STREAM și SOCK_DGRAM, dar în funcție de familia de adrese și protocoale aleasă mai pot exista și altele. Mai folosite sunt însă opțiunile suplimentare de “rafinare” a calității serviciului. Pentru stabilirea sau aflarea stării unei opțiuni se folosesc primitivele getsockopt și setsockopt. Aceste opțiuni se referă în principal la comportarea soclului (sunt de nivel SOL_SOCKET), dar pot afecta și comportamentul protocoalelor folosite. De exemplu opțiunea TCP_NODELAY se referă la comportamentul protocolului TCP, aflat pe nivelul IPPROTO_TCP din punct de vedere al soclurilor. În continuare sunt descrise opțiunile de bază care afectează comportamentul soclurilor:
Un alt aspect care trebuie avut în vedere la construirea aplicațiilor ce se bazează pe socluri este ordine a octeților. Un apel al unei primitive de soclu înseamnă folosirea unui serviciu de nivelul transport, adică un serviciu de rețea și nu local. Reprezentarea numerelor poate diferi pe mașini diverse. De asemenea, ea poate fi diferită pe mașina care folosește soclurile față de standardul de reprezentare a datelor în rețea. Ordinea octeților în reprezentarea cuvintelor sunt de două feluri:
Conform standardelor, protocoalele de rețea folosesc convenția Big-Endian de reprezentare a cuvintelor. Dacă o aplicație trimite într-un apel de primitivă informații ce trebuie interpretate de rețea, de exemplu specificarea portului și a adresei IP, aplicația trebuie să se asigure că folosește reprezentarea corespunzătoare a datelor. Acest aspect trebuie tratat și la transmiterea datelor între mașini cu modalități diferite de ordonare a octeților.
Pentru conversia datelor între cele două reprezentări există câteva funcții standard, care pot fi considerate ca aparținând primitivelor de socluri:
Aceaste funcții nu convertesc cuvintele decât dacă este necesar, în funcție de arhitectura gazdei. De exemplu, pe o mașină Machintosh, apelul oricărei funcții de mai sus întoarce același cuvânt cu care a fost apelată. Folosirea acestor funcții este indicată pentru portabilitate.
În apelul primitivelor soclurilor apar anumite structuri, care sunt de asemenea standard. Structura cea mai folosită este cea de adresă. Deoarece soclurile sunt o interfață pentru mai multe protocoale/arhitecturi, structura adresei de rețea are o formă generală:
struct sockaddr {
unsigned short sa_family; // familia adresei, AF_XXX
char sa_data[14]; // adresa de protocol, 14 octeți
};
Pentru TCP/IP familia adresei este AF_INET, iar pentru adresa de protocol (care este de fapt adresa IP împreună cu portul) se folosește următoarea structură:
struct sockaddr_in {
short int sin_family; // familia adresei
unsigned short int sin_port; // port
struct in_addr sin_addr; // adresa IP
unsigned char sin_zero[8]; // nefolosiți
};
Această structură este de fapt o interpretare a structurii de mai înainte. Ultimii opt octeți sunt menținuți pentru a avea aceeași dimensiune. Pe caz general o primitivă de soclu cere o strctură sockaddr, iar un apel în cazul în care familia este AF_INET va transmite o structură sockaddr_in („in” vine de la InterNet).
Structura in_addr este de fapt un cuvânt de 4 octeți (32 biți) care reprezintă adresa IP. Definiția ei ca structură se datorează modului în care poate fi privită (4 octeți separați prin . sau un cuvânt). Pe unele sisteme nu se face această reprezentare, folosindu-se în schimb macrouri. În orice caz, câmpul s_addr este conținut în orice reprezentare a acestei structuri.
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
Pentru manipularea adreselor de IP există două funcții standard de conversie între adresa văzută ca un șir de caractere (de exemplu: „192.168.1.13”) și cuvântul pe care aceasta îl reprezintă (pentru acest exemplu: 218212544 = 0xd01a8c0).
În apelul primei funcții se consideră că șirul de caractere conține rețea.gazdă. De exemplu, adresa 192.168.13 va fi interpretată ca 192.168.0.13. Analog, 192.13 va fi de fapt 192.0.0.13. Specificarea unor astfel de adrese este utilizată pentru adresele IP de clasă A și B. În cazul în care funcția nu reușește să interpreteze șirul de caractere ca o adresă IP corectă întoarce INADDR_NONE (= 0xffffffff).
Apelul celei de-a doua funcții are ca parametru o structură in_addr și întoarce totdeauna un rezultat valid (orice cuvânt de 4 octeți poate reprezenta o adresă IP).
Coduri de eroare
O parte, care nu trebuie în nici un caz neglijată, în abordarea soclurilor o reprezintă și codurile de eroare. În general, primitivele soclurilor care au de raportat o eroare întorc o valoare oarecare care să însemne că a apărut o eroare (de exemplu –1 în UNIX și INVALID_SOCKET în Windows) și stabilesc o nouă valoare pentru ultima eroare. Pentru a afla codul erorii apărute, aplicația trebuie să execute funcția GetLastError. Codurile de eroare sunt diferite de pe un sistem pe altul, dar există o serie de erori care se consideră standard pentru socluri. În general, codurile de eroare ce apar în manipularea soclurilor încep de la 10.000, după definiția inițială din BSD.
Socluri în UNIX
6
Prima implementare a soclurilor a fost făcută pe un sistem UNIX, respectiv BSD-UNIX (Berkeley Software Design – UNIX) versiunea 4.2. În timp această implementare a fost îmbunătățită în următoarele sisteme. La ora actuală definiția standard a soclurilor este considerată ca fiind cea implementată în versiunea 4.3 a aceleiași distribuții, BSD-UNIX.
Implementările soclurilor pe alte sisteme au condus inevitabil la modificarea unor specificații față de cea de pe sistemele UNIX. În orice caz, implementarea de bază de la care s-a pornit este cea de pe UNIX. De aceea soclurile în UNIX sunt o referință pentru orice programator de socluri, chiar dacă acesta dezvoltă aplicații pe alte sisteme.
În sistemele de tip UNIX se pune un foarte mare accent pe comunicația între procese (IPC – Inter Process Communication). De aceea, încă de la început, soclurile au fost gândite ca o formă de IPC. Spre deosebire de alte forme, de exemplu țevile (pipes), soclurile sunt destinate comunicației între mașini. Între IPC-urile de comunicație în rețea, soclurile sunt cele mai performante din punct de vedere al vitezei și resurselor folosite. Ele pot fi văzute ca funcții de nivel elementar (low-level functions), adică implică mai mult efort din partea programatorului în administrarea erorilor, a proceselor, etc.
O altă caracterisitcă a sistemelor UNIX este că totul este fișier. Orice dispozitiv din sistem este văzut ca fișier: de exemplu o placă de sunet este un fișier. Ceea ce se scrie în fișier se va auzi în boxe, iar citirea datelor din fișier oferă detalii despre starea plăcii. De aceea o conexiune este de fapt un fișier, iar soclurile sunt descriptori ai acestor fișiere. În acest caz particular asocierea conexiunie-fișier este foarte fericită: serviciul TCP este un flux sigur de octeți, care se aseamănă cu un fișier binar cu acces secvențial. La fel ca într-un fișier într-un flux de octeți nu se poate știi dacă o anumită secvență a fost scrisă o dată sau din mai multe părți. De asemenea citirea secvențială dintr-un fișier se face de la începutul fișierul și până la sfârșitul acestuia. Sfârșitul fișierului este echivalent cu inexistența altor date primite de la entitatea pereche într-o conexiune. Asupra unei conexiuni privită ca un fișier se pot face operații simple de intrare/ieșire: scriere, citire, închidere.
Sistemele UNIX au fost dezvoltate în limbajul C. De aceea și soclurile au fost implementate în C, ca parte din sistem. Implementările în alte limbaje se bazează tot pe definiția funcțiilor și structurilor din C, așa că este naturală prezentarea soclurilor relativ la acest limbaj de programare. Descrierea sintaxei și conceptelor limbajului C nu sunt subiectul acestei lucrări și sunt presupuse ca fiind cunoscute.
Specificarea soclurilor într-un sistem coincide cu descrierea funcțiilor puse la dispoziție de acesta și a structurilor care intervin în apelul funcțiilor. Pe lângă acestea mai sunt necesare și câteva reguli de combinare a acestor funcții și eventual funcții adiționale utile în gestionarea soclurilor. În lucrarea de față, specificarea soclurilor se va rezuma la cele legate de Internet (familia de adrese este AF_INET, iar protocoalele sunt TCP și UDP).
Headere de declarare soclurilor
Funcțiile și structurile soclurilor sunt declarate în câteva headere standard. Folosirea lor într-un program trebuie precedată de includerea acestor headere. La compilarea programelor ce conțin funcții de soclu nu sunt necesare nici un fel de opțiuni suplimentare deoarece aceste funcții sunt înglobate în librăria standard glibc (inclusă în mod implicit de compilator). Directorul de headere $INCLUDE$/ (de obicei /usr/include sau /include) conține mai multe subdirectoare. Funcțiile și structurile care se referă la socluri sau operații pe fișiere compatibile cu acestea sunt distribuite în subdirectore după cum urmează: în $INCLUDE$/ sunt headere ce conțin funcții generale, în $INCLUDE$/sys/ sunt headere cu definiții de bază pentru socluri, în $INCLUDE$/netinet/ sunt headere referitoare la adrese din familia AF_INET și în $INCLUDE$/arpa/ sunt funcții adiționale legate de adrese AF_INET.
Funcțiile care lucrează cu socluri se împart în mai multe categorii după tipul soclului și scopul folosirii lui. Astfel în folosirea soclurilor de tip datagramă (SOCK_DGRAM) se folosesc alte funcții decât în cazul celor de tip flux de date (SOCK_STREAM). De asemenea, funcțiile pe care le execută o aplicație de tip server diferă de cele executate de o aplicație de tip client. În sfârșit, pentru folosirea soclurilor non-blocante se folosesc câteva funcții suplimentare pentru control.
Funcții comune
O serie de funcții sunt comune atât pentru o aplicație de tip client, cât și pentru una de tip server, pentru socluri de tip datagramă și pentru socluri de tip flux de octeți. În principal aceste funcții se referă la crearea, închiderea și stabilirea opțiunilor soclului. Tot în această categorie intră și funcțiile de manipulare a adreselor și de conversie a ordinii de reprezentare a octeților. Prezentarea lor anterioară este valabilă și pentru sistemul UNIX (cu mențiunea că trebuie incluse header-ele corespunzătoare), deoarece aceste funcții au același comportament pentru orice sistem. De asemenea structurile prezentate au aceeași reprezentare și în UNIX, motiv pentru care nu va fi reluată o prezentare a lor.
Deși câteva dintre aceste funcții nu pot fi apelate pentru socluri de tip datagramă acestea vor fi prezentate ca fiind funcții comune. Motivația este că, în general, discuția despre socluri se referă la socluri pentru servicii orientate pe conexiune, cele de tip datagramă fiind considerate o extensie.
Funcția socket
Această funcție creează un nou punct final de comunicație și întoarce un descriptor de fișiere către acesta. Soclul nou creat nu are nici o adresă sau port atribuit.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Parametri:
Observație:
În contextul lucrării de față și, în general, pentru toate aplicațiile care folosesc socluri pentru Inernet valoarea parametrului domeniu este PF_INET. Pentru acest domeniu nu sunt implementate, în UNIX, decât protocoale pentru tipurile SOCK_STREAM, SOCK_DGRAM și SOCK_RAW. Cum tipul SOCK_RAW permite accesul direct la protocoalele nivelului rețea, conform concepției pe niveluri a arhitecturii rețelei, nu ar trebui folosit la nivelul aplcație. Aceste socluri de nivel elementar (low-level) sunt în general folosite pentru implementarea unor protocoale de transport specifice.
Valoare întoarsă:
Dacă sistemul reușește să creeze un nou soclu cu succes, această funcție va întoarce descriptorul de fișier corespunzător. În caz contrar valoarea returnată va fi –1 iar variabila externă errno va conține codul erorii apărute. Ca observație generală, toate funcțiile referitoare la socluri întorc valoarea –1 în caz de insucces, codul erorii fiind disponibil în errno.
O funcție utilă în identificarea erorii este void perror(char *mesaj) din stdio.h. Această funcție afișează „mesaj: descriere_eroare\n” la ieșirea standard de eroare.
Posibilele erori în execuția acestei funcții sunt (a se vedea descrierea erorilor pentru detalii):
Exemplu:
Următorul exemplu creează un soclu internet de tip flux de date.
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
int main( void )
{
int sockfd;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if( sockfd == -1)
{
perror(“Apel SOCKET”);
return 1;
}
return 0;
}
Funcțiile getsockopt și setsockopt
Cele două funcții permit verificarea și respectiv setarea valorilor pentru anumite opțiuni ale soclului. Aceste opțiuni sunt dependente de protocolul folosit. Pentru opțiuni specifice fișierelor în general se folosește funcția fcntl (a se vedea descrierea soclurilor non-blocante în UNIX).
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t optlen);
Parametri:
Observații:
Valoarea implicită a opțiunilor este, în general, bine aleasă pentru majoritatea aplicațiilor și nu necesită schimbare. Asupra unor opțiuni se pot face însă niște observații.
SO_KEEPALIVE: Dacă se specifică această opțiune, protocolul TCP va trimite la anumite intervale pachete de control, chiar dacă aplicația nu are de trimis, primit date. Dacă se constată dispariția pachetelor, conexiunea este închisă. O aplicație care setează această opțiune trebui să verifice la fiecare operație ulterioară dacă soclul mai este valid (adică dacă conexiunea mai există).
SO_DONTROUTE: Este indicat ca aplicațiile care sunt destinate exclusiv rețelelor locale, cu alte cuvinte datagramele nu au de trecut peste nici un ruter, să seteze această opțiune pentru îmbunătățirea performanțelor atât a aplicației în sine cât și a fluxului de date din rețea.
SO_BROADCAST: Un soclu care face difuzare încarcă rețeaua cu pachete. De asemenea, pachetele difuzate sunt tratate cu o prioritate scăzută și deci au mai puține șanse să ajungă la destinație. De aceea nu este încurajată folosirea acestei opțiuni. O alternativă o reprezintă trimiterea multiplă.
SO_REUSEADDR: După închiderea unui soclu, conexiunea nu este imediat închisă de către sistem (prin folosirea unui utilitar ca netstat se poate proba acest lucru). Conform protocolului TCP se mai așteaptă un interval TIME_WAIT de timp pentru a se asigura că întradevăr nu mai există date de primit (duplicate, pachete pierdute, etc). Valoarea lui TIME_WAIT poate varia între 30 de secunde și 4 minute, iar în tot acest timp cuplul (adresă, port) este indisponibil pentru aplicații. O aplicație de tip server care se restartează nu își permite să aștepte atât timp ca să poată să-și folosescă din nou portul specific. Opțiunea SO_LINGER nu rezolvă problema: în primul rând că trebuie executată pe client și, în al doilea rând, recepția de către server a indicatorului RST nu asigură că nu mai sunt pachete „rătăcite” destinate soclului curent. Singura opțiune rămâne SO_REUSEADDR. Deși neîncurajată, folosirea acestei opțiuni nu este atât de periculoasă: o conexiune este formată dintr-un cuplu (soclu1, soclu2). Dacă serverul redeschide soclu1 pentru a accepta conexiuni, este puțin probabil ca un client să se conecteze de pe soclu2.
Folosirea acestei opțiuni este, în orice caz, indicată doar pentru socluri-server (care așteaptă conexiuni) sau pentru socluri de tip datagramă. Un client nu ar trebui să folosească această opțiune.
SO_BINDDEVICE: Setarea acestei opțiuni trebuie făcută după apelarea funcției bind și necesită drepturi de root pentru succes. O variantă de folosire este apelarea bind cu parametru adresa IP a interfaței pentru care se dorește atașarea.
SO_ERROR: Această opțiune trebuie reținută dacă se lucrează cu socluri non-blocante și va fi reluată în secțiunea respectivă. Ca o motivație prezentăm următorul exemplu: se apelează connect pe un soclu non-blocant, după care se așteaptă cu select să se termine această operație. Faptul că operația de conectare s-a terminat nu asigură că soclul este conectat.
Valoare întoarsă:
La o execuție reușită, ambele funcții întorc valoarea 0. Dacă a apărut o eroare, funcțiile întorc –1 și codul de eroare se găsește în errno:
Funcția getsockname
În UNIX un soclu care este atașat unui cuplu (adresă, port) se consideră ca având un nume, care este tocmai acest cuplu înglobat într-o structură de tip sockaddr.
#include <sys/types.h>
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* name, socklen_t *namelen);
Parametri:
Observație:
Apelul acestei funcții înainte de atașarea soclului la o adresă (prin bind sau connect) va întoarce o adresă nulă (sin_port = 0 și sin_addr = 0) din familia de adrese specifice domeniului soclului (familiei de protocoale).
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Funcția getpeername
Această funcție este folosită pentru aflarea numelui soclului de la celălalt capăt pentru un soclu conectat su forma unei structuri sockaddr.
#include <sys/types.h>
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr* name, socklen_t *namelen);
Parametri:
Observație:
Această funcție se adresează exclusiv soclurilor de tip flux de date, soclurile datagramă neavând o conexiune (și implicit nu există partener al său). Dacă dimensiunea locației de memorie receptoare este prea mică, numele este trunchiat.
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Funcțiile recv și read
Aceste două funcții permit recepționarea datelor de pe un soclu conectat și sunt destinate exclusiv soclurilor de tip flux de date. Funcția read este standard pentru citire din fișiere. Funcția recv este specifică soclurilor având opțiunea suplimentară de specificare a unor indicatori. În loc de funcția recv se mai poate folosi și funcția mai generală readfrom, care este prezentată în secțiunea referitoare la socluri neorientate pe conexiune. Chiar mai mult, există o tendință de a se renunța la această funcție în viitor.
#include <unistd.h>
ssize_t read(int sockfd, void *buf, size_t buflen);
Observație: ssize_t înseamnă signed size_t, iar acesta din urmă este un short.
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, int buflen, unsigned int flags);
Parametri:
Observație:
Apelul acestor funcții se blochează (dacă soclul este blocant) în cazul în care nu există date în coadă până la recepționarea unor date. Dacă numărul de octeți din coadă este mai mic decât buflen, doar aceștia sunt puși în buf. Dacă numărul de octeți din coadă este mai mare decât buflen, restul octeților rămân în coadă.
Dacă conexiunea a fost închisă de la celălalt capăt se întoarce valoarea 0.
Valoare întoarsă:
La o execuție reușită, funcția întoarce numărul de octeți scriși în buf. În cazul în care soclul pereche a închis normal conexiune este întoarsă valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno. Pe lângă erorile indicate mai jos, aceste funcții pot să specifice și alte coduri de eroare rezultate de la diferite protocoale utilizate în manipularea conexiunii:
Exemplu:
Următoarea funcție citește toți octeții disponibili de la un soclu.
#include <unistd.h>
char *citeste( int sockfd )
{
char *total = NULL, partial[1024];
int cititi, stocati = 0;
int sockfd;
do {
cititi = read(sockfd, (void *)&partial, sizeof(partial));
if( cititi == -1 )
{
perror(“READ”);
return total;
}
if( cititi == 0 )
{
printf(“Conexiunea inchisa de la celalalt capat”);
return total;
}
if( !total )
total = (char *)malloc(cititi);
else
total = (char *)realloc(total, cititi + stocati + 1);
memcpy(total + stocati, partial, cititi);
stocati += cititi;
} while( cititi == sizeof(partial) );
total[stocati] = ‘\0’;
return total;
}
Funcțiile send și write
Aceste două funcții permit trimiterea datelor de pe un soclu conectat și sunt destinate exclusiv soclurilor de tip flux de date. Funcția write este standard pentru scriere în fișiere. Funcția send este specifică soclurilor având opțiunea suplimentară de specificare a unor indicatori.
#include <unistd.h>
ssize_t write(int sockfd, const void *buf, size_t buflen);
Observație: ssize_t înseamnă signed size_t, iar acesta din urmă este un short.
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, void *buf, int buflen, unsigned int flags);
Parametri:
Observație:
Apelul acestor funcții se blochează (dacă soclul este blocant) în cazul în care nu există date în coadă, până la recepționarea unor date. Dacă numărul de octeți din coadă este mai mic decât buflen, doar aceștia sunt puși în buf. Dacă numărul de octeți din coadă este mai mare decât buflen, restul octeților rămân în coadă.
Valoare întoarsă:
La o execuție reușită, funcția întoarce numărul de octeți trimiși din buf. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno. Pe lângă erorile indicate mai jos, aceste funcții pot să specifice și alte coduri de eroare rezultate de la diferite protocoale utilizate în manipularea conexiunii:
Funcțiile shutdown și close
Pentru a indica sistemului că un soclu nu mai este folosit și resursele pe care le ocupă pot fi eliberate trebuie apelată funcția close. În cazul soclurilor ce sunt parte a unei conexiuni înaintea închiderii soclului este necesară informarea partenerului de această acțiune prin apelul funcției shutdown.
#include <unistd.h>
int close(int sockfd);
#include <sys/socket.h>
int shutdown(int sockfd, int how);
Parametri:
Observație:
Omiterea apelării funcției shutdown nu este o greșeală fatală, dar este indicată închiderea conexiunii înainte de închiderea soclului. Omiterea apelării funcției close, în schimb, este mai gravă și poate duce la blocarea procesului soclului pereche.
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Funcții specifice aplicațiilor de tip server
O aplicație server, în mod normal, este formată dintr-un soclu-server și mai multe socluri-client. Toate soclurile implicate sunt de tip flux de date. Un soclu server așteaptă (pasiv) cereri de conectare. După ce s-a primit o astfel de cerere aplicația o acceptă și creează un nou soclu client. Soclul client nou creat și soclul care a inițiat cererea de conectare definesc o conexiune, respectiv conexiunea între aplicația server și o aplicație client. Funcțiile referitoare la aplicațiile de tip server sunt de fapt funcții pentru soclul-server.
Funcția bind
Conform limbajului folosit relativ la soclurile din UNIX, această funcție atribuie un nume unui soclu. Un soclu nou creat nu este atașat nici unei adrese sau port (aceasta se poate proba cu funcția getsockname). Dacă se dorește ca soclul să accepte conexiuni, sau să primească datagrame, acesta trebuie să fie asociat cu un port și o adresă care să fie ulterior folosită de clienți pentru conectare.
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *address, socklen_t addrlen);
Parametri:
Observație:
Pentru atașarea soclului pe o anumită interfață se poate preciza adresa acesteia în parametrul address sau se poate folosi ulterior opțiunea SO_BINDDEVICE.
Pnetru atașarea unui soclu pe un port general cunoscut (mai mic ca 1024) sunt necesare privilegii de root pentru aplicație.
Funcția bind permite asocierea soclurilor de tip datagramă cu adrese de difuzare sau de trimitere multiplă.
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Funcția listen
Această funcție face ca soclul să intre în stare de așteptare de conexiuni și este specifică soclurilor de tip flux de octeți. Apelul acestei funcții nu blochează procesul curent ci doar specifică disponibilitatea soclului de a accepta conexiuni.
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
Parametri:
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Funcția accept
Prin apelul acestei funcții se crează un nou soclu care reprezintă acceptarea unei cereri din coada unui soclu-server. Dacă soclul nu a primit nici o cerere de stabilire a vreunei conexiuni, procesul este blocat în așteptarea unei astfel de cereri.
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockadd *address, socklen_t addrlen);
Parametri:
Observație:
Apelul acestei funcții este blocant. Dacă soclul nu este blocant și nu există cereri de conexiuni în coadă se întoarce eroarea EAGAIN.
Valoare întoarsă:
La o execuție reușită, funcția întoarce un descriptor de soclu, de același tip ca soclul-server și care este capăt final al unei conexiuni cu aplicația client. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Exemplu: un server simplu
Următorul exemplu ilustrează folosirea funcțiilor prezentate anterior. Aplicația creează un soclu-server și așteaptă cereri de conexiune. Dacă o astfel de cerere este primită se acceptă conexiunea, după care se trimite un mesaj aplicației client. Conexiunea este închisă iar aplicația așteaptă o altă cerere de conexiune. După ce au fost acceptate 10 conexiuni serverul își încetează activitatea și „face curat”.
/*
server1.c: Server simplu cu socluri blocante.
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int serverfd, itemp, clientfd;
struct sockaddr_in sa;
socklen_t sizeof_sa;
char mesaj1[] = "Conexiunea a fost preluata.\n",
mesaj2[] = "Multumim pentru apel si mai reveniti!\n";
/* Crearea soclului-server */
printf("Se incearca crearea soclului-server…");
serverfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if( serverfd == -1 )
{
fflush(stdout);
perror("\nEroare in functia socket");
return -1;
} else
printf("\tOK\n");
/*Pentru soclul de ascultare se foloseste optiunea SO_REUSEADDR*/
printf("Se incearca setarea optiunii SO_REUSEADDR…");
itemp = 1;
if( setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, (void *)&itemp, sizeof(itemp)) == -1 )
{
fflush(stdout);
perror("\nEroare in functia setsockopt");
} else
printf("\tOK\n");
/* Atribuirea unui nume */
printf("Se incearca atribuirea unui nume pentru soclu…");
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = htonl(INADDR_ANY);
sa.sin_port = htons(8984);
if( bind(serverfd, (struct sockaddr *)&sa, sizeof(sa)) == -1 )
{
fflush(stdout);
perror("\nEroare in functia bind");
close(serverfd);
return -1;
} else
printf("\tOK\n");
/* Soclul se pune in stare de ascultare */
printf("Se incearca punerea soclului in mod ascultare…");
if( listen(serverfd, 10) == -1 )
{
fflush(stdout);
perror("\nEroare in functia listen");
close(serverfd);
return -1;
} else
printf("\tOK\n");
/* Se asteapta 10 conexiuni dupa care se iese */
for( itemp = 0; itemp < 10; itemp++ )
{
/* Se asteapta pentru o noua cerere si apoi se accepta */
clientfd = accept(serverfd, (struct sockaddr *)&sa, &sizeof_sa);
if( clientfd == -1 )
{
fflush(stdout);
perror("Eroare in functia accept");
close(serverfd);
return -1;
}
printf("Client connectat de la adresa %s si portul %d\n",
inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));
/* Se transmite ceva */
printf("\tSe transmite un mesaj clientului…");
if( write(clientfd, (void *)&mesaj1, strlen(mesaj1)) == -1 )
{
fflush(stdout);
perror("\n\tEroare in functia write");
} else
if( send(clientfd, (void *)&mesaj2, strlen(mesaj2), 0) == -1 )
{
fflush(stdout);
perror("\n\tEroare in functia send");
} else
printf("\tOK\n");
/* Se inchide conexiunea */
printf("\tSe inchide conexiunea…");
shutdown(clientfd, SHUT_RDWR);
close(clientfd);
printf("\tOK\n");
}
/* Se inchide soclul-server */
printf("Serverul si-a terminat activitatea\n");
close(serverfd);
return 0;
}
Funcții specifice aplicațiilor de tip client
În general funcțiile destinate aplicațiilor server nu sunt folosite pe aplicațiile client. De fapt, ele sunt incompatibile excepție făcând funcția bind. Însă și această funcție este inutilă deoarece la crearea unei conexiuni se atribuie automat un nume soclului. Mai mult, folosirea funcției bind pentru clienți este dezaprobată și considerată ca o eroare (logică) de programare.
Funcția connect
Această funcție reprezintă încercarea activă de conectare la un server. Procesul este deblocat și funcția se termină corect în cazul în care pe server s-a executat funcția accept și soclul a fost informat de acest lucru. După execuția cu succes a acestei funcții sockfd va descrie un soclu conectat (capăt al unei conexiuni). Pe acest soclu se pot executa operații de citire și scriere.
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockadd *serv_addr, socklen_t addrlen);
Parametri:
Observație:
Deși este posibil ca apelul funcției connect să se facă pentru un soclu non-blocant, acest lucru nu este recomandabil și, oricum, destul de complicat. Mai multe detalii sunt prezentate în secțiunea referitoare la socluri non-blocante.
Funcția connect poate fi apelată și pentru socluri datagramă și reprezintă asocierea a două socluri. Trimiterea și recepționarea datagramelor se face ulterior exclusiv spre și respectiv dinspre soclul asociat.
Valoare întoarsă:
La o execuție reușită, funcția întoarce valoarea 0. Dacă a apărut o eroare, funcția întoarce –1 și codul de eroare se găsește în errno:
Exemplu:
/*
client1.c: Client cu socluri non blocante
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc, char **argv)
{
int clientfd;
struct hostent *hent;
int port;
struct sockaddr_in sa;
printf("Folosire:\n\tclient1 adresa port\n");
if( argc != 3 )
exit(0);
clientfd = socket(PF_INET, SOCK_STREAM, 0);
sa.sin_family = AF_INET;
sscanf(argv[2], "%d", &port);
sa.sin_port = htons(port);
sa.sin_addr.s_addr = inet_addr(argv[1]);
if( sa.sin_addr.s_addr == INADDR_NONE )
{
hent = gethostbyname(argv[1]);
if( !hent )
{
herror("GETHOSTBYNAME");
exit(0);
}
memcpy((char *)&sa.sin_addr.s_addr, hent->h_addr, hent->h_length);
}
if( connect(clientfd, &sa, sizeof(sa)) == -1 )
{
perror("CONNECT FAILED");
close(clientfd);
exit(0);
} else
printf("CONECTAT LA SERVER\n");
// Citire si sciere de date…
// …
// Inchidere soclu
shutdown(clientfd, SHUT_RDWR);
close(clientfd);
}
Funcții specifice pentru socluri non-blocante
Majoritatea funcțiilor care lucrează cu socluri „blochează”, adică întrerup execuția procesului curent până când au suficiente date pentru a se termina. Proprietatea de a fi blocant nu este specifică soclurilor. Ea face parte din proprietățile generale ale fișierelor. De exemplu funcția scanf care citește date de la intrarea standard este blocantă: dacă nu există date în zona tampon a intrării se așteaptă până utilizatorul introduce niște date de la tastatură. Pentru evitarea unor astfel de situații, sistemul UNIX a definit proprietatea inversă, de „non-blocare”. Această proprietate a fișierelor spune funcțiilor care blochează să se termine imediat, indiferent dacă operația poate sau nu fi executată.
De ce ar trebui ca soclurile să fie non-blocante? Să presupunem că o aplicație server a acceptat o conexiune. În continuare aplicația dorește să citească date de pe conexiunea existentă, dar să și accepte alte conexiuni în același timp. Dacă execută funcția accept și nu există cereri de conexiune în coadă aplicația se blochează pe o perioadă nedefinită și datele de la client nu mai sunt citite. Invers, dacă se execută funcția read sau recv și clientul nu transmite nici un fel de date procesul se blochează pentru totdeauna și nici o cerere de conectare nu mai poate fi interpretată.
Soluția ar fi ca să se poată testa dacă o operație blochează procesul înainte de a fi executată. Testarea se face chiar apelând funcția respectivă, dar soclul transmis ca parametru să fie non-blocant: dacă operația reușește se trece mai departe; dacă eșuează și errno este EWOULDBLOCK (sau EAGAIN) înseamnă că nu există date suficiente pentru executarea cu succes a operației. Pentru exemplul anterior, se face o buclă infinită în care se încearcă atât acceptarea de noi conexiuni cât și citirea de la conexiunea deja existentă cu socluri de tip-blocante. Dacă vreuna din cele două funcții nu dă eroare se interpretează datele rezultate și se continuă procesul.
Această soluție rezolvă intr-adevăr multe din probleme dar un proces care tot încearcă apelarea unor funcții la infinit este inacceptabilă ca performanță (ar ocupa în totalitate procesorul). Soluția mai elegantă pe care sistemul UNIX o pune la dispoziție se numește select. Această funcție (care în mod ironic este blocantă) supraveghează mai multe fișiere simultan în așteptarea ca cel puțin unul să fie disponibil pentru o operație de citire/scriere. Dacă nici un fișier nu devine disponibil pentru citire/scriere într-un interval de timp, funcția select se termină.
În concluzie, mecanismul de lucru cu socluri non-blocante este următorul: inițial soclurile se marchează ca non-blocante, după care se execută funcția select pentru aceste socluri (o singură execuție pentru toate soclurile). Pentru soclurile indicate de select ca fiind disponibile (dacă există) se execută funcția de soclu corespunzătoare (de exemplu recv).
Funcția fcntl
Această funcție poate fi folosită pentru manipularea atributelor unui descriptor de fișier. Pentru socluri această funcție este folosită doar pentru a marca un soclu ca fiind blocant sau non-blocant. De aceea prezentarea funcției este incompletă și conține doar specificațiile necesare în lucrul cu socluri.
#include <unistd.h>
#include <fcntl.h>
int fcntl(int sockfd, F_GETFL);
int fcntl(int sockfd, F_SETFL, long arg);
Parametri:
Valoare întoarsă:
În caz de succes valoarea rezultat depinde de parametrul cmd. Dacă acesta este F_GETFL funcția întoarce atributele fișierului. Pentru F_SETFL funcția întoarce 0. În caz de insucces valoarea returnată este –1 și codul de eroare se poate afla din variabila errno.
Exemplu:
Următoarea secvență este modalitatea corectă de a marca un soclu ca blocant/non-blocant:
int opts;
opts = fcntl(sockfd, F_GETFL);
opts |= O_NONBLOCK; // Soclul devine non-blocant
/* opts &= ~O_NONBLOCK; // Soclul devine blocant */
if( fcntl(sockfd, F_SETFL, opts) == -1 )
perror(“FCNTL”);
Funcția select
Pe lângă funcția efectivă, pentru executarea select mai sunt necesare câteva macrouri: FD_ZERO, FD_SET, FD_CLR și FD_ISSET care operează cu o listă de descriptori de fișiere de tip fd_set.
Trei seturi de descriptori de fișier sunt supravegheați independent.
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
int select(int highestfd, fd_set *readfs, fd_set *writefs, fd_set *exceptfs, struct timeval *tv);
F_ZERO(fd_set *set);
F_SET(int fd, fd_set *set);
F_CLR(int fd, fd_set *set);
F_ISSET(int fd, fd_set *set);
Macrouri:
Parametri:
Observații:
După terminarea funcției fiecare din seturile specificate ca parametru vor conține doar descriptorii de fișiere care și-au schimbat starea. De exemplu, toate fișierele din setul readfs sunt disponibile pentru citire.
Parametrul tv se poate să fie schimbat de către execuția funcției (depinde de sistem). Este indicată inițializarea lui înainte de fiecare apel al funcției select și considerarea lui ca nedefinită după execuția acesteia.
Valoare întoarsă:
În caz de succes funcția întoarce numărul descriptorilor de fișier care și-au schimbat starea (sunt disponibili) sau 0 dacă a fost depășită limita de timp fără ca nici unul dintre fișiere să-și fi schimbat starea. Dacă a apărut o eroare în timpul execuției acestei funcții valoarea returnată este –1 și codul de eroare se găsește în errno:
Exemplu de server cu socluri non-blocante
Înainte de a prezenta exemplul sunt necesare câteva preeliminarii, respectiv comportamentul funcțiilor pentru socluri relativ la socluri non-blocante:
Următorul exemplu este un aplicație server simplă care acceptă până la 10 conexiuni active. Serverul citește simultan de la toate soclurile conectate și confirmă numărul de octeți recepționați.
/*
Server simplu cu socluri non-blocante.
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
char mesaj_acceptare[] = "Conexiunea este acceptata de server!\n",
mesaj_refuzare[] = "Serverul are prea multe conexiuni!\n";
int serverfd,
clientfd[10];
void iesire(char *message, int exitcode)
// Iese din programul curent afisand un mesaj si ultimul cod de eroare
{
int i;
if( exitcode != 0 )
{
printf("\n");
perror(message);
}
// Se inchid toate soclurile si conexiunile active
close(serverfd);
for( i=0; i<10; i++ )
if( clientfd[i] )
{
shutdown(clientfd[i], SHUT_RDWR);
close(clientfd[i]);
}
exit(exitcode);
}
void soclu_non_blocant(sockfd)
// Seteaza un soclu ca fiind non-blocant
{
int opts;
opts = fcntl(sockfd, F_GETFL);
if( opts == -1 )
iesire("Eroare in obtinerea atributelor", -1);
opts |= O_NONBLOCK;
if( fcntl(sockfd, F_SETFL, opts) == -1 )
iesire("Eroare in setarea atributelor", -1);
}
void accepta_conexiune()
// Accepta o conexiune existenta in coada soclului-server
{
int tempfd, i;
struct sockaddr_in sa;
socklen_t sizeof_sa;
// Acceptarea efectiva
tempfd = accept(serverfd, (struct sockaddr *)&sa, &sizeof_sa);
if( tempfd == -1 )
iesire("Eroare in acceptarea conexiunii", -1);
soclu_non_blocant(tempfd);
// Gasirea unui loc liber in lista conexiunilor active
for( i=0; (i<10) && (tempfd!=-1); i++ )
if( clientfd[i] == 0 )
{
if( write(tempfd, mesaj_acceptare, sizeof(mesaj_acceptare)) == -1 )
iesire("Eroare la transmiterea de date", -1);
printf("CLIENT (%s, %d) ACCEPTAT CA %d\n", inet_ntoa(sa.sin_addr),
ntohs(sa.sin_port), i);
clientfd[i] = tempfd;
tempfd = -1;
}
// Sunt deja 10 conexiuni active: conexiunea este refuzata
if( tempfd != -1 )
{
if( write(tempfd, mesaj_refuzare, sizeof(mesaj_refuzare)) == -1 )
iesire("Eroare la transmiterea de date", -1);
printf("CLIENT REFUZAT(%s, %d)\n", inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));
shutdown(tempfd, SHUT_RDWR);
close(tempfd);
}
}
void citeste_date(clientnum)
// Citeste toate datele disponibile pe un soclu
{
int retval,
totalval = 0;
char mesaj[20];
// Se verifica mai intai daca conexiunea nu a fost inchisa
retval = recv(clientfd[clientnum], (void *)&mesaj, sizeof(mesaj), MSG_PEEK );
if( (retval == -1) || (retval == 0) )
{
printf("CLIENT %d PIERDUT\n", clientnum);
close(clientfd[clientnum]);
clientfd[clientnum] = 0;
return;
}
// Se citesc date cat timp este umplut bufferul de receptie
printf("DATE RECEPTIONATE DE LA CLIENT %d: ", clientnum);
do {
retval = read(clientfd[clientnum], (void *)&mesaj, sizeof(mesaj)-1);
if( (retval == -1) && (errno != EWOULDBLOCK) )
iesire("Eroare la citirea datelor", -1);
if( retval != -1)
{
totalval += retval;
mesaj[retval] = '\0';
printf("%s", mesaj, retval);
}
}while( retval == sizeof(mesaj)-1 );
if( ((strlen(mesaj) > 0) && (mesaj[strlen(mesaj)-1] != '\n')) || (strlen(mesaj) == 0) )
printf("\n");
sprintf(mesaj, "Receptionati %d octet(i)\n", totalval);
send(clientfd[clientnum], &mesaj, strlen(mesaj), 0);
}
void executie_select()
// Se creeaza lista descriptorilor de soclu si se executa select
{
int i;
fd_set rdfs;
struct timeval tv;
FD_ZERO(&rdfs);
FD_SET(serverfd, &rdfs);
for( i=0; i<10; i++ )
if( clientfd[i] )
FD_SET(clientfd[i], &rdfs);
tv.tv_sec = 1;
tv.tv_usec = 0;
if( select(FD_SETSIZE, &rdfs, (fd_set *)NULL, (fd_set *)NULL, &tv) == -1 )
iesire("Eroare in select", -1);
// Se interpreteaza setul rezultat in urma executiei select
if( FD_ISSET(serverfd, &rdfs) )
accepta_conexiune();
for( i=0; i<10; i++ )
if( clientfd[i] && FD_ISSET(clientfd[i], &rdfs) )
citeste_date(i);
}
int main()
// Programul principal
{
int itemp;
struct sockaddr_in sa;
memset((void *)clientfd, 0, sizeof(clientfd));
// Se creeaza soclul
serverfd = socket(PF_INET, SOCK_STREAM, 0);
if( serverfd == -1 )
iesire("Eroare la crearea soclului", -1);
soclu_non_blocant(serverfd);
itemp = 1;
if( setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, (void *)&itemp, sizeof(itemp)) == -1 )
iesire("Nu pot seta parametru", -1);
// Se denumeste soclul
sa.sin_family = AF_INET;
sa.sin_port = htons(8984);
sa.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(serverfd, (struct sockaddr *)&sa, sizeof(sa)) == -1 )
iesire("Nu pot atasa un nume", -1);
// Mod acceptare conexiuni
if( listen(serverfd, 5) == -1 )
iesire("Nu se poate asculta", -1);
// Se executa bucla de select-uri
while( 1 )
executie_select();
}
Funcții specifice pentru socluri de tip datagramă. Trimiterea multiplă
Spre deosebire de soclurile de tip flux de date care sunt împărțite în socluri-server și socluri-client, un soclu de tip datagramă funcționează atât ca server cât și client. Un soclu datagramă este capabil să trimită și să primească date fără prea multe operații pregătitoare. O aplicație care vrea să transmită datagrame pur și simplu creează un soclu și trimite date. Pentru recepția lor este necesar, în plus, denumirea soclului. Pentru aceasta trebuie ca aplicația să apeleze funcția bind, care funcționează identic pentru socluri datagramă ca și pentru cele flux de octeți.
Observație:
Soclurile datagramă nu asigură că datele ajung la destinație. Dimensiunea unei datagrame trebuie să fie, în general, mică. O datagramă mai mare poate să nu ajungă la destinație din cauza fragmentărilor sau din cauza protocolului folosit. Ca standard s-a stabilit ca dimensiunea minimă acceptată de orice implementare TCP/IP pentru datagrame să fie 576, așa că este indicat ca aceasta să fie și limita maximă a datagramelor trimise de aplicație.
Funcția receivefrom este o extensie a funcției recv. În cazul în care argumentul from este NULL, recvfrom face exact același lucru ca și recv. Parametrii comuni cu recv au aceeași semnificație și valori posibile.
Parametrul from, înainte de apelul funcției, conține adresa de la care se dorește primirea mesajelor și nu poate fi nulă pentru socluri de tip datagramă. După un apel reușit al funcției în argumentul from se găsește adresa de la care a fost primită datagrama. Pentru recepționarea datagramelor de la orice adresă este necesar ca parametrul from să conțină sin_addr == INADDR_ANY și sin_port == 0, sau mai simplu, zona de memorie indicată de acest pointer să conțină zerouri.
Parametrul fromlen trebuie să conțină dimensiunea zonei de memorie indicată de from la apelul funcției. După apel va conține dimensiunea numelui soclului care a trimis datagrama.
Observație:
Dacă dimensiunea zonei tampon de recepție a datelor (buf) este mai mică decât dimensiunea datagramei care trebuie primită, restul datagramei este pierdut.
Funcția sendto este o extensie a funcției send, prezentată anterior. Parametrul to reprezintă adresa cărei îi este adresată datagrama. Folosirea unei adrese INADDR_ANY este incorectă, pentru trimiterea multiplă trebuie folosite adrese din clasa D.
Pentru difuzarea unei datagrame în rețea este necesar ca soclul să aibă opțiunea SO_BROADCAST activată, iar parametrul to al funcției sendto trebuie să fie o adresă de tip INADDR_BROADCAST.
Difuzarea datagramelor nu este încurajată. O alternativă la difuzare este trimiterea multiplă. Această particularitate specifică soclurilor datagramă reprezintă avantajul asupra soclurilor orientate pe conexiune. Principiul de funcționare este inițierea unei adrese de grup, de clasă D, la care toate soclurile doritoare să adere. Lucrul cu astfel de grupuri prezintă următoarea facilităte: un soclu nu trebuie să fie membru al grupului pentru a trimite o datagramă înspre acest grup.
Un soclu care dorește să primească mesaje de la grup trebuie să adere la acest grup. Aderarea se face prin folosirea unei opțiuni (setsockopt) de nivel SOL_IP. Opțiunea de aderare este IP_ADD_MEMBERSHIP, iar pentru renunțarea la grup se folosește opțiunea IP_DROP_MEMBERSHIP. Valoarea opțiunii este dată printr-o structură
struct ip_mreq{
struct in_addr imr_multiaddr; // Adresa grupului
struct in_addr imr_address; // Adresa interfeței locale
}
Pe lângă această opțiune, tot la nivelul SOL_IP, mai există opțiunea IP_MULTICAST_LOOP și IP_MULTICAST_TTL, care permit activarea sau dezactivarea buclei locale pentru trimiterea multiplă respectiv mărirea duratei de viață a datagramelor. Opțiunea IP_MULTICAST_TTL este utilă pentru grupuri în care datagramele trebuie să treacă peste un număr oarecare de rutere.
Pentru ilustrarea modalității de lucru cu grupuri de adrese de clasă D, cât și pentru exemplificarea folosirii soclurilor de tip datagramă, se prezintă aplicația următoare:
/*
Aplicatie simpla pentru socluri de tip datagrama cu trimitere multipla
Folosirea:
dgram1 “mesaj” – trimite un mesaj la grupul definit si se termina
dgram1 – adera la grupul definit si receptioneaza mesaje
– trimite datagrame citite de la intrarea standard
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#define GROUP_ADDRESS "234.5.6.7"
int sockfd;
struct sockaddr_in sa;
socklen_t sizeof_sa;
void justsend(char *message)
// trimite o datagrama la grupul definit
{
sa.sin_family = AF_INET;
sa.sin_port = htons(8984);
sa.sin_addr.s_addr = inet_addr(GROUP_ADDRESS);
if( sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&sa, sizeof(sa)) == -1 )
perror("Cannot SENDTO");
}
int main(int argc, char **argv)
// program principal
{
int opts, retrecv, itemp = 1;
struct ip_mreq ipm;
fd_set rdfs;
char message[100];
struct timeval tv;
// crearea soclu
if( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1 )
{
perror("SOCKET");
return -1;
}
// daca exista mesaj in lista de argumente il trimite si iese
if( argc > 1 )
{
justsend(argv[1]);
close(sockfd);
return 0;
}
// soclul este non-blocant
opts = fcntl(sockfd, F_GETFL);
opts |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, opts);
// optiunea de reutilizare a adresei
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &itemp, sizeof(itemp));
// denumirea soclului
sa.sin_family = AF_INET;
sa.sin_port = htons(8984);
sa.sin_addr.s_addr = inet_addr(GROUP_ADDRESS);
if( bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1 )
{
perror("BIND");
close(sockfd);
return -1;
}
// atasarea la grupul de trimitere multipla GROUP_ADDRESS
ipm.imr_multiaddr.s_addr = inet_addr(GROUP_ADDRESS);
ipm.imr_interface.s_addr = htonl(INADDR_ANY);
if( setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *)&ipm, sizeof(ipm)) == -1 )
{
perror("SETSOCKOPT");
close(sockfd);
return -1;
}
// ciclu pentru select
while(1)
{
// formarea setului pentru fisiere disponibile pentru citire
FD_ZERO(&rdfs);
FD_SET(0, &rdfs); // intrarea standard
FD_SET(sockfd, &rdfs);
tv.tv_sec = 5;
tv.tv_usec = 0;
if( select(FD_SETSIZE, &rdfs, 0, 0, &tv) == -1 )
{
perror("SELECT");
close(sockfd);
return -1;
}
// daca exista date la intrarea standard
if( FD_ISSET(0, &rdfs) )
{
scanf("%s", message, sizeof(message));
// daca se citeste sirul "quit" se iese
if( !strcmp(message, "quit") )
{
setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
(char *)&ipm, sizeof(ipm));
close(sockfd);
return 0;
}
// se transmite datagrama la grup
justsend(message);
}
// daca exista datagrame disponibile pentru citire
if( FD_ISSET(sockfd, &rdfs) )
{
memset(&sa, 0, sizeof(sa));
sizeof_sa = sizeof(sa);
if( (retrecv = recvfrom(sockfd, &message, sizeof(message), 0,
(struct sockaddr *)&sa, &sizeof_sa)) == -1 )
{
perror("RECVFROM");
close(sockfd);
return -1;
}
message[retrecv] = '\0';
printf("%s(%d): %s\n", inet_ntoa(sa.sin_addr),
ntohs(sa.sin_port), message);
}
}
}
Alte funcții utile în manipularea soclurilor
Pe lângă funcțiile standard de manipulare a soclurilor în programarea aplicațiilor destinate comunicării în rețea mai intervin și funcții generale de sistem. După cum a fost prezentat anterior, manipularea soclurilor non-blocante se face cu ajutorul funcțiilor standard fcntl și select. Sistemele UNIX mai pun la dispoziție și alte funcții sistem ce pot fi utilizate în conjuncție cu acestea. Lucrarea de față se va rezuma la a aminti două tipuri suplimentare: funcții pentru procese și funcții de bază de date rețea.
Sistemul UNIX este un sistem multi-tasking în care fiecare aplicație poate fi despărțită în mai multe procese care să se execute în paralel. Mai mult, fiecare proces poate fi împărțit în mai multe fire de execuție care, de asemenea, se execută simultan. Diferența dintre proces și fir de execuție este că un fir de execuție împarte aceeași zonă de date cu procesul ce l-a creat. Pentru implementarea firelor de execuție în UNIX trebuie folosită o librărie suplimentară (pthreads), adică ele nu fac parte din standardul UNIX. Procesele, în schimb, au suport inclus în librăria standard libc. Dintre funcțiile destinate manipulării proceselor doar câteva sunt cu adevărat utile în programarea soclurilor:
Algoritmul general de lucru cu procese mutliple și socluri este următorul: înainte de a se executa o operație blocantă asupra soclului se creează un proces copil cu fork. Procesul părinte își continuă activitatea în timp ce procesul copil execută operația blocantă. Comunicarea între cele două procese se poate face printr-o țeavă (pipe). După deblocare, procesul copil își informează părintele despre starea sa. În orice moment procesul părinte poate opri execuția procesului copil cu funcția kill.
Ceea de-a doua categorie de funcții sistem utile este formată din funcții bază de date rețea. Aceste funcții sunt definite în header-ul <netdb.h> și mai sunt descrise ca funcții de forma getXbyY. Scopul acestor funcții este obținerea de date suplimentare referitoare la gazde, rețea, protocoale și servicii. Pentru o gazdă, de exemplu, se dorește obținerea unor informații cât mai detaliate cum ar fi nume, toate adresele de interfețe, nume alternative sub care este cunoscută. Informația referitoare la o gazdă pot fi interpretate ca fiind câmpurile unei înregistrări dintr-o bază de date a gazdelor și poate fi înglobată într-o structură:
struct hostent{
char *h_name; // numele oficial al gazdei
char **h_aliases; // lista de nume alternative
int h_addrtype; // tipul de adresă
socklen_t h_length; // lungimea unei adrese
char **h_addr_list; // lista de adrese
#define h_addr h_addr_list[0] // prima adresă
}
Obținerea unei structuri valide se face printr-un procedeu similar cu interogarea unei baze de date, cu funcții getXbyY. De exemplu, pentru structura declarată avem:
Spre deosebire de funcțiile soclurilor, funcțiile bază de date rețea folosesc pentru generarea eventualei erori variabila externă h_errno, definită în <netdb.h>.
Observație:
Funcțiile bază de date sunt blocante. Rezolvarea numelui de domeniu implică comunicarea cu anumite servere, ceea ce ia timp și în tot acest timp procesul este blocat. Pentru aceste funcții nu este suportat modul non-blocant de apel.
Socluri in Windows (WinSock)
7
Cum stiva de protocoale TCP/IP a fost implementată pe majoritatea sistemelor de operare, era inevitabilă implementarea soclurilor în familia sistemelor Windows ale firmei Microsoft. Pentru identificarea soclurilor din sistemele Windows se va folosi în continuare termenul winsoclu. Prin termenul Windows se înțelege orice sistem de operare Microsoft Windows de versiune mai mare de 3.0 (de exemplu Microsoft Windows NT, Microsoft Windows 98 SE, Microsoft Windows 2000).
Specificațiile pentru winsocluri reprezintă o interfață în Windows pentru programarea aplicațiilor de rețea, care se bazează pe specificațiile soclurilor Berkeley. Această interfață se compune, pe de o parte, din rutinele în stil standard specifice soclurilor Berkeley și, pe de altă parte, din funcții specifice Windows care valorifică arhitectura bazată pe mesaje a acestui sistem. Conform viziunii Microsoft, winsoclurile se doresc o a fi interfață de programare a aplicațiilor (API – Application Programming Interface): cei care dezvoltă aplicații să le folosească pur și simplu, iar producătorii de software pentru rețea să se conformeze cu aceste specificații.
Setul de protocoale TCP/IP a fost implementat în Windows astfel încât să fie conform cu specificațiile winsoclurile, acesta fiind și punctul de start în definirea winsoclurilor, dar mai pot exista și alte modele de rețea care să fie conforme cu această interfață (de exemplu IPX).
Deși winsoclurile au fost destinate atât sistemelor pe 16 biți și care nu sunt multi-proces (Windows 3.1), definiția comună a soclurilor presupune sisteme pe 32 de biți multiproces.
Pentru compatibilitate cu soclurile Berkeley, toate funcțiile implementate în BSD-UNIX 4.3 (referitoare la socluri) sunt prezente și în specificațiilor winsoclurilor. Folosirea funcțiilor specifice Windows sunt opționale, cu excepția WSAStartup și WSACleanup, dar este de preferat folosirea lor.
Prima versiune a winsoclurilor, WinSock 1.0, a apărut în 1992 ca urmare a colaborării producătorilor și utilizatorilor (de software de rețea) pentru găsirea unei interfețe general valabilă între nivelul transport și nivelul aplicație. După câteva revizuiri apărute la scurt timp, la începutul anului 1993 a apărut versiunea WinSock 1.1. Această versiune a corectat problemele ce au apărut în practică și devenit un standard pentru winsocluri. Pentru protocoalele TCP/IP această versiune este suficientă.
Discuțiile pe tema winsoclurilor au continuat și la sfârșitul anului 1994 a apărut WinSock 2.0. Deși compatibili 100% cu WinSock 1.1, această versiune vine cu o serie de concepții noi și reprezintă „o nouă generație” de winsocluri. Dezvoltarea unui standard din WinSock 2.0 a fost mai anevoioasă. După mai multe revizuiri și îmbunătățiri, de abia pe la jumătatea lui 1997 s-a ajuns la o versiune finală, respectiv 2.2.2. Până la acest punct, WinSock 2.0 nu a fost inclus în nici un sistem de operare (Windows 98, de exemplu, nu are înglobat suport pentru versiunea 2 de winsocluri).
Ca regulă generală, termenul de winsoclu se referă la specificațiile din versiunea WinSock 1.1. Referirea la versiunea WinSock 2 se face explicit. Deoarece această lucrare își propune prezentarea soclurilor ca interfață pentru protocoalele TCP/IP, nu se va insista asupra noii generații de winsocluri, acestea având prea puține schimbări din acest punct de vedere.
Winsoclurile acceptă doar două tipuri de protocoale: de tip flux de date și de tip datagramă. Winsoclurile de tip datagramă acceptă difuzarea prin folosirea adresei INADDR_BROADCAST. Trimiterea multiplă este suportată dar nedocumentată.
Winsoclurile nu numai că acceptă modul non-blocant (denumit asincron în Windows), dar se recomandă folosirea acestuia. Mai mult, documentațiile Microsoft sugerează insistent să fie folosite winsocluri asincrone.
Deosebirile față de soclurile din UNIX
Pentru scrierea de aplicații bazate pe winsocluri este necesară includerea unui singur header, <winsock.h>. Acest header conține toate funcțiile și structurile compatibile cu soclurile Berkeley și, în plus, funcțiile specifice winsoclurilor. Acest header definește termenul _WINSOCKAPI_ care poate fi folosit în condiții preprocesor pentru scriere de cod portabil. La compilarea aplicațiilor este necesară includerea librărie winsock.lib, iar pentru execuția lor este obligatorie existența în sistem a librărie winsock.dll.
Înainte de apelarea oricărei funcții din biblioteca winsock trebuie apelată funcția de inițializare WSAStartup. Înainte de terminarea aplicației trebuie apelată funcția WSACleanup pentru eliberarea datelor temporare necesare lucrului cu winsocluri.
Spre deosebire de UNIX, în Windows descriptorul unui winsoclu este de tip SOCKET, care este un întreg fără semn. Din această cauză un descriptor invalid nu trebuie considerat ca fiind –1, ci INVALID_SOCKET. Deoarece în Windows o variabilă de tip SOCKET este doar un descriptor pentru un winsoclu și nu descriptor de fișier în WinSock 1.1 au fost reimplementate doar acele funcții de manipulare a fișierelor care au fost considerate utile în lucrul cu winsocluri (respectiv select și ioctlsocket). Folosirea altor operații normale pentru fișiere în lucrul cu winsocluri este greșită, iar rezultatul este imprevizibil (de exemplu read, write și close).
Valoarea întoarsă de o funcție la apariția unei erori este SOCKET_ERROR. Deși în prezent SOCKET_ERROR este definit ca –1, se recomandă folosirea sa explicită.
Pentru aflarea codului de eroare generat de o funcție de winsocluri NU se folosește variabila externă errno sau h_errno, ci se apelează funcția WSAGetLastError sau GetLastError. Pentru compatibilitate se poate face următoarea definire:
#define errno WSAGetLastError()
Constantele care reprezintă coduri de eroare sunt prefixate cu „WSA”. De exemplu EWOULDBLOCK pentru winsocluri este WSAEWOULDBLOCK. Deși sunt definite și în forma standard, se recomandă folosirea codurilor de eroare prefixate cu WSA.
Reprezentarea pointerilor care sunt folosiți în orice funcție de winsocluri trebuie să fie de tip far. Pentru a facilita scrierea de programe ce folosesc acestă restricție, winsoclurile pun la dispoziție tipuri de pointeri predefinite, ca de exemplu LPHOSTENT.
Dacă în UNIX se putea manipula un set fd_set de descriptori de fișiere în vederea unui apel select, pentru winsocluri este obligatorie folosirea macrourilor FD_XXX pentru a adăuga, șterge sau verifica un set de descriptori de winsocluri.
Două dintre funcțiile standard UNIX au fost redenumite din cauza conflictelor pe care le generau cu funcțiile Windows: close a devenit closesocket, iar ioctl a devenit ioctlsocket. Winsoclurile nu implementează funcția fcntl. Ca urmare, pentru specificarea modului sincron/asincron pentru un winsoclu se folosește funcția ioctlsocket (a se vedea descrierea acestei funcții).
Numărul maxim de winsocluri acceptabile depinde de protocol și implementarea sa în Windows. Folosirea unei valori ca fiind „sigură” este incorectă. Pentru parametrul funcției select, în <winsock.h> este definit termenul FD_SETSIZE ca fiind 64, însă nu este recomandabilă folosirea acestuia.
În implementarea winsoclurilor nu sunt acceptate toate opțiunile posibile pentru socluri Berkeley. Opțiunile acceptate în versiunea 1.1 sunt următoarele:
Opțiunile din implementarea BSD care nu sunt acceptate pentru winsocluri sunt: SO_RCVLOWAT, SO_RCVTIMEO, SO_SNDLOWAT, SO_SNDTIMEO, SO_PRIORITY.
Implementarea winsoclurilor aduce în schimb o opțiune nouă, SO_DONTLINGER care specifică dacă opțiunea SO_LINGER este validă sau nu. Această opțiune este de tip BOOLEAN și are o valoare implicită true.
Următorul scurt exemplu ilustrează o modalitate de scriere de cod care să fie compatibil atât cu specificațiile soclurilor Berkeley, cât și cu cele ale winsoclurilor. Pentru compilarea sub UNIX se folosește comanda:
„cc –o portabil –DIS_UNIX portabil1.c”
/*
Program compilabil pe UNIX si Windows, dupa cum este definit
sau nu termenul IS_UNIX.
*/
#include <stdio.h>
#ifdef IS_UNIX
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#else
#include <winsock.h>
#endif
// Definirea de termeni
#ifndef _WINSOCKAPI_
#define SOCKET_ERROR -1
#define INVALID_SOCKET -1
#define closesocket close
typedef int SOCKET;
typedef struct hostent *LPHOSTENT;
int GetLastError()
{
return errno;
}
int WSAGetLastError()
{
return h_errno;
}
#endif
void quit()
{
#ifdef _WINSOCKAPI_
WSACleanup();
#endif
exit(0);
}
int main(int argc, char **argv)
{
SOCKET sockfd;
struct sockaddr_in sa;
int port;
u_long address;
LPHOSTENT hent;
#ifdef _WINSOCKAPI_
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if( err != 0 )
{
printf("Versiunea 1.1 de winsocluri nu este acceptata!");
exit(0);
}
#endif
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if( sockfd == INVALID_SOCKET )
{
printf("Eroare la crearea soclului: %d\n", GetLastError());
quit();
}
sscanf(argv[2], "%d", &port);
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
address = inet_addr(argv[1]);
if( address == htonl(INADDR_NONE) )
{
hent = gethostbyname(argv[1]);
if( !hent )
{
printf("Eroare in GETHOSTBYNAME: %d\n", WSAGetLastError());
quit();
}
memcpy((char *)&sa.sin_addr.s_addr, hent->h_addr, hent->h_length);
}
#ifdef __WINSOCKAPI_
sa.sin_addr.S.un.S_addr = address;
#else
sa.sin_addr.s_addr = address;
#endif
if( connect(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == SOCKET_ERROR )
printf("Nu ma pot conecta: %d\n", GetLastError());
else
printf("Conectat la server.\n");
/* … */
closesocket(sockfd);
quit();
return 0;
}
Funcții preluate de la soclurile din UNIX
Toate funcțiile și structurile prezentate în capitolul referitor la socluri în general sunt valabile și pentru winsocluri (hton, ntoh, inet_addr, inet_ntoa) în exact aceeași formă și interpretare. Ele sunt incluse în <winsock.h>.
Următoarele funcții sunt preluate din implementarea soclurilor Berkely în UNIX. Comportamentul se dorește a fi identic cu funcțiile inițiale, cu observațiile de rigoare pentru fiecare funcție. Pentru detalii referitoare la o anumită funcție se poate consulta descrierea acesteia din capitolul anterior.
Pentru toate funcțiile care întorc valoarea –1 la apariția unei erori se consideră că întorc valoarea SOCKET_ERROR sau, după caz INVALID_SOCKET. Codul de eroare asociat se obține cu WSAGetLastError și următoarele coduri sunt specifice winsoclurilor:
Versiunea WinSock 1.1 nu suportă decât tipurile SOCK_STREAM și SOCK_DGRAM de winsocluri.
Funcții specifice Windows din WinSock 1.1
Sistemele Windows, spre deosebire de sistemele UNIX sunt de tip mesaj: sistemul trimite în permanență mesaje la aplicație. Aplicația, pe de altă parte, trebuie să fie reentrantă, adică să permită recepționarea acestor mesaje. Dacă o funcție blochează procesul curent atunci mesajele trimise de sistem nu mai pot fi procesate.
Multe din funcțiile winsoclurilor blochează. Faptul că winsoclurile pot trece în modul non-blocant nu rezolvă problema: după cum s-a văzut anterior funcția select însăși este funcție blocantă.
Pentru a rezolva problema blocării procesului, winsoclurile prevăd două soluții. Prima soluție este implementată intern de către interfața soclurilor: dacă aplicația apelează o funcție blocantă, subsistemul winsoclurilor nu se blochează ci simulează o pseudo-blocare. Subsistemul winsoclurilor lansează, pe de o parte, un nou fir de execuție care să se ocupe de apelul blocant și, concomitent, intră într-o buclă de procesare a mesajelor. Opțional, tratarea mesajelor poate să fie făcută de aplicație (a se vedea WSASetBlockingHook).
Această pseudo-blocare poate face ca aplicația să mai apeleze o funcție blocantă. În acest caz subsistemul winsoclurilor nu va intra într-o altă pseudo-blocare ci va termina noua funcție cu eroarea EINPROGRESS. Cu alte cuvinte, implementarea winsoclurilor permite o singură blocare pentru fiecare proces. Determinarea faptului că un proces este în pseudo-blocare se face cu funcția WSAIsBlocking, iar terminarea blocării sale cu funcția WSACancellBlockingCall.
Cea de-a doua soluție, recomandată de către specificațiile Microsoft, este folosirea winsoclurilor non-blocante în conjuncție cu o funcție asincronă (non-blocantă) select, respectiv WSAAsyncSelect. Intern, pentru această funcție se aplică același procedeu de pseudo-blocare. În ceea ce privește aplicația, apelul WSAAsyncSelect se termină imediat, iar rezultatul său efectiv (care presupune terminarea pseudo-blocării) este trimis înapoi aplicației sub forma unui mesaj.
În Windows orice aplicație are o fereastră principală. Orice fereastră se înregistrează în sistem menționând și o funcție de tratare a mesajelor. Un winsoclu este parte a unei aplicații. În consecință oricare winsoclu aparține de o fereastră și mesajele destinate winsoclului pot fi procesate de funcția ferestrei.
Pe de altă parte orice fereastră are un identificator (handle) unic în sistem. Astfel pentru ca o aplicație să fie informată asupra evenimentelor legate de un winsoclu, este suficient să trimită sistemului identificatorul winsoclului și identificatorul ferestrei principale.
Tot în categoria funcțiilor specifice Windows intră și funcțiile de inițializare și curățire a winsoclurilor, precum și funcția de identificarea a ultimului cod de eroare apărute.
Funcțiile WSAStartup și WSACleanup
Aceste funcții inițializează și, respectiv, eliberează subsistemul de gestionare a winsoclurilor. Trebuie apelate înainte de apelul oricărei funcții specifice winsoclurilor și, respsectiv, înainte de ieșirea din aplicație.
#include <winsock.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
int WSACleanup(void);
Parametri:
Observații:
Specificarea versiunii este implementată pentru asigurarea compatibilității cu versiunile următoare de winsocluri. Procesul de inițializare a subsistemului winsoclurilor necesită negocierea versiunii folosite: atât aplicația cât și subsistemul își comunică prin execuția acestei funcții versiunea maximă suportată și comunică celuilalalt dacă această valoare este acceptabilă. Încheierea negocierii duce, în caz de succes, la folosirea versiunii indicată de wVersion. Posibile scenarii în desfășurarea negocierii:
În cazul în care aplicația nu acceptă folosirea versiunii negociate, ea trebuie să apeleze funcția WSACleanup înainte de terminare. Această funcție închide toate conexiunile active și dealocă resursele folosite.
Pentru fiecare apel WSAStartup trebuie să existe un apel WSACleanup. O aplicație naivă poate apela WSACleanup până aceasta întoarce o eroare pentru a se asigura că subsistemul winsoclurilor a fost închis.
Execuția lui WSACleanup nu este permisă dintr-o funcție de tratare a mesajelor asociată unui pseudo-blocaj.
Valoare întoarsă:
Ambele valori întorc 0 în caz de reușită și SOCKET_ERROR altfel. Pentru WSAStartup nu se poate folosi funcția WSAGetLastError pentru obținerea codului ultimei erori, aceste putând fi obținut cu funcția GetLastError.
Pentru WSAStartup:
Pentru WSACleanup:
Exemplu:
Următorul exemplu ilustrează modalitatea de negociere pentru o aplicație care dorește folosirea versiunii 1.1 a subsistemului winsoclurilor:
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return;
}
/* Subsistemul este acceptabil */
Funcția WSAIsBlocking
#include <winsock.h>
BOOL WSAIsBlocking(void);
Această funcție determină dacă este activă pseudo-blocarea pentru procesul curent. Apelul funcției select este considerat ca blocant (când este cazul).
Dacă se dorește apelarea unei funcții care poate să blocheze procesul este indicată verificarea valorii întoarsă de această funcție în prealabil.
Valoarea întoarsă este TRUE sau FALSE și funcția nu generează nici un fel de eroare.
Funcția WSACancelBlockingCall
Oprește execuția pseudo-blocării pentru procesul curent, pentru orice funcție blocantă. Funcția respectivă se va termina cu eroarea WSAEINTR.
#include <winsock.h>
int WSACancelBlockingCall(void);
Observații:
Această funcție este normal folosită în două situații:
Aplicația interpretează un mesaj primit în timp ce un apel blocant este în desfășurare și funcția este „agățată” de procedeul de pseudo-blocare în timp ce un apel blocant se desfășoară.
În prima situație funcția se termină cu succes (când este cazul), dar apelul blocant nu este întrerupt imediat: un mesaj este transmis, prin intermediul sistemului, pseudo-blocării și când acesta îl primește se oprește.
În cea de-a doua situație pseudo-blocarea este terminată imediat după ieșirea din funcția „agățătoare”.
Impactul întreruperii unui apel blocant este diferit. Pentru funcțiile accept și select starea winsoclului nu este afectată. Winsoclul (winsoclurile) implicate în acțiunea întreruptă rămân valide și asupra lor se poate face orice operație. Pentru celelalte funcții întreruperea duce la trecerea winsoclului asociat într-o stare nedefinită. Unica operație care trebuie executată ulterior este închiderea winsoclului cu closesocket, eventual cu specificarea opțiunii SO_LINGER cu o valoare 0 (închidere fără înștiințare). În cazul în care a fost întreruptă o operație closesocket, soclul trebuie reînchis, dar fără o valoare pozitivă pentru opțiunea SO_LINGER.
În cazul funcției connect, deși blocare se termină, dar winsoclul continuă să ocupe resurse până când conexiunea este stabilită (și resetată) sau a expirat (la nivel de protocol). Acest fapt este observabil în cazul în care aplicația încearcă să mai creeze un winsoclu (și nu mai sunt resurse disponibile) sau reîncearcă să se conecteze la aceeași destinație.
Datorită faptului că blocarea nu este terminată imediat, ci cât de curând, este posibil ca un apel blocant să se termine cu succes chiar dacă a fost apelată funcția WSACancelBlockingCall. Singurul mod în care se poate afla dacă apelul blocant a fost întrerupt este prin codul de eroare întors de apelul respectiv.
Valoare întoarsă:
În caz de succes, această funcție întoarce 0. Altfel ea întoarce SOCKET_ERROR și codurile de eroare posibile sunt:
Funcțiile WSASetBlockingHook și WSAUnhookBlockingHook
Aceste funcții permit stabilirea și respectiv restaurarea funcției implicite pentru prelucrarea mesajelor în timpul pseudo-blocării.
#include <winsock.h>
FARPROC WSASetBlockingHook(FARPROC lpBlockFunc)
int WSAUnhookBlockingHook(void);
Aceste funcții au fost implementate în special pentru sistemele Windows care nu erau multithreading. Actual nu mai sunt folosite decât pentru a se apela funcția WSACancelBlockingCall. Apelarea funcției specificată ca parametru se face doar în cazul derulării unui apel blocant.
Funcțiile WSAGetLastError și WSASetLastError
Aceste funcții se folosesc pentru a obține și respectiv a stabili codul de eroare pentru ultima operație care a eșuat.
#include <winsock.h>
void WSASetLastError(int iError);
int WSAGetLastError(void);
Windows pune la dispoziția aplicațiilor o funcție sistem, GetLastError pentru aflarea ultimei erori apărute în aplicația apelantă. WSAGetLastError reprezintă o extindere a acestei funcții specială pentru winsocluri. Intern, WSAGetLastError apelează funcția GetLastError și rezultatul întors poate să nu fie o eroare specifică winsoclurilor, ci mai generală. În mod identic WSASetLastError se bazează pe funcția de sistem SetLastError.
Definiția codurilor de eroare specifice winsoclurilor este făcută astfel încât să nu se ajungă la contradicții sau ambiguități cu erorile standard de Windows. Funcțiile specifice winsoclurilor sunt implementate astfel încât să întoarcă doar coduri de eroare definite de subsistemul winsoclurilor, respectiv cele prefixate cu „WSA”. În scrierea aplicațiilor este recomandată folosirea acestor definiții de coduri de eroare (prefixate).
Motivația existenței unor funcții pentru obținerea și stabilirea ultimei erori în opoziție cu existența unei variabile externe (errno în cazul sistemelor UNIX) este garantarea integrității codului de eroare într-o aplicație cu mai multe fire de execuție. În plus, funcțiile asigură compatibilitatea cu versiunile următoare ale implementării winsoclurilor.
Funcția WSAAsyncSelect
Este versiunea asincronă a funcției select. Informează fereastra principală a aplicație de schimbările survenite în starea unui winsoclu.
#include <winsock.h>
int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
Parametri:
Observații:
Pentru soclul s dat ca argument, orice apel al funcției WSAAsyncSelect anulează un apel anterior al său. Dacă se dorește informarea aplicației asupra mai multor evenimente acestea trebuie să fie date ca parametru la un singur apel al funcției (de exemplu FD_READ | FD_CLOSE). Din această cauză pentru un soclu nu se pot genera mesaje diferite pentru evenimente diferite.
Dacă pentru un winsoclu a fost trimis un mesaj de informare simularea funcției select este oprită, adică nu se mai transmit alte mesaje referitoare la schimbarea stării acestui winsoclu. Aplicația trebuie să reapeleze funcția WSAAsyncSelect dacă mai dorește recepționarea evenimentelor.
Un apel în care lEvent este 0 anulează transmiterea de mesaje pentru un winsoclu.
Valoarea primită de către funcția ferestrei ca LOWORD(lParam) este unul (singur) dintre indicatorii specificați (FD_READ, FD_CLOSE, etc). Pentru manipularea lui lParam winsoclurile pun la dispoziție două macrouri, a căror utilizare este recomandată:
#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
Valoare întoarsă:
În caz de reușită, WSAAsyncSelect întoarce valoarea 0, altfel SOCKET_ERROR. Codurile de eroare posibil generate sunt:
Funcții bază de date pentru winsocluri
În Windows interogarea unui server pentru aflarea detaliilor referitoare la un nume de domeniu nu este standard. În consecință, nu există funcții sistem pentru protocolul DNS. Implementarea winsoclurilor vine să acopere această lipsă a sistemului și definește câteva funcții, cele mai des folosite din UNIX. Structurile de date folosite de aceste funcții au fost și ele portate (definite corespunzător) în Windows cu același nume și aceleași câmpuri.
Pe lângă acestea mai sunt și alte funcții de tip getXbyY implementate de către winsocluri, dar folosirea lor este ocazională: getprotobyname, getprotobynumber, getservbyname, getservbyport. Aceste funcții oferă informații complete despre protocoale și servicii.
Modalitatea de funcționare a acestor funcții, respectiv prin conectarea la un server de nume, face ca toate apelurile getXbyY să fie blocante. Câteodată ele pot fi rezolvate și local dar, în esență, funcțiile bază de date rămân blocante. Acest lucru nu a putut fi acceptat pentru implementarea winsoclurilor, așa că s-au definit funcții asincrone corespunzătoare. La fel ca și pentru celelalte funcții ce blochează, pentru funcțiile asincrone getXbyY se creează o pseudo-blocare care permite fluxul mesajelor.
Pentru fiecare funcție getXbyY în WinSock 1.1 se definește funcția asincronă corespunzătoare WSAAsyncGetXbyY. În plus, se mai definește funcția WSACancelAsyncRequest care permite întreruperea execuției unei operații bază de date rețea. Deoarece toate funcțiile de acest tip se comportă la fel, nu vor fi detaliate decât două, respectiv WSAGetHostByName și WSACancelAsyncRequest.
Funcția WSAGetHostByName
Obține informații referitoare la o gazdă în mod asincron, aceasta fiind specificată prin numele său de domeniu. Aplicația este înștiințată de rezultatul operației printr-un mesaj trimis ferestrei specificate.
#include <winsock.h>
HANDLE WSAAsyncGetHostByName(HWND hWnd, unsigned int wMsg, char FAR *name, char FAR *buf, int buflen);
Parametri:
Observații:
Pentru fiecare funcție WSAGetXbyY se inițializează un proces paralel de executare a operației, independent de existența altora de acelați tip. Teoretic se pot desfășura oricâte operații getXbyY asincrone simultan, limita lor fiind impusă doar de resursele sistemului. Ca urmare, fiecare dintre aceste operații bază de date asincronă este identificată de un „mâner” (handle).
După terminarea execuției operației fereastra indicată primește un mesaj wMsg în care wParam este identificatorul operației. Datorită modului de funcționare, WSAAsyncGetXByY poate produce două tipuri de erori: erori de inițializare (imediat după apel) și erori apărute în cursul operație. Acestea din urmă sunt trimise în mesaj ca HIWORD(lParam). În fine, LOWORD(lParam) reprezintă dimensiunea necesară pentru depozitarea tuturor datelor. Dacă operația a eșuat datorită insuficienței de memorie-destinație, aplicația poate să aloce o zonă de memorie de dimensiunea specificată în mesaj și să reapeleze funcția asincronă. Pentru manipularea argumentului lParam sunt implementate și recomandate două macrouri:
#define WSAGETASYNCERROR(lParam) HIWORD(lParam)
#define WSAGETASYNCBUFLEN(lParam) LOWORD(lParam)
Zona de date buf va conține toate datele referite de o structură hostent, structură ce se află la începutul zonei de memorie.
De exemplu, dacă zona de date este definită ca
TCHAR HostEntData[MAXGETHOSTSTRUCT];
obținerea unui pointer la structura efectivă hostent se face cu
#define HostEnt ((hostent *)HostEntData[0]).
Valoare întoarsă:
În cazul în care inițializarea procesului de găsire a informațiilor a putut fi realizată, funcția întoarce un HANDLE valid. În caz contrar este întoarsă valoarea 0 și ulimul cod de eroare poate avea una din valorile:
Codurile de eroare ce pot apărea în WSAGETASYNCERROR(lParam) la primirea mesajului sunt:
Funcția WSACancelAsyncRequest
Se folosește pentru a întrerupe o operație asincronă de bază de date.
#include <winsock.h>
int WSACancelAsyncRequest(HANDLE hAsyncTaskHandle);
Parametrul hAsyncTaskHandle reprezintă identificatorul operației asincrone care se dorește a fi întreruptă. Ca restul funcțiilor implementate de winsocluri, această funcție întoarce 0 în caz de reușită și SOCKET_ERROR în caz contrar. Codul specific de eroare, WSAEALREADY, înseamnă că operația asincronă a fost deja terminată. Mesajul emis de aceasta poate să fi fost sau nu prelucrat de către aplicație.
Exemplu pentru winsocluri asincrone
Următorul exemplu de program ilustrează folosirea winsoclurilor pentru crearea unui client simplu.
/*
client2.cpp – Client simplu cu socluri non-blocante
*/
#include "stdafx.h"
#include "resource.h"
#include <stdio.h>
#include <winsock.h>
#define MAX_LOADSTRING 100
// Global Variables:
HINSTANCE hInst;
TCHAR szWindowClass[] = "SOCKETCLIENTWND";
TCHAR szAddressError[] = "Formatul adresei de server este \"adresa_ip port\"\n\
Exemplu: 127.0.0.1 8984";
// Foward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// Definitii aditionale:
#define WM_SOCKETNOTIFY WM_USER + 100
// Variabile globale:
HWND hMainWnd, hList, hEdit, hButton;
SOCKET clientSocket;
// Functii aditionale:
BOOL InitControls(HINSTANCE);
BOOL InitSockets();
void Afiseaza(TCHAR *, …);
void ClientAsyncSelect();
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
// Inregistrarea clasei ferestrei principale si creare ei
MyRegisterClass(hInstance);
if (!InitInstance (hInstance, nCmdShow))
return FALSE;
// Initializarea controalelor
if( !InitControls(hInstance) )
return FALSE;
// Initializarea winsoclurilor:
if( !InitSockets() )
{
Afiseaza("(LOCAL)Eroare in la initializarea soclului client…");
SetWindowText(hButton, "Quit");
SetWindowLong(hButton, GWL_ID, IDC_QUIT);
} else
Afiseaza("(LOCAL)Soclu client initializat");
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
// Inregistrarea clasei ferestrei principale
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_CLIENT2);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = (LPCSTR)IDC_CLIENT2;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
return RegisterClassEx(&wcex);
}
// Crearea ferestrei principale
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
Inst = hInstance;
MainWnd = CreateWindow(szWindowClass, "Exemplu de socluri non-blocante asincrone",
S_OVERLAPPEDWINDOW & ~WS_SIZEBOX & ~WS_MAXIMIZE, CW_USEDEFAULT, CW_USEDEFAULT,
600, 400, NULL, NULL, hInstance, NULL);
if (!hMainWnd)
return FALSE;
ShowWindow(hMainWnd, nCmdShow);
UpdateWindow(hMainWnd);
return TRUE;
}
// Functia de procesare a mesajelor pentru fereastra principala
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
TCHAR adresa[10], string[1024], *msg;
int port, sizeof_sa, size;
sockaddr_in sa;
BOOL bError;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_EXIT:
case IDC_QUIT:
DestroyWindow(hWnd);
break;
// Se incearca o conectare la adresa specificata in controlul Edit
case IDC_CONNECT:
GetWindowText(hEdit, string, sizeof(string)-1);
bError = (_stscanf(string, "%s %d", adresa, &port) != 2);
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
if( (sa.sin_addr.S_un.S_addr = inet_addr(adresa)) == INADDR_NONE )
{
hostent *hent;
hent = gethostbyname(adresa);
if( hent == NULL )
bError = TRUE;
else
strncpy((char *)&sa.sin_addr, hent->h_addr,
hent->h_length);
}
if( bError )
{
MessageBox(hMainWnd, szAddressError,"Adresa de server \
invalida", MB_OK | MB_ICONSTOP);
SetFocus(hEdit);
SendMessage(hEdit, EM_SETSEL, 0, -1);
break;
}
if( connect(clientSocket, (sockaddr *)&sa, sizeof(sa)) != -1 )
{
Afiseaza("(LOCAL)Conectare imediata");
ClientAsyncSelect();
break;
}
if( WSAGetLastError() != WSAEWOULDBLOCK )
{
Afiseaza("(LOCAL)Eroare la apelarea connect: %d",
WSAGetLastError());
break;
}
Afiseaza("(LOCAL)Operatia de conectare initiata.");
WSAAsyncSelect(clientSocket, hMainWnd, WM_SOCKETNOTIFY,
FD_CONNECT);
break;
// Se trimit datele din controlul Edit
case IDC_SEND:
size = GetWindowTextLength(hEdit);
if( size <= 0 )
break;
msg = new TCHAR[size+1];
GetWindowText(hEdit, msg, size+1);
if( (size = send(clientSocket, msg, strlen(msg), 0)) == 0 )
SendMessage(hMainWnd, WM_SOCKETNOTIFY, clientSocket,
FD_CLOSE);
else
Afiseaza("(LOCAL)S-au trimis %d octeti: %s", size, msg);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
// Soclul si-a schimbat starea
case WM_SOCKETNOTIFY:
if( wParam != clientSocket )
break;
// Operatia de conectare a luat sfarsit
if( WSAGETSELECTEVENT(lParam) == FD_CONNECT )
{
Afiseaza("(LOCAL)Operatia de conectare terminata");
if( !WSAGETSELECTERROR(lParam) )
{
sizeof_sa = sizeof(sa);
getpeername(clientSocket, (sockaddr *)&sa, &sizeof_sa);
Afiseaza("(LOCAL)Conectare acceptata cu %s pe portul %d",
inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));
ClientAsyncSelect();
} else
Afiseaza("(LOCAL)Conectarea a esuat cu eroare: %d",
WSAGETSELECTERROR(lParam));
}
// Conexiunea a fost inchisa de celalalt capat
if( WSAGETSELECTEVENT(lParam) == FD_CLOSE )
{
Afiseaza("(REMOTE)Conexiunea a fost pierduta");
SetWindowText(hButton, "Quit");
SetWindowLong(hButton, GWL_ID, IDC_QUIT);
shutdown(clientSocket, 2);
closesocket(clientSocket);
break;
}
// S-au receptionat date
if( WSAGETSELECTEVENT(lParam) == FD_READ )
{
int size;
TCHAR *msg;
ioctlsocket(clientSocket, FIONREAD, (u_long *)&size);
msg = new TCHAR[size+1];
if( (size = recv(clientSocket, msg, size, 0)) == 0 )
SendMessage(hMainWnd, WM_SOCKETNOTIFY, clientSocket, FD_CLOSE);
else
{
msg[size] = '\0';
Afiseaza("(REMOTE)S-au receptionat %d octeti: %s", size, msg);
ClientAsyncSelect();
}
}
break;
// Inainte de iesirea din program se face curatenie
case WM_DESTROY:
MessageBox(NULL, "Multumesc pentru folosirea programului", "Exit",
MB_OK | MB_ICONINFORMATION);
closesocket(clientSocket);
WSACleanup();
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// Crearea controalelor
BOOL InitControls(HINSTANCE hInstance)
{
RECT rc;
GetClientRect(hMainWnd, &rc);
// Control de tip LISTBOX
hList = CreateWindowEx(WS_EX_LEFT,"LISTBOX", "Controlul lista",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_BORDER |
LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | LBS_NOSEL,
rc.left, rc.top, rc.right – rc.left, (rc.bottom – rc.top) – 20, hMainWnd,
NULL, hInstance, NULL);
if( !hList )
return FALSE;
ShowWindow(hList, SW_SHOW);
UpdateWindow(hList);
// Control de tip EDIT
hEdit = CreateWindowEx(WS_EX_LEFT,"EDIT", "adresa serverului port",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
rc.left, rc.bottom – 20, (rc.right – rc.left) – 70, 20, hMainWnd,
(HMENU)IDC_EDITBOX, hInstance, NULL);
if( !hEdit )
return FALSE;
ShowWindow(hEdit, SW_SHOW);
UpdateWindow(hEdit);
// Control de tip BUTTON
hButton = CreateWindow("BUTTON", "Connect",
WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON,
rc.right – 70, rc.bottom – 20, 70, 20, hMainWnd,
(HMENU)IDC_CONNECT, hInstance, NULL);
if( !hButton )
return FALSE;
ShowWindow(hButton, SW_SHOW);
UpdateWindow(hButton);
// Selectia implicita a controlului EDIT
SetFocus(hEdit);
SendMessage(hEdit, EM_SETSEL, 0, (LPARAM)-1);
return TRUE;
}
// Apelul functiei WSAStartup si socket
BOOL InitSockets()
{
WORD wVersionRequested = MAKEWORD(1, 1);
WSADATA WSAData;
// Procesul de negociere al versiunii
if( WSAStartup(wVersionRequested, (LPWSADATA)&WSAData) != 0 )
{
Afiseaza("(LOCAL)Eroare la executia WSAStartup: %d", GetLastError());
return FALSE;
}
Afiseaza("(LOCAL)Subsistemul winsoclurilor initializat");
Afiseaza("(LOCAL)%s Versiune maxima: %d.%d Versiune folosita: %d.%d",
WSAData.szDescription, LOBYTE(WSAData.wHighVersion), HIBYTE(WSAData.wHighVersion),
LOBYTE(WSAData.wVersion), HIBYTE(WSAData.wVersion));
// Se creaza un nou winsoclu
clientSocket = socket(PF_INET, SOCK_STREAM, 0);
if( clientSocket == INVALID_SOCKET )
{
Afiseaza("(LOCAL)Eroare la crearea winsoclului: %d", WSAGetLastError());
return FALSE;
}
// Se marcheaza nonblocking
int btemp = 1;
if( ioctlsocket(clientSocket, FIONBIO, (u_long *)&btemp) == -1 )
{
Afiseaza("(LOCAL)Nu s-a putut marca non-blocant: %d", WSAGetLastError());
return FALSE;
}
return TRUE;
}
// Introduce o linie in controlul lista
void Afiseaza(TCHAR *format, …)
{
TCHAR msg[1024];
va_list vl;
va_start(vl, format);
// va_arg( vl, TCHAR * );
wvsprintf(msg, format, vl);
va_end( vl );
SendMessage(hList, LB_ADDSTRING, 0, (LPARAM)msg);
SendMessage(hList, LB_SETSEL, 1, (LPARAM)-1);
}
// Face o operatie de select asincrona
void ClientAsyncSelect()
{
if( GetWindowLong(hButton, GWL_ID) != IDC_SEND )
{
SetWindowText(hButton, "Send");
SetWindowLong(hButton, GWL_ID, IDC_SEND);
}
if( WSAAsyncSelect(clientSocket, hMainWnd, WM_SOCKETNOTIFY, FD_READ | FD_CLOSE) == -1 )
{
Afiseaza("(LOCAL)AsyncSelect a esuat: %d. Conexiunea se inchide.",
WSAGetLastError());
shutdown(clientSocket, 2);
closesocket(clientSocket);
SetWindowText(hButton, "Quit");
SetWindowLong(hButton, GWL_ID, IDC_QUIT);
}
}
Socluri Windows și MFC
8
Punctul final al lucrării de față este prezentarea unei modalități avansate de programarea pentru winsocluri.
O interfață de programarea a aplicațiilor (API) reprezintă o colecție de funcții și structuri de bază necesare pentru utilizarea unui subsistem sau chiar a sistemului. Winsoclurile reprezintă, la rândul lor, o interfață de programare a aplicațiilor.
Observație:
Deși termenul de API este mai general, în limbajul curent este deseori folosit în loc de Windows API (totalitatea funcțiilor sistem din Windows).
API-urile sunt implementate pentru limbajul C și nu folosesc facilitățile limbajului orientat pe obiecte C++. Folosirea lor este destul de greoaie: de fiecare dată trebuie făcute aceleași succesiuni de operații, trebuie tratate toate erorile posibile, etc. De exemplu, pentru crearea unei ferestre trebuie creată o clasă pentru fereastră cu RegisterClass. Fereastra trebuie creată pe baza acestei clase și apoi activată. În plus mai trebuie implementată o funcție care să manipuleze mesajele destinate ferestrei.
O formă mai avansată de programare o reprezintă încapsularea funcțiilor API în clase de obiecte.
MFC (Microsoft Foundation Classes) este un sistem de clase creat pe baza funcțiilor API și care reprezintă o interfață orientată pe obiecte pentru programarea aplicațiilor. Grupul AFX (care a creat aceste clase) a avut în vedere ca următoarele obiective să fie îndeplinite pentru MFC:
Prima versiune de MFC a apărut în 1992 și în timp a fost îmbunătățită și completată. În 1995, o dată cu apariția lui Microsoft Visual C++ 2.1, a apărut versiune 3.1 a claselor MFC care includ suport pentru winsocluri ce folosesc specificațiile WinSock 1.1. Până în prezent MFC a ajuns la versiune 6.0, dar clasele suport pentru winsocluri au rămas neschimbate.
Observație:
Clasele MFC 6.0 destinate winsoclurilor nu folosesc noile specificații introduse în Windows Sockets 2.
Pentru a fi consecventă cu programarea orientată pe obiecte definiția unui winsoclu pentru MFC este puțin diferită: un winsoclu este un obiect (al sistemului) prin care aplicațiile Windows trimit și primesc pachete în rețea. Un winsoclu are un tip și un proces asociat și poate avea un nume. El poate comunica cu alt soclu ce folosește stiva de protocoale TCP/IP. Această comunicare se face bidirecțional, adică este duplex-integral. Ca și în cazul winsoclurilor, MFC-ul permite folosirea a două tipuri de socluri: flux de octeți și datagramă.
MFC definește două clase pentru manipularea winsoclurilor. Un obiect ce aparține uneia dintre aceste clase se numește obiect-soclu. Fiecare obiect-soclu încapsulează un descriptor de winsoclu și oferă aplicației operații asupra acestuia. Descriptorul de winsoclu este de tip SOCKET, iar relația dintre tipul de date SOCKET și winsoclu este echivalentă cu relația dintre HWND și fereastră.
Conform concepției MFC, clasele pentru manipularea winsoclurilor corespund la două modele de programare:
Aspecte generale ale winsoclurilor în MFC
Un obiect-soclu poate fi în „modul blocant” sau „modul non-blocant”. Metodele obiectelor-soclu care sunt în modul blocant (sincrone) nu se termină decât atunci când și-au încheiat operația asociată. Obiectul-soclu a cărui funcție a fost apelată nu poate face nimic până când metoda nu se termină. Acest lucru se întâmplă pentru un obiect din clasa CSocket întotdeauna sau pentru un obiect din clasa CAsyncSocket când acesta este definit în mod explicit ca blocant.
Pentru un obiect din clasa CAsyncSocket (asincron) o metodă se termină imediat, chiar dacă nu și-a terminat operația asociată. În acest caz un cod de eroare WSAEWOULDBLOCK este generat.
În aplicațiile cu mai multe fire de execuție folosirea obiectelor din clasa CSocket se poate face fără a fi afectată funcționalitatea. Dacă se operează cu mesajele primite de utilizator în subprocesul principal și operațiile cu obiectele din clasa CSocket se fac într-un subproces secundar aplicația „răspunde” la comenzile utilizatzorului. În schimb, dacă o aplicație are un singur fir de execuție, procesarea mesajelor și operațiile cu winsoclul trebuie alternate. Acest lucru se face prin folosirea de obiecte din clasa CAsyncSocket sau suprascrierea funcției OnMessagePending pentru o clasă derivată din CSocket.
Clasa CSocket este implementată astfel încât să opereze cu winsocluri blocante fără a fi afectată execuția aplicației și interacțiunea acesteia cu utilizatorul. Deoarece este mai abstractă decât CAsyncSocket și oferă posibilitatea de serializare este recomandabilă folosirea de winsocluri blocante în același fir de execuție sau într-unul separat.
Recomandările pentru obiectele-socluri diferă de cele pentru winsocluri. Firma Microsoft a susținut întotdeauna migrarea către noile versiuni de Windows și acesta este un exemplu elocvent în acest sens:
Pentru sistemele UNIX, care au fost dintotdeauna multi-proces, nu se recomandă „cu ardoare” folosirea vreunuia dintre cele două moduri pentru un soclu. Este la latitudinea programatorului dacă folosește socluri blocante în procese separate, socluri non-blocante sau, pur și simplu, socluri blocante într-un singur proces (care pot fi oricând oprite de către sistem).
Implementarea winsoclurilor s-a făcut în 1992 pe vremea când Microsoft oferea ca sistem de operare Windows 3.1. Acest sistem nu era multi-proces și deci folosirea soclurilor blocante reprezenta o problemă. Modul blocant pentru winsocluri a fost totuși implementat (printr-o metodă pseudo-blocantă după cum a fost prezentată în capitolul anterior) dar, bineînțeles, puternic nerecomandată.
În 1995 apărea Windows95, sistem pe 32 de biți și multi-proces, pentru care Microsoft a făcut o campanie susținută de promovare. Producătorii de software au fost încurajați să dezvolte aplicații cu mai multe fire de execuție, iar acest lucru a afectat și winsoclurile. Clasele MFC pentru winsocluri au fost create în această perioadă. Aplicațiile puteau folosi winsocluri blocante în fire de execuție separate, fapt care motivează derivarea lui CSocket din CAsyncSocket și nu invers.
Un obiect-soclu are asociat un nume, care se compune dintr-o adresă și un port. În metodele claselor CAsyncSocket și, implicit, CSocket sunt acceptate atât structuri de adresă sockaddr de formatul definit pentru socluri UNIX și winsocluri cât și adrese IP în reprezentare cu punct sau chiar nume de domeniu. Aceste metode ușurează munca programatorului de a face diferite conversii și operații de umplere a unei structuri. Pentru porturi în MFC se păstrează convenția de porturi general cunoscute. De asemenea o valoare de port egală cu 0 în operații de atribuire a unui nume unui obiect-soclu are ca rezultat selectarea automată a unui port disponibil mai mare decât 1024.
Un alt aspect care necesită rediscutarea în cazul obiectelor-soclu este ordinea octeților. După cum s-a prezentat anterior pe diferite sisteme pot exista două moduri de reprezentare a cuvintelor: little-endian și big-endian. Pentru protocoalele de rețea standardul este big-endian. În opoziție, pentru MFC este standardizat modul de reprezentare little-endian în serializarea obiectelor. Asupra ordinii octeților trebuie avut grijă în două situații:
Următorul exemplu arată cum se poate serializa un obiect astfel încât să se respecte regula big-endian de ordine a octeților.
struct Message
{
long m_lMagicNumber;
short m_nCommand;
short m_nParam1;
long m_lParam2;
void Serialize( CArchive& ar );
};
void Message::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << (DWORD)htonl(m_lMagicNumber);
ar << (WORD)htons(m_nCommand);
ar << (WORD)htons(m_nParam1);
ar << (DWORD)htonl(m_lParam2);
}
else
{
WORD w;
DWORD dw;
ar >> dw;
m_lMagicNumber = ntohl((long)dw);
ar >> w ;
m_nCommand = ntohs((short)w);
ar >> w;
m_nParam1 = ntohs((short)w);
ar >> dw;
m_lParam2 = ntohl((long)dw);
}
}
Observație:
Serializarea implicită a obiectelor se face întotdeauna în reprezentare little-indian. Chiar dacă o aplicație rulează pe un sistem Windows de pe o mașină RISC (unde datele sunt reprezentate în modul big-endian) obiectele pe care această aplicație le scrie folosind clasa CArchive sunt reprezentate în modul little-indian. Acest lucru asigură că dacă două aplicații comunică și transmit obiecte serializate ele își păstrează structura de la un capăt la celălalt.
Chiar dacă reprezentarea obiectelor serializate își păstrează consistența, din punct de vedere al standardelor de rețea reprezentarea lor în format little-indian. Să presupunem că avem o aplicație despre care nu știm cum a fost scrisă sau cum transmite datele în rețea și se cunosc doar structurile transmise. Dacă se presupune că aplicația, lucrând pe rețea, transmite date în formatul standard rețea și de fapt aplicația serializează obiecte MFC datele interpretate sunt greșite.
Pe lângă ordinea octeților în trimiterea de date mai este importantă și recunoașterea șirurilor de caractere. MFC oferă o clasă foarte puternică de manipulare a șirurilor de caractere și anume CString. Această clasă lucrează cu șiruri de caractere de până la 2 miliarde de octeți. Mai mult, clasa CString poate să lucreze cu șiruri UNICODE în care reprezentarea unui caracter se face pe 2 octeți. Dacă se lucrează cu obiecte din clasa CSocket asociat cu CArchive este probabil să fie serializate obiecte de tip CString. Dacă o aplicație non-MFC dorește să citească acest șir trebuie să aibă în vedere modul de serializare al său:
// CString serialization code
// String format:
// UNICODE strings are always prefixed by 0xff, 0xfffe
// if < 0xff chars: len:BYTE, TCHAR chars
// if >= 0xff characters: 0xff, len:WORD, TCHAR chars
// if >= 0xfffe characters: 0xff, 0xffff, len:DWORD, TCHARs
CArchive& AFXAPI operator<<(CArchive& ar, const CString& string)
{
// special signature to recognize unicode strings
#ifdef _UNICODE
ar << (BYTE)0xff;
ar << (WORD)0xfffe;
#endif
if (string.GetData()->nDataLength < 255)
{
ar << (BYTE)string.GetData()->nDataLength;
}
else if (string.GetData()->nDataLength < 0xfffe)
{
ar << (BYTE)0xff;
ar << (WORD)string.GetData()->nDataLength;
}
else
{
ar << (BYTE)0xff;
ar << (WORD)0xffff;
ar << (DWORD)string.GetData()->nDataLength;
}
ar.Write(string.m_pchData, string.GetData()->nDataLength*sizeof(TCHAR));
return ar;
}
Modul de tratare a mesajelor Windows de către MFC se bazează pe proprietatea de polimorfism a obiectelor din C++. În general, pentru fiecare mesaj se poate asocia o metodă virtuală auto-apelată de tratare. O aplicație care dorește să interpreteze acest mesaj nu are decât să suprascrie această metodă într-o clasă derivată.
Pentru winsoclurile asincrone se poate genera un mesaj Windows care să fie trimis aplicației când apare un eveniment de rețea. În clasa CAsyncSocket sunt definite metode auto-apelate pentru fiecare eveniment ce poate apărea în legătura cu un winsoclu. Aplicația care folosește obiecte-soclu poate deriva o clasă din CAsyncSocket și poate suprascriere aceste metode. Acest procedeu poate fi aplicat și pentru CSocket (aceasta fiind derivată din CAsyncSocket).
După cum se observă, dacă o clasă este derivată din CSocket un obiect-soclu al acestei clase se comportă atât ca blocant cât și ca non-blocant. De aceea clasa CSocket este considerată foarte puternică și folosirea ei este indicată în defavoarea clasei CAsyncSocket.
Observație:
O singură excepție există în moștenirea metodelor auto-apelate în CSocket: funcția OnConnect nu este niciodată apelată. Pentru realizarea unei conexiuni, apelul funcției Connect pentru un obiect din clasa CSocket se face totdeauna în mod blocant.
Modele de programare
Folosirea clasei CAsyncSocket în mod direct presupune mai multe obligații din partea programatorului. Clasa de obiecte-soclu asincrone oferă funcții de nivel-scăzut pentru manipularea winsoclurilor. De fapt această clasă este doar o împachetare a funcțiilor API pentru winsocluri integrate în MFC. Programatorii care doresc să folosească această clasă trebuie să aibă cunoștințe despre rețele de calculatoare și să trateze fiecare din aspectele legate de acestea. Modelul de programare a obiectelor din clasa CAsyncSocket cuprinde patru pași de bază:
Modelul de programare cu ajutorul obiectelor din clasa CAsyncSocket este util pentru aplicațiile care doresc un control strict asupra winsoclurilor. În acest model, însă, trebuie tratate toate aspectele generale legate de winsocluri:
Modelul avansat de programare a obiectelor-soclu în MFC este reprezentat de folosirea clasei CSocket în conjuncție cu clasa CArchive. Clasa CSocket poate fi folosită și solitar, ca o clasă de obiecte-soclu mai abstracte, dar folosirea lor în sensul specificațiilor MFC este împreună cu arhivele. Arhivele reprezintă o metodă de serializare a obiectelor, adică salvarea lor într-un fișier și respectiv refacerea lor după o copie dintr-un fișier. Pentru serializarea obiectelor prin winsocluri a fost creată clasa CSocketFile care simulează comportarea acestora ca fișiere binare.
Observație:
Folosirea acestui model este posibilă doar pentru winsocluri de tip flux de octeți. Folosirea obiectelor-soclu de tip datagramă este posibilă în clasa CSocket, dar fără suport de arhive.
În mod ironic, după complicarea lucrului cu socluri, Microsoft a revenit la ceea ce au însemnat soclurile Berkley de la bun început: o interfață de tip fișier pentru nivelul transport, pentru care sunt valide anumite operații de bază cu fișiere. Dar, tocmai datorită acestor complicări, rezultatul final nu este la fel de bun ca cel de la care sa plecat.
Modelul obiectelor-soclu cu arhive presupune crearea mai multor obiecte MFC. Pașii necesari pentru crearea unei aplicații server sau a uneia client sunt în număr de șapte și sunt comuni, mai puțin pasul 3:
Obiectele din clasa CArchive oferă programatorului metoda IsBufferEmpty care este utilă (chiar necesară) în lucrul cu obiecte-soclu. Dacă în coada de recepție există mai multe obiecte serializate trebuie efectuată citirea lor până când coada de recepție este goală. Metoda IsBufferEmpty trebuie apelată pentru a se asigura că toate obiectele primite sunt citite.
Funcționarea acestui model de programare a obiectelor-soclu se face pe baza obiectului-fișier, din clasa CSocketFile. Deși această clasă este derivată din CFile, majoritatea funcțiilor specifice fișierelor nu sunt suportate: poziționarea (Seek, GetLength, SetLength, GetPosition) sau accesul exclusiv (LockRange, UnlockRange) nu își au sensul în lucrul cu winsocluri și de aceea au fost eliminate.
Obiectul-soclu se află în două stări efective: câteodată este blocant și câteodată este non-blocant. În mod normal el este în starea non-blocantă și așteaptă să fie informat asupra unui eveniment rețea. Dacă, de exemplu, s-au recepționat date el intră în stare blocantă în ideea că „cineva” va citi datele primite. Cât timp se află în mod blocant, obiectul-soclu nu mai este informat asupra evenimentelor rețea. Ca urmare, deși mai sunt adăugate date în obiectul-fișier, obiectul-soclu nu este informat de acest lucru.
În fine, în folosirea arhivelor și a obiectelor-fișier trebuie avut în vedere că există o zonă tampon de memorie în care datele sunt scrise înainte de a fi salvate. Astfel, dacă o aplicație serializează un obiect (îl salvează) într-o arhivă conectată la un obiect-soclu acesta poate să nu fie trimis imediat aplicației de la celălalt capăt. Pentru trimiterea datelor se folosește metoda Flush a arhivei care forțează scrierea datelor.
Cele două moduri de programare a obiectelor-soclu pot fi sintetizate în următoarea schemă care prezintă secvența operațiilor pentru comunicarea prin winsocluri de tip flux de date:
Clasa CAsyncSocket
Un obiect din clasa CAsyncSocket reprezintă un obiect-soclu, adică un punct final de comunicație. El încapsulează funcțiile API pentru winsocluri și oferă o interfață orientată-obiect pentru manipularea acestora la nivel elementar. Folosirea obiectelor din această clasă implică un efort mai mare din partea programatorului care trebuie să se ocupe de ordinea octeților, conversia șirurilor de caractere și modul winsoclului (blocant sau non-blocant).
Modelul de programare asociat clasei implică: construirea obiectului și crearea winsoclului corespunzător (Create); după caz apelarea metodei Connect sau a metodelor Listen și Accept și, în final, distrugerea lui. Destructorul clasei apelează automat metoda Close, așa că nu este necesară apelarea ei explicită pentru închiderea conexiunii.
Clasa CAsyncSocket are un singur atribut (m_hSocket de tip SOCKET) și mai multe metode membru grupate pe categorii de utilizare:
Implicit winsoclurile create cu această clasă sunt non-blocante. Pentru crearea unui winsoclu blocant se recomandă folosirea clasei CSocket și nu setarea opțiunii respective pentru obiectele din clasa CAsyncSocket.
Pe lângă funcțiile standard pentru winsocluri, clasa CAsyncSocket aduce în plus câteva metode specifice programării orientate pe obiecte destinate simplificării lucrului cu winsocluri.
Constructorul CAsyncSocket::CAsyncSocket
CAsyncSocket();
Orice clasă trebuie să aibă un constructor implicit. Acest constructor este apelat la crearea obiectului din clasa respectivă. El este responsabil de alocarea de memorie pentru obiect și inițializarea tabelei virtuale (VTBL) a obiectului. Un obiect dintr-o clasă poate fi creat în două moduri: static sau dinamic.
CAsyncSocket staticObject; // obiect creat static
CAsyncSocket *dinamicObject;
dinamicObject = new CAsyncSocket(); // obiect creat dinamic
Observație:
Pentru folosirea oricărei alte funcții în afară de CAsyncSocket::FromHandle trebuie mai întâi construit obiectul.
Constructorul nu face altceva decât să inițializeze obiectul din clasa CAsyncSocket. El nu creează nici un fel de winsoclu și orice apel de metodă care folosește winsoclul atașat va produce o eroare de tip WSAENOTSOCK.
Dacă se creează obiectul în mod static apelarea constructorului se face automat. Pentru un obiect dinamic trebuie apelat explicit constructorul prin operatorul new.
Metoda CAsyncSocket::Create
Funcția Create inițializează un winsoclu și îl atașează obicetului-soclu. Este echivalent cu apelul succesiv al funcțiilor socket și bind pentru winsoclu.
BOOL Create(UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL);
Parametri:
Observație:
Dacă se apelează metoda Create, nu mai este necesar apelul metodei Bind. Mai mult, apelul metodei Bind după Create va eșua.
Pentru obiecte-soclu care vor fi folosite în apelul metodei Accept nu trebuie apelată metoda Create. În caz contrar metoda Accept va întoarce o eroare.
Valoare întoarsă:
În caz de succes întoarce valoarea TRUE. Dacă metoda întoarce valoare FALSE eroarea ce a cauzat eșecul funcției se poate afla prin apelul CAsyncSocket::GetLastError() și codul erorii este unul din cele posibile pentru funcțiile de winsoclu socket și bind.
Metoda CAsyncSocket::Attach
Această metodă atașează un obiect-soclu unui winsoclu și viceversa. Obiectul-soclu apelant va avea toate caracteristicile winsoclului: tip, este conectat sau nu, este denumit sau nu, etc.
BOOL Attach(SOCKET hSocket, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE);
Parametri:
Observații:
Un winsoclu poate să fie atașat la mai multe obiecte-soclu, însă unul singur dintre acestea este informat de evenimentele din starea winsoclului (respectiv ultimul pentru care s-a apelat metoda Attach).
Valoare întoarsă:
Metoda întoarce TRUE sau FALSE după cum operația de atașare a reușit sau nu. O valoare FALSE denotă, de obicei, că descriptorul de winsoclu transmis ca parametru este invalid/nu există.
Metoda CAsyncSocket::Detach
Este metoda inversă lui Attach. Permite detașarea winsoclului de obiectul-soclu. După apelul metodei, atributul m_hSocket a obiectului-soclu va avea valoarea 0.
SOCKET Detach();
Observații:
După apelul metodei Detach orice metodă referitoare la winsoclu va eșua. În plus obiectul-soclu poate fi distrus fără ca winsoclul să fie afectat.
Utilitatea metodelor Attach și Detach o reprezintă folosirea unui winsoclu de mai multe fire de execuție. Două fire de execuție nu pot folosi același obiect-soclu, dar pot comunica unul altuia descriptorul de winsoclu corespunzător. Fiecare din cele două fire atașează/detașează winsoclul unui obiect-soclu și îl utilizează în funcție de necesitate.
Valoare întoarsă:
Metoda întoarce descriptorul winsoclului care era atașat obiectului-soclu sau INVALID_SOCKET dacă obiectul nu avea un winsoclu atașat.
Metoda CAsyncSocket::FromHandle
Întoarce un pointer la ultimul obiect-soclu care are atașat descriptorul de winsoclu dat ca parametru din firul de execuție curent.
CAsyncSocket *FromHandle(SOCKET hSocket);
Parametri:
Observații:
Metoda este statică, adică nu este chiar o metodă a obiectului ci o funcție a clasei. Clasa obiectului-soclu întors nu este neapărat CAsyncSocket, ci poate să fie și o clasă derivată din acesta (de exemplu CSocket).
Dacă se apelează funcția din alt fir de execuție decât cel al obiectului-soclu valoarea întoarsă este NULL.
Valoare întoarsă:
Este un pointer la un obiect-soclu în cazul în care există unul atașat winsoclului dat ca parametru sau NULL în caz contrar. De asemenea, dacă descriptorul de winsoclu este invalid valoarea întoarsă este NULL.
Exemplu:
Modalitatea standard de apelare a acestei funcții de clasă este următoare, cu variante corespunzătoare pentru obiecte din clase derivate:
CAsyncSocket *pSocket = CAsyncSocket::FromHandle(hSocket); // apel standard
CSocket *pSocket = (CSocket *)CAsyncSocket::FromHandle(hSocket); // obiect din clasa detivată
CSocket *pSocket = CSocket::FromHandle(hSocket); // apel standard pentru clasa CSocket
CAsyncSocket *pSocket = CSocket::FromHandle(hSocket); // eroare de compilare în conversie
Metodele de tip CAsyncSocket::OnEvent
virtual void OnAccept(int nErrorCode);
virtual void OnConnect(int nErrorCode);
virtual void OnReceive(int nErrorCode);
virtual void OnSend(int nErrorCode);
virtual void OnClose(int nErrorCode);
virtual void OnOutOfBandData(int nErrorCode);
Aceste metode sunt definite ca suport pentru suprascriere în clasele derivate. În implementarea clasei CAsyncSocket ele nu fac nimic (au declarația) nulă. Ele în schimb sunt apelate automat la apariția evenimentului corespunzător. Astfel pentru tratarea evenimentelor programatorul nu trebuie decât să definească o clasă derivată din CAsyncSocket și să suprascrie metoda dorită. Datorită proprietatății de polimorfism a obiectelor, metoda suprascrisă din clasa derivată este automat apelată.
Observație:
Polimorfismul nu funcționează decât pentru obiectele construite în mod dinamic. Clasa obiectului trebuie să fie clasa de bază, iar la construirea acestuia se specfică clasa derivată.
Exemplu:
// declararea clasei
class CMySocket :public CAsyncSocket;
{
public:
CMySocket() {};
void OnReceive(int nErrorCode);
}
// implementarea clasei
void CMySocket::OnReceive(int nErrorCode)
{
printf(“Am receptionat date!\n”);
}
// folosire
CAsyncSocket *pVirtualObject;
pVirtualObject = new CMySocket();
/*…*/
pVirtualObject->OnReceive(0); // va scrie Am receptionat date!\n
Clasa CSocket
Clasa CSocket este derivată din CAsyncSocket și moștenește toate funcțiile Windows Sockets API pe care aceasta le înglobează. Un obiect din clasa CSocket reprezintă o formă mai abstractă a interfeței winsoclurilor decât CAsyncSocket. CSocket operează cu ajutorul claselor CSocketFile și CArchive pentru trimiterea și recepționarea datelor.
De asemenea, clasa CSocket asigură buna funcționare a winsoclurilor în mod sincron, lucru esențial pentru folosirea operațiilor cu arhive. Nici una din metodele moștenite Receive, Send, ReceiveFrom, SendTo și Accept nu produce eroarea WSAEAGAIN pentru obiectele din clasa CSocket ci blochează execuția firului respectiv de execuție până la terminarea operației. În plus, aceste funcții pot fi întrerupte cu metoda CancelBlockingCall.
Folosirea obiectelor din clasa CSocket implică, în plus față de obiectele-soclu în general, inițializarea și asociarea unui fișier-obiect și de arhive pentru citire/scriere. Pe lângă metodele noi de construire, clasa CSocket mai implementează și alte câteva metode suplimentare:
Constructorul CSocket::CSocket
CSocket();
Construiește un obiect-soclu din clasa CSocket și inițializează tabela virtuală asociată obiectului. Un obiect-soclu trebuie creat înainte de a apela orice metodă a sa în afară de CSocket::FromHandle.
Metoda CSocket::Create
Creează un winsoclu, îi asociază o adresă (îl denumește) și îl atașează obiectului-soclu.
BOOL Create(UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, LPCTSTR lpszSocketAddress = NULL);
Parametri:
Observații:
Winsoclul nou creat este de tip blocant. Obiectul-soclu creat este informat despre toate evenimentele legate de acesta.
Valoare întoarsă:
Această metodă întoarce TRUE dacă s-a putut creea un winsoclu și s-a asociat cu obiectul-soclu apelant. În caz contrar întoarce FALSE și codul de eroare se poate obține cu metoda GetLastError.
Metoda CSocket::IsBlocking
Determină dacă un apel blocant este în desfășurare sau nu.
BOOL IsBlocking();
Valoare întoarsă:
Metoda întoarce TRUE sau FALSE după cum există sau nu apel blocant în desfășurare. Apelul blocant se referă doar la winsoclul asociat obiectului-soclu. Dacă winsoclul este invalid, valoarea întoarsă este FALSE.
Metoda CSocket::CancelBlockingCall
void CancelBlockingCall();
Dacă winsoclul asociat obiectului-soclu se află într-o operație blocantă, această metodă întrerupe această operație. Dacă winsoclul nu este blocat sau este invalid, această funcție nu face nimic.
Observație:
Ca implementare, această metodă pune un mesaj în coada de mesaje a aplicației destinat opririi operației blocante și se întoarce imediat. Ca urmare, operația blocantă nu este imediat oprită ci atunci când mesajul ajunge să fie interpretat.
Metoda CSocket::OnMessagePending
Este o metodă auto-apelată care permite interpretarea mesajelor din coada de mesaje în timp ce se execută o operație blocantă asupra winsoclului atașat.
virtual void OnMessagePending(int nErrorCode);
Observații:
O aplicație care implementează o clasă derivată din CSocket trebuie să suprascrie această funcție pentru a căuta anumite mesaje din coada de mesaje. Pentu a obține mesajele din coadă se folosește funcția API PeekMessage.
Implementarea implicită a metodei tratează mesajul WM_PAINT și, eventual, oprește operația în desfășurare în cazul apariției unei erori. Este recomandată apelarea metodei clasei CSocket în cazul în care clasa derivată nu interpretează mesajele apărute.
Aplicație: discuții on-line
Pentru exemplificarea programării soclurilor am ales o aplicație de transmitere și recepționare de mesaje instante. Lucrarea de față își propune prezentarea atât a soclurilor Berkeley în general, cât și a unei metode de programare avansată a acestora, respectiv folosirea soclurilor încapsulate în clase MFC.
Aplicația, în consecință, este un exemplu a folosirii claselor MFC pentru programarea soclurilor. Aplicația este de tip client-server și se compune din două programe: un server care acceptă conexiuni pe un anumit port și un client care se conectează la acest server.
Exemplul ales tratează majoritatea aspectelor de programare avansată a soclurilor: Folosirea claselor CSocket și CAsyncSocket este ilustrată prin implementarea unor clase derivate din acestea. Ambele modele de programare prezentate în acest capitol (modelul CAsyncSocket și modelul CSocket cu arhive) sunt implementate în clasele derivate. În plus, aplicația folosește ambele tipuri de socluri: orientate pe conexiune și datagramă.
Pentru realizarea aplicației s-a folosit programul de dezvoltare a aplicațiilor Microsoft Visual C++, versiunea 6.0, inclus în pachetul de programe Microsoft Visual Studio. În scrierea acesteia s-au folosit MFC AppWizard și Class Wizard (specifice pentru Microsoft Visual C++), care prezintă avantajul generării unui cod stabil și ușor de înțeles. Deși, teoretic, programele rezultate pot fi compilate cu orice compilator care are inclus suport pentru sistemul de clase MFC, se recomandă insistent folosirea programului Microsoft Visual C++.
Deoarece aplicația folosește componente visuale specifice Windows, ea nu este portabilă pe alte sisteme. Din clasa sistemelor Microsoft Windows, aplicația rulează pe Microsoft Windows 9x, Microsoft Windows NT 4.0 cu Service Pack 6 și orice altă versiune de Windows ulterior apărută. Pentru aceste sisteme, folosirea claselor MFC și a compilatorului Visual C++ asigură o compatibilitate totală.
Pe lângă ilustrarea folosirii soclurilor, aplicația poate fi un exemplu elocvent și pentru programarea orientată pe obiecte, folosirea claselor MFC și a controalelor Windows.
Obiectivele aplicației
O dată cu apariția sistemelor avansate de operare și a diferitelor arhitecturi dezvoltate de diferite companii, modalitatea de scriere a programelor s-a schimbat și ea. În trecut, un programator se gândea 5 minute la ce vrea să facă aplicația pe care urmează să o scrie, după care deschidea o aplicație de tip compilator și se apuca de „butonat”. După o oră de scris, compila programul și două ore depana programul să-i corecteze erorile logice.
În prezent această „tehnică” (care oricum nu era recomandată) nu mai este posibil de aplicat. Dacă un programator dorește să scrie o aplicație Windows care să facă câteva lucruri simple, algoritmul este următorul: 3 ore stă și se gândește ce facilități trebuie să aibă programul, studiază clasele pe care le-ar putea folosi și vede ce avantaje/dezavantaje are fiecare. Dacă ajunge la o concluzie o notează pe hârtie și, eventual, mai discută cu alți programatori să vadă dacă a gândit bine. Abia după aceea se apucă de implementat programul, lucru care ia o jumătate de oră. Timpul de corectare a erorilor de logică este și el redus semnificativ.
Dacă aplicația este mai complicată (de exemplu pentru rețea), timpul și complexitatea proiectării aplicației crește considerabil. Proiectarea aplicației nu mai este banală, iar stabilirea obiectivelor ei este esențială.
În stabilirea obiectivelor pentru aplicația de discuții on-line s-au avut în vedere două aspecte: aplicația trebuie să fie un exemplu, adică să acopere anumite detalii de implementare, și, pe de altă parte, aplicația trebuie să aibă funcționalitate.
Obiective referitoare la implementare:
Aplicația este împărțită în server și client pentru a fi mai ușor de urmărit codul și respectiv obiectivele celor două capete de comunicație.
Obiective referitoare la funcționalitatea serverului:
Obiective referitoare la funcționalitatea clientului:
Implementare
Scriere aplicației a fost organizată în două etape. În prima etapă au fost scrise serviciile și protocoalele de comunicare între server și client, iar în cea de-a doua etapă s-a realizat interfața-utilizator a aplicației. Cea de-a doua etapă nu se încadrează în tematica acestei lucrări, motiv pentru care nu va fi detaliată.
La nivel de comunicare între server și client aplicația pune la dispoziție două tipuri de servicii: serviciul de căutare al serverelor și serviciul de cerere-răspuns.
Serviciul de căutare al serverelor se bazează pe modelul CAsyncSocket și folosește winsocluri de tip datagramă. Protocolul este următorul: serverele creează câte un obiect-soclu de tip datagramă și „aderă” la un grup de trimitere multiplă. Adresa grupului de trimitere multiplă este aceeași pentru toată aplicația discuții on-line: 228.9.8.4 și este o adresă oarecare din clasa D de adrese IP. Clienții care doresc să afle ce servere sunt active la un moment dat trimit o datagramă tip (care este de fapt un șir de caractere: “Client activ cere raspuns de la server”) și își anunță adresa la care așteaptă răspuns. Serverele care primesc această datagramă trimit ca răspuns o datagramă ce conține numele serverului, descrierea acestui, adresa și portul pe care ascultă.
Unitatea de date a serviciului este un șir de caractere de lungime variabilă, iar primitivele sunt Setup și SearchServers pentru client, respectiv Setup și SendTo pentru server. Implementarea serviciului se face într-o clasă derivată din CAsyncSocket.
Clasa CDatagramSocket:
// Definire
class CDatagramSocket : public CAsyncSocket
{
public:
CDatagramSocket();
CDatagramSocket(CWnd *pParent);
virtual ~CDatagramSocket();
void SearchServers();
BOOL Setup(UINT uPort = 0);
public:
virtual void OnReceive(int nErrorCode);
protected:
CWnd * m_pParent;
};
// Implementare
CDatagramSocket::CDatagramSocket()
{
m_pParent = NULL;
}
CDatagramSocket::~CDatagramSocket()
{
}
CDatagramSocket::CDatagramSocket(CWnd *pParent)
{
m_pParent = pParent;
}
BOOL CDatagramSocket::Setup(UINT uPort)
{
int iTrue = 1;
if( !Create(uPort, SOCK_DGRAM) )
return FALSE;
ip_mreq mreq;
mreq.imr_interface.S_un.S_addr = INADDR_ANY;
mreq.imr_multiaddr.S_un.S_addr = inet_addr("228.9.8.4");
if( !SetSockOpt(IP_ADD_MEMBERSHIP, (void *)&mreq, sizeof(mreq), IPPROTO_IP ) )
return FALSE;
DWORD ttl = 10;
SetSockOpt(IP_MULTICAST_TTL, (void *)&ttl, sizeof(ttl), IPPROTO_IP);
return TRUE;
}
void CDatagramSocket::OnReceive(int nErrorCode)
{
TCHAR *dgram;
SOCKADDR_IN sa;
int size, sizeof_sa = sizeof(sa);
if( IOCtl(FIONREAD, (DWORD *)&size) && size && !nErrorCode )
{
dgram = new TCHAR[size+1];
size = ReceiveFrom(dgram, size, (SOCKADDR *)&sa, &sizeof_sa);
dgram[size] = '\0';
((CStatusDlg *)m_pParent)->NewServer(inet_ntoa(sa.sin_addr), ntohs(sa.sin_port),
dgram);
delete [] dgram;
}
CSocket::OnReceive(nErrorCode);
}
void CDatagramSocket::SearchServers()
{
SOCKADDR_IN sa;
int port;
TCHAR dgram[] = "Client activ cere raspuns de la server";
sa.sin_family = AF_INET;
sa.sin_addr.S_un.S_addr = inet_addr("228.9.8.4");
for( port = 8984; port < 9001; port++ )
{
sa.sin_port = htons(port);
SendTo(dgram, sizeof(dgram), (SOCKADDR *)&sa, sizeof(sa));
}
}
Serviciul de comunicare între server și client este ceva mai complex. El se bazează pe winsocluri de tip SOCK_STREAM și este un serviciu sigur, dar de tip conexiune orientată pe pachete (unități de date) și nu flux de octeți. Unitatea de date pentru protocolul asociat acestui serviciu este încapsulată într-o clasă, CMessage și constă din trei câmpuri:
Implementarea acestui serviciu și a protocolului aferent se face într-o clasă derivată din CSocket, care folosește modelul CSocket cu arhive. În acest sens clasa pachetului de date, CMessage, încapsulează o metodă de citire/scriere în arhive. Punctul cel mai sensibil în implementarea unei clase derivate din CSocket îl reprezintă citirea datelor venite de la entitatea pereche. Dacă datele trimise nu sunt corecte, aplicația s-ar bloca la încercarea de citire a lor. Pentru evitarea acestei probleme, la citirea datelor se pornește un timer și dacă s-a depășit un anumit interval de timp se întrerupe citirea.
Practic se suprascriu metodele OnReceive și OnMessagePending. În metoda OnMessagePending se caută mesajul WM_TIMER și la apariția lui se apelează CancelBlockingCall:
void CClientSocket::OnReceive(int nErrorCode)
{
CMessage message;
try
{
do{
m_uTimerId = 1;
while( !(m_uTimer = m_pParent->SetTimer(m_uTimerId, 1000, NULL)) )
m_uTimerId++;
message.Serialize(*m_pArchiveIn);
m_pParent->KillTimer(m_uTimerId);
((CRoomDlg *)m_pParent)->ConnectionMessage(m_hSocket, message);
}while( !m_pArchiveIn->IsBufferEmpty() );
}
catch( CException *pE )
{
pE->Delete();
m_pParent->KillTimer(m_uTimerId);
((CRoomDlg *)m_pParent)->ConnectionError(m_hSocket);
}
}
BOOL CClientSocket::OnMessagePending()
{
LPMSG pMsg = new MSG;
if( ::PeekMessage(pMsg, m_pParent->m_hWnd, WM_TIMER, WM_TIMER, PM_REMOVE) )
{
CancelBlockingCall();
return TRUE;
}
return CSocket::OnMessagePending();
}
La prima vedere se poate crede că limitarea citirii datelor la un interval de timp (care este foarte mic) nu este corectă și poate duce la închiderea unor conexiuni mai lente și care totuși sunt bune. Acest lucru nu se întâmplă: fiecare unitate de date transmisă se trimite ca un singur pachet TCP. Înainte de a fi apelată funcția OnReceive pachetul este primit complet de către nivelul transport, iar citirea lui efectivă este instantanee.
Rezultat
După stabilirea obiectivelor și implementarea părții de comunicare între server și client vine partea „ușoară”. Această parte o reprezintă interacțiunea cu utilizatorul: ce și cum afișează programele, cum sunt introduse datele necesare de către utilizatori, etc.
Conform cu obiectivul impus de simplitate și ușurință de înțelegere a codului, fereastra principală a ambelor aplicații este de tip dialog. Folosirea arhitecturii Document/View pentru acestă aplicație presupunea mult mai multe linii de cod, iar rezultatul ar fi fost o aceeași interfață.
După realizarea acestui ultim pas, aplicația este funcțională și se pot începe testările. După teste peste teste și eventuale modificări/îmbunătățiri aplicația este finalizată. Înainte de a fi prezentată aplicația mai este puțin „coafată” și se obține rezultatul final:
Server:
La rularea aplicației se lansează un dialog în care se pot specifica numele serverului, o descriere a acestuia și mesajul de întâmpinare care va fi trimis clienților imediat după conectare. Implicit numele serverului este numele sub care este cunoscut calculatorul gazdă pe rețeaua locală. Se poate reveni în orice moment asupra setărilor serverului apelând opțiunea Opțions din meniul General.
După stabilirea acestora serverul își începe activitatea. Inițial caută un port liber între 8984 și 9000. Dacă nu găsește nici unul liber utilizatorul este informat că serverul nu este activ din lipsă de porturi disponibile. În cazul în care găsește un port liber se „agață” de el și intră în modul de ascultare a cererilor de conexiune.
Orice fel de cerere care apare de la clienți este listată în controlul cu Informații de control. Lista cu informații de control se poate „curăța” dacă se folosește butonul Curata sau opțiunea Clear din meniul General.
În lista din dreapta (Utilizatori) sunt toți clienții conectați la un moment dat. Pentru un anumit client se poate da R-Click pe nume și apare un meniu cu opțiuni de afișare informații suplimentare și eliminarea respectivului utilizator.
Mai multe detalii despre program se pot afla apăsând pe butonul Despre sau alegând opțiunea About din meniul Help. Dacă fereastra principală este minimizată, aplicația se „ascunde” automat în SysTray unde este afișată iconița corespunzătoare. Pentru refacerea ferestrei se poate da D-Click pe această iconiță.
Ieșirea din program și oprirea activității serverului se face apăsând pe butonul Iesire sau alegând opțiunea Exit din meniul General.
Client:
Imediat după lansare, aplicația client trimite o cerere de căutare de servere. Serverele care răspund sunt listate în controlul de server din panoul Stare curenta. Pentru conectarea la un server trebuie selectat serverul din această listă, introdus un nume care va fi folosit la conectare și apăsat pe butonul Connect. Dacă nu au fost urmați cei doi pași utilizatorul este informat de eroare.
Dacă s-a reușit conectarea la server se deschide un nou panou, ce are numele identic cu numele serverului. În noul panou se pot trimite mesaje și se pot vedea mesajele trimise de ceilalți clienți din camera de discuții respectivă. În dreapta sunt listați toți utilizatorii curenți ai serverului. Numele cu care s-a făcut conectarea la server se poate schimba în orice moment apăsând butonul ce are ca text numele actual.
Dacă se dă R-Click pe numele unui utilizator din lista de clienți ai serverului apare un meniu cu opțiunea de a trimite un mesaj privat sau o alertă. Dacă se primește o alertă, fereastra clientului apare în prim-plan împreună cu un mesaj corespunzător.
Pentru ieșirea dintr-o cameră de discuții se apasă pe butonul Iesire.
Între panouri se poate naviga apăsând pe numele fiecăruia. Dacă se alege opțiunea Cauta Servere din meniul General lista de servere este refăcută cu noile servere active.
Bibliografie
Andrew S. Tanenbaum – Rețele de calculatoarea, ediția a treia; ISBN: 973-97706-3-0; Editura Computer Press AGORA, 1998
Richard W. Stevens – UNIX Network Programming, volumul I; ISBN: 0-13-490012-X; Editura Prentice-Hall, 1996
Richard W. Stevens – Advanced Programming in the UNIX Environment; ISBN: 0-201-56317-7; Editura Pretince-Hall, 1997
Chane Cullens, Mark Davidson – Utilizare Visual C++ 4, ediție specială; ISBN: 973-601-631-5
Cristian George Savu – Ghidul programatorului Visual C++ 5; ISBN: 973-9431-09-7; Editura ALL EDUCATIONAL
Viktor Toth – Visual C++ 4 Unleashed; ISBN 0-672-30874-6; Editura Sams Publishing 1996
Mark Sportack, Keith Johnson – High-performance Netwoking Unleashed; Editura Macmillan Computer Publishing 1996
Preston Gralla – How Intranets Work; ISBN: 1-56276-441-1; Editura Macmillan Computer Publishing 1996
John Ray – Special Edition Using TCP/IP; ISBN: 078-971-897-9; Editura Macmillan Computer Publishing 1999
Martin Bligh – TCP/IP Blueprints; ISBN 067-231-055-4; Editura Macmillan Computer Publishing 1997
Richard W. Stevens – TCP/IP Illustrated, volumul I; Editura Pretince-Hall, 1998
Tim Parker – Teach Yourself TCP/IP in 14 Days, ediția a doua; Editura Sams Publishing
Documentație electronică:
Martin Hall, Mark Towfiq, Geoff Arnold, David Treadwell, Henry Sanders – Windows Sockets: An Open Interface for Network Programming under Microsoft Windows, Version 1.1;
http://microdyne.com/
Martin Hall, Dave Treadwell, Mark Towfiq – Windows Sockets 2: An Interface for Transparent Network Programming Under Microsoft Windows, Version 2.2;
http://www.stardust.com/wsresource/winsock2/ws2ident.html
Unix Sockets Programming – Frequent Asked Questions;
http://www.lcg.org/sock-faq/
Beej's Guide to Network Programming – Using Internet Sockets; Version 1.5.5;
http://www.ecst.csuchico.edu/~beej/guide/net
BSD: A quick and dirty primer – Jim Frost;
http://www.std.com/~jimf
Internet RFC/STD/FYI/BCP Archives;
http://www.faqs.org/rfcs/index.html
Internetworking Technology Overview;
http://www.cisco.com/
James F. Curose, Keith W. Ross – Computer Networking;
http://www.awlonline.com/kurose-ross
Writing IP Multicast-enabled Applications;
http://www.ipmulticast.com/
Windows Sockets Programming;
http://www.winsock.com/
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Comunicarea Prin Socluri (ID: 161316)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
