Skip to content

25. 应用练习:第8版

25.1. 简介

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

应用程序的架构保持不变:

Image

25.2. Web 服务器

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

Image

文件夹 [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客户端

Image

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] 中定义:

Image

我们之前已经学习过 |myutils| 脚本。该脚本是一个机器作用域模块,本课程中的各个脚本均可通过以下语句将其导入:

import 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] 层进行测试:

Image

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

Image

  • [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