Die Bibliothek websockets, ohne asyncio · Übung
Ein Server in ein paar Zeilen
interop.py hat den Client der Bibliothek auf den Server aus Kapitel 5 gerichtet. Diese Lektion dreht das um: Der Server wird zu dem der Bibliothek. In Kapitel 5 hat serve_conversation serve_handshake das Upgrade beantwortet hatte. Mit der Bibliothek passt der Teil, bei dem es um das Echo geht, in einen Handler von etwa sechs Zeilen.
Ein Handler und seine Verbindung
Ein Handler ist eine
connection.recv(timeout=...)wartet höchstens so viele Sekunden auf die nächste Nachricht und gibt sie zurück: einenstrfür eine Textnachricht,bytesfür eine binäre. Wenn nichts rechtzeitig ankommt, löst esTimeoutErroraus.connection.send(message)sendet eine Nachricht: einenstrals Text,bytesals binär.connection.close(code, reason)sendet einen Close-Frame mit diesem Code und Grund und führt den Schließ-Handshake aus.connection.request.pathist der Pfad, nach dem der Client gefragt hat, etwa/echo. Spätere Kapitel verwenden ihn; dieses nicht.
Alles andere erledigt die Bibliothek. Für jeden Client, der sich verbindet, führt sie den Handshake aus und ruft dann deinen Handler in einem eigenen Thread auf: das Muster mit einem Thread pro Verbindung aus Networks with Python, Kapitel 9. Wenn der Handler zurückkehrt, schließt die Bibliothek die Verbindung mit 1000, falls der Handler sie nicht schon geschlossen hat.
Drei Clients verbinden sich gleichzeitig. Wie oft ruft die Bibliothek echo_handler auf?
echo_handler(connection) ist die Echo-MAX_MESSAGES Nachrichten als Echo zurückgesendet wurden, lies eine mit connection.recv(timeout=WAIT) und sende sie mit connection.send direkt zurück. Wenn du den server_reply mit connection.close(1000, "message limit reached"). WAIT und MAX_MESSAGES stehen bereits oben in echo_library.py: 2 Sekunden und 10 Nachrichten.
Bedienen
run_echo(talk, *, serve=serve) bedient den Handler für ein Gespräch und übergibt den talk, eine Funktion, die den Client spielt. Sie beginnt so:
with serve(echo_handler, "127.0.0.1", 0) as server:
port = server.socket.getsockname()[1]
serve aus websockets.sync.server bindet und lauscht, wie socket.create_server, und gibt ein Server-echo_handler selbst, ohne es aufzurufen: Die Bibliothek ruft es auf, einmal pro Verbindung. Port 0 lässt das System einen freien Port wählen, und server.socket ist der lauschende getsockname()[1] den Port so, wie du es in Networks getan hast.
Noch wird nichts bedient. server.serve_forever() nimmt Verbindungen an und startet für jede einen Handler-Thread, und es kehrt erst zurück, wenn der Server heruntergefahren wird. Also braucht es einen eigenen Thread: Erzeuge threading.Thread(target=server.serve_forever), wobei du die talk(port) aufrufst.
Wenn der with-Block endet, aus welchem Grund auch immer, ruft das Server-Objekt shutdown() auf. Das schließt den lauschenden Socket, schließt jede noch offene Verbindung mit 1001 und lässt serve_forever zurückkehren. Führe nach dem Block den join des Threads mit timeout=WAIT aus und gib zurück, was talk zurückgegeben hat.
Der serve=serve ist eine Nahtstelle, wie connect= in Networks with Python: run_echo verwendet das serve der Bibliothek, außer ein Aufrufer übergibt ein anderes, und Submit übergibt einen Stellvertreter, der aufzeichnet, was dein Code damit macht.
Was lässt serve_forever zurückkehren?
Ein Gespräch mit der Bibliothek verwendet mehr Threads als deines. Bei einer lokalen Zählung lief ein Gespräch mit dem Server und dem Client der Bibliothek in einem Prozess mit sieben Threads, und mit fünf, als die
Was weg ist
Leg echo_handler neben den Server aus Kapitel 5. Der Handshake ist weg: kein serve_handshake, kein check_upgrade, kein accept_value. Das Demaskieren ist weg, und der Pong auch: Die Bibliothek beantwortet jeden Ping, sobald er ankommt, und recv gibt dir immer nur Nachrichten. Auch der Schließ-Handshake ist weg: Wenn der Client einen Close sendet, beantwortet die Bibliothek ihn, die Arbeit, die dein close_frame und wait_for_close erledigt haben. Was bleibt, ist der Teil, der dieses Programm betrifft: wie viele Nachrichten, was zurückgesendet wird, wann geschlossen wird und mit welchem Code.
Was davon tut dein Server aus Kapitel 5, das echo_handler nicht mehr tut?
Bevor du auf Run klickst
Run gibt auf der Standardfehlerausgabe einen Block von etwa zehn Zeilen aus, der wie ein Absturz aussieht. In dieser Lektion ist das zu erwarten, und hier ist der Grund. Der Client aus Kapitel 5 sendet seine drei Nachrichten und schließt dann mit 1000 und dem Grund done. Dein Handler ist zurück in recv und wartet auf eine vierte Nachricht, und recv löst stattdessen ConnectionClosedOK aus. Die echo_handler, und die Bibliothek protokolliert sie. Der Block beginnt mit der Zeile connection handler failed, setzt sich mit einem
websockets.exceptions.ConnectionClosedOK: received 1000 (OK) done; then sent 1000 (OK) done
Diese letzte Zeile sagt, dass die Verbindung trotzdem sauber geschlossen wurde: Die Bibliothek hat den Close des Clients empfangen und denselben Close zurückgesendet. Submit akzeptiert in dieser Lektion einen Handler, der diese Ausnahme entkommen lässt, und Lektion 4 fängt sie ab, sodass der Block verschwindet.
Klicke jetzt auf Run. Der Abschnitt zu Lektion 2 lässt den Client des Kurses aus Kapitel 5, von Hand gebaut, gegen deinen Bibliotheks-Server laufen. Eine fertige Version, auf der Website ausgeführt, hat Folgendes ausgegeben:
Lesson 2: your library server, the Chapter 5 client
{'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}
Der handgebaute Client hat gepingt und seinen Pong bekommen, drei Nachrichten gesendet, drei Echos bekommen und mit 1000 geschlossen; die Bibliothek hat jeden Frame beantwortet. Die späteren Abschnitte sagen noch “not written yet”. Klicke dann auf Submit.
Wenn dich die Threads verwirren, frag Monty:
Warum braucht serve_forever einen eigenen Thread, wenn die Bibliothek schon für jeden Handler einen Thread startet?
Der Server spricht mit einem handgebauten Client. In der nächsten Lektion wandert auch die Clientseite in die Bibliothek.
Aufgabe
Schreibe zwei echo_library.py, in dieser Reihenfolge. Submit nennt die erste, die noch NotImplementedError auslöst.
echo_handler(connection): Solange weniger alsMAX_MESSAGESNachrichten als Echo zurückgesendet wurden, lies eine mitconnection.recv(timeout=WAIT)und sende sie mitconnection.sendunverändert zurück. Rufe nach derconnection.close(1000, "message limit reached")auf. Noch keintry: Lektion 4 fügt es hinzu.run_echo(talk, *, serve=serve): Lies inwith serve(echo_handler, "127.0.0.1", 0) as server:denmit server.socket.getsockname()[1], starte einenthreading.Thread(target=server.serve_forever)und rufetalk(port)auf. Führe nach demwith-Block den join des Threads mittimeout=WAITaus und gib zurück, wastalkzurückgegeben hat.
Klicke auf Run: Der Abschnitt zu Lektion 2 sollte {'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000} ausgeben, und der in der Lektion beschriebene Block connection handler failed erscheint einmal auf der Standardfehlerausgabe.
Klicke dann auf Submit. Die Prüfung öffnet nie einen echo_handler Stellvertreter-Verbindungen mit Nachrichten, die sie dir nicht gezeigt hat, Text und binär: Sie erwartet, dass jede unverändert zurückgesendet wird, in Reihenfolge, höchstens MAX_MESSAGES davon (sie probiert auch ein kleineres MAX_MESSAGES), kein Lesen über die Grenze hinaus, ein Timeout von höchstens 2 Sekunden bei jedem recv und einen Close mit 1000. Ein Client, der früh schließt, darf deinen Handler in dieser Lektion eine run_echo ein Stellvertreter-serve und beobachtet den Thread, den du startest, ohne ihn auszuführen: serve aufgerufen mit echo_handler, "127.0.0.1" und 0, ein Thread mit dem Ziel serve_forever, gestartet vor talk, talk mit dem Port aufgerufen, der Server nach talk heruntergefahren, auch wenn talk eine Ausnahme auslöst, ein join mit Timeout nach dem Herunterfahren und der talk zurückgegeben. Behalte import threading und schreibe threading.Thread(...). server.shutdown() selbst im Block aufzurufen ist in Ordnung, und eine for-Schleife über range(MAX_MESSAGES) ebenfalls. Das Importieren von echo_library.py darf nichts ausgeben.