Skip to content

8. برمجة TCP-IP

8.1. معلومات عامة

8.1.1. بروتوكولات الإنترنت

نقدم هنا مقدمة لبروتوكولات الاتصال عبر الإنترنت، والمعروفة أيضًا باسم مجموعة TCP/IP (بروتوكول التحكم في الإرسال / بروتوكول الإنترنت)، والتي سُميت على اسم البروتوكولين الرئيسيين. يُنصح القارئ بأن يكون لديه فهم عام لكيفية عمل الشبكات، ولا سيما بروتوكولات TCP/IP، قبل الشروع في تطوير التطبيقات الموزعة.

النص التالي هو ترجمة جزئية لمقطع موجود في "LAN Workplace for DOS – Administrators Guide" (دليل المسؤول عن LAN Workplace لنظام DOS) الصادر عن شركة NOVELL، وهو وثيقة تعود إلى أوائل التسعينيات.


ينبع المفهوم العام لإنشاء شبكة من أجهزة الكمبيوتر غير المتجانسة من الأبحاث التي أجرتها وكالة DARPA (وكالة مشاريع الأبحاث المتقدمة للدفاع) في الولايات المتحدة. طورت DARPA مجموعة البروتوكولات المعروفة باسم TCP/IP، والتي تسمح للأجهزة غير المتجانسة بالتواصل مع بعضها البعض. تم اختبار هذه البروتوكولات على شبكة تسمى ARPAnet، والتي أصبحت فيما بعد الإنترنت. تحدد بروتوكولات TCP/IP تنسيقات وقواعد الإرسال والاستقبال التي تكون مستقلة عن بنية الشبكة والأجهزة المستخدمة.

الشبكة التي صممتها DARPA وتديرها بروتوكولات TCP/IP هي شبكة مبدلة بالحزم. تنقل هذه الشبكة المعلومات عبر الشبكة في أجزاء صغيرة تسمى الحزم. وبالتالي، إذا أرسل جهاز كمبيوتر ملفًا كبيرًا، فسيتم تقسيمه إلى أجزاء صغيرة يتم إرسالها عبر الشبكة ليتم إعادة تجميعها في الوجهة. يحدد TCP/IP تنسيق هذه الحزم، وهي:

  • مصدر الحزمة
  • الوجهة
  • الطول
  • النوع

8.1.2. نموذج OSI

تتبع بروتوكولات TCP/IP بشكل عام نموذج الشبكة المفتوحة المعروف باسم OSI (نموذج مرجعي لترابط الأنظمة المفتوحة) الذي حددته منظمة ISO (منظمة المعايير الدولية). يصف هذا النموذج شبكة مثالية حيث يمكن تمثيل الاتصال بين الأجهزة بنموذج مكون من سبع طبقات:

Image

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

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

في كل طبقة، تقوم عملية الإرسال المسؤولة عن إرسال المعلومات بإرسالها إلى عملية استقبال على الجهاز الآخر الذي ينتمي إلى نفس الطبقة التي تنتمي إليها. وتقوم بذلك وفقًا لقواعد معينة تُعرف باسم بروتوكول الطبقة. وبالتالي، نحصل على مخطط الاتصال النهائي التالي:

Image

أدوار الطبقات المختلفة هي كما يلي:

الطبقة المادية
تضمن نقل البتات عبر وسيط مادي. تشمل هذه الطبقة معدات طرفية لمعالجة البيانات (DPTE) مثل المحطات الطرفية أو أجهزة الكمبيوتر، بالإضافة إلى معدات إنهاء دوائر البيانات (DCTE) مثل أجهزة التضمين/التفكيك، وأجهزة التعدد، والمركّزات. النقاط الرئيسية في هذا المستوى هي:
. اختيار ترميز المعلومات (تناظري أو رقمي)
. اختيار وضع الإرسال (متزامن أو غير متزامن).
وصلة البيانات
يخفي الخصائص المادية للطبقة المادية. يكتشف أخطاء الإرسال ويصححها.
الشبكة
يدير المسار الذي يجب أن تسلكه المعلومات المرسلة عبر الشبكة. ويُطلق على ذلك اسم التوجيه: أي تحديد المسار الذي يجب أن تسلكه المعلومات للوصول إلى وجهتها.
النقل
يتيح الاتصال بين تطبيقين، في حين أن الطبقات السابقة كانت تسمح فقط بالاتصال بين الأجهزة. يمكن أن تكون إحدى الخدمات التي توفرها هذه الطبقة هي تعدد الإرسال: يمكن لطبقة النقل استخدام اتصال شبكة واحد (من جهاز إلى جهاز) لنقل البيانات الخاصة بتطبيقات متعددة.
الجلسة
توفر هذه الطبقة خدمات تسمح للتطبيق بفتح جلسة عمل والحفاظ عليها على جهاز بعيد.
العرض
تهدف إلى توحيد تمثيل البيانات عبر الأجهزة المختلفة. وبالتالي، سيتم "تنسيق" البيانات الصادرة من الجهاز A بواسطة طبقة العرض الخاصة بالجهاز A وفقًا لتنسيق قياسي قبل إرسالها عبر الشبكة. عند وصولها إلى طبقة العرض الخاصة بالجهاز B الوجهة، والذي سيتعرف عليها بفضل تنسيقها القياسي، سيتم تنسيقها بشكل مختلف حتى يتمكن التطبيق الموجود على الجهاز B من التعرف عليها.
التطبيق
في هذا المستوى، نجد التطبيقات التي تكون عمومًا قريبة من المستخدم، مثل البريد الإلكتروني أو نقل الملفات.

8.1.3. نموذج TCP/IP

نموذج OSI هو نموذج مثالي لم يتم تحقيقه بالكامل قط. تقترب مجموعة بروتوكولات TCP/IP منه بالطريقة التالية:

Image

الطبقة المادية

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

إيثرنت

هذا هو الاسم الذي أُطلق على تقنية الشبكة المحلية ذات التبديل الحزمي التي اخترعت في مركز أبحاث زيروكس (Xerox PARC) في أوائل السبعينيات، وقامت شركات زيروكس وإنتل وديجيتال إيكويبمنت بتوحيد معاييرها في عام 1978. تتكون الشبكة ماديًا من كابل متحد المحور يبلغ قطره حوالي 1.27 سم وطوله يصل إلى 500 متر. ويمكن تمديدها باستخدام مكررات، على ألا يفصل بين أي جهازين أكثر من مكررين. الكابل سلبي: حيث توجد جميع المكونات النشطة على الأجهزة المتصلة بالكابل. ويتم توصيل كل جهاز بالكابل عبر بطاقة وصول إلى الشبكة تتكون من:

  • جهاز إرسال واستقبال يكتشف وجود الإشارات على الكابل ويحول الإشارات التناظرية إلى إشارات رقمية والعكس صحيح.
  • مقرن يستقبل الإشارات الرقمية من جهاز الإرسال والاستقبال وينقلها إلى الكمبيوتر للمعالجة، أو العكس.

الميزات الرئيسية لتقنية إيثرنت هي كما يلي:

  • سعة 10 ميجابت في الثانية.
  • توبولوجيا الناقل: جميع الأجهزة متصلة بنفس الكابل

Image

  • شبكة البث — ترسل الجهاز المرسل المعلومات عبر الكابل مع عنوان الجهاز المستقبل. ثم تتلقى جميع الأجهزة المتصلة هذه المعلومات، ولا يحتفظ بها سوى المستلم المقصود.
  • طريقة الوصول هي كما يلي: يستمع جهاز الإرسال الراغب في الإرسال إلى الكابل — ثم يكتشف ما إذا كانت موجة حاملة موجودة أم لا، حيث يشير وجودها إلى أن عملية الإرسال جارية. هذه هي تقنية CSMA (الوصول المتعدد باستشعار الموجة الحاملة). في حالة عدم وجود موجة حاملة، قد يقرر جهاز الإرسال الإرسال بدوره. قد يتخذ عدة أجهزة إرسال هذا القرار. تختلط الإشارات المرسلة معًا: وهذا ما يُسمى بالتصادم. يكتشف جهاز الإرسال هذه الحالة: أثناء الإرسال عبر الكابل، يستمع أيضًا إلى ما يمر عبره بالفعل. إذا اكتشف أن المعلومات التي تنتقل عبر الكابل ليست هي التي أرسلها، يستنتج أن تصادمًا قد حدث وسيتوقف عن الإرسال. وستفعل أجهزة الإرسال الأخرى التي كانت ترسل الشيء نفسه. سيستأنف كل جهاز الإرسال بعد تأخير عشوائي يعتمد على جهاز الإرسال الفردي. تسمى هذه التقنية CD (Collision Detect). وبالتالي، تسمى طريقة الوصول CSMA/CD.
  • عنونة 48 بت. لكل جهاز عنوان، يُشار إليه هنا بالعنوان المادي، وهو مكتوب على البطاقة التي تربطه بالكابل. يُسمى هذا العنوان عنوان إيثرنت الجهاز.

طبقة الشبكة

في هذه الطبقة، نجد بروتوكولات IP و ICMP و ARP و RARP.

IP (بروتوكول الإنترنت)
ينقل الحزم بين عقدتين في الشبكة
ICMP
(بروتوكول رسائل التحكم في الإنترنت)
يسهل ICMP الاتصال بين برنامج بروتوكول IP على جهاز ما وبرنامج بروتوكول IP على جهاز آخر. وبالتالي، فهو بروتوكول لتبادل الرسائل داخل بروتوكول IP نفسه.
ARP
(بروتوكول تحليل العناوين)
يربط عنوان الإنترنت الخاص بجهاز ما بعنوانه الفعلي
RARP
(بروتوكول تحويل العناوين العكسي)
يربط العنوان المادي للجهاز بعنوان الإنترنت الخاص به

طبقات النقل/الجلسة

تتضمن هذه الطبقة البروتوكولات التالية:

TCP (بروتوكول التحكم في الإرسال)
يضمن التسليم الموثوق للمعلومات بين عميلين
UDP (بروتوكول مخطط بيانات المستخدم)
يضمن توصيل المعلومات بشكل غير موثوق بين عميلين

طبقات التطبيق/العرض/الجلسة

توجد بروتوكولات متنوعة هنا:

TELNET
محاكي محطة طرفية يسمح للجهاز A بالاتصال بالجهاز B كمحطة طرفية
FTP (بروتوكول نقل الملفات)
يتيح نقل الملفات
TFTP (بروتوكول نقل الملفات البسيط)
يتيح نقل الملفات
SMTP (بروتوكول نقل البريد البسيط)
يتيح تبادل الرسائل بين مستخدمي الشبكة
DNS (نظام أسماء النطاقات)
يحول اسم الجهاز إلى عنوان الإنترنت الخاص به
XDR (تمثيل البيانات الخارجية)
تم إنشاؤه بواسطة Sun Microsystems، ويحدد معيارًا لتمثيل البيانات المستقل عن الجهاز
RPC (استدعاء الإجراءات عن بُعد)
تم تعريفه أيضًا بواسطة Sun، وهو بروتوكول اتصال بين التطبيقات البعيدة، مستقل عن طبقة النقل. هذا البروتوكول مهم: فهو يعفي المبرمج من الحاجة إلى معرفة تفاصيل طبقة النقل ويجعل التطبيقات قابلة للنقل. يعتمد هذا البروتوكول على بروتوكول XDR
NFS (نظام ملفات الشبكة)
تم تعريفه أيضًا بواسطة Sun، ويسمح هذا البروتوكول لجهاز واحد بـ"رؤية" نظام الملفات الخاص بجهاز آخر. وهو يعتمد على بروتوكول RPC المذكور أعلاه

8.1.4. كيف تعمل بروتوكولات الإنترنت

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

Image

لنأخذ مثالاً: تطبيق FTP، المحدد في طبقة التطبيقات، والذي يتيح نقل الملفات بين الأجهزة.

  • يقوم التطبيق بتسليم سلسلة من البايتات ليتم نقلها إلى طبقة النقل.
  • تقسم طبقة النقل هذه السلسلة من البايتات إلى مقاطع TCP وتضيف رقم المقطع إلى بداية كل مقطع. يتم تمرير المقاطع إلى طبقة الشبكة، التي يحكمها بروتوكول IP.
  • تقوم طبقة IP بإنشاء حزمة تغلف مقطع TCP المستلم. في رأس هذه الحزمة، تضع عناوين الإنترنت لأجهزة المصدر والوجهة. كما تحدد العنوان الفعلي لجهاز الوجهة. يتم تمرير الحزمة بأكملها إلى طبقة ربط البيانات والطبقة المادية، أي إلى بطاقة الشبكة التي تربط الجهاز بالشبكة المادية.
  • وهناك، يتم تغليف حزمة IP بدورها في إطار مادي وإرسالها إلى مستلمها عبر الكابل.
  • على الجهاز الوجهة، تقوم طبقة الارتباط البياناتي والطبقة المادية بالعكس: فهي تفك تغليف حزمة IP من الإطار المادي وتمررها إلى طبقة IP.
  • تتحقق طبقة IP من صحة الحزمة: فهي تحسب مجموعًا اختباريًا استنادًا إلى البتات المستلمة، والتي يجب أن تجدها في رأس الحزمة. إذا لم يكن الأمر كذلك، يتم رفض الحزمة.
  • إذا اعتُبرت الحزمة صالحة، تقوم طبقة IP بفك تغليف مقطع TCP الموجود بداخلها وتمريره إلى طبقة النقل.
  • تفحص طبقة النقل — وهي طبقة TCP في مثالنا — رقم المقطع للتأكد من أن المقاطع مرتبة بالترتيب الصحيح.
  • كما أنها تحسب مجموعًا اختباريًا لمقطع TCP. إذا تبين أنه صحيح، ترسل طبقة TCP إقرارًا بالاستلام إلى الجهاز المصدر؛ وإلا، يتم رفض مقطع TCP.
  • كل ما يتبقى لطبقة TCP هو نقل جزء البيانات من المقطع إلى التطبيق المقصود لاستلامه في الطبقة الأعلى.

8.1.5. معالجة المشكلات على الإنترنت

يمكن أن تكون عقدة الشبكة جهاز كمبيوتر أو طابعة ذكية أو خادم ملفات — أي شيء، في الواقع، يمكنه الاتصال باستخدام بروتوكولات TCP/IP. لكل عقدة عنوان مادي بتنسيق يعتمد على نوع الشبكة. في شبكة إيثرنت، يتم ترميز العنوان المادي على 6 بايت. عنوان شبكة X.25 هو رقم مكون من 14 رقمًا.

