python socket TCP UDP threading reti

Socket in Python — TCP, UDP e Threading

I socket sono un’astrazione software che permette la comunicazione tra processi, sia sulla stessa macchina che su macchine diverse collegate in rete. In Python, il modulo socket (parte della libreria standard) fornisce un’interfaccia alle API di rete del sistema operativo.

Un socket viene identificato dalla combinazione di:

  • indirizzo IP (o hostname) della macchina
  • numero di porta su cui il processo è in ascolto
  • protocollo di trasporto utilizzato (TCP o UDP)

Creazione di un socket

La funzione socket.socket() accetta due argomenti principali:

OpzioneDescrizione
AF_INETFamiglia di indirizzi IPv4
AF_INET6Famiglia di indirizzi IPv6
SOCK_STREAMProtocollo TCP (connessione affidabile, orientata ai byte)
SOCK_DGRAMProtocollo UDP (datagrammi senza connessione)
import socket

# Socket TCP con IPv4 (la combinazione più comune)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Socket UDP con IPv4
s_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# I parametri di default sono AF_INET e SOCK_STREAM
s = socket.socket()

Metodi principali

MetodoDescrizione
bind(address)Assegna un indirizzo locale (host, porta) al socket. Usato dai server.
listen(backlog)Mette il socket in ascolto. backlog = max connessioni in coda.
accept()Accetta una connessione in arrivo. Restituisce (conn, address).
connect(address)Stabilisce la connessione verso un server. Usato dai client.
send(data)Invia dati. Attenzione: potrebbe non inviare tutti i byte!
sendall(data)Invia tutti i dati garantendo la trasmissione completa.
recv(bufsize)Riceve al massimo bufsize byte. Restituisce b'' se la connessione chiude.
sendto(data, addr)Invia dati a un indirizzo specifico (UDP).
recvfrom(bufsize)Riceve dati e restituisce (data, address) (UDP).
settimeout(secs)Imposta un timeout in secondi. None = bloccante, 0 = non bloccante.
close()Chiude il socket e rilascia le risorse.

Attenzione: send() vs sendall() — send() può inviare meno byte del previsto se il buffer del kernel è pieno. Usare sempre sendall() per garantire la trasmissione completa.

Socket TCP

Ciclo di vita di una connessione TCP

SERVER                          CLIENT
socket()                        socket()
bind()                          |
listen()                        |
accept() <------- connect() ----+
    |                               |
recv() <---------- sendall() -------+
    |                               |
sendall() ---------> recv() --------+
    |                               |
close()                         close()

Server TCP base

import socket

HOST = '127.0.0.1'
PORT = 9000

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_REUSEADDR evita l'errore 'Address already in use' dopo un riavvio rapido
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server in ascolto su {HOST}:{PORT}')

conn, addr = server.accept()
print(f'Connessione accettata da {addr}')

while True:
    data = conn.recv(1024)
    if not data:
        break
    print(f'Ricevuto: {data.decode()}')
    conn.sendall(data)  # echo

conn.close()
server.close()

Client TCP

import socket

HOST = '127.0.0.1'
PORT = 9000

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))

messaggio = 'Ciao, sono il client!'
client.sendall(messaggio.encode())

risposta = client.recv(1024)
print(f'Risposta dal server: {risposta.decode()}')

client.close()

Context manager (modo consigliato)

Il costrutto with garantisce la chiusura automatica del socket anche in caso di eccezione:

import socket

HOST = '127.0.0.1'
PORT = 9000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    client.sendall(b'Messaggio di prova')
    risposta = client.recv(1024)
    print(f'Risposta: {risposta.decode()}')
# Qui il socket è già chiuso automaticamente

Gestione delle eccezioni

import socket

HOST = '127.0.0.1'
PORT = 9000

try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
        client.settimeout(5)
        client.connect((HOST, PORT))
        client.sendall(b'Test')
        risposta = client.recv(1024)
        print(f'Ricevuto: {risposta.decode()}')

except socket.timeout:
    print('Errore: il server non ha risposto entro 5 secondi')
except ConnectionRefusedError:
    print(f'Errore: nessun server attivo su {HOST}:{PORT}')
except OSError as e:
    print(f'Errore di rete: {e}')

Server TCP multi-client con Threading

Un thread viene creato per ogni client che si connette, permettendo di gestire più connessioni contemporaneamente:

