Twisted

بنية تطبيقات المصادر المفتوحة (المجلد 2)Twisted

بنية تطبيقات المصادر المفتوحة (المجلد 2) Twisted

Jessica McKellar

إذا استمتعت بهذه الكتب، فربما يعجبك أيضًا التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.

‏Twisted هو محرّك شبكات مدفوع بالأحداث (event-driven) مكتوب بلغة Python. وقد وُلد في مطلع الألفية الثانية، حين لم يكن في متناول كتّاب الألعاب الشبكية سوى القلّة من المكتبات القابلة للتوسّع، ولا مكتبات عابرة للمنصّات إطلاقًا، بأي لغة كانت. حاول مؤلّفو Twisted تطوير ألعاب ضمن مشهد الشبكات القائم، فواجهون صعوبات، ورأوا حاجة واضحة إلى إطار عمل شبكات قابل للتوسّع ومدفوع بالأحداث وعابر للمنصّات، فقرّروا أن يضيفوا هذا الإطار بأنفسهم، متعلّمين من أخطاء ومشاقّ الكتّاب السابقين في مجال الألعاب والتطبيقات الشبكية.

يدعم Twisted بروتوكولات نقل وتطبيق شائعة كثيرة، منها TCP وUDP وSSL/TLS وHTTP وIMAP وSSH وIRC وFTP. ومثل اللغة التي كُتب بها، فهو «يشمل كل ما تحتاجه» (batteries-included)؛ إذ يأتي Twisted مع تطبيقات عميل وتطبيقات خادم لكل بروتوكولاته، إضافة إلى أدوات تسهّل ضبط ونشر تطبيقات Twisted الجاهزة للإنتاج من سطر الأوامر.

21.1. لماذا Twisted؟

في عام 2000، كان glyph، مبتكر Twisted، يعمل على لعبة جماعية نصية اسمها Twisted Reality. كانت فوضى كبيرة من الخيوط (threads)، بثلاثة خيوط لكل اتصال، مكتوبة بلغة Java. فهناك خيط للمدخلات يُعيَّن عند القراءة، وخيط للمخرجات يُعيَّن عند الكتابة بشكل ما، وخيط «للمنطق» ينام في انتظار انتهاء المؤقّتات أو تكدّس الأحداث. وحين كان اللاعبون يتحرّكون في المشهد الافتراضي ويتفاعلون، كانت الخيوط تدخل في تعارضات ميتة (deadlock)، وتتلف الذواكر المؤقتة (caches)، ولم يكن منطق القفل صحيحًا على الإطلاق—لقد جعل استعمال الخيوط البرمجيات معقّدة وملتوية وصعبة التوسّع.

بحثًا عن بدائل، اكتشف Python، ولا سيما وحدة select في Python لتعدّد الإرسال (multiplexing) للمدخلات/المخرجات من كائنات التدفّق مثل المقابس (sockets) والأنابيب (تصف مواصفة Unix أحادية الإصدار، الإصدار 3 (SUSv3) واجهة select). وفي ذلك الوقت، لم تكن Java تكشف واجهة select الخاصة بنظام التشغيل ولا أي واجهة برمجية أخرى للمدخلات/المخرجات غير المتزامنة (أُضيفت حزمة java.nio الخاصة بالمدخلات/المخرجات غير الحاجبة في J2SE 1.4 الصادرة عام 2002). وأثبت نموذج أولي سريع للعبة بلغة Python باستخدام select على الفور أنه أقلّ تعقيدًا وأكثر موثوقية من النسخة متعددة الخيوط.

تحوّل على الفور إلى Python وselect والبرمجة المدفوعة بالأحداث، فكتب عميلًا وخادمًا للعبة بلغة Python باستخدام واجهة select. لكنه أراد بعدها أن يفعل المزيد. وجوهرًا، أراد أن يتمكّن من تحويل النشاط الشبكي إلى استدعاءات دوالّ على الكائنات داخل اللعبة. ماذا لو استطعت استقبال البريد الإلكتروني داخل اللعبة، مثل خادم البريد في Nethack؟ ماذا لو كان لكل لاعب في اللعبة صفحة رئيسية؟ وجد glyph نفسه بحاجة إلى عملاء وخوادم IMAP وHTTP جيّدة في Python تستخدم select.

لجأ أولًا إلى Medusa، وهي منصّة طُوّرت في منتصف التسعينيات لكتابة خوادم الشبكات بلغة Python بالاعتماد على وحدة asyncore. وasyncore معالِج مقابس غير متزامن يبني واجهة موزِّع (dispatcher) وواجهة استدعاءات فوق واجهة select في نظام التشغيل.

كان هذا اكتشافًا ملهمًا لـ glyph، لكن في Medusa عيبان:

  1. كانت في طريقها إلى فقدان الصيانة بحلول عام 2001، حين بدأ glyph العمل على Twisted Reality.
  2. ‏asyncore غلاف رقيق جدًا حول المقابس، لدرجة أن مبرمجي التطبيقات ظلّوا مضطرين إلى التعامل مع المقابس مباشرةً. وهذا يعني أن قابلية النقل تبقى مسؤولية المبرمج. إضافةً إلى ذلك، كان دعم asyncore لويندوز معطوبًا في ذلك الوقت، وكان glyph يعلم أنه يريد تشغيل عميل بواجهة رسومية على ويندوز.

