اذهب إلى المحتوى

لقد غير الويب وجه العالم، ولكن جوهره لم يتغير كثيرًا، إذ لا تزال معظم الأنظمة تتبع القواعد نفسها التي وضعها تيم بيرنرز لي Tim Berners-Lee، ولا تزال معظم خوادم الويب تتعامل مع أنواع الرسائل نفسها وبذات الطريقة، لذا سنوضح في هذا المقال الشيفرة البرمجية لخادم ويب بسيط محدود لا يتجازو كوده البرمجي 500 سطر، لنساعدكم على فهم كيفية عمل خادم الويب بشكل أفضل من خلال الاطلاع على شيفرته البرمجية مفتوحة المصدر.

معلومات أساسية عن خوادم الويب

تستخدم جميع البرامج على الويب تقريبًا مجموعة من معايير الاتصال تسمى بروتوكولات الإنترنت Internet Protocols أو IPs اختصارًا، وما يهمنا من هذه البروتوكولات حاليًا هو بروتوكول التحكم في الإرسال Transmission Control Protocol -أو TCP/IP اختصارًا- والذي يجعل الاتصال بين الحواسيب مشابهًا لقراءة وكتابة الملفات فنحن نفتح قناة اتصال، ثم نقرأ أو نكتب البيانات، ثم نغلق القناة.

تتواصل البرامج التي تستخدم بروتوكول الإنترنت من خلال المقابس Sockets، حيث يمثل كل مقبس أحد طرفي قناة اتصال من نقطة إلى نقطة مثل الهاتف الذي يكون على أحد طرفي مكالمة هاتفية. يتكون المقبس من عنوان IP يحدد جهازًا معينًا ورقم منفذ على هذا الجهاز، حيث يتألف عنوان IP من أربعة أرقام مكونة من 8 بتات مثل ‎174.136.14.108‎، ويطابق نظام أسماء النطاقات Domain Name System أو DNS هذه الأرقام مع أسماء مثل ‎aosabook.org‎، مما يسهل علينا تذكرها. رقم المنفذ هو رقم ضمن المجال من 0 إلى 65535، والذي يحدد المقبس الفريد على الجهاز المضيف، فإذا كان عنوان IP يشبه رقم هاتف شركة، فيمكن القول بأن رقم المنفذ هو امتداد لهذا الرقم. وتكون المنافذ ذات الأرقام من 0 إلى 1023 محجوزة ليستخدمها نظام التشغيل، ويمكن لأي شخص آخر استخدام المنافذ المتبقية.

يمثل بروتوكول نقل النص التشعبي Hypertext Transfer Protocol أو HTTP إحدى الطرق التي تتبادل البرامج البيانات من خلالها عبر بروتوكول IP، ويُعدّ بروتوكول HTTP بسيطًا إذ يرسل العميل طلبًا يحدد من خلاله ما يريده عبر اتصال المقبس، ثم يرسل الخادم بعض البيانات في الاستجابة كما في الشكل التالي:

001 دورة HTTP

دورة HTTP

عندما يرسل العميل أو متصفح الإنترنت طلبًا إلى الخادم، فإن الخادم يرد عليه بإرسال البيانات المطلوبة. ويمكن لهذه البيانات أن تأتي إما من ملف مخزَّن مسبقًا على القرص الصلب مثل صفحة HTML محفوظة سابقًا، أو يتم توليدها وإنشاؤها لحظيًا عند طلبها، أو الاثنين فقد تكون صفحة HTML ثابتة ويُضاف لها محتوى مولّد ديناميكيًا. فالخادم عند استجابته لا يكون محدودًا بمصدر واحد للبيانات، بل يمكنه أن يختار الأنسب أو يجمع بين عدة مصادر.

تكمن أهمية طلب HTTP الذي يرسله المتصفح إلى السيرفر في أنه مجرد نص، إذ يمكن لأي برنامج إنشاء طلب أو تحليله. ولكن يجب أن يحتوي هذا النص على الأجزاء الموضحة في الشكل التالي لفهمه:

002 طلب HTTP

طلب HTTP

