0%

Een gesprek op loopback · oefening

Sluiten in beide richtingen

Na les 1 eindigt elk gesprek met één kant die ophangt. De server kan een client die klaar was niet onderscheiden van een client waarvan het programma crashte, en geen van beide kanten kan zeggen waarom ze stopte. heeft hier een echt einde voor, de sluithandshake, en die is gebouwd uit de close- die je in hoofdstuk 4 codeerde.

De sluithandshake

RFC 6455 beschrijft hem in secties 1.4, 5.5.1 en 7.1.1:

  1. Eén kant, welke ook, stuurt een close-frame: 8, een code van 2 bytes zoals 1000 (“normal closure”, normale afsluiting), en een optionele reden in .

  2. De andere kant antwoordt met een eigen close-frame. Na het sturen van een close stuurt een eindpunt geen data meer.

  3. Zodra beide closes zijn uitgewisseld, sluit in het normale geval de server de als eerste, en wacht de client daarop.

De afbeelding toont beide volgordes, de client eerst bovenaan en de server eerst daaronder. Kijk in elke helft welke kant de TCP-verbinding sluit, en wat de laatste recv van de client teruggeeft.

Welke kant ook de eerste close stuurt, de andere antwoordt met een close, daarna sluit de server de TCP-verbinding, en de recv van de client geeft een lege bytes-waarde terug; een tekst die al onderweg was naar de server toen die sloot, wordt gelezen en weggegooid.

In woorden, de bovenste helft: de client stuurt een gemaskeerde close met code 1000 en de reden “done”; de server antwoordt met een niet-gemaskeerde close met dezelfde code en reden, en sluit daarna de TCP-verbinding; de volgende recv van de client geeft b"" terug, en pas dan is het gesprek voorbij.

De RFC zegt dat de beantwoordende close de code die hij ontving “typically” (doorgaans) terugstuurt. Deze cursus stuurt de code en de reden terug, en dat is precies wat de bibliotheek websockets die je in hoofdstuk 6 tegenkomt ook doet.

Waarom de server TCP als eerste sluit: RFC 6455 sectie 7.1.1 vraagt erom zodat de server, niet de client, de toestand TIME_WAIT vasthoudt, een korte periode waarin de kant die als eerste sloot nog een registratie van de afgelopen verbinding bijhoudt. Die ene zin over TCP is alles wat deze les nodig heeft.

Beide close-frames zijn uitgewisseld. Wie sluit in het normale geval de TCP-verbinding als eerste?

Twee hulpfuncties

close_frame(event, *, mask_key=None) bouwt het antwoord op een close-event. read_message meldt een close als {"type": "close", "code": ..., "reason": ...}, dus het antwoord is ws_frames.close_payload(event["code"], event["reason"]) in een close-frame, gemaskeerd als er een mask_key wordt gegeven, omdat het antwoord van een client gemaskeerd moet zijn zoals elk clientframe. Een close 1000 “bye” wordt beantwoord met deze bytes:

88 05 03 e8 62 79 65

88 is een laatste close-frame, 05 de payloadlengte, 03 e8 de code 1000, en de laatste drie bytes zijn “bye”.

wait_for_close(read_exact, role) leest events met ws_frames.read_message(read_exact, role=role) en laat elk ervan vallen totdat er een close aankomt, en geeft dan de code ervan terug. Zo wacht een kant die haar close al heeft gestuurd op het antwoord: al het andere dat aankomt, is data die de peer stuurde voordat hij de close zag, en wordt gelezen en weggegooid.

De client eerst

Dit is de volgorde in het nette gesprek van Run. Na de laatste echo doet client_conversation het volgende:

  1. het stuurt een gemaskeerde close: client_frame(ws_frames.OP_CLOSE, ws_frames.close_payload(1000, "done"), random_bytes=random_bytes);

  2. het roept wait_for_close(read_exact, "client") aan voor het antwoord van de server en bewaart de code ervan;

  3. het roept nog één keer sock.recv(1) aan, dat b"" teruggeeft als de server TCP heeft gesloten;

  4. het geeft de echo’s en die code terug.

Stap 3 gebruikt sock.recv rechtstreeks, niet read_exact, omdat b"" hier het verwachte einde is, geen fout.

Aan de kant van de server controleert serve_conversation elk event voordat het antwoordt: een close wordt beantwoord met sock.sendall(close_frame(event)), en de geeft meteen {"messages": n, "code": event["code"]} terug. Ze leest niets meer. Als ze terugkeert, sluit het with-blok rond de verbinding in de bedrading de , en dat is de server die TCP als eerste sluit.

De server eerst

