0%

De bibliotheek websockets, zonder asyncio · oefening

Een server in een paar regels

interop.py richtte de client van de bibliotheek op de server uit hoofdstuk 5. Deze les draait dat om: de server wordt die van de bibliotheek. In hoofdstuk 5 las serve_conversation , beantwoordde het , echode het, telde het en voerde het de sluithandshake uit, nadat serve_handshake de upgrade had beantwoord. Met de bibliotheek past het deel dat over echoën gaat in een handler van ongeveer zes regels.

Een handler en zijn verbinding

Een handler is een die jij schrijft en de bibliotheek aanroept. Hij neemt één aan, de verbinding met één client, en gebruikt daar een paar dingen op:

  • connection.recv(timeout=...) wacht hooguit zoveel seconden op het volgende bericht en geeft het terug: een str voor een tekstbericht, bytes voor een binair bericht. Als er niets op tijd aankomt, werpt het TimeoutError op.

  • connection.send(message) stuurt een bericht: een str als tekst, bytes als binair.

  • connection.close(code, reason) stuurt een close-frame met die code en reden, en voert de sluithandshake uit.

  • connection.request.path is het pad waar de client om vroeg, zoals /echo. Latere hoofdstukken gebruiken het; dit hoofdstuk niet.

De bibliotheek doet al het andere. Voor elke client die verbinding maakt, voert ze de uit en roept ze daarna je handler aan op een eigen thread: het patroon van een thread per verbinding uit Networks with Python hoofdstuk 9. Als de handler terugkeert, sluit de bibliotheek de verbinding met 1000 als de handler dat nog niet heeft gedaan.

Drie clients maken tegelijk verbinding. Hoe vaak roept de bibliotheek echo_handler aan?

echo_handler(connection) is de echolus uit hoofdstuk 5 met het loodgieterswerk eruit. Zolang er minder dan MAX_MESSAGES berichten zijn geëchood, lees je er een met connection.recv(timeout=WAIT) en stuur je het meteen terug met connection.send. Door de ongewijzigd te sturen, blijft het type behouden, dus tekst gaat terug als tekst en binair als binair, de keuze die server_reply met maakte. Sluit na de met connection.close(1000, "message limit reached"). WAIT en MAX_MESSAGES staan al bovenaan echo_library.py: 2 seconden en 10 berichten.

Bedienen

run_echo(talk, *, serve=serve) bedient de handler voor één gesprek en geeft de aan talk, een functie die de client speelt. Het begint zo:

with serve(echo_handler, "127.0.0.1", 0) as server:
    port = server.socket.getsockname()[1]

serve, uit websockets.sync.server, bindt en luistert, zoals socket.create_server, en geeft een serverobject terug. Geef echo_handler zelf mee, zonder hem aan te roepen: de bibliotheek roept hem aan, één keer per verbinding. Met poort 0 kiest het systeem een vrije poort, en server.socket is de luisterende , dus getsockname()[1] leest de poort zoals je in Networks deed.

Er wordt nog niets bediend. server.serve_forever() accepteert verbindingen en start voor elk een handlerthread, en het keert pas terug als de server wordt afgesloten. Het heeft dus een eigen thread nodig: maak threading.Thread(target=server.serve_forever), waarbij je de meegeeft zonder haar aan te roepen, en start die voordat je talk(port) aanroept.

Als het with-blok eindigt, om welke reden ook, roept het serverobject shutdown() aan. Dat sluit de luisterende socket, sluit elke verbinding die nog open is met 1001, en laat serve_forever terugkeren. Join na het blok de thread met timeout=WAIT, en geef terug wat talk teruggaf.

De serve=serve is een naad (seam), zoals connect= in Networks with Python: run_echo gebruikt de serve van de bibliotheek tenzij een aanroeper een andere meegeeft, en Submit geeft een stand-in mee die noteert wat je code ermee doet.

Waardoor keert serve_forever terug?

Een gesprek met de bibliotheek gebruikt meer threads dan dat van jou. Bij een lokale telling draaide één gesprek met de server en de client van de bibliotheek in één proces zeven threads, en vijf met de -pings van de bibliotheek uitgezet; hoofdstuk 8 behandelt die pings. Het aantal hangt af van wanneer je telt, en daarom telt niets in deze cursus threads. Ze eindigen allemaal als het gesprek sluit en de server wordt afgesloten.

Wat er weg is

