11. برمجة الإنترنت
11.1. Généralités
11.1.1. بروتوكولات الإنترنت
نقدم هنا مقدمة لبروتوكولات الاتصال على الإنترنت، والتي تُعرف أيضًا باسم مجموعة بروتوكولات TCP/IP (بروتوكول التحكم في النقل / بروتوكول الإنترنت)، نسبةً إلى اسمي البروتوكولين الرئيسيين. قد يكون من المفيد أن يكون لدى القارئ فهم شامل لكيفية عمل الشبكات، ولا سيما بروتوكولات TCP/IP قبل الشروع في بناء التطبيقات الموزعة. النص التالي هو ترجمة جزئية لنص موجود في وثيقة "Lan Workplace for Dos - Administrator's Guide" من NOVELL، وهي وثيقة تعود إلى أوائل التسعينيات.
يأتي المفهوم العام لإنشاء شبكة من أجهزة الكمبيوتر غير المتجانسة من الأبحاث التي أجرتها وكالة DARPA (وكالة مشاريع الأبحاث المتقدمة للدفاع) في الولايات المتحدة. طورت الوكالة مجموعة البروتوكولات المعروفة باسم TCP/IP التي تسمح للأجهزة غير المتجانسة بالتواصل فيما بينها. تم اختبار هذه البروتوكولات على شبكة تسمى ARPAnet، وهي الشبكة التي أصبحت لاحقًا شبكة INTERNET. تحدد بروتوكولات TCP/IP تنسيقات وقواعد الإرسال والاستقبال المستقلة عن تنظيم الشبكات والمعدات المستخدمة.
الشبكة التي صممها DARPA وتديرها بروتوكولات TCP/IP هي شبكة تبديل حزم. تقوم هذه الشبكة بنقل المعلومات عبر الشبكة على شكل أجزاء صغيرة تسمى الحزم. وبالتالي، إذا قام جهاز كمبيوتر بإرسال ملف كبير، فسيتم تقسيمه إلى أجزاء صغيرة يتم إرسالها عبر الشبكة ليتم إعادة تجميعها عند الوصول إلى الوجهة. يحدد TCP/IP تنسيق هذه الحزم، أي:
- منشأ الحزمة
- الوجهة
- الطول
- النوع
11.1.2. الطراز OSI
تتبع بروتوكولات TCP/IP تقريبًا نموذج الشبكة المفتوحة المسمى OSI (نموذج مرجعي لترابط الأنظمة المفتوحة) الذي حددته المنظمة الدولية للمعايير (ISO). يصف هذا النموذج شبكة مثالية حيث يمكن تمثيل الاتصال بين الأجهزة بنموذج مكون من سبع طبقات:
![]() |
تتلقى كل طبقة خدمات من الطبقة السفلية وتقدم خدماتها إلى الطبقة العليا. لنفترض أن تطبيقين موجودين على جهازين مختلفين A و B يرغبان في التواصل: فهما يفعلان ذلك على مستوى الطبقة Application. ولا يحتاجان إلى معرفة كل تفاصيل عمل الشبكة: فكل تطبيق يمرر المعلومات التي يرغب في إرسالها إلى الطبقة التي تليه: الطبقة Présentation. وبالتالي، لا يحتاج التطبيق سوى إلى معرفة قواعد التفاعل مع الطبقة Présentation.
وبمجرد وصول المعلومات إلى الطبقة Présentation، تنتقل وفقًا لقواعد أخرى إلى الطبقة Session وهكذا دواليك، حتى تصل المعلومات إلى الوسيط المادي وتُنقل فعليًا إلى الجهاز المقصود. وهناك، ستخضع للمعالجة العكسية لتلك التي خضعت لها على الجهاز المرسل.
في كل طبقة، تقوم العملية المرسلة المكلفة بإرسال المعلومات بإرسالها إلى عملية مستقبلة على الجهاز الآخر الذي ينتمي إلى نفس الطبقة. وتقوم بذلك وفقًا لقواعد معينة تسمى بروتوكول الطبقة. وبالتالي، يكون مخطط الاتصال النهائي كما يلي:
![]() |
دور الطبقات المختلفة هو كما يلي:
تضمن نقل البتات عبر وسيط مادي. توجد في هذه الطبقة أجهزة طرفية لمعالجة البيانات (E.T.T.D.) مثل المحطة الطرفية أو الكمبيوتر، بالإضافة إلى أجهزة إنهاء دوائر البيانات (E.T.C.D.) مثل المُعدِّل/المُفكِّك، والمُضاعِف، والمُركِّز. النقاط المهمة في هذا المستوى هي:
| |
يخفي الخصائص الفيزيائية للطبقة الفيزيائية. يكتشف أخطاء الإرسال ويصححها. | |
يدير المسار الذي يجب أن تتبعه المعلومات المرسلة عبر الشبكة. ويُطلق على ذلك اسم routage: تحديد المسار الذي يجب أن تتبعه المعلومات حتى تصل إلى مستلمها. | |
يسمح بالاتصال بين تطبيقين في حين أن الطبقات السابقة كانت تسمح فقط بالاتصال بين الأجهزة. قد تكون إحدى الخدمات التي توفرها هذه الطبقة هي التعدد: يمكن لطبقة النقل استخدام نفس اتصال الشبكة (من جهاز إلى جهاز) لنقل المعلومات الخاصة بعدة تطبيقات. | |
نجد في هذه الطبقة خدمات تسمح لتطبيق ما بفتح جلسة عمل والحفاظ عليها على جهاز بعيد. | |
وتهدف هذه الطبقة إلى توحيد عرض البيانات على الأجهزة المختلفة. وبالتالي، فإن البيانات الواردة من جهاز A سيتم "تغليفها" بواسطة طبقة Présentation الخاصة بجهاز A، وفقًا لتنسيق قياسي قبل إرسالها عبر الشبكة. وعند وصولها إلى الطبقة Présentation للجهاز المستلم B الذي سيتعرف عليها بفضل تنسيقها القياسي، سيتم تزيينها بطريقة أخرى حتى يتعرف عليها تطبيق الجهاز B. | |
في هذه المرحلة، نجد التطبيقات التي عادةً ما تكون قريبة من المستخدم مثل البريد الإلكتروني أو نقل الملفات. |
11.1.3. النموذج TCP/IP
النموذج OSI هو نموذج مثالي لم يتم تنفيذه من قبل. تقترب مجموعة البروتوكولات TCP/IP منه بالشكل التالي:
![]() |
الطبقة المادية
في الشبكة المحلية، نجد عادةً تقنية إيثرنت أو توكن رينغ. ونحن نقدم هنا تقنية إيثرنت فقط.
إيثرنت
هذا هو الاسم الذي أُطلق على تقنية الشبكات المحلية ذات التبديل الحزمي التي اخترعتها شركة Xerox في أوائل السبعينيات وتم توحيدها من قبل Xerox و Intel و Digital Equipment في عام 1978. تتكون الشبكة ماديًا من كابل متحد المحور يبلغ قطره حوالي 1.27 سم وطوله 500 متر كحد أقصى. ويمكن تمديدها باستخدام répéteurs، بحيث لا يمكن أن يفصل بين جهازين أكثر من مكررين. الكابل سلبي: جميع العناصر النشطة موجودة على الأجهزة المتصلة بالكابل. يتم توصيل كل جهاز بالكابل عن طريق بطاقة وصول إلى الشبكة تتضمن:
- جهاز إرسال (transceiver) يكتشف وجود إشارات على الكابل ويحول الإشارات التناظرية إلى إشارات رقمية والعكس.
- مقرن يستقبل الإشارات الرقمية من جهاز الإرسال وينقلها إلى الكمبيوتر للمعالجة أو العكس.
الخصائص الرئيسية لتقنية إيثرنت هي كما يلي:
- سعة 10 ميجابت/ثانية.
- توبولوجيا الحافلة: جميع الأجهزة متصلة بنفس الكابل
![]() |
- شبكة البث - تقوم الجهاز المرسل بنقل المعلومات عبر الكابل مع عنوان الجهاز المستلم. ثم تستقبل جميع الأجهزة المتصلة هذه المعلومات، ولا يحتفظ بها سوى الجهاز المخصص لها.
- طريقة الوصول هي كما يلي: يقوم جهاز الإرسال الراغب في الإرسال بالاستماع إلى الكابل — فيكتشف عندئذٍ وجود موجة حاملة أم لا، حيث يعني وجودها أن عملية إرسال جارية. هذه هي تقنية CSMA (الوصول المتعدد باستشعار الموجة الحاملة). في حالة عدم وجود موجة حاملة، يمكن لجهاز الإرسال أن يقرر الإرسال بدوره. وقد يتخذ عدة أجهزة هذا القرار. تتداخل الإشارات المرسلة: ويُقال إن هناك تضاربًا. يكتشف جهاز الإرسال هذه الحالة: ففي الوقت الذي يبث فيه عبر الكابل، يستمع إلى ما يمر فعليًا عبره. وإذا اكتشف أن المعلومات التي تمر عبر الكابل ليست هي التي أرسلها، يستنتج أن هناك تصادمًا ويتوقف عن الإرسال. وستفعل أجهزة الإرسال الأخرى التي كانت تبث الشيء نفسه. سيستأنف كل جهاز إرساله بعد فترة عشوائية تعتمد على كل جهاز إرسال. تسمى هذه التقنية CD (Collision Detect). وبالتالي، تسمى طريقة الوصول CSMA/CD.
- عنونة على 48 بت. لكل جهاز عنوان، يُسمى هنا العنوان الفعلي، وهو مسجل على البطاقة التي تربطه بالكابل. يُسمى هذا العنوان، العنوان Ethernet للجهاز.
طبقة الشبكة
نجد في هذه الطبقة البروتوكولات IP و ICMP و ARP و RARP.
تنقل الحزم بين عقدتين في الشبكة | |
يقوم ICMP بالاتصال بين برنامج بروتوكول IP لجهاز ما وبرنامج جهاز آخر. وهو بالتالي بروتوكول لتبادل الرسائل داخل بروتوكول IP نفسه. | |
يقوم بمطابقة عنوان الإنترنت الخاص بالجهاز --> العنوان الفعلي للجهاز | |
يقوم بمطابقة العنوان الفعلي للجهاز --> عنوان الإنترنت للجهاز |
طبقات النقل/الجلسة
في هذه الطبقة، نجد البروتوكولات التالية:
يضمن تسليم المعلومات بشكل موثوق بين عميلين | |
يضمن تسليم غير موثوق للمعلومات بين عميلين |
طبقات التطبيق/العرض/الجلسة
توجد هنا بروتوكولات متنوعة:
محاكي طرفي يسمح لجهاز A بالاتصال بجهاز B كطرفي | |
يسمح بنقل الملفات | |
يسمح بنقل الملفات | |
يسمح بتبادل الرسائل بين مستخدمي الشبكة | |
يحول اسم الجهاز إلى عنوان الإنترنت الخاص بالجهاز | |
تم إنشاؤه بواسطة sun MicroSystems، ويحدد تمثيلًا قياسيًا للبيانات، مستقلًا عن الأجهزة | |
تم تعريفه أيضًا بواسطة Sun، وهو بروتوكول اتصال بين التطبيقات البعيدة، مستقل عن طبقة النقل. هذا البروتوكول مهم: فهو يعفي المبرمج من معرفة تفاصيل طبقة النقل ويجعل التطبيقات قابلة للنقل. يعتمد هذا البروتوكول على بروتوكول XDR | |
الذي حددته شركة Sun أيضًا، ويتيح هذا البروتوكول لجهاز ما "رؤية" نظام الملفات الخاص بجهاز آخر. وهو يعتمد على البروتوكول RPC السابق |
11.1.4. كيفية عمل بروتوكولات الإنترنت
تستخدم التطبيقات المطورة في بيئة TCP/IP عادةً العديد من بروتوكولات هذه البيئة. يتواصل برنامج التطبيق مع الطبقة العليا من البروتوكولات. تنقل هذه الطبقة المعلومات إلى الطبقة السفلية وهكذا دواليك حتى تصل إلى الوسيط المادي. وهناك، يتم نقل المعلومات فعليًا إلى الجهاز المستلم حيث تعبر الطبقات نفسها مرة أخرى، ولكن في الاتجاه المعاكس هذه المرة، حتى تصل إلى التطبيق المستلم للمعلومات المرسلة. يوضح الرسم البياني التالي مسار المعلومات:
![]() |
لنأخذ مثالاً: التطبيق FTP، المحدد على مستوى الطبقة Application والذي يسمح بنقل الملفات بين الأجهزة.
- يُصدر التطبيق سلسلة من البايتات ليتم نقلها إلى الطبقة transport.
- يقوم الطبقة transport بتقسيم سلسلة البايتات هذه إلى segments و TCP، ويضيف رقم كل مقطع في بداية كل مقطع. يتم تمرير المقاطع إلى طبقة الشبكة التي يحكمها بروتوكول IP.
- تقوم طبقة IP بإنشاء حزمة تغلف المقطع TCP المستلم. في مقدمة هذه الحزمة، تضع عناوين الإنترنت لأجهزة المصدر والوجهة. كما تحدد العنوان الفعلي لجهاز الوجهة. يتم تمرير كل ذلك إلى طبقة ربط البيانات والربط الفعلي، أي إلى بطاقة الشبكة التي تربط الجهاز بالشبكة الفعلية.
- وهناك، يتم تغليف الحزمة IP بدورها في إطار مادي وإرسالها إلى المستلم عبر الكابل.
- على الجهاز المستلم، تقوم طبقة ربط البيانات والربط المادي بالعكس: فهي تفك تغليف الحزمة IP من الإطار المادي وتمررها إلى طبقة IP.
- تتحقق الطبقة IP من صحة الحزمة: فهي تحسب مجموعًا يعتمد على البتات المستلمة (checksum)، وهو المجموع الذي يجب أن تجده في رأس الحزمة. وإذا لم يكن الأمر كذلك، يتم رفض الحزمة.
- إذا تم إعلان صحة الحزمة، تقوم الطبقة IP بفك تغليف المقطع TCP الموجود فيها وتمريره إلى الطبقة transport.
- تقوم الطبقة transport، وهي الطبقة TCP في مثالنا، بفحص رقم المقطع من أجل استعادة الترتيب الصحيح للمقاطع.
- كما تحسب أيضًا مجموعًا للتحقق من صحة المقطع TCP. إذا تم العثور عليه صحيحًا، ترسل الطبقة TCP إشعارًا بالاستلام إلى الجهاز المصدر، وإلا يتم رفض المقطع TCP.
- ولم يتبق للطبقة TCP سوى نقل جزء البيانات من المقطع إلى التطبيق المستلم لها في الطبقة الأعلى.
11.1.5. مشاكل العنونة في الإنترنت
يمكن أن يكون noeud في شبكة ما جهاز كمبيوتر أو طابعة ذكية أو خادم ملفات، أو أي شيء في الواقع يمكنه الاتصال باستخدام بروتوكولات TCP/IP. لكل عقدة عنوان مادي بتنسيق يعتمد على نوع الشبكة. في شبكة إيثرنت، يتم ترميز العنوان المادي على 6 بايت. عنوان شبكة X25 هو رقم مكون من 14 رقمًا.
عنوان الإنترنت الخاص بالعقدة هو عنوان منطقي: فهو مستقل عن الأجهزة والشبكة المستخدمة. وهو عنوان مكون من 4 بايتات يحدد في آن واحد شبكة محلية وعقدة في هذه الشبكة. عادةً ما يتم تمثيل عنوان الإنترنت في شكل 4 أرقام، وهي قيم البايتات الأربعة، مفصولة بنقطة. وبالتالي، فإن عنوان جهاز Lagaffe في كلية العلوم في أنجيه هو 193.49.144.1، وعنوان جهاز Liny هو 193.49.144.9. ومن ذلك نستنتج أن عنوان الإنترنت للشبكة المحلية هو 193.49.144.0. ويمكن أن تضم هذه الشبكة ما يصل إلى 254 عقدة.
ونظرًا لأن عناوين الإنترنت أو عناوين IP مستقلة عن الشبكة، يمكن لجهاز في شبكة A التواصل مع جهاز في شبكة B دون الاهتمام بنوع الشبكة التي يوجد عليها: يكفي أن يعرف عنوانه IP. يتولى بروتوكول IP لكل شبكة إجراء التحويل بين عنوان IP <--> العنوان الفعلي، في كلا الاتجاهين.
يجب أن تكون عناوين IP مختلفة جميعها. في فرنسا، يتولى INRIA تخصيص عناوين IP. في الواقع، تمنح هذه الهيئة عنوانًا لشبكتك المحلية، على سبيل المثال 193.49.144.0 لشبكة كلية العلوم في أنجيه. يمكن لمسؤول هذه الشبكة بعد ذلك تخصيص عناوين IP من 193.49.144.1 إلى 193.49.144.254 كما يشاء. عادةً ما يتم تسجيل هذا العنوان في ملف خاص بكل جهاز متصل بالشبكة.
11.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 هو كما يلي:
يتكون عنوان الشبكة من 7 بتات وعنوان العقدة من 24 بتة. وبالتالي، يمكن أن يكون هناك 127 شبكة من الفئة A، تحتوي كل منها على ما يصل إلى 224 عقدة.
الفئة B
هنا، العنوان IP: I1.I2.I3.I4 له الشكل R1.R2.N1.N2 حيث
R1.R2 هي عنوان الشبكة
N1.N2 هو عنوان جهاز في هذه الشبكة
وبشكل أكثر دقة، فإن شكل عنوان IP من الفئة B هو كما يلي:
يتكون عنوان الشبكة من 2 بايت (14 بت بالضبط) وكذلك عنوان العقدة. وبالتالي، يمكن أن يكون هناك 214 شبكة من الفئة B تحتوي كل منها على ما يصل إلى 216 عقدة.
الفئة C
في هذه الفئة، يكون العنوان IP: I1.I2.I3.I4 بالشكل R1.R2.R3.N1 حيث
R1.R2.R3 هي عنوان الشبكة
N1 هو عنوان جهاز في هذه الشبكة
وبشكل أكثر دقة، فإن شكل عنوان IP من الفئة C هو كما يلي:
![]() |
يتكون عنوان الشبكة من 3 بايت (ناقص 3 بت) وعنوان العقدة من 1 بايت. وبالتالي، يمكن أن يكون هناك 221 شبكة من الفئة C تضم ما يصل إلى 256 عقدة.
نظرًا لأن عنوان الجهاز Lagaffe التابع لكلية العلوم في أنجيه هو 193.49.144.1، نرى أن البايت الأعلى قيمة يساوي 193، أي 11000001 بالثنائي. نستنتج من ذلك أن الشبكة من الفئة C.
العناوين المحجوزة
- بعض عناوين IP هي عناوين شبكات وليست عناوين عقد في الشبكة. وهي تلك التي يكون فيها عنوان العقدة 0. وبالتالي، فإن العنوان 193.49.144.0 هو عنوان IP لشبكة كلية العلوم في أنجيه. ونتيجة لذلك، لا يمكن لأي عقدة في الشبكة أن يكون لها عنوان صفر.
- عندما لا تحتوي عنوان العقدة في عنوان IP إلا على أرقام 1، يكون لدينا عنوان بث: يشير هذا العنوان إلى جميع العقد في الشبكة.
- في شبكة من الفئة C، التي تسمح نظريًا بـ 2⁸ = 256 عقدة، إذا استبعدنا العنوانين المحظورين، فلن يتبقى سوى 254 عنوانًا مسموحًا به.
11.1.5.2. بروتوكولات التحويل بين عنوان الإنترنت <--> العنوان الفعلي
لقد رأينا أنه عند إرسال المعلومات من جهاز إلى آخر، يتم تغليفها في حزم عند عبورها الطبقة IP. وتكون هذه الحزم بالشكل التالي:
![]() |
وبالتالي، تحتوي الحزمة IP على عناوين الإنترنت للجهازين المصدر والوجهة. وعندما يتم إرسال هذه الحزمة إلى الطبقة المسؤولة عن إرسالها عبر الشبكة المادية، تُضاف إليها معلومات أخرى لتشكيل الإطار المادي الذي سيتم إرساله في النهاية عبر الشبكة. على سبيل المثال، يكون تنسيق الإطار على شبكة إيثرنت كما يلي:
![]() |
في الإطار النهائي، يوجد العنوان المادي للجهازين المرسل والمرسل إليه. كيف يتم الحصول عليهما؟
تتعرف الجهاز المرسل على عنوان IP للجهاز الذي يريد التواصل معه، ويحصل على العنوان المادي لهذا الجهاز باستخدام بروتوكول خاص يسمى ARP (بروتوكول تحليل العناوين).
- ترسل حزمة من نوع خاص تسمى حزمة ARP تحتوي على العنوان IP للجهاز الذي نبحث عن عنوانه الفعلي. كما حرصت على وضع عنوانها الخاص IP وعنوانها الفعلي فيها.
- يتم إرسال هذه الحزمة إلى جميع العقد في الشبكة.
- وتتعرف هذه العقد على الطبيعة الخاصة للحزمة. وتقوم العقدة التي تتعرف على عنوانها IP في الحزمة بالرد عن طريق إرسال عنوانها الفعلي إلى مرسل الحزمة. كيف يمكنها ذلك؟ لقد عثرت في الحزمة على عنوان IP وعنوان المرسل الفعلي.
- وبالتالي، يتلقى المرسل العنوان الفعلي الذي كان يبحث عنه. ويخزنه في الذاكرة ليتمكن من استخدامه لاحقًا في حالة إرسال حزم أخرى إلى نفس المستلم.
عادةً ما يتم تسجيل عنوان IP الخاص بجهاز ما في أحد ملفاته، وبالتالي يمكنه الرجوع إليه لمعرفة هذا العنوان. يمكن تغيير هذا العنوان: يكفي تعديل الملف. أما العنوان الفعلي، فهو مسجل في ذاكرة بطاقة الشبكة ولا يمكن تغييره.
عندما يرغب المسؤول في تنظيم شبكته بطريقة مختلفة، قد يضطر إلى تغيير عناوين IP لجميع العقد، وبالتالي تحرير ملفات التكوين المختلفة للعقد المختلفة. قد يكون هذا مملًا ومصدرًا للأخطاء إذا كان هناك عدد كبير من الأجهزة. تتمثل إحدى الطرق في عدم تخصيص عنوان IP للأجهزة: حيث يتم إدخال رمز خاص في الملف الذي من المفترض أن تجد فيه الجهاز عنوانها IP. وعندما تكتشف الجهاز أنه لا يوجد لديه عنوان IP، فإنه يطلبه وفقًا لبروتوكول يسمى RARP (بروتوكول تحليل العناوين العكسي). ثم ترسل عبر الشبكة حزمة خاصة تسمى حزمة RARP، مشابهة للحزمة ARP السابقة، تضع فيها عنوانها الفعلي. يتم إرسال هذه الحزمة إلى جميع العقد التي تتعرف عندئذٍ على حزمة RARP. أحدها، يُسمى الخادم RARP، يمتلك ملفًا يحدد التوافق بين العنوان الفعلي <--> العنوان IP لجميع العقد. ثم يرد على مرسل الحزمة RARP، بإعادة إرسال عنوانه IP إليه. وبالتالي، فإن أي مسؤول يرغب في إعادة تكوين شبكته، ما عليه سوى تعديل ملف التوافق الخاص بالخادم RARP. ويجب أن يكون لهذا الملف عادةً عنوان ثابت IP يجب أن يكون قادراً على معرفته دون الحاجة إلى استخدام بروتوكول RARP بنفسه.
11.1.6. طبقة الشبكة المعروفة باسم طبقة IP للإنترنت
يحدد بروتوكول IP (بروتوكول الإنترنت) الشكل الذي يجب أن تتخذه الحزم والطريقة التي يجب أن تدار بها عند إرسالها أو استلامها. يُطلق على هذا النوع الخاص من الحزم اسم داتاغرام IP. وقد قدمناها سابقًا:
![]() |
المهم هو أنه بالإضافة إلى البيانات المراد إرسالها، تحتوي حزمة البيانات IP على عناوين الإنترنت للجهازين المرسل والمستلم. وبذلك يعرف الجهاز المستلم من الذي يرسل له الرسالة.
وعلى عكس إطار الشبكة الذي يحدد طوله وفقًا للخصائص الفيزيائية للشبكة التي يمر عبرها، فإن طول حزمة البيانات IP يحدده البرنامج، وبالتالي سيكون هو نفسه على شبكات فيزيائية مختلفة. لقد رأينا أنه عند النزول من طبقة الشبكة إلى الطبقة المادية، تم تغليف حزمة البيانات IP في إطار مادي. وقد قدمنا مثالاً على الإطار المادي لشبكة إيثرنت:
تنتقل الإطارات المادية من عقدة إلى أخرى نحو وجهتها التي قد لا تكون على نفس الشبكة المادية التي توجد عليها الآلة المرسلة. وبالتالي، يمكن تغليف الحزمة IP تباعًا في إطارات مادية مختلفة على مستوى العقد التي تربط بين شبكتين من نوعين مختلفين. وقد تكون الحزمة IP كبيرة جدًا بحيث لا يمكن تغليفها في إطار مادي. يقوم برنامج IP في العقدة التي تحدث فيها هذه المشكلة بتقسيم الحزمة IP إلى fragments وفقًا لقواعد محددة، ثم يتم إرسال كل منها على الشبكة المادية. ولن يتم إعادة تجميعها إلا عند وصولها إلى وجهتها النهائية.
11.1.6.1. التوجيه
التوجيه هو طريقة توجيه الحزم IP إلى وجهتها. هناك طريقتان: التوجيه المباشر والتوجيه غير المباشر.
التوجيه المباشر
يشير التوجيه المباشر إلى توجيه حزمة IP مباشرة من المرسل إلى المستلم داخل نفس الشبكة:
- الجهاز المرسل لحزمة بيانات IP لديه عنوان IP الخاص بالمستلم.
- وهي تحصل على العنوان الفعلي لهذا الأخير عن طريق بروتوكول ARP أو من جداولها، إذا كان هذا العنوان قد تم الحصول عليه مسبقًا.
- وترسل الحزمة عبر الشبكة إلى هذا العنوان الفعلي.
التوجيه غير المباشر
يشير التوجيه غير المباشر إلى توجيه حزمة IP إلى وجهة تقع على شبكة أخرى غير تلك التي ينتمي إليها المرسل. في هذه الحالة، تختلف أجزاء عنوان الشبكة في عناوين IP لأجهزة المصدر والوجهة. تتعرف جهاز المصدر على هذه النقطة. ثم ترسل الحزمة إلى عقدة خاصة تسمى جهاز التوجيه (router)، وهي عقدة تربط شبكة محلية بشبكات أخرى وتجد عنوانها IP في جداولها، وهو عنوان تم الحصول عليه في البداية إما من ملف أو من ذاكرة دائمة أو حتى عبر معلومات متداولة على الشبكة.
يتم توصيل جهاز التوجيه بشبكتين وله عنوان IP داخل هاتين الشبكتين.
![]() |
في المثال أعلاه:
. الشبكة رقم 1 لها عنوان الإنترنت 193.49.144.0 والشبكة رقم 2 لها العنوان 193.49.145.0.
. داخل الشبكة رقم 1، عنوان جهاز التوجيه هو 193.49.144.6 وعنوانه داخل الشبكة رقم 2 هو 193.49.145.3.
تتمثل مهمة جهاز التوجيه في تحويل الحزمة IP التي يستقبلها، والموجودة ضمن إطار مادي نموذجي للشبكة رقم 1، إلى إطار مادي يمكن أن ينتقل عبر الشبكة رقم 2. إذا كان عنوان IP الخاص بمستلم الحزمة موجودًا في الشبكة رقم 2، فسيرسل الموجه الحزمة إليه مباشرةً، وإلا فسيرسلها إلى موجه آخر، يربط الشبكة رقم 2 بشبكة رقم 3 وهكذا دواليك.
11.1.6.2. رسائل الخطأ والتحكم
يوجد أيضًا في طبقة الشبكة، أي على نفس مستوى بروتوكول IP، بروتوكول ICMP (بروتوكول رسائل التحكم في الإنترنت). ويستخدم هذا البروتوكول لإرسال رسائل حول التشغيل الداخلي للشبكة: العقد المعطلة، الازدحام عند أحد أجهزة التوجيه، إلخ... يتم تغليف رسائل ICMP في حزم IP وإرسالها عبر الشبكة. تتخذ طبقات IP في العقد المختلفة الإجراءات المناسبة وفقًا لرسائل ICMP التي تتلقاها. وبالتالي، لا يرى التطبيق نفسه أبدًا هذه المشكلات الخاصة بالشبكة.
ستستخدم العقدة المعلومات ICMP لتحديث جداول التوجيه الخاصة بها.
11.1.7. طبقة النقل: بروتوكولا UDP و TCP
11.1.7.1. بروتوكول UDP: بروتوكول مخطط بيانات المستخدم
يسمح بروتوكول UDP بتبادل غير موثوق للبيانات بين نقطتين، أي أن التوجيه الصحيح لحزمة إلى وجهتها غير مضمون. يمكن للتطبيق، إذا رغب في ذلك، إدارة هذا الأمر بنفسه، على سبيل المثال بالانتظار بعد إرسال رسالة، لإيصال استلام، قبل إرسال الرسالة التالية.
في الوقت الحالي، على مستوى الشبكة، تحدثنا عن عناوين IP للأجهزة. ولكن على جهاز واحد، يمكن أن تتعايش في نفس الوقت عمليات مختلفة يمكنها جميعًا التواصل. لذلك، عند إرسال رسالة، يجب الإشارة ليس فقط إلى عنوان الجهاز المستلم IP، بل أيضًا إلى "اسم" العملية المستلمة. هذا الاسم هو في الواقع رقم يُسمى رقم المنفذ. بعض الأرقام محجوزة للتطبيقات القياسية: المنفذ 69 لتطبيق tftp (بروتوكول نقل الملفات البسيط) على سبيل المثال.
تُسمى الحزم التي يديرها بروتوكول UDP أيضًا داتاغرامات. وهي تأخذ الشكل التالي:
سيتم تغليف هذه الداتاغرامات في حزم IP، ثم في إطارات مادية.
11.1.7.2. بروتوكول TCP: بروتوكول التحكم في النقل
لضمان اتصالات آمنة، لا يكفي بروتوكول UDP: يجب على مطور التطبيقات أن يضع بنفسه بروتوكولًا يسمح له بالتحقق من التوجيه الصحيح للحزم. بروتوكول TCP (بروتوكول التحكم في النقل) يتجنب هذه المشاكل. وخصائصه هي كما يلي:
- تقوم العملية التي ترغب في الإرسال أولاً بإنشاء اتصال مع العملية المستلمة للمعلومات التي سترسلها. يتم هذا الاتصال بين منفذ في الجهاز المرسل ومنفذ في الجهاز المستقبل. يتم إنشاء مسار افتراضي بين المنفذين، ويكون مخصصاً فقط للعمليتين اللتين قامتا بإجراء الاتصال.
- تتبع جميع الحزم المرسلة من العملية المصدر هذا المسار الافتراضي وتصل بالترتيب الذي تم إرسالها به، وهو ما لم يكن مضمونًا في بروتوكول UDP حيث كان من الممكن أن تتبع الحزم مسارات مختلفة.
- تتميز المعلومات المرسلة بطابعها المستمر. ترسل العملية المرسلة المعلومات وفقًا لوتيرتها الخاصة. ولا يتم إرسال هذه المعلومات بالضرورة على الفور: ينتظر بروتوكول TCP حتى يتوفر لديه ما يكفي لإرسالها. يتم تخزينها في بنية تسمى المقطع TCP. وبمجرد ملء هذا المقطع، سيتم إرساله إلى الطبقة IP حيث سيتم تغليفه في حزمة IP.
- يتم ترقيم كل مقطع يتم إرساله بواسطة بروتوكول TCP. يتحقق بروتوكول TCP المستلم من استلامه للمقاطع بالترتيب الصحيح. لكل مقطع يتم استلامه بشكل صحيح، يرسل إشعارًا بالاستلام إلى المرسل.
- وعندما يتلقى المرسل هذا الإقرار، يقوم بإبلاغ العملية المرسلة بذلك. وبالتالي، يمكن لهذه العملية معرفة أن المقطع قد وصل إلى وجهته، وهو ما لم يكن ممكنًا مع بروتوكول UDP.
- إذا لم يتلق بروتوكول TCP، الذي أرسل مقطعًا، إشعارًا بالاستلام بعد مرور فترة معينة، فإنه يعيد إرسال المقطع المعني، مما يضمن جودة خدمة توصيل المعلومات.
- الدائرة الافتراضية التي تم إنشاؤها بين العمليتين المتواصلتين هي full-duplex: وهذا يعني أن المعلومات يمكن أن تنتقل في كلا الاتجاهين. وبالتالي، يمكن للعملية الوجهة إرسال إقرارات الاستلام حتى في الوقت الذي تواصل فيه العملية المصدر إرسال المعلومات. وهذا يسمح، على سبيل المثال، لبروتوكول المصدر TCP بإرسال عدة مقاطع دون انتظار إشعار الاستلام. وإذا أدرك بعد فترة معينة أنه لم يتلق إشعار الاستلام لمقطع معين رقم n، فسيستأنف إرسال المقاطع من هذه النقطة.
11.1.8. طبقة التطبيقات
فوق البروتوكولين UDP و TCP، توجد بروتوكولات قياسية متنوعة:
TELNET
يسمح هذا البروتوكول لمستخدم جهاز A في الشبكة بالاتصال بجهاز B (يُسمى غالبًا الجهاز المضيف). يقوم TELNET بمحاكاة ما يُسمى محطة طرفية عالمية على الجهاز A. وبالتالي، يتصرف المستخدم كما لو كان لديه محطة طرفية متصلة بالجهاز B. يعتمد Telnet على بروتوكول TCP.
FTP: (بروتوكول نقل الملفات)
يسمح هذا البروتوكول بتبادل الملفات بين جهازين بعيدين وكذلك بإجراء عمليات على الملفات مثل إنشاء مجلدات على سبيل المثال. وهو يعتمد على بروتوكول TCP.
TFTP: (التحكم في نقل الملفات البسيط)
هذا البروتوكول هو نسخة معدلة من FTP. وهو يعتمد على بروتوكول UDP وهو أقل تطوراً من FTP.
DNS: (نظام أسماء النطاقات)
عندما يرغب مستخدم في تبادل الملفات مع جهاز بعيد، عبر FTP على سبيل المثال، يجب أن يعرف عنوان الإنترنت لهذا الجهاز. على سبيل المثال، لإجراء FTP على الجهاز Lagaffe بجامعة أنجيه، يجب تشغيل FTP على النحو التالي: FTP 193.49.144.1
وهذا يتطلب وجود دليل يربط بين الجهاز <--> عنوان IP. ومن المحتمل أن يتم تعيين الأجهزة في هذا الدليل بأسماء رمزية مثل:
الجهاز DPX2/320 التابع لجامعة أنجيه
الجهاز Sun التابع لجامعة أنجيه ISERPA
من الواضح أنه سيكون من الأفضل تسمية جهاز ما باسم بدلاً من عنوانه IP. وهنا تبرز مشكلة تفرد الاسم: فهناك ملايين الأجهزة المتصلة ببعضها البعض. قد يتصور المرء أن تقوم هيئة مركزية بتخصيص الأسماء. لكن هذا سيكون بلا شك أمراً مرهقاً. وقد تم في الواقع توزيع إدارة الأسماء على نطاقات. يتم إدارة كل مجال من قبل هيئة عادة ما تكون صغيرة الحجم وتتمتع بحرية تامة في اختيار أسماء الأجهزة. وهكذا، تنتمي الأجهزة في فرنسا إلى المجال fr، وهو مجال تديره مؤسسة Inria في باريس. ولتبسيط الأمور أكثر، يتم توزيع الإشراف مرة أخرى: يتم إنشاء مجالات داخل المجال fr. وهكذا، تنتمي جامعة أنجيه إلى المجال univ-Angers. يتمتع القسم الذي يدير هذا المجال بحرية تامة في تسمية أجهزة شبكة جامعة أنجيه. في الوقت الحالي، لم يتم تقسيم هذا المجال. ولكن في جامعة كبيرة تضم العديد من الأجهزة المتصلة بالشبكة، قد يتم تقسيمه.
تم تسمية الجهاز DPX2/320 التابعة لجامعة أنجيه تم تسميتها Lagaffe، في حين تم تسمية PC و 486DX50 باسم liny. كيف يمكن الإشارة إلى هذه الأجهزة من الخارج؟ من خلال تحديد التسلسل الهرمي للمجالات التي تنتمي إليها. وبالتالي، سيكون الاسم الكامل لجهاز Lagaffe هو:
Lagaffe.univ-Angers.fr
داخل المجالات، يمكن استخدام أسماء نسبية. وبالتالي، داخل المجال fr وخارج المجال univ-Angers، يمكن الإشارة إلى الجهاز Lagaffe بـ
Lagaffe.univ-Angers
وأخيرًا، داخل المجال univ-Angers، يمكن الإشارة إليها ببساطة بـ
Lagaffe
وبالتالي، يمكن لأي تطبيق الإشارة إلى جهاز ما باسمه. ولكن في النهاية، لا بد من الحصول على عنوان الإنترنت الخاص بهذا الجهاز. كيف يتم ذلك؟ لنفترض أننا نريد من جهاز A الاتصال بجهاز B.
- إذا كانت الجهاز B تنتمي إلى نفس المجال الذي تنتمي إليه الجهاز A، فمن المحتمل أن نجد عنوانها IP في ملف على الجهاز A.
- وإلا، فستجد الجهاز A في ملف آخر أو في نفس الملف السابق قائمة ببعض خوادم الأسماء مع عناوينها IP. يتولى خادم الأسماء مهمة الربط بين اسم الجهاز وعنوانه IP. سترسل الجهاز A طلبًا خاصًا إلى أول خادم أسماء في قائمتها، يُسمى طلب DNS، والذي يتضمن بالتالي اسم الجهاز المطلوب. إذا كان الخادم المستفسر لديه هذا الاسم في سجلاته، فسيرسل إلى الجهاز A العنوان المطابق IP. وإلا، فسيجد الخادم هو الآخر في ملفاته قائمة بخوادم الأسماء التي يمكنه الاستعلام عنها. وسيقوم بذلك. وهكذا سيتم الاستعلام عن عدد معين من خوادم الأسماء، ليس بطريقة عشوائية بل بطريقة تقلل من عدد الطلبات. وإذا تم العثور على الجهاز في النهاية، فستصل الإجابة إلى الجهاز A.
XDR: (تمثيل بيانات eXternal)
تم إنشاء هذا البروتوكول بواسطة sun MicroSystems، وهو يحدد تمثيلًا قياسيًا للبيانات، مستقلًا عن الأجهزة.
RPC: (استدعاء الإجراء عن بُعد)
تم تعريفه أيضًا بواسطة sun، وهو بروتوكول اتصال بين التطبيقات البعيدة، مستقل عن طبقة النقل. هذا البروتوكول مهم: فهو يعفي المبرمج من معرفة تفاصيل طبقة النقل ويجعل التطبيقات قابلة للنقل. يعتمد هذا البروتوكول على بروتوكول XDR
NFS: نظام الملفات الشبكي
تم تعريفه أيضًا بواسطة Sun، ويتيح هذا البروتوكول لجهاز ما "رؤية" نظام الملفات الخاص بجهاز آخر. وهو يعتمد على البروتوكول السابق RPC.
11.1.9. خاتمة
قدمنا في هذه المقدمة بعض الخطوط العريضة لبروتوكولات الإنترنت. لمزيد من التعمق في هذا المجال، يمكن قراءة الكتاب الممتاز لدوغلاس كومر:
العنوان TCP/IP: الهندسة والبروتوكولات والتطبيقات.
المؤلف دوغلاس COMER
الناشر InterEditions
11.2. فئات .NET لإدارة عناوين IP
يتم تعريف جهاز على شبكة الإنترنت بشكل فريد بواسطة عنوان IP (بروتوكول الإنترنت) الذي يمكن أن يتخذ شكلين:
- IPv4 : مشفرة على 32 بت وممثلة بسلسلة من النوع "I1.I2.I3.I4" حيث In هو رقم بين 1 و 254. هذه هي عناوين IP الأكثر شيوعًا حاليًا.
- IPv6: مشفرة على 128 بت وممثلة بسلسلة من النوع "[I1.I2.I3.I4.I5.I6.I7.I8]" حيث In هي سلسلة من 4 أرقام سداسية عشرية. في هذا المستند، لن نستخدم عناوين IPv6.
يمكن أيضًا تعريف الجهاز باسم فريد. هذا الاسم ليس إلزاميًا، حيث تستخدم التطبيقات في النهاية عناوين IP الخاصة بالأجهزة. وهي موجودة لتسهيل الأمر على المستخدمين. وبالتالي، من الأسهل، باستخدام متصفح، طلب http://www.ibm.com بدلاً من URL http://129.42.17.99، على الرغم من أن الطريقتين ممكنتان.
يمكن أن يكون لجهاز ما عدة عناوين IP إذا كان متصلاً فعلياً بعدة شبكات في نفس الوقت. وعندها يكون له عنوان IP على كل شبكة.
يمكن تمثيل عنوان IP بطريقتين في .NET:
- على شكل سلسلة أحرف "I1.I2.I3.I4" أو "[I1.I2.I3.I4.I5.I6.I7.I8]"
- في شكل كائن من النوع IPAddress
الفئة IPAddress
من بين الطرق M والخصائص P والثوابت C للفئة IPAddress، نجد ما يلي:
P | عائلة العنوان IP. النوع AddressFamily هو تعداد. القيمتان الشائعتان هما: AddressFamily.InterNetwork: لعنوان IPv4 AddressFamily.InterNetworkV6: لعنوان IPv6 | |
C | العنوان IP "0.0.0.0". عندما ترتبط خدمة بهذا العنوان، فهذا يعني أنها تقبل العملاء على جميع عناوين IP للجهاز الذي تعمل عليه. | |
C | العنوان IP "127.0.0.1". يُسمى "عنوان الحلقة". عندما ترتبط خدمة بهذا العنوان، فهذا يعني أنها لا تقبل سوى العملاء الموجودين على نفس الجهاز الذي تعمل عليه. | |
C | العنوان IP "255.255.255.255". عندما ترتبط خدمة بهذا العنوان، فهذا يعني أنها لا تقبل أي عميل. | |
M | يحاول تمرير العنوان IP ipString بالشكل "I1.I2.I3.I4" في شكل كائن عنوان IPAddress. يعرض true إذا نجحت العملية. | |
M | يعرض true إذا كان عنوان IP هو "127.0.0.1" | |
M | يعرض العنوان IP في صيغة "I1.I2.I3.I4" أو "[I1.I2.I3.I4.I5.I6.I7.I8]" |
يتم الربط بين العنوان IP <--> nomMachine بواسطة خدمة موزعة على الإنترنت تسمى DNS (نظام أسماء النطاقات). تسمح الطرق الثابتة لفئة Dns بإجراء ربط العنوان IP <--> nomMachine:
تُرجع عنوان IPHostEntry من عنوان IP في شكل سلسلة أو من اسم جهاز. تطلق استثناءً إذا تعذر العثور على الجهاز. | |
تُرجع عنوان IPHostEntry من عنوان IP من النوع IPAddress. تُطلق استثناءً إذا تعذر العثور على الجهاز. | |
يعرض اسم الجهاز الذي يعمل عليه البرنامج الذي ينفذ هذه التعليمات | |
تُرجع عناوين IP للجهاز المحدد باسمه أو بأحد عناوينه IP. |
تغلف مثيل IPHostEntry عناوين IP والأسماء المستعارة واسم الجهاز. النوع IPHostEntry هو كما يلي:
P | جدول عناوين الجهاز IP | |
P | الأسماء المستعارة DNS للجهاز. هذه هي الأسماء المطابقة للعناوين المختلفة IP للجهاز. | |
P | اسم المضيف الرئيسي للجهاز |
لننظر إلى البرنامج التالي الذي يعرض اسم الجهاز الذي يعمل عليه ثم يعطي بشكل تفاعلي التوافقات بين العنوان IP <--> اسم الجهاز:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// يعرض اسم الجهاز المحلي
// ثم يقدم معلومات تفاعلية عن أجهزة الشبكة
// المحددة باسم أو عنوان IP
// الجهاز المحلي
Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
// أسئلة وأجوبة تفاعلية
string machine;
IPHostEntry ipHostEntry;
while (true) {
// إدخال اسم أو عنوان IP للجهاز المطلوب
Console.Write("Machine recherchée (rien pour arrêter) : ");
machine = Console.ReadLine().Trim().ToLower();
// انتهى؟
if (machine == "") return;
// إدارة الاستثناءات
try {
// البحث عن آلة
ipHostEntry = Dns.GetHostEntry(machine);
// اسم الجهاز
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// عناوين الجهاز
Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
Console.Write(", {0}" , ipHostEntry.AddressList[i]);
}
Console.WriteLine();
// الأسماء المستعارة للجهاز
if (ipHostEntry.Aliases.Length != 0) {
Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
Console.Write(", {0}" , ipHostEntry.Aliases[i]);
}
Console.WriteLine();
}
} catch {
// الجهاز غير موجود
Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
}
}
}
}
}
يؤدي التنفيذ إلى النتائج التالية:
11.3. أساسيات برمجة الإنترنت
11.3.1. مقدمة
لننظر إلى الاتصال بين جهازين بعيدين A و B:
![]() |
عندما يرغب تطبيق 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: هاتان الخدمتان لا تقبلان نفس الأوامر. لديهما بروتوكول حوار مختلف.
11.3.2. خصائص بروتوكول TCP
لن ندرس هنا سوى الاتصالات الشبكية التي تستخدم بروتوكول النقل TCP. ولنتذكر هنا خصائص هذا البروتوكول:
- تقوم العملية التي ترغب في الإرسال أولاً بإنشاء اتصال مع العملية المستلمة للمعلومات التي سترسلها. يتم هذا الاتصال بين منفذ في الجهاز المرسل ومنفذ في الجهاز المستقبل. يتم إنشاء مسار افتراضي بين المنفذين، ويكون مخصصاً فقط للعمليتين اللتين قامتا بإجراء الاتصال.
- تتبع جميع الحزم التي ترسلها العملية المصدرية هذا المسار الافتراضي وتصل بالترتيب الذي تم إرسالها به
- تتميز المعلومات المرسلة بطابعها المستمر. ترسل العملية المرسلة المعلومات وفقًا لوتيرتها الخاصة. ولا يتم إرسال هذه المعلومات بالضرورة على الفور: ينتظر بروتوكول TCP حتى يتوفر لديه ما يكفي لإرسالها. ويتم تخزينها في بنية تسمى المقطع TCP. وبمجرد ملء هذا المقطع، سيتم إرساله إلى الطبقة IP حيث سيتم تغليفه في حزمة IP.
- يتم ترقيم كل مقطع يتم إرساله بواسطة بروتوكول TCP. يتحقق بروتوكول TCP المستلم من استلامه للمقاطع بالتسلسل الصحيح. لكل مقطع يتم استلامه بشكل صحيح، يرسل إشعارًا بالاستلام إلى المرسل.
- وعندما يتلقى المرسل هذا الإقرار، يقوم بإبلاغ العملية المرسلة بذلك. وبالتالي، يمكن لهذه العملية معرفة أن المقطع قد وصل إلى وجهته.
- إذا لم يتلق بروتوكول TCP، الذي أرسل مقطعًا، إشعارًا بالاستلام بعد مرور فترة معينة، فإنه يعيد إرسال المقطع المعني، مما يضمن جودة خدمة توصيل المعلومات.
- الدائرة الافتراضية التي تم إنشاؤها بين العمليتين المتواصلتين هي full-duplex: وهذا يعني أن المعلومات يمكن أن تنتقل في كلا الاتجاهين. وبالتالي، يمكن للعملية الوجهة إرسال إشعارات الاستلام حتى في الوقت الذي تواصل فيه العملية المصدر إرسال المعلومات. وهذا يسمح، على سبيل المثال، لبروتوكول المصدر TCP بإرسال عدة مقاطع دون انتظار إشعار الاستلام. وإذا أدرك بعد فترة معينة أنه لم يتلق إشعار الاستلام لمقطع معين رقم n، فسيستأنف إرسال المقاطع من هذه النقطة.
11.3.3. العلاقة بين العميل والخادم
غالبًا ما تكون الاتصالات عبر الإنترنت غير متماثلة: تبدأ الآلة A اتصالاً لطلب خدمة من الآلة B: تحدد أنها تريد فتح اتصال مع الخدمة SB1 للآلة B. وتقبل هذه الأخيرة أو ترفض. إذا وافقت، يمكن للجهاز A إرسال طلباته إلى الخدمة SB1. ويجب أن تتوافق هذه الطلبات مع بروتوكول الحوار الذي تفهمه الخدمة SB1. وبذلك ينشأ حوار من طلب واستجابة بين الجهاز A الذي يُسمى جهاز العميل والجهاز B الذي يُسمى جهاز الخادم. سيقوم أحد الشريكين بإنهاء الاتصال.
11.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
fermer la connexion
11.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
11.4. اكتشف بروتوكولات الاتصال على الإنترنت
11.4.1. مقدمة
عندما يتصل عميل بخادم، يتم بعد ذلك إقامة حوار بينهما. وتشكل طبيعة هذا الحوار ما يُسمى ببروتوكول اتصال الخادم. ومن بين بروتوكولات الإنترنت الأكثر شيوعًا ما يلي:
- HTTP: بروتوكول نقل HyperText - بروتوكول الحوار مع خادم الويب (خادم HTTP)
- SMTP: بروتوكول نقل البريد البسيط - بروتوكول الاتصال بخادم إرسال البريد الإلكتروني (خادم SMTP)
- POP: بروتوكول مكتب البريد - بروتوكول الاتصال بخادم تخزين البريد الإلكتروني (خادم POP). يتعلق الأمر هنا باسترداد رسائل البريد الإلكتروني المستلمة وليس بإرسالها.
- FTP: بروتوكول نقل الملفات - بروتوكول الاتصال بخادم تخزين الملفات (الخادم FTP).
تتميز جميع هذه البروتوكولات بأنها بروتوكولات تعتمد على سطور نصية: حيث يتبادل العميل والخادم سطور نصية. إذا كان لدينا عميل قادر على:
- إنشاء اتصال بخادم Tcp
- عرض الأسطر النصية التي يرسلها الخادم إليه على وحدة التحكم
- إرسال الأسطر النصية التي يدخلها المستخدم إلى الخادم
فإننا نكون قادرين على التواصل مع خادم TCP الذي يستخدم بروتوكولًا يعتمد على سطور نصية، شريطة أن نكون على دراية بقواعد هذا البروتوكول.
برنامج telnet الموجود على أجهزة Unix أو Windows هو أحد هذه العملاء. على أجهزة Windows، يوجد أيضًا أداة تسمى putty وسنستخدمها هنا. يمكن تنزيل putty من العنوان [http://www.putty.org/]. وهو ملف قابل للتنفيذ (.exe) يمكن استخدامه مباشرة. سنقوم بتكوينه بالطريقة التالية:
![]() |
- [1]: عنوان IP لخادم Tcp الذي نريد الاتصال به أو اسمه
- [2]: منفذ الاستماع لخادم Tcp
- [3]: اختيار الوضع Raw الذي يشير إلى اتصال TCP خام.
- [4]: استخدام الوضع Never لمنع إغلاق نافذة العميل putty إذا أغلق الخادم الاتصال.
- [6,7]: عدد الأعمدة/الصفوف في وحدة التحكم
- [5]: الحد الأقصى لعدد الأسطر المحفوظة في الذاكرة. يمكن لخادم HTTP إرسال العديد من الأسطر. يجب أن يكون من الممكن "التمرير" عليها.
![]() |
- [8,9]: للاحتفاظ بالإعدادات السابقة، قم بتسمية التكوين [8] وحفظه [9].
- [11,12]: لاستعادة تكوين محفوظ، حدده [11] وقم بتحميله [12].
بعد تكوين هذه الأداة، دعونا نكتشف بعض البروتوكولات TCP.
11.4.2. بروتوكول HTTP (بروتوكول النقل HyperText)
لنقوم بتوصيل عميلنا TCP بخادم الويب الخاص بالجهاز istia.univ-angers.fr [2]، المنفذ 80 [3]:
![]() |
في وحدة التحكم الخاصة بـ putty، نقوم بإنشاء الحوار HTTP التالي:
- السطور 1-4 هي طلب العميل، الذي تمت كتابته على لوحة المفاتيح
- السطور 5-19 هي استجابة الخادم
- السطر 1: صيغة GET UrlDocument HTTP/1.1 - نطلب عنوان URL /، c.a.d. جذر موقع الويب [istia.univ-angers.fr].
- السطر 2: صيغة Host: machine:port
- السطر 3: صيغة Connection: [mode de la connexion]. يشير الوضع [close] إلى أن الخادم يجب أن يغلق الاتصال بمجرد إرساله للرد. الوضع [Keep-Alive] يطلب تركه مفتوحًا.
- السطر 4: سطر فارغ. تسمى الأسطر 1-3 رؤوس HTTP. قد تكون هناك رؤوس أخرى غير تلك المعروضة هنا. يُشار إلى نهاية رؤوس HTTP بسطر فارغ.
- الأسطر 5-13: رؤوس HTTP من استجابة الخادم - تنتهي هنا أيضًا بسطر فارغ.
- الأسطر 14-19: المستند المرسل من الخادم، وهو هنا مستند HTML
- السطر 5: صيغة HTTP/1.1 رمز msg - يشير الرمز 200 إلى أنه تم العثور على المستند المطلوب.
- السطر 6: تاريخ ووقت الخادم
- السطر 7: تعريف البرنامج الذي يوفر خدمة الويب - هنا خادم Apache على نظام Linux / Debian
- السطر 8: تم إنشاء المستند ديناميكيًا بواسطة PHP
- السطر 9: ملف تعريف العميل - إذا أراد العميل أن يتم التعرف عليه عند اتصاله التالي، فعليه إعادة إرسال ملف التعريف هذا في رؤوسه HTTP.
- السطر 10: يشير إلى أنه بعد تقديم المستند المطلوب، سيقوم الخادم بإغلاق الاتصال
- السطر 11: سيتم إرسال المستند على شكل أجزاء (chunked) وليس ككتلة واحدة.
- السطر 12: طبيعة المستند: هنا مستند HTML
- السطر 13: السطر الفارغ الذي يشير إلى نهاية رؤوس HTTP الخاصة بالخادم
- السطر 14: رقم سداسي عشري يشير إلى عدد الأحرف في الكتلة الأولى من المستند. عندما يكون هذا الرقم 0 (السطر 19)، سيعرف العميل أنه تلقى المستند بالكامل.
- الأسطر 15-18: جزء من المستند المستلم.
تم إغلاق الاتصال وأصبح العميل putty غير نشط. لنعيد الاتصال [1] ونمسح الشاشة من العروض السابقة [2,3]:
![]() |
الحوار هذه المرة هو التالي:
- السطر 1: تم طلب مستند غير موجود
- السطر 5: رد الخادم HTTP بالرمز 404 مما يعني أنه لم يتم العثور على المستند المطلوب.
إذا طلبنا هذا المستند باستخدام متصفح Firefox:

إذا طلبنا عرض شفرة المصدر [Affichage/Code source]:
نحصل على الأسطر 13-22 التي استلمها عميلنا putty. وتكمن فائدة هذا في أنه يعرض لنا أيضًا رؤوس الرسالة HTTP الخاصة بالرد. ومن الممكن أيضًا الحصول عليها باستخدام Firefox.
11.4.3. بروتوكول SMTP (بروتوكول نقل البريد البسيط)
![]() |
تعمل الخوادم SMTP عمومًا على المنفذ 25 [2]. يتم الاتصال بالخادم [1]. هنا، يجب عمومًا اختيار خادم
ينتمي إلى نفس المجال IP الذي تنتمي إليه الآلة لأن خوادم SMTP غالبًا ما تكون مكونة لقبول الطلبات الواردة من الآلات التي تنتمي إلى نفس المجال الذي تنتمي إليه. علاوة على ذلك، غالبًا ما تكون برامج الحماية أو برامج مكافحة الفيروسات على الأجهزة الشخصية مكونة بحيث لا تقبل أي اتصال بالمنفذ 25 من جهاز خارجي. قد يكون من الضروري إذن إعادة تكوين برنامج الحماية أو برنامج مكافحة الفيروسات هذا.
يظهر الحوار SMTP في نافذة العميل putty كما يلي:
فيما يلي (D) طلب من العميل، (R) رد من الخادم.
- السطر 1: (R) رسالة ترحيب من الخادم SMTP
- السطر 2: (D) الأمر HELO لقول مرحبًا
- السطر 3: (R) رد من الخادم
- السطر 4: (D) عنوان المرسل، على سبيل المثال البريد الإلكتروني من: someone@gmail.com
- السطر 5: (R) رد الخادم
- السطر 6: (D) عنوان المستلم، على سبيل المثال rcpt to: someoneelse@gmail.com
- السطر 7: (R) رد الخادم
- السطر 8: (D) يشير إلى بداية الرسالة
- السطر 9: (R) رد الخادم
- الأسطر 10-12: (D) الرسالة المراد إرسالها تنتهي بسطر يحتوي على نقطة فقط.
- السطر 13: (R) رد الخادم
- السطر 14: (D) يشير العميل إلى أنه قد انتهى
- السطر 15: (R) رد الخادم الذي يقوم بعد ذلك بإغلاق الاتصال
11.4.4. بروتوكول POP (بروتوكول مكتب البريد)
![]() |
تعمل خوادم POP عمومًا على المنفذ 110 [2]. يتم الاتصال بالخادم [1]. الحوار POP في نافذة العميل putty هو كما يلي:
- السطر 1: (R) رسالة ترحيب من الخادم POP
- السطر 2: (D) يقدم العميل معرفه POP، c.a.d. اسم المستخدم الذي يستخدمه لقراءة بريده
- السطر 3: (R) رد الخادم
- السطر 4: (D) كلمة مرور العميل
- السطر 5: (R) رد الخادم
- السطر 6: (D) يطلب العميل قائمة رسائله
- السطور 7-12: (R) قائمة الرسائل الموجودة في صندوق بريد العميل، بالصيغة [N° du message taille en octets du message]
- السطر 13: (D) طلب الرسالة رقم 64
- الأسطر 14-25: (R) الرسالة رقم 64 مع الأسطر 15-22، رؤوس الرسالة، والأسطر 23-24 نص الرسالة.
- السطر 26: (D) يشير العميل إلى أنه انتهى
- السطر 27: (R) رد من الخادم الذي سيقوم بعد ذلك بإغلاق الاتصال.
11.4.5. بروتوكول FTP (بروتوكول نقل الملفات)
بروتوكول FTP أكثر تعقيدًا من البروتوكولات المذكورة سابقًا. لاكتشاف الأسطر النصية المتبادلة بين العميل والخادم، يمكن استخدام أداة مثل FileZilla [http://www.filezilla.fr/].
![]() |
Filezilla هو عميل FTP يوفر واجهة Windows لنقل الملفات. يتم ترجمة إجراءات المستخدم على واجهة Windows إلى أوامر FTP التي يتم تسجيلها في [1]. هذه طريقة جيدة لاكتشاف أوامر بروتوكول FTP.
11.5. فئات .NET في برمجة الإنترنت
11.5.1. اختيار الفئة المناسبة
يوفر إطار العمل .NET فئات مختلفة للعمل مع الشبكة:
![]() |
- فئة Socket هي الفئة التي تعمل على أقرب مستوى من الشبكة. وهي تتيح إدارة الاتصال بالشبكة بدقة. يشير المصطلح socket إلى مقبس كهربائي. وقد تم توسيع نطاق هذا المصطلح ليشير إلى مقبس شبكة برمجي. في اتصال TCP-IP بين جهازين A و B، هناك اثنان من sockets يتواصلان مع بعضهما البعض. يمكن للتطبيق العمل مباشرة مع sockets. وهذا هو الحال بالنسبة للتطبيق A أعلاه. يمكن أن يكون المقبس مقبس client أو serveur.
- إذا كنا نرغب في العمل على مستوى أقل دقة من مستوى فئة Socket، فيمكننا استخدام الفئات
- TcpClient لإنشاء عميل Tcp
- TcpListener لإنشاء خادم Tcp
توفر هاتان الفئتان للتطبيق الذي يستخدمهما رؤية أبسط للاتصال الشبكي من خلال إدارة التفاصيل التقنية لإدارة المقابس نيابة عنه.
- توفر .NET فئات خاصة ببعض البروتوكولات:
- الفئة SmtpClient لإدارة بروتوكول SMTP للاتصال بخادم SMTP لإرسال رسائل البريد الإلكتروني
- الفئة WebClient لإدارة بروتوكولات HTTP أو FTP للاتصال بخادم ويب.
تجدر الإشارة إلى أن الفئة Socket كافية بحد ذاتها لإدارة أي اتصال TCP/IP، ولكننا سنسعى قبل كل شيء إلى استخدام الفئات ذات المستوى الأعلى لتسهيل كتابة تطبيق TCP/IP.
11.5.2. الفئة TcpClient
الفئة TcpClient هي الفئة المناسبة في معظم الحالات لإنشاء عميل لخدمة TCP. وتضم من بين منشئيها C، وأساليبها M، وخصائصها P، ما يلي:
C | ينشئ اتصال TCP مع الخدمة التي تعمل على المنفذ المحدد (port) للجهاز المحدد (hostname). على سبيل المثال new TcpClient("istia.univ-angers.fr",80) للاتصال بالمنفذ 80 للجهاز istia.univ-angers.fr | |
P | المقبس الذي يستخدمه العميل للتواصل مع الخادم. | |
M | يحصل على تدفق للقراءة والكتابة إلى الخادم. هذا التدفق هو الذي يسمح بالتبادل بين العميل والخادم. | |
M | يغلق الاتصال. يتم إغلاق المأخذ والتدفق NetworkStream أيضًا | |
P | صحيح إذا تم إنشاء الاتصال |
تمثل الفئة NetworkStream تدفق الشبكة بين العميل والخادم. وهي مشتقة من الفئة Stream. تتبادل العديد من تطبيقات العميل-الخادم أسطر نصية تنتهي بأحرف نهاية السطر "\r\n". لذلك من المفيد استخدام كائنات StreamReader و StreamWriter لقراءة وكتابة هذه الأسطر في تدفق الشبكة. وبالتالي، إذا أقامت آلة M1 اتصالاً بآلة M2 باستخدام كائن TcpClient client1 وتبادلتا أسطر النص، فسيكون بإمكانها إنشاء تدفقات القراءة والكتابة على النحو التالي:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
الأمر
تعني أن تدفق الكتابة من client1 لن يمر عبر مخزن مؤقت بل سيذهب مباشرة إلى الشبكة. هذه النقطة مهمة. بشكل عام، عندما يرسل client1 سطرًا نصيًا إلى شريكه، فإنه يتوقع ردًا عليه. لن يأتي هذا الرد أبدًا إذا كان السطر قد تم تخزينه مؤقتًا على الجهاز M1 ولم يتم إرساله أبدًا إلى الجهاز M2.
لإرسال سطر نصي إلى الجهاز M2، سنكتب:
لقراءة رد M2، نكتب:
لدينا الآن العناصر اللازمة لكتابة البنية الأساسية لعميل إنترنت لديه بروتوكول الاتصال الأساسي التالي مع الخادم:
- يرسل العميل طلبًا مكونًا من سطر واحد
- يرسل الخادم استجابة موجودة في سطر واحد
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// يتم الاتصال بالخدمة
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة الطلب - الاستجابة
while (true) {
// الطلب قادم من لوحة المفاتيح
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// انتهى؟
if (demande.Trim().ToLower() == "bye")
break;
// إرسال الطلب إلى الخادم
writer.WriteLine(demande);
// قراءة الرد من الخادم
réponse = reader.ReadLine();
// معالجة الرد
...
}
}
}
}
}
} catch (Exception e) {
// خطأ
...
}
}
}
}
- السطر 11: إنشاء اتصال العميل - تضمن الجملة using أن الموارد المرتبطة بها سيتم تحريرها عند الخروج من using.
- السطر 12: فتح تدفق الشبكة في جملة using
- السطر 13: إنشاء واستخدام تدفق القراءة في بند using
- السطر 14: إنشاء واستخدام تدفق الكتابة في بند using
- السطر 16: عدم تخزين تدفق الإخراج مؤقتًا
- الأسطر 18-31: دورة طلب العميل / استجابة الخادم
- السطر 26: يرسل العميل طلبه إلى الخادم
- السطر 28: ينتظر العميل استجابة الخادم. هذه عملية معطلة مثل عملية القراءة من لوحة المفاتيح. ينتهي الانتظار بوصول سلسلة تنتهي بـ "\n" أو بنهاية التدفق. سيحدث هذا إذا أغلق الخادم الاتصال الذي فتحه مع العميل.
11.5.3. الفئة TcpListener
الفئة TcpListener هي الفئة المناسبة في معظم الحالات لإنشاء خدمة TCP. وتضم من بين منشئيها C، وأساليبها M، وخصائصها P، ما يلي:
C | تنشئ خدمة TCP التي ستنتظر (listen) طلبات العملاء على منفذ تم تمريره كمعلمة (port) يُسمى منفذ الاستماع. إذا كانت الآلة متصلة بعدة شبكات IP، فإن الخدمة تستمع على كل شبكة من هذه الشبكات. | |
C | نفس الشيء، لكن الاستماع لا يحدث إلا على عنوان IP المحدد. | |
M | يبدأ الاستماع لطلبات العملاء | |
M | يقبل طلب العميل. ثم يفتح اتصالاً جديداً معه، يُسمى اتصال الخدمة. المنفذ المستخدم من جانب الخادم عشوائي ويتم اختياره بواسطة النظام. يُسمى منفذ الخدمة. AcceptTcpClient يُرجع كنتيجة الكائن TcpClient المرتبط من جانب الخادم باتصال الخدمة. | |
M | يتوقف عن الاستماع إلى طلبات العملاء | |
P | مقبس الاستماع للخادم |
البنية الأساسية لخادم TCP الذي يتبادل البيانات مع عملائه وفقًا للبروتوكول التالي:
- يرسل العميل طلبًا مكونًا من سطر واحد
- يرسل الخادم استجابة موجودة في سطر واحد
قد تبدو كما يلي:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace ... {
public class ... {
...
// يتم إنشاء خدمة الاستماع
TcpListener ecoute = null;
try {
// يتم إنشاء الخدمة - ستستمع على جميع واجهات الشبكة الخاصة بالجهاز
ecoute = new TcpListener(IPAddress.Any, port);
// يتم تشغيلها
ecoute.Start();
// حلقة الخدمة
TcpClient tcpClient = null;
// حلقة لا نهائية - سيتم إيقافها بواسطة Ctrl-C
while (true) {
// في انتظار عميل
tcpClient = ecoute.AcceptTcpClient();
// يتم توفير الخدمة بواسطة مهمة أخرى
ThreadPool.QueueUserWorkItem(Service, tcpClient);
// العميل التالي
}
} catch (Exception ex) {
// الإبلاغ عن الخطأ
...
} finally {
// نهاية الخدمة
ecoute.Stop();
}
}
// -------------------------------------------------------
// تقديم الخدمة لعميل
public static void Service(Object infos) {
// يتم استرداد العميل المطلوب خدمته
Client client = infos as Client;
// تشغيل الوصلة TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة قراءة الطلب/كتابة الرد
bool fini=false;
while (! fini) != null) {
// انتظار طلب العميل - عملية معطلة
demande=reader.ReadLine();
// تحضير الرد
réponse=...;
// إرسال الرد إلى العميل
writer.WriteLine(réponse);
// الطلب التالي
}
}
}
}
}
} catch (Exception e) {
// خطأ
...
} finally {
// نهاية العميل
...
}
}
}
}
- السطر 14: يتم إنشاء خدمة الاستماع لمنفذ معين وعنوان IP معين. يجب أن نتذكر هنا أن الجهاز لديه عنوانين على الأقل IP: العنوان "127.0.0.1" وهو عنوانه الداخلي، والعنوان "I1.I2.I3.I4" الذي يمتلكه على الشبكة التي يتصل بها. قد يكون لها عناوين أخرى IP إذا كانت متصلة بعدة شبكات IP. يشير IPAddress.Any إلى جميع عناوين IP لجهاز ما.
- السطر 16: تبدأ خدمة الاستماع. كانت قد تم إنشاؤها مسبقًا ولكنها لم تكن تستمع بعد. الاستماع يعني انتظار طلبات العملاء.
- الأسطر 20-26: تتكرر حلقة انتظار طلب العميل / خدمة العميل لكل عميل جديد
- السطر 22: يتم قبول طلب أحد العملاء. تعرض الطريقة AcceptTcpClient مثيل TcpClient يُسمى الخدمة:
- قدم العميل طلبه باستخدام مثيله الخاص TcpClient على جانب العميل والذي سنسميه TcpClientDemande
- يقبل الخادم هذا الطلب باستخدام AcceptTcpClient. تنشئ هذه الطريقة مثيل TcpClient على جانب الخادم، والذي سنسميه TcpClientService. وبذلك يكون لدينا اتصال TCP مفتوح بين المثيلين TcpClientDemande <--> TcpClientService.
- يتم الاتصال بين العميل والخادم الذي يحدث بعد ذلك عبر هذا الاتصال. لا يتدخل خدمة الاستماع بعد الآن.
- السطر 24: حتى يتمكن الخادم من معالجة عدة عملاء في وقت واحد، يتم توفير الخدمة بواسطة مؤشرات ترابط، مؤشر ترابط واحد لكل عميل.
- السطر 32: يتم إغلاق خدمة الاستماع
- السطر 38: الطريقة التي تنفذها مؤشر الترابط لخدمة عميل. تتلقى كمعلمة مثيل TcpClient المتصل بالفعل بالعميل الذي يجب خدمته.
- الأسطر 38-71: نجد هنا كودًا مشابهًا لكود العميل Tcp الأساسي الذي درسناه سابقًا.
11.6. أمثلة على عملاء/خوادم TCP
11.6.1. خادم صدى
نقترح كتابة خادم صدى سيتم تشغيله من نافذة DOS بواسطة الأمر:
ServeurEcho port
يعمل الخادم على المنفذ الذي تم تمريره كمعلمة. ويكتفي بإعادة إرسال الطلب الذي أرسله العميل إليه. البرنامج هو التالي:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
// استدعاء: serveurEcho المنفذ
// خادم الصدى
// يعيد إلى العميل السطر الذي أرسله إليه
namespace Chap9 {
public class ServeurEcho {
public const string syntaxe = "Syntaxe : [serveurEcho] port";
// البرنامج الرئيسي
public static void Main(string[] args) {
// هل هناك حجة؟
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// يجب أن تكون هذه الحجة عددًا صحيحًا >0
int port = 0;
if (!int.TryParse(args[0], out port) || port<=0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
return;
}
// يتم إنشاء خدمة الاستماع
TcpListener ecoute = null;
int numClient = 0; // رقم العميل التالي
try {
// يتم إنشاء الخدمة - ستستمع على جميع واجهات الشبكة الخاصة بالجهاز
ecoute = new TcpListener(IPAddress.Any, port);
// يتم تشغيلها
ecoute.Start();
// المتابعة
Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
// خيوط الخدمة
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// حلقة الخدمة
TcpClient tcpClient = null;
// حلقة لا نهائية - سيتم إيقافها بواسطة Ctrl-C
while (true) {
// في انتظار عميل
tcpClient = ecoute.AcceptTcpClient();
// يتم توفير الخدمة بواسطة مهمة أخرى
ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// العميل التالي
numClient++;
}
} catch (Exception ex) {
// يتم الإبلاغ عن الخطأ
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
} finally {
// نهاية الخدمة
ecoute.Stop();
}
}
// -------------------------------------------------------
// تقديم الخدمة لعميل خادم الصدى
public static void Service(Object infos) {
// استرداد العميل المطلوب خدمته
Client client = infos as Client;
// تقديم الخدمة للعميل
Console.WriteLine("Début de service au client {0}", client.NumClient);
// تشغيل الوصلة TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة قراءة الطلب/كتابة الاستجابة
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// متابعة وحدة التحكم
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// صدى الطلب إلى العميل
writer.WriteLine("[{0}]", demande);
// تتبع وحدة التحكم
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// تتوقف الخدمة عندما يرسل العميل "bye"
if (demande.Trim().ToLower() == "bye")
break;
}
}
}
}
}
} catch (Exception e) {
// خطأ
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
// نهاية العميل
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
}
// معلومات العميل
internal class Client {
public TcpClient CanalTcp { get; set; } // الاتصال بالعميل
public int NumClient { get; set; } // رقم العميل
}
}
تتوافق بنية خادم الإيكو مع البنية الأساسية لخوادم Tcp التي تم عرضها سابقًا. لن نعلق إلا على الجزء الخاص بـ "خدمة العميل":
- السطر 79: يتم قراءة طلب العميل
- السطر 83: يتم إرساله إلى العميل محاطًا بأقواس
- السطر 79: تتوقف الخدمة عندما يغلق العميل الاتصال
في نافذة DOS، نستخدم الملف القابل للتنفيذ لمشروع C#:
ثم نقوم بتشغيل عميلين putty ونقوم بتوصيلهما بالمنفذ 100 للجهاز localhost:
![]() |
يصبح عرض وحدة التحكم لخادم الصدى كما يلي:
يرسل العميل 1 ثم العميل 0 النصوص التالية:
![]() |
- [1]: العميل رقم 1
- [2]: العميل رقم 0
- [3]: وحدة التحكم في خادم الصدى
![]() |
- في [4]: العميل 1 ينفصل باستخدام الأمر bye.
- في [5]: يكتشف الخادم ذلك
يمكن إيقاف الخادم بواسطة Ctrl-C. ثم يكتشفه العميل رقم 0 [6].
11.6.2. عميل لخادم الصدى
نكتب الآن عميلاً للخادم السابق. سيتم استدعاؤه على النحو التالي:
ClientEcho nomServeur port
يتصل بالجهاز nomServeur على المنفذ port ثم يرسل إلى الخادم أسطر نصية يعيدها الخادم إليه كصدى.
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
// الاتصال بخادم الصدى
// يتم استلام أي سطر يتم كتابته على لوحة المفاتيح كصدى
class ClientEcho {
static void Main(string[] args) {
// الصياغة
const string syntaxe = "pg machine port";
// عدد الحجج
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// يتم تدوين اسم الخادم
string serveur = args[0];
// يجب أن يكون المنفذ عددًا صحيحًا >0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// يمكن العمل
string demande = null; // طلب العميل
string réponse = null; // استجابة الخادم
try {
// يتم الاتصال بالخدمة
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة الطلب - الاستجابة
while (true) {
// الطلب قادم من لوحة المفاتيح
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// انتهى؟
if (demande.Trim().ToLower() == "bye")
break;
// إرسال الطلب إلى الخادم
writer.WriteLine(demande);
// قراءة الرد من الخادم
réponse = reader.ReadLine();
// جاري معالجة الرد
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
// خطأ
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
تتوافق بنية هذا العميل مع البنية الأساسية العامة المقترحة لعملاء Tcp. فيما يلي النتائج التي تم الحصول عليها في التكوين التالي:
- يتم تشغيل الخادم على المنفذ 100 في نافذة DOS
- على نفس الجهاز، يتم تشغيل عميلين في نافذتي DOS أخريين
في نافذة العميل A (رقم 0) تظهر العروض التالية:
في نافذة العميل B (رقم 1):
في نافذة الخادم:
العميل A رقم 0 ينقطع اتصاله:
وحدة التحكم في الخادم:
11.6.3. عميل عام TCP
سنقوم بكتابة عميل Tcp عام سيتم تشغيله على النحو التالي: ClientTcpGenerique منفذ الخادم. سيكون عمله مشابهًا لعمل عميل putty ولكنه سيحتوي على واجهة وحدة تحكم ولن يقدم خيارات التكوين.
في التطبيق السابق، كان بروتوكول الحوار معروفًا: كان العميل يرسل سطرًا واحدًا ويستجيب الخادم بسطر واحد. لكل خدمة بروتوكولها الخاص، كما توجد الحالات التالية:
- يجب على العميل إرسال عدة أسطر من النص قبل الحصول على رد
- قد تتضمن استجابة الخادم عدة أسطر من النص
لذلك، فإن دورة إرسال سطر واحد إلى الخادم / استلام سطر واحد مرسل من الخادم، لا تناسب جميع الحالات. لإدارة البروتوكولات الأكثر تعقيدًا من بروتوكول echo، سيحتوي عميل Tcp العام على مؤشرين:
- سيقوم الخيط الرئيسي بقراءة أسطر النص المكتوبة على لوحة المفاتيح وإرسالها إلى الخادم.
- سيعمل مؤشر ترابط ثانوي بالتوازي وسيخصص لقراءة أسطر النص المرسلة من الخادم. بمجرد استلامه لأحدها، يعرضها على وحدة التحكم. لا يتوقف مؤشر الترابط إلا عندما يغلق الخادم الاتصال. وبالتالي، فهو يعمل بشكل مستمر.
والرمز هو كما يلي:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
// يستقبل كمعلمة خصائص الخدمة بالشكل التالي: الخادم المنفذ
// يتصل بالخدمة
// يرسل إلى الخادم كل سطر يتم كتابته على لوحة المفاتيح
// ينشئ مؤشر ترابط لقراءة الأسطر النصية المرسلة من الخادم بشكل مستمر
class ClientTcpGenerique {
static void Main(string[] args) {
// الصياغة
const string syntaxe = "pg serveur port";
// عدد الحجج
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// يتم تدوين اسم الخادم
string serveur = args[0];
// يجب أن يكون المنفذ عددًا صحيحًا >0
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// يتم الاتصال بالخدمة
TcpClient tcpClient = null;
try {
tcpClient = new TcpClient(serveur, port);
} catch (Exception ex) {
// خطأ
Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
// النهاية
return;
}
// يتم تشغيل مؤشر ترابط منفصل لقراءة أسطر النص المرسلة من الخادم
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// يتم قراءة أوامر لوحة المفاتيح في الخيط الرئيسي
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string demande = null; // طلب من العميل
try {
// يتم استغلال اتصال العميل
using (tcpClient) {
// يتم إنشاء تدفق كتابة إلى الخادم
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة الطلب - الاستجابة
while (true) {
demande = Console.ReadLine();
// انتهى؟
if (demande.Trim().ToLower() == "bye")
break;
// إرسال الطلب إلى الخادم
writer.WriteLine(demande);
}
}
}
}
} catch (Exception e) {
// خطأ
Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
}
}
// مؤشر ترابط قراءة العميل <-- الخادم
public static void Receive(object infos) {
// بيانات محلية
string réponse = null; // استجابة الخادم
// إنشاء تدفق الإدخال
try {
using (TcpClient tcpClient = infos as TcpClient) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// حلقة قراءة مستمرة لسطور النص من تدفق الإدخال
while ((réponse = reader.ReadLine()) != null) {
// عرض وحدة التحكم
Console.WriteLine("<-- {0}", réponse);
}
}
}
}
} catch (Exception ex) {
// خطأ
Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// الإبلاغ عن انتهاء مؤشر ترابط القراءة
Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
}
}
}
}
- السطر 34: يتصل العميل بالخادم
- السطر 43: يتم تشغيل مؤشر ترابط لقراءة أسطر النص من الخادم. يجب أن ينفذ الطريقة Receive في السطر 73. يتم تمرير المثيل TcpClient الذي تم توصيله بالخادم إلى هذه الطريقة.
- الأسطر 57-64: حلقة إدخال أوامر لوحة المفاتيح / إرسال الأوامر إلى الخادم. يتم إدخال أوامر لوحة المفاتيح بواسطة الخيط الرئيسي.
- الأسطر 75-98: الطريقة Receive التي يتم تنفيذها بواسطة مؤشر الترابط الخاص بقراءة أسطر النص. تتلقى هذه الطريقة كمعلمة المثيل TcpClient الذي تم توصيله بالخادم.
- الأسطر 84-87: حلقة مستمرة لقراءة أسطر النص المرسلة من الخادم. لا تتوقف هذه الحلقة إلا عندما يغلق الخادم الاتصال المفتوح مع العميل.
فيما يلي بعض الأمثلة التي تكرر تلك المستخدمة مع العميل putty في الفقرة 11.4. يتم تنفيذ العميل في وحدة تحكم DOS.
بروتوكول HTTP
يُرجى من القارئ الرجوع إلى التفسيرات الواردة في الفقرة 11.4.2. ونحن نقتصر في تعليقاتنا على ما يتعلق بالتطبيق تحديدًا:
- السطر 28: بعد إرسال السطر 27، أغلق الخادم HTTP الاتصال، مما تسبب في إنهاء مؤشر ترابط القراءة. أما مؤشر الترابط الرئيسي الذي يقرأ الأوامر المكتوبة على لوحة المفاتيح فهو لا يزال نشطًا. ويقوم الأمر الموجود في السطر 29، المكتوب على لوحة المفاتيح، بإيقافه.
بروتوكول SMTP
يُطلب من القارئ إعادة قراءة الشروحات الواردة في الفقرة 11.4.3 واختبار الأمثلة الأخرى المستخدمة مع العميل putty.
11.6.4. ، خادم TCP عام
ننتقل الآن إلى خادم
- يعرض على الشاشة الأوامر المرسلة من قبل عملائه
- ويرسل إليهم كردّ الأسطر النصية التي يكتبها المستخدم على لوحة المفاتيح. وبالتالي، فإن هذا الأخير هو الذي يقوم بدور الخادم.
يتم تشغيل البرنامج في نافذة DOS بواسطة: ServeurTcpGenerique portEcoute، حيث portEcoute هو المنفذ الذي يجب على العملاء الاتصال به. سيتم توفير الخدمة للعميل بواسطة خيطين:
- الخيط الرئيسي الذي:
- يعالج العملاء واحدًا تلو الآخر وليس بشكل متوازٍ.
- يقرأ الأسطر التي يكتبها المستخدم على لوحة المفاتيح ويرسلها إلى العميل. سيشير المستخدم باستخدام الأمر bye إلى أنه يغلق الاتصال مع العميل. ونظرًا لأن وحدة التحكم لا يمكن استخدامها لعميلين في وقت واحد، فإن خادمنا لا يعالج سوى عميل واحد في كل مرة.
- خيط ثانوي مخصص حصريًا لقراءة سطور النص المرسلة من العميل
أما الخادم فلا يتوقف أبدًا إلا بضغط المستخدم على Ctrl-C على لوحة المفاتيح.
لنلقِ نظرة على بعض الأمثلة. يتم تشغيل الخادم على المنفذ 100 ونستخدم العميل العام لـ paragraphe11.6.3 للتواصل معه. نافذة العميل هي كما يلي:
السطور التي تبدأ بـ <-- هي تلك المرسلة من الخادم إلى العميل، أما السطور الأخرى فهي تلك المرسلة من العميل إلى الخادم. نافذة الخادم هي كما يلي:
السطور التي تبدأ بـ <-- هي تلك المرسلة من العميل إلى الخادم، أما البقية فهي تلك المرسلة من الخادم إلى العميل. تشير السطر 9 إلى أن مؤشر ترابط قراءة طلبات العميل قد توقف. لا يزال مؤشر ترابط الخادم الرئيسي في انتظار الأوامر المكتوبة على لوحة المفاتيح لإرسالها إلى العميل. لذا، يجب كتابة الأمر bye في السطر 10 على لوحة المفاتيح للانتقال إلى العميل التالي. لا يزال الخادم نشطًا في حين أن العميل 1 قد انتهى. يتم تشغيل عميل ثانٍ لنفس الخادم:
تصبح نافذة الخادم كما يلي:
بعد السطر 6 أعلاه، أصبح الخادم في حالة انتظار عميل جديد. يمكن إيقافه باستخدام Ctrl-C.
لنحاكي الآن خادم ويب عن طريق تشغيل خادمنا العام على المنفذ 88:
لنستخدم الآن متصفحًا ونطلب http://localhost:88/exemple.html. سيتصل المتصفح عندئذ بالمنفذ 88 الخاص بالجهاز localhost ثم يطلب الصفحة /exemple.html:
![]() |
لنلقِ نظرة الآن على نافذة الخادم الخاص بنا:
نكتشف الرؤوس HTTP المرسلة من المتصفح. وهذا يسمح لنا باكتشاف رؤوس HTTP أخرى غير تلك التي سبق أن صادفناها. لنقم بإعداد رد لعميلنا. المستخدم الذي يستخدم لوحة المفاتيح هو هنا الخادم الحقيقي ويمكنه إعداد رد يدويًا. لنتذكر الرد الذي قدمه خادم الويب في المثال السابق:
دعونا نحاول تقديم رد مماثل مع الالتزام بالحد الأدنى:
لقد اقتصرنا في ردنا على الرؤوس HTTP في الأسطر 1-4. لا نذكر حجم المستند الذي سنرسله (Content-Length) بل نكتفي بالقول إننا سنغلق الاتصال (Connection: close) بعد إرساله. وهذا يكفي للمتصفح. عند رؤية الاتصال مغلقًا، سيعرف أن استجابة الخادم قد انتهت وسيعرض الصفحة HTML التي تم إرسالها إليه. هذه الأخيرة هي تلك الموجودة في الأسطر 6-9. يقوم المستخدم بعد ذلك بإغلاق الاتصال بالعميل عن طريق لوحة المفاتيح بكتابة الأمر bye، السطر 10. عند هذا الأمر من لوحة المفاتيح، يقوم الخيط الرئيسي بإغلاق الاتصال بالعميل. يؤدي هذا إلى استثناء السطر 11. تم قطع خيط قراءة أسطر النص من العميل بشكل مفاجئ بسبب إغلاق الاتصال بالعميل وأطلق استثناءً. بعد السطر 12، ينتظر الخادم وصول عميل جديد.
يعرض متصفح العميل الآن ما يلي:
![]() |
إذا قمنا في ما سبق بإجراء Affichage/Source لمعرفة ما تلقّاه المتصفح، نحصل على [2]، أي بالضبط ما أرسلناه من الخادم العام.
رمز الخادم العام TCP هو التالي:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
public class ServeurTcpGenerique {
public const string syntaxe = "Syntaxe : ServeurGénérique Port";
// البرنامج الرئيسي
public static void Main(string[] args) {
// هل هناك حجة؟
if (args.Length != 1) {
Console.WriteLine(syntaxe);
Environment.Exit(1);
}
// يجب أن تكون هذه الحجة عددًا صحيحًا >0
int port = 0;
if (!int.TryParse(args[0], out port) || port <= 0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
Environment.Exit(2);
}
// يتم إنشاء خدمة الاستماع
TcpListener ecoute = null;
try {
// يتم إنشاء الخدمة
ecoute = new TcpListener(IPAddress.Any, port);
// يتم تشغيلها
ecoute.Start();
// المتابعة
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// في انتظار عميل
Console.WriteLine("Attente du client suivant...");
TcpClient tcpClient = ecoute.AcceptTcpClient();
Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
// يتم تشغيل مؤشر ترابط منفصل لقراءة أسطر النص المرسلة من العميل
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// تتم قراءة أوامر لوحة المفاتيح في الخيط الرئيسي
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string réponse = null; // استجابة الخادم
// استغلال اتصال العميل
using (tcpClient) {
// يتم إنشاء تدفق كتابة إلى العميل
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// حلقة إدخال الردود من لوحة المفاتيح
while (true) {
réponse = Console.ReadLine();
// انتهى؟
if (réponse.Trim().ToLower() == "bye")
break;
// إرسال الطلب إلى العميل
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// الإبلاغ عن الخطأ
Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// نهاية الاستماع
ecoute.Stop();
}
}
// مؤشر ترابط القراءة الخادم <-- العميل
public static void Receive(object infos) {
// البيانات المحلية
string demande = null; // طلب من العميل
string idClient=null; // هوية العميل
// استغلال اتصال العميل
try {
using (TcpClient tcpClient = infos as TcpClient) {
// هوية العميل
idClient = tcpClient.Client.RemoteEndPoint.ToString();
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// حلقة قراءة متواصلة لسطور النص في تدفق الإدخال
while ((demande = reader.ReadLine()) != null) {
// عرض وحدة التحكم
Console.WriteLine("<-- {0}", demande);
}
}
}
}
} catch (Exception ex) {
// خطأ
Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
} finally {
// الإبلاغ عن نهاية مؤشر ترابط القراءة
Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
}
}
}
}
- السطر 29: تم إنشاء خدمة الاستماع ولكن لم يتم تشغيلها. وهي تستمع إلى جميع واجهات الشبكة الخاصة بالجهاز.
- السطر 31: تم تشغيل خدمة الاستماع
- السطر 34: حلقة انتظار لا نهائية للعملاء. سيقوم المستخدم بإيقاف الخادم باستخدام Ctrl-C.
- السطر 37: انتظار عميل - عملية معطلة. عند وصول العميل، تمثل المثيل TcpClient الذي تم إنشاؤه بواسطة الطريقة AcceptTcpClient جانب الخادم في اتصال مفتوح مع العميل.
- السطر 40: يتم تكليف مؤشر ترابط منفصل بتدفق قراءة طلبات العميل.
- السطر 45: استخدام الاتصال بالعميل في جملة using للتأكد من إغلاقه مهما حدث.
- السطر 47: استخدام تدفق الشبكة في جملة using
- السطر 48: إنشاء تدفق كتابة على تدفق الشبكة في جملة using
- السطر 50: لن يتم تخزين تدفق الكتابة مؤقتًا
- الأسطر 52-59: حلقة إدخال الأوامر المراد إرسالها إلى العميل عبر لوحة المفاتيح
- السطر 69: نهاية خدمة الاستماع. لن يتم تنفيذ هذه التعليمات هنا أبدًا لأن الخادم يتم إيقافه بواسطة Ctrl-C.
- السطر 78: الطريقة Receive التي تعرض بشكل مستمر على وحدة التحكم أسطر النص المرسلة من العميل. نجد هنا ما سبق ذكره بالنسبة للعميل العام TCP.
11.6.5. ، عميل ويب
لقد رأينا في المثال السابق بعض رؤوس HTTP التي أرسلها متصفح:
سنقوم بكتابة عميل ويب يتم تمرير معلمة URL إليه ويقوم بعرض النص المرسل من الخادم على الشاشة. سنفترض أن هذا الخادم يدعم بروتوكول HTTP 1.1. من الرؤوس السابقة، لن نستخدم سوى ما يلي:
- تشير الرأس الأولى إلى المستند المطلوب
- والثاني يشير إلى الخادم الذي تم الاستعلام عنه
- الثالث أننا نرغب في أن يغلق الخادم الاتصال بعد الرد علينا.
إذا استبدلنا في السطر 1 أعلاه GET بـ HEAD، فلن يرسل لنا الخادم سوى الرؤوس HTTP وليس المستند المحدد في السطر 1.
سيتم استدعاء عميل الويب الخاص بنا على النحو التالي: ClientWeb URL cmd، حيث URL هوURL المطلوب و cmd هي إحدى الكلمتين الرئيسيتين GET أو HEAD للإشارة إلى ما إذا كنا نريد الرؤوس فقط (HEAD) أو محتوى الصفحة أيضًا (GET). لنلقِ نظرة على مثال أول:
- السطر 1، نطلب الرؤوس فقط HTTP (HEAD)
- الأسطر 2-9: استجابة الخادم
إذا استخدمنا GET بدلاً من HEAD في استدعاء عميل الويب، فسنحصل على نفس النتيجة التي نحصل عليها مع HEAD بالإضافة إلى نص المستند المطلوب.
رمز عميل الويب هو كما يلي:
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
class ClientWeb {
static void Main(string[] args) {
// الصياغة
const string syntaxe = "pg URI GET/HEAD";
// عدد الحجج
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// يُلاحظ أن URI مطلوب
string stringURI = args[0];
string commande = args[1].ToUpper();
// التحقق من صحة URI
if(! stringURI.StartsWith("http://")){
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI غير صحيح
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// التحقق من الطلب
if (commande != "GET" && commande != "HEAD") {
// الطلب غير صحيح
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// يتم الاتصال بالخدمة
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// طلب URL - إرسال الرؤوس HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// قراءة الرد
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// عرض الرد على وحدة التحكم
Console.WriteLine(réponse);
}
}
}
}
}
} catch (Exception e) {
// يتم عرض الاستثناء
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
الشيء الجديد الوحيد في هذا البرنامج هو استخدام فئة Uri. يتلقى البرنامج URL (محدد موقع الموارد الموحد) أو URI (معرف الموارد الموحد) بالصيغة http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... تسمح لنا فئة Uri بتفكيك سلسلة URL إلى عناصرها المختلفة.
- الأسطر 26-33: يتم إنشاء كائن Uri من السلسلة stringURI المستلمة كمعلمة. إذا كانت السلسلة URI المستلمة كمعلمة غير صالحة (عدم وجود البروتوكول أو الخادم، ...)، يتم إصدار استثناء. وهذا يسمح لنا بالتحقق من صحة المعلمة المستلمة. بمجرد إنشاء الكائن Uri، يمكننا الوصول إلى العناصر المختلفة لهذا Uri. وبالتالي، إذا تم إنشاء الكائن uri من الكود السابق من السلسلة http://serveur:port/document?param1=val1¶m2=val2;...، فسيكون لدينا:
- uri.Host=serveur،
- uri.Port=port،
- uri.Path=document،
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. عميل ويب يدير عمليات إعادة التوجيه
لا يدعم عميل الويب السابق أي إعادة توجيه محتملة لـ URL الذي طلبه. فيما يلي مثال:
- السطر 2: يشير الرمز 302 Found إلى إعادة توجيه. العنوان الذي يجب أن يُعاد توجيه المتصفح إليه موجود في نص المستند، السطر 16.
مثال ثانٍ:
- السطر 2: يشير الرمز 301 Moved Permanently إلى إعادة توجيه. العنوان الذي يجب أن يعيد المتصفح التوجيه إليه مذكور في السطر 6، في رأس HTTP Location.
مثال ثالث:
- السطر 2: يشير الرمز 302 Moved Temporarily إلى إعادة توجيه. العنوان الذي يجب أن يعيد المتصفح التوجيه إليه مذكور في السطر 5، في رأس HTTP Location.
مثال رابع مع خادم IIS محلي على الجهاز:
- السطر 2: يشير الرمز 302 Object moved إلى إعادة توجيه. العنوان الذي يجب أن يعيد المتصفح التوجيه إليه مذكور في السطر 5، في رأس HTTP Location. تجدر الإشارة إلى أنه على عكس الأمثلة السابقة، فإن عنوان إعادة التوجيه نسبي. العنوان الكامل هو في الواقع http://localhost/localstart.asp.
نقترح إدارة عمليات إعادة التوجيه عندما يحتوي السطر الأول من الرؤوس HTTP على الكلمة الرئيسية moved (بغض النظر عن حالة الأحرف) وعندما يكون عنوان إعادة التوجيه موجودًا في الرأس HTTP Location.
إذا استخدمنا الأمثلة الثلاثة الأخيرة، فسنحصل على النتائج التالية:
عنوان URL: http://www.bull.com
- السطر 11: تتم إعادة التوجيه إلى العنوان الموجود في السطر 6
عنوان URL: http://www.gouv.fr
- السطر 11: يتم إعادة التوجيه إلى عنوان السطر 6
عنوان URL: http://localhost
- السطر 13: يتم إعادة التوجيه إلى عنوان السطر 6
- السطر 15: تم رفض وصولنا إلى الصفحة http://localhost/localstart.asp.
البرنامج الذي يدير إعادة التوجيه هو التالي:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Chap9 {
class ClientWebAvecRedirection {
static void Main(string[] args) {
// الصياغة
const string syntaxe = "pg URI GET/HEAD";
// عدد الحجج
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// يُلاحظ أن URI مطلوب
string stringURI = args[0];
string commande = args[1].ToUpper();
// التحقق من صحة URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI غير صحيح
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// التحقق من الطلب
if (commande != "GET" && commande != "HEAD") {
// الطلب غير صحيح
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
const int nbRedirsMax = 1; // لا يُسمح بأكثر من إعادة توجيه واحدة
int nbRedirs = 0; // عدد عمليات إعادة التوجيه الجارية
// تعبير عادي للعثور على إعادة توجيه URL
Regex location = new Regex(@"^Location: (.+?)$");
try {
// يمكن أن يكون هناك عدة URL لطلبها في حالة وجود عمليات إعادة توجيه
while (nbRedirs <= nbRedirsMax) {
// إدارة إعادة التوجيه
bool redir = false;
bool locationFound = false;
string locationString = null;
// يتم الاتصال بالخدمة
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// يتم طلب URL - إرسال الرؤوس HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// قراءة السطر الأول من الرد
string premièreLigne = reader.ReadLine();
// صدى الشاشة
Console.WriteLine(premièreLigne);
// إعادة توجيه؟
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
// هناك إعادة توجيه
redir = true;
nbRedirs++;
}
// رؤوس HTTP التالية حتى العثور على السطر الفارغ الذي يشير إلى نهاية الرؤوس
string réponse = null;
while ((réponse = reader.ReadLine()) != "") {
// يتم عرض الرد
Console.WriteLine(réponse);
// إذا كان هناك إعادة توجيه، يتم البحث عن رأس Location
if (redir && !locationFound) {
// نقارن السطر الحالي بالتعبير العلائقي location
Match résultat = location.Match(réponse);
if (résultat.Success) {
// إذا تم العثور عليه، يتم تدوين URL الخاص بإعادة التوجيه
locationString = résultat.Groups[1].Value;
// يتم تسجيل أنه تم العثور على
locationFound = true;
}
}
}
// تم استنفاد الرؤوس HTTP - نكتب السطر فارغًا
Console.WriteLine(réponse);
// ثم ننتقل إلى نص الوثيقة
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// هل انتهينا؟
if (!locationFound || nbRedirs > nbRedirsMax)
break;
// هناك إعادة توجيه يجب إجراؤها - نقوم بإنشاء عنوان Uri الجديد
try {
if (locationString.StartsWith("http")) {
// عنوان http الكامل
uri = new Uri(locationString);
} else {
// عنوان http النسبي لـ Uri الحالي
uri = new Uri(uri, locationString);
}
// سجل وحدة التحكم
Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
} catch (Exception ex) {
// مشكلة مع عنوان URI
Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
}
}
} catch (Exception e) {
// عرض الاستثناء
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
بالنسبة للإصدار السابق، التغييرات هي كما يلي:
- السطر 46: التعبير العادي لاسترداد عنوان إعادة التوجيه في الرأس HTTP Location: address.
- السطر 49: يمكن الآن تنفيذ الكود الذي كان يُنفذ سابقًا لعنوان Uri واحد على التوالي لعدة عناوين Uri.
- السطر 66: يتم قراءة السطر الأول من رؤوس HTTP المرسلة من الخادم. وهو الذي يحتوي على الكلمة الرئيسية moved إذا تم نقل المستند المطلوب.
- الأسطر 71-75: يتم التحقق مما إذا كان السطر الأول يحتوي على الكلمة الرئيسية moved. إذا كان الأمر كذلك، يتم تدوينها.
- الأسطر 79-93: قراءة الرؤوس الأخرى HTTP حتى الوصول إلى السطر الفارغ الذي يشير إلى نهايتها. إذا كان السطر الأول يعلن عن إعادة توجيه، فإننا نركز عندئذٍ على العنوان HTTP Location: address لتخزين عنوان إعادة التوجيه في locationString.
- الأسطر 98-100: يتم عرض بقية استجابة الخادم HTTP على وحدة التحكم.
- الأسطر 105-106: تم استغلال عنوان Uri المطلوب بالكامل وعرضه. إذا لم يكن هناك إعادة توجيه أو إذا تم تجاوز عدد عمليات إعادة التوجيه المسموح بها، يتم الخروج من البرنامج.
- الأسطر 108-122: إذا كان هناك إعادة توجيه، يتم حساب عنوان Uri الجديد المطلوب. هناك بعض العمليات الحسابية التي يجب القيام بها اعتمادًا على ما إذا كان عنوان إعادة التوجيه الذي تم العثور عليه مطلقًا (السطر 111) أو نسبيًا (السطر 114).
11.7. فئات .NET المتخصصة في بروتوكول معين على الإنترنت
في الأمثلة السابقة للعميل الويب، تمت إدارة بروتوكول HTTP باستخدام عميل TCP. لذلك كان علينا إدارة بروتوكول الاتصال المحدد المستخدم بأنفسنا. كان بإمكاننا إنشاء عميل SMTP أو POP بطريقة مماثلة. يوفر إطار العمل .NET فئات متخصصة لبروتوكولي HTTP و SMTP. تعرف هذه الفئات بروتوكول الاتصال بين العميل والخادم وتجنب على المطور الحاجة إلى إدارتها. سنقدمها الآن.
11.7.1. classeWebClient
توجد فئة WebClient قادرة على التواصل مع خادم الويب. لنأخذ مثال عميل الويب الوارد في الفقرة 11.6.5، والذي تمت معالجته هنا باستخدام الفئة WebClient.
using System;
using System.IO;
using System.Net;
namespace Chap9 {
public class Program {
public static void Main(string[] args) {
// الصيغة: [prog] Uri
const string syntaxe = "pg URI";
// عدد الحجج
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// يتم تسجيل URI المطلوب
string stringURI = args[0];
// التحقق من صحة URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI غير صحيح
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
try {
// إنشاء عميل ويب
using (WebClient client = new WebClient()) {
// إضافة رأس HTTP
client.Headers.Add("user-agent", "st");
using (Stream stream = client.OpenRead(uri)) {
using (StreamReader reader = new StreamReader(stream)) {
// عرض استجابة خادم الويب
Console.WriteLine(reader.ReadToEnd());
// عرض رؤوس استجابة الخادم
Console.WriteLine("---------------------");
foreach (string clé in client.ResponseHeaders.Keys) {
Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
}
Console.WriteLine("---------------------");
}
}
}
} catch (WebException e1) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
} catch (Exception e2) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
}
}
}
}
- السطر 35: تم إنشاء عميل الويب ولكن لم يتم تكوينه بعد
- السطر 37: نضيف رأس HTTP إلى الطلب HTTP الذي سيتم إجراؤه. سنكتشف أن رؤوسًا أخرى سيتم إرسالها بشكل افتراضي.
- السطر 38: يطلب عميل الويب عنوان URI الذي أدخله المستخدم ويقوم بقراءة المستند المرسل. تفتح الدالة [WebClient].OpenRead(Uri) الاتصال مع Uri وتقوم بقراءة الرد. وهنا تكمن فائدة هذه الفئة. فهي تتولى الحوار مع خادم الويب. تكون نتيجة طريقة OpenRead من النوع Stream وتمثل المستند المطلوب. ولا تشكل الرؤوس HTTP المرسلة من الخادم والتي تسبق المستند في الرد جزءًا منه.
- السطر 39: يتم استخدام StreamReader والسطر 41، وطريقتها ReadToEnd لقراءة الرد بالكامل.
- الأسطر 44-46: يتم عرض رؤوس HTTP من استجابة الخادم. يمثل [WebClient].ResponseHeaders مجموعة ذات قيم، حيث تكون المفاتيح هي أسماء رؤوس HTTP والقيم هي سلاسل الأحرف المرتبطة بهذه الرؤوس.
- السطر 51: الاستثناءات التي يتم رفعها أثناء تبادل بين العميل والخادم هي من النوع WebException.
لنلقِ نظرة على بعض الأمثلة.
نقوم بتشغيل الخادم العام TCP الذي تم إنشاؤه في الفقرة 6.4.6:
نقوم بتشغيل عميل الويب السابق بالطريقة التالية:
عنوان URI المطلوب هو عنوان الخادم العام. ثم يعرض هذا الخادم الرؤوس HTTP التي أرسلها إليه عميل الويب:
ونرى بذلك:
- أن عميل الويب يرسل 3 رؤوس HTTP بشكل افتراضي (الأسطر 3 و5 و6)
- السطر 4: الرأس الذي قمنا بإنشائه بأنفسنا (السطر 37 من الكود)
- أن عميل الويب يستخدم بشكل افتراضي الطريقة GET (السطر 3). هناك طرق أخرى من بينها POST و HEAD.
الآن لنطلب موردًا غير موجود:
- السطر 2: حصلنا على استثناء من النوع WebException لأن الخادم رد برمز 404 Not Found للإشارة إلى أن المورد المطلوب غير موجود.
أخيرًا، لننهي الأمر بطلب مورد موجود:
الملف istia.univ-angers.txt الذي أنتجته الأوامر هو التالي:
- السطر 1: المستند HTML المطلوب.
- الأسطر 3-10: رؤوس الاستجابة HTTP بترتيب ليس بالضرورة هو الترتيب الذي تم إرسالها به.
تحتوي الفئة WebClient على طرق تسمح باستلام مستند (طرق DownLoad) أو إرساله (طرق UpLoad):
لتنزيل مورد كجدول بايت (صورة على سبيل المثال) | |
لتنزيل مورد وحفظه في ملف محلي | |
لتنزيل مورد واسترداده كسلسلة أحرف (ملف html على سبيل المثال) | |
مكافئ OpenRead ولكن لإرسال البيانات إلى الخادم | |
نظير DownLoadData ولكن إلى الخادم | |
مكافئ لـ DownLoadFile ولكن إلى الخادم | |
نظير DownLoadString ولكن باتجاه الخادم | |
لإرسال بيانات أمر إلى الخادم POST واسترداد النتائج في شكل جدول بايت. يطلب الأمر POST مستندًا مع إرسال المعلومات اللازمة للخادم لتحديد المستند الفعلي المراد إرساله. يتم إرسال هذه المعلومات كوثيقة إلى الخادم، ومن هنا جاء اسم الطريقة UpLoad. يتم إرسالها خلف السطر الفارغ من الرؤوس HTTP في شكل param1=valeur1¶m2=valeur2&...:
يمكن طلب نفس المستند باستخدام الطريقة GET:
يكمن الاختلاف بين الطريقتين في أن المتصفح الذي يعرض عنوان URI المطلوب سيعرض /document في حالة POST، و /document?param1=valeur1¶m2=valeur2&... في حالة GET. |
11.7.2. الفئات WebRequest / WebResponse
في بعض الأحيان، لا تكون الفئة WebClient مرنة بما يكفي للقيام بما نريده. لنعد إلى مثال عميل الويب مع إعادة التوجيه الذي تمت دراسته في الفقرة 11.6.6. نحتاج إلى إرسال الرأس HTTP:
لقد رأينا أن الرؤوس HTTP التي يصدرها عميل الويب افتراضيًا هي التالية:
كما رأينا أنه من الممكن إضافة رؤوس HTTP إلى ما سبق باستخدام المجموعة [WebClient].Headers. فقط السطر 1 ليس رأسًا ينتمي إلى المجموعة Headers لأنه لا يتخذ شكل المفتاح: القيمة. لم أجد طريقة لتغيير GET إلى HEAD في السطر 1 انطلاقًا من الفئة WebClient (ربما لم أبحث جيدًا؟). عندما تصل الفئة WebClient إلى حدودها القصوى، يمكن الانتقال إلى الفئات WebRequest / WebResponse:
- WebRequest: تمثل كامل طلب عميل الويب.
- WebResponse: تمثل كامل استجابة خادم الويب
لقد ذكرنا أن الفئة WebClient تدير مخططات http: و https: و ftp: و file:. لا تتخذ الطلبات والاستجابات لهذه البروتوكولات المختلفة الشكل نفسه. لذلك من الضروري التعامل مع النوع الدقيق لهذه العناصر بدلاً من نوعها العام WebRequest و WebResponse. لذلك سنستخدم الفئات:
- HttpWebRequest و HttpWebResponse لعميل HTTP
- FtpWebRequest، FtpWebResponse لعميل FTP
نعالج الآن باستخدام الفئتين HttpWebRequest و HttpWebresponse مثال عميل الويب مع إعادة التوجيه الذي تمت دراسته في الفقرة 11.6.6. الرمز هو كما يلي:
using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
namespace Chap9 {
class WebRequestResponse {
static void Main(string[] args) {
// الصياغة
const string syntaxe = "pg URI GET/HEAD";
// عدد الحجج
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// يُلاحظ أن URI مطلوب
string stringURI = args[0];
string commande = args[1].ToUpper();
// التحقق من صحة URI
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI غير صحيح
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// التحقق من الطلب
if (commande != "GET" && commande != "HEAD") {
// الطلب غير صحيح
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// يتم تكوين الطلب
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// يتم تنفيذه
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// النتيجة
Console.WriteLine("---------------------");
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
// رؤوس HTTP
Console.WriteLine("---------------------");
foreach (string clé in httpWebResponse.Headers.Keys) {
Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
}
Console.WriteLine("---------------------");
// المستند
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
// يتم عرض الرد على وحدة التحكم
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// يتم استرداد الرد
HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
} catch (Exception e2) {
// يتم عرض الاستثناء
Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
}
}
}
}
- السطر 40: يتم إنشاء كائن من النوع WebRequest باستخدام الطريقة الثابتة WebRequest.Create(Uri uri) حيث uri هو عنوان URI للوثيقة المراد تنزيلها. نظرًا لأننا نعلم أن بروتوكول Uri هو HTTP، يتم تغيير نوع النتيجة إلى HttpWebRequest من أجل الوصول إلى العناصر المحددة لبروتوكول Http.
- السطر 41: نحدد الطريقة GET / POST / HEAD من السطر الأول من الرؤوس HTTP. وهنا ستكون GET أو HEAD.
- السطر 42: في شبكة خاصة تابعة لشركة ما، غالبًا ما تكون أجهزة الشركة معزولة عن الإنترنت لأسباب أمنية. لهذا الغرض، تستخدم الشبكة الخاصة عناوين إنترنت لا تقوم أجهزة توجيه الإنترنت بتوجيهها. يتم ربط الشبكة الخاصة بالإنترنت عبر أجهزة خاصة تسمى البروكسي، وهي أجهزة متصلة في الوقت نفسه بشبكة الشركة الخاصة وبالإنترنت. وهذا مثال على أجهزة ذات عناوين متعددة مثل IP. لا يمكن لجهاز في الشبكة الخاصة أن ينشئ بنفسه اتصالاً بخادم على الإنترنت، مثل خادم ويب على سبيل المثال. بل يجب عليه أن يطلب من جهاز بروكسي القيام بذلك نيابة عنه. يمكن لجهاز proxy أن يستضيف خوادم proxy لبروتوكولات مختلفة. يُشار إلى الوكيل HTTP للإشارة إلى الخدمة التي تتولى إجراء الطلبات HTTP نيابة عن أجهزة الشبكة الخاصة. في حالة وجود خادم بروكسي HTTP، يجب الإشارة إليه في الحقل [WebRequest].proxy. على سبيل المثال، سنكتب:
إذا كان الوكيل HTTP يعمل على المنفذ 3128 للجهاز pproxy.istia.uang. نضع null في الحقل [WebRequest].proxy إذا كان الجهاز يتمتع بوصول مباشر إلى الإنترنت ولا يحتاج إلى المرور عبر وكيل.
- السطر 44: تطلب الطريقة GetResponse() المستند المحدد بواسطة عنوان Uri الخاص به وتُرجع كائن WebRequestResponse الذي يتم تحويله هنا إلى كائن HttpWebResponse. يمثل هذا الكائن استجابة الخادم لطلب المستند.
- السطر 47:
- [HttpWebResponse].ResponseUri: هو عنوان URI للخادم الذي أرسل المستند. في حالة إعادة التوجيه، قد يختلف هذا العنوان عن عنوان URI للخادم الذي تم الاستعلام عنه في البداية. تجدر الإشارة إلى أن الكود لا يدير إعادة التوجيه. يتم إدارتها تلقائيًا بواسطة الطريقة GetResponse. مرة أخرى، هذه هي ميزة الفئات عالية المستوى مقارنة بالفئات الأساسية لبروتوكول TCP.
- [HttpWebResponse].StatusCode، [HttpWebResponse].StatusDescription تمثل السطر الأول من الرد، على سبيل المثال: HTTP/1.1 200 OK. StatusCode يساوي 200 و StatusDescription يساوي OK.
- السطر 50: [HttpWebResponse].Headers هي مجموعة رؤوس HTTP للاستجابة.
- السطر 55: [HttpWebResponse].GetResponseStream: هو التدفق الذي يسمح بالحصول على المستند الموجود في الرد.
- السطر 61: قد تحدث استثناء من النوع WebException
- السطر 63: [WebException].Response هو الرد الذي تسبب في إثارة الاستثناء.
فيما يلي مثال على التنفيذ:
- السطران 1 و 3: الخادم الذي رد ليس هو نفسه الذي تم الاستعلام عنه. لذلك حدثت إعادة توجيه.
- الأسطر 5-11: الرؤوس HTTP المرسلة من الخادم
11.7.3. التطبيق: عميل بروكسي لخادم ترجمة ويب
نوضح الآن كيف تسمح لنا الفئات السابقة باستغلال موارد الويب.
11.7.3.1. L'application
توجد مواقع ترجمة على الويب. الموقع الذي سيتم استخدامه هنا هو http://trans.voila.fr/traduction_voila.php:
![]() | يتم إدخال النص المراد ترجمته في [1]، ويتم اختيار اتجاه الترجمة في [2]. يتم طلب الترجمة بواسطة [3] ويتم الحصول عليها في [4]. |
سنقوم بكتابة تطبيق ويندوز عميل للتطبيق أعلاه. لن يقوم هذا التطبيق بأي شيء أكثر من تطبيق الموقع [trans.voila.fr]. وستكون واجهته كما يلي:
![]() |
11.7.3.2. بنية التطبيق
سيكون للتطبيق بنية من طبقتين كما يلي:
![]() |
11.7.3.3. مشروع Visual Studio
سيكون مشروع Visual Studio كما يلي:
![]() |
- في [1]، يتكون الحل من مشروعين،
- [2]: أحدهما للطبقة [dao] والكيانات التي تستخدمها،
- [3]: والآخر لواجهة Windows
11.7.3.4. المشروع [dao]
يتكون المشروع [dao] من العناصر التالية:
- IServiceTraduction.cs: الواجهة المعروضة على الطبقة [ui]
- ServiceTraduction: تنفيذ هذه الواجهة
- WebTraductionsException: استثناء خاص بالتطبيق
الواجهة IServiceTraduction هي كما يلي:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// اللغات المستخدمة
IDictionary<string, string> LanguesTraduites { get; }
// الترجمة
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- السطر 6: الخاصية LanguesTraduites تعرض قاموس اللغات التي يقبلها خادم الترجمة. يحتوي هذا القاموس على إدخالات بالشكل ["fe","Français-Anglais"] حيث تشير القيمة إلى اتجاه الترجمة، هنا من الفرنسية إلى الإنجليزية، والمفتاح "fe" هو رمز يستخدمه خادم الترجمة trans.voila.fr.
- السطر 8: الطريقة Traduire هي طريقة الترجمة:
- texte هو النص المراد ترجمته
- deQuoiVersQuoi هي إحدى مفاتيح قاموس اللغات المترجمة
- تقوم الطريقة بترجمة النص
ServiceTraduction هي فئة تنفيذ لواجهة IServiceTraduction. سنفصلها في القسم التالي.
WebTraductionsException هي فئة الاستثناء التالية:
using System;
namespace entites {
public class WebTraductionsException : Exception {
// رمز الخطأ
public int Code { get; set; }
// الشركات المصنعة
public WebTraductionsException() {
}
public WebTraductionsException(string message)
: base(message) {
}
public WebTraductionsException(string message, Exception e)
: base(message, e) {
}
}
}
- السطر 7: رمز خطأ
11.7.3.5. عميل الويب [ServiceTraduction]
لنعد إلى بنية تطبيقنا:
![]() |
الفئة [ServiceTraduction] التي يتعين علينا كتابتها هي عميل لخدمة الترجمة على الويب [trans.voila.fr]. لكتابتها، علينا أن نفهم
- ما الذي يتوقعه خادم الترجمة من عميله
- ما الذي يرسله في المقابل إلى عميله
لنلقِ نظرة على مثال للحوار بين العميل والخادم الذي يحدث أثناء عملية الترجمة. لنعد إلى المثال المقدم في مقدمة التطبيق:
![]() | يتم إدراج النص المراد ترجمته في [1]، ويتم اختيار اتجاه الترجمة في [2]. يتم طلب الترجمة بواسطة [3] ويتم الحصول عليها في [4]. |
للحصول على الترجمة [4]، أرسل المتصفح الطلب التالي GET (المعروض في حقل العنوان):
http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=ce+chien+est+malade
وهي سهلة الفهم إلى حد ما:
- http://trans.voila.fr/traduction_voila.php هو عنوان URL لخدمة الترجمة
- isText=1 يبدو أنه يشير إلى أننا نتعامل مع نص
- translationDirection يشير إلى اتجاه الترجمة، وهو هنا Français-Anglais
- stext هو النص المراد ترجمته في شكل يُسمى URL المشفر. في الواقع، لا يمكن أن تظهر بعض الأحرف في عنوان URL. هذا هو الحال، على سبيل المثال، بالنسبة للمسافة التي تم تشفيرها هنا بعلامة +. يوفر إطار عمل .Net الطريقة الثابتة System.Web.HttpUtility.UrlEncode للقيام بعملية الترميز هذه.
ونستنتج أنه لاستعلام خادم الترجمة، يمكن لفئتنا [ServiceTraduction] استخدام السلسلة
حيث سيتم استبدال العلامتين {0} و {1} على التوالي بمعنى الترجمة والنص المراد ترجمته.
كيف يمكننا معرفة معاني الترجمة التي يقبلها الخادم؟ في لقطة الشاشة أعلاه، تظهر اللغات المترجمة في القائمة المنسدلة. إذا نظرنا في المتصفح (عرض / مصدر) إلى كود HTML للصفحة، نجد ما يلي بالنسبة للقائمة المنسدلة:
هذا ليس كود HTML نظيفًا جدًا، حيث إن كل علامة <option> يجب أن تُغلق عادةً بعلامة </option>. ومع ذلك، فإن سمات value تعطينا قائمة برموز الترجمة التي يجب إرسالها إلى الخادم. في قاموس LanguesTraduites الخاص بواجهة IServiceTraduction، ستكون المفاتيح هي سمات value المذكورة أعلاه، وستكون القيم هي النصوص المعروضة في القائمة المنسدلة.
الآن دعونا ننظر (عرض / المصدر) إلى المكان الذي توجد فيه في صفحة HTML الترجمة التي أرسلها خادم الترجمة:
توجد الترجمة في منتصف صفحة HTML التي تم إرجاعها. كيف يمكن العثور عليها؟ يمكننا استخدام تعبير عادي مع التسلسل <div class="txtTrad">...</div> لأن العلامة <div class="txtTrad"> موجودة فقط في هذا المكان من صفحة HTML. التعبير العادي في C# الذي يسمح باسترداد النص المترجم هو التالي:
لدينا الآن العناصر اللازمة لكتابة فئة التنفيذ ServiceTraduction للواجهة IServiceTraduction:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;
namespace dao {
public class ServiceTraduction : IServiceTraduction {
// خصائص التكوين التلقائي للخدمة
public IDictionary<string, string> LanguesTraduites { get; set; }
public string UrlServeurTraduction { get; set; }
public string ProxyHttp { get; set; }
public String RegexTraduction { get; set; }
// الترجمة
public string Traduire(string texte, string deQuoiVersQuoi) {
// هل الترجمة المطلوبة ممكنة؟
if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
}
// النص المراد ترجمته
string texteATraduire = HttpUtility.UrlEncode(texte);
// عنوان URL المطلوب
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
// تعبير عادي للعثور على الترجمة في الرد
Regex patternTraduction = new Regex(RegexTraduction);
// استثناء
WebTraductionsException exception = null;
// ترجمة
string traduction = null;
try {
// يتم تكوين الاستعلام
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = "GET";
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
// تنفيذها
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// المستند
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
bool traductionTrouvée = false;
string ligne = null;
while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
// البحث عن الترجمة في السطر الحالي
MatchCollection résultats = patternTraduction.Matches(ligne);
// تم العثور على الترجمة؟
if (résultats.Count != 0) {
traduction = résultats[0].Groups[1].Value.Trim();
traductionTrouvée = true;
}
}
// تم العثور على الترجمة؟
if (!traductionTrouvée) {
exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
}
}
}
} catch (Exception e) {
exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
}
// استثناء؟
if (exception != null) {
throw exception;
} else {
return traduction;
}
}
}
}
- السطر 12: الخاصية LanguesTraduites للواجهة IServiceTraduction - تم تهيئتها من الخارج
- السطر 13: الخاصية UrlServeurTraduction هي عنوان URL المطلوب من خادم الترجمة: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} حيث يجب استبدال العلامة {0} باتجاه الترجمة والعلامة {1} بالنص المراد ترجمته - تم تهيئتها من الخارج
- السطر 14: الخاصية ProxyHttp هي الوكيل Http المحتمل استخدامه، على سبيل المثال: pproxy.istia.uang:3128 - تم تهيئتها من الخارج
- السطر 15: الخاصية RegexTraduction هي التعبير العادي الذي يسمح باسترداد الترجمة في تدفق HTML الذي يرسله خادم الترجمة، على سبيل المثال @"<div class=""txtTrad"">(.*?)</div>" - يتم تهيئتها من الخارج
- سيتم تهيئة هذه الخصائص الأربع، في تطبيقنا، بواسطة Spring.
- الأسطر 20-22: يتم التحقق من وجود معنى الترجمة المطلوب في قاموس اللغات المترجمة. إذا لم يكن الأمر كذلك، يتم إصدار استثناء.
- السطر 24: يتم ترميز النص المراد ترجمته ليصبح جزءًا من عنوان URL
- السطر 26: يتم إنشاء عنوان URI لخدمة الترجمة. إذا كانت الخاصية UrlServeurTraduction هي السلسلة http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}، يتم استبدال العلامة {0} بمعنى الترجمة والعلامة {1} بالنص المراد ترجمته.
- السطر 28: يتم إنشاء نموذج البحث عن الترجمة في استجابة html لخادم الترجمة.
- السطران 33 و60: تتم عملية الاستعلام عن خادم الترجمة في try / catch
- السطر 35: يتم إنشاء الكائن HttpWebRequest الذي سيتم استخدامه لاستعلام خادم الترجمة باستخدام عنوان Uri للوثيقة المطلوبة.
- السطر 36: طريقة الاستعلام هي GET. يمكن الاستغناء عن هذه التعليمات، لأن GET هي على الأرجح الطريقة الافتراضية للكائن HttpWebRequest.
- السطر 37: يتم تعيين الخاصية Proxy للكائن HttpWebRequest.
- السطر 39: يتم إرسال الطلب إلى خادم الترجمة واسترداد الرد الذي يكون من النوع HttpWebResponse.
- السطران 41-42: يتم استخدام StreamReader لقراءة كل سطر من الرد HTML للخادم.
- الأسطر 45-53: في كل سطر من الرد، نبحث عن الترجمة. وعند العثور عليها، نتوقف عن قراءة الرد HTML ونغلق جميع التدفقات التي فتحناها.
- الأسطر 55-57: إذا لم يتم العثور على ترجمة في استجابة HTML، يتم إعداد استثناء من النوع WebTraductionsException للإبلاغ عن ذلك.
- الأسطر 60-62: إذا حدثت استثناء أثناء التبادل بين العميل والخادم، يتم تغليفها في استثناء من النوع WebTraductionsException للإبلاغ عن ذلك.
- الأسطر 64-68: إذا تم تسجيل استثناء، يتم إطلاقه، وإلا يتم عرض الترجمة التي تم العثور عليها.
يفترض مثالنا أن الوكيل Http لا يتطلب مصادقة. إذا لم يكن الأمر كذلك، فسنكتب شيئًا مثل:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
استخدمنا هنا WebRequest / WebResponse بدلاً من WebClient لأننا لا نحتاج إلى استغلال كامل استجابة HTML من خادم الترجمة. بمجرد العثور على الترجمة في هذه الاستجابة، لم نعد بحاجة إلى باقي أسطر الاستجابة. لا تسمح الفئة WebClient بالقيام بذلك.
فيما يلي برنامج لاختبار الفئة ServiceTraduction:
using System;
using System.Collections.Generic;
using dao;
using entites;
namespace ui {
class Program {
static void Main(string[] args) {
try {
// إنشاء خدمة الترجمة
ServiceTraduction serviceTraduction = new ServiceTraduction();
// تعبير عادي للعثور على الترجمة
serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
// عنوان URL لخادم الترجمة
serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
// قاموس اللغات المترجمة
Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
languesTraduites["fe"]= "Français-Anglais";
languesTraduites["fs"]= "Français-Espagnol";
languesTraduites["ef"]= "Anglais-Français";
serviceTraduction.LanguesTraduites = languesTraduites;
// الوكيل
//serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
// ترجمة
string texte = "ce chien est perdu";
string deQuoiVersQuoi = "fe";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "l'été sera chaud";
deQuoiVersQuoi = "fs";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "my tailor is rich";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "xx";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
} catch (WebTraductionsException e) {
// خطأ
Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
}
}
}
}
النتائج التي تم الحصول عليها هي كما يلي:
يتم ترجمة مشروع [dao] الخاص بالحل إلى DLL HttpTraductions.dll:
![]() |
11.7.3.6. الواجهة الرسومية للتطبيق
لنعد إلى بنية تطبيقنا:
![]() |
نكتب الآن الطبقة [ui]. هذه هي موضوع مشروع [ui] للحل قيد الإنشاء:
![]() |
يحتوي المجلد [lib] [3] على بعض ملفات DLL المشار إليها في مشروع [4]:
- تلك اللازمة لـ Spring: Spring.Core، Common.Logging، antlr.runtime
- الملف الخاص بطبقة [dao]: HttpTraductions
يحتوي الملف [App.config] على تكوين Spring:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Traductions sur le web</description>
<!-- خدمة الترجمة -->
<object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
<property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<property name="LanguesTraduites">
<dictionary key-type="string" value-type="string">
<entry key="fe" value="Français-Anglais"/>
<entry key="ef" value="Anglais-Français"/>
...
<entry key="ei" value="Anglais-Italien"/>
<entry key="ie" value="Italien-Anglais"/>
</dictionary>
</property>
</object>
</objects>
</spring>
</configuration>
- السطر 15: الكائنات التي سيتم إنشاء مثيل لها بواسطة Spring. سيكون هناك كائن واحد فقط، وهو الموجود في السطر 18 الذي ينشئ مثيل خدمة الترجمة باستخدام الفئة ServiceTraduction الموجودة في DLL HttpTraductions.
- السطر 19: الخاصية UrlServeurTraduction للفئة ServiceTraduction. هناك مشكلة مع الحرف & في عنوان URL. هذا الحرف له معنى في ملف XML. لذا يجب حمايته. وينطبق الأمر نفسه على أحرف أخرى سنواجهها لاحقًا في الملف. يجب استبدالها بتسلسل [&code;]: & بـ [&]، و < بـ [<] > بـ [>]، و " بـ ["].
- السطر 21: الخاصية ProxyHttp للفئة ServiceTraduction. تبقى خاصية غير مُهيأة في null. عدم تعريف هذه الخاصية يعني أنه لا يوجد وكيل Http.
- السطر 23: الخاصية RegexTraduction للفئة ServiceTraduction. في التعبير العادي، كان لا بد من استبدال الأحرف [< > "] بمكافئاتها المحمية.
- الأسطر 24-33: الخاصية LanguesTraduites للفئة ServiceTraduction.
يتم تنفيذ البرنامج [Program.cs] عند بدء تشغيل التطبيق. وفيما يلي كوده:
using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
namespace ui {
static class Program {
/// <summary>
/// نقطة الدخول الرئيسية للتطبيق.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- رمز المطور
// إنشاء مثيل خدمة الترجمة
IApplicationContext ctx = null;
Exception ex = null;
ServiceTraduction serviceTraduction = null;
try {
// سياق Spring
ctx = ContextRegistry.GetContext();
// طلب مرجع لخدمة الترجمة
serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
} catch (Exception e1) {
// تخزين الاستثناء
ex = e1;
}
// نموذج للعرض
Form form = null;
// هل حدثت استثناء؟
if (ex != null) {
// نعم - يتم إنشاء رسالة الخطأ المراد عرضها
StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
Exception e = ex;
while (e != null) {
msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
e = e.InnerException;
}
// إنشاء نافذة خطأ يتم تمرير رسالة الخطأ المراد عرضها إليها
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
// ستكون هذه هي النافذة التي سيتم عرضها
form = form2;
} else {
// كل شيء سار على ما يرام
// إنشاء واجهة رسومية [Form1] يتم تمرير مرجع خدمة الترجمة إليها
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
// هذه هي النافذة التي سيتم عرضها
form = form1;
}
// عرض النافذة
Application.Run(form);
}
}
}
تم استخدام هذا الكود بالفعل في تطبيق Impôts الإصدار 6، في الفقرة 7.6.2.
- يتم إنشاء خدمة الترجمة في السطر 27 بواسطة Spring. إذا تم إنشاء الخدمة بنجاح، فسيتم عرض النموذج [Form1] (السطور 52-55)، وإلا فسيتم عرض نموذج الخطأ [Form2] (السطور 36-48).
النموذج [Form2] هو النموذج المستخدم في تطبيق Impôts الإصدار 6 وقد تم شرحه في الفقرة 7.6.4.
نموذج [Form1] هو التالي:
![]() |
رقم | النوع | الاسم | الدور |
1 | TextBox | textBoxTexteATraduire | مربع إدخال النص المراد ترجمته MultiLine=true |
2 | ComboBox | comboBoxLangues | قائمة اتجاهات الترجمة |
3 | زر | buttonTraduire | لطلب ترجمة النص [1] في الاتجاه [2] |
4 | TextBox | textBoxTraduction | ترجمة النص [1] |
رمز النموذج [Form1] هو التالي:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
namespace ui {
public partial class Form1 : Form {
// خدمة الترجمة
public ServiceTraduction ServiceTraduction { get; set; }
// قاموس اللغات
Dictionary<string, string> languesInversées = new Dictionary<string, string>();
// المنشئ
public Form1() {
InitializeComponent();
}
// التحميل الأولي للنموذج
private void Form1_Load(object sender, EventArgs e) {
// إنشاء قاموس اللغات العكسي
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// اللغات
string langues = ServiceTraduction.LanguesTraduites[code];
// إضافة (لغات، رمز) إلى القاموس العكسي
languesInversées[langues] = code;
}
// ملء القائمة المنسدلة حسب الترتيب الأبجدي للغات
string[] languesCombo = languesInversées.Keys.ToArray();
Array.Sort<string>(languesCombo);
foreach (string langue in languesCombo) {
comboBoxLangues.Items.Add(langue);
}
// اختيار اللغة الأولى
if (comboBoxLangues.Items.Count != 0) {
comboBoxLangues.SelectedIndex = 0;
}
}
private void buttonTraduire_Click(object sender, EventArgs e) {
// هل هناك شيء تريد ترجمته؟
string texte = textBoxTexteATraduire.Text.Trim();
if (texte == "") return;
// ترجمة
try {
textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
} catch (Exception ex) {
textBoxTraduction.Text = ex.Message;
}
}
}
}
- السطر 10: إشارة إلى خدمة الترجمة. تم تهيئة هذه الخاصية العامة بواسطة [Program.cs]، السطر 53. وعندما يتم تنفيذ الطرق Form1_Load (السطر 20) أو buttonTraduire_Click (السطر 40)، يكون هذا الحقل قد تم تهيئته بالفعل.
- السطر 12: قاموس اللغات المترجمة مع إدخالات من النوع ["Français-Anglais","fe"]، c.a.d. عكس قاموس LanguesTraduites الذي تم إرجاعه بواسطة خدمة الترجمة.
- السطر 20: يتم تنفيذ الطريقة Form1_Load عند تحميل النموذج.
- الأسطر 22-27: يتم استخدام القاموس serviceTraduction.LanguesTraduites ["fe","Français-Anglais"] لإنشاء القاموس languesInversées ["Français-Anglais", "fe"].
- السطر 29: languesCombo هو جدول مفاتيح القاموس languesInversées، c.a.d. جدول عناصر ["Français-Anglais"]
- السطر 30: يتم فرز هذا المصفوف لعرض معاني الترجمة في القائمة المنسدلة حسب الترتيب الأبجدي
- الأسطر 31-33: يتم ملء قائمة اللغات المنسدلة.
- السطر 40: الطريقة التي يتم تنفيذها عندما ينقر المستخدم على الزر [Traduire]
- السطر 46: يكفي استدعاء الطريقة serviceTraduction.Traduire لطلب الترجمة. المعلمة الأولى هي النص المراد ترجمته، والثانية هي رمز اتجاه الترجمة. يتم العثور على هذا الرمز في القاموس languesInversées بناءً على العنصر المحدد في قائمة اللغات المنسدلة.
- السطر 48: في حالة وجود استثناء، يتم عرضه بدلاً من الترجمة.
11.7.3.7. Conclusion
أظهر هذا التطبيق أن عملاء الويب في إطار العمل .NET يسمحون لنا باستغلال موارد الويب. تكون التقنية متشابهة في كل مرة:
- تحديد عنوان URI المطلوب الاستعلام عنه. يتم تعيين معلمات عنوان URI هذا في معظم الأحيان.
- الاستعلام عنها
- العثور على ما نبحث عنه في استجابة الخادم باستخدام التعبيرات العادية
هذه التقنية عشوائية. في الواقع، مع مرور الوقت، قد يتغير عنوان Uri الذي يتم الاستعلام عنه أو التعبير النمطي الذي يسمح بالعثور على النتيجة المتوقعة. لذلك من الأفضل وضع هاتين المعلومتين في ملف تكوين. لكن هذا قد يكون غير كافٍ. سنرى في الفصل التالي أن هناك موارد أكثر استقرارًا على الويب: خدمات الويب.
11.7.4. عميل SMTP (بروتوكول نقل البريد البسيط) مع الفئة SmtpClient
عميل SMTP هو عميل لخادم SMTP، وهو خادم إرسال البريد. تغلف الفئة .NET SmtpClient احتياجات مثل هذا العميل بالكامل. لا يحتاج المطور إلى معرفة تفاصيل بروتوكول SMTP. نحن على دراية بهذا البروتوكول. وقد تم عرضه في الفقرة 11.4.3.
نقدم الفئة SmtpClient في إطار تطبيق ويندوز بسيط يسمح بإرسال رسائل بريد إلكتروني مع مرفقات. سيتصل التطبيق بالمنفذ 25 لخادم SMTP. نذكر أن في معظم أنظمة ويندوز، تقوم جدران الحماية أو برامج مكافحة الفيروسات الأخرى بحظر الاتصالات بالمنفذ 25. لذا، من الضروري تعطيل هذه الحماية لاختبار التطبيق:
![]() |
سيكون للعميل Smtp بنية أحادية الطبقة:
![]() |
مشروع Visual Studio هو التالي:
![]() |
الواجهة الرسومية [SendMailForm.cs] للتطبيق هي كما يلي:
![]() |
رقم | النوع | الاسم | الدور |
1 | TextBox | textBoxServeur | اسم الخادم SMTP الذي يجب الاتصال به |
2 | NumericUpDown | numericUpDownPort | المنفذ الذي سيتم الاتصال به |
3 | TextBox | textBoxExpediteur | عنوان مرسل الرسالة |
4 | TextBox | textBoxTo | عناوين المستلمين بالشكل: العنوان1، العنوان2، ... |
5 | TextBox | textBoxCc | عناوين المستلمين في نسخة (CC=Carbon Copy) بالشكل: العنوان1، العنوان2، ... |
6 | TextBox | textBoxBcc | عناوين المستلمين في نسخة مخفية (BCC=Blind Carbon Copy) بالشكل: العنوان 1، العنوان 2، ... ستتلقى جميع العناوين في حقول الإدخال الثلاثة هذه نفس الرسالة مع نفس المرفقات. سيتمكن مستلمو الرسالة من معرفة العناوين الموجودة في الحقلين 4 و 5، ولكن لن يتمكنوا من معرفة العناوين الموجودة في الحقل 6. وبالتالي، فإن Bcc هي طريقة لإضافة شخص ما إلى نسخة من الرسالة دون أن يعلم مستلمو الرسالة الآخرون بذلك. |
7 | زر | buttonAjouter | لإضافة مرفق إلى الرسالة |
8 | ListBox | listBoxPiecesJointes | قائمة المرفقات التي يجب إرفاقها بالرسالة |
9 | TextBox | textBoxSujet | موضوع الرسالة |
10 | TextBox | textBoxMessage | نص الرسالة. MultiLine=true |
11 | Button | buttonEnvoyer | لإرسال الرسالة والمرفقات المحتملة |
12 | TextBox | textBoxRésultat | يعرض ملخصًا للرسالة المرسلة أو رسالة خطأ في حالة حدوث مشكلة |
13 | زر | buttonEffacer | للمسح [12] |
OpenfileDialog | openFileDialog1 | عنصر تحكم غير مرئي يسمح باختيار مرفق من نظام الملفات المحلي |
في المثال السابق، الملخص المعروض في [12] هو التالي:
Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc :
Bcc :
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,
Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
Cordialement,
ST
رمز النموذج [SendMailForm.cs] هو التالي:
using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;
namespace Chap9 {
public partial class SendMailForm : Form {
public SendMailForm() {
InitializeComponent();
}
// إضافة مرفق
private void buttonAjouter_Click(object sender, EventArgs e) {
// تعيين إعدادات مربع الحوار openfileDialog1
openFileDialog1.InitialDirectory = Application.ExecutablePath;
openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
openFileDialog1.FilterIndex = 0;
openFileDialog1.FileName = "";
// عرض مربع الحوار واسترداد نتيجته
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
// استرداد اسم الملف
listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
}
}
private void textBoxServeur_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void setStatutEnvoyer() {
buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
}
// إزالة مرفق
private void buttonRetirer_Click(object sender, EventArgs e) {
// المرفق محدد؟
if (listBoxPiecesJointes.SelectedIndex != -1) {
// إزالته
listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
// يتم تحديث زر "إزالة"
buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
}
}
private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
// المرفق محدد؟
if (listBoxPiecesJointes.SelectedIndex != -1) {
// تحديث زر "إزالة"
buttonRetirer.Enabled = true;
}
}
// إرسال الرسالة مع مرفقاتها
private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
}
private void textBoxTo_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void textBoxSujet_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void buttonEffacer_Click(object sender, EventArgs e) {
textBoxResultat.Text = "";
}
}
}
لن نعلق على هذا الرمز لأنه لا يحتوي على أي مستجدات. لفهم الطريقة buttonAjouter_Click في السطر 14، يُرجى من القارئ إعادة قراءة الفقرة 7.5.1.
الطريقة buttonEnvoyer_Click في السطر 55، التي ترسل البريد هي كما يلي:
private void buttonEnvoyer_Click(object sender, EventArgs e) {
try {
// الساعة الرملية
Cursor = Cursors.WaitCursor;
// عميل SMTP
SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
// الرسالة
MailMessage message = new MailMessage();
// المرسل
message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
message.From = message.Sender;
// المستلمون
Regex marqueur = new Regex("\\s*,\\s*");
string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
foreach (string destinataire in destinataires) {
if (destinataire.Trim() != "") {
message.To.Add(new MailAddress(destinataire));
}
}
// CC
string[] copies = marqueur.Split(textBoxCc.Text.Trim());
foreach (string copie in copies) {
if (copie.Trim() != "") {
message.CC.Add(new MailAddress(copie));
}
}
// BCC
string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
foreach (string blindCopie in blindCopies) {
if (blindCopie.Trim() != "") {
message.Bcc.Add(new MailAddress(blindCopie));
}
}
// الموضوع
message.Subject = textBoxSujet.Text.Trim();
// نص الرسالة
message.Body = textBoxMessage.Text;
// المرفقات
foreach (string attachement in listBoxPiecesJointes.Items) {
message.Attachments.Add(new Attachment(attachement));
}
// إرسال الرسالة
smtpClient.Send(message);
// موافق - يتم عرض ملخص
StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
textBoxSujet.Clear();
msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
textBoxTo.Clear();
msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
textBoxCc.Clear();
msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
textBoxBcc.Clear();
msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
foreach (string attachement in listBoxPiecesJointes.Items) {
msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
}
msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
listBoxPiecesJointes.Items.Clear();
textBoxResultat.Text = msg.ToString();
} catch (Exception ex) {
// يتم عرض الخطأ
textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
}
// مؤشر عادي
Cursor = Cursors.Arrow;
}
- السطر 6: يتم إنشاء عميل Smtp. ويحتاج إلى معلمتين: اسم الخادم SMTP والمنفذ الذي يعمل عليه
- السطر 8: يتم إنشاء رسالة من النوع MailMessage. وهي التي ستقوم بتغليف الرسالة بالكامل المراد إرسالها.
- السطر 10: يتم إدخال عنوان البريد الإلكتروني Sender الخاص بالمرسل. عنوان البريد الإلكتروني هو مثيل من النوع MailAddress تم إنشاؤه من سلسلة الأحرف "xx@yy.zz". يجب أن تكون هذه السلسلة بالشكل المتوقع لعنوان بريد إلكتروني، وإلا يتم إصدار استثناء. في هذه الحالة، سيتم عرضها في الحقل textBoxResultat (السطر 63) بشكل غير سهل الاستخدام.
- الأسطر 13-19: يتم وضع عناوين البريد الإلكتروني للمستلمين في قائمة "إلى" (To) للرسالة. يتم استرداد هذه العناوين من الحقل textBoxTo. يسمح التعبير العادي في السطر 13 باسترداد العناوين المختلفة التي تفصلها فاصلة.
- الأسطر 21-26: نكرر نفس العملية لتهيئة الحقل CC في الرسالة بالعناوين الموجودة في نسخة الحقل textBoxCc.
- الأسطر 28-33: نكرر نفس العملية لتهيئة حقل "Bcc" في الرسالة بعناوين النسخة الخفية الموجودة في الحقل textBoxBcc.
- السطر 35: يتم تهيئة حقل Subject للرسالة بموضوع الحقل textBoxSujet.
- السطر 37: يتم تهيئة حقل Body للرسالة بنص الرسالة textBoxMessage.
- الأسطر 39-41: يتم إرفاق المرفقات بالرسالة. يتم إضافة كل مرفق في شكل كائن Attachment إلى حقل Attachments في الرسالة. يتم إنشاء مثيل لكائن Attachment من المسار الكامل للملف المراد إرفاقه في نظام الملفات المحلي.
- السطر 43: يتم إرسال الرسالة باستخدام طريقة Send الخاصة بعميل Smtp.
- الأسطر 45-60: كتابة ملخص الإرسال في الحقل textBoxResultat وإعادة تعيين النموذج.
- السطر 63: عرض أي خطأ محتمل
11.8. عميل Tcp عام غير متزامن
11.8.1. مقدمة
في جميع الأمثلة الواردة في هذا الفصل، كانت الاتصالات بين العميل والخادم تتم في الوضع المعطل الذي يُعرف أيضًا بالوضع المتزامن:
- عندما يتصل عميل بخادم، فإنه ينتظر رد الخادم على هذا الطلب قبل المتابعة.
- عندما يقرأ العميل سطرًا نصيًا أرسله الخادم، فإنه يظل معطلاً حتى يرسل الخادم هذا السطر.
- على جانب الخادم، تعمل مؤشرات الترابط التي توفر الخدمة للعميل بنفس الطريقة المذكورة أعلاه.
في واجهات المستخدم الرسومية، غالبًا ما يكون من الضروري عدم تعطيل المستخدم أثناء العمليات الطويلة. والحالة التي غالبًا ما يُستشهد بها هي تنزيل ملف كبير الحجم. أثناء هذا التنزيل، يجب ترك المستخدم حرًا في مواصلة التفاعل مع واجهة المستخدم الرسومية.
نقترح هنا إعادة كتابة عميل Tcp العام الوارد في الفقرة 11.6.3 بإجراء التغييرات التالية عليه:
- ستكون الواجهة رسومية
- أداة الاتصال بالخادم ستكون كائن Socket
- سيكون وضع الاتصال غير متزامن:
- سيقوم العميل ببدء اتصال بالخادم ولكنه لن يظل معطلاً في انتظار إقامته
- سيقوم العميل ببدء إرسال البيانات إلى الخادم ولكنه لن يظل معطلاً في انتظار انتهاء الإرسال
- سيبدأ العميل في استقبال البيانات الواردة من الخادم ولكنه لن يظل معطلاً في انتظار انتهاء ذلك.
دعونا نذكر أين يقع كائن Socket في اتصال العميل/الخادم Tcp:
![]() |
فئة Socket هي التي تعمل الأقرب إلى الشبكة. وهي تسمح بإدارة اتصال الشبكة بدقة. يشير مصطلح socket إلى مقبس كهربائي. وقد تم توسيع نطاق هذا المصطلح ليشير إلى مقبس شبكة برمجي. في اتصال TCP-IP بين جهازين A و B، هناك اثنان من sockets يتواصلان مع بعضهما البعض. يمكن للتطبيق العمل مباشرة مع sockets. وهذا هو الحال بالنسبة للتطبيق A أعلاه. يمكن أن يكون المقبس مقبس client أو serveur.
11.8.2. الواجهة الرسومية للعميل Tcp غير المتزامن
تطبيق Visual Studio هو التالي:
![]() |
[ClientTcpAsynchrone.cs] هي الواجهة الرسومية. وهي كما يلي:
![]() |
رقم | النوع | الاسم | الدور |
1 | TextBox | textBoxNomServeur | اسم خادم Tcp المراد الاتصال به |
2 | NumericUpDown | numericUpDownPortServeur | المنفذ الذي سيتم الاتصال به |
3 | RadioButton | radioButtonLF radioButtonRCLF | لتحديد علامة نهاية السطر التي يجب على العميل استخدامها: LF "\n" أو RCLF "\r\n" |
4 | زر | buttonConnexion | للاتصال بالمنفذ [2] للخادم [1]. يكون نص الزر [Connecter] عندما لا يكون العميل متصلاً بخادم، و [Déconnecter] عندما يكون متصلاً. |
5 | TextBox | textBoxMsgToServeur | رسالة يتم إرسالها إلى الخادم بمجرد إتمام الاتصال. عندما يضغط المستخدم على المفتاح [Entrée]، يتم إرسال الرسالة مع علامة نهاية السطر المختارة في [3] |
6 | ListBox | listBoxEvts | قائمة تعرض الأحداث الرئيسية للاتصال بين العميل والخادم: الاتصال، قطع الاتصال، إغلاق التدفق، أخطاء الاتصال |
7 | ListBox | listBoxDialogue | قائمة تعرض رسائل الحوار بين العميل والخادم |
8 | زر | buttonRazEvts | لمسح القائمة [6] |
4 | زر | buttonRazDialogue | لمسح القائمة [7] |
مبادئ عمل هذه الواجهة هي كما يلي:
- يقوم المستخدم بتوصيل عميل TCP الرسومي الخاص به بخدمة TCP باستخدام [1, 2, 3, 4].
- يقبل مؤشر ترابط غير متزامن بشكل مستمر جميع البيانات المرسلة من خادم Tcp ويعرضها في القائمة [7]. هذا المؤشر غير مرتبط بالأنشطة الأخرى للواجهة.
- يمكن للمستخدم إرسال رسائل إلى الخادم حسب رغبته باستخدام [5]. يتم إرسال كل رسالة بواسطة مؤشر ترابط غير متزامن. على عكس مؤشر ترابط الاستقبال الذي لا يتوقف أبدًا، ينتهي مؤشر ترابط الإرسال بمجرد إرسال الرسالة. سيتم استخدام مؤشر ترابط غير متزامن جديد للرسالة التالية.
- تنتهي الاتصالات بين العميل والخادم عندما يقوم أحد الطرفين بإنهاء الاتصال. يمكن للمستخدم اتخاذ هذه المبادرة باستخدام الزر [4] الذي يظهر عليه العنوان [Déconnecter] بمجرد إقامة الاتصال.
فيما يلي لقطة شاشة لعملية تنفيذ:
![]() |
- في [1]: الاتصال بخدمة POP
- في [2]: عرض الأحداث التي وقعت أثناء الاتصال
- في [3]: الرسالة المرسلة من الخادم POP عند انتهاء الاتصال
- في [4]: أصبح الزر [Connecter] هو الزر [Déconnecter]
![]() |
- في [1]، تم إرسال الأمر quit إلى الخادم POP. رد الخادم بـ +OK goodbye وأغلق الاتصال
- في [2]، تم الكشف عن هذا الإغلاق من جانب الخادم. ثم أغلق العميل الاتصال من جانبه.
- في [3]، عاد الزر [Déconnecter] ليصبح زرًا [Connecter]
11.8.3. اتصال غير متزامن بالخادم
يؤدي الضغط على الزر [Connecter] إلى تنفيذ الطريقة التالية:
private void buttonConnexion_Click(object sender, EventArgs e) {
// تسجيل الدخول أم الخروج؟
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- السطر 3: قد يكون اسم الزر [Connecter] أو [Déconnecter].
طريقة الاتصال هي كما يلي:
using System.Net.Sockets;
...
namespace Chap9 {
public partial class ClientTcp : Form {
const int tailleBuffer = 1024;
private Socket client = null;
private byte[] data = new byte[tailleBuffer];
private string réponse = null;
private string finLigne = "\r\n";
// المفوضون
public delegate void writeLog(string log);
public ClientTcp() {
InitializeComponent();
}
....................................
private void connexion() {
// التحقق من البيانات
string nomServeur = textBoxNomServeur.Text.Trim();
if (nomServeur == "") {
logEvent("indiquez le nom du serveur");
return;
}
// المتابعة
logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
try {
// إنشاء مأخذ توصيل
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// اتصال غير متزامن
client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
} catch (Exception ex) {
logEvent(String.Format("erreur de connexion : {0}", ex.Message));
return;
}
}
// تم الاتصال
private void connecté(IAsyncResult résultat) {
// استرداد مأخذ توصيل العميل
Socket client = résultat.AsyncState as Socket;
...
}
// متابعة العملية
private void logEvent(string msg) {
....
}
}
}
- السطر 1: فئة Socket جزء من مساحة الأسماء System.Net.Sockets.
يجب مشاركة عدد معين من البيانات بين عدة طرق في النموذج. وهي كما يلي:
- السطر 7: client هو مأخذ الاتصال مع الخادم
- السطران 6 و8: سيتلقى العميل رسائله في مصفوفة بايتات data.
- السطر 9: response هو الرد المرسل من الخادم.
- السطر 10: finLigne هي علامة نهاية السطر التي يستخدمها العميل Tcp - يتم تهيئتها افتراضيًا على RCLF ولكن يمكن للمستخدم تعديلها باستخدام أزرار الاختيار [3].
تقوم الإجراء connexion في السطر 19 بإجراء الاتصال بخادم Tcp:
- السطور 21-25: يتم التحقق من أن اسم الخادم غير فارغ. إذا لم يكن الأمر كذلك، يتم تسجيل الحدث في listBoxEvts بواسطة الطريقة logEvent في السطر 49.
- السطر 27: يتم الإبلاغ عن أن الاتصال سيتم
- السطر 30: يتم إنشاء كائن Socket الضروري للاتصال Tcp-Ip. يقبل المنشئ ثلاثة معلمات:
- AddressFamily addressFamily: عائلة عناوين IP للعميل والخادم، وهنا عناوين IPv4 (AddressFamily.InterNetwork)
- SocketType socketType: نوع المأخذ. النوع SocketType.Stream مناسب لاتصالات Tcp-Ip
- ProtocolType protocolType: نوع بروتوكول الإنترنت المستخدم، وهو هنا بروتوكول TCP
- السطر 32: يتم إجراء الاتصال بشكل غير متزامن. يتم بدء الاتصال ولكن التنفيذ يستمر دون انتظار انتهائه. تقبل الطريقة [Socket].BeginConnect أربعة معلمات:
- IPAddress ipAddress: عنوان IP للجهاز الذي تعمل عليه الخدمة المطلوب الاتصال بها
- Int32 port: منفذ الخدمة
- AsyncCallBack asyncCallBack: AsyncCallBack هو نوع مندوب:
يجب أن تكون الطريقة asyncCallBack التي تم تمريرها كمعلمة ثالثة للطريقة BeginConnect طريقة تقبل نوع IAsyncCallBack ولا تُرجع أي نتيجة. هذه هي الطريقة التي سيتم استدعاؤها عند إتمام الاتصال. نمرر هنا كمعلمة ثالثة الطريقة connecté من السطر 41.
- (تابع)
- الكائن state: كائن يتم تمريره إلى الدالة asyncCallBack. تتلقى هذه الدالة (انظر الوكيل أعلاه) معلمة ar من النوع IAsyncResult. يمكن استرداد الكائن state في ar.AsyncState (السطر 43). نمرر هنا كمعلمة رابعة مأخذ توصيل العميل.
- السطر 38: انتهت الطريقة. يمكن للمستخدم التفاعل مرة أخرى مع واجهة المستخدم الرسومية. يتم الاتصال في الخلفية، بالتوازي مع إدارة أحداث واجهة المستخدم الرسومية. وبالتوازي أيضًا، سيتم استدعاء الطريقة connecté في السطر 41 عند انتهاء الاتصال، سواء انتهى بنجاح أو فشل.
رمز الطريقة connecté هو كما يلي:
// تم الاتصال
private void connecté(IAsyncResult résultat) {
// استرداد مأخذ توصيل العميل
Socket client = résultat.AsyncState as Socket;
try {
// إنهاء العملية غير المتزامنة
client.EndConnect(résultat);
// المتابعة
logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
// نموذج
buttonConnexion.Text = "Déconnecter";
// قراءة غير متزامنة للبيانات الواردة من الخادم
réponse = "";
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
} catch (SocketException e) {
logEvent(String.Format("erreur de connexion : {0}", e.Message));
return;
}
}
// استلام البيانات
private void lecture(IAsyncResult résultat) {
// استرداد مأخذ توصيل العميل
Socket client = résultat.AsyncState as Socket;
...
}
- السطر 4: يتم استرداد مأخذ توصيل العميل في المعلمة résultat التي تتلقاها الطريقة. تجدر الإشارة إلى أن هذا الكائن هو الذي تم تمريره كمعلمة رابعة للطريقة BeginConnect.
- السطر 7: تنتهي محاولة الاتصال بواسطة الطريقة EndConnect التي يجب تمرير المعلمة résultat التي استلمتها الطريقة إليها.
- السطر 9: يتم تسجيل الحدث في قائمة الأحداث
- السطر 11: يتحول الزر [Connecter] إلى زر [Déconnecter] حتى يتمكن المستخدم من طلب قطع الاتصال.
- السطر 13: يتم تهيئة استجابة الخادم. سيتم تحديثها من خلال استدعاءات متكررة للطريقة غير المتزامنة BeginReceive.
- السطر 14: أول استدعاء للطريقة غير المتزامنة BeginReceive. يتم استدعاء هذه الطريقة بالمعلمات التالية:
- byte[] buffer: المخزن المؤقت الذي سيتم وضع البيانات التي سيتم استلامها فيه - هنا المخزن المؤقت هو data
- int offset: الموضع الذي تبدأ منه البيانات التي سيتم استلامها في المخزن المؤقت - هنا الإزاحة هي 0، c.a.d. أي أن البيانات توضع بدءًا من البايت الأول من المخزن المؤقت.
- int size: حجم المخزن المؤقت بالبايت - هنا الحجم هو tailleBuffer.
- SocketFlags socketFlags: تكوين المأخذ - هنا لا يوجد أي تكوين
- AsyncCallBack asyncCallBack: الطريقة التي يجب استدعاؤها عند انتهاء الاستلام. سيحدث ذلك إما لأن المخزن المؤقت قد تلقى بيانات أو لأن الاتصال قد تم إغلاقه. هنا، طريقة الاستدعاء هي الطريقة lecture في السطر 22.
- الكائن state: الكائن الذي سيتم تمريره إلى طريقة الاستدعاء asyncCallBack. هنا، يتم تمرير مقبس العميل مرة أخرى.
تجدر الإشارة إلى أن كل هذا يحدث دون أي تدخل من المستخدم، بخلاف طلب الاتصال الأولي باستخدام الزر [Connecter]. في نهاية الأسلوب connecté، يتم تنفيذ أسلوب آخر في الخلفية: الأسلوب lecture الذي ندرسه الآن.
// استلام البيانات
private void lecture(IAsyncResult résultat) {
// استرداد مأخذ توصيل العميل
Socket client = résultat.AsyncState as Socket;
int nbOctetsReçus = 0;
bool erreur = false;
try {
// عدد البايتات المستلمة
nbOctetsReçus = client.EndReceive(résultat);
if (nbOctetsReçus == 0) {
// الخادم لم يعد يستجيب
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// حدثت مشكلة في الاستلام
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// انتهى؟
if (nbOctetsReçus == 0 || erreur) {
// يتم فصل العميل عند الحاجة
déconnexion();
// يتم عرض نهاية الرد
afficherRéponseServeur(réponse, true);
// نهاية القراءة
return;
}
// يتم استرداد البيانات المستلمة
string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
// تتم إضافتها إلى البيانات المستلمة بالفعل
réponse += données;
// يتم عرض الرد
afficherRéponseServeur(réponse, false);
// يتم متابعة القراءة
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
}
- السطر 2: يتم تشغيل الطريقة lecture في الخلفية عندما يتلقى المخزن المؤقت data بيانات أو عندما يتم إغلاق الاتصال بواسطة الخادم.
- السطر 9: يتم إنهاء طلب القراءة غير المتزامن بواسطة EndReceive. هنا أيضًا، يجب استدعاء هذه الطريقة باستخدام المعلمة التي تم استلامها بواسطة دالة الاستدعاء. تعرض الطريقة EndReceive عدد البايتات التي تم استلامها في المخزن المؤقت للقراءة.
- السطر 10: إذا كان عدد البايتات صفرًا، فهذا يعني أن الاتصال قد تم إغلاقه بواسطة الخادم.
- السطر 12: يتم تسجيل الحدث في قائمة الأحداث
- السطر 14: يتم معالجة أي استثناء محتمل
- السطران 16-17: يتم تسجيل الحدث في قائمة الأحداث وتسجيل الخطأ
- السطر 20: يتم التحقق مما إذا كان يجب إغلاق الاتصال
- السطر 22: يتم إغلاق الاتصال من جانب العميل باستخدام طريقة déconnexion التي سنرى لاحقًا.
- السطر 24: استجابة الخادم، c.a.d. يتم عرض المتغير العام réponse في قائمة الحوار listBoxDialogue باستخدام طريقة خاصة afficherRéponseServeur.
- السطر 26: نهاية الطريقة غير المتزامنة lecture
- السطر 29: يتم وضع البايتات المستلمة في سلسلة أحرف بتنسيق UTF8.
- السطر 31: تتم إضافتها إلى الرد قيد الإنشاء
- السطر 33: يتم عرض الرد في القائمة listBoxDialogue.
- السطر 35: نعود إلى انتظار البيانات الواردة من الخادم
في النهاية، لا تتوقف الطريقة غير المتزامنة lecture أبدًا. فهي تقرأ البيانات الواردة من الخادم بشكل مستمر وتعرضها في القائمة listBoxDialogue. ولا تتوقف إلا عندما يتم إغلاق الاتصال إما من قبل الخادم أو من قبل المستخدم نفسه.
11.8.4. قطع الاتصال بالخادم
يؤدي الضغط على الزر [Déconnecter] إلى تنفيذ الطريقة التالية:
private void buttonConnexion_Click(object sender, EventArgs e) {
// اتصال أم قطع اتصال؟
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- السطر 3: قد يكون نص الزر [Connecter] أو [Déconnecter].
تضمن الطريقة déconnexion قطع اتصال العميل:
private void déconnexion() {
// إغلاق المأخذ
if (client != null && client.Connected) {
try {
// متابعة
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
// قطع الاتصال
client.Shutdown(SocketShutdown.Both);
client.Close();
// نموذج
buttonConnexion.Text = "Connecter";
} catch (Exception ex) {
// متابعة
logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
}
}
}
- السطر 3: إذا كان العميل موجودًا ومتصلاً
- السطر 6: يتم الإعلان عن قطع الاتصال في listBoxEvts. توفر الخاصية client.RemoteEndPoint الزوج (عنوان IP، المنفذ) للطرف الآخر من الاتصال، c.a.d هنا من الخادم.
- السطر 8: يتم إغلاق تدفق البيانات من المأخذ باستخدام الطريقة ShutDown. تدفق البيانات من المأخذ ثنائي الاتجاه: يقوم المأخذ بإرسال واستقبال البيانات. يمكن أن يكون معلمة الطريقة ShutDown: ShutDown.Receive لإغلاق تدفق الاستقبال، أو Shutdonw.Send لإغلاق تدفق الإرسال، أو ShutDown.Both لإغلاق كلا التدفقين.
- السطر 9: يتم تحرير الموارد المرتبطة بالمقبس
- السطر 11: يصبح الزر [Déconnecter] الزر [Connecter]
- الأسطر 12-15: إدارة أي استثناء محتمل
11.8.5. إرسال البيانات إلى الخادم بشكل غير متزامن
عندما يقوم المستخدم بالتحقق من صحة الرسالة في الحقل textBoxMsgToServeur، يتم تنفيذ الطريقة التالية:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// زر [Entrée] ؟
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- الأسطر 3-5: إذا ضغط المستخدم على المفتاح [Entrée] وكان مأخذ العميل متصلاً، يتم إرسال رسالة الحقل textBoxMsgToServeur باستخدام الطريقة envoyerMessage.
الطريقة envoyerMessage هي كما يلي:
private void envoyerMessage() {
// إرسال رسالة بشكل غير متزامن
// الرسالة
byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
// تم إرسالها
client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
// حوار
logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
// إعادة تعيين الرسالة
textBoxMsgToServeur.Clear();
}
- السطر 4: نضيف إلى الرسالة علامة نهاية السطر الخاصة بالعميل ونضعها في جدول البايتات message.
- السطر 6: بدأ البث غير المتزامن باستخدام الطريقة BeginSend. معلمات BeginSend مطابقة لتلك الخاصة بالطريقة BeginReceive. عند انتهاء عملية الإرسال غير المتزامن للرسالة، سيتم استدعاء الطريقة écriture.
- السطر 8: تتم إضافة الرسالة المرسلة إلى قائمة listBoxDialogue من أجل تتبع الحوار بين العميل والخادم
- السطر 10: يتم مسح الرسالة المرسلة من واجهة المستخدم الرسومية
طريقة الاستدعاء écriture هي كما يلي:
private void écriture(IAsyncResult résultat) {
// نتيجة إرسال رسالة
Socket client = résultat.AsyncState as Socket;
try {
client.EndSend(résultat);
} catch (Exception e) {
// حدثت مشكلة في الإرسال
logEvent(String.Format("erreur d'émission : {0}", e.Message));
}
}
- السطر 4: تتلقى طريقة الاستدعاء écriture معلمة نتيجة من النوع IAsyncResult.
- السطر 3: في المعلمة résultat، يتم استرداد مأخذ توصيل العميل. كان هذا المأخذ هو المعلمة الخامسة لطريقة BeginSend.
- السطر 5: يتم إنهاء عملية الإرسال غير المتزامنة.
لا ننتظر انتهاء إرسال رسالة ما لإعادة السيطرة إلى المستخدم. وبذلك يمكن للمستخدم إرسال رسالة ثانية بينما لم ينته إرسال الرسالة الأولى.
11.8.6. عرض الأحداث والحوار بين العميل والخادم
يتم عرض الأحداث بواسطة الطريقة logEvents:
// متابعة العملية
private void logEvent(string msg) {
listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
}
private void logEventCallBack(string msg) {
// عرض الرسالة
msg = msg.Replace(finLigne, " ");
listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
- السطر 2: تتلقى الطريقة logEvents كمعلمة الرسالة المراد إضافتها إلى القائمة listBoxEvts.
- السطر 3: لا يمكن استخدام المكون listBoxEvents مباشرة. في الواقع، يتم استدعاء الطريقة logEvents بواسطة نوعين من الخيوط:
- الخيط الرئيسي المالك للواجهة الرسومية، على سبيل المثال عندما يشير إلى أن محاولة اتصال جارية
- خيط ثانوي يضمن عملية غير متزامنة. هذا النوع من الخيوط لا يمتلك المكونات ويجب التحكم في وصوله إلى مكون C بواسطة عملية C.Invoke. تشير هذه العملية إلى عنصر التحكم C بأن خيطًا يريد إجراء عملية عليه. تقبل الطريقة Invoke معلمتين:
- وظيفة استدعاء من النوع delegate. سيتم تنفيذ وظيفة الاستدعاء هذه بواسطة الخيط المالك للواجهة الرسومية وليس بواسطة الخيط الذي ينفذ الطريقة C.Invoke.
- كائن سيتم تمريره إلى دالة الاستدعاء.
هنا، المعلمة الأولى التي يتم تمريرها إلى الطريقة Invoke هي مثيل للمندوب التالي:
public delegate void writeLog(string log);
يحتوي الوكيل writeLog على معلمة من النوع string ولا يُرجع أي نتيجة. ستكون المعلمة هي الرسالة المراد تسجيلها في listBoxEvts.
السطر 3، المعلمة الأولى التي تم تمريرها إلى الطريقة Invoke هي الطريقة logEventCallBack في السطر 6. وهي تتوافق تمامًا مع توقيع الوكيل writeLog. المعلمة الثانية التي يتم تمريرها إلى الطريقة Invoke هي الرسالة التي سيتم تمريرها كمعلمة إلى الطريقة logEventCallBack.
العملية Invoke هي عملية متزامنة. يتم حظر تنفيذ الخيط الثانوي حتى يقوم الخيط المالك للعنصر بتنفيذ طريقة الاستدعاء.
- السطر 6: تتلقى طريقة الاستدعاء التي ينفذها مؤشر ترابط واجهة المستخدم الرسومية الرسالة المراد عرضها في عنصر التحكم listBoxEvts.
- السطر 9: يتم تسجيل الحدث في المرتبة الأولى من القائمة بحيث تظهر الأحداث الأحدث في أعلى القائمة.
يتم عرض رسائل الحوار بين العميل والخادم بواسطة الطريقة logDialogue:
// متابعة الحوار
private void logDialogue(string msg) {
listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
}
private void logDialogueCallBack(string msg) {
// عرض الرسالة
msg = msg.Replace(finLigne, " ");
listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
المبدأ هو نفسه كما في الطريقة logEvent.
يتم عرض الرسائل التي يتلقاها العميل بواسطة الطريقة afficherRéponseServeur:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
المعلمة الأولى هي الرسالة المراد عرضها. وقد تتكون هذه الرسالة من عدة أسطر. ففي الواقع، يقرأ العميل البيانات الواردة من الخادم على شكل كتل بحجم tailleBuffer (1024) بايت. ويمكن أن تحتوي هذه الكتل المكونة من 1024 بايت على أسطر متنوعة يمكن التعرف عليها من خلال علامة نهاية السطر "\n". قد يكون السطر الأخير غير مكتمل، حيث تكون علامة نهاية السطر الخاصة به ضمن الـ 1024 بايت التالية. تبحث الطريقة في الرسالة عن الأسطر التي تنتهي بـ "\n" ثم تطلب من logDialogue عرضها. يحدد المعامل الثاني للطريقة ما إذا كان يجب عرض آخر سطر تم العثور عليه أو تركه في المخزن المؤقت ليتم استكماله بالرسالة التالية. الكود معقد إلى حد ما ولا يهمنا هنا. ولذلك لن يتم التعليق عليه.
11.8.7. الخلاصة
يمكن معالجة المثال نفسه باستخدام عمليات متزامنة. هنا، لا يضيف الجانب غير المتزامن للواجهة الرسومية الكثير للمستخدم. ومع ذلك، إذا قام المستخدم بالاتصال ثم أدرك أن الخادم "لم يعد يستجيب"، فإنه يمكنه قطع الاتصال بفضل استمرار واجهة المستخدم الرسومية في الاستجابة للأحداث أثناء تنفيذ العمليات غير المتزامنة. وقد سمح لنا هذا المثال المعقد نسبيًا بتقديم مفاهيم جديدة:
- استخدام المقابس
- استخدام الطرق غير المتزامنة. ما تمت مناقشته هنا هو جزء من معيار. توجد طرق غير متزامنة أخرى تعمل على نفس النموذج.
- تحديث عناصر التحكم في واجهة المستخدم الرسومية بواسطة مؤشرات الترابط الثانوية.
تقدم الاتصالات Tcp / Ip غير المتزامنة مزايا أكثر أهمية للخادم من تلك التي أظهرها المثال السابق. من المعروف أن الخادم يخدم عملائه باستخدام خيوط ثانوية. إذا كان مجموع خيوطه يحتوي على N خيطًا، فهذا يعني أنه لا يمكنه خدمة سوى N عميل في وقت واحد. إذا كانت الخيوط N جميعها تقوم بعملية حجب (متزامنة)، فلن تكون هناك خيوط متاحة لعميل جديد حتى تنتهي إحدى العمليات الحاجبة وتحرر خيطًا. إذا تم إجراء عمليات غير متزامنة بدلاً من عمليات متزامنة على الخيوط، فلن يتم حجب أي خيط أبدًا ويمكن إعادة استخدامه بسرعة لعملاء جدد.
11.9. تطبيق نموذجي، الإصدار 8: خادم حساب الضرائب
11.9.1. بنية الإصدار الجديد
نستأنف تطبيق حساب الضرائب الذي تمت معالجته بالفعل بأشكال مختلفة. لنتذكر أحدث نسخة منه، وهي الإصدار 7 من الفقرة 9.8.
![]() |
كانت البيانات موجودة في قاعدة بيانات وكانت الطبقة [ui] عبارة عن واجهة رسومية:
![]() |
سنعيد استخدام هذه البنية ونوزعها على جهازين:
![]() |
- ستستضيف آلة [serveur] الطبقتين [metier] و [dao] من الإصدار 7. سيتم إنشاء طبقة Tcp/Ip [serveur] [1] لتمكين عملاء الإنترنت من الاستعلام عن خدمة حساب الضريبة.
- ستستضيف آلة [client] الطبقة [ui] من الإصدار 7. سيتم إنشاء طبقة Tcp/Ip [client] [2] لتمكين الطبقة [ui] من الاستعلام عن خدمة حساب الضريبة.
تتغير البنية بشكل جذري هنا. كانت الإصدار 7 تطبيق ويندوز أحادي المستخدم. أما الإصدار 8 فيصبح تطبيق عميل/خادم على الإنترنت. وسيتمكن الخادم من خدمة عدة عملاء في وقت واحد.
سنقوم أولاً بكتابة الجزء [serveur] من التطبيق.
11.9.2. خادم حساب الضرائب
11.9.2.1. مشروع Visual Studio
![]() |
سيكون مشروع Visual Studio كما يلي:
![]() |
- في [1]، المشروع. ويحتوي على العناصر التالية:
- [ServeurImpot.cs]: خادم Tcp/Ip لحساب الضريبة في شكل تطبيق وحدة التحكم.
- [dbimpots.sdf]: قاعدة البيانات SQL Server compact من الإصدار 7 الموصوفة في الفقرة 9.8.5.
- [App.config]: ملف تكوين التطبيق.
- في [2]، يحتوي المجلد [lib] على ملفات DLL اللازمة للمشروع:
- [ImpotsV7-dao]: طبقة [dao] من الإصدار 7
- [ImpotsV7-metier]: الطبقة [metier] من الإصدار 7
- [antlr.runtime, CommonLogging, Spring.Core] لـ Spring
- في [3]، مراجع المشروع
11.9.2.2. تكوين التطبيق
يتم استخدام الملف [App.config] بواسطة Spring. ومحتواه كما يلي:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- الأسطر 16-20: تكوين الطبقة [dao] المرتبطة بقاعدة SQL Server compact
- الأسطر 21-23: تكوين الطبقة [metier].
هذا هو ملف التكوين المستخدم في الطبقة [ui] من الإصدار 7. وقد تم عرضه في الفقرة 9.8.4.
11.9.2.3. تشغيل الخادم
عند بدء تشغيل الخادم، تقوم تطبيق الخادم بإنشاء مثيلات الطبقات [metier] و [dao] ثم تعرض واجهة وحدة التحكم الإدارية:
تقبل وحدة التحكم الإدارية الأوامر التالية:
لتشغيل الخدمة على منفذ معين | |
لإيقاف الخدمة. يمكن بعد ذلك إعادة تشغيلها على نفس المنفذ أو منفذ آخر. | |
لتنشيط صدى الحوار بين العميل والخادم على وحدة التحكم | |
لتعطيل الصدى | |
لعرض حالة الخدمة (نشطة / غير نشطة) | |
للخروج من التطبيق |
لنقم بتشغيل الخادم:
لنقم الآن بتشغيل عميل TCP الرسومي غير المتزامن الذي تمت دراسته سابقًا في الفقرة 11.8.

تم توصيل العميل. يمكنه إرسال الأوامر التالية إلى خادم حساب الضرائب:
للحصول على قائمة الأوامر المسموح بها | |
لحساب الضريبة لشخص لديه nbEnfants أطفال وراتب قدره salaireAnnuel يورو. marié يساوي o إذا كان الشخص متزوجًا، و n في حالة عدم الزواج. | |
لإنهاء الاتصال بالخادم |
فيما يلي مثال على الحوار:
![]() |
على جانب الخادم، تعرض وحدة التحكم ما يلي:
لنقم بتشغيل الصدى ونبدأ حوارًا جديدًا من العميل الرسومي:
![]() |
ثم تعرض وحدة التحكم الإدارية ما يلي:
- السطر 1: تم تنشيط صدى الحوار بين العميل والخادم
- السطر 2: وصل عميل
- السطر 3: أرسل الأمر [aide]
- الأسطر 4-7: رد الخادم على 4 أسطر.
دعونا نوقف الخدمة:
- السطر 1: نطلب إيقاف الخدمة (وليس التطبيق نفسه)
- السطر 2: استثناء ناتج عن توقف الخادم الذي كان في حالة انتظار للعميل بشكل مفاجئ بسبب إغلاق خدمة الاستماع.
- السطر 3: يمكن الآن إعادة تشغيل الخدمة عن طريق start port أو إيقافها عن طريق quit.
قبل إيقاف خدمة الاستماع، كان يتم خدمة عميل عبر اتصال آخر. لم يتم إغلاق هذا الاتصال بإغلاق مقبس الاستماع. يمكن للعميل الاستمرار في إرسال الأوامر: حيث يستمر مؤشر ترابط الخدمة الذي كان مرتبطًا به قبل إغلاق خدمة الاستماع في الرد عليه:

11.9.3. كود خادم Tcp لحساب الضرائب
![]() |
1 ![]() |
رمز الخادم [ServeurImpot.cs] هو التالي:
...
namespace Chap9 {
public class ServeurImpot {
// بيانات مشتركة بين الخيوط والطرق
private static IImpotMetier metier = null;
private static int port;
private static TcpListener service;
private static bool actif = false;
private static bool echo = false;
// البرنامج الرئيسي
public static void Main(string[] args) {
// مثيلات الطبقات [metier] و [dao]
IApplicationContext ctx = null;
metier = null;
try {
// سياق Spring
ctx = ContextRegistry.GetContext();
// يُطلب مرجع على الطبقة [metier]
metier = (IImpotMetier)ctx.GetObject("metier");
// تكوين مجموعة الخيوط
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// يقرأ أوامر إدارة الخادم المكتوبة على لوحة المفاتيح في حلقة لا نهائية
string commande = null;
string[] champs = null;
while (true) {
// موجه
Console.Write("Serveur de calcul d'impôt >");
// قراءة الأمر
commande = Console.ReadLine().Trim().ToLower();
champs = Regex.Split(commande, @"\s+");
// تنفيذ الأمر
switch (champs[0]) {
case "start":
// نشط؟
if (actif) {
//خطأ
Console.WriteLine("Le serveur est déjà actif");
} else {
// التحقق من المنفذ
if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
Console.WriteLine("Syntaxe : start port. Port incorrect");
} else {
// تشغيل خدمة الاستماع
ThreadPool.QueueUserWorkItem(doEcoute, null);
}
}
break;
case "echo":
// صدى بدء / إيقاف
if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
Console.WriteLine("Syntaxe : echo start / stop");
} else {
echo = champs[1] == "start";
}
break;
case "stop":
// نهاية الخدمة
if (actif) {
service.Stop();
actif = false;
}
break;
case "status":
// حالة الخادم
if (actif) {
Console.WriteLine("Le service est lancé sur le port {0}", port);
} else {
Console.WriteLine("Le service n'est pas lancé}");
}
break;
case "quit":
// الخروج من التطبيق
Console.WriteLine("Fin du service");
Environment.Exit(0);
break;
default:
// أمر غير صحيح
Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
break;
}
}
} catch (Exception e1) {
// عرض استثناء
Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
return;
}
}
private static void doEcoute(Object data) {
...
}
....
}
}
- الأسطر 18-21: يتم إنشاء مثيلات الطبقات [metier] و [dao] بواسطة Spring الذي تم تكوينه بواسطة [App.config]. ثم يتم تهيئة المتغير العام metier في السطر 6.
- الأسطر 24-25: يتم تكوين مجموعة مؤشرات الترابط للتطبيق بحد أدنى وأقصى يبلغ 10 مؤشرات ترابط.
- السطور 30-86: حلقة إدخال أوامر إدارة الخدمة (start، stop، quit، echo، status).
- السطر 32: موجه الخادم لكل أمر جديد
- السطر 34: قراءة أمر المسؤول
- السطر 35: يتم تقسيم الأمر إلى حقول لتحليله
- السطور 38-52: الأمر start port الذي يهدف إلى تشغيل خدمة الاستماع
- السطر 40: إذا كانت الخدمة نشطة بالفعل، فلا يوجد ما يجب فعله
- السطر 45: يتم التحقق من وجود المنفذ وصحة عمله. إذا كان الأمر كذلك، يتم تعيين المتغير العام port في السطر 7.
- السطر 49: ستتم إدارة خدمة الاستماع بواسطة مؤشر ترابط ثانوي حتى يتمكن مؤشر الترابط الرئيسي من الاستمرار في تنفيذ أوامر وحدة التحكم. إذا نجحت الطريقة doEcoute في الاتصال، يتم تهيئة المتغيرات العامة service في السطر 8 و actif في السطر 9.
- الأسطر 53-60: الأمر echo start / stop الذي يقوم بتنشيط / تعطيل صدى الحوار بين العميل والخادم على وحدة التحكم
- السطر 58: يتم تعيين المتغير العام echo في السطر 7
- الأسطر 61-67: الأمر stop الذي يوقف خدمة الاستماع.
- السطر 64: إيقاف خدمة الاستماع
- الأسطر 68-75: الأمر status الذي يعرض حالة الخدمة (نشطة / غير نشطة)
- الأسطر 76-80: الأمر quit الذي يوقف كل شيء.
يقوم مؤشر الترابط المسؤول عن الاستماع إلى طلبات العملاء بتنفيذ الطريقة التالية doEcoute:
private static void doEcoute(Object data) {
// خيط الاستماع لطلبات العملاء
try {
// إنشاء الخدمة
service = new TcpListener(IPAddress.Any, port);
// تشغيلها
service.Start();
// الخادم نشط
actif = true;
// المتابعة
Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
// حلقة خدمة العملاء
TcpClient tcpClient = null;
// رقم العميل
int numClient = 0;
// حلقة لا نهائية
while (true) {
// في انتظار عميل
tcpClient = service.AcceptTcpClient();
// يتم توفير الخدمة بواسطة مهمة أخرى
ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// العميل التالي
numClient++;
}
} catch (Exception ex) {
// الإبلاغ عن الخطأ
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
}
}
// معلومات العميل
internal class Client {
public TcpClient CanalTcp { get; set; } // الاتصال بالعميل
public int NumClient { get; set; } // رقم العميل
}
لدينا هنا كود مشابه لكود خادم الصدى الذي تمت دراسته في الفقرة 11.6.1. سنعلق فقط على ما هو مختلف:
- السطر 7: تم إطلاق خدمة الاستماع
- الخط 9: نلاحظ أن الخدمة أصبحت نشطة الآن
الخط 21، يتم خدمة العملاء بواسطة مؤشرات ترابط الخدمة التي تنفذ الطريقة التالية doService:
private static void doService(Object infos) {
// استرداد العميل المطلوب خدمته
Client client = infos as Client;
// تقديم الخدمة للعميل
Console.WriteLine("Début du service au client {0}", client.NumClient);
// تشغيل الاتصال TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// إرسال رسالة ترحيب إلى العميل
writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
// حلقة قراءة الطلب/كتابة الرد
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// متابعة وحدة التحكم
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// تحليل الطلب
demande = demande.Trim().ToLower();
// طلب فارغ؟
if (demande.Length == 0) {
// طلب خاطئ
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// تفكيك الطلب إلى حقول
string[] champs = Regex.Split(demande, @"\s+");
// تحليل
switch (champs[0].ToLower()) {
case "aide":
writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
break;
case "impot":
// يتم حساب الضريبة
writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
break;
case "aurevoir":
serviceFini = true;
writeClient(writer, client.NumClient, "Au revoir...");
break;
default:
writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
break;
}
}
}
}
}
}
} catch (Exception e) {
// خطأ
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
private static void writeClient(StreamWriter writer, int numClient, string message) {
// صدى وحدة التحكم؟
if (echo) {
Console.WriteLine("---> Client {0} : {1}", numClient, message);
}
// إرسال رسالة إلى العميل
writer.WriteLine(message);
}
مرة أخرى، لدينا هنا كود مشابه لكود خادم الصدى الذي تمت دراسته في الفقرة 11.6.1. لن نعلق إلا على ما هو مختلف:
- السطر 15: بمجرد اتصال العميل، يرسل له الخادم رسالة ترحيب.
- الأسطر 19-52: حلقة قراءة أوامر العميل. تتوقف الحلقة عندما يرسل العميل الأمر "aurevoir".
- السطر 27: حالة الأمر الفارغ
- السطر 34: يتم تقسيم الطلب إلى حقول لتحليله
- السطر 37: الأمر aide: يطلب العميل قائمة بالأوامر المسموح بها
- السطر 40: الأمر impot: يطلب العميل حساب الضريبة. يتم الرد بالرسالة التي تعيدها الطريقة calculImpot التي سنفصلها لاحقًا.
- السطر 44: الأمر aurevoir: يشير العميل إلى أنه قد انتهى.
- السطر 45: نستعد للخروج من حلقة قراءة طلبات العملاء (السطور 19-52)
- السطر 46: نرد على العميل برسالة وداع
- السطر 48: أمر غير صحيح. نرسل إلى العميل رسالة خطأ.
يتم معالجة الأمر impot بواسطة الطريقة calculImpot التالية:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
// طلب تحديد الحالة الاجتماعية (متزوج/غير متزوج) nbEnfants salaireAnnuel
// يلزم 4 حقول
if (champs.Length != 4) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// الحقول [1]
string marié = champs[1];
if (marié != "o" && marié != "n") {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// الحقول [2]
int nbEnfants;
if (!int.TryParse(champs[2], out nbEnfants)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// الحقول [3]
int salaireAnnuel;
if (!int.TryParse(champs[3], out salaireAnnuel)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// حسناً - نحسب الضريبة
int impot = 0;
try {
impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
return impot.ToString();
} catch (Exception ex) {
return ex.Message;
}
}
- السطر 1: تتلقى الطريقة كمعلمة ثالثة جدول حقول الأمر impot. إذا تمت صياغته بشكل صحيح، يكون على الشكل impot marié nbEnfants salaireAnnuel. تعرض الطريقة كنتيجة الرد الذي سيتم إرساله إلى العميل.
- السطر 4: يتم التحقق من أن الأمر يحتوي على 4 حقول
- السطر 8: يتم التحقق من صحة الحقل marié
- السطر 14: يتم التحقق من صحة الحقل nbEnfants
- السطر 19: يتم التحقق من صحة الحقل salaireAnnuel
- السطر 25: يتم حساب الضريبة باستخدام الطريقة CalculerImpot من الطبقة [metier]. تجدر الإشارة إلى أن هذه الطبقة مغلفة في DLL.
- السطر 26: إذا أعطت الطبقة [metier] نتيجة، يتم إرجاعها إلى العميل.
- السطر 28: إذا أطلقت الطبقة [metier] استثناءً، يتم إرجاع رسالة الاستثناء إلى العميل.
11.9.4. العميل الرسومي لخادم Tcp لحساب الضرائب
11.9.4.1. مشروع Visual Studio
![]() |
سيكون مشروع Visual Studio للعميل الرسومي كما يلي:
![]() |
- في [1]، مشروعا الحل، واحد لكل طبقة من طبقتي التطبيق
- في [2]، عميل Tcp الذي يلعب دور الطبقة [metier] للطبقة [ui]. لذلك سنستخدم المصطلحين.
- في [3]، الطبقة [ui] من الإصدار 7، مع اختلاف بسيط سنتحدث عنه
11.9.4.2. الطبقة [metier]
لم تتغير واجهة IImpotMetier. فهي لا تزال هي نفسها الموجودة في الإصدار 7:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
تنفيذ هذه الواجهة هو الفئة [ImpotMetierTcp] التالية:
using System.Net.Sockets;
using System.IO;
namespace Metier {
public class ImpotMetierTcp : IImpotMetier {
// معلومات [serveur]
private string Serveur { get; set; }
private int Port { get; set; }
// حساب الضريبة
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// يتم الاتصال بالخدمة
using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// تدفق الإخراج غير المخزن مؤقتًا
writer.AutoFlush = true;
// تخطي رسالة الترحيب
reader.ReadLine();
// الطلب
writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
// الرد
return int.Parse(reader.ReadLine());
}
}
}
}
}
}
}
- السطر 7: اسم أو عنوان IP لخادم TCP لحساب الضرائب
- السطر 8: منفذ الاستماع لهذا الخادم
- سيتم تهيئة هاتين الخاصيتين بواسطة Spring عند إنشاء مثيل للفئة [ImpotMetierTcp].
- السطر 11: طريقة حساب الضريبة. عند تنفيذها، تكون الخصائص Serveur و Port قد تم تهيئتها بالفعل. نجد في الكود النهج الكلاسيكي لعميل TCP
- السطر 13: يتم فتح الاتصال بالخادم
- الأسطر 14-16: يتم استرداد (السطر 14) تدفق الشبكة المرتبط بهذا الاتصال والذي يتم استخلاص تدفق قراءة منه (السطر 15) وتدفق كتابة (السطر 16).
- السطر 18: يجب ألا يكون تدفق الكتابة مخزّنًا في ذاكرة مؤقتة
- السطر 20: هنا، يجب أن نتذكر أنه عند فتح الاتصال، يرسل الخادم إلى العميل سطرًا أول وهو رسالة الترحيب "مرحبًا بك في خادم حساب الضريبة". يتم قراءة هذه الرسالة وتجاهلها.
- السطر 22: يتم إرسال أمر من النوع: impot o 2 60000 إلى الخادم لطلب حساب ضريبة شخص متزوج ولديه طفلان وراتب سنوي قدره 60000 يورو.
- السطر 24: يرد الخادم بمبلغ الضريبة في صيغة "4282" أو برسالة خطأ إذا كانت الطلبية غير صحيحة (وهذا لن يحدث هنا) أو إذا واجهت عملية حساب الضريبة مشكلة. هنا، لا يتم التعامل مع الحالة الأخيرة، لكن كان من الأفضل بالتأكيد القيام بذلك. في الواقع، إذا كان السطر الذي تمت قراءته عبارة عن رسالة خطأ، فسيتم إطلاق استثناء لأن التحويل إلى عدد صحيح سيفشل. سيكون الاستثناء الذي تسترده واجهة المستخدم الرسومية خطأ تحويل، في حين أن الاستثناء الأصلي ذو طبيعة مختلفة تمامًا. يُطلب من القارئ تحسين هذا الكود.
- الأسطر 25-28: تحرير جميع الموارد المستخدمة باستخدام جملة "using".
يتم تجميع الطبقة [metier] في DLL ImpotsV8-metier.dll:

