docs:lac_mac_9_wsgi_intro

مقدمة عن إطار الويب الماورائي ويز-جي WSGI

هذه المقالة نشرت في مجلة مجتمع لينكس العربي

تمهيد عن السلسلة

بمناسبة إطلاق مشروع إطار الويب المتواضع الخاص بمشروع ثواب والذي يحمل اسم الصحابي عكاشة بن محصن رضي الله عنه قررت أن أكتب سلسلة سنتعلم فيها ماذا توفر لنا لغة البرمجة بايثون من طرق لعمل تطبيقات ويب. في هذه الحلقة سأشرح إطار الويب الماورائي meta web framework للغة بايثون واسمه WSGI. وفي الحلقة القادمة (إن شاء الله) سنتحدث عن إطار الويب “عكاشة”.

ما قبل WSGI

هل تذكروم CGI

لا أعرف إن كان هناك من لا زال يستعملها لكن لا بأس من التعرف عليها. الطريقة التقليدية في عمل تطبيقات الويب التفاعلية هي أن يقوم الخادم بتنفيذ برنامج (غالبا سكربت موجود في cgi-bin) يمرر له ما يُرايد منه عبر متغيرات البيئة environment variables ثم يأخذ المخرجات التي تكون وفق معايير HTTP أي أنها ترويسات بينها سطر جديد ثم بعد الترويسات سطر جديد آخر ثم يبدأ محتوى الرد. مثلا لنكتب تطبيق CGI بلغة bash كما يلي:

#! /bin/bash
echo 'Content-Type: text/plain'
echo ''
echo 'Hello, world!'

هذه الطريقة تسمى CGI أي Common Gateway Interface وهي لا تتعلق بأي لغة معينة حيث تكون بأي لغة يمكن للخادم تنفيذها. أما التفاعلية فتأتي من أننا يمكننا أن نطبع ما نشاء مثلا الوقت أو نعمل cat لمحتويات ملف أو نتحدث مع قاعدة بيانات أو نعالج ما تم تمريره لنا عبر متغيرات البيئة مثلا المتغير QUERY_STRING يحمل الطلب (الذي يكون بعد علامة الاستفهام “؟” التي بعد اسم الملف) مثلا إن كان اسم الملف هو test.sh وتم زيارة http://localhost/cgi-bin/test.sh?text=foo&id=2 فإن قيمة ذاك المتغير هي text=foo&id=2 عيوب هذه الطريقة أنه مع كل طلب جديد يصل للخادم يتم تحميل برنامج وتنفيذه يعني لو قام 10 أشخاص بزيارة موقعك فإن البرنامج سيتم تنفيذه 10 مرات (في كل مرة تشعيب fork). ومن عيوبها كثرة الأعمال المملة التي عليك القيام بها لعمل أشياء بسيطة وكما نعلم كلما زاد التعقيد قل الأمن (مثلا قد تنس عمل escape لبعض المتغيرات قبل عرضها)

mod_python و fast_cgi وغيرها من الطرق

لتوفير تحويل مفسر اللغة التي كتب لها السكربت فإن استخدام مفسر واحد مضمن داخل الخادم يكون حل أفضل وهذا هو ما يقوم به mod_python حيث أنه جزء من خادم أباتشي وكلما يصل طلب يقوم mod_python الموجود مسبقا في الذاكرة باستلامه ومعالجته. لكن يجب كتابة التطبيق ليستعمل mod_python وليس cgi. وهناك العديد من الطرق لكتابة تطبيقات الويب منها أن تكتب خادم ويب خاص بك حتى يكون لك كامل التحكم وتتجنب تحميل ثم تشغيل تطبيق الويب مع كل طلب (لأن تطبيق الويب هو نفسه الخادم) ومما يسهل عليك ذلك SimpleHTTPServer 1) أو خادم الويب في paste.httpserver 2) أو في twisted-web 3) وفي جميع الأحوال يمكن تشغيل هذا الخادم داخليا على منفذ عالي مثل 8080 ثم الطلب من خادم أباتشي أن يكون مجرد وكيل يمرر الطلبات من العالم الخارجي له وبالعكس. أما fast_cgi فهي بروتوكول متعدد اللغات يعمل في الوسط بين تطبيق cgi العادي (يعني لا داع لإعادة كتابة تطبيق cgi) والخادم فيوفر عدد مرات تشغيل التطبيق من جديد. كل تلك الحلول تتطلب كتابة التطبيق بطرق مختلفي لكل حل (ف cgi أو fast_cgi تختلف تماما عن mod_python وهما يختلفان عن twisted-python وهكذا). لنأخذ مثلا على تطبيق واحد مثل ويكي اسمها MionMion 4) التي ما هي إلا تطبيق ويب بلغة بايثون فلو كنت أنت مطورها فلعل تسأل نفسك وفق أي من هذه الحلول ستكتبها ؟ وإن قررت دعم أكثر من واحد فإن تبين وجود مشكلة أمنية في أحد التنفيذها هل تنعكس على الاخرى أم لا ؟ هل ستعيد كتابة الكثير من الأجزاء المملة من البرنامج ؟ طيب إن كنت ستصمم إطار ويب مثل Django أو Pylons فأي من هذه الأسالب ستدعم ؟ ماذا ستقول للمستخدمين الذين لا يدعم خادمهم إلا واحدة دون الأخريات من تلك الطرق.

