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:
-
Creare
l' interfaccia remota
-
Estendere
l’interfaccia java.rmi.Remote
-
Sollevare
l’eccezione java.rmi.Remote.Exception per ogni metodo dell’interfaccia
-
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")
:
-
implementare
l’interfaccia MyServerInterface
-
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
|