عنوان الإنترنت للعقدة هو عنوان منطقي: فهو مستقل عن الأجهزة والشبكة المستخدمة. وهو عنوان مكون من 4 بايت يحدد كلاً من الشبكة المحلية والعقدة الموجودة على تلك الشبكة. عادةً ما يتم تمثيل عنوان الإنترنت بأربعة أرقام — قيم البايتات الأربعة — مفصولة بنقطة. وبالتالي، فإن عنوان الجهاز Lagaffe في كلية العلوم في أنجيه هو 193.49.144.1، وعنوان الجهاز Liny هو 193.49.144.9. ومن هذا، يمكننا استنتاج أن عنوان الإنترنت للشبكة المحلية هو 193.49.144.0. يمكن أن يصل عدد العقد في هذه الشبكة إلى 254 عقدة.

نظرًا لأن عناوين الإنترنت، أو عناوين IP، مستقلة عن الشبكة، يمكن لجهاز على الشبكة A التواصل مع جهاز على الشبكة B دون الاهتمام بنوع الشبكة التي يعمل عليها: فهو يحتاج فقط إلى معرفة عنوان IP الخاص به. يتولى بروتوكول IP لكل شبكة التحويل بين عنوان IP والعنوان الفعلي، في كلا الاتجاهين.

يجب أن تكون جميع عناوين IP فريدة. وتقع مسؤولية تخصيصها على عاتق المنظمات الرسمية. في الواقع، تخصص هذه المنظمات عنوانًا للشبكات المحلية، على سبيل المثال 193.49.144.0 لشبكة كلية العلوم في أنجيه. يمكن لمسؤول هذه الشبكة بعد ذلك تخصيص عناوين IP من 193.49.144.1 إلى 193.49.144.254 حسبما يراه مناسبًا. يتم تخزين هذا العنوان عمومًا في ملف محدد على كل جهاز متصل بالشبكة.

8.1.5.1. فئات عناوين IP

عنوان IP هو تسلسل مكون من 4 بايتات، غالبًا ما يُكتب على النحو I1.I2.I3.I4، والذي يحتوي في الواقع على عنوانين:

  • عنوان الشبكة
  • عنوان عقدة على تلك الشبكة

اعتمادًا على حجم هذين الحقلين، تنقسم عناوين IP إلى 3 فئات: الفئات A و B و C.

الفئة A

عنوان IP: I1.I2.I3.I4 له الشكل R1.N1.N2.N3 حيث

R1 هو عنوان الشبكة

N1.N2.N3 هو عنوان جهاز على تلك الشبكة

وبشكل أكثر دقة، فإن تنسيق عنوان IP من الفئة A هو كما يلي:

Image

يبلغ طول عنوان الشبكة 7 بتات، بينما يبلغ طول عنوان المضيف 24 بتة. وبالتالي، يمكن أن يكون هناك 127 شبكة من الفئة A، تحتوي كل منها على ما يصل إلى 2²⁴ مضيفًا.

الفئة ب

هنا، يكون عنوان IP: I1.I2.I3.I4 على الشكل R1.R2.N1.N2 حيث

R1.R2 هو عنوان الشبكة

N1.N2 هو عنوان جهاز على تلك الشبكة

وبشكل أكثر دقة، فإن تنسيق عنوان IP من الفئة B هو كما يلي:

Image

يبلغ حجم عنوان الشبكة 2 بايت (14 بت، على وجه الدقة)، وكذلك عنوان العقدة. وهذا يعني أنه يمكن أن يكون هناك 2¹⁴ شبكة من الفئة B، تحتوي كل منها على ما يصل إلى 2¹⁶ عقدة.

الفئة C

في هذه الفئة، يكون عنوان IP: I1.I2.I3.I4 على الشكل R1.R2.R3.N1 حيث

R1.R2.R3 هو عنوان الشبكة

N1 هو عنوان جهاز على تلك الشبكة

وبشكل أكثر دقة، فإن تنسيق عنوان IP من الفئة C هو كما يلي:

Image

يبلغ طول عنوان الشبكة 3 بايت (ناقص 3 بتات)، بينما يبلغ طول عنوان المضيف بايت واحد. وبالتالي، يمكن أن يصل عدد شبكات الفئة C إلى 2²¹ شبكة، تحتوي كل منها على ما يصل إلى 256 مضيفًا.

وبما أن عنوان جهاز Lagaffe في كلية العلوم بمدينة أنجيه هو 193.49.144.1، نلاحظ أن البايت الأعلى قيمة هو 193، وهو 11000001 بالثنائي. يمكننا استنتاج أن الشبكة هي شبكة من الفئة C.

العناوين المحجوزة

. بعض عناوين IP هي عناوين شبكات وليست عناوين عقد داخل الشبكة. وهي تلك التي يكون فيها عنوان العقدة محددًا بـ 0. وبالتالي، فإن العنوان 193.49.144.0 هو عنوان IP لشبكة كلية العلوم في أنجيه. وبالتالي، لا يمكن لأي عقدة على الشبكة أن تحمل العنوان صفر.

. عندما يتكون عنوان العقدة في عنوان IP بالكامل من أرقام 1، فإنه يكون عنوان بث: يشير هذا العنوان إلى جميع العقد الموجودة على الشبكة.

. في شبكة من الفئة C، التي تسمح نظريًا بـ 2⁸ = 256 عقدة، إذا أزلنا العنوانين المحظورين، يتبقى لدينا 254 عنوانًا مسموحًا به فقط.

8.1.5.2. بروتوكولات تحويل عنوان الإنترنت <--> العنوان الفعلي

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

Image

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

Image

في الإطار النهائي، توجد العناوين المادية للجهازين المصدر والوجهة. كيف يتم الحصول عليها؟

يستطيع الجهاز المرسل، الذي يعرف عنوان IP للجهاز الذي يريد التواصل معه، الحصول على العنوان المادي لهذا الأخير باستخدام بروتوكول محدد يسمى ARP (بروتوكول تحليل العناوين).

  • ترسل الجهاز نوعًا خاصًا من الحزم يُسمى حزمة ARP تحتوي على عنوان IP للجهاز الذي يتم البحث عن عنوانه الفعلي. كما تتضمن الحزمة عنوان IP الخاص بها وعنوانها الفعلي.
  • يتم إرسال هذه الحزمة إلى جميع العقد الموجودة على الشبكة.
  • وتتعرف هذه العقد على الطبيعة الخاصة للحزمة. وتستجيب العقدة التي تتعرف على عنوان IP الخاص بها في الحزمة بإرسال عنوانها الفعلي إلى مرسل الحزمة. كيف يمكنها القيام بذلك؟ لقد عثرت على عنوان IP وعنوان مرسل الحزمة الفعليين في الحزمة.
  • وبالتالي، يتلقى المرسل العنوان الفعلي الذي كان يبحث عنه. ويخزنه في الذاكرة حتى يتمكن من استخدامه لاحقًا إذا لزم إرسال حزم أخرى إلى نفس المستلم.

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

عندما يرغب المسؤول في إعادة تنظيم الشبكة، قد يحتاج إلى تغيير عناوين IP لجميع العقد وبالتالي تعديل ملفات التكوين المختلفة لكل عقدة. قد يكون هذا الأمر مملًا وعرضة للأخطاء إذا كان هناك العديد من الأجهزة. تتضمن إحدى الطرق عدم تخصيص عنوان IP للأجهزة: بدلاً من ذلك، يتم إدخال رمز خاص في الملف حيث تجد الجهاز عادةً عنوان IP الخاص به. عندما تكتشف الجهاز أنه لا يوجد لديه عنوان IP، يطلب الجهاز عنوانًا باستخدام بروتوكول يسمى RARP (بروتوكول تحليل العناوين العكسي). ثم يرسل حزمة خاصة تسمى حزمة RARP عبر الشبكة — مشابهة لحزمة ARP السابقة — تحتوي على عنوانه الفعلي. يتم إرسال هذه الحزمة إلى جميع العقد التي تتعرف على حزمة RARP. تحتفظ إحدى هذه العقد، والتي تسمى خادم RARP، بملف يحتوي على تخطيطات العناوين المادية <--> عناوين IP لجميع العقد. ثم تستجيب لمرسل حزمة RARP بإرسال عنوان IP الخاص بها. لذلك، لا يحتاج المسؤول الذي يرغب في إعادة تكوين شبكته سوى تعديل ملف التخطيط على خادم RARP ( ). يجب أن يكون لهذا الخادم عادةً عنوان IP ثابت يجب أن يكون قادرًا على معرفته دون الحاجة إلى استخدام بروتوكول RARP نفسه.

8.1.6. طبقة الشبكة، المعروفة باسم طبقة IP للإنترنت

يحدد بروتوكول الإنترنت (IP) التنسيق الذي يجب أن تتخذه الحزم وكيفية التعامل معها أثناء الإرسال أو الاستقبال. يُطلق على هذا النوع المحدد من الحزم اسم مخطط بيانات IP. وقد ناقشنا ذلك سابقًا:

Image

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

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

Image

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

8.1.6.1. التوجيه

التوجيه هو طريقة توجيه حزم بروتوكول الإنترنت (IP) إلى وجهتها. وهناك طريقتان: التوجيه المباشر والتوجيه غير المباشر.

التوجيه المباشر

يشير التوجيه المباشر إلى إرسال حزمة IP مباشرة من المرسل إلى المستلم داخل نفس الشبكة:

  • يحتوي الجهاز الذي يرسل حزمة بيانات IP على عنوان IP الخاص بالمستلم.
  • ويحصل على العنوان الفعلي للمستلم عبر بروتوكول ARP أو من جداوله، إذا كان قد تم الحصول على هذا العنوان بالفعل.
  • ويقوم بإرسال الحزمة عبر الشبكة إلى ذلك العنوان الفعلي.

التوجيه غير المباشر

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

يتم توصيل جهاز التوجيه بشبكتين وله عنوان IP داخل كلتا الشبكتين.

Image

في المثال أعلاه:

  • الشبكة رقم 1 لها عنوان الإنترنت 193.49.144.0 والشبكة رقم 2 لها العنوان 193.49.145.0.
  • داخل الشبكة رقم 1، يمتلك جهاز التوجيه العنوان 193.49.144.6، ويمتلك العنوان 193.49.145.3 داخل الشبكة رقم 2.

تتمثل مهمة جهاز التوجيه في أخذ حزمة IP التي يتلقاها — والموجودة داخل إطار مادي نموذجي للشبكة رقم 1 — ووضعها في إطار مادي يمكنه الانتقال عبر الشبكة رقم 2. إذا كان عنوان IP لوجهة الحزمة موجودًا داخل الشبكة رقم 2، فسيرسل جهاز التوجيه الحزمة إليها مباشرةً؛ وإلا، فسيرسلها إلى جهاز توجيه آخر، يربط الشبكة رقم 2 بالشبكة رقم 3، وهكذا دواليك.

8.1.6.2. رسائل الخطأ والتحكم

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

ستستخدم العقدة معلومات ICMP لتحديث جداول التوجيه الخاصة بها.

8.1.7. طبقة النقل: بروتوكولا UDP و TCP

8.1.7.1. بروتوكول UDP: بروتوكول مخطط بيانات المستخدم

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

حتى الآن، على مستوى الشبكة، ناقشنا عناوين IP للأجهزة. ومع ذلك، على جهاز واحد، يمكن أن تتعايش عمليات مختلفة في وقت واحد، ويمكن لجميعها التواصل مع بعضها البعض. لذلك، عند إرسال رسالة، من الضروري تحديد ليس فقط عنوان IP للجهاز الوجهة، ولكن أيضًا "اسم" العملية الوجهة. هذا الاسم هو في الواقع رقم، يُسمى رقم المنفذ. يتم حجز أرقام معينة للتطبيقات القياسية: المنفذ 69 لتطبيق TFTP (بروتوكول نقل الملفات البسيط)، على سبيل المثال.

تُسمى الحزم التي يعالجها بروتوكول UDP أيضًا بالدياتاجرامات. وهي تأخذ الشكل التالي:

Image

يتم تغليف هذه المخططات البياناتية في حزم IP، ثم في إطارات مادية.

8.1.7.2. بروتوكول TCP: بروتوكول التحكم في الإرسال

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

يتجنب بروتوكول TCP (بروتوكول التحكم في الإرسال) هذه المشاكل. وفيما يلي خصائصه:

  • تقوم العملية التي ترغب في الإرسال أولاً بإنشاء اتصال مع العملية التي تستقبل المعلومات التي توشك على إرسالها. يتم إجراء هذا الاتصال بين منفذ على الجهاز المرسل ومنفذ على الجهاز المستقبل. وبذلك يتم إنشاء مسار افتراضي بين المنفذين، والذي سيتم حجزه حصريًا للعمليتين اللتين أنشأتا الاتصال.
  • تتبع جميع الحزم المرسلة من قبل العملية المصدر هذا المسار الافتراضي وتصل بالترتيب الذي تم إرسالها به، وهو ما لم يكن مضمونًا في بروتوكول UDP نظرًا لأن الحزم يمكن أن تتبع مسارات مختلفة.
  • تبدو البيانات المرسلة متصلة. ترسل العملية المرسلة البيانات وفقًا لسرعتها الخاصة. لا يتم إرسال هذه البيانات بالضرورة على الفور: ينتظر بروتوكول TCP حتى يتوفر لديه ما يكفي لإرساله. يتم تخزينها في بنية تسمى مقطع TCP. بمجرد امتلاء هذا المقطع، يتم إرساله إلى طبقة IP، حيث يتم تغليفه في حزمة IP.
  • يتم ترقيم كل مقطع يرسله بروتوكول TCP. يتحقق بروتوكول TCP المستلم من أنه يتلقى المقاطع بالتسلسل. لكل مقطع يتم استلامه بشكل صحيح، يرسل إقرارًا بالاستلام إلى المرسل.
  • وعندما يستلمها المستقبل، يقوم بإخطار المرسل. وبذلك يمكن للمرسل التأكد من أن المقطع قد تم تسليمه بنجاح، وهو ما لم يكن ممكنًا مع بروتوكول UDP.
  • إذا لم يتلق بروتوكول TCP الذي أرسل مقطعًا ما إقرارًا بالاستلام بعد مرور فترة زمنية معينة، فإنه يعيد إرسال المقطع المعني، مما يضمن جودة خدمة توصيل المعلومات.
  • الدائرة الافتراضية التي يتم إنشاؤها بين عمليتي الاتصال هي دائرة ثنائية الاتجاه: وهذا يعني أن المعلومات يمكن أن تتدفق في كلا الاتجاهين. وبالتالي، يمكن لعملية الوجهة إرسال إقرارات حتى أثناء استمرار عملية المصدر في إرسال المعلومات. وهذا يسمح، على سبيل المثال، لبروتوكول TCP المصدر بإرسال شرائح متعددة دون انتظار إقرار. وإذا أدرك، بعد فترة زمنية معينة، أنه لم يتلق إقرارًا باستلام مقطع معين رقم n، فسيستأنف إرسال المقاطع من تلك النقطة.

