Skip to content

10. خدمات الويب

10.1. مقدمة

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

  • يتم استضافتها بواسطة خوادم الويب، وبالتالي فإن بروتوكول التبادل بين العميل والخادم هو HTTP (بروتوكول النقل HyperText)، وهو بروتوكول أعلى من TCP-IP.
  • تتمتع خدمة الويب ببروتوكول تواصل قياسي بغض النظر عن الخدمة المقدمة. تقدم خدمة الويب خدمات متنوعة مثل S1، S2، ...، Sn. كل خدمة من هذه الخدمات تتوقع معلمات مقدمة من العميل وتُرجع إليه نتيجة. بالنسبة لكل خدمة، يحتاج العميل إلى معرفة:
    • الاسم الدقيق للخدمة إذا
    • قائمة المعلمات التي يجب تزويدها بها ونوعها
    • نوع النتيجة التي تعيدها الخدمة

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

  • ولأسباب أمنية تتعلق بالهجمات القادمة من الإنترنت، تمتلك العديد من المؤسسات شبكات خاصة ولا تفتح على الإنترنت سوى منافذ معينة من خوادمها: وبشكل أساسي المنفذ 80 الخاص بخدمة الويب. أما جميع المنافذ الأخرى فهي مغلقة. ولذلك، فإن تطبيقات العميل-الخادم، كما تم عرضها في الفصل السابق، تُبنى داخل الشبكة الخاصة (الإنترانت) ولا يمكن الوصول إليها عمومًا من الخارج. ويؤدي استضافة خدمة ما داخل خادم ويب إلى إتاحتها لجميع مستخدمي الإنترنت.
  • يمكن تمثيل خدمة الويب ككائن بعيد. وبذلك تصبح الخدمات المقدمة طرقًا لهذا الكائن. يمكن للعميل الوصول إلى هذا الكائن البعيد كما لو كان محليًّا. وهذا يخفي الجزء المتعلق بالاتصال الشبكي بالكامل ويسمح بإنشاء عميل مستقل عن هذه الطبقة. فإذا تغيرت هذه الطبقة، فلن يتعين تعديل العميل. وهذه ميزة هائلة وربما تكون الميزة الرئيسية لخدمات الويب.
  • وكما هو الحال مع تطبيقات العميل-الخادم TCP/IP التي تم عرضها في الفصل السابق، يمكن كتابة كل من العميل والخادم بأي لغة برمجة. ويتبادلان أسطرًا نصية. وتتألف هذه الأسطر من جزأين:
    • الرؤوس اللازمة لبروتوكول HTTP
    • وجسم الرسالة. بالنسبة لرد الخادم على العميل، يكون هذا الرد بتنسيق XML (لغة ترميز eXtensible). أما بالنسبة لطلب العميل الموجه إلى الخادم، فيمكن أن يتخذ جسم الرسالة عدة أشكال، منها XML. قد يكون لطلب العميل XML تنسيق خاص يُسمى SOAP (بروتوكول الوصول البسيط إلى الكائنات). في هذه الحالة، تتبع استجابة الخادم أيضًا التنسيق SOAP.

10.2. المتصفحات و XML

ترسل خدمات الويب XML إلى عملائها. قد تتفاعل المتصفحات بشكل مختلف عند استلام هذا التدفق XML. يحتوي Internet Explorer على ورقة أنماط محددة مسبقًا تسمح بعرضه. أما Netscape Communicator فلا يمتلك ورقة الأنماط هذه ولا يعرض رمز XML المستلم. يجب عرض الكود المصدري للصفحة المستلمة للوصول إلى XML. فيما يلي مثال على الرمز XML التالي:

<?xml version="1.0" encoding="utf-8"?>
<string xmlns="st.istia.univ-angers.fr">bonjour de nouveau !</string>

سيعرض Internet Explorer الصفحة التالية:

Image

بينما سيعرض متصفح Netscape Navigator:

Image

إذا قمنا بعرض الكود المصدري للصفحة التي استقبلها متصفح «نتسكيب»، فسنحصل على:

Image

لقد تلقى Netscape بالفعل نفس البيانات التي تلقاها Internet Explorer، لكنه عرضها بشكل مختلف. في ما يلي، سنستخدم Internet Explorer لعرض لقطات الشاشة.

10.3. خدمة ويب أولى

سنتعرف على خدمات الويب من خلال مثال بسيط للغاية يتوفر بثلاث نسخ.

10.3.1. الإصدار 1

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

باستخدام VS.NET، لنقم بإنشاء مشروع جديد باستخدام الخيار [Fichier/Nouveau/Projet]:

Image

تجدر الإشارة إلى النقاط التالية:

  • نوع المشروع هو Visual Basic (الإطار الأيسر)
  • نموذج المشروع هو خدمة ويب ASP.NET (الإطار الأيمن)
  • المساحة خالية. هنا، سيتم استضافة خدمة الويب بواسطة خادم ويب محلي IIS. وبالتالي، سيكون عنوانه http://localhost/[chemin] حيث يتعين تحديد [chemin]. هنا، نختار المسار http://localhost/polyvbnet/demo. سيقوم VS.NET عندئذٍ بإنشاء مجلد لهذا المشروع. أين؟ يحتوي الخادم IIS على جذر لشجرة المستندات الويب التي يقدمها. لنسمي هذا الجذر <IISroot>. وهو يتوافق مع URL http://localhost. ونستنتج من ذلك أن URL http://localhost/polyvbnet/demo سيتم ربطه بالمجلد <IISroot>/polyvbnet/demo. عادةً ما يكون <IISroot> هو المجلد \inetpub\wwwroot على القرص الذي تم تثبيت IIS عليه. في مثالنا، هو القرص E. وبالتالي، فإن المجلد الذي أنشأه VS.NET هو المجلد e:\inetpub\wwwroot\polyvbnet\demo:

Image

وكما هو الحال دائمًا، هناك عدد كبير جدًّا من المجلدات التي تم إنشاؤها. وهي ليست دائمًا ذات فائدة. لن نوضح سوى تلك التي نحتاجها في وقت معين. قام VS.NET بإنشاء مشروع:

Image

نجد بعض الملفات الموجودة في المجلد الفعلي للمشروع. الأمر الأكثر أهمية بالنسبة لنا هو الملف الذي يحمل الامتداد asmx. هذا هو امتداد خدمات الويب. يتم إدارة خدمة الويب بواسطة VS.NET كتطبيق ويندوز، c.a.d، وهو تطبيق يحتوي على واجهة رسومية ورمز برمجي لإدارته. ولهذا السبب، لدينا نافذة تصميم:

Image

لا تحتوي خدمة الويب عادةً على واجهة رسومية. فهي تمثل كائنًا يمكن استدعاؤه عن بُعد. وتحتوي على طرق تستدعيها التطبيقات. لذلك سننظر إليها ككائن تقليدي مع ميزة خاصة تتمثل في إمكانية إنشاء مثيل لها عن بُعد عبر الشبكة. ولذلك، لن نستخدم نافذة التصميم التي يقدمها VS.NET. بل سنركز بدلاً من ذلك على كود الخدمة باستخدام خيار «العرض/الكود»:

Image

هناك عدة نقاط جديرة بالملاحظة:

  • اسم الملف هو Service1.asmx.vb وليس Service1.asmx. سنعود إلى محتوى الملف Service1.asmx لاحقًا.
  • نجد نافذة كود مشابهة لتلك التي كانت لدينا عند إنشاء تطبيقات ويندوز باستخدام VS.NET

الكود الذي تم إنشاؤه بواسطة VS.NET هو التالي:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService

#المنطقة "رمز تم إنشاؤه بواسطة مصمم خدمات الويب"

    Public Sub New()
        MyBase.New()

        'هذا الاستدعاء مطلوب من قبل مصمم خدمات الويب.
        InitializeComponent()

        'أضف رمز التهيئة الخاص بك بعد الاستدعاء InitializeComponent()

    End Sub

    'مطلوب من قبل مصمم خدمات الويب
    Private components As System.ComponentModel.IContainer

    'REMARQUE: الإجراء التالي مطلوب من قبل مصمم خدمات الويب
    'يمكن تعديلها باستخدام مصمم خدمات الويب.  
    'لا تقم بتعديلها باستخدام محرر الكود.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: هذا الإجراء مطلوب من قبل مصمم خدمات الويب
        'لا تقم بتعديلها باستخدام محرر الكود.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#نهاية المنطقة

    ' EXEMPLE DE SERVICE WEB
    ' يعرض مثال الخدمة HelloWorld() السلسلة Hello World.
    ' لإنشاء المشروع، لا تقم بتعليق الأسطر التالية، ثم احفظ المشروع وقم بإنشائه.
    ' لاختبار خدمة الويب هذه، تأكد من أن ملف .asmx هو الصفحة الافتتاحية
    ' واضغط على F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

End Class

أولاً، لاحظ أننا أمام فئة، وهي فئة Service1 التي تنحدر من فئة WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

وهذا يقودنا إلى استيراد مساحة الأسماء System.Web.Services:


Imports System.Web.Services

يسبق إعلان الفئة سمة تجميع:


<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService

تشير السمة System.Web.Services.WebService() إلى أن الفئة التي تليها هي خدمة ويب. تقبل هذه السمة معلمات متنوعة، من بينها معلمة تُسمى NameSpace. وتُستخدم هذه المعلمة لوضع خدمة الويب في مساحة أسماء. ففي الواقع، يمكننا أن نتصور وجود عدة خدمات ويب تُسمى «meteo» في العالم. لذا نحتاج إلى وسيلة للتمييز بينها. ومساحة الأسماء هي التي تتيح ذلك. فقد يُسمى أحدها [espacenom1].meteo، وآخر [espacenom2].meteo. ونجد هنا مفهومًا مشابهًا لمساحات أسماء الفئات. قام VS.NET تلقائيًا بإنشاء كود ووضعه في منطقة من الكود المصدري:

#المنطقة "كود تم إنشاؤه بواسطة مصمم خدمات الويب"

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

تنتهي الدرس بمثال على ما يمكن أن تكون عليه خدمة الويب:


#نهاية المنطقة

    ' EXEMPLE DE SERVICE WEB
    ' يعرض مثال الخدمة HelloWorld() السلسلة Hello World.
    ' لإنشاء المشروع، لا تقم بتعليق الأسطر التالية، ثم احفظ المشروع وقم بإنشائه.
    ' لاختبار خدمة الويب هذه، تأكد من أن ملف .asmx هو الصفحة الافتتاحية
    ' واضغط على F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

بناءً على ما سبق، نقوم بتنقيح الكود ليصبح كما يلي:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function Bonjour() As String
        Return "bonjour !"
    End Function
End Class

أصبح الأمر أكثر وضوحًا الآن.

  • خدمة الويب هي فئة مشتقة من الفئة WebService
  • يتم تعريف الفئة بالسمة <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. وبالتالي، نضع خدمتنا في مساحة الأسماء st.istia.univ-angers.fr.
  • يتم تحديد طرق الفئة بواسطة السمة <WebMethod()> التي تشير إلى أننا نتعامل مع طريقة يمكن استدعاؤها عن بُعد عبر الشبكة