تكون طريقةHTTP إما GET لجلب المعلومات أو POST لإرسال بيانات النموذج أو رفع الملفات. ويحدد عنوان URL ما يريده العميل، حيث يكون في أغلب الأحيان مسارًا إلى ملف على القرص الصلب مثل ‎/research/experiments.html‎، ولكن يقرر الخادم ما يجب فعله به. ويكون إصدار HTTP إما HTTP/1.0 أو HTTP/1.1، ولسنا بصدد معرفة الاختلافات بينهما في نطاق المقال الحالي.

ترويسات HTTP هي أزواج مفتاح وقيمة كما في الأمثلة الثلاث التالية:

Accept: text/html
Accept-Language: en, fr
If-Modified-Since: 16-May-2024

قد تظهر المفاتيح عدة مرات في ترويسات HTTP على عكس جداول التعمية Hash التي تسمح بمفتاح واحد فقط لكل قيمة، وهذا يسمح للطلب بتحديد استعداده لقبول عدة أنواع من المحتوى. يتكون جسم الطلب من أي بيانات إضافية مرتبطة بالطلب، ويُستخدَم عند إرسال البيانات عبر نماذج الويب أو عند رفع الملفات وغير ذلك. يجب أن يكون هناك سطر فارغ بين آخر ترويسة وبداية جسم الطلب للإشارة إلى نهاية الترويسات، وتخبر إحدى الترويسات التي اسمها ‎Content-Length‎ الخادمَ بعدد البايتات المتوقع قراءتها في جسم الطلب.

سيكون تنسيق الاستجابة HTTP Response مشابهًا لتنسيق الطلب HTTP Request كما في الشكل التالي:

003 استجابة HTTP

استجابة HTTP

إذًا سيكون للإصدار والترويسات وجسم الطلب التنسيق والمعنى نفسه في كل من الطلب والاستجابة. لكن سيتضمن السطر الأول من الاستجابة رمز حالة Status Code الذي يشير إلى ما حدث عند معالجة الطلب، حيث يدل الرمز 200 على النجاح، ويدل الرمز 404 على أن المورد المطلوب غير موجود، ويكون للرموز الأخرى معانٍ أخرى، بينما تكرر عبارة الحالة هذه المعلومات في صيغة مفهومة بشريًا مثل ‎OK‎ أوnot found‎.

هناك شيئان آخران إضافيان نحتاج إلى معرفتهما حول بروتوكول HTTP وهما أن بروتوكول HTTP عديم حالة Stateless، بمعنى أنه يعالج طلب بمفرده، ولا يتذكر الخادم أي شيء بين طلب وآخر ولا من أنت وما فعلته من قبل، حتى لو كنت قبل دقيقة على نفس الموقع. فإذا أراد تطبيقٌ ما تتبع شيء معين مثل هوية المستخدم، فسيفعل ذلك باستخدام ملف تعريف الارتباط Cookie، التي تتضمن سلسلة نصية من أحرف قصيرة يرسلها الخادم إلى العميل، ثم يعود العميل إلى الخادم لاحقًا. فإذا أراد المستخدم أداء بعض الوظائف التي تتطلب حفظ الحالة عبر عدة طلبات، فسينشئ الخادم ملف تعريف ارتباط جديد ويخزنه في قاعدة بيانات ويرسله إلى متصفحه، وبالتالي يستخدم الخادم ملف تعريف الارتباط للبحث عن معلومات حول ما يفعله المستخدم عندما يعيد المتصفح إرسال ملف تعريف الارتباط.

الشيء الثاني الذي نحتاج إلى معرفته حول HTTP هو أنه يمكن إلحاق عنوان URL بمعاملات لتوفير مزيد من المعلومات، فمثلًا إذا استخدمنا محرك بحث، فيجب تحديد مصطلحات البحث الخاصة بنا بإضافتها إلى المسار في عنوان URL، ولكن يجب إضافة معاملات إلى عنوان URL من خلال إضافة الرمز ? إلى عنوان URL متبوعًا بأزواج ‎key=value‎ نفصل بينها بالرمز &. يطلب عنوان ‎http://www.google.ca?q=Python‎ من جوجل مثلًا البحث عن صفحات متعلقة ببايثون، فالمفتاح هو الحرف ‎q‎ والقيمة هي ‎Python‎، بينما يخبر الاستعلام ‎http://www.google.ca/search?q=Python&client=Firefox‎ جوجل أننا نستخدم متصفح فايرفوكس. يمكننا تمرير أي معاملات نريدها هنا، ولكن يتحكم التطبيق الذي يعمل على موقع الويب في تحديد المعاملات التي يجب الانتباه إليها وكيفية تفسيرها.

