Eén server, veel clients · oefening
Stuur een snapshot bij binnenkomst
De enveloppen uit les 1 staan klaar, en geen server verstuurt ze. Deze les schrijft het eerste deel van die server: wat er gebeurt als een kijker verbinding maakt.
Wat er al in scoreboard_server.py staat
Open scoreboard_server.py en lees de eerste drie
make_board(teams) geeft het bord terug, een "version" (0), "scores" (elk team op 0), "viewers" (een lege dict, die les 3 vult) en "lock" (een threading.Lock, die les 3 ook aan het werk zet). Elke functie die volgt, krijgt dit bord als
make_handler(board) geeft de handler terug die de bibliotheek aanroept. In hoofdstuk 6 gaf je serve een functie met één argument, de verbinding. De handler van het scorebord heeft ook het bord nodig, dus make_handler definieert in zichzelf een kleine functie die de verbinding aanneemt en handle_connection(connection, board) aanroept, en geeft die binnenste functie terug als board, dus één aanroep van serve kan elke verbinding hetzelfde bord geven.
handle_connection(connection, board) stuurt door op basis van het pad. Hij haalt het pad uit connection.request.path, dat het hele request-target is, query inbegrepen, en knipt het met urlsplit terug tot het paddeel, zoals in /watch gaat naar viewer_handler, /score gaat naar scorekeeper_handler (les 6), en elk ander pad wordt meteen gesloten met 1008.
De bibliotheek roept elke handler aan op zijn eigen thread, één per verbinding, zoals je in hoofdstuk 6 zag. Er kunnen dus meerdere aanroepen van viewer_handler tegelijk tegen één bord draaien. Dat doet ertoe zodra het bord verandert, en de functie die je vandaag schrijft, is daar nog niet veilig voor; in les 3 verandert dat.
Eerst het hele bord
Een kijker kan op elk moment binnenkomen: vóór de eerste vraag, of halverwege de quiz. Als de server alleen veranderingen stuurde, zou een kijker die na drie scores binnenkwam een leeg bord tonen tot de vierde score aankwam, en zelfs dan zou hij alleen de verandering van één team kennen. Daarom is het eerste bericht dat elke kijker ontvangt een snapshot van het hele bord zoals het nu is. Alles daarna is een nieuwere snapshot.
Een kijker komt binnen na drie scores. Wat is het eerste bericht dat hij ontvangt?
Een kopie van de scores
snapshot_message(board) bouwt die envelop: type snapshot, de versie van het bord en zijn scores. De scores hebben één stap meer nodig dan je misschien verwacht. Als je board["scores"] zelf in de snapshot zet, krijg je twee namen voor één dict, en elke latere verandering van het bord zou door de snapshot heen zichtbaar zijn. Een kopie houdt de punten vast waarmee de snapshot gemaakt is:
same zag de verandering; copy niet. In dit hoofdstuk wordt elke snapshot verstuurd zodra hij gemaakt is, dus het verschil valt zelden op. In hoofdstuk 8 wacht een snapshot voordat hij verstuurd wordt, terwijl het bord blijft veranderen, en daar is het de kopie die hem juist houdt.
Waarom kopieert snapshot_message board["scores"]?
Een stille kijker is een normale kijker
Na zijn snapshot heeft een kijker niets te zeggen. Hij kijkt. De server moet de handler toch laten doorlopen, want terugkeren uit de handler sluit de verbinding, en hij moet merken wanneer de kijker vertrekt.
watch_until_closed(connection) doet dat met een connection.recv(timeout=WAIT). Er kunnen drie dingen gebeuren:
er komt een bericht binnen: lees het en gooi het weg, want kijkers sturen niets wat het scorebord gebruikt;
TimeoutError: er kwam 2 seconden niets binnen, en dat is precies wat een kijker doet, dus ga de lus opnieuw rond;ConnectionClosed: de kijker is vertrokken, dus keer terug.
Hier verschilt een kijker van de echohandler. De echohandler sloot een client die 2 seconden niets zei, omdat een echoclient die stil is nergens op wacht. Een kijker die stil is, wacht op de volgende score. Wat een kijker beëindigt, is zijn eigen close, of de server die afsluit en daarbij elke open verbinding sluit met 1001. De time-out is er nog steeds zodat elke wachttijd een einde heeft en de lus de controle houdt.
recv(timeout=WAIT) werpt TimeoutError op in de handler van een kijker. Wat moet de handler doen?
viewer_handler(connection, board) bestaat dan uit twee regels: stuur scoreboard_messages.encode_message(snapshot_message(board)), en roep watch_until_closed(connection) aan. Bereik de functies van de andere scoreboard_messages., zoals de meegeleverde code doet.
Uitvoeren
Klik op Run. De sectie van les 2 geeft viewer_handler een stand-in-verbinding die één keer stil is en dan sluit. Een afgemaakte versie, op de site uitgevoerd, drukte af:
Lesson 2: a viewer joins
viewer A was sent [{'type': 'snapshot', 'version': 7, 'scores': {'Owls': 4, 'Foxes': 0}}]
De sectie van les 3 draait nu ook je handler, en die eindigt niet netjes:
Lesson 3: the registry
while A is connected, the registry holds: []
after A's handler returned, the registry holds: []
ConnectionClosedError: no close frame received or sent
Het register blijft leeg omdat nog niets een kijker registreert. De laatste regel is een kijker wiens verbinding al weg was toen zijn snapshot werd verstuurd: send wierp een
De volgende les legt elke verbonden kijker vast, zodat een score ze allemaal kan bereiken, en zorgt ervoor dat elke kijker uit dat overzicht verdwijnt, hoe zijn verbinding ook eindigt.
Opdracht
Schrijf in scoreboard_server.py drie NotImplementedError opwerpt.
snapshot_message(board): geef{"type": "snapshot", "version": board["version"], "scores": board["scores"].copy()}terug. Verander niets aan het bord.watch_until_closed(connection): roep in eenwhile True-connection.recv(timeout=WAIT)aan binnentry. Gooi weg wat het teruggeeft. BijTimeoutErrorlus je door. BijConnectionClosedkeer je terug.viewer_handler(connection, board): stuurscoreboard_messages.encode_message(snapshot_message(board))en roep daarnawatch_until_closed(connection)aan.
Klik op Run en lees de sectie van les 2, en klik daarna op Submit. De controle gebruikt een bord dat je nog niet hebt gezien (versie 12, Owls 31 en Café Quiz 7) en een stand-in-verbinding die stil is, "hello from a viewer" stuurt, weer stil is en dan sluit. Ze verwacht precies één bericht, verstuurd vóór de eerste recv: de snapshot. Ze verwacht dat recv wordt aangeroepen tot de close, elke keer met een time-out boven 0 en hoogstens 2 seconden, dat er niets wordt teruggestuurd op het bericht van de kijker, dat de handler terugkeert zonder een dict(board["scores"]) is net zo goed als .copy(), en je mag de snapshot ook binnen viewer_handler bouwen als je dat liever doet.