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

RoomServer

RoomServer è la classe che implementa una stanza di gioco.

Analizziamone le variabili, per iniziare a valutare le scelte implementative.

Innanzitutto, per come è strutturato il sistema NetGame, il RoomServer sarà ‘alle dipendenze’ di un MainServer, il cui riferimento sarà mantenuto nella variabile mainServer.

Inoltre dovrà essere in contatto con tutti i RoomServer analoghi della rete; queste informazioni sono mantenute nell’ Hashtable remoteServers, che non è altro che la tabella dei server remoti, in cui ogni entry è costituita dalla coppia: Nome del MainServer (Address) – RoomServerAgent.

Infine ad ogni RoomServer è associato un gioco. La classe RoomServer rappresenta un Server di stanza generico, e può essere utilizzato per ospitare qualsiasi tipo di gioco compatibile con NetGame. Le informazioni sul gioco ospitato sono mantenute nella variabile  IGameServer game. Grazie ad esso è possibile reperire informazioni sul gioco, come per esempio il numero massimo e minimo dei giocatori ammessi (siccome IGameServer implementa l’interfaccia IGame), inoltre è possibile creare nuove istanze di server di gioco per ospitare le partite in esecuzione localmente.

Queste variabili, in particolare remoteServers e lUsers, danno un’idea di come avvengano le comunicazioni tra i RoomServer e con i RoomClient :

Compito primario del RoomServer è quello di gestire gli utenti presenti nella stanza nonché le partite.

Ogni RoomServer ha necessità di conoscere tutti i nomi degli utenti (per esempio per impedire sul nascere il tentativo di ingresso di un utente con un nick già in uso o per comunicare agli utenti i nick presenti nella stanza senza dover richiederli ogni volta ai server remoti). Ogni Roomserver comunque comunicherà direttamente solo con gli utenti locali, mentre si rivolgerà agli altri RoomServer per comunicare in modo indiretto con gli utenti remoti. Questo si riflette nelle strutture con cui si mantengono informazioni sugli utenti presenti, infatti per i remoti basterà un Vector contenente i nick (che sono stringhe) : Vector rUsers; mentre per quelli locali bisognerà per ogni utente avere anche un riferimento al corrispondente ClientAgent, attraverso il quale lo si può contattare : Hashtable lUsers (nick – ClientAgent).

Per la gestione delle partite il RoomServer disporrà di più strutture:

Hashtable matches E’ la tabella che contiene l’elenco delle partite in definizione. NomePartita-Lista (Vector) dei nick dei giocatori
Hashtable startedMatches E’ la tabella che contiene l’elenco delle partite iniziate. NomePartita-Lista dei nick dei giocatori    
Hashtable hostedMatches E’ la tabella che contiene l’elenco delle partite iniziate ospitate da questo host. NomePartita-IGameServer corrispondente
Hashtable remoteHostedMatches E’ la tabella che contiene l’elenco delle partite iniziate ospitate da un altro host. Nome MainServer-Lista dei nomi delle partite

Oltre a queste sono necessarie altre strutture per implementare il protocollo 2PC. Esse sono :

Hashtable connectingServer MainServer e IRoomServerToServer del server che si sta connettendo
Hashtable creatingMatches Tabella delle partite in creazione. (matchName - nick del creatore) 
Hashtable enteringMatch Tabella degli utenti entranti in una partita. (matchName - [Players])
Hashtable exitingMatch Tabella degli utenti uscenti da una partita. (matchName - [Players])
Vector startingMatches Tabella delle partite in partenza.

Organizzazione ad eventi

Sono poi presenti due variabili di tipo EventDispatcher per comunicare con i client locali (clientEventDispatcher) e con i server remoti (serverEventDispatcher).

Tali oggetti sono della classe EventDispatcher ed operano rispettivamente  su lUsers e remoteServers (sono così inizializzati nel costruttore di RoomServer). L’utilità di tali oggetti come si è detto sta sostanzialmente nel disaccoppiare l’attività normale del RoomServer rispetto alle chiamate remote, che altrimenti lo rallenterebbero di molto. Facendo uso di queste classi, ed utilizzando la gerarchia di eventi opportunamente introdotta (cfr. package netgame.shared.events), la comunicazione con i clients avviene semplicemente utilizzando la seguente istruzione :

    clientEventDispatcher.addEvent(/* Evento */);

mentre per comunicare con i servers basta usare :

    serverEventDispatcher.addEvent(/* Evento */);

In realtà ciò che accade dietro le quinte è che il thread interno ai dispatcher, opportunamente sincronizzato, passa l’evento agli EventHandler intermediari (ClientAgent e RoomServerAgent) ed essi a loro volta invocheranno, in base al tipo di evento, il corrispondente metodo remoto rispettivamente su RoomClient e RoomServer. Tutto questo accade concorrentemente, eliminando così almeno in parte le attese altrimenti necessarie per i completamenti in sequenza delle chiamate remote.

