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 nessunconnect(). I client UDP si limitano a inviare.settimeout()impone una scadenza alla chiamata di ricezione. Se nessuna risposta arriva entro due secondi, la chiamata sollevaOSErrorinvece di bloccarsi per sempre – un modo naturale per rilevare un pacchetto perso.Il blocco
withchiude 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.