0%

Masken und Steuerframes · Übung

Sag, warum du schließt

Dein Decoder erkennt einen Close- jetzt als Steuerframe und gibt seine Payload als Bytes zurück, aber diese Bytes bedeuten ihm noch nichts. Ein Close-Frame ist die Art, wie eine Seite der anderen sagt, dass das Gespräch vorbei ist, und seine Payload kann sagen, warum: mit einer Zahl, dem , und einem optionalen Grund in Worten.

Was eine Close-Payload enthält

RFC 6455, Abschnitt 5.5.1, erlaubt zwei Formen. Die Payload ist entweder leer, oder sie beginnt mit einem 2 Byte langen vorzeichenlosen Code in , gefolgt von einem Grund in , der leer sein darf. Hier sind ein Close mit dem Code 1000 und sonst nichts und derselbe Close mit dem Grund “done”:

88 02 03 e8
88 06 03 e8 64 6f 6e 65

88 ist FIN plus 8, Close. Das nächste Byte ist die Länge, 2 oder 6. 03 e8 ist 1000 als Big-Endian-16-Bit-Zahl, die struct.pack("!H", 1000) baut und struct.unpack("!H", ...)[0] zurückliest, genau wie bei der 16-Bit-Länge in Kapitel 3. 64 6f 6e 65 ist “done” in UTF-8. Ein Close ist ein Steuerframe, also ist seine ganze Payload höchstens 125 Bytes lang, und der Grund bekommt, was der Code übrig lässt: höchstens 123 Bytes.

Was gibt das aus?

import struct
packed = struct.pack("!H", 1001).hex(" ")
number = struct.unpack("!H", bytes([0x03, 0xF1]))[0]
print(packed, number)

Die Codes, die dieser Kurs verwendet

Abschnitt 7.4.1 des RFC definiert, was die Codes im Bereich um 1000 bedeuten. Das sind die, die dieser Kurs sendet oder liest:

CodeBedeutungIn einem Close-Frame gesendet?
1000normales Schließen: Das Gespräch ist beendetja
1001going away, der Endpunkt geht weg, etwa ein Server, der herunterfährtja
1002Protokollfehler: Die Gegenseite hat eine Framing-Regel verletztja
1003nicht unterstützte Daten: Der Endpunkt kann diese Art von Daten nicht annehmenja
1005im Close-Frame war kein vorhandennie; von einem Empfänger gemeldet
1006die Verbindung endete ganz ohne Close-Framenie; von einem Empfänger gemeldet
1007ungültige Payload-Daten, etwa eine Textnachricht, die kein UTF-8 istja
1008Richtlinienverstoß, die allgemeine Ablehnung, wenn kein anderer Code passtja
1009Nachricht zu großja
1011der Server ist auf eine unerwartete gestoßenja

1005 und 1006 unterscheiden sich vom Rest. Sie beschreiben, was ein Empfänger gesehen hat, und sie existieren, damit ein Programm “die Gegenseite hat einen leeren Close gesendet” oder “die Verbindung ist einfach zu Ende gegangen” mit einer Zahl melden kann, wie jedes andere Ende. Sie reisen nie in einem Close-Frame.

Codes, die nicht reisen dürfen

Die Tabelle des RFC ist nicht die ganze . Abschnitt 11.7 des RFC richtet die IANA Close Code Number Registry ein, eine Liste, die nach der Veröffentlichung des RFC wachsen kann, und sie ist gewachsen: 1012 (Dienst-Neustart), 1013 (später erneut versuchen) und 1014 (fehlerhaftes Gateway) sind registriert, und echte Server senden sie. Ein Code ist also nicht ungültig, nur weil die eigene Tabelle des RFC bei 1011 aufhört. Das Register teilt die Zahlen so ein:

  • unter 1000: nicht verwendet;

  • 1004: reserviert;

  • 1005, 1006 und 1015: Kennzeichnungen, die ein Endpunkt über sich selbst meldet, nie gesendet (1015 bedeutet, dass der - gescheitert ist);

  • 1016 bis 2999: niemandem zugewiesen;

  • 3000 bis 3999: von Bibliotheken und Frameworks registriert;

  • 4000 bis 4999: für die private Verwendung durch eine Anwendung.

Damit bleiben 1000 bis 1003, 1007 bis 1014 und 3000 bis 4999 als die Codes, die in einem Close-Frame vorkommen dürfen. Diese Menge wurde beim Schreiben dieses Kurses mit der Prüfung verglichen, die die Bibliothek websockets (die du ab Kapitel 6 verwendest) auf Close-Codes anwendet, und die beiden stimmen bei jedem Code überein; mit der aktuellen Seite des Registers wurde sie nicht verglichen.

Ein Server schließt mit 1012 (Dienst-Neustart). Akzeptiert parse_close_payload das?

Was ist der längste Grund, den ein Close-Frame tragen kann?

Einen Close lesen