8.1.8. طبقة التطبيق

فوق بروتوكولي UDP و TCP، توجد بروتوكولات قياسية متنوعة:

TELNET

يسمح هذا البروتوكول لمستخدم على الجهاز <bdi dir="ltr" class="odt-ltr-term">A</bdi> على الشبكة بالاتصال بالجهاز <bdi dir="ltr" class="odt-ltr-term">B</bdi> (الذي يُسمى غالبًا الجهاز المضيف). يحاكي <bdi dir="ltr" class="odt-ltr-term">TELNET</bdi> ما يُسمى بالمحطة الطرفية العالمية على الجهاز <bdi dir="ltr" class="odt-ltr-term">A.</bdi> وبالتالي، يتصرف المستخدم كما لو كان لديه محطة طرفية متصلة بالجهاز <bdi dir="ltr" class="odt-ltr-term">B.</bdi> يعتمد <bdi dir="ltr" class="odt-ltr-term">Telnet</bdi> على بروتوكول <bdi dir="ltr" class="odt-ltr-term">TCP.</bdi>

FTP: (بروتوكول نقل الملفات)

يتيح هذا البروتوكول تبادل الملفات بين جهازين بعيدين، بالإضافة إلى إجراء عمليات على الملفات مثل إنشاء الدلائل، على سبيل المثال. ويعتمد على بروتوكول <bdi dir="ltr" class="odt-ltr-term">TCP.</bdi>

TFTP: (بروتوكول نقل الملفات البسيط)

هذا البروتوكول هو نوع من أنواع بروتوكول <bdi dir="ltr" class="odt-ltr-term">FTP.</bdi> يعتمد على بروتوكول <bdi dir="ltr" class="odt-ltr-term">UDP</bdi> وهو أقل تعقيدًا من بروتوكول <bdi dir="ltr" class="odt-ltr-term">FTP.</bdi>

DNS: (نظام أسماء النطاقات)

عندما يرغب المستخدم في تبادل الملفات مع جهاز بعيد، عبر <bdi dir="ltr" class="odt-ltr-term">FTP</bdi> على سبيل المثال، يجب أن يعرف عنوان الإنترنت الخاص بذلك الجهاز. على سبيل المثال، لاستخدام <bdi dir="ltr" class="odt-ltr-term">FTP</bdi> على جهاز <bdi dir="ltr" class="odt-ltr-term">Lagaffe</bdi> في جامعة أنجيه، ستحتاج إلى تشغيل <bdi dir="ltr" class="odt-ltr-term">FTP</bdi> على النحو التالي: <bdi dir="ltr" class="odt-ltr-term">FTP 193.49.144.1</bdi>

وهذا يتطلب وجود دليل يربط الأجهزة بعناوين <bdi dir="ltr" class="odt-ltr-term">IP.</bdi> وفي هذا الدليل، من المرجح أن يتم تعيين الأجهزة بأسماء رمزية مثل:

جهاز <bdi dir="ltr" class="odt-ltr-term">DPX2/320</bdi> في جامعة أنجيه

جهاز <bdi dir="ltr" class="odt-ltr-term">Sun</bdi> في <bdi dir="ltr" class="odt-ltr-term">ISERPA</bdi> في أنجيه

من الواضح أنه سيكون من الأنسب الإشارة إلى جهاز ما باسمه بدلاً من عنوان <bdi dir="ltr" class="odt-ltr-term">IP</bdi> الخاص به. وهذا يثير مسألة تفرد الأسماء: فهناك ملايين الأجهزة المتصلة ببعضها البعض. قد يتصور المرء وجود هيئة مركزية تقوم بتعيين الأسماء. ولا شك أن ذلك سيكون أمراً مرهقاً للغاية. وقد تم في الواقع توزيع السيطرة على الأسماء عبر **النطاقات**. تدار كل نطاق من قبل منظمة صغيرة الحجم عمومًا تتمتع بحرية كاملة في اختيار أسماء الأجهزة. وبالتالي، تنتمي الأجهزة في فرنسا إلى نطاق **fr**، الذي تديره <bdi dir="ltr" class="odt-ltr-term">Inria</bdi> في باريس. ولتبسيط الأمور أكثر، يتم توزيع السيطرة بشكل أكبر: يتم إنشاء نطاقات داخل نطاق **fr**. وبالتالي، تنتمي جامعة أنجيه إلى نطاق **<bdi dir="ltr" class="odt-ltr-term">univ-Angers</bdi>**. ويتمتع القسم الذي يدير هذا النطاق بحرية كاملة في تسمية الأجهزة الموجودة على شبكة جامعة أنجيه. في الوقت الحالي، لم يتم تقسيم هذا النطاق إلى أقسام فرعية. ولكن في جامعة كبيرة تضم العديد من الأجهزة المتصلة بالشبكة، قد يتم ذلك.

تم تسمية جهاز <bdi dir="ltr" class="odt-ltr-term">DPX2/320</bdi> في جامعة أنجيه *باسم* <bdi dir="ltr" class="odt-ltr-term">Lagaffe</bdi>، بينما تم تسمية جهاز كمبيوتر شخصي 486DX50 *باسم <bdi dir="ltr" class="odt-ltr-term">liny</bdi>*. كيف يمكنك الإشارة إلى هذه الأجهزة من الخارج؟ عن طريق تحديد التسلسل الهرمي للنطاقات التي تنتمي إليها. وبالتالي، سيكون الاسم الكامل لجهاز <bdi dir="ltr" class="odt-ltr-term">Lagaffe</bdi> هو:
        Lagaffe.univ-Angers.fr
يمكن استخدام الأسماء النسبية داخل النطاقات. وبالتالي، داخل نطاق **fr** وخارج نطاق **<bdi dir="ltr" class="odt-ltr-term">univ</bdi>**-<bdi dir="ltr" class="odt-ltr-term">Angers</bdi>، يمكن الإشارة إلى جهاز <bdi dir="ltr" class="odt-ltr-term">Lagaffe</bdi> على النحو التالي
        Lagaffe.univ-Angers
وأخيرًا، داخل نطاق *<bdi dir="ltr" class="odt-ltr-term">univ-Angers</bdi>*، يمكن الإشارة إليها ببساطة على النحو التالي
        Lagaffe
وبالتالي، يمكن للتطبيق الإشارة إلى جهاز ما باسمه. لكن في النهاية، لا تزال بحاجة إلى الحصول على عنوان <bdi dir="ltr" class="odt-ltr-term">IP</bdi> الخاص بالجهاز. كيف يتم ذلك؟ لنفترض أننا نريد، من الجهاز <bdi dir="ltr" class="odt-ltr-term">A</bdi>، التواصل مع الجهاز <bdi dir="ltr" class="odt-ltr-term">B.</bdi>
  • إذا كان الجهاز B ينتمي إلى نفس المجال الذي ينتمي إليه الجهاز A، فمن المرجح أن يتم العثور على عنوان IP الخاص به في ملف موجود على الجهاز A.
  • وإلا، فسيجد الجهاز A، في ملف آخر أو في نفس الملف السابق، قائمة بعدة خوادم أسماء مع عناوين IP الخاصة بها. وخادم الأسماء مسؤول عن ربط اسم الجهاز بعنوان IP الخاص به. وسيقوم الجهاز A بإرسال طلب خاص إلى أول خادم أسماء في قائمته، يُعرف باسم استعلام DNS، والذي يتضمن اسم الجهاز المطلوب. إذا كان الخادم المستفسر عنه يحتوي على هذا الاسم في سجلاته، فسيرسل عنوان IP المقابل إلى الجهاز A. وإلا، فسيجد الخادم أيضًا في ملفاته قائمة بخوادم الأسماء التي يمكنه الاستفسار منها. ثم يقوم بذلك. وبالتالي، سيتم الاستفسار من عدد من خوادم الأسماء، ليس بشكل عشوائي بل بطريقة تقلل من عدد الطلبات. إذا تم العثور على الجهاز أخيرًا، فسيتم إرسال الرد إلى الجهاز A.

XDR: (تمثيل البيانات الخارجية)

تم إنشاء هذا البروتوكول بواسطة شركة <bdi dir="ltr" class="odt-ltr-term">Sun Microsystems</bdi>، وهو يحدد معيارًا لتمثيل البيانات بشكل مستقل عن الجهاز.

RPC: (استدعاء الإجراءات عن بُعد)

تم تعريفه أيضًا بواسطة <bdi dir="ltr" class="odt-ltr-term">Sun</bdi>، وهو بروتوكول اتصال بين التطبيقات البعيدة، مستقل عن طبقة النقل. هذا البروتوكول مهم: فهو يعفي المبرمج من الحاجة إلى معرفة تفاصيل طبقة النقل ويجعل التطبيقات قابلة للنقل. يعتمد هذا البروتوكول على بروتوكول <bdi dir="ltr" class="odt-ltr-term">XDR</bdi>

NFS: نظام ملفات الشبكة

تم تعريفه أيضًا بواسطة <bdi dir="ltr" class="odt-ltr-term">Sun</bdi>، ويسمح هذا البروتوكول لجهاز واحد بـ&quot;رؤية&quot; نظام الملفات الخاص بجهاز آخر. وهو يعتمد على بروتوكول <bdi dir="ltr" class="odt-ltr-term">RPC</bdi> الموصوف أعلاه.

8.1.9. الخلاصة

في هذه المقدمة، عرضنا بعض الميزات الرئيسية لبروتوكولات الإنترنت. لمزيد من الاستكشاف في هذا المجال، يمكن للقراء الرجوع إلى كتاب دوغلاس كومر الممتاز:

العنوان: TCP/IP: الهندسة المعمارية والبروتوكولات والتطبيقات.

المؤلف: دوغلاس كومر

الناشر: InterEditions

8.2. إدارة عناوين الشبكة في Java

8.2.1. التعريف

يتم تعريف كل جهاز على الإنترنت بواسطة عنوان أو اسم فريد. تتم إدارة هذين العنصرين في Java بواسطة فئة InetAddress، التي تتضمن الطرق التالية:

byte [] getAddress()
تُرجع 4 بايت من عنوان IP لمثيل InetAddress الحالي
String getHostAddress()
تُرجع عنوان IP لمثيل InetAddress الحالي
String getHostName()
تُرجع اسم الإنترنت لمثيل InetAddress الحالي
String toString()
تُرجع عنوان IP/اسم الإنترنت لمثيل InetAddress الحالي
InetAddress getByName(String Host)
ينشئ مثيل InetAddress للجهاز المحدد بواسطة Host. يرمي استثناءً إذا كان Host غير معروف. يمكن أن يكون Host اسم الإنترنت لجهاز أو عنوان IP الخاص به بالصيغة I1.I2.I3.I4
InetAddress getLocalHost()
ينشئ مثيل InetAddress للجهاز الذي يعمل عليه البرنامج الذي يحتوي على هذه التعليمات.

8.2.2. بعض الأمثلة

8.2.2.1. تحديد الجهاز المحلي


import java.net.*;
 
public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// fin try
  }// fine hand
}// fin class

نتائج التنفيذ هي كما يلي:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

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

8.2.2.2. تحديد أي جهاز


import java.net.*;
 
public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // we retrieve the argument
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // we try to obtain the machine address
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// fin try
  }// fine hand
}// fin class

باستخدام استدعاء getByName في Java، نحصل على النتائج التالية:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

باستخدام استدعاء getbyname في Java shiva.istia.univ-angers.fr، نحصل على:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

باستخدام استدعاء Java **getbyname www.ibm.com**، نحصل على:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. برمجة TCP-IP

8.3.1. معلومات عامة

Image

عندما يرغب تطبيق AppA على الجهاز A في التواصل مع تطبيق AppB على الجهاز B عبر الإنترنت، فإنه يحتاج إلى معرفة عدة أمور:

  • عنوان IP أو اسم المضيف للجهاز B
  • رقم المنفذ الذي يستخدمه تطبيق AppB. وذلك لأن الجهاز B قد يستضيف العديد من التطبيقات التي تعمل على الإنترنت. وعندما يتلقى معلومات من الشبكة، يجب أن يعرف التطبيق الذي تستهدفه هذه المعلومات. وتصل التطبيقات الموجودة على الجهاز B إلى الشبكة من خلال واجهات تُعرف أيضًا باسم منافذ الاتصال. وترد هذه المعلومات في الحزمة التي يتلقاها الجهاز B حتى يمكن توصيلها إلى التطبيق الصحيح.
  • بروتوكولات الاتصال التي يفهمها الجهاز B. في دراستنا، سنستخدم بروتوكولات TCP-IP فقط.
  • بروتوكول الاتصال الذي يقبله تطبيق AppB. في الواقع، ستتواصل الجهازان A و B مع بعضهما البعض. وسيتم تغليف ما يقولانه ضمن بروتوكولات TCP/IP. ومع ذلك، عندما يتلقى تطبيق AppB، في نهاية السلسلة، المعلومات المرسلة من تطبيق AppA، يجب أن يكون قادرًا على تفسيرها. وهذا مشابه للحالة التي يتواصل فيها شخصان، A و B، عبر الهاتف: حيث يتم نقل محادثتهما عبر الهاتف. يتم ترميز الكلام كإشارات بواسطة الهاتف A، ويتم إرساله عبر خطوط الهاتف، ويستقبله الهاتف B، حيث يتم فك ترميزه. ثم يسمع الشخص B الكلام. وهنا يأتي دور مفهوم بروتوكول الاتصال: إذا كان A يتحدث الفرنسية و B لا يفهم تلك اللغة، فلن يتمكن A و B من التواصل بشكل فعال.

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

8.3.2. خصائص بروتوكول TCP