إذا كانت الرموز ? و & من المحارف الخاصة، فيحب الهروب منها، ويجب إيجاد طريقة لوضع علامة اقتباس مزدوجة ضمن سلسلة محارف مُحدَّدة بهذه العلامات، لذا يمثل معيار ترميز URL المحارف الخاصة باستخدام الرمز % متبوعًا برمز مكون من رقمين مع استبدال المسافات بالمحرف +، وبالتالي يمكنك البحث في جوجل عن ‎grade = A+‎ مع المسافات من خلال استخدام العنوان ‎http://www.google.ca/search?q=grade+%3D+A%2B

مكتبات بايثون للاتصال بالخادم

للسهولة يستخدم معظم المطورين مكتبات خاصة لفتح المقابس وإنشاء طلبات HTTP وتحليل الاستجابات مثل مكتبة ‎urllib2‎ في بايثون، وهي بديل لمكتبة سابقة اسمها ‎urllib‎، كما تُعَد مكتبة ‎Requests‎ بديلًا أسهل في الاستخدام من مكتبة ‎urllib2‎. يستخدم المثال التالي مكتبة ‎Requests‎ لتنزيل صفحة من موقع للكتب:

import requests
response = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')
print 'status code:', response.status_code
print 'content length:', response.headers['content-length']
print response.text
status code: 200
content length: 61
<html>
  <body>
    <p>Test page.</p>
  </body>
</html>

يرسل التابع ‎request.get‎ طلب GET إلى الخادم ويعيد كائن يحتوي على الاستجابة، ويكون العضو ‎status_code‎ الخاص بهذا الكائن هو رمز حالة الاستجابة، والعضو ‎content_length‎ هو عدد البايتات في بيانات الاستجابة، و ‎text‎ هو البيانات الفعلية وهو في حالتنا صفحة HTML.

تطوير خادم ويب بسيط

بعد أن تعرفنا على أساسيات عمل بروتوكلات الإنترنت، أصبحنا الآن مستعدين لكتابة خادم ويب بسيط، حيث تتألف فكرته الأساسية من الخطوات البسيطة التالية:

  1. الانتظار حتى يتصل شخص ما بهذا الخادم ويرسل طلب HTTP
  2. تحليل هذا الطلب
  3. اكتشاف ما يطلبه
  4. جلب هذه البيانات أو توليدها ديناميكيًا
  5. تنسيق البيانات بتنسيق HTML
  6. إرسال هذه البيانات

تكون الخطوات 1 و 2 و 6 هي نفسها في جميع التطبيقات، لذا تحتوي مكتبة بايثون المعيارية على الوحدة ‎BaseHTTPServer‎ التي تنجز هذه الخطوات نيابة عنا، لذا نهتم فقط بالخطوات من 3 إلى 5 كما هو موضح فيما يلي:

import BaseHTTPServer

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    '''Handle HTTP requests by returning a fixed 'page'.'''

    # الصفحة المراد إرسالها
    Page = '''\
<html>
<body>
<p>Hello, web!</p>
</body>
</html>
'''

    # معالجة طلب‫ GET‬
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(self.Page)))
        self.end_headers()
        self.wfile.write(self.Page)

#----------------------------------------------------------------------

if __name__ == '__main__':
    serverAddress = ('', 8080)
    server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
    server.serve_forever()

يحلل الصنف ‎BaseHTTPRequestHandler‎ طلب HTTP الوارد ويحدد التابع التي يحتوي عليه، حيث إذا كان التابع هو GET، فسيستدعي هذا الصنف التابع ‎do_GET‎. يعدل الصنف ‎RequestHandler‎ هذا التابع لتوليد صفحة بسيطة ديناميكيًا، حيث يُخزَّن النص في متغير على مستوى الصنف ‎Page‎، والذي نرسله إلى العميل بعد إرسال رمز استجابة 200، وتخبر الترويسة ‎Content-Type‎ العميل بتفسير البيانات على أنها بتنسيق HTML، وتخبره بطول الصفحة. يؤدي استدعاء التابع ‎end_headers‎ إلى إدراج السطر الفارغ الذي يفصل الترويسات عن الصفحة نفسها.

