0%

Ein Gespräch auf Loopback · Übung

Die Verbindung scheitern lassen

Zwei Enden funktionieren jetzt: Jede Seite kann schließen, und beide melden 1000. Der unhöfliche Client in Run bekommt immer noch die alte Behandlung. Nach Lektion 2 lauten seine Zeilen so:

A rude client:
client: the server closed the connection without a close frame
server: stopped by ValueError: (1002, 'the frame breaks the masking rule')

Der Codec hat seine Aufgabe erfüllt: Er hat einen unmaskierten abgelehnt und mit dem Code 1002 gesagt, warum. Aber der ging direkt nach oben und aus serve_conversation heraus, beendete den Thread des Servers, und der Client hat nie erfahren, was er falsch gemacht hat. Diese Lektion verwandelt diese Ausnahme in einen Close-Frame.

Schließen und Scheitern

Ein Close ist ein Gespräch, das beide Seiten gemeinsam beenden. RFC 6455, Abschnitt 7.1.7, beschreibt das andere Ende, das Scheiternlassen der Verbindung, für den Fall, dass eine Gegenseite das Protokoll verletzt hat. Der Endpunkt, der es bemerkt, “SHOULD send a Close frame with an appropriate status code”, soll also einen Close-Frame mit einem passenden senden, “MAY omit” ihn, darf ihn also weglassen, wenn er glaubt, dass die Gegenseite ihn wahrscheinlich nicht liest, und muss aufhören zu verarbeiten, was die Gegenseite sendet, auch einen Close, der als Antwort gesendet wird.

Ein scheiternder Server tut also drei Dinge: Er sendet einen Close-Frame mit dem Code für die verletzte Regel, er liest nichts mehr, und er kehrt zurück. Dieser Kurs sendet diesen Close immer, wenn der noch funktioniert, weil der Code der Gegenseite, und dir, genau sagt, welche Regel verletzt wurde.

Codes aus dem Codec

Seit dem Projekt aus Kapitel 4 ist jede Ablehnung in read_message ein ValueError(code, reason). Der Code ist exc.args[0] und der Grund exc.args[1]:

  • 1002, Protokollfehler: ein unmaskierter Frame an einen Server, reservierte Bits oder , ein Steuerframe, der zu lang oder nicht final ist, ein , der nicht reisen darf;

  • 1003, eine Nachricht, die dieser Codec nicht annehmen kann: jedes Fragment, weil der Lern-Codec fragmentierte Nachrichten nicht zusammensetzt;

  • 1007, eine Textnachricht oder ein Close-Grund, der kein gültiges ist;

  • 1009, Nachricht zu groß: ein Frame, dessen Header mehr als MAX_SIZE Bytes ankündigt, abgelehnt, bevor ein einziges Payload-Byte gelesen wird.

failure_frame(exc) baut den Close aus diesen beiden : ws_frames.close_payload(exc.args[0], exc.args[1]) in einem unmaskierten Close-Frame. Für ValueError(1009, "x") ist das:

88 03 03 f1 78

03 f1 ist 1009, und 78 ist “x”.

Ein maskierter binärer Frame kündigt 2.000.000 Bytes an, mehr als MAX_SIZE. Was sendet der Server?

Eine Gegenseite, die verschwindet

Zwei andere Ausnahmen können einen Lesevorgang beenden, und keine davon ist eine verletzte Regel:

  • ConnectionError: read_exact hat b"" bekommen, weil die Gegenseite die ohne Close-Frame geschlossen hat, wie es der Client aus Lektion 1 getan hat;

  • TimeoutError: Die Gegenseite hat 2 Sekunden lang nichts gesendet, das Timeout, das die Verdrahtung auf jedem angenommenen Socket setzt.

In beiden Fällen gibt es niemanden, an den man einen Close senden könnte, oder keinen Grund zu glauben, dass ein Close gelesen würde. Der Server sendet nichts und meldet den Code 1006, den RFC 6455 genau dafür reserviert: Die Verbindung endete ohne Close-Frame. 1006 erscheint immer nur in Meldungen. Wie 1005 darf er nie in einem Frame reisen.

Die Verbindung des Clients bricht mitten in einem Gespräch ab. Was sendet der Server?

Deine Aufgabe

Schreibe failure_frame(exc). Umschließe dann die von serve_conversation, einschließlich der Schließschritte danach, mit try, mit zwei Handlern:

