Indice - Progetto di Reti di Calcolatori - Fabio Adani e Marco Chiesi
Implementazione

RMI

La scelta di utilizzare l’API RMI di Java, ha obbligato a delle precise scelte implementative e di organizzazione delle classi.

Come si è visto nel progetto, sono parecchie le classi remote dell’applicazione. Riassumendo, esse sono : MainServer, RoomServer, ClientAgent, UserDBServer dalla parte server e MainClient e RoomClient dalla parte client.

Ognuna di esse estende la classe java.rmi.server.UnicastRemoteObject. Questa non è una scelta obbligata, ma sicuramente è molto vantaggiosa, perché in questo modo si ottiene una classe remota già pronta ad accettare richieste senza preoccuparsi dell’attivazione  e dell’esportazione.

In particolare, per quanto ci interessa, le caratteristiche di questa classe prevedono :

  • Per la comunicazione usa le socket di default per le RMI. Più in dettaglio, viene usato il livello di trasporto tcp, ed inoltre viene garantita ove necessario, per esempio in caso di presenza di un firewall, la comunicazione attraverso classi remote mediante il protocollo http
  • L’oggetto è sempre attivo. Questo potrebbe essere un aspetto negativo, perché magari si potrebbe volere un’attivazione solo su necessità. Una soluzione di questo tipo è ottenibile estendendo la classe  java.rmi.activation.Activatable. Si presuppone però che nel nostro caso qualsiasi oggetto remoto sia continuamente sollecitato e quindi un’attivazione dinamica risulterebbe più costosa che utile
  • L’oggetto viene esportato automaticamente, cioè appena creato è pronto a servire richieste remote, attendendo richieste su una porta assegnata dinamicamente. Questo avviene in modo completamente trasparente, a parte l’obbligo di considerare l’eccezione RemoteException nel costruttore, che è presente proprio per questo motivo, cioè perché l’esportazione potrebbe non avere successo.

La Sun non specifica rigorosamente quale modello di gestione dei thread sia attuato per la gestione dell’esecuzione dei thread remoti. In generale, un metodo remoto potrebbe o no eseguire in un thread separato. Quindi, siccome non c’è nessuna garanzia, si è dovuto considerare il caso peggiore, cioè quello di esecuzione concorrente, e di conseguenza si è dovuto progettare il sistema di modo che fosse thread-safe, facendo opportunamente uso di classi, metodi e blocchi synchronized.

Ciò che viene invece garantito e la sequenzialità delle invocazioni remote tra uno stesso client ed uno stesso server. Questo vincolo è dovuto al limitato numero di porte che una JVM può aprire.

Nonostante l’elevato numero di oggetti remoti presenti nel sistema, l’RMIRegistry non avrà un grande lavoro da fare. Infatti gli unici oggetti che sono registrati presso il server di nomi sono quelli della classe MainServer e UserDBServer. Si registrano mediante l’istruzione :

Naming.bind(localAddress, this);

Le corrispondenze mantenute dall’RMIRegistry sono infatti utili solo in fase di bootstrap del sistema, ed in questa fase, sia per i clienti che per nuovi server, è basilare contattare semplicemente un Mainserver (ed uno UserDBServer se il MainServer vuole fare controlli sugli utenti). I riferimenti a tutti gli altri oggetti remoti, che saranno massicciamente utilizzati, sono poi passati attraverso metodi remoti.
Per esempio il MainClient ottiene un riferimento al MainServer (mediante l’interfaccia remota IClientAccess) con :

clientAccess = (IClientAccess) Naming.lookup(server.getAddress());

a questo punto può ottenere un riferimento al ClientAgent grazie al valore di ritorno di un metodo dell’interfaccia IClientAccess:

clientAgent = clientAccess.login(user.getUserName(), user.getUserPassword(), this, roomClient);

Analogamente si comportano le altre classi.

Questo approccio però ha portato anche una contropartita; le specifiche RMI infatti impongono che un oggetto remoto passato come parametro possa implementare solo interfacce remote. Quindi si sono dovute dichiarare remote anche interfacce e metodi che in realtà hanno una semantica locale.

