Κατανεμημένα Αντικείμενα με Java RMI

Οι εφαρμογές Java RMI συνήθως αποτελούνται από δύο ξαχωριστά προγράμματα, το διακομιστή και τον πελάτη. Ένα τυπικό πρόγραμμα διακομιστή δημιουργεί τα απομακρυσμένα αντικείμενα, διαμορφώνει τη πρόσβαση σε αυτά τα αντικείμενα και περιμένει αιτήσεις πρόσβασης προς τα αντικείμενα από πελάτες.  Ένα τυπικό πρόγραμμα πελάτη αποκτά πρόσβαση σε απομακρυσμένα αντικείμενα κάποιου διακομιστή και στη συνέχεια καλεί μεθόδους που εφαρμόζονται στα αντικείμενα αυτά για να υλοποιήσει την εφαρμογή του. Το RMI παρέχει το μηχανισμό για την επικοινωνία μεταξύ αυτών των προγραμμάτων. Οι εφαρμογές αυτές λέγονται και εφαρμογές κατανεμημένων αντικειμένων (distributed object application).

Μια εφαρμογή κατανεμημένων αντικειμένων πρέπει να μπορεί να κάνει τα παρακάτω:

Η εικόνα που ακολουθεί δείχνει μια εφαρμογή κατανεμημένων αντικειμένων που χρησιμοποιεί το μητρώο RMI για να εντοπίσει ένα απομακρυσμένο αντικείμενο. Πρώτα ο RMI διακομιστής (server) καλεί το RMI μητρώο (registry) για να καταχωρίσει (bind) ένα απομακρυσμένο αντικείμενο με ένα όνομα. Κατόπιν ο RMI πελάτης (client) αναζητά το απομακρυσμένο αντικείμενο με το όνομά του στο RMI  registry και καλεί μια μέθοδο που εφαρμόζεται στο αντικείμενο. Στην εικόνα ακόμη βλέπουμε οτι το RMI μπορεί να χρησιμοποιήσει υπάρχοντες web servers στο πελάτη ή και το διακομιστή για να φορτώσει ορισμούς κλάσεων, αν αυτό απαιτείται.

the RMI system, using an existing web server, communicates from serve to client and from client to server

Πλεονεκτήματα Δυναμικής Φόρτωσης Κώδικα

Ένα από τα κεντρικά και μοναδικά χαρακτηριστικά του RMI είναι η δυνατότητά του να μεταφορτώνει τον ορισμό της κλάσης ενός αντικειμένου αν αυτή η κλάση δεν ορίζεται στην JVM του παραλήπτη. Όλοι οι τύποι και οι συμπεριφορές ενός αντικειμένου, όπως ήταν διαθέσιμη σε μια JVM, μπορούν να μεταφερθούν σε μια άλλη JVM. Με αυτό το τρόπο η συμπεριφορά μιας εφαρμογής μπορεί να τροποποιηθεί δυναμικά, εισάγοντας χαρακτηριστικά από άλλο σύστημα.

Απομακρυσμένα: Διεπιφάνειες, Αντικείμενα και Μέθοδοι

Όπως όλες οι εφαρμογές Java, μια κατανεμημένη εφαρμογή σε Java RMI κτίζεται με διεπιφάνειες και κλάσεις. Οι διεπιφάνεις ορίζουν μεθόδους. Οι κλάσεις υλοποιούν τις μεθόδους που δηλώθηκαν στις διεπιφάνειες, και, πιθανώς ορίζουν και άλλες μεθόδους. Τα αντικείμενα ανήκουν σε κλάσεις, δηλαδή σχετίζονται με συγκεκριμένες μεθόδους μέσω συγκεκριμένων διεπιφανειών. Σε μια κατανεμημένη εφαρμογή, μερικές υλοποιήσεις κλάσεων μπορεί να βρίσκονται σε μια JVM αλλά κάποιες άλλες όχι. Τα αντικείμενα που σχετίζονται με μεθόδους που καλούνται από απομακρυσμένες JVMs λέγονται απομακρυσμένα αντικείμενα.