Una soluzione di questo tipo pone però un problema per quanto riguarda la realizzazione del protocollo 2PC. Per la fase di prepare esso prevede che il coordinatore conosca il risultato di tale fase su tutti i partecipanti. In sostanza si vuole che le chiamate remote di prepare siano sì non bloccanti per il RoomServer, ma anche sincrone. Per questo si predispone, prima di ogni invocazione di un metodo prepare, un oggetto della classe Synchronizer opportunamente configurato con il numero dei partecipanti da contattare. Tutte le classi che implementano un evento di prepare del resto accettano, come argomento nel costruttore, proprio un  oggetto Synchronizer.

Quindi tipicamente la fase di prepare si presenta in questo modo

    Synchronizer sync = new Synchronizer(remoteServers.size());
    serverEventDispatcher.addEvent(new PrepareXXXEvent(var1, var2, ..., varN, sync));
    sync.waitForFinish();
    boolean res = sync.getResult();

L’oggetto sync raccoglie in questo modo i risultati delle prepare sui server remoti. L’istruzione sync.waitForFinish() ritorna solo quando

            O il sync riceve una risposta negativa

            O il sync riceve tutte le risposte positive

Nel primo caso res assumerà valore false, mentre nel secondo sarà true. In dipendenza di tale valore verrà poi posta in atto o la fase di abort o quella di commit.

Sincronizzazione interna

Un altro dettaglio implementativo riguardo ai metodi del 2PC sta nell’acquisizione dei lock delle risorse coinvolte. Le risorse sono state ordinate nel seguente modo :

connectingServer, matches, startedMatches, enteringMatch, exitingMatch, startingMatches, creatingMatches, hostedMatches, lUsers, rUsers

Occorre richiedere i lock su tali risorse in questo ordine (naturalmente solo quelle di cui si ha bisogno in quel caso), senza poterne richiedere uno dopo averne rilasciato un altro (protocollo 2 phase lock).

La richiesta del lock avviene semplicemente mediante

    synchronized (risorsa)  {
        //Codice in cui si usa la risorsa
    }

Tali metodi saranno quindi strutturati come una serie di blocchi synchronized innestati. Si è cercato di ritardare il più possibile l’acquisizione di un lock, cioè richiedendolo nel punto più prossimo possibile all’uso della risorsa, osservando comunque i vincoli sopra presentati.

Questo perché potrebbe in realtà non essercene bisogno, per esempio se un controllo sulle risorse già acquisite ha rilevato l’impossibilità di continuare l’elaborazione.

Inoltre in alcuni punti è stato possibile evitare di richiedere i lock delle risorse avendo premura di realizzare le operazioni in un ordine corretto. Per esempio in commitEnterMatch() bisogna spostare un utente dalla lista degli utenti in ingresso nella partita (enteringMatch) in quella degli utenti iscritti nella partita (matches). Se questo spostamento avviene prima togliendo e poi mettendo, non si hanno problemi di consistenza né di errore, perché comunque nel caso di ingresso di un nuovo utente si controllerà per prima la struttura matches.

    if (enteringMatch.containsKey(matchName) && 
                ((Vector)enteringMatch.get(matchName)).contains(nick)) {
        ((Vector)matches.get(matchName)).add(nick);
        ((Vector)enteringMatch.get(matchName)).remove(nick);
        if (((Vector)enteringMatch.get(matchName)).size()==0)
            enteringMatch.remove(matchName);
    }

A parte questi dettagli, l’implementazione dei metodi per il protocollo Two-Phase-Commit, ricalca la struttura già presentata nel progetto.

Comunicazione con altre classi

RoomServer implementa l’interfaccia IRoomServerToServer per la comunicazione con gli altri RoomServer.

Oltre a questi presenta tutti i metodi necessari per interfacciarsi da una parte con il MainServer e dall’altra con i clienti.

Dopo aver creato una stanza di gioco (opportunamente configurata con l’oggetto IGameServer corrispondente) il MainServer comunica al RoomServer i server remoti con cui deve connettersi, cioè i server di stanza che ospitano lo stesso gioco nella rete a cui appartiene.

Questa operazione è svolta dal metodo

    public void connectToRemoteServers(Hashtable remoteS)

La Hashtable passata come parametro contiene i bindings (indirizzo del MainServer remoto – riferimento al RoomServer) su tale nodo (quindi IRoomServerToServer).