هنا، سنقوم فقط بفحص اتصالات الشبكة باستخدام بروتوكول النقل TCP. دعونا نستعرض خصائص هذا البروتوكول:

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

8.3.3. العلاقة بين العميل والخادم

غالبًا ما يكون الاتصال عبر الإنترنت غير متماثل: تبدأ الآلة أ الاتصال لطلب خدمة من الآلة ب، محددةً أنها تريد فتح اتصال مع الخدمة SB1 على الآلة ب. تقبل الآلة ب أو ترفض. إذا وافق، يمكن للجهاز A إرسال طلباته إلى الخدمة SB1. يجب أن تتوافق هذه الطلبات مع بروتوكول الاتصال الذي تفهمه الخدمة SB1. وبذلك يتم إنشاء حوار طلب-استجابة بين الجهاز A، المعروف باسم جهاز العميل، والجهاز B، المعروف باسم جهاز الخادم. سيقوم أحد الشريكين بإغلاق الاتصال.

8.3.4. بنية العميل

ستكون بنية برنامج الشبكة الذي يطلب خدمات تطبيق الخادم كما يلي:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
        tant que ce n'est pas fini
            préparer une demande
            l'émettre vers la machine B
            attendre et récupérer la réponse
            la traiter
        fin tant que
finsi

8.3.5. بنية الخادم

ستكون بنية البرنامج الذي يقدم الخدمات على النحو التالي:

ouvrir le service sur la machine locale
tant que le service est ouvert
        se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
        lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

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

ستكون مهمة الخدمة بالهيكل التالي:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

8.3.6. فئة Socket

8.3.6.1. التعريف

الأداة الأساسية التي تستخدمها البرامج التي تتواصل عبر الإنترنت هي «السوكت» (socket). وتعني هذه الكلمة الإنجليزية «مأخذ كهربائي»، وقد تم توسيع معناها هنا لتشير إلى «مأخذ شبكي». لكي يتمكن التطبيق من إرسال واستقبال المعلومات عبر الإنترنت، فإنه يحتاج إلى منفذ شبكة، أي مقبس. تم إنشاء هذه الأداة في الأصل في إصدارات بيركلي من نظام يونكس. ومنذ ذلك الحين تم نقلها إلى جميع أنظمة يونكس وكذلك إلى بيئة ويندوز. كما أنها موجودة على الآلات الافتراضية لـ جافا في شكلين: فئة Socket لتطبيقات العميل وفئة ServerSocket لتطبيقات الخادم. نوضح هنا بعض منشئات وأساليب فئة Socket:

public Socket(String host, int port)
يفتح اتصالاً عن بُعد بالمنفذ port على الجهاز المضيف
public int getLocalPort()
تُرجع رقم المنفذ المحلي الذي يستخدمه المأخذ
  
public int getPort()
تُرجع رقم المنفذ البعيد الذي يتصل به المقبس
  
public InetAddress getLocalAddress()
تُرجع عنوان InetAddress المحلي الذي يرتبط به المأخذ
  
public InetAddress getInetAddress()
تُرجع عنوان InetAddress البعيد الذي يرتبط به المأخذ
  
public InputStream getInputStream()
تُرجع دفق إدخال يُستخدم لقراءة البيانات المرسلة من الشريك البعيد
  
public OutputStream getOutputStream()
تُرجع تيار إخراج يُستخدم لإرسال البيانات إلى الشريك البعيد
  
public void shutdownInput()
يغلق تيار الإدخال الخاص بالمقبس
  
public void shutdownOutput()
يغلق دفق الإخراج للمقبس
  
public void close()
يغلق المأخذ وتدفقات الإدخال/الإخراج الخاصة به
  
public String toString()
تُرجع سلسلة "تمثل" المأخذ
 

8.3.6.2. إنشاء اتصال بخادم

لقد رأينا أنه لكي يقوم الجهاز A بإنشاء اتصال بخدمة على الجهاز B، فإنه يحتاج إلى معلومتين:

  • عنوان IP أو اسم المضيف للجهاز B
  • رقم المنفذ الذي تعمل عليه الخدمة المطلوبة

المنشئ

    public Socket(String  host, int  port);

يُنشئ مأخذ توصيل ويقوم بتوصيله بجهاز host على المنفذ port. يُطلق مُنشئ الدالة هذا استثناءً في حالات مختلفة:

  • عنوان غير صحيح
  • منفذ غير صحيح
  • رفض الطلب

نحتاج إلى معالجة هذا الاستثناء:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // la connexion a échoué - on traite l'erreur
        ….
    }

إذا نجح طلب الاتصال، يتم تخصيص منفذ محلي للعميل للتواصل مع الجهاز ب. وبمجرد إقامة الاتصال، يمكن استرداد هذا المنفذ باستخدام الطريقة التالية:

public int getLocalPort();

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

public int getPort();

8.3.6.3. إرسال المعلومات عبر الشبكة

يمكنك الحصول على دفق كتابة على المقبس — وبالتالي على الشبكة — باستخدام الطريقة:

public OutputStream getOutputStream();

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

8.3.6.4. قراءة المعلومات من الشبكة

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

public InputStream getInputStream();

كل ما يتم قراءته من هذا الدفق يأتي من منفذ الخدمة الخاص بجهاز الخادم. بالنسبة للتطبيقات التي تحتوي على مربع حوار يتكون من أسطر نصية تنتهي بسطر جديد، سنرغب في استخدام طريقة readLine. للقيام بذلك، نقوم بتحويل InputStream إلى BufferedReader، الذي يحتوي على طريقة readLine(). قد تؤدي القراءة إلى إلقاء استثناء.

8.3.6.5. إغلاق الاتصال

يتم ذلك باستخدام الطريقة:

public void close();

قد تثير هذه الطريقة استثناءً. يتم تحرير الموارد المستخدمة، ولا سيما منفذ الشبكة.

8.3.6.6. بنية العميل

لدينا الآن العناصر اللازمة لوصف البنية الأساسية لعميل الإنترنت:


    Socket  sClient=null;
    try{
            // connect to the service running on port P of machine M
        sClient=new Socket(M,P);
 
        // create client socket I/O streams
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
 
        // request-response loop
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // preparing the application
            demande=…
            // we send it
            out.println(demande);
            // we read the answer
            réponse=in.readLine();
            // the answer is processed

        }
        // it's over
        sClient.close();
    } catch(Exception e){
        // we handle the exception
        ….
    }

لم نحاول معالجة الأنواع المختلفة من الاستثناءات التي يولدها منشئ Socket أو الطرق readLine و getInputStream و getOutputStream و close من أجل الحفاظ على بساطة المثال. تم دمج كل شيء في استثناء واحد.

8.3.7. فئة ServerSocket

8.3.7.1. التعريف

هذه الفئة مخصصة لإدارة مآخذ التوصيل من جانب الخادم. نوضح هنا بعض منشئات وأساليب هذه الفئة:

public ServerSocket(int port)
ينشئ مأخذ توصيل في وضع الاستماع على المنفذ port
public ServerSocket(int port, int count)
مثل ما سبق، ولكنه يضبط حجم قائمة الانتظار على count، أي الحد الأقصى لعدد اتصالات العملاء التي سيتم وضعها في قائمة الانتظار إذا كان الخادم مشغولاً عند وصول اتصال العميل.
public int getLocalPort()
يعيد رقم منفذ الاستماع الذي يستخدمه المقبس
public InetAddress getInetAddress()
تُرجع عنوان InetAddress المحلي الذي يرتبط به المأخذ
public Socket accept()
يضع الخادم في حالة انتظار للاتصال (عملية حظر). عند وصول اتصال العميل، يعرض مأخذ توصيل يتم من خلاله تقديم الخدمة للعميل.
public void close()
يغلق المأخذ وتدفقات الإدخال/الإخراج الخاصة به
public String toString()
تُرجع سلسلة "تمثل" المأخذ
public void close()
يغلق مقبس الخدمة ويحرر الموارد المرتبطة به

8.3.7.2. فتح الخدمة

يتم ذلك باستخدام المنشئين التاليين:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

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

8.3.7.3. قبول طلب اتصال

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

    public Socket accept();

تُرجع هذه الطريقة مثيل Socket: وهو مأخذ توصيل الخدمة، الذي سيتم من خلاله تقديم الخدمة، غالبًا بواسطة مؤشر ترابط آخر. قد تُطلق الطريقة استثناءً.

8.3.7.4. القراءة/الكتابة عبر مأخذ توصيل الخدمة

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

8.3.7.5. تحديد العميل

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

    public InetAddress getInetAddress()

الخاصة بفئة Socket. وهذا يتيح الوصول إلى عنوان IP واسم العميل.

8.3.7.6. إغلاق الخدمة

يتم ذلك باستخدام الطريقة

    public void close();

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

8.3.7.7. البنية الأساسية للخادم

بناءً على ما سبق، يمكننا كتابة الهيكل الأساسي للخادم:


SocketServer sEcoute=null;
try{
    // ouverture du service
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);
 
    // traitement des demandes de connexion
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // attente et acceptation d'une demande
        sService=sEcoute.accept();
 
        // le service est rendu par une autre tâche à laquelle on passe la socket de service
        new Service(sService).start();
 
        // on se remet en attente des demandes de connexion
    }
    // c'est fini - on clôt le service
    sEcoute.close();
} catch (Exception e){
    // on traite l'exception

}

فئة Service هي مؤشر ترابط قد يبدو كما يلي:


public class Service extends Thread{
 
    Socket sService;        // service socket
 
    // manufacturer
    public Service(Socket S){
        sService=S;
    }
 
// run
public void run(){
    try{
        // create input-output flows
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
 
    // request-response loop
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // we read the request
        demande=in.readLine();
 
        // we treat it 

 
        // we prepare the answer
        réponse=…

        // we send it
        out.println(réponse);
    }
    // it's over
    sService.close();
    } catch(Exception e){
    // we handle the exception
    ….
    }// try
} // run

8.4. التطبيقات

8.4.1. خادم Echo

نقترح كتابة خادم إيكو يتم تشغيله من نافذة DOS باستخدام الأمر:

    java serveurEcho port

يعمل الخادم على المنفذ الذي تم تمريره كمعلمة. وهو ببساطة يعيد إرسال الطلب الذي أرسله العميل إليه، مع هوية العميل (IP+الاسم). ويقبل اتصالين في قائمة الانتظار الخاصة به. لدينا هنا جميع مكونات خادم TCP. البرنامج كالتالي:

// call: serveurEcho port
// echo server
// returns the line sent to the customer


import java.net.*;
import java.io.*;

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // main program
    public static void main (String arg[]){

     // is there an argument
     if(arg.length != 1)
        erreur(syntaxe,1);

     // this argument must be integer >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // create the listening socket
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // follow-up
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // service loop
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // waiting for a customer
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // we identify the link
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // the service is provided by another task
        new traiteClientEcho(service).start();
     }// end while
    }// fine hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// end class


// provides service to an echo server client

class traiteClientEcho extends Thread{

    private Socket service;            // service socket
    private BufferedReader in;        // iNPUTS
    private PrintWriter out;            // output flow

