MokaByte Numero 07 - Aprile 1997

 
La Remote Method 
Invocation API
 
di
Fabrizio Giudici 
 

 



   Java è diventato "il linguaggio di Internet" ed oggi vanno molto di moda i programmi distribuiti. In questo articolo esamineremo la Remote Method Invocation API (RMI API), uno strumento di grande interesse per lo sviluppo di applicazioni distribuite.



Se si vuole progettare un’applicazione distribuita con un linguaggio a basso livello ("programming on bare bones", come dicono gli anglosassoni), per esempio in C, bisogna occuparsi direttamente almeno delle seguenti operazioni fondamentali:
definire un protocollo di comunicazione per il trasferimento dei dati;
definire un ulteriore protocollo per codificare le varie "azioni" da eseguire remotamente
gestire direttamente i socket per la comunicazione.
1. Serializzazione

Tipicamente quando un’applicazione diventa sofisticata la sua struttura dati diventa sempre più articolata; ad esempio, in un tipico database ad oggetti, per definizione, possono essere contenuti dati di diversa natura. Siccome l’utilità di un database è rendere disponibili i dati che contiene, si pone il problema di tradurre una struttura dati complessa in una sequenza di byte che possa essere scritta in un file o spedita su una connessione di rete, per poterlo ricostruire in un secondo momento (oggetto persistente) o per ottenerne una copia su una macchina diversa. Queste operazioni si chiamano, rispettivamente, serializzazione e deserializzazione, anche se, in pratica, si usa il primo termine per comprenderle entrambe.

Serializzare un oggetto è un’operazione apparentemente facile: basta scrivere su uno stream (termine generale che utilizzeremo per indicare un file od un socket) tutti i campi che esso contiene, uno dopo l’altro; per deserializzare l’oggetto è sufficiente rileggere gli stessi campi con lo stesso ordine. Sono quindi operazioni meccaniche e ripetitive, che però possono essere insidiose ed introdurre proditoriamente seri bug qualora vengano codificate manualmente. Tanto per fare un esempio, bisogna ricordarsi di mantenere l’ordine con cui i vari campi vengono scritti e letti sullo stream; e cosa succede, poi, se un oggetto contiene un altro oggetto?

Più in generale, se si vuole serializzare un grafo contenente oggetti per i quali esistono riferimenti multipli, l’oggetto in questione va effettivamente serializzato solo la prima volta, mentre per le successive è sufficiente scrivere un semplice riferimento. A questo punto appare evidente la necessità di definire un formato ben definito (un protocollo) con il quale i dati vengono scritti sullo stream. E tale formato deve essere standard, in modo che si possano costruire applicazioni a partire da diverse librerie di componenti senza doversi preoccupare della loro compatibilità.

Java risolve tutti questi problemi automaticamente: è sufficiente che una classe asserisca di implementare l’interfaccia Serializable (che peraltro non contiene alcun metodo, e quindi non impone nessun lavoro aggiuntivo al programmatore) perché essa possa essere serializzata grazie alle classi ObjectOutput ed ObjectInput, come si vede nell’esempio:


MyObject può essere di complessità arbitraria, è sufficiente che essa, al pari di tutti gli oggetti che contiene, implementi l’interfaccia Serializable. Quasi tutti gli oggetti standard di Java soddisfano questa proprietà, così è necessario occuparsi esplicitamente solo di poche eccezioni.

Come fa Java a serializzare automaticamente gli oggetti? È molto semplice: le ultime versioni del JDK contengono una API, chiamata Reflection Core, con cui è possibile ispezionare dinamicamente un oggetto per ottenere una lista dei campi che esso contiene e per accedere ai loro contenuti. Utilizzando il Reflection Core, i progettisti di Sun hanno "semplicemente" implementato, una volta per tutte, un algoritmo in grado di attraversare un grafo di oggetti e serializzarlo in modo consistente. Ancora una volta "write once and run everywhere"

2. La Remote Method Invocation API (RMI)