نحتاج إلى الأسطر الثلاثة الأخيرة لبدء تشغيل الخادم، حيث يحدد السطر الأول من هذه الأسطر عنوان الخادم كمجموعة مؤلفة من سلسلة نصية فارغة تعني التشغيل على الجهاز الحالي والقيمة 8080 التي تمثل المنفذ. ننشئ بعد ذلك نسخة من ‎BaseHTTPServer.HTTPServer‎ مع هذا العنوان واسم صنف معالج الطلب كمعاملات، ثم نطلب منه التشغيل إلى الأبد حتى إنهائه باستخدام الاختصار ‎Control-C‎.

لن يؤدي تشغيل هذا البرنامج من سطر الأوامر إلى عرض أي شيء:

$ python server.py

ننتقل بعد ذلك إلى العنوان ‎http://localhost:8080‎ باستخدام المتصفح، وسنحصل على ما يلي في المتصفح:

Hello, web!

وسنحصل على ما يلي في الصدفة Shell:

127.0.0.1 - - [24/Feb/2014 10:26:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET /favicon.ico HTTP/1.1" 200 -

لم نطلب هنا ملفًا معينًا، لذا طلب المتصفح المجلد الجذري لأي شيء يقدمه الخادم / في السطر الأول، ويظهر السطر الثاني لأن المتصفح يرسل تلقائيًا طلبًا ثانيًا لملف صورة بالاسم ‎/favicon.ico‎، والذي سيعرضه كأيقونة في شريط العناوين عند وجوده.

عرض القيم

لنعدل خادم الويب الخاص بنا لعرض بعض القيم المضمَّنة في طلب HTTP بهدف التدريب، حيث سنفصل إنشاء الصفحة عن إرسالها للحفاظ على ترتيب شيفرتنا البرمجية كما يلي:

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    # قالب الصفحة

    def do_GET(self):
        page = self.create_page()
        self.send_page(page)

    def create_page(self):
        # نضع شيئًا هنا

    def send_page(self, page):
        # نضع شيئًا هنا

ويكون التابع ‎send_page‎ كما يلي:

   def send_page(self, page):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(page)))
        self.end_headers()
        self.wfile.write(page)

يُعَد القالب الخاص بالصفحة التي نريد عرضها مجرد سلسلة نصية تحتوي على جدول HTML مع بعض العناصر النائبة Placeholders للتنسيق كما يلي:

   Page = '''\
<html>
<body>
<table>
<tr>  <td>Header</td>         <td>Value</td>          </tr>
<tr>  <td>Date and time</td>  <td>{date_time}</td>    </tr>
<tr>  <td>Client host</td>    <td>{client_host}</td>  </tr>
<tr>  <td>Client port</td>    <td>{client_port}s</td> </tr>
<tr>  <td>Command</td>        <td>{command}</td>      </tr>
<tr>  <td>Path</td>           <td>{path}</td>         </tr>
</table>
</body>
</html>
'''

ويكون التابع الذي يملأ هذه الصفحة كما يلي:

   def create_page(self):
        values = {
            'date_time'   : self.date_time_string(),
            'client_host' : self.client_address[0],
            'client_port' : self.client_address[1],
            'command'     : self.command,
            'path'        : self.path
        }
        page = self.Page.format(**values)
        return page

لم تتغير البنية الرئيسية للبرنامج، فهو ينشئ نسخة من الصنف ‎HTTPServer‎ مع عنوان ومعالج الطلب كمعاملات، ثم يخدم الطلبات إلى الأبد، حيث إذا شغلنا البرنامج وأرسلنا طلب من متصفح على العنوان ‎http://localhost:8080/something.html‎، فسنحصل على النتيجة التالية:

 Date and time  Mon, 24 Feb 2014 17:17:12 GMT
  Client host    127.0.0.1
  Client port    54548
  Command        GET
  Path           /something.html

