مانتی پایتون-پرده اول-polling


مقدمه:

وقتی که درخواستی از سمت مرورگر به سمت سرور ارسال می‌شود، مرورگر رفرش می‌شود. یعنی برای هر درخواست ارسالی به سمت سرور، مرورگر رفرش می‌شود تا پاسخ ارسالی از جانب سرور را دریافت و پردازش کند. اما بهتر نیست که پروسه ارسال درخواست و دریافت پاسخ بدون رفرش شدن صفحه اتفاق بیافتد؟ این خواسته را می‌توان با کمک تکنیک‌های Ajax ی جامعه‌ی عمل پوشاند. به درخواست‌ها و پاسخ‌هایی از این نوع که بدون رفرش شدن صفحه ارسال و دریافت می‌شوند، اصطلاحاً polling گفته می‌شود. این مطلب در قالب یک نمایش ارائه خواهد شد. یک طرف این نمایش رها خواهد بود که توسعه‌ دهنده‌ای باتجربه یک شرکت نرم‌افزاری است و در طرف دیگر نیما خواهد بود که به تازگی به شرکت مذکور پیوسته است. رها قصد دارد شیوه‌ی ارسال درخواست و دریافت پاسخ بدون رفرش شدن صفحه را به نیما آموزش دهد.

پیش‌نیاز‌ها

برای ادامه پیش‌نیازهایی ضروری است. انتظار می‌رود که با پایتون راحت باشید. همچنین درکی ابتدایی از JavaScript داشته باشید. البته آشنایی با فریم‌ورک‌های پایتونی مانند Flask و کتابخانه‌ی Tornado شاید در ادامه‌ی کار ضروری به نظر برسد. تمام کدها بر روی سیستم عامل گنو/لینوکس توزیع کوبونتو اجرا شده‌اند.

چه چیزی خواهیم ساخت؟

صفحه‌ای خواهیم ساخت که فقط یک دکمه دارد و با کلیک بر روی دکمه درخواستی به سرور ارسال شده و پاسخ دریافت می‌شود و پاسخ حاوی لینک یک عکس است. سپس آدرس در یک تگ img قرار داده خواهد شد تا تصویر نمایش داده شود. تمام مراحل بدون رفرش شده صفحه انجام خواهد شد.

ایجاد یک API

رها: سلام نیما

نیما: سلام

رها: آماده‌ای که شروع کنیم؟

نیما: بله

رها: قدم اول ساخت یک API خواهد بود.

نیما: این API چه استفاده‌ای خواهد داشت؟

رها: هر بار که درخواستی ارسال می‌شود، درنهایت به این API خواهد رسید و پاسخی جی‌سانی به صورت

{"status": "success", "message": imageAddress.png}

خواهیم داشت.

نیما: به نظرم این کاری بیهوده است!

رها: چرا؟

نیما: خب از API های آماده مانند این استفاده کنید.

رها: بله شما درست می‌فرمایید ولی احتمالاً در چند روز آینده اینترنت قطع خواهد شد و ما نمی‌خواهیم که کارمان لنگ بماند. از آن گذشته این API از جانب دیگر توسعه‌دهندگان شرکت نیز استفاده خواهد شد.

نیما: خیلی خب. برای ساخت این API از چه فریم‌ورکی استفاده خواهیم کرد؟

رها: مهم نیست. ولی اکثر توسعه دهندگان شرکت ما با پایتون راحت هستند. به نظرم یکی از فریم‌ورک‌های پایتونی گزینه مناسبی باشد.

نیما: من فلسک را ترجیح می‌دهم.

رها: خب بیاید شروع کنیم. virtualenv و virtualenvwrapper را نصب شده دارید؟

نیما: نه

رها: تا من یک قهوه می‌نوشم، این دو ابزار را نصب کنید!

نیما: نصب شد.

رها: حالا یک محیط ایزوله برای API ایجاد می‌کنیم.

mkvirtualenv api

نظر من: با استفاده از mkproject یک پروژه ایجاد کنید. بدین ترتیب یک پوشه به صورت خودکار به یک محیط ایزوله بایند خواهد شد.