Ένα αντικείμενο γίνεται απομακρυσμένο υλοποιώντας μια απομακρυσμένη διεπιφάνεια (remote interface), με τα παρακάτω χαρακτηριστικά:

Το RMI αντιμετωπίζει τα απομακρυσμένα αντικείμενα διαφορετικά από τα τοπικά. Αντί να στείλει ένα αντίγραφο της υλοποίησης απο τη μια JVM στην άλλη, αυτό που στέλνει έιναι ένα στέλεχος (stub) του απομακρυσμένου αντικειμένου. Το στέλεχος λειτουργεί ώς τοπικός αντιπρόσωπος, ή πληρεξούσιος (proxy), του απομακρυσμένου αντικειμένου, και στην ουσία σε αυτό αναφέρεται το πρόγραμμα πελάτης. Ο πελάτης καλεί μια μέθοδο στο αντικείμενο-πληρεξούσιο, που είναι υπεύθυνο να εκτελέσει την κλήση στο πραγματικό απομακρυσμένο αντικείμενο.

Το στέλεχος μπορεί να εκτελέσει μόνο τις μεθόδους που ορίζονται στην απομακρυσμένη διεπιφάνεια και όχι άλλες μεθόδους που πιθανά είναο διαθέσιμες στο απομακρυσμένο αντικείμενο αλλά δεν έχουν δηλωθεί στην απομακρυσμένη διεπιφάνεια.

Ανάπτυξη Κατανεμημένων Εφαρμογών με RMI

Η ανάπτυξη εφαρμογών με RMI περιλαμβάνει 4 βήματα:
  1. Υλοποίηση των προγραμμάτων διακομιστή και πελάτη. Από τη πλευρά του διακομιστή η εφαρμογή περιλαβάνει τον ορισμό της απομακρυσμένης διεπιφάνειας και την υλοποίηση των κλάσεων των απομακρυσμένων αντικειμένων.
  2. Μεταγλώττιση του πηγαίου κώδικα. Αρκεί η χρήση τουjavac compiler. Πριν τη 5η εκδοση της Java έπρεπε να χρησιμοποιηθεί και ο rmic compiler για ορισμένες κλάσεις.
  3. Οι ορισμοί των απομακρυσμένων διεπιφανειών και οι κλάσεις των απομακρυσμένων αντικειμένων γίνονται γνωστές μέσω διακομιστή ιστού.
  4. Εκκίνηση του μητρώου RMI, του διακομιστή και πελάτη.

Υλοποίηση Διακομιστή RMI

Η εφαρμογή RMI που θα αναπτυχθεί είναι μια απλή 'μηχανή υπολογισμών': ο διακομιστής RMI δέχεται από τους πελάτες υπολογιστικές εργασίες, τις εκτελεί και επιστρέφει τα αποτελέσματα. Ο κώδικας του διακομιστή περιλαμβάνει μια απομακρυσμένη διεπαφή και μια κλάση απομακρυσμένων αντικειμένων. Η διεπαφή ορίζει τις μεθόδους που μπορεί να καλέσει ο πελάτης. Ουσιαστικά είναι αυτό που 'βλέπει' ο πελάτης από το απομακρυσμένο αντικείμενο. Η κλάση παρέχει την υλοποίηση των μεθόδων και των σχετικών αντικειμένων.

Σχεδιασμός Απομακρυσμένης Διεπιφάνειας

Στο πυρήνα της μηχανής υπολογισμών βρίσκεται ένα πρωτόκολλο για την υποβολή, εκτέλεση και επιστροφή των υπολογιστικών εργασιών. Το πρωτόκολλο αυτό εκφράζεται μέσω της απομακρυσμένης διεπιφάνειας.
remote communication between a client and the compute engine

Η μηχανή υπολογισμού δεν 'γνωρίζει' εκ των προτέρων την υπολογιστική εργασία που θα εκτελέσει. Ο πελάτης καθορίζει την υπολογιστική εργασία. Ο διακομιστής διαθέτει την υπολογιστική του υποδομή για την εκτέλεση της εργασίας και την επικοινωνιακή του υποδομή για την επικοινωνία με το πελάτη.

