Skip to content

29. تمرین عملی: نسخه ۱۱

29.1. مقدمه

در نسخه‌های قبلی اپلیکیشن محاسبه مالیات مبتنی بر کلاینت/سرور، لایه [métier] که قوانین کسب‌وکار این محاسبه را پیاده‌سازی می‌کند، در سمت سرور قرار داشت. اکنون پیشنهاد می‌کنیم آن را به سمت کلاینت منتقل کنیم. مزیت آن چیست؟ بخشی از کارهایی که قبلاً توسط سرور انجام می‌شد، به سمت کلاینت منتقل خواهد شد. اگر سناریویی را در نظر بگیریم که در آن یک سرور توسط N کلاینت مورد پرس‌وجو قرار می‌گیرد، N محاسبه تجاری مالیاتی توسط کلاینت‌ها انجام خواهد شد. در نسخه‌های قبلی، سرور این N محاسبه تجاری را انجام می‌داد. از آنجایی که دیگر محاسبات تجاری را انجام نمی‌دهد، سرور سریع‌تر به کلاینت‌های خود پاسخ خواهد داد و بنابراین قادر خواهد بود همزمان به تعداد بیشتری از آنها خدمات ارائه دهد.

معماری کلاینت/سرور اکنون به شرح زیر است:

Image

  • لایه [métier] [10] به صورت [12] روی کلاینت کپی شده است؛
  • یک اسکریپت جدید، [main2] [11]، به کلاینت اضافه شده است؛

کلاینت وب دو روش برای محاسبه مالیات برای فهرست مودیان مالیاتی یافت‌شده در [3] خواهد داشت:

  • از روش نسخه قبلی استفاده کند. این روش از لایه سرور [métier] [10] استفاده می‌کند. اسکریپت [main] از این روش استفاده خواهد کرد؛
  • به سادگی داده‌های مرجع مالیاتی را از سرور ([2-4]) درخواست کرده و سپس از لایه سمت کلاینت ([métier] و [12]) استفاده کنید؛

عملکرد این دو روش را مقایسه خواهیم کرد.

29.2. سرور وب

ساختار دایرکتوری وب‌سرور به شرح زیر خواهد بود:

Image

  • پوشه [http-servers/06] در ابتدا با کپی کردن پوشه [http-servers/05] ایجاد می‌شود. در واقع، ما ویژگی‌های نسخهٔ قبلی ۱۰ را حفظ خواهیم کرد. ما صرفاً یک ویژگی جدید به آن اضافه خواهیم کرد. این موضوع در گنجاندن یک کنترل‌کنندهٔ جدید، [get_admindata_controller] [1]، منعکس شده است. کنترل‌کننده دیگر، [calculate_tax_controllerصرفاً کنترل‌کننده قبلی [index_controller] است که نام آن تغییر کرده است؛

29.3. پیکربندی

سرور دو سرویس URL را ارائه خواهد داد:

  • [/calculate-tax] برای محاسبه مالیات برای فهرستی از مودیان که در بدنه یک POST ارسال شده است. بنابراین این معادل URL و [/] در نسخه قبلی 10 است؛
  • [/get-admindata] رشته jSON را که حاوی داده‌های مرجع مالیاتی است، بازمی‌گرداند؛

پیکربندی [config] هر یک از این ورودی‌های URL را با کنترل‌کننده‌ای که آن را پردازش می‌کند مرتبط می‌سازد:


    # فرهنگ کنترل‌کنندهٔ برنامهٔ وب
    import calculate_tax_controller, get_admindata_controller
    controllers = {"calculate-tax": calculate_tax_controller, "get-admindata": get_admindata_controller}
    config['controllers'] = controllers

29.4. اسکریپت اصلی [main]

اسکریپت اصلی [main] ساختار اسکریپت [main] نسخه قبلی را بازسازی می‌کند:


#منتظر یک پارامتر mysql یا pgres است
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# برنامه را پیکربندی کنید
import config
config = config.configure({'sgbd': sgbd})

# وابستگی‌ها


#بازیابی داده‌ها از مراجع مالیاتی
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:
    # خطا ثبت می‌شود
    …

# اگر خطایی رخ داده باشد، فرآیند متوقف می‌شود
if erreur:
    sys.exit(2)

# برنامه Flask می‌تواند شروع شود
app = Flask(__name__)

# کنترل‌کننده اصلی
def main_controller() -> tuple:
    # بازیابی اقدام درخواستی
    dummy, action=request.path.split('/')
    logger = None
    try:
        # لاگ‌گیر
        logger = Logger(config["logsFilename"])
        # ذخیره آن در پیکربندی مرتبط با نخ
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #درخواست ثبت می‌شود
        logger.write(f"[index] requête : {request}\n")
        # در صورت درخواست، نخ متوقف می‌شود
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #وقفه به‌صورت تصادفی تنظیم می‌شود، به‌طوری که برخی رشته‌ها قطع می‌شوند و برخی دیگر قطع نمی‌شوند
            aléa = randint(01)
            if aléa == 1:
                # لاگ قبل از مکث
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #مکث
                time.sleep(sleep_time)
        # درخواست توسط پردازشگر درخواست اجرا می‌شود
        controller = config['controllers'][action]
        résultat, status_code = controller.execute(request, config)
        #آیا خطای مرگبار رخ داده است؟
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # ایمیلی برای مدیر برنامه ارسال می‌شود
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # پاسخ ثبت می‌شود
        logger.write(f"[index] {résultat}\n")
        #ارسال پاسخ
        print(résultat)
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # در صورت امکان خطا را ثبت کنید
        if logger:
            logger.write(f"[index] {erreur}")
        # آماده‌سازی پاسخ برای مشتری
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        #ارسال پاسخ
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # فایل لاگ را ببندید اگر باز شده باشد
        if logger:
            logger.close()

#محاسبه مالیات
@app.route('/calculate-tax', methods=['POST'])
@auth.login_required
def calculate_tax():
    #کنترل به کنترل‌کننده اصلی واگذار می‌شود
    return main_controller()

# استخراج داده‌ها از سازمان مالیاتی
@app.route('/get-admindata', methods=['GET'])
@auth.login_required
def get_admindata():
    # واگذاری به حسابرس ارشد
    return main_controller()

#فقط کنترل
if __name__ == '__main__':
    # راه‌اندازی سرور
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • خطوط ۸۸–۹۳: تابع [calculate_taxURL و [/calculate-tax] را پردازش می‌کند؛
  • خطوط ۹۵–۱۰۰: تابع [get_admindata] توابع URL و [/get-admindata] را پردازش می‌کند؛
  • این دو تابع به خودی خود کاری انجام نمی‌دهند. آنها فوراً کنترل را در خطوط ۳۷–۸۶ به کنترل‌کننده اصلی [main_controller] واگذار می‌کنند؛
  • خطوط ۳۷–۸۶: کنترل‌کننده اصلی [main_controller] چیزی جز تابع [index] از نسخه قبلی نیست، با یک تفاوت جزئی: در حالی که تابع [index] تنها یک URL را پردازش می‌کرد، در اینجا [main_controller] دو URL را پردازش می‌کند. بنابراین باید این موارد توسط یکی از دو کنترل‌کننده [calculate_tax_controller, get_admin,data_controller] پردازش شوند؛
  • خطوط ۳۹–۴۰: اقدام درخواستی، [calculate_tax] یا [get_admindata]، بازیابی می‌شود. این اطلاعات در مسیر URL [request.path] گنجانده شده است. بسته به مورد، [request.path] یا [/get-admindata] یا [/calculate_tax] است. تقسیم خط ۴۰ دو عنصر تولید می‌کند:
    • رشتهٔ خالی برای بخشی که قبل از / قرار دارد؛
    • نام اکشن درخواستی برای بخش پس از /؛
  • خطوط ۶۲–۶۳: پس از بازیابی اقدام برای URL، می‌دانیم برای پردازش URL از کدام کنترل‌کننده استفاده کنیم. این اطلاعات در پیکربندی برای [config] گنجانده شده است؛

29.5. کنترل‌کننده‌ها

کنترل‌کننده [calculate_tax_controller] چیزی جز کنترل‌کننده [index_controller] از نسخه قبلی نیست.

کنترل‌کننده [get_admindata_controller] به شرح زیر است:


from flask_api import status
from werkzeug.local import LocalProxy

def execute(request: LocalProxy, config: dict) -> tuple:
    # بازگرداندن پاسخ
    return {"réponse": {"result": config["admindata"].asdict()}}, status.HTTP_200_OK
  • کنترل‌کننده‌های URL و [/get-admindata] باید رشته jSON را از داده‌های مرجع مالیاتی بازگردانند؛
  • خط ۶: این داده‌ها توسط اسکریپت اصلی [main] بازیابی شده و در دیکشنری [config] به شکل یک شیء [AdminData] قرار داده شده‌اند. ما دیکشنری این شیء را بازمی‌گردانیم؛

29.6. تست‌های Postman

ما سرور وب SGBD و سرور ایمیل [hMailServer] را راه‌اندازی می‌کنیم. سپس با استفاده از کلاینت Postman، مالیات چند مؤدی را محاسبه می‌کنیم:

Image

در کنسول Postman، مکالمه کلاینت/سرور به شرح زیر است:


POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[ 
{ 
"marié": "oui", 
"enfants": 2, 
"salaire": 55555 
}, 

{ 
"marié": "oui", 
"enfants": 3, 
"salaire": 200000 
} 
]

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: Wed, 29 Jul 2020 07:02:07 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…]}}

