Ce sunt socket-urile Windows și cum să le vizualizați. Rezolvarea problemei de stare pe termen lung într-un mediu multi-threaded. Stabilirea unei conexiuni client cu serverul

WinSock sau socket Windows este o interfață de programare a aplicațiilor ( API ), creat pentru a implementa aplicații bazate pe protocol într-o rețea TCP/IP . Folosit pentru muncă WSOCK 32.DLL . Această bibliotecă se află în folderul \ Sistem 32 de cataloage de sistem Windows.

Există două versiuni WinSock:

WinSock 1.1 - acceptă numai protocol TCP/IP;

WinSock 2.0 - acceptă software suplimentar.

WinSock 1.1 a dat impuls dezvoltării World wide web și a permis accesul la Internet pentru utilizatorul mediu de PC Windows . Dacă scopul versiunii 1.1 a fost să rezolve o problemă, atunci obiectivul WinSock 2.0 - face mediu de rețea mai bun, mai rapid și mai fiabil. ÎN WinSock 2.0 a adăugat suport pentru alte protocoale de transport și o nouă funcționalitate pentru a asigura fiabilitatea schimbului de informații din rețea. WinSock 2.0 vă permite să creați aplicații independente de protocolul de transport care funcționează cu TCP/IP (Transmission Control Protocol/Internet Protocol), UDP (User Datagram Protocol), IPX/SPX (Internetwork Packet Exchange/Sequenced Packet Exchange), NetBEUI (NetBios Extended User Interface) ). O eficiență mai mare în astfel de aplicații este obținută prin I/O partajat și socketuri partajate.

Specificația WinSock împarte funcțiile în trei tipuri:

Blocare și neblocare (funcții Berkeley);

Informațional (obținerea de informații despre nume de domenii, servicii, protocoale Internet);

Inițializarea și deinițializarea bibliotecii.

O funcție de blocare este o funcție care oprește un program înainte de a se finaliza; neblocarea este o functie care se executa in paralel cu programul. O listă a principalelor funcții necesare pentru a crea o aplicație este dată în tabelele 1, 2, 3. Toate descrierile funcțiilor WinSock sunt date în format de limbaj C, iar exemple de apeluri lor sunt în Delphi.

Programare în rețea cu prize Windows

Named pipes sunt potrivite pentru organizarea comunicării între procese atât în ​​cazul proceselor care rulează pe același sistem, cât și în cazul proceselor care rulează pe computere conectate între ele printr-o rețea locală sau globală. Aceste capabilități au fost demonstrate folosind sistemul client-server dezvoltat în Capitolul 11, începând cu Programul 11.2.

Cu toate acestea, atât conductele numite, cât și cutiile poștale (pe care le vom folosi mai jos pentru simplitate) termen general„conductele denumite”, dacă diferențele dintre ele nu joacă un rol semnificativ) au dezavantajul că nu sunt un standard industrial. Această circumstanță face dificilă portarea unor programe precum cele discutate în capitolul 11 ​​către sisteme care nu aparțin familiei. Windows , deși conductele numite sunt independente de protocol și pot rula peste multe protocoale standard din industrie, de ex. TCP/IP.

Abilitatea de a interacționa cu alte sisteme este oferită în Windows suport priza ( prize) Prize Windows prize analogice compatibile și aproape exacte Prize Berkeley , jucând de facto rolul unui standard industrial. În acest capitol, utilizați API-ul Windows Sockets (sau „Winsock” ") este prezentat folosind exemplul unui sistem client-server modificat din Capitolul 11. Sistemul rezultat este capabil să funcționeze în rețele globale folosind protocolul TCP/IP , care, de exemplu, permite serverului să accepte cereri de la clienți UNIX sau oricare altul decât sisteme Windows.

Cititorii familiarizați cu interfața Prize Berkeley , dacă se dorește, poate sări direct în exemple care nu numai că folosesc socket-uri, ci și introduc noi capabilități de server și demonstrează tehnici suplimentare de lucru cu biblioteci care oferă suport sigur pentru fire.

Implicarea unor mijloace bazate pe standarde de asigurare a interoperabilității între sisteme eterogene, interfață Winsock oferă programatorilor acces la protocoale și aplicații de nivel înalt precum ftp, http, RPC și COM, care oferă împreună un set bogat de modele la nivel înalt care oferă suport pentru interprocese interacțiunea în rețea pentru sisteme cu arhitecturi diferite.

În acest capitol, sistemul client-server specificat este folosit ca mecanism pentru demonstrarea interfeței Winsock , iar pe măsură ce serverul este modificat, îi vor fi adăugate altele noi oportunități interesante. În special, vom folosi punctele de intrare pentru prima dată DLL (Capitolul 5) și servere în proces DLL . (Aceste noi caracteristici ar fi putut fi incluse în versiunea originală a programului în Capitolul 11, dar acest lucru ți-ar fi dispărut atenția de la dezvoltarea arhitecturii sistemului de bază.) În cele din urmă, exemple suplimentare vă vor arăta cum să creați sigur, reintrant, multithread. biblioteci.

De la interfata Winsock trebuie să se conformeze standardelor din industrie, convențiile sale de denumire și stilurile de programare sunt oarecum diferite de cele pe care le-am întâlnit în timp ce lucram cu funcțiile descrise mai devreme Windows. Strict vorbind, Winsock API nu face parte Câștigă 32/64. În plus, Winsock oferă funcții suplimentare care nu sunt supuse standardelor; Aceste funcții sunt utilizate numai atunci când este absolut necesar. Printre alte beneficii oferite Winsock , trebuie remarcată portabilitatea îmbunătățită a programelor rezultate către alte sisteme.

Prize Windows

Winsock API a fost dezvoltat ca o extensie API-ul Berkley Sockets pentru mediu Windows și, prin urmare, este susținut de toate sistemele Windows . La beneficii Winsock pot fi atribuite următoarele:

Migrarea codului existent scris pentru API-ul Berkeley Sockets , se realizează direct.

sisteme Windows ușor de integrat în rețele folosind ambele Protocolul IPv 4 TCP/IP , și o versiune care se răspândește treptat IPv 6. Pe lângă orice altceva, versiune IPv 6 permite utilizarea mai multor IP -adrese, depășind bariera existentă a adresei versiunii de 4 octeți IPv4.

Prizele pot fi partajate cu I/O suprapuse Windows (Capitolul 14), care, printre altele, face posibilă scalarea serverelor pe măsură ce numărul clienților activi crește.

Prizele pot fi considerate ca descriptori (cum ar fi MÂNER ) când utilizați funcții ReadFile și WriteFile și, cu unele limitări, atunci când utilizați alte funcții, așa cum socketurile sunt folosite ca descriptori de fișiere în UNIX . Această caracteristică este utilă atunci când trebuie să utilizați I/O asincron și porturi de completare I/O.

Există, de asemenea, extensii suplimentare, neportabile.

WinSock este o interfață de programare a aplicațiilor de rețea implementată pe toate platformele Victorie 32, interfața principală pentru a accesa diferite protocoale de rețea de bază. Interfața moștenește mult din implementare Socket-uri Berkeley (BSD) pe platformele UNIX. În mediile Win 32 a devenit complet independent de protocol, mai ales odată cu lansarea WinSock 2.

Termenul prize ) este folosit pentru a desemna descriptorii furnizorilor de transport. ÎN Victorie Socket-ul 32 este diferit de descriptorul de fișier și, prin urmare, este reprezentat de un tip separat PRIZĂ . Din punctul de vedere al modelului de referință Interfață Winsock OSI situat la nivel de sesiune si transport. Guvernat de Windows Straturile Aplicație, Prezentare și Sesiune sunt în primul rând relevante pentru aplicația dvs. C Există diferențe semnificative în implementările socket-urilor UNIX și Windows , ceea ce creează probleme evidente. Bibliotecă WinSock acceptă două tipuri de prize: sincrone (blocare) și asincrone (neblocare). Socketurile sincrone dețin controlul în timp ce operația este în desfășurare, în timp ce socketurile asincrone revin imediat controlul, continuă execuția în fundal și notifică codul de apel când se termină.

Windows 3.x moștenit a acceptat numai socluri asincrone, deoarece într-un mediu cu multitasking corporativ, preluarea controlului asupra unei sarcini „suspend” toate celelalte, inclusiv sistemul însuși. OS Windows 9 x și NT /2000/XP suporta ambele tipuri de socluri, insa, datorita faptului ca socketurile sincrone sunt mai usor de programat decat cele asincrone, acestea din urma nefiind utilizate pe scara larga. Prize de familie de protocol TCP/IP sunt folosite pentru a face schimb de date între nodurile de Internet.

Socket-urile sunt împărțite în două tipuri: stream și datagramă. Prizele de flux funcționează pe o bază orientată spre conexiune, oferind o identificare puternică a ambelor părți și asigurând integritatea și succesul livrării datelor. Socket-urile Datagram funcționează fără a stabili o conexiune și nu oferă nici identificarea expeditorului, nici controlul succesului livrării datelor, dar sunt considerabil mai rapide decât socket-urile de streaming. Alegerea unuia sau altui tip de socket este determinată de protocolul de transport pe care funcționează serverul, clientul nu poate stabili o conexiune de streaming cu serverul de datagrame.

Winsock și modelul OSI

Furnizori de transport din catalog Winsock listat de WSAEnumProtocols , se lucrează la nivelul de transport al modelului OSI , adică fiecare dintre ele oferă schimb de date. Cu toate acestea, toate aparțin unui tip de protocol, iar un protocol de rețea funcționează la nivel de rețea, deoarece determină modul în care este adresat fiecare nod din rețea. De exemplu, UDP și TCP acestea sunt transporturi, deși ambele aparțin protocolului IP. Interfață Winsock situat între straturile de sesiune și de transport. Winsock vă permite să deschideți, să închideți și să gestionați o sesiune pentru orice transport dat. Guvernat de Windows Trei niveluri superioare: aplicație, prezentare și sesiune, legate în principal de aplicație Winsock . Cu alte cuvinte, aplicația Winsock gestionează toate aspectele sesiunii de comunicare și, dacă este necesar, formatează datele în funcție de obiectivele programului.

Prize Windows

Este util să ne uităm la modul în care protocoalele disponibile utilizează caracteristicile Winsock. Această interfață se bazează pe conceptul de priză. Un socket este un descriptor pentru un furnizor de transport. În Win32, un socket este diferit de un descriptor de fișier și, prin urmare, este reprezentat de un tip separat SOCKET. Priza este creată de una dintre cele două funcții:

// Cod 1.06

