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