MokaByte Numero 13 - Novembre 1997
RMI in pratica
 
di
Michele
Sciabarrà
 



Tra le innumerevoli innovazioni introdotte nel JDK 1.1 abbiamo la gestione di oggetti remoti, Questa interessante possibilità, nascosta dietro la poco significativa sigla di RMI (Remote Method Invocation) spalanca le porte allo sviluppo di applicazioni realmente distribuite. In sostanza RMI consente di scrivere programmi in Java che girano "sparsi" tra varie JVM distribuite sulla rete.Come agli albori di Java, tutti ne parlano ma sembra che pochi la usino veramente. In questo articolo racconteremo le peripezie e le trappole in cui siamo incorsi sviluppando una miniapplicazione che usa RMI.


Dopo aver letto le specifiche di RMI, in effetti sorge il dubbio che una cosa simile, che sulla carta ha un che di miracoloso, funzioni davvero. In realtà funziona, ma non è semplicissimo riuscire ad utilizzarla: ci sono alcuni problemi che insorgono, che possono essere realmente disarmanti. In alcuni casi sono rimasto delle ore a cercare una soluzione per andare avanti. Alla fine sono riuscito a scoprire come si fa, e distillerò il succo delle mie ricerche in questo articolo. Leggendolo, forse potete realmente risparmiare qualche giorno di tentativi quando deciderete di sviluppare utilizzando RMI.
Questo articolo richiede come prerequisito una conoscenza almeno teorica di RMI, in quanto andremo direttamente al nocciolo e faremo pratica. Sono già apparsi articoli introduttivi, come uno di Fabrizio Giudici su MokaByte o quello di prossima pubblicazione di Giovanni Puliti su Computer Programming. Questo articolo fa seguito ad essi e presenta una impostazione pratica.
Il nostro progettino consiste per nello sviluppo di una semplicissima applet che chiama i metodi di un server RMI. L’applet mostra un campo testo in cui è possibile digitare qualcosa. Man mano che si digita, il testo viene manipolato chiamando una funzione remota (una semplice inversione), e il risultato visualizzato. L’applet non ha nessuna utilità ma farla funzionare è stato un tour-de-force alla scoperta di RMI. Attenzione: l’applet che svilupperemo funziona solo con un browser realmente conforme al JDK 1.1, attualmente solo HotJava e Netscape Navigator 4.03 con la patch per il JDK 1.1.

RMI e i browser
Innanzitutto, una osservazione importante. RMI è molto adatta per l'uso con i Web Browser e le applet. Consente infatti di spostare sul server molte operazioni normalmente svolte dal client. Questo è molto utile ad esempio con le applet che accedono ad un database. Invece di caricare sul client un driver JDBC da utilizzare per l'accesso al database, si può incapsulare la gestione del database in un server RMI che giri magari sulla stessa macchina del database. Questa soluzione presenta parecchi vantaggi che non staremo a discutere qui. Quello di cui invece vogliamo discutere è: con quali browser è possibile usare RMI. Innanzitutto naturalmente HotJava 1.0, interamente in Java e basato sul JDK 1.1, quindi il primo a supportare pienamente il JDK 1.1 e di conseguenza RMI. Un altro serio contendente è il Netscape Navigator 4.03. Questa versione però non consente ancora di utilizzare applet che utilizzano il modello ad eventi del JDK 1.1. Comunque Netscape è decisa a fornirlo con una prossima versione del Navigator, ed è già possibile scaricare dal sito di Netscape la patch 1.1 (attualmente in PreRelease 2). Per questo motivo l’applet che vedremo è stata sviluppata con Netscape Navigator 4.03 con la patch installata. RMI invece non è assolutamente supportata con Microsoft Internet Explorer 4.0. Nonostante le affermazioni di Microsoft che sul fatto che Explorer è conforme al JDK 1.1, in realtà sono state escluse dalla libreria standard proprio le classi di RMI. Un’altra differenza si riscontra nella virtual machine, che non supporta JNI. Ci sono molte altre piccole differenze che rendono difficoltosa la compatibilità della JVM Microsoft con il resto del mondo Java. Queste incompatibilità hanno portato Sun a citare in giudizio Microsoft per rottura di contratto di licenza del Java, che imponeva di mantenere la compatibilità con il Java così come viene definito da Sun. Microsoft a sua volta ha citato Sun per rottura della compatibilità all’indietro del JDK 1.1 rispetto al JDK 1.0. Comunque sia, queste diatribe sono al di fuori degli obiettivi di questo articolo, quindi ritorniamo all’argomento principale.