لم نحصل على خطأ 404 بالرغم من أن الصفحة ‎something.html‎ غير موجودة كملف على القرص الصلب، وسبب ذلك هو أن خادم الويب هو مجرد برنامج، ويمكنه أن يتخذ أي إجراء بناءً على البرمجة التي أعد بها،  ويمكنه بدلاً من إرجاع صفحة 404 Not Found -إذا كان الملف غير موجود- أن يرسل صفحة افتراضية مع رسالة تخبر المستخدم أن الصفحة غير موجودة، أو يرسل صفحة عشوائية مثلاً صفحة من ويكيبيديا أو صفحة أخرى تفاعلية، أو يُعيد توجيهنا إلى صفحة أخرى

إرسال الصفحات الثابتة

أبسط شكل من أشكال خوادم الويب هو الذي يرسل ملفات HTML أو أي موارد أخرى كما هي إلى المتصفح، لذا سنعمل في هذه الخطوة على توفير صفحات الويب من القرص الصلب بدلًا من توليدها تلقائيًا، حيث سنبدأ بإعادة كتابة التابع ‎do_GET‎ كما يلي:

   def do_GET(self):
        try:

            # اكتشاف المطلوب بالضبط
            full_path = os.getcwd() + self.path

            # المطلوب غير موجود
            if not os.path.exists(full_path):
                raise ServerException("'{0}' not found".format(self.path))

            # المطلوب هو ملف
            elif os.path.isfile(full_path):
                self.handle_file(full_path)

            # المطلوب هو شيء لا نستطيع معالجته
            else:
                raise ServerException("Unknown object '{0}'".format(self.path))

        # معالجة الأخطاء
        except Exception as msg:
            self.handle_error(msg)

يفترض هذا التابع أنه يمكن توفير أي ملفات موجودة ضمن المجلد الذي يعمل فيه خادم الويب، والذي نحصل عليه باستخدام التابع ‎os.getcwd‎، ويدمجه مع المسار المقدم في عنوان URL الذي تضعه المكتبة في ‎self.path‎ تلقائيًا، ويبدأ هذا المسار دائمًا بالرمز / للحصول على مسار الملف الذي يريده المستخدم.

إن لم يكن هذا المسار موجودًا أو إن لم يكن ملفًا، فسيبلغ التابع عن خطأ من خلال رفع استثناء والتقاطه، بينما إذا كان المسار يتطابق مع ملف، فسيستدعي تابعًا مساعدًا بالاسم ‎handle_file‎ لقراءة المحتويات وإعادتها. يقرأ التابع التالي مثلًا الملف فقط ويستخدم التابع ‎send_content‎ الموجود مسبقًا لإرساله إلى العميل:

   def handle_file(self, full_path):
        try:
            with open(full_path, 'rb') as reader:
                content = reader.read()
            self.send_content(content)
        except IOError as msg:
            msg = "'{0}' cannot be read: {1}".format(self.path, msg)
            self.handle_error(msg)

نلاحظ هنا أننا نفتح الملف في الوضع الثنائي الممثل بالحرف ‎b‎ في ‎rb‎، هذا يعني أن بايثون تقرأ الملف كما هو دون محاولة تعديل أو تغيير تنسيق النصوص مثل نهاية السطور. وتُعَد قراءة الملف بالكامل في الذاكرة عند تقديمه فكرة سيئة في الحياة الواقعية، فقد يحتوي الملف على عدة جيجابايتات من بيانات الفيديو، ولن نتطرق إلى هذه الحالة في مقالنا.

نحتاج إلى كتابة تابع لمعالجة الأخطاء وقالب لصفحة الإبلاغ عن الأخطاء كما يلي:

   Error_Page = """\
        <html>
        <body>
        <h1>Error accessing {path}</h1>
        <p>{msg}</p>
        </body>
        </html>
        """

    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg)
        self.send_content(content)

يعمل هذا البرنامج بنجاح، ولكن توجد مشكلة فيه تتمثل في أنه يعيد دائمًا رمز الحالة 200 حتى عندما لا تكون الصفحة المطلوبة موجودة. تحتوي الصفحة المرسلة في هذه الحالة على رسالة خطأ، ولكن لن يعرف المتصفح بفشل الطلب لأنه لا يستطيع قراءة اللغة الإنجليزية، لذا نحتاج إلى تعديل التابعين ‎handle_error‎ و ‎send_content‎ كما يلي:

   # معالجة الكائنات غير المعروفة
    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg)
        self.send_content(content, 404)

    # إرسال المحتوى الفعلي
    def send_content(self, content, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)