def serve_conversation(sock, read_exact, *, max_messages=MAX_MESSAGES):
    count = 0
    try:
        ...  # Lesson 2's loop and closing steps, unchanged
    except ValueError as exc:
        sock.sendall(failure_frame(exc))
        return {"messages": count, "code": exc.args[0]}
    except (ConnectionError, TimeoutError):
        return {"messages": count, "code": 1006}

count ist der Name, den deine Schleife für die bisher als Echo zurückgesendeten Nachrichten verwendet. Zwei getrennte Klauseln except ConnectionError und except TimeoutError funktionieren genauso gut. Beschränke die Handler auf diese Ausnahmetypen: Ein breites except Exception würde eine verletzte Regel wie eine verschwundene Gegenseite behandeln und überhaupt keinen Close senden.

Warum nicht warten

Nach einem Close in Lektion 2 hat der Server auf die Antwort gewartet. Nach einem Scheitern tut er das nicht. Warten würde bedeuten, der Gegenseite noch einmal zu vertrauen. Ein Client, der einen unmaskierten Frame gesendet hat, ist vielleicht ein kaputtes Programm oder etwas, das gar kein -Client ist, und was er als Nächstes sendet, könnte ein weiterer falscher Frame sein, ein Close oder nichts; im letzten Fall würde der Server sein Timeout von 2 Sekunden absitzen, während sein Thread grundlos festgehalten wird. Den Close zu senden und sofort zurückzukehren gibt der Gegenseite das eine, was sie braucht, den Code, und beendet den Teil des Servers.

Warum wartet der Server nicht auf den Close des Clients, nachdem er die Verbindung hat scheitern lassen?

Run

Klicke auf Run. Nach dieser Lektion werden die Zeilen des unhöflichen Clients zu:

A rude client:
client: {'type': 'close', 'code': 1002, 'reason': 'the frame breaks the masking rule'}
server: {'messages': 0, 'code': 1002}
server thread still running: False

Der unhöfliche Client liest jetzt einen Close-Frame, der seinen Fehler nennt, und der Thread des Servers endet mit einem Ergebnis statt mit einer Ausnahme. Das erste Gespräch meldet weiterhin auf beiden Seiten 1000.

Klicke dann auf Submit. Es gibt serve_conversation Frames, die jede der oben genannten Regeln verletzen, einen Header, der zu viele Bytes ankündigt, ohne dass etwas dahinter kommt, und Clients, die nach einer Nachricht verschwinden oder verstummen, und prüft den Close, den du sendest, den Code, den du zurückgibst, und dass nach einem Scheitern nichts gelesen wird.

Dein Server hat jetzt eine Antwort auf jede Art, wie ein Gespräch enden kann. Bisher hast du das in Run gesehen, und Submit hat es geprüft; die nächste Lektion schreibt eigene Tests, die es beweisen, über zwei verbundene Sockets ganz ohne .

Aufgabe

Schreibe eine und erweitere eine in loopback_demo.py.

  1. failure_frame(exc): Gib den unmaskierten Close- für einen ValueError(code, reason) zurück: ws_frames.close_payload(exc.args[0], exc.args[1]) in ws_frames.encode_frame(ws_frames.OP_CLOSE, ...).

  2. serve_conversation: Umschließe die und die Schließschritte danach mit try. Sende bei ValueError as exc failure_frame(exc) und gib sofort {"messages": n, "code": exc.args[0]} zurück, ohne erneut zu lesen. Sende bei ConnectionError oder TimeoutError nichts und gib {"messages": n, "code": 1006} zurück. Fange nur diese ab, nicht Exception.

Klicke auf Run: Der unhöfliche Client sollte jetzt {'type': 'close', 'code': 1002, ...} empfangen, und das erste Gespräch sollte weiterhin auf beiden Seiten 1000 melden.

Klicke dann auf Submit. Die Prüfung ruft failure_frame direkt auf und gibt serve_conversation Stellvertreter-Clients, die einen unmaskierten Text senden, einen Text, der kein ist, ein erstes Fragment und einen Header, der 1.000.001 Bytes ankündigt, ohne dass eine Payload dahinter kommt, jeweils am Stück, ein Byte nach dem anderen und jeweils zwei Bytes geliefert. Jeder muss einen Close mit 1002, 1007, 1003 oder 1009 bekommen, und der Stellvertreter verstummt danach, sodass ein Server, der weiterliest oder die zu große Payload liest, ewig wartet und scheitert. Clients, die ihre Verbindung schließen oder nach einem Text verstummen, müssen ihr Echo bekommen und sonst nichts, und ein Ergebnis mit 1006. Ein sauberer Close muss weiterhin wie in Lektion 2 funktionieren.