وبالتالي، فإن الفئة التي توفر خدمة الويب لدينا تسمى Bonjour وتحتوي على طريقة واحدة تسمى هي الأخرى Bonjour وتُرجع سلسلة أحرف. نحن جاهزون لإجراء الاختبار الأول.

  • لنقم بتشغيل خادم الويب IIS إذا لم يكن قد تم ذلك بعد
  • لنستخدم الخيار «تصحيح/تشغيل بدون تصحيح». VS.NET

سيقوم VS.NET بعد ذلك بترجمة التطبيق بالكامل، وتشغيل متصفح (غالبًا ما يكون Internet Explorer إن كان موجودًا)، وعرض عنوان URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

لماذا عنوان URL http://localhost/polyvbnet/demo/Service1.asmx؟ لأنه كان الملف .asmx الوحيد في المشروع:

Image

لو كان هناك عدة ملفات .asmx، لكان علينا تحديد الملف الذي يجب تنفيذه أولاً. ويتم ذلك بالنقر بزر الماوس الأيمن على ملف .asmx المعني واختيار الخيار [Définir comme page de démarrage].

Image

قد يثير اهتمامنا معرفة محتوى الملف service1.asmx. ففي الواقع، باستخدام VS.NET، قمنا بمعالجة الملف service1.asmx.vb وليس الملف service1.asmx. ويوجد هذا الملف في مجلد المشروع:

Image

لنفتحه باستخدام محرر نصوص (مثل Notepad أو غيره). نحصل على المحتوى التالي:

<%@ WebService Language="vb" Codebehind="Service1.asmx.vb" Class="demo.Bonjour" %>

يحتوي الملف على توجيه بسيط موجه إلى الخادم IIS يشير إلى:

  • أننا نتعامل مع خدمة ويب (الكلمة الرئيسية WebService)
  • أن لغة فئة هذه الخدمة هي Visual Basic (Language="vb")
  • أن مصدر هذه الفئة سيوجد في الملف Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • أن الفئة التي تنفذ الخدمة تسمى demo.Bonjour (Class="demo.Bonjour"). تجدر الإشارة إلى أن VS.NET قد وضعت الفئة Bonjour في مساحة أسماء demo التي هي أيضًا اسم المشروع.

لنعد إلى الصفحة التي تم الحصول عليها عبر الرابط http://localhost/polyvbnet/demo/Service1.asmx:

Image

من الذي كتب الكود HTML للصفحة أعلاه؟ ليس نحن، ونحن نعلم ذلك. إنه IIS، الذي يعرض خدمات الويب بطريقة قياسية. تقدم لنا هذه الصفحة رابطين. لنتبع الرابط الأول [Description du service]:

Image

أوبس... هذا هو XML الذي يبدو غامضًا بعض الشيء. لكن دعونا نلاحظ مع ذلك URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. افتح متصفحًا، واكتب هذا الرابط مباشرةً. ستحصل على نفس النتيجة السابقة. لنتذكر إذن أن الرابط http://serviceweb?WSDL يتيح الوصول إلى الوصف XML لخدمة الويب. لنعد إلى الصفحة الرئيسية ونختار الرابط [Bonjour]. لنتذكر أن Bonjour هي إحدى طرق خدمة الويب. لو كانت هناك طرق متعددة، لكانت جميعها معروضة هنا. نحصل على الصفحة الجديدة التالية:

Image

لقد قمنا عمدًا باقتطاع جزء من الصفحة التي حصلنا عليها حتى لا نثقل عرضنا التوضيحي. لنلاحظ مرة أخرى عنوان URL الذي حصلنا عليه:

http://localhost/polyvbnet/demo/Service1.asmx?op=Bonjour

إذا أدخلنا عنوان URL هذا مباشرةً في متصفح، فسنحصل على نفس النتيجة المذكورة أعلاه. يُطلب منا استخدام الزر [Appeler]. فلنفعل ذلك. نحصل على صفحة جديدة:

Image

إنه مرة أخرى XML. نجد فيه معلومتين كانتا موجودتين في خدمة الويب الخاصة بنا:

  • مساحة الاسم st.istia.univ-angers.fr لخدمتنا

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • القيمة التي أرجعتها طريقة Bonjour:

        Return "bonjour !"

ماذا تعلمنا؟

  • كيفية كتابة خدمة ويب S
  • كيفية استدعائها

سنركز الآن على كتابة خدمة ويب دون الاستعانة بـ VS.NET.

10.3.2. الإصدار 2

في المثال السابق، قام VS.NET بالعديد من المهام بمفرده. هل من الممكن إنشاء خدمة ويب بدون هذه الأداة؟ الإجابة هي نعم، وسنوضح ذلك الآن. باستخدام محرر نصوص، سنقوم بإنشاء خدمة الويب التالية:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour2
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function getBonjour() As String
        Return "bonjour de nouveau !"
    End Function
End Class

تسمى الفئة Bonjour2 وتحتوي على طريقة تسمى getBonjour. وقد وُضعت هذه الطريقة في الملف demo2.vb الذي يقع بدوره ضمن شجرة المجلدات الخاصة بالخادم IIS في المجلد E:\Inetpub\wwwroot\polyvbnet\demo2. وهي فئة VB.NET تقليدية يمكن تجميعها:

dos>vbc /out:demo2 /t:library /r:system.dll /r:system.web.services.dll demo2.vb

dos>dir
02/03/2004  18:04                  286 demo2.vb
02/03/2004  18:10                   77 demo2.asmx
02/03/2004  18:12                3 072 demo2.dll

نضع التجميع demo2.dll في مجلد bin (هذا الاسم إلزامي):

dos>dir bin
02/03/2004  18:12                3 072 demo2.dll

نقوم الآن بإنشاء الملف demo2.asmx. هذا هو الملف الذي سيتم استدعاؤه من قبل عملاء الويب. ومحتواه كما يلي:

<%@ WebService Language="vb" class="Bonjour2,demo2"%>

لقد سبق أن تعرفنا على هذه التوجيهية. وهي تشير إلى أن:

  • أن فئة خدمة الويب تسمى Bonjour2 وتوجد في التجميع demo2.dll. سيبحث IIS عن هذا التجميع في أماكن مختلفة، ولا سيما في مجلد bin الخاص بخدمة الويب. ولهذا السبب، قمنا بوضع التجميع demo2.dll هناك.

يمكننا الآن إجراء اختبارات متنوعة. نتأكد من أن IIS نشط، ثم نطلب عبر متصفح الويب عنوان URL http://localhost/polyvbnet/demo2/demo2.asmx:

Image

ثم الرابط http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

ثم URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour، حيث getBonjour هو اسم الطريقة الوحيدة لخدمة الويب الخاصة بنا:

Image

نستخدم الزر [Appeler] المذكور أعلاه:

Image

نحصل بالفعل على نتيجة استدعاء الطريقة getBonjour لخدمة الويب. نحن نعرف الآن كيفية إنشاء خدمة ويب بدون vs.net. سنقوم من الآن فصاعدًا بتجاهل طريقة إنشاء خدمة الويب لنركز فقط على الملفات الأساسية.

10.3.3. الإصدار 3

استخدمت النسختان السابقتان من خدمة الويب [Bonjour] ملفين:

  • ملف .asmx، وهو نقطة الدخول إلى خدمة الويب
  • ملف .vb، وهو شفرة المصدر لخدمة الويب

نوضح هنا أنه يمكن الاكتفاء بملف .asmx وحده. كود الخدمة demo3.asmx هو كما يلي:

<%@ WebService Language="vb" class="Bonjour3"%>

Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour3
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function getBonjour() As String
        Return "bonjour en version3 !"
    End Function
End Class

نلاحظ أن كود الخدمة موجود الآن مباشرةً في الملف المصدر لـ demo3.asmx. التوجيه

<%@ WebService Language="vb" class="Bonjour3"%>

لم تعد تشير إلى فئة في تجميع خارجي، بل إلى فئة موجودة في نفس الملف المصدر. لنضع هذا الملف في المجلد <IISroot>\polyvbnet\demo3:

Image

لنقم بتشغيل IIS ونطلب عنوان URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

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

Image

10.3.4. الإصدار 4

نركز هنا على تهيئة الخادم IIS. لقد دأبنا حتى الآن على وضع خدمات الويب الخاصة بنا في الشجرة الجذرية <IISroot> للخادم IIS، وهو هنا [e:\inetpub\wwwroot]. نوضح هنا أنه يمكننا وضع الخدمة الويب في أي مكان. ويتم ذلك باستخدام المجلدات الافتراضية لـ IIS. لنضع خدمتنا في المجلد التالي:

Image

المجلد [D:\data\devel\vbnet\poly\chap9\demo3] غير موجود في شجرة المجلدات الخاصة بالخادم IIS. يجب إعلام الخادم بذلك عن طريق إنشاء مجلد افتراضي باسم IIS. لنقم بتشغيل IIS ونختار الخيار [Avancé] أدناه:

Image

تظهر لنا قائمة بالمجلدات الافتراضية. لن نتطرق إليها بالتفصيل. نقوم بإنشاء مجلد افتراضي جديد باستخدام الزر [Ajouter] أعلاه:

Image

