Laborator 10 - Comunicarea prin rețea

Prezentare teoretică

Pentru a putea discuta despre comunicarea într-o rețea, este important să cunoaștem următoarele noțiuni:

Adresa fizică

Adresa fizică, numită și adresa MAC este un număr pe 48 de biți care identifică unic o placă de rețea. Acest număr este inscripționat în placa de rețea de către producător, deci, în general nu poate fi schimbat. Adresa MAC este folosită pentru transmisia de date la nivelul de access la rețea din stiva TCP/IP.

O adresă MAC este reprezentată prin gruparea celor 48 de biți în câte 6 octeți și scrierea fiecărui octet in baza 16 (ex: 00:A0:C9:14:C8:29 sau 00A0-C914-C829).

Adresa IP

O adresă IP identifică o stație într-o rețea și se folosește pentru transmisia de date la nivelul rețea din stiva TCP/IP.

Practic, IP-ul unei stații este un număr, pe 32 de biți în cazul protocolului IPv4 sau pe 128 de biți în cazul protocolului IPv6. Uzual, adresele IP sunt scrise sub forma restransă. În cazul IPv4, adresa IP este scrisă sub formă de 4 numere în baza zecimală, cu valori între 0 și 255, separate prin . (ex: 192.168.0.14), iar în cazul IPv6, adresa IP este scrisă sub formă de 8 grupuri numere în baza hexazecimală, cu valori cuprinse între 0000 și ffff, separate prin : (ex: 2001:0db8:85a3:0000:0000:8a2e:0370:7334).

Port

Portul este un identificator folosit pentru transmisia de date la nivelul aplicație.

Pentru a asigura că două aplicații comunică prin rețea trebuie să specificăm adresa IP a stațiilor care comunică, dar și portul. În timp ce adresa IP asigură că pachetele ajung la destinație, portul asigură că pachetul primit este folosit de aplicația potrivită.

De exemplu, două calculatoare pot să comunice atât pentru a face schimb de email-uri, dar și pentru a realiza o video-conferință. Astfel, cele două calculatoare fac schimb pe mesaje pentru ambele aplicații (email și video-conferință). În timp ce adresele IP sunt folosite pentru a ne asigura că toate pachetele ajung unde trebuie, aplicațiile de email și de video-conferintă vor avea atribuit câte un port diferit, pe baza căruia mesajele vor fi distribuite aplicației potrivite.

În aplicațiile dezvoltate de noi trebuie să ne asigurăm ca alegem o valoare pentru port care nu e folosită de alte aplicații existente pe sistem. Altfel, riscăm să obținem conflicte. De aceea alegem valoarile 8000 sau 8080, care nu sunt folosite de servicii cunoscute.

Socket

Pentru a putea realiza transmisia de mesaje pe rețea, vom folosit structuri de tip socket.

Un socket permite transmisia de mesaje între aplicații de pe aceeasi mașină sau de pe mașini fizice diferite, într-o manieră similară cu cea folosind file descriptori.

În Python, vom folosi modulul socket pentru operațiile privind comunicarea prin rețea. Printre operațiile pe care le putem realiza folosind modulul socket se numără:

Obținerea de informații despre stația curentă

Funcția gethostname returnează adresa IPv4 a stației locale.

socket.gethostname()

Obținerea de informații despre alte elemente din rețea

Funcția getaddrinfo întoarce o listă cu 5 elemente care conțin informații despre o adresă și un port specificat. Lista returnată conține următoarele informații: (family, type, proto, canonname, sockaddr), iar sockaddr e o altă listă ce conține (address, port).

socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)

Funcția gethostbyname returnează adresa IPv4 a unui hostname.

socket.gethostbyname(hostname)

Crearea de conexiuni TCP

Folosind sockeți putem realiza două tipuri de transmisii de date: UDP sau TCP.