Η απομακρυσμένη διεπιφάνεια ορίζεται στο 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, unbindrebind μόνο σε μητρώο ΡΜΙ στο τοπικό σύστημα. Αυτή η πολιτική αποτρέπει μια απομακρυσμένη εφαρμογή να τροποποιήσει το μητρώο του διακομιστή. Η κλήση της μεθόδου lookup, μπορεί να γίνει από τοπικές ή απομακρυσμένες εφαρμογές.
  • Στο μητρώο RMI καταχωρείται το στέλεχος και όχι το απομακρυσμένο αντικείμενο καθ' αυτό. Έτσι, όταν ένας πελάτης αναζητά ένα απομακρυσμένο αντικείμενο με το όνομά του, του επιστρέφεται ένα αντίγραφο του στελέχους του απομακρυσμένου αντικειμένου. Με αυτό το τρόπο η απομακρυσμένη κλήση αντικειμένου είναι μια κλήση με αναφορά.

Με την επιτυχή καταχώρηση στο μητρώο RMI, η μέθοδος main εμφανίζει ένα μήνυμα που δηλώνει οτι ο διακομιστής είναι έτοιμος να επεξεργαστεί εισερχόμενα αιτήματα και τερματίζει. Δεν είναι απαραίτητο να υπάρχει κάποιο είδος βρόχου αναμονής και επεξεργασίας εισερχομένων αιτημάτων. Το αντικείμενο ComputeEngine δεν θα τερματιστεί ούτε θα θεωρηθεί garbage, όσο υπάρχει τουλάχιστο μια αναφορά σε αυτό από κάποια τοπική ή απομακρυσμένη JVM. Από τη στιγμή που το μητρώο RMI περιέχει μια αναφορά στο αντικείμενο ComputeEngine, η συνθήκη ικανοποιείται και το σύστημα εκτέλεσης του RMI διατηρεί 'εν ζωή' τη διεργασία του αντικειμένου ComputeEngine. Το αντικείμενο θα τερματιστεί μόνο αν διαγραφεί η καταχώρησή του από το μητρώο RMI και δεν υπάρχουν πελάτες με ενεργές απομακρυσμένες αναφορές σε αυτό.

Το τελευταίο τμήμα κώδικα στη μέθοδο main συλλαμβάνει τυχόν εξαιρέσεις. Ο μόνος τύπος εξαίρεσης είναι ο RemoteException που μπορεί να προκληθεί από τις κλήσεις UnicastRemoteObject.exportObject και rebind. Και στις δύο περιπτώσεις το πρόγραμμα απλά εμφανίζει τα σχετικά μηνύματα σφάλματος και τερματίζει.


Υλοποίηση Πελάτη RMI

Η 'υπολογιστική μηχανή' είναι μάλλον απλή: εκτελεί υπολογιστικές εργασίες που στέλνουν οι πελάτες. Μια εφαρμογή πελάτη RMI για την 'υπολογιστική μηχανή' είναι πιο σύνθετη γιατί, εκτός από την επικοινωνία με το διακομιστή RMI, πρέπει να παράσχει και το κώδικα της υπολογιστικής εργασίας καθ' αυτής.

Ο πελάτης αποτελείται από δύο κλάσεις. Η πρώτη κλάση, η ComputePi, αναζητά και αποκτά μια αναφορά στο απομακρυσμένο αντικείμενο Compute. Στη συνέχεια δημιουργεί μια υπολογιστική εργασία, δηλαδή ένα αντικείμενο Task, και ζητά την εκτέλεση της υπολογιστικής εργασίας από το διακομιστή μέσω του απομακρυσμένου αντικειμένου. Η δεύτερη κλάση, η Pi, υλοποιεί τη διεπιφάνεια Task και περιγράφει την υπολογιστική εργασία: η εργασία της κλάσης Pi είναι ο υπολογισμός του the pi symbol με δεδομένη ακρίβεια δεκαδικών ψηφίων. Το αντικείμενο κλάσης Pi έχει ως μοναδικό όρισμα την απαιτούμενη ακρίβεια και επιστρέφει ένα java.math.BigDecimal.

Η μη-απομακρυσμένη διεπιφάνεια Task έχει ήδη οριστεί ως εξής:

package compute;

public interface Task<T> {
T execute();
}

Η πρώτη κλάση αναζητά και αποκτά μια αναφορά στο απομακρυσμένο αντικείμενο Compute,  The definition of the task class Pi 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.

the flow of messages between the compute engine, the registry, and the client.

Η κλάση Pi υλοποιεί τη διεπιφάνεια Task και υπολογίζει το the pi symbol για δοσμένη ακρίβεια. Για το παράδειγμά μας  η υπολογιστική εργασία δεν είναι τόσο σημαντική, αρκεί  να είναι κάπως απαιτητική ώστε να έχει νόημα η απομακρυσμένη εκτέλεση σε κάποιο ισχυρότερο σύστημα.

Η κλάση 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.


Mεταγλώττιση της Εφαρμογής RMI

Σε ένα σχετικά πραγματικό σενάριο, χρειαζόμαστε τρία διαφορετικά πακέτα:
Ένας εύχρηστος τρόπος διαχείρισης των πακέτων είναι ως αρχεία JAR.  Τυπικά, ένας προγραμματιστής θα ετοιμάσει τα δύο πρώτα πακέτα. Οι διεπιφάνειες θα πρέπει να είναι διαθέσιμες ώστε κάποιοι άλλοι προγραμματιστές να τις μεταφορτώσουν και να ετοιμάσουν το τρίτο πακέτο, δηλαδή τους πελάτες.
Ξεκινούμε με τις διεπιφάνειες. Έστω οτι ο πηγαίως κώδικας βρίσκεται στο κατάλογο /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 στο URL http://zaphpod/~serveruser/, όπου host το όνομα του υπολογιστή. Οπότε μπορούμε να δημιουργήσουμε έναν υποκατάλογο /home/serveruser/public_html/classes για την διανομή των αρχείων class ή JAR. Ο κατάλογος αυτός θα είναι ορατός μέσω HTTP στο URL http://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 στο URL http://ford/~clientuser/classes, όπου ford το όνομα του υπολογιστή.


Εκτέλεση Εφαρμογής RMI

Eκκίνηση μητρώου RMI

Πριν εκτελέσουμε το πρόγραμμα του διακομιστή πρέπει να ξεκινήσουμε το μητρώο 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 υπάρχουν οι εξής βασικές εναλλακτικές.

	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 δεν απαιτεί κάποια ιδιαίτερα δικαιώματα για να εκτελεστεί.

Ρυθμίσεις μεταφόρτωσης κλάσεων

Πρώτα ξεκινούμε το διακομιστή.  Στην εντολή εκκίνησης πρέπει να ορίσουμε τα παρακάτω: 

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:

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

Η εικόνα που ακολουθεί δείχνει τη χρήση των διακομιστών ιστού για την μεταφόρτωση των κλάσεων.

the registry, the compute engine, and the client obtaining classes during program execution

'Οταν ο διακομιστής ComputeEngine καταχωρεί το απομακρυσμένο αντικείμενο στο μητρώο RMI, το μητρώο μεταφρορτώνει τις διεπιφάνειες Compute και Task όπου βασίζεται το στέλεχος του απομακρυσμένου αντικειμένου. Αυτές οι κλάσεις μπορούν να μεταφορτωθούν είτε από το τοπικό σύστημα αρχείων του διακομιστή ComputeEngine είτε από τον διακομιστή Ιστού, ανάλογα με το URL που δίνεται.

Ο πελάτης ComputePi έχει τις διεπιφάνειες Compute και Task στο class path, άρα δε χρειάζεται να τις μεταφορτώσει από το διακομιστή.

Η κλάση Pi μεταφορτώνεται στη JVM του διακομιστή ComputeEngine όταν το αντικείμενο Pi δίνεται ως παράμετρος στην απομακρυσμένη κλήση της μεθόδου executeTask επί του αντικειμένου ComputeEngine. Η κλάση Pi φορτώνεται στο διακομιστή από τον διακομιστή Ιστού ή από το απομακρυσμένο σύστημα αρχείων του πελάτη, ανάλογα με το URL που δίνεται.