اکنون بیایید URL و [/get-admindata] را به همراه GET درخواست کنیم:

Image

گفت‌وگوی کلاینت/سرور در کنسول Postman به شرح زیر است:


GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT

{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}

29.7. کلاینت وب

Image

Image

پوشه [http-clients/06] در ابتدا با کپی کردن پوشه [http-clients/05] ایجاد می‌شود. کار اصلاح اساساً شامل موارد زیر است:

  • تغییر پیکربندی [config_layers] به گونه‌ای که اکنون شامل یک لایه [métier] است. پیش از این، این پیکربندی تنها یک لایه داشت، [dao]؛
  • افزودن یک متد جدید به لایه [dao]؛
  • نوشتن یک اسکریپت [main2] که برای محاسبه مالیات مودیان، به لایه [métier] مشتری متکی خواهد بود؛

29.7.1. پیکربندی لایه‌های کلاینت

پیکربندی لایه در دو نقطه انجام می‌شود:

  • در پیکربندی [config]، که باید در وابستگی‌های کلاینت، پوشه‌ای را که حاوی پیاده‌سازی لایه [métier] است، شامل شود. این پوشه قبلاً در وابستگی‌ها گنجانده شده بود:

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",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # اسکریپت‌های پیکربندی
        script_dir,
        # لاگ‌گیر
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

سپس فایل [config_layers] باید اصلاح شود:


def configure(config: dict) -> dict:
    # نمادین‌سازی لایه‌های کاربردی

    #لایه [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    #لایه DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    #لایه پیکربندی رندر می‌شود
    return {
        "dao": dao,
        "métier": métier
    }
  • خطوط ۴–۶: نمونه‌سازی لایه [métier]؛
  • خطوط ۱۳–۱۶: لایه [métier] به فرهنگ لغت لایه‌ها اضافه می‌شود؛

29.7.2. پیاده‌سازی لایه [dao]

Image

لایه [dao] دارای رابط زیر [InterfaceImpôtsDaoWithHttpClient] خواهد بود:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao

class InterfaceImpôtsDaoWithHttpClient(AbstractImpôtsDao):

    #محاسبه مالیات
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass
  • خط ۵: رابط [InterfaceImpôtsDaoWithHttpClient] از کلاس انتزاعی [AbstractImpôtsDao] که دسترسی به سیستم فایل مشتری را مدیریت می‌کند، ارث می‌برد. توجه داشته باشید که این کلاس یک متد انتزاعی [get_admindata] دارد؛
  • خطوط ۷–۱۰: متد [calculate_tax_in_bulk_mode] که در نسخه قبلی تعریف کردیم، امکان محاسبه مالیات برای فهرستی از مودیان را فراهم می‌کند؛

این رابط توسط کلاس زیر پیاده‌سازی شده است، [ImpôtsDaoWithHttpClient]:


# واردات‌ها

import json

import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpClient import InterfaceImpôtsDaoWithHttpClient