لا نرفع الاستثناء ‎ServerException‎ عند عدم العثور على ملف، بل نولد صفحة خطأ بدلًا من ذلك، فالغرض من هذا الاستثناء هو الإشارة إلى خطأ في شيفرة الخادم، بينما تظهر صفحة الخطأ التي أنشأها التابع ‎handle_error‎ عندما يخطئ المستخدم في شيء ما مثل إرسال عنوان URL لملف غير موجود.

ملاحظة: سنستخدم التابع ‎handle_error‎ عدة مرات في هذا المقال، بما في ذلك الحالات لا يكون فيها رمز الحالة 404 مناسبًا، لذا لنحاول التفكير في كيفية توسيع هذا البرنامج بحيث يمكن توفير رمز استجابة الحالة بسهولة.

سرد محتويات المجلدات

يمكننا تعليم خادم الويب لعرض قائمة بمحتويات مجلد عندما يكون المسار في عنوان URL يمثل مجلدًا وليس ملفًا، كما يمكن جعله يبحث في هذا المجلد عن الملف ‎index.html‎ لعرضه وعرض قائمة بمحتويات المجلد فقط إن لم يكن هذا الملف موجودًا.

سيؤدي بناء ذلك في التابع ‎do_GET‎ خطأ، إذ سيحتوي التابع الناتج على تداخل طويل من تعليمات ‎if‎ التي تتحكم في سلوكيات الخادم الخاصة، فالحل الصحيح هو العودة إلى الخطوة السابقة وحل المشكلة العامة من خلال معرفة ما يجب فعله بعنوان URL، لذا سنعيد كتابة التابع ‎do_GET‎ كما يلي:

   def do_GET(self):
        try:

            # اكتشاف المطلوب بالضبط
            self.full_path = os.getcwd() + self.path

            # اكتشاف كيفية معالجة المطلوب
            for case in self.Cases:
                handler = case()
                if handler.test(self):
                    handler.act(self)
                    break

        # معالجة الأخطاء
        except Exception as msg:
            self.handle_error(msg)

تبقى الخطوة الأولى نفسها، والتي تتمثل في اكتشاف المسار الكامل للشيء المطلوب، ثم تتكرر هذه النسخة من الشيفرة البرمجية ضمن حلقة على مجموعة من الحالات المخزنة في قائمة بدلًا من استخدام مجموعة من الاختبارات المضمنة، ويكون لكل حالة كائن مع تابعين هما: ‎test‎ الذي يخبرنا ما إذا كان الخادم قادرًا على معالجة الطلب، و ‎act‎ الذي يتخذ بعض الإجراءات، حيث يُعالَج الطلب ونخرج من الحلقة بمجرد العثور على الحالة الصحيحة.

تغير أصناف الحالات الثلاثة التالية سلوك الخادم السابق كما يلي:

class case_no_file(object):
    '''File or directory does not exist.'''

    def test(self, handler):
        return not os.path.exists(handler.full_path)

    def act(self, handler):
        raise ServerException("'{0}' not found".format(handler.path))

class case_existing_file(object):
    '''File exists.'''

    def test(self, handler):
        return os.path.isfile(handler.full_path)

    def act(self, handler):
        handler.handle_file(handler.full_path)

class case_always_fail(object):
    '''Base case if nothing else worked.'''

    def test(self, handler):
        return True

    def act(self, handler):
        raise ServerException("Unknown object '{0}'".format(handler.path))

نبني قائمة معالجات الحالات في بداية الصنف ‎RequestHandler‎ كما يلي:

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    '''
    If the requested path maps to a file, that file is served.
    If anything goes wrong, an error page is constructed.
    '''

    Cases = [case_no_file(),
             case_existing_file(),
             case_always_fail()]

    ...everything else as before...

أدى هذا التعديل على شيفرتنا البرمجية إلى جعل الخادم أكثر تعقيدًا، إذ زاد حجم الملف من 74 إلى 99 سطرًا دون إنجاز وظيفة جديدة، ولكن سنرى الفائدة من هذا التعديل عندما نحاول تعليم الخادم عرض صفحة ‎index.html‎ لمجلد ما عند وجود هذه الصفحة، وعرض قائمة بمحتويات المجلد عند عدم وجودها، حيث يكون المعالج للحالة الأولى كما يلي:

