Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Versione 0.8: una connessione di test

Introduzione

Come accennato nel capitolo precedente, la classe Connection
definisce i metodi generici per connettersi ad un remoto e scambiare i messaggi del protocollo ma non implementano alcuna vera connessione di rete. Le vere operazioni di trasmissione sono lasciate ai quattro metodi astratti:

  • doConnect
  • doSendMessage
  • doReadMessage
  • doClose

E' arrivato quindi il momento di scrivere la classe derivata da Connection che implementa questi quattro metodi ma, prima buttarci sul protocollo TCP/IP, scriveremo una classe di test che simula la connessione dal lato server ma che, in realtà, non esegue alcuna vera connessione di rete. Anche se ai neofiti sembra una perdita di tempo, vi assicuro che i vantaggi nel disporre di una connessione di test sono notevoli e si ripagano in fretta:

  • si può testare estensivamente il protocollo applicativo
  • si verifica che le eccezioni siano gestite correttamente

Il contenuto della cartella V080

La classe ConnectionTest è implementata nel file sorgente omonimo del package mastermind.net.impl (dove impl stà per implementations) che si riferisce al package che conterrà le implementazioni delle vere connessioni. Questo package conterrà anche la classe che implementa la vera connessione TCP/IP che però sarà descritta nel capitolo successivo:

nome file package descrizione
ConnectionTest.java mastermind.net.impl classe di test per una connessione
ConnectionTCP.java mastermind.net.impl classe di test per una connessione
MasterMind08.java mastermind.game nuove funzionalità di rete

La classe ConnectionTest

Si tratta di una derivata della classe Connection usata per il test della applicazione. Essa simula la connessione remota dal lato del server e riceve i messaggi di notifica inviati dal server di gioco; anzichè inoltrarli alla macchina remota (che non esiste) provvede a creare i messaggi di risposta che il server si aspetta dalla macchina client.

I messaggi per i quali il server di gioco si aspetta una risposta sono, sostanzialmente, solo tre:

  • 103, presentation: il server invia questo messaggio al client e si aspetta in risposta il messaggio di presentazione del remoto
  • 301, newGame: una nuova partita è stata creata ed il server di gioco si aspetta di ricevere un messaggio con la solution del remoto
  • 304, swapTurn: la partita è entrata nella seconda fase ed il turno di gioco è cambiato; se il turno attuale è quello del remoto, il server di gioco si aspetta di ricevere un messaggio con la guess del remoto

Questa classe di test introduce anche un ritardo fittizio dal momento in cui riceve il messaggio di notifica al momento in cui invia la risposta: il ritardo è pari ad un terzo del valore specificato nella proprietà di sistema guessDelay (vedi La proprietà guessDelay). Il ritardo fittizio introdotto nella connessione simula la latenza della rete.

I membri dati

La classe ConnectionTest definisce i membri dati privati dal momento che non si predeve di derivare ulteriormente da questa classe:

  • Params params: i parametri di gioco, necessari al codice per generare la solution e le guess: i parametri vengono notificati con il messaggio di tipo 301 (=new game)
  • int forceError: il codice di forzatura degli errori, vedi Forzare le eccezioni
  • int delay: il ritardo da introdurre nelle operazioni usato per simulare la latenza della trasmissione
  • serverPort: la porta del server: essendo una connessione di test, non esiste alcuna "porta del server": il dato è utilizzato per forzare una eccezione in fase di connessione (vedi Forzare le eccezioni)
  • Message response: il messaggio di risposta che il server di aspetta e preparato nel metodo doSendMessage; vedi Simulare la connessione
  • String serverSolution: la soluzione del umano (il programmatore): la connessione di test genera le guess in modo casuale ma è possibile forzarla ad inviare una guess vincente; questo si ottiene intercettando il messaggio di tipo 202 (=have solution) e memorizzando la soluzione del programmatore in questo membro dati

Il costruttore

Il costruttore di ConnectionTest è semplice: esso si limita ad estrarre le informazioni per la connessione dalle proprietà della applicazione. Ci sono solo due dati interessanti:

  • la proprietà guessDelay usata per introdurre il ritardo fittizio
  • la proprietà serverPort usata per forzare le eccezioni in fase di connessione

I metodi astratti

Dovendo emulare una vera connessione di rete, la classe connessione di test avrà il compito di sovrascrivere i metodi astratti della classe da cui deriva implementando la effettiva elaborazione della connessione e dell'invio / ricezione dei messaggi.
La classe dovrà quindi sovrascrivere i metodi astratti usati dal worker-thread (vedi I metodi di I/O della connessione) e che eseguono le effettive operazioni di connessione:

  • doConnect: essendo una connessione di test la operazione va sempre a buon fine a meno che non sia stato forzato un errore di connessione
  • doClose: non essendoci una vera connessione da chiudere, questo metodo non esegue alcuna operazione
  • doSendMessage: invia il messaggio al remoto; non essendoci alcun remoto questo metodo viene elaborato in modo particolare che analizzeremo in dettaglio in Simulare la connessione dal momento che è in questo metodo che vengono elaborate le risposte attese dal server
  • doReadMessage: questo metodo restituisce il messaggio di risposta preparato da doSendMessage
  • isServer: ritorna sempre TRUE.
  • getRemoteAddr: ritorna la stringa "ConnectionTEST"

