WSGI: واجهة تطبيق الخادم لـ Python

نشرت: 2022-03-11

في عام 1993 ، كان الويب لا يزال في مهده ، مع حوالي 14 مليون مستخدم و 100 موقع. كانت الصفحات ثابتة ولكن كانت هناك بالفعل حاجة لإنتاج محتوى ديناميكي ، مثل الأخبار والبيانات المحدثة. استجابة لذلك ، قام Rob McCool وغيره من المساهمين بتطبيق واجهة البوابة المشتركة (CGI) في خادم الويب HTTPd التابع للمركز الوطني لتطبيقات الحوسبة الفائقة (NCSA) (رائد Apache). كان هذا هو أول خادم ويب يمكنه خدمة المحتوى الذي تم إنشاؤه بواسطة تطبيق منفصل.

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

بايثون على الويب وظهور WSGI

منذ إنشاء CGI ، تغير الكثير. أصبح نهج CGI غير عملي ، حيث تطلب إنشاء عملية جديدة عند كل طلب ، مما يؤدي إلى إهدار الذاكرة ووحدة المعالجة المركزية. ظهرت بعض الأساليب الأخرى منخفضة المستوى ، مثل FastCGI] (http://www.fastcgi.com/) (1996) و mod_python (2000) ، مما يوفر واجهات مختلفة بين أطر عمل ويب Python وخادم الويب. مع انتشار الأساليب المختلفة ، انتهى اختيار المطور لإطار العمل بتقييد خيارات خوادم الويب والعكس صحيح.

لمعالجة هذه المشكلة ، في عام 2003 اقترح فيليب ج. إبي PEP-0333 ، واجهة بوابة خادم ويب Python (WSGI). كانت الفكرة هي توفير واجهة عالمية عالية المستوى بين تطبيقات Python وخوادم الويب.

في عام 2003 ، قام PEP-3333 بتحديث واجهة WSGI لإضافة دعم Python 3. في الوقت الحاضر ، تستخدم جميع أطر عمل Python تقريبًا WSGI كوسيلة ، إن لم تكن الوسيلة الوحيدة ، للتواصل مع خوادم الويب الخاصة بهم. هذه هي الطريقة التي يقوم بها Django و Flask والعديد من الأطر الشعبية الأخرى.

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

واجهة Python WSGI

تحدد WSGI القواعد البسيطة التي يجب أن يتوافق معها الخادم والتطبيق. لنبدأ بمراجعة هذا النمط العام.

واجهة تطبيق الخادم Python WSGI.

واجهة التطبيق

في Python 3.5 ، تعمل واجهات التطبيق على النحو التالي:

 def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]

في Python 2.7 ، لن تكون هذه الواجهة مختلفة كثيرًا ؛ التغيير الوحيد هو أن يتم تمثيل الجسم بواسطة كائن str ، بدلاً من bytes واحد.

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

  • يجب أن يكون قابلاً للاستدعاء مع معلمات environ و start_response .
  • يجب استدعاء رد نداء start_response قبل إرسال الجسم.
  • يجب أن يُرجع مقطعًا متكررًا بأجزاء من نص المستند.

مثال آخر على كائن يتوافق مع هذه القواعد وينتج نفس التأثير هو:

 class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body

واجهة الخادم

قد يتفاعل خادم WSGI مع هذا التطبيق مثل هذا:

 def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()

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

خصوصية أخرى لمسؤوليات الخادم هي استدعاء طريقة close الاختيارية على مكرر الاستجابة ، إذا كانت موجودة. كما هو موضح في مقالة Graham Dumpleton هنا ، غالبًا ما يتم التغاضي عنها في WSGI. استدعاء هذه الطريقة ، إن وجدت ، يسمح للتطبيق بتحرير أي موارد قد لا يزال يحتفظ بها.

حجة environ التطبيق القابل للاستدعاء

يجب أن تكون معلمة environ كائن قاموس. يتم استخدامه لتمرير الطلب ومعلومات الخادم إلى التطبيق ، بنفس الطريقة التي يعمل بها CGI. في الواقع ، جميع متغيرات بيئة CGI صالحة في WSGI ويجب على الخادم تمرير كل ما ينطبق على التطبيق.

