Maskers en controleframes · eindproject
Eindproject: Project: een volledige framecodec
Elke regel van dit hoofdstuk woont nu in ws_frames.py, maar in aparte encode_frame en decode_frame, de limieten voor controleframes in decode_frame, de close-payload in parse_close_payload, en fragmenten worden nog nergens geweigerd. Een programma dat van een verbinding leest, wil geen vijf functies aanroepen en zelf read_message, en geeft elke weigering in de codec de
Wat read_message teruggeeft
read_message(read_exact, *, role, max_size=MAX_SIZE) leest precies één read_exact en geeft een van vijf events terug, elk een
| Het frame | Het event |
|---|---|
| tekst | {"type": "text", "data": ...}, de payload gedecodeerd uit str |
| binair | {"type": "binary", "data": ...}, de payload als bytes |
{"type": "ping", "data": ...}, de payload als bytes | |
| pong | {"type": "pong", "data": ...}, de payload als bytes |
| close | {"type": "close", "code": ..., "reason": ...}, zoals parse_close_payload ze leest |
role zegt welke kant leest. Een "server" leest de frames van zijn client, dus roept hij decode_frame aan met require_mask=True; een "client" leest de frames van zijn server, met require_mask=False. max_size gaat ongewijzigd door naar decode_frame.
Niet alles wat misgaat, is een peer die een regel breekt. Als de verbinding midden in een frame eindigt, werpt read_exact ConnectionError op, en read_message laat die ongewijzigd door. Hoofdstuk 5 meldt dat einde als 1006, de code voor een verbinding die eindigde zonder close-frame, en dat is een ander verhaal dan een protocolfout.
Geef elke weigering haar code
Als een peer een regel breekt, sluit de lezer de verbinding met een code die zegt waarom. Tot nu toe was elke weigering in de codec een
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)
Een exception bewaart de waarden waarmee ze is gemaakt in haar attribuut args, een as exc vindt de code in exc.args[0] en de reden in exc.args[1]. Dit blok drukt True af, en daarna 1002 (1002, 'opcode 3 is reserved'): een ValueError met twee waarden afdrukken toont de hele tuple. Hoofdstuk 5 leest exc.args[0] om te kiezen welk close-frame het stuurt.
Hier is elke weigering op het leespad, elf die al in de codec zitten en drie die read_message toevoegt:
| # | Functie | Weigering | Code |
|---|---|---|---|
| 1, 2 | parse_header_bytes | een gereserveerde (RSV) bit gezet; een gereserveerde opcode | 1002 |
| 3, 4, 5 | read_length | een lengte van 16 bits onder 126; een lengte van 64 bits met de hoogste bit gezet; een lengte van 64 bits onder 65.536 | 1002 |
| 6 | decode_frame | MASK niet wat de rol eist (de richtingsregel) | 1002 |
| 7, 8 | decode_frame | een controleframe van meer dan 125 bytes; een controleframe met FIN uit (één raise mag beide dekken) | 1002 |
| 9 | decode_frame | een payload groter dan max_size | 1009 |
| 10, 11 | parse_close_payload | een payload van één byte; een code die niet mag reizen | 1002 |
| nieuw | read_message | FIN uit of opcode 0; tekst die geen UTF-8 is; een close-reden die geen UTF-8 is | 1003; 1007; 1007 |
De meeste weigeringen van de codec zijn 1002, protocolfout: de peer brak een framingregel. Een frame boven de groottelimiet breekt geen regel, dus krijgt het 1009, bericht te groot. Een fragment breekt ook geen regel: zoals les 5 uitlegde, voegt de leercodec gefragmenteerde berichten niet samen, en 1003, niet-ondersteunde data, is de minst verkeerde code voor die grens van de codec. De fouten aan de coderende kant, in encode_frame, reply_to_ping en close_payload, houden hun ene melding. Ze melden een fout in je eigen programma, niet iets wat een peer stuurde, dus er hoort geen sluitcode bij.
Wat drukt dit af?
exc = ValueError(1009, "too big")
print(exc.args[0], exc.args[1])
Specifiek vóór algemeen
Ongeldige UTF-8 duikt op twee plekken op: een tekstpayload, en de reden van een close. In beide werpt .decode("utf-8") UnicodeDecodeError op, en read_message maakt daar 1007 van, ongeldige payloaddata. Zet voor een tekstframe het decoderen in een try-blok met except UnicodeDecodeError: en werp ValueError(1007, ...) op vanuit de handler. Doe voor een close-frame hetzelfde rond de aanroep van parse_close_payload.
Noem in die handler UnicodeDecodeError, niet ValueError. UnicodeDecodeError is een soort ValueError, dus een handler voor ValueError zou ook de 1002-weigeringen vangen die parse_close_payload opwerpt voor een payload van één byte of een code die niet mag reizen, en die zouden als 1007 naar buiten komen. Door de specifieke exception te noemen, gaan die weigeringen onaangeroerd door met hun eigen code.
Een server leest een gemaskeerd tekstframe waarvan de payload c3 28 is. Welke code werpt read_message op?
Twee rollen, één functie
Hetzelfde frame kan voor de ene rol in orde zijn en voor de andere een protocolfout. Een gemaskeerd frame is precies wat een server verwacht en precies wat een client moet weigeren, en het role beslist dat. Alles na de richtingsregel is voor beide kanten hetzelfde: de limieten, de close-payload, de UTF-8-controle en de weigering van fragmenten.
read_message(read_exact, role="client") leest een gemaskeerd frame. Wat gebeurt er?
Fragmenten vóór inhoud
read_message voegt zijn controles toe nadat decode_frame een frame heeft teruggegeven, dus tegen die tijd is elke framingregel toegepast: de richtingsregel, de limieten voor controleframes en de groottelimiet. De fragmentcontrole komt daarna, voordat de opcode beslist welk event er wordt gebouwd. Een vervolgframe heeft opcode 0, en dat hoort bij geen van de vijf events. En een tekstframe met FIN uit bevat maar een deel van een bericht, dat midden in een teken kan ophouden dat in UTF-8 meerdere bytes kost; als je het eerst decodeerde, zou je het weigeren met 1007 en de tekst van de peer de schuld geven, terwijl de echte reden de eigen grens van de codec is, 1003.
Je opdracht
In ws_frames.py:
Werk de tabel hierboven af: verander elke weigering in
parse_header_bytes,read_length,decode_frameenparse_close_payloadvanValueError(message)inValueError(code, message)met de code die de tabel geeft. Rij 7 en 8 mogen éénraisedelen.Schrijf
read_message(read_exact, *, role, max_size=MAX_SIZE): roepdecode_frameaan metrequire_maskingesteld voor de rol enmax_sizedoorgegeven; werpValueError(1003, ...)op voor een frame met FIN uit of opcode 0; geef het event voor elke opcode terug, en werpValueError(1007, ...)op voor tekst of een close-reden die geen UTF-8 is. LaatConnectionErrordoor.
Druk op Run. De sectie van les 6 geeft een server het gemaskeerde “Hello” van de RFC, een lege gemaskeerde ping en een gemaskeerde close 1000, en drukt de drie events af die je read_message teruggeeft:
Lesson 6: what a server reads
[{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}]
Druk daarna op Submit. De controle draait eerst een paar onbekende gevallen uit les 1 tot en met 4. Daarna loopt ze de tabel rij voor rij af, stuurt read_message één frame dat elke regel breekt, en noemt de eerste functie waarvan de weigering de aanroeper bereikt zonder de juiste code. Daarna leest ze elk soort event voor beide rollen, in één keer afgeleverd, één byte en twee bytes per keer, twee frames direct achter elkaar, kapotte frames die geweigerd moeten worden met hun code en een str als reden, een te groot frame dat geweigerd moet worden voordat de payload gelezen wordt, en een verbinding die midden in een frame eindigt.
De leercodec is nu af voor berichten die in één frame passen, en verder niets: hij voegt geen fragmenten samen, en hij opent geen
Opdracht
Maak de leercodec in ws_frames.py af.
Geef elke weigering op het leespad haar
, als ValueError(code, message)met de code eerst:# Weigering Code 1, 2 parse_header_byteseen gereserveerde (RSV) bit gezet; een gereserveerde 1002 3, 4, 5 read_lengtheen lengte van 16 bits onder 126; een lengte van 64 bits met de hoogste bit gezet; een lengte van 64 bits onder 65.536 1002 6 decode_frameMASK niet wat de rol eist 1002 7, 8 decode_frameeen controleframe van meer dan 125 bytes; een controleframe met FIN uit 1002 9 decode_frameeen payload groter dan max_size1009 10, 11 parse_close_payloadeen payload van één byte; een code die niet mag reizen 1002 Laat de fouten van
encode_frame,reply_to_pingenclose_payloadzoals ze zijn.Schrijf
read_message(read_exact, *, role, max_size=MAX_SIZE). Decodeer éénmet require_mask=Truealsrole"server"is enFalseals het"client"is, en geefmax_sizedoor. WerpValueError(1003, ...)op voor een frame met FIN uit of opcode 0. Geef{"type": "text", "data": str},{"type": "binary", "data": bytes},{"type": "ping", "data": bytes},{"type": "pong", "data": bytes}of{"type": "close", "code": int, "reason": str}terug. WerpValueError(1007, ...)op als een tekstpayload of een close-reden geenis. Laat een ConnectionErroruitread_exactongewijzigd door.
Druk op Run: de sectie van les 6 moet [{'type': 'text', 'data': 'Hello'}, {'type': 'ping', 'data': b''}, {'type': 'close', 'code': 1000, 'reason': ''}] afdrukken. Druk daarna op Submit. De controle kijkt alleen naar wat read_message teruggeeft en opwerpt, met de code als exc.args[0] en een str als reden in exc.args[1]. Ze draait onbekende gevallen uit les 1 tot en met 4 opnieuw, loopt de tabel op volgorde af met één frame per rij en noemt de eerste functie waarvan de weigering zonder de juiste code aankomt, en leest daarna elk event voor beide rollen in één keer, één byte en twee bytes per keer, twee frames direct achter elkaar, de weigeringen met hun codes (een te groot frame vóór elke leesactie van zijn payload), en een frame dat halverwege is afgebroken, dat ConnectionError moet opwerpen. Events mogen extra sleutels bevatten.