Vragen om van protocol te wisselen · eindproject
Eindproject: Project: één handshake op loopback
Je serverkant parset, beslist en antwoordt; je clientkant vraagt en controleert. De twee hebben nog nooit met elkaar gepraat. In dit project doen ze dat, over een echte
Wat je bouwt
Drie ws_handshake.py en de derde in handshake_demo.py:
serve_handshake(conn)is de helft van de server, voor één verbinding die al geaccepteerd is. Maak een reader metmake_reader(conn)en lees de verzoekkop metread_head. Als lezen of parsenopwerpt, omdat de kop te lang of misvormd is, stuur dan refuse(400)en geefNoneterug. Als de client sluit voordat de kop eindigt, werptread_headConnectionErrorop; stuur dan niets en geefNoneterug. Stuur anders het antwoord datcheck_upgradeteruggeeft. Geef na een 101 de reader terug, en na een weigeringNone.client_handshakeis de helft van de client, voor eendie al verbonden is. Maak een sleutel met make_client_key(random_bytes), stuur het verzoek metsendall, lees de antwoordkop metread_headvia een eigen reader, en controleer hem metcheck_accept_response, datValueErroropwerpt voor alles behalve de juiste 101. Geef de reader terug.run_loopback()zet ze op een echte verbinding: een listener op127.0.0.10, een thread die één client accepteert en serve_handshakedraait, een client in de hoofdthread die verbinding maakt enclient_handshakedraait, een regel of twee uitvoer die zegt wat elke kant zag, en een join van de thread.
client_handshake begint met de regel die al in ws_handshake.py staat:
def client_handshake(sock, host, path, *, random_bytes=os.urandom):
...
De * maakt random_bytes keyword-only, zoals connect= in je fetch uit run_loopback laat het weg.
Bytes na de kop
Zodra de server zijn 101 heeft gestuurd, mag hij meteen recv in de client kan het einde van de kop en het begin van het eerste frame samen teruggeven. Je kwam dit tegen in Networks with Python: wat er na het einde van een bericht aankomt, is het begin van het volgende, en het blijft in de ontvangstbuffer. RFC 6455 vraagt een client om op de 101 te wachten voordat hij een frame stuurt, maar je server kan een voorzichtige client niet van een gretige onderscheiden, dus behandelt hij wat er na de verzoekkop komt op dezelfde manier.
read_head houdt die bytes al in reader["buffer"]. Daarom geven beide helften de reader terug in plaats van niets. Het lezen van frames dat je in de volgende hoofdstukken bouwt, begint vanuit die buffer, precies waar de kop eindigde.
De eerste recv van de server gaf het hele upgradeverzoek terug, gevolgd door 11 bytes van een frame. Wat moet serve_handshake met die 11 bytes doen?
read_head beschermt ook tegen een kop die nooit eindigt: voorbij MAX_HEAD, 8192 bytes, werpt hij ValueError op, en je server antwoordt 400.
De bedrading
De bedrading is het patroon dat je in Networks with Python hoofdstuk 9 schreef: een listener, een thread per verbinding, joins met time-outs. Hier is er één verbinding, en het programma speelt beide kanten. De regels:
Bind
127.0.0.1en poort 0, zodat het systeem een vrije poort kiest.listener.getsockname()[1]vertelt je welke.Geef de listener een time-out met
settimeout(WAIT)voordat de threadaccept()aanroept, zodat een client die nooit verbinding maakt de serverthread niet kan vasthouden.WAITis de constante van 2 seconden bovenaanhandshake_demo.py.Geef ook de geaccepteerde socket zijn eigen time-out, voordat
serve_handshakeervan leest. Zoals je in Networks with Python zag, begint de socket dieaccept()teruggeeft zonder time-out, wat de listener ook heeft.Maak verbinding met
socket.create_connection(("127.0.0.1", port), timeout=WAIT), zodat elkerecvinclient_handshakeook begrensd is.Join de serverthread met een time-out.
De serverfunctie draait in haar eigen thread, dus ze kan geen resultaat teruggeven aan run_loopback. Geef haar naast de listener een serve_handshake een reader teruggaf. Het starten en joinen van de thread ziet er zo uit:
server = threading.Thread(target=accept_one, args=(listener, seen), daemon=True)
server.start()
...
server.join(timeout=WAIT)
accept_one is de serverfunctie, die één client accepteert en serve_handshake aanroept; seen is de dictionary. daemon=True, een keyword dat je nog niet nodig had, betekent dat een thread die nog draait het programma niet in leven kan houden nadat de hoofdthread eindigt, een vangnet onder de time-outs.
Gebruik with voor de listener, de geaccepteerde verbinding en de clientsocket, zodat elk ervan op elk pad sluit.
Wat Submit wel en niet kan zien
Submit controleert de twee helften zoals de eerdere lessen deden, zonder echte verbinding. Het geeft serve_handshake en client_handshake stand-in-sockets die de bytes in één keer afleveren, één byte per keer en twee bytes per keer, gevolgd door het begin van een frame, en dan stil worden zonder te sluiten, zoals een live peer zou doen. Het stuurt verzoeken die geweigerd moeten worden en een kop die nooit eindigt. Daarna geeft het het verzoek van je client aan je serve_handshake en geeft het antwoord terug, zodat je twee helften met elkaar praten.
run_loopback zelf wordt alleen op de regels hierboven gecontroleerd: het adres en de poort, de twee settimeout-aanroepen, de time-out van de client, en die van de join. Dat controleert hoe de code geschreven is, niet wat ze doet: een echte verbinding is jouw bewijs in Run, niet dat van de controle.
Run
Druk op Run. Na de vier lessecties drukt run_loopback af wat elke kant zag. Hier is het einde van een run van het afgewerkte project:
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
Jouw formulering mag anders zijn, en dat is prima. Er wachten nul bytes omdat je server nog niets stuurt na zijn 101. Als het programma stopt met TimeoutError, wacht één kant op bytes die de andere nooit stuurde; de recv die wachtte.
Als iets in de bedrading je verbaast, vraag het Monty:
Waarom heeft mijn serverthread zijn eigen settimeout nodig op de socket die accept() teruggaf, als de listener er al een heeft?
Als de run eindigt met een omgeschakelde verbinding en geen thread die nog draait, druk je op Submit.
De verbinding is open, beide kanten zijn het erover eens dat ze WebSocket spreekt, en geen van beide kan al iets zeggen: ze hebben een manier nodig om een bericht te sturen, en het volgende hoofdstuk bouwt die, frame voor frame.
Opdracht
Schrijf drie NotImplementedError opwerpt, dus je krijgt ze één voor één.
serve_handshake(conn)inws_handshake.py: maak een reader metmake_reader(conn), lees de kop metread_head, en parse hem metparse_upgrade_request. Stuur bijrefuse(400)metconn.sendallen geefNoneterug. Stuur bijConnectionErrorniets en geefNoneterug. Stuur anders watcheck_upgradeteruggeeft, en geef de reader terug als het een 101 was,Noneals het een weigering was.client_handshake(sock, host, path, *, random_bytes=os.urandom)inws_handshake.py: maak een sleutel metmake_client_key(random_bytes), stuurbuild_upgrade_request(host, path, key)metsock.sendall, lees de antwoordkop metread_headviamake_reader(sock), controleer hem metcheck_accept_response, en geef de reader terug.run_loopback()inhandshake_demo.py, met een eigen serverfunctie ernaast: bind127.0.0.10 (met socket.create_serverof metbindenlisten), geef de listenersettimeout(WAIT), start eenthreading.Threadwaarvan de functie één client accepteert metconn, address = listener.accept(),conn.settimeout(WAIT)aanroept enserve_handshake(conn)draait; maak vanuit de hoofdthread verbinding metsocket.create_connection(..., timeout=WAIT), draaiclient_handshake, druk af wat elke kant zag, en join de thread met een time-out.
Druk op Run: na de lessecties voltooien je twee helften één
Druk daarna op Submit. De controle geeft beide helften stand-in-serve_handshake. Voor run_loopback leest ze je code in handshake_demo.py en controleert ze alleen de regels: het adres ("127.0.0.1", 0), settimeout op de listener en op de socket die accept() teruggeeft, een timeout op create_connection, een Thread, en een time-out op elke join. Je afgedrukte regels worden niet gecontroleerd. Het importeren van een van beide bestanden mag niets afdrukken.