MokaByte Numero 32  -  Luglio Agosto 99 
Serializzazione & RMI
di
Andrea Trentini
Il top delle comunicazioni via rete


Stanchi di usare le Socket in maniera piatta? Volete trasmettere direttamente oggetti da un host all'altro? Volete invocare un metodo su un oggetto istanziato in un'altra JVM? Provate con Serializzazione e Remote Method Invocation! Dall'utilizzo pratico alle considerazioni più squisitamente filosofiche... 

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

MokaByte Web  1999 - www.mokabyte.it

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