Kapitel 10 · Abschlussprojekt
Abschlussprojekt: Eine Live-Anzeigetafel
Abschlussprojekt: Den Vertrag der Anzeigetafel schreiben
Jeder Teil der Anzeigetafel existiert: Beitritts-Snapshots, zusammenfassende Outboxen, pl_ws.py aufruft, ein für diesen Kurs geschriebenes Programm, das jeden Client spielt. Niemand könnte sie aus einem
Das Projekt enthält schon die Infrastruktur, die dir der Kurs beigebracht hat: eine Platzprüfung und zwei neue Schlüsselwörter für open_server, zwei Kommandozeilen-Parser, drei neue Einträge auf der Tafel (last_version, open und finished), ein halb geschriebenes scoreboard_client.py, tests/test_client.py, eine README.md und eine Gliederung in SCOREBOARD_PROTOCOL.md. Der Vertrag kommt zuerst: Du schreibst auf, was beide Programme versprechen, und bringst dann den Server dazu, die eine Regel einzuhalten, die er hinzufügt.
Was der Vertrag sagen muss
SCOREBOARD_PROTOCOL.md ist für jemanden, der einen Client schreiben wird und deinen Code nie gesehen hat. Jede ihrer acht Überschriften beantwortet eine Frage, die diese Person stellen würde:
Paths: Wohin verbinde ich mich, was muss ein Punktezähler senden, um hereingelassen zu werden, und welche Antworten können mich vor dem Upgrade abweisen?
Messages: Was enthält jeder der drei Nachrichtentypen, und wie viele Punkte darf eine Wertung tragen?
Versions: Wer nummeriert die Tafeln, und was mache ich mit einer, die nicht neuer ist als die letzte, die ich gezeigt habe?
Joining: Was kommt zuerst an?
Closing: Welcher
beendet meine Verbindung, und wann? Bounds: Wie viele Verbindungen dürfen offen sein, wie viele Wertungen ergeben ein Quiz, und wie lange läuft ein Server?
Exit codes: Was bedeuten 0, 1 und 2 für jedes Programm?
Not in this version: Was darf ich nicht erwarten?
Alles darin ist etwas, das ein Client beobachten kann. Die Locks, Outboxen und Threads des Servers bleiben draußen, weil kein Client den Unterschied bemerken könnte.
Was davon gehört in SCOREBOARD_PROTOCOL.md?
Wie ein Quiz endet
make_board(teams, max_updates=10) speichert die Länge des Quiz als board["last_version"]. Das Quiz ist vorbei, sobald board["version"] sie erreicht, und dann sollte jeder Zuschauer den letzten Snapshot und ein Schließen mit 1000 empfangen.
Der naheliegende Weg ist, den ganzen Server mit Code 1000 herunterzufahren, sobald apply_score die letzte Version erzeugt. Aber shutdown() schließt jede offene Verbindung aus Threads, die es selbst startet, nicht aus dem eigenen Thread jedes Zuschauers, also kann ein Zuschauer, dessen Thread den letzten Snapshot noch nicht gesendet hat, geschlossen werden, bevor er ihn bekommt. Eine so geschriebene Version hat den Quizabend ausgeführt, den du später in Lektion 5 ausführst, auf einem lokalen Computer, nicht auf der Seite, mit drei Updates und einem absichtlich gebremsten Zuschauer S. In allen drei Läufen hat Zuschauer S nur Version 0 empfangen, wie in diesen Zeilen aus dem ersten:
run 1: viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
run 1: viewer S received versions [0], then the server closed it with 1000 'the quiz is over'
Zuschauer S hat nie eine Wertung gesehen.
Die Regel aus Kapitel 8 behebt das: Der eigene Thread jedes Zuschauers ist der einzige Thread, der an ihn sendet, und ein Schließen ist auch ein Senden. Also beendet der Handler jedes Zuschauers das Quiz für seinen eigenen Zuschauer: Er sendet einen Snapshot aus seiner Outbox; hat die Version dieses Snapshots board["last_version"] erreicht, ruft er connection.close(1000, "the quiz is over") auf und kehrt zurück; sein finally entfernt ihn aus dem Register.
Prüfe die Version des Snapshots, den du gerade gesendet hast, nicht board["version"]: Die Tafel kann die letzte Version erreichen, während ein älterer Snapshot noch unterwegs ist, und ein Schließen in diesem Moment würde den letzten überspringen. Das Zusammenfassen macht die Regel sicher, weil nichts den letzten Snapshot ersetzen kann. Ein Zuschauer, der nach dem Ende des Quiz beitritt, bekommt den letzten Snapshot als Beitritts-Snapshot und wird sofort geschlossen.
apply_score behält die andere Hälfte: Sobald die Version die letzte erreicht hat, löst es ValueError("the quiz is over") aus, bevor sich etwas ändert, und scorekeeper_handler aus Kapitel 9 macht daraus einen error-Umschlag.
Wer sendet im Bild jedes Schließen?

