Vai al contenuto

Periodo 2 · Reti e socket

In questa lezione impari a

  • spiegare indirizzi IP, porte, TCP e UDP
  • scrivere un client e un server TCP con i socket
  • usare i socket UDP
  • gestire più client con i thread

Come due computer si parlano davvero: dai concetti base di rete, alla scrittura di un client e un server che si scambiano messaggi, fino a una piccola chat funzionante.

gennaio – marzo · ~10 settimane · 30 ore · 5 moduli + progetto

2.1 Richiami di rete e modello client-server

Durata indicativa: 2 settimane

Prima di scrivere codice, servono tre concetti minimi per capire chi parla con chi in rete.

  • Indirizzo IP — identifica quale computer nella rete (es. 192.168.1.10). È come l'indirizzo civico di un palazzo.
  • Porta — identifica quale programma, dentro quel computer, deve ricevere i dati (es. porta 80 per un sito web, 5000 per la vostra app Flask). È come il numero dell'appartamento dentro il palazzo.
  • Modello client-server — il server è il programma che sta in ascolto, pronto a rispondere; il client è chi si collega e fa richieste. Un browser è un client, il sito che visitate gira su un server.

TCP vs UDP: affidabile o veloce?

Quando due programmi si scambiano dati in rete, possono farlo in due modi diversi:

  • TCP — prima di mandare dati, stabilisce una connessione stabile, e garantisce che tutto arrivi, nell'ordine giusto (se un pezzo si perde, viene rispedito). Più lento, ma affidabile. Usato per: pagine web, email, trasferimento file.
  • UDP — spedisce i dati senza stabilire nulla in anticipo e senza controllare che siano arrivati. Più veloce, ma può perdere pezzi per strada. Usato per: streaming video, chiamate vocali, giochi online — dove un pacchetto perso ogni tanto è meno grave di un ritardo.

Un buon modo per ricordarli: TCP è come una raccomandata con ricevuta di ritorno (lenta ma sicura), UDP è come urlare un'informazione nella stanza — arriva subito, ma se qualcuno non sente non ve ne accorgete.

Esercizio pratico.

  1. Sul PC di laboratorio, aprite un terminale ed eseguite netstat -ano (Windows) o ss -tulpn (Linux): osservate le connessioni attive e le porte in ascolto
  2. Aprite Wireshark, avviate una cattura sulla vostra interfaccia di rete, poi visitate un sito qualsiasi dal browser: individuate i pacchetti TCP relativi a quella connessione
  3. Provate a riconoscere, tra le connessioni elencate da netstat/ss, la porta 80 o 443 (i siti web) e confrontatela con quanto visto in Wireshark

Cosa consegnare: uno screenshot dell'output del terminale con almeno 3 connessioni evidenziate e spiegate a parole (chi è il client, chi il server, che porta usa).

2.2 Socket TCP

Durata indicativa: 2 settimane

Un socket è il "punto di collegamento" che un programma usa per parlare in rete — pensatelo come una presa telefonica: da una parte il server la mette in ascolto, dall'altra il client ci si collega.

Il server

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # TCP
server.bind(("0.0.0.0", 5050))   # ascolta su tutte le interfacce, porta 5050
server.listen(1)             # accetta 1 connessione alla volta
print("Server in ascolto sulla porta 5050...")

connessione, indirizzo = server.accept()
print(f"Client connesso da {indirizzo}")

while True:
    dati = connessione.recv(1024)   # riceve fino a 1024 byte
    if not dati:
        break
    print(f"Ricevuto: {dati.decode()}")
    connessione.sendall(dati)       # rimanda indietro lo stesso messaggio (echo)

connessione.close()

Il client

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 5050))  # indirizzo e porta del server

while True:
    messaggio = input("Scrivi un messaggio (o 'esci'): ")
    if messaggio == "esci":
        break
    client.sendall(messaggio.encode())
    risposta = client.recv(1024)
    print(f"Il server ha risposto: {risposta.decode()}")

client.close()

encode() e decode() servono perché i socket viaggiano in byte, non in testo: encode() trasforma una stringa in byte prima di spedirla, decode() fa il percorso inverso quando la si riceve.

Esercizio pratico.

  1. Scrivete il server e il client come sopra (server di "echo": rimanda indietro ciò che riceve) e provateli su due terminali diversi dello stesso PC
  2. Modificate il server in modo che, invece di rimandare lo stesso messaggio, si comporti come una calcolatrice remota: il client manda una stringa tipo "3+5", il server la calcola e rimanda il risultato

Cosa consegnare: il codice di server e client della calcolatrice remota, con una breve demo (anche solo a voce) di 2-3 operazioni funzionanti.

2.3 Socket UDP

Durata indicativa: 2 settimane