رها: بعد از ایجاد محیط ایزوله، باید فریم‌ورک فلسک را نصب کنیم. دستور نصب را می‌دانید؟

نیما: بله

pip install flask

رها: خیلی خب. یک پوشه به نام polling ایجاد می‌کنیم(شما به هر نامی که دوست داشتید، ایجاد کنید). بعد پوشه دیگری به نام api درون پوشه قبلی ایجاد می‌کنیم(شما به هر نامی که دوست داشتید، ایجاد کنید). در نهایت یک فایل به نام app.py ایجاد می‌کنیم و البته یک پوشه دیگر به نام static در کنار این فایل ایجاد می‌کنیم.

نیما: همه‌ی این‌ها را از قبل می‌دانستم!

رها: هممممم

رها: خب، اولین کاری که باید انجام دهیم ایجاد یک پوشه دیگر در داخل پوشه static به نام img است. عکس‌ها در این پوشه قرار می‌گیرند. ۲۰ یا ۳۰ عکس دانلود کنید و در این پوشه قرار دهید.

نیما: با چه فرمتی؟

رها: مهم نیست. ولی همه یک فرمت داشته باشند مثلاً png .

نیما: عکس‌ها را دانلود کردم و به این صورت نام گذاری کردم.

010.png  014.png  018.png  021.png  025.png  029.png  032.png  036.png  03.png   043.png  047.png  050.png  07.png
011.png  015.png  019.png  022.png  026.png  02.png   033.png  037.png  040.png  044.png  048.png  051.png  08.png
012.png  016.png  01.png   023.png  027.png  030.png  034.png  038.png  041.png  045.png  049.png  05.png   09.png
013.png  017.png  020.png  024.png  028.png  031.png  035.png  039.png  042.png  046.png  04.png   06.png

رها: خب. بیا شروع به نوشتن کنیم. محتوای فایل app.py این گونه خواهد بود:

import random

from flask import Flask, url_for


app = Flask(__name__)


@app.route("/polling/api/v1.0/rndimg")
def api():
    numbers = [str(number) for number in range(1, 52)]
    leading_zero_numbers = []
    for number in numbers:
        leading_zero_numbers.append(number.zfill(len(number)+1))

    random_number = random.choice(leading_zero_numbers)
    image = f"img/{random_number}.png"
    return {
        "status": "success", 
        "message": url_for(
                       "static", 
                       filename=image,
                       _external=True
        )}

رها: در خط اول پکیج استاندارد random را ایمپورت کردیم. در خط بعد Flask و url_for را از فلسک ایمپورت کردیم. در خط بعد نیز یک شی app ساختیم. و در نهایت یک function view به نام api ساختیم.

نیما:شاید route ی که برای این view نوشته شده است، خیلی طولانی باشد؟

رها: بله! ولی چون بعداً توسعه دهندگان دیگری از شرکت از این api استفاده خواهند کرد، بهتر است که آدرس به صورت استاندارد نوشته شود. بخش اول نام api است. بخش دوم کلمه‌ی api را ذکر می‌کنیم. بخش بعد مربوط است به ورژن api. و درنهایت کاری که از api درخواست می‌شود در انتها قرار می‌گیرد. با این روش، مدیریت endpoint ها راحت خواهد بود و هر آدرس در یک فضای نام است و تغییرات بعدی راحت‌تر انجام خواهد شد.

نیما: چرا یک لیست ۵۲ عنصره از اعداد ساختیم و بعد به رشته تبدیل کردیم؟

رها: چون شما ۵۲ عکس را دانلود کرده‌اید و آن‌ها را به صورت leading zero نام‌گذاری نموده‌اید. برای کار با این دست‌گل شما این لیست از اعداد را ساختیم. متوجه‌ هستید که این اعداد ساخته شده‌اند و به رشته تبدیل شده‌اند و هنوز leading zero نشده‌اند. برای این کار یک لیست خالی به نام leading_zero_numbers ساخته‌ایم و بعد با هر بار پیمایش عناصر لیست numbers و با استفاده از متد zfill اعداد(رشته‌های عددی) را leading zero می‌کنیم. در نهایت عنصری رندم از leading_zero_numbers انتخاب می‌کنیم و آدرس عکس را در متغیر image قرار می‌دهیم. در نهایت خروجی json را خواهیم داشت.