في حين أن هناك العديد من المفاتيح الاختيارية التي يمكن تمريرها ، إلا أن العديد منها إلزامي. أخذ طلب GET التالي كمثال:

 $ curl 'http://localhost:8000/auth?user=obiwan&token=123'

هذه هي المفاتيح التي يجب أن يوفرها الخادم ، والقيم التي سيأخذونها:

مفتاح قيمة تعليقات
REQUEST_METHOD "GET"
SCRIPT_NAME "" يعتمد إعداد الخادم
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" يعتمد إعداد الخادم
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) قدم العميل رؤوس HTTP
wsgi.version (1, 0) tuple مع إصدار WSGI
wsgi.url_scheme "http"
wsgi.input كائن يشبه الملف
wsgi.errors كائن يشبه الملف
wsgi.multithread False True إذا كان الخادم متعدد الخيوط
wsgi.multiprocess False True إذا كان الخادم يدير عمليات متعددة
wsgi.run_once False True إذا كان الخادم يتوقع تشغيل هذا البرنامج النصي مرة واحدة فقط (على سبيل المثال: في بيئة CGI)

الاستثناء من هذه القاعدة هو أنه إذا كان أحد هذه المفاتيح فارغًا (مثل CONTENT_TYPE في الجدول أعلاه) ، فيمكن حذفها من القاموس ، وسيفترض أنها تتوافق مع السلسلة الفارغة.

wsgi.input و wsgi.errors

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

يجب أن يحتوي هذان المفتاحان على كائنات تشبه الملفات ؛ أي الكائنات التي توفر واجهات يمكن قراءتها أو الكتابة إليها كتدفقات ، تمامًا مثل الكائن الذي نحصل عليه عندما نفتح ملفًا أو مقبسًا في Python. قد يبدو هذا صعبًا في البداية ، لكن لحسن الحظ ، توفر لنا بايثون أدوات جيدة للتعامل مع هذا الأمر.

أولاً ، ما نوع التدفقات التي نتحدث عنها؟ وفقًا لتعريف WSGI ، يجب أن يعالج wsgi.input و wsgi.errors كائنات bytes في كائنات Python 3 و str في Python 2. في كلتا الحالتين ، إذا كنا نرغب في استخدام مخزن مؤقت في الذاكرة لتمرير البيانات أو الحصول عليها من خلال WSGI الواجهة ، يمكننا استخدام الفئة io.BytesIO .

على سبيل المثال ، إذا كنا نكتب خادم WSGI ، فيمكننا توفير نص الطلب للتطبيق مثل هذا:

  • بالنسبة إلى Python 2.7
 import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
  • بالنسبة إلى Python 3.5
 import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)

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

  • بالنسبة إلى Python 2.7
 readstr = environ['wsgi.input'].read() # returns str object
  • بالنسبة إلى Python 3.5
 readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object

يجب استخدام دفق wsgi.errors للإبلاغ عن أخطاء التطبيق إلى الخادم ، ويجب إنهاء الأسطر بـ \n . يجب أن يعتني خادم الويب بالتحويل إلى سطر آخر ينتهي وفقًا للنظام.

وسيطة start_response للاستدعاء

يجب أن تكون الوسيطة start_response قابلة للاستدعاء مع وسيطتين مطلوبتين ، وهما status headers ، ووسيطة اختيارية ، exc_info . يجب أن يتم استدعاؤه بواسطة التطبيق قبل إرسال أي جزء من الجسم مرة أخرى إلى خادم الويب.

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

في الجزء الثاني ، أطلقنا على start_response قبل إعطاء الجزء الأول (وفي هذه الحالة فقط) من جسم الاستجابة. كلتا الطريقتين صالحة ضمن مواصفات WSGI.

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

وسيطة status الخاصة بـ start_response