class case_directory_index_file(object):
    '''Serve index.html page for a directory.'''

    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        return os.path.isdir(handler.full_path) and \
               os.path.isfile(self.index_path(handler))

    def act(self, handler):
        handler.handle_file(self.index_path(handler))

يبني التابع المساعد ‎index_path‎ مسار الوصول إلى الملف ‎index.html‎، حيث يمنع وضع هذا التابع في معالج الحالات من تعقيد الصنف ‎RequestHandler‎ الرئيسي. ويتحقق التابع ‎test‎ مما إذا كان المسار يمثل مجلدًا يحتوي على الصفحة ‎index.html‎، ويطلب التابع ‎act‎ من معالج الطلب الرئيسي تقديم هذه الصفحة.

التغيير الوحيد المطلوب في الصنف ‎RequestHandler‎ هو إضافة كائن ‎case_directory_index_file‎ إلى قائمة الحالات ‎Cases‎ كما يلي:

   Cases = [case_no_file(),
             case_existing_file(),
             case_directory_index_file(),
             case_always_fail()]

إذا كان المجلد لا يحتوي على صفحة ‎index.html‎، فسيبقى تابع الاختبار نفسه بحيث يتحقق مما إذا كان المسار يمثل مجلدًا يحتوي على الصفحة ‎index.html‎ دون إدراجها باستخدام ‎not‎.

class case_directory_no_index_file(object):
    '''Serve listing for a directory without an index.html page.'''

    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        return os.path.isdir(handler.full_path) and \
               not os.path.isfile(self.index_path(handler))

    def act(self, handler):
        ???

يجب أن ينشئ التابع ‎act‎ قائمة بمحتويات المجلد ويعيدها، ولكن لا تسمح الشيفرة البرمجية الحالية بذلك، إذ يستدعي ‎RequestHandler.do_GET‎ التابع ‎act‎، لكنه لا يتوقع أو يتعامل مع قيمة معادة منه. لنضف الآن تابعًا إلى الصنف ‎RequestHandler‎ لتوليد قائمة بمحتويات المجلد، ونستدعيه من التابع ‎act‎ الخاص بمعالج الحالة كما يلي:

class case_directory_no_index_file(object):
    '''Serve listing for a directory without an index.html page.'''

    # يبقى‫ index_path و test كما في السابق‬

    def act(self, handler):
        handler.list_dir(handler.full_path)

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    # ما تبقى من الشيفرة البرمجية 

    # كيفية عرض قائمة المجلد
    Listing_Page = '''\
        <html>
        <body>
        <ul>
        {0}
        </ul>
        </body>
        </html>
        '''

    def list_dir(self, full_path):
        try:
            entries = os.listdir(full_path)
            bullets = ['<li>{0}</li>'.format(e) 
                for e in entries if not e.startswith('.')]
            page = self.Listing_Page.format('\n'.join(bullets))
            self.send_content(page)
        except OSError as msg:
            msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
            self.handle_error(msg)

بروتوكول CGI

لن يرغب معظم الأشخاص في تعديل شيفرة خادم الويب الخاص بهم لإضافة وظائف جديدة، لذا تدعم الخوادم آلية تسمى واجهة البوابة المشتركة Common Gateway Interface -أو CGI اختصارًا- والتي توفر طريقة معيارية لخادم الويب لتشغيل برنامج خارجي لتلبية الطلبات.

لنفترض مثلًا أننا نريد تمكين الخادم من عرض الوقت المحلي في صفحة HTML في برنامج مستقل كما يلي:

from datetime import datetime
print '''\
<html>
<body>
<p>Generated {0}</p>
</body>
</html>'''.format(datetime.now())

نضيف معالج الحالة التالي ليشغّل خادم الويب هذا البرنامج:

class case_cgi_file(object):
    '''Something runnable.'''

    def test(self, handler):
        return os.path.isfile(handler.full_path) and \
               handler.full_path.endswith('.py')

    def act(self, handler):
        handler.run_cgi(handler.full_path)