La RMI API è un insieme di classi che permettono di sviluppare applicazioni Java distribuite in rete senza preoccuparsi di questi dettagli tecnici di "basso livello", che vengono gestiti automaticamente, secondo metodologie standard definite dai progettisti di Sun e senza dover ricorrere a tool di sviluppo esterni. La RMI API è consistente con il resto dell’architettura Java per quanto riguarda la portabilità, la sicurezza, la gestione degli errori con le eccezioni, la gestione della memoria con la garbage collection, il caricamento dinamico delle classi.
I vantaggi della RMI API sono evidenti: essa fornisce un’astrazione del concetto di oggetto remoto e fa risparmiare tempo al programmatore, che può quindi dedicarsi sulla parte "creativa" del proprio lavoro. Probabilmente a questo punto la frase suona ripetitiva… ma stiamo ancora seguendo la logica "write once and run anywhere"!

Vediamo ora con un semplice esempio come è possibile implementare con RMI una semplice applicazione distribuita.

2.1. Primo step: definizione ed implementazione degli oggetti remoti
Nel gergo di RMI, si definisce oggetto remoto un oggetto i cui metodi possono essere richiamati da una macchina virtuale diversa da quella in cui l’oggetto risiede. Un’interfaccia remota è invece un’interfaccia che dichiara i metodi usati da un oggetto remoto. Questo modello è illustrato in figura 1.


Figura 1: Modello ad oggetti della RMI
 

Per poter utilizzare un oggetto con la RMI API, per prima cosa è necessario scrivere la sua interfaccia remota definendo i metodi che esso utilizza (non è necessario definire i metodi che non vengono invocati remotamente).

Supponiamo quindi di avere un oggetto che riceve dei parametri, esegue un calcolo e restituisce un risultato:

Per semplicità, nell’esempio viene effettuata una concatenazione di due stringhe; in pratica non ci sono limitazioni e si possono passare argomenti e risultati di complessità arbitraria (quindi anche interi grafi di oggetti), a patto che essi siano composti di oggetti serializzabili.
Supponendo di voler usare un oggetto della classe MyServer con la RMI API, per prima cosa dobbiamo scrivere la sua interfaccia remota:
 

Ogni interfaccia remota deve estendere java.rmi.Remote (che peraltro è vuota, cioè priva di metodi, ma viene utilizzata dal runtime per verificare la consistenza di quanto stiamo facendo) ed ogni metodo deve dichiarare di poter lanciare l’eccezione java.rmi.RemoteException, che viene utilizzata per segnalare un eventuale errore sul canale di comunicazione. Infatti, se è vero che la RMI API consente di utilizzare oggetti remoti con la stessa semplicità con cui vengono utilizzati oggetti locali, è da ricordare che comunque ogni invocazione di un metodo su un oggetto remoto coinvolge una serie di operazioni più o meno complesse che possono fallire (ad esempio per un problema sulla rete).

Una volta scritta l’interfaccia, bisogna modificare la classe originale:


Oltre a dichiarare di implementare l’interfaccia remota, bisogna anche estendere UnicastRemoteServer, una classe predefinita che fornisce il supporto per referenziare l’oggetto. UnicastRemoteServer discende da altre due classi, RemoteServer (una superclasse comune per tutte le implementazioni di oggetti remoti) e RemoteObject (che semplicemente ridefinisce hashcode() e equals() in modo da far funzionare correttamente i confronti tra oggetti remoti). La presenza di RemoteServer fa capire che possono esistere implementazioni di oggetti remoti diverse da UnicastRemoteServer, anche se per il momento quest'ultima è l'unica supportata.

2.2. Secondo step: generazione di stub e skeleton
E evidente che l’oggetto MyServer verrà istanziato ed andrà in esecuzione sul lato server della nostra connessione. Sul lato client, invece, non ha senso istanziare direttamente un oggetto di tipo MyServer: infatti si otterrebbe semplicemente una seconda copia di MyServer, completamente indipendente e scollegata da quella che già gira sul server, mentre noi vogliamo invece che entrambe le macchine referenzino un’unica istanza.

Tuttavia, sul lato client deve pur esistere un oggetto sul quale sia possibile invocare il metodo compute()… Anzi, per essere precisi, quest’oggetto deve anche occuparsi di generare un messaggio sul canale di comunicazione per "avvisare" il server che è stata richiesta un’operazione, attendere la risposta con i risultati e restituirli al chiamante. Questo tipo di oggetto viene chiamato stub (termine in quest’accezione traducibile in italiano con la parola "surrogato"). Il programma sul client, di fatto, "crede" di avere a disposizione il vero oggetto MyServer, mentre in realtà opera con un suo "surrogato".

