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.
De sluithandshake
RFC 6455 beschrijft hem in secties 1.4, 5.5.1 en 7.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 . De andere kant antwoordt met een eigen close-frame. Na het sturen van een close stuurt een eindpunt geen data meer.
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.

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:
het stuurt een gemaskeerde close:
client_frame(ws_frames.OP_CLOSE, ws_frames.close_payload(1000, "done"), random_bytes=random_bytes);het roept
wait_for_close(read_exact, "client")aan voor het antwoord van de server en bewaart de code ervan;het roept nog één keer
sock.recv(1)aan, datb""teruggeeft als de server TCP heeft gesloten;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 {"messages": n, "code": event["code"]} terug. Ze leest niets meer. Als ze terugkeert, sluit het with-blok rond de verbinding in de bedrading de
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 close_payload(1005) werpt 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 loopback_demo.py. Submit noemt de eerste nieuwe functie die nog NotImplementedError opwerpt.
close_frame(event, *, mask_key=None): de close die een close-event beantwoordt, met dezelfde code en reden, gebouwd metws_frames.close_payloadenws_frames.encode_frame(ws_frames.OP_CLOSE, ..., mask_key=mask_key). Antwoord bij code 1005 met een lege payload.wait_for_close(read_exact, role): lees events metws_frames.read_message(read_exact, role=role), laat alles vallen wat geen close is, en geef de code van de close terug.serve_conversation: beantwoord een close-event metclose_frame(event)en geef meteen{"messages": n, "code": event["code"]}terug. Stuur bijmax_messagesclose 1000 (reden “message limit reached”), roepwait_for_close(read_exact, "server")aan, en geef{"messages": n, "code": 1000}terug. Lees niets na de close van de client.client_conversation: stuur na de laatste echo een gemaskeerde close 1000 metclient_frame, haal de code van de server op uitwait_for_close(read_exact, "client"), roep nog één keersock.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 metclose_frame(event, mask_key=random_bytes(4)), roepsock.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 close_frame aan met de codes 1000, 1012 en 4000, een lege close, en een wait_for_close met serve_conversation stand-in-clients die sluiten na een 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.