0%

Um einen Protokollwechsel bitten · Abschlussprojekt

Abschlussprojekt: Projekt: Ein Handshake auf Loopback

Deine Serverseite zerlegt, entscheidet und antwortet; deine Clientseite fragt und prüft. Die beiden haben noch nie miteinander geredet. In diesem Projekt tun sie es, über eine echte , in einem Python-Programm auf .

Was du baust

Drei , die ersten beiden in ws_handshake.py und die dritte in handshake_demo.py:

  • serve_handshake(conn) ist die Hälfte des Servers, für eine Verbindung, die bereits angenommen ist. Erzeuge einen Reader mit make_reader(conn) und lies den Anfragekopf mit read_head. Wenn das Lesen oder Zerlegen auslöst, weil der Kopf zu lang oder fehlerhaft ist, sende refuse(400) und gib None zurück. Wenn der Client schließt, bevor der Kopf endet, löst read_head ConnectionError aus; sende nichts und gib None zurück. Andernfalls sende die Antwort, die check_upgrade zurückgibt. Gib nach einer 101 den Reader zurück und nach einer Ablehnung None.

  • client_handshake ist die Hälfte des Clients, für einen , der bereits verbunden ist. Erzeuge einen Schlüssel mit make_client_key(random_bytes), sende die Anfrage mit sendall, lies den Antwortkopf mit read_head über einen eigenen Reader und prüfe ihn mit check_accept_response, die bei allem außer der richtigen 101 ValueError auslöst. Gib den Reader zurück.

  • run_loopback() bringt sie auf eine echte Verbindung: ein Listener auf 127.0.0.1 0, ein Thread, der einen Client annimmt und serve_handshake ausführt, ein Client im Haupt-Thread, der sich verbindet und client_handshake ausführt, eine oder zwei Zeilen Ausgabe darüber, was jede Seite gesehen hat, und ein join des Threads.

client_handshake beginnt mit der Zeile, die ws_handshake.py bereits enthält:

def client_handshake(sock, host, path, *, random_bytes=os.urandom):
    ...

Der * macht random_bytes zu einem reinen Schlüsselwortargument, wie connect= in deinem fetch aus with Python: Ein Aufrufer muss es beim Namen nennen, und run_loopback lässt es weg.

Bytes nach dem Kopf

Sobald der Server seine 101 gesendet hat, darf er sofort - senden. TCP hält den Kopf und diese Frames nicht auseinander: Ein recv im Client kann das Ende des Kopfs und den Anfang des ersten Frames zusammen zurückgeben. Das kennst du aus Networks with Python: Was nach dem Ende einer Nachricht ankommt, ist der Anfang der nächsten, und es bleibt im Empfangspuffer. RFC 6455 verlangt von einem Client, auf die 101 zu warten, bevor er einen Frame sendet, aber dein Server kann einen vorsichtigen Client nicht von einem eiligen unterscheiden, also behandelt er alles, was auf den Anfragekopf folgt, genauso.

read_head behält diese Bytes bereits in reader["buffer"]. Deshalb geben beide Hälften den Reader zurück statt nichts. Das Lesen von Frames, das du in den nächsten Kapiteln baust, beginnt bei diesem Puffer, genau dort, wo der Kopf endete.

Das erste recv des Servers hat die ganze Upgrade-Anfrage zurückgegeben, gefolgt von 11 Bytes eines Frames. Was muss serve_handshake mit diesen 11 Bytes tun?

read_head schützt auch vor einem Kopf, der nie endet: Jenseits von MAX_HEAD, 8192 Bytes, löst es ValueError aus, und dein Server antwortet mit 400.

Die Verdrahtung

Die Verdrahtung ist das Muster, das du in Networks with Python, Kapitel 9, geschrieben hast: ein Listener, ein Thread pro Verbindung, joins mit Timeouts. Hier gibt es eine Verbindung, und das Programm spielt beide Enden. Die Regeln:

  • Binde an 127.0.0.1 und Port 0, damit das System einen freien Port wählt. listener.getsockname()[1] sagt dir, welchen.

  • Gib dem Listener mit settimeout(WAIT) ein Timeout, bevor der Thread accept() aufruft, damit ein Client, der sich nie verbindet, den Server-Thread nicht festhalten kann. WAIT ist die 2-Sekunden-Konstante oben in handshake_demo.py.

  • Gib auch dem angenommenen Socket ein eigenes Timeout, bevor serve_handshake von ihm liest. Wie du in Networks with Python gesehen hast, beginnt der Socket, den accept() zurückgibt, ohne Timeout, ganz gleich, welches der Listener hat.

  • Verbinde dich mit socket.create_connection(("127.0.0.1", port), timeout=WAIT), damit auch jedes recv in client_handshake begrenzt ist.

  • Führe den join des Server-Threads mit einem Timeout aus.

Die Server-Funktion läuft in ihrem eigenen Thread, also kann sie kein Ergebnis an run_loopback zurückgeben. Übergib ihr zusammen mit dem Listener ein und lass sie darin speichern, was sie gesehen hat, etwa ob serve_handshake einen Reader zurückgegeben hat. Das Starten und der join des Threads sehen so aus:

server = threading.Thread(target=accept_one, args=(listener, seen), daemon=True)
server.start()
...
server.join(timeout=WAIT)

