0%

Die Bibliothek websockets, ohne asyncio · Übung

Lesen, wie eine Verbindung geendet hat

Der Handler aus Lektion 2 sendet korrekte Echos, und jedes Gespräch, das er bedient, endet trotzdem mit einem . Lies den Block, den Run ausgegeben hat: recv hat ConnectionClosedOK ausgelöst, weil der Client zuerst geschlossen hat, nichts in echo_handler hat die abgefangen, also hat sie den Handler verlassen, und die Bibliothek hat sie als connection handler failed protokolliert. Die Verbindung selbst wurde sauber geschlossen, wie die letzte Zeile des Blocks gesagt hat. Ein Client, der schließt, ist das normale Ende eines Gesprächs, also sollte der Handler seine dort beenden. Diese Lektion bringt ihn dazu und verwendet dann dieselben Ausnahmen, um zu melden, wie ein Gespräch geendet hat.

OK und Error

Sobald eine Verbindung geschlossen ist, löst jedes spätere recv und send darauf eine Ausnahme aus websockets.exceptions aus. Welche, hängt davon ab, wie die Verbindung geendet hat:

  • ConnectionClosedOK, wenn in jede Richtung ein Close- ging und beide 1000, 1001 oder gar keinen Code trugen (ein leerer Close, der als 1005 gemeldet wird);

  • ConnectionClosedError in jedem anderen Fall: ein Close mit einem anderen Code, etwa 1008, oder eine Verbindung, die ganz ohne Close-Frame endete.

Diese Aufteilung ist die eigene Entscheidung der Bibliothek darüber, was als normales Ende gilt, keine Regel aus RFC 6455. Beide Klassen sind Arten von ConnectionClosed, dem Namen, den echo_library.py bereits importiert. Eine except-Klausel, die eine Klasse nennt, fängt auch jede Ausnahme ab, die eine Art dieser Klasse ist, also fängt except ConnectionClosed: beide ab.

Der Server schließt mit 1008. Was löst das nächste recv des Clients aus?

Was angekommen ist

Jede dieser Ausnahmen trägt die Close-Frames. Nenne die Ausnahme in der except-Zeile, um sie zu lesen:

try:
    connection.recv(timeout=WAIT)
except ConnectionClosed as exc:
    print(exc.rcvd)

exc.rcvd ist der Close-Frame, der von der Gegenseite angekommen ist, mit .code und .reason, und exc.sent ist der, den diese Seite gesendet hat. Beide können None sein. Wenn exc.rcvd is None gilt, ist kein Close-Frame angekommen: Die endete ohne einen. Kapitel 5 hat diesen Fall als 1006 gemeldet, den Code, der nie gesendet wird und immer nur eine Verbindung meldet, die ohne Close-Frame endete, und describe_close tut dasselbe.

Also prüft describe_close(exc) zuerst exc.rcvd is None und gibt dann {"code": 1006, "reason": ""} zurück. Andernfalls gibt es Code und Grund von exc.rcvd in derselben Art von zurück. Verwende is None für die Prüfung, wie bei jedem Test auf None.

Älterer Code liest exc.code. Das funktioniert in der Version, die du festgelegt hast, noch, gibt aber eine DeprecationWarning aus, die auf rcvd.code verweist, also lies den Close-Frame über rcvd.

exc.rcvd ist None. Was ist passiert?

Auch send absichern

recv ist nicht der einzige Aufruf, der die Verbindung geschlossen vorfinden kann. Der Client kann schließen, nachdem dein recv zurückgekehrt ist und bevor dein send läuft; dann löst send ConnectionClosedOK aus. Also deckt das try beide Zeilen des Schleifenrumpfs ab:

try:
    message = connection.recv(timeout=WAIT)
    connection.send(message)
except ConnectionClosed:
    return

Das andere Ende ist ein Client, der nichts sagt. Wenn innerhalb von WAIT keine Nachricht ankommt, löst recv Pythons eingebautes TimeoutError aus. Ein Client, der 2 Sekunden lang nichts gesendet hat, wartet auf nichts von diesem Server, also schließe ihn selbst, mit 1000 und einem Grund wie "no message arrived in time", und kehre zurück. Gib dem try eine zweite Klausel, except TimeoutError:, die das tut.

Warum deckt der try-Block sowohl send als auch recv ab?

Deine Aufgabe