باستخدام الزر [Parcourir]، نحدد المجلد الفعلي الذي يحتوي على خدمة الويب، وهو في هذه الحالة المجلد [D:\data\devel\vbnet\poly\chap9\demo3]. نقوم بتسمية هذا المجلد باسم منطقي (افتراضي): [virdemo3]. وهذا يعني أن المستندات الموجودة داخل المجلد الفعلي [D:\data\devel\vbnet\poly\chap9\demo3] ستكون متاحة على الشبكة عبر عنوان URL [http://<machine>/virdemo3]. تحتوي نافذة الحوار أعلاه على إعدادات أخرى نتركها كما هي. نضغط على «موافق» لإغلاق النافذة. تظهر المجلد الافتراضي الجديد في قائمة المجلدات الافتراضية الخاصة بـ IIS:

Image

الآن، نفتح متصفحًا ونطلب عنوان URL [http://localhost/virdemo3/demo3.asmx]. نحصل على نفس النتيجة السابقة:

Image

10.3.5. الخلاصة

لقد أوضحنا عدة طرق لإنشاء خدمة ويب. وفيما بعد، سنستخدم طريقة الإصدار 3 لإنشاء الخدمة وطريقة 4 لتحديد موقعها. وبذلك لن نحتاج إلى VS.NET. ومع ذلك، تجدر الإشارة إلى فائدة استخدام VS.NET لما يقدمه من مساعدة في تصحيح الأخطاء. تتوفر أدوات مجانية لتطوير تطبيقات الويب، ولا سيما المنتج WebMatrix الذي ترعاه شركة مايكروسوفت والذي يمكن العثور عليه على URL [http://www.asp.net/webmatrix]. إنها أداة ممتازة لبدء البرمجة على الويب دون الحاجة إلى استثمار مسبق.

10.4. خدمة ويب للعمليات

لنفترض وجود خدمة ويب تقدم خمس وظائف:

  1. إضافة(a,b) التي ستُرجع a+b
  2. طرح(a,b) التي ستُرجع a-b
  3. الضرب(a,b) التي ستُنتج a*b
  4. القسمة(a,b) التي ستُرجع a/b
  5. «كل شيء»(a,b) التي ستُرجع المصفوفة [a+b,a-b,a*b,a/b]

رمز VB.NET لهذه الخدمة هو التالي:


<%@ WebService language="VB" class=operations %>

imports system.web.services

<WebService(Namespace:="st.istia.univ-angers.fr")> _
   Public Class operations
      Inherits WebService

      <WebMethod>  _
      Function ajouter(a As Double, b As Double) As Double
         Return a + b
      End Function 
   
      <WebMethod>  _
      Function soustraire(a As Double, b As Double) As Double
         Return a - b
      End Function 

      <WebMethod>  _
      Function multiplier(a As Double, b As Double) As Double
         Return a * b
      End Function 

      <WebMethod>  _
      Function diviser(a As Double, b As Double) As Double
         Return a / b
      End Function 

      <WebMethod>  _
      Function toutfaire(a As Double, b As Double) As Double()
         Return New Double() {a + b, a - b, a * b, a / b}
      End Function 
   End Class 

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

  • تسبق الطرق سمة <WebMethod()> التي تشير للمترجم إلى الطرق التي يجب «نشرها» c.a.d، أي إتاحتها للعميل. أما الطريقة التي لا يسبقها هذا السمة، فستكون غير مرئية للعملاء البعيدين. وقد تكون هذه طريقة داخلية تستخدمها طرق أخرى، ولكنها غير مخصصة للنشر.
  • تُشتق الفئة من الفئة WebService المُعرَّفة في مساحة الأسماء System.Web.Services. هذا التوريث ليس إلزاميًا دائمًا. في هذا المثال على وجه الخصوص، يمكن الاستغناء عنه.
  • تسبق الفئة نفسها سمة <WebService(Namespace="st.istia.univ-angers.fr")> تهدف إلى توفير مساحة أسماء لخدمة الويب. يمنح مزود الفئات مساحة أسماء لفئاته من أجل تزويدها باسم فريد، وبالتالي تجنب التعارضات مع فئات مزودين آخرين قد تحمل الاسم نفسه. الأمر نفسه ينطبق على خدمات الويب. يجب أن يكون من الممكن تحديد كل خدمة ويب باسم فريد، وهو هنا st.istia.univ-angers.fr.
  • لم نقم بتعريف منشئ. لذا، سيتم استخدام منشئ الفئة الأم ضمناً.

الكود المصدري السابق ليس موجهًا مباشرةً إلى المُجمِّع VB.NET بل إلى خادم الويب IIS. يجب أن يحمل اللاحقة .asmx وأن يتم حفظه في شجرة المجلدات الخاصة بخادم الويب. وهنا نقوم بحفظه تحت الاسم operations.asmx في المجلد <IISroot>\polyvbnet\operations:

Image

نربط بهذا المجلد الفعلي المجلد الافتراضي IIS [operations]:

دعونا ندخل إلى الخدمة باستخدام متصفح. الملف URl المطلوب هو [http://localhost/operations/operations.asmx]:

Image

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

Image

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

      <WebMethod>  _
      Function ajouter(a As Double, b As Double) As Double
         Return a + b
      End Function 

تجدر الإشارة إلى أن الصفحة قد استخدمت أسماء المعلمات a و b المستخدمة في تعريف الأسلوب. عند استخدام الزر Appeler، نحصل على الاستجابة التالية في نافذة منفصلة بالمتصفح:

Image

وإذا استخدمنا في المثال أعلاه [Affichage/Source]، فسنحصل على الكود التالي:

Image

لنكرر العملية مع الطريقة [toutfaire]:

Image

نحصل على الصفحة التالية:

Image

لنستخدم الزر [Appeler] أعلاه:

Image

في جميع الأحوال، تكون استجابة الخادم على النحو التالي:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • الاستجابة بتنسيق XML
  • السطر 1 قياسي ويكون موجودًا دائمًا في الرد
  • الأسطر التالية تعتمد على نوع النتيجة (double، ArrayOfDouble)، وعدد النتائج، ومساحة أسماء خدمة الويب (st.istia.univ-angers.fr هنا).

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

Image

ولنتبع الرابط [ajouter]. في الصفحة المعروضة، تظهر طريقتان لاستدعاء الدالة [ajouter] لخدمة الويب:

Image

Image

Image

تُسمى هاتان الطريقتان للوصول إلى وظائف خدمة الويب على التوالي: HTTP-POST و SOAP. سنقوم الآن بفحصهما واحدة تلو الأخرى.

ملاحظة: في الإصدارات الأولى من VS.NET، كانت هناك طريقة ثالثة تُسمى HTTP-GET. في تاريخ كتابة هذا المستند (مارس 2004)، يبدو أن هذه الطريقة لم تعد متاحة. وهذا يعني أن خدمة الويب التي تم إنشاؤها بواسطة VS.NET لا تقبل طلبات GET. وهذا لا يعني أنه لا يمكن كتابة خدمات ويب تقبل طلبات GET، لا سيما باستخدام أدوات أخرى غير VS.NET أو ببساطة يدويًّا.

10.5. عميل HTTP-POST

نتبع الطريقة التي تقترحها خدمة الويب:

Image

دعونا نعلق على ما هو مكتوب. أولًا، يجب على عميل الويب إرسال الرؤوس التالية:

POST /operations/operations.asmx/ajouter HTTP/1.1
يقوم عميل الويب بإرسال طلب POST إلى URL /operations/operations.asmx/ajouter وفقًا لبروتوكول HTTP الإصدار 1.1
HOST: localhost
يتم تحديد الجهاز المستهدف للطلب. هنا هو localhost. أصبح هذا الرأس إلزاميًا بموجب الإصدار 1.1 من بروتوكول HTTP
Content-Type: application/x-www-form-urlencoded
يتم هنا تحديد أنه بعد الرؤوس HTTP سيتم إرسال معلمات إضافية بتنسيق urlencoded. يستبدل هذا التنسيق بعض الأحرف برموزها السداسية العشرية.
Content-length: 7
هذا هو حجم سلسلة المعلمات بالأحرف التي سيتم إرسالها بعد الرؤوس HTTP.

تتبع رؤوس القسم HTTP سطر فارغ ثم سلسلة المعلمات المكونة من POST إلى [Content-Length] حرفًا بالصيغة a=XX&b=YY حيث XX و YY هما السلسلتان «المشفرتان بترميز URL» لقيم المعلمتين a و b. لدينا ما يكفي من المعلومات لتكرار ما ورد أعلاه باستخدام عميل TCP العام الذي استخدمناه سابقًا في الفصل الخاص ببرمجة TCP/IP:

  • نقوم بتشغيل IIS
  • الخدمة متاحة على عنوان URL [http://localhost/operations/operations.asmx]
  • نستخدم عميل TCP العام في نافذة DOS
dos>clttcpgenerique localhost 80
Commandes :
POST /operations/operations.asmx/ajouter HTTP/1.1
HOST: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-length: 7

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

نلاحظ أولاً أننا أضفنا الرأس [Connection: close] لطلب من الخادم إغلاق الاتصال بعد إرسال الرد. وهذا أمر ضروري هنا. فإذا لم نذكر ذلك، فسيبقي الخادم الاتصال مفتوحًا بشكل افتراضي. إلا أن الرد عبارة عن سلسلة من أسطر النص لا تنتهي آخرها بعلامة نهاية السطر. ويحدث أن عميلنا العام TCP يقرأ أسطر النص التي تنتهي بعلامة نهاية السطر باستخدام الطريقة ReadLine. إذا لم يقم الخادم بإغلاق الاتصال بعد إرسال السطر الأخير، فإن العميل يتعطل لأنه ينتظر علامة نهاية السطر التي لا تصل. أما إذا أغلق الخادم الاتصال، فإن الطريقة ReadLine الخاصة بالعميل تنتهي ولا يتعطل العميل.

فور استلام السطر الفارغ الذي يشير إلى نهاية الرؤوس HTTP، يرسل الخادم IIS أول رد:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--

هذا الرد المكون حصريًا من الرؤوس HTTP يُشير للعميل بأنه يمكنه إرسال الأحرف السبعة التي ذكر أنه يرغب في إرسالها. ما نقوم به:

a=2&b=3

يجب ملاحظة هنا أن عميل TCP الخاص بنا يرسل أكثر من 7 أحرف لأنه يرسلها مع علامة نهاية السطر (WriteLine). لا يشكل ذلك مشكلة بالنسبة للخادم الذي سيأخذ فقط الأحرف السبعة الأولى من الأحرف المستلمة، وذلك لأن الاتصال يُغلق بعد ذلك (Connection: close). كانت هذه الأحرف الزائدة ستشكل مشكلة لو بقي الاتصال مفتوحًا، لأنها كانت ستُعتبر على أنها قادمة من الأمر التالي للعميل. بمجرد استلام المعلمات، يرسل الخادم ردّه:

<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>

لدينا الآن العناصر اللازمة لكتابة عميل مبرمج لخدمتنا على الويب. سيكون هذا عميل وحدة التحكم يُسمى httpPost2 ويُستخدم على النحو التالي:

dos>httpPost2 http://localhost/operations/operations.asmx

Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]

soustraire 8 9
--> POST /operations/operations.asmx/soustraire HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">-1</double>
[résultat=-1]

fin

dos>

يتم استدعاء العميل عن طريق تمرير URL من خدمة الويب إليه:

dos>httpPost2 http://localhost/operations/operations.asmx

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

fonction a b

حيث «function» هي دالة خدمة الويب التي يتم استدعاؤها (الجمع، الطرح، الضرب، القسمة)، و«a» و«b» هما القيمتان اللتان ستعمل عليهما هذه الدالة. على سبيل المثال:

ajouter 6 7

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

ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]

نجد أعلاه التبادل الذي سبق أن صادفناه مع عميل TCP العام مع اختلاف واحد: الرأس HTTP Connection: Keep-Alive يطلب من الخادم عدم إغلاق الاتصال. وبالتالي يبقى هذا الاتصال مفتوحًا للعملية التالية للعميل، الذي لا يحتاج بالتالي إلى إعادة الاتصال بالخادم. ومع ذلك، فإن هذا يجبره على استخدام طريقة أخرى غير ReadLine() لقراءة استجابة الخادم، حيث نعلم أن هذه الاستجابة عبارة عن سلسلة من الأسطر لا تنتهي آخرها بعلامة نهاية السطر. وبمجرد الحصول على استجابة الخادم بالكامل، يقوم العميل بتحليلها للعثور على نتيجة العملية المطلوبة وعرضها:

[résultat=13]

دعونا نلقي نظرة على كود العميل الخاص بنا:


' مساحات الأسماء
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web

' عميل خدمة ويب operations
Public Module clientPOST

    Public Sub Main(ByVal args() As String)
        ' الصياغة
        Const syntaxe As String = "pg URI"
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}

        ' عدد الوسيطات
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' يُلاحظ أن URI مطلوب
        Dim URIstring As String = args(0)

        ' يتم الاتصال بالخادم
        Dim uri As Uri = Nothing        ' l'URI du service web
        Dim client As TcpClient = Nothing        ' la liaison tcp du client avec le serveur
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Try
            ' الاتصال بالخادم
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' يتم إنشاء تدفقات الإدخال والإخراج للعميل TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI غير صحيح أو مشكلة أخرى
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

        ' إنشاء قاموس وظائف خدمة الويب
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' يتم إدخال طلبات المستخدم عبر لوحة المفاتيح
        ' بالصيغة function a b
        ' تنتهي بالأمر «fin»
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande
        Dim fonction As String = Nothing        ' nom d'une fonction du service web
        Dim a, b As String        ' les arguments des fonctions du service web

        ' تطلب من المستخدم
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")

        ' إدارة الأخطاء
        Dim erreurCommande As Boolean
        Try
            ' حلقة إدخال الأوامر المكتوبة على لوحة المفاتيح
            While True
                ' لا توجد أخطاء في البداية
                erreurCommande = False
                ' قراءة الأمر
                commande = Console.In.ReadLine().Trim().ToLower()
                ' انتهى؟
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' تقسيم الأمر إلى حقول
                champs = Regex.Split(commande, "\s+")
                Try
                    ' يلزم وجود ثلاثة حقول
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' يجب أن يكون الحقل 0 دالة معترف بها
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' يجب أن يكون الحقل 1 رقمًا صالحًا
                    a = champs(1)
                    Double.Parse(a)
                    ' يجب أن يكون الحقل 2 رقمًا صالحًا
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' أمر غير صالح
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' يتم إرسال الطلب إلى خدمة الويب
                If Not erreurCommande Then executeFonction([IN], OUT, uri, fonction, a, b)
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' انتهى الاتصال بين العميل والخادم
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' عرض الأخطاء
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' عرض الخطأ
        System.Console.Error.WriteLine(msg)
        ' إيقاف مع خطأ
        Environment.Exit(exitCode)
    End Sub
End Module

هناك عناصر سبق أن صادفناها عدة مرات ولا تحتاج إلى تعليقات خاصة. لنلقِ نظرة الآن على كود الأسلوب executeFonction حيث توجد العناصر الجديدة:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' تنفيذ الدالة (a,b) على خدمة الويب ذات عنوان URI URI
        ' تتم عمليات التبادل بين العميل والخادم عبر تدفقات IN و OUT
        ' توجد نتيجة الدالة في السطر
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' التي أرسلها الخادم

        ' بناء سلسلة الاستعلام
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' إنشاء جدول الرؤوس HTTP المراد إرساله
        Dim entetes(5) As String
        entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: application/x-www-form-urlencoded"
        entetes(3) = "Content-Length: " & nbChars
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = ""

        ' إرسال الرؤوس HTTP إلى الخادم
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' الإرسال إلى الخادم
            OUT.WriteLine(entetes(i))
            ' عرض على الشاشة
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

        ' قراءة الرد الأول من خادم الويب HTTP/1.1 100
        Dim ligne As String = Nothing
        ' سطر من تدفق القراءة
        ligne = [IN].ReadLine()
        While ligne <> ""
            'عرض
            Console.Out.WriteLine(("<-- " + ligne))
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While
        'صدى السطر الأخير
        Console.Out.WriteLine(("<-- " + ligne))

        ' إرسال معلمات الطلب
        OUT.Write(requête)
        ' صدى
        Console.Out.WriteLine(("--> " + requête))

        ' بناء التعبير النمطي الذي يسمح بتحديد حجم الرد XML
        ' في تدفق استجابة خادم الويب
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' قراءة الرد الثاني من خادم الويب بعد إرسال الطلب
        ' يتم حفظ قيمة السطر Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' إظهار النص على الشاشة
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length؟
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While
        ' إظهار السطر الأخير
        Console.Out.WriteLine("<--")

        ' بناء التعبير النمطي الذي يسمح باسترجاع النتيجة
        ' في تدفق استجابة خادم الويب
        Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' يتم قراءة بقية استجابة خادم الويب
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' يتم تقسيم الرد إلى أسطر نصية
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' يتم البحث في أسطر النص عن النتيجة
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' المتابعة
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' مقارنة السطر الحالي بالنموذج
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' هل تم العثور على النتيجة؟
            If MatchRésultat.Success Then
                ' تدوين النتيجة
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' عرض النتيجة
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

أولاً، يرسل العميل HTTP-POST طلبه بتنسيق POST:


        ' بناء سلسلة الاستعلام
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' إنشاء جدول الرؤوس HTTP المراد إرساله
        Dim entetes(5) As String
        entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: application/x-www-form-urlencoded"
        entetes(3) = "Content-Length: " & nbChars
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = ""

        ' إرسال الرؤوس HTTP إلى الخادم
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' الإرسال إلى الخادم
            OUT.WriteLine(entetes(i))
            ' عرض على الشاشة
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

في الرأس

--> Content-Length: 7

يجب تحديد حجم المعلمات التي سيقوم العميل بإرسالها خلف الرؤوس HTTP:

--> a=6&b=7

ولذلك، نستخدم الكود التالي:


        ' إنشاء سلسلة الاستعلام
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

تقوم الطريقة HttpUtility.UrlEncode(سلسلة أحرف) بعض أحرف chaîne إلى %n1n2، حيث n1n2 هو رمز ASCII للحرف الذي تم تحويله. الأحرف التي يستهدفها هذا التحويل هي جميع الأحرف التي لها معنى خاص في استعلام POST (المسافة، وعلامة =، وعلامة &، ...). هنا، عادةً ما تكون الطريقة HttpUtility.UrlEncode غير ضرورية لأن a و b هما عددان لا يحتويان على أي من هذه الأحرف الخاصة. وهي مستخدمة هنا على سبيل المثال. وهي تحتاج إلى مساحة الأسماء System.Web. بمجرد أن يرسل العميل رؤوسه HTTP:

--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->

يستجيب الخادم برأس HTTP 100 Continue:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--

يكتفي الكود بقراءة هذا الرد الأول وعرضه على الشاشة:


        ' قراءة الرد الأول من خادم الويب HTTP/1.1 100
        Dim ligne As String = Nothing
        ' سطر من تدفق القراءة
        ligne = [IN].ReadLine()
        While ligne <> ""
            'إرجاع
            Console.Out.WriteLine(("<-- " + ligne))
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While
        'إرجاع السطر الأخير
        Console.Out.WriteLine(("<-- " + ligne))

بمجرد قراءة هذا الرد الأول، يجب على العميل إرسال معلماته:

--> a=6&b=7

ويقوم بذلك باستخدام الرمز التالي:

        ' إرسال معلمات الطلب
        OUT.Write(requête)
        ' إظهار
        Console.Out.WriteLine(("--> " + requête))

ثم يقوم الخادم بإرسال ردّه. ويتكون هذا الرد من جزأين:

  1. رؤوس HTTP تنتهي بسطر فارغ
  2. الرد بتنسيق XML
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>

في البداية، يقرأ العميل الرؤوس HTTP للعثور على السطر Content-Length واسترداد حجم الرد XML (وهو 90 في هذه الحالة). ويتم استرداد هذا الحجم باستخدام تعبير منتظم. كان من الممكن القيام بذلك بطريقة أخرى، وربما بطريقة أكثر فعالية.


        ' بناء التعبير النمطي الذي يسمح بتحديد حجم الرد XML
        ' في تدفق استجابة خادم الويب
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' قراءة الرد الثاني من خادم الويب بعد إرسال الطلب
        ' يتم حفظ قيمة السطر Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' إظهار النص على الشاشة
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length؟
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While
        ' عرض السطر الأخير
        Console.Out.WriteLine("<--")

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

<-- <double xmlns="st.istia.univ-angers.fr">13</double>

مرة أخرى باستخدام تعبير منتظم. وبمجرد العثور على النتيجة، يتم عرضها.

[résultat=13]

ونهاية كود العميل هي كما يلي:


        ' بناء التعبير النمطي الذي يسمح باسترجاع النتيجة
        ' في تدفق استجابة خادم الويب
        Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' يتم قراءة بقية استجابة خادم الويب
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' يتم تقسيم الرد إلى أسطر نصية
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' يتم البحث في أسطر النص عن النتيجة
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' المتابعة
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' مقارنة السطر الحالي بالنموذج
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' هل تم العثور على النتيجة؟
            If MatchRésultat.Success Then
                ' تدوين النتيجة
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' عرض النتيجة
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. عميل SOAP

ندرس هنا عميلاً ثانياً سيستخدم حواراً بين العميل والخادم من النوع SOAP (بروتوكول الوصول البسيط إلى الكائنات). يُعرض علينا مثال على الحوار الخاص بالوظيفة ajouter:

Image

Image

طلب العميل هو طلب POST. لذلك سنجد بعض آليات العميل السابق. والفرق الرئيسي هو أنه في حين كان العميل HTTP-POST يرسل المعلمات a و b بالشكل

    a=A&b=B

، فإن العميل SOAP يرسلها بتنسيق XML أكثر تعقيدًا:

POST /operations/operations.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "st.istia.univ-angers.fr/ajouter"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ajouter xmlns="st.istia.univ-angers.fr">
      <a>double</a>
      <b>double</b>
    </ajouter>
  </soap:Body>
</soap:Envelope>

ويحصل في المقابل على رد XML وهو أيضًا أكثر تعقيدًا من الردود التي رأيناها سابقًا:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ajouterResponse xmlns="st.istia.univ-angers.fr">
      <ajouterResult>double</ajouterResult>
    </ajouterResponse>
  </soap:Body>
</soap:Envelope>

على الرغم من أن الطلب والاستجابة أكثر تعقيدًا، إلا أن الآلية المستخدمة هي نفسها HTTP المستخدمة مع العميل HTTP-POST. وبالتالي، يمكن كتابة العميل SOAP على غرار العميل HTTP-POST. وفيما يلي مثال على التنفيذ:

dos>clientsoap1 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
[résultat=7]

الطريقة الوحيدة التي تتغير هي executeFonction. يقوم العميل SOAP بإرسال رؤوس HTTP الخاصة بطلبه. وهي ببساطة أكثر تعقيدًا قليلاً من تلك الخاصة بـ HTTP-POST:

ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->

الرمز الذي يولدها:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' يُنفذ الدالة (a,b) على خدمة الويب ذات عنوان URI URI
        ' تتم عمليات التبادل بين العميل والخادم عبر تدفقات IN و OUT
        ' توجد نتيجة الدالة في السطر
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' التي أرسلها الخادم
        ' بناء سلسلة الاستعلام SOAP

        Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
        requêteSOAP += "<soap:Body>" + ControlChars.Lf
        requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
        requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
        requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
        requêteSOAP += "</soap:Body>" + ControlChars.Lf
        requêteSOAP += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = requêteSOAP.Length

        ' إنشاء جدول الرؤوس HTTP المراد إرساله
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " & nbCharsSOAP
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""

        ' إرسال الرؤوس HTTP إلى الخادم
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' الإرسال إلى الخادم
            OUT.WriteLine(entetes(i))
            ' عرض على الشاشة
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

عند استلام هذا الطلب، يرسل الخادم أول رد له والذي يعرضه العميل:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--

رمز قراءة هذا الرد الأول هو التالي:


        ' قراءة الرد الأول من خادم الويب HTTP/1.1 100
        Dim ligne As String = Nothing
        ' سطر من تدفق القراءة
        ligne = [IN].ReadLine()
        While ligne <> ""
            'عرض
            Console.Out.WriteLine(("<-- " + ligne))
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While        'while
        'صدى السطر الأخير
        Console.Out.WriteLine(("<-- " + ligne))

سيقوم العميل الآن بإرسال معلماته بتنسيق XML في ما يُسمى بـ«المغلف» SOAP:

--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>

الرمز:

1
2
3
4
        ' إرسال معلمات الطلب
        OUT.Write(requêteSOAP)
        ' صدى
        Console.Out.WriteLine(("--> " + requêteSOAP))

ثم سيرسل الخادم ردّه النهائي:

<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>

يعرض العميل على الشاشة الرؤوس HTTP التي تم استلامها، بينما يبحث فيها عن السطر Content-Length:


        ' بناء التعبير النمطي الذي يسمح بتحديد حجم الاستجابة XML
        ' في تدفق استجابة خادم الويب
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' قراءة الرد الثاني من خادم الويب بعد إرسال الطلب
        ' يتم حفظ قيمة السطر Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' إظهار النص على الشاشة
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length؟
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While        'while
        ' إظهار السطر الأخير
        Console.Out.WriteLine("<--")

بمجرد معرفة الحجم N للاستجابة XML، يقرأ العميل N حرفًا من تدفق استجابة الخادم، ويقسم السلسلة المستردة إلى أسطر نصية لعرضها على الشاشة والبحث فيها عن العلامة XML للنتيجة: <ajouterResult>7</ajouterResult> وعرض هذا الأخير:


        ' بناء التعبير النمطي الذي يسمح باسترجاع النتيجة
        ' في تدفق استجابة خادم الويب
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' يتم قراءة بقية استجابة خادم الويب
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' يتم تقسيم الرد إلى أسطر نصية
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' يتم البحث في أسطر النص عن النتيجة
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' المتابعة
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' مقارنة السطر الحالي بالنموذج
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' هل تم العثور على النتيجة؟
            If MatchRésultat.Success Then
                ' تدوين النتيجة
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'السطر التالي
        Next i
        ' عرض النتيجة
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. تغليف التبادلات بين العميل والخادم

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

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

10.7.1. فئة التغليف

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


' مساحات الأسماء
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic

' clientSOAP لخدمة عمليات الويب
Public Class clientSOAP

    ' متغيرات المثيل
    Private uri As uri = Nothing    ' l'URI du service web
    Private client As TcpClient = Nothing    ' la liaison tcp du client avec le serveur
    Private [IN] As StreamReader = Nothing    ' le flux de lecture du client
    Private OUT As StreamWriter = Nothing    ' le flux d'écriture du client
    ' قاموس الوظائف
    Private dicoFonctions As New Hashtable
    ' قائمة الوظائف
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' التفصيل
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

    ' منشئ
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)

        ' تشغيل وضع التفصيل
        Me.verbose = verbose

        ' الاتصال بالخادم
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' إنشاء تدفقات الإدخال والإخراج للعميل TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' إنشاء قاموس وظائف خدمة الويب
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' إغلاق الاتصال بالخادم
    Public Sub Close()
        ' نهاية الاتصال بين العميل والخادم
        [IN].Close()
        OUT.Close()
        client.Close()
    End Sub

    ' executeFonction
    Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
        ' تنفيذ الدالة (a,b) على خدمة الويب ذات عنوان URI URI
        ' تتم عمليات التبادل بين العميل والخادم عبر تدفقات IN و OUT
        ' نتيجة الدالة موجودة في السطر
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' التي أرسلها الخادم

        ' هل الدالة صالحة؟
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If

        ' هل الحجتان a و b صالحتان؟
        Dim doubleA As Double = 0
        Try
            doubleA = Double.Parse(a)
        Catch
            Return "[argument [" + a + "] incorrect (double)]"
        End Try
        Dim doubleB As Double = 0
        Try
            doubleB = Double.Parse(b)
        Catch
            Return "[argument [" + b + "] incorrect (double)]"
        End Try

        ' القسمة على الصفر؟
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If

        ' إنشاء سلسلة الاستعلام SOAP
        Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
        requêteSOAP += "<soap:Body>" + ControlChars.Lf
        requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
        requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
        requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
        requêteSOAP += "</soap:Body>" + ControlChars.Lf
        requêteSOAP += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = requêteSOAP.Length

        ' إنشاء جدول الرؤوس HTTP المراد إرساله
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " + nbCharsSOAP.ToString
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""

        ' يتم إرسال الرؤوس HTTP إلى الخادم
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' الإرسال إلى الخادم
            OUT.WriteLine(entetes(i))
            ' عرض على الشاشة
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i

        ' قراءة الرد الأول من خادم الويب HTTP/1.1 100
        Dim ligne As String = Nothing
        ' سطر من تدفق القراءة
        ligne = [IN].ReadLine()
        While ligne <> ""
            'إخراج
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While
        'صدى السطر الأخير
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' إرسال معلمات الطلب
        OUT.Write(requêteSOAP)
        ' صدى
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If

        ' بناء التعبير النمطي الذي يسمح بتحديد حجم الرد XML
        ' في تدفق استجابة خادم الويب
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' قراءة الرد الثاني من خادم الويب بعد إرسال الطلب
        ' يتم حفظ قيمة السطر Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' إظهار النص على الشاشة
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' Content-Length؟
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' السطر التالي
            ligne = [IN].ReadLine()
        End While

        ' عرض السطر الأخير
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' بناء التعبير النمطي الذي يسمح باستخراج النتيجة
        ' في تدفق استجابة خادم الويب
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' يتم قراءة بقية استجابة خادم الويب
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' تقسيم الرد إلى أسطر نصية
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' يتم البحث في أسطر النص عن النتيجة
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' متابعة
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' comparaison ligne courante au modèle
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' هل تم العثور عليها؟
            If MatchRésultat.Success Then
                ' تدوين النتيجة
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i

        ' نُرجع النتيجة
        Return strRésultat
    End Function
End Class

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


    ' المُنشئ
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)

    ' executeFonction
    Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String


    ' إغلاق الاتصال بالخادم
    Public Sub Close()

