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.*;

 // remote interface p
ublic 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.*;

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo
}// end of class

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

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

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

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

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

Naming.rebind(String nom, Remote obj)

حيث

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

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

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

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo

    // service creation
    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 ");
        }
    }// hand
}// end of class

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

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 ويتوقف عندما يكون السطر المكتوب هو "end".

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

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

; public class cltEch

    o { public static void main(String a
        rg[]){ // syntax : cltEcho URLService

        // argument verification
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // client-server dialogue
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // service location
            serveur=(interEcho) Naming.lookup(urlService);                
            // loop for reading msg to be sent to echo server
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // send msg to server and receive response
                reponse=serveur.echo(msg);
                // follow-up
                System.out.println("Réponse serveur : " + reponse);
                // next msg
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // it's over
            System.exit(0);
        // error management        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// hand
}// class                

لا يوجد ما يميز هذا العميل بشكل خاص، باستثناء العبارة التي تسترد مرجع الخادم:

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

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


            Naming.rebind("srvEcho",serveurEcho);

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

    rmi://machine:port/nom_service

حيث

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

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

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

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

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

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

للتمييز بوضوح بين مكونات جانب الخادم وجانب العميل، سنضع الخادم في الدليل echo\server والعميل في الدليل 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

سيؤدي ذلك إلى إنشاء ملفين آخرين بتنسيق .class من ملف srvEcho.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 هي خدمة التسجيل. هنا، يتم تشغيلها في الخلفية في نافذة موجه أوامر 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. العميل والخادم على جهازين مختلفين

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

  • الخادم على جهاز يعمل بنظام Windows
  • العميل على جهاز يعمل بنظام Linux

يتم تشغيل الخادم كما في السابق على جهاز Windows. على جهاز Linux، تم نقل ملفات .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

ثم، على جهاز 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
-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

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

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

         // installation of a security manager Sy
        stem.setSecurityManager(new RMISecurityManager()); 

        // service launch and registration
        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 ");
        }
    }// hand

نقوم بتجميع وإنشاء الملفين 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

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

<mark style="background-color: #ffff00"><bdi dir="ltr" class="odt-ltr-term">start j:</bdi>\\<bdi dir="ltr" class="odt-ltr-term">jdk12</bdi>\\bin\\<bdi dir="ltr" class="odt-ltr-term">java -Djava.security.policy</bdi>=<bdi dir="ltr" class="odt-ltr-term">mypolicy srvEcho</bdi></mark>

حيث

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

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

grant {
    // Allow everything for now
    permission java.security.AllPermission;
};

يتمتع البرنامج هنا بأذونات كاملة.

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

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

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

  • أوقف خادم echo، ثم خدمة الدليل
  • أعد تشغيل خدمة الدليل أثناء وجودك في دليل غير دليل الخادم
  • العودة إلى دليل الخادم بدء تشغيل خادم 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

يوجد الأمر في سطر واحد. تُستخدم الكلمة الرئيسية <mark style="background-color: #ffff00">java.rmi.server.codebase</mark> لتحديد عنوان 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

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

