0%

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 , in één Python-programma op .

Wat je bouwt

Drie , de eerste twee in 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 met make_reader(conn) en lees de verzoekkop met read_head. Als lezen of parsen opwerpt, omdat de kop te lang of misvormd is, stuur dan refuse(400) en geef None terug. Als de client sluit voordat de kop eindigt, werpt read_head ConnectionError op; stuur dan niets en geef None terug. Stuur anders het antwoord dat check_upgrade teruggeeft. Geef na een 101 de reader terug, en na een weigering None.

  • client_handshake is de helft van de client, voor een die al verbonden is. Maak een sleutel met make_client_key(random_bytes), stuur het verzoek met sendall, lees de antwoordkop met read_head via een eigen reader, en controleer hem met check_accept_response, dat ValueError opwerpt voor alles behalve de juiste 101. Geef de reader terug.

  • run_loopback() zet ze op een echte verbinding: een listener op 127.0.0.1 0, een thread die één client accepteert en serve_handshake draait, een client in de hoofdthread die verbinding maakt en client_handshake draait, 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 with Python: een aanroeper moet het bij naam noemen, en run_loopback laat het weg.

Bytes na de kop

Zodra de server zijn 101 heeft gestuurd, mag hij meteen - sturen. TCP houdt de kop en die frames niet uit elkaar: één 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.1 en 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 thread accept() aanroept, zodat een client die nooit verbinding maakt de serverthread niet kan vasthouden. WAIT is de constante van 2 seconden bovenaan handshake_demo.py.

  • Geef ook de geaccepteerde socket zijn eigen time-out, voordat serve_handshake ervan leest. Zoals je in Networks with Python zag, begint de socket die accept() teruggeeft zonder time-out, wat de listener ook heeft.

  • Maak verbinding met socket.create_connection(("127.0.0.1", port), timeout=WAIT), zodat elke recv in client_handshake ook 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 mee, en laat haar daarin opslaan wat ze zag, bijvoorbeeld of 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 noemt 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 , in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt, dus je krijgt ze één voor één.

  1. serve_handshake(conn) in ws_handshake.py: maak een reader met make_reader(conn), lees de kop met read_head, en parse hem met parse_upgrade_request. Stuur bij refuse(400) met conn.sendall en geef None terug. Stuur bij ConnectionError niets en geef None terug. Stuur anders wat check_upgrade teruggeeft, en geef de reader terug als het een 101 was, None als het een weigering was.

  2. client_handshake(sock, host, path, *, random_bytes=os.urandom) in ws_handshake.py: maak een sleutel met make_client_key(random_bytes), stuur build_upgrade_request(host, path, key) met sock.sendall, lees de antwoordkop met read_head via make_reader(sock), controleer hem met check_accept_response, en geef de reader terug.

  3. run_loopback() in handshake_demo.py, met een eigen serverfunctie ernaast: bind 127.0.0.1 0 (met socket.create_server of met bind en listen), geef de listener settimeout(WAIT), start een threading.Thread waarvan de functie één client accepteert met conn, address = listener.accept(), conn.settimeout(WAIT) aanroept en serve_handshake(conn) draait; maak vanuit de hoofdthread verbinding met socket.create_connection(..., timeout=WAIT), draai client_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-, nooit een echte: verzoeken en antwoorden in één keer afgeleverd, één byte per keer en twee bytes per keer, elk gevolgd door het begin van een dat in de reader moet blijven die je teruggeeft, en daarna geen bytes meer en geen sluiting, dus een helft die leest totdat de verbinding sluit, wacht eeuwig en faalt. Ze stuurt een verzoek met versie 8, een kop met een regel zonder dubbele punt, een kop die nooit eindigt, en een client die halverwege zijn kop sluit. Ze draait je client tegen je eigen 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.