9. JAVA RMI
9.1. مقدمة
لقد رأينا كيفية إنشاء تطبيقات الشبكة باستخدام أدوات الاتصال التي تسمى sockets. في تطبيق العميل/الخادم المبني على هذه الأدوات، فإن الرابط بين العميل والخادم هو بروتوكول الاتصال الذي اعتمداه للتواصل. يمكن كتابة التطبيقين بلغات مختلفة: Java، على سبيل المثال، للعميل، وPerl للخادم، أو أي تركيبة أخرى. لدينا بالفعل تطبيقان متميزان متصلان ببروتوكول اتصال معروف لكليهما. علاوة على ذلك، فإن الوصول إلى الشبكة عبر المقابس (sockets) ليس شفافًا لتطبيق Java: يجب أن يستخدم فئة Socket، وهي فئة تم إنشاؤها خصيصًا لإدارة أدوات الاتصال هذه المعروفة باسم المقابس (sockets).
تتيح لك Java RMI (استدعاء الطرق عن بُعد) إنشاء تطبيقات شبكية بالخصائص التالية:
- تطبيقات العميل/الخادم هي تطبيقات Java في طرفي الاتصال
- يمكن للعميل استخدام الكائنات الموجودة على الخادم كما لو كانت محلية
- تصبح طبقة الشبكة شفافة: لا يتعين على التطبيقات القلق بشأن كيفية نقل المعلومات من نقطة إلى أخرى.
النقطة الأخيرة هي عامل في قابلية النقل: إذا تغيرت طبقة الشبكة لتطبيق RMI، فلن يكون من الضروري إعادة كتابة التطبيق نفسه. بل إن فئات RMI في لغة Java هي التي ستحتاج إلى التكييف مع طبقة الشبكة الجديدة.
مبدأ اتصال RMI هو كما يلي:
- يتم كتابة تطبيق Java قياسي على الجهاز A. وسيعمل هذا التطبيق كخادم. وللقيام بذلك، سيتم "نشر" بعض كائناته على الجهاز A، حيث يتم تشغيل التطبيق، وستصبح بعد ذلك خدمات.
- يتم كتابة تطبيق 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
في الفئة السابقة، نجد:
- الطريقة التي تقوم بعملية echo
- منشئ لا يقوم بأي شيء سوى استدعاء منشئ الفئة الأصلية. وهو موجود لإعلان أنه يمكن أن يرمي استثناء RemoteException.
سننشئ مثيلًا لهذه الفئة باستخدام طريقة main. لكي يكون الكائن/الخدمة متاحًا من الخارج، يجب إنشاؤه وتسجيله في دليل الكائنات التي يمكن الوصول إليها من الخارج. يقوم العميل الذي يرغب في الوصول إلى كائن بعيد بالخطوات التالية:
- يتصل بخدمة الدليل على الجهاز الذي يوجد فيه الكائن المطلوب. تعمل خدمة الدليل هذه على منفذ يجب أن يعرفه العميل (1099 افتراضيًا). يطلب العميل من الدليل مرجعًا إلى كائن/خدمة، مع توفير اسمه. إذا كان هذا الاسم يتوافق مع كائن/خدمة في الدليل، فإن الدليل يعيد مرجعًا إلى العميل، يمكن للعميل من خلاله التواصل مع الكائن/الخدمة البعيدة.
- من تلك اللحظة فصاعدًا، يمكن للعميل استخدام هذا الكائن البعيد كما لو كان محليًا
بالعودة إلى الخادم الخاص بنا، يتعين علينا إنشاء كائن من نوع srvEcho وتسجيله في دليل الكائنات التي يمكن الوصول إليها من الخارج. ويتم هذا التسجيل باستخدام طريقة rebind الخاصة بفئة Naming:
حيث
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 ويتوقف عندما يكون السطر المكتوب هو "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
لا يوجد ما يميز هذا العميل بشكل خاص، باستثناء العبارة التي تسترد مرجع الخادم:
تذكر أن خدمة echo الخاصة بنا تم تسجيلها في دليل الخدمات الخاص بالجهاز الذي توجد عليه باستخدام التعليمات التالية:
Naming.rebind("srvEcho",serveurEcho);
وبالتالي، يستخدم العميل أيضًا طريقة من فئة Naming للحصول على مرجع للخادم الذي يريد استخدامه. تأخذ طريقة البحث المستخدمة عنوان URL للخدمة المطلوبة كمعلمة. يتخذ عنوان URL هذا شكل عنوان URL قياسي:
حيث
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
في دليل العميل، نجد ملف المصدر التالي:
بالإضافة إلى ملف 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. في الواقع، لم نسمع عن هذه الفئة من قبل. في العميل، تم تحديد موقع الخادم باستخدام العبارة التالية:
هنا، 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، الأمر:
سيؤدي ذلك إلى إنشاء ملفين آخرين بتنسيق .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
تتطلب هذه الخطوة الأخيرة خدمة تسجيل. يتم تشغيلها باستخدام الأمر:
rmiregistry هي خدمة التسجيل. هنا، يتم تشغيلها في الخلفية في نافذة موجه أوامر Windows باستخدام الأمر start. مع تنشيط التسجيل، يمكننا إنشاء خدمة echo وتسجيلها في سجل الخدمات. مرة أخرى، يتم تشغيلها في الخلفية باستخدام الأمر start:
يعمل خادم echo في نافذة DOS جديدة ويعرض الناتج المطلوب:
كل ما تبقى هو تشغيل العميل واختباره:
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 هو ملف نصي يحدد أذونات البرنامج. وفي هذه الحالة، يكون كما يلي:
يتمتع البرنامج هنا بأذونات كاملة.
لنبدأ من جديد. انتقل إلى دليل الخادم وقم بما يلي:
- ابدأ خدمة الدليل: 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 الموجودة على الجهاز الذي يستضيف الخادم.

