MokaByte Numero  43  - Luglio Agosto 2000
Networking in Java 
V puntata: RMI 
la teoria
di 
Stefano Rossini
In questo articolo verrà poposta una panoramica
sulle API RMI di Java

La volta scorsa avevamo introdotto le Socket, un meccanismo di astrazione per la comunicazione in rete, indipendentemente dal dispositivo fisico tramite il quale effettuiamo la connessione.

Nelle scorsa puntata dedicata al Networking [1] si è visto come è possibile trasmettere oggetti (purchè serializzabili) sui socket tramite gli appositi stream(ObjectOutputStream e ObjectInputStream) mediante i relativi metodi(writeObject() e readObject()).
Progettare un’applicazione distribuita utilizzando direttamente i socket comporta una buona mole di lavoro in quanto si richiede :
  • utilizzo diretto dei socket tramite le classi Socket, ServerSocket e gestione dei relativi stream
  • definizione di un protocollo per il trasferimento dei dati (la serializzazione)
  • definizione di un ulteriore protocollo per codificare le varie azioni possibili 
(es: messaggi di configurazione, di segnalazione, dati, ....)

Quindi siamo noi che dobbiamo “impacchettare” i dati nel formato giusto e interpretarli nel modo corretto.
Nel caso di gestione di un quantitativo considerevole di macchine distribuite questo può richiedere l’utilizzo di dati complessi e di molti parametri.
Fondamentalmente quindi si può dire che l’implementazione di una connessione via socket risolve solo il problema di fondo (come instaurare la connessione) ma lascia in sospeso tutta la parte che concerne lo scambio delle informazioni.
Java propone per lo sviluppo di applicazioni distribuite la tecnologia RMI: Remote Method Invocation.
 
 
 

Cos'è RMI ?
RMI e un' insieme di API potente ma semplice che permette di sviluppare applicazioni distribuite in rete, dove le risorse sono allocate su macchine diverse e quindi con spazio di indirizzamento diverso fra loro.
Perchè usare RMI ?

  • Maggiore  astrazione

  • Con RMI si scarica in locale una rappresentazione dell'oggetto remoto (che quindi si trova su una JVM remota) e lo si tratta a tutti gli effetti come se si trattasse di un elemento locale. E' RMI che gestisce tale trasferimento occupandosi della gestione della comunicazione fino al "basso livello"(i socket!). Questa astrazione ci permette di concentrarci sull'applicazione e non sui dettagli di comunicazione. In conclusione il client (intendendo sia il programma che il programmatore della applicazione lato client) non ha che in minima parte la percezione del fatto di stare utilizzando un oggetto remoto.
  • Maggiore eleganza 

  • Una volta ottenuta la rappresentazione dell'oggetto remoto (detto stub, surrogato) sulla propria macchina, il codice necessario ad invocare un metodo remoto è identico a quello utilizzato per un oggetto locale: nome_oggetto_remoto.nome_metodo(lista_dei_parametri);
    Da un punto di vista sintattico non vi è alcuna differenza fra un oggetto locale ed uno remoto.
  • Non necessita di un protocollo di definizione dei messaggi

  • La gestione della comunicazione tra applicativo locale e remoto è completamente gestita da RMI senza la necessità di doversi implementare un protocollo proprietario per tale gestione.
  • Utilizzo del paradigma ad oggetti


Prima dell'intoduzione di RMI erano già disponibili soluzioni al problema dell'esecuzione di parti di codice in remoto, come ad esempio la Remote Procedure Call (RPC): con questa tecnologia è possibile gestire procedure facenti parte di applicazioni residenti in remoto rispetto al chiamante. Le RPC sono strettamente legate al concetto di processo e di procedura e quindi male si inseriscono nel contesto del paradigma ad oggetti. 
E' questo il motivo principale che ha fatto nascere l'esigenza di una tecnologia apposita per la gestione di oggetti ditribuiti, la quale vede in RMI la soluzione full-Java completamente Object Oriented.

Architettura RMI
L’architettura RMI può essere schematizzare come rappresentato in figura 1.
 



Figura 1 — Organizzazione a strati dell'architettura RMI

La suddivisione verticale vede da una parte il lato il lato client dall’altra il server in esecuzione su due JVM diverse.
Vediamo brevemente le funzionalità dei singoli livelli (per un’accurata descrizione, vedere [2]):

