Ein Gespräch auf Loopback · Übung
In beide Richtungen schließen
Nach Lektion 1 endet jedes Gespräch damit, dass eine Seite auflegt. Der Server kann einen Client, der fertig ist, nicht von einem Client unterscheiden, dessen Programm abgestürzt ist, und keine Seite kann sagen, warum sie aufgehört hat.
Der Schließ-Handshake
RFC 6455 beschreibt ihn in den Abschnitten 1.4, 5.5.1 und 7.1.1:
Eine Seite, egal welche, sendet einen Close-Frame:
8, einen 2 Byte langen Code wie 1000 (“normal closure”, normales Schließen) und einen optionalen Grund in . Die andere Seite antwortet mit einem eigenen Close-Frame. Nachdem ein Endpunkt einen Close gesendet hat, sendet er keine Daten mehr.
Sobald beide Closes ausgetauscht sind, schließt im Normalfall der Server zuerst die
, und der Client wartet darauf.
Das Bild zeigt beide Reihenfolgen, oben den Client zuerst und darunter den Server zuerst. Achte in jeder Hälfte darauf, welche Seite die TCP-Verbindung schließt und was das letzte recv des Clients zurückgibt.

In Worten, die obere Hälfte: Der Client sendet einen maskierten Close mit dem Code 1000 und dem Grund “done”; der Server antwortet mit einem unmaskierten Close, der denselben Code und Grund trägt, und schließt dann die TCP-Verbindung; das nächste recv des Clients gibt b"" zurück, und erst dann ist das Gespräch vorbei.
Der RFC sagt, dass der antwortende Close “typically”, also üblicherweise, den empfangenen Code wiederholt. Dieser Kurs wiederholt den Code und den Grund, und genau das tut auch die Bibliothek websockets, die dir in Kapitel 6 begegnet.
Warum der Server TCP zuerst schließt: RFC 6455, Abschnitt 7.1.1, verlangt das, damit der Server und nicht der Client den Zustand TIME_WAIT hält, eine kurze Zeitspanne, in der die Seite, die zuerst geschlossen hat, einen Eintrag über die beendete Verbindung behält. Mehr als diesen einen Satz über TCP braucht diese Lektion nicht.
Beide Close-Frames sind ausgetauscht. Wer schließt im Normalfall zuerst die TCP-Verbindung?
Zwei Hilfsfunktionen
close_frame(event, *, mask_key=None) baut die Antwort auf ein Close-Ereignis. read_message meldet einen Close als {"type": "close", "code": ..., "reason": ...}, also ist die Antwort ws_frames.close_payload(event["code"], event["reason"]) in einem Close-Frame, maskiert, wenn ein mask_key angegeben ist, weil die Antwort eines Clients wie jeder Client-Frame maskiert sein muss. Ein Close 1000 “bye” wird mit diesen Bytes beantwortet:
88 05 03 e8 62 79 65
88 ist ein finaler Close-Frame, 05 die Payload-Länge, 03 e8 der Code 1000, und die letzten drei Bytes sind “bye”.
wait_for_close(read_exact, role) liest Ereignisse mit ws_frames.read_message(read_exact, role=role) und verwirft jedes, bis ein Close ankommt, und gibt dann dessen Code zurück. So wartet eine Seite, die ihren Close bereits gesendet hat, auf die Antwort: Alles andere, was ankommt, sind Daten, die die Gegenseite gesendet hat, bevor sie den Close gesehen hat, und es wird gelesen und weggeworfen.
Der Client zuerst
Das ist die Reihenfolge im wohlerzogenen Gespräch von Run. Nach dem letzten Echo tut client_conversation Folgendes:
Es sendet einen maskierten Close:
client_frame(ws_frames.OP_CLOSE, ws_frames.close_payload(1000, "done"), random_bytes=random_bytes);es ruft
wait_for_close(read_exact, "client")für die Antwort des Servers auf und behält ihren Code;es ruft noch einmal
sock.recv(1)auf, wasb""zurückgibt, wenn der Server TCP geschlossen hat;es gibt die Echos und diesen Code zurück.
Schritt 3 verwendet sock.recv direkt, nicht read_exact, weil b"" hier das erwartete Ende ist und kein Fehler.
Auf der Seite des Servers prüft serve_conversation jedes Ereignis, bevor es antwortet: Ein Close wird mit sock.sendall(close_frame(event)) beantwortet, und die {"messages": n, "code": event["code"]} zurück. Sie liest nichts mehr. Wenn sie zurückkehrt, schließt der with-Block um die Verbindung in der Verdrahtung den
Der Server zuerst
Der Server beginnt den Schließ-Handshake, wenn er seine Grenze erreicht, wie in der unteren Hälfte des Bildes. Nach dem Echo, mit dem max_messages erreicht ist, sendet er Close 1000 mit dem Grund “message limit reached”, ruft wait_for_close(read_exact, "server") auf und gibt {"messages": n, "code": 1000} zurück.
Ein Client hat vielleicht noch einen Text gesendet, bevor der Close des Servers ihn erreicht hat. wait_for_close liest diesen Text und verwirft ihn, weil ein Server, der seinen Close gesendet hat, nichts mehr sendet.
Auch der Client muss darauf vorbereitet sein. Während er auf ein Echo wartet, kann stattdessen ein Close ankommen. Dann antwortet er mit close_frame(event, mask_key=random_bytes(4)), ruft sock.recv(1) für das Ende der Verbindung auf und gibt die Echos, die er hat, mit dem Code des Close zurück.
Der Server hat an der Grenze seinen Close gesendet, und ein Text-Frame vom Client kommt vor dem Close des Clients an. Was tut der Server?
Der Code, den niemand sendet
Ein Close-Frame darf eine leere Payload haben, 88 00 von einem Server. parse_close_payload meldet ihn als Code 1005, den RFC 6455 als Kennzeichnung für “kein close_payload(1005) löst close_frame zuerst auf 1005 und beantwortet einen leeren Close mit einem leeren Close.
Der Close-Frame eines Clients hat eine leere Payload. Was trägt der Close des Servers?
Run
Klicke auf Run. Die Ausgabe nach dieser Lektion:
A conversation:
client: {'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}
server: {'messages': 3, 'code': 1000}
server thread still running: False
A rude client:
client: the server closed the connection without a close frame
server: stopped by ValueError: (1002, 'the frame breaks the masking rule')
server thread still running: False
Beide Seiten melden jetzt 1000: Das Gespräch endete, weil beide sich einig waren, dass es vorbei ist. Der unhöfliche Client ist unverändert; um ihn kümmert sich Lektion 3.
Klicke dann auf Submit. Es führt close_frame und wait_for_close für sich aus, gibt serve_conversation Stellvertreter-Clients, die auf jede der oben genannten Arten schließen, lässt client_conversation gegen einen Stellvertreter-Server laufen, der seinen Close beantwortet, und gegen einen, der zuerst schließt, und lässt deinen Client und deinen Server gemeinsam ein Gespräch schließen.
Eine wohlerzogene Gegenseite bekommt jetzt ein sauberes Ende. Eine Gegenseite, die die Regeln verletzt, bringt den Thread des Servers immer noch mit einer Ausnahme zum Absturz, und die nächste Lektion bringt den Server dazu, ihr mit einem eigenen Close zu antworten.
Aufgabe
Schreibe zwei loopback_demo.py. Submit nennt die erste neue Funktion, die noch NotImplementedError auslöst.
close_frame(event, *, mask_key=None): der Close, der auf ein Close-Ereignis antwortet, mit demselben Code und Grund, gebaut mitws_frames.close_payloadundws_frames.encode_frame(ws_frames.OP_CLOSE, ..., mask_key=mask_key). Antworte bei Code 1005 mit einer leeren Payload.wait_for_close(read_exact, role): Lies Ereignisse mitws_frames.read_message(read_exact, role=role), verwirf alles, was kein Close ist, und gib den Code des Close zurück.serve_conversation: Beantworte ein Close-Ereignis mitclose_frame(event)und gib sofort{"messages": n, "code": event["code"]}zurück. Sende beimax_messagesClose 1000 (Grund “message limit reached”), rufewait_for_close(read_exact, "server")auf und gib{"messages": n, "code": 1000}zurück. Lies nach dem Close des Clients nichts mehr.client_conversation: Sende nach dem letzten Echo mitclient_frameeinen maskierten Close 1000, hole den Code des Servers auswait_for_close(read_exact, "client"), rufe noch einmalsock.recv(1)für das Ende der Verbindung auf und gib die Echos mit diesem Code zurück. Wenn statt eines Echos ein Close ankommt, beantworte ihn mitclose_frame(event, mask_key=random_bytes(4)), rufesock.recv(1)auf und gib die bisherigen Echos mit dem Code des Close zurück.
Klicke auf Run: Beide Seiten des ersten Gesprächs sollten 1000 melden.
Klicke dann auf Submit. Die Prüfung öffnet nie einen close_frame mit den Codes 1000, 1012 und 4000, einem leeren Close und einem wait_for_close mit serve_conversation Stellvertreter-Clients, die nach einem max_messages=2 weitersenden, jeweils am Stück, ein Byte nach dem anderen und jeweils zwei Bytes geliefert, und dann verstummen, ohne zu schließen, sodass ein Server, der über den Close des Clients hinaus liest, ewig wartet und scheitert. Sie lässt client_conversation gegen einen Stellvertreter-Server laufen, der deinen Close beantwortet, und gegen einen, der nach einem Echo zuerst schließt, und prüft, dass dein Client liest, bis der Server die Verbindung schließt. Zum Schluss reden dein Client und dein Server über Stellvertreter miteinander, einmal schließt der Client und einmal schließt der Server zuerst, und sie müssen Byte für Byte übereinstimmen.