Hoofdstuk 5 · oefening
Een gesprek op loopback
Berichten uitwisselen na de handshake
Je 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
make_read_exact(reader)geeft eenread_exact(n)terug over een echte. Het is de ontvangstbuffer uit Networks with Python achter de naad (seam) die decode_framesinds hoofdstuk 3 aanneemt: hij begint met de bytes die de handshake inreader["buffer"]achterliet, roeptrecvaan totdat hijnbytes heeft, en werptConnectionErrorop als de peer eerst sluit.serve_connection(conn)draait de handshake van de server en daarnaserve_conversation.client_connection(sock, host, path, texts)draait de handshake van de client en daarnaclient_conversation.run_once(talk)is dezelfde bedrading die je in het project van hoofdstuk 2 schreef: een listener op127.0.0.10 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 {"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 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
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:
Stuur een gemaskeerde ping met een payload naar keuze, zoals
b"are you there?", en lees het antwoord metws_frames.read_message(read_exact, role="client").Als het antwoord geen pong met diezelfde payload is, werp dan
ValueError(1002, "the server did not answer the ping")op, dezelfde vorm met tweedie de codec gebruikt voor een gebroken regel. Stuur elke tekst met
client_frame(ws_frames.OP_TEXT, text, random_bytes=random_bytes), lees de echo, en bewaar de"data"ervan.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 loopback_demo.py, in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt, dus je krijgt ze één voor één.
server_reply(event): geef voor een tekst- of binair eventws_frames.encode_frameterug met dezelfdeen data en zonder ; voor een een pong met dezelfde payload en zonder masker; voor een pong b"".client_frame(opcode, payload, *, random_bytes=os.urandom): geefws_frames.encode_frame(opcode, payload, mask_key=random_bytes(4))terug, één nieuwe sleutel per aanroep.serve_conversation(sock, read_exact, *, max_messages=MAX_MESSAGES): lees events metws_frames.read_message(read_exact, role="server"), stuurserver_reply(event)metsock.sendall, tel alleen tekst- en binaire berichten, en geef, zodra ermax_messageszijn geëchood,{"messages": n, "code": None}terug zonder opnieuw te lezen.client_conversation(sock, read_exact, texts, *, random_bytes=os.urandom): stuur een ping met een payload naar keuze, lees het antwoord metrole="client", en werpValueError(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 elkmet 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 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.