9.12. Socket UDP

Il traffico UDP in Python viene inviato e ricevuto con due metodi su un socket datagram: sendto() per lanciare un datagram verso una destinazione scelta, e recvfrom() per ricevere un datagram e scoprire da dove proviene. Ogni chiamata trasporta un messaggio autonomo; non c’è alcuno stato di connessione.

9.12.1. Inviare un datagram

L’invio UDP più semplice è una riga di Python sopra un costruttore di socket:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()

Questo invia b"hello" alla porta 9000 su 192.168.1.20 e prosegue. MicroPython sceglie una porta sorgente effimera; lo script non deve fare il bind di nulla.

Inviare lo stesso payload a molte destinazioni è semplicemente un ciclo – il socket è riutilizzabile tra un invio e l’altro, e non c’è alcuna connessione da impostare:

targets = [
    ("192.168.1.20", 9000),
    ("192.168.1.21", 9000),
    ("192.168.1.22", 9000),
]

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    for addr in targets:
        s.sendto(b"hello", addr)

9.12.2. Ricevere un datagram

Per ricevere i datagram, il socket deve rivendicare una porta nota che i mittenti useranno come destinazione. È la chiamata bind()

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    data, src = s.recvfrom(1024)
    print("from", src, "got", data)

L’indirizzo "0.0.0.0" significa «ogni interfaccia IPv4 sulla camera» – qualunque sia l’interfaccia Wi-Fi o Ethernet che fa entrare i pacchetti, la porta 9000 appartiene a questo socket.

L’argomento 1024 di recvfrom() è il numero massimo di byte da leggere nel buffer restituito. I datagram UDP che superano questa dimensione verranno troncati; scegliere il valore in modo che corrisponda al datagram più grande che l’applicazione si aspetta.

recvfrom() restituisce (data, src): i byte ricevuti e l’indirizzo del mittente. L’indirizzo del mittente è ciò a cui rispondere, rendendo facile scrivere un piccolo protocollo richiesta/risposta:

while True:
    request, src = s.recvfrom(1024)
    if request == b"ping":
        s.sendto(b"pong", src)

Per impostazione predefinita recvfrom() blocca finché non arriva un datagram. Le tecniche per fare in modo che non blocchi – timeout, socket non bloccanti, asyncio – sono in Socket con asyncio.

9.12.3. Una richiesta e una risposta

Due brevi script: uno invia una richiesta, l’altro riceve e risponde.

Il ricevitore:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    req, src = s.recvfrom(64)
    print("got", req, "from", src)
    s.sendto(b"ack: " + req, src)

Il mittente:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.settimeout(2.0)                              # 2 s reply window
    s.sendto(b"ping", ("192.168.1.20", 9000))
    try:
        reply, _ = s.recvfrom(64)
        print("reply:", reply)
    except OSError:
        print("no reply in 2 s -- packet lost?")

Alcune cose degne di nota nel mittente:

  • Nessun bind() e nessun connect(). I client UDP si limitano a inviare.

  • settimeout() impone una scadenza alla chiamata di ricezione. Se nessuna risposta arriva entro due secondi, la chiamata solleva OSError invece di bloccarsi per sempre – un modo naturale per rilevare un pacchetto perso.

  • Il blocco with chiude automaticamente il socket.

9.12.4. Limiti di dimensione dei datagram

In teoria i datagram UDP possono arrivare fino a circa 64 KB, ma il limite pratico è molto più piccolo. Ogni collegamento sul percorso tra mittente e ricevitore ha una Maximum Transmission Unit (MTU) – il più grande blocco singolo di byte che quel collegamento può trasportare in un frame. Sia Ethernet sia Wi-Fi lo limitano a circa 1500 byte, e quasi ogni percorso internet riconduce a quel limite da qualche parte.

Quando un datagram supera la MTU di un collegamento che deve attraversare, il livello di rete lo suddivide in frammenti più piccoli e li riassembla a destinazione. L’UDP stesso non vede mai la suddivisione, ma i frammenti hanno diverse proprietà scomode:

  • Se anche un solo frammento va perso, l’intero datagram viene scartato dal ricevitore – non esiste ritrasmissione per singolo frammento. La probabilità di perdita cresce con il numero di frammenti.

  • Alcune reti e firewall scartano completamente i pacchetti frammentati, considerandoli sospetti.

  • Il riassemblaggio costa memoria al ricevitore, che su un microcontrollore è scarsa.

La regola pratica sulla camera: mantenere i messaggi UDP ben al di sotto di 1500 byte. Circa 1400 byte lasciano spazio per le intestazioni IP e UDP, eventuali overhead di tunneling aggiunti dal percorso e piccole variazioni di MTU tra collegamenti Ethernet, Wi-Fi e VPN. Le applicazioni che devono inviare più di questo dovrebbero suddividere i dati a livello applicativo oppure passare al TCP, che gestisce automaticamente la suddivisione e il riassemblaggio.

9.12.5. Insidie comuni

  • Dimenticare che l’UDP può perdere pacchetti. Codice che funziona perfettamente su una rete locale tranquilla a volte fallisce in modi sottili su una più trafficata o più estesa. Progettare sempre considerando la possibilità che il messaggio non sia arrivato.

  • Ricevitore non in bind prima che il mittente invii. Un datagram inviato a una porta su cui nessuno è in ascolto viene scartato silenziosamente. Avviare prima il ricevitore.

  • Inviare un datagram più grande della MTU del percorso. Vedere la sezione precedente – mantenere i messaggi sotto i ~1400 byte.

Le tecniche viste sopra coprono quasi ogni uso dell’UDP a cui la camera ricorre. La pagina successiva fa l’equivalente per il TCP.