28. Anwendungsübung: Version 10
28.1. Einleitung
In den Beispielen für Clients des Steuerberechnungsservers sendeten die Threads N Anfragen nacheinander, wenn sie N Steuerzahler verarbeiten mussten. Die Idee hier ist, eine einzige Anfrage zu senden, die die N Steuerzahler umfasst. Für jeden von ihnen müssen die Informationen [verheiratet, Kinder, Gehalt] gesendet werden. Diese können als Parameter gesendet werden:
- in der URL. Dies führt zu einer langen, bedeutungslosen URL;
- im Hauptteil der HTTP-Anfrage. Wir wissen, dass dieser Hauptteil für den Benutzer, der einen Browser verwendet, verborgen ist;
In beiden Fällen können Sie eine [GET]- oder [POST]-Anfrage verwenden. Wir werden eine POST-Anfrage verwenden, bei der die Parameter in den HTTP-Anfragetext eingebettet sind.
Die Client-Server-Architektur hat sich nicht geändert:

28.2. Der Webserver

Der Ordner [http-servers/05] wird zunächst durch Kopieren des Ordners [http-servers/02] erstellt. Wir kehren zum JSON-Austausch zwischen Client und Server zurück. Wir haben gesehen, dass der Wechsel von JSON zu XML sehr einfach ist.
28.2.1. Konfiguration
Die Konfiguration [config, config_database, config_layers] bleibt gegenüber früheren Versionen unverändert. Wir werden nicht noch einmal darauf eingehen.
28.2.2. Das Hauptskript [main]
Das Skript [main] ist identisch mit dem im Ordner [http-servers/02], den wir kopiert haben. Nur ein Punkt unterscheidet sich:
- Zeile 2: Auf die URL / wird nun über eine POST-Anfrage zugegriffen;
28.2.3. Der [index_controller]
Der [index_controller] entwickelt sich wie folgt:
- Zeile 9: Der Controller empfängt:
- die Anfrage des Clients;
- die Serverkonfiguration [config];
- Zeilen 14–18: Wir rufen den POST-Body ab. Die im HTTP-Request-Body gekapselten Parameter können auf verschiedene Arten kodiert sein. Eine davon haben wir bereits kennengelernt: [x-www-form-urlencoded]. Hier verwenden wir eine andere Kodierung: JSON;
- Zeile 18: [request.data] ruft den Body der HTTP-Anfrage ab. Hier rufen wir Text ab, und wir wissen, dass dieser Text JSON ist, der eine Liste von Dictionaries darstellt [married, children, salary];
- Zeilen 19–24: Wir rufen diese Liste von Dictionaries ab;
- Zeilen 22–24: Wenn das Abrufen des JSON-Datenstroms fehlgeschlagen ist, protokollieren wir den Fehler;
- Zeilen 26–28: Wenn wir feststellen, dass das abgerufene Objekt keine Liste oder eine leere Liste ist, protokollieren wir den Fehler;
- Zeilen 29–38: Wenn eine Liste erfolgreich abgerufen wurde, überprüfen wir, ob es sich tatsächlich um eine Liste von Dictionaries handelt;
- Zeilen 40–43: Wenn ein Fehler aufgetreten ist, brechen wir hier ab und senden eine Fehlerantwort an den Client;
- Zeilen 45–69: Wir überprüfen nun jedes der Wörterbücher:
- sie müssen die Schlüssel [married, children, salary] enthalten;
- sie müssen es uns ermöglichen, ein gültiges [TaxPayer]-Objekt zu erstellen;
- Zeilen 65–69: Wenn in einem Wörterbuch ein Fehler erkannt wird, wird dieser unter dem Schlüssel „error“ in dasselbe Wörterbuch aufgenommen;
- Zeilen 72–75: Die Wörterbücher, die Fehler enthalten, wurden in der Liste [list_errors] gesammelt. Ist diese Liste nicht leer, wird sie in einer Fehlerantwort an den Client gesendet;
- Zeile 77: An dieser Stelle wissen wir, dass wir aus dem Hauptteil der vom Client gesendeten Anfrage eine Liste von Objekten des Typs [TaxPayer] erstellen können;
- Zeilen 84–91: Wir verarbeiten die Liste der empfangenen Wörterbücher;
- Zeile 86: Aus einem Wörterbuch erstellen wir ein [TaxPayer]-Objekt;
- Zeile 89: Wir berechnen die Steuer für diesen [TaxPayer];
- Zeile 91: Wir wissen, dass [TaxPayer] durch die Steuerberechnung geändert wurde. Wir wandeln es in ein Wörterbuch um und fügen es einer Ergebnisliste hinzu;
- Zeile 93: Diese Ergebnisliste wird an den Client gesendet;
28.2.4. Server-Test
Wir werden den Server mit einem Postman-Client testen:
- Wir starten den Webserver, das DBMS und den Mailserver [hMailServer];
- Wir starten den Postman-Client und dessen Konsole (Strg-Alt-C);