كان glyph يواجه احتمال تنفيذ منصّة شبكات بنفسه، وأدرك أن Twisted Reality فتحت الباب أمام مسألة لا تقلّ إثارة عن لعبته.

ومع مرور الوقت، تحوّل Twisted Reality — أي اللعبة — إلى Twisted — أي منصّة الشبكات — التي تفعل ما لم تفعله منصّات الشبكات القائمة في Python:

  • استعمال البرمجة المدفوعة بالأحداث بدلًا من البرمجة متعددة الخيوط.
  • أن تكون عابرة للمنصّات: توفير واجهة موحّدة لأنظمة إشعار الأحداث التي توفّرها أنظمة التشغيل الكبرى.
  • أن تكون «شاملة لكل ما تحتاجه» (batteries-included): توفير تطبيقات جاهزة للبروتوكولات الشائعة في طبقة التطبيق، بحيث يصبح Twisted مفيدًا للمطوّرين على الفور.
  • الامتثال لـ RFCs، وإثبات هذا الامتثال عبر مجموعة اختبارات متينة.
  • جعل استعمال عدّة بروتوكولات شبكية معًا أمرًا يسيرًا.
  • أن تكون قابلة للتوسّع.

21.2. بنية Twisted

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

البرمجة المدفوعة بالأحداث (event-driven programming) هي نموذج برمجي يحدّد تدفّق البرنامج أحداثٌ خارجية. ويتميز بحلقة أحداث (event loop) وباستعمال دوالّ استدعاء (callbacks) لتشغيل الإجراءات عند وقوع الأحداث. أمّا النموذجان البرمجيان الشائعان الآخران فهما البرمجة المتزامنة (أحادية الخيط) والبرمجة متعددة الخيوط.

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

الشكل 21.1: نماذج الخيوط

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

في النسخة متعددة الخيوط من البرنامج، تُنفَّذ المهام الثلاث التي تحجز تنفيذها أثناء عملها في خيوط تحكّم منفصلة. ويدير نظام التشغيل هذه الخيوط، وقد تعمل على عدّة معالجات في الوقت نفسه أو بالتناوب على معالج واحد. وهذا يتيح لبعض الخيوط أن تحقق تقدّمًا بينما يحجز غيرها على موارد. وهو غالبًا أكثر كفاءة زمنيًا من البرنامج المتزامن المكافئ، لكن على المبرمج أن يكتب شيفرة تحمي الموارد المشتركة التي يمكن الوصول إليها من عدّة خيوط في الوقت نفسه. وقد يصعب التبرير المنطقي للبرامج متعددة الخيوط، لأن على المبرمج الآن أن ينتبه إلى سلامة الخيوط عبر التسلسل (locking)، أو إعادة الدخول (reentrancy)، أو التخزين المحلي لكل خيط (thread-local storage)، أو آليات أخرى، وكلّها عند تنفيذها على نحو خاطئ قد تؤدّي إلى أخطاء دقيقة ومؤلمة.

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

النموذج المدفوع بالأحداث خيار جيّد عادةً حين تتوفّر الشروط الآتية:

  1. مهام كثيرة، تكون…
  2. مستقلّة في معظمها (فلا تحتاج إلى الاتصال ببعضها أو الانتظار بعضها)، و…
  3. بعض هذه المهام يحجز تنفيذها في انتظار الأحداث.

وهو أيضًا خيار جيّد حين يضطر التطبيق إلى مشاركة بيانات قابلة للتغيّر بين المهام، لأنه لا يتعيّن عندئذٍ إجراء أي مزامنة.

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

إعادة استعمال التطبيقات القائمة

كانت هناك بالفعل عملاء وخوادم شائعة كثيرة لمختلف بروتوكولات الشبكات حين أُنشئ Twisted. فلماذا لم يستخدم glyph ببساطة Apache أو IRCd أو BIND أو OpenSSH أو أي من التطبيقات الأخرى الموجودة مسبقًا، التي كان على Twisted أن يعيد تنفيذ عملائها وخوادمها من الصفر؟

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

‏Twisted من ناحية أخرى، تُكتب العملاء والخوادم بلغة Python باستخدام واجهة متّسقة. وهذا يجعل من اليسير كتابة عملاء وخوادم جديدة، ومشاركة الشيفرة بين العملاء والخوادم، ومشاركة منطق التطبيق بين البروتوكولات، واختبار شيفرتك.

المفاعل (Reactor)

يطبّق Twisted نمط التصميم reactor (المفاعل)، الذي يصف تعدّد الإرسال (demultiplexing) وتوزيع الأحداث من مصادر متعدّدة إلى معالجاتها ضمن بيئة أحادية الخيط.

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

يحقّق المفاعل في جوهره الآتي:

while True:
    timeout = time_until_next_timed_event()
    events = wait_for_events(timeout)
    events += timed_events_until(now())
    for event in events:
        event.process()