De server begint de sluithandshake als hij zijn limiet bereikt, zoals in de onderste helft van de afbeelding. Na de echo die max_messages volmaakt, stuurt hij close 1000 met de reden “message limit reached”, roept hij wait_for_close(read_exact, "server") aan, en geeft hij {"messages": n, "code": 1000} terug.

Een client kan nog een tekst hebben gestuurd voordat de close van de server hem bereikte. wait_for_close leest die tekst en laat hem vallen, omdat een server die zijn close heeft gestuurd niets meer stuurt.

De client moet hier ook klaar voor zijn. Terwijl hij op een echo wacht, kan er in plaats daarvan een close aankomen. Dan antwoordt hij met close_frame(event, mask_key=random_bytes(4)), roept hij sock.recv(1) aan voor het einde van de verbinding, en geeft hij de echo’s die hij heeft terug, met de code van de close.

De server heeft bij de limiet zijn close gestuurd, en er komt een tekstframe van de client aan vóór de close van de client. Wat doet de server?

De code die niemand stuurt

Een close-frame mag een lege payload hebben, 88 00 van een server. parse_close_payload meldt dat als code 1005, die RFC 6455 reserveert als label voor “er was geen aanwezig”. Je codec uit hoofdstuk 4 weigert 1005 in een frame te zetten: close_payload(1005) werpt op. Daarom controleert close_frame eerst op 1005 en beantwoordt het een lege close met een lege close.

Het close-frame van een client heeft een lege payload. Wat draagt de close van de server?

Run

Druk op Run. De uitvoer na deze les:

A conversation:
client: {'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}
server: {'messages': 3, 'code': 1000}
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

Beide kanten melden nu 1000: het gesprek eindigde omdat beide het erover eens waren dat het voorbij was. De onbeschofte client is onveranderd; die is voor les 3.

Druk daarna op Submit. Het draait close_frame en wait_for_close apart, geeft serve_conversation stand-in-clients die op elk van de manieren hierboven sluiten, draait client_conversation tegen een stand-in-server die zijn close beantwoordt en een die als eerste sluit, en laat je client en je server samen een gesprek afsluiten.

Een nette peer krijgt nu een schoon einde. Een peer die de regels breekt, doodt de thread van de server nog steeds met een exception, en de volgende les laat de server hem antwoorden met een eigen close.

Opdracht

Schrijf twee en breid er twee uit in loopback_demo.py. Submit noemt de eerste nieuwe functie die nog NotImplementedError opwerpt.

  1. close_frame(event, *, mask_key=None): de close die een close-event beantwoordt, met dezelfde code en reden, gebouwd met ws_frames.close_payload en ws_frames.encode_frame(ws_frames.OP_CLOSE, ..., mask_key=mask_key). Antwoord bij code 1005 met een lege payload.

  2. wait_for_close(read_exact, role): lees events met ws_frames.read_message(read_exact, role=role), laat alles vallen wat geen close is, en geef de code van de close terug.

  3. serve_conversation: beantwoord een close-event met close_frame(event) en geef meteen {"messages": n, "code": event["code"]} terug. Stuur bij max_messages close 1000 (reden “message limit reached”), roep wait_for_close(read_exact, "server") aan, en geef {"messages": n, "code": 1000} terug. Lees niets na de close van de client.

  4. client_conversation: stuur na de laatste echo een gemaskeerde close 1000 met client_frame, haal de code van de server op uit wait_for_close(read_exact, "client"), roep nog één keer sock.recv(1) aan voor het einde van de verbinding, en geef de echo’s met die code terug. Als er een close aankomt in plaats van een echo, beantwoord die dan met close_frame(event, mask_key=random_bytes(4)), roep sock.recv(1) aan, en geef de echo’s tot dan toe terug met de code van de close.

Druk op Run: beide kanten van het eerste gesprek moeten 1000 melden.

Druk daarna op Submit. De controle opent nooit een . Ze roept close_frame aan met de codes 1000, 1012 en 4000, een lege close, en een , en wait_for_close met vóór de close. Ze geeft serve_conversation stand-in-clients die sluiten na een , een tekst en een binair bericht, een lege close sturen, meteen sluiten met een code en reden die je nog niet hebt gezien, of blijven sturen na de eigen close van de server bij max_messages=2, elk in één keer afgeleverd, één byte per keer en twee bytes per keer, en daarna stil zonder te sluiten, dus een server die voorbij de close van de client leest, wacht eeuwig en faalt. Ze draait client_conversation tegen een stand-in-server die je close beantwoordt, en een die na één echo als eerste sluit, en controleert dat je client leest totdat de server de verbinding sluit. Tot slot praten je client en je server via stand-ins met elkaar, één keer waarbij de client sluit en één keer waarbij de server als eerste sluit, en ze moeten byte voor byte overeenkomen.