L’intefaccia remota
RMI consente di gestire un oggetto remoto, ed è una sorta di client/server trasparente: infatti invece di definire un protocollo e una codifica dei dati per far comunicare client e server, sul server "gira" un oggetto che viene "pilotato" dal client, invocando i suoi metodi. Il passaggio dei paramatri e il ritorno di risultati avviene utilizzando la serializzazione, che consente di scrivere e rileggere da uno stream un qualsiasi oggetto Java che supporti la serializzazione. Per prima cosa dobbiamo prendere un oggetto che vogliamo gestire da remoto, e suddividerlo in una interfaccia e una implementazione. L’interfaccia descrive l’oggetto dal punto di vista del client, mentre l’implementazione è l’oggetto effettivo che gira sul server. In realtà l’interfaccia è una istanza di uno stub che si trova sul client (istanziata quando viene aperta la connessione) la quale comunica con uno skel che gira sul server e invoca effettivamente i metodi dell’implementazione. Rimando ad un articolo introduttivo su RMI per meglio chiarire questa architettura. Quello che dobbiamo fare in pratica è definire l’interfaccia della nostra funzione di inversione:

package reverse;
 
 

public interface Reverse  extends java.rmi.Remote{

    public String reverse(String s) throws java.rmi.RemoteException;

}


Notiamo subito due cose: innanzitutto dobbiamo implementare java.rmi.Remote. Si tratta di una interfaccia la cui definizione è vuota. Come in altri casi simili (come java.io.Serializable) questa interfaccia serve da marcatore, che consente di dichiarare che una classe supporta una determinata caratteristica. In questo caso il programmatore specifica che l’interfaccia è utilizzabile per accedere ad un oggetto remoto. Una altra cosa da notare è che tutti i metodi sollevano una eccezione speciale, RemoteException, in grado di propagarsi attraverso la rete, trasportando eventuali eccezioni che si sono verificate sul server.
 

Serializzazione dei parametri
Una altra importantissima condizione, che in questo caso non si nota bene, è che sia i parametri dei metodi che i valori ritornati siano serializzabili. I tipi primitivi e quelli predefiniti escono dalla fabbiaca di per sè serializzabili, quindi nel nostro caso non ci sono grandi problemi. Come parametri e valori ritornati usiamo solo stringhe.
In generale però spesso si desidera passare e ritornare come oggetti remoti altre classi definite dall’utente. In questo caso è necessario rendersi bene conto di una cosa. Quando il client invoca un metodo remoto, effettua una copia dell’oggetto dal client al server, e lo stesso succede dal server al client quando viene ritornato il risultato. Nella invocazione normale si passa semplicemente un riferimento all’oggetto, e se un metodo lo modifica l’oggetto passato, questa modifica si ripercuoterà sul chiamante che al ritorno troverà l’oggetto modificato. Invece nell’invocazione di metodi remoti, il server ottiene una copia dell’oggetto che è indipendente dal quella passata come parametro, quindi modificare un oggetto non è sufficiente per ritornare un risultato: bisogna anche ritornarlo come risultato per fare sì che il chiamante veda le modifiche. Questo ha anche un impatto sull’efficienza. Non si può passare a cuor leggero come parametro un oggetto complesso, altrimenti si potrebbe anche copiare involontariamente una enorme quantità di dati (e in Java questo può succedere più facilmente di quanto non sembri). Poichè la copia di oggetti in andata (passaggio parametri) e ritorno (valori risultanti) avviene utilizzando il meccanismo della serializzazione è necessario rendere esplicitamente serializzabili le classi definite dall’utente. I tipi primitivi e molti classi della API 1.1 sono serializzabili. Per rendere serializzabili oggetti che hanno come campi solo oggetti serializzabili è sufficiente dichiarare implements java.io.Serializable. In casi più complessi è necessario provvedere ad una serializzazione ad hoc. Per esempio non è usare passare come argomenti di un oggetto remoto classi come java.io.InputStream che non sono serializzabili.

L’implementazione
Vediamo subito l’implementazione dell’oggetto remoto:

package reverse;
 
 

import java.rmi.*;

import java.rmi.server.*;

import java.rmi.registry.*;
 
 