ولها السمات التالية:


    ' متغيرات المثيل
    Private uri As Uri = Nothing    ' l'URI du service web
    Private client As TcpClient = Nothing    ' la liaison tcp du client avec le serveur
    Private [IN] As StreamReader = Nothing    ' le flux de lecture du client
    Private OUT As StreamWriter = Nothing    ' le flux d'écriture du client
    ' قاموس الوظائف
    Private dicoFonctions As New Hashtable
    ' قائمة الوظائف
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' التفصيل
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

يتم تمرير معلمتين إلى المُنشئ:

  1. معلمة URI الخاصة بخدمة الويب التي يجب أن يتصل بها
  2. قيمة منطقية verbose التي، في الواقع، تطلب عرض عمليات تبادل الشبكة على الشاشة، وإلا فلن يتم عرضها.

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

تسمح الطريقة Close بإغلاق الاتصال بالخادم.

أما الطريقة ExecuteFonction فهي الطريقة التي تمت كتابتها للعميل SOAP الذي تمت دراسته، مع بعض الاختلافات الطفيفة:

  1. لم يعد من الضروري تمرير المعلمات uri و IN و OUT كمعلمات إلى الطريقة، حيث أصبحت الآن سمات للمثيل يمكن الوصول إليها من جميع طرق المثيل
  2. الطريقة ExecuteFonction التي كانت تُرجع سابقًا نوعًا من نوع void وتعرض نتيجة الدالة على الشاشة، تُرجع الآن هذه النتيجة وبالتالي نوعًا من نوع string.

