Οι εφαρμογές Java RMI συνήθως αποτελούνται από δύο ξαχωριστά προγράμματα, το διακομιστή και τον πελάτη. Ένα τυπικό πρόγραμμα διακομιστή δημιουργεί τα απομακρυσμένα αντικείμενα, διαμορφώνει τη πρόσβαση σε αυτά τα αντικείμενα και περιμένει αιτήσεις πρόσβασης προς τα αντικείμενα από πελάτες. Ένα τυπικό πρόγραμμα πελάτη αποκτά πρόσβαση σε απομακρυσμένα αντικείμενα κάποιου διακομιστή και στη συνέχεια καλεί μεθόδους που εφαρμόζονται στα αντικείμενα αυτά για να υλοποιήσει την εφαρμογή του. Το RMI παρέχει το μηχανισμό για την επικοινωνία μεταξύ αυτών των προγραμμάτων. Οι εφαρμογές αυτές λέγονται και εφαρμογές κατανεμημένων αντικειμένων (distributed object application).Μια εφαρμογή κατανεμημένων αντικειμένων πρέπει να μπορεί να κάνει τα παρακάτω:
Η εικόνα που ακολουθεί δείχνει μια εφαρμογή κατανεμημένων αντικειμένων που χρησιμοποιεί το μητρώο RMI για να εντοπίσει ένα απομακρυσμένο αντικείμενο. Πρώτα ο RMI διακομιστής (server) καλεί το RMI μητρώο (registry) για να καταχωρίσει (bind) ένα απομακρυσμένο αντικείμενο με ένα όνομα. Κατόπιν ο RMI πελάτης (client) αναζητά το απομακρυσμένο αντικείμενο με το όνομά του στο RMI registry και καλεί μια μέθοδο που εφαρμόζεται στο αντικείμενο. Στην εικόνα ακόμη βλέπουμε οτι το RMI μπορεί να χρησιμοποιήσει υπάρχοντες web servers στο πελάτη ή και το διακομιστή για να φορτώσει ορισμούς κλάσεων, αν αυτό απαιτείται.
- Εντοπισμός απομακρυσμένων αντικειμένων. Παρέχονται μηχανισμοί αναφορές σε απομακρυσμένα αντικείμενα. Για παράδειγμα, μια εφαρμογή μπορεί να καταχωρίσει τα απομακρυσμένα αντικείμενα που διαθέται μέσω του μητρώου RMI, που είναι μια απλή υπηρεσία ονομασίας. Εναλλακτικά μια εφαρμογή μπορεί να περάσει ή να επιστρέψει αναφορές σε απομακρυσμένα αντικείμενα στα πλαίσια μιας κλήσης άλλης απομακρυσμένης μεθόδου.
- Επικοινωνία με απομακρυσμένα αντικείμενα. Οι λεπτομέρειες της επικοινωνίας με τα απομακρυσμένα αντικείμενα αναλαμβάνονται από το RMI. Για τον προγραμματιστή, η επικοινωνία φαίνεται σαν μια κανονική κλήση μεθόδου.
- Φόρτωση ορισμών κλάσεων. Το RMI παρέχει μηχανισμούς για το φόρτωμα ορισμών των κλάσεων και των δεδομένων των αντικειμένων που μεταφέρονται.
Ένα από τα κεντρικά και μοναδικά χαρακτηριστικά του RMI είναι η δυνατότητά του να μεταφορτώνει τον ορισμό της κλάσης ενός αντικειμένου αν αυτή η κλάση δεν ορίζεται στην JVM του παραλήπτη. Όλοι οι τύποι και οι συμπεριφορές ενός αντικειμένου, όπως ήταν διαθέσιμη σε μια JVM, μπορούν να μεταφερθούν σε μια άλλη JVM. Με αυτό το τρόπο η συμπεριφορά μιας εφαρμογής μπορεί να τροποποιηθεί δυναμικά, εισάγοντας χαρακτηριστικά από άλλο σύστημα.
Όπως όλες οι εφαρμογές Java, μια κατανεμημένη εφαρμογή σε Java RMI κτίζεται με διεπιφάνειες και κλάσεις. Οι διεπιφάνεις ορίζουν μεθόδους. Οι κλάσεις υλοποιούν τις μεθόδους που δηλώθηκαν στις διεπιφάνειες, και, πιθανώς ορίζουν και άλλες μεθόδους. Τα αντικείμενα ανήκουν σε κλάσεις, δηλαδή σχετίζονται με συγκεκριμένες μεθόδους μέσω συγκεκριμένων διεπιφανειών. Σε μια κατανεμημένη εφαρμογή, μερικές υλοποιήσεις κλάσεων μπορεί να βρίσκονται σε μια JVM αλλά κάποιες άλλες όχι. Τα αντικείμενα που σχετίζονται με μεθόδους που καλούνται από απομακρυσμένες JVMs λέγονται απομακρυσμένα αντικείμενα.Ένα αντικείμενο γίνεται απομακρυσμένο υλοποιώντας μια απομακρυσμένη διεπιφάνεια (remote interface), με τα παρακάτω χαρακτηριστικά:
- Η απομακρυσμένη διεπιφάνεια
extends java.rmi.Remote
.- Κάθε μέθοδος της απομακρυσμένης διεπιφάνειας δηλώνει
java.rmi.RemoteException
στη δομήthrows
, πέρα από τις ειδικές εξαιρέσεις της εφαρμογής.Το RMI αντιμετωπίζει τα απομακρυσμένα αντικείμενα διαφορετικά από τα τοπικά. Αντί να στείλει ένα αντίγραφο της υλοποίησης απο τη μια JVM στην άλλη, αυτό που στέλνει έιναι ένα στέλεχος (stub) του απομακρυσμένου αντικειμένου. Το στέλεχος λειτουργεί ώς τοπικός αντιπρόσωπος, ή πληρεξούσιος (proxy), του απομακρυσμένου αντικειμένου, και στην ουσία σε αυτό αναφέρεται το πρόγραμμα πελάτης. Ο πελάτης καλεί μια μέθοδο στο αντικείμενο-πληρεξούσιο, που είναι υπεύθυνο να εκτελέσει την κλήση στο πραγματικό απομακρυσμένο αντικείμενο.
Το στέλεχος μπορεί να εκτελέσει μόνο τις μεθόδους που ορίζονται στην απομακρυσμένη διεπιφάνεια και όχι άλλες μεθόδους που πιθανά είναο διαθέσιμες στο απομακρυσμένο αντικείμενο αλλά δεν έχουν δηλωθεί στην απομακρυσμένη διεπιφάνεια.
Η ανάπτυξη εφαρμογών με RMI περιλαμβάνει 4 βήματα:
- Υλοποίηση των προγραμμάτων διακομιστή και πελάτη. Από τη πλευρά του διακομιστή η εφαρμογή περιλαβάνει τον ορισμό της απομακρυσμένης διεπιφάνειας και την υλοποίηση των κλάσεων των απομακρυσμένων αντικειμένων.
- Μεταγλώττιση του πηγαίου κώδικα. Αρκεί η χρήση του
javac
compiler. Πριν τη 5η εκδοση της Java έπρεπε να χρησιμοποιηθεί και οrmic
compiler για ορισμένες κλάσεις.- Οι ορισμοί των απομακρυσμένων διεπιφανειών και οι κλάσεις των απομακρυσμένων αντικειμένων γίνονται γνωστές μέσω διακομιστή ιστού.
- Εκκίνηση του μητρώου RMI, του διακομιστή και πελάτη.
Στο πυρήνα της μηχανής υπολογισμών βρίσκεται ένα πρωτόκολλο για την υποβολή, εκτέλεση και επιστροφή των υπολογιστικών εργασιών. Το πρωτόκολλο αυτό εκφράζεται μέσω της απομακρυσμένης διεπιφάνειας.
Η μηχανή υπολογισμού δεν 'γνωρίζει' εκ των προτέρων την υπολογιστική εργασία που θα εκτελέσει. Ο πελάτης καθορίζει την υπολογιστική εργασία. Ο διακομιστής διαθέτει την υπολογιστική του υποδομή για την εκτέλεση της εργασίας και την επικοινωνιακή του υποδομή για την επικοινωνία με το πελάτη.
Η απομακρυσμένη διεπιφάνεια ορίζεται στο
:
compute.Compute
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
<T> T executeTask(Task<T> t) throws RemoteException;
}Η διεπιφάνεια
Compute
ορίζεται ως απομακρυσμένη, δηλαδή μπορεί να κληθεί από άλλη JVM, μέσω της δήλωσηςextends java.rmi.Remote
. Οποιοδήποτε αντικείμενο υλοποιεί αυτή τη διεπιφάνεια ορίζεται ως απομακρυσμένο αντικείμενο.Η μέθοδος
executeTask
είναι η μοναδική μέθοδος-μέλος της απομακρυσμένης διεπιφάνειας, άρα είναι μια απομακρυσμένη μέθοδος. Γι' αυτό το λόγο δηλώνεταιthrows java.rmi.RemoteException
. Αυτή η εξαίρεση προκαλείται από το RMI αν κατά την κλήση απομακρυσμένης μεθόδου προκύψει σφάλμα επικοινωνίας.Η μέθοδος
executeTask
στη διεπιφάνειαCompute
υλοποιεί τη διεπιφάνειαcompute.Task
. Η διεπιφάνειαουσιαστικά ορίζει ένα γενικευμένο τύπο υπολογιστικής εργασίας που θα εκτελέσει ο διακομιστής. Η μοναδική μέθοδος-μέλος της διεπιφάνειας είναι η
compute.Task
:
execute
package compute;
public interface Task<T> {
T execute();
}H μέθοδος
execute
δεν έχει παραμέτρους εισόδου-εφαρμόζεται απλά επί ενός αντικειμένου-και δεν προκαλεί εξαιρέσεις. Σημειώστε οτι η διεπιφάνεια
δεν έχει δήλώση
compute.Task
extends java.rmi.Remote
, επομένως δεν ορίζεται ως απομακρυσμένη. Αυτό σημαίνει οτι δεν είναι άμεσα ορατή από την εφαρμογή του πελάτη, και δε χρειάζεται να δηλωθεί μεthrows
java.rmi.RemoteException
.
Η διεπιφάνεια
Task
δέχεται μια παράμετρο γενικευμένου τύπου, τηT
, που είναι ο τύπος που επιστρέφει ο υπολογισμός. Η μέθοδοςexecute
επιστρέφει την αποτέλεσμα τύπουT
.Με τη σειρά της, η μέθοδος
Compute
.executeTask
δέχεται ως παράμετρο ένα αντικείμενο που υλοποιεί τη διεπιφάνειαTask
καθώς και ένα στιγμιότυπο του γενικευμένου τύπου επιστροφής<Τ>
.Ένα απομακρυσμένο αντικείμενο που υλοποιεί την διεπιφάνεια
Compute
μπορεί να εκτελέσει οποιαδήποτε υπολογιστική εργασία αρκεί να υλοποιείται μέσω κλάσεων που υλοποιούν τη διεπιφάνειαTask
. Οι κλάσεις αυτές μπορεί να περιέχουν οποιαδήποτε δεδομένα και μεθόδους είναι απαραίτητα για την εκτέλεση της υπολογιστικής εργασίας.
Επειδή τα αντικείμενα που υλοποιούν τη διεπιφάνεια
Task
είναι γραμμένα σε Java, οι υλοποήσεις τους μπορούν να μεταφορτωθούν από τη JVM του πελάτη στη JVM του διακομιστή, δηλαδή της 'υπολογιστικής μηχανής'. Με αυτό το τρόπο οι πελάτες μπορούν να 'μάθουν' στο διακομιστή νέες υπολογιστικές εργασίες, οι οοίες δεν έχουν ρητά εγκατασταθεί στο διακομιστή, αλλά μεταφέρονται κατά την εκτέλεση της εφαρμογής RMI.
Το RMI μεταφέρει τις τιμές των απομακρυσμένων αντικείμενων (pass by value). Γι' αυτό το σκοπό χρησιμοποιεί τον μηχανισμό σειριοποίησης αντικειμένων (object serialization) της Java. Ένα αντικείμενο θεωρείται σειριοποιήσιμο αν η κλάση του δηλωθεί με
implements java.io.Serializable
. Επομένως οι κλάσεις που θα υλοποιήσουν τη διεπιφάνειαTask
θα πρέπει να υλοποιούν και τη διεπιφάνειαSerializable
.
Εδώ θα συζητήσουμε την υλοποίηση της κλάσης της υπολογιστικής μηχανής. Γενικά, μια κλάση που υλοποιεί μια απομακρυσμένη διεπιφάνεια πρέπει να κάνει τουλάχιστο τα εξής:
- Δήλωση των απομακρυσμένων διεπιφανειών
- Ορισμός ενός κατασκευαστή για κάθε απομακρυσμένο αντικείμενο
- Υλοποίηση των απομακρυσμένων μεθόδων-μελών των απομακρυσμένων διεπαφών
Ένας διακομιστής RMI πρέπει να δημιουργήσει τα απομακρυσμένα αντικείμενα, να τα εξάγει (export) στο περιβάλλον εκτέλεσης και να ενημερώσει το μητρώο RMI, ώστε να μπορούν να δεχτούν απομακρυσμένες κλήσεις. Οι λειτουργίες αρχικοποίησης αυτή μπορεί είτε να περιλαμβάνεται στην υλοποίηση του απομακρυσμένου αντικειμένου είτε να αποτελεί ξεχωριστή διαδικασία. Η διαδικασία αρχικοποίησης πρέπει να κάνει τα ακόλουθα:
- Δημιουργία και εγκατάσταση διαχειριστή ασφάλειας
- Δημιουργία απομακρυσμένων αντικειμένων
- Εξαγωγή και καταχώρηση απομακρυσμένων αντικειμένων
Η κλάση
υλοποιεί την απομακρυσμένη διεπιφάνεια
engine.ComputeEngine
Compute
και περιλαμβάνει και τη διαδικασία αρχικοποίησηmain
της 'υπολογιστικής μηχανής' :
package engine;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;
public class ComputeEngine implements Compute {
public ComputeEngine() {
super();
}
public <T> T executeTask(Task<T> t) {
return t.execute();
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine, 0);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}Ο κώδικας συζητείται στις παραγράφους που ακολουθούν.
Δήλωση των Απομακρυσμένων Αντικειμένων που Υλοποιούνται
Η κλάση υλοποίησης της 'υπολογιστικής μηχανής' δηλώνεται ως:public class ComputeEngine implements ComputeΗ κλάση υλοποιεί την απομακρυσμένη διεπιφάνεια
compute.Compute
, επομένως μπορεί να κληθεί ως απομακρυσμένο αντικείμενο.Κατασκευαστής Απομακρυσμένων Αντικειμένων
Η κλάσηComputeEngine
έχει ένα απλό κατασκευαστή που δε δέχεται ορίσματα. Ο κώδικας είναι ο εξής:Ο κατασκευαστής απλά καλεί ένα γενικευμένο κατασκευαστή χωρίς ορίσματα, της κλάσηςpublic ComputeEngine() {
super();
}Object
. Η κλήση αυτή θα γινόταν ακόμη και αν ο κατασκευαστήςComputeEngine
είχε παραληφθεί εντελώς.Υλοποιήσεις Απομακρυσμένων Μεθόδων
Η κλάση ενός απομακρυσμένου αντικειμένου παρέχει υλοποιήσεις για κάθε απομακρυσμένη μέθοδο που ορίζεται στις απομακρυσμένες διεπιφάνειες. Η διεπιφάνειαCompute
περιέχει μόνο μια απομακρυσμένη μέθοδο, τηνexecuteTask
, που υλοποιείται ως εξής:public <T> T executeTask(Task<T> t) {
return t.execute();
}Αυτή η μέθοδος υλοποιεί το πρωτόκολλο επικοινωνίας του απομακρυσμένου αντικειμένου
ComputeEngine
και των πελατών. Κάθε πελάτης περνά στο αντικείμενοComputeEngine
ένα αντικείμενοTask
το οποίο αποτελεί μια συγκεκριμένη υλοποίηση της μεθόδουexecute
της διεπιφάνειας του αντικειμένουTask
. Το αντικείμενοComputeEngine
εκτελεί την υπολογιστική εργασία και επιστρέφει το αποτέλεσμα της μεθόδουexecute
στον πελάτη.Πέρασμα Παραμέτρων στο RMI
Οι παράμετροι (ορίσματα, επιστροφές) πρός και από τις απομακρυσμένες μεθόδους μπορούν να είναι σχεδόν οποιουδήποτε τύπου, όπως τοπικά αντικείμενα, απομακρυσμένα αντικείμενα και βασικοί τύποι δεδομένων. Πιο συγκεκριμένα, οποιαδήποτε οντότητα οποιουδήποτε τύπου μπορεί να χρησιμοποιοηθεί ως παράμετρος, αρκεί αυτή η οντότητα να είναι στιγμιότυποενός βαικού τύπου, ενός απομακρυσμένου αντικειμένου ή ενός σειριοποιήσιμου (serializable) αντικειμένου, δηλαδή αντικειμένου που υλοποιεί τη διεπιφάνειαjava.io.Serializable.
Μερικοί τύποι αντικειμένων δεν ανήκουν στις παραπάνω κατηγορίες. Πρόκειται κυρίως για αντικείμενα, όπως νήματα ή περιγραφείς αρχείων, που περιέχουν πληροφορία που αφορά συγκεκριμένο χώρο διευθύνσεων. Από την άλλη πλευρά πολλές βαικές κλάσεις, όπως τα πακέτα
java.lang
καιjava.util
, υλοποιούν τη διεπιφάνειαSerializable
.Οι κανόνες περάσματος παραμέτρων είναι οι εξής:
- Τα απομακρυσμένα αντικείμενα περνούν ουσιαστικά με αναφορά (by reference). Η αναφορά ενός απομακρυσμένου αντικειμένου είναι ένα στέλεχος (stub), που είναι ένας πληρεξούσιος στη πλευρά του πελάτη που υλοποιεί όλες τις απομακρυσμένες διεπιφάνειες που υλοποιεί το απομακρυσμένο αντικείμενο.
- Τα τοπικά αντικείμενα περνούν με αντιγραφή (by copy), μέσω σειριοποίησης αντικειμένων. Εξ' ορισμού, όλα τα πεδία ενός αντικειμένου αντιγράφονται, εκτός από τα
static
ήtransient
.
Το πέρασμα με αναφορά ενός απομακρυσμένου αντικειμένου, σημαίνει οτι οποιεσδήποτε αλλαγές συμβαίνουν στο αντικείμενο, μέσω της κλήσης απομακρυσμένης μεθόδου, εφαρμόζονται στο πρωτότυπο απομακρυσμένο αντικείμενο. Βέβαια, μόνο οι απομακρυσμένες διεπαφές του απομακρυσμένου αντικειμένου είναι διαθέσιμες στον πελάτη. Οι μέθοδοι που είναι ορισμένες ως μη-απομακρυσμένες δεν είναι ορατές στο πελάτη.
Για παράδειγμα, αν περάσετε μια αναφορά σε στιγμιότυπο της κλάσης
ComputeEngine
, ο πελάτης θα έχει πρόσβαση μόνο στη μέθοδοexecuteTask
, αλλά δεν θα μπορεί να δεί τον κατασκευαστή αντικειμένων της κλάσηςComputeEngine
, τη μέθοδοmain
της κλάσης, ή την υλοποίηση άλλων μεθόδων της κλάσηςjava.lang.Object
.In the parameters and return values of remote method invocations, objects that are not remote objects are passed by value. Thus, a copy of the object is created in the receiving Java virtual machine. Any changes to the object's state by the receiver are reflected only in the receiver's copy, not in the sender's original instance. Any changes to the object's state by the sender are reflected only in the sender's original instance, not in the receiver's copy.
Υλοποίηση Μεθόδου
main
του Διακομιστή
Η πιο σύνθετη μέθοδος στην υλοποίηση της κλάσηςComputeEngine
είναι η μέθοδοςmain
. Η μέθοδοςmain
ξεκινά ένα αντικείμενο της κλάσηςComputeEngine
και γι' αυτό απαιτείται αρχικοποίηση και άλλες ρυθμίσεις του διακομιστή. Δεν πρόκειται για απομακρυσμένη μέθοσο. Επειδή η μέθοδοςmain
είναι δηλωμένηstatic
, είναι μάλλον συνδεδεμένη με τη κλάσηComputeEngine
συνολικά παρά με το αντικείμενο που ξεκινά.
Εγκατάσταση Διαχειριστή Ασφάλειας
Η πρώτη εργασία της μεθόδουmain
είναι η εγκατάσταση ενός διαχειριστή ασφάλειας, που προστατεύει τους πόρους του συστήματος από κακόβουλο κώδικα που πιθανώς να εκτελεστεί στη JVM του διακομιστή, μέσω της 'υπολογιστικής μηχανής'. Ο διαχειριστής ασφάλειας διασφαλίζει οτι ο κώδικας που θα μεταφορτωθεί θα υπακούει σε συγκεκριμενες πολιτικές ασφάλειας.
Ο διαχειριστής ασφάλειας καθορίζει αν ο κώδικας που μεταφορτώθηκε έχει πρόσβαση στο τοπικό σύστημα αρχείων ή μπορεί να εκτελέσει άλλες προνομιακές λειτουργίες. Αν ένα πρόγραμμα RMI δεν εγκαταστήσει διαχειριστή ασφάλειας, το RMI δεν θα μεταφορτώσει κλάσεις (παρά μόνο από το τοπικό class path) για αντικείμενα που είναι παράμετροι σε κλήσεις απομακρυσμένων μεθόδων.Ο παρακάτω κώδικας εγκαθιστά τον διαχειριστή ασφάλειας:
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}Δημιουργία, Εξαγωγή και Καταχώρηση Απομακρυσμένων Αντικειμένων
Στη συνέχεια η μέθοδοςmain
δημιουργεί ένα απομακρυσμένο αντικείμενο της κλάσηςComputeEngine
και το εξάγει στο σύστημα εκτέλεσης του RMI:Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine, 0);Η
static
μέθοδοςUnicastRemoteObject.exportObject
εξάγει το απομακρυσμένο αντικείμενο ώστε να μπορεί να δέχεται κλήσεις των απομακρυσμένων μεθόδων του από πελάτες. Το δεύτερο όρισμα, τύπουint
, καθορίζει τη θύρα TCP port που θα χρησιμοποιήσει το σύστημα εκτέλεσης RMI για να 'ακούει' τις εισερχόμενες κλήσεις. Συνήθως θέτουμε μηδέν, αφήνοντας την απόφαση στο σύστημα εκτέλεσης του RMI ή στο λειτουργικό σύστημα. Όμως θα μπορούσαμε να θέσουμε ένα συγκεκριμένο αριθμό. Αν η κλήση της μεθόδουexportObject
επιστρέψει με επιτυχία, το απομακρυσμένο αντικείμενο κλάσηςComputeEngine
είναι έτοιμο να επεξεργαστεί εισερχόμενες απομακρυσμένες κλήσεις.H μέθοδος
exportObject
επιστρέφει ένα στέλεχος του απομακρυσμένου αντικειμένου. Σημειώστε οτι τόσο ηnew
όσο και ηexportObject
έχουν τύπο επιστροφής
Compute
, όχιComputeEngine
, γιατί το απομακρυσμένο αντικείμενο είναι μεν της κλάσηςComputeEngine
αλλά υλοποιεί μόνο την απομακρυσμένη διεπιφάνεια.Η μέθοδος
main
μέσω της δομήςtry
/catch
συλλαμβάνει εξαιρέσεις τύπουRemoteException
που μπορεί να παράγει ηexportObject
. Σε ένα διαφορετικό χειρισμό, χωρίς τη δομήtry
/catch
, η μέθοδοςRemoteException
θα έπρεπε να δηλωθεί στη φράσηthrows
της μεθόδουmain
. Η εξαίρεσηRemoteException
μπορεί να προκληθεί αν παρουσιαστούν προβλήματα στους δικτυακούς πόρους, όπως για παράδειγμα αν η θύρα που επιλέγουμε είναι δεσμευμένη.Για να μπορέσει ο πελάτης να αποστείλει μια κλήση μεθόδου στο απομακρυσμένο αντικείμενο του διακομιστή, θα πρέπει να γνωρίζει μια αναφορά σε αυτό το αντικείμενο. Η αναφορά στο απομακρυσμένο αντικείμενο συνήθως προκύπτει ως επιστροφή μιας κλήσης μεθόδου. Η επιστροφή μπορεί να περιέχει είτε την ίδια την αναφορά ή μια δομή δεδομένων που περιέχει αναφορές.
Το RMI υποστηρίζει ένα ειδικό τύπο απομακρυσμένου αντικειμένου, το μητρώο RMI (RMI registry), που επιτρέπει την εύρεση των αναφορών σε απομακρυσμένα αντικείμενα. Το μητρώο RMI είναι μια απλή υπηρεσία ονομασίας απομακρυσμένων αντικειμένων που επιτρέπει τους πελάτες να λαμβάνουν αναφορές σε απομακρυσμένα αντικείμενα με βάση το όνομα. Συνήθως το μητρώο χρησιμοποιείται για τον εντοπισμό του πρώτου απομακρυσμένου αντικειμένου, το οποίο στη συνέχεια παραπέμπει πιθανώς σε άλλα απομακρυσμένα αντικείμενα.
Η απομακρυσμένη διεπιφάνεια
java.rmi.registry.Registry
είναι το API για τη καταχώρηση και αναζήτηση απομακρυσμένων αντικειμένων στο μητρώο RMI. Η κλάσηjava.rmi.registry.LocateRegistry
παρέχει στατικές μεθόδους για τη σύνθεση απομακρυσμένων αναφορών σε ένα μητρώο που βρίσκεται σε συγκεκριμένη υποδοχή δικτύου (διέυθυνση IP και θύρα). Εφ'όσον ένα απομακρυσμένο αντικείμενο κατχωρηθεί στο μητρώο RMI του διακομιστή, οι πελάτες μπορούν να το αναζητήσουν στο μηρώο με το όνομά του (το οποίο πρέπει να γνωρίζουν), να λάβουν την απομακρυσμένη αναφορά (που περιλαμβάνει όλες τις πληροφορίες επικοινωνίας) και να τη χρησιμοποιήσουν για να καλέσουν απομακρυσμένες μεθόδους επ' αυτού του αντικειμένου.Στο κώδικα της μεθόδου
main
βλέπουμε οτι κατ'αρχή ορίζεται ένα όνομα για την 'υπολογιστική μηχανή':
String name = "Compute";Στη συνέχεια, αφού εντοπιστεί το μητρώο RMI του διακομιστή, το όνομα καταχωρείται στο μητρώο -σε συνδυασμό με το αντίστοιχο στέλεχος του απομακρυσμένου αντικειμένου που έχει δημιουργηθεί:
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);Η κλήση της μεθόδου
rebind
αποτελεί μια απομακρυσμένη κλήση στο τοπικό μητρώο RMI. Ως απομακρυσμένη -τυπικά- κλήση, μπορεί να προκαλέσειRemoteException
, που συλλαμβάνεται από τη δομήcatch
στο τέλος της μεθόδουmain
.Για τη κλήση της μεθόδου
Registry.rebind
σημειώστα τα παρακάτω:
- Η κλήση
LocateRegistry.getRegistry
χωρίς παραμέτρους αναζητά μητρώο RMI στο τοπικό σύστημα (localhost) και στη προπειλεγμένη θύρα 1099. Αν θέλετε μπορείτε να δηλώσετε άλλη θύρα, αφού η μέθοδος δέχεται μια παράμετροint
).- Για λόγους ασφαλείας, μια εφαρμογή μπορεί να καλέσει τις μεθόδους
bind
,unbind
,ήrebind
μόνο σε μητρώο ΡΜΙ στο τοπικό σύστημα. Αυτή η πολιτική αποτρέπει μια απομακρυσμένη εφαρμογή να τροποποιήσει το μητρώο του διακομιστή. Η κλήση της μεθόδουlookup
, μπορεί να γίνει από τοπικές ή απομακρυσμένες εφαρμογές.- Στο μητρώο RMI καταχωρείται το στέλεχος και όχι το απομακρυσμένο αντικείμενο καθ' αυτό. Έτσι, όταν ένας πελάτης αναζητά ένα απομακρυσμένο αντικείμενο με το όνομά του, του επιστρέφεται ένα αντίγραφο του στελέχους του απομακρυσμένου αντικειμένου. Με αυτό το τρόπο η απομακρυσμένη κλήση αντικειμένου είναι μια κλήση με αναφορά.
Με την επιτυχή καταχώρηση στο μητρώο RMI, η μέθοδος
main
εμφανίζει ένα μήνυμα που δηλώνει οτι ο διακομιστής είναι έτοιμος να επεξεργαστεί εισερχόμενα αιτήματα και τερματίζει. Δεν είναι απαραίτητο να υπάρχει κάποιο είδος βρόχου αναμονής και επεξεργασίας εισερχομένων αιτημάτων. Το αντικείμενοComputeEngine
δεν θα τερματιστεί ούτε θα θεωρηθεί garbage, όσο υπάρχει τουλάχιστο μια αναφορά σε αυτό από κάποια τοπική ή απομακρυσμένη JVM. Από τη στιγμή που το μητρώο RMI περιέχει μια αναφορά στο αντικείμενοComputeEngine
, η συνθήκη ικανοποιείται και το σύστημα εκτέλεσης του RMI διατηρεί 'εν ζωή' τη διεργασία του αντικειμένουComputeEngine
. Το αντικείμενο θα τερματιστεί μόνο αν διαγραφεί η καταχώρησή του από το μητρώο RMI και δεν υπάρχουν πελάτες με ενεργές απομακρυσμένες αναφορές σε αυτό.Το τελευταίο τμήμα κώδικα στη μέθοδο
main
συλλαμβάνει τυχόν εξαιρέσεις. Ο μόνος τύπος εξαίρεσης είναι οRemoteException
που μπορεί να προκληθεί από τις κλήσειςUnicastRemoteObject.exportObject
καιrebind
. Και στις δύο περιπτώσεις το πρόγραμμα απλά εμφανίζει τα σχετικά μηνύματα σφάλματος και τερματίζει.
Η 'υπολογιστική μηχανή' είναι μάλλον απλή: εκτελεί υπολογιστικές εργασίες που στέλνουν οι πελάτες. Μια εφαρμογή πελάτη RMI για την 'υπολογιστική μηχανή' είναι πιο σύνθετη γιατί, εκτός από την επικοινωνία με το διακομιστή RMI, πρέπει να παράσχει και το κώδικα της υπολογιστικής εργασίας καθ' αυτής.
Ο πελάτης αποτελείται από δύο κλάσεις. Η πρώτη κλάση, η
ComputePi
, αναζητά και αποκτά μια αναφορά στο απομακρυσμένο αντικείμενοCompute
. Στη συνέχεια δημιουργεί μια υπολογιστική εργασία, δηλαδή ένα αντικείμενοTask
, και ζητά την εκτέλεση της υπολογιστικής εργασίας από το διακομιστή μέσω του απομακρυσμένου αντικειμένου. Η δεύτερη κλάση, ηPi
, υλοποιεί τη διεπιφάνειαTask
και περιγράφει την υπολογιστική εργασία: η εργασία της κλάσηςPi
είναι ο υπολογισμός του με δεδομένη ακρίβεια δεκαδικών ψηφίων. Το αντικείμενο κλάσηςPi
έχει ως μοναδικό όρισμα την απαιτούμενη ακρίβεια και επιστρέφει έναjava.math.BigDecimal
.Η μη-απομακρυσμένη διεπιφάνεια
έχει ήδη οριστεί ως εξής:
Task
package compute;
public interface Task<T> {
T execute();
}Η πρώτη κλάση αναζητά και αποκτά μια αναφορά στο απομακρυσμένο αντικείμενο
Compute
, The definition of the task classPi
is shown later.Η κύρια κλάση του πελάτη είναι
:
client.ComputePi
package client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.math.BigDecimal;
import compute.Compute;
public class ComputePi {
public static void main(String args[]) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Registry registry = LocateRegistry.getRegistry(args[0]);
Compute comp = (Compute) registry.lookup(name);
Pi task = new Pi(Integer.parseInt(args[1]));
BigDecimal pi = comp.executeTask(task);
System.out.println(pi);
} catch (Exception e) {
System.err.println("ComputePi exception:");
e.printStackTrace();
}
}
}Όπως και ο διακομιστής, έτσι και ο πελάτης ξεκινά με την εγκατάσταση ενός διαχειριστή ασφάλειας. Το βήμα αυτό είναι απαραίτητο γιατί η λήψη του στελέχους (δηλαδή της αναφοράς) του απομακρυσμένου αντικειμένου μπορεί να απαιτεί την μεταφόρτωση ορισμών κάποιων κλάσεων από το διακομιστή. Το σύστημα εκτέλεσης RMI δεν θα επιτρέψει τη μεταφόρτωση αν δεν λειτουργεί διαχειριστής ασφάλειας.
Στη συνέχεια, ο πελάτης καθορίζει το όνομα του απομακρυσμένου αντικειμένου. Το όνομα,
Compute
, πρέπει να είναι το ίδιο με αυτό που χρησιμοποίησε ο διακομιστής για να καταχωρήσει το αντικείμενο στο μητρώο RMI. Επομένως πρέπει να είναι γνωστό στο πελάτη εκ των προτέρων. Κατόπιν ο πελάτης αναζητά το μητρώο RMI του διακομιστή. Η κλήση της μεθόδουLocateRegistry.getRegistry
επιστρέφει μια αναφορά στο μητρώο RMI του διακομιστή. Το όρισμα της γραμμής γραμμής εντολών,args[0]
, είναι το όνομα του διακομιστή όπου βρίσκεται το μητρώο RMI και το απομακρυσμένο αντικείμενο. Επομένως αυτή η πληροφορία πρέπει να είναι γνωστή στο πελάτη εκ των προτέρων. Η μέθοδοςLocateRegistry.getRegistry
θα μπορούσε να κληθεί και με δύο ορίσματα, το δεύτερο τύπουint
, σε περίπτωση που το μητρώο RMI δεν 'ακούει' στη προεπιλεγμένη θύρα 1099 αλλά σε κάποια άλλη. Εν τέλει ο πελάτης αποκτά μια αναφορά στο απομακρυσμένο αντικείμενο μέσω της κλήσης της μεθόδουregistry.lookup
, που δέχεται ως όρισμα το όνομα του απομακρυσμένου αντικειμένου και επιστρέφει ένα αντίγραφοcomp
του στελέχους του απομακρυσμένου αντικειμένου που υλοποιεί τη διεπιφάνειαCompute
.
Στην επόμενη γραμμή ο πελάτης δημιουργεί την υπολογιστική εργασία, δηλαδή ένα αντικείμενο
Pi task
που υλοποιεί τη διεπιφάνειαTask
. Ως όρισμα στο κατασκευαστή του αντικειμένου κλάσηςPi
περνούμε το δεύτερο όρισμα της γραμμής εντολών,args[1]
, σε μορφή ακεραίου. Το όρισμα αυτό καθορίζει την ακρίβεια υπολογισμού σε αριθμό δεκαδικών ψηφίων. Τελικά, ο πελάτης καλεί την απομακρυσμένη μέθοδοexecuteTask
επί του απομακρυσμένου αντικειμένουCompute comp
. H μέθοδοςexecuteTask
δέχεται ως παράμετρο την υπολογιστική εργασίαtask
, η οποία αφού εκτελεστεί στον διακομιστή επιστρέφει ένα αντικείμενο τύπουBigDecimal
, το οποίο αποθηκεύεται στοresult
. Ο πελάτης πριν τερματίσει εμφανίζει το αποτέλεσμα.
Η εικόνα δείχνει την επικοινωνία μεταξύ του πελάτη
ComputePi
, του μητρώουrmiregistry
, και του διακομιστήComputeEngine
.Η κλάση
Pi
υλοποιεί τη διεπιφάνειαTask
και υπολογίζει το για δοσμένη ακρίβεια. Για το παράδειγμά μας η υπολογιστική εργασία δεν είναι τόσο σημαντική, αρκεί να είναι κάπως απαιτητική ώστε να έχει νόημα η απομακρυσμένη εκτέλεση σε κάποιο ισχυρότερο σύστημα.Η κλάση
είναι η παρακάτω:
client.Pi
package client;
import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;
public class Pi implements Task<BigDecimal>, Serializable {
private static final long serialVersionUID = 227L;
/** constants used in pi computation */
private static final BigDecimal FOUR =
BigDecimal.valueOf(4);
/** rounding mode to use during pi computation */
private static final int roundingMode =
BigDecimal.ROUND_HALF_EVEN;
/** digits of precision after the decimal point */
private final int digits;
/**
* Construct a task to calculate pi to the specified
* precision.
*/
public Pi(int digits) {
this.digits = digits;
}
/**
* Calculate pi.
*/
public BigDecimal execute() {
return computePi(digits);
}
/**
* Compute the value of pi to the specified number of
* digits after the decimal point. The value is
* computed using Machin's formula:
*
* pi/4 = 4*arctan(1/5) - arctan(1/239)
*
* and a power series expansion of arctan(x) to
* sufficient precision.
*/
public static BigDecimal computePi(int digits) {
int scale = digits + 5;
BigDecimal arctan1_5 = arctan(5, scale);
BigDecimal arctan1_239 = arctan(239, scale);
BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
arctan1_239).multiply(FOUR);
return pi.setScale(digits,
BigDecimal.ROUND_HALF_UP);
}
/**
* Compute the value, in radians, of the arctangent of
* the inverse of the supplied integer to the specified
* number of digits after the decimal point. The value
* is computed using the power series expansion for the
* arc tangent:
*
* arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 +
* (x^9)/9 ...
*/
public static BigDecimal arctan(int inverseX,
int scale)
{
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf(inverseX);
BigDecimal invX2 =
BigDecimal.valueOf(inverseX * inverseX);
numer = BigDecimal.ONE.divide(invX,
scale, roundingMode);
result = numer;
int i = 1;
do {
numer =
numer.divide(invX2, scale, roundingMode);
int denom = 2 * i + 1;
term =
numer.divide(BigDecimal.valueOf(denom),
scale, roundingMode);
if ((i % 2) != 0) {
result = result.subtract(term);
} else {
result = result.add(term);
}
i++;
} while (term.compareTo(BigDecimal.ZERO) != 0);
return result;
}
}Σημειώστε οτι όλες οι σειριοποιήσιμες κλάσεις, δηλαδή κλάσεις που υλοποιούν τη διεπιφάνεια
Serializable
έμεσα ή άμεσα, πρέπει να δηλώσουν ένα πεδίοprivate
static
final
με όνομαserialVersionUID
που διασφαλίζει τη συμβατότητα μεταξύ διαφορετικών σειριοποιημένων εκδόσεων. Αν δεν υπάρχει προηγούμενη έκδοση της κλάσης, τότε η τιμή του πεδίου αυτού μπορεί να είναι οποιαδήποτε τιμήlong
value, όπως η227L
που χρησιμοποιείται στοPi
, αρκεί αυτή η τιμή να διατηρηθεί και σε μελλοντικές εκδόσεις.Ο κατασκευαστής της κλάσης
Pi
καλείται μέσω τηςnew Pi
και απλά αποθηκεύει τη παράμετροdigits
στο τοπικό πεδίο. Η ουσιαστική εκτέλεση της εφαρμογής ξεκινά όταν το αντικείμενοPi task
δίνεται ως παράμετρος στη μέθοδο
executeTask
. Σε αυτό το σημείο, ο κώδικας της κλάσηςPi
μεταφορτώνεται μέσω του RMI στην JVM του διακομιστή ως υλοποίηση της διεπιφάνειαςTask
, και έτσι γίνεται 'κατανοητος' από το απομακρυσμένο αντικείμενο της 'υπολογιστικής μηχανής' που αντιστοιχεί στο στέλεχοςCompute comp.
Έτσι η κλήση της μεθόδουcomp.executeTask(task)
του πελάτη εκτελείται ωςtask.execute()
στο διακομιστή. H μέθοδοςtask.execute()
χρησιμοποιεί για την υπολογιστική εργασίας τη μέθοδοtask.computePi(digits)
. Το αποτέλεσμα είναι το αντικείμενοBigDecimal
, που επιστρέφεται στο πελάτη ωςpi
, το οποίο και τυπώνεται ως αποτέλεσμα του υπολογισμού.Το γεγονός οτι το αντικείμενο που υλοποιεί τη διεπιφάνεια
Task
εν τέλει εκτελεί το κώδικα της κλάσηςPi
δεν ενδιαφέρει το αντικείμενοComputeEngine
. Αντί γι' αυτή την υπολογιστική εργασία, θα μπορούσε να είναι κάποια άλλη υπολογιστικά απαιτητική δουλειά, όπως για παράδειγμα η παραγωγή ενός μεγάλου πρώτου αριθμού. Αυτό που θα άλλαζε θα ήταν κυρίως η κλάση υπολογιστικής εργασίας του πελάτη και, λιγότερο, η κλάση-οδηγός του πελάτη. Το μόνο που ενδιαφέρει το απομακρυσμένο αντικείμενοCompute
είναι οτι κάθε αντικείμενο που παραλαμβάνει υλοποιεί τη μέθοδοexecute
όπως έχει προδιαγραφεί από τη διεπιφάνειαTask.
Σε ένα σχετικά πραγματικό σενάριο, χρειαζόμαστε τρία διαφορετικά πακέτα:
Ένας εύχρηστος τρόπος διαχείρισης των πακέτων είναι ως αρχεία JAR. Τυπικά, ένας προγραμματιστής θα ετοιμάσει τα δύο πρώτα πακέτα. Οι διεπιφάνειες θα πρέπει να είναι διαθέσιμες ώστε κάποιοι άλλοι προγραμματιστές να τις μεταφορτώσουν και να ετοιμάσουν το τρίτο πακέτο, δηλαδή τους πελάτες.
compute
–Οι διεπιφάνιες
Compute
καιTask
που πρέπει να είναι γνωστές τόσο στο διακομιστή όσο και στο πελάτηengine
–Η κλάση της 'υπολογιστικής μηχανής'
ComputeEngine
του διακομιστήclient
–Η κλάση-οδηγός του πελάτη
ComputePi
ο και η κλάση της υπολογιστικής εργασίαςPi
του πελάτη
Ξεκινούμε με τις διεπιφάνειες. Έστω οτι ο πηγαίως κώδικας βρίσκεται στο κατάλογο/home/serveruser/src
. Γράφουμε:
cd /home/serveruser/src
javac compute/Compute.java compute/Task.java
jar cvf compute.jar compute/*.classΗ εντολή
jar
εξ'αιτίας της επιλογής-v
εμφανίζει τα παρακάτω:added manifest
adding: compute/Compute.class(in = 307) (out= 201)(deflated 34%)
adding: compute/Task.class(in = 217) (out= 149)(deflated 31%)Τώρα το αρχείο
compute.jar
μπορεί να διανεμηθεί στους προγραμματιστές που θα θελήσουν να γράψουν κώδικα εφαρμογής πελάτη, αφού η JVM του πελάτη πρέπει να έχει αυτές τις κλάσεις στο class path της.
Επιπλέον, τα αρχεία της διεπιφάνειας πρέπει να είναι διαθέσιμα και στο μητρώο RMI, όπου καταχωρείται το απομακρυσμένο αντικείμενο, αφού κλάση στελέχους
ComputeEngine
υλοποιεί τη διεπιφάνειαCompute
, που αναφέρεται στη διεπαφήTask
. Ένας απλός τρόπος διάθεσης αρχείων class ή JAR είναι η τοποθέτησή τους σε ένα web-accessible κατάλογο. Στο παράδειγμά μας θεωρούμε οτι ο υπολογιστής που δουλεύουμε διαθέτει Apache web server και επιπλέον έχουμε εναργοποιήσει την επιλογή που επιτρέπει στους χρήστες να έχουν δικό τους web-accesssible κατάλογο. Ο κατάλογος αυτός συνήθως είναι ο/home/serveruser/public_html
και είναι ορατός μέσω HTTP στο URLhttp://zaphpod/~serveruser/
, όπου host το όνομα του υπολογιστή. Οπότε μπορούμε να δημιουργήσουμε έναν υποκατάλογο/home/serveruser/public_html/classes
για την διανομή των αρχείων class ή JAR. Ο κατάλογος αυτός θα είναι ορατός μέσω HTTP στο URLhttp://zaphpod/~serveruser/classes.
Σε περίπτωση που ο διακομιστής RMI δεν διαθέτει διακομιστή Ιστού, μπορούμε αντί για πλήρη διακομιστή να χρησιμοποιήσουμε ένα απλό διακομιστή HTTP.
Το πακέτοengine
περιέχει τη κλάσηComputeEngine
και τις διεπιφάνειεςCompute
καιTask
. 'Εστω οτι ο κώδικας βρίσκεται στο κατάλογο/home/server-user/src/engine
. Επίσης χρειαζόμαστε το αρχείοcompute.jar
στο class path της μεταγλώττισης. Έστω οτι έχουμε ήδη τοποθετήσει το αρχείοcompute.jar
στο κατάλογο/home/serveruser/public_html/classes
. Γράφουμε:cd /home/serveruser/src
javac -cp /home/serveruser/public_html/classes/compute.jar engine/ComputeEngine.java
Η κλάση της 'υπολογιστικής μηχανής' δεν χρειάζεται να γίνει web-accessible αφού δεν είναι δημοσιοποιήσιμη.
Το πακέτοclient
περιέχει δύο κλάσεις, τηComputePi
, δηλαδή το πρόγραμμα-οδηγό του πελάτη, και τηPi
, την υπολογιστική εργασία που υλοποιεί τη διεπιφάνειαTask
. Εκτός από τα αρχεία της διεπιφάνειας, τα οποία πρέπει να είναι διαθέσιμα κατά τη μεταγλώττιση, το RMI πρέπει να μπορεί να μεταφορτώνει τις απαραίτητες κλάσεις από το πελάτη προς το διακομιστή κατά την εκτέλεση της εφαρμογής. Για το σκοπό αυτό χρησιμοποιείται ο ίδιος μηχανισμός που ήδη αναφέρθηκε, δηλαδή ένας web-accessible κατάλογος και ένας διακομιστής Ιστού.Έστω οτι τα αρχεία
ComputePi.java
καιPi.java
βρίσκονται στο κατάλογο/home/clientuser/src/client
. Για τη μεταγλώττιση απαιτείται και το αρχείοcompute.jar
. Επομένως το αρχείο αυτό πρέπει να έχει μεταφορτωθεί στο κατάλληλο κατάλογο, έστω/home/clientuser/public_html/classes
. Tότε γράφουμε:
cd /home/clientuser/src
javac -cp /home/clientuser/public_html/classes/compute.jar
client/ComputePi.java client/Pi.java
mkdir /home/clientuser/public_html/classes/client
cp client/Pi.class
/home/clientuser/public_html/classes/clientΜόνο η κλάση
Pi
θα δημοσιοποιηθεί στο κατάλογο/home/clientuser/public_html/classes
. Ο κατάλογος αυτός θα είναι ορατός μέσω HTTP στο URLhttp://ford/~clientuser/classes
, όπου ford το όνομα του υπολογιστή.
Πριν εκτελέσουμε το πρόγραμμα του
διακομιστή πρέπει να ξεκινήσουμε το μητρώο RMI στο σύστημα του
διακομιστή. Η εκκίνηση του μητρώου RMI στο διακομιστή zaphod
γίνεται ως εξής:
rmiregistry &
Εξ' ορισμού το μητρώο 'ακούει' στη θύρα 1099. Μπορούμε να ορίσουμε διαφορετική θύρα ως εξής:
rmiregistry 2001 &
Ρυθμίσεις class paths
Τα προγράμματα του πελάτη και του διακομιστή περιέχουν εντολές για την εκκίνηση του διαχειριστή ασφαλείας. Ο διαχειριστής ασφαλείας ενεργοποιείται οποτεδήποτε το περιβάλλον εκτέλεσης της Java (η τοπική JVM που εκτελεί το κώδικά μας) προσπαθεί να εκτελέσει κάποια κλάση η οποία δεν περιλαμβάνεται στα class paths που έχουν δηλωθεί κατά την εκκίνηση της JVM. Συνήθως τα classpaths του περιβάλλοντος εκτέλεσης περνούν είτε μέσω της μεταβλητής περιβάλλοντος
CLASSPATH
είτε μέσω ορισμάτων της επιλογήςjava -cp <classpaths>.
Σε περίπτωση που οι κλάσεις που εκτελούνται βρίσκονται στα class paths τότε ο διαχειριστής ασφαλείας δεν ενεργοποιείται, αφού θεωρούνται γνωστές στο περιβάλλον εκτέλεσης. Αν, για παράδειγμα, η εκκίνηση της JVM γίνει από τον κατάλογο όπου βρίσκονται οι κλάσεις, τότε, με δεδομένο οτι ο τρέχων κατάλογος συνήθως είνα στο class path, ο διαχειριστής ασφαλείας δεν ενεργοποιείται. Όμως, στη γενική περίπτωση που οι κλάσεις πελάτη και διακομιστή βρίσκονται σε καταλόγους (ή και σε διαφορετικά συστήματα) που δεν περιλαμβάνονται στα class paths τότε ενεργοποιείται ο διαχειριστής ασφάλειας.
Ειδικότερα για τις εφαρμογές RMI υπάρχουν οι εξής βασικές εναλλακτικές.
- Όλες οι κλάσεις βρίσκονται στον ίδιο κατάλογο -ή, ισοδύναμα- βρίσκονται στα class paths της JVM που εκτελεί το κώδικα. Στο παράδειγμα μας, θα μπορούσαμε να έχουμε ένα μόνο κατάλογο
/home/rmiuser/src/
ο οποίος περιέχει όλο το κώδικα της εφαρμογής, ή εναλλακτικά θα μπορούσαμε να είχαμε δύο καταλόγους που θα συμπεριλαμβάνονται στα class paths και των δύο των εφαρμογών.java -cp /home/rmiuser/src <java.class.name>
java -cp /home/serveruser/src:/home/clientuser/src <java.class.name>
Ρυθμίσεις διαχειριστή ασφαλείας
Αν δεν έχουμε ιδιαίτερα προβλήματα ασφαλείας, τότε μπορούμε στους δύο καταλόγους -ή στα δύο διαφορετικά συστήματα- που εκτελούνται τα προγράμματα πελάτη και διακομιστή να έχουμε δύο αντίγραφα ενός πανομοιότυπου γενικευμένου αρχείου πολιτικών ασφάλειας, έστω
general.policy
:grant {
permission java.security.AllPermission;
};Και στις δύο τις περιπτώσεις η κλήση της JVM έχει τη μορφή:
java -Djava.security.policy=<policy.file.name> <java.class.name>Επιπλέον, σε περίπτωση που το πρόγραμμα δεν περιέχει εντολές για την εκκίνηση του διαχειριστή ασφαλείας, η εκκίνηση του διαχειριστή ασφαλείας μπορεί να γίνει από τη γραμμή εντολών αντί μέσω του προγράμματος, :
java -Djava.security.managerΗ παραπάνω σύνταξη ισχύει τόσο για την εκκίνηση του προγράμματος πελάτη όσο και του διακομιστή.
-Djava.security.policy=<policy.file.name> <java.class.name>
Αν θέλουμε να είμαστε αυστηροί στα θέματα ασφάλειας, τότε το κάθε αρχείο πολιτικών ασφάλειας μπορεί να αναφέρεται σε συγκεκριμένο όνομα διαδρομής (code base) από όπου φορτώνονται κλάσεις, ανεξάρτητα από τις ρυθμίσεις του class path. Για παράδειγμα, στο κατάλογο όπου εκτελείται το πρόγραμμα του διακομιστή, πρέπει να υπάρχει ένα αρχείο πολιτικών ασφαλείαςserver.policy
:grant codeBase "file:/home/serveruser/src/" {
permission java.security.AllPermission;
};Αντίστοιχο αρχείο πολιτικών ασφάλειας του πελάτη,
client.policy
:grant codeBase "file:/home/clientuser/src/" {
permission java.security.AllPermission;
};Και στα δύο αρχεία, δίνονται πλήρεις άδειες στα αρχεία του τοπικού class path, επειδή αυτός ο κώδικας είναι έμπιστος, αλλά δε δίνονται καθόλου άδειες σε κώδικα που μεταφορτώνεται από αλλού. Έτσι ο διακομιστής της 'υπολογιστικής μηχανής' απαγορεύει στις υπολογιστικές εργασίες να κάνουν οτιδήποτε απαιτεί ιδιαίτερα δικαιώματα ασφαλείας. Η υπολογιστική εργασία
Pi
δεν απαιτεί κάποια ιδιαίτερα δικαιώματα για να εκτελεστεί.
Ρυθμίσεις μεταφόρτωσης κλάσεων
Πρώτα ξεκινούμε το διακομιστή. Στην εντολή εκκίνησης πρέπει να ορίσουμε τα παρακάτω:
- Πρώτο, μέσω της επιλογής
cp
πρέπει να ορίσουμε τα class paths όπου βρίσκονται τα εκτελέσιμα του διακομιστή και της διεπιφάνειαςcompute.jar
.- Δεύτερο, μέσω της ιδιότητας
java.rmi.server.codebase
, δηλώνουμε το URL όπου δημοσιοποιούνται τα αρχεία του διακομιστή. Στο παράδειγμά μας, διαθέσιμο είναι το αρχείο JARcompute.jar
που περιέχει τις υλοποιήσεις της διεπιφάνειαςCompute
καιTask
. Σε περίπτωση που χρειαζόταν να οριστεί όχι μόνο ένα αρχείο αλλά κατάλογος, τότε το URL θα έπρεπε να τελειώνει με /.
- Τρίτο, μέσω της ιδιότητας
java.rmi.server.hostname
, ορίζουμε το όνομα του διακομιστήzaphod
. Εξ' ορισμού το σύστημα εκτέλεσης του RMI χρησιμοποιεί την διεύθυνση IP που επιστρέφει η κλήσηjava.net.InetAddress.getLocalHost
. Με την ιδιότηταjava.rmi.server.hostname
μπορούμε να επιλέξουμε είτε ένα όνομα ή εναλλακτική IP διεύθυνση.- Τέταρτο, μέσω της ιδιότητας
java.java.scurity.policy
, ορίζουμε το αρχείο πολιτικών ασφαλείας.
java -cp /home/serveruser/src:/home/serveruser/public_html/classes/compute.jar
-Djava.rmi.server.codebase=http://zaphod/~serveruser/classes/compute.jar
-Djava.rmi.server.hostname=zaphod.some.place
-Djava.security.policy=server.policy
engine.ComputeEngine
Μετά την εκκίνηση του διακομιστή, μπορούμε να ξεκινήσουμε και το πελάτη στον υπολογιστήford
:
- Πρώτο, μέσω της επιλογής
cp
πρέπει να ορίσουμε τα class paths όπου βρίσκονται τα εκτελέσιμα του πελάτη και της διεπιφάνειαςcompute.jar
.- Δεύτερο, μέσω της ιδιότητας
java.rmi.server.codebase
, δηλώνουμε το URL όπου δημοσιοποιούνται τα αρχεία του πελάτη. Στο παράδειγμά μας, ορίζεται ολόκληρος κατάλογος, αλλά θα μπορούσε να δηλωθεί μόνο η κλάσηPi
.- Τρίτο, μέσω της ιδιότητας
java.java.scurity.policy
, ορίζουμε το αρχείο πολιτικών ασφαλείας.- Τέταρτο, περνούμε τα δύο ορίσματα της γραμμής εντολών του πελάτη, το όνομα του διακομιστή και την ακρίβεια υπολογισμού του .
java -cp /home/clientuser/src:/home/clientuser/public_html/classes/compute.jar
-Djava.rmi.server.codebase=http://ford/~clientuser/classes/
-Djava.security.policy=client.policy
client.ComputePi zaphod.some.place 45.Μετά τον υπολογισμό, στην οθόνη του πελάτη θα εμφανιστεί:
3.141592653589793238462643383279502884197169399Η εικόνα που ακολουθεί δείχνει τη χρήση των διακομιστών ιστού για την μεταφόρτωση των κλάσεων.
'Οταν ο διακομιστής
ComputeEngine
καταχωρεί το απομακρυσμένο αντικείμενο στο μητρώο RMI, το μητρώο μεταφρορτώνει τις διεπιφάνειεςCompute
καιTask
όπου βασίζεται το στέλεχος του απομακρυσμένου αντικειμένου. Αυτές οι κλάσεις μπορούν να μεταφορτωθούν είτε από το τοπικό σύστημα αρχείων του διακομιστήComputeEngine
είτε από τον διακομιστή Ιστού, ανάλογα με το URL που δίνεται.Ο πελάτης
ComputePi
έχει τις διεπιφάνειεςCompute
καιTask
στο class path, άρα δε χρειάζεται να τις μεταφορτώσει από το διακομιστή.
Η κλάση
Pi
μεταφορτώνεται στη JVM του διακομιστήComputeEngine
όταν το αντικείμενοPi
δίνεται ως παράμετρος στην απομακρυσμένη κλήση της μεθόδουexecuteTask
επί του αντικειμένουComputeEngine
. Η κλάσηPi
φορτώνεται στο διακομιστή από τον διακομιστή Ιστού ή από το απομακρυσμένο σύστημα αρχείων του πελάτη, ανάλογα με το URL που δίνεται.