accept_one ist die Server-Funktion, die einen Client annimmt und serve_handshake aufruft; seen ist das Dictionary. daemon=True, ein Schlüsselwort, das du bisher nicht gebraucht hast, bedeutet, dass ein noch laufender Thread das Programm nicht am Leben halten kann, nachdem der Haupt-Thread endet, ein Sicherheitsnetz unter den Timeouts.

Verwende with für den Listener, die angenommene Verbindung und den Client-Socket, damit jeder auf jedem Weg geschlossen wird.

Was Submit sehen kann und was nicht

Submit prüft die beiden Hälften so, wie es die früheren Lektionen getan haben, ohne echte Verbindung. Es gibt serve_handshake und client_handshake Stellvertreter-Sockets, die die Bytes am Stück liefern, ein Byte nach dem anderen und jeweils zwei Bytes, gefolgt vom Anfang eines Frames, und dann verstummen, ohne zu schließen, wie es eine echte Gegenseite tun würde. Es sendet Anfragen, die abgelehnt werden müssen, und einen Kopf, der nie endet. Dann übergibt es die Anfrage deines Clients an dein serve_handshake und reicht die Antwort zurück, sodass deine beiden Hälften miteinander reden.

run_loopback selbst wird nur auf die Regeln oben geprüft: die Adresse und den Port, die beiden settimeout-Aufrufe, das Timeout des Clients und das des joins. Das prüft, wie der Code geschrieben ist, nicht, was er tut: Eine echte Verbindung ist dein Beleg in Run, nicht der des Prüfers.

Run

Klicke auf Run. Nach den vier Lektionsabschnitten gibt run_loopback aus, was jede Seite gesehen hat. Hier ist das Ende eines Laufs des fertigen Projekts:

client: the server answered 101 with the right accept value
client: 0 bytes are waiting after the head
server: switched to WebSocket: True
server thread still running: False

Deine Formulierung darf abweichen, und das ist in Ordnung. Null Bytes warten, weil dein Server nach seiner 101 noch nichts sendet. Wenn das Programm mit TimeoutError anhält, wartet eine Seite auf Bytes, die die andere nie gesendet hat; der nennt das recv, das gewartet hat.

Wenn dich etwas an der Verdrahtung verwirrt, frag Monty:

Warum braucht mein Server-Thread ein eigenes settimeout auf dem Socket, den accept() zurückgibt, wenn der Listener schon eins hat?

Wenn der Lauf mit einer umgestellten Verbindung endet und kein Thread mehr läuft, klicke auf Submit.

Die Verbindung ist offen, beide Seiten sind sich einig, dass sie WebSocket spricht, und keine kann bisher etwas sagen: Sie brauchen eine Möglichkeit, eine Nachricht zu senden, und das nächste Kapitel baut sie, einen Frame nach dem anderen.

Aufgabe

Schreibe drei , in dieser Reihenfolge. Submit nennt die erste, die noch NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.

  1. serve_handshake(conn) in ws_handshake.py: Erzeuge einen Reader mit make_reader(conn), lies den Kopf mit read_head und zerlege ihn mit parse_upgrade_request. Sende bei mit conn.sendall die Antwort refuse(400) und gib None zurück. Sende bei ConnectionError nichts und gib None zurück. Andernfalls sende, was check_upgrade zurückgibt, und gib den Reader zurück, wenn es eine 101 war, None, wenn es eine Ablehnung war.

  2. client_handshake(sock, host, path, *, random_bytes=os.urandom) in ws_handshake.py: Erzeuge einen Schlüssel mit make_client_key(random_bytes), sende build_upgrade_request(host, path, key) mit sock.sendall, lies den Antwortkopf mit read_head über make_reader(sock), prüfe ihn mit check_accept_response und gib den Reader zurück.

  3. run_loopback() in handshake_demo.py, mit einer eigenen Server-Funktion daneben: Binde an 127.0.0.1 0 (mit socket.create_server oder mit bind und listen), gib dem Listener settimeout(WAIT), starte einen threading.Thread, dessen Funktion einen Client mit conn, address = listener.accept() annimmt, conn.settimeout(WAIT) aufruft und serve_handshake(conn) ausführt; verbinde dich aus dem Haupt-Thread mit socket.create_connection(..., timeout=WAIT), führe client_handshake aus, gib aus, was jede Seite gesehen hat, und führe den join des Threads mit einem Timeout aus.

Klicke auf Run: Nach den Lektionsabschnitten schließen deine beiden Hälften einen ab.

Klicke dann auf Submit. Die Prüfung gibt beiden Hälften Stellvertreter-, nie einen echten: Anfragen und Antworten am Stück, ein Byte nach dem anderen und jeweils zwei Bytes geliefert, jeweils gefolgt vom Anfang eines , der in dem zurückgegebenen Reader bleiben muss, und dann keine weiteren Bytes und kein Schließen, sodass eine Hälfte, die liest, bis die Verbindung geschlossen wird, ewig wartet und scheitert. Sie sendet eine Anfrage mit Version 8, einen Kopf mit einer Zeile ohne Doppelpunkt, einen Kopf, der nie endet, und einen Client, der mitten in seinem Kopf schließt. Sie lässt deinen Client gegen dein eigenes serve_handshake laufen. Für run_loopback liest sie deinen Code in handshake_demo.py und prüft nur die Regeln: die Adresse ("127.0.0.1", 0), settimeout auf dem Listener und auf dem Socket, den accept() zurückgibt, ein timeout bei create_connection, einen Thread und ein Timeout bei jedem join. Deine ausgegebenen Zeilen werden nicht geprüft. Das Importieren beider Dateien darf nichts ausgeben.