‏reactor القائم على واجهة poll (الموصوفة في مواصفة Unix أحادية الإصدار، الإصدار 3 (SUSv3)) هو الافتراضي الحالي على جميع المنصّات. ويدعم Twisted إضافةً إلى ذلك عددًا من واجهات تعدّد الإرسال عالية الحجم الخاصة بكل منصّة. وتشمل المفاعلات الخاصة بالمنصّات مفاعل KQueue القائم على آلية kqueue في FreeBSD، ومفاعلًا قائمًا على epoll للأنظمة التي تدعم واجهة epoll (وهي لينكس 2.6 حاليًا)، ومفاعل IOCP القائم على منافذ إكمال الإدخال/الإخراج في ويندوز.

ومن أمثلة التفاصيل المعتمدة على التنفيذ التي يتكفّل بها Twisted في عملية الاستطلاع (polling):

  • حدود الشبكة ونظام الملفات.
  • سلوك التخزين المؤقّت (buffering).
  • كيفية اكتشاف انقطاع الاتصال.
  • القيم التي تُعاد في حالات الخطأ.

كما يتكفّل تنفيذ المفاعل في Twisted باستعمال واجهات عدم الحجب الأساسية على نحو صحيح، ومعالجة الحالات الحدّية النادرة على نحو صحيح. ولا تكشف Python واجهة IOCP إطلاقًا، لذا تحتفظ Twisted بتنفيذها الخاص.

إدارة سلاسل الاستدعاءات

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

لنستعرض بعض مزالق البرامج المدفوعة بالأحداث بمقارنة نسختين متزامنة وغير متزامنة من أداة مبسّطة لجلب عناوين URL، مكتوبة بشبه بايثون:

جالب URL المتزامن:

import getPage

def processPage(page):
    print page

def logError(error):
    print error

def finishProcessing(value):
    print "Shutting down..."
    exit(0)

url = "http://google.com"
try:
    page = getPage(url)
    processPage(page)
except Error, e:
    logError(error)
finally:
    finishProcessing()

جالب URL غير المتزامن:

from twisted.internet import reactor
import getPage

def processPage(page):
    print page
    finishProcessing()

def logError(error):
    print error
    finishProcessing()

def finishProcessing(value):
    print "Shutting down..."
    reactor.stop()

url = "http://google.com"
# getPage takes: url, 
#    success callback, error callback
getPage(url, processPage, logError)

reactor.run()

في جالب URL غير المتزامن، تُشغّل reactor.run() حلقة أحداث المفاعل. وفي كلٍّ من النسختين المتزامنة وغير المتزامنة، تقوم دالة getPage افتراضية بالعمل الخاص بجلب الصفحة. وتُستدعى processPage إذا نجح الجلب، وتُستدعى logError إذا أُلقي استثناء Exception أثناء محاولة جلب الصفحة. وفي الحالتين معًا، تُستدعى finishProcessing بعد ذلك.

يقابل الاستدعاء الخاص بـ logError في النسخة غير المتزامنة الجزءَ except من الكتلة try/except في النسخة المتزامنة. ويقابل الاستدعاء الخاص بـ processPage الجزءَ else، ويقابل الاستدعاء غير المشروط لـ finishProcessing الجزءَ finally.

في النسخة المتزامنة، وبفضل بنية كتلة try/except، تُستدعى إحدى الدالتين logError وprocessPage بالضبط، وتُستدعى finishProcessing دائمًا مرة واحدة؛ أمّا في النسخة غير المتزامنة فيصبح من مسؤولية المبرمج استدعاء السلسلة الصحيحة من استدعاءات النجاح والخطأ. فلو أُهملت، بسبب خطأ برمجي، استدعاءُ finishProcessing داخل processPage أو logError ضمن سلسلة الاستدعاءات الخاصة بكل منهما، لما توقّف المفاعل قطّ، ولظلّ البرنامج يعمل إلى الأبد.

يسترشد هذا المثال المبسّط إلى التعقيد الذي أرهق المبرمجين في السنوات الأولى من تطوير Twisted. وقد استجاب Twisted لهذا التعقيد بتنمية كائن اسمه Deferred.

كائنات Deferred

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

تحمل كائنات Deferred زوجًا من سلاسل الاستدعاءات، واحدة للنجاح (callbacks) وأخرى للأخطاء (errbacks). وتبدأ كائنات Deferred بسلسلتين فارغتين. وتضيف كل واحدة منهما أزواجًا من الاستدعاءات ومعالجات الأخطاء للتعامل مع النجاحات والإخفاقات عند كل نقطة في معالجة الأحداث. وعندما تصل نتيجة غير متزامنة، يُطلَق الكائن Deferred (fired)، وتُستدعى الاستدعاءات أو معالجات الأخطاء المناسبة بالترتيب الذي أُضيفت به.

إليك نسخة من شبه كود جالب URL غير المتزامن تستعمل كائنات Deferred:

from twisted.internet import reactor
import getPage

def processPage(page):
    print page

def logError(error):
    print error

def finishProcessing(value):
    print "Shutting down..."
    reactor.stop()

url = "http://google.com"
deferred = getPage(url) # getPage returns a Deferred
deferred.addCallbacks(success, logError)
deferred.addBoth(stop)

reactor.run()

في هذه النسخة، تُستدعى معالجات الأحداث نفسها، لكنها جميعًا مسجّلة مع كائن Deferred واحد، بدلًا من انتشارها في الشيفرة وتمريرها وسائط إلى getPage.