Zet echo_handler naast de server uit hoofdstuk 5. De handshake is weg: geen serve_handshake, geen check_upgrade, geen accept_value. Demaskeren is weg, en de pong ook: de bibliotheek beantwoordt elke ping zodra die aankomt, en recv geeft je alleen ooit berichten. De sluithandshake is ook weg: als de client een close stuurt, beantwoordt de bibliotheek die, het werk dat je close_frame en wait_for_close deden. Wat overblijft, is het deel dat over dit programma gaat: hoeveel berichten, wat er teruggestuurd wordt, wanneer er gesloten wordt en met welke code.

Welke van deze dingen doet je server uit hoofdstuk 5 die echo_handler niet meer doet?

Voordat je op Run drukt

Run drukt een blok van ongeveer tien regels naar standaardfout af dat op een crash lijkt. In deze les is dat te verwachten, en dit is waarom. De client uit hoofdstuk 5 stuurt zijn drie berichten en sluit daarna met 1000 en de reden done. Je handler zit weer in recv, wachtend op een vierde bericht, en recv werpt in plaats daarvan ConnectionClosedOK op. De verlaat echo_handler, en de bibliotheek logt hem. Het blok begint met de regel connection handler failed, gaat verder met een waarvan de bestandspaden per machine verschillen, en eindigt met deze regel:

websockets.exceptions.ConnectionClosedOK: received 1000 (OK) done; then sent 1000 (OK) done

Die laatste regel zegt dat de verbinding toch netjes sloot: de bibliotheek ontving de close van de client en stuurde dezelfde close terug. Submit accepteert in deze les een handler die deze exception laat ontsnappen, en les 4 vangt hem, zodat het blok verdwijnt.

Druk nu op Run. De sectie van les 2 draait de met de hand gebouwde client uit hoofdstuk 5 van de cursus tegen je bibliotheekserver. Een afgewerkte versie, gedraaid op de site, drukte af:

Lesson 2: your library server, the Chapter 5 client
{'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000}

De zelfgebouwde client pingde en kreeg zijn pong, stuurde drie berichten, kreeg drie echo’s, en sloot met 1000; de bibliotheek beantwoordde elk frame. De latere secties zeggen nog “not written yet”. Druk daarna op Submit.

Als de threads je verbazen, vraag het Monty:

Waarom heeft serve_forever een eigen thread nodig als de bibliotheek al een thread voor elke handler start?

De server praat met een zelfgebouwde client. In de volgende les verhuist ook de clientkant naar de bibliotheek.

Opdracht

Schrijf twee in echo_library.py, in deze volgorde. Submit noemt de eerste die nog NotImplementedError opwerpt.

  1. echo_handler(connection): lees, zolang er minder dan MAX_MESSAGES berichten zijn geëchood, er een met connection.recv(timeout=WAIT) en stuur het ongewijzigd terug met connection.send. Roep na de connection.close(1000, "message limit reached") aan. Nog geen try: les 4 voegt die toe.

  2. run_echo(talk, *, serve=serve): lees in with serve(echo_handler, "127.0.0.1", 0) as server: de met server.socket.getsockname()[1], start een threading.Thread(target=server.serve_forever), en roep talk(port) aan. Join na het with-blok de thread met timeout=WAIT en geef terug wat talk teruggaf.

Druk op Run: de sectie van les 2 moet {'echoes': ['hello', 'café ✓', 'third message'], 'code': 1000} afdrukken, en het blok connection handler failed dat in de les wordt beschreven, verschijnt één keer op standaardfout.

Druk daarna op Submit. De controle opent nooit een . Ze geeft echo_handler stand-in-verbindingen met berichten die ze je niet heeft laten zien, tekst en binair: ze verwacht dat elk ervan ongewijzigd wordt teruggestuurd, op volgorde, hooguit MAX_MESSAGES (ze probeert ook een kleinere MAX_MESSAGES), geen leesactie voorbij de limiet, een time-out van hooguit 2 seconden op elke recv, en een close met 1000. Een client die vroeg sluit, mag je handler in deze les laten opwerpen. Ze geeft run_echo een stand-in-serve en houdt de thread in de gaten die je start, zonder hem te draaien: serve aangeroepen met echo_handler, "127.0.0.1" en 0, een thread met als target serve_forever gestart vóór talk, talk aangeroepen met de poort, de server afgesloten na talk, ook als talk opwerpt, een join met een time-out na het afsluiten, en de van talk teruggegeven. Houd import threading en schrijf threading.Thread(...). Zelf server.shutdown() aanroepen binnen het blok is prima, en een for-lus over range(MAX_MESSAGES) ook. Het importeren van echo_library.py mag niets afdrukken.