public class ReverseImplextends UnicastRemoteObject implements Reverse{

        private static void log(String s) {

                System.out.println("ReverseServer "+s);

        }
 
 

        ReverseImpl()throws java.rmi.RemoteException {

            super(); }
 
 

        public String reverse(String s) throws java.rmi.RemoteException{

                log("reverse: "+s);

                StringBuffer sb = new StringBuffer();

                int n = s.length();

                for(int i = n-1; i>=0; i--)

                        sb.append(s.charAt(i));

                return sb.toString();

        }

}

Osservazioni:innazitutto la classe deve implementare l’interfaccia che vogliamo remotizzare, e deve essere una estensione di java.rmi.server.UnicastRemoteObject.
In realtà le classi del package java.rmi.server definiscono un framework per lo sviluppo di vari tipi di oggetti remoti, ma si tratta di classi astratte che oltre ad essere estese devono definire operazioni complesse. La sola classe server pronta all’uso è UnicastRemoteObject. Tutte le operazioni di inizializzazione e l’interazione con il registro (vedere più avanti) sono gestite dal costruttore di questo oggetto, richiamato dal super() di ReverseImpl. In realtà così com’è la dichiarazione del costruttore di default non sarebbe necessaria, è solo stata esplicitata per chiarezza e per evidenziare il fatto che il "lavoro sporco" di inizializzazione dell’oggetto è fatto dal costruttore della classe base di ReverseImpl.
 

Esportare l’oggetto
A questo punto il più è fatto. Ci rimane però di mettere in comunicazione il server con il client. Quindi occorre innanzitutto fare un server che costruisce rende disponibili ad altri l’implementazione. La classe server è la seguente:

package reverse;
 
 

import java.rmi.*;

import java.rmi.server.*;

import java.rmi.registry.*;
 
 

public class ReverseServer{

 private static void log(String s){

  System.out.println("ReverseServer "+s);

 }
 
 

public static void main(String[] args){

    System.setSecurityManager(new RMISecurityManager());

        try {

            log("Binding...");

            ReverseImpl rs = new ReverseImpl();

            Naming.rebind("reverse", rs);

            log("READY");

            }

        catch(Exception e) {

            e.printStackTrace();

            log(e.getMessage());

        }

    }

}

I passi da seguire sono: Il passo 1 è necessario perché un server di oggetti remoti è potenzialmente molto pericoloso. Infatti internamente vengono caricati dalla rete le classi stub, ed un hacker potrebbe imbrogliare le carte inviando una classe che sembra uno stub ma in realtà fa delle cose dannose. Per questo motivo il RMISecurityManager imposta possibilità di accesso ridotte per gli stub.
Il passo 2 è la creazione dell’oggetto server che deve essere creato prima di poter essere esportato al passo 3. La funzione di Naming.rebind chiama il registro di oggetti remoti e gli comunica che adesso esiste un oggetto chiamato reverse, il quale può essere invocato remotamente.
La registrazione degli oggetti sul registro viene fatta utilizzando un URL il cui formato completo è rmi://<host>:<port>/<object>. Abbreviando, i default sono, per <host>, localhost, e per <port> 1099, ma nulla vieta di registrare un oggetto in un registro che gira su un altro host o che ascolta in un’altra porta.
 

Importare l’oggetto
Esaminiamo adesso il codice dell’applet che utilizza l’oggetto remoto:

package reverse;
 
 

import java.applet.*;

import java.awt.*;

import java.awt.event.*;

import java.rmi.*;
 
 

public class ReverseClient  extends Applet implements TextListener{

 Label text = new Label("");

 TextField input = new TextField();

 Reverse reverse = null;
 
 

 public void init(){

  try {

    reverse = (Reverse)Naming.lookup("//"+getDocumentBase().getHost()+"/reverse");

  }

  catch(Exception e) {

    e.printStackTrace();

    add(new Label(e.getMessage()));

    return;

  }

  setLayout(new BorderLayout());

  add("North", text);

  add("South", input);

  input.addTextListener(this);

 }
 
 

 public void textValueChanged(TextEvent te){

  try {

    text.setText(reverse.reverse(input.getText()));

  }

  catch(Exception e) {

    e.printStackTrace(); }

 }

}