يُنشَأ كائن Deferred بمرحلتين من الاستدعاءات. أولًا، تضيف addCallbacks الاستدعاءَ processPage ومعالجَ الأخطاء logError إلى المرحلة الأولى من سلسلتيهما. ثم تضيف addBoth الدالة finishProcessing إلى المرحلة الثانية من السلسلتين معًا. وعلى نحو بياني، تبدو سلاسل الاستدعاءات كما في الشكل 21.2.

الشكل 21.2: سلاسل الاستدعاءات

لا يمكن إطلاق الكائن Deferred إلا مرة واحدة؛ ومحاولة إطلاقه مرّة أخرى تُطلق استثناء Exception. وهذا يمنح كائنات Deferred دلالة أقرب إلى دلالة كتل try/except في نظائرها المتزامنة، مما يجعل معالجة الأحداث غير المتزامنة أسهل في التتبّع المنطقي ويتفادى الأخطاء الدقيقة الناتجة عن استدعاء الاستدعاءات أكثر من مرة أو أقل من مرة لحدث واحد.

فهم كائنات Deferred جزء مهم من فهم تدفّق برامج Twisted. غير أنه عند استعمال التجريدات عالية المستوى التي يوفّرها Twisted لبروتوكولات الشبكات، فكثيرًا ما لا حاجة إلى استعمال كائنات Deferred مباشرةً على الإطلاق.

تجريد Deferred قوي، وقد اقتبسته منصّات مدفوعة بالأحداث كثيرة أخرى، منها jQuery وDojo وMochikit.

وسائط النقل (Transports)

تمثّل وسائط النقل الاتصال بين طرفين يتبادلان البيانات عبر شبكة. وهي مسؤولة عن وصف تفاصيل الاتصال، مثل كونه قائمًا على التدفّق (stream-oriented) أو على مخططات البيانات (datagram-oriented)، وعن التحكم في التدفّق (flow control)، وعن الموثوقية. وتُعدّ TCP وUDP ومقابس Unix أمثلة على وسائط النقل. وقد صُمّمت لتكون «وحدات وظيفية بأدنى حدّ ممكن وقابلة لإعادة الاستعمال إلى أقصى حدّ» (minimally functional units that are maximally reusable)، وهي مفصولة عن تطبيقات البروتوكولات، مما يتيح للعدّة من البروتوكولات أن تستعمل نوع وسيلة النقل نفسه. وتطبّق وسائط النقل واجهة ITransport التي تضمّ الدوالّ التالية:

write كتابة بعض البيانات إلى الاتصال المادي، بالتسلسل، وبصورة غير حاجبة.
writeSequence كتابة قائمة من النصوص إلى الاتصال المادي.
loseConnection كتابة جميع البيانات المعلّقة ثم إغلاق الاتصال.
getPeer الحصول على العنوان البعيد لهذا الاتصال.
getHost الحصول على عنوان هذا الطرف من الاتصال.

كما أن فصل وسائط النقل عن البروتوكولات يجعل اختبار الطبقتين أسهل. إذ يمكن لوسيلة نقل وهمية (mock transport) أن تكتب البيانات في سلسلة نصية لفحصها.

البروتوكولات

تصف البروتوكولات كيفية معالجة أحداث الشبكة بصورة غير متزامنة. وتُعدّ HTTP وDNS وIMAP أمثلة على بروتوكولات التطبيق. وتطبّق البروتوكولات واجهة IProtocol التي تضمّ الدوالّ التالية:

makeConnection إنشاء اتصال مع وسيلة نقل ومع خادم.
connectionMade تُستدعى عند إنشاء الاتصال.
dataReceived تُستدعى كلّما وصلت بيانات.
connectionLost تُستدعى عند إغلاق الاتصال.

تُوضّح العلاقة بين المفاعل والبروتوكولات ووسائل النقل بأفضل شكل من خلال مثال. وإليك تنفيذين كاملين لخادم صدى وعميل صدى، أولًا الخادم:

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
    def dataReceived(self, data):
        # As soon as any data is received, write it back
        self.transport.write(data)

class EchoFactory(protocol.Factory):
    def buildProtocol(self, addr):
        return Echo()

reactor.listenTCP(8000, EchoFactory())
reactor.run()

ثمّ العميل:

from twisted.internet import reactor, protocol

class EchoClient(protocol.Protocol):
   def connectionMade(self):
       self.transport.write("hello, world!")

   def dataReceived(self, data):
       print "Server said:", data
       self.transport.loseConnection()

   def connectionLost(self, reason):
       print "connection lost"

class EchoFactory(protocol.ClientFactory):
   def buildProtocol(self, addr):
       return EchoClient()

   def clientConnectionFailed(self, connector, reason):
       print "Connection failed - goodbye!"
       reactor.stop()

   def clientConnectionLost(self, connector, reason):
       print "Connection lost - goodbye!"
       reactor.stop()

reactor.connectTCP("localhost", 8000, EchoFactory())
reactor.run()