public static void main(String arg[]){
         // syntax : cltEcho machine po
        rt // machine: machine where the echo server 
        operates // port: port where the service directory operates on the echo servic

        e machine // argument ve
        rification if(ar
            g.length!=1){ System.err.println("Syntaxe : pg u
            rl_service_rmi"
        )

        ; System.exit(1); } // installation
         of a security manager System.setSecurityManager(n

        ew RMISecurityManager()); 
         // client-server dia
        logue String urlServi
        ce=arg[0]; Buf
        feredReader in=null;
         String msg=null; S

        tring reponse=null; interEcho serveur=null; try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// hand

ثم نمضي كما يلي:

  • إعادة ترجمة cltEcho.java
  • نقل ملفات .class إلى جهاز Linux
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/server/ ولم يتم العثور عليه. ومع ذلك، هذا هو المكان الذي يوجد فيه بالضبط. إذا نظرنا عن كثب إلى قائمة الطرق المرتبطة بالخطأ، نجد هذه الطريقة: 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://..، أي أن البروتوكول المستخدم هو HTTP. ليس من الواضح كيف يطلب العميل فئاته ويحصل عليها من الخادم. ربما يطلبها باستخدام عنوان URL المحدد بواسطة الكلمة الرئيسية *java.rmi.server.codebase*، والتي يتم تعيينها عند تشغيل الخادم. لذلك قررنا تشغيل الخادم باستخدام الأمر الجديد التالي:

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 الوصول إليه على الجهاز الذي سيتم تخزين الفئات عليه. في مثالنا هذا، يعمل الخادم على جهاز يعمل بنظام Windows مزود بخادم HTTP من Microsoft 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
  • على جهاز Linux، قم بتشغيل العميل:
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. ملخص

جانب خادم 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/server/

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.

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

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

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

import java.rmi.*;

 // remote interface p
ublic interface interSQL extends Remote{ 
     public String connect(String pilote, String url, String id, String mdp
        ) throws java.rmi.RemoteExcept
    ion; public String[] executeSQL(String requete, String separa
        teur) throws java.rmi.RemoteE
    xception; public Str
        ing close() throws java.rmi.Re
moteException; }

فيما يلي وظائف الطرق المختلفة:

Connect: يتصل العميل بقاعدة بيانات بعيدة، موفرًا برنامج التشغيل وعنوان URL الخاص بـ JDBC، بالإضافة إلى معرفه وكلمة مروره للوصول إلى قاعدة البيانات. يعرض الخادم سلسلة تشير إلى نتيجة الاتصال:

    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: كتابة الخادم

فيما يلي شفرة المصدر بلغة جافا لخادم SQL. ويتطلب فهمها الإلمام بإدارة قواعد البيانات عبر JDBC وبناء خوادم RMI. ومن المفترض أن تسهل التعليقات المرفقة بالبرنامج عملية الفهم.

// imported packages
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// class srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // global class data
    private Connection DB;

    // ------------- manufacturer
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- connect
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // connection to url database via driver
        // identification with identity id and password mdp

        String resultat=null;            // result of the method
        try{
            // loading the driver
            Class.forName(pilote);
            // connection request
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // error
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // end
        return resultat;
    }            

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

        // executes a SQL query on the DB database
        // and puts the results in an array of strings

        // data required to execute the request
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // create query container
            S=DB.createStatement();
            // request execution
            if (! S.execute(requete)){
                // update request
                // returns the number of lines updated
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // it was a query request
            // retrieve results
            RS=S.getResultSet();
            // number of Resultset fields
            int nbChamps=RS.getMetaData().getColumnCount();
            // we exploit them
            while(RS.next()){
                // create results line
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // add to results vector
                resultats.addElement(ligne);
            }// while
            // end of results analysis
            // free up resources
            RS.close();
            S.close();
            // we return the results
            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){
            // error
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- close
    public String close() throws RemoteException {
        // closes database connection
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // return result
        return resultat;
    }

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

        // security manager
        System.setSecurityManager(new RMISecurityManager());

        // service launch
        srvSQL serveurSQL=null;
        try{
            // creation
            serveurSQL=new srvSQL();
            // registration
            Naming.rebind("srvSQL",serveurSQL);
            // follow-up
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // error
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// hand

}// class

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

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

    urlserviceAnnuaire pilote urlBase id mdp separateur

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

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

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

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

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

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

فيما يلي مثال على المعلمات المحتملة:

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

مع ما يلي:

urlserviceAnnuaire    srvSQL
اسم <bdi dir="ltr" class="odt-ltr-term">RMI</bdi> لخادم <bdi dir="ltr" class="odt-ltr-term">SQL</bdi>
pilote     sun.jdbc.odbc.JdbcOdbcDriver
برنامج التشغيل القياسي لقواعد البيانات المزودة بواجهة <bdi dir="ltr" class="odt-ltr-term">ODBC</bdi>
urlBase    jdbc:odbc:articles
لاستخدام قاعدة بيانات المقالات المدرجة في قواعد بيانات <bdi dir="ltr" class="odt-ltr-term">ODBC</bdi> العامة لجهاز <bdi dir="ltr" class="odt-ltr-term">Windows</bdi>
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 {

    // global class data
    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[]){
        // syntax : cltSQL urlServiceAnnuaire separator driver url id mdp
        // urlServiceAnnuaire : url of the directory of services RMI to contact
        // driver: driver to be used for the database to be processed
        // urlBase: jdbc url of the database to be used
        // id: user identity
        // mdp: password
        // separator: string separating fields in query results

        // check number of arguments
        if(arg.length!=6)
            erreur(syntaxe,1);

        // init database connection parameters
        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];        

        // installation of a security manager
        System.setSecurityManager(new RMISecurityManager());

        // client-server dialogue
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // follow-up
            System.out.println("--> Connexion au serveur RMI en cours...");
            // service location
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // follow-up
            System.out.println("--> Connexion à la base de données en cours");
            // initial database connection request
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // follow-up
            System.out.println("<-- "+reponse);
            // response analysis
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // loop for reading requests to be sent to the server SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // send request to server and receive response
                lignes=serveurSQL.executeSQL(requete,separateur);
                // follow-up
                afficheLignes(lignes);
                // following request
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // follow-up
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // close the connection
            reponse=serveurSQL.close();
            // follow-up
            System.out.println("<-- " + reponse);
            // end
            System.exit(0);
        // error management        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// try
    }// hand

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

    // ------------ error
    private static void erreur(String msg, int exitCode){
        // error msg display
        System.err.println(msg);
        // possible release of resources
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // we leave
        System.exit(exitCode);
    }// error

}// class    

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: الاختبار باستخدام الخادم والعميل على نفس جهاز Windows

  • يتم تشغيل خدمة الدليل في دليل غير دليل الخادم والعميل
F:\>start j:\jdk12\bin\rmiregistry
  • ضع ملف mypolicy التالي في دلائل العميل والخادم
grant {
    // Allow everything for now
    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 ,

--> Connection to server RMI in progress...
--> Current database connection
<-- 200 Successful connection
--> Requête : select nom, stock_actu from articles order by stock_actu desc
<-- 101 vélo.31
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 panther,11
<-- 101 leopard,11
<-- 101 sperm whale,10
<-- 101 rifle,10
<-- 101 arc,10
--> Query: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Requête : select nom,stock_actu from articles order by stock_actu asc
<-- 101 sperm whale,9
<-- 101 rifle,9
<-- 101 arc.9
<-- 101 panther,10
<-- 101 leopard,10
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 vélo.31
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

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

  • إذا لزم الأمر، أوقف الخادم وخدمة الدليل
  • انقل ملفات .class الخاصة بالعميل إلى جهاز يعمل بنظام Linux
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 الخاص بجهاز Windows
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
  • قم بتشغيل العميل على جهاز Linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,9,6
<-- 101 canoeing,13.7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,10.7
<-- 101 panther,10.7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,6,6
<-- 101 canoeing,7,7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,7,7
<-- 101 panther,7,7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

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 من التمرين السابق.