0%

Hoofdstuk 5 · oefening

Een gesprek op loopback

Berichten uitwisselen na de handshake

Je uit hoofdstuk 2 schakelt een verbinding om naar , en je codec uit hoofdstuk 3 en 4 zet berichten om in en terug. Elk ervan werkt, en elk ervan heeft tot nu toe alleen gedraaid. In dit hoofdstuk delen ze één verbinding: een server en een client in één Python-programma op 127.0.0.1, die berichten uitwisselen totdat een van beide zegt dat het gesprek voorbij is, met elke wachttijd begrensd.

De projectboom is veranderd. loopback_demo.py is nieuw, en het is het bestand dat je het hele hoofdstuk aanpast. ws_handshake.py en ws_frames.py zijn de afgewerkte versies van de cursus van de handshake uit hoofdstuk 2 en de codec uit hoofdstuk 4, zodat dit hoofdstuk begint vanuit bekende bestanden, hoe je eigen code er ook uitzag; niets in dit hoofdstuk past ze aan. De demo uit hoofdstuk 4, fakes.py en de frametests hebben hun werk gedaan en zijn weg.

Wat er al geschreven is

Open loopback_demo.py en lees de meegeleverde vóór de lege.

  • make_read_exact(reader) geeft een read_exact(n) terug over een echte . Het is de ontvangstbuffer uit Networks with Python achter de naad (seam) die decode_frame sinds hoofdstuk 3 aanneemt: hij begint met de bytes die de handshake in reader["buffer"] achterliet, roept recv aan totdat hij n bytes heeft, en werpt ConnectionError op als de peer eerst sluit.

  • serve_connection(conn) draait de handshake van de server en daarna serve_conversation. client_connection(sock, host, path, texts) draait de handshake van de client en daarna client_conversation.

  • run_once(talk) is dezelfde bedrading die je in het project van hoofdstuk 2 schreef: een listener op 127.0.0.1 0 met een time-out van 2 seconden, de server op een thread, de client in de hoofdthread met een eigen time-out van 2 seconden, en een join met een time-out. Het drukt af wat elke kant teruggaf, of de die haar stopte, in plaats van die op te werpen.

main draait run_once twee keer, één keer met een nette client en één keer met een onbeschofte, en alleen als je het bestand draait, vanwege de regel if __name__ == "__main__": onderaan. Les 5 vervangt deze bedrading voor één gesprek door je eigen bedrading die meerdere clients bedient.

De helft van de server

ws_frames.read_message(read_exact, role="server") leest één frame en geeft het terug als event, een kleine zoals {"type": "text", "data": "hello"} of {"type": "ping", "data": b"p?"}. Met role="server" eist het dat elk frame gemaskeerd is, zoals elk clientframe moet zijn.

De regel van de server is één event erin, hooguit één frame eruit, en server_reply(event) bevat die regel:

  • tekst komt terug als tekst en binair als binair, met dezelfde data, niet gemaskeerd, omdat een server nooit maskeert;

  • een krijgt een pong met dezelfde payload, niet gemaskeerd;

  • een pong krijgt niets: geef b"" terug.

serve_conversation(sock, read_exact, *, max_messages=MAX_MESSAGES) draait een : lees een event, stuur server_reply(event) met sock.sendall, en tel het als het tekst of binair was. b"" sturen stuurt niets, dus het geval van de pong heeft geen eigen tak nodig. Zodra er max_messages berichten zijn geëchood, stop je en geef je {"messages": n, "code": None} terug. De code blijft voorlopig None, omdat niets in deze les een gesprek met een code beëindigt; les 2 vult haar in.

De limiet telt berichten, geen frames. Een ping is de peer die vraagt “ben je er nog?”, en die beantwoorden hoort bij het gezond houden van de verbinding, niet bij de inhoud van het gesprek.

max_messages is 3. Een client stuurt een ping en drie teksten. Hoeveel echo's stuurt de server?

De helft van de client

Elk frame dat een client stuurt, wordt gemaskeerd met een nieuwe sleutel van 4 bytes. client_frame(opcode, payload, *, random_bytes=os.urandom) bouwt er een: vraag random_bytes(4) om een sleutel en geef ws_frames.encode_frame(opcode, payload, mask_key=key) terug. In een echt programma is random_bytes os.urandom; een test kan in plaats daarvan een functie meegeven die bekende bytes teruggeeft, dezelfde keyword-only naad die client_handshake voor zijn sleutel gebruikt.

De sleutel moet voor elk frame nieuw zijn. Hoofdstuk 4 liet zien waarom maskeren bestaat: een script in een browser mag de exacte bytes die het netwerk oversteken niet kunnen kiezen, omdat een proxy of cache op het pad bytes die op lijken voor het echte werk zou kunnen aanzien. RFC 6455 sectie 5.3 vraagt om die reden om een nieuwe, onvoorspelbare sleutel per frame. Een sleutel die voor elk frame opnieuw wordt gebruikt, is na het eerste frame voorspelbaar.