I nomi con cui MainServer e UserDBServer si registrano sull’RMIRegistry hanno la seguente forma:

//nodo:porta/Nome_del_Server

e possono essere specificati dall’interfaccia grafica. La porta di default del servizio rmi è la 1099.

Con l’RMI di Java è possibile effettuare automaticamente il download di classi che non sono presenti nel classpath della JVM locale. Per fare questo però sarebbe necessario sul nodo remoto la presenza di un Web Server che effettui il trasferimento.
Nel nostro caso questo non risulta necessario, grazie alla suddivisione in packages delle classi già presentata e alla distribuzione delle classi necessarie a client e server in opportuni file JAR.

In dettaglio, il file JAR del server (NetGameServer.jar) contiene :

  • Tutte le classi del package netgame.shared
  • Tutte le classi del package netgame.server
  • Le classi MainClient_stub e RoomClient_stub

Mentre il file JAR del client (NetGameClient.jar) contiene :

  • Tutte le classi del package netgame.shared
  • Tutte le classi del package netgame.client
  • Le classi MainServer_stub e ClientAgent_stub

In realtà è stato necessario mettere nel file JAR del client anche le classi IServerAccess e ServerState. La prima risulta necessaria perché è un'interfaccia remota implementata dal MainServer ma, riguardando solo la comunicazione tra server, è stata posta nel package netgame.server. La seconda è una diretta conseguenza di questo perché ServerState è una classe restituita da un metodo (addServer) di tale interfaccia.

Le definizioni delle interfacce remote sono state poste nel package shared, di modo da essere condivise da tutti.
Comunque, disponendo di un web-server, è possibile effettuare il caricamento dinamico di queste classi. Nel caso bisognerebbe avviare client e server con la proprietà java.rmi.server.codebase opportunamente settata (http://localhost/<classpath>).

Per poter caricare classi remote occorre installare un security manager; solo così si può garantire che le classi caricate non compiano operazioni per cui non sono abilitate. Si è utilizzato in questo progetto l’RMISecurityManager, del package java.rmi:

System.setSecurityManager(new RMISecurityManager())

In congiunzione al security manager si è utilizzato un policy file (java.policy) in cui sono esplicitate le operazioni ammesse. In realtà in questa versione tale file non effettua restrizioni :

grant { permission java.security.AllPermission; };

Parametri relativi a RMI passati all’avvio

Sostanzialmente all’avvio del MainClient e del MainServer vengono impostati due parametri relativi ad RMI. Essi sono :

  • java.rmi.server.codebase
  • java.rmi.server.hostname

Il primo indica il percorso (un URL) delle classi che provengono da quella JVM. Esso assume il seguente valore :

file:/%NETGAME_HOME%\jar\NetGameServer.jar (o NetGameClient.jar)

dove la variabile d’ambiente NETGAME_HOME, precedentemente impostata, individua la directory che ospita l’applicazione.

Evidentemente, come anche si diceva prima, questo non permette il caricamento dinamico delle classi, inoltre se ci si deve registrare presso l’RMIRegistry, esso deve essere necessariamente locale (perché l’invocazione Naming.bind() – così come lookup() – è in realtà un’invocazione remota, in cui viene passato un oggetto stub, la cui classe, se non conosciuta dall’RMIRegistry, deve essergli trasmessa)

La proprietà java.rmi.server.hostname invece serve per specificare il nome o l’indirizzo IP da usare per riferire gli oggetti remoti esportati dalla JVM locale.
Questo potrebbe essere utile, per esempio se un nodo ha più indirizzi IP, per specificare quello da utilizzare effettivamente.
Si consideri per esempio il caso di un nodo avente una connessione su LAN (IP : 192.168.1.1) e contemporaneamente una connessione ad internet (IP, assegnato dinamicamente : 242.42.42.42). E’ evidente che gli oggetti remoti di NetGame, per ricevere invocazioni remote da altri delle rete presenti in Internet, dovranno essere reperibili all’indirizzo IP di internet.
Per questo l’avvio del MainServer è in realtà mediato da una classe accessoria (StartServer) che tra le altre cose si occupa di individuare l’indirizzo IP adatto e di impostare opportunamente suddetta proprietà.
Per fare questo si può utilizzare il metodo InetAddress.getAllByName() che restituisce tutti gli indirizzi IP di un nodo, scegliendo poi quello opportuno.
Purtroppo però l’implementazione di tale metodo prevede che se come parametro è passato un indirizzo IP numerico, viene restituito solo quello, senza indagare eventuali altri indirizzi associati. Mentre se gli si passa “localhost” restituisce semplicemente l’indirizzo di loopback 127.0.0.1. Quindi per far funzionare NetGame in una situazione come quella descritta sopra bisogna impostare la proprietà manualmente.

(A proposito si riporta la casistica dei risultati di diversi tentativi per ottenere l’IP dell’host locale con scheda Ethernet IP=192.168.1.1 e connessione ad internet via modem attiva)

InetAddress.getAllByName("localhost")        =  [localhost/127.0.0.1]
InetAddress.getAllByName("127.0.0.1")        =  [127.0.0.1/127.0.0.1]
InetAddress.getAllByName("192.168.1.1")      =  [192.168.1.1/192.168.1.1]
InetAddress.getLocalHost().getHostAddress()  =  192.168.1.1 

Come si può vedere l’indirizzo internet purtroppo non compare mai)

