MokaByte Numero 18 - Aprile 1998

 
Programmazione 
avanzata in RMI  
di

Fabrizio Giudici

 

 


 



 

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:

  1. RMI, come dice il suo stesso nome, permette l’invocazione di metodi su un oggetto remoto, che cioè risiede in un’altra Virtual Machine, eventualmente su un altro nodo della rete.
  2. Il meccanismo funziona a partire dalla definizione di un’interfaccia che descrive quali sono i metodi remoti invocabili.
  3. Il programmatore deve scrivere l’implementazione dell’interfaccia, che è poi l’oggetto remoto vero e proprio, mentre un compilatore apposito, rmic, genera automaticamente due oggetti che gestiscono le due estremità della comunicazione (lo stub e lo skeleton).
  4. Infine un registro apposito (rmiregistry) permette di associare oggetti remoti ai loro nomi, e di ottenere una reference remota di un oggetto a partire dal suo nome.


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

il client stesso diventa un oggetto remoto invocabile dal server. Questo meccanismo funziona anche con gli applet, dal momento che il SecurityManager dei browser, in via del tutto eccezionale, chiude un occhio e permette l’istanziazione, di fatto, di un server socket.

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.*;

public interface Server extends Remote

  {

    public String doSomething (String x)

      throws RemoteException;

    public void addListener (RemoteListener l)

      throws RemoteException;

    public void removeListener (RemoteListener l)

      throws RemoteException;

  }

