MokaByte
Numero 18 - Aprile 1998
|
|||
|
avanzata in RMI |
||
|
|||
RMI è
uno strumento molto potente per la realizzazione di applicazioni distribuite
in Java. Usato normalmente permette ad un oggetto client di accedere a
metodi di un oggetto server. In questo articolo vedremo una tecnica di
programmazione più avanzata che consente invocazioni remote simmetriche
(usando cioè un modello peer-to-peer al posto del tradizionale client-server),alcuni
suggerimenti per l’uso di RMI con un firewall e descriveremo alcuni "trucchi
del mestiere" che possono risparmiarci un mal di testa durante la fase
di sviluppo…
Quest’articolo presuppone la conoscenza di base della RMI API e del suo funzionamento. I fondamenti della RMI API sono stati descritti sul numero di Aprile 1997 di Mokabyte (in ), ma per praticità riassumiamo brevemente i concetti fondamentali:
Dunque RMI funziona basandosi sul paradigma client/server: un server istanzia e registra gli oggetti remoti che vengono messi a disposizione di uno o più client. È un meccanismo chiaramente asimmetrico, dal momento che l’"iniziativa" parte sempre dal client. Cosa succede se invece abbiamo bisogno di un’interazione simmetrica, se cioè vogliamo che su due nodi della rete ci siano oggetti che possono invocarsi reciprocamente? Una possibilità è quella di lanciare il demone rmiregistry e registrare gli oggetti remoti su entrambi gli host: a questo punto abbiamo configurato due server, ed ognuna delle due macchine può essere il client dell’altra. A parte la complessità della soluzione, essa ha un grave difetto: non può funzionare con un applet. Infatti, per motivi di sicurezza un browser impedisce ad un applet di registrare di un server socket. Eppure RMI può essere molto utile per implementare facilmente il meccanismo di comunicazione tra un applet ed il server… come risolvere il problema?
La modalità peer-to-peer
Fortunatamente, RMI mette a disposizione una modalità che non tutti conoscono: la modalità peer-to-peer che, contrapponendosi alla client/server, permette l’invocazione reciproca tra due oggetti remoti richiedendo che SOLO UNO di essi (il server) si registri con il rmiregistry. Il suo funzionamento è veramente molto semplice: se un client (ad esempio un applet) ha già effettuato con successo il collegamento con il server remoto, mediante la chiamata
L’ultimo problema da risolvere è la definizione di riferimenti esterni: dal momento che non usiamo l’rmiregistry per registrare il client, non è possibile usare il consueto metodo Naming.lookup(). Il client deve passare la sua reference al server, ad esempio come parametro di un apposito metodo remoto. Infatti, se è vero che RMI usualmente passa gli oggetti by value, nel caso essi implementino l’interfaccia Remote (e quindi siano oggetti remoti) RMI usa la modalità by reference.
Comprensibilmente quanto detto potrebbe essere non chiarissimo ad un primo impatto, ma l'esempio seguente dovrebbe chiarire le idee.
Definiamo prima di tutto un'interfaccia remota:
import java.rmi.*; |
Vediamo dunque
la definizione di RemoteListener:
import java.rmi.*; |
import java.rmi.*; |
La parte interessante è il metodo notifyListeners(), che manda un messaggio a tutti i listener che si sono registrati. Fondamentalmente questo metodo non fa altro che ciclare su tutti gli elementi del vettore di listener e richiamare il loro metodo remoteEvent(); ma è importante notare la gestione di un'eventuale errore di comunicazione. Si deve infatti tener presente la possibilità che uno dei client perda la connessione, o venga terminato in modo non pulito, non richiamando il metodo removeListener() per annullare la propria registrazione. In questi casi verrebbe generata una RemoteException, che va gestita localmente per evitare che, propagandosi al di fuori del metodo notifyListeners(), possa bloccare il server. L'operazione da eseguire è rimuovere il listener che ha provocato l'errore (si noti il passaggio intermedio di duplicare il vettore di listener prima di enumerarne gli elementi: è un'operazione necessaria dal momento che le Enumeration di Java non sono robuste e "patiscono" la modifica contestuale del vettore a cui si riferiscono).
Per terminare vediamo come realizzare il client:
import java.rmi.*; |
Usare RMI con un Firewall
In una tipica applicazione del mondo reale, la rete viene protetta da possibili aggressori esterni attraverso un firewall. Non è possibile spiegare esaustivamente in questo articolo cos’è e come opera un firewall (tenendo anche conto di tutte le possibili variazioni sul tema), ma per la nostra trattazione è sufficiente sapere che un firewall altro non è che una macchina con due interfacce di rete (una verso il "mondo esterno" e l'altra verso una LAN) che costituiscono un punto di passaggio obbligato per raggiungere una certa rete locale. Un software apposito decide quali messaggi di rete possono passare indisturbati e quali devono essere bloccati.
Il firewall viene configurato per garantire l’accesso solo a quei servizi che l’amministratore di sistema ha classificato come "leciti" o "non pericolosi", e sbarra la strada a tutto il restante traffico. Per esempio un firewall può far passare SMTP (email), FTP, HTTP, e bloccare tutto il resto. Quasi sicuramente un firewall non è configurato per il protocollo JRMI, cioè quello usato da RMI, dal momento che è un protocollo "esotico".
Per poter operare attraverso un firewall, RMI mette a disposizione due meccanismi che "mascherano" il traffico JRMI in modo da farlo apparire come traffico HTTP (supponendo ovviamente che esso possa attraversare il firewall). Entrambi i meccanismi sono implementati a livello di RMI transport (riguardate lo schema in apertura di articolo) e quindi sono praticamente trasparenti al programmatore, al quale al massimo viene richiesto di definire qualche parametro di configurazione aggiuntivo. Vediamo ora come funzionano.
Se il meccanismo di trasporto RMI non riesce a contattare il server direttamente, esso tenta una connessione sulla porta 80 (quella del protocollo HTTP), "impacchettando", per così dire, la richiesta RMI in una HTTP POST (il tipico messaggio che viene generato da una form HTML). I server RMI si accorgono della situazione, ed estraggono automaticamente la richiesta RMI dal corpo della POST; al momento di mandare indietro una risposta al client, la reimpacchettano in un messaggio HTTP per permettere di nuovo l’attraversamento del firewall. Tutta quest’operazione viene chiamata HTTP tunnelling.
Non è richiesta nessuna modifica al codice Java, ma il programmatore deve solo accertarsi della configurazione del firewall ed agire di conseguenza:
Come si diceva prima, possono essere richieste alcune operazioni di configurazione sul client e sul server:
Come ultima considerazione, va purtroppo tenuto presente che, in caso di HTTP tunneling, RMI diventa almeno dieci volte più lento (ovviamente il valore esatto dipende dalle tante variabili in gioco: velocità del firewall, del server WWW, eccetera).
Considerazioni per la fase di sviluppo e debugging
Ci sono alcuni dettagli da tenere a mente durante la fase di sviluppo e debugging, che possono provocare parecchi grattacapi al programmatore di turno. Il concetto chiave da tenere presente è il seguente: quando il vostro server esegue un’operazione sull’rmiregistry (bind(), rebind(), unbind()), essa non viene eseguita direttamente dalla Virtual Machine dell’applicazione, ma viene semplicemente "girata" all’rmiregistry, che tipicamente gira in una VM differente (infatti esso viene lanciato a parte con l’omonimo comando). L’rmiregistry, inoltre, è molto pignolo sugli indirizzi Internet, e pretende che ogni IP coinvolto in una transazione RMI corrisponda ad un ben preciso nome mnemonico.
Tipicamente ci sono due problemi causati da questa situazione:
Va tenuto
presente che questo problema non coinvolge solo la fase di sviluppo e debugging,
ma può far fallire la connessione tra un client (ad esempio
un applet) ed un server… ed al momento non è risolvibile.
Per questo motivo, attualmente è consigliabile non usare RMI per
progetti che prevedono il collegamento di utenti via modem o comunque
da indirizzi IP dinamici o non completamente definiti da un equivalente
nome mnemonico.
Ed infine, un
paio di trucchi per il debugging: se impostate la proprietà
di sistema java.rmi.server.logCalls con il valore TRUE potrete visualizzare
su terminale (più precisamente su stderr) un log delle
operazioni del server, che può essere reso ancora più
dettagliato impostando la proprietà sun.rmi.transport.proxy.logLevel
con il valore VERBOSE. Queste informazioni possono rivelarsi preziosissime
per trovare la causa di errori di configurazione.
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|