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

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.

Indietro Inizio pagina Avanti
Indice   Fabio Adani e Marco Chiesi