RMI vs LMI

Transcript

RMI vs LMI
RMI vs LMI
In questa lezione metteremo in risalto alcune differenze sostanziali tra
programmazione locale (su una singola macchina) e programmazione
remota e quindi distribuita su più macchine.
L’invocazione di metodo remoti (RMI) sebbene usi una sintassi simile
a quella per l’invocazione di metodi locali (LMI) in realtà ha una
semantica profondamente differente.
La sintassi di una RMI ha grosso modo la seguente struttura:
try {
result = remoteObj.method(args);
}
catch (RemoteException e){
// qui si gestisce l’eccezione remota
}
Rivediamo allora la semantica di LMI e poi definiamo quella di RMI per
differenza.
Massimo Merro
Programmazione di Rete
74 / 127
Caratteristiche di LMI
Per poter eseguire un metodo di un oggetto di cui abbiamo una
referenza dobbiamo fornire argomenti in numero e tipo appropriati.
Un metodo può o meno avere la clausola “throws” (eccezioni).
Chi invoca un metodo deve gestire tramite l’istruzione “catch” le
eccezioni dichiarate nella clausola “throws” del metodo, oppure deve
dichiarare una clausola “throws”.
Passaggio dei parametri e risultati di una invocazione LMI:
Tipi di dato primitivi vengono passati per valore;
Oggetti vengono passati per referenza.
Le eccezioni vengono ritornate per referenza.
Se il metodo termina normalmente allora è stato invocato (e quindi
eseguito) esattamente una volta.
Se un metodo ritorna un’eccezione non catturata da un’istruzione
“catch” allora tale metodo è stato invocato una sola volta fino al
punto in cui è stata emessa l’eccezione.
Oggetti locali sono soggetti ad una garbage-collection standard.
Massimo Merro
Programmazione di Rete
75 / 127
Caratteristiche di RMI
Un metodo remoto può essere invocato solo attraverso un’interfaccia
remota di cui tale metodo faccia parte.
La dichiarazione di un metodo remoto all’interno di una interfaccia
deve contenere la clausola “throws” per gestire eccezioni remote.
I client di tali metodi remoti devono catturare e gestire le eccezioni
remote attraverso il costrutto “catch”.
Passaggio dei parametri e risultati di una invocazione RMI:
Tipi di dato primitivi vengono passati per valore;
Oggetti (locali) vengono passati per deep copy;
Oggetti remoti esportati vengono passati per referenza remota;
Oggetti remoti non esportati vengono passati per deep copy.
java.lang.RemoteObject è una sottoclasse di java.lang.Object.
Se un’invocazione remota termina (normalmente o attraverso
un’eccezione) allora il metodo è stato invocato “al più una volta”.
Gli oggetti remoti sono soggetti ad una garbage-collection distribuita
che precede quella locale.
Massimo Merro
Programmazione di Rete
76 / 127
Semantica per LMI e RMI
In LMI l’intero supporto di base (compilazione, istruzioni della JVM,
esecuzione, etc) sono affidabili al 100% (a meno che non si arresti la
macchina su cui è in esecuzione).
Inoltre l’esecuzione di una LMI è sincrona. Vale a dire che lo stesso
processore esegue sia il codice del metodo chiamato e sia il codice del
chiamante. Ciò vuol dire che per riprendere la propria esecuzione, il
chiamante deve solo attendere che termini il metodo invocato.
In RMI l’invocazione di un metodo è sicuramente meno affidabile:
1
2
3
4
Durante la comunicazione su rete certi pacchetti possono andare persi
e devono essere ritrasmessi (fault-taulerance).
Certi pacchetti possono essere deliberatamente scartati dai routers per
evitare congestionamenti.
In generale, RMI coinvolge due o più hosts che potrebbero fallire.
Infine, anche la porzione di rete che collega i due host può fallire;
facendo erroneamente credere al client che la macchina remota sia
fallita quando invece il problema è del collegamento di rete.
Massimo Merro
Programmazione di Rete
77 / 127
Fallimenti remoti
Nei Client RMI non si possono ignorare i fallimenti remoti.
Un client infatti deve gestire le eccezioni remote in maniera
appropriata a seconda dell’applicazione che implementa.
Un’eccezione remota può avere origine localmente (!!!) o
remotamente. Ad esempio:
errori in fase di codifica/decodifica degli argomenti/risultati del metodo
remoto
errori di installazione
errori transitori o permanenti della rete
errori transitori o permanenti di Java
errori transitori o permanenti del sistema delle risorse.
Massimo Merro
Programmazione di Rete
78 / 127
Fallimenti parziali
Nel caso in cui emergano eccezioni remote non è facile capire se
l’invocazione remota è fallita completamente o parzialmente.
Ad esempio, un server remoto potrebbe aver aggiornato con successo
un database a seguito di un’invocazione remota che però non è
terminata correttamente per via di un fallimento nella comunicazione
verso il client.
Dal suo punto di vista il server immagina che l’invocazione remota
abbia avuto successo mentre in realtà il client riscontra un fallimento
in ricezione.
Perciò i client RMI dovrebbero essere progettati in maniera tale da
gestire anche fallimenti parziali.
Questo è evidente quando si ha a che fare con operazioni su basi di
dati remote.
Riassumendo i fallimenti parziali dipendono dal fatto che com RMI il
supporto di base è molto meno affidabile rispetto a LMI.
Massimo Merro
Programmazione di Rete
79 / 127
Tempo di latenza
I client di server remoti devono tener conto del tempo di latenza tra
quando il metodo viene invocato a quando questo viene eseguito.
Visto che il tempo di latenza è inevitabile, quanto tempo dovrebbe
aspettare un client prima di sospettare un fallimento?
La risposta a tale domanda dipende da::
Banda di trasmissione della rete
Carico di lavoro della rete
Carico di lavoro del server
tempo stimato per l’esecuzione del server
ritardi nell’invocazione dovuti a RMI.
Di defaut un client RMI aspetta indefinitamente che un metodo
remoto ritorni un risultato.
Comunque è possibile settare un timeout nel socket del client, in
modo che se un metodo remoto prende più tempo del previsto viene
lanciata una eccezione InterruptedIOException che si manifesta al
client come una UnmarshalException.
Massimo Merro
Programmazione di Rete
80 / 127
Java Object Serialization
Quando un client invoca un metodo di un server remoto gli argomenti
vengono trasmessi dal client al server, mentre i risultati vengono
trasmessi in senso inverso.
Prima di essere trasmessi, sia gli argomenti che i risultati vengono
codificati.
Similmente, alla ricezione, prima di essere utilizzati, sia gli argomenti
che i risultati devono essere decodificati.
Il processo di codifica viene chiamato marshalling mentre quello di
decodifica viene chiamato unmarshalling.
Se gli argomenti o i risultati di una invocazione remota contengono
oggetti (locali), allora RMI utilizza un processo di serializzazione di
oggetti (object serialization).
La serializzazione serve a convertire un insieme di oggetti, che
contengono referenze ad altri oggetti, in un flusso (stream) di bytes.
Massimo Merro
Programmazione di Rete
81 / 127
Tale flusso può essere spedito in rete attraverso un socket, può essere
memorizzato in un file per elaborazioni successive.
Si noti che attraverso la serializzazione è possibile salvare in un file lo
stato di un oggetto. Stato che può essere recuperato in un secondo
momento, eventualmente dopo l’esecuzione dell’applicazione stessa
che ha creato l’oggetto stesso.
Massimo Merro
Programmazione di Rete
82 / 127
Oggetti Serializable
Tutti gli oggetti (locali) che vengono utilizzati come argomento o risultato
di invocazioni remote devono essere serializzabili. Ovvero:
La sua classe deve implementare l’interfaccia
java.io.Serializable (oppure la sua estensione
java.io.Externalizable);
Il punto precedente riguarda la parte di stato definita localmente nella
classe in questione. Ma come ci si comporta con quelle variabili che
vengono ereditate da una o più superclassi?
Se la superclasse implementa anch’essa Serializable allora non vi
sono problemi.
Altrimenti, bisogna usare i metodi ObjectOutputStream.writeObject() e
ObjectInputStream.readObject() per gestire direttamente la
serializzazione/deserializzazione. In tal caso le superclassi
non-serializzabili devono avere un costruttore di default senza
argomenti che sia accessibile dalla classe in questione.
Un’alternativa a tale procedura è quella di dichiarare la classe in
questione Externalizable (chi è interessato può approfondire).
Massimo Merro
Programmazione di Rete
83 / 127
Le variabili di tipo oggetto di una classe serializzabile devono:
o riferirsi ad un oggetto che sia anch’esso serializzabile
o essere dichiarate come static o transient.
oppure rappresentare un oggetto nullo; si noti che è l’oggetto che deve
essere serializzabile al momento dell’invocazione remota, non la sua
classe.
Se utilizzati è necessario ridefinire i metodi equals() e hashCode
ereditati dalla classe java.lang.Object.
Una classe serializzabile non deve necessariamente implementare dei
metodi visto che l’interfaccia Serializable è vuota.
Se durante la fase di marshalling o unmarshalling viene incontrato un
oggetto non serializzabile allora viene lanciata un’eccezione
NotSerializableException che si manifesta presso il client sotto
forma di MarshalException oppure UnmarshalException.
Si noti che codificare un oggetto in un file è cosa relativamente
semplice. Mentre è tutt’altro che banale ricostruire un oggetto,
possibilmente su una macchina differente, a partire da un file.
Ad esempio, in C e C++ la serializzazione/deserializzazione è
abbastanza gravosa per il programmatore.
Massimo Merro
Programmazione di Rete
84 / 127
Grafo di Oggetti
Vediamo più in dettaglio cosa avviene nella serializzazione.
Quando si serializza un oggetto, si costruisce un grafo di cui l’oggetto
in questione è la radice. Si aggiungono poi tutti gli oggetti a cui
l’oggetto fa riferimento, e cosı̀ via ricorsivamente.
In un grafo di oggetti un arco tra un oggetto A e uno B indica che
l’oggetto A può invocare un metodo dell’oggetto B, e quindi ha una
variabile del tipo della classe di B.
Dato un oggetto appartenente ad un grafo di oggetti il suo in-degree
è dato dal numero di archi incidenti nell’oggetto; l’out-degree il
numero archi uscenti dall’oggetto.
La serializzazione e la successiva deserializzazione devono preservare il
grafo degli oggetti associato ad un oggetto serializzabile.
Gli oggetti di un grafo sono tutti distinti.
Si noti, che un oggetto può essere riferito più di una volta.
Perciò la costruzione del grafo degli oggetti richiede l’uso di memoria
per evitare di codificare due volte lo stesso oggetto.
Massimo Merro
Programmazione di Rete
85 / 127
Processo di serializzazione
La serializzazione può essere fatta implicitamente dal sistema RMI,
durante il marshalling, oppure può essere gestita direttamente dal
programmatore utilizzando il metodo ObjectOutputStream.writeObject().
Quando un oggetto viene serializzato, si memorizza su uno stream di
output lo stato (cioè le variabili) di tutte le sue superclassi e le
variabili proprie dell’oggetto.
Java assegna un serial number a ciascun oggetto del grafo.
Le variabili static e transient vengono ignorate. La serializzazione
trasmette solo lo stato dell’oggetto.
Il processo di serializzazione viene applicato ricorsivamente a tutti gli
elementi del grafo.
Ogni oggetto del grafo viene serializzato una volta sola; successivi
riferimenti all’oggetto sono codificati attraverso un meccanismo di
condivisione delle referenze, utilizzando i serial numbers.
Massimo Merro
Programmazione di Rete
86 / 127
Comunque, ad essere più precisi, l’informazione memorizzata nello
stream è ben di più che il grafo degli oggetti coinvolti. Oltre agli
oggetti, vengono memorizzate anche le segnature delle classi relative.
Ma di quali classi stiamo parlando? Se, ad esempio, a è un oggetto di
una classe An . In generale, esiste una relazione di sottoclasse che
conduce fino alla classe Object.
Cosı̀, per ciascun oggetto, oltre al grafo degli oggetti, viene
memorizzato nello stream la seguente informazione:
La descrizione della classe più vicina all’instanza, ovvero An
Le variabili dell’oggetto visto come un’istanza di An−1
Le variabili dell’oggetto visto come un’istanza di An−2
. . . e cosı̀ via fino ad arrivare ad A1 .
Massimo Merro
Programmazione di Rete
87 / 127
Nella descrizione della classe sono riportate tutta una serie di informazioni
come:
versione ID della classe (un intero usato per validare i file .class);
Un booleno ad indicare se sono stati usati i metodi writeObject() e
readObject();
numero dei campi serializzabili;
descrizione dei metodi e delle variabili (nome e tipo);
una decrizione della superclasse (se serializzabile);
eventuali informazione prodotte dall’invocazione del metodo
ObjectOutputStream.annotateClass() (come vedremo in seguito
tale informazione è cruciale in Java RMI).
Quindi, non solo lo stato dell’oggetto, ma anche il suo tipo viene
serializzato.
Massimo Merro
Programmazione di Rete
88 / 127
Processo di deserializzazione
La deserializzazione può essere fatta implicitamente dal sistema RMI,
durante l’unmarshalling, oppure può essere gestita direttamente dal
programmatore utilizzando il metodo ObjectInputStream.readObject().
IMPORTANTE: Durante la serializzazione può essere lanciata un
eccezione IOException oppure ClassNotFoundException se non è
possibile caricare (dinamicamente tramite codebase) la classe
corrispondente ad un oggetto del grafo che viene deserializzato.
Quando si deserializza un oggetto, viene ricostruito il grafo degli
oggetti per ricostruire la causalità delle invocazioni.
Le uniche differenze che vi sono tra un oggetto prima e dopo la
serializzazione sono:
il metodo hashcode() (se non è stato reimplementato)
l’assenza dei campi static o transient
il comportamento degli operatori == e !=; l’oggetto originale e la sua
deserializzazione hanno differenti chiavi in java.util.Hashtable e
quindi risultano differenti rispetto al metodo equals().
Massimo Merro
Programmazione di Rete
89 / 127