Pentru oricare din cele două tipuri de transmisii, trebuie să creăm un obiect nou de tip socket folosind funcția socket (family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None). În funcție de parametrii pasați constructorului, socket-ul este configurat pentru un anumit tip de transfer de date.

  • family - reprezintă tipul de adresare folosit (ex: pentru IPv4, tipul folosit este AF_INET, pentru IPv6 tipul este AF_INET6); în cazul nostru, vom folosi AF_INET (socket.AF_INET, în Python);
  • type - reprezintă tipul de transfer de date folosit; în general, vom alege între SOCK_STREAM (pentru transferul TCP) și SOCK_DGRAM (pentru transferul UDP);
  • proto - reprezintă protocolul folosit, în cazul nostru vom lăsa valoarea prestabilită;
  • fileno - reprezintă un file descriptor, iar dacă acest parametru este pasat, toate celelalte 3 valori sunt stabilite automat pe baza acestuia.
import socket
s = socket.socket()

Pentru transferul de tip TCP, se folosește paradigma client-server, în care un proces așteaptă conexiuni (server), în timp ce alte procese se pot conecta la acesta (client). Folosind TCP datele transmise ajung cu siguranță la destinație sau se primește o eroare.

Inițializarea serverului

Pentru a porni un server folosind modului socket, trebuie să efectăm următoarele operații:

  • Să atribuim o adresă și un port socket-ului (bind)
  • Să specificăm că așteptăm conexiuni și câte (listen)
  • Să acceptăm conexiunile primite (accept); funcția întoarce un tuplu cu două valori: conexiunea și o adresă; conexiunea este o variabilă de tip socket, care va fi folosită pentru transmisia de mesaje; adresa este adresa programului care s-a conectat.

Dacă adresa IP pasată funcției bind este 0.0.0.0, socket-ul acceptă conexiuni de pe toate plăcile de rețea.

Exemplu
import socket
s = socket.socket()
s.bind(('0.0.0.0', 8000))
s.listen(0)
conn, addr = serv.accept()

Inițializarea clientului

Pentru a iniția o conexiune din partea serverului, folosind modului socket, trebuie să apelăm funcția connect.

Pentru a încheia conexiunea cu serverul, vom folosi funcția close.

Exemplu
import socket
s = socket.socket()
s.connect(('localhost', 8000))
# send/receive data
s.close()

Pentru a ne conecta la un serviciu ce rulează pe mașina locală, putem folosi adresa localhost sau 127.0.0.1.

Schimbul de date pe rețea

Trimiterea de date

Pentru a trimite date folosind modulul socket putem folosi funcțiile send și sendto.

socket.send(bytes[, flags])
socket.sendto(bytes, address)
socket.sendto(bytes, flags, address)

Funcția send trimite date doar dacă socket-ul este conectat la un alt socket, deci poate fi folosită doar pentru o transmisie de tip TCP.

Funcția sendto trimite date la o adresă pasată ca parametru, fără a necesita o conexiune stabilită între cele două obiecte socket, deci poate fi folosită pentru o transmisie de tip UDP.

Parametrul address este un tupul de tipul (adress, port), ex: (“0.0.0.0”, 8000).

Primirea de date

Pentru a primi date folosind modulul socket putem folosi funcțiile recv și recvfrom.

socket.recv(bufsize[, flags])
socket.recvfrom(bufsize[, flags])

Funcția recv citește date până la dimensiunea maximă, specificată ca parametru (bufzise) și le returnează sub forma unui obiect de tip bytes. Această funcție este potrivită pentru transmiterea de tip TCP, unde avem o conexiune constantă cu un alt socket.

Funcția recvfrom citește date similar cu funcția recv, dar întoarce două obiecte: mesajul și adresa sursei. Astfel, putem folosi adresa returnată pentru a trimite mesaje înapoi. Aceasta funcție este potrivită pentru transmiterea de tip UDP, unde nu avem o conexiune cu un alt socket.

Obiectul address returnat este un tupul de tipul (adress, port), ex: (“0.0.0.0”, 8000).

Exemple

În aceste exemple vom crea un proces care așteaptă continuu mesaje si răspunde la acestea (server) și un alt proces care trimite un mesaj și asteaptă un răspuns (client).

