Lectura video:
Carte:
TCP (Transport Control Protocol) este un protocol ce furnizează transmisie garantată (cât timp există conexiune), în ordine și o singură dată, a octeţilor de la transmiţător la receptor. Acest protocol asigură stabilirea unei conexiuni între cele două calculatoare pe parcursul comunicaţiei, și este descris în RFC 793. Protocolul TCP are următoarele proprietăţi:
Explicaţii header:
Pentru a înțelege mai bine cum funcționează protocolul TCP, vom studia o captura a mesajelor TCP trimise de către chrome la accesarea unui website folosind CloudShark. Ne interesează doar pachetele TCP din captura, nu și cele cu TLS (folosit pentru encriptie peste TCP).
În primele 3 pachete TCP, putem observă operația de three way handshake între client (browser) și server. În acest caz, observăm că numărul de secvență atât la server cât și la client pornește de la 0 (SEQ = 0, ACK = 0). Următoarele pachete pe care le observăm sunt datele trimise între cele două entități. Putem observă cum numărul de secvență crește cu dimensiunea în bytes a mesajelor trimise.
În cadrul laboratorului de astăzi, pentru a realiza conexiunea vom folosi funcții precum connect și accept.
Data trecuta am discutat functiile socket, bind, recvfrom si sendto pe care le puteam folosi pentru a trimite datagrame UDP. Astazi, vom folosi tre functii noi: connect, listen si accept. Aceste functii sunt folosite pentru stabilirea unei conexiuni. Mai mult, astazi vom folosi functiile send si rev in locul functiilor recvfrom si sendto deoarece odata stabiltia o conexiune, nu mai trebuie sa specificam destinatia. Gasiti in imaginea de mai jos un overview a cum sunt realizate acestea.
In client, după ce am creat socketul, acesta trebuie să se conecteze la server (e.g. sa initieze si stabileasca un three way handshake). Pentru asta vom folosi funcția connect():
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Argumentul sockfd este un descriptor de fişier obţinut în urma apelului socket(), addr conţine portul şi adresa IP ale serverului, iar addrlen este dimensiunea celui de-al doilea parametru. Ca şi în cazul celorlate funcţii, rezultatul este -1 în caz de eroare, iar în caz de succes 0.
Comunicaţia prin conexiune stabilă este asimetrică. Mai precis, unul din cele două procese implicate joacă rol de server, iar celălalt joacă rol de client. Cu alte cuvinte, serverul trebuie să îi asocieze socketului propriu o adresă pe care oricare client trebuie să o cunoască, şi apoi să “asculte” pe acel socket cererile ce provin de la clienţi. Mai mult decât atât, în timp ce serverul este ocupat cu tratarea unei cereri, există posibilitatea de a întârzia cererile ce provin de la alţi clienţi, prin plasarea lor într-o coadă de aşteptare. Setarea unui socket pentru a fi pasiv se face prin intermediul funcției neblocante listen():
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); /* Usage example: After calling bind in the server, we listen at most 5 connections */ if ((listen(sockfd, 5)) != 0) { printf("Listen failed...\n"); exit(0); }
Argumentul sockfd reprezintă descriptorul de fişier obţinut în urma apelului socket(), iar backlog indică numărul de conexiuni acceptate în coada de aşteptare. Conexiunile care se fac de către clienți vor aştepta în aceasta coadă până când se face accept(), şi nu pot fi mai mult de backlog conexiuni în aşteptare. Apelul listen() întoarce 0 în caz de succes şi -1 în caz de eroare.
Ce se întâmplă în momentul în care un client încearcă să apeleze connect() către o maşină şi un port pe care s-a facut în prealabil listen()? Conexiunea va fi pusă în coada de aşteptare până în momentul în care se face un apel de accept() de către server. Acest apel întoarce un nou socket care va fi folosit pentru această conexiune:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); /* Usage example: after calling listen we can call accept to accept a connection from the queue */ int len; struct sockaddr_in cli; /* cli and len are written by the call with the info about the connected client (e.g. port, address) */ connfd = accept(sockfd, (struct sockaddr *)&cli, &len);
Argumentul sockfd reprezintă socketul pe care s-a făcut listen() (deci cel întors de apelul socket()). Funcția accept() întoarce un nou socket, care va fi folosit pentru operații send() / recv(). addr reprezintă un pointer spre o structură de tip struct sockaddr în care se va afla informaţia despre conexiunea făcuta (ce maşină de pe ce port a iniţiat conexiunea). Noul socket obţinut prin apelul accept() va fi folosit în continuare pentru operaţiile de transmisie și recepție de date.
Aceste două funcţii se folosesc pentru a transmite date prin sockeţi de tip stream sau sockeţi datagramă conectaţi. Sintaxa pentru trimitere şi primire este asemănătoare. Pentru trimitere, se folosește funcția send():
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int connfd, const void *buf, size_t len, int flags);
Argumentul connfd este socketul căruia se dorește să se trimită date (fie este returnat de apelul socket(), fie de apelul accept()). Argumentul buf este un pointer către adresa de memorie unde se găsesc datele ce se doresc a fi trimise, iar argumentul len reprezintă numărul de octeți din memorie începand de la adresa respectivă ce se vor trimite. Functia send() întoarce numărul de octeți efectiv trimiși (acesta poate fi mai mic decât numărul care s-a precizat că se dorește a fi trimis, adică len). În caz de eroare, funcția returnează -1, setându-se corespunzător variabila globală errno.
Pentru recepție de date, se folosește funcția recv():
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int connfd, void *buf, size_t len, int flags);
În cadrul funcției recv(), argumentul connfd reprezintă socketul de unde se citesc datele, buf reprezintă un pointer către o adresă din memorie unde se vor scrie octeții citiți, iar len reprezintă numărul maxim de octeți ce se vor citi. Funcția recv() întoarce numărul de octeți efectiv citiți în buf sau -1 în caz de eroare.
Observații:
Pentru a intelege mai bine, vom studia urmatoare implementare simpla de server si client folosinds API-ul de sockets:
Pentru implementarea cerințelor, vom porni de la acest schelet de cod. Treceți cu atenție peste scheletul de cod.