ClientAgent
La classe ClientAgent rappresenta
il tramite nella comunicazione tra un client ed un server
di NetGame, in entrambi i versi della comunicazione. Questo
è schematicamente rappresentato nella seguente figura
Come si può vedere ClientAgent
rappresenta un oggetto remoto, e come tale dovrà estendere
la classe UnicastRemoteObject , oltre ad implementare
un’interfaccia remota (IClientAgent )
Un oggetto della classe ClientAgent
viene creato dal MainServer ogni volta che un
utente entra nel un sistema. Il costruttore ha la seguente
forma :
public ClientAgent(String nick,
MainServer mainServer, IMainClient mainClient, IRoomClient
userClient) throws RemoteException
quindi alla creazione le vengono indicati
MainServer , MainClient e RoomClient
(questi ultimi tramite interfacce remote, in quanto oggetti
remoti) con cui comunicare. Essi non varieranno mai.
Il RoomServer invece può variare perché un utente
in una stessa connessione al sistema NetGame può entrare ed
uscire da più stanze.
L’impostazione della stanza di gioco viene fatta in seguito
all’invocazione joinGameRoom() da parte del client.
I metodi di ClientAgent
possono essere suddivisi in due grandi categorie, che corrispondono
di fatto all’esigenza di comunicare da una parte con i servers
e dall’altra con i clients.
I clienti possono invocare i metodi
dell’interfaccia IGameClient .
Per quanto riguarda invece i metodi
che devono essere invocati dal server sul client, essi sono
:
receiveMsg(String nick, String
text) ; Viene inviato al client un messaggio pubblico
di un utente della stanza
void receivePrivateMsg(String
mitt, String text) ; Invia al client un messaggio privato
di un utente della stanza
void joinedGameRoom(String nick) ;
Comunica al client che un nuovo utente è entrato nella stanza
void leftGameRoom(String nick) ;
Comunica al client che un utente è uscito dalla stanza
void createdMatch(String nick,
String matchName) ; Comunica al client che un utente
ha creato una nuova
void enteredMatch(String nick,
String matchName) ; Comunica al client che un utente
è entrato in una partita
void exitedMatch(String nick,
String matchName) ; Comunica al client che un utente
è uscito da una partita
void deletedMatch(String matchName) ;
Comunica al client che è stata cancellata
void startedMatch(String matchName) ;
Comunica al client che una partita è cominciata
void finishedMatch(String matchName) ;
Comunica al client che una partita è finita
In realtà questi metodi non saranno
invocati direttamente dal RoomServer . Il RoomSever
si limiterà a depositare un evento corrispondente all’azione
che deve comunicare al ClientAgent , ed il ClientAgent
stesso, tramite un’opportuna classe interna che vedremo, si
preoccuperà di smistarlo al client, senza quindi bloccare
l’elaborazione del server.
Di fatto, ClientAgent implementa anche l’interfaccia
IEventHandler , mettendo a disposizione appunto
il metodo :
public void addEvent(NetgameEvent
event)
che sarà quello effettivamente invocato
per le operazioni descritte precedentemente. Tali metodi potranno
quindi essere dichiarati private, in quanto invocabili solo
all’interno di ClientAgent .
Ci sono poi altri metodi usati dal server per comunicare col
client :
IGameClient getGameClient() ;
Restituisce il client del gioco posseduto dal client e relativo
alla stanza in cui si trova correntemente. Si tratta naturalmente
di un riferimento remoto e viene richiesto al momento dell’avvio
di una partita.
void callFinishMatch() ;
Dice al client di terminare la partita che sta giocando: si
limita ad invocare callFinishMatch() sul RoomClient ;
void leave() ; Invocato
in seguito alla richiesta di un client di disconnettersi o
della rilevazione della caduta del client. Esce dalla partita
corrente (anche se in esecuzione) ed esce dalla stanza.
void roomServerShuttingDown() ;
Metodo invocato dal RoomServer su tutti i client
connessi quando và offline
void serverOff() ; Metodo
invocato dal MainClient su tutti i clients locali
quando va offline. Semplicemente comunica l’evento al client.
Organizzazione
ad eventi
La classe interna che implementa il
gestore di eventi è :
ClientAgentEventHandler extends
EventHandler
Essa implementa il metodo handleEvent()
in modo che esso invochi i metodi corrispondenti all’evento
sul client.
Possiamo schematizzare il flusso di
operazioni che avviene quando il RoomServer deve
invocare un metodo sul RoomClient nel seguente
modo (assumiamo per esempio il caso di creazione di una partita)
:
- Il
RoomServer invoca il metodo addEvent(CreateMatchEvent(…))
sul ClientAgent
- Il metodo
ClientAgent.addEvent() invoca il
metodo addEvent() sul ClientAgentEventHandler
locale
- Il metodo
ClientAgentEventHandler.addEvent()
(come definito nella superclasse EventHandler )
pone l’evento nella coda locale ed eventualmente sveglia
il thread che effettua la comunicazione con il client. A
questo punto la chiamata da parte del RoomServer
è terminata, ed esso può continuare il suo lavoro senza
attendera la terminazione della chiamata su ogni singolo
client.
- Tale thread, all'interno del metodo
handleEvent() ,
effettua la chiamata remota createdMatch(…)
sul RoomClient con cui il ClientAgent
è in contatto.
Sincronizzazione
dei metodi
Siccome la classe ClientAgent
fa da rappresentante per la comunicazione in entrambi i versi
da client e server, occorre rendere mutuamente esclusivi i
metodi invocabili da tali oggetti, onde evitare che in seguito
ad un accesso contemporaneo vengano eseguite delle sfortunate
sequenze di istruzioni che portino all’inconsistenza lo stato
proprio del ClientAgent .
Si può ottenere semplicemente questo dichiarando synchronized
tali metodi (quindi quelli dell’interfaccia IClientAgent
e quelli di invocazione inversa del server sul client precedentemente
descritti).
Operando con chiamate remote, questo
può portare a problemi che a livello locale non si presenterebbero.
Lo sostanziale differenza da prendere in considerazione in
questo caso è che una chiamata su un oggetto remoto evidentemente
non può venire eseguita dallo stesso processo (thread) del
chiamante (che rimane invece in attesa sulla macchina client),
ma viene gestita da un processo creato sulla macchina del
server. Questo può portare a problemi di deadlock, considerando
che il possesso del lock di una risorsa è gestito a livello
di thread.
Si consideri al riguardo il seguente caso :
il thread t1, in possesso
del lock dell’oggetto o1, invoca il metodo m2 sull’oggetto
o2.
Il metodo o2.m2() prevede
l’invocazione di un metodo synchronized sull’oggetto 01
Nel caso locale, tale sequenza di elaborazione
non presenta alcun problema, perché le due invocazioni sono
fatte in realtà dallo stesso thread, che ha quindi i diritti
necessari.
Nel caso distribuito invece si presenta
una situazione di deadlock, perché la seconda invocazione
viene realizzata da un thread diverso rispetto a t1 e quindi
non può entrare in possesso del lock di o1 prima che non l’abbia
rilasciato t1; il problema è che invece t1 aspetta proprio
che tale invocazione vada a buon fine per poi rilasciarlo.
Quindi c’è deadlock.
In ClientAgent tale situazione
si presenta in un paio di occasioni. In particolare vi sono
dei metodi di RoomClient (es. createdMatch() )
che effettuano, nel loro body, un’invocazione di ritorno sul
ClientAgent , chiedendo gli utenti in un match
o le partite definite nella stanza (generalmente per aggiornare
l’interfaccia grafica).
Di conseguenza occorre evitare di mettere
synchronized tali metodi. Essi sono :
getMatches()
getStartedMatches()
getUsersInMatch()
Questo del resto non comporta rischi
di consistenza, perché tali metodi non modificano lo stato
del client.
Modifica dello
stato
Come si è detto il ClientAgent
mantiene informazioni riguardo allo stato del client. In particolare
ha la stanza in cui si trova (private RoomServer
actualRoom ), la partita a cui sta partecipando
(private String match ) e se tale partita è in
esecuzione o no (private boolean matchInExe ).
I metodi invocati dal client, se per
qualche motivo non riescono ad andare a buon fine sul server,
lanciano un’eccezione, che il client dovrà catturare e trattare.
Si sono messe quindi le modifiche sulle
variabili di stato nelle invocazioni dei metodi di ritorno,
cioè in quelle invocate dal server su un client. Quando un
client tratta una di esse controlla se si tratta di lui e
nel caso modifica lo stato.
Questa scelta, del resto necessaria
perché un’operazione potrebbe non essere consentita a livello
RoomServer , insieme a controlli che vengono effettuati
a livello ClientAgent , per non interrogare inutilmente
il RoomServer , possono ipoteticamente portare
a delle segnalazioni di errori al client, in realtà non consistenti.
Si pensi per esempio al seguente caso. Supponiamo che un client
entri ed esca velocemente da una partita. RoomServer.enterMatch()
va a buon fine e si mette un evento nell’EventHandler
del ClientAgent ; però prima che questo evento
possa essere trattato viene invocato dal client ClientAgent.exitMatch() .
Tale operazione a livello di RoomServer sarebbe
del tutto lecita, però il ClientAgent vede un
errore ( a lui il client risulta non essere iscritto a nessuna
partita) e di conseguenza lancia un’eccezione al RoomClient ,
senza nemmeno contattare il RoomServer .
Viene in questo caso rilevato un errore
a livello RoomClient e segnalato all’utente (non
sei nella partita), mentre in realtà a livello di RoomServer
è presente.
Ad ogni modo questi errori non sono gravi e non portano a
informazioni errate sul server; semplicemente il cliente dovrà
ritentare l’operazione. Inoltre anche l’interfaccia grafica
è stata realizzata in modo da prevenire tali situazioni: l’operazione
di uscita non sarà resa possibile finché non sarà notificato
al client che l’ingresso è andato a buon fine (tra l’altro,
questo è un esempio di quanto si diceva riguardo a controlli
multipli) e questo è fatto sicuramente dopo aver modificato
lo stato nel ClientAgent .
Caduta del client
Un altro compito di ClientAgent
è quello di rilevare l’eventuale caduta del client e mettere
in moto le operazioni per trattarla a livello di server.
Semplicemente (e magari un po’ semplicisticamente)
si ritiene un client caduto quando viene intercettata un’eccezione
di comunicazione (RemoteException ) durante l’invocazione
di un metodo remoto di MainClient o RoomClient .
In questo caso viene invocato il metodo clientDisconnected()
sul MainServer locale, che comunicherà poi a
tutti (tutte i RoomServer e tutti i server remoti,
l’uscita dal sistema del cliente).
Se un utente chiede di uscire dal sistema
avendo una partita in corso, il ClientAgent ,
nel metodo leave() richiamato dal MainServer ,
invoca sul client il metodo callFinishMatch() ,
con il quale si impone al client di gioco di terminare la
sua partecipazione alla partita in corso.
Nel caso di disconnessione del Server
invece viene invocato su ogni istanza di ClientAgent
relativa ad ogni client il metodo serverOff() .
Per rendere veloce la disconnessione del server questo metodo
non fa nulla tranne chiamare l’omonimo metodo sul MainClient ,
che solo così verrà notificato del fatto che il server a cui
era connesso è andato offline.
|