Skip to content

9. JAVA RMI

9.1. مقدمة

لقد رأينا كيفية إنشاء تطبيقات شبكة باستخدام أدوات الاتصال المسماة sockets. في تطبيق عميل/خادم مبني على هذه الأدوات، يكون الرابط بين العميل والخادم هو بروتوكول الاتصال الذي اعتمداه للتواصل. يمكن كتابة التطبيقين بلغات مختلفة: Java على سبيل المثال للعميل، وPerl للخادم أو أي تركيبة أخرى. لدينا بالفعل تطبيقان منفصلان مرتبطان ببروتوكول اتصال معروف لكليهما. علاوة على ذلك، فإن الوصول إلى الشبكة عبر المقابس (sockets) ليس شفافًا لتطبيق Java: يجب أن يستخدم فئة Socket، وهي فئة تم إنشاؤها خصيصًا لإدارة أدوات الاتصال هذه وهي sockets.

تسمح JAVA RMI (استدعاء الطريقة عن بُعد) بإنشاء تطبيقات شبكة ذات الخصائص التالية:

  1. تطبيقات العميل/الخادم هي تطبيقات Java في طرفي الاتصال
  2. يمكن للعميل استخدام الكائنات الموجودة على الخادم كما لو كانت محلية
  3. تصبح طبقة الشبكة شفافة: لا يتعين على التطبيقات الاهتمام بكيفية نقل المعلومات من نقطة إلى أخرى.

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

مبدأ الاتصال RMI هو كما يلي:

  1. يتم كتابة تطبيق Java كلاسيكي على جهاز A. وسيقوم هذا الجهاز بدور الخادم. وللقيام بذلك، سيتم "نشر" بعض كائناته على جهاز A الذي يتم تشغيل التطبيق عليه، لتصبح عندئذٍ خدمات.
  2. يتم كتابة تطبيق Java كلاسيكي على جهاز B. سيقوم هذا الجهاز بدور العميل. سيكون له حق الوصول إلى الكائنات/الخدمات المنشورة على الجهاز A، أي أنه سيتمكن، عبر مرجع بعيد، من التعامل معها كما لو كانت محلية. ولهذا الغرض، سيحتاج إلى معرفة بنية الكائن البعيد الذي يريد الوصول إليه (الأساليب والخصائص).

9.2. لنتعلم من خلال المثال

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

9.2.1. تطبيق الخادم

9.2.1.1. الخطوة 1: واجهة الكائن/الخادم

الكائن البعيد هو مثيل لفئة يجب أن تنفذ واجهة Remote المحددة في الحزمة java.rmi. طرق الكائن التي ستكون متاحة عن بُعد هي تلك المُعلنة في واجهة مشتقة من واجهة Remote:

import java.rmi.*;

 // الواجهة البعيدة pu
blic interface interEcho extends Remote{ 
    public String echo(String msg) throws java.rmi.RemoteException;
 }

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

9.2.1.2. الخطوة 2: كتابة كائن الخادم

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

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// فئة تنفذ الصدى البعيد
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // منشئ
    public srvEcho() throws RemoteException{
        super();
    }// نهاية المنشئ

    // طريقة تنفيذ الصدى
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// نهاية الصدى
}// نهاية الفئة

في الفئة السابقة، نجد:

  1. الطريقة التي تقوم بالترديد
  2. منشئ لا يفعل شيئًا سوى استدعاء منشئ الفئة الأم. وهو موجود لإعلان أنه يمكنه توليد استثناء من النوع RemoteException.

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

  1. يتصل بخدمة الدليل الخاصة بالجهاز الذي يوجد عليه الكائن الذي يرغب فيه. تعمل خدمة الدليل هذه على منفذ يجب أن يعرفه العميل (1099 افتراضيًا). يطلب العميل من الدليل مرجعًا لكائن/خدمة يحدد اسمها. إذا كان هذا الاسم هو اسم كائن/خدمة موجودة في الدليل، فإن الدليل يرسل إلى العميل مرجعًا يمكن للعميل من خلاله التفاعل مع الكائن/الخدمة البعيدة.
  2. منذ تلك اللحظة، يمكن للعميل استخدام هذا الكائن البعيد كما لو كان محليًا

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

Naming.rebind(String nom, Remote obj)

مع

الاسم: الاسم الذي سيتم ربطه بالكائن البعيد

obj: الكائن البعيد

وبالتالي، تصبح فئتنا srvEcho كما يلي:

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// الفئة التي تنفذ الصدى البعيد
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // منشئ
    public srvEcho() throws RemoteException{
        super();
    }// نهاية المنشئ

    // طريقة تنفيذ الصدى
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// نهاية الصدى

    // إنشاء الخدمة
    public static void main (String arg[]){
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// main
}// نهاية الفئة

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

9.2.1.3. الخطوة 3: ترجمة تطبيق الخادم

في هذه المرحلة، يمكننا ترجمة الخادم الخاص بنا. نقوم بترجمة الملف interEcho.java من الواجهة interEcho وكذلك الملف srvEcho.java من الفئة srvEcho. ونحصل على الملفات .class المقابلة: interEcho.class و srvEcho.class.