SOCKET WSASocket (

Int af,

tip int

Protocolul int

LPWSAPROTOCOL_INFO lpProtocolInfo,

GRUPA g,

DWORD dwFlags

priză SOCKET (

Int af,

tip int

Protocolul int

Primul parametru, af, specifică familia de adrese de protocol. De exemplu, dacă trebuie să creați un socket UDP sau TCP, trebuie să înlocuiți constanta AF_ INET pentru a face referire la protocolul IP. Al doilea parametru, tipul, este tipul de soclu pentru acest protocol. Poate fi una dintre următoarele valori: SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW și SOCK_RDM. Al treilea parametru, protocolul, specifică un anumit transport dacă există mai multe intrări pentru o anumită familie de adrese și tip de socket. În tabel Tabelul 1.2 listează valorile utilizate în familia de adrese, tipul de soclu și câmpurile de protocol pentru un anumit transport de rețea.

Primii trei parametri inițiali pentru crearea unei prize sunt împărțiți în trei niveluri. Prima și cea mai importantă este familia de adrese. Specifică protocolul utilizat în prezent și limitează utilizarea celui de-al doilea și al treilea parametri. De exemplu, familia de adrese ATM (AF_ATM) permite numai socluri simple (SOCK_RAW). De asemenea, alegerea familiei de adrese și a tipului de priză limitează alegerea protocolului.

Cu toate acestea, puteți trece valoarea 0 în parametrul de protocol. În acest caz, sistemul selectează un furnizor de transport pe baza celorlalți doi parametri, af și tip. Când listați intrările de director pentru protocoale, ar trebui să verificați valoarea câmpului dwProviderFlags din structura WSAPROTOCOL_INFO. Dacă este egal cu PFL_ MATCHES_PROTOCOL_ZERO, acesta este transportul standard utilizat dacă valoarea 0 este transmisă în socket-ul sau parametrul protocolului WSASocket.

După ce listați toate protocoalele folosind WSAEnumProtocols, trebuie să transmiteți structura WSAPROTOCOL_INFO funcției WSASocket ca parametru lpProtocolInfo.

Apoi trebuie să specificați constanta FROM_PROTOCOL_INFO în toți cei trei parametri (af, tip și protocol) - pentru ei vor fi folosite valorile din structura WSAPROTOCOL_INFO transmisă. Aceasta indică o intrare specifică de protocol.

Acum să ne uităm la ultimele două steaguri de la WSASocket. Parametrul de grup este întotdeauna 0 deoarece nicio versiune de Winsock nu acceptă grupuri de socket. Parametrul dwFlags specifică unul sau mai multe dintre următoarele indicatoare:

WSA_FLAG_OVERLAPPED;

WSA_FLAG_MULTIPOINT_C_ROOT;

WSA_FLAG_MULTIPOINT_C_LEAF;

WSA_FLAG_MULTIPOINT_D_ROOT;

WSA_FLAG_MULTIPOINT_D_LEAF.

Primul steag, WSA_FLAG_OVERLAPPED, indică faptul că acest socket permite I/O suprapuse, care este unul dintre mecanismele de comunicare oferite de Winsock (vezi capitolele mai târziu). Când un socket este creat de funcția socket, steag-ul WSA_FLAG_OVERLAPPED este setat în mod implicit. Este recomandat să setați întotdeauna acest flag atunci când utilizați WSASocket. Ultimele patru steaguri se aplică socket-urilor multicast.

Funcții server API

Serverul este un proces care așteaptă ca clienții să se conecteze pentru a-și deservi cererile. Serverul ar trebui să asculte pentru conexiuni pe numele standard. În TCP/IP, acest nume este adresa IP și numărul de port al interfeței locale. Fiecare protocol are propria sa schemă de adresare și, prin urmare, propriile caracteristici de denumire. Primul pas în stabilirea unei conexiuni este legarea unui socket al unui protocol dat la numele său standard folosind funcția bind. Al doilea este să comutați priza în modul de ascultare folosind funcția de ascultare. În cele din urmă, serverul trebuie să accepte conexiunea clientului folosind accept sau WSAAccept.

Este înțelept să luați în considerare fiecare apel API necesar pentru a lega, asculta și stabili o conexiune cu clientul. Apelurile de bază pe care clientul și serverul trebuie să le facă pentru a stabili un canal de comunicare sunt:

funcția de legare

După crearea unui socket pentru un anumit protocol, ar trebui să îl legați la o adresă standard apelând funcția bind:

// Cod 3.04

Int bind(

PRIZE s,

int namelen

Parametrul s specifică socket-ul pe care sunt ascultate conexiunile client. Al doilea parametru, cu tipul struct sockaddr , este pur și simplu un buffer de uz general. De fapt, trebuie să plasați o adresă în acest buffer care să corespundă standardelor protocolului utilizat și apoi când chemând bind aruncați-o pentru a tasta struct sockaddr. Fișierul antet Winsock definește tipul SOCKADDR, corespunzător structurii struct sockaddr. Acest tip va fi folosit pentru concizie mai târziu în capitol. Ultimul parametru specifică dimensiunea structurii adresei transmise, în funcție de protocol. De exemplu, următorul cod ilustrează legarea pe o conexiune TCP:

// Cod 3.05

PRIZA s;

struct sockaddr_in tcpaddr;

Port int = 5150;

S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

Tcpaddr.sin_family = AF_INET;

Tcpaddr.sin_port = htons(port);

Tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

Bind(e, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));

Mai multe detalii despre structura sockaddr_in au fost discutate în secțiunea de adresare TCP/IP a capitolului anterior. Există un exemplu de creare a unui socket de flux și apoi de configurare a unei structuri de adrese TCP/IP pentru a accepta conexiuni client. În acest caz, socket-ul indică interfața IP implicită cu numărul de port 5150. În mod oficial, apelul la blind leagă socket-ul la o interfață IP și la un port.

Dacă apare o eroare, funcția bind returnează SOCKET_ERROR. Cea mai frecventă eroare la apelarea bind WSAEADDRINUSE. În cazul TCP/IP, aceasta înseamnă că un alt proces este deja asociat cu interfața IP locală și numărul portului, sau sunt în starea TIME_WAIT. Apelarea bind din nou pe un socket deja legat returnează o eroare WSAEFAULT.

^ Funcția de ascultare

Acum trebuie să puneți priza în modul de ascultare. Funcția bind asociază doar un socket cu o anumită adresă. Pentru a pune o priză în starea de așteptare a conexiunilor de intrare, utilizați funcția API de ascultare:

// Cod 3.06

Int asculta (

PRIZE s,

Int restante

Primul parametru este socket-ul asociat. Parametrul backlog determină lungimea maximă a cozii de conexiuni care așteaptă să fie procesate, ceea ce este important atunci când se solicită mai multe conexiuni la server. Fie valoarea acestui parametru 2, apoi atunci când trei cereri de client sunt primite simultan, primele două conexiuni vor fi plasate într-o coadă de așteptare, iar aplicația le va putea procesa. A treia cerere va returna o eroare WSAECONNREFUSED. După ce serverul acceptă conexiunea, cererea este eliminată din coadă și o alta îi ia locul. Semnificația backlogului depinde de furnizorul de protocol. Valoarea nevalidă este înlocuită cu cea mai apropiată valoare validă. Nu există o modalitate standard de a obține valoarea reală a restanțelor.

Erorile asociate cu ascultarea sunt destul de simple. Cel mai comun este WSAEINVAL, ceea ce înseamnă de obicei că funcția bind nu a fost apelată înainte de ascultare. Uneori apare o eroare WSAEADDRINUSE la apelarea listen, dar mai des apare la apelarea bind.

^ Funcțiile acceptă și WSAAccept

Totul este pregătit pentru a accepta conexiuni client și puteți apela funcția accept sau WSAAccept. Prototip Accept:

// Cod 3.07

SOCKET accept(

PRIZE s,

Struct sockaddr FAR * adresă,

Int FAR * addrlen

Soclul asociat parametrului în starea de ascultare. Al doilea parametru este adresa structurii actuale SOCKADDR_IN, iar addrlen este o referință la lungimea structurii SOCKADDR_IN. Pentru un socket al altui protocol, puteți înlocui SOCKADDR_IN cu structura SOCKADDR corespunzătoare protocolului respectiv. Apelul de acceptare servește prima cerere de conectare din coadă. La finalizare, structura addr va conține informații despre adresa IP a clientului care a trimis cererea, iar parametrul addrlen va conține dimensiunea structurii.

În plus, accept returnează un nou descriptor de socket corespunzător conexiunii client acceptate. Toate operațiunile ulterioare cu acest client trebuie să folosească noul socket. Soclul de ascultare original este folosit pentru a accepta alte conexiuni client și rămâne în modul de ascultare.

Winsock 2 are o funcție WSAAccept care poate stabili conexiuni în funcție de rezultatul evaluării condiției:

//Cod 3.08

SOCKET WSAAccept(

PRIZE s,

Struct sockaddr FAR * adresă,

adresa LPINT,

LPCONDITIONPROC lpfnCondition,

DWORD dwCallbackData

Primii trei parametri sunt aceiași ca în accept pentru Winsock 1. Parametrul lpfnCondition este un pointer către o funcție numită atunci când clientul solicită. Determină dacă o conexiune poate fi acceptată și are următorul prototip:

//Cod 3.09

int CALLBACK ConditionFunc(

LPWSABUF lpCallerId,

LPWSABUF lpCallerData,

LPQOS lpSQOS,

LPQOS lpGQOS,

LPWSABUF lpCalleeId,

LPWSABUF lpCalleeData,

GROUP FAR*g,

DWORD dwCallbackData

Parametrul lpCallerId transmis prin valoare conține adresa obiectului de conectare. Structura WSABUF este utilizată de multe funcții Winsock 2 și este definită după cum urmează:

// Cod 3.10

typedef struct _WSABUF

U_long len;

Char FAR * buf;

) WSABUF , FAR * LPWSABUF ;

În funcție de utilizarea sa, câmpul len specifică dimensiunea buffer-ului la care face referire câmpul buf sau cantitatea de date din buffer-ul.

Pentru lpCallerId, parametrul buf indică structura adresei de protocol prin care se realizează conexiunea. Pentru a accesa informațiile corect, pur și simplu aruncați indicatorul buf către tipul SOCKADDR corespunzător. Când se utilizează protocolul TCP/IP, aceasta trebuie să fie o structură SOCKADDR_IN care să conțină adresa IP a clientului care se conectează. Majoritate protocoale de rețea identificarea abonatului de suport pentru acces la distanță în etapa de solicitare.

Parametrul lpCallerData conține datele trimise de client în timpul solicitării de conectare. Dacă aceste date nu sunt specificate, acestea sunt NULL. Majoritatea protocoalelor de rețea, cum ar fi TCP/IP, nu utilizează informații de conectare. Pentru a afla dacă un protocol acceptă această caracteristică, puteți să vă uitați la intrarea corespunzătoare din directorul Winsock apelând funcția WSAEnumProtocols (vezi Capitolul 1).

Următorii doi parametri, lpSQOS și lpGQOS, specifică nivelul de calitate a serviciului solicitat de client. Ambii parametri se referă la o structură care conține informații despre cerințele de lățime de bandă de transmisie și recepție. Dacă clientul nu solicită parametrii de calitate a serviciului (QoS), atunci aceștia sunt NULL. Diferența dintre ele este că lpSQOS este folosit pentru o singură conexiune, iar IpGQOS este folosit pentru grupuri de prize. Grupurile de socket nu sunt implementate sau suportate în Winsock 1 și 2. Mai multe despre QoS în capitolele ulterioare.

Parametrul lpCalleeId este o altă structură WSABUF care conține adresa locală la care este conectat clientul. Câmpul buf indică obiectul SOCKADDR al familiei de adrese corespunzătoare. Aceste informații sunt utile dacă serverul rulează pe o mașină multicast. Dacă serverul este asociat cu adresa INADDR_ANY, cererile de conectare vor fi servite pe orice interfață de rețea, iar parametrul conține adresa interfeței care a acceptat conexiunea.

Parametrul lpCalleeData completează lpCallerData. Se referă la o structură WSABUF pe care serverul o poate folosi pentru a trimite date către client în timpul stabilirii conexiunii. Dacă furnizorul dvs. de servicii acceptă această caracteristică, câmpul len specifică numărul maxim de octeți de trimis. În acest caz, serverul copiază până la acest număr de octeți în blocul buf al structurii WSABUF și actualizează câmpul len pentru a indica câți octeți sunt transferați. Dacă serverul nu trebuie să returneze informații despre conexiune, atunci înainte de a reveni funcție condiționată Când o conexiune este acceptată, câmpul len va fi setat la 0. Dacă furnizorul nu acceptă transmiterea datelor de conexiune, câmpul len va fi setat la 0. Majoritatea protocoalelor: de fapt, toate suportate de platformele Win32 nu acceptă comunicarea conexiunii .

După procesarea parametrilor trecuți funcției condiționate, serverul trebuie să decidă dacă acceptă, respinge sau întârzie cererea de conectare. Dacă conexiunea este acceptată, funcția condiționată va returna CF_ASSERT, dacă CF_REJECT este respins. Dacă din anumite motive nu se poate lua o decizie în acest moment, CF_DEFER este returnat.

Odată ce serverul este gata să proceseze cererea, apelează funcția WSAAccept. Funcția condiționată rulează în același proces ca WSAAccept și ar trebui să ruleze cât mai repede posibil. În protocoalele suportate de platformele Win32, cererea clientului este întârziată până când este evaluată valoarea funcției condiționate. În cele mai multe cazuri, stiva de rețea de bază poate accepta deja conexiunea până la apelarea funcției condiționate. Când CF_REJECT revine, stiva îl închide pur și simplu (vezi capitolele mai târziu).

Dacă apare o eroare, valoarea returnată este INVALID_SOCKET, cel mai adesea WSAEWOULDBLOCK. Apare atunci când priza este în modul asincron sau neblocant și nu există nicio conexiune de recepție. Dacă funcția condiționată returnează CF_DEFER, WSAAccept va genera o eroare WSATRY_AGAIN dacă CF_REJECT WSAECONNREFUSED.

Funcții API client

Partea client este mult mai simplă și sunt necesari doar trei pași pentru a stabili o conexiune: creați un socket folosind funcția socket sau WSASocket; rezolvați numele serverului (în funcție de protocolul utilizat); inițiați o conexiune utilizând funcția connect sau WSAConnect.

Din capitolul 2, știți cum să creați un socket și să rezolvați un nume de gazdă IP, așa că singurul pas rămas este stabilirea unei conexiuni. Capitolul 2 a analizat, de asemenea, rezoluția numelor pentru alte familii de protocoale.

^ Stările TCP

Nu trebuie să știți despre stările TCP pentru a utiliza Winsock, dar acestea vă pot ajuta să înțelegeți mai bine ce se întâmplă cu protocolul atunci când apelați API-urile Winsock. În plus, mulți programatori întâmpină aceleași probleme la închiderea socket-urilor, stările TCP fiind cele mai interesante.

Starea inițială a oricărui socket este ÎNCHIS. Odată ce clientul inițiază conexiunea, un pachet SYN este trimis către server și socket-ul clientului intră în starea SYN_SENT. La primirea pachetului SYN, serverul trimite un pachet SYN-și-ACK, iar clientul răspunde cu un pachet ACK. Din acest moment, socket-ul clientului intră în starea ESTABLISHED. Dacă serverul nu trimite un pachet SYN-ACK, clientul expiră și revine la starea ÎNCHIS.

Dacă soclul serverului este conectat și ascultă pe interfața și portul local, atunci este în starea LISTEN. Când clientul încearcă să stabilească o conexiune, serverul primește un pachet SYN și răspunde cu un pachet SYN-ACK. Starea soclului serverului se schimbă în SYN_RCVD. În cele din urmă, după ce clientul trimite un pachet ACK, socket-ul serverului este plasat în starea ESTABLISHED.

Există două moduri de a închide o conexiune. Dacă acest proces este pornit de aplicație, atunci închiderea se numește activă, în caz contrar se numește pasivă. În fig. 3.2 prezintă ambele tipuri de închidere. Când o conexiune este închisă activ, aplicația trimite un pachet FIN. Dacă aplicația apelează closesocket sau shutdown (cu al doilea argument SD_SEND), trimite un pachet FIN către gazdă și starea socketului se schimbă în FIN_WAIT_1. De obicei, nodul răspunde cu un pachet ACK și soclul intră în starea FIN_WAIT_2. Dacă și nodul închide conexiunea, trimite un pachet FIN, iar computerul răspunde cu un pachet ACK și pune socket-ul în starea TIME_WAIT.

Starea TIME_WAIT se mai numește și starea de așteptare 2*MSL MSL Maximum Segment Lifetime, cu alte cuvinte, timpul în care un pachet rămâne în rețea înainte de a fi aruncat. Fiecare pachet IP are un câmp time-to-live (TTL). Dacă este 0, atunci pachetul poate fi aruncat. Fiecare router care servește pachetul scade valoarea TTL cu 1 și transmite pachetul. Odată ce aplicația intră în starea TIME_WAIT, ea rămâne acolo pentru două perioade de timp egale cu MSL. Acest lucru permite TCP, dacă pachetul ACK final este pierdut, să-l retrimite, urmat de trimiterea FIN. După 2*MSL, priza intră în starea ÎNCHIS.

Rezultatul celorlalte două metode de închidere active este starea TIME_WAIT. În cazul precedent, doar o parte a trimis un FIN și a primit un răspuns ACK, iar nodul a rămas liber să transmită date până când a fost închis. Există alte două metode posibile aici. În primul caz, închidere simultană, computerul și nodul solicită simultan închiderea: computerul trimite un pachet FIN către nod și primește un pachet FIN de la acesta.

Apoi, ca răspuns la pachetul FIN, computerul trimite un pachet ACK și schimbă starea soclului în ÎNCHIDERE. După ce computerul primește un pachet ACK de la gazdă, socket-ul intră în starea TIME_WAIT.

Al doilea caz de închidere activă este o variație a închiderii simultane: socket-ul trece imediat de la starea FIN_WAIT_1 la starea TIME_WAIT. Acest lucru se întâmplă dacă o aplicație trimite un pachet FIN și imediat după aceea primește un pachet FIN-ACK de la gazdă. În acest caz, nodul confirmă pachetul FIN al aplicației prin trimiterea propriului pachet, la care aplicația răspunde cu un pachet ACK.

Semnificația principală a stării TIME_WAIT este că, în timp ce conexiunea așteaptă expirarea 2*MSL, perechea de prize implicată în conexiune nu poate fi reutilizată. O pereche de prize este o combinație de porturi IP locale și de la distanță. Unele implementări TCP nu permit reutilizarea niciunuia dintre porturile unei perechi de socketuri care se află în starea TIME_WAIT. Implementarea Microsoft nu are acest defect. Dacă încercați să vă conectați la o pereche de prize care se află în starea TIME_WAIT, va apărea o eroare WSAEADDRINUSE. O soluție la problemă (altul decât așteptarea sfârșitului stării TIME_WAIT a unei perechi de prize folosind portul local) este să utilizați opțiunea socket SO_REUSEADDR. SO_REUSEADDR este discutat mai detaliat în capitolele următoare.

În cele din urmă, este util să luăm în considerare închiderea pasivă. În acest scenariu, aplicația primește un pachet FIN de la gazdă și răspunde cu un pachet ACK. În acest caz, socket-ul aplicației intră în starea CLOSE_WAIT. Deoarece nodul și-a închis partea, nu mai poate trimite date, dar aplicația este liberă să facă acest lucru până când își închide partea conexiunii. Pentru a-și închide partea, aplicația trimite un pachet FIN, după care socket-ul TCP al aplicației este plasat în starea LAST_ACK. După primirea unui pachet ACK de la gazdă, socket-ul aplicației revine la starea ÎNCHIS.

^ Funcțiile Connect și WSAConnect

Rămâne să discutăm despre stabilirea reală a conexiunii. Acest lucru se face apelând connect sau WSAConnect. Mai întâi ne putem uita la versiunea Winsock 1 a acestei funcții:

// Cod 3.11

Int connect(

PRIZA s

const struct sockaddr FAR * nume,

int namelen

Parametrii se explică aproape de la sine: s socketul TCP real pentru a stabili conexiunea, denumirea structurii adresei socketului (SOCKADDR_IN) pentru TCP, descriind serverul la care să se conecteze, denumirea lungimii variabilei căii. Versiunea Winsock 2 a acestei funcții este definită după cum urmează:

// Cod 3.12

Int WSAConnect(

PRIZE s,

const struct sockaddr FAR * nume,

Nume int

LPWSABUF lpCallerData,

LPWSABUF lpCalleeData,

LPQOS lpSQOS,

LPQOS lpGQOS

Primii trei parametri sunt aceiași ca în funcția de conectare. Următoarele două: lpCallerData și lpCalleeData, sunt buffer-uri de șir utilizate pentru a primi și trimite date în momentul stabilirii conexiunii. Parametrul lpCallerData indică un buffer care conține date trimise de client către server împreună cu cererea de conectare; lpCallerData într-un buffer cu date returnate de server în timpul stabilirii conexiunii. Ambele variabile sunt structuri WSABUF, iar pentru lpCallerData, câmpul len trebuie să indice lungimea datelor din buffer-ul tampon transmis. În cazul lpCalleeData, câmpul len determină dimensiunea buffer-ului tampon unde datele sunt primite de la server. Ultimii doi parametri, lpSQOS și lpGQOS, se referă la structurile QoS care definesc cerințele de lățime de bandă de trimitere și recepție ale conexiunii stabilite. Parametrul lpSQOS specifică cerințele pentru socket-uri și lpGQOS pentru un grup de socket-uri. Grupurile de socket nu sunt complet acceptate în acest moment. O valoare lpSQOS de zero înseamnă că aplicația nu are cerințe de calitate a serviciului.

Dacă computerul la care vă conectați nu are un proces care rulează ascultând pe acest port, funcția de conectare va returna o eroare WSAECONNREFUSED. O altă eroare, WSAETIMEDOUT, apare atunci când destinația apelată este inaccesibilă, de exemplu, din cauza unei defecțiuni a echipamentelor de comunicații în drumul către nod sau a nodului indisponibil în rețea.

Transfer de date

Cel mai important lucru în programarea în rețea este să poți trimite și primi date. Funcțiile send și WSASend sunt folosite pentru a trimite date printr-un socket. În mod similar, funcțiile recv și WSARecv există pentru a primi date.

Toate tampoanele utilizate la trimiterea și primirea datelor sunt compuse din elemente char. Adică, aceste funcții nu sunt concepute pentru a funcționa cu codificarea UNICODE. Acest lucru este deosebit de important pentru Windows CE, deoarece folosește UNICODE în mod implicit. Există două modalități de a trimite un șir de caractere UNICODE: în forma sa originală sau turnat la tipul char. Nuanța este că atunci când se specifică numărul de caractere care trebuie trimise sau primite, rezultatul funcției care determină lungimea șirului trebuie înmulțit cu 2, deoarece fiecare caracter UNICODE ocupă 2 octeți din matricea șirurilor. O altă modalitate: mai întâi convertiți șirul din UNICODE în ASCII folosind funcția WideCharToMultiByte.

Toate funcțiile pentru primirea și trimiterea datelor returnează codul SOCKET_ERROR atunci când apare o eroare. Puteți apela funcția WSAGetLastError pentru a obține informații mai detaliate despre eroare. Cele mai frecvente erori sunt WSAECONNABORTED și WSAECONNRESET. Ambele apar atunci când conexiunea este închisă, fie prin expirarea timpului, fie prin închiderea conexiunii de către nodul egal. O altă eroare comună este WSAEWOULDBLOCK, care apare de obicei atunci când se utilizează socluri neblocante sau asincrone. În esență, înseamnă că funcția nu poate fi executată în acest moment. Următorul capitol va descrie diferite metode Winsock I/O care vă vor ajuta să evitați aceste erori.

^ Funcțiile Send și WSASend

Funcția de trimitere API pentru trimiterea datelor printr-un socket este definită după cum urmează:

// Cod 3.13

Trimitere int(

PRIZE s,

Const char FAR * buf,

Inteligent,

steaguri int

Parametrul s specifică socket-ul către care să se trimită datele. Al doilea parametru, buf, indică un buffer de caractere care conține datele de trimis. Al treilea len, specifică numărul de caractere trimise din buffer. Iar ultimul parametru, flags, poate lua valorile 0, MSG_DONTROUTE, MSG_OOB sau rezultatul unui OR logic peste oricare dintre acești parametri. Când este specificat indicatorul MSG_DONTROUTE, transportul nu va direcționa pachetele de ieșire. Gestionarea acestei cereri este lăsată la latitudinea protocolului de bază (de exemplu, dacă transportul nu acceptă această opțiune, cererea este ignorată). Indicatorul MSG_OOB indică faptul că datele ar trebui trimise în afara benzii, adică urgent.

Dacă are succes, funcția de trimitere va returna numărul de octeți transferați, în caz contrar, va returna o eroare SOCKET_ERROR. Unul dintre greșeli tipice WSAECONNABORTED, apare atunci când conexiunea virtuală este întreruptă din cauza unei erori de protocol sau a unui timeout. În acest caz, priza trebuie închisă deoarece nu mai poate fi folosită. Eroarea WSAECONNRESET apare dacă o aplicație de pe gazda la distanță, după efectuarea unei închideri hardware, resetează conexiunea virtuală sau se termină în mod neașteptat sau gazda la distanță repornește. În această situație, priza trebuie să fie și el închisă. O altă eroare WSAETIMEDOUT, apare adesea atunci când conexiunea este pierdută din cauza defecțiunilor rețelei sau a defecțiunii sistemului de la distanță fără avertisment.

Funcția Winsock versiunea 2 WSASend, un analog al trimiterii, este definită după cum urmează:

// Cod 3.14

Int WSASend(

PRIZE s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

DWORD dwFlags,

Un socket este un descriptor de sesiune de conexiune valid. Al doilea parametru indică o structură WSABUF sau o matrice a acestor structuri. Al treilea specifică numărul de structuri WSABUF care urmează să fie transmise. Structura WSABUF include bufferul de caractere în sine și lungimea acestuia. Poate apărea întrebarea: de ce trebuie să trimiteți mai mult de un buffer simultan? Aceasta se numește I/O scatter-gather. Acest lucru va fi discutat mai în detaliu mai târziu, dar deocamdată se poate observa că atunci când se utilizează mai multe buffere pentru a trimite date pe un soclu de conexiune, o serie de buffere este trimisă începând cu primul și terminând cu ultima structură WSABUF.

Parametrul lpNumberOfBytesSent este un pointer către un DWORD care, după apelarea WSASend, conține numărul total de octeți trimiși. Parametrul flags dwFlags este același ca în funcția de trimitere. Ultimele două pointere, 1pOverlapped și lpCompletionROUTINE, sunt folosite pentru I/O suprapuse, unul dintre modelele I/O asincrone suportate de Winsock (vezi și capitolul următor).

WSASend setează parametrul lpNumberOfBytesSent la numărul de octeți scriși. Dacă are succes, funcția returnează 0, altfel SOCKET_ERROR. Erorile sunt aceleași ca pentru funcția de trimitere.

^ Funcția WSASendDisconnect

Această funcție specializată este rar utilizată. Ea definit astfel:

// Cod 3.15

Int WSASendDisconnect(

PRIZE s,

LPWSABUF lpOUT boundDisconnectData

^ Date urgente

Dacă o aplicație trebuie să trimită informații cu prioritate mai mare peste un soclu de flux, poate desemna aceste informații ca date în afara benzii (OOB). Aplicația de pe cealaltă parte a conexiunii primește și procesează date OOB printr-un canal logic separat, independent din punct de vedere conceptual de fluxul de date.

În TCP, transmisia de date OOB este implementată prin adăugarea unui marker de 1 bit (numit URG) și a unui pointer de 16 biți în antetul segmentului TCP, care permite evidențierea octeților importanți în traficul de bază. În prezent, există două moduri prin care TCP poate aloca date urgente. RFC 793, care descrie TCP și conceptul de date urgente, afirmă că indicatorul de urgență din antetul TCP este decalajul pozitiv al octetului care urmează octetului de date urgente. Cu toate acestea, RFC 1122 tratează acest offset ca un pointer către octetul de urgență însuși.

În specificația Winsock, termenul OOB se referă atât la date OOB independente de protocol, cât și la implementarea mecanismului de transmitere a datelor urgente în TCP. Pentru a verifica dacă există date urgente în coadă, funcția ioctlsocket este apelată cu parametrul SIOCATMARK. Mai multe detalii despre această funcție în capitolele următoare.

Winsock oferă mai multe modalități de transmitere a datelor urgente. Puteți să le încorporați într-un flux obișnuit sau, prin dezactivarea acestei caracteristici, să apelați o funcție separată care returnează numai date urgente. Parametrul SO_OOBINLINE controlează comportamentul datelor OOB (acesta este discutat în detaliu în capitolele următoare).

În unele cazuri, datele urgente sunt folosite de programele Telnet și Rlogin. Dacă nu intenționați să scrieți propriile versiuni ale acestor programe, ar trebui să evitați utilizarea datelor urgente - acestea nu sunt standardizate și pot avea implementări diferite pe alte platforme decât Win32. Dacă trebuie să transferați urgent unele informații din când în când, puteți crea o priză de control separată pentru date urgente și puteți furniza conexiunea principală pentru transferul normal de date.

Funcția WSASendDisconnect începe procesul de închidere a soclului și trimite datele corespunzătoare. Este disponibil numai pentru protocoalele care acceptă închiderea treptată și transferul de date atunci când are loc. Niciunul dintre furnizorii de transport existenți nu acceptă în prezent transmiterea datelor de închidere a conexiunii. Funcția WSASendDisconnect se comportă în mod similar cu oprirea cu parametrul SD_SEND, dar trimite și datele conținute în parametrul boundDisconnectData. După ce îl apelați, este imposibil să trimiteți date prin soclu. Dacă eșuează, WSASendDisconnect returnează SOCKET_ERROR. Erorile întâlnite la rularea funcției sunt similare cu erorile de trimitere.

^ Funcțiile recv și WSARecv

Funcția recv este instrumentul principal pentru recepționarea datelor printr-un socket. Este definit astfel:

// Cod 3.16

Int recv(

PRIZA s

char FAR * buf,

Inteligent,

steaguri int

Parametrul s specifică soclul pentru primirea datelor. Al doilea parametru, buf, este un buffer de caractere pentru datele primite, iar len specifică numărul de octeți de primire sau dimensiunea bufferului tampon. Ultimul parametru, flags, poate fi 0, MSG_PEEK, MSG_OOB sau rezultatul unui OR logic al oricăruia dintre acești parametri. Desigur, 0 înseamnă nicio acțiune specială. Indicatorul MSG_PEEK specifică faptul că datele disponibile trebuie copiate în buffer-ul de primire în timp ce rămân în memoria tampon de sistem. Când este finalizată, funcția returnează și numărul de octeți în așteptare.

Nu este recomandat să citiți mesajele în acest fel. Nu numai atât, deoarece există două apeluri de sistem (unul pentru a citi datele și unul fără steag MSG_PEEK pentru a șterge datele), performanța este redusă. În unele cazuri, această metodă pur și simplu nu este de încredere. Este posibil ca cantitatea de date returnate să nu corespundă cu suma totală disponibilă. În plus, prin stocarea datelor în tampoanele de sistem, sistemul lasă din ce în ce mai puțină memorie pentru a găzdui datele primite. Acest lucru reduce dimensiunea ferestrei TCP pentru toți expeditorii, împiedicând aplicația să atingă performanțe maxime. Cel mai bine este să copiați toate datele în propriul buffer și să le procesați acolo. Steagul MSG_OOB a fost discutat mai devreme când s-a discutat despre trimiterea datelor.

Utilizarea recv pe socket-uri orientate pe mesaj sau datagramă are mai multe implicații. La apelarea recv, dacă dimensiunea datelor în așteptare este mai mare decât memoria tampon furnizată, atunci eroarea WSAEMSGSIZE apare după ce tamponul este complet umplut. Eroarea depășită a dimensiunii mesajului apare numai atunci când se utilizează protocoale orientate către mesaje. Protocoalele de streaming memorează datele primite și le furnizează în întregime atunci când sunt solicitate de o aplicație, chiar dacă există mai multe date care așteaptă să fie procesate decât dimensiunea bufferului. Prin urmare, eroarea WSAEMSGSIZE nu poate apărea atunci când lucrați cu protocoale de streaming.

Funcția WSARecv are capacități suplimentare în comparație cu recv: acceptă I/O suprapuse și notificări de datagrame fragmentate.

// Cod 3.17

int WSARecv(

PRIZE s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPOWORD lpNumberOfBytesRecvd,

LPDWORD lpFlags,

LPWSAOVERLAPPED lpOveflapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Priza de conectare a parametrului. Al doilea și al treilea parametru definesc tampoanele pentru primirea datelor. Pointerul lpBuffers se referă la o matrice de structuri WSABUF, iar dwBufferCount specifică numărul de astfel de structuri din matrice. Parametrul lpNumberOfBytesReceived, dacă operația de recuperare a datelor se finalizează imediat, indică numărul de octeți primiți de acest apel. Parametrul lpFlags poate fi MSG_PEEK, MSG_OOB, MSG_PARTIAL sau rezultatul unui SAU logic al oricăruia dintre acești parametri.

Indicatorul MSG_PARTIAL poate avea semnificații și semnificații diferite în funcție de modul în care este utilizat. Pentru protocoalele orientate pe mesaj, acest flag este setat după apelarea WSARecv (dacă întregul mesaj nu poate fi returnat din cauza spațiului tampon insuficient). În acest caz, fiecare apel ulterior către WSARecv setează steag-ul MSG_PARTIAL până când este citit întregul mesaj. Dacă acest flag este transmis ca parametru de intrare, operațiunea de primire a datelor trebuie să se finalizeze de îndată ce datele sunt disponibile, chiar dacă este doar o parte a mesajului. Indicatorul MSG_PARTIAL este utilizat numai cu protocoale orientate pe mesaje. Fiecare intrare de protocol din directorul Winsock conține un steag care indică suportul pentru această caracteristică (vezi și capitolele următoare). Opțiunile lpOverlapped și lpCompletionROUTINE sunt utilizate în operațiunile I/O suprapuse (discutate în capitolul următor).

^ Funcția WSARecvDisconnect

Această funcție este inversa lui WSASendDisconnect și este definită astfel:

// Cod 3.18

Int WSARecvDisconnect(

PRIZE s,

LPWSABUF lpInboundDisconnectData

La fel ca WSASendDisconnect, parametrii săi sunt un mâner de soclu de conexiune și o structură WSABUF reală pentru a primi date. Funcția acceptă numai date de închidere a conexiunii trimise de cealaltă parte de către funcția WSASendDisconnect, nu poate fi utilizată pentru a primi date obișnuite. În plus, imediat după primirea datelor, nu mai primește de la distanță, ceea ce este echivalent cu apelarea opririi cu parametrul SD_RECV.

^ Funcția WSARecvEx

Această funcție este o extensie specială Microsoft pentru Winsock 1. Este identică cu recv, cu excepția faptului că parametrul flags este transmis prin referință. Acest lucru permite furnizorului de bază să seteze steag MSG_PARTLAL.

// Cod 3.19

Int PASCAL FAR WSARecvEx(

PRIZA s

char FAR * buf,

Inteligent,

int * steaguri

Dacă datele primite nu constituie un mesaj complet, steag-ul MSG_PARTIAL este returnat în parametrul flags. Este folosit numai cu protocoale orientate pe mesaje. Când steag-ul MSG_PARTIAL este trecut ca parte a parametrului flags la primirea unui mesaj incomplet, funcția iese imediat, returnând datele primite. Dacă nu există suficient spațiu tampon pentru a accepta întregul mesaj, WSARecvEx va returna o eroare WSAEMSGSIZE și datele rămase vor fi eliminate. Există o diferență între steag-ul MSG_PARTIAL și eroarea WSAEMSGSIZE: în cazul unei erori, întregul mesaj a sosit, dar tamponul corespunzător este prea mic pentru a-l primi. Indicatoarele MSG_PEEK și MSG_OOB pot fi, de asemenea, utilizate în WSARecvEx.

Încheierea unei sesiuni

Când ați terminat de utilizat socket-ul, trebuie să închideți conexiunea și să eliberați toate resursele asociate cu mânerul prizei apelând funcția dosesocket. Utilizarea sa incorectă poate duce la pierderea datelor, așa că înainte de a apela dosesocket, sesiunea trebuie să fie încheiată corect cu funcția de închidere.

^ Funcția de oprire

O aplicație scrisă corect anunță destinatarul când datele au fost trimise. Nodul ar trebui să facă același lucru. Acest comportament se numește terminarea grațioasă a sesiunii și se realizează folosind funcția shutdoum:

// Cod 3.23

Închidere int(

PRIZE s,

Int cum

Parametrul arc poate fi SD_RECEIVE, SD_SEND sau SD_BOTH. Valoarea SD_RECEIVE interzice toate apelurile ulterioare către orice funcție de primire a datelor, aceasta nu afectează protocoalele de nivel inferior. Dacă există date în coada de socket TCP sau dacă ajung mai târziu, conexiunea este resetată. Socket-urile UDP într-o situație similară continuă să primească date și să le pună în coadă. SD_SEND dezactivează toate apelurile ulterioare la funcțiile de trimitere a datelor. În cazul socket-urilor TCP, după ce destinatarul confirmă primirea tuturor datelor trimise, este trimis un pachet FIN. În cele din urmă, SD_BOTH dezactivează atât primirea, cât și trimiterea.

^ Funcția closesocket

Această funcție închide priza. Este definit astfel:

// Cod 3.24

int closesocket(SOCKET s);

Apelarea closesocket eliberează mânerul socketului și toate operațiunile ulterioare pe socket vor avea ca rezultat o eroare WSAENOTSOCK. Dacă nu există alte referințe la socket, toate resursele asociate cu handle-ul vor fi eliberate, inclusiv datele din coadă.

Apelurile asincrone în așteptare care provin din orice fir în acest proces sunt anulate în mod silențios. Operațiunile I/O suprapuse în așteptare sunt de asemenea eliminate. Toate evenimentele care rulează, procedurile și porturile de terminare asociate cu I/O blocate vor eșua cu o eroare WSA_OPERATION_ABORTED. (Modelele I/O asincrone și neblocante sunt discutate mai detaliat în capitolul următor.) Un alt factor care afectează comportamentul funcției closesocket este valoarea parametrului socket SO_LINGER (care este descris pe deplin în capitolele următoare).

Protocoale fără conexiune

Principiul de funcționare al unor astfel de protocoale este diferit, deoarece folosesc alte metode pentru trimiterea și primirea datelor. Să discutăm mai întâi despre receptor (sau server), deoarece un receptor fără conexiune nu este mult diferit de serverele care necesită conexiune.

Receptor

Procesul de primire a datelor pe o priză fără conexiune este simplu. În primul rând, un socket este creat folosind socket sau funcția WSASocket. Apoi socket-ul este legat de interfața pe care vor fi primite datele folosind funcția bind (ca în cazul protocoalelor orientate spre sesiune). Diferența este că nu poți apela asculta sau accepta: în schimb trebuie doar să aștepți ca datele primite să fie primite. Deoarece nu există nicio conexiune în acest caz, priza de recepție poate primi datagrame de la orice mașină din rețea. Cel mai simplu funcția de primire recvform.

// Cod 3.28

Int recvfrom(

PRIZE s,

Char FAR* buf,

Inteligent,

steaguri int,

Struct sockaddr FAR * de la,

int FAR * fromlen

Primii patru parametri sunt aceiași ca pentru funcția recv, inclusiv valori valide pentru steaguri-. MSG_OOB și MSG_PEEK. Parametrul from este o structură SOCKADDR pentru acest protocol socket de ascultare, dimensiunea structurii adresei este referită de fromlen. Când apelul revine, structura SOCKADDR va conține adresa stației de lucru care trimite datele.

Winsock 2 folosește o versiune diferită de recvform WSARecvForm:

//Cod 3.29

Int WSARecvFrom(

PRIZE s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesRecvd,

LPDWORD lpFlags,

Struct sockaddr FAR * lpFrom,

LPINT lpFromlen,

LPWSAOVERLAPPED lpSuprapus,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Diferența dintre versiuni este utilizarea structurii WSABUF pentru a obține date. Puteți furniza unul sau mai multe buffer-uri WSABUF specificând numărul lor în divBufferCount, în acest caz, I/O complexe este posibilă. Numărul total de octeți citiți este transmis către lpNumberOfBytesRecvd. La apelarea funcției WSARecvFrom, lpFlags poate lua următoarele valori: 0 (dacă nu există parametri), MSG_OOB, MSG_PEEK sau MSG_PARTIAL. Aceste steaguri pot fi combinate folosind o operație logică SAU. Dacă semnalul MSG_PARTlAL este specificat la apelarea funcției, furnizorul va redirecționa datele chiar dacă este primită doar o parte a mesajului. La întoarcere, indicatorul este setat la MSG_PARTIAL numai dacă mesajul este primit parțial. La întoarcere, WSARecvFrom va seta parametrul lpFrom (un pointer către o structură SOCKADDR) la adresa computerului care trimite. Din nou, lpFromLen indică dimensiunea structurii SACKADDR, dar în această funcție este un pointer către un DWORD. Ultimii doi parametri, lpOverlapped și lpCompletionROUTINE, sunt utilizați pentru I/O suprapuse (vezi capitolul următor).

O altă modalitate de a primi (trimite) date pe prize fără conexiune este stabilirea unei conexiuni (deși acest lucru sună ciudat). Odată ce soclul este creat, puteți apela connect sau WSAConnect, setând parametrul SOCKADDR la adresa computer la distanță care trebuie contactat. De fapt, nu are loc nicio conexiune. Adresa socket-ului transmisă funcției de conectare este asociată cu socket-ul, astfel încât funcțiile recv și WSARecv pot fi utilizate în loc de recvfrom sau WSARecvFrom (deoarece sursa de date este cunoscută). Dacă aplicația dvs. trebuie să comunice cu un singur punct final la un moment dat, puteți utiliza capacitatea de a conecta o soclu de datagramă.

Expeditor

Există două moduri de a trimite date printr-o priză fără conexiune. Prima și cea mai simplă este să creați un socket și să apelați funcția sendto sau WSASendTo. Merită să vă uitați mai întâi la funcția sendto:

// Cod 3.30

Int sendto(

PRIZE s,

const char FAR * buf,

Inteligent,

steaguri int,

Const struct sockaddr FAR * to,

int tolen

Parametrii acestei funcții sunt aceiași cu recvfrom, cu excepția buffer-ului de date de trimis și len care indică câți octeți de trimis. Parametrul to este un pointer către o structură SOCKADDR cu adresa stației de lucru destinatare.

De asemenea, puteți utiliza funcția WSASendTo din Winsock 2:

// Cod 3.31

Int WSASendTo(

PRIZE s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesSent,

DWORD dwFlags,

Const struct sockaddr FAR * lpTo,

Int iToLen,

LPWSAOVERLAPPED lpSuprapus,

LPWSAOVERLAPPED_COMPLETION ROUTINE lpCompletionROUTINE

Din nou, funcția WSASendTo este similară cu predecesorul său. Acceptă un pointer către una sau mai multe structuri WSABUF cu date de trimis către destinatar ca parametru lpBuffers, iar divBufferCount specifică numărul de structuri. Pentru I/O complexe, pot fi trimise mai multe structuri WSABUF. Înainte de a ieși, WSASendTo setează al patrulea parametru lpNumberOfBytesSent la numărul de octeți efectiv trimiși destinatarului. Structura parametrului lpTo SOCKADDR pentru acest protocol cu ​​adresa receptorului. Parametrul iToLen lungimea structurii SOCKADDR. Ultimii doi parametri, lpOverlapped și lpCompletionROUTINE, sunt utilizați pentru I/O suprapuse (vezi și capitolul următor).

Ca și în cazul primirii datelor, o priză fără conexiune poate fi conectată la o adresă de punct final și poate fi trimisă datele utilizând funcțiile de trimitere și WSASend. După crearea acestei legături, nu puteți utiliza funcțiile sendto sau WSASendTo pentru a schimba date cu o altă adresă va fi generată eroarea WSAEISCONN. Singura modalitate de a deconecta un socket este să apelați funcția closesocket cu mânerul socketului și apoi să creați un nou socket.

Model de intrare-ieșirela rândul său, determină modul în care aplicația va procesa operațiunile I/O pentru un anumit socket.

Winsock oferă două moduri de socket: blocare și neblocare, precum și mai multe modele interesante Porturi I/O care ajută aplicațiile să gestioneze operațiunile I/O pe mai multe socluri simultan într-o manieră asincronă: blocare, selectare, WSAAsyncSelect, WSAEventSelect, I/O suprapuse și port de finalizare. Toate platformele Windows oferă moduri de operare blocante și neblocante pentru socluri. Cu toate acestea, nu toate modelele I/O sunt disponibile pe toate platformele. Următorul tabel arată disponibilitatea modelelor pe diferite platforme Windows.

Specificația Winsock 2.0 permite următoarele modele de bază pentru efectuarea operațiunilor I/O pe un socket:

blocarea I/O,

multiplexarea I/O folosind select() pe un soclu care blochează sau neblochează,

I/O asincron neblocant folosind mesaje de evenimente de rețea Windows WSAAsyncSelect(),

I/O neblocante cu evenimente de rețea asincrone WSAEventSelect(),

intrare/ieșire combinată (sau I/O suprapuse),

portul de finalizare.

Moduri de priză

După cum am menționat deja, socket-urile Windows pot efectua operațiuni I/O în două moduri: blocare și neblocare. În modul de blocare, apelurile către funcțiile Winsock care efectuează operațiuni I/O, cum ar fi trimitere și recuperare, așteaptă până la finalizarea operațiunii înainte de a returna controlul aplicației. În modul Winsock neblocant, funcțiile oferă imediat controlul aplicației. Aplicațiile care rulează pe platformele Windows CE și Windows 95 (Winsock 1), care acceptă doar anumite modele de intrare/ieșire, sunt forțate să efectueze acțiuni specifice cu socket-uri de blocare și neblocare pentru a gestiona corect diferite situații.

1.1. Modul de blocare

Blocarea socket-urilor creează unele inconveniente, deoarece apelurile către oricare dintre funcțiile Winsock API sunt blocate de ceva timp. Majoritatea aplicațiilor Winsock urmează un model producător-consumator în care aplicația citește sau scrie un anumit număr de octeți și îi procesează. Următorul fragment de cod ilustrează acest model:

ciorap SOCKET;

tampon de caractere;

int făcut = 0,

Err;

În timp ce(!terminat)

// primim date

Err = recv(sock, buffer, sizeof (tampon));

Dacă (err == SOCKET_ERROR)

// gestionează eroarea de recepție

Printf("recv a eșuat cu eroarea %dn",

WSAGetLastError());

Întoarcere;

// procesarea datelor

ProcessReceivedData(buffer);

Problema cu codul de mai sus este că funcția recv nu poate oferi niciodată controlul aplicației decât dacă unele date ajung pe socket-ul dat. Unii programatori verifică datele în așteptare pe un socket apelând fie recv cu steag MSG_PEEK, fie ioctlsocket cu opțiunea FIONREAD. Verificarea datelor în așteptare pe un socket fără a le accepta este considerată o practică de programare proastă și ar trebui evitată prin orice citire valoroasă a datelor din memoria tampon de sistem. Pentru a evita această metodă, trebuie să prevenim blocarea completă a aplicației din cauza lipsei de date în așteptare, fără a apela un control pentru a vedea dacă există. O soluție la această problemă ar fi împărțirea aplicației în două fire: unul care citește și unul care procesează date, ambele partajând un buffer de date comun. Accesat de obiectul de sincronizare ca eveniment sau mutex. Sarcina firului de citire este să citească datele primite din rețea pe soclu într-un buffer partajat. Când firul de citire a considerat minim suma necesară date destinate firului de procesare, comută evenimentul în starea semnalată, informând astfel firul de procesare că există date în bufferul partajat de procesat. Firul de procesare, la rândul său, preia date din buffer și le procesează.

Următoarea bucată de cod arată implementarea acestei metode, implementând două funcții: una care furnizează citirea datelor din rețea (ReadingThread), cealaltă procesarea datelor (ProcessingThread):

#define MAX_BUFFER_SIZE 4096

// Inițializați secțiunea critică

// și evenimente de resetare automată înainte ca firele să fie inițializate

date CRITICAL_SECTION;

MANEAZĂ hEvent;

ciorap SOCKET;

tampon CHAR;

// creează un soclu de citire

// citește firul

Void Reading Thread (nulat)

Int nTotal = 0,

NRead = 0,

NStânga = 0,

NBytes = 0;

În timp ce (adevărat)

NTotal = 0;

NStânga = NUM_BYTES_REQUIRED;

În timp ce(nTotal< NUM_BYTES_REQUIRED)

NRead = recv(sock, &(buffer), nLeft, 0);

Dacă (nRead == -1)

Printf("eroare");

ExitThread();

NTotal += nRead;

NStânga -= nRead;

NBytes += nRead;

SetEvent(hEvent);

// procesare fir

Void Processing Thread(void)

În timp ce (adevărat)

// așteaptă date

WaitForSingleObject(hEvent);

EnterCriticalSection(&date);

DoSomeComputationOnData(buffer);

// elimină datele procesate din buffer

nBytes -= NUM_BYTES_NEQUIRED;

LeaveCriticalSection(&date);

Principala dificultate în programarea soclurilor de blocare este suportarea transmiterii și recepționării datelor pentru mai mult de un soclu. Folosind implementarea anterioară, aplicația trebuie modificată pentru a avea o pereche de fire de execuție de citire și procesare pe socket. Acest lucru adaugă ceva muncă de rutină pentru programator și complică codul. Singurul dezavantaj este că aplicația nu se scalează bine cu un număr mare de prize.

1.2. Mod non-blocare

O alternativă la blocarea prizelor este neblocarea. Prizele non-blocante sunt mai promițătoare, dar avantajul lor față de cele blocante nu este mare. Următorul exemplu arată cum să creați un socket și să îl comutați în modul neblocant:

ciorap SOCKET;

Nb lung nesemnat = 1;

Int err;

Sock = socket(AF_INET, SOCK_STREAM, 0);

Err = ioctlsocket(sock, FIONBIO, (nesemnat long *) &nb);

if (err == SOCKET_ERROR)

//eroare la trecerea prizei în modul neblocant

După trecerea soclului în modul neblocant, apelurile API Winsock legate de primirea, transmiterea datelor sau gestionarea conexiunilor vor returna imediat controlul aplicației, fără a aștepta finalizarea operațiunii curente. În cele mai multe cazuri, aceste apeluri returnează o eroare de tip WSAEWOULDBLOCK, ceea ce înseamnă că operația nu a avut timp să se finalizeze în perioada apelului de funcție. De exemplu, funcția recv va returna WSAEWOULDBLOCK dacă nu există date în așteptare în memoria tampon de sistem pentru un socket dat. Adesea sunt necesare apeluri suplimentare de funcții până când returnează un mesaj care indică finalizarea cu succes a operațiunii.

Deoarece majoritatea apelurilor de funcții neblocante eșuează cu o eroare WSAEWOULDBLOCK, ar trebui să verificați toate codurile de returnare și să fiți pregătit pentru un apel eșuat în orice moment. Mulți programatori fac o mare greșeală apelând o funcție tot timpul până când returnează un cod de returnare de succes. De exemplu, apelarea constantă a recv într-o buclă în timp ce așteptați ca 100 de octeți de date să fie citite nu este mai bună decât apelarea recv în modul de blocare cu parametrul MSG_PEEK. Modelele Winsock I/O pot ajuta o aplicație să determine când un socket este gata să citească sau să transmită date.

Fiecare dintre moduri - blocare și neblocare - are propriile sale dezavantaje și avantaje. Prizele de blocare sunt mai ușor de utilizat din punct de vedere conceptual, dar pot fi dificil de gestionat atunci când există un număr mare de conexiuni sau când datele sunt transferate în cantități diferite și pe perioade diferite de timp. Pe de altă parte, socket-urile neblocante sunt mai complexe, deoarece este nevoie de a scrie un cod mai complex pentru a gestiona capacitatea de a primi coduri de returnare de tip WSAEWOULDBLOCK cu fiecare apel la funcțiile Winsock API. Modelele socket I/O ajută o aplicație să gestioneze transferul de date pe una sau mai multe conexiuni simultan într-o manieră asincronă.

Model de port de terminare

Ultimul model I/O pe care îl vom analiza este modelul portului de completare. Un port de completare este un mecanism special din cadrul sistemului de operare prin care o aplicație folosește un grup de fire multiple cu scopul exclusiv de a procesa operațiuni I/O asincrone suprapuse.

Aplicațiile care sunt forțate să proceseze numeroase cereri asincrone (vorbim despre sute și mii de solicitări primite simultan de exemplu, pe motoarele de căutare sau servere populare precum www.microsoft.com), folosind acest mecanism pot procesa cererile I/O mult mai rapid și mai eficient decât pur și simplu începerea unui nou thread pentru a procesa cererea primită. Suportul pentru acest mecanism este inclus în Windows NT, Windows 2000, Windows XP și Windows Server 2003 și este eficient în special pe sistemele multiprocesor. Astfel, codul demo, care este publicat pe MSDN, este conceput pentru o platformă hardware cu 16 procesoare.

Pentru funcționarea acestui model, este necesar să se creeze un obiect software special al nucleului de sistem, care a fost numit „portul de finalizare”. Acest lucru se realizează folosind funcția CreateIoCompletionPort(), care va asocia acest obiect cu unul sau mai mulți descriptori de fișiere (socket) (vezi exemplul de mai jos în secțiunea 4.5.1.1) și care va gestiona operațiunile I/O suprapuse, folosind un anumit număr de fire. pentru a deservi cererile finalizate.

În primul rând, trebuie să creăm un obiect software - un port de completare I/O, care va fi folosit pentru a gestiona mai multe cereri I/O pentru orice număr de descriptori de socket. Acest lucru se face prin apelarea funcției CreateIoCompletionPort(), care este definită ca:

HANDLE CreateIoCompletionPort(

HANDLE FileHandle ,

HANDLE ExistingCompletionPort,

DWORD CompletionKey,

DWORD NumberOfConcurrentThreads

Înainte de a analiza parametrii în detaliu, trebuie remarcat faptul că această funcție este de fapt utilizată în două scopuri diferite:

Pentru a crea un obiect port de terminare

Asociați un mâner cu un port de completare

Când creați inițial un obiect port de completare, singurul parametru de interes este NumberOfConcurrentThreads; primii trei parametri nu sunt semnificativi. Parametrul NumberOfConcurrentThreads este specific deoarece specifică numărul de fire de execuție cărora li se permite să se execute simultan pe portul de finalizare. În teorie, avem nevoie doar de un fir pentru fiecare procesor individual pentru a servi portul de completare și pentru a evita schimbarea contextului firului. Setarea acestui parametru la 0 spune sistemului să permită atâtea fire de execuție câte procesoare există pe sistem. Următorul apelul creează un port de completare I/O:

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0, 0);

Ca rezultat, funcția va returna un mâner care este utilizat pentru a identifica portul de terminare atunci când îi este atribuit un mâner de socket.

Modele I/O

Selectați

Acest model oferă o modalitate mai controlată de blocare. Deși vă permite să lucrați cu prize de blocare, mă voi concentra pe modul neblocant. Principiul acestui model va deveni clar dacă vă uitați la ilustrație:

Dialogul dintre program și WinSock va fi următorul:

Program: „Bine, spune-mi când este cel mai bun moment să încerc din nou.”

WinSock: „Sigur, stai un minut”

"Încearcă din nou!"

Program: „Trimite aceste date”

WinSock: „Gata!”

Poate ați observat că acest model arată similar cu o priză de blocare. Acest lucru se datorează faptului că select blochează. Primul apel încearcă să efectueze o operație WinSock. În acest caz, operația ar bloca execuția procesului principal, dar funcția nu poate fi executată și eșuează. Apoi controlul este transferat la firul principal al programului, care, la rândul său, apelează metoda select (adică, programul apelează modelul pentru a determina momentul potrivit pentru a încerca din nou). Va aștepta cel mai bun moment pentru a repeta funcția WinSock.

Dar aici poate apărea o întrebare complet corectă: dacă acest model se blochează, atunci de ce îl folosim pentru prize neblocante? Faptul este că această metodă poate „așteaptă” mai multe evenimente. Mai jos este un prototip al funcției de selectare:

selectați (nfds:DWORD, readfds:DWORD, writefds:DWORD, cu excepția fds:DWORD, timeout:DWORD)

Select determină starea unuia sau mai multor socluri, oferind sincronizare I/O dacă este necesar. Primul parametru este ignorat, ultimul parametru este folosit pentru a determina timpul optim de „așteptare” pentru funcție. Parametrii rămași determină setul de prize:

readfds este un set de socketuri care vor fi verificate pentru lizibilitate.

writefds - un set de socketuri care vor fi verificate pentru scriere.

exceptfds - un set de socket-uri care vor fi verificate pentru erori.

„Lizibil” înseamnă că datele au ajuns pe soclu și că citirea în sine după selectare este aceeași cu primirea datelor. „Înregistrabil” înseamnă că acum este momentul potrivit pentru a transfera date deoarece... destinatarul poate fi dispus să le accepte. Exceptfds este folosit pentru a detecta erorile de la conexiunile neblocante.

WSAASyncSelect

Majoritatea programelor ferestre folosesc special casete de dialog, pentru a obține informații de la utilizator sau invers. WinSock oferă o modalitate prin care notificările de evenimente din rețea interacționează cu procesarea mesajelor Windows. Funcția WSAAsyncSelect vă permite să înregistrați o notificare pentru un anumit eveniment din rețea ca mesaj Windows familiar.

WSAAsyncSelect (s:DWORD, hWnd:DWORD, wMsg:DWORD, lEvent:DWORD)

Această caracteristică necesită un mesaj special (wMsg) care este selectat de utilizator. Și procedura ferestrei trebuie să proceseze chiar acest mesaj. lEvent este o mască de biți care specifică evenimentul care va fi raportat. Desenul pentru acest model se poate face astfel:

Să presupunem că primul mesaj dorește să trimită unele date către socket folosind send. Deoarece priza nu este blocată, funcția se va finaliza instantaneu. Apelul funcției poate avea succes, dar acest lucru nu se întâmplă aici. Presupunând că WSAAsyncSelect a fost configurat pentru a ne anunța despre evenimentul FD_WRITE, în cele din urmă vom primi un mesaj de la WinSock care ne spune că evenimentul a avut loc. În acest caz, acesta este evenimentul FD_WRITE, care înseamnă ceva de genul „Sunt gata, încercați să vă trimiteți datele.” Astfel, în gestionarea mesajelor, programul încearcă să trimită date, iar încercarea reușește.

Conversația dintre program și WinSock este similară cu modelul selectat, singura diferență este metoda de notificare: un mesaj în fereastră în loc de un apel sincron pentru selectare. În timp ce select blochează procesul principal care așteaptă să apară un eveniment, un program care utilizează WSAAsyncSelect poate continua procesarea mesajelor Windows până când nu apar niciun eveniment:

Programul se înregistrează pentru a notifica evenimentele din rețea prin mesaje ferestre

Program: „Trimite aceste date”

WinSock: „Nu pot face asta acum”

Programul procesează un mesaj

Programul procesează un alt mesaj

Programul primește un mesaj de notificare de la WinSock

Program: „Trimite aceste date”

WinSock: „Gata! »

WSAAsyncSelect oferă o modalitate mai „Windows” de notificare și este destul de ușor de utilizat. Pentru serverele cu lățime de bandă mică (mai puțin de 1000 de conexiuni) această metodă este destul de bună. Dezavantajul este că mesajele în fereastră în sine nu sunt foarte rapide și, de asemenea, că Windows este necesar pentru a utiliza acest model (adică programul trebuie să fie GUI).

WSAEventSelect

Notă: „obiectul eveniment” va fi înțeles în continuare ca un eveniment specific de rețea. Cert este că aici evenimentul este considerat ca o clasă =).

WSAEventSelect poate fi numit o rudă a WSAAsyncSelect, care funcționează într-un mod foarte similar, dar folosește obiecte eveniment în loc de mesaje ferestre. Există anumite avantaje în acest sens, dintre care unul este eficiența (obiectele eveniment sunt mai rapide decât mesajele din fereastră). Interpretarea grafică a acestui model pare puțin mai complicată decât precedentul, dar de fapt nu este:

Programul se înregistrează pentru a notifica evenimentele din rețea prin obiecte eveniment

Program: „Trimite aceste date”

WinSock: „Nu pot face asta acum”

Programul așteaptă ca un eveniment să îl semnaleze

Program: „Trimite aceste date”

WinSock: „Gata! »

Este greu să desenezi o imagine pentru această funcție, deoarece obiectele eveniment sunt un mecanism foarte puternic care poate fi folosit căi diferite. Am ales aici un exemplu simplu de utilizare. Din desen și dialog, esența acestui model, după părerea mea, este mai mult decât clară.

La început, acest model este similar cu unul de blocare: aștepți un eveniment despre care vei fi anunțat. Acest lucru este adevărat, dar în același timp vă puteți crea propriul obiect eveniment. Toate obiectele eveniment fac parte din WinAPI pe care o folosește WinSock. WinSock are unele funcții pentru crearea de obiecte, dar acestea sunt de fapt funcții API din pachetul WinSock.

Tot ceea ce face WinSock în acest model este să semnaleze un obiect eveniment atunci când acel eveniment este pe cale să se întâmple.

Funcția care înregistrează evenimentul de rețea WSAEventSelect:

WSAEventSelect (s:DWORD, hEventObject:DWORD, lNetworkEvents:DWORD)

WSAAsyncSelect vă va trimite un mesaj când a avut loc un eveniment de rețea (FD_READ, FD_WRITE etc.) Spre deosebire de WSAAsyncSelect, WSAEventSelect are o singură modalitate de notificare: semnalizarea unui obiect eveniment. Acest lucru permite acestui model să fie utilizat atât în aplicații GUI, și în cele de consolă. Ce evenimente au avut loc pot fi găsite folosind WSAEnumNetworkEvents.

Introducere în I/O suprapuse

I/O suprascris este foarte eficient, mai ales atunci când este implementat bine (vă permite să gestionați o mulțime de conexiuni). Acest lucru este valabil mai ales când se combină I/O suprapuse cu porturi de terminare. După cum am spus mai devreme, în cele mai multe cazuri utilizarea I/O suprapuse este inutilă, dar voi încerca totuși să acopăr acest subiect, cel puțin pentru dezvoltarea generală.

În modelele discutate mai devreme, au fost trimise unele notificări (cum ar fi „date disponibile” sau „gata să încerce să trimită date”, etc.) atunci când au avut loc anumite evenimente din rețea. Modelele suprapuse trimit și notificări, dar nu despre apariția evenimentelor din rețea, ci despre finalizarea acestora. Când o funcție WinSock este apelată, aceasta poate fie să reușească, fie să eșueze cu codul de eroare WSA_IO_PENDING. Când utilizați modele suprapuse, veți fi anunțat când operațiunea este finalizată. Aceasta înseamnă că trebuie doar să așteptați până când operația este finalizată.

Costul acestei abordări eficiente este o implementare dificilă. Dacă nu aveți nevoie de o eficiență foarte bună, atunci este mai bine să utilizați modelele descrise anterior. În plus, sistemele de operare Windows 9x/ME nu acceptă pe deplin modelele I/O suprapuse.

La fel ca modelele de notificare a evenimentelor din rețea, modelul de suprapunere poate fi, de asemenea, implementat în moduri diferite. Ele diferă prin modul în care sunt notificate: blocare, interogare, rutine de terminare și porturi de terminare.

I/O suprascris: blocare

Primul model I/O suprapus despre care voi vorbi folosește un obiect eveniment pentru a semnala finalizarea. Acest model este similar în multe privințe cu WSAEventSelect, dar diferența este că obiectul este setat la o stare semnalizată atunci când operațiunea WinSock se încheie, mai degrabă decât atunci când are loc un eveniment de rețea.

Program: „Trimite aceste date”

WinSock: „Bine, dar nu le pot trimite acum”

Programul așteaptă un semnal de la obiectul eveniment care indică faptul că funcția sa finalizat

Din imagine și dialog, esența lucrării acestui model este clară. După cum puteți vedea, funcția WinSock are loc simultan cu firul principal al programului (în acest exemplu, firul principal așteaptă un semnal de la eveniment). Când un eveniment primește un semnal de la o funcție WinSock (linia întreruptă în figură), acesta intră într-o stare semnalizată și trimite un semnal către firul principal că funcția a fost finalizată, iar firul principal procesează semnalul primit și trece la executând următoarea comandă.

I/O suprascris: sondaj

La fel ca în modelul de polling menționat anterior, în acest model este posibilă și interogarea stării de execuție a unei operații (deși în pollinge descris anterior nu am solicitat starea de execuție, ci pur și simplu am primit date despre finalizarea nereușită a funcției). . Dar firul principal al programului știa când funcția a eșuat și când invers). Puteți utiliza funcția WSAGetOverlappedResult pentru a afla starea unei operațiuni în desfășurare. Interpretarea grafică a interogării suprapuse este foarte asemănătoare cu cea a interogării obișnuite, cu excepția faptului că funcția WinSock este executată în același timp în care programul interogează pentru ca funcția să fie finalizată.

Program: „Trimite aceste date”

Program: „L-ai trimis încă?”

WinSock: „Nu”

Program: „L-ai trimis încă?”

WinSock: „Nu”

Program: „L-ai trimis încă?”

WinSock: „Nu”

Program: „L-ai trimis încă?”

WinSock: „Da! »

Și aici repet: acest model nu este foarte bun, deoarece face ca procesorul să intre în panică. Prin urmare, nu recomand utilizarea acestui model.

I/O suprascrise: rutine de finalizare

Procedurile de completare sunt proceduri de apel invers (adică, apelate ca răspuns la o anumită acțiune. De acum înainte mă voi referi la aceste proceduri drept proceduri de apel invers) care sunt apelate atunci când se finalizează o operațiune. Totul pare simplu aici, dar există un truc: aceste proceduri sunt numite în contextul firului care a început operația. Ce înseamnă? Imaginați-vă un fir care a solicitat o operație de scriere suprapusă. WinSock efectuează această operație în timp ce firul tău rulează și el. Deci WinSock are propriul thread pentru această operațiune. Când operațiunea este finalizată, WinSock trebuie să apeleze procedura de rechemare. Dacă se întâmplă acest lucru, procedura apelată va fi executată în contextul firului WinSock. Aceasta înseamnă că firul care a apelat operația de scriere se va executa în același timp cu procedura de apelare. Problema este că nu există sincronizare cu firul apelant și nu știe dacă operația s-a finalizat (cu excepția cazului în care i se spune acest lucru de un fir paralel).

Pentru a evita acest lucru, WinSock se asigură că procedura de revocare are loc pe același fir din care provine cererea. Acest lucru se face folosind APC (Asynchronous Procedure Call), un mecanism încorporat în Windows. Acest lucru poate fi considerat ca „injectarea” unei proceduri în firul principal de execuție a programului. Astfel, firul de execuție va executa mai întâi procedura și apoi va face ceea ce a făcut înainte de a fi „implementat”. Desigur, sistemul nu poate ordona firul de execuție: „Nu mai faceți tot ce făceai și procesează mai întâi această procedură”.

Pentru a asigura „injectarea” în locul potrivit, mecanismul APC necesită ca firul să fie în așa-numita stare de așteptare de notificare. Fiecare fir are propria coadă APC în care procedurile așteaptă să fie apelate. Când un fir de execuție intră în starea de așteptare de notificare, acesta indică faptul că este gata să deservească coada APC.

I/O suprascris cu rutine de finalizare utilizează APC pentru a notifica când s-a încheiat o operațiune.

Program: „Trimite aceste date”

WinSock: „Bine, dar nu le pot trimite acum”

Programul intră într-o stare de așteptare de notificare

Funcția sa încheiat

Starea de așteptare primește un semnal că funcția s-a finalizat

Funcția de retragere este executată și controlul trece la program

APC poate fi puțin greu de înțeles, dar este în regulă. Aceasta este doar o scurtă introducere. De obicei, un fir de execuție așteaptă până când este apelată o procedură de apel invers, care se ocupă de eveniment (pe care funcția WinSock a fost finalizată) și returnează controlul programului. Apoi firul principal efectuează operațiunile necesare (dacă există) și revine din nou la starea de așteptare.

I/O suprascrise: porturi de terminare

În cele din urmă ajungem la ultimul și poate cel mai eficient model I/O: I/O suprapuse cu porturi de completare. Portul de terminare este un mecanism disponibil în nucleele NT (9x nu le acceptă) care permite gestionarea eficientă a firelor. Spre deosebire de toate modelele luate în considerare, „porturile de finalizare” au propriul control al fluxului. După cum probabil ați observat, toate ilustrațiile anterioare erau ceva de genul graficelor în funcție de timp. Pentru acest model, nu am făcut un astfel de grafic și dialog de program cu WinSock, pentru că este puțin probabil ca acest lucru să ajute la clarificarea situației. În schimb, am desenat o imagine a mecanismului în sine, care oferă o idee bună despre ceea ce se întâmplă:

Ideea este următoarea: odată ce un port de completare este creat, socket-urile pot fi asociate cu acesta. Din acest punct de vedere, când operațiunea I/O suprapusă se încheie, o notificare corespunzătoare este trimisă către portul de finalizare. Există fire de lucru similare pe port care sunt blocate. Când sosește o notificare, portul preia un fir din coada de fire inactivă și îl activează. Acest fir de execuție procesează evenimentul finalizat și se blochează pe port.

Există o anumită limită a numărului de fire în portul de completare, dar de obicei nu toate sunt active în același timp, ceea ce permite reducerea cozii de fire. Prin crearea unui port ca acesta, puteți specifica câte fire vor fi active.

În acest model nu există nicio legătură între flux și conexiune. Fiecare thread poate interacționa cu un eveniment care ajunge în port. Acest model nu este ușor de implementat, dar odată implementat, puteți lucra cu mii de conexiuni.

Tutorial pentru a juca pe WINSOCK

Prize(prizele) sunt o interfață unificată de nivel înalt pentru interacțiunea cu protocoalele de telecomunicații. În literatura tehnică există diverse traduceri ale acestui cuvânt - se numesc prize, conectori, cartușe, țevi etc. Din cauza lipsei unui termen consacrat în limba rusă, în acest articol prizele vor fi numite prize și nimic altceva.

Programarea socket-urilor nu este dificilă în sine, ci este descrisă destul de superficial în literatura disponibilă, iar Windows Sockets SDK conține o mulțime de erori atât în ​​documentația tehnică, cât și în demo-urile însoțitoare. În plus, există diferențe semnificative în implementarea socket-urilor în UNIX și în Windows, ceea ce creează probleme evidente.

Autorul a încercat să ofere cea mai completă și coerentă descriere, acoperind nu numai punctele principale, ci și unele subtilități necunoscute programatorilor obișnuiți. Domeniul limitat al articolului din jurnal nu ne permite să vorbim despre totul, prin urmare, a trebuit să ne concentrăm doar pe o singură implementare de socket - biblioteca Winsock 2, un limbaj de programare - C/C++(deși ceea ce s-a spus este în mare parte acceptabil pentru Delphi, Perl etc.) și un tip de prize - blocarea prizelor sincrone.

ALMA MATER

Principalul ajutor în prizele de învățare este Windows Sockets 2 SDK. SDK este documentație, un set de fișiere antet și instrumente pentru dezvoltatori. Documentația nu este foarte bună - dar este totuși scrisă destul de competent și permite, deși nu fără dificultate, stăpânirea prizei chiar și fără ajutorul oricărei alte literaturi. Mai mult, majoritatea cărților disponibile pe piață sunt în mod clar inferioare Microsoft în completitudinea și atenția descrierii. Singurul dezavantaj al SDK-ului este că este în întregime în engleză (pentru unii acest lucru este foarte important).

Dintre instrumentele incluse în SDK, în primul rând aș dori să evidențiez utilitarul sockeye.exe - acesta este un adevărat „banc de testare” pentru dezvoltator. Vă permite să apelați în mod interactiv diverse funcții de priză și să le manipulați la discreția dvs.

Programele demonstrative, din păcate, nu sunt lipsite de erori, uneori destul de crude și sugestive - au fost testate deloc aceste exemple? (De exemplu, în codul sursă al programului simples.c, în apelul la funcțiile send și sendto, sizeof este folosit în loc de strlen) În același timp, toate exemplele conțin multe comentarii detaliate și dezvăluie tehnici destul de interesante de programare netradițională, așa că merită totuși să le cunoaștem.

Dintre resursele WEB dedicate programării socket-urilor și a tot ceea ce este legat de acestea, în primul rând aș dori să remarc următoarele trei: sockaddr.com; www.winsock.com și www.sockets.com.

Prezentare generală a prizei

Biblioteca Winsock acceptă două tipuri de prize - sincron (blocabil) Și asincron (neblocante). Socketurile sincrone dețin controlul în timp ce operația este în desfășurare, în timp ce socketurile asincrone revin imediat controlul, continuă execuția în fundal și notifică codul de apel când se termină.

Windows 3.x acceptă numai socket-uri asincrone, deoarece într-un mediu cu multitasking corporativ, preluarea controlului asupra unei sarcini „atârnă” toate celelalte, inclusiv sistemul însuși. Windows 9x\NT acceptă ambele tipuri de socket, însă, datorită faptului că socket-urile sincrone sunt mai ușor de programat decât cele asincrone, acestea din urmă nu sunt utilizate pe scară largă. Acest articol este dedicat exclusiv prizelor sincrone (asincronul este un subiect pentru o discuție separată).

Socket-urile vă permit să lucrați cu o varietate de protocoale și sunt un mijloc convenabil de comunicare interprocesor, dar în acest articol vom vorbi doar despre socket-uri din familia de protocoale TCP/IP, care sunt folosite pentru a face schimb de date între nodurile de Internet. Toate celelalte protocoale, cum ar fi IPX/SPX, NetBIOS, nu vor fi luate în considerare din cauza domeniului limitat al articolului din jurnal.

Indiferent de tip, prizele sunt împărțite în două tipuri - streaming Și datagrama . Prizele de flux funcționează pe bază de conexiune cu conexiune, oferind o identificare puternică a ambelor părți și garantând integritatea și succesul livrării datelor. Socket-urile Datagram funcționează fără a stabili o conexiune și nu oferă nici identificarea expeditorului, nici controlul succesului livrării datelor, dar sunt considerabil mai rapide decât socket-urile de streaming.

Alegerea unuia sau altui tip de socket este determinată de protocolul de transport pe care funcționează serverul - clientul nu poate stabili o conexiune de streaming cu serverul de datagrame.

cometariu : Socketurile de datagramă se bazează pe protocolul UDP, în timp ce socketurile de streaming se bazează pe TCP.

Primul pas, al doilea, al treilea

Pentru a lucra cu biblioteca Winsock 2.x, trebuie să includeți directiva „ #include ", si in Linie de comanda linkerul specifică „ws2_32.lib”. În mediul de dezvoltare Microsoft Visual Studio, tot ce trebuie să faceți este să faceți clic<Alt-F7>, mergeți la fila „Link” și la lista de biblioteci listate în linia „Object/Library modules”, adăugați „ws2_32.lib”, separând-o de restul cu un caracter spațiu.

Înainte de a putea începe să utilizați funcțiile bibliotecii Winsock, trebuie să o pregătiți pentru lucru apelând funcția " intWSAstartup (WORD wVersionRequested, LPWSADATA lpWSAData)" prin transmiterea octetului înalt al cuvântului wVersionRequested numărul versiunii necesare, iar în minor - numărul subversiune.

Argument lpWSAData trebuie să indice structura WSADATA, care, după inițializarea cu succes, va conține informații despre producătorul bibliotecii. Nu prezintă un interes deosebit și aplicația îl poate ignora. Dacă inițializarea eșuează, funcția returnează o valoare diferită de zero.

Al doilea pas– crearea unui obiect „socket”. Acest lucru este realizat de funcția " PRIZĂpriză (int af, int tip, int protocol)". Primul argument din stânga indică familia de protocoale utilizate. Pentru aplicațiile de Internet, ar trebui să fie setat la AF_INET.

Următorul argument specifică tipul de soclu care trebuie creat - streaming (SOCK_STREAM) sau datagrama (SOCK_DGRAM) (există și socket-uri brute, dar nu sunt acceptate de Windows - vezi secțiunea „Socket-uri brute”).

Ultimul argument specifică ce protocol de transport trebuie utilizat. O valoare de zero corespunde selecției implicite: TCP pentru socket-uri de flux și UDP pentru socket-uri de datagramă. În cele mai multe cazuri, nu are rost să setați manual protocolul și de obicei se bazează pe selecția automată implicită.

Dacă funcția reușește, returnează mânerul socketului, în caz contrar, INVALID_SOCKET.

Următorii pași depind de dacă aplicația este un server sau un client. Mai jos, aceste două cazuri vor fi descrise separat.

Client: pasul trei - Pentru a stabili o conexiune cu o gazdă la distanță, soclul de flux trebuie să apeleze funcția "intconectați (SOCKET s, const struct sockaddr FAR* name, int namelen)". Prizele de datagramă funcționează fără conexiune, deci de obicei nu apelați funcția de conectare.

Notă: în spatele cuvântului „de obicei” se află un truc de programare complicat - apelul de conectare permite unui soclu de datagramă să schimbe date cu nodul nu numai cu funcțiile sendto, recvfrom, ci și cu cele mai convenabile și compacte send și recv. Această subtilitate este descrisă în SDK-ul Winsocket și este utilizată pe scară largă atât de Microsoft însuși, cât și de dezvoltatorii terți. Prin urmare, utilizarea sa este complet sigură.

Primul argument din stânga este descriptorul socket returnat de funcția socket; al doilea este un indicator către structură " sockaddr", conținând adresa și portul nodului la distanță cu care se stabilește conexiunea. Structura sockaddr este folosită de multe funcții, așa că descrierea acesteia este inclusă într-o secțiune separată „Adresa unu, adresa doi”. Ultimul argument îi spune funcția de dimensiunea structurii sockaddr.

După conectează apel sistemul încearcă să stabilească o conexiune cu nodul specificat. Dacă din anumite motive acest lucru nu se poate face (adresa este setată incorect, nodul nu există sau este suspendat, computerul nu este în rețea), funcția va returna o valoare diferită de zero.

Server: pasul trei– Înainte ca serverul să poată utiliza un socket, trebuie să-l lege la o adresă locală. O adresă locală, ca orice altă adresă de Internet, constă dintr-o adresă IP a gazdei și un număr de port. Dacă serverul are mai multe adrese IP, atunci socket-ul poate fi asociat cu toate acestea simultan (pentru a face acest lucru, în loc de adresa IP, ar trebui să specificați constanta INADDR_ANY egală cu zero) sau cu oricare anume.

Legarea se face apelând funcția " intlega (SOCKET s, const struct sockaddr FAR* name, int namelen)". Primul argument din stânga este descriptorul socket returnat de funcția socket, urmat de un pointer către structura sockaddr și lungimea acesteia (vezi secțiunea " Adresa unu, adresa doi").

Strict vorbind, clientul trebuie să asocieze și socket-ul cu o adresă locală înainte de a-l folosi, totuși, funcția de conectare face acest lucru, asociind socket-ul cu unul dintre porturile alese aleatoriu din intervalul 1024-5000. Serverul trebuie să „stea” pe un port predeterminat, de exemplu, 21 pentru FTP, 23 pentru telnet, 25 pentru SMTP, 80 pentru WEB, 110 pentru POP3 etc. Prin urmare, el trebuie să efectueze legarea „manual”.

Funcția returnează zero dacă are succes, diferit de zero în caz contrar.

Server: pasul patru - După ce a finalizat legarea, serverul de streaming intră în modul de așteptare a conexiunilor, apelând funcția " intasculta (SOCKET s, int backlog)", Unde s– descriptor de soclu și restante– dimensiunea maximă permisă a cozii de mesaje.

Dimensiunea cozii limitează numărul de conexiuni procesate simultan, așa că ar trebui să o alegeți cu înțelepciune. Dacă coada este complet plină, următorul client va primi un refuz atunci când încearcă să stabilească o conexiune (pachet TCP cu steag RST setat). În același timp, numărul maxim rezonabil de conexiuni este determinat de performanța serverului, cantitatea de RAM etc.

Serverele de datagrame nu apelează funcția de ascultare deoarece lucrează fără a stabili o conexiune și poate apela imediat recvfrom pentru a citi mesajele primite imediat după legare, ocolind pasii al patrulea și al cincilea.

Server: pasul cinci– cererile de conectare sunt preluate din coadă de către funcția " PRIZĂAccept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen)", care creează automat o nouă priză, efectuează legarea și își întoarce mânerul, și în structură sockaddrînregistrează informații despre clientul conectat (adresa IP și portul). Dacă coada este goală când se apelează accept, funcția nu revine controlul până când se stabilește cel puțin o conexiune cu serverul. Dacă apare o eroare, funcția returnează o valoare negativă.

Pentru a lucra în paralel cu mai mulți clienți, imediat după eliminarea unei cereri din coadă, generați un nou thread (proces), trecându-i descriptorul socket-ului creat de funcția accept, apoi eliminați din nou următoarea cerere din coadă etc. În caz contrar, până la finalizarea unui client, serverul nu va putea servi pe toți ceilalți.

împreună - Odată ce conexiunea este stabilită, prizele de flux pot comunica cu gazda la distanță apelând funcții " inttrimite (SOCKET s, const char FAR * buf, int len, int steaguri)" Și " intrecv (SOCKET s, char FAR* buf, int len, int steaguri)" pentru a trimite și respectiv a primi date.

Funcţie trimite returnează controlul imediat după executare, indiferent dacă partea care primește a primit sau nu datele noastre. La finalizarea cu succes, funcția returnează cantitatea transmise (netransmis!) date - adică finalizarea cu succes nu indică livrarea reușită! În general, TCP (pe care se bazează socketurile de flux) garantează livrarea cu succes a datelor către destinatar, dar numai dacă conexiunea nu este întreruptă prematur. Dacă conexiunea este întreruptă înainte de încheierea transferului, datele vor rămâne netransmise, dar codul de apel nu va primi nicio notificare în acest sens! Și eroarea este returnată numai dacă conexiunea este întreruptă inainte de apelând funcția trimite!

Functia recv returnează controlul numai după ce a primit cel puțin un octet. Mai exact, ea se așteaptă la sosirea unui întreg datagrame. O datagrama este o colecție de unul sau mai multe pachete IP trimise printr-un apel de trimitere. Mai simplu spus, fiecare apel recv la un moment dat primește tot atâtea octeți cât au fost trimiși de funcția de trimitere. Aceasta presupune că funcția recv este prevăzută cu un buffer suficient de mare, altfel va trebui apelată de mai multe ori. Cu toate acestea, pentru toate apelurile ulterioare, datele vor fi preluate din tamponul local și nu vor fi primite din rețea, deoarece Un furnizor TCP nu poate primi o „parte” dintr-o datagramă, ci doar întreaga ei.

Ambele funcții pot fi controlate folosind steaguri, a trecut într-o variabilă de tip int ca al treilea argument din stânga. Această variabilă poate lua una dintre cele două valori: MSG _ARUNCA O PRIVIREȘi MSG _OOB.

Indicatorul MSG_PEEK face ca funcția recv să se uite la date în loc să le citească. Vizualizarea, spre deosebire de citire, nu distruge datele vizualizate. Unele surse afirmă că atunci când steag-ul MSG_PEEK este setat, funcția recv nu întârzie controlul dacă nu există date în tamponul local disponibile pentru primire imediată. Nu este adevarat! În mod similar, uneori întâlniți o declarație complet falsă conform căreia funcția de trimitere cu setul de steag MSG_PEEK returnează numărul de octeți deja transmiși (apelul de trimitere nu blochează controlul). De fapt, funcția de trimitere ignoră acest steag!

Steagul MSG_OOB este pentru transmitere și recepție urgent (Out Of Band) date. Datele urgente nu au un avantaj față de altele atunci când sunt trimise prin rețea, ci vă permit doar să îndepărtați clientul de procesarea normală a fluxului de date obișnuite și să îi oferiți informații „urgente”. Dacă datele au fost transmise de funcția de trimitere cu steag-ul MSG_OOB setat, pentru a le citi, trebuie setat și steag-ul MSG_OOB al funcției recv.

cometariu: Este foarte recomandat să vă abțineți de la utilizarea datelor sensibile la timp în aplicațiile dvs. În primul rând, sunt complet opționale - este mult mai simplu, mai fiabil și mai elegant să creați o conexiune TCP separată. În al doilea rând, nu există un consens cu privire la implementarea lor, iar interpretările diferiților producători sunt foarte diferite unul de celălalt. Astfel, dezvoltatorii nu au ajuns încă la un acord final asupra locului în care ar trebui să indice indicatorul de urgență: fie către ultimul octet de date urgente, fie către octetul care urmează ultimul octet de date urgente. Ca urmare, expeditorul nu poate fi niciodată sigur că destinatarul va putea interpreta corect cererea sa.

Există, de asemenea, steag-ul MSG_DONTROUTE, care indică transferul datelor fără rutare, dar nu este acceptat de Winsock și, prin urmare, nu este discutat aici.

Un soclu de datagramă poate folosi, de asemenea, funcțiile de trimitere și recepție dacă apelează mai întâi la conectare (vezi „ Client: Etapa al treilea"), dar are și funcții proprii, „personale”: „ intTrimite catre (SOCKET s, const char FAR * buf, int len,int flags, const struct sockaddr FAR * to, int tolen)" Și " intrecvfrom (SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen)".

Ele sunt foarte asemănătoare cu send și recv, singura diferență este că sendto și recvfrom necesită o indicație explicită a adresei nodului pentru a primi sau transmite date. Apelul recvfrom nu necesită setarea preliminară a adresei nodului de trimitere - funcția acceptă toate pachetele care sosesc la portul UDP specificat de la toate adresele și porturile IP. Dimpotrivă, expeditorul ar trebui să răspundă la același port din care a venit mesajul. Deoarece funcția recvfrom scrie adresa IP și numărul portului clientului după ce a primit un mesaj de la acesta, programatorul de fapt nu trebuie să facă altceva decât să treacă trimitere către același pointer către structura sockaddr care a fost transmisă anterior funcției recvfrem care a primit mesajul de la client.

Inca un detaliu– protocolul de transport UDP, pe care se bazează socket-urile de datagramă, nu garantează livrarea cu succes a mesajelor, iar această sarcină cade pe umerii dezvoltatorului însuși. Acest lucru poate fi rezolvat, de exemplu, prin trimiterea de către client a unei confirmări că datele au fost primite cu succes. Adevărat, clientul nu poate fi sigur că confirmarea va ajunge la server și nu se va pierde undeva pe parcurs. Confirmarea primirii confirmării este inutilă, deoarece este recursiv indecidabilă. Este mai bine să nu folosiți deloc prize de datagramă pe canale nesigure.

În toate celelalte privințe, ambele perechi de funcții sunt complet identice și funcționează cu aceleași steaguri - MSG_PEEK și MSG_OOB.

Toate cele patru funcții returnează SOCKET_ERROR (== -1) când apare o eroare.

Notă:În UNIX, socket-urile pot fi tratate exact ca fișierele obișnuite, în special, pot fi scrise și citite cu funcțiile de scriere și citire. Windows 3.1 nu a acceptat această caracteristică, așa că la portarea aplicațiilor UNIX pe Windows, toate apelurile de scriere și citire au trebuit să fie înlocuite cu send și, respectiv, recv. În Windows 95 cu Windows 2.x instalat, această omisiune a fost corectată - acum descriptorii de socket pot fi transferați la funcțiile ReadFil, WriteFile, DuplicateHandle etc.

Pasul șase, ultimul– funcția „ are scopul de a închide conexiunea și de a distruge priza intclosesocket (PRIZĂ s)", care returnează o valoare nulă dacă operația se finalizează cu succes.

Înainte de a ieși din program, trebuie să apelați funcția " intWSACleanup (nud)" pentru a deinițializa biblioteca WINSOCK și a elibera resursele utilizate de această aplicație. Atenţie : Încheierea unui proces folosind ExitProcess nu eliberează automat resurse de socket!

Notă: tehnicile mai avansate pentru închiderea unei conexiuni sunt Protocolul TCP vă permite să închideți selectiv conexiunea fiecărei părți, lăsând cealaltă parte activă. De exemplu, un client poate spune serverului că nu îi va mai trimite date și închide conexiunea client (server), cu toate acestea, este gata să continue să primească date de la acesta atâta timp cât serverul le trimite, adică dorește să plece. conexiunea "client (server") deschisă.

Pentru a face acest lucru, trebuie să apelați funcția „int închide(SOCKET s ,int how)", trecând în argumentul how una dintre următoarele valori: SD_RECEIVE pentru a închide canalul serverului (client), SD_SEND pentru a închide canalul client (server) și în final SD_BOTH pentru a închide ambele canale.

Ultima opțiune se compară favorabil cu closesocket prin închiderea „soft” a conexiunii - va fi trimisă o notificare către nodul de la distanță că dorește să închidă conexiunea, dar această dorință nu va fi implementată până când acel nod nu va întoarce confirmarea. În acest fel, nu trebuie să vă faceți griji că conexiunea va fi închisă în cel mai inoportun moment.

Atenţie: apelarea opririi nu scutește nevoia de a închide priza cu funcția closesocket!

Sun tree

Pentru a demonstra mai clar relația dintre funcțiile de socket între ele, mai jos este un arbore de apeluri care arată ordinea în care apelurile de funcție ar trebui să urmeze în funcție de tipul de socket (streaming sau datagramă) și de tipul de procesare a cererii (client sau server).

client server

conectați |-sendto TCP UDP

| |-recvdin | |

|-trimite asculta |

|-trimite |-trimite la

|-recv |-recvform

Lista 29 Secvență de apeluri la funcții de socket pentru diferite operații

Adresa unu, adresa doi

Adresele sunt acolo unde este cea mai mare confuzie și nu ar strica să aducem puțină claritate. În primul rând, structura sockaddr este definită astfel:

u_short sa_family; // familie de protocol

// (de obicei AF_INET)

char sa_data; // Adresa IP și portul gazdei

Lista 30 Definiție de structură sockaddr depreciată

Acum depreciat, Winsock 2.x l-a înlocuit cu structura sockaddr_in, definită după cum urmează:

struct sockaddr_in

scurt sin_familie; // familie de protocol

// (de obicei AF_INET)

u_short sin_port; // port

struct in_addr sin_addr; // Adresa IP

char sin_zero; // coada

Lista 31 Definiția modernă a structurii sockaddr_in

În general, nimic nu s-a schimbat (și a meritat să îngrădiți grădina?), înlocuirea unui număr întreg scurt nesemnat cu un întreg scurt semnat pentru a reprezenta o familie de protocoale nu dă nimic. Dar acum adresa nodului este prezentată sub forma a trei câmpuri - sin_port (numerele portului), sin_addr (adresele IP ale nodurilor) și o „coadă” de opt zero octeți, care rămâne din matricea de paisprezece caractere sa_data . Pentru ce este? Faptul este că structura sockaddr nu este legată în mod special de internet și poate funcționa cu alte rețele. Adresele unor rețele necesită mult mai mult de patru octeți pentru reprezentarea lor, așa că trebuie să le luați cu rezervă!

Structura in_addr este definită după cum urmează:

struct in_addr(

struct ( u_char s_b1,s_b2,s_b3,s_b4; ) S_un_b;

// Adresa IP

struct ( u_short s_w1,s_w2; ) S_un_w;

// Adresa IP

u_long S_addr; // Adresa IP

Lista 32 Definirea structurii in_addr

După cum puteți vedea, constă dintr-o adresă IP scrisă în trei „figuri” - o secvență de patru octeți (S_un_b), o pereche de cuvinte de doi octeți (S_un_W) și un număr întreg lung (S_addr) - alegeți să gustați Dar este nu atat de simplu! Multe programe, manuale tehnice și chiar demonstrații care vin cu Winsock SDK se referă la un membru „misterios” al structurii s_addr care nu este descris în mod explicit în SDK! De exemplu, iată o linie din fișierul „Simples.h”: „local.sin_addr. s_adr= (!interfață)?INADDR_ANY:inet_addr(interfață);"

Ce este?! Privind în fișierul „winsock2.h” puteți găsi următoarele: „#define s_addr S_un.S_addr”. Da, dar acesta este echivalentul lui s_addr, adică. Adresa IP scrisă ca un întreg lung!

În practică, puteți folosi atât sockaddr „învechit” cât și sockaddr_in „noufangled” cu același succes. Cu toate acestea, deoarece prototipurile celorlalte funcții nu s-au schimbat, atunci când utilizați sockaddr_in, va trebui să efectuați constant conversii explicite, de exemplu, astfel: " sockaddr_in dest_addr; conecta (mysocket, (struct sockaddr* ) &dest_addr, sizeof(dest_addr)".

Pentru a converti o adresă IP scrisă ca o secvență de caractere precum „127.0.0.1” într-o secvență numerică de patru octeți, utilizați „ nesemnat lunginet_addr (const char FAR * cp)". Este nevoie de un pointer către un șir de caractere și, dacă are succes, îl convertește într-o adresă IP de patru octeți, sau -1 dacă acest lucru nu este posibil. Rezultatul returnat de funcție poate fi atribuit unui element al structurii sockaddr_in după cum urmează: " struct sockaddr_in dest_addr; dest_addr.sin_addr.S_addr=inet_addr("195.161.42.222");". Folosind structura sockaddr ar arăta astfel: " struc sockaddr dest_addr; ((unsigned int *)(&dest_addr.sa_data+2)) = inet_addr("195.161.42.222");"

O încercare de a transmite inet_addr un nume de domeniu gazdă eșuează. Puteți afla adresa IP a unui astfel de domeniu folosind funcția " struct hostent FAR *gethostbyname (const char FAR * nume);„. Funcția accesează DNS și returnează răspunsul său în structura hostent sau null dacă serverul DNS nu a putut determina adresa IP a acestui domeniu.

Structura gazdei arată astfel:

char FAR * h_name; // numele oficial al nodului

char FAR * FAR * h_aliases; // nume alternative

// nod (matrice de șiruri de caractere)

scurt h_addrtype; // Tip de Adresă

lungime h_scurtă; // lungimea adresei

// (de obicei AF_INET)

char FAR * FAR * h_adr_list; // lista de indicatori

//la adrese IP

// zero este sfârșitul listei

Lista 33 Definirea structurii gazdei

Ca și în cazul in_addr, multe programe și exemple incluse în SDK-ul Winsock folosesc pe scară largă câmpul nedocumentat al structurii h_addr. De exemplu, aici este o linie din fișier "simplec.c" "memcpy(&(server.sin_addr),hp->h_addr ,hp->h_lungime);" Căutând în „winsock2.h”, puteți afla ce înseamnă: „ #define h_addr h_adr_list".

Acum asta e interesant! Cert este că cu unii nume de domenii Mai multe adrese IP sunt asociate simultan. Dacă un nod eșuează, clientul poate încerca să se conecteze la altul sau pur și simplu să selecteze un nod cu cel mai mare curs de schimb. Dar în exemplul de mai sus, clientul folosește doar prima adresă IP din listă și le ignoră pe toate celelalte! Desigur, acest lucru nu este fatal, dar tot va fi mai bine dacă în programele dumneavoastră țineți cont de posibilitatea de a vă conecta la alte adrese IP dacă este imposibil să stabiliți o conexiune cu prima.

Funcția gethostbyname așteaptă intrare numai nume de domenii, dar nu adrese IP digitale. Între timp, regulile de „bună formă” impun ca clientului să i se ofere posibilitatea de a specifica atât nume de domenii, cât și adrese IP digitale.

Soluția este următoarea - este necesar să se analizeze șirul trimis de client - dacă este o adresă IP, apoi se trece la funcția inet_addr, în caz contrar - gethostbyaddr, presupunând că este un nume de domeniu. Pentru a distinge adresele IP de numele de domenii, mulți programatori folosesc un truc simplu: dacă primul caracter al liniei este un număr, este o adresă IP, în caz contrar este un nume de domeniu. Cu toate acestea, acest truc nu este complet sincer - numele de domenii pot începe cu un număr, de exemplu, „666.ru”, ele se pot termina și cu un număr, de exemplu, membrii subdomeniului „666” se pot adresa nodului „666”. .ru” ca „666” „. Lucrul amuzant este că (teoretic) ar putea exista nume de domenii care nu se pot distinge sintactic de adresele IP! Prin urmare, în opinia autorului acestui articol, cel mai bine este să procedați astfel: trecem șirul introdus de utilizator la funcția inet_addr, dacă returnează o eroare, atunci numim gethostbyaddr.

Pentru a rezolva problema inversă - determinarea unui nume de domeniu prin adresa IP, funcția " struct HOSTENT FAR *gethostbyaddr (const char FAR * addr, int len, int tip)", care este exact același cu gethostbyname, cu excepția faptului că argumentul său nu este un pointer către un șir care conține numele, ci un pointer către o adresă IP de patru octeți. Încă două argumente specifică lungimea și tipul acestuia (4 și, respectiv, AF_INET ).

Determinarea numelui gazdei după adresa sa poate fi utilă pentru serverele care doresc să-și cunoască clienții direct.

Pentru a converti o adresă IP scrisă în format de rețea într-un șir de caractere, funcția " char FAR *inet _ ntoa (struct in_addr)", care ia ca intrare o structură in_addr și returnează un pointer către un șir dacă conversia are succes și zero în caz contrar.

Ordinea octeților de rețea

Winsock pentru toată lumea (partea 1)

Deci, ce este Winsock și cu ce se mănâncă? Pe scurt, Winsock este o interfață care simplifică dezvoltarea aplicațiilor de rețea sub Windows. Tot ce trebuie să știm este că Winsock este o interfață între o aplicație și protocolul de transport care realizează transferul de date.

Să nu intrăm în detalii arhitectura interioara, pentru că nu ne interesează modul în care este structurat intern, ci modul de utilizare a funcțiilor pe care Winsock le oferă utilizatorului pentru lucru. Sarcina noastră este să înțelegem mecanismul de acțiune al WinsockAPI folosind exemple specifice. "La ce poate fi folosit asta? Cu siguranță există biblioteci care simplifică lucrul cu rețele și au o interfață simplă?" - tu intrebi. Sunt parțial de acord cu această afirmație, dar în opinia mea, biblioteci complet universale orientate către toate sarcinile nu pot exista. Și în plus, este mult mai plăcut să-ți dai seama de totul, fără a te simți stânjenit în fața unei „cutii negre” al cărei principiu de funcționare nu îl înțelegi, ci îl folosești doar ca unealtă :) Tot materialul este conceput pentru începători. Cred că nu vor fi probleme în a-l stăpâni. Dacă mai aveți întrebări, scrieți la [email protected]. Voi raspunde tuturor. Pentru a ilustra exemplele, vom folosi fragmente de cod Microsoft VC++. Asadar, haideti sa începem!

Winsock - de unde să încep?

Deci, prima întrebare este - dacă există Winsock, atunci cum să-l folosești? În realitate, totul nu este atât de complicat. Etapa întâi - conectarea bibliotecilor și antetelor.

#include „winsock.h” sau #include „winsock2.h” - în funcție de versiunea de Winsock pe care o vei folosi
De asemenea, toate fișierele lib corespunzătoare (Ws2_32.lib sau Wsock32.lib) trebuie să fie incluse în proiect

Pasul 2 - inițializare.

Acum putem folosi în siguranță funcțiile WinsockAPI. ( lista plina funcțiile pot fi găsite în secțiunile relevante din MSDN).

Pentru a inițializa Winsock, apelați funcția WSAStartup

int WSAStartup(WORD wVersionRequested, (în) LPWSADATA lpWSAData (out));


Parametru WORD wVersionRequested - octet mic - versiune, octet mare - subversiune, interfață Winsock. Versiunile posibile sunt 1.0, 1.1, 2.0, 2.2... Pentru a „asambla” acest parametru folosim macro-ul MAKEWORD. De exemplu: MAKEWORD (1, 1) - versiunea 1.1. Versiunile ulterioare se disting prin prezența de noi funcții și mecanisme de extindere. Parametrul lpWSAData este un pointer către structura WSADATA. La întoarcerea de la o funcție, această structură conține informații despre versiunea WinsockAPI pe care am inițializat-o. În principiu, îl poți ignora, dar dacă cineva este interesat de ceea ce este înăuntru, nu fi leneș, deschide documentația;)

Iată cum arată în practică:

WSADATA ws;
//...
dacă (FAILED (WSASstartup (MAKEWORD(1, 1), &ws)))
{
// Eroare...
eroare = WSAGetLastError();
//...
}


În acest caz, puteți obține informații extinse despre eroare apelând WSAGetLastError(). Această funcție returnează un cod de eroare (tastați int)

Pasul 3 - crearea unei prize.

Deci, putem trece la următoarea etapă - crearea principalului mijloc de comunicare în Winsock - priza. Din perspectiva WinsockAPI, un socket este un mâner care poate primi sau trimite date. În practică, totul arată așa: creăm un socket cu anumite proprietăți și îl folosim pentru a ne conecta, primi/transmite date etc. Acum să facem o mică digresiune... Deci, atunci când creăm un socket, trebuie să specificăm parametrii acestuia: socket-ul folosește protocolul TCP/IP sau IPX (dacă TCP/IP, atunci ce tip etc.). Deoarece următoarele secțiuni ale acestui articol se vor concentra pe protocolul TCP/IP, ne vom concentra pe caracteristicile socket-urilor care utilizează acest protocol. Putem crea două tipuri principale de socket-uri care funcționează folosind protocolul TCP/IP - SOCK_STREAM și SOCK_DGRAM (vom lăsa socket-ul RAW în pace deocamdată :)). Diferența este că pentru primul tip de socluri (se mai numesc și TCP sau soclu bazat pe conexiune), pentru a trimite date, socket-ul trebuie să mențină constant o conexiune cu destinatarul, în timp ce livrarea pachetului către destinatar este garantată. . În cel de-al doilea caz, nu este nevoie de o conexiune permanentă, dar informații despre dacă pachetul a sosit sau nu sunt imposibil de obținut (așa-numitele prize UDP sau fără conexiune). Atât primul cât și al doilea tip de prize au aplicațiile lor practice. Să începem cunoștințele noastre cu socket-uri cu socket-uri TCP (bazate pe conexiune).

În primul rând, să declarăm:

Puteți crea o priză folosind funcția socket

socket SOCKET (int af (in), // protocol (TCP/IP, IPX...)
tip int (în), // tip socket (SOCK_STREAM/SOCK_DGRAM)
int protocol (în) // pentru aplicații Windows poate 0
);


Exemplu:

dacă (INVALID_SOCKET == (s = soclu (AF_INET, SOCK_STREAM, 0)))
{
// Eroare...
eroare = WSAGetLastError();
// ...
}


În caz de eroare, funcția returnează INVALID_SOCKET. În acest caz, puteți obține informații extinse despre eroare apelând WSAGetLastError().

Pasul 4 - stabiliți o conexiune.

În exemplul anterior, am creat un socket. Ce ar trebui să facem cu el acum? :) Acum putem folosi acest socket pentru a face schimb de date cu alți clienți Winsock și nu numai. Pentru a stabili o conexiune cu o altă mașină, trebuie să cunoașteți adresa IP și portul acesteia. Mașina de la distanță trebuie să „asculte” acest port pentru conexiunile de intrare (adică, acționează ca un server). În acest caz, aplicația noastră este un client.

Pentru a stabili o conexiune, utilizați funcția de conectare.

int connect(SOCKET s, // socket (socket-ul nostru)
const struct sockaddr FAR *nume, // adresa
int namelen // lungimea adresei
);


Exemplu:

//Declară o variabilă pentru a stoca adresa
sockaddr_in s_addr;

// Completați-l:
ZeorMemory(&s_addr, sizeof(s_addr));
// tipul adresei (TCP/IP)
s_addr.sin_family = AF_INET;
//adresa serverului. Deoarece TCP/IP reprezintă adrese în formă numerică, apoi pentru traducere
// adresele folosesc funcția inet_addr.
s_addr.sin_addr.S_un.S_addr = inet_addr("193.108.128.226");
// Port. Folosim funcția htons pentru a converti numărul portului din cel obișnuit în reprezentarea //TCP/IP.
s_addr.sin_port = htons(1234);


Dacă există o eroare, funcția returnează SOCKET_ERROR.
Acum socket s este asociat cu mașina de la distanță și poate trimite/primi date numai de la aceasta.

Pasul 5 - trimiteți datele.

Pentru a trimite date folosim funcția de trimitere

int send(SOCKET s, // trimitere socket
const char FAR *buf, // pointer către un buffer cu date
int len, // lungimea datelor
);


Exemplu de utilizare a acestei funcții:

if (SOCKET_ERROR == (trimite (s, (car*) & buff), 512, 0))
{
// Eroare...
eroare = WSAGetLastError();
// ...
}


Dacă există o eroare, funcția returnează SOCKET_ERROR.
Lungimea pachetului de date este limitată de protocolul însuși. Vom vedea cum să aflăm lungimea maximă a unui pachet de date data viitoare. Funcția nu revine până când datele nu au fost trimise.

Pasul 6 - acceptați datele.

Funcția recv ne permite să primim date de la aparatul cu care am stabilit anterior o conexiune.

int recv(SOCKET s, // socket destinatar
char FAR *buf, // adresa tampon pentru primirea datelor
int len, // lungimea buffer-ului pentru primirea datelor
int steaguri // steaguri (poate fi 0)
);


Dacă nu cunoașteți în avans dimensiunea datelor primite, atunci lungimea buffer-ului de recepție ar trebui să fie nu mai mică de dimensiune maximă pachet, în caz contrar, mesajul s-ar putea să nu încapă în el și va fi tăiat. În acest caz, funcția returnează o eroare.
Exemplu:

int actual_len = 0;

Dacă (SOCKET_ERROR == (actual_len = recv (s, (char*) & buff), max_packet_size, 0))
{
// Eroare...
eroare = WSAGetLastError();
// ...
}


Dacă sunt primite date, atunci funcția returnează dimensiunea pachetului de date primit (în exemplu - actual_len Dacă există o eroare, funcția returnează SOCKET_ERROR). Rețineți că funcțiile de trimitere/recv vor aștepta până când apare un timeout sau un pachet de date este trimis/primit. În consecință, acest lucru provoacă o întârziere în funcționarea programului. Citiți cum să evitați acest lucru în numerele următoare.

Pasul 6 - închideți conexiunea.

Procedura de închidere a unei conexiuni active se face folosind funcțiile de închidere și closesocket. Există două tipuri de închideri de conexiune: abortive și grațioase. Primul tip este închiderea de urgență a unei prize (closesocket). În acest caz, conexiunea este întreruptă imediat. Apelarea closesocket are un efect imediat. După apelarea closesocket, priza nu mai este accesibilă. Cum să închideți un socket folosind shutdown/closesocket citiți în numerele viitoare, deoarece acest subiect necesită cunoștințe mai complete despre Winsock.

int shutdown(SOCKET s, // Soclul care urmează să fie închis
int cum // Metoda de închidere
);


int closesocket(SOCKET s // Priză închisă
);


Exemplu:

inchidere priza(e);

După cum puteți vedea, mecanismul de schimb de date Winsock pe care l-am considerat este foarte simplu. Programatorului i se cere doar să dezvolte propriul „protocol” pentru comunicarea între mașinile de la distanță și să-l implementeze folosind aceste funcții. Desigur, exemplele pe care le-am analizat nu reflectă toate capacitățile Winsock. În articolele noastre vom încerca să luăm în considerare cele mai importante, după părerea noastră, caracteristici ale lucrului cu Winsock. Rămâneţi aproape. :)
Citiți în numărul următor:

  • Scriem o aplicație simplă winsock.
  • Prize UDP - receptie/livrare pachete negarantate
  • Rezolvăm problema „blocării” prizelor.

Ajunge teorie, dă-mi WinSock!

Deci, acum există două versiuni de WinSock: 1.1 și 2. Vă sugerez să folosiți a doua versiune. Ce vom face mai departe? Vom scrie o versiune client-server a jocului „Rock-Paper-Scissors”. Serverul va fi o aplicație de consolă multi-threaded, clientul va fi scris în DirectX.

Pentru început, vă voi arăta cum este organizat WinSock, vă voi explica diferitele moduri de programare a socket-urilor și vă voi descrie funcțiile care sunt folosite pentru a face acest lucru. După ce vom avea de-a face cu WinSock, îl vom folosi pentru a scrie jucăria pe care am menționat-o mai sus.

Pentru a utiliza WinSock, trebuie să includeți fișierul antet corespunzător și să adăugați ws2_32.lib la proiectul tău. Acum totul este gata de programat sub WinSock. Dar cum funcționează și de unde să începem?

Există mai multe moduri de a programa socket-uri. poți să folosești funcții de bază UNIX/Berkley sau funcții specializate pentru Microsoft Windows sau utilizați versiunea MFC orientată pe obiecte a socket-urilor. La început, am vrut să folosesc versiunea OO a socket-urilor, pentru că... Cred că orele fac API-ul mai digerabil. Dar acestea nu sunt doar cursuri, acestea sunt MFC. „Fecut cât mai dificil posibil” este sloganul lor. Nu, MFC este grozav, dar faptul că trebuie să creezi o aplicație Win32 și să gestionezi mesajele socket Windows trimise programului tău este frustrant, mai ales în cazul unui server. De ce trebuie să facem serverul ca o aplicație Win32? Este inutil. Pentru a ușura lucrurile, vom folosi cele mai elementare funcții ale serverului UNIX/Berkley.

Tipuri de date.

Sockaddr_in(sockaddr)

Descriere: tip sockaddr_in folosit pentru a descrie o conexiune prin prize. Conține, de asemenea, adresa IP și numărul portului. Aceasta este o versiune orientată TCP sockaddr. Noi vom folosi sockaddr_in pentru a crea prize.

struct sockaddr_in (scurt sin_family; // tip de protocol (trebuie să fie AF_INET) u_short sin_port; // Numărul portului socketului struct in_addr sin_addr; // adresa IP char sin_zero[ 8 ] ; // nefolosit };

Descriere: WSAData folosit atunci când încărcați și inițializați biblioteca ws2_32.dll. Acest tip este folosit ca valoare de returnare a funcției WSAstartup(). Utilizați-l pentru a determina versiunea WinSock pe computer.

Descriere: Acest tip de date este folosit pentru a stoca un descriptor de socket. Acești descriptori sunt utilizați pentru a identifica socket-ul. În adevăr, SOCKET este doar un int nesemnat.

Ei bine, să începem programarea

Mai întâi trebuie să descărcați ws2_32.dll:

// Acest cod trebuie să fie prezent în orice program care utilizează WinSock WSADATA w; // folosit pentru a stoca informații despre versiunea socketului int eroare = WSAStartup (0x0202 , &w) ; // completați w dacă (eroare) ( // o eroareîntoarcere ; ) dacă (w.wVersion != 0x0202 ) ( // versiune greșită a socket-urilor! WSACleanup(); // descărcați ws2_32.dllîntoarcere ; )

Probabil aveți o întrebare - ce înseamnă 0x0202? Aceasta înseamnă versiunea 2.2. Dacă este necesară versiunea 1.1, atunci acest număr trebuie schimbat în 0x0101. WSAStartup() populează variabila WSADATA și încarcă biblioteca de socket dinamic. WSACleanup(), în consecință, îl descarcă.

Creați o priză

SOCKET s = socket(AF_INET, SOCK_STREAM, 0); // Creați un socket

În general, acesta este tot ceea ce este necesar pentru a crea un socket, dar tot trebuie să legați ( lega) cu un port când doriți să începeți direct să lucrați cu el. Constanta AF_INET este definită undeva în winsock2.h. Dacă o funcție vă cere să faceți ceva de genul unei familii de adrese ( adresa familiei) sau int af, trebuie doar să specificați AF_INET. Constanta SOCK_STREAM este necesară pentru a crea un socket de streaming (TCP/IP). De asemenea, puteți crea un socket UPD, dar, așa cum sa menționat mai sus, nu este la fel de fiabil ca TCP. Valoarea ultimului parametru va fi zero. Aceasta înseamnă doar că protocolul corect va fi selectat automat pentru dvs. (ar trebui să fie TCP/IP).

Numim portul dorit la o priză (legăm un port și o priză):

// Tine minte! Ar trebui să legați doar socket-urile serverului, nu clientul // Funcția WSAStartup apelată sockaddr_in addr; // variabilă pentru socket TCP addr.sin_family = AF_INET; // Adresa familiei - Internet addr.sin_port = htons(5001); // Atribuiți portul 5001 la socket addr.sin_addr.s_addr = htonl (INADDR_ANY) ; // Fără o anumită adresă if (bind(s, (LPSOCKADDR) &addr, sizeof (adr) ) == SOCKET_ERROR) ( // Eroare WSACleanup () ; // descarcă WinSock return ; )

Acest cod poate părea confuz, dar nu este. Adr descrie o priză, specificând portul. Dar poate apărea întrebarea: „Dar adresa IP?” L-am setat ca INADDR_ANY. Acest lucru ne va permite să nu ne facem griji cu privire la o anumită adresă. Trebuie doar să indicăm numărul portului pe care dorim să-l folosim pentru conexiune. De ce folosim htons() și htonl()? Aceste funcții convertesc variabilele de tip scurt și, respectiv, lung, într-un format ușor de înțeles de rețea. De exemplu, dacă numărul portului este 7134 (un număr scurt), atunci trebuie să apelați funcția htons(7134). Pentru adresa IP trebuie să folosim htonl(). Dar dacă vrem să setăm de fapt adresa IP? Trebuie să folosim funcția inet_addr(). De exemplu, inet_addr("129.42.12.241"). Această funcție convertește un șir de adresă, elimină punctele din acesta și îl convertește într-un tip lung.

Ascultarea portului asociat ( port de ascultare)

// WSAStartup() numit // SOCKET-urile indică deja către socket-ul creat if (ascultă(e,5) ==SOCKET_ERROR) ( // eroare! Ascultarea nu este posibilă WSACleanup(); întoarcere ; ) //asculta:

Aici am acceptat o conexiune de la un client care dorește să se alăture jocului. Mai este ceva interesant în linie asculta(SOCKET s, int backlog). Ce s-a întâmplat restante? Restante este numărul de clienți care se pot conecta în timp ce folosim priza, adică acești clienți vor trebui să aștepte în timp ce serverul negociază conexiuni cu alți clienți. De exemplu, dacă specificați 5 ca restanță și 7 persoane încearcă să se alăture, atunci ultimele 2 vor primi mesaje de eroare și vor fi forțate să stabilească o conexiune mai târziu. De obicei, acest parametru variază de la 2 la 10, în funcție de capacitatea maximă a serverului.

Încercarea de conectare la o priză ( încercați și conectați priza)

// WSAStartup() numit // SOCKET-urile indică deja către socket-ul creat // s este asociat cu portul folosind sockaddr_in. sockaddr_in target; target.sin_family = AF_INET; // adresa familiei - Internet target.sin_port = htons(5001); // server port target.sin_addr.s_addr = inet_addr ("52 .123 .72 .251 ") ; // Adresa IP a serverului if (conectează(e, țintă, dimensiunea (țintă) ) == SOCKET_ERROR) ( // Eroare de conexiune WSACleanup(); întoarcere ; )

Practic, asta este tot ceea ce înseamnă o solicitare de conectare. Variabil ţintă indică priza la care încercăm să ne conectăm (server). Funcția connect() solicită un socket (partea client), o descriere a socket-ului connect (partea server) și dimensiunea acelei variabile de descriere. Această funcție pur și simplu trimite o cerere de conectare și așteaptă un răspuns de la server, raportând în același timp orice erori care apar.

Se primește conexiune ( acceptând o conexiune)

// WSAStartup() numit // SOCKET-urile indică deja către socket-ul creat // s este asociat cu portul folosind sockaddr_in. // socket s ascultă#define MAX_CLIENTS 5 ; // doar pentru claritate int număr_de_clienți = 0 ; Client SOCKET[ MAX_CLIENTS] ; // prize client sockaddr client_sock[ MAX_CLIENTS] ; // descrierea socket-urilor clientuluiîn timp ce (număr_de_clienți< MAX_CLIENTS) // sunt MAX_CLIENTS clienți conectați?( client[ numărul_de_clienți] = // acceptă cererea de conectare accept (s, client_sock[ number_of_clients] , &addr_size) ; if (client[ number_of_clients] == INVALID_SOCKET) ( // Eroare de conexiune WSACleanup(); întoarcere ; ) altfel ( // clientul s-a alăturat cu succes // începe un fir pentru a comunica cu clientul startThread (client[ numărul_de_clienți] ); număr_de_clienți++; ) )

În general, totul este clar aici. Numărul de clienți nu trebuie să fie specificat de expresia MAX_CLIENTS. Este folosit aici doar pentru a îmbunătăți claritatea și ușurința de înțelegere a acestui cod. număr_de_clienți- o variabilă care conține numărul de clienți conectați. client - o matrice de tip SOCKET, care este folosită pentru a stoca descriptori de socket ai clienților conectați. client_sock - tip matrice sockaddr, care conține informații despre tipul conexiunii, numărul portului etc. De obicei, nu avem nevoie de el, deși unele funcții necesită o descriere a conexiunii ca parametru. Bucla principală așteaptă doar o solicitare de conexiune, apoi o acceptă și pornește un fir pentru a comunica cu clientul.

Se trimit date ( scris sau trimitere)

// SOCKET s este inițializat tampon de caractere[11]; // buffer de 11 caractere sprintf(buffer, „Orice:”); trimite (s, buffer, sizeof (buffer), 0);

Al doilea parametru al funcției send() este o variabilă de tip char FAR *buf, care este un pointer către buffer-ul cu datele pe care dorim să le trimitem. Al treilea parametru este dimensiunea buffer-ului trimis. Ultimul parametru este pentru setarea diferitelor steaguri. Nu îl vom folosi și îl vom lăsa ca zero.

Se primesc date ( citind sau primind)

// SOCKET s este inițializat tampon de caractere[80]; // buffer de 80 de caractere recv (s, buffer, sizeof (tampon), 0);

recv(), este în mare măsură similar cu trimite(), cu excepția faptului că nu transmitem date, ci le primim.

Convertește o adresă șir (adresă IP sau nume de gazdă) într-o adresă numerică utilizată la conectare ( rezolvarea adresei IP).

Biblioteca de rețea Winsock

Lucrul cu rețeaua prin componentele delphi este foarte convenabil și destul de simplu, dar prea lent. Acest lucru poate fi rezolvat prin accesarea directă a bibliotecii ferestre de rețea - winsock. Astăzi ne vom familiariza cu elementele de bază.

Ce este winsock

Biblioteca winsock constă dintr-un singur fișier, winsock.dll. Este foarte potrivit pentru crearea de aplicații simple deoarece oferă toate funcțiile necesare pentru a crea o conexiune și a primi/transfer fișiere. Dar nici măcar nu încercați să creați un sniffer. Nu există nimic în winsock pentru a accesa antetele pachetelor. ms a promis că va construi aceste lucruri necesare unei persoane avansate în winsock2, dar, ca întotdeauna, ne-a dat o plimbare pe fund șmirghelși a spus, bine, ne vom descurca. Ceea ce este grozav la această bibliotecă este că toate funcțiile sale sunt aceleași pentru multe platforme și limbaje de programare. Deci, de exemplu, dacă scriem un scanner de porturi, acesta poate fi ușor transferat în limbajul C/C++ și chiar scris ceva asemănător în *nix, pentru că acolo funcțiile de rețea se numesc la fel și au aproape aceiași parametri. Diferența între rețea biblioteca windowsși Linux este minim, deși există. Dar așa ar trebui să fie, pentru că Bill nu poate acționa ca o ființă umană și trebuie neapărat să se arate. Vă voi avertiza imediat că vom studia winsock2, iar delphi acceptă doar prima versiune. Pentru a-l vedea pe al doilea, trebuie să descarce fișiere antet pentru versiunea 2 pot fi găsite pe Internet. Întreaga activitate a bibliotecii de rețea este construită în jurul conceptului de socket - acesta este ca un canal de rețea virtuală. Pentru a vă conecta la server, trebuie să pregătiți un astfel de canal pentru lucru și apoi vă puteți conecta la orice port al sideboard-ului. Toate acestea se văd cel mai bine în practică, dar voi încerca să vă ofer acum un algoritm general pentru lucrul cu socket-uri:
1. Inițializați biblioteca winsock.
2. Inițializați soclul (canalul de comunicare). După inițializare, ar trebui să avem o variabilă care să indice noul canal. Socket-ul creat este, s-ar putea spune, un port deschis pe computer. Există porturi nu numai pe bufet, ci și pe al tău, iar atunci când datele sunt transferate între computere, se întâmplă între porturile de rețea.
3. Vă puteți alătura serverului. În fiecare funcție pentru lucrul cu o rețea, primul parametru trebuie să fie o variabilă care indică canalul creat prin care va avea loc conexiunea.

Să începem winsock

Primul lucru pe care trebuie să-l faceți este să porniți biblioteca (pentru Unixoids acest lucru nu trebuie făcut). Pentru a face acest lucru, trebuie să apelați funcția wsastartup. Are doi parametri:
- Versiunea de winsock pe care vrem să o începem. Pentru versiunea 1.0 trebuie să specificați makeword(1,0), dar avem nevoie de al doilea, ceea ce înseamnă că vom specifica makeword(2,0).
- O structură de tip twsadata, în care vor fi returnate informații despre winsock-ul găsit.
Acum vom învăța cum să închidem biblioteca. Pentru a face acest lucru, trebuie să apelați funcția wsacleanup, care nu are parametri. În principiu, dacă nu închideți winsock, atunci nu se va întâmpla nimic critic. După părăsirea programului, totul se va închide de la sine, pur și simplu ștergerea lucrurilor inutile imediat după utilizare este o bună practică în codare.

Primul exemplu

Să scriem imediat un exemplu care va inițializa winsock și va afișa informații despre acesta. Creați un nou proiect în delphi. Acum trebuie să conectați fișierele antet winsock versiunea 2 la el. Pentru a face acest lucru, mergeți la secțiunea de utilizări și adăugați acolo modulul winsock2. Dacă încercați să compilați acest proiect gol acum, delphi se va plânge de modulul adăugat. Acest lucru se datorează faptului că nu poate găsi fișierele în sine. Dacă ați descărcat fișierele antet winsock2, puteți face acest lucru în două moduri:
1. Salvați noul proiect într-un director și plasați fișierele winsock2.pas, ws2tcpip.inc, wsipx.inc, wsnwlink.inc și wsnetbs.inc acolo. Inconvenientul acestei metode este că fișierele de antet trebuie adăugate la fiecare proiect care utilizează winsock2.
2. Puteți arunca aceste fișiere în directorul delphilib, iar apoi orice proiect le va găsi cu siguranță.

Shkodim

Acum creați un formular cu un buton și un câmp de ieșire. După aceea, creați un handler de evenimente onclick pentru buton și scrieți următorul text acolo:
procedură tform1.button1click(emițător: obiect);
var
info:twsadata;
ÎNCEPE
wsastartup(makeword(2,0), info);
versionedit.text:=inttostr(info.wversion);
descriptionedit.text:=info.szdescription;
systemstatusedit.text:=info.szsystemstatus;
wsacleanup;
Sfârşit;

La început am început winsock folosind wsastartup. În ea solicit a 2-a versiune, iar informații despre starea actuală îmi vor fi returnate în structura de informații. După aceea, afișez informațiile primite pentru vizionare publică. Am o ușoară problemă la scoaterea informațiilor despre versiune, deoarece proprietatea wversion a structurii de informații este de tip numeric și trebuie să o convertesc într-un șir pentru a o scoate. Pentru a face acest lucru, fac conversia folosind inttostr.

Pregătirea conectorului

Înainte de a face o conexiune la server, trebuie să pregătiți și priza pentru lucru. Asta vom face. Pentru a vă pregăti, trebuie să executați funcția socket, care are trei parametri:
1. Tipul de adresare utilizat. Suntem interesați de Internet, așa că vom indica pf_inet sau af_inet. După cum puteți vedea, ambele valori sunt foarte asemănătoare și arată aceeași adresare, doar în primul caz operația va fi sincronă, iar în al doilea asincron.
2. Protocol de bază. Aici trebuie sa indicam pe baza carui protocol se va desfasura lucrarea. Trebuie să știți că există două protocoale de bază - tcp (cu o conexiune de încredere) și udp (care nu face conexiuni, ci pur și simplu scuipă date în port). Pentru tcp, trebuie să specificați sock_stream în acest parametru, iar dacă aveți nevoie de udp, atunci specificați sock_dgram.
3. Aici putem indica ce protocol anume ne interesează. Există nenumărate valori posibile aici (de exemplu, ipproto_ip, ipport_echo, ipport_ftp etc.). Dacă doriți să vedeți totul, atunci deschideți fișierul winsock2.pas și rulați o căutare pentru ipport_, și tot ce veți găsi sunt protocoalele posibile.

Sincronicitate/asincronie

Acum vreau să vă prezint funcționarea porturilor sincrone și asincrone. Diferența dintre aceste două moduri este următoarea. Lucru sincron: atunci când apelați o funcție, programul se oprește și așteaptă să se finalizeze execuția. Să presupunem că ați solicitat o conexiune la server. Programul încetinește imediat și așteaptă până când apare o conexiune sau o eroare. Lucru asincron: În acest mod, programul nu se împiedică de fiecare funcție de rețea. Să presupunem că ați solicitat aceeași conexiune cu serverul. Programul dvs. trimite o solicitare de conectare și continuă imediat să efectueze următoarele acțiuni, fără a aștepta contactul fizic cu bufetul. Acest lucru este foarte convenabil (dar dificil de codat) deoarece puteți folosi timpul până când contactul are loc în propriile scopuri. Singurul lucru pe care nu îl puteți face este să apelați funcțiile de rețea până când apare un contact fizic real. Dezavantajul este că tu însuți trebuie să monitorizezi când se va termina funcția și poți continua să lucrezi cu rețeaua.

Conexiune completă

Priza este gata, ceea ce înseamnă că vă puteți conecta la server. În acest scop, biblioteca winsock are o funcție de conectare. Această funcție are trei parametri:
1. Variabila socket pe care am primit-o după apelarea funcției socket.
2. Structura de tip tsockaddr.
3. Mărimea structurii specificată în al doilea parametru. Pentru a afla dimensiunea, puteți utiliza funcția sizeof și puteți specifica o structură ca parametru.
Structura lui tsockaddr este foarte complexă și nu are rost să o descriem complet. Este mai bine să-l cunoaștem în practică, dar deocamdată voi arăta doar câmpurile principale care trebuie completate.
sin_family - familie de adrese utilizate. Aici trebuie să specificați același lucru care a fost specificat în primul parametru la crearea socket-ului (pentru noi acesta este Pf_inet sau af_inet).
sin_addr este adresa serverului la care vrem să ne alăturăm.
sin_port - portul la care dorim să ne conectăm.
In realitate va arata asa:

var
adresa: tsockaddr;
ÎNCEPE
addr.sin_family:= af_inet;
addr.sin_addr:= nume server;
addr.sin_port:= htons(21);
connect(fsocket, @addr, sizeof(addr));
Sfârşit;

închide

Și în sfârșit - o funcție pentru închiderea conexiunii - closesocket. Trebuie să specificați o variabilă socket ca parametru.