Waarom vraagt client_frame voor elk frame om een nieuwe sleutel?

client_conversation(sock, read_exact, texts, *, random_bytes=os.urandom) praat met de server:

  1. Stuur een gemaskeerde ping met een payload naar keuze, zoals b"are you there?", en lees het antwoord met ws_frames.read_message(read_exact, role="client").

  2. Als het antwoord geen pong met diezelfde payload is, werp dan ValueError(1002, "the server did not answer the ping") op, dezelfde vorm met twee die de codec gebruikt voor een gebroken regel.

  3. Stuur elke tekst met client_frame(ws_frames.OP_TEXT, text, random_bytes=random_bytes), lees de echo, en bewaar de "data" ervan.

  4. Geef {"echoes": [...], "code": None} terug.

Geef random_bytes door aan elke aanroep van client_frame, zodat de ene functie die de aanroeper je gaf elke sleutel levert.

Run, en een einde dat er geen is

Druk op Run. Dit is de uitvoer na deze les:

A conversation:
client: {'echoes': ['hello', 'café ✓', 'third message'], 'code': None}
server: stopped by ConnectionError: the peer closed the connection
server thread still running: False
A rude client:
client: the server closed the connection without a close frame
server: stopped by ValueError: (1002, 'the frame breaks the masking rule')
server thread still running: False

De client kreeg alle drie de echo’s, het vinkje in café ✓ inbegrepen. De regel “stopped by” van de server is het verwachte resultaat van deze les, geen fout. De client was klaar met zijn teksten en verliet zijn with-blok, dat zijn socket sloot. De server, nog in zijn lus, riep recv aan, kreeg b"", en read_exact wierp ConnectionError op. Vanaf de kant van de server zien een client die klaar was en een client die crashte er precies hetzelfde uit.

De onbeschofte client stuurt één niet-gemaskeerd tekstframe. De codec weigert het met ValueError(1002, ...), de exception beëindigt de thread van de server, en de client ziet zijn verbinding zonder een woord sluiten. Les 3 laat de server zo’n client fatsoenlijk antwoorden.

Run drukt server: stopped by ConnectionError: the peer closed the connection af. Wat is er gebeurd?

Als het eerste gesprek drie echo’s afdrukt, druk je op Submit. Het controleert je vier functies met stand-in-sockets, nooit een echte, en laat daarna je client en je server via die sockets met elkaar praten.

Het gesprek werkt, maar het heeft geen einde: de ene kant hangt op en de andere moet raden waarom. Les 2 geeft WebSocket-gesprekken hun echte einde, de sluithandshake.

Opdracht

Schrijf vier in loopback_demo.py, in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt, dus je krijgt ze één voor één.

  1. server_reply(event): geef voor een tekst- of binair event ws_frames.encode_frame terug met dezelfde en data en zonder ; voor een een pong met dezelfde payload en zonder masker; voor een pong b"".

  2. client_frame(opcode, payload, *, random_bytes=os.urandom): geef ws_frames.encode_frame(opcode, payload, mask_key=random_bytes(4)) terug, één nieuwe sleutel per aanroep.

  3. serve_conversation(sock, read_exact, *, max_messages=MAX_MESSAGES): lees events met ws_frames.read_message(read_exact, role="server"), stuur server_reply(event) met sock.sendall, tel alleen tekst- en binaire berichten, en geef, zodra er max_messages zijn geëchood, {"messages": n, "code": None} terug zonder opnieuw te lezen.

  4. client_conversation(sock, read_exact, texts, *, random_bytes=os.urandom): stuur een ping met een payload naar keuze, lees het antwoord met role="client", en werp ValueError(1002, "the server did not answer the ping") op tenzij het een pong met dezelfde payload is; stuur daarna elke tekst en lees de echo ervan vóór de volgende; geef {"echoes": [...], "code": None} terug. Bouw elk met client_frame(..., random_bytes=random_bytes).

Druk op Run: het eerste gesprek moet drie echo’s afdrukken, en de regel “stopped by ConnectionError” van de server is na deze les te verwachten.

Druk daarna op Submit. De controle opent nooit een . Ze roept server_reply en client_frame rechtstreeks aan. Ze geeft serve_conversation een stand-in-socket waarvan de client een gemaskeerde ping, een tekst en een binair bericht stuurt, of een tekst, een pong, een lege ping en een binair bericht, in één keer afgeleverd, één byte per keer en twee bytes per keer, en dan stil wordt zonder te sluiten, dus een server die voorbij zijn limiet leest, wacht eeuwig en faalt. Ze geeft client_conversation een stand-in-server die antwoordt op wat je client stuurt, controleert dat elk frame gemaskeerd is met zijn eigen sleutel uit random_bytes, en één keer je ping met de verkeerde payload beantwoordt. Tot slot praat je client met een correcte stand-in-server, en de bytes die hij stuurde, worden aan je serve_connection gegeven, waarvan de antwoorden byte voor byte moeten overeenkomen met die van die server. Het importeren van loopback_demo.py mag niets afdrukken.