9.12. UDP-sockets¶
UDP-verkeer wordt in Python verzonden en ontvangen met twee methoden op een datagram-socket: sendto() om een datagram af te vuren op een gekozen bestemming, en recvfrom() om een datagram te ontvangen en uit te vinden waar het vandaan kwam. Elke aanroep verplaatst één op zichzelf staand bericht; er is geen verbindingsstatus.
9.12.1. Een datagram verzenden¶
De eenvoudigste UDP-verzending is één regel Python bovenop een socket-constructor:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()
Dat verzendt b"hello" naar poort 9000 op 192.168.1.20 en gaat verder. MicroPython kiest een vluchtige bronpoort; het script hoeft niets te binden.
Dezelfde payload naar veel bestemmingen verzenden is gewoon een lus – de socket is herbruikbaar tussen verzendingen, en er is geen verbinding op te zetten:
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. Een datagram ontvangen¶
Om datagrammen te ontvangen, moet de socket een bekende poort claimen die de afzenders als hun bestemming zullen gebruiken. Dat is de bind()-aanroep:
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)
Het "0.0.0.0"-adres betekent “elke IPv4-interface op de camera” – welke Wi-Fi- of Ethernet-interface de pakketten ook binnenbrengt, poort 9000 behoort tot deze socket.
Het 1024-argument voor recvfrom() is het maximale aantal bytes dat in de geretourneerde buffer wordt gelezen. UDP-datagrammen boven deze grootte worden afgekapt; kies de waarde zodat ze overeenkomt met het grootste datagram dat de applicatie verwacht.
recvfrom() retourneert (data, src): de ontvangen bytes, en het adres van de afzender. Het adres van de afzender is waar je naartoe antwoordt, wat het eenvoudig maakt om een klein verzoek/antwoord-protocol te schrijven:
while True:
request, src = s.recvfrom(1024)
if request == b"ping":
s.sendto(b"pong", src)
Standaard blokkeert recvfrom() totdat er een datagram aankomt. De patronen om dit niet te laten blokkeren – time-outs, niet-blokkerende sockets, asyncio – staan op Sockets met asyncio.
9.12.3. Een verzoek en een antwoord¶
Twee korte scripts: het ene verstuurt een verzoek, het andere ontvangt en antwoordt.
De ontvanger:
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)
De afzender:
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?")
Een paar zaken die het vermelden waard zijn in de afzender:
Geen
bind()en geenconnect(). UDP-clients versturen gewoon.settimeout()zet een deadline op de ontvangstaanroep. Als er binnen twee seconden geen antwoord binnenkomt, werpt de aanroepOSErrorop in plaats van eeuwig te blokkeren – een natuurlijke manier om een verloren pakket te detecteren.Het
with-blok sluit de socket automatisch.
9.12.4. Limieten op datagramgrootte¶
UDP-datagrammen kunnen in theorie tot ongeveer 64 KB groot zijn, maar de praktische limiet is veel kleiner. Elke schakel in het pad tussen de afzender en de ontvanger heeft een Maximum Transmission Unit (MTU) – het grootste enkele blok bytes dat die schakel in één frame kan vervoeren. Ethernet en Wi-Fi beperken dit beide tot ongeveer 1500 bytes, en bijna elk internetpad herleidt ergens tot die limiet.
Wanneer een datagram de MTU overschrijdt van een schakel die het moet oversteken, splitst de netwerklaag het op in kleinere fragmenten en stelt ze opnieuw samen op de bestemming. UDP zelf ziet de splitsing nooit, maar fragmenten hebben verschillende ongemakkelijke eigenschappen:
Als ook maar één fragment verloren gaat, wordt het hele datagram bij de ontvanger weggegooid – er is geen hertransmissie per fragment. De kans op verlies groeit met het aantal fragmenten.
Sommige netwerken en firewalls laten gefragmenteerde pakketten volledig vallen en beschouwen ze als verdacht.
Het opnieuw samenstellen kost geheugen bij de ontvanger, dat op een microcontroller schaars is.
De praktische regel op de camera: houd UDP-berichten ruim onder de 1500 bytes. Ongeveer 1400 bytes laat ruimte voor de IP- en UDP-headers, eventuele tunneling-overhead die het pad toevoegt, en kleine variaties in MTU tussen Ethernet-, Wi-Fi- en VPN-schakels. Applicaties die meer dan dat moeten verzenden, moeten ofwel de data op applicatieniveau in stukken opdelen ofwel overstappen op TCP, dat het opdelen en opnieuw samenstellen automatisch afhandelt.
9.12.5. Veelvoorkomende valkuilen¶
Vergeten dat UDP pakketten kan verliezen. Code die perfect werkt op een rustig lokaal netwerk faalt soms op subtiele manieren op een druk of groter netwerk. Ontwerp altijd voor de mogelijkheid dat het bericht niet aankwam.
Ontvanger niet gebonden voordat de afzender verstuurt. Een datagram dat naar een poort wordt verstuurd waarop niemand luistert, wordt stilzwijgend weggegooid. Start eerst de ontvanger.
Een datagram verzenden dat groter is dan de MTU van het pad. Zie de vorige sectie – houd berichten onder ~1400 bytes.
De bovenstaande patronen dekken bijna elk UDP-gebruik waarvoor de camera wordt ingezet. De volgende pagina doet het equivalent voor TCP.