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:
| Opzione | Descrizione |
|---|---|
AF_INET | Famiglia di indirizzi IPv4 |
AF_INET6 | Famiglia di indirizzi IPv6 |
SOCK_STREAM | Protocollo TCP (connessione affidabile, orientata ai byte) |
SOCK_DGRAM | Protocollo 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
| Metodo | Descrizione |
|---|---|
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()vssendall()—send()può inviare meno byte del previsto se il buffer del kernel è pieno. Usare sempresendall()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) | |
|---|---|---|
| Connessione | Orientata, affidabile | Senza connessione |
| Ordine pacchetti | Garantito | Non garantito |
| Velocità | Più lento | Più veloce |
| Uso tipico | Web, email, file | Streaming, 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).
Riepilogo
| Concetto | Da ricordare |
|---|---|
sendall() | Usare sempre al posto di send() |
SO_REUSEADDR | Impostare sempre sui server |
with statement | Garantisce la chiusura automatica |
| Eccezioni | Gestire sempre con try/except |
| Thread daemon | Terminano automaticamente con il main |
| TCP vs UDP | TCP = affidabile; UDP = veloce |
EC