Pro e contro di RMI

La scelta di usare RMI può essere motivata dai seguenti vantaggi:

  • Astrazione. Possibilità di ragionare e progettare il software ad un livello di astrazione più elevato, dato che è più facile pensare in termini di invocazione di metodi, piuttosto che di lettura e scrittura di byte su uno stream.
  • Trasparenza. Uno dei vantaggi fondamentali è che grazie all'RMI è possibile invocare metodi remoti proprio come se si trattasse di metodi di oggetti locali. Il sistema pensa automaticamente a gestire la comunicazione.
  • Sincronizzazione forte. L'invocazione remota di metodi, così come avviene per le normali chiamate di procedura remote (RPC), sono intrinsecamente sincrone, in quanto il chiamante rimane in attesa del ritorno del metodo (rendez-vous esteso).
  • Apertura del sistema. Nelle ultime versioni di Java, l'RMI si è arricchito del protocollo IIOP (Internet Inter-Orb Protocol), il che permette all'applicazione di interagire con oggetti CORBA, che possono essere realizzati anche in altri linguaggi. In realtà noi non useremo questa caratteristica ma ci accontenteremo dell'RMI puro di Java, ma è comunque utile sapere che con qualche piccola modifica l'applicazione può essere resa compatibile con CORBA.

Accanto a questi però ci sono anche alcuni svantaggi:

  • Efficienza. Un'implementazione fatta usando RMI è chiaramente meno efficiente rispetto ad esempio all'uso di normali socket in quanto è necessario introdurre un certo overhead per la gestione delle chiamate. Tuttavia la differenza di prestazioni nella presente applicazione risulta di poca importanza.
  • Difficoltà di configurazione. Se da un lato usare RMI semplifica la parte di progettazione di un sistema, dall'altro lato complica le cose dal punto di vista della configurazione, in quanto, come si è visto, perchè RMI funzioni correttamente è necessario impostare alcuni parametri.
  • Sincronizzazione. In certi casi la forte sincronizzazione delle RMI risulta uno svantaggio anzichè un vantaggio poichè invece sarebbe preferibile avere delle chiamate non bloccanti.
  • Vincoli di implementazione. E' richiesto che ogni classe remota che viene passata come parametro di una invocazione remota implementi solamente interfacce remote. Per questo è stato necessario dichiarare remote anche alcune interfacce i cui metodi non erano in realtà remoti.
Indietro Inizio pagina Avanti
Indice   Fabio Adani e Marco Chiesi