نیما: می‌دانم که url_for آدرس عکس‌ها را به دست می‌دهد اما کاربرد آرگومان _external چیست؟

رها: url_for آدرس نسبی را برگشت ‌می‌دهد. با آرگومان مذکور آدرس کامل به دست می‌آید. در ضمن آدرس نسبی را که خودمان هم داشتیم. آدرس نسبی آدرسی است که در متغیر image ذخیره شده است. به آدرس عکس‌ها در جایی دیگر نیاز خواهیم داشت که خارج از api است پس به آدرس کامل عکس‌ها نیاز خواهیم داشت.

نیما: چرا از Error Handling استفاده نکردیم؟

رها: گاماس گاماس.
به همین دلیل api را ورژن بندی کردیم! چگونگی اجرای این api را می‌دانید؟

نیما: بله

flask run

رها: یک نکته را فراموش کرده‌اید؟!! چون در محیط development هستیم، باید متغیرهایی محیطی را تنظیم کنیم تا reloader و debugger فعال شوند. البته در مورد api به debugger نیازی نداریم. علاوه بر آن باید app.py را به فریم‌ورک معرفی کنیم. پس باید دو متغیر محیطی را تنظیم کنیم.

export FLASK_APP=app.py
export FLASK_ENV=development

در زمان اجرا نیز با دستور --no-debugger عدم نیاز به دیباگر را به فریم‌ورک اعلام می‌کنیم. حالا سرور را اجرا می‌کنیم.

flask run --no-debugger

سرور بر روی پورت ۵۰۰۰ بالا می‌آید. البته می‌توانستیم پورت را تغییر دهیم.

نیما: خب. api آماده شده است. چطور می‌توانیم آن را تست کنیم؟

رها: چه ایده‌ای به ذهنت می‌رسد؟

نیما: اگر من باشم، برای تست این api از مرورگر استفاده نمی‌کنم!

رها: آفرین. در شرکت‌ ما برای تست api ها از دو ابزار استفاده می‌شود: curl و postman .
اولی خط فرمانی‌است و دومی گرافیکی. اما curl نیاز ما را برای تست این api برطرف می‌کند. curl را نصب شده دارید؟

نیما: همممم نه!

رها: تا من به این تماس پاسخ می‌دهم، curl را نصب کنید.

نیما: نصب شد.

رها: خیلی خب. با دستور زیر، api را تست می‌کنیم

curl http://127.0.0.1:5000/polling/api/v1.0/rndimg

خروجی جی‌سان به صورت زیر است:

{
  "message": "http://127.0.0.1:5000/static/img/023.png", 
  "status": "success"
}

پس api به درستی کار می‌کند.

نیما: برای تست این api، نصب curl زیاده‌روی نیست؟

رها: curl همچون حلقه‌ی ننیاست. اگر بر آن مسلط شوی،‌ قدرتش را به تو نشان خواهد داد.

خب نوشتن api تمام شد.

ایجاد سرور درخواست دهنده

کار بعدی نوشتن سروری برای فرستادن درخواست به api و دریافت پاسخ از آن است. چه فریم‌روکی را برای این‌ کار مناسب می‌دانید؟

نیما: شاید Django گزینه خوبی باشد؟

رها: بنابراین ما از Tornado استفاده خواهیم کرد!

pip install tornado

در همان پوشه polling یک پوشه دیگر به نام polling_server ایجاد می‌کنیم و یک فایل به نام server.py و دو پوشه به نام‌های templates و static درون آن ایجاد می‌کنیم. فایل server.py به صورت زیر خواهد بود

from urllib.request import urlopen

from tornado.web import RequestHandler, Application
from tornado.ioloop import IOLoop


class IndexHandler(RequestHandler):
    def get(self):
        self.render("foo.html")

    def post(self):
        data = ""
        with urlopen("http://127.0.0.1:5000/polling/api/v1.0/rndimg") as response:
            for line in response:
                data += line.decode("utf-8")

        self.write(data)


