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
Was du baust
Drei 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 mitmake_reader(conn)und lies den Anfragekopf mitread_head. Wenn das Lesen oder Zerlegenauslöst, weil der Kopf zu lang oder fehlerhaft ist, sende refuse(400)und gibNonezurück. Wenn der Client schließt, bevor der Kopf endet, löstread_headConnectionErroraus; sende nichts und gibNonezurück. Andernfalls sende die Antwort, diecheck_upgradezurückgibt. Gib nach einer 101 den Reader zurück und nach einer AblehnungNone.client_handshakeist 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 mitsendall, lies den Antwortkopf mitread_headüber einen eigenen Reader und prüfe ihn mitcheck_accept_response, die bei allem außer der richtigen 101ValueErrorauslöst. Gib den Reader zurück.run_loopback()bringt sie auf eine echte Verbindung: ein Listener auf127.0.0.10, ein Thread, der einen Client annimmt und serve_handshakeausführt, ein Client im Haupt-Thread, der sich verbindet undclient_handshakeausfü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 run_loopback lässt es weg.
Bytes nach dem Kopf
Sobald der Server seine 101 gesendet hat, darf er sofort 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.1und 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 Threadaccept()aufruft, damit ein Client, der sich nie verbindet, den Server-Thread nicht festhalten kann.WAITist die 2-Sekunden-Konstante oben inhandshake_demo.py.Gib auch dem angenommenen Socket ein eigenes Timeout, bevor
serve_handshakevon ihm liest. Wie du in Networks with Python gesehen hast, beginnt der Socket, denaccept()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 jedesrecvinclient_handshakebegrenzt 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 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 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 NotImplementedError auslöst, sodass du sie eine nach der anderen angehst.
serve_handshake(conn)inws_handshake.py: Erzeuge einen Reader mitmake_reader(conn), lies den Kopf mitread_headund zerlege ihn mitparse_upgrade_request. Sende beimit conn.sendalldie Antwortrefuse(400)und gibNonezurück. Sende beiConnectionErrornichts und gibNonezurück. Andernfalls sende, wascheck_upgradezurückgibt, und gib den Reader zurück, wenn es eine 101 war,None, wenn es eine Ablehnung war.client_handshake(sock, host, path, *, random_bytes=os.urandom)inws_handshake.py: Erzeuge einen Schlüssel mitmake_client_key(random_bytes), sendebuild_upgrade_request(host, path, key)mitsock.sendall, lies den Antwortkopf mitread_headübermake_reader(sock), prüfe ihn mitcheck_accept_responseund gib den Reader zurück.run_loopback()inhandshake_demo.py, mit einer eigenen Server-Funktion daneben: Binde an127.0.0.10 (mit socket.create_serveroder mitbindundlisten), gib dem Listenersettimeout(WAIT), starte einenthreading.Thread, dessen Funktion einen Client mitconn, address = listener.accept()annimmt,conn.settimeout(WAIT)aufruft undserve_handshake(conn)ausführt; verbinde dich aus dem Haupt-Thread mitsocket.create_connection(..., timeout=WAIT), führeclient_handshakeaus, 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
Klicke dann auf Submit. Die Prüfung gibt beiden Hälften Stellvertreter-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.