Trascurando i dettagli del funzionamento dell’applet, che non sono pertinenti agli scopi dell’articolo (e poiché usano il nuovo modello ad eventi sono utilizzabili solo in un browser completamente conforme al JDK 1.1), il punto significativo è solo

    reverse = (Reverse)Naming.lookup("//"+getDocumentBase().getHost()+"/reverse");
 

Ricordiamo che le applet possono aprire connessioni di rete soltanto all’host da cui sono state scaricate, e questo spiega il getDocumentBase().getHost(), che si aspetta di trovare il registry degli di oggetti remoti sullo stesso host del Web Server. Dopodichè, utilizzando Naming.lookup(<rmi-url>) viene localizzato l’oggetto remoto, il quale deve risiedere anche esso (per i soliti limiti delle connessioni TCP) sulla stessa macchina del server. Infine l’oggetto ritornato viene convertito nell’interfaccia che consente di usarlo. Il resto è un semplice uso dell’oggetto, come se fosse un oggetto locale. Non dobbiamo però dimenticare le implicazioni di efficienza legate alla copia degli oggetti via serializzazione quando avviene l’invocazione.

Compilazione e avvio
Abbiamo riscontrato qualche dolente nota avviene in fase di compilazione e avvio. Innanzitutto, non basta definire il client e il server, occorre anche generare le classi stub e skel, reverse.ReverseImpl_Stub e reverse.ReverseImpl_Skel. A costruirle pensa il compilatore rmic ma sembra non volerne sapere di funzionare (almeno quello che avevamo noi) senza specificare esplicitamente sulla riga di comando sia il classpath che la directory di destinazione. Ecco il batch che abbiamo utilizzato e che ci ha portato a generare dopo innumerevoli tentativi le classi nella directory di destinazione corretta:
 


Il batch tradisce che abbiamo utilizzato il JBuilder, col quale è compreso il JDK 1.1.2, ma funziona pure con il JDK 1.1.3. Come parametro per rmic occorre specificare la classe dell’oggetto server, specificato come nome di classe. Il risultato della compilazione è la comparsa delle due classi Stub e Skel nella sottodirectory reverse, dove devono stare. Se non si specifica -d . le classi compaiono nella directory corrente! Misteri di Sun...
Dunque abbiamo tutto, possiamo far partire il server. Prima, però del server occorre avviare il registro di oggetti remoti, rmiregistry. Siamo incappati in due nuovi problemi. Primo problema: il programma server non parte se non si imposta esplicitamente il classpath. Normalmente col JDK 1.1, digitando java <classe> il classpath viene ricavato direttamente dall’interprete, senza doversene preoccupare. Invece java reverse.RemoteServer termina dichiarando che non trova la classe Skel. Il problema non siamo riusciti a definirlo con esattezza, comunque la soluzione è stata anche questa volta di definire esplicitamente il classpath con uno switch sulla riga di comando. Ma non è finita qui. Dovendo attivare sia il rmiregistry che il programma, ci è sembrato naturale inserire in un batch sia l’avvio del registry che l’avvio del server. Invece il sistema si pianta. Dopo vari tentativi abbiamo capito che il problema si verifica se il server parte troppo presto. Probabilmente il registry fa delle inizializzazioni, e se, prima che terminino, un oggetto tenta di registrarsi, succede il finimondo. La soluzione, banale, è quella di fare una pausa dopo l’avvio del registry.

Conclusioni
Abbiamo visto dunque come RMI è una realtà tangibile, seppur piuttosto primitiva. L’uso è relativamente semplice ma ci sono delle limitazioni che dimostrano come questa tecnologia sia ancora sostanzialmente agli albori. Per esempio, i problemi dell’rmic e rmiregistry mostrano come il sistema abbia ricevuto poco feedback dagli utenti. Tra l’altro, come minimo il rmiregistry deve poter essere utilizzabile come servizio di Windows NT. Notiamo anche che, se il rmic è fornito insieme a tool quali il JBuilder, in quest’ultimo è assente ogni forma di integrazione tra l’IDE e rmic. Ci sono anche voci che Sun abbia deciso di abbandonare RMI in favore di CORBA, ma in tal caso non si spiegherebbe la causa contro Microsoft per l’esclusione proprio di RMI dal Java di Microsoft (che non è la sola incompatibilità, comunque è la più eclatante). È più probabile che RMI evolverà per integrarsi con CORBA, diventando forse uno dei mattoni con cui sarà costruita l’integrazione.
 
 
 
 

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it