if __name__ == "__main__":
    app = Application(
        handlers=[
            (r"/", IndexHandler),
        ],
        template_path="templates",
        static_path="static",
        debug=True,
    )
    app.listen(8000)
    instance = IOLoop.instance()
    instance.start()

نیما(ملتمسانه): آیا امکان استفاده از جنگو وجود نداشت؟

رها: در خط اول برای ارسال درخواست و دریافت پاسخ urlopen را از urllib.request ایمپورت کرده‌ایم.

نیما: به خدا پکیجی راحت‌تر به نام requests برای این‌ کار وجود دارد. چرا از آن استفاده نکنیم؟!

رها: فرض کن یک هفته اینترنت قطع باشد و نتوانی پکیجی نصب کنی و تنها پکیج‌های استاندارد در دسترس باشند؟
سه خط بعدی چند کلاس و تابع را از تورنادو ایمپورت کرده‌ایم. RequestHandler شبیه یک فریم‌ورک وب عمل می‌کند و درخواست‌ها را دریافت، پردازش می‌کند و به درخواست‌ها پاسخ می‌دهد. Application کلاسی است که نمونه‌ای از آن را ایجاد می‌کنیم و در عمل با آن شی سرور را اجرا می‌کنیم. البته می‌توانیم کلاسی بسازیم که از Application ارث‌بری کندو … که فعلاً توضیح نمی‌دهم. IOLoop همان‌گونه که از نام آن بر‌می‌آید. حلقه‌ای را بر روی یک نخ برای گرفتن درخواست‌هایی که io bound هستند، ایجاد می‌کند. پس لابد متوجه شده‌ای که تورنادو برای کارهایی که با cpu سروکار دارند یا اصطلاحاً cpu bound هستند، چندان مناسب نیست. با Parallelism و Asynchronous آشنایی دارید؟

نیما: نه زیاد

رها: شاید این لینک و این لینک بتواند کمکی کند.
بعد یک کلاس به نام IndexHandler ایجاد کرده‌ایم که ازRequestHandler ارث بری می‌کند و دو متد get و post را برای مدیریت‌ درخواست‌ها نوشته‌ایم. متد get یک فایل html را رندر می‌کند. در متد post یک متغیر به نام data ایجاد کرده‌ایم. بعد با context manager ( لینک ) urlopen یک درخواست به api ایجاد می‌کنیم. پاسخی که از api برگشت داده می‌شود به صورت encode شده است. بنابراین با پیمایش خط به خط پاسخ و decode کردن هر خط و درنهایت پیوست هر خط به متغیر data، پاسخ درخواست را در متغیر data به صورت رشته‌ای خواهیم داشت. در نهایت با استفاده از متد write متغیر data به صورت جی‌سان به کلاینت فرستاده می‌شود. خطوط آخر هم مربوط به نمونه سازی از کلاس Application و نگاشت route به handler و نیز تعیین پوشه فایل‌های تمپلیت و فایل‌های استاتیک برای تورنادو است. در نهایت سرور بر روی پورت ۸۰۰۰ به درخواست‌ها پاسخ خواهد داد.

ایجاد صفحه Html

نیما: قدم بعدی چیست؟

رها: باید در پوشه‌ی template یک فایل html بسازیم. به همان نامی که در متد get برای رندر قرار دادیم. محتوای این فایل به صورت زیر است.

<!doctype html>
<html>
<head>
  <title>Foo</title>
  <script src="{{ static_url('js/script.js')  }}"></script>
</head>
</body>
    <button type="button" onclick="manage()" />Get</button>
    <img id="demo" style="display: none;" width=400 height=400 />
</body>
</html>

همان‌گونه که متوجه شده‌ای در قسمت head اسکریپتی را به صفحه اضافه کرده‌ایم. در ادامه یک دکمه خواهیم داشت که با کلیک شدن یک تابع جاوااسکریپتی اجرا خواهد شد و در نهایت یک تگ img با آی‌دی demo و استایلی برای مخفی شدن عکس.

نیما: عملکرد static_url به چه صورت است؟