Con UDP non c'è una connessione stabile da aprire: si manda un pacchetto direttamente all'indirizzo e porta di destinazione, senza sapere se qualcuno lo riceverà davvero.

Server UDP

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP
server.bind(("0.0.0.0", 5051))
print("Server UDP in ascolto sulla porta 5051...")

while True:
    dati, indirizzo_client = server.recvfrom(1024)
    print(f"Ricevuto da {indirizzo_client}: {dati.decode()}")
    server.sendto(dati, indirizzo_client)  # echo

Client UDP

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    messaggio = input("Scrivi un messaggio (o 'esci'): ")
    if messaggio == "esci":
        break
    client.sendto(messaggio.encode(), ("127.0.0.1", 5051))
    risposta, _ = client.recvfrom(1024)
    print(f"Il server ha risposto: {risposta.decode()}")

Notate le differenze nel codice: niente connect() né accept() — con UDP si usa direttamente sendto() e recvfrom(), specificando ogni volta l'indirizzo di destinazione, perché non esiste una connessione "aperta" da ricordare.

Esercizio pratico.

  1. Riscrivete il server e il client di echo del punto 2.2 in versione UDP, come sopra
  2. Elencate, a parole, almeno 3 differenze concrete che notate nel codice rispetto alla versione TCP
  3. Bonus: provate a chiudere il server e mandare comunque un messaggio dal client. Cosa succede rispetto a quando lo fareste con TCP?

Cosa consegnare: codice UDP funzionante + le differenze annotate.

2.4 Server multi-client

Durata indicativa: 2 settimane

I server visti finora hanno un limite grosso: accept() gestisce un solo client alla volta. Se un secondo client prova a collegarsi mentre il server sta ancora parlando con il primo, deve aspettare in coda.

La soluzione si collega a quanto visto nel Periodo 1: ogni volta che arriva un nuovo client, il server apre un thread dedicato a lui, e torna subito pronto ad accettarne altri.

import socket
import threading

def gestisci_client(connessione, indirizzo):
    print(f"Client connesso: {indirizzo}")
    while True:
        dati = connessione.recv(1024)
        if not dati:
            break
        print(f"[{indirizzo}] {dati.decode()}")
        connessione.sendall(dati)
    connessione.close()
    print(f"Client disconnesso: {indirizzo}")

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 5050))
server.listen()
print("Server multi-client in ascolto...")

while True:
    connessione, indirizzo = server.accept()
    # un thread dedicato per ogni client, il ciclo torna subito ad ascoltare
    t = threading.Thread(target=gestisci_client, args=(connessione, indirizzo))
    t.start()

Il ciclo principale fa solo una cosa: accettare nuovi client e "delegare" la conversazione a un thread. Così può accettarne un altro subito dopo, senza aspettare che il primo finisca — esattamente il concetto di reattività visto nel Periodo 1.

Esercizio pratico.

  1. Scrivete il server multi-thread come sopra e collegatevi con 2-3 client contemporaneamente (da terminali o PC diversi del laboratorio)
  2. Trasformatelo in una chat testuale: invece di rimandare l'eco al solo mittente, il server deve inoltrare ogni messaggio ricevuto a tutti gli altri client connessi (suggerimento: mantenete una lista condivisa delle connessioni attive, e attenzione a proteggerla con un Lock se più thread la modificano insieme — vi ricorda qualcosa del Periodo 1?)

Cosa consegnare: server e client della chat, con una demo tra almeno 2 postazioni del laboratorio.

2.5 Progetto del periodo

Durata indicativa: 2 settimane

Esercizio.

Obiettivo

Realizzare un'applicazione client-server completa, funzionante tra postazioni diverse della rete scolastica, che metta insieme socket e multi-threading.

Scegliete un tema (o proponetene uno voi, va approvato dal docente)

  • Chat testuale multi-utente — evoluzione diretta dell'esercizio del punto 2.4, magari con nickname per ogni utente
  • Gioco semplice a turni — es. tris (tris a due giocatori collegati da PC diversi) o "indovina il numero" multiplayer
  • Sistema di messaggi — un client invia un messaggio a un utente specifico (identificato da un nome), il server lo consegna solo a lui

Requisiti minimi

  • Server in grado di gestire più client contemporaneamente (con thread)
  • Comunicazione funzionante tra almeno due postazioni fisiche diverse del laboratorio, non solo sullo stesso PC
  • Gestione corretta della disconnessione di un client (il server non deve bloccarsi se un client si chiude)
  • Codice commentato nei punti chiave

Cosa consegnare: codice sorgente funzionante (su GitHub o cartella condivisa) + demo live tra due postazioni diverse della rete scolastica.

academy-pc.it · materiale didattico — Modulo Reti e Programmazione con Socket, TPSIT quarta