Pentru a putea testa comunicarea pe rețea folosind un singur calculator, vom crea două fișiere sursă, fiecare simulând comportamentul unei stații diferite în rețea. Pentru a testa comunicarea, vom rula cele două programe în paralel.

Exemplu UDP

Server

import socket
 
buffersize = 2048
# create socket
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# bind socket to address and port
s.bind (("0.0.0.0", 8000))
 
# listen for data forever and reply with "Message received"
while True:
    data, addr = s.recvfrom(buffersize)
    print ("Data from {} is {}".format (addr, data))
    msg = str.encode ("Message received")
    s.sendto (msg, addr)

Client

import socket
buffersize = 2048
 
# create socket
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# create bytes object
msg = str.encode("Hello")
# send message
s.sendto (msg, ("localhost", 8000))
# read message
data, addr = s.recvfrom (buffersize)
print ("Data from {} is {}".format (addr, data))

Pentru a testa exemplul, rulați prima dată serverul și apoi clientul într-un alt terminal.

Exemplu TCP

Server

import socket
 
buffersize = 2048
# create socket
s = socket.socket (family=socket.AF_INET, type=socket.SOCK_STREAM)
# bind socket to address and port
s.bind (("0.0.0.0", 8000))
# wait for connections
s.listen (0)
# accept connections forever
while True:
    conn, addr = s.accept ()
    print ("Connected to {}".format (addr))
 
    # listen for data and reply with "Message received"
    while True:
        data = conn.recv(buffersize)
        # client finished sending message
        if not data:
            break
        print ("Received {}".format (data))
        msg = str.encode ("Message received")
        conn.send (msg)
    # close connection
    conn.close ()

Client

import socket
buffersize = 2048
 
# create socket
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# connect to server
s.connect (("localhost", 8000))
# create bytes object
msg = str.encode("Hello")
# send message
s.send (msg)
# read message
data = s.recv (buffersize)
# close connection
s.close ()
 
print ("Data from {} is {}".format (addr, data))

Exerciţii de laborator

Exercițiul 1 - Informații rețea

  1. Creați un program care afișează adresa fizică, adresa IP și numele stației.
  2. Afișați adresa IP a google.com.

Exercițiul 2 - Netcat

Netcat este un utilitar în linie de comandă care permite efectuarea de operații pe rețea. În acest exercițiu îl vom folosi pentru a schimba mesaje cu programele python din exemplele de mai sus.

  1. Rulați aplicația server din exemplul de transmisie TCP, după care într-un alt terminal, în paralel, rulați comanda: netcat -z -v localhost 8000. Observați răspunsul primit.
  2. Rulați aplicația server din exemplul de transmisie UDP, după care într-un alt terminal, în paralel, rulați comanda: echo test | netcat -u localhost 8000. Observați comportamentul rezultat (opțiunea -u specifică modul de transmisie UDP).
  3. Trimiteți un mesaj către programul server TCP folosind netcat.
  4. Rulați comanda netcat -lu -p 8080, care pornește un server UDP pe portul 8080. Modificați exemplul din laborator pentru a trimite un mesaj server-ului pornit.
  5. Rulați comanda netcat -l -p 8080, care pornește un server TCP pe portul 8080. Modificați exemplul din laborator pentru a trimite un mesaj server-ului pornit.

Exercițiul 3 - UDP

Creați două programe care comunică în mod UDP prin socket astfel încât unul din programe să primească trei numere separate prin ; și să răspundă cu media aritmetică a acestora.

Exercițiul 4 - Server TCP

Simulați un server TCP care primește comenzi bash și răspunde cu rezultatul acestora. Creați un client care trimite comenzi și afișează rezultatul primit de la server. Hint: folositi un pipe pentru a stoca output-ul comenzii lansate.

Exercițiul 6 - Server TCP multi-threaded

Modificați serverul creat la punctul anterior pentru a suporta mai multe conexiuni simultane. Folosiți threaduri în implementare. Hint: programul principal acceptă conexiunile, iar odată stabilită conexiunea, porniți un nou thread care primește ca parametru conexiunea.

sde/laboratoare/10_ro_python.txt · Last modified: 2020/04/23 17:29 by ioana_maria.culic
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