تشغيل سكربت الخادم يبدأ خادم TCP يستمع إلى الاتصالات على المنفذ 8000. ويستخدم الخادم بروتوكول Echo، وتُكتب البيانات عبر وسيلة نقل من نوع TCP. وتشغيل العميل يُنشئ اتصال TCP بالخادم، ويعيد ردّ الخادم (echo)، ثم ينهي الاتصال ويوقف المفاعل. وتُستخدم المصانع (factories) لإنتاج نسخ من البروتوكولات لطرفي الاتصال. والتواصل غير متزامن على كلا الطرفين؛ ويتكفّل connectTCP بتسجيل الاستدعاءات مع المفاعل ليُبلَّغ عند توفّر بيانات قابلة للقراءة من مقبس.

التطبيقات

‏Twisted هو محرّك لإنتاج خوادم وعملاء شبكات قابلين للتوسّع وعابرين للمنصّات. وجعل نشر هذه التطبيقات بطريقة معيارية في بيئات الإنتاج جزء مهمّ من انتشار منصّة كهذه على نطاق واسع.

ولتحقيق ذلك، طوّر Twisted بنية تطبيقات Twisted (Twisted application infrastructure)، أي طريقة قابلة لإعادة الاستعمال والتهيئة لنشر تطبيق Twisted. وهي تتيح للمبرمج تفادي الشيفرة المتكرّرة (boilerplate) عبر ربط التطبيق بأدوات قائمة لتخصيص طريقة تشغيله، بما في ذلك التحوّل إلى عملية خفية (daemonization) والتسجيل في السجلّات، واستعمال مفاعل مخصّص، وتحليل الأداء (profiling)، وغيرها.

تتألف بنية التطبيقات من أربعة أجزاء رئيسية: الخدمات (Services)، والتطبيقات (Applications)، وإدارة الإعدادات (عبر ملفات TAC والإضافات)، وأداة سطر الأوامر twistd. ولتوضيح هذه البنية، سنحوّل خادم الصدى من القسم السابق إلى تطبيق (Application).

الخدمة (Service)

الخدمة (Service) هي أي شيء يمكن تشغيله وإيقافه ويلتزم بواجهة IService. ويأتي Twisted مع تطبيقات خدمات لبروتوكولات TCP وFTP وHTTP وSSH وDNS وكثير غيرها. ويمكن للخدمات المتعدّدة أن تسجّل نفسها مع تطبيق واحد.

جوهر واجهة IService هو:

startService تشغيل الخدمة. وقد يشمل ذلك تحميل بيانات الإعداد، أو إنشاء اتصالات بقاعدة البيانات، أو البدء في الاستماع على منفذ
stopService إيقاف الخدمة. وقد يشمل ذلك حفظ الحالة على القرص، أو إغلاق اتصالات قاعدة البيانات، أو إيقاف الاستماع على منفذ

تستخدم خدمتنا للصدى بروتوكول TCP، لذا يمكننا استعمال التطبيق الافتراضي TCPServer في Twisted لهذه الواجهة IService.

التطبيق (Application)

التطبيق (Application) هو الخدمة العليا التي تمثّل تطبيق Twisted بأكمله. وتسجّل الخدمات نفسها مع التطبيق، وتبحث أداة النشر twistd الموصوفة أدناه عن التطبيقات فتُشغّلها.

سننشئ تطبيق صدى (echo Application) تسجّل معه خدمة الصدى نفسها.

ملفات TAC

عند إدارة تطبيقات Twisted في ملف Python عادي، يكون المبرمج مسؤولًا عن كتابة الشيفرة التي تشغّل المفاعل وتوقفه وتضبط التطبيق. أمّا ضمن بنية تطبيقات Twisted، فتوجد تطبيقات البروتوكولات في وحدة (module) منفصلة، وتُسجَّل الخدمات التي تستعمل تلك البروتوكولات في ملف إعداد تطبيق Twisted (TAC)، ويتولّى المفاعلَ والإعدادَ أداة خارجية.

لتحويل خادم الصدى إلى تطبيق صدى، يمكننا اتباع خوارزمية بسيطة:

  1. نقل أجزاء البروتوكول من خادم الصدى إلى وحدة خاصة بها.
  2. داخل ملف TAC: إنشاء تطبيق صدى.
  3. إنشاء نسخة من خدمة TCPServer ستستعمل EchoFactory الخاصة بنا، وتسجيلها مع التطبيق.

ستتولّى twistd، التي سنناقشها أدناه، شيفرة إدارة المفاعل. وينتهي شيفرة التطبيق إلى الشكل التالي:

ملف echo.py:

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
    def dataReceived(self, data):
        self.transport.write(data)

class EchoFactory(protocol.Factory):
    def buildProtocol(self, addr):
    return Echo()

ملف echo_server.tac:

from twisted.application import internet, service
from echo import EchoFactory

application = service.Application("echo")
echoService = internet.TCPServer(8000, EchoFactory())
echoService.setServiceParent(application)

twistd

‏twistd (تُنطق «توست-دي») أداة عابرة للمنصّات لنشر تطبيقات Twisted. فهي تشغّل ملفات TAC وتتكفّل بتشغيل التطبيق وإيقافه. وكجزء من مقاربة Twisted «الشاملة لكل ما تحتاجه» في برمجة الشبكات، تأتي twistd مع عدد من رايات الإعداد المفيدة، منها تحويل التطبيق إلى عملية خفية، وموقع ملفات السجلّ، والإسقاط (dropping) للصلاحيات، والتشغيل داخل chroot، والتشغيل تحت مفاعل غير افتراضي، بل وتشغيل التطبيق تحت مُحلِّل أداء (profiler).

