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; 1 is 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 dezelfde recv()- / 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 met close().

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 één recv(4) van b"hihi", of als twee recv(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 conn is 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óór bind() 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.