Parimenti, sul server deve esistere un oggetto in grado di ricevere il messaggio, interpretarlo, eseguire l’operazione sull’oggetto di tipo MyServer, raccogliere il risultato e rispedirlo indietro. Questo tipo di oggetto viene chiamato skeleton (scheletro). Sul server, quindi, l’oggetto MyServer "crede" di interagire con un altro oggetto locale, mentre in realtà ha a che fare con lo skeleton che fa la "controfigura" dell’oggetto remoto (figura 2).
 
 


Figura 2: Architettura della RMI
 
 
 

Riassumendo, ogni oggetto remoto deve avere, oltre alla sua implementazione, un’interfaccia, uno stub e uno skeleton. Questi ultimi due vengono automaticamente generati da un apposito compilatore, rmic, che è incluso nel JDK a partire dalla versione 1.1. Se compiliamo MyServer.java e lanciamo il comando


otteniamo automaticamente MyServer_stub.class e MyServer_skel.class. Da notare che rmic opera direttamente sul codice compilato (MyServer.class) e non sul sorgente (MyServer.java).

2.3. Terzo step: registrazione e referenziazione di un oggetto remoto
A questo punto abbiamo tutto il necessario per far funzionare in remoto l’oggetto MyServer, ma bisogna definire le modalità di collegamento tra client e server.

Sul lato server, il programma che gestisce MyServer deve "comunicare al mondo" che possiede un oggetto disponibile per l’invocazione remota. Per questo scopo è sufficiente richiamare un metodo statico di una classe di RMI, bind(), che associa l’istanza di MyServer ad un nome che verrà usato per identificarlo:
 


Ovviamente la registrazione può fallire, ed in tal caso un’opportuna eccezione viene lanciata da Naming.bind(). Da notare che il nome che viene passato tra apici a Naming.bind() è arbitrario, cioè non deve necessariamente coincidere con il nome della classe di cui l’oggetto remoto è istanza.

Le registrazioni vengono gestite da un apposito programma, rmiregistry, che deve essere stato preventivamente lanciato sul lato server. Questo programma è di fatto un demone, che si mette anche in ascolto su una porta (per default la 1099) per rispondere alle eventuali interrogazioni in rete di client che vogliono ottenere un reference ad un oggetto già registrato. Queste interrogazioni vengono effettuate sul lato client subito prima di effettuare il collegamento con l’oggetto remoto:
 


L’argomento da passare al metodo statico Naming.lookup() è una URL contenente il nome della macchina che ospita l’oggetto remoto ed, ovviamente, il nome con cui l’oggetto è stato registrato; non ha senso specificare il protocollo usato per la connessione, in quanto questo è un dettaglio implementativo di cui si occupa la RMI API.
Anche in questo caso gli eventuali errori vengono segnalati con opportune eccezioni (da ricordare che anche server.compute() può fallire e lanciare un’eccezione di tipo java.rmi.RemoteException).
Si noti che l’oggetto server non è dichiarato come MyServer, ma come MyServerInterface. Infatti, in virtù di quanto si è detto nel paragrafo precedente, lookup() sul client restituisce di fatto un MyServer_stub e non un oggetto di tipo MyServer.

2.4. Class Loader, Security Manager e Garbage Collection
Java carica dinamicamente le classi necessarie all’invocazione dei metodi di oggetti remoti, sia per quanto riguarda le classi che fanno effettivamente parte della RMI API, sia per le coppie stub/skeleton di tutti gli oggetti remoti coinvolti. Dal momento che stub e skeleton risiedono sulla macchina server, essi devono essere scaricati dalla rete: di quest’operazione si occupa lo speciale class loader di RMI, java.rmi.RMIClassLoader, usando come URL sorgente il contenuto della proprietà java.rmi.server.codebase. Nel caso il programma client sia un applet, tale proprietà è automaticamente impostata a partire dall’indirizzo dell’host da cui l’applet è stato scaricato; nel caso di un’applicazione essa va invece settata esplicitamente, per esempio da riga di comando:
 


