Pentru a simula o retea virtuala vom folosi Mininet. Vom avea nevoie de pachetele mininet si openvswitch-testcontroller.
sudo apt update sudo apt install mininet openvswitch-testcontroller xterm sudo pip3 install mininet
Pentru a avea un font mai mare in terminalele deschise de mininet:
echo "xterm*font: *-fixed-*-*-*-18-*" >> ~/.Xresources xrdb -merge ~/.Xresources
Porturile sunt conceptul ce ne ajuta sa facem multiplexare la nivel de aplicatie.
In mod obisnuit, cand ne referim la un port, ne gandim la un slot fizic ce asigura legatura dintre un calculator si dispozitive periferice (tastatura, imprimanta) sau dintre calculator si Internet (cablu Ethernet).
In contextul retelelor de comunicatie, un port este un numar asociat unei aplicatii (nu unui host). Daca o aplicatie doreste sa comunice cu alte aplicatii (aflate pe masini diferite sau aplicatii ce ruleaza local), aceasta expune un port, o locatie logica prin care accepta conexiuni si prin care se realizeaza schimbul de date.
Aceste numere permit aplicatiilor sa partajeze concurent resursele de retea.(Serverul de mail nu asteapta terminarea altor procese ce implica reteaua (web surfing) pentru a putea trimite un mail la destinatie).
Numerele sunt reprezentate pe 2 octeti, de la 0 la 65535, insa cele pana la 1024 sunt rezervate pentru aplicatii standard, precum:
O lista completa a porturilor rezervate gasiti pe site-ul IANA
Pentru a identifica o aplicatie cu care vrem sa comunicam este nevoie de adresa IP a masinii pe care ruleaza si portul deschis de aplicatie. De exemplu, prin localhost:80 solicitam accesul la aplicatia de pe masina locala ce ofera servicii web.
Serviciu neorientat conexiune: nu se stabileste o conexiune intre client si server. Asadar, serverul nu va asteapta apeluri de conexiune, ci asteapta direct datagrame de la clienti. Acest tip de comunicare este intalnit in sistemele client-server in care se transmit putine mesaje si in general prea rar pentru a mentine o conexiune activa intre cele doua entitati. (Un exemplu in acest caz este DNS-ul).
Nu se garanteaza ordinea primirii mesajelor si nici prevenirea pierderilor pachetelor. UDP-ul se utilizeaza mai ales in retelele in care exista o pierdere foarte mica de pachete si in cadrul aplicatiilor pentru care pierderea unui pachet nu este foarte grava (Un exemplu: aplicatiile streaming video).
Are un overhead foarte mic, in comparatie cu celelalte protocoale de transport (Are un header de 8 bytes, in comparatie cu TCP-ul care are minim 20 bytes)
Un segment UDP se numeste datagrama si este format dintr-un header de 8 bytes, urmat de date (In general ne vom referi la datele transmise de aplicatie drept payload) dupa cum putem observa in figura:
Portul sursa este ales random de catre masina sursa a pachetului dintre porturile libere existente pe acea masina. Este un numar pe 16 biti, intre 0 si 65535. Identifica procesul UDP care a transmis segmentul.
Portul destinatie este portul pe care masina destinatie poate receptiona pachete. Identifica procesul UDP care va procesa datele primite.
Length este lungimea in octeti (bytes) a datagramei (header size + data size).
Checksum este valoarea sumei de verificare pentru datagrama. In sectiunea urmatoare este prezentat modul de calcul a checksum-ului pentru un pachet UDP.
In unele cazuri, nu avem nevoie de transmisie reliable peste UDP (e.g. streaming web unde daca pierdem cateva cadre, putem interpola la receiver). Totusi, deseori avem nevoie sa stim cu certitudine ca un mesaj a ajuns in partea cealalta. Pentru a rezolva aceasta problema vom studia implementari de tip sliding window, mai exact cazul simplu in care fereastra de transmisie este egala cu 1.
Un exemplu de protocol reliable peste UDP este Quick UDP Internet Connections (QUIC) dezvoltat de Google in 2012.
Unul dintre cele mai simple metode de a transmite datele peste un mediu in care datagramele se pot pierde este protocolul stop and wait. In cadrul protocolului, sender-ul va trimite un frame si la primirea unei datagrame de ACK, va trimite urmatorul frame. Sender-ul foloseste un timeout pentru retransmisie in cazul in care o datagrama este pierduta.
Stop and wait este un caz particular de protocol ce foloseste sliding windows, in cazul acesta windows_size = 1. Pentru a intelege mai bine conceptul de fereastra glsianta va recomandam urmatorul simulator ce implementeaza doi algoritmi diferiti: go-back-n si selective repeat.
De notat faptul ca aceasta metoda este generala si nu se aplica doar la nivelul transport. TCP foloseste o fereastra de transmitere, dar intalnim acest concept si la nivele mai joase din stiva OSI.
In cadrul laboratorului nu vom implementa protocolul UDP ci vom folosi implementarea existenta din kernel-ul de Linux. Acest lucru se realizeaza prin intermediul API-ului de sockets. Un socket este un canal generalizat de comunicare intre procese, reprezentat in Unix print-un descriptor de fisiere*. El ofera posibilitatea de comunicare intre procese aflate pe masini diferite intr-o retea.
* Un file descriptor este un handle prin care un proces comunica cu resursele sistemului.
In continuare, se vor prezenta principalele functii pentru manipularea socketilor,
Intr-o arhitectura client-server, clientul trimite request-uri (cere resurse) catre server, iar acesta din urma trimite inapoi un raspuns (cu resursa).
Pasi urmati pentru a schimba mesaje folosind UDP la nivelul Transport folosind API-ul de sockets sunt urmatorii:
Pentru a afla mai multe informatii, putem accesa urmatorul capitol5.2 socket()—Get the File Descriptor!
#include <sys/types.h> #include <sys/socket.h> /* creare socket in C */ /* int socket(int domain, int type, int protocol); */ /* pentru UDP, folosim un socket de tip SOCK_DGRAM */ int sockid = socket(PF_INET, SOCK_DGRAM, 0); if (sockid == -1) { /* trateaza eroare */ }
Explicatii:
Utilizata in server pentru lega un socket de un port si eventual de un subnet. Pentru a afla mai multe informatii, putem accesa 5.3 bind()—What port am I on?.
#include <sys/types.h> #include <sys/socket.h> /*int bind(int sockfd, struct sockaddr *my_addr, int addrlen)*/ struct sockaddr myaddr; memset(&myaddr, 0, sizeof(servaddr)); myaddr.sin_family = AF_INET; // IPv4 /* INADDR_ANY = 0.0.0.0 as uint32 */ myaddr.sin_addr.s_addr = INADDR_ANY; myaddr.sin_port = htons(atoi(8888)); int rs = bind(sockfd, myaddr, sizeof(servaddr); /* in urma apelului, sockfd va avea adresa my_addr */ if (rs == -1) { /* trateaza eroare* / }
Explicatii:
Observam ca apelul bind utilizeaza o variabila de tip struct sockaddr_in.
#include <netinet/net.h> /* tip de date pentru a retine adresa unui socket in cazul comunicatiei prin Internet * are nevoie de un numar de port si adresa IP a calculatorului */ struct sockaddr_in { unsigned short sin_family; unsigned short int sin_port; struct in_addr sin_addr; }
Explicatii:
Functile sunt folosite pentru a primi/trimite o datagrama peste un socket. Mai multe detalii gasiti aici.
#include <sys/types.h> #include <sys/socket.h> struct sockaddr to; // Filling server information memset(&to, 0, sizeof(servaddr)); to.sin_family = AF_INET; to.sin_port = htons(8888); int rc = inet_aton("127.0.0.1", &to.sin_addr); int byteswrite = sendto(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *to, int addrlen); if (byteswrite == -1) { /* trateaza eroare */ } /* from va fi populata de apelul recvfrom si va contine informatii despre cine a trimis datagrama catre noi */ struct sockaddr from; int bytesread = recvfrom(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *from, int *addrlen); if (bytesread == -1) { /* trateaza eroare */ }
Explicatii:
Pentru a inchide un socket se foloseste functia de inchidere a unui descriptor de fisier din Unix:
#include <unistd.h> int close(ind fd);
Acest lucru va impiedica alte citiri si scrieri din socket. Pentru mai mult control asupra socketului, se foloseste functia shutdown(), care permite intreruperea comunicatiei selectiv, schimband modul de utilizare a legaturii full-duplex.
Pentru a afla mai multe informatii, putem accesa 5.9 close() and shutdown()—Get outta my face!.
#include <sys/socket.h> int shutdown(int sockfd, int how);
Explicatii:
API-ul de sockets este o o abstractizare foarte raspandita. Practic majoritatea interactiunilor cu internetul o sa fie prin intermediul API-ul de sockets din sistemul de operare. Spre exemplu, orice implica networking in python functioneaza peste o abstractizare de sockets din limbaj care cheama apelurile de sistem de linux de sockets. In codul de mai jos putem vedea un astfel de exemplu. De notat ca la fel se intampla si in Java, Haskell, etc.
Porniti de la urmatorul schelet de cod.
[Task-ul 1]: Plecând de la scheletul de cod oferit, scrieți o pereche de programe (client și server) care să permită transferul unui fișier binar de la client la server.
dd if=/dev/urandom of=file.bin bs=512 count=1000
Pentru a vedea dacă fișierul s-a trimis corect, folosiți md5sum. Sumele de control ale celor 2 fișiere trebuie să fie identice.
Specificații:
La final, vom studia folosind Wireshark datagramele trimise de catre client.
Mai mult, daca sunteti in aceasi retea (e.g. pe acelasi WiFi) va puteti grupa cate doi pentru a trimite un fisier de la unu la celalalt. Mai exact, unul dintre voi va porni un server cu bind pe INADDR_ANY (0.0.0.0), iar clientul va putea trimite catre ip-ul celuilalt un fisierul. Felicitari, ati facut o aplicatie simpla de transfer de fisiere.
[Task-ul 2]:
Dacă ați rulat task-ul 1 cu ambele componente pe aceeași mașină, probabil totul a mers bine, însă o soluție naivă (care trimite datagrame doar de la sender la receiver), ascunde 2 probleme:
Pentru a rezolva aceste probleme, vom implementați un protocol de tipul stop-and-wait.
sudo python3 topology.py
2.1 Folosind simulatorul de la aceasta adresa vom simula protocolul stop and wait. Vom selecta urmatorii parametrii in protocol: protocol selective repeat si windows size 1. Pentru a pierde un pachet in timpul sumularii vom da click pe acesta.
2.2 Plecând de la scheletul de cod pentru task-ul 2 implementati protocolul stop-and-wait. Vom folosi mininet pentru a simula un link defectuos intre un sender (host 1) si u nreceiver (host 2). Topologia este un simpla:
L1 L2 h1 <--> router <--> h2
Link 1 - 10 Mbps, 5ms delay, 5% packet loss
Link 2 - 10 Mbps, 5ms delay, 6% packet loss
sudo python3 topology.py
În urma rulării comenzii, vi se vor deschide două terminale, pe două host-uri având atribuite următoarele adrese:
h1 - 192.168.1.100 h2 - 172.16.0.100
struct timeval tv; tv.tv_sec = 1; tv.tv_usec = 0; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv));
Ulterior, caz de timeout recvfrom va întoarce -1 și va seta errno la EAGAIN sau EWOULDBLOCK.
Task 3. Bonus. Implementati un protocol ce foloseste sliding window, precum go-back-n sau selective repeat. Va recomandam simulatorul acesta pentru a intelege mai bine functionalitatea.
Task 4. Bonus. Pentru topologia de mai sus si valorile date pentru L1 si L2, care este dimensiunea optima a ferestrei sender-ului?
Optimal size = (size of the link in Mb/s) x (round trip delay in seconds)
Afland dimensiunea maxima a unei datagrame UDP putem calcula dimensiunea optima a ferestrei.
sin_addr
din struct sockaddr_in
in string si invers avem functiile: inet_ntoa si inet_aton.
int enable = 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) perror("setsockopt(SO_REUSEADDR) failed");