0%

Masken und Steuerframes · Abschlussprojekt

Abschlussprojekt: Projekt: Ein vollständiger Frame-Codec

Jede Regel dieses Kapitels steckt jetzt in ws_frames.py, aber in getrennten . Das Maskieren steckt in 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 selbst prüfen. Es will einen Aufruf, der beantwortet: “Was hat die andere Seite gerade gesendet?”. Dieses Projekt schreibt diesen Aufruf, read_message, und gibt jeder Ablehnung im Codec den , den eine Verbindung dafür senden würde.

Was read_message zurückgibt

read_message(read_exact, *, role, max_size=MAX_SIZE) liest genau einen über read_exact und gibt eines von fünf Ereignissen zurück, jedes ein :

Der FrameDas Ereignis
Text{"type": "text", "data": ...}, die Payload aus in einen 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 mit einer Meldung. Gib jeder auch ihren Code, als ersten vor der Meldung:

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 , also findet ein Handler, der den Fehler 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:

#FunktionAblehnungCode
1, 2parse_header_bytesein reserviertes (RSV-)Bit gesetzt; ein reservierter Opcode1002
3, 4, 5read_lengtheine 16-Bit-Länge unter 126; eine 64-Bit-Länge mit gesetztem oberstem Bit; eine 64-Bit-Länge unter 65.5361002
6decode_frameMASK entspricht nicht dem, was die Rolle verlangt (die Richtungsregel)1002
7, 8decode_frameein Steuerframe über 125 Bytes; ein Steuerframe mit nicht gesetztem FIN (ein raise darf beides abdecken)1002
9decode_frameeine Payload über max_size1009
10, 11parse_close_payloadeine Payload aus einem Byte; ein Code, der nicht reisen darf1002
neuread_messageFIN nicht gesetzt oder Opcode 0; Text, der kein UTF-8 ist; ein Close-Grund, der kein UTF-8 ist1003; 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:

  1. Arbeite die Tabelle oben durch: Ändere jede Ablehnung in parse_header_bytes, read_length, decode_frame und parse_close_payload von ValueError(message) in ValueError(code, message) mit dem Code, den die Tabelle angibt. Die Zeilen 7 und 8 dürfen sich ein raise teilen.

  2. Schreibe read_message(read_exact, *, role, max_size=MAX_SIZE): Rufe decode_frame mit require_mask passend zur Rolle auf und gib max_size weiter; löse ValueError(1003, ...) aus für einen Frame mit nicht gesetztem FIN oder Opcode 0; gib für jeden Opcode das Ereignis zurück und löse ValueError(1007, ...) aus für Text oder einen Close-Grund, der kein UTF-8 ist. Lass ConnectionError durch.

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 . Kapitel 5 bringt ihn auf eine echte Verbindung, zwischen einem Server und einem Client in einem Programm.

Aufgabe

Stelle den Lern-Codec in ws_frames.py fertig.

  1. Gib jeder Ablehnung auf dem Lesepfad ihren , als ValueError(code, message) mit dem Code zuerst:

    #AblehnungCode
    1, 2parse_header_bytesein reserviertes (RSV-)Bit gesetzt; ein reservierter 1002
    3, 4, 5read_lengtheine 16-Bit-Länge unter 126; eine 64-Bit-Länge mit gesetztem oberstem Bit; eine 64-Bit-Länge unter 65.5361002
    6decode_frameMASK entspricht nicht dem, was die Rolle verlangt1002
    7, 8decode_frameein Steuerframe über 125 Bytes; ein Steuerframe mit nicht gesetztem FIN1002
    9decode_frameeine Payload über max_size1009
    10, 11parse_close_payloadeine Payload aus einem Byte; ein Code, der nicht reisen darf1002

    Lass die Fehler von encode_frame, reply_to_ping und close_payload, wie sie sind.

  2. Schreibe read_message(read_exact, *, role, max_size=MAX_SIZE). Dekodiere einen mit require_mask=True, wenn role gleich "server" ist, und mit False, wenn es "client" ist, und gib max_size weiter. Löse ValueError(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öse ValueError(1007, ...) aus, wenn eine Text-Payload oder ein Close-Grund kein ist. Lass einen ConnectionError aus read_exact unverä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.