Prologo
Perché
accoppiare questi due argomenti? Quando si parla di connessioni in rete
di solito vengono in mente le Socket, attraverso cui possiamo trasmettere
byte avanti e indietro. Raramente però i neofiti (o, meglio, chi
si avvicina per la prima volta a Java arrivando da linguaggi meno evoluti)
si accorgono che sono disponibili funzionalità abbastanza potenti
per trasmettere sia tipi primitivi (di cui in questo articolo però
non parleremo) che oggetti interi, anche di tipo complesso, cioè
oggetti che puntano ad altri oggetti. E' inoltre possibile invocare un
metodo su un oggetto che è stato istanziato in un'altra JVM (quindi
potenzialmente su un host diverso da quello in cui ci troviamo noi) quasi
come se fosse un metodo di un oggetto locale (il quasi è d'obbligo
e capiremo il perchè). Tutto ciò viene fornito dalle API
di Java in maniera pressochè trasparente ed entrambi i meccanismi
si appoggiano sulle Socket di base per poter operare (di fatto realizzano
dei protocolli di comunicazione). Ritornando alla domanda iniziale, perchè
unire le due cose? Perchè l'RMI si basa pesantemente sulla
serializzazione e non si può anche solo pensare di usarlo senza
sapere le conseguenze e i vincoli da rispettare per farlo funzionare.
Questa
trattazione è articolata in una forma "quasi per punti", senza dilungarsi
troppo in parole e mettendo molti pezzi di codice e schemi. Però
non aspettatevi un tutorial sulla serializzazione o sull'RMI (ne trovate
di belli sul sito della JavaSoft), questo articolo vuole essere uno stimolo
per aprire discussioni tecnico-filosofiche su grafi di oggetti, class loading,
etc. Spero di darvi da pensare ;-)
La serializzazione
Scopo:
trasmettere un’istanza su uno Stream (quindi sia via rete che su file)…
o meglio... trasmettere un GRAFO di istanze (oggetti che puntano ad altri
oggetti).
Ci
sarà da discutere sui meccanismi per non distruggere il grafo!
Come
si usa?
Chi
trasmette (o scrive su file) deve costruire un ObjectOutputStream
e
poi utlizzare il metodo writeObject(Object o), chi riceve (o legge
da un file) deve creare un ObjectInputStream e usare il metodo Object
readObject(). Vediamo i due esempi (in rete).
La serializzazione:
la trasmissione
...
try{
Socket s=new Socket(host,port);
ObjectOutputStream
o=
new ObjectOutputStream(s.getOutputStream());
o.writeObject(new
Message(“CIAO”));
}
catch(…)
{...}
...
La serializzazione:
la ricezione
...
try{
Socket s=new Socket(host,port);
ObjectInputStream
i=
new ObjectInputStream(s.getInputStream());
System.out.println(i.readObject());
}
catch(…)
{...}
...
NOTA:
dal lato del ricevitore entra in gioco il ClassLoader (su cui poi torneremo)
Cosa viene
trasmesso?
Quando
mando un oggetto complesso l'ObjectOutputStream "segue i link", cioè
oltre a mandare l'istanza che gli ho passato segue tutti i reference che
trova nell'oggetto e manda anche gli oggetti puntati (ricorsivamente! quindi
attenzione allo spaghetti code! rischiate di mandar giù tutto
ciò che avete istanziato!).
I
problemi (almeno i dubbi ;-) sorgono se ho una situazione tipo questa:
Arriva
così…
… o
così?
Per
fortuna arriva correttamente, cioè esattamente come era in partenza.
Infatti l'ObjectStream ha memoria di ciò che trasmette per cui una
ritrasmissione viene riconosciuta e trattata come tale (viene passato un
handle che dice: questo è già passato di qui… ecco il ref).
Quindi gli oggettini puntati sia da A che da B vengono effettivamente trasmessi
UNA VOLTA SOLA.
E
se adesso vogliamo fare così?
Cioè
ho una situazione in cui l'istanza che volgio trasmettere è sempre
la stessa, ma ne modifico lo stato e voglio rispedirla. Cosa viene trasmesso?
Per
SFORTUNA si comporta come prima... [repetita juvant] l'ObjectStream ha
memoria di ciò che trasmette per cui una ritrasmissione viene riconosciuta
e trattata come tale (viene passato un handle che dice: questo è
già passato di qui… ecco il ref).
Volendo
quindi usare sempre lo stesso oggetto da “palleggiare” avanti e indietro,
lo vedrò muoversi effettivamente SOLO LA PRIMA VOLTA!
Come
posso fare? Uso un metodo, reset(), che svuota la tabella dell’ObjectStream
e quindi lo "riporta allo stato iniziale", non ha più traccia degli
oggetti passati e quindi se rimando la stessa istanza questa viene ritrasmessa.
Attenzione!
Quando uso il reset() su un ObjectStream non posso più usarlo
per mandare oggetti complessi (crosslinked) perchè il grafo ne risulterebbe
rovinato (vedere l'immagine coi "gemelli"). Ne segue che devo decidere
"a priori" come voglio usare un certo ObjectStream: per mandare grafi di
oggetti o oggetti che cambiano stato.
Quali
sono i vincoli per poter “trasmettere/salvare” un’istanza?
Un
oggetto, per essere trasferibile, deve essere (quindi implementare l’interfaccia)
Serializable
o Externalizable (entrambe nel package java.io).
Serializable
è una “tag interface”, cioè non ha metodi, serve a "flaggare"
un oggetto come trasferibile attraverso uno stream.
Una
classe è serializzabile/esternalizzabile se lo dichiara esplicitamente
o deriva da una classe che lo è già e… NON ROMPE IL “CONTRATTO”!
Cioè non basta dichiarare di essere trasmissibile, bisogna anche
esserlo veramente!!!
Come
faccio a sapere se la mia classe è veramente serializzabile?
La definizione è ricorsiva:
1)
una classe è serializzabile se TUTTI (quelli non “transient”)
i suoi attributi lo sono;
2)
i tipi primitivi sono serializzabili.
Ergo,
una classe di soli tipi primitivi è già serializzabile (ma
devo sempre dichiararla tale), mentre una classe mista (con dei link) lo
è se tutto ciò che punta lo è a sua volta.
A
questo punto c'è da domandarsi quali oggetti NON sono serializzabili...
in generale quelli che usano risorse “native” della macchina, ad es. i
Thread e i ResultSet. Sulla documentazione del JDK c’è sempre scritto
se lo sono (basta vedere se implementano Serializable/Externalizable direttamente
o indirettamente). Sorprendentemente quasi tutte le AWT sono serializzabili,
nonostante usino oggetti nativi (i peer). In realtà non è
così sorprendente, infatti i peer vengono (ri)creati al momento
della visualizzazione di un component... e vengono ignorati durante la
serializzazione poichè sono considerati transienti.
Transienti?
Che vuol dire? Se voglio rendere una classe serializzabile ANCHE se contiene
attributi NON serializzabili… posso farlo, basta poter indicare quei campi
come "ignorabili" dal meccanismo di serializzazione. A tali campi basta
aggiungere il modificatore transient. Come in:
public
class DaTrasmettere implements Serializable{
int count; // serializzabile
String messaggio; // serializzabile
transient Thread t; // NON serializzabile !!!
}
Tutti
i campi transient vengono saltati durante la serializzazione, per
cui bisogna prevedere qualche tipo di reinizializzazione all’arrivo...
up to you, sorry :-(
Serializable/Externalizable
La differenza
fra le due definizioni (esplicitate dalle due interfacce) consiste semplicemente
nel fatto che, se un oggetto è Serializable, quando viene trasmesso
l'ObjectStream lo converte in uno stream di byte (da cui il termine serializzazione
;-) usando il meccanismo interno automatico (protocollo definito nella
specifica, vedere riferimenti bibliografici). Se invece un oggetto è
Externalizable verrà convertito in uno stream attraverso due metodi
(readExternal e writeExternal, che devono essere obbligatoriamente forniti
dallo sviluppatore) chiamati dallo stream per leggere e scrivere l'oggetto
stesso, in questo caso con un protocollo implementato esplicitamente da
chi ha sviluppato la classe. Externalizable serve se NON voglio usare il
meccanismo standard e voglio leggere e scrivere il MIO oggetto alla MIA
maniera (“la classe è mia e me la gestisco io” :-)
La ricezione
dell’oggetto, una nota…
Quando
un'istanza esce da uno stream, tra le informazioni ad essa associate c’è
il nome della classe, che, se non è ancora stata caricata, va caricata
(e questo è normale)... ma ci troviamo (se ad esempio lavoriamo
con la rete) SU UNA MACCHINA DIVERSA!!! Il CLASSPATH potrà anche
essere uguale (a quello della JVM di partenza), ma punta a file potenzialmente
diversi (sono copie)! ATTENZIONE QUINDI ALLE VERSIONI DEI .class !!! Con
un Applet NON c’è problema, le classi vengono scaricate dal server
(viene usato l'URLClassLoader). Col ClassLoader standard ciò non
avviene, le classi devono essere presenti sul disco locale. Potete comunque
installare dei class loader custom per scaricare classi da un po' ovunque
;-)
Da
quel che ho potuto leggere nella documentazione, per il momento non c'è
la possibilità di spedire le classi (intendo proprio i .class) sullo
stesso ObjectStream usato per le istanze, anche se è evidente che
ci stanno pensando (alcuni metodi sono messi lì in previsione).
Ovviamente ci sarà da pensare a eventuali problemi di sicurezza...
potrei infatti "sovrascrivere" (non in senso letterale, ma sarebbe possibile
far caricare alla JVM un "fake") una classe con una "classe virus" facendo
eseguire codice pericoloso per la JVM bersaglio ;-)
Riferimenti
bibliografici
-
la specifica
ftp://ftp.javasoft.com/docs/jdk1.2/serial-spec-JDK1.2.pdf
-
le FAQ
http://java.sun.com/products/jdk/serialization/faq
RMI
Scopo:
poter chiamare un metodo su un oggetto che NON si trova nella stessa JVM.
Come
si usa: il Client NON istanzia l’oggetto, ma se lo fa dare dal rmiregistry
della macchina remota attraverso la chiamata di Naming.lookup(“URL dell’oggetto”),
dove l'URL dell'oggetto è qualcosa che assomiglia a: “rmi://HostnameOrIpAddr/ObjectName”.
Una volta ottenuto il reference all'oggetto è possibile chiamare
un metodo, tale metodo viene eseguito nella JVM REMOTA! (il carico di CPU
è remoto) attraverso la normale dot notation (in realtà
c’è in più il try&catch).
RmiClient
(esempio)
public static
void main(String argv[]) {
try{
RemoteStampaI
r = (RemoteStampaI) Naming.lookup("Stampa");
System.out.println(r.stampa(”dal
client"));
}
catch(Exception
e) {
e.printStackTrace();
}
}
Il
Server invece ISTANZIA l’oggetto e lo rende disponibile (lo pubblica) attraverso
il rmiregistry con la chiamata di Naming.bind(“ObjectName”,remoteObj)…
e basta, si mette in attesa di clienti...
RmiServer
(esempio)
public
static void main(String argv[]) {
try{
RemoteStampa r=new RemoteStampa();
Naming.bind("Stampa",r);
System.out.println("il server Stampa e' pronto.");
}
catch(Exception
e){
e.printStackTrace();
}
}
Il meccanismo
dell'RMI funziona attraverso due classi che realizzano l'effetiva comunicazione
(via Socket) tra client e server. Queste due classi, lo Stub e lo Skeleton,
vengono generate automaticamente da un compilatore apposito (rmic) che
si mangia la classe remota, genera due file (..._Stub.java e ..._Skel.java)
e li compila. Il meccanismo è quello del proxy (vi rimando al mio
articolo sul pattern proxy che trovate su MokaByte di Giugno).
Passaggio
dei parametri
I metodi
di un oggetto remoto possono accettare parametri e ritornare valori. Ogni
parametro che passo o mi faccio tornare deve essere Serializable (infatti
viene trasmesso attraverso un ObjectStream). Attenzione però che
cambia la semantica rispetto alla situazione "locale", infatti un oggetto
remoto NON si sposta mai dalla JVM in cui è stato istanziato, mentre
tutti gli altri tipi di oggetti vengono copiati (viene prorpio clonato
l'oggetto, per cui se un metodo modifica lo stato dell'oggetto stesso il
chiamante non se ne accorge).
Come costruisco
un oggetto “remoto” ?
Nell'ordine
-
importo
java.rmi.* e java.rmi.server.*
-
estendo
UnicastRemoteObject
-
metto
un costruttore con “throws RemoteException”
-
ogni metodo
“remoto” con “throws RemoteException”
-
scrivo
l’interfaccia (che deve estendere Remote)
Un oggetto
remoto
import
java.rmi.*;
import
java.rmi.server.*;
public
class RemoteStampa extends UnicastRemoteObject
implements
RemoteStampaI{
public
RemoteStampa() throws RemoteException {}
public
String StampaCiao(String s) throws RemoteException{
System.out.println("Ciao "+s); // questa si vede sul server
return "Ho salutato "+s;
}
}
Un oggetto
remoto (l’interfaccia)
import
java.rmi.*;
import
java.rmi.server.*;
public
interface RemoteStampaI extends Remote
{
public
String StampaCiao(String s)
throws RemoteException;
}
Come compilo
un oggetto “remoto” ?
Per OGNI
classe che estende UnicastRemoteObject bisogna generare una
coppia di classi, lo Stub e lo Skeleton, per cui PRIMA compilo tutto normalmente
(javac), POI uso
rmic NomeClasse
per generare
stub e skel, vengono generati dei .class che prendono i nomi:
<NomeClasse>_Stub.class
<NomeClasse>_Skel.class
usando
l'opzione -keepgenerated di rmic vedo i .java generati, cioè
non vengono cancellati dopo la compilazione.
Come viene
trovato l’oggetto sull’altra macchina?
Serve
l'rmiregistry, che di solito si lancia a parte (“start rmiregistry”, su
windoze), ma è anche possibile crearlo/raggiungerlo da dentro java,
usando la classe LocateRegistry. L'rmiregistry è un server che ascolta
sulla porta 1099 (programmabile). Quando un client deve trovare un oggetto
registrato (cioè in ascolto su una porta assegnata dinamicamente
all'atto della registrazione) contatta il rmiregistry e si fa dire dove
è l'oggetto.
Garbage
Collection
Cambia
anche un pochettino il concetto di garbage collection nel caso di oggetti
remoti. Un oggetto remoto resta in vita se ci sono reference che lo puntano
(e questo è normale), ma anche se ci sono remote reference
che lo puntano (e questo è speciale ;-). Altra cosa, finchè
un remote object è registrato nel rmiregistry NON viene mai distrutto.
Qualche
problemino sorge se la JVM che possiede un remote link ad un remote object
non riesce a comunicare correttamente la dereferenziazione del proprio
link (per esempio nel caso di una terminazione improvvisa). In questi casi
il remote object crede di essere ancora puntato da qualcuno e non viene
mai distrutto... :-(
Factory
method
Non tutti
gli oggetti remoti che voglio usare devono passare per la registrazione
presso il rmiregistry. Posso farmi creare un oggetto remoto da un altro
oggetto remoto (attraverso un factory method), in questo caso ottengo il
reference senza passare per il meccanismo bind-lookup.
Considerazioni
finali
RMI è
comodo, molto simile alla chiamata di metodo locale, però NON si
deve trattare una chiamata RMI alla stessa stregua di una locale, sia per
motivi di prestazioni che di comportamento, ad esempio un oggetto Java
c’è SEMPRE se ho il ref, un oggetto remoto può essere stato
distrutto nel frattempo (basta che termini la JVM che lo ha pubblicato).
Un
difetto di RMI è quello di essere un meccanismo "java only", infatti
non funziona fra ambienti (ma soprattutto linguaggi) diversi, ove sia necessario
avere la stessa funzionalità fra ambienti (linguaggi) diversi basta
usare CORBA. RMI è chiamato anche “CORBA dei poveri” ;-)
Nella
versione nuova di RMI (nel JDK1.2) non c’è più lo SKELETON,
viene generato solo lo stub. Per utilizzare la nuova versione di RMI, anche
se avete il JDK1.2 dovete usare un'opzione di rmic (-v1.2) altrimenti
vengono generate le classi "old style".
Sempre
nella nuova versione di RMI esistono gli "oggetti attivabili", cioè
quegli oggetti che vengono istanziati "on demand" (alla servlet
per intenderci), occupando memoria solo quando serve davvero.
Riferimenti
bibliografici
-
La HomePage
dell'RMI http://java.sun.com/products/jdk/rmi/index.html
-
La specifica
ftp://ftp.javasoft.com/docs/jdk1.2/rmi-spec-JDK1.2.pdf
-
Le FAQ
http://java.sun.com/products/jdk/rmi/faq.html
|