Livello di applicazione

  • RMI Client : applicazione che effettua le chiamate ai metodi di oggetti remoti risiedenti sul lato server
  • RMI Server : applicazione che gestisce gli oggetti serventi
E' importante tenere ben presente che non viene spostato l'oggetto fisico remoto, ma viene inviata solo una sua rappresentazione (serializzata) e successivamente ricreata(deserializzata) una copia identica. Al momento della ricreazione il run time locale del client creerà un oggetto nuovo, riempiendo i suoi dati con quelli impostati dal client. Sotto il livello di applicazione ci sono i due elementi fondamentali dell’architettura RMI: stub e skeleton.
  • Stub : fornisce una simulazione locale sulla JVM del client dell’oggetto remoto 
  • Skeleton : è l’oggetto remoto in esecuzione sulla JVM del server
Remote Reference Layer
  • RRL: instaura un collegamento virtuale tra stub e skeleton di tipo sequenziale e per questo si richiede che i parametri da passare ai metodi siano serializzabili.
  • Transport Layer
  • TL:  a questo livello si perde la concezione di oggetto remoto e/o locale in quanto si instaura un collegamento fisico per la trasmissione di sequenza di byte (oggetti serializzati) attraverso i socket su protocllo TCP/IP
Il livello RRL e TL si occupano di gestire il protocollo di conversione delle invocazioni dei metodi, dell'impacchettamento dei riferimenti ai vari oggetti del passaggio dei parametri.
RMI in pratica
Vediamo di descrivere ora come realizzare un’applicazione RMI client/server.
Anche se dal punto di vista della programmazione a oggetti sarebbe più corretto parlare di classi, in questo caso si parlerà genericamente di oggetti remoti e locali intendendo sia il tipo(la classe) che la variabile(l'istanza della classe).

Partiamo da un oggetto MyServer per il momento non remoto.

public class MyLocalServer {
 public void String concat(String a, String b) {
  return a + b;
 }
}

Il metodo compute in questo caso esegue una concatenazione fra i due argomenti passati in input restituendo in uscita la stringa risultante. 
Vediamo ora i passi da compiere per creare la versione remota di tale classe utilizzando le API RMI contenute nei package java.rmi e java.rmi.server. 
Il primo passo consiste nel creare un'interfaccia remota di MyLocalServer per remotizzare tale classe.
Si eseguono i seguenti punti:

  1. Creare l' interfaccia remota
  2. Estendere l’interfaccia java.rmi.Remote
  3. Sollevare l’eccezione java.rmi.Remote.Exception per ogni metodo dell’interfaccia
  4. Controllo dei tipi dei parametri


Un oggetto remoto viene referenziato dal client  tramite un reference ad un’interfaccia che deve obbligatoriamente estendere l’interfaccia java.rmi.Remote
Come in altri casi simili (vedi java.io.Serializable) l’interfaccia java.rmi.Remote è vuota, non contiene alcun metodo.Essa serve da “marcatore”, cioè consente di dichiarare che una classe supporta una determinata caratteristica. Nel nel caso di Serializable si definisce che l’oggetto è serializzabile, mentre, con java.rmi.Remote, il programmatore definisce che l’interfaccia è utilizzabile per accedere ad un oggetto remoto (cioè invocarne i metodi) e protegge l’applicazione da anomalie derivanti dall’utilizzo di risorse remote
Tutti i metodi devono sollevare l’eccezione RemoteException che è in grado di propagarsi lungo la rete.
L’ultimo passo è controllare che i parametri dei metodi siano serializzabili. Nel nostro caso specifico i parametri sono entrambi di classe String e quindi il vincolo è soddisfatto(verificabile  eseguendo da console il seguent comando : serialver java.lang.String). 

public interface MyServerInterface extends Remote {
 public String concat(String a, String b) throws RemoteException;
}

Definita l’interfaccia remota si deve modificare leggermente la classe di partenza (che rinomino da MyLocalServer a MyServerImpl per enfatizzare la "remotizzazione") :

  1. implementare l’interfaccia MyServerInterface
  2. estendere la classe java.rmi.UnicastRemoteObject


public class MyServerImpl extends UnicastRemoteObject
       implements MyServerInterface {
 public MyServer() throws RemoteException { }

 public String concat(String a, String b) throws RemoteException {
  return a + b;
 }
}

Oltre a dichiarare di implementare l’interfaccia precedentemente definita, si deve anche estendere la classe UnicastRemoteObject, una classe predefinita che serve per referenziare l’oggetto remoto.
La classe UnicastRemoteObject deriva dalle due classi, RemoteServer e RemoteObject: la prima è una superclasse comune per tutte le implementazioni di oggetti remoti, mentre l’altra semplicemente ridefinisce hashcode() ed equals() in modo da permettere correttamente i confronti tra oggetti remoti. 
Abbiamo ottenuto un oggetto MyServerImpl i cui metodi possono essere eseguiti da una applicazione client non residente sulla stessa macchina virtuale mediante l'interfaccia MyServerInterface.


Figura 2 — Struttura gerarchica delle classi ed interfacce RMI

Dopo questa trasformazione l’oggetto è visibile dall’esterno, ma ancora non utilizzabile dal meccanismo di RMI; si devono infatti creare i cosiddetti stub e skeleton. 
Essi sono ottenibili in maniera molto semplice per mezzo del compilatore rmic, disponibile all’interno del JDK 1.1 e successivi: partendo dal bytecode ottenuto dopo la compilazione dell’oggetto remoto, questo tool produce lo stub e lo skeleton relativi.
Ad esempio, riconsiderando il caso della classe MyServerImpl, con una operazione del tipo

rmic MyServerImpl

si ottengono i due file MyServerImpl_stub.class e MyServerImpl_skel.class.
Il file skeleton non viene più utilizzato nella versione 2 del SDK.
A questo punto si hanno a disposizione tutti i componenti per utilizzare l’oggetto remoto MyServerImpl, ma si deve provvedere a rendere possibile il collegamento tra client e server per l’invocazione remota.
Quindi creiamo una nuova classe il cui compito è di permettere l’utilizzo dell’oggetto remoto.
Si utilizza il metodo statico java.rmi.Naming.bind che associa all’istanza dell’oggetto remoto un nome logico con cui tale oggetto può essere identificato in rete. 

public class RmiServer  {
   public RmiServer() {
     try {
     // creo un'istanza dell'oggetto remoto
      MyServerImpl server = new MyServerImpl();
     // effettuo la registrazione mediante un nome simbolico
     java.rmi.Naming.bind("MyService", server);
     System.out.println("RmiServer : bind done ...");
     System.out.println("MyService is now available ...");

Per registrare il servizio si utilizza il metodo statico java.rmi.Naming.bind o il metodo statico rebind (quest'ultimo per evitare conflitti con assegnazioni precedenti).
La classe Naming è un registro che mappa dei nomi logici con i references dell'interfacce degli oggetti remoti.
In questo modo l'applicazione server "comunica al mondo" che è disponibile, e quindi referenziabile, in modo remoto l'oggetto avente nome logico MyService.
Questa operazione, detta registrazione, può fallire e in tal caso viene generata una eccezione in funzione del tipo di errore. In particolare si avrà una AlreadyBoundException nel caso in cui il nome logico sia già stato utilizzato per un’altra associazione, una MalformedURLException per errori nella sintassi dell’URL, mentre il runtime produrrà RemoteException per tutti gli altri tipi di errore legati alla gestione da remoto dell’oggetto.
Ogni associazione nome logico – oggetto remoto è memorizzata in un apposito registro gestito dall'applicazione rmiregistry che deve essere lanciata sul lato server prima di ogni bind.
Il client a questo punto è in grado di ottenere un reference all’oggetto con una ricerca presso l’host remoto utilizzando il nome logico con cui l’oggetto è stato registrato.

Ad esempio si potrebbe scrivere una classe RmiClient nel seguente modo :

public class RmiClient {
    public static void main(String[] args) {
         String urlRmiHost = "rmi://server_name:1099/MyService";
          try { 
// dichiaro un reference di interfaccia MyServerInterface e utilizzo il metodo Naming.lookup per 
// ottenere il reference dell’interfaccia remota
              MyServerInterface serverRef = (MyServerInterface)Naming.lookup(urlRmiHost);
 // utilizzo il reference per effettuare le invocazioni ai metodi remoti
  System.out.println(serverRef.concat("Hello ", "world !")); 

Per ottenere il reference si utilizza il metodo statico Naming.lookup(), la cui invocazione può essere considerata sul lato client il corrispettivo alla operazione di bind sul server.
L’URL passato come parametro alla lookup() identifica il nome della macchina che ospita l’oggetto remoto e il nome con cui l’oggetto è stato registrato (il nome logico del servizio nel nostro esempio è MyService)
Si noti come sul client si ottiene un reference non tanto all’oggetto remoto, ma piuttosto all’interfaccia remota, dato che effettivamente sul client arriva lo stub, e non l’oggetto vero e proprio.
Entrambe le operazioni di registrazione e di ricerca accettano come parametro un URL avente il seguente formato:

rmi://host:port/name

dove host è il nome del server RMI, port la porta dove si mette in ascolto il registry, name il nome logico. Di default la porta TCP è la 1099, ed in questo caso si può ometterla nell’url.

Sul server non è necessario specificare l’host, dato che per default assume l’indirizzo della macchina stessa sulla quale l’applicazione server RMI è mandata in esecuzione. 
Il server RMI prima di essere mandato in esecuzione richiede che venga lanciato l'rmiregistry, un demone in ascolto sulla porta TCP (di default la 1099) per le eventuali richieste di accesso ad oggetti remoti registrati nel registro Naming.Se si vuole cambiare la porta d'ascolot di default bisogna specificarlo (vedi parametro numero_porta) da riga di comando.

rmiregistry numero_porta

A questo punto abbiamo tutto quello che ci serve per potere mandare in secuzione il nostro semplice esempio.
Sulla macchina server sulla quale abbiamo creato i files RmiServer.java,MyServerInterface.java,MyServerImpl.java e RmiClient.java provvediamo alla compilazione (javac *.java) e alla generazione dello stub e skeleton(rmic MyServerImpl).
Otteniamo i seguenti files .class : RmiServer,MyServerInterface,MyServerImpl, MyServerImpl_Stub, MyserverImpl_Skeleton e RmiClient.
Dobbiamo portare su ogni macchina client (ad esempio con una sessione FTP, o ancor meglio con un meccanismo di download automatico) i seguenti files: MyServerInterface.class, MyServerImpl_Stub.class e RmiClient.class.
Sulla macchina server mandiamo in esecuzione in background l'applicazione rmiregistry(start rmiregistry da shell di Dos sui sistemi Windows, oppure rmiregistry &  sui sistemi Unix).
Poi si può procedere ad avviare la nostra applicazione server : java RmiServer
A questo punto sulla macchina client mandiamo in esecuzione il client : java RmiClient.
Sulla console dove è in eseczuione il client comparirà il fatidico saluto : Hello World !



RMI e le Applet
Il client RMI può essere anche un’Applet, purchè si utilizzi Netscape come browser (dalla versione 4.05 in avanti) e non Explorer che non supporta RMI (vi ricordate la sentenza Sun-Microsot a tale riguardo ?!)
In questo caso il server RMI deve essere in esecuzione sullo stesso host da cui l’applet viene scaricata per mezzo di una pagina HTML , dato che per motivi di sicurezza, un’Applet può instaurare una connessione con il server da cui proviene ( almeno che non siano Applet “fidate”).
Per questo motivo si utilizzano i metodi getDocumentBase(),che ritorna l’URL del server dal quale si è scaricata l’Applet, e su tale URL invochiamo il metodo getHost() che restituisce il nome dell’host in formato stringa al fine di localizzare l’oggetto remoto.
 
 

public class RmiApplet extends java.applet.Applet {

    TextField tf = new TextField("");

    public void init() 
    {
     ...
      add(tf);
      try {
        MyServerInterface serverRef =
            (MyServerInterface)Naming.lookup("//" + 
             getDocumentBase().getHost()+"/MyService");
        tf.setText(serverRef.concat("Hello"," World !"));
... 

L’utilizzo del reference ottenuto è esattamente lo stesso del caso dell’applicazione RmiClient.
Download del codice da remoto
La creazione della coppia stub–skeleton (tramite il compilatore rmic) permette di disaccoppiare l’applicazione e dar vita al meccanismo di invocazione remota di RMI.
Lo spostamento dello stub al server avviene mediante il principio della serializazione, e quindi solo le informazioni importanti di un oggetto vengono spostate lungo la rete; tali informazioni sono utilizzate per poter ricostruire l’istanza di tale oggetto dall’altra parte del socket.
Questa impostazione ha numerose conseguenze: se il client riceve via serializzazione un oggetto remoto, per poterlo deserializzare e istanziare deve poter accedere al codice di tale oggetto remoto. Tipicamente questo significa dover fornire le classi al client installando i vari .class sulla macchina locale.
Nel caso di applicazioni utilizzanti il browser si può utilizzare direttamente il classloader della JVM del client per scaricare tali file: utilizzando un server HTTP infatti è possibile effettuare il download delle classi direttamente da Internet. In tale situazione non c’è differenza fra oggetti remoti che devono essere presenti in locale per la deserializzazione, o normali classi Java necessarie per far funzionare l’applet.
In questa condizione le varie classi sono localizzate automaticamente in Internet, facendo riferimento al codebase di provenienza ( l’unico indirizzo dal quale il codice può essere scaricato).
Per quanto riguarda invece le applicazioni, la mancanza del browser complica le cose, dato che devono essere risolti due problemi: il primo è come effettuare il download vero e proprio delle classi, e il secondo come localizzare il server dal quale scaricare tale codice.
Per il primo aspetto non ci si deve preoccupare più di tanto, dato che esiste un oggetto apposito che effettua tale operazione: si tratta della classe RMIClassLoader che fa parte del package rmi.server, e può essere vista come una evoluzione della ClassLoader. 
Per il secondo punto Java offre una soluzione molto elegante, semplice e soprattutto automatizzata: invece di cablare le informazioni relative a dove reperire il codice nel client, si può pensare di memorizzarle nel server e passarle al client. Inoltre questo meccanismo fa parte del protocollo RMI per cui tale informazione viene passata direttamente al client al momento del bisogno.
Per fare questo è necessario mandare in esecuzione il server RMI specificando nella opzione java.rmi.server.codebase l’URL presso cui prelevare via HTTP le classi necessarie.

Ad esempio, usando il comando 

Java –Djava.rmi.server.codebase  = http://nome_host:port/rmi_dir/ ServerRmi

ServerRmi indica il nome della applicazione server RMI, mentre nome_host:port specifica l’indirizzo Internet e la porta dove è in funzione il server HTTP. Tale demone dovrà avere accesso alle classi remote che dovranno essere posizionate nella directory rmi_dir.

Strettamente legato al problema del download del codice vi è quello della sicurezza.
Java effettua un controllo molto scrupoloso utilizzando la classe SecurityManager che, nel caso di RMI si chiama RMISecurityManager. 
Dal punto di vista del client, se nessun RMISecurityManager è stato installato, allora potranno essere caricate classi solamente dal classpath locale. Se il client viceversa tenta di invocare metodi di oggetti inviati al server per mezzo di una sorta di RMI rovesciato, e il server non possiede i vari stub localmente, tali invocazioni falliranno, e il client riceverà una eccezione remota.
 
 
 

Conclusioni 
La scelta di utilizzare RMI è consigliabile nel caso in cui si debba implementare una struttura a oggetti distribuiti full-java(sia il lato client che quello server devono essere realizzati obbligatoriamente in tale prospettiva), in maniera semplice e veloce. L'alta semplicità di RMI si paga a volte nei confronti delle tecnologie concorretni più sofisticate (e complicate) che offrono maggiore potenzialità e scalabilità.
Ad esempio è importante ricordare che l'istanza dell'oggetto remoto registrata dall'RMI server è l'unica disponibile e quindi condivisa da tutti i client possibili.
Questo problema (come altri) introdotti dalla semplicità concetttuale di RMI possono essere rimossi in modo elegante mediante l'uso dei pattern.
Ad esempio per permettere ad ogni client RMI di avere una propria istanza dell'oggetto remoto si può attuare una classe server Rmi che implementa il factory pattern al fine di allocare(su server) e gestire(su client) un'istanza privata alclient stesso. [4].
Vi lascio ad Andrea per la parte pratica con un esempio di RMI, pattern & Co.
 
 
 

Bibliografia
[1] A. trentini - "Networking in Java", Mokabyte N.39, Marzo 2000
[2] G.Puliti - "Remote Method Invocation", Mokabyte N.15, Giugno 1998
[3] M.Sciabarrà - "RMI in pratica", Mokabyte N.13, Novembre 1997
[4 ] G.Puliti - "Moka Hints : Fattorie Remote", Mokabyte N.41, Maggio 2000
 

 
Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it