0%

Eén server, veel clients · oefening

Broadcast elke verandering

Het register vindt elke verbonden kijker, en het bord verandert nog steeds nooit. Deze les schrijft de verandering: apply_score telt de punten van een score op bij het bord, en broadcast stuurt het nieuwe bord naar elke kijker.

Eén verandering, één houding

apply_score(board, message) ontvangt een score-envelop die parse_message al heeft gecontroleerd, dus het team is een en de punten liggen tussen 1 en 10. Binnen één with board["lock"]:-blok doet hij vier dingen, in volgorde:

  1. controleer het team: als message["team"] geen sleutel van board["scores"] is, werp dan ValueError("unknown team") op voordat er iets verandert;

  2. tel de punten op bij de score van dat team;

  3. tel 1 op bij board["version"];

  4. bouw de nieuwe snapshot met snapshot_message(board), geef hem aan broadcast(board, snapshot) en geef hem terug.

De is voor les 6, waar de handler van de scorebijhouder elke score beantwoordt met de nieuwe snapshot. Het doet ertoe dat je de snapshot teruggeeft die binnen de houding is gemaakt: als de aanroeper het bord opnieuw las nadat de lock was vrijgegeven, kon een andere score het al veranderd hebben, en dan kreeg de scorebijhouder te horen over een bord dat zijn score niet had opgeleverd. De teamcontrole staat hier en niet in check_message, omdat alleen het bord weet welke teams meespelen.

Alle vier stappen delen één houding, om de reden uit les 3. Het binnenkomen neemt dezelfde lock, dus een kijker komt binnen ofwel vóór de verandering, en is dan geregistreerd om haar te ontvangen, ofwel erna, en dan bevat zijn snapshot haar al.

Eén keer coderen, vaak sturen

broadcast(board, snapshot) wordt door apply_score aangeroepen terwijl de lock wordt vastgehouden, dus hij mag de lock niet zelf nemen: een thread die een lock neemt die hij al vasthoudt, wacht eeuwig op zichzelf. Hij codeert de snapshot één keer met scoreboard_messages.encode_message, en stuurt daarna diezelfde tekst naar elke kijker in board["viewers"]. Elke kijker krijgt dezelfde bytes, dus er is geen reden om ze voor elke kijker opnieuw te coderen. Het coderen controleert ook de envelop, één keer, voordat er iets naar wie dan ook wordt verstuurd.

Een kijker die weg is

Een kijker kan op elk moment verdwijnen: een dichtgeklapte laptop, een weggevallen netwerk. Het register weet dat pas als iets de verbinding probeert te gebruiken. Dan werpt send ConnectionClosed op, die je in hoofdstuk 6 tegenkwam. Eén verdwenen kijker mag de anderen hun update niet kosten, dus vang de rond elke send en ga door naar de volgende kijker.

De verdwenen kijker moet ook het register verlaten. De voor de hand liggende regel, hem binnen de verwijderen, mislukt, want een mag niet van grootte veranderen terwijl een for-lus erdoorheen loopt:

Try it

Verzamel dus de id’s van mislukte kijkers tijdens de lus in een , en verwijder ze erna. Als je met .items() door het register loopt, krijg je elke id en zijn verbinding samen:

failed = []
for viewer_id, viewer in board["viewers"].items():
    try:
        viewer.send(text)
    except ConnectionClosed:
        failed.append(viewer_id)

Elke verbinding opzoeken met board["viewers"][viewer_id] werkt net zo goed. Als de eigen handler van die kijker later zijn finally-blok bereikt, vindt zijn remove_viewer de id niet meer, en pop met een standaardwaarde maakt dat onschadelijk:

Wat drukt dit af?

d = {"a": 1}
print(d.pop("b", None), d.pop("a", None), d)

In broadcast kwamen de id’s een moment eerder uit het register, dus daar is een gewone pop(viewer_id) prima.

Wat gebeurt er met de handler van de verdwenen kijker? Zijn thread zit nog in watch_until_closed. Zijn volgende recv werpt ook ConnectionClosed op, de handler vertrekt via except en finally, en remove_viewer vindt niets om te verwijderen. Twee threads proberen allebei de kijker te verwijderen, en welke als tweede komt, doet niets.