In echo_library.py:

  1. Schreibe describe_close(exc): {"code": 1006, "reason": ""}, wenn exc.rcvd is None gilt, und sonst Code und Grund aus exc.rcvd;

  2. stelle echo_handler fertig: Setze in der Schleife recv und send in ein try; kehre bei ConnectionClosed zurück; rufe bei TimeoutError connection.close(1000, "no message arrived in time") auf und kehre zurück. Die Grenze und der abschließende Close nach MAX_MESSAGES bleiben, wie sie sind.

Run

Klicke auf Run. Die Standardfehlerausgabe bleibt diesmal leer: kein Block connection handler failed. Drei neue Abschnitte verbinden sich mit dem Client der Bibliothek, lesen, bis die Verbindung geschlossen ist, und geben den Klassennamen der Ausnahme aus und das, was dein describe_close daraus gemacht hat. Eine fertige Version, auf der Website ausgeführt, hat Folgendes ausgegeben:

Lesson 4: you close
ConnectionClosedOK, {'code': 1000, 'reason': 'done'}
Lesson 4: the server closes with 1008
ConnectionClosedError, {'code': 1008, 'reason': 'this path is closed'}
Lesson 4: the server vanishes
ConnectionClosedError, {'code': 1006, 'reason': ''}

Im ersten schließt der Client mit 1000 und done, und der Server antwortet mit demselben Close, also tragen beide Frames 1000: OK. Im zweiten schließt der Handler eines Bibliotheks-Servers sofort mit 1008, also ist das Ende ein Error, und rcvd enthält Code und Grund des Servers. Im dritten beantwortet ein Server, der von Hand aus serve_handshake aus Kapitel 2 gebaut ist, das Upgrade mit einer 101 und schließt dann die TCP-Verbindung ohne Close-Frame: ein Error, bei dem rcvd auf None gesetzt ist, und deine meldet das als 1006. Lies ending und read_the_ending in library_demo.py, um zu sehen, wie jeder Fall erzeugt wird. Klicke dann auf Submit.

Dein Handler besteht aus einer Handvoll Zeilen, und dein Client aus ein paar mehr. Die nächste Lektion legt sie neben die Funktionen, die du in den Kapiteln 2 bis 5 geschrieben hast, und nennt, was die Bibliothek tut, das diese nie getan haben.

Aufgabe

In echo_library.py:

  1. Schreibe describe_close(exc). Wenn exc.rcvd is None gilt, gib {"code": 1006, "reason": ""} zurück. Andernfalls gib {"code": exc.rcvd.code, "reason": exc.rcvd.reason} zurück.

  2. Stelle echo_handler fertig. Setze in der das recv und das send in ein try. Kehre bei ConnectionClosed zurück. Rufe bei TimeoutError connection.close(1000, "no message arrived in time") auf und kehre zurück. Behalte die Grenze und den Close mit 1000 nach MAX_MESSAGES Nachrichten.

Klicke auf Run: Es erscheint kein Block connection handler failed, und die drei Abschnitte zu Lektion 4 geben ConnectionClosedOK, {'code': 1000, 'reason': 'done'}, ConnectionClosedError, {'code': 1008, 'reason': 'this path is closed'} und ConnectionClosedError, {'code': 1006, 'reason': ''} aus.

Klicke dann auf Submit. Die Prüfung baut die , die die Bibliothek auslöst, aus den eigenen Klassen der Bibliothek und übergibt sie an describe_close: OK- und Error-Enden mit Codes und Gründen, die du nicht gesehen hast, einen Close-, der angekommen ist, ohne dass einer zurückgesendet wurde, einen Close-Frame, der gesendet wurde, ohne dass einer angekommen ist, und gar keinen Close-Frame. exc.code statt exc.rcvd.code zu lesen scheitert, weil die Prüfung die Deprecation-Warnung der Bibliothek in einen Fehler verwandelt. Sie gibt echo_handler Stellvertreter-Verbindungen: einen Client, der nach zwei Nachrichten schließt, eine Verbindung, die ohne Close-Frame endet, ein send, das die Verbindung geschlossen vorfindet, einen Client, der nichts rechtzeitig sendet (ein Close mit 1000 wird erwartet und kein weiteres recv), und zehn Nachrichten, gefolgt von weiteren. Jeder Fall muss damit enden, dass der Handler zurückkehrt, nicht damit, dass er eine Ausnahme auslöst. Zwei except-Klauseln, eine für ConnectionClosedOK und eine für ConnectionClosedError, sind statt except ConnectionClosed: in Ordnung.