يمكننا تشغيل تطبيق خادم الصدى الخاص بنا بما يلي:

$ twistd -y echo_server.tac

في أبسط هذه الحالات، تُشغّل twistd نسخةً من التطبيق في وضع العملية الخفية (daemonized)، وهي تكتب في twistd.log. وبعد تشغيل التطبيق وإيقافه، يبدو السجلّ كالتالي:

2011-11-19 22:23:07-0500 [-] Log opened.
2011-11-19 22:23:07-0500 [-] twistd 11.0.0 (/usr/bin/python 2.7.1) starting up.
2011-11-19 22:23:07-0500 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2011-11-19 22:23:07-0500 [-] echo.EchoFactory starting on 8000
2011-11-19 22:23:07-0500 [-] Starting factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Received SIGTERM, shutting down.
2011-11-19 22:23:20-0500 [-] (TCP Port 8000 Closed)
2011-11-19 22:23:20-0500 [-] Stopping factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Main loop terminated.
2011-11-19 22:23:20-0500 [-] Server Shut Down.

يسمح تشغيل خدمة باستخدام بنية تطبيقات Twisted للمطوّرين بتخطّي كتابة شيفرة متكرّرة (boilerplate) للوظائف الشائعة في الخدمات مثل السجلّات والتحوّل إلى عملية خفية. كما أنه يؤسّس واجهة سطر أوامر معيارية لنشر التطبيقات.

الإضافات (Plugins)

البديل عن النظام القائم على ملفات TAC لتشغيل تطبيقات Twisted هو نظام الإضافات. فبينما يجعل نظام TAC من السهل تسجيل تدرّجات هرمية بسيطة من الخدمات المعرَّفة مسبقًا ضمن ملف إعداد التطبيق، يجعل نظام الإضافات من السهل تسجيل خدمات مخصّصة كأوامر فرعية لأداة twistd، وتوسيع واجهة سطر الأوامر الخاصة بالتطبيق.

باستعمال هذا النظام:

  1. كل ما يلزم أن يبقى مستقرًّا هو واجهة برمجية الإضافات (plugin API)، مما يجعل من السهل لمطوّري الأطراف الثالثة توسيع البرمجية.
  2. قابلية اكتشاف الإضافات مُرمَّزة (codified). إذ يمكن تحميل الإضافات وحفظها عند تشغيل البرنامج لأول مرة، أو إعادة اكتشافها في كل مرة يبدأ فيها البرنامج، أو استطلاعها بصورة متكرّرة أثناء التشغيل، مما يتيح اكتشاف إضافات جديدة مثبّتة بعد بدء البرنامج.

لتوسيع برنامج باستعمال نظام إضافات Twisted، كل ما يلزم فعله هو إنشاء كائنات تطبّق واجهة IPlugin ووضعها في موقع معيّن يعرف نظام الإضافات أنّ يبحث فيه.

وبما أننا حوّلنا خادم الصدى بالفعل إلى تطبيق Twisted، فإن تحويله إلى إضافة Twisted أمر مباشر. إلى جانب وحدة echo السابقة، التي تضمّ تعريفَي البروتوكول Echo وEchoFactory، نضيف مجلدًا اسمه twisted يحتوي على مجلد فرعي اسمه plugins يحوي تعريف إضافة خادم الصدى. وستتيح لنا هذه الإضافة تشغيل خادم صدى وتحديد المنفذ المستعمل كوسائط لأداة twistd:

from zope.interface import implements

from twisted.python import usage
from twisted.plugin import IPlugin
from twisted.application.service import IServiceMaker
from twisted.application import internet

from echo import EchoFactory

class Options(usage.Options):
    optParameters = [["port", "p", 8000, "The port number to listen on."]]

class EchoServiceMaker(object):
    implements(IServiceMaker, IPlugin)
    tapname = "echo"
    description = "A TCP-based echo server."
    options = Options

    def makeService(self, options):
        """
        Construct a TCPServer from a factory defined in myproject.
        """
        return internet.TCPServer(int(options["port"]), EchoFactory())

serviceMaker = EchoServiceMaker()

سيظهر خادم الصدى الخاص بنا الآن بوصفه خيار خادم في مخرجات twistd --help، وتشغيل twistd echo --port=1235 سيبدأ خادم صدى على المنفذ 1235.

يأتي Twisted مع نظام مصادقة قابل للتبديل للخوادم اسمه twisted.cred، ومن الاستعمالات الشائعة لنظام الإضافات إضافة نمط مصادقة إلى تطبيق. فيمكن استعمال twisted.cred AuthOptionMixin لإضافة دعم سطر الأوامر لمختلف أنواع المصادقة جاهزًا، أو لإضافة نوع جديد. فمثلًا يمكن إضافة مصادقة عبر قاعدة بيانات كلمات مرور محلية على Unix أو عبر خادم LDAP باستعمال نظام الإضافات.