class ImpôtsDaoWithHttpClient(InterfaceImpôtsDaoWithHttpClient):

    # تولیدکننده
    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

    def get_admindata(self) -> AdminData:
        # اجازهٔ انتشار استثناها

        # URL سرویس
        config_server = self.__config_server
        config_services = config_server['url_services']
        url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"

        # اتصال
        if config_server['authBasic']:
            response = requests.get(url_service,
                                    auth=(
                                        config_server["user"]["login"],
                                        config_server["user"]["password"]))
        else:
            response = requests.get(url_service)

        # حالت اشکال‌زدایی؟
        if self.__debug:
            # لاگ‌گیر
            if not self.__logger:
                self.__logger = self.__config['logger']
            # لاگ‌گیری
            self.__logger.write(f"{response.text}\n")

        #
        status_code = response.status_code
        # نتیجه به صورت یک دیکشنری
        résultat = json.loads(response.text)
        # اگر کد وضعیت 200 نباشد OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(58, résultat['réponse']['erreurs'])
        # نتیجه را (یک دیکشنری) بازگردانید
        return AdminData().fromdict(résultat["réponse"]["result"])

    #محاسبه مالیات در حالت دسته‌ای
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # اجازه می‌دهد استثناءها منتقل شوند

        
  • خط ۱۳: کلاس [ImpôtsDaoWithHttpClient] رابط [InterfaceImpôtsDaoWithHttpClient] را پیاده‌سازی می‌کند. بنابراین از کلاس [AbstractImpôtsDao] ارث می‌برد؛
  • خطوط ۶۵–۶۶: متد [calculate_tax_in_bulk_mode] که در نسخه قبلی بررسی شده است؛
  • خطوط ۲۹–۶۲: متد [get_admindata]، که کلاس والد [AbstractImpôtsDao] آن را به صورت انتزاعی (abstract) اعلام کرده است. بنابراین، این متد در کلاس فرزند پیاده‌سازی می‌شود؛
  • خطوط ۳۳–۳۵: URL سرویس وب که متد [get-admindata] باید آن را پرس‌وجو کند، مشخص می‌شود. این سرویس‌های URL در پیکربندی [config] مشتری تعریف شده‌اند:

       # سرور محاسبه مالیات
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculate-tax",
                "get-admindata""/get-admindata"
            }
        },
  • (ادامه)
    • خطوط ۹–۱۲: دو ورودی سرویس وب‌سرور URL؛
  • خطوط ۳۷–۴۴: سرویس URL به‌صورت همگام پرس‌وجو می‌شود؛
  • خطوط ۴۶–۴۲: اگر پیکربندی ایجاب کند، پاسخ سرور ثبت می‌شود؛
  • خط ۵۷: می‌دانیم که سرور یک رشته jSON را از یک فرهنگ لغت ارسال کرده است؛
  • خطوط ۵۸–۶۰: اگر وضعیت پاسخ HTTP برابر با ۲۰۰ نباشد، یک استثنا پرتاب می‌شود؛
  • خطوط ۶۱–۶۲: شیء [AdminData] که شامل داده‌های مرجع مالیاتی ارسال‌شده توسط سرور است، بازگردانده می‌شود؛

29.8. اسکریپت‌های [main, main2]

اسکریپت [main] همان اسکریپت نسخه قبلی است. این اسکریپت از متد [calculate_tax_in_bulk_mode] در لایه [dao] استفاده می‌کند و در نتیجه از لایه سرور [métier] بهره می‌برد؛

اسکریپت [main2] همانند اسکریپت [main] عمل می‌کند اما از لایه سمت کلاینت [métier] استفاده می‌کند:


# پی‌کنفورم کردن برنامه

import config
config = config.configure({})

#وابستگی‌ها
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# کد
try:
    # لاگ‌گیر
    logger = Logger(config["logsFilename"])
    # آن را در پیکربندی ذخیره کنید
    config["logger"] = logger
    # شروع ثبت گزارش
    logger.write("début du calcul de l'impôt des contribuables\n")
    # لایه را بازیابی کنید [dao]
    dao = config["layers"]["dao"]
    #بازیابی مالیات‌دهندگان
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # مالیات‌دهندگان؟
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #داده‌ها را از مراجع مالیاتی بازیابی می‌کند
    admindata = dao.get_admindata()
    #محاسبه مالیات مودیان از طریق لایه [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    #نتایج در فایل jSON ذخیره می‌شوند
    dao.write_taxpayers_results(taxpayers)
# به جز BaseException به‌عنوان خطا:
# # نمایش خطا
# print(f"خطای زیر رخ داده است: {error}")
finally:
    # بستن لاگ‌گیر
    if logger:
        # پایان لاگ
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # بستن لاگر
        logger.close()
    # انجام شد
    print("Travail terminé...")
  • خطوط ۲۶–۲۷: داده‌ها از سرور سازمان مالیاتی بازیابی می‌شوند؛
  • خطوط ۲۸–۳۱: مالیات مودیان سپس به‌صورت محلی محاسبه می‌شود؛