يمكن لعميل 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، بالإضافة إلى معرفه وكلمة مروره للوصول إلى قاعدة البيانات. يعرض الخادم سلسلة تشير إلى نتيجة الاتصال:
executeSQL: يطلب العميل تنفيذ استعلام SQL على قاعدة البيانات التي يتصل بها. ويحدد الحرف الذي يجب أن يفصل بين الحقول في النتائج التي يتم إرجاعها إليه. يعرض الخادم مصفوفة من السلاسل:
لاستعلام تحديث قاعدة البيانات، حيث n هو عدد الصفوف التي تم تحديثها
إذا تسبب الاستعلام في حدوث خطأ
إذا لم يُرجع الاستعلام أي نتائج
إذا أعاد الاستعلام نتائج. الصفوف التي يعيدها الخادم هي نتائج الاستعلام.
close: يقوم العميل بإغلاق اتصاله بقاعدة البيانات البعيدة. يعرض الخادم سلسلة تشير إلى نتيجة هذا الإغلاق:
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 بالمعلمات التالية:
directoryServiceURL: عنوان URL RMI لخدمة الدليل التي سجلت خادم SQL
driver: برنامج التشغيل الذي يجب أن يستخدمه خادم SQL لإدارة قاعدة البيانات
urlBase: عنوان URL JDBC لقاعدة البيانات المراد إدارتها
id: معرف العميل أو null في حالة عدم وجود معرف
password: كلمة مرور العميل أو null في حالة عدم وجود كلمة مرور
separator: الحرف الذي يجب أن يستخدمه خادم SQL لفصل الحقول في صفوف نتائج الاستعلام
فيما يلي مثال على المعلمات المحتملة:
مع ما يلي:
اسم <bdi dir="ltr" class="odt-ltr-term">RMI</bdi> لخادم <bdi dir="ltr" class="odt-ltr-term">SQL</bdi>
برنامج التشغيل القياسي لقواعد البيانات المزودة بواجهة <bdi dir="ltr" class="odt-ltr-term">ODBC</bdi>
لاستخدام قاعدة بيانات المقالات المدرجة في قواعد بيانات <bdi dir="ltr" class="odt-ltr-term">ODBC</bdi> العامة لجهاز <bdi dir="ltr" class="odt-ltr-term">Windows</bdi>
لا توجد هوية
لا توجد كلمة مرور
سيتم فصل الحقول في النتائج بفاصلة
بمجرد التشغيل باستخدام المعلمات المذكورة أعلاه، يتبع العميل الخطوات التالية:
- يقوم بالاتصال بخادم RMI srvSQL، وهو خادم RMI موجود على نفس الجهاز الذي يوجد عليه العميل
- يطلبون الاتصال بقاعدة بيانات المقالات
- يطلب من المستخدم كتابة استعلام SQL
- يرسله إلى خادم SQL
- يعرض النتائج التي أرجعها الخادم على الشاشة
- يطلب من المستخدم مرة أخرى كتابة استعلام 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
- يتم تشغيل خدمة الدليل في دليل غير دليل الخادم والعميل
- ضع ملف mypolicy التالي في دلائل العميل والخادم
- بدء تشغيل الخادم
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
- أعد تشغيل خدمة الدليل
- أعد تشغيل الخادم باستخدام معلمات مختلفة عن تلك المستخدمة في الاختبار السابق
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)، وهو ما يتطلبه التمرين الوارد في الفصل الخاص بقواعد البيانات. لو كنا سنكتب هذا التطبيق بالطريقة التقليدية:
- سيكون لدينا عميل وخادم يمكن كتابتهما بلغات مختلفة
- كان العميل والخادم سيتواصلان عن طريق تبادل أسطر النص وسيكون بينهما حوار قد يبدو كالتالي:
حيث تحدد المعلمتان الأوليان مكان العثور على الخادم، وتشير المعلمات الأربع التالية إلى معلمات الاتصال لقاعدة البيانات التي سيتم استخدامها
قد يرد الخادم بشيء مثل:
لطلب من الخادم تنفيذ استعلام SQL على قاعدة البيانات المتصلة بالعميل. الفاصل هو الحرف المستخدم لفصل الحقول في أسطر الاستجابة.
قد يرد الخادم بشيء مثل
للاستعلام الخاص بتحديث قاعدة البيانات، حيث n هو عدد الصفوف التي تم تحديثها
إذا تسبب الاستعلام في حدوث خطأ
إذا لم يُرجع الاستعلام أي نتائج
إذا أعاد الاستعلام نتائج. الصفوف التي يعيدها الخادم هي نتائج الاستعلام.
لإغلاق الاتصال بقاعدة البيانات البعيدة. قد يعرض الخادم سلسلة تشير إلى نتيجة هذا الإغلاق:
هنا نرى أنه إذا تمكنا من إنشاء تطبيق تقليدي باستخدام بروتوكول من النوع الموصوف أعلاه، فيمكننا استنتاج بنية محتملة لخادم RMI. حيث يوجد في البروتوكول رسالة من العميل إلى الخادم من النوع:
يمكن أن يكون لدينا طريقة داخل خادم RMI
ويجب أن تكون هذه الطريقة، التي يمكن للعميل الوصول إليها، جزءًا من واجهة الخادم المنشورة.
وختامًا، لاحظ أن خادمنا لا يتعامل حاليًا إلا مع عميل واحد: فهو لا يستطيع التعامل مع عدة عملاء بالشكل الذي كُتب به حاليًا. في الواقع، إذا اتصل عميل بقاعدة البيانات B1، يقوم الخادم بإنشاء كائن Connection بقيمة DB=DB1. وإذا طلب عميل ثانٍ اتصالًا بقاعدة البيانات B2، يقوم الخادم بإنشائه بقيمة Connection DB=DB2، مما يؤدي إلى قطع اتصال العميل الأول بقاعدة البيانات B1.
9.4. تمارين
9.4.1. التمرين 1
قم بتوسيع خادم SQL السابق بحيث يمكنه التعامل مع عدة عملاء.
9.4.2. التمرين 2
اكتب برنامج Java الصغير للتجارة الإلكترونية المقدم في تمارين فصل JDBC بحيث يعمل مع خادم RMI من التمرين السابق.