0%

Die Bibliothek websockets, ohne asyncio · Abschlussprojekt

Abschlussprojekt: Projekt: Der Echo-Dienst, neu gebaut

Du hast einen Handler, einen Client und eine Möglichkeit zu lesen, wie ein Gespräch geendet hat, jedes für sich geschrieben und geprüft. Was du noch nicht hast, ist ein Programm, das ein Mensch ausführen könnte. In Kapitel 5 war dieses Programm loopback_demo.py mit seiner : ein begrenzter Server, Clients nacheinander, eine Zeile pro Client und ein Exit-Code. Dieses Projekt baut dasselbe Programm auf der Bibliothek, in echo_library.py, mit drei weiteren .

Was du baust

echo_session(url, texts, *, connect=connect) ist echo_once, das zusätzlich meldet, wie das Gespräch geendet hat. Verbinde dich mit denselben drei Grenzen und proxy=None. Setze im with-Block alles in ein einziges try: Sende jeden Text und lies sein Echo, wie zuvor; schließe dann mit connection.close(1000, "done") und rufe noch einmal connection.recv(timeout=WAIT) auf. Sobald der Schließ- vorbei ist, löst dieses recv ConnectionClosed aus, und except ConnectionClosed as exc: gibt {"echoes": echoes, "close": describe_close(exc)} zurück.

Die in das try zu setzen ist für das andere Ende wichtig. Der Server, den du geschrieben hast, schließt zuerst, wenn er MAX_MESSAGES erreicht, und ein Server kann verschwinden. In beiden Fällen löst das nächste recv oder send ConnectionClosed aus, und dasselbe except gibt die Echos, die angekommen sind, mit dem Close zurück, der sie beendet hat: 1000 und message limit reached für einen Server an seiner Grenze, 1006 für einen, der verschwunden ist.

parse_args(argv) liest dieselben drei Optionen wie das Projekt aus Kapitel 5, mit denselben Grenzen, und lehnt alles außerhalb davon mit parser.error ab, was das Programm mit dem Status 2 stoppt:

  • --clients, ein int von 1 bis MAX_CLIENTS (5), Standardwert 2;

  • --messages, ein int von 1 bis MAX_MESSAGES (10), Standardwert 3;

  • --deadline, ein float über 0 und höchstens 8 Sekunden, Standardwert 5.0.

main(argv=None, *, serve=serve, connect=connect, now=time.monotonic) setzt alles zusammen:

  1. Lies die Optionen mit parse_args(argv) und berechne, wann die Frist abläuft: now() plus --deadline;

  2. bediene echo_handler auf "127.0.0.1" 0 in einer with-Anweisung, gib den Port aus und starte serve_forever in einem Thread, wie in run_echo;

  3. für jeden Client, nummeriert ab 1: Wenn now() die Frist erreicht hat, gib aus, dass der Client nicht gestartet wurde, und zähle ihn als gescheitert; gib ihm andernfalls --messages Texte deiner Wahl und führe echo_session mit der ws://127.0.0.1:PORT/echo und connect=connect aus;

  4. wenn echo_session OSError auslöst, gib aus, dass der Client gescheitert ist, zähle ihn als gescheitert und mach mit dem nächsten Client weiter; ConnectionRefusedError und TimeoutError sind beide Arten von OSError;

  5. gib andernfalls eine Zeile für den Client aus, etwa wie viele Echos er bekommen hat und den , und zähle ihn nur dann als Erfolg, wenn er jedes Echo und einen Close mit 1000 bekommen hat;

  6. führe nach dem with-Block den join des Threads mit timeout=WAIT aus und gib 0 zurück, wenn jeder Client erfolgreich war, 1, wenn einer es nicht war.

Der Wortlaut deiner Zeilen ist dir überlassen, ebenso wie du zählst: eine von Ergebnissen, ein Zähler für Erfolge oder etwas anderes, das den richtigen Exit-Code ergibt.

Wieder Nahtstellen

serve= und connect= sind aus demselben Grund , aus dem connect= es in Networks with Python war: damit ein Aufrufer Stellvertreter übergeben kann. Submit übergibt ein serve, dessen Server aufzeichnet, was du damit machst, und nichts bedient, und ein connect, das vorgegebene Verbindungen ausgibt: eine, die Echos sendet, eine, deren Server nach einem Echo schließt, eine, deren Server deinen Close mit 1001 beantwortet, eine, die in ein Timeout läuft, und eine, die abgelehnt wird. main muss sein connect an echo_session weitergeben; ohne connect=connect in diesem Aufruf würde echo_session seinen Standardwert verwenden, das echte connect der Bibliothek.

now= ist die injizierte Uhr aus Python in der Praxis. Submit übergibt eine, die über die Frist springt, sobald sich der erste Client verbunden hat, und erwartet, dass kein zweiter Client startet.

Das Herunterfahren des Servers braucht keinen eigenen Code. Weil der Server in einer with-Anweisung verwendet wird, ruft das Verlassen des Blocks auf jedem Weg shutdown() auf: nach dem letzten Client, nach einem Client, der gescheitert ist, und sogar, wenn eine , die dein Code nicht abgefangen hat, den Block verlassen sollte.

echo_session löst für den zweiten Client TimeoutError aus. Fährt der Server trotzdem herunter?

Run

