التطبيقات الشبكيه عاده تكون لنوعين من البرامج:
- الأول يهتم بكيفيه تبادل الملفات والبيانات بين الClient والـ Server ويتم التعامل مع هذا النوع باستخدام بروتكولات FTP,SMTP,HTTP وغيرها من البروتكولات.
- النوع الثاني من التطبيقات يهتم بكيفيه تشغيل برنامج أو داله في الجهاز الأخر مثل Telnet و Remote Procedure Call أختصارا RPC .
لغه جافا من اللغات الموجهه للكائنات OO ، وبالتالي سوف تطبق مفهوم RPC ولكن عن طريق الكائنات ، ومن هنا جائت تقنيات الـ Distributed Object والتي تسمح لي باستدعاء داله موجوده في كائن بعيد بدون الدخول في تفاصيل ارسال واستقبال البيانات …
والطريقه الأولى و التقليديه في كتابه برامج الشكبات Client-Server معروفه لدى الأغلب ، حيث يقوم العميل (مثلا طالب) بتعبئه Form ويرسل البيانات الى الخادم ، والذي يقوم بمعالجتها ويرجع النتيجه أو تخزينها للأستفاده منها لاحقا …
الشكل التالي بين Classical Client-Server Model :
لكي يقوم المبرمج ببرمجه برنامج مثل هذا عليه أن يقوم بتحديد عده امور مهمه :
- طريقه برمجه الـ Client-Server ، هل هم بالأعتماد على TCP-Socket أو UDP-Socket.
- كيفيه ارسال البيانات الى الجهه الأخرى بصوره مفهمومه للجهه الأخرى Formatted Understandable Data
- كيفيه عمل parse للرساله القادمه واستخلاص المعلومات منها ..
كل هذه الأمور على المبرمج أن يضعها في الأعتبار قبل و أثناء كتابه البرنامج ، والا فلن يعمل بالشكل المطلوب ..فمثلا لو قام الطالب Client بالضفط على زر Delete .. يجب أن تذهب هذه المعلومه الى الطرف الأخر بطريقه ما .. ويقوم الطرف الأخر بمعرفه هذه المعلومه والقيام بالوظيفه المطلوبه …ويرجع النتيجه بطريقه مفهومه يفهمها الكلاينت .
الذي نريده هنا هو ميكانيكه تسمح لي عمل نفس الـ Client-Server بدون الحاجه الى تلك الخطوات أعلاه ، فقط أقوم بالضغط على الزر ولا أهتم بكيفيه الأرسال ، ولا كيفيه فهم المعلومه ولا توجد حاجه لparse ولا غيره ، أيضا قد يكون الخادم مكتوب باللغه أخرى مثلا سي++. المشكله هنا أن الكائن الذي يقوم بهذا العمل غير موجود لدينا في الجهاز المحلي ..
الحل سوف يكون عن طريق عمل “واجهه تستطيع التعامل مع الخادم” Proxy في جهه العميل ، وعندما نضغط على ذالك الزر يقوم العميل باستدعاء الداله الموجوده في الـ Proxy .. وبعدها يقوم هذا الـ Proxy بالأتصال مع الخادم بطريقه يعرفها هو … بنفس الأمر السيرفر قد لا يعلم عن الكلاينت شيء ، لذلك يكون فيه Proxy هو الأخر يتخاطب مع الكلاينت .. أي أن الـ Server-Proxy يقوم بالتخاطب مع الـ client-Proxy .
أنظر الصوره التاليه لتبين لك :
هنا طريقه التخاطب بين تلك الـ Proxies قد يعتمد على التنقيه المستخدمه ، وأشهر ثلاث تقنيات هما :
RMI , CORBA , SOAP
RMI :
أختصار لـ Remote Method Invocation وتعتمد على فكره أستدعاء الكلاينت (يعمل في JVM) لداله في كائن بعيد موجود في الخادم (يعمل على JMV مختلفه) ، ويشترط أن يكون الخادم والعميل مكتوبين بجافا ، وتستخدم هذه التقنيه RMI Protocol لتطبيق التخاطب الذي يتعامل مع TCP Socket .وباستخدام RMI سوف نعطي العميل الشعور بأنه لا وجود لكائنات بعيده .. بالضبط كأنه يتعامل مع كائن محلى ، وهذه أحد ميزات الـ RMI .
CORBA :
أختصار لـ Comman Object Request Borker Architecture ..
وظيفتها نفس وظفيه الـ RMI وهي أستدعاء داله في كائن بعيد ، لكن يمكن أن يكون الكائن البعيد مكتوب بأي لغه لا يهم .. وتستخدم CORBA بروتكول يسمى Internet Inter-ORB Protocol واختصارا IIOP .
SOAP :
أختصار لـ Simple Object Access Protocol
وهي تقريبا نفس وظيفه CORBA ، ولكن تعتمد في عملها على ملفات XML .
CORBA و SOAP تعمل على أي لغه لا يشترط جافا ، وهنا سوف نستخدم لغه خاصه لوصف الدوال الموجوده في السيرفر والتي يستطيع الكلاينت التعامل معها .. في CORBA نستخدم لغه تسمى Interface Defenition Langauge واختصارا IDL (يمكن أن نستخدم RMI مع بروتكول IIOP ولن نحتاج الى IDL) . أما في SOAP نستخدم Web Services Description Language أختصارا WSDL . طبعا هذه ليست لغات كجافا أو سي ، لكنها فقط لغه لوصف الـ Interface الذي يستطيع أستخدامه الكلاينت …
طبعا أسهل هذه التقنيات هي RMI وهي موضوع حديثنا اليوم ، أما CORBA فهي تحتاج موضوع اخر لها هي والـSOAP والتي تستخدم عند العمل مع Web-Service .
ما هو مفهوم RMI
بنظره سريعه في أي كود RMI ، سوف نجد أنه يبعدنا تماما من التفاصيل ، فعندما نريد أن نستدعي داله موجوده في الخادم Server ، كل ما على أن أكتب سطر واحد وسأحصل على Reference لهذا الكائن . بعدها أستدعى الداله بشكل عادي كما لو أنها كانت Local . اذا كانت هناك قيمه مرسله Parameters و قيم راجعه return Value ، ستذهب بطريقه ما الى المكان الصحيح وبدون تدخل منك .. فعلا RMI تجعل الحياه أكثر سهوله .
أنظر للصوره التاليه والتي توضح هذه العمليه بشكل Abstract :
لندخل الى العمق أكثر ولنرى كيف يقوم RMI بارسال القيم المرسله والراجعه .. أولا عندما يقوم الكلاينت باستدعاء داله موجوده في كائن بعيد سوف يقوم client باستدعاء الداله stub الموجوده في Proxy لديه .. ويمرر لها المتغيرات المطلوب ارسالها .. بعد ذلك يبدأ الـ Stub بعمل معالجه لتلك المتغيرات حتى يرسلها .. فلن يرسلها هكذا .. هذه العمليه تسمى Parameter Marshalling .
في عمليه الـ Marshalling سوف ينظر أولا الى نوع المتغير أو الكائن ، فاذا كان متغير عادي Primitve Data Type فيقوم بارساله كما هو بالطبع بعد تحويله الى بايت Byte بترميز يعرف بـ Big-Endian ..
أما اذا كان المتغير هو كائن reference فلا يمكن أن يرسل بهذه الطريقه ، لأنه يمكن أن يحتوي على كائنات أخرى فيه وتلك الكائنات تحتوي أيضا على كائنات وهكذا . لذلك في عمليه ارسال الكائن سوف نقوم بترميزه بـ serialization وبالتالي تذهب البايتات بطريقه معينه متسلسله الى الجهه الأخرى .وعندما تصل الى الجهه الثانيه سوف يقوم بتجميع هذه البايتات بعمليه تدعى De-Serialization . لذلك في حاله التعامل مع الكائنات (قيم مرسله أو راجعه) يجب أن يكون الكائن لديك يطبق impelements الـ serialization .
الأن بعدما أنتهي الـ stub من عمليه Encoded سوف يرسل هذه البايتات الى الجهه الثانيه ، ويقوم الReceiver Object باستقبال هذه البايتات وتجميعها unmarshals the parameters وبعدها يقوم يتحديد الكائن المراد ثم استدعاء الداله المراده وفي حال كان هناك قيمه راجعه منها يقوم هذا الـ receiver object بعمل Marshals لهذه القيمه الراجعه ويعيدها الى Stub الذي يقوم بعمليه unmarshals للقيمه الراجعه ويرجعها الى الكلاينت الذي استدعى الـ Stub .
الصوره التاليه توضح عمليه Parameter Marshalling :
ربما تبدوا العمليه معقده وغير مفهومه ، لكن كما ذكرت لا حاجه للمبرمج بمعرفه تلك الخطوات ، لأن من ميزات RMI هي عمل أخفاء لكل هذه التفاصيل Good Tranceparency .
حطوات البرمجة RMI Programming Steps
هناك خطوات معينه للبرمجه في RMI سوف نذكرها هنا بشكل سريع وسوف تتضح بشكل أكبر عندما تنتاول الأمثله ان شاء الله .
أولا الخادم server :
عمل ملف Interface نضع فيه جميع الدوال التي نريد العميل أن يستدعيها .
نقوم بعمل ملف Impl للأنترفيس أعلاه ونقوم بتعريف الدوال التي صرحنا عنها .
نقوم بعمل ملف Server ونقوم بعمل Object من الملف Impl ونقوم بتسجيله في ملف يسمى registry باسم ما .
الأن الـ server جاهز لأتصال العميل واستدعاء الدوال من الكائن الموجود به .
ثانيا الكلاينت Client :
نقوم بالأتصال بملف registry ونطلب الأسم الذي كتبه السيرفر عند تسجيله للكائن .
سوف نحصل على ذلك الكائن ومنه نستدعي الداله المطلوبه .. (حتى يتضح لك سوف نحصل على Special Reference للكائن وعندما نستدعي الداله سوف يستدعي الداله الصحيحه وتنفذ هناك في السيرفر وترجع القيمه الراجعه لدينا) .
طبعا مع التعامل مع الـ Exception المناسبه .. وهي في حالتنا هذه RemoteException .. وهكذا تكون أصبحت خبير في RMI Programming.
مثال عملي Hello RMI World
نأخذ الأن مثال على RMI Programming .. ونبدأ بالمثال الشهير Hello World . نريد أن نكتب كائن يعمل في السيرفر يحتوي على داله ترجع Hello ، ويقوم العميل باستدعاء هذه الداله عن بعد ..
نبدأ بالبرمجه في جهه الخادم :
سوف نحتاج أولا الى Interface يحتوي على جميع الدوال التي نريد العميل استدعائها وهنا فقط سوف تكون لدينا داله واحده وهي طباعه ..هذا الـ Interface يجب أن يقوم بعمل وراثه من الكلاس Remote حتى يقوم كلاس أخر (يمثل الكائن البعيد) بعمل تطبيق لهذا الأنترفيس .. اضافه الى أن الدوال التي سنكتبها في هذا الـ Interface يجب أن تقوم بعمل Throws لنوع معين من الException هو RemoteException لأنه قد تحدث مشاكل اثناء استدعاء الداله مثلا قطع الأتصال مع الخادم أو انهيار بالشبكه أو أيه مشكله أخرى .. لذلك جميع الدوال سوف تتعامل مع هذا الـ Exception .
جميع الكلاسات الخاصه ببرمجه RMI موجوده في الباكج java.rmi .. لنرى الأن ملف Interface ولنطلق عليه Hello :
// Hello Remote Object Interface import java.rmi.Remote ; import java.rmi.RemoteException ; public interface Hello extends Remote { public String getHelloMessage() throws RemoteException ; }
أنتهت الخطوه الأولى ، الأن نقوم بكتابه كلاس يطبق هذا الـInterface ، بالاضافه الى الوراثه من UnicastRemoteObject وهو الكلاس الخاص بأمور الـ Marshalling وارسال واستقبال البيانات ..وهو موجود في الباكج java.rmi.server
لنرى ملف HelloImpl (جرت العاده أن تكون نهايه هذا الملف بـ Impl) :
// Hello Implementation import java.rmi.RemoteException ; import java.rmi.server.UnicastRemoteObject ; public class HelloImpl extends UnicastRemoteObject implements Hello { public HelloImpl () throws RemoteException { } public String getHelloMessage() throws RemoteException { return "Hello Distributed Computing" ; } }
الخطوه الثالثه وهي كتابه السيرفر Server ، وهنا سوف نقوم بعمل كائن من HelloImpl وندخل هذا الكائن في الـ registry ونعطيه أي اسم ما، وبالطبع الكلاينت يجب أن يكون لديه هذا الأسم لكي يحصل على الـ reference فيما بعد ..
الداله :
Naming.rebind(objectName,myObject);
هي التي تقوم باضافه سجل في ملف registry يحتوي على اسم الكائن وعنوانه Reference . طبعا هذه الخدمه Registry سو ف نقوم بتشغيلها قبل أن يعمل السيرفر حتى تتم اضافه هذا السجل فيه .. يمكنك عن طريق هذه الخدمه أن تسجل كائن باسم ما ، ويقوم الكلاينت فيما بعد بالأتصال بهذه الخدمه بالأسم المعين ليحصل على الكائن .. طبعا الأضافه في ملف Registry تتم فقط من جهه السيرفر .الكلاينت يطلب من الـ Registry كائن عن طريق الأسم ..
ملف Server ، لاحظ أنه بعد عمل rebind سوف نقوم بطباعه جمله تدل أن السيرفر يعمل الأن وفي حال اتصال …
// RMI Server import java.rmi.RemoteException ; import java.rmi.Naming ; import java.net.MalformedURLException ; public class Server { final static String HOST = "localhost" ; public static void main (String args[]) { try { HelloImpl myObject = new HelloImpl(); String objectName = "rmi://" + HOST + "/MyHello" ; Naming.rebind(objectName,myObject); System.out.println("Binding Complete ....."); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
الأن الى هنا أنتهينا من البرمجه في جهه الخادم .. نأتي الأن لجهه العميل ..
كل ما علينا في جهه الكلاينت الحصول على reference للكائن ، ومن ثم استدعاء تلك الداله ، فقط .. ويتم الحصول على الكائن من خلال الداله lookUp الموجوده في الكلاس Naming ..
Hello myObject = (Hello) Naming.lookup(objectName);
الأن بعد الحصول على reference للكائن سوف يكون نوعه Object ، لذلك نقوم بعمل cast الى النوع Hello .. ومن ثم نستدعي الداله بشكل عادي
// RMI Client import java.rmi.Naming ; import java.rmi.RemoteException ; import java.rmi.NotBoundException ; import java.net.MalformedURLException ; public class Client { public final static String HOST = "localhost" ; public static void main (String args[]) { try { String objectName = "rmi://" + HOST + "/MyHello" ; Hello myObject = (Hello) Naming.lookup(objectName); System.out.println(myObject.getHelloMessage()); } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
في البدايه وللتبسيط وقبل أن نرى موضوع RMI Deployment ، ضع كل الملفات في مجلد واحد ، نريد أن نختبر البرنامج في Local Machine .
الأن قم أولا بترجمه جميع الملفات:
javac *.java
الأن ستخرج لك 4 ملفات .class
قم بترجمه ملف HelloImpl.class باستخدام المترجم الخاص بـ rmi :
rmic HelloImpl
(من غير كتابه الأمتداد) ..
الناتج هنا سوف يكون ملف Stub باسم HelloImpl_Stub.class . هذا الملف يجب أن يتواجد عند الكلاينت حتى يعمل البرنامج بشكل صحيح هو والملف Hello.class (لأننا في الكلاينت سوف نقوم بعمل Cast لهذا النوع) .. لذلك عليك أن نتسخ هذين الملفات الى الكلاينت في حال كان مجلد الكلاينت في مكان أخر أو في جهاز ثاني .. حاليا انسى الكلام واستمر على أساس أنهم جميعهم في نفس المسار ، وبعد قليل نتناول هذا الموضوع بشكل أوسع .
الأن (في جهه السيرفر ، لكننا حاليا نعمل وجميعهم في جهاز واحد ) قم بتشغيل ملف rmiregistry .. وذلك من خلال سطر الأوامر أكتب :
rmiregistry
ولن يكون هناك مؤشر أو جمله طباعه تدل على أنه يعمل .. فقط ستجد أن title Bar لسطر الأوامر أصبح يحتوي على rmiregistry وهو دليل على عمل هذه الخدمه ..
الأن شغل الخادم في نافذه اوامر جديده :
java server
وستجد أن الجمله Binding Complete …..تم طباعتها على الشاشه ..وهكذا يكون السيرفر يعمل ، والخدمه registry تعمل ايضا .. لن تستطيع ايقافها الا بالضفط على CTRL+C .
أخيرا شغل ملف الكلاينت :
java Client
وستجد جمله Hello Distributed Computing امامك في الشاشه (حيث تم استدعاء الداله getHelloMessage في السيرفر ، والناتج من هذه الداله هذا الString الذي تم طباعته في الكلاينت ) ..
تأكد من فهم ماذا حصل ،، تم استدعاء الداله getHelloMessage الموجوده في السيرفر ، وانتفذت هناك والقيمه الراجعه رجعت للعميل ، الذي قام بطباعتها على الشاشه .. هذه هي RMI بمنتهى البساطه .
مثال أخر Bank Service
نأخذ مثال أخر لتأكيد فهم الموضوع ، على مثال لعميل يريد أن يدخل بياناته لدى البنك ، ويقوم بالبحث عن معلوماته من خلال الأسم .. هذه الدوال سوف تكون في السيرفر ويقوم الكلاينت باستدعاء هذه الدوال من بعد ، ويمرر لها المعاملات وترجع له الناتج ..
ملف BankAccount وهو يمثل حساب العميل في البنك .. لاحظ أنه يطبق الأنترفيس Serializable.. وهكذا لجميع الكائنات في RMI يجب أن تكون مطبقه لهذا الأنترفيس والى فلن تستطيع ارسالها
// Person Account import java.io.Serializable ; public class BankAccount implements Serializable { private String firstName ; private String lastName ; private double balance ; public BankAccount () { setFirstName(""); setLastName(""); setBalance(0.0); } public BankAccount (String fn , String ln , double balance) { setFirstName(fn); setLastName(ln); setBalance(balance); } public String getFirstName() { return firstName ; } public String getLastName () { return lastName ; } public double getBalance () { return balance ; } public void setFirstName (String firstName ) { this.firstName = firstName ; } public void setLastName (String lastName) { this.lastName = lastName ; } public void setBalance (double balance ) { this.balance = balance ; } }
ملف BankAccountInterface :
// Bank Account Remote Object Interface import java.rmi.Remote ; import java.rmi.RemoteException ; public interface BankAccountInterface extends Remote { public void insertEmployee(String firstName,String lastName,double balance) throws RemoteException ; public String getEmployee (String firstName , String lastName ) throws RemoteException ; }
ملف BankAccountImpl :
// Bank Account Implementation import java.rmi.RemoteException ; import java.rmi.server.UnicastRemoteObject ; import java.util.Vector ; public class BankAccountImpl extends UnicastRemoteObject implements BankAccountInterface { private Vector<BankAccount> dataBase = new Vector<BankAccount>(); public BankAccountImpl() throws RemoteException { } public void insertEmployee (String fn , String ln , double balance ) throws RemoteException { BankAccount tmp = new BankAccount(fn,ln,balance); dataBase.add(tmp); } public String getEmployee (String fn , String ln ) throws RemoteException { String result = "" ; for (int i=0 ; i<dataBase.size() ; i++) { BankAccount tmp = dataBase.elementAt(i) ; if ( tmp.getFirstName().equals(fn) && tmp.getLastName().equals(ln) ) result = tmp.getFirstName() + " " + tmp.getLastName () + " " + tmp.getBalance () ; } return result ; } }
ملف BankServer ولن يختلف عن السيرفر في المثال السابق :
// Bank Server import java.rmi.RemoteException ; import java.rmi.Naming ; import java.net.MalformedURLException ; public class BankServer { private static final String HOST = "localhost" ; public static void main (String args[]) { try { String myObjectName = "rmi://" + HOST + "/MyAccount" ; BankAccountImpl myObject = new BankAccountImpl(); Naming.rebind(myObjectName,myObject); System.out.println("Binding Complete ...."); } catch (RemoteException e) { e.printStackTrace() ; } catch (MalformedURLException e) { e.printStackTrace() ; } } }
ملف BankClient وسوف نقوم باستدعاء الدوال
// Bank Client import java.rmi.RemoteException ; import java.rmi.NotBoundException ; import java.rmi.Naming ; import java.net.MalformedURLException ; public class BankClient { private static final String HOST = "localhost" ; public static void main (String args[]) { try { String myObjectName = "rmi://" + HOST + "/MyAccount" ; BankAccountInterface myObject = (BankAccountInterface) Naming.lookup(myObjectName); myObject.insertEmployee("Wajdy","Essam",30444); myObject.insertEmployee("Ahmed","Ali",2324); myObject.insertEmployee("Sami","Essam",3131); System.out.println ( myObject.getEmployee("Wajdy","Essam") ); } catch (RemoteException e) { e.printStackTrace() ; } catch (NotBoundException e) { e.printStackTrace() ; } catch (MalformedURLException e) { e.printStackTrace() ; } } }
لتشغيل الملفات ، أكتب هذه الأوامر في سطر الأوامر ، وقد تحتاج الى 3 نوافذ أحدها لـ registry والأخرى للخادم والثالثه للعميل :
javac *.java
rmic BankAccountImpl
registry
java BankServer
java BankClient
واستمتع بالنتائج ..
في حال لاحظت المثالين السابقين ان جميع الملفات كانت في جهاز واحد ، طبعا الأمر غير مفيد بشكل كبير ، نحن نريد أن يكون كل من السيرفر والكلاينت يعمل في جهاز منفصل عن الأخر .. لذلك يجب أن نفصل ملفات السيرفر في جهاز وملفات الكلاينت في جهاز ..
لكن ما هي ملفات السيرفر -في المثال السابق- :
BankAccount.java
BankAccountImpl.java
BankAccountInterface.java
BankServer.java
وبعد الترجمه سوف يخرج لنا :
BankAccount.class
BankAccountInterface.class
BankAccountImpl.class
BankServer.class
BankAccountImpl_Stub.class
أما ملفات الكلاينت :
BankClient.java
ولن نستطيع ترجمه الكلاينت (الذي يعمل في جهاز منفصل ) والسبب أنه يحتوي على عمليه كاست من Object الى الكائن BankAccountInterface وهو لا يملكه ، أيضا كما ذكرنا أن الStub هو الذي يتعامل حقيقه مع السيرفر ونحن هنا لا نملك ملف BankAccountImpl_Stub.class .. لذلك يجب أن يتوفر للكلاينت هذين الملفين حتى يعمل …
السؤال الأن كيف يمكن أن نتقل ملفات class هذه الى الجهاز الثاني ؟
بالطبع يمكن أن تقوم بارسالها بشكل عادي ، أي تقوم بعمل نسخ ولصق لهذه الملفات في الكلاينت ، وهكذا كل شيء سيعمل بشكل سليم 100%،
لكن في حال كان الجهاز الثاني بعيد ، فلن نستطيع ذلك .. الحل هو تحميل هذه الكلاسات من مكان ما (web server ) وقت تشغيل العميل Loading Classes at Runtime .. أي أننا سنقوم بتحميل الملفات وقت تشغيل العميل ،
Dynamic code downloading
أحد ميزات لغه جافا هي أنك تستطيع تحميل كود Byte Code (ملفات .class) من موقع URL وتنفذه في جهازك … مثلا عندما تقوم بفتح Applet فإن الـ JVM الموجوده في متصفح الأنترنت تقوم أولا بتحميل الByte Code من السيرفر الى جهازك ، ومن ثم تبدأ في تنفيذه ..
RMI قامت بتطيق نفس الفكره ، حيث تستطيع باستخدام RMI API (وليس JVM كما في الأبليت) بتحميل الكلاسات الموجوده في الجهاز البعيد ، وهنا نحن نريد تحميل ملف Stub الذي يجب أن يكون موجود لدى الكلاينت حتى يعمل بشكل صحيح ..
وتستطيع تحميل الكلاسات من الجهاز البعيد عن طريق تحديد الموقع في CodeBase .. هذه الcodebase يمكن أن نعتبرها مصدر للكود ، ويقوم العميل عندما يبدأ في العمل بالذهاب الى هذا المكان وتحميل الكود ويبدأ العمل بعدها . ويكمن التعامل مع codebase في الأبليت أو الApplication العادي .
حاليا نريد تحميل كلاس Stub (واي كلاسات ثانيه في حال أحتاج العميل لذلك ، مثلا القيمه الراجعه من السيرفر كائن غير معروف لدي الكلاينت) ، هنا سنقوم بتحديد موقع الكلاسات عندما نشغل السيرفر .. وعندما يتصل الكلاينت بالسيرفر سوف يبحث لديه في classPath ، فاذا لم يجد الكلاسات فسوف يذهب الى ذلك المكان ويقوم بتحميل الكلاسات التي يحتاجها ..
يقوم السيرفر بتحديد موقع الكلاسات عن طريق :
نقوم بكتابتها وقت تشغيل السيرفر ، بالمعامل -D .. لا تنسى / في الأخر ..
الصور التاليه ، تشرح طريقه العمل بشكل مبسط وجيد …
هنا الصوره السابقه تشرح كيف يقوم الأبليت بتحميل ملفات الكلاسات التي يحتاجها ثم يقوم بتنفيذها .
هنا نوضح الصوره السابقه الكثير من الأمور ، تبدأ بتحديد الخادم موقع ملفات الكلاسات ، بعدها (الخطوه1) يقوم بتسجل اسم الكائن ، بعدها يقوم الكلاينت بأخذ الكائن الراجع بعد طلبه ، ويقوم بتحميل ملف Stub من المكان الذي حدده الخادم ويعود للكلاينت ومنه في الصوره القادمه) يقوم باستدعاء الداله البعيده ..
طبعا يمكن أن يرسل الكلاينت كائن غير معروف للسيرفر ، هنا في هذه الحاله سوف يضطر الكلاينت أن يحدد للسيرفر موقع الكلاس لهذا الكائن حتى يقوم بتحميله ، يعني أي جهه تحدد للأخرى موقع تحميل الكلاسات للكائنات التي لا تعرفها الجهه … الصوره التاليه توضح ذلك .
هناك مسأله بسيطه وهي أن الكلاينت (في Application ) لا يستطيع تحميل الكلاسات بشكل مباشر لأنها قد تحتوي على كود خبيث -هي من أعدادات اللغه الأفتراضيه- ، لذلك يجب أن تحدد له ذلك ، وهو موضوع RMI Security .
RMI Security
في حال كانت جميع الكلاسات متوفره في الجهازين ، فلن توجد مشكله لأننا لن نحمل أي كود من أي جهه .. لكن في حال أحتاج الكلاينت لكلاس من السيرفر غير موجود في الكلاينت ، سوف يرسل له السيرفر هذا الكلاس ولكن سوف يبدأ هذا الكائن بالعمل مباشره بدون تدخل منك فورا بعد عمليه Deserialization .. وبالطبع مثل هذا قد يمثل خطوره لذلك لغه جافا بالوضع الأفتراضي لها لا تسمح لك بهذا (ما عدا في الأبليت) .
المسؤول عن تحميل هذه الكلاسات هو SecureClassLoader ويجب أن نحدد لهذا الكائن حدود عمله restrictions موجوده في java.policy ، ويمكن أن نغير فيها عن طريق الأداه policytool .. أيضا هناك الكائن RMISecurityManager (وهو أبن لـ SecurityManager) ويمكن عن طريقه أن نتحكم بـ security policy . وهو الذي يسمح في الحقيقه للـ Applet بالعمل وتحميل الملفات، لذلك لكي نستطيع تحميل الكلاسات يجب أن نقوم بعمل كائن من RMISecurityManager .
اذا يمكننا أن نتحكم بـ security policy عن طريق الأداه policytool أو عن طريق العمل مع الكائن RMISecurityManager .. لكن العمل المباشر مع هذا الكائن لن يفيد كثيرا لأنه يعتمد على الأعدادات الأفتراضيه في security policy ، لذلك يمكن أن ننشيء Security Manager خاص بنا يسمح بكل شيء وهو عن طريق وراثه الكلاس RMISecurityManager واعاده تعريف الداله checkPermission ..
مثال :
// Zero security Manager import java.security.Permission ; import java.rmi.RMISecurityManager ; public class ZeroSecurityManager extends RMISecurityManager { public void checkPermission ( Permission permission ) { System.out.println("Check Permission for : " + permission.toString()); } }
الأن عندما يبدأ الكلاينت العمل يجب أن نحدد له ما يفعله عن طريق الداله setSecurityManager ونمرر له الكائن SecurityManager أو أي Subclass منه .
Fibonacci Computing
المثال الأخير (شامل لكل المفاهيم ان شاء الله) وهو لسيرفر لديه داله تحسب رقم الحد الفلاني في سلسله Fibonacci الشهيره .. ونريد بأن يعمل على جهازين منفصلين ، وقد تمت تجربته بين نظام windows Server2003 و Windows Xp ..
الغرض من البرنامج هو أن يقوم العميل باستدعاء داله حساب Fibonacci في السيرفر ويمرر لها رقم ، وترجع هذه الداله الناتج ويطبع لدى الكلاينت .. نريد أن يكون الكلاينت مره Application ومره Applet ..
نبدأ من جهه الخادم :
سوف يكون لديه ثلاثه ملفات :
Fibonacci هو الأنترفيس
FibonacciImpl هو تطبيق لهذا الأنترفيس
FibonacciServer هو ملف السيرفر
ملف Fibonacci.java :
import java.rmi.RemoteException ; import java.rmi.Remote ; import java.math.BigInteger ; public interface Fibonacci extends Remote { public BigInteger getFibonacci(int number) throws RemoteException ; public BigInteger getFibonacci(BigInteger bi) throws RemoteException ; }
ملف FibonacciImpl.java :
// Fibonacci Implementation import java.rmi.server.UnicastRemoteObject ; import java.rmi.RemoteException ; import java.math.BigInteger ; public class FibonacciImpl extends UnicastRemoteObject implements Fibonacci { public FibonacciImpl () throws RemoteException { } public BigInteger getFibonacci (int number ) throws RemoteException { return this.getFibonacci( new BigInteger("" + number) ) ; } public BigInteger getFibonacci (BigInteger bInt) throws RemoteException { BigInteger zero = new BigInteger("0"); BigInteger one = new BigInteger("1"); if (bInt.equals(zero)) return one; if (bInt.equals(one)) return one; BigInteger i = one; BigInteger low = one; BigInteger high = one; while (i.compareTo(bInt) == -1) { BigInteger temp = high; high = high.add(low); low = temp; i = i.add(one); } return high; } }
ملف FibonacciServer.java :
// Fibonacci Server import java.rmi.Naming ; import java.rmi.RemoteException ; import java.net.MalformedURLException ; public class FibonacciServer { private static final String HOST = "yourHost"; public static void main (String args[]) { try { FibonacciImpl myObject = new FibonacciImpl(); String myObjectName = "rmi://" + HOST + "/MyFib" ; Naming.rebind(myObjectName,myObject); System.out.println("Server Ready to Word ......"); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
نقوم في السيرفر بترجمه هذه الملفات :
javac *.java
ونقوم بتفح مترجم rmi :
rmic FibonacciImpl
الأن خرجت الملفات :
Fibonacci.class
FibonacciImpl.class
FibonacciServer.class
FibonacciImpl_Stub.class
نقوم الأن بنقل ملفات الـ Class التي سوف يقوم الكلاينت بتحميلها من هناك .. ننقلها في web-Server .. بعدها أحذف ملف FibonacciImpl_Stub.class من المجلد الحالي ، فقط دع هذا الملف في الويب سيرفر ، حتى عندما نشغل السيرفر يتصل بهذا الملف ..
ونشغل ملف registry :
rmiregistry
وأخيرا نشغل الخادم :
java – Djava.rmi.server.codebase=http://MySite/file/ FibonacciServer
الأن نبدأ العمل في الكلاينت :
سوف يكون لديه ثلاثه ملفات :
Fibonacci.java هو الأنترفيس
ZeroSecurityManager.java
FibonacciClient.java
ملف الأنترفيس هو نفسه الذي في السيرفر ويجب أن يكون هنا ، حتى يتم عمل Compiler للـFibonacciClient.java .
ملف ZeroSecurityManager.java عرضنا كوده في الأعلى ..
FibonacciClient.java :
import java.rmi.RemoteException ; import java.rmi.Naming ; import java.rmi.NotBoundException ; import java.net.MalformedURLException ; public class FibonacciClient { private static final String HOST = "ServerHost"; public static void main(String args[]) { try { if ( System.getSecurityManager() == null ) System.setSecurityManager(new ZeroSecurityManager()); String myObjectName = "rmi://" + HOST + "/MyFib" ; Fibonacci myObject = (Fibonacci) Naming.lookup(myObjectName); System.out.println( myObject.getFibonacci(34) ) ; System.out.println( myObject.getFibonacci(55) ) ; System.out.println( myObject.getFibonacci(155) ) ; } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
الأن :
ترجم الملفات الثلاثه :
javac *.java
وشغل الكلاينت :
java FibonacciClient
وسترى المخرج أخييييرا على الشاشه .
في حال أردنا العمل على Applet ، نكتب ملف AppletFibonacci.java ونضعه في السيرفر ، ونترجمه ، وننقل ملف الكلاس الى الويب سيرفر مع بقيه الكلاسات .. بالأضافه الى صفحه html يتصل من خلالها العميل ..
AppletFibonacci.java
import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.rmi.*; import java.math.BigInteger; public class AppletFibonacci extends Applet { private TextArea resultArea = new TextArea("", 20, 72, TextArea.SCROLLBARS_BOTH); private TextField inputArea = new TextField(24); private Button calculate = new Button("Calculate"); private String server; public void init( ) { this.setLayout(new BorderLayout( )); Panel north = new Panel( ); north.add(new Label("Type a non-negative integer")); north.add(inputArea); north.add(calculate); this.add(resultArea, BorderLayout.CENTER); this.add(north, BorderLayout.NORTH); Calculator c = new Calculator( ); inputArea.addActionListener(c); calculate.addActionListener(c); resultArea.setEditable(false); server = "rmi://" + this.getCodeBase( ).getHost( ) + "/MyFib"; } class Calculator implements ActionListener { public void actionPerformed(ActionEvent evt) { try { String input = inputArea.getText( ); if (input != null) { BigInteger index = new BigInteger(input); Fibonacci f = (Fibonacci) Naming.lookup(server); BigInteger result = f.getFibonacci(index); resultArea.setText(result.toString( )); } } catch (Exception ex) { resultArea.setText(ex.getMessage( )); } } } }
HTML Page :
<html> <head> <title>RMI Applet</title> </head> <body> <h1>RMI Applet</h1> <p> <applet align="center" code="AppletFibonacci" width="300" height="100"> </applet> </p> </body> </html>
بعد ترجمه AppletFibonacci ضع ملف الكلاس في السيرفر هو وصفحه الـ HTML ، وسوف تتم العمليه بنجاح ..
الأن ترجم في السيرفر :
java FibonacciServer
لا داعى لوجود codebase لأن الأبليت سوف يحمل الملفات بنفسه ..
وفي الكلاينت أدخل على صفحه HTML الموجوده بالسيرفر من خلال متصفح الأنترنت
الى هنا نكون أنهينا الموضوع وليس بشكل كامل ، فموضوع الـ security موضوع متداخل كثيرا وبه الكثير من الأمور الغامضه .. اضافه الى أن تحميل كود من الجهه الثانيه غير مضمون كثيرا ، فقد تحصل مشاكل مثلا قطع الأتصال أو مشكله في اعدادك للبرنامج ، لذلك اذا استطعت نقل الكلاسات يدويا فهذا أفضل بكثير ويخرجك من جميع هذه المتاهات .
المصادر
Dynamic code downloading using RMI
Java Network Programming, 3rd Edition
Introduction to Network Programming in java
Core Java 2 , Part II
جميل جميل. نقل الكلاسات عبر الـ codebase، و CORBA-IIOP-IDL معلومات جديد بالنسبة لي. شكراً وجدي.
لدي بعض الملاحظات هنا:
1- ابتداءً من جافا 5، يمكن الاستغناء عن rmic كما هو مذكور في هذه الصفحة: http://docs.oracle.com/javase/tutorial/rmi/overview.html
2- الـ port الافتراضي الذي يستخدمه الـ rmiregistry هو 1099، ومن الممكن تغييره.
3- في المثال الأول، يمكن للكلاس HelloImpl يرث كلاس آخر غير UnicastRemoteObject، ولكن في هذه الحالة يجب أن يطبق Serializable ويتم تغيير كود السيرفر إلى:
;()Hello myObject = new HelloImpl
;(Hello stub = (Hello) UnicastRemoteObject.exportObject(myObject, 0
;”String objectName = “rmi://” + HOST + “/MyHello
;(Naming.rebind(objectName, myObject
4- عند تشغيل الـ rmiregistry، يجب أن يكون تعريف جميع الـ interface موجود في الـ CLASSPATH الخاص بالـ rmiregistry’s session حتى تتجنب الخطأ ClassNotFoundException.
5- يمكن تجاهل النقطة السابقة باستخدام embedded RMI registry، مثال:
;(Registry registry = LocateRegistry.createRegistry(1099
;(registry.rebind(name, obj
6- استخدم JApplet بدلاً من Applet.
7- بالنسبة لطرق التخاطب الثلاثة اللتي ذكرتها، ألا يعتبر الـ Rest من ضمنها؟
مجهود رائع يا وجدي 🙂
رااائع جدا ماشاء الله شرح مبسط وواضح. .
شرح راااائع جزاك الله كل خير
ممتاااااز