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 loopback_demo.py.
serve_clients(listener, results, *, max_clients=2, max_messages=MAX_MESSAGES, deadline=5.0, now=time.monotonic) ist die
Gib dem Listener mit
listener.settimeout(WAIT)ein Timeout.Berechne
stop_at = now() + deadline, einmal, vor der Schleife.Nimm an, solange weniger als
max_clientsErgebnisse existieren undnow() < stop_atgilt. WennacceptTimeoutErrorauslöst, ist kein Client rechtzeitig gekommen: Gib die bisherigen Ergebnisse zurück.Gib jedem angenommenen
sein eigenes settimeout(WAIT), schließe ihn mitwithund hängeserve_connection(conn, max_messages=max_messages)anresultsan.Gib
resultszurück.
Der Thread kann keinen main zurückreichen, also hängt er 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 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:
Lies die Optionen.
Öffne den Listener mit
socket.create_server(("127.0.0.1", 0))in einemwith-Block, lies denmit getsockname()[1]und giblistening on 127.0.0.1:PORTmit dem echten Port aus.Starte
serve_clientsin einemthreading.Threadund übergib den Listener und eine Ergebnisliste alsargsundmax_clients,max_messagesunddeadlinealskwargs. Die Grenze des Servers istMAX_MESSAGES, nicht--messages: Die Grenze gehört dem Server, nicht dem, was diedarüber sagt, was Clients senden werden. --messagesentscheidet, wie viele Texte jeder Client sendet.Verbinde dich für jeden Client im Haupt-Thread mit
socket.create_connection(("127.0.0.1", port), timeout=WAIT)in einemwith-Block, führeclient_connectionmit Texten wie"client 1, message 2"aus und gibclient N: E echoes, closed with Caus. Wenn der ClientOSErroroderauslöst, gib aus, dass er gescheitert ist, und mach mit dem nächsten weiter. Führe den join des Server-Threads mit
timeout=WAITaus, innerhalb deswith-Blocks des Listeners, damit der Listener nicht unter einem Thread geschlossen wird, der vielleicht noch annimmt.Gib für jedes Ergebnis
server: client N sent M messages, closed with Caus.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
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
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 loopback_demo.py:
serve_clients(listener, results, *, max_clients=2, max_messages=MAX_MESSAGES, deadline=5.0, now=time.monotonic): Rufelistener.settimeout(WAIT)auf, berechne einmalstop_at = now() + deadlineund nimm an, solangelen(results) < max_clientsundnow() < stop_atgelten. Gib jedem angenommenensettimeout(WAIT), schließe ihn mitwithund hängeserve_connection(conn, max_messages=max_messages)anresultsan. WennacceptTimeoutErrorauslöst, gib die bisherigen Ergebnisse zurück. Rufe die Uhr nur übernowauf.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) mitargparse; lehne einenaußerhalb seiner Grenzen mit parser.error(...)ab.main(argv=None): Lies die Optionen; bindesocket.create_server(("127.0.0.1", 0))in einemwith-Block und giblistening on 127.0.0.1:PORTaus; führeserve_clientsin einemthreading.Threadmitmax_clients=args.clients,max_messages=MAX_MESSAGESunddeadline=args.deadlineaus; verbinde aus dem Haupt-Thread jeden Client mitsocket.create_connection(..., timeout=WAIT)und führeclient_connectionmit--messagesTexten aus, wobei du pro Client eine Zeile ausgibst; führe den join des Threads mittimeout=WAITaus, 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 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.