25. 应用练习:第8版
25.1. 简介
我们将编写一个新的客户端/服务器应用程序。该服务器的创新之处在于它将管理一个会话。我们将税务管理数据放入作用域为 [application] 的对象中,而是放入作用域为 [session] 的对象中。这样做会导致代码性能下降。 当一个对象可被所有用户以只读方式共享时,将其设为作用域为 [application] 的对象,比设为作用域为 [session] 的对象更为合适。 这样至少可以节省带宽,因为会减少会话 Cookie 的大小。但我们希望展示一个客户端/服务器应用程序,其中客户端和服务器之间会交换会话 Cookie。
应用程序的架构保持不变:

25.2. Web 服务器
服务器脚本的目录结构如下:

文件夹 [http-servers/03] 最初是通过复制文件夹 [http-servers/02] 获得的。随后对其进行修改。
25.2.1. 配置
配置与|上一版本|相同,仅在脚本 [config] 中进行了少量修改:
# 绝对依赖项
absolute_dependencies = [
# 项目文件夹
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# 常量、区间
f"{root_dir}/impots/v05/entities",
# index_controller
f"{script_dir}/../controllers",
# 脚本 [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
]
- 第 17 行:我们将重写一个控制器,用于处理 [index] 函数(该函数处理 URL);
- 第 21 行:使用 |上一版本| 中的实用程序;
25.2.2. 主脚本 [main]
新脚本 [main] 对上一版本的主脚本 [main] 进行了一些修改:
# Flask 应用程序可以启动
app = Flask(__name__)
# 会话密钥
app.secret_key = os.urandom(12).hex()
- 第 4 行:为应用程序创建一个密钥。众所周知,该密钥是管理会话所必需的;
此外,[main] 代码中不再要求提供税务数据。以下行已被删除:
# 从税务局获取数据
erreur = False
try:
# admindata 将作为应用程序范围内的只读数据
config["admindata"] = config["layers"]["dao"].get_admindata()
# 成功日志
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# 记录错误
erreur = True
# 错误日志
log = f"L'erreur suivante s'est produite : {ex}"
# 控制台
print(log)
# 日志文件
logger.write(f"{log}\n")
# 向管理员发送邮件
send_adminmail(config, log)
此外,[index_controller] 控制器支持一个额外参数,即 Flask 会话:
from flask import request, Flask, session
….
# 由控制器执行该请求
résultat, status_code = index_controller.execute(request, session, config)
25.2.3. 控制器 [index_controller]
控制器 [index_controller] 现支持会话管理:
# 导入依赖项
import os
import re
import threading
from flask_api import status
from werkzeug.local import LocalProxy
# URL 参数设置:/?已婚=xx&子女=yy&工资=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# 依赖项
from TaxPayer import TaxPayer
# 初始时无错误
erreurs = []
…
# 出现错误?
if erreurs:
# 向客户返回错误响应
return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST
# 无错误,可以继续工作
# 获取与该线程关联的配置
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# 执行请求
réponse = None
try:
# 最简单的情况——admindata 已处于会话中
if session.get('client_id') is not None:
# 获取会话信息
client_id = session.get('client_id')
admindata = AdminData().fromdict(session.get('admindata'))
# 日志
logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
else:
# 检索税务管理数据
admindata = config["layers"]["dao"].get_admindata()
# 为 admindata 建立会话
session['admindata'] = admindata.asdict()
# 为客户分配编号并将该编号存入会话
# 这将使我们能够在服务器日志中追踪该客户
client_id = os.urandom(12).hex()
session['client_id'] = client_id
# 日志
logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
# 计算税款
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# 将响应返回给客户
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, ImpôtsError) as erreur:
# 向客户端返回响应
return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- 第14行:控制器接收来自Web客户端的当前会话;
- 第35-38行:如果客户端拥有会话,则该会话包含两个键:
- [client_id]:客户编号(第37行);
- [admindata]:以字典形式存储的税务管理数据(第38行);
- 第 35 行:检查会话是否包含预期的两个键之一;
- 第42-51行:处理客户端会话尚未初始化的情况;
- 第43行:从[dao]层获取税务部门的数据;
- 第45行:将这些数据以字典形式存入会话;
- 第48行:为客户分配一个随机编号。该编号对每个客户而言都是唯一的;
- 第49行:将该编号存入会话;
- 第51行:记录税务数据已通过[dao]层获取。访问[dao]层通常成本较高,因此需加以限制。 此处的思路是:先从 [dao] 层获取税务数据一次,将其存入会话,并在同一客户的后续请求中从中读取。需提醒的是,这并非最佳解决方案。由于税务部门的数据对所有客户都相同,应将其存放在应用程序作用域的对象中;
- 第35-40行:处理客户会话已在先前请求中初始化的情况;
- 第 37 行:从会话中获取客户编号;
- 第38行:从会话中获取税务部门的数据;
- 第40行:记录客户已在会话中获取税务部门数据;
25.3. Web客户端

