0%

Langsame Clients und verlorene Verbindungen · Übung

Eine tote Gegenseite bemerken

Zuschauer senden jetzt aus ihren eigenen Threads, und ein Zuschauer, der schließt, verlässt das Register innerhalb einer halben Sekunde. Eine Art von Zuschauer bleibt trotzdem für immer: einer, der weg ist, ohne es zu sagen. Ein Handy verlässt die WLAN-Reichweite, ein Laptopdeckel wird zugeklappt, ein Kabel wird gezogen. Kein Close- kommt an, und das recv des Servers gibt nie b"" zurück, um zu sagen, dass das andere Ende geschlossen hat, weil die Maschine, die es schließen würde, nicht mehr erreichbar ist. Ein send an diesen Zuschauer gelingt trotzdem, weil es, wie Lektion 1 gezeigt hat, nur Bytes in Puffer legt. Ein recv(timeout=0) im Leerlauf findet nichts Wartendes vor, und genau das findet es auch bei einem gesunden, stillen Zuschauer.

Die einzige Möglichkeit, eine tote Gegenseite von einer stillen zu unterscheiden, ist eine Frage, die eine Antwort braucht. hat eine: einen , den du nach Kapitel 4 mit einem Pong mit derselben Payload beantwortest. Eine lebende Gegenseite antwortet innerhalb von Augenblicken. Eine tote nie.

Drei Zahlen

Die Bibliothek sendet diese Pings für dich, nach dem eigenen Zeitplan der Verbindung, und drei ihrer Einstellungen entscheiden, wie lange ein toter Zuschauer bleiben kann:

  • ping_interval: wie oft sie einen Ping sendet;

  • ping_timeout: wie lange sie auf den Pong wartet;

  • close_timeout: wie lange sie nach dem Senden eines Close-Frames darauf wartet, dass die andere Seite das Schließen abschließt.

Wenn ping_timeout ohne Pong verstreicht, lässt die Bibliothek die Verbindung scheitern: Sie sendet einen Close-Frame mit dem Code 1011 und dem Grund keepalive ping timeout, schließt dann ihr eigenes Ende der Verbindung, sodass das recv der Gegenseite b"" zurückgeben würde, sobald sie so weit gelesen hat, und wartet dann bis zu close_timeout, dass die Seite der Gegenseite schließt. Eine tote Gegenseite schließt nie, also wartet die Bibliothek das ganze Close-Timeout ab. Erst dann lösen recv und send des Handlers eine aus. Das Bild zeigt das als Zeitleiste, die nach unten läuft, für einen Zuschauer, dessen letzter Pong am Anfang kam.

Ein Ping-Intervall nach dem letzten Pong geht ein Ping hinaus, ein Ping-Timeout später sendet die Bibliothek Close 1011 und schließt ihr Ende, und ein Close-Timeout danach löst der nächste Blick des Handlers ConnectionClosedError aus, und der Zuschauer verlässt das Register.

In Worten: Ein Ping-Intervall nach dem letzten Pong geht ein Ping hinaus; ein Ping-Timeout später sendet die Bibliothek Close 1011 und schließt ihr Ende; ein Close-Timeout später löst der nächste Blick des Handlers ConnectionClosedError aus, und der Zuschauer verlässt das Register.

Die Standardwerte sind 20, 20 und 10 Sekunden. Sie passen zum Internet, wo ein langsames Mobilfunknetz Sekunden für eine Antwort brauchen kann. Eine Vorführung auf einer Maschine machen sie sehr langsam, also übergibt dieses Kapitel für jeden 1,0 Sekunden: PING_INTERVAL, PING_TIMEOUT und CLOSE_TIMEOUT oben in scoreboard_server.py. In lokalen Tests wurde ein stiller Client mit 0,5 s für jeden etwa 1,5 s nach seinem letzten Pong getrennt und mit 1,0 s für jeden etwa 3 s danach. Mit den Ping-Werten von 0,5 s und dem Standard-Close-Timeout hat der Handler erst etwas gehört, als der stille Client nach 5 s endlich seinen eigenen schloss. Ein echter toter Client schließt seinen Socket nie, also würden die vollen 10 s vergehen.

Mit ping_interval=1.0, ping_timeout=1.0 und dem Standardwert close_timeout=10: Wie lange kann ein toter Zuschauer nach seinem letzten Pong ungefähr registriert bleiben?

Was der Handler sieht

Wenn die Bibliothek aufgibt, löst das nächste recv oder send des Handlers ConnectionClosedError aus. Lies es so, wie du es in Kapitel 6 gelernt hast: exc.sent ist der Close-Frame, den diese Seite gesendet hat, mit exc.sent.code 1011 und exc.sent.reason keepalive ping timeout, und exc.rcvd ist None, weil kein Close-Frame zurückkam. Dein describe_close aus Kapitel 6 liest rcvd, würde auf dem Server also 1006 melden: Es ist kein Close-Frame angekommen.

In viewer_handler gibt es nichts Neues zu schreiben. Seine nächste Leerlaufprüfung ruft recv(timeout=0) auf, das den ConnectionClosedError auslöst; except ConnectionClosed: pass beendet die still; der finally-Block entfernt den Zuschauer. Die Leerlaufprüfung aus Lektion 3 ist das, womit ein toter Zuschauer bemerkt wird.

