نظرنا حتى الآن في سلسلة تعلم البرمجة في نظام التشغيل وقدرته على إدارة العمليات، وفي كيفية جعل السكربتات تنفذ برامج موجودةً من قبل، وكذلك استنساخ البرامج والتواصل بين تلك النسخ باستخدام الأنابيب pipes، وسندرس في هذا المقال الاتصال بالعمليات التي تعمل على حاسوب آخر عبر شبكة ما، أو بعمليات قد تكون جاريةً على نفس الحاسوب، حيث ستكون الشبكة هنا شبكةً منطقيةً logical network، وهذا النوع هو المثال الأول الذي سننظر فيه. وعلى ذلك سنشرح في هذا المقال ما يلي:
- مقدمةً بسيطةً عن الشبكات.
- أساسيات المقابس sockets.
- إنشاء عملية الخادم server process.
- إنشاء عملية العميل client process.
- التواصل من خلال المقابس.
مقدمة في الشبكات
يعمل خادم الويب web server على حاسوب في مكان ما في الشبكة، ونستطيع الوصول إليه من حاسوبنا إذا كان لدينا عنوان الويب Uniform Resource Locator واختصارًا URL، والذي هو نوع من عناوين الشبكات يحوي معلومات حول العنوان في الشبكة، واللغة التي يتحدث بها -أو البروتوكول protocol-، وكذلك مكانه على خادم الملفات التي نريد جلبها، وسنفترض أن للقارئ خلفيةً أساسيةً عن الإنترنت، ويعرف أن للحواسيب المتصلة بها عناوين.
لكن كيف تتصل تلك الحواسيب ببعضها؟ طبعًا لا يتسع المقام هنا لشرح مفصل حول الشبكات، لذا يُرجع في هذا إلى أكاديمية حسوب التي فيها شرح ماتع في أساسيات الشبكات يمكن النظر فيه، وكذلك في هذا المصدر بالإنجليزية، وخلاصة ما نريد قوله في الشبكات هو أنه عند حاجة حاسوبين على شبكة ما للاتصال ببعضهما فإنها يتصلان من خلال إرسال حزمة بيانات من حاسوب لآخر، وتشبه تلك الحزمة إلى حد كبير مظروفًا يُرسل في طرد عبر البريد مع ورقة بداخله، حيث تمثل تلك الورقة البيانات، ويمثل المظروف ترويسة الطرد packet header التي تحوي عناوين المرسل والمستقبل، ويحدد جهاز التوجيه router أو المحول switch موقع الحاسوب المستقبِل على الشبكة، ثم يوجه الحزمة إلى جهاز توجيه أو راوتر في تلك المنطقة، وتصل الحزمة في النهاية إلى نفس الجزء من الشبكة الذي يحوي الحاسوب الهدف، ويتعرف الحاسوب الهدف على عنوانه ويفتح الطرد أو الحزمة، ثم يرسل حزمة تأكيد مرةً أخرى إلى المرسل ليخبره أن الرسالة قد وصلت.
وللحزم قيمة عظمى لا تتعداها، فيما يتعلق بحجم البيانات التي ترسلها، خلافًا للخدمات البريدية التقليدية، ويمكن تشبيه ذلك بخدمة لإرسال خطاب يتكون من ورقة واحدة فقط، أما الرسائل الطويلة فنحتاج إلى إرسالها في عدة خطابات يحوي كل منها ورقةً واحدةً فقط، ويجب على الطرف المستقبل أن يجمع تلك الأجزاء بالترتيب، حيث يضاف رقم متسلسل إلى الورقة -مثل ترقيم الصفحات-، وذلك لتُرسَل رسالة خطأ من المستقبل إلى المرسل إذا لم تصل صفحة ما أو لم تظهر في الوقت المحدد لها، وعلى الرغم من انتفاء الحاجة إلى ذلك غالبًا، لأن الحاسوب يتكفل به وكذلك نظام التشغيل وبرمجيات الشبكات، لكن هذه الأمور تستحق أن نعلمها على أي حال، نظرًا لتعذر الاعتماد على موثوقية نقل البيانات أو استمراريته عند استخدام شبكة ما، إذ تقع الأخطاء العرضية بلا شك، ويجب أن نكون مستعدين لفقدان البيانات أو تلفها.
الاتصال بالشبكة
لنترك الكلام المجرد ولندرس التفاصيل العملية لكيفية كتابة تطبيق متصل بالشبكة، حيث نحتاج إلى إنشاء برنامج يعمل مثل خادم server ويكون على حاسوب ما، وبرنامج للعميل client على حاسوب أو عدة حواسيب أخرى متصلة بالشبكة التي يتصل بها الخادم، كما نحتاج إلى آلية تتيح التواصل بين البرنامجين وتعمل في كامل الشبكة، وقد رأينا أن لكل حاسوب عنوانًا، يتكون من عنوان IP الذي يحوي أربعة أرقام تفصل بينها نقاط، لا شك أننا رأيناها من قبل في عناوين الويب، ويضيف التطبيق المتصل بالشبكة عنصرًا آخر إلى ذلك العنوان، يُعرف باسم المنفذ port.
المنافذ والبروتوكولات
يُحدَّد المنفذ بنقطتين رأسيتين :
متبوعتين برقم المنفذ، ويضاف ذلك إلى عنوان IP العادي، فنصل مثلًا إلى المنفذ 80 في العنوان 127.0.0.1
بالشكل 127.0.0.1:80
، وتُحفظ بعض أرقام المنافذ لأغراض خاصة تكون في الغالب لبروتوكولات تطبيقات الإنترنت المختلفة، والبروتوكول -أو الميثاق- هو مجموعة من القواعد وتعريفات الرسائل التي تحدد كيفية عمل الخدمة، فالمنفذ 80 هو المنفذ القياسي المستخدم في خوادم الويب لطلبات http، أما المنفذ 25 فيُستخدم لبريد SMTP، وبناء عليه يتصرف الحاسوب مثل خادم لبعض الخدمات في نفس الوقت، بعرض تلك الخدمات من خلال منافذها المختلفة، ويمكن توضيح هذا بسهولة بإضافة رقم المنفذ 80 إلى عنوان خادم الويب مثل http://www.google.com:80، حيث ينبغي أن تفتح الصفحة بلا مشاكل لأن المتصفح يتصل بالمنفذ 80 افتراضيًا إن لم يتوفر منفذ آخر، لهذا يكثر استخدام المنفذ 8080 مثل منفذ اختبار للإصدارات الجديدة من مواقع الويب قبل إطلاقها.
ورغم قلة عدد تلك المنافذ المحجوزة إلا أننا نستطيع استخدام أرقام المنافذ التي بين 1000 و60000 للتطبيقات المخصصة bespoke applications دون تعارض، لكن يفضل جعل رقم المنفذ قابلًا للإعداد من خلال متغير لبيئة النظام مثلًا، أو ملف إعدادات config file، أو بواسطة معامل سطر أوامر، نظرًا لوجود احتمال -ولو ضئيل- أن يختار برنامج آخر نفس المنفذ على الحاسوب، ولن نشرح ذلك هنا لكن يجب الانتباه إلى أنه وارد في التطبيقات الحقيقية، حين لا يكون لدينا تحكم كامل بحاسوب الخادم، لذا يجب اتخاذ مثل تلك الاحتياطات.
بعد أن عرفنا الآلية، فإن السؤال التالي هو: كيف نصل الشيفرة بأحد تلك المنافذ؟
المقابس Sockets
المقابس هي أبسط آلية اتصال يمكن استخدامها بين الشبكات، وفيها يُقدَّم المقبس للشبكة على أنه منفذ في عنوان IP، وتُنشأ المقابس في بايثون وتُستخدم من خلال استيراد وحدة socket
، ويجب أن نكتب خادمًا لينشئ المقبس، ويربطه مع منفذ، لنتمكن بعدها من استخدامه، ونراقب ذلك المقبس منتظرين الطلبات الواردة إليه، ثم نكتب عميلًا ليتصل بالمقبس على ذلك المنفذ، ثم يتصل العميل بالمنفذ ويقبل الخادم ذلك الاتصال، ثم ينشئ الخادم منفذًا مؤقتًا جديدًا يُستخدم لعملية التواصل -أي عمليات الإرسال والاستقبال send/recv الفعلية- مع الخادم أثناء عملية النقل، مما يحرر المنفذ لمزيد من طلبات الاتصال.
يمكن توضيح ما سبق في الصورة التالية:
تظهر هنا مشكلة اختبار مثل تلك التطبيقات، إذ لا نستطيع أن نعرف إن كان الخادم يعمل أم لا بدون العميل، كذلك فإن العميل لا يستطيع فعل شيء دون الخادم، لذا ينبغي أن يكون لدينا كل من العميل والخادم.
غير أنه يمكن إنشاء أي عدد من العملاء الآخرين بمجرد كتابة الخادم، شرط أن يتصلوا بمقبسه من خلال بروتوكول الرسائل المناسب، ونرى أمثلة ذلك في متصفحات الويب المختلفة التي تستطيع جميعها أن تتصل بأي خادم http، ويمكن بطريقة مماثلة كتابة العديد من الخوادم المختلفة بمجرد نشر البروتوكول، وينبغي هنا ألا توجد مشكلة في عمل أي عميل وأي خادم معًا، وهذا السلوك هو أحد أسباب انتشار تلك التطبيقات، فهو يشكل بيئةً مفتوحةً وقابلةً للتوسع، تكون فيها إحدى نهايات أي زوج من أزواج العميل/الخادم قابلةً للتحسين دون تعطيل النهاية الأخرى.
إنشاء الخادم
سننشئ خادمًا بسيطًا يستجيب للطلبات بأن يعيد رسالة ترحيب وعدد الطلبات التي عالجها من قبل.
import socket # STREAMing و InterNET أنشئ مقبس # TCP/IP المعروف باسم serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # والمنفذ 2007 localhost استخدم serversocket.bind(('localhost', 2007)) # استعد لاستقبال الطلبات serversocket.listen(5) connections = 0 while True: # عالج الاتصالات من العملاء (clientSocket, address) = serversocket.accept() connections += 1 print( "Connection %d using port %d" % (connections, address[1]) ) # clientSocket افعل شيئًا باستخدام while True: req = clientSocket.recv(100) if not req: break # client closed connection message = 'Thankyou!, processed connection number %d' % connections clientSocket.send(message) clientSocket.close()
نلاحظ هنا عدة أمور:
-
يشير هذا المزيج من
AF_INET
وSOCK_STREAM
إلى أننا سنستخدم بروتوكول TCP/IP، وكل بروتوكولات عناوين IP الأخرى متاحة من خلال استخدام تجميعات ثوابت أخرى، لكن TCP/IP هو الأشهر منها وهو ما سنستخدمه. -
مررنا القيمة
5
إلىlisten()
، وهي تمثل العدد الأقصى للاتصالات التي يمكن أن تنتظر في طابور المنفذ، لأننا نُعالج الطلب قبل أن تتجمع طلبات كثيرة في قائمة الانتظار تلك، ونشتق عمليةً مستقلةً لتنفيذ المعالجة الحقيقية إذا أردنا تحسين كفاءتها -انظر الملاحظة الخامسة أدناه-، مما يسمح للخادم أن يسحب الرسائل من قائمة الانتظار في أسرع وقت ممكن، ولا نحتاج زيادة عدد الاتصالات المسموح لها بالانتظار عن 5 اتصالات إلا في حالة الخوادم شديدة الزحام. -
ينشئ العملاء اتصالًا جديدًا لكل عملية تبادل بيانات، ولا تكون لدينا بيانات متاحة إذا انتهت عملية التبادل، وحينئذ ننهي حلقة
while
الداخلية ونعود إلى انتظار اتصال جديد. -
عالجنا طلب العميل في شيفرة الخادم، ولا بأس بهذا لأنها معالجة طفيفة، لكنها قد تستغرق وقتًا كبيرًا إذا كانت في أحد التطبيقات التجارية الكبيرة، عندها نشتق عمليةً أخرى في تلك الحالة لمعالجة تلك العملية خاصةً -ربما باستخدام وحدة
subprocess
التي ذكرناها في مقال نظم التشغيل-، ونترك الخادم يعود لسحب الطلبات من قائمة انتظاره. -
لا توجد طريقةً لإنهاء عملية الخادم، فهي تعمل بلا نهاية إلا إذا حدث خطأ، فإذا أردنا إنهاءها فسنستخدم أداةً من نظام التشغيل نفسه، من خلال برنامج TaskManager في ويندوز مثلًا، أو
kill
في يونكس.
ينبغي أن يكون الخادم جاهزًا للعمل الآن وينتظر طلبات العملاء، لكن ليس لدينا عميل ليرسل تلك الطلبات، لذا سننشئه الآن.
إنشاء العميل
إنشاء العميل client هي عملية سهلة بقدر سهولة إنشاء الخادم، إذ يرسل طلبات متكررةً إلى الخادم بفواصل زمنية قدرها ثانية واحدة، ويطبع استجابة الخادم:
import socket,time # أنشئ المقبس serverAddress = ('localhost',2007) # أرسل بعض الطلبات for n in range(5): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(serverAddress) try: sock.send('dummy request\n') data = sock.recv(100) if not data: break # لا توجد بيانات من الخادم print( data ) time.sleep(1) finally: # نرتب الآن ما سبق sock.close()
لدينا بعض الملاحظات هنا:
-
نستخدم
connect
للوصول إلى المقبس، ثم نستخدم نفس واجهةsend/recv
التي يستخدمها الخادم، لكن يكون التسلسل معكوسًا لأن العميل هو الذي يبدأ عملية تبادل البيانات. - كان من الممكن إرسال أو استقبال بيانات أكثر في عملية واحدة، لكننا اخترنا هذه الطريقة ببساطة لإبراز أرقام الاتصال المختلفة القادمة من الخادم، فالعميل هو الذي يقرر متى ينهي عملية التبادل وليس الخادم، إلا إذا حدث خطأ ما.
-
لاحظ استخدام
try/finally
لضمان إغلاق المقبس حتى في حالة رفع استثناء exception، وهذا مفيد لتقليل المراجعة والتصحيح لاحقًا، لأن بعض أنظمة التشغيل تترك المقابس مفتوحةً لفترة طويلة، مما يعني أنها ستستهلك موارد النظام.
تشغيل البرامج
نريد أن نتأكد أن الخادم يعمل أولًا قبل أن نستطيع تشغيل البرامج، ثم نبدأ برنامج عميل واحد أو أكثر ليتصل به، ولا نستطيع تشغيل هذه البرامج عبر الشبكة لأننا جعلنا الخادم على الحاسوب المحلي فقط localhost
، لذا نحتاج إلى بدء عدد من جلسات الطرفية على حاسوبنا.
توضح الصورة التالية الخادم وهو يعمل على يمين الصورة، واثنين من العملاء على يسارها:
لاحظ أن خرج كلا العميلين يُظهر تسلسل الرسائل التي استلمت من الخادم، وتُظهر رسائل الخادم الاتصالات وأرقام المنافذ المؤقتة المسندة إلى الخادم.
دليل جهات الاتصال الشبكي
بنينا في المقال السابق: التواصل بين العمليات نسخةً تعمل على خادم من دليل جهات الاتصال الذي نطوره، وسميناها address_srv.py، وسنستخدمها في هذا المقال لبناء نسخة مبنية على المقابس، وسيكون الاختلاف الواضح بين نسخة IPC السابقة وبين هذه النسخة هو إمكانية وجود أكثر من عميل واحد يستطيع الوصول إلى دليل جهات الاتصال، وسيكون العملاء على حواسيب مختلفة قطعًا.
وكانت الدوال التي المتاحة في address_srv
هي:
-
readBook(filename)
-
saveBook(book,filename)
-
addEntry(book, name, data)
-
removeEntry(book, name)
-
findEntry(book, name)
برنامج الخادم
لا زلنا بحاجة إلى كتابة برنامج خادم يعالج الطلبات الواردة من العملاء ويستدعي الدالة المناسبة، رغم أننا حولنا البرنامج إلى دوال مخدمات server style functions في المرة السابقة، وتسمى مثل تلك الآلية ببعث الرسائل dispatching messages، وستكون الشيفرة شبيهةً للغاية بالأمثلة البسيطة السابقة.
سيكون البرنامج الرئيسي كما يلي:
import socket, address_srv addresses = address_srv.readBook() # أعد المقبس serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(('localhost', 2007)) serversocket.listen(5) print( 'Server started and listening on port 2007...' ) # عالج الاتصالات من العملاء while True: (clientSocket, address) = serversocket.accept() # عالج أوامر دليل جهات الاتصال while True: s = clientSocket.recv(1024) try: cmd,data = s.split(':') except ValueError: break print( 'received request: ', cmd ) if cmd == "add": details = data.split(',') name = details[0] entry = ','.join(details[1:]) s = address_srv.addEntry(addresses, name, entry) address_srv.saveBook(addresses) elif cmd == "rem": s = address_srv.removeEntry(addresses, data) address_srv.saveBook(addresses) elif cmd == "fnd": s = address_srv.findEntry(addresses, data) else: s = "ERROR: Unrecognised command: " + cmd clientSocket.send(s) clientSocket.close()
نلاحظ هنا أن شيفرة المعالجة الرئيسية للمقبس هي نفسها الشيفرة السابقة، بينما وضعنا معالجة بيانات الطلب في بنية try/except
لالتقاط البيانات غير المكتملة من العميل، أما غير ذلك فإن هذه النسخة تكاد تطابق نسخة IPC في المقال السابق.
برنامج العميل
صار لدينا الآن خادم يعمل في الخلفية، ونريد كتابة برنامج عميل يتحدث إليه، وسيكون مشابهًا لنسخة IPC أيضًا، لكنه سيوجد في سكربت خاصة به، وسنتمكن من تشغيل أكثر من نسخة في نفس الوقت:
import socket serverAddress = ('localhost', 2007) menu = ''' 1) Add Entry 2) Delete Entry 3) Find Entry 4) Quit ''' # اتصل بالخادم sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(serverAddress) while True: print( menu ) try: choice = int(input('Choose an option[1-4] ')) except: continue if choice == 1: name = input('Enter the name: ') num = input('Enter the House number: ') street= input('Enter the Street name: ') town = input('Enter the Town: ') phone = input('Enter the Phone number: ') data = "%s,%s %s, %s, %s" % (name,num,street,town,phone) cmd = "add:%s" % data elif choice == 2: name = input('Enter the name: ') cmd = 'rem:%s' % name elif choice == 3: name = input('Enter the name: ') cmd = 'fnd:%s' % name elif choice == 4: break else: print( "Invalid choice, must be between 1 and 4." ) continue # تحدث إلى الخادم try: sock.send(cmd) data = sock.recv(250) if not data: break # no data from server print( data ) finally: sock.close()
نلاحظ هنا أن عملية معالجة القائمة هي نفسها في المثال السابق، وأن أوامر الاتصالات مجموعة في بضعة أسطر في الأسفل، وهذا أيضًا مشابه لمثال العميل الذي تقدم شرحه.
الانتقال إلى الشبكة
كانت مقابسنا إلى الآن على حاسوب محلي، ونريد أن ننقلها إلى شبكة حقيقية ويكون لدينا عمليات عميل/خادم حقيقية أيضًا، ويسهل تنفيذ ذلك بتغيير العناوين المستخدمة في استدعاء bind()
في الخادم واستدعاء connect()
في العميل، واستبدال عنوان IP -سواءً النسخة الرقمية أو الاسمية منه- للحاسوب الذي يعمل عليه الخادم بالمرجع المشير إلى 'localhost'
، ويمكن تشغيل عدة نسخ من العميل على كل حاسوب في نفس الوقت إذا كان لدينا عدة أجهزة على نفس الشبكة، وسيعالج الخادم الطلبات من تلك النسخ.
أما في الحياة العملية فقد نبذل مزيدًا من الجهد في التعامل مع تحليل أسماء DNS مثلًا، ونضيف اختبارات تتحقق من الأخطاء وآليات المهل الزمنية timeouts لكي لا يُعلِّق الخادم ويتأثر، لأن الشبكات الحقيقية أقل موثوقيةً وأكثر عرضةً للأخطاء، غير أننا لن نتحدث عن تلك الآليات والمهام، وإنما نذكرها فقط لوجوب حسابها عند حل مثل تلك المشكلات.
المزيد من المعلومات
كُتبت الكثير من الشروحات في برمجة المقابس، لعل أبرزها Socket How-To الذي كتبه جوردُن ماكمِلان Gordon McMillan، وهو يشرح كثيرًا من عيوب برمجة المقابس ويقدم الحلول المقترحة للتعامل معها، إضافةً إلى توثيق وحدة socket في بايثون، والذي لا غنى عن قراءته.
كما تحتوي العديد من الكتب على أقسام عن البرمجة باستخدام المقابس، ونخص بالذكر منها كتاب Python Network Programming الذي كتبه جون جورزن John Goerzen، ويدور حول برمجة الشبكات مغطيًا كثيرًا من جوانب برمجة المقابس.
والجميل في الأمر أنه يمكن تنفيذ أغلب مهام برمجة الشبكات بمستوىً أعلى إذا كنا نستخدم أحد بروتوكولات الإنترنت القياسية، مثل http و smtp و telnet وغيرها، ففي بايثون مثلًا وحدات تنفذ تلك البروتوكولات في طبقة المقابس لئلا نضطر نحن إلى ذلك، وسندرس في المقالات القادمة كيف نبسط برمجة الويب باستخدام http، من خلال وحدات المستوى الأعلى تلك، أما عند اختيار العمل في برمجة الشبكات ليكون تخصصًا مهنيًا فتُستخدم لغة rebol لأنها تولي أهميةً لخصوصية المهام، وفيها دعم للعديد من مهام الشبكة.
وإن أردت التعمق أكثر في الشبكات، فقد ترجمت أكاديمية حسوب كتابًا مهمًا ونشرت مقالاته تحت وسم "أساسيات الشبكات " يمكنك الرجوع إليه.
خاتمة
نرجو في نهاية هذا المقال أن تكون تعلمت ما يلي:
- تمثَّل اتصالات شبكات الحواسيب بعناوين IP ومنافذ ports.
- تتصل المقابس بالمنافذ.
-
تراقب مقابس الخوادم الاتصالات وتستمع لها عبر
listen
، وتقبلها إذا التقطتها بواسطةaccept
، وهذا يحدد منفذًا جديدًا ليستخدمه العميل الجديد. -
يجب أن تستقبل الخوادم رسائل العملاء على المنفذ الجديد من خلال
recv
، وترسلsend
الردود إلى العملاء. -
يتصل عملاء المقابس بمقبس الخادم من خلال
connect
، ويرسلون البيانات إليه من خلالsend
، ثم يستقبلون البيانات مرةً أخرى عبرrecv
في صورة ردود. -
يغلق العميل المقبس باستخدام
close
عند انتهاء عملية تبادل البيانات.
ترجمة -بتصرف- للفصل السابع والعشرين: Network Communications من كتاب Learn To Program لصاحبه Alan Gauld.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.