عادةً ما يستخدم العميل الفئة clientSOAP بالطريقة التالية:

  1. إنشاء كائن من النوع clientSOAP الذي سيقوم بإنشاء الاتصال بخدمة الويب
  2. الاستخدام المتكرر للطريقة executeFonction
  3. إغلاق الاتصال بخدمة الويب باستخدام الطريقة Close.

دعونا ندرس عميلاً أول.

10.7.2. عميل وحدة التحكم

نستأنف هنا العميل SOAP الذي تمت دراسته عندما لم تكن الفئة clientSOAP موجودة، ونقوم بإعادة تنظيمه بحيث يستخدم هذه الفئة الآن:


' مساحات الأسماء
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' طلب URI من خدمة الويب operations
    ' تنفيذ الأوامر المكتوبة على لوحة المفاتيح بشكل تفاعلي
    Public Sub Main(ByVal args() As String)
        ' قواعد النحو
        Const syntaxe As String = "pg URI [verbose]"

        ' عدد الوسيطات
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' تفصيلي؟
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' يتم الاتصال بخدمة الويب 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' خطأ في الاتصال
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

        ' يتم إدخال طلبات المستخدم عبر لوحة المفاتيح
        ' بالصيغة function a b - وتنتهي بالأمر end
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' مطالبة للمستخدم
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)

        ' إدارة الأخطاء
        Dim erreurCommande As Boolean
        Try
            ' حلقة إدخال الأوامر المكتوبة على لوحة المفاتيح
            While True
                ' في البداية لا توجد أخطاء
                erreurCommande = False
                ' قراءة الأمر
                commande = Console.In.ReadLine().Trim().ToLower()
                ' انتهى؟
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' تقسيم الأمر إلى حقول
                champs = Regex.Split(commande, "\s+")
                ' يلزم وجود ثلاثة حقول
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' تسجيل الخطأ
                    erreurCommande = True
                End If
                ' إرسال الطلب إلى خدمة الويب
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' الطلب التالي
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' نهاية الاتصال بين العميل والخادم
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' عرض الأخطاء
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' عرض الخطأ
        System.Console.Error.WriteLine(msg)
        ' إيقاف مع خطأ
        Environment.Exit(exitCode)
    End Sub
End Module

أصبح العميل الآن أبسط بكثير ولا يحتوي على أي اتصال شبكي. يقبل العميل معلمتين:

  1. معرف URI لخدمة الويب operations
  2. الكلمة الرئيسية الاختيارية verbose. إذا كانت موجودة، فسيتم عرض عمليات التبادل عبر الشبكة على الشاشة.

