0%

Ein Server, viele Clients · Übung

Beim Beitritt einen Snapshot senden

Die Umschläge aus Lektion 1 sind fertig, und kein Server sendet sie. Diese Lektion schreibt den ersten Teil dieses Servers: was passiert, wenn sich ein Zuschauer verbindet.

Was schon in scoreboard_server.py steht

Öffne scoreboard_server.py und lies die ersten drei ; sie sind vorgegeben.

make_board(teams) gibt die Tafel zurück, ein mit vier Schlüsseln: "version" (0), "scores" (jedes Team auf 0), "viewers" (ein leeres Dictionary, das Lektion 3 füllt) und "lock" (ein threading.Lock, das ebenfalls Lektion 3 einsetzt). Jede folgende Funktion bekommt diese Tafel als , also lebt nichts über das Spiel in einer globalen .

make_handler(board) gibt den Handler zurück, den die Bibliothek aufruft. In Kapitel 6 hast du serve eine Funktion übergeben, die ein Argument nimmt, die Verbindung. Der Handler der Anzeigetafel braucht außerdem die Tafel. Deshalb definiert make_handler in sich eine kleine Funktion, die die Verbindung nimmt und handle_connection(connection, board) aufruft, und gibt diese innere Funktion als zurück. Die innere Funktion merkt sich board, also kann ein einziger serve-Aufruf jeder Verbindung dieselbe Tafel geben.

handle_connection(connection, board) verteilt nach Pfad. Sie nimmt den Pfad aus connection.request.path, dem ganzen Anfrageziel samt Query, und kürzt ihn mit urlsplit auf den Pfadteil, wie in with Python. /watch geht an viewer_handler, /score geht an scorekeeper_handler (Lektion 6), und jeder andere Pfad wird sofort mit 1008 geschlossen.

Die Bibliothek ruft jeden Handler in einem eigenen Thread auf, einem pro Verbindung, wie du in Kapitel 6 gesehen hast. Mehrere viewer_handler-Aufrufe können also gleichzeitig mit derselben Tafel laufen. Das wird wichtig, sobald sich die Tafel ändert, und die Funktion, die du heute schreibst, ist dafür noch nicht sicher; in Lektion 3 ändert sich das.

Zuerst die ganze Tafel

Ein Zuschauer kann jederzeit beitreten: vor der ersten Frage oder mitten im Quiz. Würde der Server nur Änderungen senden, würde ein Zuschauer, der nach drei Wertungen beitritt, eine leere Tafel zeigen, bis die vierte Wertung kommt, und selbst dann kennte er nur die Änderung eines Teams. Deshalb ist die erste Nachricht, die jeder Zuschauer empfängt, ein Snapshot der ganzen Tafel, so wie sie gerade ist. Alles danach ist ein neuerer Snapshot.

Ein Zuschauer tritt nach drei Wertungen bei. Welche Nachricht empfängt er zuerst?

Eine Kopie der Punktestände

snapshot_message(board) baut diesen Umschlag: snapshot, die Version der Tafel und ihre Punktestände. Die Punktestände brauchen einen Schritt mehr, als du vielleicht erwartest. Wenn du board["scores"] selbst in den Snapshot steckst, hast du zwei Namen für ein Dictionary, und jede spätere Änderung an der Tafel würde durch den Snapshot hindurch sichtbar. Eine Kopie behält die Punkte, mit denen der Snapshot erstellt wurde:

Try it

same hat die Änderung gesehen; copy nicht. In diesem Kapitel wird jeder Snapshot gesendet, sobald er erstellt ist, also zeigt sich der Unterschied selten. In Kapitel 8 wartet ein Snapshot, bevor er gesendet wird, während sich die Tafel weiter ändert, und dort ist es die Kopie, die ihn richtig hält.

Warum kopiert snapshot_message board["scores"]?

Ein stiller Zuschauer ist ein normaler Zuschauer

Nach seinem Snapshot hat ein Zuschauer nichts zu sagen. Er schaut zu. Der Server muss den Handler trotzdem am Laufen halten, denn die Rückkehr aus dem Handler schließt die Verbindung, und er muss bemerken, wann der Zuschauer geht.