     // manufacturer
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // run method
    public void run(){

         // creation of input and output flows
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// fin try
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// fin try

         // link identification is sent to the customer
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // loop read request/write response
        String demande,reponse;
        try{
             // the service stops when the client sends an end-of-file marker
            while ((demande=in.readLine())!=null){
                // echo of demand
                reponse="["+demande+"]";
                out.println(reponse);
                 // service stops when client sends "end
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// end while
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// fin try

         // close the socket
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// fin try
    }// end run

     // error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// end error

     // identifies
    private String identifie(InetAddress Host){
         // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

تم دمج الفئتين المطلوبتين للخدمة في ملف مصدر واحد. واحدة فقط منهما، وهي التي تحتوي على الدالة الرئيسية، لها السمة العامة. تتوافق بنية الخادم مع البنية العامة لخوادم TCP. تمت إضافة طريقة (identify) لتحديد الاتصال بين الخادم والعميل. فيما يلي بعض النتائج:

يتم تشغيل الخادم بواسطة الأمر

    java serveurEcho 187

ثم يعرض الرسالة التالية في نافذة وحدة التحكم:

Serveur d'écho lancé sur le port 187

لاختبار هذا الخادم، نستخدم برنامج telnet، المتوفر على كل من أنظمة Unix و Windows. Telnet هو عميل TCP عالمي مناسب لجميع الخوادم التي تقبل سطور نصية تنتهي بحرف نهاية السطر في اتصالاتها. وهذا هو الحال مع خادم echo الخاص بنا. نقوم بتشغيل أول عميل telnet على نظام Windows (2000 في هذا المثال) عن طريق كتابة telnet في نافذة DOS:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1
 
Le caractère d'escapement is 'CTRL+$'
 
Microsoft Telnet> help
 
Les commandes peuvent être abrégées. Les commandes prises en charge sont :
 
close           ferme la connexion en cours
display         affiche les paramètres d'operation
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' to display the list)
status          affiche les informations d'status
unset           annule les options (entrez 'unset ?' to display the list)
? ou help       affiche des informations d'help
 
Microsoft Telnet> set ?
NTLM            Active l'authentication NTLM.
LOCAL_ECHO      Active l'local echo.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF
 
Microsoft Telnet> set local_echo
 
Microsoft Telnet> open localhost 187

بشكل افتراضي، لا يعرض برنامج Telnet الأوامر التي يتم كتابتها على لوحة المفاتيح. لتمكين هذه الميزة، أدخل الأمر:

Microsoft Telnet> set local_echo

لفتح اتصال بالخادم، مع تحديد منفذ خدمة الإعادة (187) وعنوان الجهاز الذي يعمل عليه (localhost)، أدخل الأمر التالي:

Microsoft Telnet> open localhost 187

في نافذة DOS الخاصة بالعميل، ستتلقى بعد ذلك الرسالة التالية:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

في نافذة الخادم، تظهر الرسالة التالية:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

هنا، يشير tahe و localhost إلى نفس الجهاز. في نافذة عميل Telnet، يمكنك كتابة أسطر من النص. يقوم الخادم بإعادة عرضها:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

لاحظ أن منفذ العميل (1059) تم اكتشافه بشكل صحيح، لكن منفذ الخدمة (187) مطابق لمنفذ الاستماع (187)، وهو أمر غير متوقع. فمن المتوقع بالفعل الحصول على منفذ مقبس الخدمة بدلاً من منفذ الاستماع. يجب أن نتحقق مما إذا كنا سنحصل على نفس النتائج على نظام Unix. الآن، دعونا نطلق عميل telnet ثانٍ. تصبح نافذة الخادم كما يلي:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

في نافذة العميل الثاني، يمكنك أيضًا كتابة أسطر من النص:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

يوضح هذا أن خادم echo يمكنه خدمة عدة عملاء في نفس الوقت. يمكن إنهاء عمل عملاء Telnet عن طريق إغلاق نافذة DOS التي يعملون فيها.

8.4.2. عميل Java لخادم echo

في القسم السابق، استخدمنا عميل Telnet لاختبار خدمة الصدى. والآن سنكتب عميلنا الخاص:

// call: clientEcho machine port
// echo server client
// sends lines to the server, which echoes them back to the server

import java.net.*;
import java.io.*;

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // main program    
    public static void main (String arg[]){

     // are there two arguments
     if(arg.length != 2)
        erreur(syntaxe,1);

     // the first argument must be the name of an existing machine
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // port must be integer >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // connect to the server
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // we identify the link
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // creation of a flow for reading lines typed on the keyboard
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // creation of the input stream associated with the client socket
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // creation of the output stream associated with the client socket
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // request-response loops
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // we read the message sent by the server just after connection 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // response display
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // read a line typed on the keyboard
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // sending demand on the network
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // wait/read answer
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // response display
        System.out.println("Serveur : " +reponse);
         // is it over?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // it's over
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

يتوافق هيكل هذا العميل مع البنية العامة لعملاء TCP. هنا، قمنا بمعالجة الاستثناءات المختلفة المحتملة واحدة تلو الأخرى، مما يجعل البرنامج أثقل. فيما يلي النتائج التي تم الحصول عليها عند اختبار هذا العميل:

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

السطور التي تبدأ بـ "العميل" هي تلك التي أرسلها العميل، وتلك التي تبدأ بـ "الخادم" هي تلك التي ردها الخادم.

8.4.3. عميل TCP عام

تعمل العديد من الخدمات التي تم إنشاؤها في بدايات الإنترنت وفقًا لنموذج خادم الصدى الذي تمت مناقشته سابقًا: حيث يتكون الاتصال بين العميل والخادم من تبادل أسطر نصية. سنقوم بكتابة عميل TCP عام سيتم تشغيله على النحو التالي: java cltTCPgenerique server port

سيتصل عميل TCP هذا بالمنفذ port على الخادم server. وبمجرد الاتصال، سيقوم بإنشاء مؤشرين:

  1. خيط مسؤول عن قراءة الأوامر المكتوبة على لوحة المفاتيح وإرسالها إلى الخادم
  2. خيط مسؤول عن قراءة استجابات الخادم وعرضها على الشاشة

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

  • يجب على العميل إرسال عدة أسطر من النص قبل تلقي الرد
  • قد يحتوي رد الخادم على عدة أسطر من النص

لذلك، فإن الحلقة التي ترسل سطرًا واحدًا إلى الخادم وتستقبل سطرًا واحدًا من الخادم ليست مناسبة دائمًا. لذلك سننشئ حلقتين منفصلتين:

  • حلقة لقراءة الأوامر المكتوبة على لوحة المفاتيح لإرسالها إلى الخادم. سيشير المستخدم إلى نهاية الأوامر باستخدام الكلمة الرئيسية "fin".
  • حلقة لاستقبال وعرض ردود الخادم. ستكون هذه حلقة لا نهائية لن تتوقف إلا بإغلاق الخادم للاتصال بالشبكة أو بكتابة المستخدم للأمر "end" على لوحة المفاتيح.

للحصول على هاتين الحلقتين المنفصلتين، نحتاج إلى مؤشرين مستقلين. لنلقِ نظرة على مثال للتنفيذ حيث يتصل عميل TCP العام لدينا بخدمة SMTP (بروتوكول نقل البريد البسيط). هذه الخدمة مسؤولة عن توجيه البريد الإلكتروني إلى مستلميه. وهي تعمل على المنفذ 25 وتستخدم بروتوكول تبادل نصي.


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

دعونا نعلق على هذه التبادلات بين العميل والخادم:

  • ترسل خدمة SMTP رسالة ترحيب عندما يتصل بها العميل:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • تحتوي بعض الخدمات على أمر "help" يوفر معلومات عن الأوامر المتاحة لتلك الخدمة. هذا ليس هو الحال هنا. أوامر SMTP المستخدمة في المثال هي كما يلي:
    • mail from: sender، لتحديد عنوان البريد الإلكتروني للمرسل
    • rcpt to: recipient، لتحديد عنوان البريد الإلكتروني لمستلم الرسالة. إذا كان هناك عدة مستلمين، يتم تكرار الأمر rcpt to: عدة مرات حسب الحاجة لكل مستلم.
    • data، الذي يشير إلى خادم SMTP بأن الرسالة على وشك الإرسال. كما هو موضح في استجابة الخادم، يتكون هذا من سلسلة من الأسطر تنتهي بسطر يحتوي على نقطة فقط. قد تحتوي الرسالة على رؤوس مفصولة عن نص الرسالة بسطر فارغ. في مثالنا، قمنا بتضمين موضوع باستخدام الكلمة الرئيسية Subject:
  • بمجرد إرسال الرسالة، يمكننا إخبار الخادم بأننا انتهينا باستخدام الأمر quit. ثم يقوم الخادم بإغلاق اتصال الشبكة. يمكن لسلسلة القراءة اكتشاف هذا الحدث والتوقف.
  • ثم يكتب المستخدم "end" على لوحة المفاتيح لإيقاف مؤشر الترابط الذي يقرأ الأوامر المكتوبة على لوحة المفاتيح أيضًا.

إذا تحققنا من البريد الإلكتروني المستلم، فسنرى ما يلي (Outlook):

Image

لاحظ أن خدمة SMTP لا يمكنها تحديد ما إذا كان المرسل صالحًا أم لا. لذلك، لا يمكنك أبدًا الوثوق بحقل "من" في الرسالة. في هذه الحالة، لم يكن المرسل machin@univ-angers.fr موجودًا.

يتيح لنا عميل TCP العام هذا اكتشاف بروتوكول اتصال خدمات الإنترنت، ومن ثم إنشاء فئات متخصصة لعملاء هذه الخدمات. دعونا نستكشف بروتوكول اتصال خدمة POP (بروتوكول مكتب البريد)، الذي يتيح لنا استرداد رسائل البريد الإلكتروني المخزنة على الخادم. يعمل هذا البروتوكول على المنفذ 110.


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

الأوامر الرئيسية هي كما يلي:

  • تسجيل دخول المستخدم، حيث تقوم بإدخال معلومات تسجيل الدخول الخاصة بك على الجهاز الذي يستضيف بريدك الإلكتروني
  • كلمة مرور، حيث تدخل كلمة المرور المرتبطة باسم المستخدم السابق
  • list، للحصول على قائمة بالرسائل بالصيغة رقم، الحجم بالبايت
  • retr i، لقراءة الرسالة رقم i
  • الخروج، لإنهاء الجلسة.

دعونا الآن نستكشف بروتوكول الاتصال بين العميل وخادم الويب، الذي يعمل عادةً على المنفذ 80:


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0
 
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

يرسل عميل الويب أوامره إلى الخادم وفقًا للنمط التالي:

commande1
commande2
...
commanden
[ligne vide]

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

GET /index.html HTTP/1.0

الذي يطلب عنوان URL /index.html من الخادم ويشير إلى أنه يستخدم الإصدار 1.0 من HTTP. أحدث إصدار من هذا البروتوكول هو 1.1. يوضح المثال أن الخادم استجاب بإرسال محتويات ملف index.html ثم أغلق الاتصال، كما نرى في قراءة الاستجابة التي تشير إلى إنهاء الخيط. قبل إرسال محتويات ملف index.html، أرسل خادم الويب سلسلة من الرؤوس تليها سطر فارغ:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

السطر <html> هو السطر الأول من ملف /index.html. يُطلق على النص السابق اسم رؤوس HTTP (بروتوكول نقل النص التشعبي). لن نتطرق إلى تفاصيل هذه الرؤوس هنا، ولكن ضع في اعتبارك أن عميلنا العام يوفر الوصول إليها، مما قد يساعد في فهمها. على سبيل المثال، السطر الأول:

<-- HTTP/1.1 200 OK

يشير إلى أن خادم الويب الذي تم الاتصال به يدعم بروتوكول HTTP/1.1 وأنه عثر بنجاح على الملف المطلوب (200 OK)، حيث 200 هو رمز استجابة HTTP. الأسطر

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

يُعلم العميل بأنه سيتلقى 11,251 بايت من نص HTML (لغة ترميز النص التشعبي) وأن الاتصال سيُغلق بمجرد إرسال البيانات.

إذن، لدينا هنا عميل TCP مفيد للغاية. إنه بالتأكيد أقل قدرة من برنامج telnet الذي استخدمناه سابقًا، لكن كان من المثير للاهتمام أن نكتبه بأنفسنا. برنامج عميل TCP العام هو كما يلي:

// imported packages
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // receives the characteristics of a service as a parameter in the form
     // server port
     // connects to the service
     // creates a thread to read keyboard commands
     // these will be sent to the
     // creates a thread to read server responses
     // these will be displayed on the screen
     // the whole thing ends with the command end typed on the keyboard

   // instance variable
  private static Socket client;

    public static void main(String[] args){

         // syntax
        final String syntaxe="pg serveur port";

         // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the server name
        String serveur=args[0];

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // there may be problems
        try{
             // connect to the service
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // error
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // end
            return;
        }//catch

         // create read/write threads
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // end thread main
        return;
    }// hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class  

class ClientSend extends Thread {
    // class for reading keyboard commands
     // and send them to a server via a tcp client passed in parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientSend(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;            // network write streams
    BufferedReader IN=null;        // keyboard flow
        String commande=null;            // command read from keyboard

         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard input stream creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            System.out.println("Commandes : ");
            while(true){
                 // read command typed on keyboard
                commande=IN.readLine().trim();
                // finished?
                if (commande.toLowerCase().equals("fin")) break;
                // send order to server
                OUT.println(commande);
                 // next order
            }//while
        }catch(Exception ex){
             // error
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//run
}//class

class ClientReceive extends Thread{
    // class responsible for reading lines of text intended for a 
     // tcp client passed as parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientReceive(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//run
}//class

8.4.4. خادم TCP عام

الآن سنلقي نظرة على خادم

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

يتم تشغيل البرنامج بواسطة: java genericTCPserver listeningPort، حيث listeningPort هو المنفذ الذي يجب على العملاء الاتصال به. سيتم التعامل مع خدمة العميل بواسطة خيطين:

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

يقوم الخادم بإنشاء خيطين لكل عميل. إذا كان هناك n عميل، فسيكون هناك 2n خيطًا نشطًا في نفس الوقت. لا يتوقف الخادم نفسه أبدًا ما لم يضغط المستخدم على Ctrl-C على لوحة المفاتيح. دعونا نلقي نظرة على بعض الأمثلة.

يعمل الخادم على المنفذ 100، ونستخدم العميل العام للتواصل معه. تبدو نافذة العميل كما يلي:


E:\data\serge\MSNET\c#\réseau\client tcp générique> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

السطور التي تبدأ بـ <-- هي تلك المرسلة من الخادم إلى العميل؛ أما البقية فهي من العميل إلى الخادم. نافذة الخادم هي كما يلي:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

السطور التي تبدأ بـ <-- هي تلك المرسلة من العميل إلى الخادم. السطور N: هي تلك المرسلة من الخادم إلى العميل N. لا يزال الخادم أعلاه نشطًا على الرغم من انتهاء العميل 1. نقوم بتشغيل عميل ثانٍ لنفس الخادم:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

تبدو نافذة الخادم بعد ذلك كما يلي:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

والآن دعونا نحاكي خادم ويب عن طريق تشغيل خادمنا العام على المنفذ 88:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

الآن دعونا نفتح متصفحًا ونطلب عنوان URL http://localhost:88/exemple.html. سيتصل المتصفح بعد ذلك بالمنفذ 88 على جهاز localhost ويطلب الصفحة /example.html:

Image

الآن دعونا نلقي نظرة على نافذة الخادم لدينا:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

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

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

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

دعونا نحاول تقديم استجابة مماثلة:

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

السطور التي تبدأ بـ 2: يتم إرسالها من الخادم إلى العميل رقم 2. يغلق الأمر "end" الاتصال من الخادم إلى العميل. في ردنا، اقتصرنا على رؤوس HTTP التالية:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

نحن لا نحدد حجم الملف الذي نرسله (Content-Length)، بل نكتفي بالإشارة إلى أننا سنغلق الاتصال (Connection: close) بعد إرساله. وهذا يكفي للمتصفح. فعندما يرى أن الاتصال قد أُغلق، سيدرك أن استجابة الخادم قد اكتملت، وسيقوم بعرض صفحة HTML التي أُرسلت إليه. وفيما يلي نص الصفحة:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

ثم يقوم المستخدم بإغلاق الاتصال بالعميل عن طريق كتابة الأمر "fin". عندئذٍ يدرك المتصفح أن استجابة الخادم قد اكتملت ويمكنه عرضها:

Image

إذا قمت، في المثال أعلاه، بتحديد "عرض/المصدر" (View/Source) لمعرفة ما تلقّاه المتصفح، فستحصل على:

Image

أي، بالضبط ما تم إرساله من الخادم العام.

فيما يلي كود خادم TCP العام:

// packages
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // main program
    public static void main (String[] args){

    // receives the port of listening to customer requests
     // creates a thread to read client requests
     // these will be displayed on the screen
     // creates a thread to read keyboard commands
     // these will be sent as a reply to the customer
     // the whole thing ends with the command end typed on the keyboard

    final String syntaxe="Syntaxe : pg port";
   // instance variable
         // is there an argument
     if(args.length != 1)
        erreur(syntaxe,1);

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // we create the listening service
    ServerSocket ecoute=null;
    int nbClients=0;    // no. of customers handled
        try{
             // create the service
            ecoute=new ServerSocket(port);
             // follow-up
            System.out.println("Serveur générique lancé sur le port " + port);

             // customer service loop
            Socket client=null;
            while (true){ // infinite loop - will be stopped by Ctrl-C
                 // waiting for a customer
                client=ecoute.accept();

                 // the service is provided by separate threads
                nbClients++;

                 // create read/write threads
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// fine hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

class ServeurSend extends Thread{
    // class responsible for reading typed responses
     // and send them to a client via a tcp client passed to the

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurSend(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;        // network write streams
        String réponse=null;        // answer read from keyboard
    BufferedReader IN=null;    // keyboard flow

        // follow-up
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard flow creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            while(true){
                 // customer identification
                System.out.print("--> " + numClient + " : ");
                 // read response typed on keyboard
                réponse=IN.readLine().trim();
                // finished?
                if (réponse.toLowerCase().equals("fin")) break;
                // send response to server
                OUT.println(réponse);
                 // following response
            }//while
        }catch(Exception ex){
             // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//run
}//class

class ServeurReceive extends Thread{
    // class responsible for reading text lines sent to the server 
     // via a tcp client passed to the builder

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurReceive(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // follow-up
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//run
}//class

8.4.5. عميل ويب

في المثال السابق، رأينا بعض رؤوس HTTP التي أرسلها المتصفح:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

سنقوم بكتابة عميل ويب يأخذ عنوان URL كمعلمة ويعرض محتوى هذا العنوان على الشاشة. سنفترض أن خادم الويب الذي تم الاتصال به للحصول على عنوان URL يدعم بروتوكول HTTP 1.1. من الرؤوس المذكورة أعلاه، سنستخدم فقط ما يلي:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • يشير الرأس الأول إلى الصفحة التي نريدها
  • والثاني يحدد الخادم الذي نستعلم عنه
  • ويشير الثالث إلى أننا نريد من الخادم إغلاق الاتصال بعد الرد علينا.

إذا استبدلنا GET بـ HEAD في المثال أعلاه، فسيرسل لنا الخادم رؤوس HTTP فقط وليس صفحة HTML.

سيُطلق على عميل الويب الخاص بنا الاسم التالي: java clientweb URL cmd، حيث يمثل URL عنوان URL المطلوب، ويمثل cmd إحدى الكلمتين الرئيسيتين GET أو HEAD للإشارة إلى ما إذا كنا نريد الرؤوس فقط (HEAD) أم محتوى الصفحة أيضًا (GET). لنلقِ نظرة على المثال الأول. نبدأ بتشغيل خادم IIS ثم عميل الويب على نفس الجهاز:


dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

الاستجابة

HTTP/1.1 302 Object moved

يعني أن الصفحة المطلوبة قد تم نقلها (أي أن عنوان URL الخاص بها قد تغير). يتم توفير عنوان URL الجديد من خلال رأس Location:

Location: /IISSamples/Default/welcome.htm

إذا استخدمنا GET بدلاً من HEAD في استدعاء عميل الويب:


dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private
 
<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

نحصل على نفس النتيجة التي حصلنا عليها مع HEAD، بالإضافة إلى نص صفحة HTML. البرنامج كالتالي:

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URI GET/HEAD";

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

الميزة الجديدة الوحيدة في هذا البرنامج هي استخدام فئة URL. يتلقى البرنامج عنوان URL (محدد موقع الموارد الموحد) أو URI (معرف الموارد الموحد) بالشكل http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... تسمح لنا فئة URL بتقسيم سلسلة URL إلى مكوناتها المختلفة. يتم إنشاء كائن URL من سلسلة URL المستلمة كمعلمة:

        // vérification validité de l'URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
            // URI incorrecte
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch

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

http://serveur:port/cheminPageHTML?param1=val1;param2=val2;... 

فسيكون لدينا:

url.getHost() = server

url.getPort()=port أو -1 إذا لم يتم تحديد المنفذ

url.getPath()=HTMLPagePath أو سلسلة فارغة إذا لم يكن هناك مسار

url.getQuery() = param1=val1;param2=val2;... أو null إذا لم يكن هناك استعلام

uri.getProtocol() = http

8.4.6. معالجة عمليات إعادة التوجيه من قبل عميل الويب

لا يتعامل عميل الويب السابق مع أي إعادة توجيه لعنوان URL الذي طلبه. أما العميل التالي فيتعامل معها.

  1. يقرأ السطر الأول من رؤوس HTTP المرسلة من الخادم للتحقق مما إذا كان يحتوي على السلسلة "302 Object movedوالتي تشير إلى إعادة توجيه
  2. يقرأ الرؤوس التالية. إذا كانت هناك إعادة توجيه، يبحث عن السطر "Location: url" الذي يوفر عنوان URL الجديد للصفحة المطلوبة ويسجل عنوان URL هذا.
  3. يعرض بقية استجابة الخادم. إذا كان هناك إعادة توجيه، تتكرر الخطوات من 1 إلى 3 باستخدام عنوان URL الجديد. لا يقبل البرنامج أكثر من إعادة توجيه واحدة. يتم تحديد هذا الحد بواسطة ثابت يمكن تعديله.

فيما يلي مثال:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

البرنامج كالتالي:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URL GET/HEAD";

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        final int nbRedirsMax=1;                // no more than one redirection accepted
        int nbRedirs=0;                                    // number of redirects in progress
        String premièreLigne;                        // 1st line of the answer
        boolean redir=false;                        // indicates redirection or not
        String locationString="";                // the URL string of a possible redirection

         // regular expression to find a URL redirect
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // error management
        try{
             // you may have several URL to request if there are redirections
            while(nbRedirs<=nbRedirsMax){

                // extract useful information from URL
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // read the first line of the answer
                premièreLigne=IN.readLine();
                 // screen echo
                System.out.println(premièreLigne);

                // redirection?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // there is a redirection
                    redir=true;
                    nbRedirs++;
                }//if

                // next HTTP headers until you find the empty line signalling the end of the headers
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // the answer is displayed
                    System.out.println(réponse);
                     // if there is a redirection, we search for the Location header
                    if(redir && ! locationFound){
                        // compare the line with the relational expression location
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // if found, note the URL of redirection
                            locationString=résultat.group(1);             
                             // we note that we found
                            locationFound=true;
                        }//if
                    }//if
                    // next header
                }//while

                 // following lines of the answer
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // the answer is displayed
                    System.out.println(réponse);
                }//while
                 // close the connection
                client.close();
                 // are we done?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // there is a redirection to be made - the new URL is built
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // follow-up
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//while
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

8.4.7. خادم حساب الضرائب

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

تم إنشاء فئة أساسية باسم **impots**. سماتها هي ثلاثة مصفوفات من الأرقام:

public class impots{

  // data required for tax calculation
   // come from an external source

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // empty builder
  protected impots(){}

   // manufacturer
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

تحتوي فئة impots على منشئين:

  • منشئ يأخذ المصفوفات الثلاثة للبيانات اللازمة لحساب الضريبة
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • منشئ بدون معلمات لا يمكن استخدامه إلا من قبل الفئات الفرعية
  protected impots(){}

تم اشتقاق فئة **impotsJDBC** من هذه الفئة، مما يسمح بتعبئة المصفوفات الثلاثة limites* و*coeffR* وcoeffN* من محتويات قاعدة البيانات:

public class impotsJDBC extends impots{
  // addition of a constructor for building
   // limit tables, coeffr, coeffn from table
   // database taxes
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: DSN database name
     // userIMPOTS, mdpIMPOTS: database login/password

تمت كتابة تطبيق رسومي. استخدم التطبيق كائنًا من فئة impotsJDBC. كان التطبيق وهذا الكائن موجودين على نفس الجهاز. نقترح وضع برنامج الاختبار وكائن impotsJDBC على أجهزة مختلفة. سيكون لدينا تطبيق خادم-عميل حيث يعمل كائن impotsJDBC البعيد كخادم. تسمى الفئة الجديدة TaxServer وهي مشتقة من فئة impotsJDBC:

// imported packages
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // attributes
    int portEcoute;                // the ability to listen to customer requests
    boolean actif;                // server status

     // manufacturer
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // parent construction
        super(DSNimpots, USERimpots, MDPimpots);
         // we note the listening port
        this.portEcoute=portEcoute;
         // currently inactive
        actif=false;
         // creates and launches a thread for reading keyboard commands
         // the server will be managed using these commands
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

المعلمة الجديدة الوحيدة في المنشئ هي المنفذ المستخدم للاستماع لطلبات العميل. يتم تمرير المعلمات الأخرى مباشرة إلى الفئة الأساسية impotsJDBC. يتم التحكم في خادم الضرائب بواسطة الأوامر التي يتم إدخالها عبر لوحة المفاتيح. لذلك، نقوم بإنشاء مؤشر ترابط لقراءة هذه الأوامر. سيكون هناك أمران محتملان: start لتشغيل الخدمة، و stop لإيقافها نهائيًا. طريقة admin التي تتعامل مع هذه الأوامر هي كما يلي:

    public void admin() throws IOException{
        // reads server administration commands typed from the keyboard
         // in an endless loop
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // invite
            System.out.print("Serveur d'impôts>");
             // read command
            commande=IN.readLine().trim().toLowerCase();
             // order execution
            if(commande.equals("start")){
                // active?
                if(actif){
                     //error
                    System.out.println("Le serveur est déjà actif");
                     // we continue
                    continue;
                }//if
                 // create and launch the listening service
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // end of all execution threads
                System.exit(0);
            }//if
            else {
                // error
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//admin

إذا كان الأمر الذي تم إدخاله عبر لوحة المفاتيح هو start، يتم تشغيل مؤشر ترابط يستمع لطلبات العميل. إذا كان الأمر الذي تم إدخاله هو stop، يتم إيقاف جميع مؤشرات الترابط. يقوم مؤشر الترابط المستمع بتنفيذ طريقة listen:

    public void ecoute(){
         // thread for listening to customer requests
         // we create the listening service
        ServerSocket ecoute=null;
        try{
            // create the service
            ecoute=new ServerSocket(portEcoute);
             // follow-up
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // service loop
            Socket liaisonClient=null;
            while (true){ // infinite loop
                 // waiting for a customer
                liaisonClient=ecoute.accept();

                 // the service is provided by another task
                new traiteClientImpots(liaisonClient,this).start();

                 // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//listening thread

لدينا هنا خادم TCP كلاسيكي يستمع على المنفذ portEcoute. يتم التعامل مع طلبات العميل بواسطة طريقة run في مؤشر الترابط traiteCientImpots، والتي يتم تمرير معلمتين إليها عند الإنشاء:

  1. كائن Socket liaisonClient، الذي سيسمح بالوصول إلى العميل
  2. كائن impotsJDBC هذا، الذي يوفر الوصول إلى طريقة this.calculate لحساب الضريبة.
// -------------------------------------------------------
// provides service to a tax server client

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // customer liaison
    private BufferedReader IN;                // iNPUTS
    private PrintWriter OUT;                    // output flow
    private impotsJDBC objImpots;            // object Tax

         // manufacturer
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//manufacturer

تقوم طريقة run بمعالجة طلبات العملاء. وهي عبارة عن أسطر نصية يمكن أن تتخذ شكلين:

  1. متزوج (نعم/لا) عدد_الأطفال الراتب_السنوي
  2. endCalculation

النموذج 1 يحسب الضريبة، بينما النموذج 2 يغلق اتصال العميل بالخادم.

     // run method
    public void run(){
         // renders service to the customer
        try{
             // iNPUTS
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // output flow
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // send a welcome message to the customer
            OUT.println("Bienvenue sur le serveur d'impôts");

             // loop read request/write response
            String demande=null;
            String[] champs=null;    // elements of the request
            String commande=null;    // customer order: calculation or fincalculs
            while ((demande=IN.readLine())!=null){
                 // demand is broken down into fields
                champs=demande.trim().toLowerCase().split("\\s+");
                 // two successful applications: calcul and fincalculs
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // customer error
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // next order
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // good-bye message to customer
                    OUT.println("Au revoir...");
                     // freeing up resources
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // end
                    return;
                }//if
                 //following request
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// fin try
    }// end Run

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

     // tax calculation
    public void calculerImpôt(String[] champs){
         // processing the application: calculation married nbEnfants salaireAnnuel
         // broken down into fields in the fields table

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // validity of arguments
        try{
             // at least 4 fields are required
            if(champs.length!=4) throw new Exception();
            // married
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // children
            nbEnfants=Integer.parseInt(champs[2]);
             // salary
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // format error
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // finish
            return;
        }//if
         // tax can be calculated
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // we send the response to the customer
        OUT.println(""+impot);
    }//calculate

قد يبدو برنامج الاختبار كما يلي:

// call: serveurImpots port dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // main program
    public static void main (String[] args){

        // you need 4 arguments
        if(args.length != 4)
            erreur(syntaxe,1);

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // we create the tax server
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //error
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}// end class

نقوم بتمرير البيانات اللازمة لإنشاء كائن TaxServer إلى برنامج الاختبار، ومن هناك يقوم البرنامج بإنشاء هذا الكائن.

دعونا نجرب تشغيله للمرة الأولى:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

الأمر

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

يُنشئ كائن TaxServer الذي لا يستمع بعد لطلبات العملاء. إن الأمر start الذي يتم كتابته على لوحة المفاتيح هو الذي يبدأ عملية الاستماع هذه. أما الأمر stop فيقوم بإيقاف تشغيل الخادم. لنستخدم الآن عميلاً. سنستخدم العميل العام الذي تم إنشاؤه سابقاً. الخادم قيد التشغيل:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

يتم تشغيل العميل العام في نافذة DOS أخرى:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

يمكننا أن نرى أن العميل قد تلقى رسالة الترحيب من الخادم بنجاح. نرسل أوامر أخرى:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

نعود إلى نافذة الخادم لإيقافه:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. تمارين

8.5.1. التمرين 1 - الرسم البياني العام لعميل TCP

8.5.1.1. نظرة عامة على التطبيق

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

Image

فيما يلي معاني عناصر التحكم المختلفة:

رقم
الاسم
النوع
الدور
1
TxtRemoteHost
JTextField
اسم الجهاز الذي يقدم الخدمة المطلوبة
2
TxtPort
JTextField
منفذ الخدمة المطلوبة
3
TxtSend
JTextField
نص الرسالة التي سيقوم العميل بإرسالها إلى الخادم
4
OptRCLF
OptLF
JCheckBox
الأزرار المستخدمة لتحديد كيفية إنهاء الأسطر في حوار العميل/الخادم
RCLF: إرجاع الحامل (#13) + تغذية السطر (#10)
LF: تغذية السطر (#10)
5
LstSuivi
JList
يعرض رسائل حول حالة الاتصال بين العميل والخادم
6
LstDialogue
JList
يعرض الرسائل المتبادلة بين العميل (->) والخادم (<-)
7
CmdCancel
JButton
مخفي - يقع أسفل قائمة الحوار - يظهر عندما يكون الاتصال قيد التقدم ويسمح لك بإنهائه إذا لم يستجب الخادم

خيارات القائمة المتاحة هي كما يلي:

الخيار
الخيارات الفرعية
الدور
الاتصال
اتصال
يربط العميل بالخادم
 
قطع الاتصال
يغلق الاتصال
 
خروج
يخرج من البرنامج
الرسائل
إرسال
يرسل الرسالة من عنصر التحكم TxtSend إلى الخادم
 
ClearTracking
مسح قائمة LstSuivi
 
ClearDialogue
مسح قائمة LstDialogue
المؤلف
 
يعرض مربع حقوق النشر

8.5.1.2. كيفية عمل التطبيق

تهيئة التطبيق

عند تحميل النافذة الرئيسية للتطبيق، تحدث الإجراءات التالية:

  • يتم توسيط الورقة على الشاشة
  • يتم تنشيط خيارات القائمة "تسجيل الدخول/تسجيل الخروج" و"المؤلف" فقط
  • يتم إخفاء زر "إلغاء"
  • تكون قائمتا LstSuivi و LstDialogue فارغتين
قائمة "الاتصال/الاتصال"

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

  • يتم التحقق من صحة المنفذ: يجب أن يكون عددًا صحيحًا أكبر من 0
  • يتم تشغيل مؤشر ترابط لإنشاء الاتصال بالخادم
  • يظهر زر "إلغاء" للسماح للمستخدم بمقاطعة الاتصال الجاري
  • يتم تعطيل جميع خيارات القائمة باستثناء "الخروج" و"المؤلف"

يمكن أن ينتهي الاتصال بعدة طرق:

  1. نقر المستخدم على زر "إلغاء": يتم إيقاف مؤشر ترابط الاتصال، وتعود القائمة إلى حالتها الأولية. يشير السجل إلى أن المستخدم أغلق الاتصال.
  2. ينتهي الاتصال بحدوث خطأ: نقوم بنفس الإجراء السابق، بالإضافة إلى ذلك، نشير في السجل إلى سبب الخطأ.
  3. اكتمل الاتصال بنجاح: يتم إزالة زر "إلغاء"، ويشير السجل إلى أن الاتصال قد تم، ويتم تمكين قائمة "إعادة تعيين السجل"، وتعطيل قائمة "الاتصال"، وتمكين قائمة "قطع الاتصال"
قائمة "الاتصال/قطع الاتصال"

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

قائمة الاتصال/الخروج

يغلق هذا الخيار أي اتصال نشط بالخادم ويخرج من التطبيق.

قائمة الرسائل/الإرسال

هذا الخيار متاح فقط في حالة استيفاء الشروط التالية:

  • تم إنشاء اتصال بالخادم

  • وجود رسالة لإرسالها

إذا تم استيفاء هذه الشروط، يتم إرسال النص الموجود في حقل TxtSend (3) إلى الخادم، مع إنهاءه بتسلسل RCLF إذا تم تحديد خيار RCLF، أو بتسلسل LF في الحالات الأخرى. يتم الإبلاغ عن أي أخطاء في الإرسال في قائمة التتبع.

قائمتا RazSuivi و RazDialogue

مسح قوائم LstSuivi و LstDialogue على التوالي. يتم تعطيل هذه الخيارات عندما تكون القوائم المقابلة فارغة.

زر "إلغاء"

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

قوائم التتبع

تتتبع قائمة LstSuivi (5) عملية الاتصال. وهي تشير إلى اللحظات الرئيسية في عملية الاتصال:

  • إنشاؤه بواسطة العميل

  • إغلاقه من قبل الخادم أو العميل

  • أي أخطاء قد تحدث أثناء نشاط الاتصال

تتتبع قائمة LstDialogue (6) الحوار الذي تم إقامته بين العميل والخادم. يراقب مؤشر ترابط في الخلفية ما يحدث على مقبس اتصال العميل ويعرضه في القائمة 6.

خيار المؤلف

تفتح هذه القائمة نافذة تسمى حقوق النشر:

Image

معالجة الأخطاء

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

8.5.1.3. المهام

تنفيذ العمل الموصوف أعلاه في شكلين:

  • تطبيق مستقل
  • برنامج صغير

8.5.2. التمرين 2 - خادم الموارد

8.5.2.1. مقدمة

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

تتعدد مزايا هذا الإعداد:

  • يمكن لأي نوع من العملاء (PC، Mac، Unix، إلخ) استخدام هذه الخدمة
  • يمكن أن يكون العميل في أي مكان على الإنترنت
  • يتم تحسين موارد الحوسبة: لا يتطلب الأمر سوى عدد قليل من الأجهزة القوية. وبالتالي، يمكن لمنظمة صغيرة تفتقر إلى موارد الحوسبة استخدام هذه الخدمة مقابل رسوم تُحسب بناءً على وقت الحوسبة المستخدم.

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

نقترح تطوير خادم GRC.

8.5.2.2. الواجهة المرئية

ستكون الواجهة المرئية على النحو التالي:

Image

تعرض الواجهة قائمتين من الخوادم:

  • على اليسار، قائمة الخوادم غير النشطة، والتي تكون بالتالي متاحة لإجراء العمليات الحسابية
  • على اليمين، قائمة الخوادم المشغولة بحسابات أحد العملاء.

هيكل القائمة كما يلي:

القائمة الرئيسية
القائمة الفرعية
الدور
الخدمة
بدء
بدء تشغيل خدمة TCP على المنفذ 864
 
إيقاف
إيقاف الخدمة
 
خروج
الخروج من التطبيق
المؤلف
 
معلومات حقوق النشر

هيكل عناصر التحكم في النموذج كما يلي:

الاسم
النوع
الدور
listLibres
JList
قائمة الخوادم المتاحة
listBusy
JList
قائمة الخوادم المشغولة

8.5.2.3. كيف يعمل التطبيق

تحميل التطبيق

عند تحميل التطبيق، يتم ملء قائمة listLibres بقائمة أسماء خوادم الحوسبة التي تديرها GRC. يتم تعريف هذه الخوادم في ملف Servers يتم تمريره كمعلمة. يحتوي هذا الملف على قائمة بأسماء الخوادم، اسم واحد في كل سطر، وبالتالي يتم استخدامه لملء قائمة listLibres. يتم تمكين قائمة Start (بدء)؛ ويتم تعطيل قائمة Stop (إيقاف).

خيار الخدمة/البدء

يؤدي هذا الخيار

  • يبدأ خدمة الاستماع على المنفذ 864 للجهاز
  • يعطل قائمة "ابدأ"
  • يقوم بتمكين قائمة الإيقاف
خيار التشغيل/الإيقاف

يوقف هذا الخيار الخدمة:

  • يتم مسح قائمة الخوادم المشغولة
  • يتم ملء قائمة الخوادم المتاحة بمحتويات ملف الخوادم
  • يتم تمكين قائمة "ابدأ"
  • يتم تعطيل قائمة "إيقاف"
خيار الخدمة/الخروج

يتم إنهاء التطبيق.

الاتصال بين العميل والخادم

يتم إجراء الحوار بين العميل والخادم عبر تبادل أسطر نصية تنتهي بتسلسل RCLF. يتعرف خادم GRC على أمرين: getserver و endservice. نوضح دور هذين الأمرين بالتفصيل:

  • 1-getserver

يسأل العميل عما إذا كان خادم الحوسبة متاحًا له.

ثم يأخذ خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> أول خادم موجود في قائمة الخوادم المتاحة لديه ويعيد اسمه إلى العميل بالصيغة التالية:
        100-nom du serveur
بالإضافة إلى ذلك، فإنه ينقل الخادم المخصص للعميل إلى قائمة الخوادم المشغولة بالصيغة التالية:
        serveur (IP du client)
كما هو موضح في المثال التالي، حيث يكون الخادم *<bdi dir="ltr" class="odt-ltr-term">calcul1.istia.univ-angers.fr</bdi>* مشغولاً بخدمة العميل الذي يحمل عنوان <bdi dir="ltr" class="odt-ltr-term">IP</bdi> *193.52.43.5*:

Image

لا يمكن للعميل إرسال أمر **<bdi dir="ltr" class="odt-ltr-term">getserver</bdi>** إذا كان قد تم تخصيص خادم حوسبة له بالفعل. لذلك، قبل الرد على العميل، يتحقق خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> من أن عنوان <bdi dir="ltr" class="odt-ltr-term">IP</bdi> الخاص بالعميل غير موجود بالفعل ضمن العناوين المسجلة في قائمة الخوادم المشغولة. إذا كان الأمر كذلك، يرد خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> بما يلي:
        501-Vous avez actuellement une demande en cours
أخيرًا، هناك حالة لا يتوفر فيها أي خادم حوسبة: قائمة الخوادم المتاحة فارغة. في هذه الحالة، يرد خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> بما يلي:
        502- Il n’y a aucun serveur de calcul disponible
في جميع الحالات، بعد الرد على العميل، يقوم خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> بإغلاق الاتصال مع العميل حتى يتمكن من خدمة عملاء آخرين.
  • 2-finservice
يشير العميل إلى أنه لم يعد بحاجة إلى خادم الحوسبة الذي كان يستخدمه.

يقوم خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> أولاً بالتحقق من أن العميل هو بالفعل أحد العملاء الذين كان يخدمهم. وللقيام بذلك، يتحقق من ما إذا كان عنوان <bdi dir="ltr" class="odt-ltr-term">IP</bdi> الخاص بالعميل موجوداً ضمن العناوين المسجلة في قائمة الخوادم المشغولة. وإذا لم يكن الأمر كذلك، يرد خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> بما يلي:
        503-Aucun serveur ne vous a été attribué
إذا تم التعرف على العميل، يرد خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> بما يلي:
        101-Fin de service acceptée
وينقل خادم الحوسبة المخصص لهذا العميل إلى قائمة الخوادم المتاحة. للعودة إلى المثال السابق، إذا أرسل العميل أمر **إنهاء** الخدمة، يصبح عرض خادم <bdi dir="ltr" class="odt-ltr-term">GRC</bdi> كما يلي:

Image

بعد إرسال الرد، بغض النظر عن محتواه، يقوم خادم GRC بإغلاق الاتصال.

8.5.2.4. المهمة

اكتب التطبيق كبرنامج مستقل يمكن اختباره، على سبيل المثال، باستخدام عميل telnet أو باستخدام عميل TCP العام من التمرين السابق.

8.5.3. التمرين 3 - عميل SMTP

8.5.3.1. مقدمة

هنا، نريد إنشاء عميل لخدمة SMTP (بروتوكول نقل البريد البسيط)، الذي يسمح لك بإرسال البريد الإلكتروني. في أنظمة Unix أو Windows، يعد برنامج telnet عميلاً يعمل مع بروتوكول TCP. ويمكنه "التواصل" مع أي خدمة TCP تقبل الأوامر النصية التي تنتهي بتسلسل RCLF، أي الأحرف ASCII 13 و10. وفيما يلي مثال على محادثة مع خدمة SMTP لإرسال البريد الإلكتروني:


$ telnet istia.univ-angers.fr 25        // appel du service smtp

// الرد الوارد من خادم SMTP


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// تعليقات --------------

يمكن لبرنامج telnet الاتصال بأي خدمة باستخدام الصيغة

***<bdi dir="ltr" class="odt-ltr-term">telnet</bdi>*** **<bdi dir="ltr" class="odt-ltr-term">machine</bdi>\_service <bdi dir="ltr" class="odt-ltr-term">port</bdi>\_service**

تستخدم تبادلات العميل/الخادم أسطر نصية تنتهي بتسلسل RCLF.

تكون الردود من خدمة SMTP بالشكل التالي:

**رقم الرسالة أو**

**رقم الرسالة**

قد يرسل خادم SMTP عدة أسطر من الردود. يُشار إلى السطر الأخير من الرد برقم متبوع بمسافة، بينما يُشار إلى الأسطر السابقة من الرد برقم متبوع بشرطة -.

يشير الرقم الأكبر من أو يساوي 500 إلى رسالة خطأ.

// نهاية التعليقات

help                        // commande émise au clavier

// استجابة من خادم SMTP

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info

mail from: serge.tahe@istia.univ-angers.fr    // nouvelle commande émise au clavier

// تعليقات ---------

يحتوي الأمر mail على الصيغة التالية:

**<bdi dir="ltr" class="odt-ltr-term">mail from:</bdi> عنوان البريد الإلكتروني لمرسل الرسالة**

// نهاية التعليقات

// الرد من خادم SMTP

250 serge.tahe@istia.univ-angers.fr... Sender ok

// تعليقات

لا يتحقق خادم SMTP من صحة عنوان المرسل: فهو يقبله كما هو

// نهاية التعليقات


rcpt to: user1@istia.univ-angers.fr        // nouvelle commande émise au clavier

// تعليقات ---------

يتميز الأمر rcpt بالصيغة التالية:

**<bdi dir="ltr" class="odt-ltr-term">rcpt to:</bdi> عنوان البريد الإلكتروني لمستلم الرسالة**

إذا كان عنوان البريد الإلكتروني موجودًا على الجهاز الذي يعمل عليه خادم SMTP، فإنه يتحقق من وجوده؛ وإلا، فإنه لا يقوم بأي تحقق. إذا تم إجراء التحقق وتم الكشف عن خطأ، فسيتم الإبلاغ عنه برقم >= 500.

يمكنك إصدار عدد غير محدود من أوامر rcpt: وهذا يتيح لك إرسال رسالة إلى عدة أشخاص.

// نهاية التعليقات

// استجابة خادم SMTP

250 user1@istia.univ-angers.fr... Recipient ok
data                        // nouvelle commande émise au clavier

// تعليقات ---------

يحتوي الأمر data على الصيغة التالية:

**<bdi dir="ltr" class="odt-ltr-term">data</bdi>**

    **السطر 1**

    **السطر 2**

    **...**

    **.**

ويلي ذلك أسطر النص التي تشكل الرسالة، والتي يجب أن تنتهي بسطر يحتوي فقط على حرف "نقطة".

ثم يتم إرسال الرسالة إلى المستلم المحدد بواسطة الأمر rcpt.

// نهاية التعليقات

// استجابة من خادم SMTP

354 Enter mail, end with "." on a line by itself

// نص الرسالة المكتوب على لوحة المفاتيح


subject: essai smtp
 
essai smtp a partir de telnet
.

// تعليقات

في الأسطر النصية لأمر data، يمكنك تضمين سطر subject: لتحديد موضوع البريد الإلكتروني. يجب أن يتبع هذا السطر سطر فارغ.

// الرد من خادم SMTP

250 HAA11627 Message accepted for delivery
quit                            // nouvelle commande émise au clavier

// تعليقات

**يغلق الأمر <bdi dir="ltr" class="odt-ltr-term">quit</bdi> الاتصال بخدمة** ***<bdi dir="ltr" class="odt-ltr-term">SMTP</bdi>***

// نهاية التعليقات

// استجابة من خادم SMTP

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. الواجهة المرئية

نقترح إنشاء برنامج بالواجهة المرئية التالية:

Image

تتمتع عناصر التحكم بالوظائف التالية:

الرقم
النوع
الدور
1
JTextField
قائمة بعناوين البريد الإلكتروني مفصولة بفواصل
2
JTextField
نص موضوع الرسالة
3
JTextField
قائمة عناوين البريد الإلكتروني مفصولة بفواصل
4
JTextField
قائمة عناوين البريد الإلكتروني مفصولة بفواصل
5
JTextArea
نص الرسالة
6
JList
قائمة المتابعة
7
JList
قائمة الحوار
8
JButton
زر "إلغاء" غير ظاهر، يظهر عندما يطلب العميل الاتصال بخادم SMTP. يسمح للمستخدم بإلغاء هذا الطلب إذا لم يستجب الخادم.

8.5.3.3. القوائم

هيكل قائمة التطبيق كما يلي:

القائمة الرئيسية
القائمة الفرعية
الدور
البريد
  
 
إرسال
إرسال الرسالة من عنصر التحكم 5
 
خروج
الخروج من التطبيق
خيارات
  
 
إخفاء التتبع
إخفاء عنصر التحكم 6
 
مسح قائمة المراقبة
مسح قائمة المراقبة 6
 
إخفاء مربع الحوار
إخفاء قائمة الحوار 7
 
مسح قائمة الحوار
مسح قائمة الحوار 7
 
تكوين
يسمح للمستخدم بتحديد
- عنوان خادم SMTP الذي يستخدمه البرنامج
- عنوان بريده الإلكتروني
 
حفظ...
يحفظ التكوين السابق في ملف .ini
المؤلف
 
معلومات حقوق النشر

8.5.3.4. كيفية عمل التطبيق

قائمة الخيارات/التكوين

تعرض هذه القائمة النافذة التالية:

Image

يجب ملء كلا الحقلين حتى يصبح زر "موافق" نشطًا. يجب تخزين كلتا المعلومتين في متغيرات عامة حتى تكونا متاحتين للوحدات النمطية الأخرى.

قائمة البريد/الإرسال

لا يتوفر هذا الخيار إلا في حالة استيفاء الشروط التالية:

  • تم الانتهاء من التكوين
  • وجود رسالة لإرسالها
  • وجود موضوع
  • وجود مستلم واحد على الأقل في الحقول 1 و3 و4

إذا تم استيفاء هذه الشروط، فإن تسلسل الأحداث يكون كما يلي:

  • يتم تعيين النموذج إلى حالة يتم فيها تعطيل جميع الإجراءات التي قد تتداخل مع الحوار بين العميل والخادم
  • يتم إنشاء اتصال على المنفذ 25 للخادم المحدد في التكوين
  • ثم يتواصل العميل مع خادم SMTP وفقًا للبروتوكول الموصوف أعلاه
  • يستخدم حقل "Mail From" عنوان البريد الإلكتروني للمرسل المحدد في التكوين
  • يُستخدم الأمر "rcpt to:" لكل عنوان بريد إلكتروني موجود في الحقول 1 و3 و4
  • في الأسطر المرسلة بعد أمر البيانات، سيظهر النص التالي:
    • سطر "Subject": نص الموضوع من عنصر التحكم 2
    • a سطر Cc:: عناوين من الاختبار 3
    • a سطر "Bcc": عناوين من الاختبار 4
    • نص الرسالة للتحكم 5
    • نقطة النهاية
زر «إلغاء»

يظهر هذا الزر الموجود أسفل النموذج فقط عندما يحاول العميل الاتصال بخادم SMTP. قد يفشل هذا الاتصال لأن خادم SMTP لا يستجيب أو يستجيب بشكل غير صحيح. ويتيح زر "إلغاء" للمستخدم عندئذٍ إلغاء طلب الاتصال.

قوائم التتبع

تتتبع القائمة (6) عملية الاتصال. وهي تشير إلى اللحظات الرئيسية في عملية الاتصال:

  • إنشاء الاتصال من قبل العميل
  • إغلاقه من قبل الخادم أو العميل
  • جميع أخطاء الاتصال

تتتبع القائمة (7) الحوار SMTP الذي تم إقامته بين العميل والخادم.

ترتبط هاتان القائمتان بخيارات القائمة:

إخفاء التتبع
يخفي قائمة التتبع 6 والتسمية الموجودة فوقها. إذا كان الارتفاع الذي تشغله هذين العنصرين هو H، يتم تحريك جميع العناصر الموجودة أسفلهما لأعلى بمقدار الارتفاع H، ويتم تقليل الحجم الإجمالي للنموذج بمقدار H. بالإضافة إلى ذلك، يخفي خيار إخفاء التتبع خيار مسح التتبع الموجود أسفله.
مسح التتبع
يمسح قائمة التتبع 6
إخفاء مربع الحوار
يخفي قائمة الحوار 7، والتسمية الموجودة فوقها، وخيار القائمة "مسح الحوار" الموجود أسفلها. كما هو الحال مع "إخفاء التتبع"، يتم إعادة حساب موضع عناصر التحكم الموجودة أسفلها (ربما زر "إلغاء")، ويتم تقليل حجم النافذة.
مسح الحوار
مسح قائمة الحوار 7
خيار المؤلف

تفتح هذه القائمة نافذة تسمى حقوق النشر:

Image

معالجة الأخطاء

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

8.5.3.5. إدارة ملف التكوين

من المستحسن ألا يضطر المستخدم إلى إعادة تكوين البرنامج في كل مرة يستخدمه. لتحقيق ذلك، إذا تم تحديد خيار "خيارات/حفظ التكوين عند الخروج"، فإن إغلاق البرنامج يحفظ معلومتين تم تعيينهما عبر خيار "خيارات/تكوين"، بالإضافة إلى حالة قائمتي التتبع، في ملف sendmail.ini الموجود في نفس الدليل الذي يوجد فيه ملف .exe الخاص بالبرنامج. هذا الملف له التنسيق التالي:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

تحتوي سطور SmtpServer و ReplyAddress على معلومتين تم إدخالهما عبر قائمة Options/Configure. تشير سطور Tracking و Dialogue إلى حالة قوائم التتبع والحوار: 1 (موجود)، 0 (غير موجود).

عند تحميل البرنامج، يتم قراءة ملف sendmail.ini إذا كان موجودًا، ويتم تكوين النموذج وفقًا لذلك. إذا لم يكن ملف sendmail.ini موجودًا، يتصرف البرنامج كما لو كان موجودًا:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

إذا كان ملف sendmail.ini موجودًا ولكنه غير مكتمل (توجد أسطر مفقودة)، يتم استبدال السطر المفقود بالسطر المقابل أعلاه. وبالتالي، إذا كان السطر Suivi=... مفقودًا، فإننا نتعامل معه كما لو كان لدينا Suivi=1.

جميع الأسطر التي لا تتطابق مع النمط:

    mot clé= valeur

يتم تجاهلها، وكذلك تلك التي تكون فيها الكلمة الرئيسية غير صالحة. يمكن أن تكون الكلمة الرئيسية بأحرف كبيرة أو صغيرة: لا يهم.

في قائمة "خيارات/تكوين"، يتم عرض قيمتي SmtpServer و ReplyAddress الحاليتين. يمكن للمستخدم تعديلهما إذا رغب في ذلك.

8.5.3.6. المهام المطلوب تنفيذها

أكمل المهمة الموضحة أعلاه. يوصى بالتعامل مع إدارة ملفات التكوين في النهاية.

8.5.4. التمرين 4 - عميل POPPASS

8.5.4.1. مقدمة

نقترح إنشاء عميل TCP قادر على التواصل مع خادم POPPASSD الذي يعمل على المنفذ 106. تتيح لك هذه الخدمة تغيير كلمة مرورك على جهاز UNIX. بروتوكول الاتصال بين العميل والخادم هو كما يلي:

1 - يتم الاتصال عبر تبادل الرسائل التي تنتهي بتسلسل RCLF

2 - يرسل العميل الأوامر إلى الخادم

- يرد الخادم برسائل تبدأ برقم مكون من 3 أرقام: <bdi dir="ltr" class="odt-ltr-term">XXX.</bdi> إذا كان <bdi dir="ltr" class="odt-ltr-term">XXX</bdi>=200، فهذا يعني أن الأمر قد تم تنفيذه بنجاح؛ وإلا، فقد حدث خطأ.

3 - تسلسل التبادلات كما يلي:

A    - le client se connecte
- يرد الخادم برسالة ترحيب
B    - le client envoie USER login
- يستجيب الخادم بطلب كلمة المرور إذا تم قبول اسم المستخدم؛ وإلا، فإنه يعرض رسالة خطأ
C    - le client envoie PASS mot_de_passe
- يستجيب الخادم بطلب كلمة المرور الجديدة إذا تم قبول كلمة المرور؛ وإلا، فإنه يعرض رسالة خطأ
D    - le client envoie NEWPASS nouveau_mot_de_passe
- يستجيب الخادم بتأكيد قبول كلمة المرور الجديدة؛ وإلا، فإنه يعرض رسالة خطأ
E    - le client envoie la commande QUIT
- يرسل الخادم رسالة إنهاء ويغلق الاتصال

8.5.4.2. نموذج العميل

Image

فيما يلي معنى العناصر المختلفة:

رقم
الاسم
النوع
الدور
1
txtRemoteHost
JTextField
اسم الخادم
2
txtLogin
JTextField
تسجيل دخول المستخدم
3
txtPassword
JTextField
كلمة مرور المستخدم
4
txtNewPassword
JTextField
كلمة مرور المستخدم الجديدة
5
txtConfirmation
JTextField
تأكيد كلمة المرور الجديدة
6
lstTracking
JList
رسائل تتبع الاتصال
7
lstDialogue
JList
رسائل حوار العميل/الخادم
10
cmdCancel
JButton
غير معروض - زر يظهر عندما يكون الاتصال بالخادم قيد التقدم. يسمح لك بإيقافه.

8.5.4.3. القوائم

العنوان
اسم عنصر التحكم
الدور
اتصال
loginmenu
 
اتصال
mnuconnect
يبدأ الاتصال بالخادم
خروج
mnuQuitter
يخرج من التطبيق
الرسائل
mnuMessages
 
مسح السجل
mnuClearTracking
يُفرغ قائمة lstSuivi
مسح الحوار
mnuClearDialog
مسح قائمة lstDialogue
المؤلف
mnuAuthor
يعرض مربع حقوق النشر

8.5.4.4. كيفية عمل التطبيق

تهيئة التطبيق

عند تحميل الصفحة الرئيسية للتطبيق، تحدث الإجراءات التالية:

  • يتم توسيط الصفحة على الشاشة
  • تكون خيارات قائمة "تسجيل الدخول/تسجيل الخروج" و"المؤلف" هي الخيارات النشطة فقط
  • يتم إخفاء زر "إلغاء"
  • تكون قائمتا LstSuivi و LstDialogue فارغتين
قائمة "تسجيل الدخول/الاتصال"

هذا الخيار متاح فقط عند ملء الحقول من 1 إلى 5. يؤدي النقر على هذا الخيار إلى تنفيذ الإجراءات التالية:

  • يتم تشغيل مؤشر ترابط لإنشاء الاتصال بالخادم
  • يظهر زر "إلغاء" للسماح للمستخدم بقطع الاتصال الجاري
  • يتم تعطيل جميع خيارات القائمة باستثناء "خروج" و"المؤلف"

ثم تكون تسلسل الأحداث كما يلي:

  1. نقر المستخدم على زر "إلغاء": يتم إيقاف مؤشر ترابط الاتصال، وتعود القائمة إلى حالتها الأولية. يشير السجل إلى أن المستخدم أغلق الاتصال.
  2. يتم قبول طلب الاتصال من قبل الخادم. ثم نبدأ الحوار مع الخادم لتغيير كلمة المرور. يتم تسجيل التبادلات في هذا الحوار في قائمة LstDialogue. بمجرد اكتمال الحوار، يتم إغلاق الاتصال بالخادم وتعود قائمة النموذج إلى حالتها الأولية.
  3. طالما أن الحوار نشط، يظل زر "إلغاء" مرئيًا للسماح للمستخدم بإغلاق الاتصال إذا رغب في ذلك.
  4. في حالة حدوث أي خطأ أثناء الاتصال، يتم إغلاق الاتصال وعرض سبب الخطأ في قائمة التتبع LstSuivi.
قائمة الاتصال/الخروج

يؤدي هذا الخيار إلى إغلاق أي اتصال نشط بالخادم والخروج من التطبيق.

قائمتا ClearTracking و ClearDialog

مسح قوائم LstSuivi و LstDialogue، على التوالي. يتم تعطيل هذين الخيارين عندما تكون القوائم المقابلة فارغة.

زر "إلغاء"

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

قوائم التتبع

تتتبع قائمة LstSuivi (5) الاتصال. وهي تشير إلى اللحظات الرئيسية في الاتصال:

  • إنشاؤه من قبل العميل

  • إغلاقه من قبل الخادم أو العميل

  • أي أخطاء قد تحدث أثناء نشاط الاتصال

تتتبع قائمة LstDialogue (6) الحوار بين العميل والخادم.

خيار المؤلف

تفتح هذه القائمة نافذة تسمى نافذة حقوق النشر:

Image

معالجة الأخطاء

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

8.5.4.5. المهمة

قم بتنفيذ العمل الموصوف أعلاه كتطبيق مستقل ثم كبرنامج صغير.