25.3.1. [dao] 层
25.3.1.1. 类 [ImpôtsDaoWithHttpSession]
[dao] 层由以下 [ImpôtsDaoWithHttpSession] 类实现:
# 导入
import requests
from flask_api import status
from myutils import decode_flask_session
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):
# 构造函数
def __init__(self, config: dict):
# 父级初始化
AbstractImpôtsDao.__init__(self, config)
# 保存配置项
# 通用配置
self.__config = config
# 服务器
self.__config_server = config["server"]
# 调试模式
self.__debug = config["debug"]
# 日志记录器
self.__logger = None
# Cookie
self.__cookies = None
# 未使用的方法
def get_admindata(self) -> AdminData:
pass
# 税额计算
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# 允许异常上报
# GET 参数
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# 使用 Basic 认证登录?
if self.__config_server['authBasic']:
response = requests.get(
# 被查询服务器的 URL
self.__config_server['urlServer'],
# URL 的参数
params=params,
# Basic 身份验证
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]),
cookies=self.__cookies)
else:
# 不使用 Basic 身份验证的连接
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# 如果响应中包含 Cookie,则获取它们
if response.cookies:
self.__cookies = response.cookies
# 获取会话 Cookie
session_cookie = response.cookies.get('session')
# 对其进行解码以完成登录
if session_cookie:
# 登录器
if not self.__logger:
self.__logger = self.__config['logger']
# 进行日志记录
self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
# 调试模式?
if self.__debug:
# 日志记录器
if not self.__logger:
self.__logger = self.__config['logger']
# 正在记录日志
self.__logger.write(f"{response.text}\n")
# 响应状态码 HTTP
status_code = response.status_code
# 将响应 jSON 放入字典
résultat = response.json()
# 如果状态码不为 200,则报错 OK
if status_code != status.HTTP_200_OK:
# 已知错误已关联至响应中的键 [erreurs]
raise ImpôtsError(87, résultat['réponse']['erreurs'])
# 已知该结果与响应中的键 [result] 相关联
# 使用该结果修改输入参数
taxpayer.fromdict(résultat["réponse"]["result"])
- 第 30 行:[dao] 层将管理一个 Cookie 字典;
- 第 58 行:属性 [response.cookies] 是一个字典,用于收集服务器在 HTTP 和 [Set-Cookie] 头部中发送的 Cookie;
- 第 59 行:这些 Cookie 存储在 [dao] 层中。当同一客户端后续发出请求时,它们将被发回给服务器;
- 第60-68行:尽管并非必需,但我们仍会获取会话Cookie。在服务器发送的Cookie字典中,会话Cookie与键[session]相关联;
- 第 62-68 行:解码会话 Cookie 以完成登录;
- 第 68 行:我们稍后将详细介绍用于解码会话 Cookie 的函数 [decode_flask_session];
- 第 52 行和第 57 行:每次来自同一客户端的请求,服务器都会将发送的 Cookie 发回给该客户端。正是通过这种方式,客户端与服务器之间的 Flask 会话得以维持;
现在需要记住的是,[dao] 层将由多个线程同时执行。因此,必须检查类实例的所有属性,并确认对这些属性的并发访问是否会引发问题。 在此我们添加了 [self.__cookies] 属性(第 30 行)。该属性在第 59 行被修改。因此,所有线程都在对同一共享数据进行写入操作。但这种访问方式会引发问题:代表特定客户端的每个线程都有其专属的会话 Cookie。 实际上,其中包含每个客户(即线程)的唯一客户编号。如果不采取任何措施,线程 T2 可能会覆盖线程 T1 的 Cookie。
我们之前已经见过一种解决此问题的方法:可以在作为构造函数参数传递的文件 [config](第 17 行)中,为每个线程创建不同的键。例如,可以使用线程名称作为键:
- 第 59 行,可以这样写:
config[thread_name][‘cookies’]=cookies
- 第 52 行,则可写为:
cookies=config[thread_name][‘cookies’]
这里我们将采用一种不同的技术:每个线程(=客户端)将拥有自己的 [dao] 层。这样第 59 行就不再有问题,因为此时使用的 Cookie 属于单一客户端。
为此,我们将创建一个新的类 [ImpôtsDaoWithHttpSessionFactory]。
25.3.1.2. Flask会话解码函数
函数 [decode_flask_session] 在脚本 [myutils] 中定义:

