9.13. TCP-Sockets¶
TCP-Sockets gibt es in zwei Ausprägungen, die unterschiedlich aussehen, aber denselben zugrunde liegenden Typ teilen: Client-Sockets, die sich per connect() mit einem entfernten Server verbinden, und Server-Sockets, die mit bind(), listen() und accept() eingehende Verbindungen entgegennehmen. Beide Rollen verwenden dieselbe socket-Klasse, die in Socket-Objekte vorgestellt wurde; lediglich die darauf aufgerufenen Methoden unterscheiden sich.
9.13.1. Ein TCP-Client¶
Der einfachste Client öffnet eine Verbindung, sendet eine Anfrage, liest die Antwort und schließt sie wieder:
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() führt den Drei-Wege-Handshake durch, der in TCP – ein zuverlässiger Strom von Bytes behandelt wird, und kehrt zurück, sobald die Verbindung offen ist. send() schreibt Bytes in die Verbindung; recv() liest bis zu einer angegebenen Anzahl von Bytes daraus. Sobald die Anwendung fertig ist, fährt close() die Verbindung herunter.
Dasselbe Skript, eingebettet in das with-Anweisungsidiom aus Socket-Objekte, sodass der Socket auch dann geschlossen wird, wenn etwas eine Ausnahme auslöst:
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. Lesen bis zum Ende¶
Ein einzelner recv() liefert bis zu der angeforderten Anzahl von Bytes zurück – es können auch weniger sein, denn TCP ist ein Datenstrom und keine Folge von Nachrichten. Die Anwendung muss so lange weiterlesen, bis sie die vollständige Antwort hat:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
Die Schleife endet, wenn recv() ein leeres bytes zurückgibt. Das geschieht, wenn die Gegenseite ihre Hälfte der Verbindung sauber geschlossen hat; die Anwendung interpretiert bei dieser Art von Protokoll „Ende des Streams“ als gleichbedeutend mit „Ende der Nachricht“.
9.13.1.2. Senden bis zum Ende¶
Der umgekehrte Vorbehalt gilt für send(): Es sendet möglicherweise weniger Bytes als angefordert und gibt die Anzahl der tatsächlich geschriebenen Bytes zurück. Wiederhole bei großen Nutzdaten den noch nicht gesendeten Rest:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() führt die Schleife intern aus, sodass der meiste Code einfach diese Methode aufrufen und das manuelle Wiederholen vermeiden kann:
s.sendall(some_big_bytes)
9.13.2. Ein TCP-Server¶
Die Serverseite besteht aus vier Schritten: einen Port belegen, den Socket in den Lauschmodus versetzen, Verbindungen einzeln annehmen, über jeden angenommenen Socket kommunizieren. Ein minimaler Echo-Server:
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()
Schritt für Schritt:
bind()belegt einen Host und Port auf der Kamera."0.0.0.0"nimmt Verbindungen auf jeder Schnittstelle an; ersetzt man es durch eine bestimmte IP, wird der Listener auf diese Schnittstelle beschränkt.listen()wandelt den Socket von einem normalen in einen lauschenden Socket um. Das Argument ist der Backlog – wie viele ausstehende Verbindungen MicroPython in die Warteschlange stellt, während die Anwendung beschäftigt ist. Wähle eine kleine Zahl;1ist für die meisten Fälle ausreichend.accept()blockiert, bis sich ein Client verbindet, und gibt dann(conn, addr)zurück: einen neuen Socket, der diese eine Verbindung repräsentiert, und die Adresse des Clients. Der lauschende Socket selbst bleibt offen, um weitere Verbindungen anzunehmen.Alle Bytes der Kommunikation fließen durch
conn, den neuen Socket. Lesen und Schreiben verwenden dieselbenrecv()- /send()-Aufrufe wie auf der Clientseite.Wenn der Client die Verbindung schließt, gibt
recv()b""zurück; die innere Schleife endet und der Server schließt sein Ende mitclose().
Die äußere while True springt zu accept() zurück, um auf den nächsten Client zu warten. Der Server bedient in dieser Form jeweils einen Client; um mehrere Clients parallel auszuführen, werden entweder Threads oder asyncio benötigt. Letzteres ist das Thema der nächsten Seite.
9.13.3. Häufige Fallstricke¶
recv() als nachrichtenförmig behandeln. Das ist es nicht. Zwei
send(b"hi")-Aufrufe können als ein einzigesrecv(4)mitb"hihi"ankommen oder als zweirecv(2)-Aufrufe. Die Anwendung muss eine Rahmung (Framing) hinzufügen, wenn Nachrichtengrenzen wichtig sind – ein Zeilenumbruch, ein Längenpräfix, was auch immer.Vergessen, bei kurzen Sendevorgängen erneut zu senden. Verwende
sendall()für alles, was über ein paar hundert Bytes hinausgeht.Vergessen, den angenommenen Socket zu schließen. Jedes
connist ein eigener Socket; das Schließen des lauschenden Sockets schließt die angenommenen Sockets nicht.with-Blöcke auf beiden machen es schwer, hier etwas falsch zu machen:while True: with server.accept()[0] as conn: # ... talk on conn ...
Erneutes Binden an einen Port, der sich noch im Zustand TIME_WAIT befindet. Wenn ein Server innerhalb weniger Sekunden nach dem Schließen neu startet, kann
bind()mit „address in use“ fehlschlagen, weil MicroPython den Port noch für die vorherige Verbindung hält.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)vorbind()behebt dies.
9.13.4. Wie geht es weiter¶
Das Blockieren bei accept() bedeutet, dass der Server immer nur einen Client gleichzeitig bedienen kann. Das Blockieren bei recv() bedeutet, dass ein einziger langsamer Client die gesamte Schleife zum Stillstand bringt. Die Standardantwort auf der Kamera ist asyncio – führe jede Verbindung als eigene Aufgabe aus und lass die Ereignisschleife zwischen ihnen vermitteln. Die nächste Seite behandelt die asyncio-Versionen von allem, was auf dieser Seite steht.