9.13. TCP-sockets¶
TCP-sockets bestaan in twee vormen die er verschillend uitzien maar hetzelfde onderliggende type delen: client-sockets die met connect() verbinding maken met een externe server, en server-sockets die met bind(), listen() en accept() binnenkomende verbindingen afhandelen. Beide rollen gebruiken dezelfde socket-klasse die op Socketobjecten werd geïntroduceerd; alleen de methoden die erop worden aangeroepen verschillen.
9.13.1. Een TCP-client¶
De eenvoudigste client opent een verbinding, verstuurt een verzoek, leest het antwoord en sluit af:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)
s.close()
connect() voert de drie-weg-handshake uit die op TCP – een betrouwbare stroom van bytes wordt behandeld en keert terug zodra de verbinding open is. send() schrijft bytes naar de verbinding; recv() leest tot een opgegeven aantal bytes ervan. Zodra de applicatie klaar is, sluit close() de verbinding af.
Hetzelfde script, verpakt in het with-statement-idioom uit Socketobjecten, zodat de socket wordt gesloten zelfs als er iets een fout opwerpt:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
print(s.recv(1024))
9.13.1.1. Lezen tot het klaar is¶
Eén enkele recv() retourneert tot het gevraagde aantal bytes – het kan minder retourneren, omdat TCP een stroom is in plaats van een reeks berichten. De applicatie moet blijven lezen totdat ze het volledige antwoord heeft:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
De lus eindigt wanneer recv() een lege bytes retourneert. Dat gebeurt wanneer de andere kant zijn helft van de verbinding netjes heeft gesloten; de applicatie interpreteert “einde van de stroom” als hetzelfde als “einde van het bericht” in dit soort protocol.
9.13.1.2. Verzenden tot het klaar is¶
Het tegenovergestelde voorbehoud geldt voor send(): deze kan minder bytes verzenden dan gevraagd en retourneert het aantal daadwerkelijk geschreven bytes. Voor grote payloads moet je de niet-verzonden rest opnieuw proberen:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() voert de lus intern uit, zodat de meeste code gewoon die kan aanroepen en het handmatig opnieuw proberen kan vermijden:
s.sendall(some_big_bytes)
9.13.2. Een TCP-server¶
De serverkant bestaat uit vier stappen: claim een poort, zet de socket in luistermodus, accepteer verbindingen één voor één, communiceer op elke geaccepteerde socket. Een minimale echoserver:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")
while True:
conn, addr = server.accept()
print("connection from", addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data) # echo back
conn.close()
Stap voor stap:
bind()claimt een host en poort op de camera."0.0.0.0"accepteert op elke interface; door dit te vervangen door een specifiek IP wordt de luisteraar beperkt tot die interface.listen()schakelt de socket om van een normale socket naar een luisterende socket. Het argument is de backlog – hoeveel openstaande verbindingen MicroPython in de wachtrij zet terwijl de applicatie bezig is. Kies een klein getal;1is in de meeste gevallen prima.accept()blokkeert totdat een client verbinding maakt en retourneert dan(conn, addr): een nieuwe socket die deze ene verbinding voorstelt, en het adres van de client. De luisterende socket zelf blijft open om meer verbindingen te accepteren.Alle bytes voor het gesprek stromen door
conn, de nieuwe socket. Lezen en schrijven gebruiken dezelfderecv()- /send()-aanroepen als aan de clientkant.Wanneer de client de verbinding sluit, retourneert
recv()b""; de binnenste lus eindigt en de server sluit zijn kant metclose().
De buitenste while True springt terug naar accept() om op de volgende client te wachten. De server handelt in deze vorm één client tegelijk af; meerdere clients parallel draaien vereist ofwel threads ofwel asyncio. Dat laatste is het onderwerp van de volgende pagina.
9.13.3. Veelvoorkomende valkuilen¶
recv() behandelen alsof het berichtgebaseerd is. Dat is het niet. Twee
send(b"hi")-aanroepen kunnen binnenkomen als éénrecv(4)vanb"hihi", of als tweerecv(2)s. De applicatie moet zelf framing toevoegen als berichtgrenzen van belang zijn – een newline, een lengteprefix, wat dan ook.Vergeten opnieuw te proberen bij korte verzendingen. Gebruik
sendall()voor alles boven een paar honderd bytes.Vergeten de geaccepteerde socket te sluiten. Elke
connis een aparte socket; het sluiten van de luisterende socket sluit de geaccepteerde sockets niet.with-blokken op beide maken dit moeilijk verkeerd te doen:while True: with server.accept()[0] as conn: # ... talk on conn ...
Opnieuw binden aan een poort die nog in TIME_WAIT staat. Wanneer een server binnen een paar seconden na het sluiten opnieuw start, kan
bind()mislukken met “address in use” omdat MicroPython de poort nog vasthoudt voor de vorige verbinding.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)vóórbind()lost dit op.
9.13.4. Wat hierna komt¶
Blokkeren op accept() betekent dat de server slechts één client tegelijk kan bedienen. Blokkeren op recv() betekent dat één enkele trage client de hele lus laat hangen. Het standaardantwoord op de camera is asyncio – voer elke verbinding uit als zijn eigen taak en laat de event-lus tussen ze schakelen. De volgende pagina behandelt de asyncio-versies van alles op deze pagina.