This shows you the differences between two versions of the page.
pc:laboratoare:07 [2022/04/10 18:30] vlad_andrei.badoiu [Sockets API for TCP] |
pc:laboratoare:07 [2022/05/02 11:50] (current) vlad_andrei.badoiu [Exerciții] |
||
---|---|---|---|
Line 7: | Line 7: | ||
Carte: | Carte: | ||
* [[https://www.computer-networking.info/2nd/html/protocols/tcp.html|TCP]] | * [[https://www.computer-networking.info/2nd/html/protocols/tcp.html|TCP]] | ||
- | * [[https://beej.us/guide/bgnet/html/#client-server-background|Client-Server Background]] | + | * [[https://www.educative.io/edpresso/how-to-implement-tcp-sockets-in-c|Client-Server Background]] |
Line 27: | Line 27: | ||
* **portul destinaţie** este portul pe care maşina destinaţie poate recepţiona pachete | * **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 | * **checksum** este valoarea sumei de control pentru un pachet TCP | ||
+ | |||
+ | |||
+ | 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 [[https://www.cloudshark.org/captures/88b6405ca75d?filter=tcp.stream%20eq%200|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**. | ||
Line 35: | Line 42: | ||
{{ :pc:laboratoare:tcp_client_server_interaction.png?750 |}} | {{ :pc:laboratoare:tcp_client_server_interaction.png?750 |}} | ||
- | </note> | ||
- | In cadrul functie socket vom folosi **SOCK_STREAM** ca argument in locul **SOCK_DGRAM**. | ||
<note> | <note> | ||
+ | In cadrul functie socket vom folosi **SOCK_STREAM** ca argument in locul **SOCK_DGRAM**. | ||
+ | </note> | ||
=== connect() === | === connect() === | ||
- | După ce am creat socketul, clientul trebuie să se conecteze la server, folosind funcția [[http://man7.org/linux/man-pages/man2/connect.2.html|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 [[https://beej.us/guide/bgnet/html/#connect|connect()]]: |
<code C> | <code C> | ||
Line 95: | Line 102: | ||
#include <sys/socket.h> | #include <sys/socket.h> | ||
- | ssize_t send(int sockfd, const void *buf, size_t len, int flags); | + | ssize_t send(int connfd, const void *buf, size_t len, int flags); |
</code> | </code> | ||
- | 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//. | + | <note warning> |
+ | Atentie la socket-ul pe care send il primeste. Acesta este socket-ul returnat de accept de la un client. Sockfd il folosim doar pentru a primi conexiuni de la clienti. | ||
+ | </note> | ||
+ | |||
+ | 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 [[http://man7.org/linux/man-pages/man2/recv.2.html|recv()]]: | + | Pentru recepție de date, se folosește funcția [[https://beej.us/guide/bgnet/html/#sendrecv|recv()]]: |
<code C> | <code C> | ||
Line 106: | Line 117: | ||
#include <sys/socket.h> | #include <sys/socket.h> | ||
- | ssize_t recv(int sockfd, void *buf, size_t len, int flags); | + | ssize_t recv(int connfd, void *buf, size_t len, int flags); |
</code> | </code> | ||
- | Î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. | + | Î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: | Observații: | ||
Line 116: | Line 127: | ||
+ | Pentru a intelege mai bine, vom studia urmatoare implementare simpla de server si client folosinds API-ul de sockets: | ||
+ | <spoiler> | ||
+ | |||
+ | * **Client code, sends hello world message** | ||
+ | |||
+ | <code C> | ||
+ | #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; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | **Server code** | ||
+ | <code C> | ||
+ | #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; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | </spoiler> | ||
==== Exerciții ==== | ==== Exerciții ==== | ||
- | Pentru implementarea cerințelor, vom porni de la [[https://gitlab.cs.pub.ro/protocoale-de-comunicatie/pcom-laboratoare-public/-/tree/master/lab7|acest schelet de cod]]. | + | Pentru implementarea cerințelor, vom porni de la [[https://gitlab.cs.pub.ro/protocoale-de-comunicatie/pcom-laboratoare-public/-/tree/master/lab7|acest schelet de cod]]. Treceți cu atenție peste scheletul de cod. |
- 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. | - 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. | ||
- | - 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? | + | - Folosind Wireshark, interceptați 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? |
- 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). | - 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). | ||
+ | - Împreună cu alți colegi din aceiași rețea (e.g. pe același WiFi), incercați să vă conectați între voi. | ||
+ | |||
+ | |||
+ | <note> | ||
+ | O posibila solutie a laboratorului se gaseste [[https://ocw.cs.pub.ro/courses/_media/pc/laboratoare/lab7_-_rezolvare.zip|aici]] | ||
+ | </note> |