Masken und Steuerframes · Abschlussprojekt
Abschlussprojekt: Projekt: Ein vollständiger Frame-Codec
Jede Regel dieses Kapitels steckt jetzt in ws_frames.py, aber in getrennten encode_frame und decode_frame, die Grenzen für Steuerframes in decode_frame, die Close-Payload in parse_close_payload, und Fragmente werden noch nirgends abgelehnt. Ein Programm, das von einer Verbindung liest, will nicht fünf Funktionen aufrufen und read_message, und gibt jeder Ablehnung im Codec den
Was read_message zurückgibt
read_message(read_exact, *, role, max_size=MAX_SIZE) liest genau einen read_exact und gibt eines von fünf Ereignissen zurück, jedes ein
| Der Frame | Das Ereignis |
|---|---|
| Text | {"type": "text", "data": ...}, die Payload aus str dekodiert |
| binär | {"type": "binary", "data": ...}, die Payload als bytes |
{"type": "ping", "data": ...}, die Payload als bytes | |
| Pong | {"type": "pong", "data": ...}, die Payload als bytes |
| Close | {"type": "close", "code": ..., "reason": ...}, so wie parse_close_payload sie liest |
role sagt, welche Seite liest. Ein "server" liest die Frames seines Clients, also ruft er decode_frame mit require_mask=True auf; ein "client" liest die Frames seines Servers, mit require_mask=False. max_size geht unverändert an decode_frame weiter.
Nicht alles, was schiefgeht, ist eine Gegenseite, die eine Regel verletzt. Wenn die Verbindung mitten in einem Frame endet, löst read_exact ConnectionError aus, und read_message lässt ihn unverändert durch. Kapitel 5 meldet dieses Ende als 1006, den Code für eine Verbindung, die ohne Close-Frame endete, und das ist eine andere Geschichte als ein Protokollfehler.
Jeder Ablehnung ihren Code geben
Wenn eine Gegenseite eine Regel verletzt, schließt der Leser die Verbindung mit einem Code, der sagt, warum. Bisher war jede Ablehnung im Codec ein
try:
raise ValueError(1002, "opcode 3 is reserved")
except ValueError as exc:
print(exc.args == (1002, "opcode 3 is reserved"))
print(exc.args[0], exc)
Eine Ausnahme behält die Werte, mit denen sie erzeugt wurde, in ihrem Attribut args, einem as exc abfängt, den Code in exc.args[0] und den Grund in exc.args[1]. Dieser Block gibt True aus und dann 1002 (1002, 'opcode 3 is reserved'): Wenn ein ValueError mit zwei Werten ausgegeben wird, erscheint das ganze Tupel. Kapitel 5 liest exc.args[0], um den Close-Frame zu wählen, den es sendet.
Hier ist jede Ablehnung auf dem Lesepfad, elf, die schon im Codec stecken, und drei, die read_message hinzufügt:
| # | Funktion | Ablehnung | Code |
|---|---|---|---|
| 1, 2 | parse_header_bytes | ein reserviertes (RSV-)Bit gesetzt; ein reservierter Opcode | 1002 |
| 3, 4, 5 | read_length | eine 16-Bit-Länge unter 126; eine 64-Bit-Länge mit gesetztem oberstem Bit; eine 64-Bit-Länge unter 65.536 | 1002 |
| 6 | decode_frame | MASK entspricht nicht dem, was die Rolle verlangt (die Richtungsregel) | 1002 |
| 7, 8 | decode_frame | ein Steuerframe über 125 Bytes; ein Steuerframe mit nicht gesetztem FIN (ein raise darf beides abdecken) | 1002 |
| 9 | decode_frame | eine Payload über max_size | 1009 |
| 10, 11 | parse_close_payload | eine Payload aus einem Byte; ein Code, der nicht reisen darf | 1002 |
| neu | read_message | FIN nicht gesetzt oder Opcode 0; Text, der kein UTF-8 ist; ein Close-Grund, der kein UTF-8 ist | 1003; 1007; 1007 |
Die meisten Ablehnungen des Codecs sind 1002, Protokollfehler: Die Gegenseite hat eine Framing-Regel verletzt. Ein Frame über der Größengrenze verletzt keine Regel, also bekommt er 1009, Nachricht zu groß. Ein Fragment verletzt auch keine Regel: Wie Lektion 5 erklärt hat, setzt der Lern-Codec fragmentierte Nachrichten nicht zusammen, und 1003, nicht unterstützte Daten, ist der am wenigsten falsche Code für diese Grenze des Codecs. Die Fehler auf der Kodierseite, in encode_frame, reply_to_ping und close_payload, behalten ihre eine Meldung. Sie melden einen Fehler in deinem eigenen Programm, nicht etwas, das eine Gegenseite gesendet hat, also gehört kein Close-Code zu ihnen.
Was gibt das aus?
exc = ValueError(1009, "too big")
print(exc.args[0], exc.args[1])
Das Spezielle vor dem Allgemeinen
Ungültiges UTF-8 taucht an zwei Stellen auf: in einer Text-Payload und im Grund eines Close. In beiden löst .decode("utf-8") UnicodeDecodeError aus, und read_message macht daraus 1007, ungültige Payload-Daten. Setze bei einem Text-Frame das Dekodieren in einen try-Block mit except UnicodeDecodeError: und löse im Handler ValueError(1007, ...) aus. Mach bei einem Close-Frame dasselbe um den Aufruf von parse_close_payload herum.
Nenne in diesem Handler UnicodeDecodeError, nicht ValueError. UnicodeDecodeError ist eine Art von ValueError, also würde ein Handler für ValueError auch die 1002-Ablehnungen abfangen, die parse_close_payload für eine Payload aus einem Byte oder einen Code auslöst, der nicht reisen darf, und sie kämen als 1007 heraus. Wenn du die spezielle Ausnahme nennst, gehen diese Ablehnungen unberührt mit ihrem eigenen Code durch.
Ein Server liest einen maskierten Text-Frame, dessen Payload c3 28 ist. Welchen Code löst read_message aus?
Zwei Rollen, eine Funktion
Derselbe Frame kann für eine Rolle in Ordnung und für die andere ein Protokollfehler sein. Ein maskierter Frame ist genau das, was ein Server erwartet, und genau das, was ein Client ablehnen muss, und das role entscheidet darüber. Alles nach der Richtungsregel ist für beide Seiten gleich: die Grenzen, die Close-Payload, die UTF-8-Prüfung und die Ablehnung von Fragmenten.
read_message(read_exact, role="client") liest einen maskierten Frame. Was passiert?
Fragmente vor dem Inhalt
read_message fügt seine Prüfungen hinzu, nachdem decode_frame einen Frame zurückgegeben hat, also ist zu diesem Zeitpunkt jede Framing-Regel bereits angewendet: die Richtungsregel, die Grenzen für Steuerframes und die Größengrenze. Als Nächstes kommt die Prüfung auf Fragmente, bevor der Opcode entscheidet, welches Ereignis gebaut wird. Ein Fortsetzungsframe hat Opcode 0, der zu keinem der fünf Ereignisse gehört. Und ein Text-Frame mit nicht gesetztem FIN enthält nur einen Teil einer Nachricht, der mitten in einem Zeichen aufhören kann, das in UTF-8 mehrere Bytes braucht; ihn zuerst zu dekodieren würde ihn mit 1007 ablehnen und dem Text der Gegenseite die Schuld geben, obwohl der wahre Grund die eigene Grenze des Codecs ist, 1003.
Deine Aufgabe
In ws_frames.py:
Arbeite die Tabelle oben durch: Ändere jede Ablehnung in
parse_header_bytes,read_length,decode_frameundparse_close_payloadvonValueError(message)inValueError(code, message)mit dem Code, den die Tabelle angibt. Die Zeilen 7 und 8 dürfen sich einraiseteilen.Schreibe
read_message(read_exact, *, role, max_size=MAX_SIZE): Rufedecode_framemitrequire_maskpassend zur Rolle auf und gibmax_sizeweiter; löseValueError(1003, ...)aus für einen Frame mit nicht gesetztem FIN oder Opcode 0; gib für jeden Opcode das Ereignis zurück und löseValueError(1007, ...)aus für Text oder einen Close-Grund, der kein UTF-8 ist. LassConnectionErrordurch.
Klicke auf Run. Der Abschnitt zu Lektion 6 gibt einem Server das maskierte “Hello” des RFC, einen leeren maskierten Ping und einen maskierten Close 1000 und gibt die drei Ereignisse aus, die dein read_message zurückgibt:
Lesson 6: what a server reads
[{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}]
Klicke dann auf Submit. Die Prüfung führt zuerst ein paar unbekannte Fälle aus den Lektionen 1 bis 4 aus. Dann geht sie die Tabelle Zeile für Zeile durch, sendet read_message jeweils einen Frame, der eine Regel verletzt, und nennt die erste Funktion, deren Ablehnung den Aufrufer ohne den richtigen Code erreicht. Dann liest sie jede Art von Ereignis für beide Rollen, am Stück, ein Byte und jeweils zwei Bytes auf einmal geliefert, zwei Frames direkt hintereinander, kaputte Frames, die mit ihrem Code und einem str als Grund abgelehnt werden müssen, einen zu großen Frame, der abgelehnt werden muss, bevor seine Payload gelesen wird, und eine Verbindung, die mitten in einem Frame endet.
Der Lern-Codec ist jetzt für Nachrichten fertig, die in einen Frame passen, und für nichts darüber hinaus: Er setzt keine Fragmente zusammen, und er öffnet keinen
Aufgabe
Stelle den Lern-Codec in ws_frames.py fertig.
Gib jeder Ablehnung auf dem Lesepfad ihren
, als ValueError(code, message)mit dem Code zuerst:# Ablehnung Code 1, 2 parse_header_bytesein reserviertes (RSV-)Bit gesetzt; ein reservierter 1002 3, 4, 5 read_lengtheine 16-Bit-Länge unter 126; eine 64-Bit-Länge mit gesetztem oberstem Bit; eine 64-Bit-Länge unter 65.536 1002 6 decode_frameMASK entspricht nicht dem, was die Rolle verlangt 1002 7, 8 decode_frameein Steuerframe über 125 Bytes; ein Steuerframe mit nicht gesetztem FIN 1002 9 decode_frameeine Payload über max_size1009 10, 11 parse_close_payloadeine Payload aus einem Byte; ein Code, der nicht reisen darf 1002 Lass die Fehler von
encode_frame,reply_to_pingundclose_payload, wie sie sind.Schreibe
read_message(read_exact, *, role, max_size=MAX_SIZE). Dekodiere einenmit require_mask=True, wennrolegleich"server"ist, und mitFalse, wenn es"client"ist, und gibmax_sizeweiter. LöseValueError(1003, ...)aus für einen Frame mit nicht gesetztem FIN oder Opcode 0. Gib{"type": "text", "data": str},{"type": "binary", "data": bytes},{"type": "ping", "data": bytes},{"type": "pong", "data": bytes}oder{"type": "close", "code": int, "reason": str}zurück. LöseValueError(1007, ...)aus, wenn eine Text-Payload oder ein Close-Grund keinist. Lass einen ConnectionErrorausread_exactunverändert durch.
Klicke auf Run: Der Abschnitt zu Lektion 6 sollte [{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}] ausgeben. Klicke dann auf Submit. Die Prüfung beobachtet nur, was read_message zurückgibt und auslöst, mit dem Code als exc.args[0] und einem str als Grund in exc.args[1]. Sie führt erneut unbekannte Fälle aus den Lektionen 1 bis 4 aus, geht die Tabelle der Reihe nach mit einem Frame pro Zeile durch und nennt die erste Funktion, deren Ablehnung ohne den richtigen Code ankommt, liest dann jedes Ereignis für beide Rollen am Stück, ein Byte und jeweils zwei Bytes auf einmal, zwei Frames direkt hintereinander, die Ablehnungen mit ihren Codes (einen zu großen Frame vor jedem Lesen seiner Payload) und einen Frame, der mittendrin abgeschnitten ist und ConnectionError auslösen muss. Ereignisse dürfen zusätzliche Schlüssel enthalten.