هذه المقالة نشرت في مجلة مجتمع لينكس العربي
بمناسبة إطلاق مشروع إطار الويب المتواضع الخاص بمشروع ثواب والذي يحمل اسم الصحابي عكاشة بن محصن رضي الله عنه قررت أن أكتب سلسلة سنتعلم فيها ماذا توفر لنا لغة البرمجة بايثون من طرق لعمل تطبيقات ويب. في هذه الحلقة سأشرح إطار الويب الماورائي meta web framework للغة بايثون واسمه WSGI. وفي الحلقة القادمة (إن شاء الله) سنتحدث عن إطار الويب “عكاشة”.
لا أعرف إن كان هناك من لا زال يستعملها لكن لا بأس من التعرف عليها. الطريقة التقليدية في عمل تطبيقات الويب التفاعلية هي أن يقوم الخادم بتنفيذ برنامج (غالبا سكربت موجود في 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 حيث أنه جزء من خادم أباتشي وكلما يصل طلب يقوم 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 فأي من هذه الأسالب ستدعم ؟ ماذا ستقول للمستخدمين الذين لا يدعم خادمهم إلا واحدة دون الأخريات من تلك الطرق.
ويز-جي اختصار لكلمة 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
أما المعامل الثاني فهو عبارة عن دالة يجب أن يستدعيها التطبيق قبل أن يبدأ إرسال النتائج. فائدة استدعاء هذه الدالة هي تمكين الخادم إرسال ترويسة http وتستدعى هذه الدالة مرة واحدة في كل استجابة ويمرر لها معاملين
أما الصفحة نفسها تولد عبر إعادة قائمة بأجزاء الصفحة مثلا
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())]
لاحظ أن تطبيق الويب السابق لا يستعمل أي وحدات إضافية ولاحظ كم هو بسيط. سنبين الآن كيف يمكننا تشغيل تطبيق الويب هذا على أنواع مختلفة من الخوادم
يمكن عمل تطبيق الويب على شكل 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 عبر الكود العياري الذي تم التقدم به في 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 تسمح بتشغيل تطبيقات ويز-جي بكل سهولة انظر
كل ما عليك هو أن تعمل ملف إعدادات (في فيدورا اعمل ملف جديد داخل المجلد /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 المدمج أثناء تطوير البرنامج. أضف الأسطر التالية إلى نهاية تطبيق ويز-جي
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 للمزيد انظر