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

Comunicazione client-server

Per implementare la comunicazione bidirezionale tra client e server si sono usate le Socket di Java. In particolare il server crea una ServerSocket con la quale accetta le connessioni dei client, i quali ovviamente dovranno conoscere a priori la porta del server. Per ogni connessione viene generato un nuovo thread che si occuperà dell'interazione con quel client. Questo thread altro non è che il PlayerAgent visto in precedenza, pertanto si tratta di una classe che deriva dalla classe Thread e ne ridefinisce il metodo run().

Per gestire più ad alto livello la comunicazione, cioè ragionando "a messaggi" e non "a byte" come avviene per le socket, si fa uso degli di stream di Java, ed in particolare sono state definite due nuove classi RisikoInputStream e RisikoOutputStream che derivano rispettivamente dalle classi DataInputStream e DataOutputStream di Java. Queste nuove classi prevedono metodi per la gestione diretta dei messaggi visti in precedenza, come readMessage() e writeMessage(). Per l'implementazione dei messaggi si usa una classe RisikoMessage contenente tutti i tipi di messaggio già descritti in precedenza.

Si ricorda che lo scambio di messaggi avviene esclusivamente tra oggetti di classe PlayerAgent e RisikoClient, pertanto tali classi hanno la doppia di funzione di poter scrivere messaggi su un RisikoOutputStream e di leggerli (e gestirli in modo opportuno) da un RisikoInputStream.

Il RisikoClient ha quindi dei metodi che vengono invocati tramite l'interfaccia grafica del gioco e che provvedono ad inviare i relativi messaggi all'agente presente sul server (ad esempio sendAttack, sendMoveArmies, ecc.), ed inoltre avrà un meccanismo asincrono (realizzato quindi con un altro thread) per leggere messaggi provenienti dall'agente. All'arrivo di ogni messaggio il client gestisce opportunamente il messaggio tramite il meotdo handleMessage, modifica la sua visione dello stato del gioco ed aggiorna l'interfaccia grafica, invocandone opportuni metodi. Il metodo handleMessage in realtà non fa altro che riconoscere il tipo di messaggio appena arrivato e quindi chiama un metodo opportuno che lo gestisce, come ad esempio handleAttackResult, handlePlacedArmies, ecc.

Dall'altro lato, analogamente il PlayerAgent ha dei metodi che sono invocati dalla classe RisikoGame (che gestisce il gioco), come ad esempio sendAttack, sendPlacedArmies, ecc., e che spediscono messaggi diretti al client, ed altri metodi per la gestione dei messaggi provenienti dal client, ad esempio handleAttack, handleEndTurn, ecc.

Per una visione più completa dei metodi ora citati si rimanda al codice e alla documentazione javadoc delle classi.

Sincronizzazione

La gestione dello stato di una partita è un aspetto abbastanza delicato dell'applicazione, in quanto, data la presenza contemporanea di più giocatori, non si può escludere a priori che i partecipanti possano effettuare nello stesso momento delle operazioni incompatibili tra loro. E' quindi necessario introdurre un meccanismo per evitare che si creino dei conflitti. Come detto in precedenza, la classe che si occupa della gestione delle informazioni relative a ogni singola partita è RisikoGame. Tale classe è dotata di diversi metodi che permettono ai giocatori di modificare lo stato del sistema, i quali metodi vengono invocati dai PlayerAgent quando ricevono il corrispondente messaggio da parte del client.

Metodo Messaggio Azione corrispondente
joinGame JOIN_GAME ingresso in partita di un giocatore
leaveGame LEAVE_GAME abbandono (volontario) di un giocatore
attack ATTACK attacco di un territorio, vengono specificati i territori di arrivo e di partenza ed il numero di dadi usati
multipleAttack M_ATTACK attacco multiplo, analogo al precedente ma l'attacco procede fino a quando è possibile
numArmies NUM_ARMIES comunicazione del numero di armate da spostare in un territorio appena conquistato
placeArmies PLACE_ARMIES posizionamento di una o più armate in un determinato territorio
moveArmies MOVE_ARMIES spostamento di una o più armate tra due territori adiacenti appartenenti allo stesso giocatore
tradeCards TRADE_CARDS scambio di una combinazione di carte
endTurn END_TURN passaggio del turno al giocatore successivo