إطار الويب الماورائي ويز-جي WSGI

ما هو ويز-جي WSGI

ويز-جي اختصار لكلمة Web Server Gateway Interface وهي تلفظ بأكثر من طريقة منها wiz-gee وهي ليست إطار ويب بل هي الطريقة التي يجب أن تكتب بها أطر الويب وتطبيقاته في بايثون. لهذا يسمى meta framework أي أنه إن كتبت التطبيق بهذه الطريقة فإنه سيعمل بسهولة على أي حل تختاره. مثلا في جافا هناك tomcat وهناك jetty لكن من يعمل تطبيقات ويب بلغة جافا لا يهتم هل الخادم يعتمد على tomcat أم jetty لأن التنقل بينهما شفاف فهما يتحدثان ما يسمى في عالم جافا باسم خويدمات جافا Java Servlet. وهذا هو تماما ما تعمله WSGI في عالم بايثون. وقبل أن تسأل لا يوجد حزمة اسمها WSGI ولا تحتاج عمل import لأي شيء. بل هي الطريقة التي يجب أن يكتب بها برنامجك.

كيف يعمل ؟

إن أردت كتابة تطبيق ويب متوافق مع ويز-جي فإن تطبيق الويب الخاص بك إما أن يكون مجرد دالة واحدة أو أن يكون صنف class لكنه قابل للاستدعاء عبر الدالة call هذه الدالة تستلم معاملين هما environ و start_response أولهما عبارة عن قاموس بايثوني (أو ما يسمى associated array في لغات أخرى) عناصره متغيرات البيئة وما يقابلها من قيم ومنها 5) SCRIPT_NAME - وهو المسار من أول URI لتطبيق الويب ويجوز أن يكون خاليا PATH_INFO - وهي المسار الذي يلي التطبيق في URI مثلا إن كان الرابط هو http://localhost/app/view/file.txt يمكن أن يكون app هو SCRIPT_NAME و الباقي يعني /view/file.txt هو الملف المطلوب PATH_INFO

  • QUERY_STRING - الاستعلام المطلوب وهو الذي يكون بعد علامة الاستفهام في الروابط عندما يكون الطلب من نوع GET مثلا http://www.google.com/search?q=ojuba
  • REMOTE_ADDR - وهو عنوان IP للطرف البعيد الذي أرسل الطلب يفيد لعمل limits مثلا
  • REQUEST_METHOD - وهي طريقة الطلب وهي غالبا إما GET أو POST

أما المعامل الثاني فهو عبارة عن دالة يجب أن يستدعيها التطبيق قبل أن يبدأ إرسال النتائج. فائدة استدعاء هذه الدالة هي تمكين الخادم إرسال ترويسة http وتستدعى هذه الدالة مرة واحدة في كل استجابة ويمرر لها معاملين

  • الأول هو status وهو سلسلة نصية تحتوي نوع الرد غالبا ‪ “200 OK” ‬ 6)
  • الثاني هو قائمة عناصرها ازواج من الترويسة وقيمتها وأهمها طبعا “content-type” وقيمتها يمكن أن تكون “text/html; charset=utf-8” مثلا.

أما الصفحة نفسها تولد عبر إعادة قائمة بأجزاء الصفحة مثلا

  return ['Hello, world!']