تأتي twistd مع إضافات لكثير من بروتوكولات Twisted المدعومة، مما يحوّل تشغيل خادم إلى أمر واحد. وإليك بعض الأمثلة على خوادم twistd التي تأتي مع Twisted:

‏twistd web --port 8080 --path . تشغيل خادم HTTP على المنفذ 8080، يقدّم محتوى ثابتًا وديناميكيًا من مجلد العمل الحالي. ‏twistd dns -p 5553 --hosts-file=hosts تشغيل خادم DNS على المنفذ 5553، يحلّ النطاقات من ملف اسمه hosts بصيغة /etc/hosts. ‏sudo twistd conch -p tcp:2222 تشغيل خادم ssh على المنفذ 2222. ويجب إعداد مفاتيح ssh بشكل منفصل. ‏twistd mail -E -H localhost -d localhost=emails تشغيل خادم ESMTP POP3 يقبل البريد الموجّه إلى localhost ويحفظه في المجلد emails.

يجعل twistd من السهل تشغيل خادم لاختبار العملاء، لكنه في الوقت نفسه شيفرة قابلة للتبديل وجاهزة للإنتاج.

وفي هذا الصدد نجحت آليات نشر تطبيقات Twisted عبر ملفات TAC والإضافات وtwistd. غير أنه — وفق الروايات المتداولة — تنتهي معظم عمليات نشر Twisted الكبيرة إلى إعادة كتابة بعض أدوات الإدارة والمراقبة هذه؛ فالبنية لا توفّر لمديري الأنظمة ما يحتاجون إليه تمامًا. وهذا انعكاس لحقيقة أن Twisted لم تحظ تاريخيًا بمساهمة معمارية كبيرة من مديري الأنظمة—الأشخاص الذين هم خبراء في نشر التطبيقات وصيانتها.

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

21.3. نظرة إلى الوراء والدروس المستفادة

احتفل Twisted مؤخرًا بعيده العاشر. ومنذ نشوئه، وبدافعه من مشهد الألعاب الشبكية في مطلع الألفية الثانية، بلغ في الغالبية العظمى من هدفه: أن يكون محرّك شبكات مدفوعًا بالأحداث وقابلًا للتوسّع وعابرًا للمنصّات. ويُستخدم Twisted في بيئات الإنتاج في شركات تراوح من Google وLucasfilm إلى Justin.TV ومنصة Launchpad للتعاون البرمجي. وتطبيقات الخوادم المبنية على Twisted هي جوهر تطبيقات مفتوحة المصدر كثيرة أخرى، منها BuildBot وBitTorrent وTahoe-LAFS.

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

وكان هناك حذف مهمّ واحد، أثره يكاد يكون معدومًا في التنفيذ الحالي: Twisted Application Persistence.

ثبات تطبيقات Twisted (Twisted Application Persistence)

كان Twisted Application Persistence (TAP) طريقةً للاحتفاظ بإعداد التطبيق وحالته في ملف pickle. وكان تشغيل التطبيق باستعمال هذا المخطط عمليةً من خطوتين:

  1. إنشاء ملف pickle الذي يمثّل تطبيقًا، باستعمال أداة mktap التي لم تعد موجودة الآن.
  2. استعمال twistd لفكّ ترميز (unpickle) التطبيق وتشغيله.

استُلهِم هذا الإجراء من صور Smalltalk، ومن النفور من تكاثر لغات الإعداد الظاهرية ad hoc التي يصعب كتابة سكربتات لها، ومن الرغبة في التعبير عن تفاصيل الإعداد بلغة Python.

أدخلت ملفات TAP تعقيدًا غير مرغوب فيه على الفور. فكانت الأصناف تتغيّر في Twisted دون أن تتغيّر نسخ هذه الأصناف في ملف pickle. ومحاولة استعمال طرائق أو سمات الأصناف من نسخة أحدث من Twisted على الكائن المُرمَّز (pickled object) كانت تُسقط التطبيق. وقد أُدخل مفهوم «مُحدِّثات الإصدار» (upgraders) التي تُرقّي ملفات pickle إلى إصدارات واجهة برمجية جديدة، لكن اضطرّ حينها إلى صيانة مصفوفة من المحدِّثات وإصدارات pickle واختبارات وحدات تغطّي جميع مسارات الترقية الممكنة، وظلّ التعرّف الشامل على جميع تغييرات الواجهات صعبًا وعرضةً للأخطاء.

جرى التخلّي عن ملفات TAP وأدواتها المرتبطة، ثم حُذفت في النهاية من Twisted واستُبدلت بملفات TAC والإضافات. وأُعيد تفسير الاختصار TAP ليقصد Twisted Application Plugin، ولم يبقَ في Twisted اليوم سوى آثار ضئيلة لنظام pickle الفاشل.

كان الدرس المستفاد من انهيار ملفات TAP أن البيانات المحفوظة تحتاج، كي تبقى قابلة للصيانة بصورة معقولة، إلى مخطّط (schema) صريح. والأهمّ من ذلك، كان درسًا في إضافة التعقيد إلى مشروع: فعند التفكير في إدخال نظام جديد لحل مسألة ما، تأكّد من فهم تعقيد ذلك الحلّ واختباره جيدًا، ومن أن فوائده تستحقّ بوضوح التعقيد المضاف قبل أن تلتزم المشروع به.

