9.12. Socket-uri UDP¶
Traficul UDP în Python este trimis și primit prin două metode pe un socket de datagrame: sendto() pentru a expedia o datagramă către o destinație aleasă și recvfrom() pentru a primi o datagramă și a afla de unde provine. Fiecare apel transportă un mesaj autonom; nu există o stare de conexiune.
9.12.1. Trimiterea unei datagrame¶
Cea mai simplă trimitere UDP este o singură linie de Python deasupra unui constructor de socket:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()
Aceasta trimite b"hello" la portul 9000 de pe 192.168.1.20 și nu mai face nimic. MicroPython alege un port sursă efemer; scriptul nu trebuie să asocieze nimic.
Trimiterea aceleiași sarcini utile către mai multe destinații este doar o buclă – socket-ul este reutilizabil între trimiteri și nu există nicio conexiune de configurat:
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. Primirea unei datagrame¶
Pentru a primi datagrame, socket-ul trebuie să rezerve un port cunoscut pe care expeditorii îl vor folosi ca destinație. Acesta este apelul 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)
Adresa "0.0.0.0" înseamnă „orice interfață IPv4 de pe cameră” – indiferent de interfața Wi-Fi sau Ethernet care aduce pachete, portul 9000 aparține acestui socket.
Argumentul 1024 pentru recvfrom() este numărul maxim de octeți de citit în tamponul (buffer) returnat. Datagramele UDP mai mari decât această dimensiune vor fi trunchiate; alegeți valoarea astfel încât să corespundă celei mai mari datagrame pe care o așteaptă aplicația.
recvfrom() returnează (data, src): octeții primiți și adresa expeditorului. Adresa expeditorului este cea către care se răspunde, ceea ce face ușoară scrierea unui mic protocol de cerere/răspuns:
while True:
request, src = s.recvfrom(1024)
if request == b"ping":
s.sendto(b"pong", src)
În mod implicit, recvfrom() blochează până când sosește o datagramă. Modelele pentru a evita blocarea – limite de timp, socket-uri neblocante, asyncio – se găsesc în Socket-uri cu asyncio.
9.12.3. O cerere și un răspuns¶
Două scripturi scurte: unul trimite o cerere, unul primește și răspunde.
Receptorul:
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)
Expeditorul:
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?")
Câteva aspecte demne de remarcat la expeditor:
Niciun
bind()și niciunconnect(). Clienții UDP doar trimit.settimeout()impune o limită de timp apelului de primire. Dacă niciun răspuns nu sosește în două secunde, apelul genereazăOSErrorîn loc să blocheze la nesfârșit – o modalitate naturală de a detecta un pachet pierdut.Blocul
withînchide socket-ul automat.
9.12.4. Limite de dimensiune ale datagramelor¶
Datagramele UDP pot avea în teorie până la aproximativ 64 KB, dar limita practică este mult mai mică. Fiecare legătură de pe traseul dintre expeditor și receptor are o Unitate Maximă de Transmisie (MTU) – cel mai mare bloc unic de octeți pe care acea legătură îl poate transporta într-un singur cadru. Atât Ethernet, cât și Wi-Fi limitează acest lucru la aproximativ 1500 de octeți, iar aproape orice traseu de internet se reduce undeva la această limită.
Când o datagramă depășește MTU-ul unei legături pe care trebuie să o traverseze, nivelul de rețea o împarte în fragmente mai mici și le reasamblează la destinație. UDP în sine nu vede niciodată împărțirea, dar fragmentele au câteva proprietăți incomode:
Dacă vreun fragment se pierde, întreaga datagramă este aruncată la receptor – nu există retransmitere per fragment. Probabilitatea pierderii crește odată cu numărul de fragmente.
Unele rețele și firewall-uri aruncă complet pachetele fragmentate, tratându-le ca suspecte.
Reasamblarea consumă memorie la receptor, care pe un microcontroler este o resursă limitată.
Regula practică pe cameră: păstrați mesajele UDP cu mult sub 1500 de octeți. Aproximativ 1400 de octeți lasă loc pentru anteturile IP și UDP, orice supraîncărcare de tunelare pe care o adaugă traseul și mici variații de MTU între legăturile Ethernet, Wi-Fi și VPN. Aplicațiile care trebuie să trimită mai mult de atât ar trebui fie să fragmenteze datele la nivelul aplicației, fie să treacă la TCP, care gestionează automat împărțirea și reasamblarea.
9.12.5. Capcane frecvente¶
Uitarea faptului că UDP poate pierde pachete. Codul care funcționează perfect pe o rețea locală liniștită eșuează uneori în moduri subtile pe una mai aglomerată sau mai întinsă. Proiectați întotdeauna ținând cont de posibilitatea ca mesajul să nu fi ajuns.
Receptorul nu este asociat înainte ca expeditorul să trimită. O datagramă trimisă către un port pe care nu ascultă nimeni este aruncată în tăcere. Porniți mai întâi receptorul.
Trimiterea unei datagrame mai mari decât MTU-ul traseului. Vedeți secțiunea anterioară – păstrați mesajele sub ~1400 de octeți.
Modelele de mai sus acoperă aproape orice utilizare UDP la care recurge camera. Pagina următoare face echivalentul pentru TCP.