يجب أن تكون وسيطة status التي تم تمريرها إلى رد الاتصال start_response عبارة عن سلسلة تتكون من رمز ووصف حالة HTTP ، مفصولة بمسافة واحدة. الأمثلة الصالحة هي: '200 OK' أو '404 Not Found' .

وسيطة headers الخاصة بـ start_response

يجب أن تكون وسيطة headers التي تم تمريرها إلى رد الاتصال start_response عبارة عن list Python من tuple s ، مع تكوين كل مجموعة كـ (header_name, header_value) . يجب أن يكون كل من اسم وقيمة كل رأس سلاسل (بغض النظر عن إصدار Python). هذا مثال نادر في أي نوع مهم ، حيث أن هذا مطلوب بالفعل بواسطة مواصفات WSGI.

فيما يلي مثال صالح لما قد تبدو عليه وسيطة header :

 response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

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

وسيطة exc_info الخاصة بـ start_response

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

يمكن الحصول على مزيد من المعلومات حول هذا الموضوع في مواصفات WSGI هنا.

قيمة إرجاع start_response - رد الاتصال write

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

على الرغم من وجودها ، إلا أنها واجهة مهملة ويجب أن تمتنع التطبيقات الجديدة عن استخدامها.

توليد هيئة الاستجابة

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

لا يوجد سوى اختلاف بسيط هنا بين WSGI في Python 2 و Python 3: في Python 3 ، يتم تمثيل جسم الاستجابة بواسطة كائنات bytes ؛ في Python 2 ، النوع الصحيح لهذا هو str .

يعد تحويل سلاسل UTF-8 إلى bytes أو str مهمة سهلة:

  • بايثون 3.5:
 body = 'unicode stuff'.encode('utf-8')
  • بايثون 2.7:
 body = u'unicode stuff'.encode('utf-8')

إذا كنت ترغب في معرفة المزيد حول التعامل مع unicode و bytestring في Python 2 ، فهناك برنامج تعليمي رائع على YouTube.

يجب أن تدعم خوادم الويب التي تنفذ WSGI أيضًا رد الاتصال write للتوافق مع الإصدارات السابقة ، كما هو موضح أعلاه.

اختبار التطبيق الخاص بك بدون خادم ويب

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

خذ هذا النص الصغير ، على سبيل المثال:

 from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}

بهذه الطريقة ، قد نقوم ، على سبيل المثال ، بتهيئة بعض بيانات الاختبار والوحدات النمطية الوهمية في تطبيقنا ، وإجراء مكالمات GET لاختبار ما إذا كان يستجيب وفقًا لذلك. يمكننا أن نرى أنه ليس خادم ويب فعليًا ، ولكنه واجهات مع تطبيقنا بطريقة قابلة للمقارنة من خلال تزويد التطبيق برد اتصال start_response وقاموس مع متغيرات البيئة الخاصة بنا. في نهاية الطلب ، يستهلك مكرر نص الاستجابة ويعيد سلسلة بكل محتوياتها. يمكن إنشاء طرق مماثلة (أو طريقة عامة) لأنواع مختلفة من طلبات HTTP.

يتم إحتوائه

تعد WSGI جزءًا مهمًا من أي إطار عمل ويب Python تقريبًا.

في هذه المقالة ، لم نتطرق إلى كيفية تعامل WSGI مع عمليات تحميل الملفات ، حيث يمكن اعتبارها ميزة "متقدمة" أكثر ، وليست مناسبة لمقال تمهيدي. إذا كنت ترغب في معرفة المزيد عنها ، فقم بإلقاء نظرة على قسم PEP-3333 الذي يشير إلى معالجة الملفات.

آمل أن تكون هذه المقالة مفيدة في المساعدة في تكوين فهم أفضل لكيفية تعامل Python مع خوادم الويب ، وتسمح للمطورين باستخدام هذه الواجهة بطرق ممتعة ومبتكرة.

شكر وتقدير

أود أن أشكر محرري Nick McCrea لمساعدتي في هذا المقال. بسبب عمله ، أصبح النص الأصلي أكثر وضوحًا ولم يتم تصحيح العديد من الأخطاء.