Avendo a che fare con classi scaricate da rete, Java si pone il problema della sicurezza. A questo scopo, la RMI API definisce un apposito security manager, chiamato java.rmi.SecurityManager, che, quando si scrive un’applicazione, va esplicitamente installato prima di tentare una connessione con un oggetto remoto:


Per gli applet ciò non è necessario, in quanto è il browser, nel caso supporti la RMI, ad aver già fornito un opportuno security manager che include automaticamente le funzionalità richieste. D’altronde questa è l’unica soluzione possibile, perché in un browser non è possibile cambiare il security manager di default, per evidenti motivi di sicurezza.

A questo proposito va sottolineato che le attuali versioni dei browser più diffusi, come Netscape Navigator e Microsoft Internet Explorer, non supportano ancora la JDK 1.1, e quindi la RMI, a meno che non si intervenga manualmente sulla loro installazione, aggiornando le loro librerie di classi Java. È una situazione temporanea, perché essi verranno chiaramente aggiornati in futuro, ma se si vogliono fare esperimenti subito può valer la pena installare l’ultima versione del browser HotJava, che essendo completamente scritto in Java JDK 1.1, ovviamente supporta pienamente la RMI API.

La RMI API implementa un meccanismo di garbage collection distribuita basata sul conteggio di riferimenti: ciò vuol dire che il runtime sa in ogni istante quanti client stanno referenziando un dato oggetto remoto. Dal punto di vista del programmatore, tutto funziona come al solito: ogni oggetto viene eliminato solo quando non è più referenziato né localmente, né remotamente, e prima di essere definitivamente distrutto viene richiamato il metodo finalize(). Volendo è anche possibile ricevere una segnalazione automatica quando non ci sono più riferimenti esterni ad un oggetto remoto. A tal scopo è sufficiente implementare l’interfaccia java.rmi.unreferenced ed il metodo unreferenced(), che verrà automaticamente richiamato per segnalare l’evento.

3. RMI, CORBA ed altre tecnologie emergenti

A proposito di RMI, una domanda ricorrente tra i programmatori ad oggetti che si stanno interessando a Java è: "Come si pone RMI API nei confronti di CORBA (Common Object Request Broker Architecture), la ben nota architettura standard frequentemente utilizzata nelle grosse applicazioni distribuite ad oggetti?" Bene, sono due soluzioni diverse (ed alternative) allo stesso problema. Il punto fondamentale è che CORBA vuol essere un’architettura per sistemi eterogenei, cioè è in grado di mettere in comunicazione oggetti tra programmi scritti in differenti linguaggi di programmazione (ad esempio C++ e SmallTalk), mentre RMI API è una soluzione semplice e compatta per collegare tra di loro applicazioni Java. Quindi se si vuole scrivere un’applicazione distribuita eterogenea, diciamo parte in Java e parte in C++, RMI API non è la soluzione adatta: bisogna integrare Java con CORBA, ed allo scopo esiste un sistema di sviluppo apposito, Java IDL, sviluppato da Sun (http://splash.javasoft.com), od alternativamente Joe, sempre di Sun (http://www.sun.com/solaris/neo/joe). Non se ne può discutere in questo articolo per problemi di spazio.

Un’ultima considerazione: una delle tecnologie emergenti nel mondo di Internet è quella dei cosiddetti "agenti mobili", in parole povere programmi (o meglio, oggetti) in grado di "migrare" in rete da una macchina all’altra nel bel mezzo della loro esecuzione. La RMI API non è una tecnologia adatta per l’implementazione di agenti mobili: se è vero che con RMI oggetti residenti su macchine diverse possono comunicare tra loro, essi però non si spostano mai, ma consumano la loro vita sulla macchina dove sono stati creati. Per gli agenti mobili in Java esistono numerose API, delle quali la più significativa è la J-AAPI (nota con il nome di Aglet) sviluppata da IBM. Gli agenti mobili vengono trattati da un’altra serie di articoli, sempre su MokaByte.

Anche per questa volta dobbiamo concludere, a causa di problemi di spazio. Nella prossima puntata vedremo una delle più importanti applicazioni della RMI API: il suo supporto all’interfacciamento di un database con il World Wide Web, utilizzando la tecnologia JDBC (Java DataBase Connectivity).
 
  

 

MokaByte rivista web su Java

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