web2: درسٌ في إعادة الكتابة

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

في منتصف الألفية الثانية، قرّر مطوّرو Twisted إجراء إعادة كتابة كاملة لواجهات twisted.web بوصفها مشروعًا منفصلًا داخل قاعدة شيفرة Twisted باسم web2. وكان web2 سيضمّ تحسينات عديدة على twisted.web، منها الدعم الكامل لـ HTTP 1.1 وواجهة برمجية لتدفّق البيانات.

وُسِم web2 بأنه تجريبي، لكنه انتهى إلى أن استُعمل في مشاريع كبرى رغم ذلك، بل صدر عن غير قصد وحُزِّم من قِبل Debian. وواصل التطوير في web وweb2 بصورة متزامنة طوال سنوات، وظلّ المستخدمون الجدد محبطين على الدوام من وجود المشروعين جنبًا إلى جنب، ومن غياب رسالة واضحة عن المشروع الذي ينبغي استعماله. ولم يحدث أبدًا الانتقال إلى web2، وفي عام 2011 حُذف web2 في النهاية من قاعدة الشيفرة والموقع. وبعض تحسينات web2 يجري نقلها ببطء إلى web.

إلى حدٍّ ما بسبب web2، كسب Twisted سمعةً بأنه صعب التصفّح ومربك البنية أمام الوافدين الجدد (newcomers). وبعد سنوات، لا يزال مجتمع Twisted يعمل بجدّ لمقاومة هذه الصورة.

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

لو استطاع Twisted أن يعود إلى الصفر وينفّذ web2 مرّة أخرى، لفعل المطوّرون سلسلةً من التغييرات المتوافقة مع الإصدارات السابقة (backwards-compatible changes) وإجراءات الإهمال (deprecations) على twisted.web بدلًا من إعادة الكتابة.

مواكبة الإنترنت

لا تزال طريقة استعمالنا للإنترنت تتطوّر باستمرار. والقرار بتنفيذ مجموعة من البروتوكولات في البرمجية الأساسية يكلّف Twisted الاحتفاظ بشيفرة لكل تلك البروتوكولات. وعلى التطبيقات أن تتطوّر مع تغيّر المعايير ومع تبنّي بروتوكولات جديدة، مع الحفاظ على سياسة صارمة للتوافق مع الإصدارات السابقة.

‏Twisted مشروع يقوده المتطوّعون متطوعًا في المقام الأول، والعامل المحدود للتطوير ليس حماس المجتمع بل وقت المتطوّعين. فمثلًا صدرت RFC 2616 التي تعرّف HTTP 1.1 عام 1999، وبدأ العمل على إضافة دعم HTTP 1.1 إلى تطبيقات بروتوكول HTTP في Twisted عام 2005، واكتمل العمل عام 2009. أمّا الدعم لـ IPv6، المعرَّف في RFC 2460 الصادرة عام 1998، فقد كان قيد التنفيذ وغير مدمج حتى عام 2011.

لا بدّ للتطبيقات أيضًا من أن تتطوّر تبعًا لتغيّر الواجهات التي توفّرها أنظمة التشغيل المدعومة. فمثلًا أُضيفت آلية إشعار الأحداث epoll إلى لينكس 2.5.44 عام 2002، ونما في Twisted مفاعل قائم على epoll للاستفادة من هذه الواجهة البرمجية الجديدة. وفي عام 2007، أصدرت Apple نظام OS 10.5 Leopard بتنفيذ لـ poll لا يدعم الأجهزة، وكان سلوكه معطوبًا إلى حدّ جعل Apple لا تكشف select.poll في نسختها من Python. وقد اضطرّ Twisted منذ ذلك الحين إلى التحايل على هذه المسألة وتوثيقها للمستخدمين.

أحيانًا لا يواكب Twisted تطوّر مشهد الشبكات المتغيّر، فتُنقل التحسينات إلى مكتبات خارج البرمجية الأساسية. فمثلًا بقي مشروع Wokkel، وهو مجموعة من التحسينات على دعم Jabber/XMPP في Twisted، مشروعًا مستقلًّا ينتظر الدمج سنوات دون مسؤول يقود عملية الدمج. وحاولوا إضافة WebSockets إلى Twisted عندما بدأت المتصفّحات تتبنّى دعم البروتوكول الجديد عام 2009، لكن التطوير انتقل إلى مشاريع خارجية بعد قرار عدم تضمين البروتوكول حتى ينتقل من مسوّدة IETF إلى معيار.

ومع ذلك كلّه، فإن تكاثر المكتبات والإضافات شهادةٌ على مرونة Twisted وقابليته للتوسّع. فسياسة التطوير القائمة على الاختبارات (test-driven development) (test-driven development) المرافقة، ومعايير التوثيق والترميز، تساعد المشروع على تجنّب الانحدارات (regressions) والحفاظ على التوافق مع الإصدارات السابقة، مع ضخامة البروتوكولات والمنصّات المدعومة. وهو مشروع ناضج ومستقرّ ما يزال يتمتّع بتطوّره وباعتماده النشيط.

يتطلّع Twisted إلى أن يكون محرّك إنترنتك لعشر سنوات أخرى.