ويجوز أن نعيد أي كائن قابل للتسلسل iteratable سواءً قائمة أو مرتب أو حتى قطعة نصية واحدة أو generator أو حتى كائن ملف.

تطبيق ويب بسيط على شكل دالة

لنفرض أننا نريد عمل تطبيق ويب بسيط يعرض اسم الصفحة التي طلبت منه ويعرض الوقت منذ بزوغ يونكس بالثواني.

import time
def application(environ, start_response):
  start_response("200 OK",[('content-type', 'text/html; charset=utf-8')]);
  return ['''<html><body>
<h1>Hello!</h1>
<p>you have requested the page [%s]</p>
<p>it's [%d] since Unix epoch</p>
</body></html>''' % (environ['PATH_INFO'],time.time())]

لاحظ أن تطبيق الويب السابق لا يستعمل أي وحدات إضافية ولاحظ كم هو بسيط. سنبين الآن كيف يمكننا تشغيل تطبيق الويب هذا على أنواع مختلفة من الخوادم

  • تطبيق cgi عبر خادم apache
  • عبر google web app engine
  • عبر mod_wsgi في خادم apache
  • إطلاق خادم خاص بالتطبيق عبر paste

تطبيق ويب بسيط على شكل صنف class

يمكن عمل تطبيق الويب على شكل class وتنتقل مهمة الدالة إلى الطريقة call داخل الصنف هكذا

import time
class MyWebApp:
  def __init__(self):
    pass
 
  def __call__(self, environ, start_response):
    start_response("200 OK",[('content-type', 'text/html; charset=utf-8')]);
    return ['''<html><body>
<h1>Hello!</h1>
<p>you have requested the page [%s]</p>
<p>it's [%d] since Unix epoch</p>
</body></html>''' % (environ['PATH_INFO'],time.time())]
 
application=MyWebApp()

لاحظ أننا قمنا بعمل كائن فرد instance من ذاك الصنف وفائدة هذا تمرير أي معاملات لازمة لإنشاء التطبيق كأن تكون معاملات معايرة أو إعدادات معينة وهي التي ستمرر للطريقة init

تشغيل التطبيق السابق على CGI

من السهل جدا تحويل أي تطبيق ويز-جي ليعمل كتطبيق CGI عبر الكود العياري الذي تم التقدم به في PEP 0333 وهي تعني Python Enhancement Proposals هذا الكود تجده على الرابط

حيث نستدعي الدالة run_with_cgi المعرفة في ذاك الملف (سمه مثلا wsgi2cgi) ونمرر لها تطبيقنا هكذا

from myTestWebApp import application
from wsgi2cgi import run_with_cgi
run_with_cgi(application)

نضع هذا في مجلد cgi-bin في الخادم ونعطيه صلاحيات التنفيذ وغير ذلك من الإعدادات التقليدية كأي تطبيق CGI تقليدي. وبالمناسبة نفس الشيء يكون في fast_cgi وطبعا كود البرنامج الذي يشغله لا يحتاج الكثير من الشرح فدالة start_response ترسل الترويسات بعد تنسيقها ثم ترسل علامة سطر جديد

for header in response_headers:
  sys.stdout.write('%s: %s\r\n' % header)
sys.stdout.write('\r\n')

ثم تسير على البيانات التي أعادها التطبيق وتكتبها إلى المخرجات ثم sys.stdout.flush

تشغيل التطبيق السابق غوغل

طبعا لا داع أن نُذكّر أن غوغل تحجب بعض خدماتها بطريقة انتقائية (أي ولا تحجب خدمات أخرى) عن بعض الدول العربية التي تعتبرها الولايات المتحدة “دولا مارقة” فيما يفترض أنه تطبيق لقوانين التصدير الأمريكية.7) ومن هذا البعض خدمة غوغل كود وخدمة استضافة تطبيقات بايثون google app engine وهي خدمة مفيدة جدا حيث يمكن استضافة أي تطبيق مكتوب بلغة بايثون أو جافا. سنشرحها للفائدة فقط مع تقديم النصيحة بعدم استعمالها للأسبب المذكورة أعلاه. الطريقة هي بكل بساطة عبر استدعاء run_wsgi_app وتمرير تطبيق الويب خاصتنا له هكذا

from myTestWebApp import application
from google.appengine.ext.webapp.util import run_wsgi_app
 