يُستخدم هذان المعلمتان لإنشاء كائن clientSOAP الذي سيتولى التبادل مع خدمة الويب.


        ' يتم الاتصال بخدمة الويب 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' خطأ في الاتصال
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

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


                ' إرسال الطلب إلى خدمة الويب
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

يتم ترجمة الفئة clientSOAP في «تجميع»:

dos>vbc /r:clientSOAP.dll testClientSOAP.vb
dos>dir
04/03/2004  08:46                6 913 clientSOAP.vb
04/03/2004  09:07                7 168 clientSOAP.dll

ثم يتم ترجمة تطبيق العميل testClientSoap بواسطة:

dos>vbc /r:clientSOAP.dll /r:system.dll testClientSOAP.vb
dos>dir
04/03/2004  09:08                2 711 testClientSOAP.vb
04/03/2004  09:08                4 608 testClientSOAP.exe

فيما يلي مثال على التنفيذ غير التفصيلي:

dos>testclientsoap http://localhost/st/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 1 3
résultat=4
soustraire 6 7
résultat=-1
multiplier 4 5
résultat=20
diviser 1 2
résultat=0.5
x
syntaxe : [ajouter|soustraire|multiplier|diviser] a b
x 1 2
résultat=[fonction [x] indisponible : (ajouter, soustraire,multiplier,diviser)]
ajouter a b
résultat=[argument [a] incorrect (double)]
ajouter 1 b
résultat=[argument [b] incorrect (double)]
diviser 1 0
résultat=[division par zéro]
fin

يمكن متابعة عمليات التبادل عبر الشبكة من خلال طلب تنفيذ "مفصل":

dos>testClientSOAP http://localhost/operations/operations.asmx تفصيلي
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 4 8
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>4</a>
<b>8</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 346
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>12</ajouterResult></ajouterResponse></soap:Body></soap:Envelope>
résultat=12
fin

لنقم الآن بإنشاء عميل رسومي.

10.7.3. عميل رسومي يعمل على نظام ويندوز

سنقوم الآن باستعلام خدمة الويب الخاصة بنا باستخدام عميل رسومي سيستخدم هو الآخر الفئة clientSOAP. وستكون الواجهة الرسومية كما يلي:

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

رقم
النوع
الاسم
الدور
1
TextBox
txtURI
URI الخاص بخدمة عمليات الويب
2
زر
btnOuvrir
يفتح الاتصال بخدمة الويب
3
زر
btnFermer
يغلق الاتصال بخدمة الويب
4
ComboBox
cmbFonctions
قائمة العمليات الحسابية (الجمع، الطرح، الضرب، القسمة)
5
TextBox
txtA
معاملات العمليات الحسابية
6
TextBox
txtB
الحجة b للدوال
7
TextBox
txtRésultat
نتيجة الدالة (a, b)
8
زر
btnCalculer
يبدأ حساب الدالة (a,b)
9
TextBox
txtErreur
يعرض رسالة حالة الاتصال

هناك بعض القيود التشغيلية:

  • لا يكون الزر btnOuvrir نشطًا إلا إذا كان الحقل txtURI غير فارغ ولم يكن هناك اتصال مفتوح بالفعل
  • لا يكون الزر btnFermer نشطًا إلا عند فتح اتصال بخدمة الويب
  • لا يكون الزر btnCalculer نشطًا إلا عندما يكون الاتصال مفتوحًا ويكون الحقلان txtA و txtB غير فارغين
  • الحقلان txtRésultat و txtErreur لهما السمة ReadOnly بقيمة «صحيح»

يبدأ العميل بفتح الاتصال بخدمة الويب باستخدام الزر [Ouvrir]:

Image

بعد ذلك، يمكن للمستخدم اختيار دالة وقيمتين لـ a و b:

Image

Image

Image

Image

Image

فيما يلي كود التطبيق. وقد حذفنا كود النموذج لأنه لا أهمية له هنا.


'مساحات الأسماء
Imports System
Imports System.Windows.Forms

' فئة النموذج
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' سمات المثيل
    Dim client As clientSOAP    ' client SOAP du service web operations

#المنطقة "كود تم إنشاؤه بواسطة مصمم نماذج Windows"

    Public Sub New()
        MyBase.New()
        'هذا الاستدعاء مطلوب من قبل مصمم نماذج Windows.
        InitializeComponent()
        ' عمليات تهيئة أخرى
        myInit()
    End Sub

    'تُستخدم الطريقة البديلة Dispose للنموذج لتنظيف قائمة المكونات.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#نهاية المنطقة


    Private Sub myInit()
        ' بدء تشغيل النموذج
        cmbFonctions.SelectedIndex = 0
        btnOuvrir.Enabled = False
        btnFermer.Enabled = True
        btnCalculer.Enabled = False
    End Sub

    Private Sub txtURI_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtURI.TextChanged
        ' تغير محتوى حقل الإدخال - يتم تعيين حالة زر "فتح"
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' طلب إنشاء اتصال بخدمة الويب
        Try
            ' إنشاء كائن من النوع [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' حالات الأزرار
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' لم يعد بالإمكان تعديل URI
            txtURI.ReadOnly = True
            ' حالة العميل
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' حدث خطأ - يتم عرضه
            txtErreur.Text = ex.Message
        End Try
    End Sub

    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' إغلاق الاتصال بخدمة الويب
        client.Close()
        ' حالات الأزرار
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' حالة العميل
        txtErreur.Text = "Liaison au service web fermée"
    End Sub

    Private Sub btnCalculer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
        ' حساب دالة f(a,b)
        ' مسح النتيجة السابقة
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' حدث خطأ في الشبكة
            txtErreur.Text = ex.Message
            ' إغلاق الاتصال
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' تغيير قيمة A
        btnCalculer.Enabled = txtA.Text.Trim <> "" And txtB.Text.Trim <> ""
    End Sub

    Private Sub txtB_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtB.TextChanged
        ' تغيير قيمة B
        txtA_TextChanged(Nothing, Nothing)
    End Sub

    ' الطريقة الرئيسية
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

وهنا أيضًا، تخفي الفئة clientSOAP جميع الجوانب المتعلقة بالشبكة في التطبيق. وقد تم بناء التطبيق على النحو التالي:

  • تم وضع التجميع clientSOAP.dll الذي يحتوي على الفئة clientSOAP في مجلد المشروع
  • تم إنشاء واجهة المستخدم الرسومية clientsoapgui.vb باستخدام VS.NET ثم تم ترجمتها في نافذة DOS:
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb

dos>dir
04/03/2004  09:13                7 168 clientSOAP.dll
04/03/2004  16:44                9 866 clientsoapgui.vb
04/03/2004  16:44               11 264 clientsoapgui.exe

ثم تم تشغيل واجهة المستخدم الرسومية بواسطة:

dos>clientsoapgui

10.8. عميل بروكسي

دعونا نستعرض ما تم إنجازه للتو. لقد أنشأنا فئة وسيطة تغلف التبادلات الشبكية بين عميل وخدمة ويب وفقًا للمخطط التالي:

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

كيف يمكن إنشاء فئة الوكيل لخدمة ويب؟ ترافق خدمة الويب دائمًا ملف وصف بتنسيق XML. إذا كان ملف URI الخاص بخدمة الويب operations هو http://localhost/operations/operations.asmx، فإن ملف الوصف الخاص بها متاح على الرابط http://localhost/operations/operations.asmx?wsdl بصيغة URL كما يوضح لقطة الشاشة التالية:

Image

يوجد هنا ملف XML يصف بدقة جميع وظائف خدمة الويب مع تحديد نوع وعدد المعلمات ونوع النتيجة لكل منها. يُطلق على هذا الملف اسم ملف الخدمة WSDL لأنه يستخدم لغة WSDL (لغة وصف خدمات الويب). انطلاقًا من هذا الملف، يمكن إنشاء فئة وكيل باستخدام أداة wsdl:

dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.

dos>dir
04/03/2004  17:17                6 663 operations.vb

تقوم الأداة wsdl بإنشاء ملف مصدر VB.NET (الخيار /language=vb) يحمل اسم الفئة التي تنفذ خدمة الويب، وهي هنا operations. دعونا نلقي نظرة على جزء من الكود الذي تم إنشاؤه:

'------------------------------------------------------------------------------
' <تم إنشاؤه تلقائيًا>
'     تم إنشاء هذا الرمز بواسطة أداة.
'     إصدار وقت التشغيل: 1.1.4322.573
'
'     قد تؤدي التغييرات التي تُجرى على هذا الملف إلى حدوث سلوك غير صحيح، وستُفقد هذه التغييرات إذا 
'     تم إعادة إنشاء الكود.
' </autogenerated>
'------------------------------------------------------------------------------

Option Strict Off
Option Explicit On

Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization

'
'تم إنشاء شفرة المصدر هذه تلقائيًّا بواسطة wsdl، الإصدار=1.1.4322.573.
'

