9. JAVA RMI
9.1. مقدمه
ما دیدیم که چگونه با استفاده از ابزارهای ارتباطی معروف به sockets، برنامههای شبکهای ایجاد کنیم. در یک برنامهٔ کلاینت/سرور که با استفاده از این ابزارها ساخته شده است، پیوند بین کلاینت و سرور همان پروتکل ارتباطی است که برای برقراری ارتباط از آن استفاده میکنند. این دو برنامه میتوانند به زبانهای مختلفی نوشته شوند: برای مثال جاوا برای کلاینت، پرل برای سرور، یا هر ترکیب دیگری. در واقع دو برنامه مجزا وجود دارند که توسط یک پروتکل ارتباطی مشترک به هم متصل شدهاند. علاوه بر این، دسترسی به شبکه از طریق ساکتها برای یک برنامه جاوا شفاف نیست: این برنامه باید از کلاس Socket استفاده کند، کلاسی که به طور خاص برای مدیریت این ابزارهای ارتباطی، یعنی sockets، ایجاد شده است.
JAVA و RMI (فراخوانی متد از راه دور) امکان ایجاد برنامههای شبکهای با ویژگیهای زیر را فراهم میکنند:
- برنامههای کلاینت/سرور در هر دو انتهای ارتباط، برنامههای جاوا هستند
- کلاینت میتواند از اشیاء قرار گرفته روی سرور استفاده کند، گویی که محلی هستند
- لایه شبکه شفاف میشود: برنامهها نیازی ندارند نگران نحوه انتقال اطلاعات از نقطهای به نقطه دیگر باشند.
نقطهٔ آخر عاملی در قابلیت حمل است: اگر لایهٔ شبکهٔ یک برنامهٔ RMI تغییر کند، خودِ برنامه نیازی به بازنویسی نخواهد داشت. این کلاسهای RMI در زبان جاوا هستند که باید با لایهٔ شبکهٔ جدید سازگار شوند.
اصل پشت ارتباطات RMI به شرح زیر است:
- یک برنامه استاندارد جاوا روی ماشین A نوشته میشود. این برنامه نقش سرور را ایفا میکند. برای این کار، برخی از اشیاء آن روی ماشین A، جایی که برنامه در حال اجراست، «منتشر» میشوند و بدین ترتیب به سرویس تبدیل میگردند.
- یک برنامهٔ سنتی جاوا روی ماشین B نوشته میشود. این برنامه نقش کلاینت را ایفا میکند. این برنامه به اشیاء/خدمات منتشرشده روی ماشین A دسترسی خواهد داشت؛ به عبارت دیگر، از طریق یک مرجع راه دور، قادر خواهد بود آنها را طوری دستکاری کند که گویی محلی هستند. برای این کار، لازم است ساختار شیء راه دوری را که میخواهد به آن دسترسی پیدا کند (متدها و ویژگیها) بشناسد.
9.2. بیایید به یک مثال نگاه کنیم
نظریه پشت رابط RMI ساده نیست. برای درک بهتر، ما گام به گام فرآیند نوشتن یک برنامه کلاینت/سرور را با استفاده از پکیج جاوا RMI دنبال خواهیم کرد. ما از برنامهای که در بسیاری از کتابها در مورد RMI یافت میشود، استفاده خواهیم کرد: کلاینت یک متد واحد را روی یک شیء راه دور فراخوانی میکند که سپس یک رشته را بازمیگرداند. در اینجا، ما یک تغییر جزئی ارائه میدهیم: سرور به سادگی آنچه را که کلاینت برایش ارسال میکند، بازتاب میدهد. ما قبلاً چنین برنامهای را در این کتاب ارائه کردهایم که مبتنی بر ساکتها است.
9.2.1. برنامهٔ سمت سرور
9.2.1.1. مرحله ۱: رابط شیء/سرور
یک شیء راه دور نمونهای از کلاسی است که باید رابط Remote را که در بسته java.rmi تعریف شده است، پیادهسازی کند. متدهای شیء که بهصورت راه دور در دسترس خواهند بود، آنهایی هستند که در رابطی مشتقشده از رابط Remote اعلام شدهاند:
import java.rmi.*;
// رابط راه دور
public interface interEcho extends Remote{
public String echo(String msg) throws java.rmi.RemoteException;
}
بنابراین، در اینجا یک رابط interEcho را اعلام میکنیم که یک متد echo را به عنوان قابل دسترسی از راه دور اعلام میدارد. این متد ممکن است یک استثنا از کلاس RemoteException را پرتاب کند، کلاسی که تمام خطاهای مرتبط با شبکه را در خود گروه بندی میکند.
9.2.1.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 یک نمونه از این کلاس ایجاد کنیم. برای اینکه یک شیء یا سرویس از خارج قابل دسترسی باشد، باید در دایرکتوری اشیاء قابل دسترسی از راه دور ایجاد و ثبت شود. کلاینتی که میخواهد به یک شیء راه دور دسترسی پیدا کند، به شرح زیر عمل میکند:
- آنها با سرویس دایرکتوری روی ماشینی که شیء مورد نظر در آن قرار دارد تماس میگیرند. این سرویس دایرکتوری روی پورتی کار میکند که کلاینت باید آن را بشناسد (به طور پیشفرض ۱۰۹۹). کلاینت با مشخص کردن نام آن، یک مرجع برای یک شیء یا سرویس را از دایرکتوری درخواست میکند. اگر این نام با یک شیء یا سرویس در دایرکتوری مطابقت داشته باشد، دایرکتوری یک مرجع را به کلاینت بازمیگرداند که کلاینت میتواند از آن برای ارتباط با شیء یا سرویس راه دور استفاده کند.
- از آن نقطه به بعد، کلاینت میتواند از این شیء راه دور همانند یک شیء محلی استفاده کند
با بازگشت به سرور خود، باید یک شی از نوع srvEcho ایجاد کرده و آن را در دایرکتوری اشیاء قابل دسترسی از راه دور ثبت کنیم. این ثبتنام با استفاده از متد کلاس rebind از کلاس Naming انجام میشود:
با
name: نامی که باید با شیء راه دور مرتبط شود
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 ");
}
}// اصلی
}// پایان کلاس
با خواندن برنامه قبلی، ممکن است این تصور ایجاد شود که برنامه بلافاصله پس از ایجاد و ثبت سرویس اکو متوقف میشود. اینطور نیست. از آنجا که کلاس srvEcho از کلاس UnicastRemoteObject مشتق شده است، شیء ایجاد شده به طور نامحدود اجرا میشود: این شیء به درخواستهای کلاینت روی یک پورت ناشناس، یعنی پورتی که بسته به شرایط توسط سیستم انتخاب میشود، گوش میدهد. ایجاد سرویس بهصورت ناهمزمان است: در مثال، متد main سرویس را ایجاد میکند و به اجرا ادامه میدهد؛ در واقع عبارت «سرور اکو آماده است» را نمایش خواهد داد.
9.2.1.3. مرحله ۳: کامپایل کردن برنامه سرور
در این مرحله میتوانیم سرور خود را کامپایل کنیم. ما فایل interEcho.java را از رابط interEcho و همچنین فایل srvEcho.java را از کلاس srvEcho کامپایل میکنیم. این کار فایلهای متناظر .class را به ما میدهد: interEcho.class و srvEcho.class.
9.2.1.4. مرحله ۴: نوشتن کلاینت
ما یک کلاینت مینویسیم که URL را از سرور اکو بهعنوان پارامتر دریافت میکند، و که
- یک خط تایپشده روی صفحهکلید را میخواند
- آن را به سرور اکو ارسال میکند
- پاسخ ارسالشده توسط سرور را نمایش میدهد
- به مرحلهٔ ۱ بازمیگردد و زمانی که خط واردشده «end» باشد، متوقف میشود.
این منجر به کلاینت زیر میشود:
import java.rmi.*;
import java.io.*;
public class cltEcho {
public static void main(String arg[]){
// دستورالعمل: 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();
}// در حالی که
// تمام شد
System.exit(0);
// مدیریت خطا
} catch (Exception e){
System.err.println("Erreur : " + e);
System.exit(2);
}// سعی کنید
}// main
}//کلاس
در این کلاینت، به جز دستور درخواست ارجاع سرور، چیز غیرعادی خاصی وجود ندارد:
به یاد داریم که سرویس اکو ما با دستورالعمل زیر در دایرکتوری سرویسهای ماشینی که روی آن قرار دارد، ثبت شده بود:
Naming.rebind("srvEcho",serveurEcho);
بنابراین کلاینت نیز از متدی در کلاس Naming برای بهدستآوردن مرجع سروری که میخواهد استفاده کند، بهره میبرد. متد جستجو (lookup) مورد استفاده، URL سرویس درخواستشده را بهعنوان پارامتر میپذیرد. این URL به شکل یک URL استاندارد است:
که
rmi: اختیاری – پروتکل RMI
machine: نام یا آدرس IP ماشینی که سرور اکو روی آن در حال اجرا است – اختیاری، پیشفرض localhost.
port: پورت گوشدادن سرویس دایرکتوری روی این ماشین – اختیاری، پیشفرض 1099
nom_service: نامی که سرویس درخواستی تحت آن ثبت شده است (srvEcho در مثال ما)
آنچه بازگردانده میشود یک نمونه از رابط دور interEcho است. با فرض اینکه کلاینت و سرور روی یک ماشین نباشند، هنگام کامپایل کردن کلاینت cltEcho.java، فایل interEcho.class—که نتیجه کامپایل رابط راه دور interEcho است—باید در همان دایرکتوری موجود باشد؛ در غیر این صورت، خطای کامپایل در خطوطی که به این رابط ارجاع میدهند، رخ خواهد داد.
9.2.1.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
پس از کامپایل این دو فایل منبع، فایلهای زیر تولید میشوند:
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 که در حین کامپایل سرور ایجاد شده است:
پس از کامپایل فایل منبع، فایلهای زیر تولید میشوند:
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
در هر دو مورد، ماشین مجازی جاوا گزارش میدهد که نتوانسته کلاس srvEcho_stub را پیدا کند. در واقع، ما قبلاً هرگز از این کلاس چیزی نشنیدهایم. در کلاینت، سرور با استفاده از عبارت زیر مکانیابی شد:
در اینجا، urlservice رشته rmi://localhost/srvEcho با
Rmi: پروتکل RMI
Localhost: ماشینی که سرور روی آن در حال اجرا است – در این مورد، همان ماشینِ کلاینت. نحو آن معمولاً به شکل machine:port است. اگر پورت مشخص نشود، پورت 1099 به طور پیشفرض استفاده میشود. سرویس دایرکتوری سرور روی این پورت در حال گوش دادن است.
srvEcho: این نام سرویس خاصی است که درخواست میشود
هیچ خطایی در حین کامپایل گزارش نشد. فایل interEcho.class صرفاً باید روی رابط راه دور موجود باشد.
در زمان اجرا، اگر سرویس درخواستی srvEcho باشد، ماشین مجازی به وجود فایلی به نام srvEcho_stub.class نیاز دارد؛ به طور کلی، برای سرویس با نام X، فایلی با نام X_stub.class مورد نیاز است. این فایل تنها در زمان اجرا مورد نیاز است، نه در زمان کامپایل کلاینت. همین موضوع در مورد سرور نیز صدق میکند. پس دقیقاً این فایل چیست؟
در سمت سرور، کلاس srvEcho.class وجود دارد که شیء/سرویس از راه دور ما است. اگرچه کلاینت به این کلاس نیازی ندارد، اما برای برقراری ارتباط با آن به نوعی «تصویر» از آن نیاز دارد. در واقع، کلاینت درخواستهای خود را مستقیماً به شیء راه دور ارسال نمیکند: بلکه آنها را به تصویر محلی خود، srvEcho_stub.class، که در همان ماشین قرار دارد، ارسال میکند. این تصویر محلی، srvEcho_stub.class، با یک تصویر مشابه (srvEcho_stub.class) که روی سرور قرار دارد، ارتباط برقرار میکند. این تصویر از فایل .class روی سرور با استفاده از ابزار جاوایی به نام rmic ایجاد میشود. در ویندوز، دستور:
از فایل 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. مرحله ۶: اجرای اپلیکیشن اکو کلاینت-سرور
ما اکنون آمادهایم تا برنامهٔ کلاینت-سرور خود را اجرا کنیم. در ابتدا، کلاینت و سرور روی یک ماشین واحد اجرا خواهند شد. ابتدا باید برنامهٔ سرور خود را راهاندازی کنیم. به یاد داشته باشید که این:
- سرویس را ایجاد میکند
- آن را در دایرکتوری سرویسِ ماشینی که سرور اکو روی آن در حال اجراست، ثبت میکند
این نکته آخر مستلزم فعال بودن یک سرویس دایرکتوری است. این سرویس با استفاده از دستور زیر راهاندازی میشود:
rmiregistry سرویس دایرکتوری است. در اینجا، این سرویس با استفاده از دستور start در یک پنجره فرمان ویندوز در پسزمینه راهاندازی میشود. پس از فعال شدن دایرکتوری، میتوانیم سرویس اکو را ایجاد کرده و آن را در دایرکتوری سرویس ثبت کنیم. مجدداً، این سرویس با استفاده از دستور start در پسزمینه راهاندازی میشود:
سرور اکو در یک پنجره جدید، 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
بنابراین یک خطا وجود دارد: به نظر میرسد ماشین مجازی جاوا در حال درخواست فایل srvEcho_skel.class است که توسط ابزار rmic تولید شده اما تا به حال استفاده نشده بود. ما آن را مجدداً ایجاد کرده و به ماشین لینوکس نیز منتقل میکنیم:
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 نیاز داشته باشد. سپس سرور را روی ماشین ویندوز راهاندازی مجدد کردیم، در حالی که دو فایل 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 مورد نیاز بوده است. این فایل زمانی که کلاینت و سرور روی یک ماشین ویندوز قرار داشتند، ضروری بود. در لینوکس، آن را حذف میکنیم تا ببینیم چه اتفاقی میافتد...
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 virtual machine) سعی کرده کلاس بدنام stub را بارگذاری کند اما به دلیل عدم وجود «مدیر امنیتی» (security manager) ناموفق بوده است. به یاد داریم که در مستندات چیزی در این مورد دیده بودیم. دوباره بررسی میکنیم… و متوجه میشویم که سرور باید یک مدیر امنیتی ایجاد و نصب کند تا به کلاینتهایی که درخواست بارگذاری کلاس میکنند تضمین دهد که کلاسها امن هستند. در غیاب این مدیر امنیتی، بارگذاری کلاس غیرممکن است. به نظر میرسد این موضوع منطقی است: کلاینت لینوکس ما کلاس srvEcho_stub.class را که به آن نیاز داشت، از سرور درخواست کرد و سرور با بیان اینکه هیچ مدیر امنیتی نصب نشده است، این درخواست را رد کرد. بنابراین ما کد تابع main روی سرور را به شرح زیر اصلاح میکنیم:
//ایجاد سرویس
public static void main (String arg[]){
//نصب مدیر امنیتی
System.setSecurityManager(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 یک فایل متنی است که مجوزهای برنامه را تعریف میکند. در این مورد، به شرح زیر است:
برنامه در اینجا دارای مجوزهای کامل است.
بیایید دوباره شروع کنیم. به دایرکتوری سرور بروید و مراحل زیر را به ترتیب انجام دهید:
- سرویس دایرکتوری را راهاندازی کنید: j:\jdk12\bin\rmiregistry را اجرا کنید
- سرور را راهاندازی کنید: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
و این بار، سرور اکو (اما نه کلاینت) به درستی راهاندازی میشود. اکنون میتوانید موارد زیر را امتحان کنید:
- سرور اکو را متوقف کنید، سپس سرویس دایرکتوری را متوقف کنید.
- سرویس دایرکتوری را در حالی که در دایرکتوری متفاوتی نسبت به دایرکتوری سرور هستید، مجدداً راهاندازی کنید
- به دایرکتوری سرور بازگردید و سرور اکو را راهاندازی کنید – خطای زیر را دریافت خواهید کرد:
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
از این میتوان نتیجه گرفت که دایرکتوریای که سرویس دایرکتوری از آن راهاندازی میشود، اهمیت دارد. در این مورد، جاوا نتوانست کلاس 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 پروتکل فایل را مشخص میکند که پروتکل دسترسی به فایلهای محلی است، و دایرکتوری حاوی فایلهای .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[]){
// سینتکس: cltEcho پورت ماشین
// ماشین: ماشینی که سرور اکو روی آن اجرا میشود
// پورت: پورتی که سرویس دایرکتوری روی ماشین سرویس اکو روی آن کار میکند
// اعتبارسنجی آرگومان
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
//نصب مدیر امنیتی
System.setSecurityManager(new RMISecurityManager());
// گفتگوی کلاینت-سرور
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho 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/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://.., 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 را بازیابی کرده است.
تمام اینها ایدههایی به ما داده است و ما در حال بررسی هستیم که آیا کلاینت در حال اجرا روی سرور ویندوز نیز بدون فایل srvEcho_Stub.class کار خواهد کرد یا خیر. به دایرکتوری کلاینت میرویم، فایل srvEcho_Stub.class را در صورت وجود حذف میکنیم و کلاینت را به همان شیوهای که در لینوکس اجرا میشود، راهاندازی میکنیم:
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é
در سمت سرور ویندوز:
- سرور دارای یک مدیر امنیتی است
- با گزینههای زیر راهاندازی شد: start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
در سمت کلاینت لینوکس یا ویندوز
- کلاینت دارای یک مدیر امنیتی است
- در لینوکس، با دستور java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho اجرا شد
- در ویندوز، با دستور j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho اجرا میشد
9.2.1.9. سرور Echo روی لینوکس، کلاینتها روی ویندوز و لینوکس
اکنون سرور را به یک ماشین لینوکس منتقل کرده و کلاینتهای لینوکس و ویندوز را آزمایش میکنیم. رویه به شرح زیر است:
- فایلهای .class را از سرور به ماشین لینوکس کپی کنید
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 ماشین لینوکس قابل دسترسی باشد. در اینجا، 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 &
اکنون میتوانیم کلاینتها را آزمایش کنیم. ابتدا کلاینت ویندوز.
- به دایرکتوری کلاینت روی ماشین ویندوز بروید
- کلاینت را با استفاده از دستور زیر راهاندازی کنید:
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
کلاینت لینوکس را آزمایش کنید:
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
لطفاً توجه داشته باشید که برای کلاینت لینوکس که روی همان دستگاهی که سرور echo روی آن اجرا میشود، لازم نبود در URL برای سرویس درخواستی دستگاهی مشخص شود.
9.3. مثال دوم: سرور SQL روی یک ماشین ویندوز
9.3.1. مشکل
در فصل JDBC، ما به نحوهٔ مدیریت پایگاههای دادهٔ رابطهای پرداختیم. در مثالهای ارائهشده، برنامههای کاربردی و پایگاه داده مورد استفاده روی یک دستگاه ویندوز یکسان قرار داشتند. در اینجا پیشنهاد میکنیم یک سرور RMI را روی یک دستگاه ویندوز راهاندازی کنیم که به کلاینتهای راه دور امکان دسترسی به پایگاههای داده عمومی ODBC را روی دستگاه میزبان سرور میدهد.

کلاینت RMI میتوانست سه عملیات را انجام دهد:
- اتصال به پایگاهداده دلخواه
- ارسال پرسوجوهای SQL
- بستن اتصال
سرور پرسوجوهای SQL کلاینت را اجرا کرده و نتایج را به کلاینت بازمیگرداند. این وظیفه اصلی آن است، به همین دلیل ما آن را سرور SQL مینامیم.
ما مراحل مختلفی را که قبلاً با سرور echo دیدیم، اعمال میکنیم.
9.3.2. مرحله ۱: رابط راه دور
رابط راه دور، رابطی است که متدهای سرور RMI را که برای کلاینتهای RMI قابل دسترسی هستند، فهرست میکند. ما از رابط زیر استفاده خواهیم کرد:
import java.rmi.*;
//رابط راه دور
public interface interSQL extends Remote{
public String connect(String pilote, String url, String id, String mdp)
throws java.rmi.RemoteException;
public String[] executeSQL(String requete, String separateur)
throws java.rmi.RemoteException;
public String close()
throws java.rmi.RemoteException;
}
نقش روشهای مختلف به شرح زیر است:
اتصال: کلاینت به یک پایگاه دادهٔ راه دور متصل میشود و pilote خود را مشخص میکند، url و JDBC، و همچنین نام کاربری id و رمز عبور mdp برای دسترسی به این پایگاه داده. سرور یک رشته را که نتیجه اتصال را نشان میدهد، بازمیگرداند:
executeSQL: کلاینت درخواست میکند که پرسوجوی SQL روی پایگاه دادهای که به آن متصل است اجرا شود. این کلاینت کاراکتری را مشخص میکند که باید حوزهها را در نتایج بازگرداندهشده به آن جدا کند. سرور آرایهای از رشتهها را بازمیگرداند:
برای یک پرسوجوی بهروزرسانی پایگاه داده، که در آن n تعداد ردیفهای بهروزرسانیشده است
اگر پرسوجو خطایی ایجاد کرده باشد
اگر پرسوجو هیچ نتیجهای بازنگرداند
اگر پرسوجو نتایج را بازگردانده باشد. سطرهای بازگرداندهشده توسط سرور، سطرهای نتیجه پرسوجو هستند.
close: کلاینت اتصال خود را به پایگاه دادهٔ راه دور میبندد. سرور رشتهای را بازمیگرداند که نتیجهٔ این بستهشدن را نشان میدهد:
9.3.3. مرحله ۲: نوشتن سرور
کد منبع جاوا برای سرور 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 پایگاه داده از طریق درایور
// احراز هویت با استفاده از شناسه و رمز عبور
String resultat=null; // نتیجهٔ متد
try{
// بارگذاری درایور
Class.forName(pilote);
//درخواست اتصال
DB=DriverManager.getConnection(url,id,mdp);
// OK
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);
}// در حالی که
//پایان پردازش نتایج
//منابع آزاد میشوند
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);
}//اگر
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;
}
// ----------- اصلی
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 سرویس دایرکتوری که سرور SQL را ثبت کرده است RMI
راننده: رانندهای که سرور SQL باید برای مدیریت پایگاه داده از آن استفاده کند
urlBase: URL JDBC پایگاه دادهای که باید مدیریت شود
id: شناسهٔ مشتری یا null اگر شناسه وجود نداشته باشد
رمز عبور: رمز عبور مشتری یا null در صورت عدم وجود رمز عبور
جداساز: کاراکتری که سرور SQL باید برای جدا کردن فیلدها در سطرهای نتیجه یک پرسوجو استفاده کند
در اینجا مثالی از پارامترهای ممکن آورده شده است:
where:
نام RMI سرور SQL
رانر استاندارد برای پایگاههای داده با رابط ODBC
برای استفاده از پایگاه داده محصولی که در لیست پایگاه داده عمومی ODBC در ماشین ویندوز فهرست شده است
بدون شناسه
بدون رمز عبور
میدانها در نتایج با یک ویرگول از هم جدا خواهند شد
پس از راهاندازی با پارامترهای فوق، کلاینت مراحل زیر را دنبال میکند:
- آنها به سرور RMI srvSQL متصل میشوند، یعنی سروری به نام RMI روی همان دستگاهی که کلاینت روی آن قرار دارد
- آنها درخواست اتصال به پایگاه داده محصول را میدهند
- از کاربر میخواهد تا یک پرسوجو (SQL) را روی صفحهکلید تایپ کند
- آن را به سرور SQL ارسال میکند
- نتایج بازگرداندهشده توسط سرور را روی صفحه نمایش میدهد
- بار دیگر از کاربر میخواهد عبارت SQL را روی صفحهکلید تایپ کند. تا زمانی که عبارت کامل نشود، متوقف نخواهد شد.
کد کلاینت جاوا در ادامه آمده است. نظرات باید برای درک آن کافی باشند.
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 جداکننده درایور آدرس اینترنتی شناسه رمز عبور
//urlServiceAnnuaire: URL دایرکتوری خدمات RMI برای تماس
// راننده: رانندهای که برای دسترسی به پایگاه داده باید استفاده شود
// urlBase: URL JDBC پایگاه داده مورد استفاده
// id: شناسه کاربر
// رمز عبور: رمز عبور کاربر
// جداساز: رشتهای که فیلدها را در نتایج پرسوجو از هم جدا میکند
// بررسی تعداد آرگومانها
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. مرحله ۳: ایجاد فایلهای .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. مرحله ۴: آزمایش با سرور و کلاینت روی یک رایانه ویندوزی
- سرویس دایرکتوری در دایرکتوریای غیر از دایرکتوری سرور و کلاینت در حال اجرا است
- فایل `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
--> پرسوجو: انتخاب name، stock_actu از articles مرتبسازی بر اساس stock_actu صعودی
<-- 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. مرحله ۵: آزمایش با سرور روی یک ماشین ویندوز و کلاینت روی یک ماشین لینوکس
- در صورت لزوم، سرور و سرویس دایرکتوری را متوقف کنید
- فایلهای .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 QZXW2HTMLCc3VuLmpkYmMub2RiYy.KZGJjT2RiY0RyaXZlcgZQX jdbc:odbc:articles null null ,
--> پرسوجو: select name, stock_actu, stock_mini from articles order by name
<-- 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
--> پرسوجو: بهروزرسانی articles با شرط stock_mini<=7 به طوری که stock_mini=stock_mini
<-- 100 4
--> پرسوجو: select name, stock_actu, stock_mini from articles order by name
<-- 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. نتیجهگیری
این یک برنامهٔ جالب است که امکان دسترسی به یک پایگاه داده را از هر ایستگاه کاری در شبکه فراهم میکند. ما میتوانستیم بهراحتی آن را به روش سنتی با استفاده از ساکتها بنویسیم، که در واقع همان چیزی است که در یک تمرین فصل پایگاه دادهها مورد نیاز است. اگر این برنامه را به روش سنتی مینوشتیم:
- ما یک کلاینت و یک سرور خواهیم داشت که میتوان آنها را به زبانهای مختلف نوشت
- کلاینت و سرور از طریق تبادل خطوط متن با یکدیگر ارتباط برقرار میکردند و مکالمه آنها ممکن است چیزی شبیه به این باشد:
که دو پارامتر اول مشخص میکنند سرور را از کجا پیدا کنیم، و چهار پارامتر بعدی پارامترهای اتصال برای پایگاه داده مورد استفاده را نشان میدهند
سرور ممکن است با چیزی شبیه به این پاسخ دهد:
برای دستور دادن به سرور جهت اجرای پرسوجوی SQL روی پایگاه دادهای که به کلاینت متصل است. 'separator' کاراکتری است که برای جدا کردن فیلدها در خطوط پاسخ استفاده میشود.
سرور ممکن است با چیزی شبیه به این پاسخ دهد
برای یک پرسوجوی بهروزرسانی پایگاه داده، که در آن n تعداد سطرهای بهروزرسانیشده است
اگر درخواست خطایی ایجاد کرده باشد
اگر پرسوجو هیچ نتیجهای بازنگشت
اگر پرسوجو نتایج را بازگردانده باشد. خطوط بازگرداندهشده توسط سرور، نتایج پرسوجو هستند.
برای بستن اتصال به پایگاه دادهٔ راه دور. سرور ممکن است رشتهای را که نتیجهٔ این بستن را نشان میدهد، بازگرداند:
در اینجا میتوانیم ببینیم که اگر بتوانیم یک برنامهٔ سنتی را با استفاده از پروتکلی از نوع توصیفشده در بالا بسازیم، میتوانیم یک ساختار احتمالی برای سرور RMI استنتاج کنیم. جایی در پروتکل که پیامی از کلاینت به سرور به شکل زیر داریم:
ممکن است یک متد در داخل سرور RMI داشته باشیم
و بنابراین این متد که برای کلاینت قابل دسترسی است، باید بخشی از رابط منتشر شده سرور باشد.
در پایان، باید توجه داشت که سرور ما در حال حاضر تنها یک مشتری را مدیریت میکند: همانطور که در حال حاضر نوشته شده است، نمیتواند چندین مشتری را مدیریت کند. در واقع، اگر یک کلاینت به پایگاه دادهای به نام B1 متصل شود، یک شیء Connection (DB=DB1) توسط سرور ایجاد میشود. اگر یک کلاینت دوم درخواست اتصال به پایگاه دادهای به نام B2 را بدهد، سرور این را به صورت Connection DB=DB2 ثبت میکند و بدین ترتیب اتصال اولین کلاینت به پایگاه داده B1 را قطع مینماید.
9.4. تمرینها
9.4.1. تمرین ۱
سرور قبلی SQL را گسترش دهید تا بتواند چندین مشتری را مدیریت کند.
9.4.2. تمرین ۲
اپلت جاوا تجارت الکترونیک ارائهشده در تمرینهای فصل JDBC را طوری بنویسید که با سرور RMI از تمرین قبلی کار کند.