In considerazione del fatto che a ogni PlayerAgent corrisponde un thread di esecuzione separato, bisogna fare in modo che l'invocazione dei metodi sia mutuamente esclusiva, al fine di evitare la possibilità di raggiungere stati inconsistenti. Si pensi ad esempio al caso in cui, nella fase iniziale di disposizione delle armate, siano rimasti due giocatori con un solo carro armato da posizionare sulla mappa. Il metodo in questione (placeArmies) dopo aver modificato lo stato, controlla se tutto è pronto per cominciare e in caso positivo dà il via alla partita. In questo modo però potrebbe teoricamente capitare che a seguito dell'invocazione contemporanea dello stesso metodo da parte di due PlayerAgent distinti, il controllo della fine del posizionamento abbia successo in entrambe le invocazioni, con una conseguente doppia chiamata al metodo che inizializza la partita. In poche parole, si è in presenza di una sezione critica dovuta all'accesso contemporaneo di due processi ad una stessa risorsa. Un'altra corsa critica si può verificare ad esempio, durante il gioco, se un giocatore tenta di uscire (leaveGame) mentre il giocatore che lo precede passa il turno (endTurn). In questo caso potrebbe capitare che il turno venga assegnato ad un giocatore che non è più presente, con un conseguente blocco totale dello stato del gioco.

La realizzazione della mutua esclusione in Java è molto semplice, infatti è stato sufficiente implementare tutti i metodi sopracitati come synchronized. In questo modo è il runtime di Java che garantisce che ci sia al più un solo thread alla volta che sta agendo sullo stato della partita.

Interfaccia utente

L'interfaccia grafica del client (classe RisikoUI) è stata realizzata per mezzo del framework Swing di Java, il quale presenta notevoli vantaggi, come la ricca disponibilità di componenti e la semplicità di utilizzo. La finestra (realizzata estendendo la classe JFrame di Swing) è suddivisa in diverse parti contenenti differenti tipi di informazioni. In particolare è presente ovviamente un pannello contenente la mappa del mondo virtuale di Risiko con la relativa disposizione delle armate ed un pannello che visualizza le carte in possesso del giocatore e permette eventualmente lo scambio. Data la notevole dimensione di questi due componenti grafici, essi sono visualizzati alternativamente e si può selezionare quale vedere tramite le linguette di un componente JTabbedPane. Oltre a questi è presente un primo pannello che visualizza i messaggi provenienti dal server che aiutano il giocatore a seguire l'andamento del gioco, un secondo pannello che mostra la lista dei giocatori che partecipano alla partita con relative statistiche, un terzo pannello contenente i dadi ed i punteggi dell'ultimo lancio ed infine un pannello di controllo con il quale il giocatore può scegliere l'azione da compiere (attacco, movimento armate, fine turno, ecc.).

Qualche parola in più va spesa relativamente alla realizzazione della mappa, la quale non è soltanto un componente che visualizza lo stato attuale del gioco, ma permette anche l'interazione con il gioco da parte del giocatore. Questo perchè si vuole che sia possibile durante la partita cliccare con il mouse sulla mappa per eseguire varie azioni (posizionamento, movimento delle armate e attacchi). Si introduce quindi la necessità di riuscire a risalire a partire da un punto che è stato cliccato sulla mappa, al territorio ad esso corrispondente. Per fare questo si è creata una classe apposita XYMapper avente il compito di stabilire, date le coordinate di un punto sulla mappa, a quale territorio tale punto appartiene, tramite il metodo:
   countryAt(int x,int y)

Ecco un paio di schermate dell'applicazione (cliccare sull'immagine per vedere la versione ingrandita).

Funzionamento in ambiente NetGame