11.9.4.3. الطبقة [ui]
![]() |
الطبقة [ui] [1,3] هي تلك التي تمت دراستها في الإصدار 7 في الفقرة 9.8.4، مع ثلاثة اختلافات بسيطة:
- تختلف تكوين الطبقة [metier] في [App.config] لأن تنفيذها قد تغير
- تم تعديل واجهة المستخدم الرسومية [Form1.cs] لعرض أي استثناء محتمل
- الطبقة [metier] موجودة في DLL [ImpotsV8-metier.dll].
الملف [App.config] هو التالي:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
<property name="Serveur" value="localhost"/>
<property name="Port" value="27"/>
</object>
</objects>
</spring>
</configuration>
- السطر 16: إنشاء مثيل للطبقة [metier] باستخدام الفئة Metier.ImpotMetierTcp من DLL ImpotsV8-metier.dll
- السطران 17-18: يتم تهيئة خصائص Server و Port للفئة Metier.ImpotMetierTcp. سيكون الخادم على الجهاز localhost وسيعمل على المنفذ 27.
الواجهة الرسومية المعروضة للمستخدم هي التالية:
![]() |
- في [1]، تمت إضافة TextBox لعرض أي استثناء محتمل. لم يكن هذا الحقل موجودًا في الإصدار السابق.
بصرف النظر عن هذا التفصيل، فإن كود النموذج هو نفسه الذي تمت دراسته في الفقرة 6.4.3. يُرجى من القارئ الرجوع إليها. في [2]، نرى مثالاً للتنفيذ تم الحصول عليه باستخدام خادم تم تشغيله بالطريقة التالية:
تتوافق لقطة الشاشة [2] الخاصة بالعميل مع أسطر العميل 9 أعلاه.
11.9.5. الخلاصة
مرة أخرى، تمكنا من إعادة استخدام الكود الموجود، دون تعديلات (طبقات [metier]، [dao] للخادم) أو مع تعديلات قليلة جدًا (طبقة [ui] للعميل). وقد أصبح ذلك ممكنًا بفضل استخدامنا المنهجي للواجهات وتجسيدها باستخدام Spring. لو كنا في الإصدار 7 قد وضعنا كود الأعمال مباشرةً في معالجات الأحداث الخاصة بالواجهة الرسومية، لما كان هذا الكود قابلاً لإعادة الاستخدام. وهذا هو العيب الرئيسي للبنى أحادية الطبقة.
وأخيرًا، تجدر الإشارة إلى أن الطبقة [ui] لا تعلم أن الخادم البعيد هو الذي يحسب مبلغ الضريبة.























































