0%

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 ; die zijn meegeleverd.

make_board(teams) geeft het bord terug, een met vier sleutels: "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 , dus niets van het spel staat in een globale .

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 . De binnenste functie onthoudt 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 with Python. /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:

Try it

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 rond 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 via de naam ervan, 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 op, en niets ving die. Beide zijn werk voor les 3. Klik op Submit.

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 , in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt.

  1. snapshot_message(board): geef {"type": "snapshot", "version": board["version"], "scores": board["scores"].copy()} terug. Verander niets aan het bord.

  2. watch_until_closed(connection): roep in een while True- connection.recv(timeout=WAIT) aan binnen try. Gooi weg wat het teruggeeft. Bij TimeoutError lus je door. Bij ConnectionClosed keer je terug.

  3. viewer_handler(connection, board): stuur scoreboard_messages.encode_message(snapshot_message(board)) en roep daarna watch_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 op te werpen, en dat een latere verandering van het bord de teruggegeven snapshot laat zoals hij was. Een verbinding die eindigt zonder close- moet de handler ook stil laten eindigen. dict(board["scores"]) is net zo goed als .copy(), en je mag de snapshot ook binnen viewer_handler bouwen als je dat liever doet.