In Worten: Der Thread des Punktezählers bietet nur den letzten Snapshot an; der Thread jedes Zuschauers, auch der des gebremsten B, sendet ihn, schließt mit 1000 und geht; wer als Letzter geht, setzt finished, und erst dann stoppt der Server.
Warum schließt der eigene Handler jedes Zuschauers am Ende des Quiz seine Verbindung, statt dass der Server jede Verbindung auf einmal schließt?
Das Event finished
board["finished"] ist ein threading.Event, wie das Event jeder Outbox seit Kapitel 8. Dort hat ein Event den Thread eines Zuschauers geweckt; dieses sagt dem Programm, dass das Quiz vorbei ist und kein Zuschauer mehr auf seinen letzten Snapshot wartet. serve_scoreboard aus Lektion 2 wartet darauf, bevor es den Server stoppt.
Wer das Register nach der letzten Version leer zurücklässt, setzt es: meistens remove_viewer, wenn es den letzten Zuschauer entfernt; oder apply_score, nach seinem Broadcast, wenn überhaupt kein Zuschauer registriert ist, denn dann ruft niemand remove_viewer auf. Beide lesen die Version und das Register, während sie board["lock"] halten, also sind die beiden Tatsachen im selben Moment wahr. finished darf nie gesetzt werden, während ein Zuschauer registriert ist: Das wäre wieder der Wettlauf von oben.
Ein Zuschauer, dessen Netzwerkpfad abgestorben ist, geht erst, wenn Keepalive es bemerkt, etwa drei Sekunden später (Kapitel 8), also kann das Ende um so viel später kommen; die Frist aus Lektion 2 begrenzt das Warten.
Die letzte Wertung wird angewendet, während zwei Zuschauer registriert sind. Wann wird finished gesetzt?
Deine Aufgabe
Fülle SCOREBOARD_PROTOCOL.md mit deinen eigenen Worten aus und ändere dann apply_score, remove_viewer und viewer_handler so, dass der Server die Regel oben einhält. Jede Änderung besteht aus ein paar Zeilen um eine board["last_version"].
Führe im Terminal die Tests aus:
python -m pytest -q
echo $?
Sechs assert False-Tests scheitern bis zu den Lektionen 4 und 5, also gibt echo $?, der Exit-Status von pytest, 1 aus. Spiele dann ein kurzes Quiz:
python pl_ws.py last-update
Es baut deinen Server für ein Quiz mit zwei Updates, lässt die Zuschauer A und B beitreten (B absichtlich gebremst) und sendet drei Wertungen. Eine fertige Version hat auf der Seite ausgegeben:
scenario last-update: a quiz of two updates; viewers A and B join, B slowed down on purpose; the scorekeeper scores three times
server options from your open_server: ping_interval=1.0, ping_timeout=1.0, close_timeout=1.0
your open_server installs a request check, so the scorekeeper sends the token from token.txt
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
the scorekeeper sent 3 points for Owls; its reply was a snapshot with version 1
the scorekeeper sent 2 points for Foxes; its reply was a snapshot with version 2
the scorekeeper sent 5 points for Herons; its reply was an error with version 2: 'the quiz is over'
viewer A received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
viewer B received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
finished was set 1 time(s); the registry held 0 viewers the first time
finished was set only after both viewers had left: True
the server shut down
Vor deiner Änderung hat die dritte Wertung Version 3 erzeugt, und finished wurde nie gesetzt. Jetzt bekommt sie einen error-Umschlag bei Version 2, beide Zuschauer bekommen den letzten Snapshot und dann 1000, und finished wird einmal gesetzt, nachdem beide gegangen sind.
Der Server weiß jetzt, wie ein Quiz endet, aber er läuft immer noch nur innerhalb von pl_ws.py. Lektion 2 gibt ihm eine
Aufgabe
Behalte in SCOREBOARD_PROTOCOL.md jede Überschrift und ersetze jedes (fill in) durch eigene Sätze. Nenne unter Paths /watch und /score und die Status 401 und 503; unter Messages die snapshot, score und error und den Punktebereich, 1 bis 10; unter Closing die Codes 1000, 1001, 1008 und 1011; unter Bounds die Optionen --max-clients, --max-updates und --deadline mit ihren größten 0, 1 und 2 bedeuten.
In scoreboard_server.py:
apply_score: Löse innerhalb des Locks, bevor sich etwas ändert,ValueError("the quiz is over")aus, sobaldboard["version"]board["last_version"]erreicht hat. Rufe nach dem Broadcastboard["finished"].set()auf, wenn die Version jetzt die letzte erreicht hat undboard["viewers"]leer ist.remove_viewer: Setze nach dem Entfernen, während du die Tafel noch unter dem Lock liest,board["finished"]unter derselben. viewer_handler: Rufe nach dem Senden eines Snapshots, dessen eigene Versionboard["last_version"]erreicht hat,connection.close(1000, "the quiz is over")auf und kehre zurück. Behalte dasfinally, dasremove_vieweraufruft.
Lass jede andere
Führe python -m pytest -q aus (sechs assert False-Tests scheitern bis zu den Lektionen 4 und 5) und python pl_ws.py last-update. Klicke dann auf Check my work.
Check my work führt deinen Server mit Stellvertreter-Verbindungen und ohne Thread aus und ersetzt dabei scoreboard_server.threading durch eigene Locks und Events; behalte also import threading und erstelle Locks nur innerhalb von Funktionen. Die Prüfung führt einen Zuschauer einmal für jeden Moment aus, in dem ein anderer Thread laufen könnte, wobei dort eine und drei Wertungen ankommen: In einem Quiz, das noch lange nicht vorbei ist, muss der Zuschauer den Beitritts-Snapshot und dann nur neuere Tafeln bekommen, nie unter einem Lock gesendet; in einem Quiz, das bei diesen Wertungen endet, muss dem Zuschauer der letzte Snapshot gesendet werden, er muss dann einmal mit 1000 geschlossen werden, ohne dass nach dem Schließen etwas gesendet wird, und finished muss genau dann gesetzt werden, wenn das Quiz vorbei ist und kein Zuschauer registriert ist. Sie prüft einen Zuschauer, der nach dem Ende beitritt, einen gescheiterten, einen toten und einen abstürzenden Zuschauer, die Antworten an den Punktezähler (eine Wertung nach der letzten Version bekommt einen error-Umschlag und ändert nichts) und die Token-Prüfung aus Kapitel 9 mit einem Token, das du nicht gesehen hast. Aus SCOREBOARD_PROTOCOL.md liest sie nur die Überschriften, das Fehlen von (fill in) und die oben genannten Tatsachen; der Rest des Wortlauts ist deiner. Jeder Grund beim Schließen ist in Ordnung.