In realtà al nostro RoomServer basta conoscere un’unica istanza di RoomServer remoto. Si rivolgerà infatti ad essa mediante il metodo addServer() dell’interfaccia IRoomServerToServer, e il RoomServer contattato si occuperà di mettere in atto il protocollo 2PC per l’ingresso di un nuovo server, aggiornando tutti gli altri.

Se tutto sarà andato a buon fine, al ritorno addServer() restituirà lo stato attuale condiviso della stanza. Il RoomServer dovrà limitarsi ad aggiornare correttamente il proprio, a creare i RoomServerAgent relativi ad ogni server remoto e da quel momento in poi potrà accettare nuove richieste dai client.

Nel caso invece l’ingresso nella rete non abbia successo, sarà lanciata un’eccezione di tipo FailedConnectionException.

Per quanto riguarda invece i metodi invocabili dai client, essi non sono altro che quelli dell’interfaccia IClientAgent implementati da ClientAgent. Rispetto ad essi però presentano in più il parametro nick di tipo String, perché a livello RoomServer bisogna specificare l’utente che effettua l’operazione.

Tali metodi sono dunque i seguenti :

public void joinGameRoom(String nick, ClientAgent ctsg)

public void leaveGameRoom(String nick)

public void sendMsg(String nick, String testo)

public void sendPrivateMsg(String nick, String dest, String text)

public void createMatch(String nick, String matchName) (*)

public void enterMatch(String nick, String matchName) (*)

public void exitMatch(String nick, String matchName) (*)

public void startMatch(String nick, String matchName) (*)

il loro significato è evidente, comunque per maggiori dettagli si rimanda alla documentazione generata con javadoc e direttamente al codice.

Alcuni di essi, quelli indicati con (*), necessitano del protocollo 2PC per realizzare correttamente il loro lavoro. La loro struttura è stata  presentata nella fase di progetto.

Inizio di una partita

Vediamo ora le operazioni che compie un RoomServer quando deve avviare una partita. Innanzitutto ricordiamo che una volta iniziata una partita viene ospitata unicamente sul RoomServer del giocatore che l’ha iniziata, ovvero del primo giocatore iscritto alla partita.

Tale RoomServer riceverà quindi dal primo giocatore la richiesta di iniziare la partita (tramite invocazione del metodo startMatch()). Dopo averne verificato la possibilità (es controllando i vincoli sul numero di giocatori, ed essersi coordinato con gli altri server per garantire l’atomicità dell’operazione, le operazioni che deve compiere sono le seguenti:

  1. Reperire tutti i client di gioco (IGameClient) degli utenti iscritti alla partita
  2. Istanziare un nuovo server di gioco passandogli i riferimenti ai clienti
  3. Avviare la partita sul nuovo server

Per l’esecuzione del punto 1 il RoomServer dovrà operare diversamente a seconda che il cliente sia locale o remoto. Nel primo caso infatti potrà richiedere direttamente il riferimento  remoto al GameClient ad esso, mentre nel secondo caso dovrà passare per il suo RoomServer. Siccome l’informazione su quale server si trova un client è mantenuta a livello di MainServer, il RoomServer dovrà prima di tutto chiedere ad esso per sapere a quale RoomServer remoto riferirsi.

La creazione di un nuovo server avviene grazie al metodo newInstance() di IGameServer. Una volta creato, il nuovo server viene posto nella tabella hostedMatches.

A questo punto il server di gioco può essere avviato. Come già si è detto nel progetto, l’attivazione viene gestita da un thread separato (implementato nella classe GameStarter) per evitare che errate implementazioni del server di gioco blocchino definitivamente il RoomServer.

Le istruzioni che realizzano tutto ciò sono riportate di seguito

    Enumeration enumUsers=pls.elements();
    IGameClient igc;
    Hashtable gcls = new Hashtable();
    String cn;
    while (enumUsers.hasMoreElements()) {
        cn = (String)enumUsers.nextElement();
        if (lUsers.containsKey(cn)) { //se l'utente è locale
          igc = ((ClientAgent)lUsers.get(cn)).getGameClient();
        }
        else { //se l'utente è remoto
          String msURL = mainServer.getMainServerForUser(cn);
          RoomServerAgent stsg = (RoomServerAgent)remoteServers.get(msURL);
          igc = stsg.getGameClient(cn);
        }
        gcls.put(cn, igc);
    }
    IGameServer newGame = game.newInstance();
    hostedMatches.put(matchName, newGame);
    new GameStarter(matchName, newGame, gcls).start();

Sugli altri RoomServer l’inizio di una partita vedrà la rimozione della partita dalle tabelle matches e l’inserimento in startedMatches e remoteHostedMatches.

La fine di una partita viene comunicata al RoomServer dal GameServer mediante l’invocazione del metodo finishMatch().

Indietro Inizio pagina Avanti
Indice   Fabio Adani e Marco Chiesi