29.9. آزمایش کلاینت

در هر یک از اسکریپت‌های [main, main2]، شروع و پایان اسکریپت را ثبت می‌کنیم. این کار به ما امکان می‌دهد زمان اجرای اسکریپت را محاسبه کنیم. بیایید چند پیش‌بینی انجام دهیم:

  • اسکریپت [main] از نسخه قبلی:
    • N نخ ایجاد می‌کند که به‌طور همزمان اجرا می‌شوند؛
    • هر نخ یک دسته از مودیان را پردازش می‌کند که برای آن‌ها مالیات را از طریق یک درخواست واحد به سرور محاسبه می‌کند؛
    • از آنجایی که N نخ به طور همزمان اجرا می‌شوند، درخواست N+1ام قبل از اینکه درخواست Nام پاسخ خود را دریافت کند، ارسال می‌شود. بنابراین، N درخواست هزینه بیشتری نسبت به یک درخواست تکی دارد، اما احتمالاً نه به میزان زیاد. همچنین 11 محاسبه تجاری (تعداد مودیان) روی سرور انجام می‌شود؛
  • اسکریپت [main2] در این نسخه:
    • یک درخواست واحد به سرور ارسال می‌کند؛
    • ۱۱ محاسبه تجاری را به‌صورت محلی روی کلاینت انجام می‌دهد؛

محاسبات تجاری چه روی سرور و چه روی کلاینت انجام شوند، به همان میزان زمان نیاز دارند. بنابراین تفاوت در تعداد درخواست‌ها خواهد بود. از این رو، می‌توان انتظار داشت که زمان اجرای [main] کمی بیشتر از [main2] باشد.

ما سرور نسخه ۱۱، SGBD، و سرور ایمیل، [hMailServer] را راه‌اندازی می‌کنیم. در سمت سرور، پارامتر [sleep_time] را روی صفر تنظیم می‌کنیم تا هر دو تست تحت شرایط یکسان اجرا شوند.

اجرای اول: [main]

اجرای [main]، گزارش‌های زیر را تولید می‌کند:


2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"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}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"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-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"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}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"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}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"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}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables

زمان اجرا [051214-016079] نانوثانیه بود (خط 17 – خط 1)، یعنی 35 میلی‌ثانیه و 135 نانوثانیه.

می‌توانیم ببینیم که بین اولین درخواست ارسال‌شده به سرور و آخرین پاسخ دریافتی توسط کلاینت، مدت زمان یکسان است: [051214-016079] (خط 15 – خط 1)، 35 میلی‌ثانیه و 135 نانوثانیه.

اجرای دوم: [main2]

اجرای [main2] گزارش‌های زیر را تولید می‌کند:


2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables

زمان اجرای [349975-303520] نانوثانیه (خط ۳ – خط ۱) بود، یعنی ۴۶ میلی‌ثانیه و ۴۵۵ نانوثانیه. به‌طور غیرمنتظره‌ای، [main] سریع‌تر از [main2] است.

می‌توانیم ببینیم که یک پرس‌وجوی تکی برای [main2] به [345084-303520] (خط ۲ – خط ۱)، یعنی ۴۱ میلی‌ثانیه و ۵۶۴ نانوثانیه زمان برد. سپس محاسبه مالیات [349975-345084] (خط ۳ – خط ۲)، یعنی ۴ میلی‌ثانیه و ۹۱ نانوثانیه طول کشید. این درخواست HTTP است که زمان اجرای آن را تشکیل می‌دهد. شگفت‌آور است که در اینجا می‌توانیم ببینیم یک درخواست تکی برای [main2] ([41 millisecondes]) بیشتر از چهار درخواست همزمان برای [main] و [35 millisecondes] طول کشید.

در سمت سرور، لاگ‌ها به شرح زیر است:


2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'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-29 14:35:50.045796, Thread-3 : [index] {'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}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'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}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'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}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'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}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
  • خط ۵: اولین درخواست از کلاینت [main];
  • خط ۱۴: آخرین پاسخ به کلاینت [main]. بین این دو ۶ میلی‌ثانیه و ۶۴۷ نانوثانیه فاصله است؛
  • خطوط ۱۵–۱۶: تنها درخواست از کلاینت [main2]. پاسخ آنی است؛