'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(),  _
 System.ComponentModel.DesignerCategoryAttribute("code"),  _
 System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")>  _
Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

    '<remarks/>
    Public Sub New()
        MyBase.New
        Me.Url = "http://localhost/operations/operations.asmx"
    End Sub

    '<ملاحظات/>
    <System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)>  _

    Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
        Return CType(results(0),Double)
    End Function

    '<ملاحظات/>
    Public Function Beginajouter(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
        Return Me.BeginInvoke("ajouter", New Object() {a, b}, callback, asyncState)
    End Function

    '<ملاحظات/>
    Public Function Endajouter(ByVal asyncResult As System.IAsyncResult) As Double
        Dim results() As Object = Me.EndInvoke(asyncResult)
        Return CType(results(0),Double)
    End Function
....

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

Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

تحمل الفئة اسم operations الخاص بخدمة الويب التي تم إنشاؤها من أجلها. وهي مشتقة من الفئة SoapHttpClientProtocol:

Image

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

    Public Sub New()
        MyBase.New
        Me.Url = "http://localhost/operations/operations.asmx"
    End Sub

يقوم المنشئ بتعيين القيمة URL للسمة url الخاصة بخدمة الويب المرتبطة بالوكيل. ولا تُعرّف الفئة operations المذكورة أعلاه السمة url بنفسها. يتم توريث هذا السمة من الفئة التي ينحدر منها الوكيل: System.Web.Services.Protocols.SoapHttpClientProtocol. لننظر الآن إلى ما يتعلق بالطريقة ajouter:

    Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
        Return CType(results(0),Double)
    End Function

يمكن ملاحظة أن لها نفس التوقيع الموجود في خدمة الويب operations حيث تم تعريفها على النحو التالي:

      <WebMethod>  _
      Function ajouter(a As Double, b As Double) As Double
         Return a + b
      End Function 'ajouter

لا تظهر هنا الطريقة التي تتفاعل بها هذه الفئة مع خدمة الويب. يتم دعم هذا التفاعل بالكامل من قبل الفئة الأم System.Web.Services.Protocols.SoapHttpClientProtocol. ولا يوجد في الوكيل سوى ما يميزه عن الوكلاء الآخرين:

  • URL لخدمة الويب المرتبطة
  • تعريف أساليب الخدمة المرتبطة.

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

dos>vbc /t:library /r:system.web.services.dll /r:system.xml.dll /r:system.dll operations.vb
dos>dir
04/03/2004  17:17                6 663 operations.vb
04/03/2004  17:24                7 680 operations.dll

الآن لنكتب عميلًا يعمل على وحدة التحكم. يتم استدعاؤه بدون معلمات ويقوم بتنفيذ الأوامر التي يتم إدخالها عبر لوحة المفاتيح:

dos>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b

ajouter 4 5
résultat=9
soustraire 9 8
résultat=1
multiplier 10 4
résultat=40
diviser 6 7
résultat=0,857142857142857
toutfaire 10 20
résultats=[30,-10,200,0,5]
diviser 5 0
résultat=+Infini
fin

رمز العميل هو التالي:


' مساحات الأسماء
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' يُنفذ الأوامر المكتوبة على لوحة المفاتيح بشكل تفاعلي
    ' ويرسلها إلى خدمة الويب operations
    Public Sub Main()
        ' لم تعد هناك معلمات - حيث إن URL لخدمة الويب مبرمجة بشكل ثابت في الوكيل

        ' إنشاء قاموس بوظائف خدمة الويب
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser", "toutfaire"}
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' يتم إنشاء كائن عمليات الوكيل 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' خطأ في الاتصال
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

        ' يتم إدخال طلبات المستخدم عبر لوحة المفاتيح
        ' بالصيغة a b - وتنتهي بالأمر end
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' مطالبة للمستخدم
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)

        ' بعض البيانات المحلية
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' حلقة إدخال الأوامر المكتوبة على لوحة المفاتيح
        While True
            ' في البداية لا توجد أخطاء
            erreurCommande = False
            ' قراءة الأمر
            commande = Console.In.ReadLine().Trim().ToLower()
            ' انتهى؟
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' تقسيم الأمر إلى حقول
            champs = Regex.Split(commande, "\s+")
            Try
                ' يلزم وجود ثلاثة حقول
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' يجب أن يكون الحقل 0 دالة معترف بها
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' يجب أن يكون الحقل 1 رقمًا صالحًا
                a = Double.Parse(champs(1))
                ' يجب أن يكون الحقل 2 رقمًا صالحًا
                b = Double.Parse(champs(2))
            Catch
                ' أمر غير صالح
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' يتم إرسال الطلب إلى خدمة الويب
            If Not erreurCommande Then
                Try
                    Dim résultat As Double
                    Dim résultats() As Double
                    If fonction = "ajouter" Then
                        résultat = myOperations.ajouter(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "soustraire" Then
                        résultat = myOperations.soustraire(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "multiplier" Then
                        résultat = myOperations.multiplier(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "diviser" Then
                        résultat = myOperations.diviser(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "toutfaire" Then
                        résultats = myOperations.toutfaire(a, b)
                        Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
                        résultats(2).ToString + "," + résultats(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
                End Try
            End If
        End While
    End Sub

    ' عرض الأخطاء
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' عرض الخطأ
        System.Console.Error.WriteLine(msg)
        ' التوقف مع حدوث خطأ
        Environment.Exit(exitCode)
    End Sub
End Module

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


        ' يتم إنشاء كائن عمليات الوكيل 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' خطأ في الاتصال
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

يتم كتابة سطور الوظيفة a و b على لوحة المفاتيح. وبناءً على هذه المعلومات، يتم استدعاء الطرق المناسبة للبروكسي:


            ' إرسال الطلب إلى خدمة الويب
            If Not erreurCommande Then
                Try
                    Dim résultat As Double
                    Dim résultats() As Double
                    If fonction = "ajouter" Then
                        résultat = myOperations.ajouter(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "soustraire" Then
                        résultat = myOperations.soustraire(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "multiplier" Then
                        résultat = myOperations.multiplier(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "diviser" Then
                        résultat = myOperations.diviser(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "toutfaire" Then
                        résultats = myOperations.toutfaire(a, b)
                        Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
                        résultats(2).ToString + "," + résultats(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
                End Try

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

dos>vbc /r:operations.dll /r:system.dll /r:system.web.services.dll testClientProxy.vb

dos>dir
04/03/2004  17:17                6 663 operations.vb
04/03/2004  17:24                7 680 operations.dll
04/03/2004  17:41                4 099 testClientProxy.vb
04/03/2004  17:41                5 632 testClientProxy.exe

10.9. تكوين خدمة ويب

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

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
  <appSettings>
    <add key="nom" value="tintin"/>
    <add key="age" value="27"/>
  </appSettings>   
</configuration>

يتم وضع معلمات التهيئة في مغلف XML:

<configuration>
    <appSettings>
...
    </appSettings>
</configuration>

سيتم إعلان معلمة التهيئة التي تحمل الاسم P والقيمة V بالسطر التالي:


        <add key="P" value="V"/>

كيف تسترد خدمة الويب هذه المعلومات؟ عندما يقوم IIS بتحميل خدمة ويب، فإنه يتحقق مما إذا كان يوجد في نفس المجلد ملف web.config. إذا كان الأمر كذلك، فإنه يقرأه. يتم الحصول على القيمة V لمعلمة P من خلال الأمر:

        String P=ConfigurationSettings.AppSettings["V"];

حيث ConfigurationSettings هي فئة في مساحة الأسماء System.Configuration.

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


<%@ WebService language="VB" class=personne %>

Imports System.Web.Services
imports System.Configuration

<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
   Inherits WebService

   ' السمات
   Private nom As String
   Private age As Integer

   ' منشئ
   Public Sub New()
      ' تهيئة السمات
      nom = ConfigurationSettings.AppSettings("nom")
      age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
   End Sub

   <WebMethod>  _
   Function id() As String
      Return "[" + nom + "," + age.ToString + "]"
   End Function 

End Class 

تحتوي خدمة الويب personne على سمتين هما nom و age، اللتين يتم تهيئتهما في منشئها بدون معلمات استنادًا إلى القيم المقروءة من ملف التكوين web.config الخاص بخدمة personne. وهذا الملف هو كما يلي:


<configuration>
    <appSettings>
        <add key="nom" value="tintin"/>
        <add key="age" value="27"/>
    </appSettings>
</configuration>

كما أن خدمة الويب تحتوي على معرّف <WebMethod> بدون معلمات ويقتصر دوره على عرض السمتين nom و age. الخدمة مسجلة في ملف المصدر personne.asmx الذي يوجد مع ملف التكوين الخاص به في المجلد c:\inetpub\wwwroot\st\personne:

dos>dir
09/03/2004  08:25               632 personne.asmx
09/03/2004  08:08               186 web.config

لنربط المجلد الافتراضي IIS/config بالمجلد الفعلي السابق. لنقم بتشغيل IIS ثم نطلب عبر متصفح الويب عنوان URL http://localhost/config/personne.asmx الخاص بالخدمة personne:

Image

دعونا نتبع رابط معرّف الطريقة الفريد:

Image

لا تحتوي الطريقة id على أي معلمات. لنستخدم الزر Appeler:

Image

لقد استرجعنا بنجاح المعلومات الموجودة في ملف الخدمة web.config.

10.10. خدمة الويب IMPOTS

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

10.10.1. خدمة الويب

نبدأ بالفئة impôt التي تم إنشاؤها في الفصل الخاص بقواعد البيانات، والتي تُبنى استنادًا إلى المعلومات الموجودة في قاعدة البيانات ODBC:


' خيارات
Option Strict On
Option Explicit On 

' مساحات الأسماء
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class impôt
    ' البيانات اللازمة لحساب الضريبة
    ' مصدر خارجي
    Private limites(), coeffR(), coeffN() As Decimal

    ' المُنشئ
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' يتم التحقق من أن الجداول الثلاثة لها نفس الحجم
        Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' لا بأس
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' المنشئ 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' تُهيئ المصفوفات الثلاث القيدية، coeffR، coeffN، استنادًا إلى
        ' محتوى الجدول Timpots في قاعدة البيانات ODBC DSNimpots
        ' colLimites، colCoeffR، colCoeffN هي الأعمدة الثلاثة في هذا الجدول
        ' قد يثير استثناءً
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing        ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing        ' la commande SQL
        ' الاستعلام SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' جداول لاسترداد البيانات
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' يتم محاولة الوصول إلى قاعدة البيانات
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' يتم إنشاء كائن أمر
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' يتم تنفيذ الاستعلام
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' استغلال الجدول الذي تم استرداده
        While myReader.Read()
            ' يتم إدراج بيانات السطر الحالي في الجداول
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' تحرير الموارد
        myReader.Close()
        impotsConn.Close()

        ' يتم تحويل الجداول الديناميكية إلى جداول ثابتة
        Me.limites = New Decimal(tLimites.Count) {}
        Me.coeffR = New Decimal(tLimites.Count) {}
        Me.coeffN = New Decimal(tLimites.Count) {}
        Dim i As Integer
        For i = 0 To tLimites.Count - 1
            limites(i) = Decimal.Parse(tLimites(i).ToString())
            coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
            coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
        Next i
    End Sub

    ' حساب الضريبة
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' حساب عدد الحصص
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' حساب الدخل الخاضع للضريبة و«الناتج العائلي»
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' حساب الضريبة
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' إرجاع النتيجة
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

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


    ' منشئ
    Public Sub New()
        ' تهيئة الجداول الثلاثة للحدود، coeffR، coeffN استنادًا
        ' محتوى جدول Timpots في قاعدة البيانات ODBC DSNimpots
        ' colLimites، colCoeffR، colCoeffN هي الأعمدة الثلاثة في هذا الجدول
        ' قد يثير استثناءً

        ' يتم استرداد معلمات تكوين الخدمة
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ' يتم استغلال قاعدة البيانات
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base

يتم الآن قراءة المعلمات الخمس لمنشئ الفئة السابقة من الملف web.config الخاص بالخدمة. فيما يلي كود ملف المصدر impots.asmx. وهو يتضمن الجزء الأكبر من الكود السابق. وقد اكتفينا بوضع الأجزاء الخاصة بخدمة الويب بين علامتي اقتباس:


<%@ WebService language="VB" class=impots %>

' إنشاء خدمة ويب للضرائب
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services

<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
    Inherits WebService

    ' البيانات اللازمة لحساب الضريبة
    ' تأتي من مصدر خارجي
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' المُنشئ
    Public Sub New()
        ' يقوم بتهيئة الجداول الثلاثة للحدود، coeffR، coeffN انطلاقًا
        ' محتوى الجدول Timpots في قاعدة البيانات ODBC DSNimpots
        ' colLimites، colCoeffR، colCoeffN هي الأعمدة الثلاثة في هذا الجدول
        ' قد يثير استثناءً

        ' يتم استرداد معلمات تكوين الخدمة
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ' يتم استغلال قاعدة البيانات
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing     ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing     ' la commande SQL
        Dim myReader As OdbcDataReader     ' lecteur de données Odbc

        ' الاستعلام SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots

        ' جداول لاسترداد البيانات
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' محاولة الوصول إلى قاعدة البيانات
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' يتم إنشاء كائن أمر
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' تنفيذ الاستعلام
            myReader = sqlCommand.ExecuteReader()
            ' استغلال الجدول الذي تم استرداده
            While myReader.Read()
                ' يتم وضع بيانات السطر الحالي في الجداول
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' تحرير الموارد
            myReader.Close()
            impotsConn.Close()

            ' يتم تحويل الجداول الديناميكية إلى جداول ثابتة
            Me.limites = New Decimal(tLimites.Count) {}
            Me.coeffR = New Decimal(tLimites.Count) {}
            Me.coeffN = New Decimal(tLimites.Count) {}
            Dim i As Integer
            For i = 0 To tLimites.Count - 1
                limites(i) = Decimal.Parse(tLimites(i).ToString())
                coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
            Next i
            ' تم بنجاح
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' خطأ
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' حساب الضريبة
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' حساب عدد الحصص
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' حساب الدخل الخاضع للضريبة و«الناتج العائلي»
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' حساب الضريبة
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' إرجاع النتيجة
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' المعرف
    <WebMethod()> _
    Function id() As String
        ' للتحقق من صحة كل شيء OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

دعونا نوضح بعض التعديلات التي أُجريت على الفئة impots بخلاف تلك اللازمة لتحويلها إلى خدمة ويب:

  • قد تفشل عملية قراءة قاعدة البيانات في المنشئ. ولذلك أضفنا سمتين إلى فئتنا وطريقة واحدة:
    • تكون القيمة المنطقية OK هي vrai إذا تم قراءة قاعدة البيانات بنجاح، وتكون faux في حالة الفشل
    • تحتوي السلسلة errMessage على رسالة خطأ إذا تعذر قراءة قاعدة البيانات.
    • تسمح الطريقة id بدون معلمات بالحصول على قيمة هذين السمتين.
  • ولإدارة أي خطأ محتمل في الوصول إلى قاعدة البيانات، تم وضع جزء من كود المُنشئ المتعلق بهذا الوصول داخل try-catch.

ملف تكوين الخدمة web.config هو كما يلي:


<configuration>
    <appSettings>
        <add key="DSN" value="mysql-impots" />
        <add key="TABLE" value="timpots" />
        <add key="COL_LIMITES" value="limites" />
        <add key="COL_COEFFR" value="coeffr" />
        <add key="COL_COEFFN" value="coeffn" />
    </appSettings>
</configuration>

أثناء المحاولة الأولى لتحميل الخدمة impots، أبلغ المُجمِّع أنه لم يعثر على مساحة الاسم Microsoft.Data.Odbc المستخدمة في التوجيه:

Imports Microsoft.Data.Odbc

بعد الرجوع إلى الوثائق

  • تمت إضافة توجيه تجميع في web.config للإشارة إلى ضرورة استخدام التجميع Microsoft.Data.odbc
  • تم وضع نسخة من الملف microsoft.data.odbc.dll في المجلد bin التابع للمشروع. ويقوم مُجمِّع خدمة الويب بتفحص هذا المجلد تلقائيًّا عند البحث عن «تجميع».

يبدو أن هناك حلولًا أخرى ممكنة، لكن لم يتم تناولها بالتفصيل هنا. وبالتالي، أصبح ملف التكوين كما يلي:


<configuration>
    <appSettings>
        <add key="DSN" value="mysql-impots" />
        <add key="TABLE" value="timpots" />
        <add key="COL_LIMITES" value="limites" />
        <add key="COL_COEFFR" value="coeffr" />
        <add key="COL_COEFFN" value="coeffn" />
    </appSettings>
    <system.web>
        <compilation>
            <assemblies>
                <add assembly="Microsoft.Data.Odbc" />
            </assemblies>
        </compilation>
    </system.web>
</configuration>

محتوى الملف impots\bin:

dos>dir impots\bin
30/01/2002  02:02           327 680 Microsoft.Data.Odbc.dll

تم وضع الخدمة وملف التكوين الخاص بها في impots:

dos>dir impots
09/03/2004  10:13             4 669 impots.asmx
09/03/2004  10:19               431 web.config

تم ربط المجلد الفعلي لخدمة الويب بالمجلد الافتراضي /impots في IIS. وبذلك تصبح صفحة الخدمة كما يلي:

Image

إذا اتبعنا الرابط id:

Image

إذا استخدمنا الزر Appeler:

Image

تعرض النتيجة السابقة قيم السمتين OK (true) و errMessage (""). في هذا المثال، تم تحميل قاعدة البيانات بشكل صحيح. لم يكن الأمر كذلك دائمًا، ولهذا السبب أضفنا الطريقة id للوصول إلى رسالة الخطأ. كان الخطأ يكمن في أن اسم قاعدة البيانات DSN قد تم تعريفه على أنه مستخدم DSN، في حين كان يجب تعريفه على أنه نظام DSN. يتم هذا التمييز في مدير المصادر ODBC 32 بت:

لنعد إلى صفحة الخدمة:

Image

لنتبع الرابط calculer:

Image

نحدد معلمات الاستدعاء وننفذه:

Image

النتيجة صحيحة.

10.10.2. إنشاء وكيل خدمة impots

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

dos>wsdl /language=vb http://localhost/impots/impots.asmx
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Écriture du fichier 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.

D:\data\serge\devel\vbnet\poly\chap9\impots>dir
09/03/2004  10:20    <REP>          bin
09/03/2004  10:58             4 651 impots.asmx
09/03/2004  11:05             3 364 impots.vb
09/03/2004  10:19               431 web.config

dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
pour Microsoft (R) .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. Tous droits réservés.

dos>dir
09/03/2004  10:20    <REP>          bin
09/03/2004  10:58             4 651 impots.asmx
09/03/2004  11:09             5 120 impots.dll
09/03/2004  11:05             3 364 impots.vb
09/03/2004  10:19               431 web.config

10.10.3. استخدام الوكيل مع عميل

في الفصل الخاص بقواعد البيانات، قمنا بإنشاء تطبيق وحدة التحكم يسمح بحساب الضريبة:

dos>dir
27/02/2004  16:56             5 120 impots.dll
27/02/2004  17:12             3 586 impots.vb
27/02/2004  17:08             6 144 testimpots.exe
27/02/2004  17:18             3 328 testimpots.vb

dos>testimpots
pg DSNimpots tabImpots colLimites colCoeffR colCoeffN

dos>testimpots odbc-mysql-dbimpots impots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

كان البرنامج testimpots يستخدم آنذاك الفئة impôt التقليدية الموجودة في الملف impots.dll. وكان كود البرنامج testimpots.vb كما يلي:


Option Explicit On 
Option Strict On

' مساحات الأسماء
Imports System
Imports Microsoft.VisualBasic

' صفحة اختبار
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' برنامج تفاعلي لحساب الضرائب
        ' يقوم المستخدم بإدخال ثلاث بيانات عبر لوحة المفاتيح: متزوج nbEnfants الراتب
        ' ثم يعرض البرنامج الضريبة المستحقة
        Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' التحقق من معلمات البرنامج
        If arguments.Length <> 5 Then
            ' رسالة خطأ
            Console.Error.WriteLine(syntaxe1)
            ' نهاية
            Environment.Exit(1)
        End If        'if
        ' يتم استرداد المعلمات
        Dim DSNimpots As String = arguments(0)
        Dim tabImpots As String = arguments(1)
        Dim colLimites As String = arguments(2)
        Dim colCoeffR As String = arguments(3)
        Dim colCoeffN As String = arguments(4)

        ' إنشاء كائن ضريبة
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try

        ' حلقة لا نهائية
        While True
            ' في البداية لا توجد أخطاء
            Dim erreur As Boolean = False

            ' يتم طلب معلمات حساب الضريبة
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()

            ' هل هناك ما يجب فعله؟
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' التحقق من عدد المعلمات في السطر الذي تم إدخاله
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Integer
            If Not erreur Then
                ' التحقق من صحة المعلمات
                ' متزوج
                marié = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                nbEnfants = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
                ' الراتب
                salaire = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
            End If
            If Not erreur Then
                ' المعلمات صحيحة - يتم حساب الضريبة
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

نستخدم البرنامج نفسه لجعله يستخدم الآن خدمة الويب impots من خلال فئة الوكيل impots التي تم إنشاؤها مسبقًا. ونضطر إلى تعديل الكود قليلاً:

  • فبينما كانت الفئة الأصلية impôt تحتوي على منشئ بخمسة معلمات، فإن فئة الوكيل impots تحتوي على منشئ بدون معلمات. وكما رأينا، أصبحت المعلمات الخمس محددة الآن في ملف تكوين خدمة الويب.
  • لذلك لم يعد هناك داعٍ لتمرير هذه المعلمات الخمس كمعلمات إلى البرنامج التجريبي

والكود الجديد هو كما يلي:


Imports System
Imports Microsoft.VisualBasic

' صفحة اختبار
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' برنامج تفاعلي لحساب الضريبة
        ' يُدخل المستخدم ثلاث بيانات عبر لوحة المفاتيح: متزوج nbEnfants الراتب
        ' ثم يعرض البرنامج الضريبة المستحقة
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' إنشاء كائن «ضريبة»
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try

        ' حلقة لا نهائية
        Dim erreur As Boolean
        While True
            ' في البداية لا توجد أخطاء
            erreur = False
            ' يُطلب إدخال معلمات حساب الضريبة
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()
            ' هل هناك ما يجب فعله؟
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' التحقق من عدد المعلمات في السطر الذي تم إدخاله
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            If Not erreur Then
                ' التحقق من صحة المعلمات
                ' متزوج
                Dim marié As String = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                Dim nbEnfants As Integer = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument nbEnfants incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' الراتب
                Dim salaire As Integer = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument salaire incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' إذا كانت المعلمات صحيحة - يتم حساب الضريبة
                If Not erreur Then Console.Out.WriteLine(("impôt=" + objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

لدينا الوكيل impots.dll والمصدر testimpots في نفس المجلد.

dos>dir
09/03/2004  11:28    <REP>          bin
09/03/2004  11:09             5 120 impots.dll
09/03/2004  11:34             3 396 testimpots.vb
09/03/2004  10:19               431 web.config

نقوم بتجميع الكود المصدري testimpots.vb:

dos>vbc /r:impots.dll /r:microsoft.visualbasic.dll /r:system.web.services.dll /r:system.dll testimpots.vb
dos>dir
09/03/2004  11:28    <REP>          bin
09/03/2004  11:09             5 120 impots.dll
09/03/2004  11:05             3 364 impots.vb
09/03/2004  11:35             5 632 testimpots.exe
09/03/2004  11:34             3 396 testimpots.vb
09/03/2004  10:19               431 web.config

ثم نقوم بتنفيذها:

dos>testimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

ونحصل بالفعل على النتيجة المتوقعة.