Klicke auf Run. Der letzte Abschnitt von library_demo.py ruft dein main mit --clients 2 --messages 2 auf und gibt aus, was es zurückgibt. Eine fertige Version, auf der Website ausgeführt, hat Folgendes ausgegeben, wobei PORT für den Port steht, den das System gewählt hat:

Lesson 6: main with two clients
listening on 127.0.0.1:PORT
client 1: 2 echoes, closed with 1000
client 2: 2 echoes, closed with 1000
0

Deine Zeilen lauten so, wie du sie geschrieben hast; die 0 ist der Exit-Code, den main zurückgegeben hat, und er sollte 0 sein. Die Standardfehlerausgabe sollte leer sein, wie in Lektion 4. Auf deinem eigenen Computer, mit der Bibliothek aus deiner requirements.txt installiert, führt python echo_library.py --clients 3 dasselbe Programm aus einem aus, über die letzten beiden Zeilen der Datei.

Klicke dann auf Submit. Bevor es die drei neuen Funktionen prüft, prüft es die früheren erneut: den Handler, run_echo, echo_once und describe_close.

Wenn dich etwas an main verwirrt, frag Monty:

Warum übergibt mein main connect=connect an echo_session, wenn echo_session connect schon als Standardwert hat?

Immer noch einer nach dem anderen

Sieh dir an, was main mit dem Server der Bibliothek macht: ein Client, dann der nächste. Der Server würde problemlos mehrere Clients gleichzeitig bedienen, jeden in seinem eigenen Handler-Thread, aber dieses Programm verlangt das nie. Es gibt auch nichts, was Clients teilen könnten: Jeder redet nur mit seinem eigenen Echo. Kapitel 7 ändert beides. Es bedient mehrere Clients gleichzeitig und gibt ihnen etwas Gemeinsames, die Anzeigetafel. Eine Anzeigetafel, die sauber anhalten muss, während noch Zuschauer verbunden sind, braucht mehr als die Art dieses Programms, zwischen Clients anzuhalten, etwa offene Verbindungen mit 1001 zu schließen, wenn ihre Frist kommt; das Abschlussprojekt in Kapitel 10 kümmert sich darum.

Aufgabe

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

  1. echo_session(url, texts, *, connect=connect): Verbinde dich wie echo_once, mit open_timeout=WAIT, close_timeout=WAIT und proxy=None, in einer with-Anweisung. Sende in einem einzigen try jeden Text und lies sein Echo mit recv(timeout=WAIT), rufe dann connection.close(1000, "done") und noch einmal connection.recv(timeout=WAIT) auf. Gib in except ConnectionClosed as exc: {"echoes": echoes, "close": describe_close(exc)} zurück.

  2. parse_args(argv): --clients (int, 1 bis MAX_CLIENTS, Standardwert 2), --messages (int, 1 bis MAX_MESSAGES, Standardwert 3) und --deadline (float, über 0 und höchstens 8, Standardwert 5.0), mit parser.error für einen außerhalb seiner Grenzen.

  3. main(argv=None, *, serve=serve, connect=connect, now=time.monotonic): Lies die Optionen; setze die Frist anhand von now(); bediene echo_handler auf "127.0.0.1" 0 in einer with-Anweisung und starte serve_forever in einem threading.Thread; führe pro Client ein echo_session aus, einen nach dem anderen, mit --messages Texten und connect=connect, und starte keinen Client mehr, sobald now() die Frist erreicht hat; zähle einen Client, der OSError auslöst, weniger Echos bekommt, als er gesendet hat, oder einen anderen Close als 1000 bekommt, als gescheitert; gib pro Client eine Zeile aus; führe nach dem Block den join des Threads mit timeout=WAIT aus; gib 0 zurück, wenn jeder Client erfolgreich war, und sonst 1.

Klicke auf Run: Der Abschnitt zu Lektion 6 sollte nach einer Zeile pro Client mit 0 enden.

Klicke dann auf Submit. Die Prüfung öffnet nie einen und startet keinen Thread. Sie prüft zuerst erneut echo_handler, run_echo, echo_once und describe_close, wie es die Lektionen 2 bis 4 getan haben. Sie lässt echo_session gegen Stellvertreter-Verbindungen laufen: einen Echo-Server (sie erwartet close(1000, "done") und {"echoes": [...], "close": {"code": 1000, "reason": "done"}}), einen Server, der nach einem Echo schließt, und einen Server, der verschwindet (1006), mit denselben Grenzen für connect und recv wie in Lektion 3. Sie lässt parse_args mit den Standardwerten, den größten erlaubten Werten und Werten knapp jenseits jeder Grenze laufen. Sie lässt main mit einem Stellvertreter-serve, einem Stellvertreter-connect und einem aufzeichnenden threading- laufen: serve(echo_handler, "127.0.0.1", 0), ein Thread auf serve_forever, gestartet vor dem ersten Client, eine Verbindung pro Client zu dem Port, den der Server gemeldet hat, je --messages Texte, das Herunterfahren nach den Clients und ein join mit Timeout nach dem Herunterfahren, und 0 für zwei gute Clients. Sie erwartet 1, bei trotzdem heruntergefahrenem Server, für einen Client mit einem von zwei Echos, einen mit 1001 beantworteten Close, eine abgelehnte Verbindung und ein recv, das in ein Timeout läuft; keinen zweiten Client, sobald ihre Uhr die Frist überschreitet; und den Status 2 für --clients 9. Dein ausgegebener Wortlaut wird nicht geprüft.