我们之前已经学习过 |myutils| 脚本。该脚本是一个机器作用域模块,本课程中的各个脚本均可通过以下语句将其导入:
其中,[decode_flask_session] 函数的定义如下:
def decode_flask_session(cookie: str) -> str:
# 来源:https://www.kirsle.net/wizards/flask-session.cgi
compressed = False
payload = cookie
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data.decode("utf-8")
- 第2行:URL,我就是在那里找到这个功能的;
- 第 1 行:参数 [cookie] 是与 Web 客户端接收的 Cookie 字典中密钥 [session] 关联的字符串;
- 第3-16行:对于这段我不熟悉的代码,我将不予评论;
在文件 [__init__.py] 中添加新的导入语句:
from .myutils import set_syspath, json_response, decode_flask_session
在 PyCharm 终端中使用命令 [pip install .],将 [myutils] 的新版本安装到机器作用域模块中:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- 第 1 行:必须位于 [packages] 文件夹中才能输入此指令;
25.3.1.3. 类 [ImpôtsDaoWithHttpSessionFactory]
[ImpôtsDaoWithHttpSessionFactory] 类如下:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# 存储该参数
self.__config = config
def new_instance(self):
# 返回 [dao] 层的实例
return ImpôtsDaoWithHttpSession(self.__config)
- 类 [ImpôtsDaoWithHttpSessionFactory] 允许基于第 10-12 行中的方法 [new_instance],创建 [dao] 层的新实现;
25.3.2. 配置
用于配置 Web 客户端层的脚本 [config_layers] 已按以下方式修改:
def configure(config: dict) -> dict:
# 实例化应用程序的层
# DAO图层
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# 进行图层配置
return {
"dao_factory": dao_factory
}
- 第 5-6 行:不再像之前那样实例化一个唯一的 [dao] 层,而是实例化该层的“工厂”(factory=对象生成工厂,此处指 [dao] 层);
- 第9-11行:返回层配置;
25.3.3. 客户端的主脚本
[main]脚本与上一版本相比,变化如下:
# 配置应用程序
import config
config = config.configure({})
# 依赖项
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# 在单线程中执行 [dao] 层
# taxpayers 是一个纳税人列表
def thread_function(thread_dao, logger, taxpayers: list):
…
# 客户端线程列表
threads = []
logger = None
# 代码
try:
…
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
…
# 每个线程都必须拥有自己的 [dao] 层,以便正确管理其会话 Cookie
thread_dao = dao_factory.new_instance()
# 创建线程
thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
# 将其添加到主脚本的线程列表中
threads.append(thread)
# 启动线程——此操作为异步操作——不等待线程的结果
thread.start()
# 主线程等待其启动的所有线程结束
…
except BaseException as erreur:
# 显示错误
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# 关闭日志记录器
if logger:
logger.close()
# 操作完成
print("Travail terminé...")
# 如果因错误而中止,则结束可能仍存在的线程
sys.exit()
- 第29-30行:每个线程都有自己的[dao]图层;
25.3.4. 客户端的执行
启动 Web 服务器,启动 SGBD,启动邮件服务器 [hMailServer]。随后启动 Web 客户端脚本 [main]。 此时 [data/logs/logs.txt] 中的执行日志如下:
2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.537161, Thread-6 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : 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-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
- 总共有 6 个线程,即 6 个客户端(第 1、3、4、6、8、9 行)同时向税费计算服务器发起查询;
- 我们将关注管理 3 名纳税人的线程 [Thread-4](第 6 行)。该线程将依次向税款计算服务器发出三次请求;
- 第10行:[Thread-4]的首次请求;
- 第13行:[Thread-4] 收到了其首次请求的响应。其中包含一个会话cookie,内含服务器分配给它的编号 [fa3c83b82761c83e13217967];
- 第14行:第1位纳税人的税款;
- 第16行:[Thread-4] 向系统发送了关于第二位纳税人的请求;
- 第43行:[Thread-4] 接收了第二位纳税人的税款;
- 第45行:[Thread-4] 查询第三位纳税人;
- 第49行:[Thread-4] 接收第三位纳税人的税款;
- 第51行:[Thread-4] 已完成工作;
现在,让我们看看 [Thread-4] 的 3 个请求在服务器端是如何处理的。通过其客户端编号 [fa3c83b82761c83e13217967],我们可以在服务器日志中追踪其处理过程。
服务器端的 [data/logs/logs.txt] 日志如下:
2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=3&工资=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=2&工资=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=否&子女=0&工资=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=2&工资=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=否&子女=3&工资=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=3&工资=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=3&工资=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=否&子女=2&工资=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=2&工资=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=否&子女=0&工资=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=5&工资=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
- 第14行首次出现客户端[fa3c83b82761c83e13217967]:为了计算税款,服务器必须从数据库中检索税务部门的数据;
- 随后在第36行再次出现客户端[fa3c83b82761c83e13217967]。这一次,服务器在会话中找到了税务管理数据,从而避免了可能耗费资源的对[dao]层的访问;
- 在第42行第三次遇到客户端[fa3c83b82761c83e13217967],此时服务器再次利用了该客户端的会话;
此示例充分展示了会话对客户端的价值:其中存储了该客户端所有请求共用的数据,而这些数据的获取成本较高。
在客户端,文件 [data/output/résultats.json] 中的结果与前几个版本相同。
25.4. [dao] 层的测试
与|前几个版本|一样,我们对客户端的 [dao] 层进行测试:

该测试类将在以下环境中运行:

- [2] 的配置与我们刚刚研究的 [1] 配置完全相同;
测试类 [TestHttpClientDao] 如下所示:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'已婚': '是', '子女': 2, '工资': 55555,
# '税款': 2814, '附加费': 0, '减免': 0, '减免额': 0, '税率': 0.14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# 验证
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
if __name__ == '__main__':
# 配置应用程序
import config
config = config.configure({})
# 日志记录
logger = Logger(config["logsFilename"])
# 将其存储在配置中
config["logger"] = logger
# 获取 [dao] 层的工厂
dao_factory = config["layers"]["dao_factory"]
# 创建 [dao] 层的实例
dao = dao_factory.new_instance()
# 执行测试方法
print("tests en cours...")
unittest.main()
- 为该测试创建一个 |运行配置|;
- 启动包含完整环境的 Web 服务器;
- 执行测试;
结果如下:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0