0%

Ein Gespräch auf Loopback · Abschlussprojekt

Abschlussprojekt: Projekt: Der handgebaute Echo-Dienst

Bisher hat main über run_once, die Verdrahtung, die das Kapitel mitgeliefert hat, ein Gespräch nach dem anderen ausgeführt. In diesem Projekt schreibst du die Verdrahtung selbst und machst aus loopback_demo.py einen Befehl: python loopback_demo.py --clients 2 --messages 3 startet deinen Server auf 127.0.0.1, bedient die Clients nacheinander innerhalb einer Nachrichtengrenze und einer Frist, lässt jeden mit 1000 schließen, gibt pro Client und pro Server-Ergebnis eine Zeile aus und endet mit 0. Es ist das letzte Stück des Kurses, das aus nichts als der Standardbibliothek gebaut wird.

Was du baust

Drei in loopback_demo.py.

serve_clients(listener, results, *, max_clients=2, max_messages=MAX_MESSAGES, deadline=5.0, now=time.monotonic) ist die des Servers, und sie läuft in einem Thread:

  1. Gib dem Listener mit listener.settimeout(WAIT) ein Timeout.

  2. Berechne stop_at = now() + deadline, einmal, vor der Schleife.

  3. Nimm an, solange weniger als max_clients Ergebnisse existieren und now() < stop_at gilt. Wenn accept TimeoutError auslöst, ist kein Client rechtzeitig gekommen: Gib die bisherigen Ergebnisse zurück.

  4. Gib jedem angenommenen sein eigenes settimeout(WAIT), schließe ihn mit with und hänge serve_connection(conn, max_messages=max_messages) an results an.

  5. Gib results zurück.

Der Thread kann keinen an main zurückreichen, also hängt er an die an, die main übergeben hat, dieselbe Idee wie das outcome in run_once.

parse_args(argv) liest drei Optionen mit argparse: --clients, ein int von 1 bis 5, Standardwert 2; --messages, ein int von 1 bis 10, Standardwert 3; --deadline, ein float über 0 und höchstens 8, Standardwert 5.0. Ein außerhalb seiner Grenzen geht an parser.error(...), das die Usage-Zeile und die Meldung auf die Standardfehlerausgabe schreibt und mit dem Status 2 endet.

main(argv=None) setzt alles zusammen:

  1. Lies die Optionen.

  2. Öffne den Listener mit socket.create_server(("127.0.0.1", 0)) in einem with-Block, lies den mit getsockname()[1] und gib listening on 127.0.0.1:PORT mit dem echten Port aus.

  3. Starte serve_clients in einem threading.Thread und übergib den Listener und eine Ergebnisliste als args und max_clients, max_messages und deadline als kwargs. Die Grenze des Servers ist MAX_MESSAGES, nicht --messages: Die Grenze gehört dem Server, nicht dem, was die darüber sagt, was Clients senden werden. --messages entscheidet, wie viele Texte jeder Client sendet.

  4. Verbinde dich für jeden Client im Haupt-Thread mit socket.create_connection(("127.0.0.1", port), timeout=WAIT) in einem with-Block, führe client_connection mit Texten wie "client 1, message 2" aus und gib client N: E echoes, closed with C aus. Wenn der Client OSError oder auslöst, gib aus, dass er gescheitert ist, und mach mit dem nächsten weiter.

  5. Führe den join des Server-Threads mit timeout=WAIT aus, innerhalb des with-Blocks des Listeners, damit der Listener nicht unter einem Thread geschlossen wird, der vielleicht noch annimmt.

  6. Gib für jedes Ergebnis server: client N sent M messages, closed with C aus.

  7. Gib 0 zurück, wenn jeder Client mit 1000 geschlossen hat und der Thread angehalten hat, sonst 1. Die letzte Zeile der Datei, raise SystemExit(main()), macht daraus den Exit-Code des Befehls.

Zeit als Parameter

serve_clients ruft time.monotonic() nie selbst auf. Es ruft now() auf, dessen Standardwert time.monotonic ist, die injizierte Uhr aus Python in der Praxis. Ein echter Lauf bekommt die echte Uhr. Eine Prüfung kann eine Funktion übergeben, die 0.0 und dann 6.0 zurückgibt, und sehen, ob die Frist die Schleife stoppt, ohne sechs Sekunden zu warten.

Die Frist wird zwischen Clients geprüft, nicht während eines Gesprächs. Ein laufendes Gespräch hat bereits eigene Grenzen: ein Timeout von 2 Sekunden auf jedem Lesevorgang und eine Grenze für Nachrichten. Die Frist entscheidet nur, ob ein weiterer Client angenommen wird.

Die Frist läuft ab, während der zweite Client noch redet. Was tut serve_clients?

Ausführen

Diese Lektion hat ein . Wenn deine drei Funktionen geschrieben sind, führe den Befehl mit seinen Standardwerten aus:

python loopback_demo.py

Er gibt Folgendes aus, mit einer Portnummer an der Stelle von PORT:

listening on 127.0.0.1:PORT
client 1: 3 echoes, closed with 1000
client 2: 3 echoes, closed with 1000
server: client 1 sent 3 messages, closed with 1000
server: client 2 sent 3 messages, closed with 1000

Probiere den größten Lauf, den die Optionen erlauben, fünf Clients mit je zehn Nachrichten; er gibt für jeden der fünf eine Client-Zeile und eine Server-Zeile aus, alle mit 1000 geschlossen:

python loopback_demo.py --clients 5 --messages 10

Dann ein Wert außerhalb der Grenzen:

python loopback_demo.py --clients 6
echo $?
usage: loopback_demo.py [-h] [--clients CLIENTS] [--messages MESSAGES]
                        [--deadline DEADLINE]
loopback_demo.py: error: --clients must be 1 to 5

Das geht auf die Standardfehlerausgabe, und echo $? gibt den Exit-Code aus, 2. Deine Tests aus Lektion 4 bestehen weiterhin:

python -m pytest -q

Wenn der Lauf mit Standardwerten endet und beide Clients mit 1000 geschlossen haben, klicke auf Check my work. Es führt die Prüfungen der Lektionen 1 bis 3 erneut auf deiner fertigen Datei aus. Es gibt serve_clients einen Stellvertreter-Listener mit wartenden Clients und eine Uhr, die es steuert, und prüft die Client-Grenze, die Frist, das Timeout bei accept, beide Timeouts, dass jeder angenommene Socket geschlossen wird und dass ein Client, der verschwindet, den nächsten nicht aufhält. Es ruft parse_args mit Werten innerhalb und außerhalb der Grenzen auf. main öffnet einen echten Listener, einen Thread und Clients, also liest die Prüfung stattdessen seinen Code: die Adresse ("127.0.0.1", 0), einen Thread und ein Timeout bei jedem join und bei create_connection. Deine ausgegebenen Zeilen werden nicht geprüft, also lies sie selbst.

Woraus der Code besteht

Zähl nach, was dieser Dienst gebraucht hat. In den Versionen des Kurses der drei Dateien kommen ws_handshake.py, ws_frames.py und loopback_demo.py auf ungefähr 620 Zeilen. Der Teil, der ihn zu einem Echo-Dienst macht, jede Nachricht zurückzusenden, sind ein paar Zeilen von server_reply. Fast alles andere ist Infrastruktur, die jedes -Programm braucht und keines schreiben möchte: der , die Längenformen, das Maskieren mit einem neuen Schlüssel, das Beantworten von , der Schließ-Handshake in beide Richtungen, Fehlercodes und die Threads und Timeouts, die alles begrenzt halten.

Genau darum ging es, als du es einmal selbst gebaut hast. Kapitel 6 übergibt diese ganze Infrastruktur an die Bibliothek websockets und baut diesen Dienst in ein paar Zeilen neu, und es beginnt damit, den eigenen Client der Bibliothek auf die Version des Kurses von dem Server zu richten, den du gerade fertiggestellt hast.

Aufgabe

Schreibe drei in loopback_demo.py:

  1. serve_clients(listener, results, *, max_clients=2, max_messages=MAX_MESSAGES, deadline=5.0, now=time.monotonic): Rufe listener.settimeout(WAIT) auf, berechne einmal stop_at = now() + deadline und nimm an, solange len(results) < max_clients und now() < stop_at gelten. Gib jedem angenommenen settimeout(WAIT), schließe ihn mit with und hänge serve_connection(conn, max_messages=max_messages) an results an. Wenn accept TimeoutError auslöst, gib die bisherigen Ergebnisse zurück. Rufe die Uhr nur über now auf.

  2. parse_args(argv): --clients (int, 1 bis 5, Standardwert 2), --messages (int, 1 bis 10, Standardwert 3) und --deadline (float, über 0 und höchstens 8, Standardwert 5.0) mit argparse; lehne einen außerhalb seiner Grenzen mit parser.error(...) ab.

  3. main(argv=None): Lies die Optionen; binde socket.create_server(("127.0.0.1", 0)) in einem with-Block und gib listening on 127.0.0.1:PORT aus; führe serve_clients in einem threading.Thread mit max_clients=args.clients, max_messages=MAX_MESSAGES und deadline=args.deadline aus; verbinde aus dem Haupt-Thread jeden Client mit socket.create_connection(..., timeout=WAIT) und führe client_connection mit --messages Texten aus, wobei du pro Client eine Zeile ausgibst; führe den join des Threads mit timeout=WAIT aus, bevor der Listener geschlossen wird; gib pro Server-Ergebnis eine Zeile aus; gib nur dann 0 zurück, wenn jeder Client mit 1000 geschlossen hat und der Thread angehalten hat.

Im sollte python loopback_demo.py zwei Client-Zeilen und zwei Server-Zeilen ausgeben, alle mit 1000 geschlossen, und python loopback_demo.py --clients 6 sollte mit dem Status 2 enden.

Klicke dann auf Check my work. Es wiederholt die Prüfungen der Lektionen 1 bis 3 auf deiner fertigen Datei. Es gibt serve_clients einen Stellvertreter-Listener mit vorgegebenen Clients und eine Uhr, die es steuert: drei wartende Clients mit max_clients=2, eine Uhr, die nach dem ersten Client die Frist überschreitet, ein Listener, dessen accept in ein Timeout läuft, ein Client, der vor dem nächsten verschwindet, und max_messages=1; es prüft die Timeouts auf dem Listener und auf jedem angenommenen Socket und dass jeder angenommene Socket geschlossen wird. Es ruft parse_args mit den Standardwerten, den größten erlaubten Werten und Werten knapp außerhalb jeder Grenze auf. Es liest den Code von main und den Funktionen, die es verwendet, auf die Adresse ("127.0.0.1", 0), einen Thread und ein Timeout bei jedem join und bei create_connection, und es scheitert, wenn irgendwo in der Datei "0.0.0.0" steht. Es führt main nicht aus, liest dein Terminal nicht und prüft deine ausgegebenen Zeilen nicht.