This is an old revision of the document!


Laboratorul 07 - Protocolul de transport TCP

Lectura Laborator

TCP

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:

  • stabilirea unei conexiuni între client și server; serverul va aștepta apeluri de conexiune din partea clienților
  • garantarea ordinii primirii mesajelor şi prevenirea pierderii pachetelor
  • controlul congestiei (fereastră glisantă)
  • overhead mai mare în comparaţie cu UDP (are un header de 20 Bytes, spre deosebire de UDP, care are doar 8 Bytes).

Header TCP

Explicaţii header:

  • portul sursă este ales random de către maşina sursă a pachetului, dintre porturile libere existente pe acea maşină
  • portul destinaţie este portul pe care maşina destinaţie poate recepţiona pachete
  • checksum este valoarea sumei de control pentru un pachet TCP

Pentru a intelege mai bine cum functioneaza protocolul TCP in parctic, vom studia o captura a mesajelor TCP trimise de catre chrome la accesarea unui website folosind CloudShark. Ne intereseaza doar pachetele TCP din captura, nu si cele cu TLS (folosit pentru encriptie peste TCP).

In primele 3 pachete TCP, putem observa operatia de three way handshake intre client (browser) si server. In acest caz, observam ca numarul de secventa atat la server cat si la client porneste de la 0 (SEQ = 0, ACK = 0). Urmatoarele pachete pe care le observam sunt datele trimise intre cele doua entitati. Putem observa cum numarul de secventa creste cu dimensiunea in bytes a mesajelor trimise.

In cadrul laboratorului de astazi, pentru a realiza conexiunea vom folosi functii precum connect si accept.

Sockets API for TCP

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 cadrul functie socket vom folosi SOCK_STREAM ca argument in locul SOCK_DGRAM.

connect()

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.

listen()

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.

accept()

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.

send() / recv()

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 sockfd 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 sockfd 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:

  • recv() poate întoarce și 0, acest lucru însemnând că entitatea cu care se comunică a închis conexiunea
  • pentru scrierea/citirea în/din sockeți TCP, se pot folosi cu succes și functiile write() și read() (foarte asemănătoare cu send() și recv(), mai puțin câmpul flags, pe care oricum îl setăm pe 0).

Pentru a intelege mai bine, vom studia urmatoare implementare simpla de server si client folosinds API-ul de sockets:

Click to display ⇲

Click to hide ⇱

  • Client code, sends hello world message
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(void)
{
    int socket_desc;
    struct sockaddr_in server_addr;
    char server_message[2000], client_message[2000];
 
    // Clean buffers:
    memset(server_message,'\0',sizeof(server_message));
    memset(client_message,'\0',sizeof(client_message));
 
    // Create socket, we use SOCK_STREAM for TCP
    socket_desc = socket(AF_INET, SOCK_STREAM, 0);
 
    if(socket_desc < 0){
        printf("Unable to create socket\n");
        return -1;
    }
 
    printf("Socket created successfully\n");
 
    // Set port and IP the same as server-side:
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(2000);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 
    // Send connection request to server:
    if(connect(socket_desc, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0){
        printf("Unable to connect\n");
        return -1;
    }
    printf("Connected with server successfully\n");
 
    // Get input from the user:
    printf("Enter message: ");
    gets(client_message);
 
    // Send the message to server:
    if(send(socket_desc, client_message, strlen(client_message), 0) < 0){
        printf("Unable to send message\n");
        return -1;
    }
 
    // Receive the server's response:
    if(recv(socket_desc, server_message, sizeof(server_message), 0) < 0){
        printf("Error while receiving server's msg\n");
        return -1;
    }
 
    printf("Server's response: %s\n",server_message);
 
    // Close the socket:
    close(socket_desc);
 
    return 0;
}

Server code

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(void)
{
    int socket_desc, client_sock, client_size;
    struct sockaddr_in server_addr, client_addr;
    char server_message[2000], client_message[2000];
 
    // Clean buffers:
    memset(server_message, '\0', sizeof(server_message));
    memset(client_message, '\0', sizeof(client_message));
 
    // Create socket:
    socket_desc = socket(AF_INET, SOCK_STREAM, 0);
 
    if(socket_desc < 0){
        printf("Error while creating socket\n");
        return -1;
    }
    printf("Socket created successfully\n");
 
    // Set port and IP that we'll be listening for, any other IP_SRC or port will be dropped:
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(2000);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 
    // Bind to the set port and IP:
    if(bind(socket_desc, (struct sockaddr*)&server_addr, sizeof(server_addr))<0){
        printf("Couldn't bind to the port\n");
        return -1;
    }
    printf("Done with binding\n");
 
    // Listen for clients:
    if(listen(socket_desc, 1) < 0){
        printf("Error while listening\n");
        return -1;
    }
    printf("\nListening for incoming connections.....\n");
 
    // Accept an incoming connection from one of the clients:
    client_size = sizeof(client_addr);
    client_sock = accept(socket_desc, (struct sockaddr*)&client_addr, &client_size);
 
    if (client_sock < 0){
        printf("Can't accept\n");
        return -1;
    }
    printf("Client connected at IP: %s and port: %i\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
 
    // Receive client's message:
    // We now use client_sock, not socket_desc
    if (recv(client_sock, client_message, sizeof(client_message), 0) < 0){
        printf("Couldn't receive\n");
        return -1;
    }
    printf("Msg from client: %s\n", client_message);
 
    // Respond to client:
    strcpy(server_message, "This is the server's message.");
 
    if (send(client_sock, server_message, strlen(server_message), 0) < 0){
        printf("Can't send\n");
        return -1;
    }
 
    // Closing the socket:
    close(client_sock);
    close(socket_desc);
 
    return 0;
}

Exerciții

Pentru implementarea cerințelor, vom porni de la acest schelet de cod.

  1. Scrieți o aplicație client-server TCP în care serverul se va comporta ca ecoul clientului (echo server). Într-o buclă, clientul citește un string de la tastatură, îl trimite serverului, așteaptă răspuns de la server și îl afișează. Serverul trimite înapoi clientului același lucru pe care îl primește de la el. Atât serverul cât și clientul primesc ca argumente adresa și portul serverului.
  2. Folosidn Wireshark, interceptati pachetele TCP trimise de catre client si server. Ce semnifica primele trei pachete? Pentru ce sunt folosite primele trei pachete? De ce nu putem folosi doar doua? Care este numarul de secventa de la care incepe clientul sa trimita?
  3. Completați codul serverului de mai sus astfel încât să funcționeze cu 2 clienți (ambele apeluri de accept() trebuie făcute înainte de primul send() sau recv()). Serverul va intermedia un fel de chat între cei doi clienți: va primi ceva de la un client și va trimite celuilalt, și reciproc. Trebuie avută atenție la ordinea operațiilor (scriere și citire de pe socket) atunci când rulați clienții (în laboratorul viitor, vom folosi în server un mecanism de multiplexare care va elimina acest inconvenient; clienții nu vor mai trebui să scrie și să citească de pe socket într-o anumită ordine).
pc/laboratoare/07.1649607257.txt.gz · Last modified: 2022/04/10 19:14 by vlad_andrei.badoiu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0