watch_until_closed(connection) erledigt das mit einer um connection.recv(timeout=WAIT). Drei Dinge können passieren:

  • eine Nachricht kommt an: Lies sie und verwirf sie, denn Zuschauer senden nichts, was die Anzeigetafel verwendet;

  • TimeoutError: 2 Sekunden lang ist nichts angekommen, und genau das tut ein Zuschauer, also geh noch eine Runde durch die Schleife;

  • ConnectionClosed: Der Zuschauer ist gegangen, also kehre zurück.

Hier unterscheidet sich ein Zuschauer vom Echo-Handler. Der Echo-Handler hat einen Client geschlossen, der 2 Sekunden lang nichts sagte, denn ein stiller Echo-Client wartet auf nichts. Ein stiller Zuschauer wartet auf die nächste Wertung. Einen Zuschauer beendet sein eigenes Schließen oder das Herunterfahren des Servers, das jede offene Verbindung mit 1001 schließt. Das Timeout bleibt trotzdem, damit jedes Warten ein Ende hat und die Schleife die Kontrolle behält.

recv(timeout=WAIT) löst im Handler eines Zuschauers TimeoutError aus. Was soll der Handler tun?

viewer_handler(connection, board) besteht dann aus zwei Zeilen: Sende scoreboard_messages.encode_message(snapshot_message(board)) und rufe watch_until_closed(connection) auf. Erreiche die Funktionen des anderen über seinen Namen, scoreboard_messages., wie es der vorgegebene Code tut.

Ausführen

Klicke auf Run. Der Abschnitt zu Lektion 2 übergibt viewer_handler eine Stellvertreter-Verbindung, die einmal still ist und dann schließt. Eine fertige Version hat auf der Seite ausgegeben:

Lesson 2: a viewer joins
    viewer A was sent [{'type': 'snapshot', 'version': 7, 'scores': {'Owls': 4, 'Foxes': 0}}]

Der Abschnitt zu Lektion 3 führt deinen Handler jetzt ebenfalls aus, und er endet nicht sauber:

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

Das Register bleibt leer, weil noch nichts einen Zuschauer registriert. Die letzte Zeile ist ein Zuschauer, dessen Verbindung schon weg war, als sein Snapshot gesendet wurde: send hat eine ausgelöst, und nichts hat sie abgefangen. Beides ist Aufgabe von Lektion 3. Klicke auf Submit.

Die nächste Lektion merkt sich jeden verbundenen Zuschauer, damit eine Wertung alle erreichen kann, und sorgt dafür, dass jeder diesen Eintrag wieder verlässt, egal wie seine Verbindung endet.

Aufgabe

Schreibe in scoreboard_server.py drei , in dieser Reihenfolge. Submit nennt die erste, die noch NotImplementedError auslöst.

  1. snapshot_message(board): Gib {"type": "snapshot", "version": board["version"], "scores": board["scores"].copy()} zurück. Ändere nichts an der Tafel.

  2. watch_until_closed(connection): Rufe in einer while True- connection.recv(timeout=WAIT) innerhalb von try auf. Verwirf, was es zurückgibt. Bei TimeoutError läuft die Schleife weiter. Bei ConnectionClosed kehrst du zurück.

  3. viewer_handler(connection, board): Sende scoreboard_messages.encode_message(snapshot_message(board)) und rufe dann watch_until_closed(connection) auf.

Klicke auf Run, lies den Abschnitt zu Lektion 2 und klicke dann auf Submit. Die Prüfung verwendet eine Tafel, die du nicht gesehen hast (Version 12, Owls 31 und Café Quiz 7), und eine Stellvertreter-Verbindung, die still ist, "hello from a viewer" sendet, wieder still ist und dann schließt. Sie erwartet genau eine Nachricht, gesendet vor dem ersten recv: den Snapshot. Sie erwartet, dass recv bis zum Schließen aufgerufen wird, jedes Mal mit einem Timeout über 0 und höchstens 2 Sekunden, dass auf die Nachricht des Zuschauers nichts zurückgesendet wird, dass der Handler ohne zurückkehrt und dass eine spätere Änderung an der Tafel den zurückgegebenen Snapshot unverändert lässt. Eine Verbindung, die ohne Close- endet, muss den Handler ebenfalls still beenden. dict(board["scores"]) ist so gut wie .copy(), und du kannst den Snapshot auch in viewer_handler bauen, wenn dir das lieber ist.