9.2.1.4. الخطوة 4: كتابة العميل

نكتب عميلاً نمرر إليه كمعلمة ملف URL من خادم الإيكو والذي

  1. يقرأ سطراً تمت كتابته على لوحة المفاتيح
  2. يرسلها إلى خادم الإيكو
  3. يعرض الرد الذي يرسله الخادم
  4. يعود إلى الخطوة 1 ويتوقف عندما تكون السطر المكتوب هو "fin".

ينتج عن ذلك العميل التالي:

import java.rmi.*;
 import java.io.*

; public class cltEch

    o { public static void main(String a
        rg[]){ // الصيغة: cltEcho URLService

        // التحقق من الحجج
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // حوار العميل-الخادم
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // فتح تدفق لوحة المفاتيح
            in=new BufferedReader(new InputStreamReader(System.in));
            // تحديد موقع الخدمة
            serveur=(interEcho) Naming.lookup(urlService);                
            // حلقة قراءة الرسائل المراد إرسالها إلى خادم الصدى
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // إرسال الرسالة إلى الخادم واستلام الرد
                reponse=serveur.echo(msg);
                // المتابعة
                System.out.println("Réponse serveur : " + reponse);
                // الرسالة التالية
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // انتهى
            System.exit(0);
        // إدارة الأخطاء        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// فئة                

لا يوجد شيء مميز في هذا العميل سوى التعليمات التي تطلب مرجعًا من الخادم:

            serveur=(interEcho) Naming.lookup(urlService);

نتذكر أن خدمة الصدى الخاصة بنا قد تم تسجيلها في دليل خدمات الجهاز الذي توجد عليه باستخدام التعليمات التالية:


            Naming.rebind("srvEcho",serveurEcho);

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

    rmi://الجهاز:المنفذ/nom_service

مع

rmi: اختياري - بروتوكول rmi

machine: اسم أو عنوان IP للجهاز الذي يعمل عليه خادم الإيكو - اختياري، القيمة الافتراضية هي localhost.

port: منفذ الاستماع لخدمة الدليل في هذا الجهاز - اختياري، القيمة الافتراضية هي 1099

nom_service: الاسم الذي تم تسجيل الخدمة المطلوبة تحته (srvEcho في مثالنا)

ما يتم استرداده هو مثيل للواجهة البعيدة interEcho. إذا افترضنا أن العميل والخادم ليسا على نفس الجهاز، فعند ترجمة العميل cltEcho.java، يجب أن يتوفر في نفس الدليل الملف interEcho.class، وهو نتيجة ترجمة الواجهة البعيدة interEcho، وإلا فسيحدث خطأ في الترجمة في الأسطر التي تشير إلى هذه الواجهة.

9.2.1.5. الخطوة 5: إنشاء ملفات .class اللازمة لتطبيق العميل-الخادم

من أجل فهم ما هو موجود على جانب الخادم وما هو موجود على جانب العميل بشكل جيد، سنضع الخادم في دليل echo\serveur والعميل في دليل echo\client.

يحتوي دليل الخادم على ملفات المصدر التالية:

E:\data\java\RMI\echo\serveur>dir *.java

INTERE~1 JAV           158  09/03/99  15:06 interEcho.java
SRVECH~1 JAV           759  09/03/99  15:07 srvEcho.java

بعد ترجمة هذين الملفين المصدرين، نحصل على الملفات التالية .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class

في دليل العميل، نجد الملف المصدر التالي:

E:\data\java\RMI\echo\client>dir *.java

CLTECH~1 JAV         1 427  09/03/99  16:08 cltEcho.java

بالإضافة إلى الملف interEcho.class الذي تم إنشاؤه أثناء تجميع الخادم:

E:\data\java\RMI\echo\client>dir *.class

INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

بعد تجميع الملف المصدر، نحصل على الملفات التالية .class:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

إذا حاولنا تشغيل العميل cltEcho، فسنحصل على الخطأ التالي:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub

إذا حاولنا تشغيل الخادم srvEcho، فسنحصل على الخطأ التالي:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

في كلتا الحالتين، تشير الآلة الافتراضية Java إلى أنها لم تعثر على الفئة srvEcho_stub. وبالفعل، لم نسمع عن هذه الفئة من قبل. في العميل، تم تحديد موقع الخادم باستخدام التعليمات التالية:

            serveur=(interEcho) Naming.lookup(urlService);                

هنا، urlservice هي السلسلة rmi://localhost/srvEcho مع

Rmi: بروتوكول rmi

Localhost: الجهاز الذي يعمل عليه الخادم - هنا نفس الجهاز الذي يعمل عليه العميل. عادةً ما تكون الصيغة machine:port. في حالة عدم وجود المنفذ، سيتم استخدام المنفذ 1099 بشكل افتراضي. على هذا المنفذ، توجد خدمة دليل الخادم.

srvEcho: هو اسم الخدمة المحددة المطلوبة

عند التحويل البرمجي، لم يتم الإبلاغ عن أي خطأ. كان من الضروري فقط أن يكون الملف interEcho.class للواجهة البعيدة متاحًا.

عند التنفيذ، تطلب الآلة الافتراضية وجود ملف srvEcho_stub.class إذا كانت الخدمة المطلوبة هي الخدمة srvEcho، وبشكل عام ملف X_stub.class لخدمة X. هذا الملف ضروري فقط عند التنفيذ وليس عند ترجمة العميل. وينطبق الأمر نفسه على الخادم. فما هو هذا الملف إذن؟

يوجد على الخادم الفئة srvEcho.class التي تمثل كائننا/خدمتنا البعيدة. العميل، إذا لم يكن بحاجة إلى هذه الفئة، فإنه يحتاج مع ذلك إلى نوع من الصورة لها حتى يتمكن من التواصل معها. في الواقع، لا يوجه العميل طلباته مباشرة إلى الكائن البعيد: بل يوجهها إلى صورته المحلية srvEcho_stub.class الموجودة على نفس الجهاز الذي يعمل عليه. يتواصل هذا النموذج المحلي srvEcho_stub.class مع نموذج من نفس النوع (srvEcho_stub.class) موجود هذه المرة على الخادم. يتم إنشاء هذه الصورة من الملف .class الموجود على الخادم باستخدام أداة Java تسمى rmic. في نظام Windows، يتم تنفيذ الأمر:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\rmic srvEcho

سينتج، من الملف srvEcho.class، ملفين آخرين هما .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~2 CLA         3 264  09/03/99  16:57 srvEcho_Stub.class
SRVECH~3 CLA         1 736  09/03/99  16:57 srvEcho_Skel.class

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

على جانب الخادم:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

على جانب العميل:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

9.2.1.6. الخطوة 6: تشغيل تطبيق الصدى بين العميل والخادم

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

  • يُنشئ الخدمة
  • تسجلها في دليل خدمات الجهاز الذي يعمل عليه خادم Echo

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

start j:\jdk12\bin\rmiregistry

rmiregistry هي خدمة الدليل. يتم تشغيلها هنا في الخلفية في نافذة DOS في Windows باستخدام الأمر start. بعد تفعيل الدليل، يمكننا إنشاء خدمة Echo وتسجيلها في دليل الخدمات. هنا أيضًا، يتم تشغيلها في الخلفية باستخدام الأمر start:

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

يتم تشغيل خادم Echo في نافذة جديدة DOS ويعرض كما طُلب منه:

Serveur d’écho prêt

لم يتبق لنا سوى تشغيل عميلنا واختباره:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

9.2.1.7. العميل والخادم على جهازين مختلفين

في المثال السابق، كان العميل والخادم على نفس الجهاز. نضعهما الآن على جهازين مختلفين:

  • الخادم على جهاز يعمل بنظام ويندوز
  • العميل على جهاز يعمل بنظام لينكس

يتم تشغيل الخادم كما في المثال السابق على جهاز ويندوز. على جهاز لينكس، تم نقل ملفات .class الخاصة بالعميل:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:02 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

تم تشغيل العميل:

$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
      java.rmi.UnmarshalException: error unmarshalling call header; nested exception is: 
      java.rmi.UnmarshalException: skeleton class not found but required for client version

لذلك لدينا خطأ: يبدو أن الآلة الافتراضية Java تطلب الملف srvEcho_skel.class الذي تم إنشاؤه بواسطة الأداة المساعدة rmic ولكن لم يتم استخدامه حتى الآن. نقوم بإعادة إنشائه وننقله أيضًا إلى جهاز Linux:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:17 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

نواجه نفس الخطأ الذي واجهناه سابقًا... لذا نفكر ونعيد قراءة الوثائق المتعلقة بـ RMI. ونصل في النهاية إلى استنتاج مفاده أن الخادم نفسه ربما يحتاج إلى الملف الشهير srvEcho_Skel.class. لذلك، أعدنا تشغيل الخادم على جهاز Windows مع وجود الملفين srvEcho_Stub.class و srvEcho_Skel.class :

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~2 CLA         3 264  10/03/99   9:05 srvEcho_Stub.class
SRVECH~3 CLA         1 736  10/03/99   9:05 srvEcho_Skel.class

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

ثم على جهاز لينكس، نختبر العميل مرة أخرى وهذه المرة، يعمل:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

وبالتالي، يمكننا استنتاج أن الملفين srvEcho_Stub.class و srvEcho_Skel.class يجب أن يكونا موجودين على جانب الخادم. على جانب العميل، لم يكن هناك حاجة حتى الآن سوى للملف srvEcho_Stub.class. وقد تبين أنه ضروري عندما كان العميل والخادم على نفس جهاز Windows. في نظام Linux، نزيله لنرى...

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

لدينا خطأ مثير للاهتمام يبدو أنه يشير إلى أن الآلة الافتراضية Java حاولت تحميل الفئة الشهيرة stub ولكنها فشلت في ذلك بسبب عدم وجود "مدير أمان". نتذكر أننا رأينا شيئًا حول هذا الموضوع في الوثائق. نعود إلى الوثائق... ونجد أن الخادم يجب أن ينشئ ويثبت مدير أمان (security manager) يضمن للعملاء الذين يطلبون تحميل الفئات أن هذه الفئات سليمة. وفي غياب مدير الأمان هذا، يكون تحميل الفئات مستحيلاً. يبدو أن هذا منطقي: طلب عميلنا الذي يعمل بنظام لينكس الفئة srvEcho_stub.class التي يحتاجها من الخادم، لكن الخادم رفض ذلك قائلاً إنه لم يتم تثبيت أي مدير أمان. لذلك، نقوم بتعديل كود الدالة main في الخادم على النحو التالي:

     // إنشاء الخدمة publ
    ic static void main (String arg[]){

         // تثبيت مدير أمان System.setSecurityMan
        ager(new RMISecurityManager()); 

        // تشغيل الخدمة وتسجيلها
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// الرئيسية

نقوم بالتحويل البرمجي وإنتاج الملفين srvEcho_stub.class و srvEcho_Skel.class باستخدام الأداة rmic. يتم تشغيل خدمة الدليل (rmiregistry) ثم الخادم، ونحصل على خطأ لم يكن موجودًا من قبل!

Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)  lors du lancement du serveur d’écho

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

start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

حيث

java.security.policy هي كلمة رئيسية

mypolicy هو ملف نصي يحدد حقوق البرنامج. وهنا، يكون كما يلي:

grant {
    // السماح بكل شيء في الوقت الحالي
    permission java.security.AllPermission;
};

يتمتع البرنامج هنا بجميع الحقوق.

نبدأ من جديد. ننتقل إلى دليل الخادم ونقوم بالتالي:

  • تشغيل خدمة الدليل: start j:\jdk12\bin\rmiregistry
  • تشغيل الخادم: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

وهذه المرة، يتم تشغيل خادم echo (وليس العميل بعد) بشكل صحيح. الآن يمكنك إجراء التجربة التالية:

  • أوقف خادم الإيكو ثم خدمة الدليل
  • أعد تشغيل خدمة الدليل من دليل مختلف عن دليل الخادم
  • عد إلى دليل الخادم قم بتشغيل خادم الإيكو - ستحصل على الخطأ التالي:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

يستنتج من ذلك أن الدليل الذي يتم تشغيل خدمة الدليل منه له أهمية. هنا، لم يعثر Java على الفئة srvEcho_stub.class لأن خدمة الدليل لم يتم تشغيلها من دليل الخادم. عند تشغيل الخادم، يمكن تحديد الدليل الذي توجد فيه الفئات اللازمة للخادم:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

يكون الأمر في سطر واحد. تُستخدم الكلمة الرئيسية java.rmi.server.codebase للإشارة إلى URL للدليل الذي يحتوي على الفئات اللازمة للخادم. هنا، تحدد هذه URL بروتوكول file وهو بروتوكول الوصول إلى الملفات المحلية والدليل الذي يحتوي على ملفات .class للخادم. لذا، إذا قمنا بما يلي:

  • إيقاف خدمة الدليل
  • إعادة تشغيل خدمة الدليل من دليل آخر غير دليل الخادم
  • في دليل الخادم، قم بتشغيله باستخدام الأمر (سطر واحد):
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

يتم عندئذ تشغيل الخادم بشكل صحيح. يمكننا الآن الانتقال إلى العميل. نقوم باختباره:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

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

public static void main(String arg[]){
         // الصيغة: cltEcho منفذ الجهاز
         // الجهاز: الجهاز الذي يعمل عليه خادم الصدى 
         // المنفذ: المنفذ الذي يعمل عليه دليل الخدمات على جهاز خدمة الإيكو // الت

        حقق من الوسيطات if(arg.leng
        th!=1){ System.
            err.println("Syntaxe : pg url_service_rmi"); Sys
            tem.exit(1); 
        }

         // تثبيت برنامج إدارة الأمان System.s
        etSecurityManager(new RMISecurityManager()); //

         التواصل بين العميل والخاد
        م String urlService=arg
        [0]; BufferedReader i
        n=null; String
         msg=null; String 
        reponse=null; interEc

        ho serveur=null; try{
            ....
        } catch (Exception e){
            ....
        }// محاولة
    }// الرئيسية

ثم يتم اتباع الخطوات التالية:

  • نقوم بإعادة ترجمة cltEcho.java
  • ننقل ملفات .class إلى جهاز لينكس
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
  • يتم تشغيل العميل
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
        at java.io.FileInputStream.<init>(FileInputStream.java)
        at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
        at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
        at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
        at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is: 
        java.lang.ClassNotFoundException: srvEcho_Stub

على الرغم من المظاهر، فإننا نحرز تقدماً: لم يعد الخطأ كما كان. نلاحظ أن العميل تمكن من طلب الفئة srvEcho_Stub.class من الخادم، لكن هذا الأخير لم يعثر عليها. لذا، يجب أن يكون لدى العميل مدير أمان إذا أراد أن يطلب فئات من الخادم.

إذا نظرنا إلى الخطأ السابق، نرى أن الملف srvEcho_Stub.class تم البحث عنه في الدليل e:/data/java/rmi/echo/serveur/ ولم يتم العثور عليه. ومع ذلك، فهو موجود هناك بالفعل. إذا نظرنا بشكل أكثر دقة إلى قائمة الطرق المتورطة في الخطأ، نجد هذه الطريقة: sun.net.www.protocol.file.FileURLConnection.getInputStream. يبدو أن العميل قد فتح دفقًا باستخدام كائن من النوع FileURLConnection. نستنتج أن كل هذا له علاقة بالطريقة التي أطلقنا بها الخادم:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

يبدو أن رسالة الخطأ تشير إلى قيمة الكلمة الرئيسية java.rmi.server.codebase. عند إعادة النظر في الوثائق، نلاحظ أن قيمة هذه الكلمة الرئيسية، في الأمثلة المقدمة، هي دائمًا: http://..، c.a.d. وأن البروتوكول المستخدم هو http. لا يظهر بوضوح كيف يطلب العميل فئاته ويحصل عليها من الخادم. ربما يطلبها باستخدام URL من الكلمة الرئيسية java.rmi.server.codebase، URL المحددة عند تشغيل الخادم. لذلك قررنا تشغيل الخادم باستخدام الأمر الجديد التالي:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

أصبح البروتوكول الآن http. نحن مضطرون لنقل الملفات .class إلى مكان يمكن الوصول إليه من خادم http الخاص بالجهاز الذي سيتم تخزين الفئات عليه. في مثالنا، يعمل الخادم على جهاز ويندوز مع خادم http PWS من مايكروسوفت. جذر هذا الخادم هو d:\Inetpub\wwwroot. لذلك نتبع الخطوات التالية:

  • ننشئ الدليل d:\Inetpub\wwwroot\rmi\echo
  • نضع فيه ملفات .class الخاصة بالخادم بالإضافة إلى الملف mypolicy
  • تشغيل خادم الويب إذا لم يكن قد تم تشغيله بالفعل
  • إعادة تشغيل خدمة الدليل (rmiregistry)
  • نعيد تشغيل الخادم باستخدام الأمر
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • على جهاز لينكس، قم بتشغيل العميل:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1622 Mar 10 14:37 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

أخيرًا! لقد نجح الأمر. تمكن العميل من استرداد الملف الشهير srvEcho_Stub.class.

كل هذا أعطانا بعض الأفكار، ونحن نتساءل عما إذا كان العميل الموجود على جهاز Windows الخاص بالخادم سيعمل هو الآخر بدون الملف srvEcho_Stub.class. ننتقل إلى دليل العميل، ونحذف الملف srvEcho_Stub.class إذا كان موجودًا هناك، ونقوم بتشغيل العميل بنفس الطريقة المتبعة في نظام Linux:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 622  10/03/99  14:12 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin

9.2.1.8. Résumé

جانب خادم Windows:

  • يحتوي الخادم على مدير أمان
  • تم تشغيله باستخدام الخيارات التالية: start j:\jdk12\bin\java -Djava.security.policy=mypolicy

-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

على جانب العميل Linux أو Windows

  • يحتوي العميل على مدير أمان
  • على Linux، تم تشغيله بواسطة java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
  • على نظام Windows، تم تشغيله بواسطة j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

9.2.1.9. خادم Echo على نظام Linux، وعملاء على نظامي Windows وLinux

نقوم الآن بتشغيل الخادم على جهاز Linux ونختبر عملاء Linux و Windows. الخطوات التي يجب اتباعها هي كما يلي:

  • ننقل الملفات .class من الخادم إلى جهاز Linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 16:15 .
drwxr-xr-x   3 serge    admin        1024 Mar 10 16:09 ..
-rw-r--r--   1 serge    admin         256 Mar 10 16:09 interEcho.class
-rw-r--r--   1 serge    admin        1245 Mar 10 16:09 srvEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 16:09 srvEcho_Stub.class
  • نظرًا لأن الفئة srvEcho_Stub.class ستكون مطلوبة من قبل العملاء، فإن الدليل المختار لفئات الخادم هو دليل يمكن الوصول إليه من خادم http الخاص بجهاز Linux. هنا، URL في هذا الدليل هو http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • يتم تشغيل خدمة الدليل في الخلفية: /usr/local/bin/jdk/rmiregistry &
  • يتم تشغيل الخادم في الخلفية: /usr/local/bin/jdk/bin/java

-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur/

srvEcho &

يمكننا اختبار العملاء. عميل Windows أولاً.

  • ننتقل إلى دليل العميل على جهاز Windows
  • نقوم بتشغيل العميل باستخدام الأمر:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin

نقوم باختبار عميل Linux:

shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

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

9.3. المثال الثاني: خادم SQL على جهاز يعمل بنظام Windows

9.3.1. المشكلة

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

Image

يمكن للعميل RMI القيام بثلاث عمليات:

  • الاتصال بقاعدة البيانات التي يختارها
  • إرسال طلبات SQL
  • إغلاق الاتصال

يقوم الخادم بتنفيذ طلبات SQL الصادرة عن العميل وإرسال النتائج إليه. هذه هي مهمته الأساسية ولهذا السبب سنطلق عليه اسم خادم SQL.

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

9.3.2. الخطوة 1: الواجهة البعيدة

الواجهة البعيدة هي الواجهة التي تسرد طرق خادم RMI التي ستكون متاحة للعملاء RMI. سنستخدم الواجهة التالية:

import java.rmi.*;

 // الواجهة البعيدة pu
blic interface interSQL extends Remote{ 
    public String connect(String pilote, String url, String id, String mdp)
         throws java.rmi.RemoteExcepti
    on; public String[] executeSQL(String requete, String separat
        eur) throws java.rmi.RemoteEx
    ception; public Stri
        ng close() throws java.rmi.Rem
oteException; }

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

Connect: يتصل العميل بقاعدة بيانات بعيدة ويحدد لها pilote، و url و JDBC بالإضافة إلى هويته id وكلمة مروره mdp للوصول إلى هذه القاعدة. يعيد الخادم إليه سلسلة من الأحرف تشير إلى نتيجة الاتصال:

    200 - Connexion réussie
    500 - Echec de la connexion

executeSQL: يطلب العميل تنفيذ استعلام SQL على قاعدة البيانات التي يتصل بها. ويحدد الحرف الذي يجب أن يفصل بين الحقول في النتائج التي يتم إرجاعها إليه. يرد الخادم بجدول من السلاسل:

    100 n

لطلب تحديث قاعدة البيانات، حيث يمثل n عدد الأسطر التي تم تحديثها

    500 msg d’erreur

إذا تسبب الطلب في حدوث خطأ

    501 Pas de résultats

إذا لم ينتج عن الطلب أي نتيجة

    101 ligne1
    101 ligne2
    101 ...

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

close: يقوم العميل بإغلاق اتصاله بالقاعدة البعيدة. يرسل الخادم سلسلة تشير إلى نتيجة هذا الإغلاق:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

9.3.3. الخطوة 2: كتابة الخادم

فيما يلي مصدر Java للخادم SQL. لفهمه، يجب أن تكون قد استوعبت إدارة قواعد البيانات JDBC وكذلك بناء الخوادم RMI. من المفترض أن تسهل تعليقات البرنامج فهمه.

// الحزم المستوردة
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// فئة srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // البيانات العامة للفئة
    private Connection DB;

    // ------------- المنشئ
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- الاتصال
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // الاتصال بقاعدة البيانات url بفضل برنامج التشغيل
        // التعريف باستخدام المعرف id وكلمة المرور mdp

        String resultat=null;            // نتيجة الطريقة
        try{
            // تحميل برنامج التشغيل
            Class.forName(pilote);
            // طلب الاتصال
            DB=DriverManager.getConnection(url,id,mdp);
            // موافق
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // خطأ
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // نهاية
        return resultat;
    }            

    // ------------- executeSQL
    public String[] executeSQL(String requete, String separateur)
        throws RemoteException{

        // يُنفذ استعلام SQL على قاعدة البيانات DB
        // ويضع النتائج في مصفوفة سلاسل

        // البيانات اللازمة لتنفيذ الاستعلام
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // إنشاء حاوية الاستعلام
            S=DB.createStatement();
            // تنفيذ الاستعلام
            if (! S.execute(requete)){
                // استعلام التحديث
                // إرجاع عدد الأسطر التي تم تحديثها
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // كان هذا استعلامًا
            // استرداد النتائج
            RS=S.getResultSet();
            // عدد الحقول في مجموعة النتائج
            int nbChamps=RS.getMetaData().getColumnCount();
            // يتم استغلالها
            while(RS.next()){
                // إنشاء سطر النتائج
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // إضافة إلى متجه النتائج
                resultats.addElement(ligne);
            }// while
            // نهاية معالجة النتائج
            // تحرير الموارد
            RS.close();
            S.close();
            // إرجاع النتائج
            int nbLignes=resultats.size();
            if (nbLignes==0){
                lignes=new String[1];
                lignes[0]="501 Pas de résultats";
            } else {
                lignes=new String[resultats.size()];
                for(int i=0;i<lignes.length;i++)
                    lignes[i]=(String) resultats.elementAt(i);
            }//if
            return lignes;
        } catch (Exception e){
            // خطأ
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- إغلاق
    public String close() throws RemoteException {
        // يغلق الاتصال بقاعدة البيانات
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // إرجاع النتيجة
        return resultat;
    }

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

        // مدير الأمان
        System.setSecurityManager(new RMISecurityManager());

        // تشغيل الخدمة
        srvSQL serveurSQL=null;
        try{
            // إنشاء
            serveurSQL=new srvSQL();
            // التسجيل
            Naming.rebind("srvSQL",serveurSQL);
            // المتابعة
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // خطأ
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// فئة

9.3.4. كتابة عميل RMI

يتم استدعاء عميل الخادم RMI بالمعلمات التالية:

    urlserviceAnnuaire pilote urlBase id mdp separateur

urlserviceAnnuaire: عنوان URL RMI لخدمة الدليل التي سجلت الخادم SQL

برنامج التشغيل: برنامج التشغيل الذي يجب أن يستخدمه الخادم SQL لإدارة قاعدة البيانات

urlBase: عنوان URL JDBC لقاعدة البيانات المطلوب إدارتها

id: هوية العميل أو null في حالة عدم وجود هوية

mdp: كلمة مرور العميل أو null في حالة عدم وجود كلمة مرور

الفاصل: الحرف الذي يجب أن يستخدمه الخادم SQL لفصل الحقول في سطور نتائج الاستعلام

فيما يلي مثال على المعلمات الممكنة:

    srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

مع ما يلي:

urlserviceAnnuaire    srvSQL

اسم rmi للخادم SQL

pilote     sun.jdbc.odbc.JdbcOdbcDriver

برنامج التشغيل المعتاد للقواعد مع واجهة odbc

urlBase    jdbc:odbc:articles

لاستخدام قاعدة بيانات المنتجات المعلنة في قائمة قواعد البيانات العامة ODBC لجهاز Windows

id    null

لا توجد هوية

mdp    null

لا توجد كلمة مرور

separateur    , 

سيتم فصل حقول النتائج بفاصلة

بمجرد التشغيل باستخدام المعلمات السابقة، يتبع العميل الخطوات التالية:

  • يتصل بالخادم RMI srvSQL، أي خادم RMI على نفس الجهاز الذي يعمل عليه العميل
  • يطلب الاتصال بقاعدة بيانات المنتجات
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • يطلب من المستخدم كتابة استعلام SQL على لوحة المفاتيح
  • يرسلها إلى الخادم SQL
executeSQL(requete, ’’,’’);
  • يعرض على الشاشة النتائج التي أرسلها الخادم
  • يطلب من المستخدم مرة أخرى كتابة استعلام SQL على لوحة المفاتيح. سيتوقف عندما ينتهي الاستعلام.

فيما يلي نص Java الخاص بالعميل. التعليقات كافية لفهمه.

import java.rmi.*;
import java.io.*;

public class cltSQL {

    // بيانات الفئة العامة
    private static String syntaxe =
        "syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
    private static BufferedReader in=null;
    private static interSQL serveurSQL=null;

    public static void main(String arg[]){
        // الصياغة: cltSQL urlServiceAnnuaire فاصل برنامج تشغيل url معرف كلمة مرور
        // urlServiceAnnuaire: عنوان URL لدليل الخدمات RMI التي يجب الاتصال بها
        // برنامج التشغيل: برنامج التشغيل المطلوب استخدامه لقاعدة البيانات المطلوب استغلالها
        // urlBase: عنوان URL jdbc للقاعدة المطلوب استغلالها
        // id: هوية المستخدم
        // mdp: كلمة المرور الخاصة به
        // الفاصل: سلسلة تفصل بين الحقول في نتائج الاستعلام

        // التحقق من عدد الحجج
        if(arg.length!=6)
            erreur(syntaxe,1);

        // تعيين معلمات الاتصال بقاعدة البيانات
        String urlService=arg[0];        
        String pilote=arg[1];
        String urlBase=arg[2];
        String id, mdp, separateur;
        if(arg[3].equals("null")) id=""; else id=arg[3];
        if(arg[4].equals("null")) mdp=""; else mdp=arg[4];        
        if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];        

        // تثبيت مدير أمان
        System.setSecurityManager(new RMISecurityManager());

        // التواصل بين العميل والخادم
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // فتح تدفق لوحة المفاتيح
            in=new BufferedReader(new InputStreamReader(System.in));
            // المتابعة
            System.out.println("--> Connexion au serveur RMI en cours...");
            // تحديد موقع الخدمة
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // المتابعة
            System.out.println("--> Connexion à la base de données en cours");
            // طلب الاتصال الأولي بقاعدة البيانات
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // متابعة
            System.out.println("<-- "+reponse);
            // تحليل الرد
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // حلقة قراءة الطلبات المراد إرسالها إلى الخادم SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // إرسال الطلب إلى الخادم واستلام الرد
                lignes=serveurSQL.executeSQL(requete,separateur);
                // متابعة
                afficheLignes(lignes);
                // الطلب التالي
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// بينما
            // متابعة
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // إغلاق الاتصال
            reponse=serveurSQL.close();
            // متابعة
            System.out.println("<-- " + reponse);
            // النهاية
            System.exit(0);
        // إدارة الأخطاء        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// محاولة
    }// الرئيسية

    // ----------- AfficheLignes
    private static void afficheLignes(String[] lignes){
        for (int i=0;i<lignes.length;i++)
            System.out.println("<-- " + lignes[i]);
    }// afficheLignes

    // ------------ خطأ
    private static void erreur(String msg, int exitCode){
        // عرض رسالة الخطأ
        System.err.println(msg);
        // تحرير الموارد المحتمل
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // الخروج
        System.exit(exitCode);
    }// خطأ

}// فئة    

9.3.5. الخطوة 3: إنشاء ملفات .class

  • يتم ترجمة الخادم
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java

E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java

E:\data\java\RMI\sql\serveur>dir *.class

INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
  • تم إنشاء ملفات Stub و Skel
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL

E:\data\java\RMI\sql\serveur>dir *.class


INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
SRVSQL~2 CLA         4 491  12/03/99  17:56 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  12/03/99  17:56 srvSQL_Skel.class
  • يتم نقل الملفات interSQL.class و srvSQL_Stub.class و srvSQL_Skel.class إلى دليل العميل
E:\data\java\RMI\sql\client>dir

CLTSQL~1 JAV         3 486  11/03/99  11:39 cltSQL.java
INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
  • يتم تجميع العميل
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java

E:\data\java\RMI\sql\client>dir *.class

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
CLTSQL~1 CLA         2 839  12/03/99  18:00 cltSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class

9.3.6. الخطوة 4: الاختبارات باستخدام الخادم والعميل على نفس جهاز ويندوز

  • يتم تشغيل خدمة الدليل في دليل مختلف عن دليل الخادم والعميل
F:\>start j:\jdk12\bin\rmiregistry
  • يتم وضع ملف mypolicy التالي في مجلدات العميل والخادم
grant {
    // السماح بكل شيء في الوقت الحالي
    permission java.security.AllPermission;
};
  • يتم تشغيل الخادم
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
  • تشغيل العميل
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL 
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> الاستعلام: select name, stock_actu from articles order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> الاستعلام: تحديث المقالات تعيين stock_actu=stock_actu-1 حيث stock_actu<=11
<-- 100 5
--> الاستعلام: select الاسم,stock_actu from articles order by stock_actu asc
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.7. الخطوة 5: الاختبارات باستخدام خادم على جهاز يعمل بنظام ويندوز وعميل على جهاز يعمل بنظام لينكس

  • يتم إيقاف الخادم وخدمة الدليل عند الضرورة
  • ننقل ملفات .class من العميل إلى جهاز يعمل بنظام لينكس
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r--   1 serge    admin        2839 Mar 11 14:37 cltSQL.class
-rw-r--r--   1 serge    admin         451 Mar 11 14:37 interSQL.class
  • يتم وضع ملفات الخادم في دليل يمكن الوصول إليه من خادم http الخاص بجهاز ويندوز
D:\Inetpub\wwwroot\rmi\sql>dir

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         3 238  11/03/99  13:19 srvSQL.class
SRVSQL~2 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
MYPOLICY                81  08/06/98  15:01 mypolicy
  • إعادة تشغيل خدمة الدليل
F:\>start j:\jdk12\bin\rmiregistry
  • إعادة تشغيل الخادم بإعدادات مختلفة عن تلك المستخدمة في الاختبار السابق
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
  • تشغيل العميل على جهاز لينكس
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> الاستعلام: select الاسم,stock_actu,stock_mini from articles order by الاسم
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> الاستعلام: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> الاستعلام: select الاسم,stock_actu,stock_mini من المقالات مرتبة حسب الاسم
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.8. الخلاصة

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

  • سيكون لدينا عميل وخادم يمكن كتابتهما بلغات مختلفة
  • كان العميل والخادم سيتواصلان عن طريق تبادل أسطر نصية وسيكون الحوار بينهما على النحو التالي:
client : connect machine port pilote urlBase id mdp

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

قد يرد الخادم بشيء من هذا القبيل:

200 - Connexion réussie
500 - Echec de la connexion
client : executeSQL requete, separateur

لطلب من الخادم تنفيذ استعلام SQL على قاعدة البيانات المتصلة بالعميل. حيث يمثل الفاصل الحرف الفاصل بين الحقول في أسطر الرد.

قد يرد الخادم بشيء من هذا القبيل

    100 n

لطلب تحديث قاعدة البيانات، حيث يمثل n عدد الأسطر التي تم تحديثها

    500 msg d’erreur

إذا تسبب الطلب في حدوث خطأ

    501 Pas de résultats

إذا لم تسفر الاستعلام عن أي نتائج

    101 ligne1
    101 ligne2
    101 ...

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

client : close

لإغلاق الاتصال بقاعدة البيانات البعيدة. قد يرسل الخادم سلسلة تشير إلى نتيجة هذا الإغلاق:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

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

    commande param1 param2 ... paramq

فسيكون بإمكاننا الحصول داخل الخادم RMI على طريقة

    String commande(param1,param2,..., paramq)

وهذه الطريقة المتاحة للعميل يجب أن تكون جزءًا من واجهة الخادم المنشورة.

ولنختتم هذا الموضوع، نلاحظ أن خادمنا لا يدير حاليًا سوى عميل واحد: فهو لا يستطيع إدارة عدة عملاء، كما هو مكتوب حاليًا. في الواقع، إذا اتصل عميل بقاعدة بيانات B1، يقوم الخادم بإنشاء كائن Connection DB=DB1. إذا طلب عميل ثانٍ الاتصال بقاعدة بيانات B2، يسجل الخادم ذلك عن طريق إنشاء Connection DB=DB2، وبذلك يقطع اتصال العميل الأول بقاعدة البيانات B1.

9.4. تمارين

9.4.1. التمرين 1

قم بتوسيع الخادم SQL السابق بحيث يدير عدة عملاء.

9.4.2. التمرين 2

اكتب تطبيق Java الصغير للتجارة الإلكترونية المقدم في تمارين الفصل JDBC بحيث يعمل مع الخادم RMI من التمرين السابق.