def main():
    run_wsgi_app(application)
 
if __name__ == "__main__":
    main()

للمزيد انظر:

تشغيل التطبيق السابق mod_wsgi

يوجد وحدة لخادم أباتشي اسمها mod_wsgi تسمح بتشغيل تطبيقات ويز-جي بكل سهولة انظر

كل ما عليك هو أن تعمل ملف إعدادات (في فيدورا اعمل ملف جديد داخل المجلد /etc/httpd/conf.d/) يحتوي السطر التالي

WSGIScriptAlias /myapp /path/to/myapp.wsgi

حيث /myapp الأولى هي المسار الذي سيتم تشغيل التطبيق عند زيارته. الثاني /path/to/myapp.wsgi هو ملف مكتوب بلغة بايثون (يعامل وكأنه وحدة module) يحتوي تعريف لكائن أو متغير اسمه application وطبعا يمكن أن يكون هو نفسه الوحدة السابقة لأننا عرفنا فيه متغير/كائن بهذا الاسم. إن لم تكن تملك صلاحيات تثبيت الوحدات في النظام يمكنك وضعها في أي مكان وإضافتها للمسار الخاص ببايثون هكذا

import sys
sys.path.insert(0,'/home/omar/my-python-libs/')
from myTestWebApp import application

إن كان الصنف بحاجة لتمرير معاملات معينة يمكنك استيراد النصف ثم استهلال كائن منه هكذا

import sys
sys.path.insert(0,'/home/omar/my-python-libs/')
from myTestWebApp import MyWebApp
application=MyWebApp(arg1,arg2)

وهنا نحن لا نستدعي الدالة MyWebApp بل ننشئ كائن من نوع MyWebApp ونسمي الكائن application سيتم استدعاء application مع كل طلب يصل إلى الخادم.

إطلاق خادم خاص بالتطبيق عبر paste

إن تجربة تطبيق الويب من خلال خادم ضخم مثل أباتشي أمر مزعج تخيل نفسك تعطل خدمة الويب وتعيد تشغيل أباتشي لمجرد تعطيل سطر صغير لذلك فالأسهل هو استخدم خادم paste المدمج أثناء تطوير البرنامج. أضف الأسطر التالية إلى نهاية تطبيق ويز-جي

if __name__ == '__main__':
    from paste import httpserver
    httpserver.serve(application, host='127.0.0.1', port='8080')

السطر الأول يعني أن ما بعده ينفذ فقط إن تم تنفيذ الملف ولا ينفذ إذا كنا نستعمله كوحدة module السطر الذي يليه تحضر خادم ويب من حزمة python-paste (موجودة في مستودع فيدورا بهذا الاسم) وتشغل تطبيقنا application على هذا الخادم على المنفذ 8080 (يمكنك تغير الرقم لأي رقم غير محجوز فوق 1024) ويستمع للطلبات من الجهاز نفسه. إن جعلتها 0.0.0.0 فإنه سيستجيب لكل الطلبات عبر الشبكة المحلية أو الخارجية. بعد إضافة تلك الأسطر فقط قم بتنفيذ ملف بايثون الذي يحتوي على خادم paste ثم افتح المتصفح على المنفذ المحدد في المثال http://127.0.0.1:8080/ عندما تريد تغيير أي شيء يمكنك إيقاف البرنامج من سطر الأوامر بالضغط على CTRL+C للمزيد انظر

1) انظر http://docs.python.org/library/simplehttpserver.html وانظر من تطبيقاته pydoc حيث يمكن تشغيله كخادم بالأمر pydoc -p 8080

نقاش

Johnie, 2011/11/11 09:06

This is an article that makes you think “never tuhhogt of that!”

Nelda, 2011/12/05 05:38

Real brain power on dislapy. Thanks for that answer!

Diego, 2012/07/30 04:09

We need more insights like this in this tehard.

Otto, 2012/08/14 06:48

What a pleasure to find someone who identifies the issues so clarely

Karsen, 2012/10/26 15:13

Superb ifnormatoin here, ol'e chap; keep burning the midnight oil.

Kilee, 2013/01/21 08:07

Your post cauptres the issue perfectly!

أدخل تعليقك:
 
آخر تعديل:: 23 نيسان 2015 الساعة 00:20 (تحرير خارجي)