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.*;
// الواجهة البعيدة pu
blic interface interEcho extends Remote{
public String echo(String msg) throws java.rmi.RemoteException;
}
هنا، يتم إعلان واجهة interEcho التي تعلن أن الطريقة echo متاحة عن بُعد. من المحتمل أن تولد هذه الطريقة استثناءً من الفئة RemoteException، وهي الفئة التي تضم جميع الأخطاء المتعلقة بالشبكة.
9.2.1.2. الخطوة 2: كتابة كائن الخادم
في الخطوة التالية، يتم تعريف الفئة التي تنفذ الواجهة البعيدة السابقة. يجب أن تكون هذه الفئة مشتقة من الفئة UnicastRemoteObject، وهي الفئة التي تحتوي على الطرق التي تسمح باستدعاء الطرق عن بُعد.
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// فئة تنفذ الصدى البعيد
public class srvEcho extends UnicastRemoteObject implements interEcho{
// منشئ
public srvEcho() throws RemoteException{
super();
}// نهاية المنشئ
// طريقة تنفيذ الصدى
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// نهاية الصدى
}// نهاية الفئة
في الفئة السابقة، نجد:
- الطريقة التي تقوم بالترديد
- منشئ لا يفعل شيئًا سوى استدعاء منشئ الفئة الأم. وهو موجود لإعلان أنه يمكنه توليد استثناء من النوع RemoteException.
سنقوم بإنشاء مثيل لهذه الفئة باستخدام طريقة main. لكي يكون الكائن/الخدمة متاحًا من الخارج، يجب إنشاؤه وتسجيله في دليل الكائنات المتاحة من الخارج. يقوم العميل الذي يرغب في الوصول إلى كائن بعيد بالخطوات التالية:
- يتصل بخدمة الدليل الخاصة بالجهاز الذي يوجد عليه الكائن الذي يرغب فيه. تعمل خدمة الدليل هذه على منفذ يجب أن يعرفه العميل (1099 افتراضيًا). يطلب العميل من الدليل مرجعًا لكائن/خدمة يحدد اسمها. إذا كان هذا الاسم هو اسم كائن/خدمة موجودة في الدليل، فإن الدليل يرسل إلى العميل مرجعًا يمكن للعميل من خلاله التفاعل مع الكائن/الخدمة البعيدة.
- منذ تلك اللحظة، يمكن للعميل استخدام هذا الكائن البعيد كما لو كان محليًا
بالعودة إلى خادمنا، يجب علينا إنشاء كائن من النوع srvEcho وتسجيله في دليل الكائنات التي يمكن الوصول إليها من الخارج. يتم هذا التسجيل باستخدام طريقة الفئة rebind من الفئة Naming:
مع
الاسم: الاسم الذي سيتم ربطه بالكائن البعيد
obj: الكائن البعيد
وبالتالي، تصبح فئتنا srvEcho كما يلي:
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// الفئة التي تنفذ الصدى البعيد
public class srvEcho extends UnicastRemoteObject implements interEcho{
// منشئ
public srvEcho() throws RemoteException{
super();
}// نهاية المنشئ
// طريقة تنفيذ الصدى
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// نهاية الصدى
// إنشاء الخدمة
public static void main (String arg[]){
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// main
}// نهاية الفئة
عند قراءة البرنامج السابق، يبدو أنه سيتوقف فور إنشاء خدمة الصدى وتسجيلها. هذا ليس صحيحًا. نظرًا لأن الفئة srvEcho مشتقة من الفئة UnicastRemoteObject، فإن الكائن الذي تم إنشاؤه يعمل إلى ما لا نهاية: فهو يستمع إلى طلبات العملاء على منفذ مجهول، أي يتم اختياره من قبل النظام وفقًا للظروف. إن إنشاء الخدمة غير متزامن: في المثال، تقوم الطريقة main بإنشاء الخدمة ومواصلة تنفيذها: وستعرض بالفعل "خادم الصدى جاهز".
9.2.1.3. الخطوة 3: ترجمة تطبيق الخادم
في هذه المرحلة، يمكننا ترجمة الخادم الخاص بنا. نقوم بترجمة الملف interEcho.java من الواجهة interEcho وكذلك الملف srvEcho.java من الفئة srvEcho. ونحصل على الملفات .class المقابلة: interEcho.class و srvEcho.class.
9.2.1.4. الخطوة 4: كتابة العميل
نكتب عميلاً نمرر إليه كمعلمة ملف URL من خادم الإيكو والذي
- يقرأ سطراً تمت كتابته على لوحة المفاتيح
- يرسلها إلى خادم الإيكو
- يعرض الرد الذي يرسله الخادم
- يعود إلى الخطوة 1 ويتوقف عندما تكون السطر المكتوب هو "fin".
ينتج عن ذلك العميل التالي:
import java.rmi.*;
import java.io.*
; public class cltEch
o { public static void main(String a
rg[]){ // الصيغة: cltEcho URLService
// التحقق من الحجج
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
// حوار العميل-الخادم
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho serveur=null;
try{
// فتح تدفق لوحة المفاتيح
in=new BufferedReader(new InputStreamReader(System.in));
// تحديد موقع الخدمة
serveur=(interEcho) Naming.lookup(urlService);
// حلقة قراءة الرسائل المراد إرسالها إلى خادم الصدى
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
while(! msg.equals("fin")){
// إرسال الرسالة إلى الخادم واستلام الرد
reponse=serveur.echo(msg);
// المتابعة
System.out.println("Réponse serveur : " + reponse);
// الرسالة التالية
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
}// while
// انتهى
System.exit(0);
// إدارة الأخطاء
} catch (Exception e){
System.err.println("Erreur : " + e);
System.exit(2);
}// try
}// main
}// فئة
لا يوجد شيء مميز في هذا العميل سوى التعليمات التي تطلب مرجعًا من الخادم:
نتذكر أن خدمة الصدى الخاصة بنا قد تم تسجيلها في دليل خدمات الجهاز الذي توجد عليه باستخدام التعليمات التالية:
Naming.rebind("srvEcho",serveurEcho);
لذلك يستخدم العميل أيضًا طريقة من فئة Naming للحصول على مرجع للخادم الذي يريد استخدامه. تقبل طريقة lookup المستخدمة عنوان URL للخدمة المطلوبة كمعلمة. ويكون هذا العنوان على شكل عنوان URL كلاسيكي:
مع
rmi: اختياري - بروتوكول rmi
machine: اسم أو عنوان IP للجهاز الذي يعمل عليه خادم الإيكو - اختياري، القيمة الافتراضية هي localhost.
port: منفذ الاستماع لخدمة الدليل في هذا الجهاز - اختياري، القيمة الافتراضية هي 1099
nom_service: الاسم الذي تم تسجيل الخدمة المطلوبة تحته (srvEcho في مثالنا)
ما يتم استرداده هو مثيل للواجهة البعيدة interEcho. إذا افترضنا أن العميل والخادم ليسا على نفس الجهاز، فعند ترجمة العميل cltEcho.java، يجب أن يتوفر في نفس الدليل الملف interEcho.class، وهو نتيجة ترجمة الواجهة البعيدة interEcho، وإلا فسيحدث خطأ في الترجمة في الأسطر التي تشير إلى هذه الواجهة.
9.2.1.5. الخطوة 5: إنشاء ملفات .class اللازمة لتطبيق العميل-الخادم
من أجل فهم ما هو موجود على جانب الخادم وما هو موجود على جانب العميل بشكل جيد، سنضع الخادم في دليل echo\serveur والعميل في دليل echo\client.
يحتوي دليل الخادم على ملفات المصدر التالية:
E:\data\java\RMI\echo\serveur>dir *.java
INTERE~1 JAV 158 09/03/99 15:06 interEcho.java
SRVECH~1 JAV 759 09/03/99 15:07 srvEcho.java
بعد ترجمة هذين الملفين المصدرين، نحصل على الملفات التالية .class:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
في دليل العميل، نجد الملف المصدر التالي:
بالإضافة إلى الملف 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، يتم تنفيذ الأمر:
سينتج، من الملف srvEcho.class، ملفين آخرين هما .class:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~2 CLA 3 264 09/03/99 16:57 srvEcho_Stub.class
SRVECH~3 CLA 1 736 09/03/99 16:57 srvEcho_Skel.class
يوجد هنا بالفعل الملف srvEcho_stub.class الذي يحتاجه كل من العميل والخادم عند التنفيذ. يوجد أيضًا ملف srvEcho_Skel.class الذي لا نعرف دوره في الوقت الحالي. نقوم بعمل نسخة من الملف srvEcho_stub.class في دليل العميل والخادم ونحذف الملف srvEcho_Skel.class. وبذلك يكون لدينا الملفات التالية:
على جانب الخادم:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
على جانب العميل:
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 506 09/03/99 16:08 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
9.2.1.6. الخطوة 6: تشغيل تطبيق الصدى بين العميل والخادم
نحن جاهزون لتشغيل تطبيق العميل-الخادم الخاص بنا. في البداية، سيعمل كل من العميل والخادم على نفس الجهاز. يجب أولاً تشغيل تطبيق الخادم الخاص بنا. نتذكر أن هذا التطبيق:
- يُنشئ الخدمة
- تسجلها في دليل خدمات الجهاز الذي يعمل عليه خادم Echo
يتطلب هذا الأمر وجود خدمة دليل. يتم تشغيل هذه الخدمة بواسطة الأمر:
rmiregistry هي خدمة الدليل. يتم تشغيلها هنا في الخلفية في نافذة DOS في 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. العميل والخادم على جهازين مختلفين
في المثال السابق، كان العميل والخادم على نفس الجهاز. نضعهما الآن على جهازين مختلفين:
- الخادم على جهاز يعمل بنظام ويندوز
- العميل على جهاز يعمل بنظام لينكس
يتم تشغيل الخادم كما في المثال السابق على جهاز ويندوز. على جهاز لينكس، تم نقل ملفات .class الخاصة بالعميل:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x 2 serge admin 1024 Mar 10 10:02 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
تم تشغيل العميل:
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling call header; nested exception is:
java.rmi.UnmarshalException: skeleton class not found but required for client version
لذلك لدينا خطأ: يبدو أن الآلة الافتراضية Java تطلب الملف srvEcho_skel.class الذي تم إنشاؤه بواسطة الأداة المساعدة rmic ولكن لم يتم استخدامه حتى الآن. نقوم بإعادة إنشائه وننقله أيضًا إلى جهاز Linux:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 10:17 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
نواجه نفس الخطأ الذي واجهناه سابقًا... لذا نفكر ونعيد قراءة الوثائق المتعلقة بـ RMI. ونصل في النهاية إلى استنتاج مفاده أن الخادم نفسه ربما يحتاج إلى الملف الشهير srvEcho_Skel.class. لذلك، أعدنا تشغيل الخادم على جهاز Windows مع وجود الملفين srvEcho_Stub.class و srvEcho_Skel.class :
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~2 CLA 3 264 10/03/99 9:05 srvEcho_Stub.class
SRVECH~3 CLA 1 736 10/03/99 9:05 srvEcho_Skel.class
E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho
ثم على جهاز لينكس، نختبر العميل مرة أخرى وهذه المرة، يعمل:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
وبالتالي، يمكننا استنتاج أن الملفين srvEcho_Stub.class و srvEcho_Skel.class يجب أن يكونا موجودين على جانب الخادم. على جانب العميل، لم يكن هناك حاجة حتى الآن سوى للملف srvEcho_Stub.class. وقد تبين أنه ضروري عندما كان العميل والخادم على نفس جهاز Windows. في نظام Linux، نزيله لنرى...
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
لدينا خطأ مثير للاهتمام يبدو أنه يشير إلى أن الآلة الافتراضية Java حاولت تحميل الفئة الشهيرة stub ولكنها فشلت في ذلك بسبب عدم وجود "مدير أمان". نتذكر أننا رأينا شيئًا حول هذا الموضوع في الوثائق. نعود إلى الوثائق... ونجد أن الخادم يجب أن ينشئ ويثبت مدير أمان (security manager) يضمن للعملاء الذين يطلبون تحميل الفئات أن هذه الفئات سليمة. وفي غياب مدير الأمان هذا، يكون تحميل الفئات مستحيلاً. يبدو أن هذا منطقي: طلب عميلنا الذي يعمل بنظام لينكس الفئة srvEcho_stub.class التي يحتاجها من الخادم، لكن الخادم رفض ذلك قائلاً إنه لم يتم تثبيت أي مدير أمان. لذلك، نقوم بتعديل كود الدالة main في الخادم على النحو التالي:
// إنشاء الخدمة publ
ic static void main (String arg[]){
// تثبيت مدير أمان System.setSecurityMan
ager(new RMISecurityManager());
// تشغيل الخدمة وتسجيلها
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// الرئيسية
نقوم بالتحويل البرمجي وإنتاج الملفين srvEcho_stub.class و srvEcho_Skel.class باستخدام الأداة rmic. يتم تشغيل خدمة الدليل (rmiregistry) ثم الخادم، ونحصل على خطأ لم يكن موجودًا من قبل!
Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve) lors du lancement du serveur d’écho
يبدو أن مدير الأمان كان فعالاً للغاية. نقرأ الوثائق مرة أخرى... نلاحظ أنه عندما يكون مدير الأمان نشطاً، يجب تحديد حقوقه عند تشغيل البرنامج. يتم ذلك باستخدام الخيار التالي:
start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
حيث
java.security.policy هي كلمة رئيسية
mypolicy هو ملف نصي يحدد حقوق البرنامج. وهنا، يكون كما يلي:
يتمتع البرنامج هنا بجميع الحقوق.
نبدأ من جديد. ننتقل إلى دليل الخادم ونقوم بالتالي:
- تشغيل خدمة الدليل: start j:\jdk12\bin\rmiregistry
- تشغيل الخادم: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
وهذه المرة، يتم تشغيل خادم echo (وليس العميل بعد) بشكل صحيح. الآن يمكنك إجراء التجربة التالية:
- أوقف خادم الإيكو ثم خدمة الدليل
- أعد تشغيل خدمة الدليل من دليل مختلف عن دليل الخادم
- عد إلى دليل الخادم قم بتشغيل خادم الإيكو - ستحصل على الخطأ التالي:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub lors du lancement du serveur d’écho
يستنتج من ذلك أن الدليل الذي يتم تشغيل خدمة الدليل منه له أهمية. هنا، لم يعثر Java على الفئة srvEcho_stub.class لأن خدمة الدليل لم يتم تشغيلها من دليل الخادم. عند تشغيل الخادم، يمكن تحديد الدليل الذي توجد فيه الفئات اللازمة للخادم:
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
يكون الأمر في سطر واحد. تُستخدم الكلمة الرئيسية java.rmi.server.codebase للإشارة إلى URL للدليل الذي يحتوي على الفئات اللازمة للخادم. هنا، تحدد هذه URL بروتوكول file وهو بروتوكول الوصول إلى الملفات المحلية والدليل الذي يحتوي على ملفات .class للخادم. لذا، إذا قمنا بما يلي:
- إيقاف خدمة الدليل
- إعادة تشغيل خدمة الدليل من دليل آخر غير دليل الخادم
- في دليل الخادم، قم بتشغيله باستخدام الأمر (سطر واحد):
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
يتم عندئذ تشغيل الخادم بشكل صحيح. يمكننا الآن الانتقال إلى العميل. نقوم باختباره:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
نحصل على نفس الخطأ الذي يشير إلى عدم وجود مدير أمان. نعتقد أننا ربما أخطأنا وأنه يجب على العميل إنشاء مدير الأمان الخاص به. نترك الخادم مع مدير الأمان الخاص به ولكننا ننشئ واحدًا للعميل أيضًا. تصبح وظيفة main للعميل cltEcho.java كما يلي:
public static void main(String arg[]){
// الصيغة: cltEcho منفذ الجهاز
// الجهاز: الجهاز الذي يعمل عليه خادم الصدى
// المنفذ: المنفذ الذي يعمل عليه دليل الخدمات على جهاز خدمة الإيكو // الت
حقق من الوسيطات if(arg.leng
th!=1){ System.
err.println("Syntaxe : pg url_service_rmi"); Sys
tem.exit(1);
}
// تثبيت برنامج إدارة الأمان System.s
etSecurityManager(new RMISecurityManager()); //
التواصل بين العميل والخاد
م String urlService=arg
[0]; BufferedReader i
n=null; String
msg=null; String
reponse=null; interEc
ho serveur=null; try{
....
} catch (Exception e){
....
}// محاولة
}// الرئيسية
ثم يتم اتباع الخطوات التالية:
- نقوم بإعادة ترجمة cltEcho.java
- ننقل ملفات .class إلى جهاز لينكس
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
- يتم تشغيل العميل
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
at java.io.FileInputStream.<init>(FileInputStream.java)
at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub
على الرغم من المظاهر، فإننا نحرز تقدماً: لم يعد الخطأ كما كان. نلاحظ أن العميل تمكن من طلب الفئة srvEcho_Stub.class من الخادم، لكن هذا الأخير لم يعثر عليها. لذا، يجب أن يكون لدى العميل مدير أمان إذا أراد أن يطلب فئات من الخادم.
إذا نظرنا إلى الخطأ السابق، نرى أن الملف srvEcho_Stub.class تم البحث عنه في الدليل e:/data/java/rmi/echo/serveur/ ولم يتم العثور عليه. ومع ذلك، فهو موجود هناك بالفعل. إذا نظرنا بشكل أكثر دقة إلى قائمة الطرق المتورطة في الخطأ، نجد هذه الطريقة: sun.net.www.protocol.file.FileURLConnection.getInputStream. يبدو أن العميل قد فتح دفقًا باستخدام كائن من النوع FileURLConnection. نستنتج أن كل هذا له علاقة بالطريقة التي أطلقنا بها الخادم:
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
يبدو أن رسالة الخطأ تشير إلى قيمة الكلمة الرئيسية java.rmi.server.codebase. عند إعادة النظر في الوثائق، نلاحظ أن قيمة هذه الكلمة الرئيسية، في الأمثلة المقدمة، هي دائمًا: http://..، c.a.d. وأن البروتوكول المستخدم هو http. لا يظهر بوضوح كيف يطلب العميل فئاته ويحصل عليها من الخادم. ربما يطلبها باستخدام URL من الكلمة الرئيسية java.rmi.server.codebase، URL المحددة عند تشغيل الخادم. لذلك قررنا تشغيل الخادم باستخدام الأمر الجديد التالي:
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
أصبح البروتوكول الآن http. نحن مضطرون لنقل الملفات .class إلى مكان يمكن الوصول إليه من خادم http الخاص بالجهاز الذي سيتم تخزين الفئات عليه. في مثالنا، يعمل الخادم على جهاز ويندوز مع خادم http PWS من مايكروسوفت. جذر هذا الخادم هو d:\Inetpub\wwwroot. لذلك نتبع الخطوات التالية:
- ننشئ الدليل d:\Inetpub\wwwroot\rmi\echo
- نضع فيه ملفات .class الخاصة بالخادم بالإضافة إلى الملف mypolicy
- تشغيل خادم الويب إذا لم يكن قد تم تشغيله بالفعل
- إعادة تشغيل خدمة الدليل (rmiregistry)
- نعيد تشغيل الخادم باستخدام الأمر
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
- على جهاز لينكس، قم بتشغيل العميل:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1622 Mar 10 14:37 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
أخيرًا! لقد نجح الأمر. تمكن العميل من استرداد الملف الشهير srvEcho_Stub.class.
كل هذا أعطانا بعض الأفكار، ونحن نتساءل عما إذا كان العميل الموجود على جهاز Windows الخاص بالخادم سيعمل هو الآخر بدون الملف srvEcho_Stub.class. ننتقل إلى دليل العميل، ونحذف الملف srvEcho_Stub.class إذا كان موجودًا هناك، ونقوم بتشغيل العميل بنفس الطريقة المتبعة في نظام Linux:
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 622 10/03/99 14:12 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin
9.2.1.8. Résumé
جانب خادم Windows:
- يحتوي الخادم على مدير أمان
- تم تشغيله باستخدام الخيارات التالية: start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
على جانب العميل Linux أو Windows
- يحتوي العميل على مدير أمان
- على Linux، تم تشغيله بواسطة java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
- على نظام Windows، تم تشغيله بواسطة j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
9.2.1.9. خادم Echo على نظام Linux، وعملاء على نظامي Windows وLinux
نقوم الآن بتشغيل الخادم على جهاز Linux ونختبر عملاء Linux و Windows. الخطوات التي يجب اتباعها هي كما يلي:
- ننقل الملفات .class من الخادم إلى جهاز Linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 16:15 .
drwxr-xr-x 3 serge admin 1024 Mar 10 16:09 ..
-rw-r--r-- 1 serge admin 256 Mar 10 16:09 interEcho.class
-rw-r--r-- 1 serge admin 1245 Mar 10 16:09 srvEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 16:09 srvEcho_Stub.class
- نظرًا لأن الفئة srvEcho_Stub.class ستكون مطلوبة من قبل العملاء، فإن الدليل المختار لفئات الخادم هو دليل يمكن الوصول إليه من خادم http الخاص بجهاز Linux. هنا، URL في هذا الدليل هو http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
- يتم تشغيل خدمة الدليل في الخلفية: /usr/local/bin/jdk/rmiregistry &
- يتم تشغيل الخادم في الخلفية: /usr/local/bin/jdk/bin/java
-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur/
srvEcho &
يمكننا اختبار العملاء. عميل Windows أولاً.
- ننتقل إلى دليل العميل على جهاز Windows
- نقوم بتشغيل العميل باستخدام الأمر:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin
نقوم باختبار عميل Linux:
shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
تجدر الإشارة إلى أنه بالنسبة لعميل Linux الذي يعمل على نفس الجهاز الذي يعمل عليه خادم Echo، لم تكن هناك حاجة لتحديد الجهاز في URL للخدمة المطلوبة.
9.3. المثال الثاني: خادم SQL على جهاز يعمل بنظام Windows
9.3.1. المشكلة
لقد رأينا في الفصل JDBC كيفية إدارة قواعد البيانات العلائقية. في الأمثلة المقدمة، كانت التطبيقات وقاعدة البيانات المستخدمة موجودة على نفس جهاز Windows. نقترح هنا كتابة خادم RMI على جهاز Windows، مما سيسمح للعملاء البعيدين باستخدام قواعد البيانات العامة ODBC الخاصة بالجهاز الذي يوجد عليه الخادم.

يمكن للعميل RMI القيام بثلاث عمليات:
- الاتصال بقاعدة البيانات التي يختارها
- إرسال طلبات SQL
- إغلاق الاتصال
يقوم الخادم بتنفيذ طلبات SQL الصادرة عن العميل وإرسال النتائج إليه. هذه هي مهمته الأساسية ولهذا السبب سنطلق عليه اسم خادم SQL.
نطبق الخطوات المختلفة التي رأيناها سابقًا مع خادم الصدى.
9.3.2. الخطوة 1: الواجهة البعيدة
الواجهة البعيدة هي الواجهة التي تسرد طرق خادم RMI التي ستكون متاحة للعملاء RMI. سنستخدم الواجهة التالية:
import java.rmi.*;
// الواجهة البعيدة pu
blic interface interSQL extends Remote{
public String connect(String pilote, String url, String id, String mdp)
throws java.rmi.RemoteExcepti
on; public String[] executeSQL(String requete, String separat
eur) throws java.rmi.RemoteEx
ception; public Stri
ng close() throws java.rmi.Rem
oteException; }
دور الطرق المختلفة هو كما يلي:
Connect: يتصل العميل بقاعدة بيانات بعيدة ويحدد لها pilote، و url و JDBC بالإضافة إلى هويته id وكلمة مروره mdp للوصول إلى هذه القاعدة. يعيد الخادم إليه سلسلة من الأحرف تشير إلى نتيجة الاتصال:
executeSQL: يطلب العميل تنفيذ استعلام SQL على قاعدة البيانات التي يتصل بها. ويحدد الحرف الذي يجب أن يفصل بين الحقول في النتائج التي يتم إرجاعها إليه. يرد الخادم بجدول من السلاسل:
لطلب تحديث قاعدة البيانات، حيث يمثل n عدد الأسطر التي تم تحديثها
إذا تسبب الطلب في حدوث خطأ
إذا لم ينتج عن الطلب أي نتيجة
إذا أسفر الطلب عن نتائج. الأسطر التي يرسلها الخادم بهذه الطريقة هي أسطر نتائج الطلب.
close: يقوم العميل بإغلاق اتصاله بالقاعدة البعيدة. يرسل الخادم سلسلة تشير إلى نتيجة هذا الإغلاق:
9.3.3. الخطوة 2: كتابة الخادم
فيما يلي مصدر Java للخادم SQL. لفهمه، يجب أن تكون قد استوعبت إدارة قواعد البيانات JDBC وكذلك بناء الخوادم RMI. من المفترض أن تسهل تعليقات البرنامج فهمه.
// الحزم المستوردة
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;
// فئة srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{
// البيانات العامة للفئة
private Connection DB;
// ------------- المنشئ
public srvSQL() throws RemoteException{
super();
}
// --------------- الاتصال
public String connect(String pilote, String url, String id,
String mdp) throws RemoteException{
// الاتصال بقاعدة البيانات url بفضل برنامج التشغيل
// التعريف باستخدام المعرف id وكلمة المرور mdp
String resultat=null; // نتيجة الطريقة
try{
// تحميل برنامج التشغيل
Class.forName(pilote);
// طلب الاتصال
DB=DriverManager.getConnection(url,id,mdp);
// موافق
resultat="200 Connexion réussie";
} catch (Exception e){
// خطأ
resultat="500 Echec de la connexion (" + e + ")";
}
// نهاية
return resultat;
}
// ------------- executeSQL
public String[] executeSQL(String requete, String separateur)
throws RemoteException{
// يُنفذ استعلام SQL على قاعدة البيانات DB
// ويضع النتائج في مصفوفة سلاسل
// البيانات اللازمة لتنفيذ الاستعلام
Statement S=null;
ResultSet RS=null;
String[] lignes=null;
Vector resultats=new Vector();
String ligne=null;
try{
// إنشاء حاوية الاستعلام
S=DB.createStatement();
// تنفيذ الاستعلام
if (! S.execute(requete)){
// استعلام التحديث
// إرجاع عدد الأسطر التي تم تحديثها
lignes=new String[1];
lignes[0]="100 "+S.getUpdateCount();
return lignes;
}
// كان هذا استعلامًا
// استرداد النتائج
RS=S.getResultSet();
// عدد الحقول في مجموعة النتائج
int nbChamps=RS.getMetaData().getColumnCount();
// يتم استغلالها
while(RS.next()){
// إنشاء سطر النتائج
ligne="101 ";
for (int i=1;i<nbChamps;i++)
ligne+=RS.getString(i)+separateur;
ligne+=RS.getString(nbChamps);
// إضافة إلى متجه النتائج
resultats.addElement(ligne);
}// while
// نهاية معالجة النتائج
// تحرير الموارد
RS.close();
S.close();
// إرجاع النتائج
int nbLignes=resultats.size();
if (nbLignes==0){
lignes=new String[1];
lignes[0]="501 Pas de résultats";
} else {
lignes=new String[resultats.size()];
for(int i=0;i<lignes.length;i++)
lignes[i]=(String) resultats.elementAt(i);
}//if
return lignes;
} catch (Exception e){
// خطأ
lignes=new String[1];
lignes[0]="500 " + e;
return lignes;
}// try-catch
}// executeSQL
// --------------- إغلاق
public String close() throws RemoteException {
// يغلق الاتصال بقاعدة البيانات
String resultat=null;
try{
DB.close();
resultat="200 Base fermée";
} catch (Exception e){
resultat="500 Erreur à la fermeture de la base ("+e+")";
}
// إرجاع النتيجة
return resultat;
}
// ----------- main
public static void main (String[] args){
// مدير الأمان
System.setSecurityManager(new RMISecurityManager());
// تشغيل الخدمة
srvSQL serveurSQL=null;
try{
// إنشاء
serveurSQL=new srvSQL();
// التسجيل
Naming.rebind("srvSQL",serveurSQL);
// المتابعة
System.out.println("Serveur SQL prêt");
} catch (Exception e){
// خطأ
System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
}// try-catch
}// main
}// فئة
9.3.4. كتابة عميل RMI
يتم استدعاء عميل الخادم RMI بالمعلمات التالية:
urlserviceAnnuaire: عنوان URL RMI لخدمة الدليل التي سجلت الخادم SQL
برنامج التشغيل: برنامج التشغيل الذي يجب أن يستخدمه الخادم SQL لإدارة قاعدة البيانات
urlBase: عنوان URL JDBC لقاعدة البيانات المطلوب إدارتها
id: هوية العميل أو null في حالة عدم وجود هوية
mdp: كلمة مرور العميل أو null في حالة عدم وجود كلمة مرور
الفاصل: الحرف الذي يجب أن يستخدمه الخادم SQL لفصل الحقول في سطور نتائج الاستعلام
فيما يلي مثال على المعلمات الممكنة:
مع ما يلي:
اسم rmi للخادم SQL
برنامج التشغيل المعتاد للقواعد مع واجهة odbc
لاستخدام قاعدة بيانات المنتجات المعلنة في قائمة قواعد البيانات العامة ODBC لجهاز Windows
لا توجد هوية
لا توجد كلمة مرور
سيتم فصل حقول النتائج بفاصلة
بمجرد التشغيل باستخدام المعلمات السابقة، يتبع العميل الخطوات التالية:
- يتصل بالخادم RMI srvSQL، أي خادم RMI على نفس الجهاز الذي يعمل عليه العميل
- يطلب الاتصال بقاعدة بيانات المنتجات
- يطلب من المستخدم كتابة استعلام SQL على لوحة المفاتيح
- يرسلها إلى الخادم SQL
- يعرض على الشاشة النتائج التي أرسلها الخادم
- يطلب من المستخدم مرة أخرى كتابة استعلام SQL على لوحة المفاتيح. سيتوقف عندما ينتهي الاستعلام.
فيما يلي نص Java الخاص بالعميل. التعليقات كافية لفهمه.
import java.rmi.*;
import java.io.*;
public class cltSQL {
// بيانات الفئة العامة
private static String syntaxe =
"syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
private static BufferedReader in=null;
private static interSQL serveurSQL=null;
public static void main(String arg[]){
// الصياغة: cltSQL urlServiceAnnuaire فاصل برنامج تشغيل url معرف كلمة مرور
// urlServiceAnnuaire: عنوان URL لدليل الخدمات RMI التي يجب الاتصال بها
// برنامج التشغيل: برنامج التشغيل المطلوب استخدامه لقاعدة البيانات المطلوب استغلالها
// urlBase: عنوان URL jdbc للقاعدة المطلوب استغلالها
// id: هوية المستخدم
// mdp: كلمة المرور الخاصة به
// الفاصل: سلسلة تفصل بين الحقول في نتائج الاستعلام
// التحقق من عدد الحجج
if(arg.length!=6)
erreur(syntaxe,1);
// تعيين معلمات الاتصال بقاعدة البيانات
String urlService=arg[0];
String pilote=arg[1];
String urlBase=arg[2];
String id, mdp, separateur;
if(arg[3].equals("null")) id=""; else id=arg[3];
if(arg[4].equals("null")) mdp=""; else mdp=arg[4];
if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];
// تثبيت مدير أمان
System.setSecurityManager(new RMISecurityManager());
// التواصل بين العميل والخادم
String requete=null;
String reponse=null;
String[] lignes=null;
String codeErreur=null;
try{
// فتح تدفق لوحة المفاتيح
in=new BufferedReader(new InputStreamReader(System.in));
// المتابعة
System.out.println("--> Connexion au serveur RMI en cours...");
// تحديد موقع الخدمة
serveurSQL=(interSQL) Naming.lookup(urlService);
// المتابعة
System.out.println("--> Connexion à la base de données en cours");
// طلب الاتصال الأولي بقاعدة البيانات
reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
// متابعة
System.out.println("<-- "+reponse);
// تحليل الرد
codeErreur=reponse.substring(0,3);
if(codeErreur.equals("500"))
erreur("Abandon sur erreur de connexion à la base",3);
// حلقة قراءة الطلبات المراد إرسالها إلى الخادم SQL
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
while(! requete.equals("fin")){
// إرسال الطلب إلى الخادم واستلام الرد
lignes=serveurSQL.executeSQL(requete,separateur);
// متابعة
afficheLignes(lignes);
// الطلب التالي
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
}// بينما
// متابعة
System.out.println("--> Fermeture de la connexion à la base de données distante");
// إغلاق الاتصال
reponse=serveurSQL.close();
// متابعة
System.out.println("<-- " + reponse);
// النهاية
System.exit(0);
// إدارة الأخطاء
} catch (Exception e){
erreur("Abandon sur erreur : " + e,2);
}// محاولة
}// الرئيسية
// ----------- AfficheLignes
private static void afficheLignes(String[] lignes){
for (int i=0;i<lignes.length;i++)
System.out.println("<-- " + lignes[i]);
}// afficheLignes
// ------------ خطأ
private static void erreur(String msg, int exitCode){
// عرض رسالة الخطأ
System.err.println(msg);
// تحرير الموارد المحتمل
try{
in.close();
serveurSQL.close();
} catch(Exception e){}
// الخروج
System.exit(exitCode);
}// خطأ
}// فئة
9.3.5. الخطوة 3: إنشاء ملفات .class
- يتم ترجمة الخادم
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
- تم إنشاء ملفات Stub و Skel
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
SRVSQL~2 CLA 4 491 12/03/99 17:56 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 12/03/99 17:56 srvSQL_Skel.class
- يتم نقل الملفات interSQL.class و srvSQL_Stub.class و srvSQL_Skel.class إلى دليل العميل
E:\data\java\RMI\sql\client>dir
CLTSQL~1 JAV 3 486 11/03/99 11:39 cltSQL.java
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
- يتم تجميع العميل
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java
E:\data\java\RMI\sql\client>dir *.class
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
CLTSQL~1 CLA 2 839 12/03/99 18:00 cltSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
9.3.6. الخطوة 4: الاختبارات باستخدام الخادم والعميل على نفس جهاز ويندوز
- يتم تشغيل خدمة الدليل في دليل مختلف عن دليل الخادم والعميل
- يتم وضع ملف 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 ,
--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> الاستعلام: select name, stock_actu from articles order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> الاستعلام: تحديث المقالات تعيين stock_actu=stock_actu-1 حيث stock_actu<=11
<-- 100 5
--> الاستعلام: select الاسم,stock_actu from articles order by stock_actu asc
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.7. الخطوة 5: الاختبارات باستخدام خادم على جهاز يعمل بنظام ويندوز وعميل على جهاز يعمل بنظام لينكس
- يتم إيقاف الخادم وخدمة الدليل عند الضرورة
- ننقل ملفات .class من العميل إلى جهاز يعمل بنظام لينكس
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r-- 1 serge admin 2839 Mar 11 14:37 cltSQL.class
-rw-r--r-- 1 serge admin 451 Mar 11 14:37 interSQL.class
- يتم وضع ملفات الخادم في دليل يمكن الوصول إليه من خادم http الخاص بجهاز ويندوز
D:\Inetpub\wwwroot\rmi\sql>dir
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 3 238 11/03/99 13:19 srvSQL.class
SRVSQL~2 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
MYPOLICY 81 08/06/98 15:01 mypolicy
- إعادة تشغيل خدمة الدليل
- إعادة تشغيل الخادم بإعدادات مختلفة عن تلك المستخدمة في الاختبار السابق
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
- تشغيل العميل على جهاز لينكس
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,
--> الاستعلام: select الاسم,stock_actu,stock_mini from articles order by الاسم
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> الاستعلام: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> الاستعلام: select الاسم,stock_actu,stock_mini من المقالات مرتبة حسب الاسم
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.8. الخلاصة
لدينا هنا تطبيق مثير للاهتمام لأنه يتيح الوصول إلى قاعدة بيانات من أي جهاز على الشبكة. كان من الممكن تمامًا كتابته بالطريقة التقليدية باستخدام مآخذ التوصيل (sockets)، وهو ما يُطلب في أحد التمارين في الفصل الخاص بقواعد البيانات. إذا كتبنا هذا التطبيق بالطريقة التقليدية:
- سيكون لدينا عميل وخادم يمكن كتابتهما بلغات مختلفة
- كان العميل والخادم سيتواصلان عن طريق تبادل أسطر نصية وسيكون الحوار بينهما على النحو التالي:
حيث تحدد المعلمتان الأوليتان مكان الخادم، بينما تشير المعلمات الأربع التالية إلى معلمات الاتصال بقاعدة البيانات المطلوب استخدامها
قد يرد الخادم بشيء من هذا القبيل:
لطلب من الخادم تنفيذ استعلام 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 من التمرين السابق.