Das Keepalive deines Servers gibt einen Zuschauer auf. Welchen Code enthält sent der Ausnahme?

Eine Zeile im Log

Wenn das Close-Timeout abläuft, schreibt die Bibliothek außerdem einen Block in die Standardfehlerausgabe: eine erste Zeile, keepalive ping failed, und einen . Gekürzt auf seine erste Zeile und seine Ausnahmezeilen hat ein lokaler Lauf ausgegeben:

keepalive ping failed
TimeoutError: timed out while closing connection
websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received

Das ist die Bibliothek, die eine tote Gegenseite meldet, kein Fehler in deinem Code. Das Run dieser Lektion verwendet einen Stellvertreter-Zuschauer, also erscheint der Block hier nicht. In Lektion 6 trifft pl_ws.py auf einen echten stillen Client und gibt den Block als eine Zeile aus.

Den Server an einer Stelle konfigurieren

open_server(board, *, serve=serve) erstellt den Bibliotheksserver für eine Tafel, mit diesen Werten, und gibt ihn zurück, ohne ihn zu starten. Sein Rumpf ist ein einziger Aufruf von serve, mit dem Handler aus make_handler(board), dem Host 127.0.0.1, 0 und den drei Werten als gleichnamige Schlüsselwortargumente. Port 0 lässt das System einen freien Port wählen, und 127.0.0.1 hält den Server auf dieser Maschine. Der serve ist ein reines Schlüsselwortargument, dessen Standardwert das serve der Bibliothek ist, dieselbe Art von Nahtstelle, die run_echo in Kapitel 6 hatte: Ein Aufrufer kann eine andere übergeben und sehen, was open_server verlangt hätte. pl_ws.py tut genau das, um deine drei Werte zu erfahren, bevor es seinen Server baut, und der Abschnitt zu Lektion 4 von Run tut es auch. Wer open_server aufruft, führt den Server mit serve_forever aus und stoppt ihn mit shutdown.

Deine Aufgabe

Schreibe in scoreboard_server.py open_server(board, *, serve=serve): Rufe serve mit make_handler(board), "127.0.0.1", 0 und den drei Schlüsselwortargumenten auf und gib zurück, was es zurückgibt.

Ausführen

Klicke auf Run. Der Abschnitt zu Lektion 4 ruft dein open_server mit einem eigenen serve auf, das die Schlüsselwortargumente aufzeichnet, gibt sie aus und gibt dann viewer_handler einen Stellvertreter-Zuschauer D, dessen Leerlaufprüfung die Ausnahme auslöst, die die Bibliothek nach einem verpassten Pong auslöst, als hätte ihn aufgegeben. Eine fertige Version hat auf der Seite ausgegeben:

Lesson 4: a viewer that stopped answering pings
    ping_interval = 1.0
    ping_timeout = 1.0
    close_timeout = 1.0
    after the library gave up on D, the registry holds: []

Klicke dann auf Submit.

Lektion 5 wechselt auf die andere Seite der Verbindung: was das Programm eines Zuschauers tun soll, wenn sein Server es trennt.

Aufgabe

Schreibe in scoreboard_server.py open_server(board, *, serve=serve). Rufe den serve einmal auf, mit make_handler(board) als Handler, "127.0.0.1" als Host und 0 als , und den Schlüsselwortargumenten ping_interval=PING_INTERVAL, ping_timeout=PING_TIMEOUT und close_timeout=CLOSE_TIMEOUT. Gib zurück, was serve zurückgibt.

Rufe weder serve_forever noch shutdown auf und verwende keinen with-Block: Das erledigt, wer den Server ausführt.

Klicke auf Run: Der Abschnitt zu Lektion 4 gibt ping_interval = 1.0, ping_timeout = 1.0, close_timeout = 1.0 und after the library gave up on D, the registry holds: [] aus. Die Standardfehlerausgabe bleibt leer: Der Stellvertreter-Zuschauer D löst die der Bibliothek ohne echte Verbindung aus, also erscheint in diesem Run keine Zeile keepalive ping failed.

Klicke dann auf Submit. Die Prüfung ruft open_server mit einem Stellvertreter-serve auf, das seine aufzeichnet und sich weigert, gestartet zu werden; vorher ersetzt sie das eigene serve des , sodass nichts einen Port öffnen kann. Sie erwartet den Handler aus make_handler(board) (sie ruft diesen Handler mit einem Stellvertreter-Zuschauer auf /watch auf und prüft, dass der Zuschauer dieser Tafel beitritt), "127.0.0.1" und Port 0, und ping_interval, ping_timeout und close_timeout jeweils als Zahl über 0 und höchstens 2.0. Dann gibt sie viewer_handler einen Zuschauer, dessen zweite Leerlaufprüfung den ConnectionClosedError auslöst, den die Bibliothek nach einem verpassten auslöst (1011 gesendet, nichts empfangen), und erwartet, dass der Handler still zurückkehrt und das Register leer ist. Die Schlüsselwortargumente dürfen in beliebiger Reihenfolge kommen, und unter 1.0 sind in Ordnung.