Er zijn drie kijkers geregistreerd en de send van de tweede werpt ConnectionClosedError op. Wat doet broadcast?

Volgorde gratis, voorlopig

Elke send naar een kijker gebeurt nu terwijl een of andere thread de lock van het bord vasthoudt: de snapshot bij binnenkomst binnen add_viewer, en elke update binnen apply_score. Er houdt maar één thread tegelijk de lock vast, dus de sends naar één kijker gebeuren na elkaar, in de volgorde waarin het bord veranderde. Een kijker kan nooit versie 7 toegestuurd krijgen vóór versie 6.

Waarom kan een kijker in de server van dit hoofdstuk nooit versie 7 ontvangen vóór versie 6?

Die volgorde heeft een prijs. Zolang de send naar één kijker bezig is, wordt de lock vastgehouden, dus elke andere kijker, de volgende score en elk nieuw binnenkomen wachten erop. Sturen onder een lock is een bewuste vereenvoudiging voor dit hoofdstuk, en zo hoort een drukke server niet te werken. Hoofdstuk 8 meet wat het kost en haalt het weg.

Uitvoeren

Klik op Run. De sectie van les 4 registreert drie kijkers, laat de send van B mislukken en verwerkt één score. Een afgemaakte versie, op de site uitgevoerd, drukte af:

Lesson 4: one score, three viewers, one of them gone
    viewer A received versions [0, 1]
    viewer B received versions [0]
    viewer C received versions [0, 1]
    registry after the update: ['A', 'C']

Elke kijker ontving zijn snapshot bij binnenkomst, versie 0. A en ontvingen daarna versie 1. De send van B mislukte, dus B hield alleen zijn snapshot bij binnenkomst en verliet het register, en C hoorde de update toch. Klik daarna op Submit.

Binnenkomen, register en broadcast werken nu allemaal, en alleen de demo en Submit hebben ze geprobeerd. In de volgende les schrijf je eigen tests die elk ervan bewijzen.

Opdracht

Schrijf in scoreboard_server.py twee , in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt.

  1. broadcast(board, snapshot): de aanroeper houdt de lock van het bord al vast, dus neem hier geen lock. Codeer de snapshot één keer met scoreboard_messages.encode_message, stuur de tekst naar elke verbinding in board["viewers"], en vang ConnectionClosed rond elke send, waarbij je de id van die kijker aan een toevoegt. Verwijder na de elke id uit de lijst uit board["viewers"].

  2. apply_score(board, message): werp binnen één with board["lock"]:-blok ValueError("unknown team") op als message["team"] niet op het bord staat, voordat je iets verandert; tel anders message["points"] op bij de score van dat team, tel 1 op bij board["version"], maak de nieuwe snapshot met snapshot_message(board), roep broadcast(board, snapshot) aan en geef de snapshot terug.

Klik op Run en lees de sectie van les 4, en klik daarna op Submit. De controle houdt de lock van het bord in de gaten en legt elke schrijfactie naar de versie en de scores van het bord vast. Ze registreert vier stand-in-kijkers, van wie de tweede weg is, en verwerkt een score die je nog niet hebt gezien: ze verwacht de nieuwe snapshot terug, dat elk van de andere drie kijkers hem precies één keer ontvangt, en dat het register die drie in hun volgorde houdt. Twee scores achter elkaar moeten elke kijker bereiken als versie 1 en 2. Een score voor Bats moet opwerpen en het bord, de kijkers en de lock laten zoals ze waren. Elke verandering van het bord en elke send moet gebeuren terwijl de lock wordt vastgehouden, en de lock moet daarna vrij zijn. Een bord zonder kijkers moet toch veranderen, en broadcast, los aangeroepen terwijl de lock wordt vastgehouden, moet elke kijker bereiken die niet weg is. Itereren over board["viewers"].copy(), Exception vangen in plaats van ConnectionClosed, en per kijker één keer coderen worden allemaal geaccepteerd.