Simulare la connessione

In una vera connessione di rete il server di gioco invia al client i messaggi di notifica e si aspetta le risposte dal remoto. Pertanto, la connessione di test non deve fare altro che creare i messaggi di risposta ed inoltrarli al server di gioco. Poichè i messaggi di notifica transitano dal metodo doSendMessage è proprio in questo metodo che avviene la elaborazione delle risposte. A seconda del tipo di messaggio di notifica inviato dal server, la connessione di test prepara il messaggio di risposta:

// File: ConnectionTest.java
public int doSendMessage( Message msg ) throws MMException
{
... omissis ...
response = switch ( msg.getType() ) {
case 301 -> createSolution( msg.getParams( 1 ));
case 304 -> createGuess( msg.getInt( 1 ));
case 103 -> createPresentation();
case 202 -> storeSolution( msg.getInt( 1 ), msg.getSequence(2));
case 204 -> setForcedError( msg.getInt( 1 ), msg.getInt(2) );
case 101 -> createEchoResponse( msg.getField( 1 ));
... omissis ...
  • il metodo createEchoResponse prepara un messaggio di risposta di tipo echo-response quando viene ricevuto il messaggio di tipo 101 (=echo-request)
  • il metodo createPresentation prepara un messaggio di presentazione con nickname uguale a "ConnTEST" e versione pari a 0.8.0; questo metodo viene richiamato quando il server invia il messaggio 103
  • il metodo createSolution viene richiamato quando il server invia il messaggio di notifica con codice 301 (notifyNewGame); questo metodo ritorna un messaggio di risposta contenente la solution, ottenuta in modo casuale
  • il metodo createGuess viene richiamato quando il server invia il messaggio di notifica con codice 304 (notifySwapTurn); questo metodo ritorna un messaggio di risposta contenente la guess, ottenuta in modo casuale, ma solo se il turno di gioco attuale è quello del remoto
  • il metodo storeSolution memorizza la solution del programmatore quando riceve il messaggio di tipo 202 (=notifyHaveSolution)
  • il metodo setForcedError interpreta come un intero la guess inviata dal programmatore: il valore numerico della guess viene usato come un codice per forzare le eccezioni

Forzare le eccezioni

Uno dei grandi vantaggi della connessione di test è che è possibile scrivere del codice per forzare il sollevamento delle eccezioni e verificare che siano gestite correttamente.
La strategia usata in questa applicazione è quella di usare le sequenze scelte dal giocatore umano (che nel caso della connessione di test è il programmatore) come codici per informare la connessione di test di sollevare una eccezione nella operazione immediatamente successiva. Vi sono due tipi di codici per forzare le eccezioni: quelli in fase di connessione e quelli in fase di gioco.

Eccezioni forzate in connessione

Il codice per forzare le eccezioni in fase di connessione deve essere specificato come porta del server nella connessione di test. I codici sono i seguenti:

  • 100: solleva una CommException nel metodo doConnect
  • 101: solleva una InterruptedException nel metodo doConnect
  • 110: in fase di presentazione, il remoto usa una versione della applicazione incompatibile con quella di questo server

Eccezioni forzate nella partita

Per forzare una eccezione nel corso della partita si deve inviare il codice di errore forzato nella guess. I codici sono i seguenti:

  • 102: solleva CommException nel doSendMessage
  • 103: solleva InterruptedException nel doSendMessage
  • 104: solleva ConnectionLostException nel doReadMessage
  • 105: inserisce un messaggio malformato nel doReadMessage
  • 106: solleva IllegalArgumentException, una eccezione non prevista nel doSendMessage
  • 107: invia una sequenza di resa
  • 108: la connessione di test indovina la sequenza dell'avversario

La nuova classe MasterMind08

La classe MasterMind è la classe base della logica del gioco. Il suo compito è solo quello di visualizzare nella GUI le notifiche degli eventi provenienti dal server di gioco il quale richiama i metodi notifyXxxxxx. Le stesse notifiche vengono inviate anche al giocatore remoto dal server; abbiamo visto che il compito della classe PlayerRemote è quello di inoltrare queste notifiche al oggetto connessione (vedi PlayerRemote: i metodi di notifica).
Da parte sua, la classe connessione crea una istanza della classe Message (vedi La classe Message) e la invia sul canale di comunicazione al client il quale riceve il messaggio attraverso La interfaccia ConnectionListener. Ma quale classe deve implementare la interfaccia ConnectionListener? La scelta è abbastanza ovvia: la classe MasterMind stessa dal momento che essa è deputata a visualizzare le notifiche sulla GUI del client.

Deriveremo pertanto da masterMind una nuova classe che chiamiamo MasterMind08 (il numero è un esplicito riferimento alla versione della app) il cui compito è solo quello di implementare la interfaccia ConnectionListener. Il metodo più interessante di tutta la classe MasterMind08 è senza dubbio quello in cui viene ricevuto un messaggio sul canale di comunicazione:

// File: MasterMind08.java
/* Needed by ConnectionListener interface */
public void messageReceived(Connection connection, Message msg)
{
... omissis...
switch ( msgType ) {
case 103 : logger.warning( "received unexpected presentation message" ); break;
case 202 : haveSolution( msg.getInt(1), msg.getField(2), false); break;
case 204 : haveGuess(msg.getInt(1), msg.getField(2), false); break;
case 301 : notifyNewGame(msg.getParams( 1 )); break;
case 302 : notifyStartGame( msg.getInt( 1 )); break;
//case 303 : non esiste
case 304 : notifySwapTurn(msg.getInt(1), msg.getInt(2)); break;
case 305 : notifyHaveResults(msg.getInt(1), msg.getField(2), msg.getResults(3)); break;
case 306 : notifyEndGame(msg.getInt(1), msg.getBoolean(2)); break;
case 307 : notifySolution(msg.getInt(1), msg.getField(2)); break;
//case 308 : NOT YET IMPLEMENTED break;
default :
logger.warning( "MasterMind.messageReceived() - unknown msg type: " + msgType + ", ignored" );
break;
}
... omissis...
}

Il metodo analizza il tipo di messaggio e in base ad esso richiama i metodi propedeutici alla notifica ricevuta dal server. La tabella dei codici dei messaggi è stata discussa in Tabella dei tipi di messaggio. A che scopo scrivere la classe derivata MasterMind08? Non sarebbe stato più semplice modificare il sorgente di masterMind stessa?
Certo, sarebbe stato più semplice ma aver scritto una classe nuova ha diversi vantaggi:

  • la classe MasterMind08 è derivata da masterMind: tutte le funzionalità della classe base sono preservate
  • le modifiche da apportare alla classe originale si documentano da sole: se osservate bene, i metodi modificati hanno la annotazione @Override mentre i metodi nuovi non la hanno
  • la classe masterMind08 introduce una dipendenza che MasterMind non avrebbe: la classe derivata dipende dal package mastermind.net e questo viene anche documentato nel listato sorgente stesso.

Il nuovo Main08

Come di consueto, deriviamo la nuova classe principale Main08 da quella della versione precedente in modo da mantenerne le funzionalità pregresse. Vi sono diversi metodi da sovrascrivere ed altri nuovi, da scrivere da zero.

I metodi sovrascritti

I metodi sovrascritti sono i seguenti:

  • connectPlayers: che deve istanziare un oggetto di tipo PlayerRemote
  • actionPerformed: che deve reagire ai bottoni di comando "Abort", "Retry" e "Start" del pannello della connessione (vedi Il pannello della connessione)
  • newGame: che deve verificare se vi è una connessione remota in essere ed istanziare il mastermind server o client a seconda del caso
  • terminate: che prima di chiudere la applicazione deve chiudere la connessione in corso, se esiste

Voglio attirare la attenzione del lettore al metodo sovrascritto newGame della applicazione principale Main08.

@Override
public MasterMind newGame() throws MMException
{
... omissis ...
if ( connection != null ) {
connection.setListener( (MasterMind08) game );
}
... omissis ...
}

Per passare come argomento al metodo setListener della classe Connection l'oggetto riferito dal membro dati game è stato necessario operare un explicit cast (=cast esplicito) sull'oggetto stesso. Il motivo di ciò è che il membro dati game è di tipo MasterMind (classe base) e questo tipo di dato NON implementa la interfaccia ConnectionListener.

I metodi nuovi

I metodi nuovi sono solo due: createConnection e swapPlayers.

Il metodo createConnection

Il metodo createConnection stabilisce la connessione e ritorna un oggetto di classe PlayerRemote il quale, a sua volta, sarà restituito dal metodo createPlayer.

Il codice del metodo createConnection non è particolarmente difficile:

  • se l'indirizzo o nomehost del server è uguale a "ConnectionTEST" istanzia la connessione di test; in caso contrario istanzia una connessione TCP/IP (che però non abbiamo ancora implementato ma che lo faremo in La connessione in Mastermind)
  • viene creato il pannello dello stato di avanzamento della connessione che fungerà da ConnectionListener per gli eventi di rete
  • il pannello della connessione viene impostato come content-pane del frame principale che sarà ridimensionato dal frame stesso

Il metodo swapPlayers

Questo metodo scambia i due giocatori: il giocatore ZERO diventa giocatore UNO e viceversa. Perchè scambiare i giocatori? Dal pannello delle proprietà il giocatore umano è sempre il giocatore ZERO mentre l'avversario è sempre il giocatore UNO ma in una connessione remota entrambi i giocatori sono umani e poichè non possono esistere due giocatori ZERO, il codice scambia i giocatori dal lato client del gioco.
In questo modo, il giocatore umano dal lato server sarà il giocatore ZERO mentre l'umano dal lato client sarà il giocatore UNO.

Ulteriore documentazione

La documentazione completa dei due packages descritti in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc del progetto: The MasterMind Project Version 0.8