Oltre ad un generico metodo doSomething(), che effettua la parte computazionale vera e propria, abbiamo previsto la possibilità di registrare un RemoteListener, un oggetto remoto che riceve asincronamente la notifica di eventi dal server (in maniera del tutto analoga al meccanismo di eventi previsto dall'AWT a partire dalla versione JDK 1.1). Un RemoteListener può essere registrato o cancellato invocando i metodi addListener() e removeListener().

Vediamo dunque la definizione di RemoteListener:
 
import java.rmi.*;

interface RemoteListener extends Remote

  {

    public void remoteEvent (Object param)

      throws RemoteException;

  }

Essa prevede la presenza di un metodo che viene invocato con un parametro. Siccome il listener è remoto, esso deve estendere l'interfaccia Remote.
Ora tocca all'implementazione del server:
 
 
import java.rmi.*;

import java.rmi.server.*;

import java.util.*;

public class ServerImpl extends UnicastRemoteObject implements Server

  {

    private Vector listeners = new Vector();

    public ServerImpl() throws RemoteException

      {

      }

    public String doSomething (String x) throws RemoteException

      {

        notifyListeners(x);

        return x.toUpperCase();

      }

    public void addListener (RemoteListener listener)

      throws RemoteException

      {

        listeners.addElement(listener);

      }

    public void removeListener (RemoteListener listener)

      throws RemoteException

      { 

        listeners.removeElement(listener);

      }

    public void notifyListeners (Object param)

      {

        Vector temp = (Vector)listeners.clone();
 
 

        for (Enumeration e = temp.elements();

             e.hasMoreElements(); )

          {

            RemoteListener l = (RemoteListener)e.nextElement();

            try

              {

                l.remoteEvent(param);

              }

            catch (RemoteException ee)

              {

                listeners.remove(l);

              } 

          }

      }

    public static void main (String[] args) 

      throws Exception

      {

        ServerImpl server = new ServerImpl();

        System.err.println("Registering...");

        Naming.bind("rmi://hornet/server", server);

        System.err.println("Registered");

      }

  }

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.*;

import java.rmi.server.*;

public class Client implements RemoteListener

  {

    public void remoteEvent (Object param)

      throws RemoteException

      {

        System.err.println("REMOTE NOTIFICATION " + param); 

      }

    public void run() throws Exception

      {

        Server server = (Server)Naming.lookup("rmi://hornet/server");

        UnicastRemoteObject.exportObject(this);

        server.addListener(this);

        System.err.println(server.doSomething("test"));

      }

    public static void main (String[] args)

      throws Exception

      {

        System.setSecurityManager(new RMISecurityManager());

        Client client = new Client();

        client.run();

      }

  }

Come si vede, è sufficiente l'invocazione di exportObject() perché il client possa registrarsi presso il server con il metodo addListener().
Per eseguire i programmi di esempio illustrati, ci si deve ricordare di generare lo stub e lo skeleton con il programma rmic anche per il client: dopotutto nel nostro esempio è un oggetto remoto...
Dal momento che con la modalità peer-to-peer un client può chiedere ad un server che i suoi metodi vengano richiamati per notificare eventi asincroni, questa tecnica è detta anche "callback".
Come si è visto, ancora una volta la RMI API si è rivelata particolarmente potente e facile da usare. Ci sono alcune configurazioni di rete, tuttavia, che possono creare problemi al suo uso. In alcuni casi è sufficiente prendere le opportune precauzioni; in altri RMI evidenzia delle limitazioni nella sua corrente implementazione.
 

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:

  1. Il firewall potrebbe essere stato configurato per redirigere le richieste HTTP su una porta arbitraria; in tal caso è sufficiente lanciare il server RMI per rispondere su quella porta. Tuttavia questo approccio impedirebbe l’uso in contemporanea di un server WWW tradizionale.
  2. Alternativamente è possibile installare un apposito script CGI-BIN su un server WWW pre-esistente per gestire la situazione. Questo script deve rispondere alla URL /cgi-bin/java-rmi, ed un esempio, chiamato java-rmi.cgi, è incluso nel JDK.
Tutto questo meccanismo è controllato dai metodi createSocket() e createServerSocket() della classe java.rmi.server.RMISocketFactory, che un programmatore esperto può ridefinire nel caso voglia implementare comportamenti non standard.

Come si diceva prima, possono essere richieste alcune operazioni di configurazione sul client e sul server:

  1. Se il client non si trova nel dominio del server, è necessario che i loro nomi siano specificati in forma completa (fully qualified). Questo può essere un problema per il server: in funzione della piattaforma dove è installata e della sua configurazione, non sempre la Java Virtual Machine è in grado di ottenere automaticamente il nome del dominio in cui si trova. Per esempio, se il nome del server è myserver.domain.com, può capitare che la VM non sia in grado di ottenere la parte domain.com, definendo quindi l’host locale come myserver. In questi casi, il nome completo del server deve essere specificato nella proprietà di sistema java.rmi.server.hostname.
  2. Visto che il client tenta l’HTTP tunnelling automaticamente, nel caso di errori sulla rete, o se semplicemente il server RMI non è stato attivato o se il suo indirizzo è sbagliato, passa un po’ di tempo prima che il runtime Java si accorga del problema e segnali l’errore. Se si è sicuri che non si vuole tentare l’HTTP tunnelling, questa modalità si può disabilitare dal client impostando la proprietà di sistema java.rmi.server.disableHttp con il valore true.
Se si deve passare attraverso un firewall, solo il server RMI può esportare un oggetto remoto, e quindi la modalità peer-to-peer descritta all’inizio di questo articolo non funziona.

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:

  1. Per motivi di sicurezza, l’rmiregistry rifiuta alcune operazioni se non provengono dallo stesso host in cui è stato lanciato (d’altronde sarebbe piuttosto seccante che un altro host sulla rete registri i suoi oggetti remoti sul vostro computer…). In certe configurazioni (specialmente se c’è di mezzo un PPP – Point to Point Protocol, usato da molti sistemi operativi per connessioni TCP/IP via modem – o DHCP – un protocollo usato in alcune LAN aziendali per assegnare dinamicamente gli indirizzi IP agli host), rmiregistry viene tratto in inganno dall’assegnazione dinamica dell’indirizzo e vi lancia una AccessException. In questi casi, non c’è altra soluzione che mettere le mani sulla configurazione del sistema… ed assegnare un indirizzo IP statico.

  2. 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.

  3. L’rmiregistry ha una cache interna per gli stub che registrate. Se quindi siete in fase di sviluppo e state modificando il vostro codice, poi lo ricompilate e rifate un run, l’rmiregistry mantiene la versione precedente! Come conseguenza vi ritrovate un’eccezione di disallineamento di versione (interface mismatch). L’unica soluzione è ricordarsi di fermare e far ripartire l’rmiregistry ad ogni tentativo. Per semplificarsi la vita, in fase di sviluppo può essere vantaggioso inglobare l’rmiregistry nel proprio programma server, così si è sicuri che esso viene fatto ripartire ogni volta. È un’operazione molto semplice: basta richiamare il metodo
java.rmi.Registry.createRegistry(port);   specificando il numero della porta su cui il registro deve rispondere. A volte, il comportamento dell’rmiregistry è tale per cui possono passare anche un paio di minuti prima che una Naming.lookup() ritorni un risultato. Tipicamente ciò capita quando avete una macchina in cui è stato configurato l’uso di un nameserver DNS, ma che è scollegata dalla rete (ad esempio perché generalmente viene connessa con un modem che durante la fase di sviluppo rimane spento). In queste circostanze, se non è possibile riattivare il collegamento con la rete, è opportuno tentare alcune riconfigurazioni del DNS (che variano da caso a caso e da sistema operativo a sistema operativo, ma in generale è sufficiente disabilitare il DNS, usare solo il nome localhost durante lo sviluppo dei propri programmi, accertandosi che esso sia propriamente definito dal sistema operativo).

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 rivista web su Java

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