import socket
import threading

HOST = '127.0.0.1'
PORT = 9000

def gestisci_client(conn, addr):
    """Funzione eseguita in un thread separato per ogni client."""
    print(f'[+] Connesso: {addr}')
    try:
        conn.sendall(b'Benvenuto! Scrivi qualcosa e premi invio.')
        while True:
            dati = conn.recv(1024)
            if not dati:
                break
            print(f'[{addr}] Ricevuto: {dati.decode().strip()}')
            conn.sendall(dati)  # echo
    except OSError as e:
        print(f'[{addr}] Errore: {e}')
    finally:
        conn.close()
        print(f'[-] Disconnesso: {addr}')

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server in ascolto su {HOST}:{PORT}')

try:
    while True:
        conn, addr = server.accept()
        t = threading.Thread(target=gestisci_client, args=(conn, addr), daemon=True)
        t.start()
        print(f'Thread attivi: {threading.active_count() - 1}')
except KeyboardInterrupt:
    print('\nServer arrestato con Ctrl+C')
finally:
    server.close()

Thread daemon

Un thread daemon viene terminato automaticamente quando il programma principale finisce:

import threading

t = threading.Thread(target=funzione, daemon=True)
t.start()
# Quando main termina, t viene terminato automaticamente

Socket UDP

UDP è un protocollo senza connessione: i dati vengono inviati come datagrammi indipendenti, senza garanzia di consegna o ordine. È più veloce di TCP.

TCP (SOCK_STREAM)UDP (SOCK_DGRAM)
ConnessioneOrientata, affidabileSenza connessione
Ordine pacchettiGarantitoNon garantito
VelocitàPiù lentoPiù veloce
Uso tipicoWeb, email, fileStreaming, giochi, DNS

Server UDP

import socket

HOST = '127.0.0.1'
PORT = 9001

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
    server.bind((HOST, PORT))
    print(f'Server UDP in ascolto su {HOST}:{PORT}')

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

Client UDP

import socket

HOST = '127.0.0.1'
PORT = 9001

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    messaggio = 'Ciao via UDP!'
    client.sendto(messaggio.encode(), (HOST, PORT))

    risposta, server_addr = client.recvfrom(1024)
    print(f'Risposta da {server_addr}: {risposta.decode()}')

Opzioni utili

Informazioni sull’host

import socket

print(socket.gethostname())                                      # nome host locale
print(socket.gethostbyname(socket.gethostname()))                # IP locale
info = socket.getaddrinfo('www.google.com', 80, socket.AF_INET)
print(info[0][4])                                                # IP risolto

Timeout e modalità non bloccante

s.settimeout(None)    # bloccante (default)
s.settimeout(5.0)     # timeout 5 secondi
s.setblocking(False)  # non bloccante (equivale a settimeout(0))

Opzioni socket con setsockopt

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   # evita 'Address already in use'
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)   # rileva connessioni cadute
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)   # disabilita algoritmo di Nagle

Esercizi

Livello base

  • Modifica il client TCP in modo che invii 5 messaggi in sequenza ('Messaggio 1', 'Messaggio 2', …) e stampi ogni risposta.
  • Crea un server TCP che risponde sempre con il messaggio ricevuto convertito in MAIUSCOLO.
  • Testa il server UDP inviando tre messaggi consecutivi dal client.

Livello intermedio

  • Crea una calcolatrice di rete: il client invia una stringa come '10 + 5' e il server risponde con il risultato. Gestisci divisione per zero e operatori non validi.
  • Modifica il server multi-client per contare i client connessi e includerlo nel messaggio di benvenuto.
  • Implementa un timeout nel client: se il server non risponde entro 3 secondi, stampa un messaggio di errore.

Livello avanzato

  • Crea un server di file: il client invia il nome di un file e il server risponde con il suo contenuto.
  • Implementa una chat multi-utente: il server riceve messaggi da ogni client e li ritrasmette a tutti gli altri (broadcast).
ConcettoDa ricordare
sendall()Usare sempre al posto di send()
SO_REUSEADDRImpostare sempre sui server
with statementGarantisce la chiusura automatica
EccezioniGestire sempre con try/except
Thread daemonTerminano automaticamente con il main
TCP vs UDPTCP = affidabile; UDP = veloce