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