- in [1]: Wir senden eine [POST]-Anfrage;
- in [2]: die URL des Servers;
- in [3]: der Textkörper der HTTP-Anfrage;
- in [5]: Wir geben an, dass dieser Textkörper als JSON-Zeichenkette gesendet werden soll;
- in [4]: Wir wechseln in den [raw]-Modus, um eine JSON-Zeichenkette kopieren und einfügen zu können;
- in [6]: Fügen Sie die JSON-Zeichenkette ein, die Sie aus einer der [results.json]-Dateien für die verschiedenen Versionen entnommen haben. Behalten Sie dann für jeden Steuerzahler nur die Eigenschaften [married, salary, children] bei;

- in [7] sehen wir uns die HTTP-Header an, die der Postman-Client an den Server senden wird;
- in [8] sehen wir, dass er einen [Content-Type]-Header sendet, der angibt, dass die Anfrage einen JSON-kodierten Body enthält. Dies ist auf die zuvor in [5] getroffene Auswahl zurückzuführen;

- in [9-12]: Wir fügen die vom Server erwarteten Anmeldedaten in die Anfrage ein;
Wir senden diese Anfrage. Die Antwort des Servers lautet wie folgt:

- in [3] haben wir JSON erhalten;
- in [4] die Steuer der Steuerzahler;
Sehen wir uns den Client-Server-Dialog an, der in der Postman-Konsole (Strg-Alt-C) stattgefunden hat:
Der Postman-Client hat den folgenden Text gesendet:
- Zeile 1: die POST-Anfrage an den Server;
- Zeile 2: der HTTP-Authentifizierungsheader;
- Zeile 3: Der Client teilt dem Server mit, dass er eine JSON-Zeichenkette sendet und dass diese Zeichenkette 824 Byte lang ist (Zeile 11);
- Zeilen 13–69: der JSON-Body der Anfrage;
Der Server antwortete mit folgendem Text:
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
- Zeile 1: Die Anfrage war erfolgreich;
- Zeile 2: Der Hauptteil der Serverantwort ist eine JSON-Zeichenkette. Sie ist 1461 Byte lang (Zeile 3);
- Zeile 7: die JSON-Antwort des Servers;
Testen wir nun einige Fehlerfälle.
Fall 1: Wir senden irgendetwas
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3
abc
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
- Zeile 13: Die Zeichenkette [abc] wurde gesendet, die keine gültige JSON-Zeichenkette ist (Zeile 3);
- Zeile 15: Der Server antwortet mit einem 400-Fehlercode;
- Zeile 21: Die JSON-Antwort des Servers;
Fall 2: Senden wir eine gültige JSON-Zeichenkette, die keine Liste ist
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17
{"att1":"value1"}
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}
Fall 3: Senden wir eine JSON-Zeichenkette, die eine Liste ist, deren Elemente nicht alle Wörterbücher sind
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7
[0,1,2]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT
{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}
Fall 4: Senden wir eine Liste von Dictionaries mit einem Dictionary, das nicht die richtigen Schlüssel enthält
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19
[{"att1":"value1"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT
{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}
Fall 5: Senden wir eine Liste von Wörterbüchern, wobei ein Wörterbuch fehlende Schlüssel enthält:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18
[{"marié":"oui"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT
{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}
Fall 6: Senden wir eine Liste von Wörterbüchern, wobei ein Wörterbuch die richtigen Schlüssel enthält, einige jedoch falsche Werte:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46
[{"marié":"x", "enfants":"x", "salaire":"x"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT
{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}
28.3. Der Web-Client

Die Datei [http-clients/05] (Version 10) wird zunächst durch Kopieren der Datei [http-clients/02] (Version 7) erstellt. Anschließend wird sie modifiziert.
28.3.1. Die [dao]-Schicht
Die [dao]-Schicht wird durch die folgende Klasse [ImpôtsDaoWithHttpClient] implementiert:
- Zeilen 1–26: Der Code bleibt derselbe wie in Version 7 und anderen Versionen;
- Zeilen 27–70: Es wird eine neue Methode [calculate_tax_in_bulk_mode] eingeführt, deren Zweck darin besteht, die Steuer für eine Liste von Steuerzahlern zu berechnen;
- Zeile 28: [taxpayers] ist diese Liste von Steuerzahlern;
- Zeilen 31–39: Wir wandeln eine Liste von [TaxPayer]-Objekten mithilfe der Funktion [map] in eine Liste von Wörterbüchern um;
- Zeilen 34–38: Die verwendete Lambda-Funktion wandelt ein Objekt vom Typ [TaxPayer] in ein Wörterbuch vom Typ [dict] um, das nur die Schlüssel [married, children, salary] enthält. Dazu verwenden wir den Parameter [included_keys] aus der Methode [BaseEntity.asdict]. Beachten Sie, dass Sie das vordefinierte Wörterbuch [taxpayer.__dict__] verwenden müssen, um die genauen Namen der Eigenschaften zu ermitteln, die in die Parameter [excluded_keys, included_keys] aufgenommen werden sollen;
- Zeilen 41–48: Verbindung zum Server herstellen und dessen HTTP-Antwort abrufen;
- Zeilen 44, 48:
- Wir verwenden die statische Methode [requests.post], um eine POST-Anfrage an den Server zu senden;
- Der Parameter [json] wird verwendet, um anzugeben, dass der POST-Body eine JSON-Zeichenkette ist. Dies hat zwei Konsequenzen:
- Das dem benannten Parameter [json] zugewiesene Objekt, in diesem Fall eine Liste von Dictionaries, wird in eine JSON-Zeichenkette konvertiert;
- der Header
wird in die HTTP-Header des POST-Aufrufs aufgenommen;
- Zeile 59: Die JSON-Antwort des Servers wird in das [result]-Dictionary deserialisiert;
- Zeilen 61–63: Vom Server gesendete Fehler werden behandelt;
- Zeile 65: Die Ergebnisse der Steuerberechnung befinden sich in einer Liste von Dictionaries;
- Zeilen 67–69: Diese Ergebnisse werden verwendet, um die ursprüngliche Liste der Steuerzahler [taxpayers] zu aktualisieren, die ursprünglich von der Methode in Zeile 28 empfangen wurde;
- Zeile 70: Hier wurde die ursprüngliche Liste der Steuerzahler mit den Ergebnissen der Steuerberechnung aktualisiert;
28.3.2. Das Hauptskript [main]
Das Hauptskript [main] entwickelt sich wie folgt: Nur die Funktion [thread_function], die von den vom Client erstellten Threads ausgeführt wird, wird geändert. Der Rest des Codes bleibt unverändert.
- Zeilen 9–10: Während wir zuvor eine Schleife hatten, die jeden Steuerzahler nacheinander an die Methode [dao.calculate_tax] übergab, rufen wir hier die Methode [dao.calculate_tax_in_bulk_mode] einmalig auf und übergeben ihr alle Steuerzahler;
28.3.3. Client-Ausführung
Wir werden die Ausführungszeiten der Versionen vergleichen:
- 7, bei der jeder Steuerzahler Gegenstand einer HTTP-Anfrage ist;
- 10 (diese hier), bei der die Steuerzahler in einer einzigen HTTP-Anfrage zusammengefasst werden;
Zunächst Version 6. Um die beiden Versionen zu vergleichen, setzen wir die Eigenschaft [sleep_time] des Servers auf Null, damit keine erzwungene Wartezeit für Threads entsteht. Die Client-Protokolle lauten wie folgt:
2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
…
2020-07-28 14:20:45.913065, Thread-3 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]
Die Ausführungszeit des Clients zur Berechnung der Steuer für 11 Steuerzahler beträgt somit [913065-811347= 101718], d. h. etwa 102 Millisekunden.
Machen wir dasselbe mit Version 10 (Server-Sleep-Zeit auf Null gesetzt). Die Client-Protokolle sehen dann wie folgt aus:
2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables
Die Ausführungszeit des Clients für die Berechnung der Steuer für 11 Steuerzahler beträgt somit [935958-871428= 64530 ns] (Zeile 8 – Zeile 1), d. h. etwa 65 Millisekunden. Diese neue Version 10 bietet somit einen Leistungsgewinn von etwa 57 % gegenüber Version 7.
28.3.4. Tests der [dao]-Schicht des Clients

Der [TestHttpClientDao]-Test für den Client in Version 10 ist dem in Version 7 sehr ähnlich:
- Zeile 14: Anstatt die Methode [dao.calculate_tax] aufzurufen, rufen wir die Methode [dao.calculate_tax_in_bulk_mode] auf und übergeben ihr eine Liste (durch eckige Klammern gekennzeichnet) von Steuerzahlern;
Alle Tests sind bestanden.