رها: مشابه url_for فلسک است.

ایجاد اسکریپت JS

نیما: قدم بعدی ساخت فایل script.js در پوشه static است.

رها: آفرین. می‌توانیم اسکریپت را در پوشه static/js/ قرار دهیم که کدهایمان تمیزتر باشند.

محتوای فایل script.js به صورت زیر خواهد بود

function manage() {
    let xhrObject = false;
    let self = this;

    self.xhrObject = new XMLHttpRequest();
    self.xhrObject.open("POST", "/", true);
    self.xhrObject.onreadystatechange = function() {
        if (self.xhrObject.readyState == 4 && self.xhrObject.status == 200)        
updatePage(self.xhrObject.response);
} self.xhrObject.send(); } function updatePage(response) { response = JSON.parse(response); element = document.getElementById("demo"); element.src = response.message; element.style.display = "block"; }

معلوم است که تابعی به نام manage ساخته‌ایم. این همان تابعی است که با کلیک بر روی دکمه اجرا خواهد شد. بعد یک متغیر به نام xhrObject می‌سازیم و به آن مقدار اولیه false می‌دهیم. البته من برنامه نویس جاوااسکریپت نیستم.

نیما: خب خدا رو شکر!

رها: چی‌ی‌ی؟؟؟

نیما: هااااا. خب عملکرد خطوط بعدی به چه صورت است؟

رها: چون به استفاده از self عادت دارم، this را که به شی جاری اشاره می‌کند به متغیر self نسبت می‌دهم.

برای ارسال درخواست و دریافت پاسخ بدون رفرش صفحه از تکنیک‌های Ajax ی استفاده می‌شود که یکی از این تکنیک‌ها استفاده از شی XMLHttpRequest است.

یک نمونه از XMLHttpRequest می سازیم و آن را xhrObject نامگذاری می‌کنیم. در خط بعد متد open از شی را فراخوانی می‌کنیم. این متد اتصال به سرور را ایجاد می‌کند(اما هنوز درخواست فرستاده نشده است) این متد سه آرگومان دارد. اولی نوع HTTP Method است که من post قرار داده‌ام. دومی آدرسی است که درخواست به آن ارسال می‌شود و سومی نوع ارسال درخواست از نظر همزمانی و ناهمزمانی است که برای بهره‌مندی از خواص ناهمزمانی باید همواره true باشد(اگر true نباشد، ارسال درخواست در زمینه عملاً بلاموضوع است)
خط بعد یک شنودگر را تنظیم می‌کند. هر تغییری در خاصیت readyState شی xhrObject باعث اجرای یک تابع خواهد شد. البته خاصیت readyState پنج مقدار متفاوت دارد که نمایانگر وضعیت درخواست است. مقادیر readyState به صورت زیر هستند:

UNSENT                 -> 0
OPEND                  -> 1
LOADING                -> 2
HEADERS_RECEIVED       -> 3
DONE                   -> 4

اگر مقدار readyState برابر ۴ باشد و کد درخواست ۲۰۰ باشد، تابع callback اجرا خواهد شد. تابع کال‌بک به نوبه خود تابع updatePage را اجرا خواهد کرد و پاسخ را به عنوان آرگومان به آن پاس خواهد داد. این تابع نیز پاسخ را serialize خواهد کرد و src را برای تگ img مقدار دهی کرده و عکس را قابل مشاهده خواهد کرد.

نیما: ضرورت استفاده از تورنادو را متوجه نشدم؟

رها: تورنادو همچون شمشیر آندوریل است که با آن می‌توان هر کاری را انجام داد.

نتیجه

با استفاده از فلسک یک api ساختیم که در هر بار اجرا، آدرس یک عکس را به صورت json به ما می‌داد. با تورنادو یک سرور ایجاد کردیم تا درخواست‌ها را به api ارسال کند و در نهایت با تکنیک‌های ajaxی درخواست‌‌ها و پاسخ‌ها را بدون رفرش شدن صفحه فرستادیم و دریافت کردیم.


رپو پروژه

۰ نظر
طراح قالب : عرفـــ ـــان قدرت گرفته از بلاگ بیان