يكون الاختبار بسيطًا، حيث إذا انتهى مسار الملف باللاحقة ‎.py‎، فسيشغل الصنف ‎RequestHandler‎ هذا البرنامج.

   def run_cgi(self, full_path):
        cmd = "python " + full_path
        child_stdin, child_stdout = os.popen2(cmd)
        child_stdin.close()
        data = child_stdout.read()
        child_stdout.close()
        self.send_content(data)

ولكن يُعَد هذا الاختبار غير آمن، فإذا عرف شخصٌ ما مسار الوصول إلى ملف بايثون على الخادم، فسيسمح له بتشغيله دون القلق بشأن البيانات التي يمكنه الوصول إليها أو من احتوائه على حلقة لا نهائية أو أي شيء آخر. تستخدم شيفرتنا البرمجية دالة المكتبة ‎popen2‎ التي أُوقِفت مع استخدام وحدة subprocess، ولكن ‎popen2‎ هي الأداة الأنسب للاستخدام في مثالنا.

تُعَد الفكرة الأساسية لبروتوكول CGI بسيطة بغض النظر عن هذه المشكلة الأمنية، حيث تتألف من الخطوات التالية:

  1. تشغيل البرنامج في عملية فرعية
  2. التقاط كل ما ترسله هذه العملية الفرعية إلى الخرج
  3. إرسال الخرج الناتج إلى العميل الذي قدم الطلب

يكون بروتوكول CGI الكامل أكبر من ذلك، فمثلًا يسمح بالمعاملات في عنوان URL التي يمررها الخادم إلى البرنامج المُشغَّل، ولكن لا تؤثر هذه التفاصيل على البنية العامة للنظام التي أصبحت متداخلة، إذ كان لدى الصنف ‎RequestHandler‎ تابع واحد في البداية هو ‎handle_file‎ للتعامل مع المحتوى، وأضفنا حالتين خاصتين في التابعين ‎list_dir‎ و ‎run_cgi‎. ليست هذه التوابع الثلاثة في مكانها الصحيح لأن التوابع الأخرى تستخدمها، ويتمثل الحل في إنشاء صنف أب لجميع معالجات الحالات، ثم ننقل التوابع الأخرى إلى هذا الصنف إذا كانت مشتركة فقط بين معالجين أو أكثر. سيكون الصنف ‎RequestHandler‎ في النهاية كما يلي:

class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    Cases = [case_no_file(),
             case_cgi_file(),
             case_existing_file(),
             case_directory_index_file(),
             case_directory_no_index_file(),
             case_always_fail()]

    # كيفية عرض الخطأ
    Error_Page = """\
        <html>
        <body>
        <h1>Error accessing {path}</h1>
        <p>{msg}</p>
        </body>
        </html>
        """

    # تصنيف الطلب ومعالجته
    def do_GET(self):
        try:

            # اكتشاف المطلوب بالضبط
            self.full_path = os.getcwd() + self.path

            # اكتشاف كيفية معالجة المطلوب
            for case in self.Cases:
                if case.test(self):
                    case.act(self)
                    break

        # معالجة الأخطاء
        except Exception as msg:
            self.handle_error(msg)

    # معالجة الكائنات غير المعروفة
    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg)
        self.send_content(content, 404)

    # إرسال المحتوى الفعلي
    def send_content(self, content, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)

ويكون الصنف الأب لمعالجات الحالة الخاصة بنا كما يلي:

class base_case(object):
    '''Parent for case handlers.'''

    def handle_file(self, handler, full_path):
        try:
            with open(full_path, 'rb') as reader:
                content = reader.read()
            handler.send_content(content)
        except IOError as msg:
            msg = "'{0}' cannot be read: {1}".format(full_path, msg)
            handler.handle_error(msg)

    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        assert False, 'Not implemented.'

    def act(self, handler):
        assert False, 'Not implemented.'

ويكون المعالج لملف موجود مسبقًا كما يلي:

class case_existing_file(base_case):
    '''File exists.'''

    def test(self, handler):
        return os.path.isfile(handler.full_path)

    def act(self, handler):
        self.handle_file(handler, handler.full_path)

الخاتمة

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

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

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

ترجمة -وبتصرّف- للمقال A Simple Web Server لصاحبه Greg Wilson

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...