MokaByte
Numero 13 - Novembre 1997
|
|||
|
|
||
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;Osservazioni:innazitutto la classe deve implementare l’interfaccia che vogliamo remotizzare, e deve essere una estensione di java.rmi.server.UnicastRemoteObject.
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();
}
}
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;I passi da seguire sono:
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());
}
}
}
Importare
l’oggetto
Esaminiamo adesso
il codice dell’applet che utilizza l’oggetto remoto:
package reverse;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
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(); }
}
}
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 ricerca
nuovi collaboratori
|
||
|