In considerazione del fatto che Risiko dovrà essere inseribile come modulo nell'ambiente NetGame, risulta necessario seguire alcune regole nella definizione di interfacce e classi che fanno parte dell'applicazione. In particolare, come si può notare nella figura sottostante, è necessario creare due interfacce remote IRisikoServer e IRisikoClient, due classi RisikoServer e RisikoClient ed un ultima classe Risiko.

L'interfaccia IRisikoServer contiene dei metodi propri del gioco invocati dal client sul server, mentre IRisikoClient invece contiente metodi invocati dal server sul client. In realtà, nel caso di Risiko, dato che l'implementazione delle comunicazioni è fatta usando le socket e non RMI, l'interfaccia IRisikoServer non presenta alcun metodo, mentre IRisikoClient presenta l'unico metodo
   void start(String host, int port, String name, String match)
che ha il compito di comunicare al client i dati necessari perchè esso riesca a connettersi alla socket in ascolto sul Server e ad entrare nella opportuna partita.

Le classi RisikoClient e RisikoServer sono semplicemente le implementazioni delle interfacce appena viste e costituiscono in pratica i due terminali per mezzo dei quali avviene la comunicazione. La classe Risiko invece implementa tutti quei metodi che forniscono informazioni generali sul gioco. Per una trattazione più generale si veda la documentazione di NetGame.

La classe RisikoClient viene caricata ed inizializzata dal client di NetGame quando un utente entra nella stanza di Risiko, dopodichè rimane in attesa fino a quando non ha inizio una partita, momento nel quale viene aperta la finestra contenente l'interfaccia utente ed in cui inizia lo scambio di messaggi.

La classe RisikoServer viene inizializzata al momento del caricamento dei server di stanza da parte del server di NetGame. Al momento dell'inizializzazione il server di Risiko non fa altro che creare una ServerSocket (viene usata la prima porta libera disponibile) sulla quale rimane in ascolto in attesa della connessione da parte dei client. Chiaramente questo avviene usando un thread separato apposito (RisikoServerThread). Ogni volta che il server riceve una connessione crea un nuovo thread (PlayerAgent) che si occupa completamente della gestione della comunicazione con il client. L'inizio di una partita avviene quando il giocatore che l'ha creata, comunica al RoomServer di NetGame di voler cominciare, il quale provvede a sua volta a comunicare al RisikoServer che è ora di cominciare. Il RisikoServer possiede dei riferimenti ai client che partecipano ad una data partita e quindi invoca il metodo start (visto prima) sui client, comunicando loro l'host e la porta a cui devono connettersi. Da questo punto in poi ha inizio il protocollo a scambio di messaggi illustrato in precedenza.

Funzionamento stand-alone

Nel caso di funzionamento come applicazione stand-alone, cioè indipendente dalla piattaforma NetGame, ci sono alcune differenze che bisogna mettere in conto se si desidera che l'applicazione sia in grado di funzionare. La differenza principale è che in questo caso il client deve conoscere a priori l'host e la porta a cui deve connettersi, visto che queste informazioni normalmente sono fornite dal framework di NetGame. Per rendere possibile ciò è necessario che l'utente sia in grado di specificare l'host e la porta (oltre al proprio) nick, e per questo è stata predisposta una finestra che viene visualizzata all'avvio del client che richiede all'utente queste informazioni.

Anche a livello di server è quindi necessaria la possibilità di specificare il numero di porta su cui aprire la socket, che di default è la 42042. Oltre alla porta si deve anche specificare il numero di giocatori partecipanti a ciascuna partita, cosa che invece non era necessaria in NetGame, dato che in quel caso era il creatore che decideva quando iniziare, indipendentemente dal numero di partecipanti. In modalità stand-alone invece, per semplificare l'implementazione si è fatto in modo che il numero di giocatori sia fissato dal server e al raggiungimento di questo numero la partita abbia inizio automaticamente. In questo modo si è evitato di dover reimplementare la parte preliminare di creazione della partita, che è svolta normalmente da NetGame.

Indietro Inizio pagina Avanti
Indice   Fabio Adani e Marco Chiesi