parse_close_payload verwandelt die Payload zurück in {"code": ..., "reason": ...}, und es muss vier Fälle unterscheiden:

  • Leer. Gültig. Es war kein Code vorhanden, also melde 1005 mit dem Grund "". Hier ist die Kennzeichnung des Empfängers am Werk: Die Gegenseite hat nicht 1005 gesendet, sie hat nichts gesendet.

  • Ein Byte. Ungültig: Es gibt keinen Platz für einen 2 Byte langen Code.

  • Ein Code, der nicht reisen darf, etwa 1004 oder 1005. Ungültig: Die Gegenseite hat die Regeln verletzt.

  • Ein Grund, der kein UTF-8 ist. Ungültig. .decode("utf-8") löst bei solchen Bytes UnicodeDecodeError aus, und du kannst ihn durchlassen: UnicodeDecodeError ist eine Art von , also fängt ein Aufrufer, der ValueError abfängt, auch ihn ab.

try:
    bytes.fromhex("c3 28").decode("utf-8")
except ValueError:
    print("a UnicodeDecodeError, caught as a ValueError")

Dieselben Prüfungen schützen die andere Richtung. close_payload(code, reason="") weigert sich, eine Payload mit einem Code zu bauen, der nicht gesendet werden darf, oder mit einem Grund, der die Payload länger als 125 Bytes macht, gezählt nach dem Kodieren.

Der Close-Frame einer Gegenseite hat eine leere Payload. Welchen Code meldet parse_close_payload?

Deine Aufgabe

Schreibe in ws_frames.py die drei für Lektion 4:

  1. close_code_allowed(code): True für 1000 bis 1003, 1007 bis 1014 und 3000 bis 4999, False für jeden anderen Code.

  2. close_payload(code, reason=""): der Code als 2 Bytes in Netzwerk-Byte-Reihenfolge, dann der Grund in UTF-8. Löse ValueError aus für einen Code, den close_code_allowed ablehnt, und für eine Payload über 125 Bytes.

  3. parse_close_payload(payload): {"code": 1005, "reason": ""} für eine leere Payload; sonst der 2 Byte lange Code und der als UTF-8 dekodierte Grund. Löse ValueError aus für eine Payload aus einem Byte und für einen Code, der nicht reisen darf; ein Grund, der kein UTF-8 ist, darf UnicodeDecodeError auslösen.

Klicke auf Run. Der erste Abschnitt zu Lektion 4 baut eine Close-Payload 1000 “done” und liest sie zurück. Der zweite versucht dasselbe mit 1005, und show gibt den ValueError aus, den dein close_payload auslöst; die Meldung ist deine eigene, also kann sie anders lauten als diese:

Lesson 4: close 1000 and back
{'payload': '03 e8 64 6f 6e 65', 'parsed': {'code': 1000, 'reason': 'done'}}
Lesson 4: close 1005, which no one may send
  ValueError: close code 1005 may not be sent
Lesson 6: what a server reads
  not written yet

Klicke dann auf Submit. Die Prüfung fragt close_code_allowed nach jedem Code von 0 bis 5.000. Sie baut Payloads für die Codes des RFC und für einen Code und Grund, die du nicht gesehen hast, darunter eine mit einem Grund von genau 123 Bytes und eine, die ein Byte länger ist, und erwartet ValueError für Codes, die nicht gesendet werden dürfen. Sie liest Payloads mit Codes aus dem Register und Gründen in UTF-8 und erwartet ValueError für eine Payload aus einem Byte, für Codes, die nicht reisen dürfen, und für einen Grund, der kein UTF-8 ist. Jeder Frame bisher hat eine ganze Nachricht getragen; die nächste Lektion sieht sich Nachrichten an, die in Stücken ankommen.

Aufgabe

Schreibe die drei für Lektion 4 in ws_frames.py:

  1. close_code_allowed(code): True für 1000 bis 1003, 1007 bis 1014 und 3000 bis 4999; False für jeden anderen Code.

  2. close_payload(code, reason=""): der Code als 2 Bytes in , dann der als kodierte Grund. Löse aus für einen Code, den close_code_allowed ablehnt, und für eine Payload, die länger als 125 Bytes ist (ein Grund von höchstens 123 Bytes nach dem Kodieren).

  3. parse_close_payload(payload): {"code": 1005, "reason": ""} für eine leere Payload; sonst {"code": ..., "reason": ...} mit dem 2 Byte langen Big-Endian-Code und dem als UTF-8 dekodierten Rest. Löse ValueError aus für eine Payload aus einem Byte und für einen Code, der nicht reisen darf. Ein Grund, der kein UTF-8 ist, darf UnicodeDecodeError auslösen, und das ist ein ValueError.

Klicke auf Run: Die Abschnitte zu Lektion 4 sollten {'payload': '03 e8 64 6f 6e 65', 'parsed': {'code': 1000, 'reason': 'done'}} und dann deinen ValueError für 1005 ausgeben. Klicke dann auf Submit. Die Prüfung vergleicht close_code_allowed für jeden Code von 0 bis 5.000 mit der Regel, baut die Payloads des RFC und eine unbekannte mit einem Häkchen als Grund, akzeptiert einen 123 Byte langen Grund und lehnt einen 124 Byte langen ab, lehnt Codes wie 1004, 1005, 1006, 1015, 999, 2999 und 5000 in beide Richtungen ab, liest die unbekannten Codes 1012, 1014, 3000 und 4999 mit Gründen in UTF-8 und lehnt eine Payload aus einem Byte und einen Grund ab, der kein UTF-8 ist. Dein Ergebnis- darf zusätzliche Schlüssel enthalten.