|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
In questo capitolo e nei due successivi implementeremo la connessione remota di MasterMind e scriveremo le classi che gestiranno lo scambio dei messaggi tra i due giocatori umani remoti. Le classi che scriveremo in questo capitolo sono classi astratte, che non implementano un particolare protocollo di rete ma che sono la base per poter poi implementare i veri protocolli di rete con una certa facilità.
I files sorgente delle classi che si occupano della connessione remoata si trovano nel package mastermind.net il quale possiede due sottopackages:
mastermind.net.impl che contiene le classi che rappresentano l'effettiva implementazione di un protocollo di rete mastermind.net.udp che contiene le classi che implementano un particolare protocollo di rete, basato su IP, usato per la ricerca dei giocatori in una rete locale; questo argomento sarà affrontato in un capitolo a se stante: Version 0.9: la ricerca dei giocatori in rete| nome file | package | descrizione |
|---|---|---|
| MessageException.java | mastermind.net | messaggi invalidi o malformati |
| CommException.java | mastermind.net | errori sul canale di comunicazione |
| ConnectionLostException.java | mastermind.net | connessione chiusa dal remoto |
| Connection.java | mastermind.net | classe base astratta |
| ConnectionListener.java | mastermind.net | gestore degli eventi di comunicazione |
| ConnectionPanel.java | mastermind.net | pannello di avanzamento dello stato |
| Message.java | mastermind.net | il messaggio scambiato tra i giocatori |
| PlayerRemote.java | mastermind.net | il giocatore remoto |
| RecvQueue.java | mastermind.net | la coda di ricezione dei messaggi |
| SendQueue.java | mastermind.net | la coda di invio dei messaggi |
| ConnectionTCP.java | mastermind.net.impl | implementazione del protocollo TCP/IP |
| ConnectionTest.java | mastermind.net.impl | implementazione connessione di test |
| MasterMindClient.java | mastermind.game | il client mastermind |
| RecvWorker.java | mastermind/test/tcp | classe di supporto alla app ostile |
| TCPClient.java | mastermind/test/tcp | una app GUI ostile |
| TestMessage.java | mastermind/test/tcp | app CLI di test della classe Message |
| Main08.java | mastermind.app | classe principale versione 0.8 |
Considerato il numero piuttosto elevato su sorgenti, una breve panoramica è doverosa. Cominciamo con le classi eccezione:
MessageException: viene sollevata quando un messaggio ricevuto sul canale di comunicazione non rispetta il formato previsto dal protocollo oppure se le informazioni contenute sono incompatibili tra di loro CommException: viene sollevata quando intervengono errori sul canale di comunicazione; queste eccezioni dovrebbero essere recuperabili tentando una nuova connessione col remoto ConnectionLostException: derivata da CommException questa eccezione è un caso particolare di errore sul canale; viene sollevata quando la connessione viene chiusa, di solito intenzionalmente, dal remotoLe classi che si occupano della connessione, ricezione ed invio dei messaggi sono le seguenti:
Connection.java: si tratta di una classe base astratta che fornisce la logica di tutte le operazioni prepedeutiche: apertura e chiusura del canale, invio e ricezione dei messaggi; ConnectionListener.java: definisce la interfaccia del gestore di tutti gli eventi che scaturiscono dalla connessione; i metodi definiti nella interfaccia vengono richiamati dai threads separati usati nella classe Connection per non bloccare la GUI Message.java: questa classe definisce la formalità ed il contenuto di un messaggio Mastermind e definisce i metodi per ottenere le informazioni dal messaggio PlayerRemote.java: il tipo di giocatore remoto viene rappresentato da questa classe, derivata da Player che sovrascrive i metodi di notifica; unico scopo di questa classe è quello di inoltrare i messaggi di notifica provenienti dal server sul canale di comunicazione SendQueue.java: questa classe implementa la coda di invio dei messaggi sul canale di comunicazione; la coda è una ottima tecnica per l'invio di messaggi su un canale di comunicazione; essa risolve una serie di problemi che avremo modo di affrontare RecvQueue.java: questa è la coda di ricezione dei messaggi anche se nella app Mastermind una coda in ricezione non è affatto necessariaCome accennato in precedenza, le classi che fanno parte del package mastermind.net sono classi generiche, che gestiscono la logica della comunicazione ma non implementano alcun protocollo di rete reale. La effettiva comunicazione tra i due giocatori remoti viene realizzata da classi specializzate, derivate da Connection; il compito di queste classi specializzate è quello di implementare un vero e proprio protocollo di rete. Al momento vi sono solo due implementazioni di protocolli di rete, entrambe fanno parte del package mastermind.net.impl (impl sta per implementations)
ConnectionTCP: questa classe implementa il protocollo TCP/IP ConnectionTest: questa classe implementa un protocollo fittizio in grado di simulare una vera connessione ma che invece viene usata per il test del protocollo applicativoInfine, il package mastermind.test.tcp contiene una applicazione malevola, ostile scritta al solo scopo di mettere sotto stress il server di gioco e cercare di mandarlo in crisi; vogliamo verificare come reagisce il server a questi attacchi.
Prima di scrivere le classi che implementeranno la comunicazione tra giocatori remoti è necessario stabilire un protocollo di comunicazione, cioè quali messaggi e di che tipo le applicazioni dovranno scambiarsi. Abbiamo già incontrato un primo semplice esempio di protocollo applicativo nel capitolo Un piccolo telnet - il protocollo.
Possiamo suddividere i messaggi scambiati dalla applicazione MasterMind in tre categorie:
Tra i messaggi di servizio nel Mastermind includiamo il messaggio di presentazione nel quale i due giocatori si presentano l'un l'altro. In questo messaggio sono contenute le seguenti informazioni:
Un secondo messaggio di servizio è il cosidetto messaggio ECHO: si tratta di un messaggio in cui una delle due parti invia al remoto il timestamp di sistema, normalmente espresso in millisecondi. Questo messaggio sarà chiamato ECHO-REQUEST.
La controparte risponde a questo messaggio inviando un messaggio ECHO-RESPONSE che contiene lo stesso timestamp contenuto nella ECHO-REQUEST: calcolando la differenza tra il timestamp del messaggio ed il timestamp di ricezione della risposta si può ottenere la latenza della connessione cioè il ritardo in millisecondi introdotto dalla rete.
Il nome ECHO che ho dato a questo particolare tipo di messaggio deriva dal protocollo IP che prevede un particolare tipo di pacchetto denominato ICMP (Internet Control Message Protocol = protocollo dei messaggi internet di controllo) che ha proprio lo stesso scopo: calcolare la latenza della rete. Lo scambio del messaggio ECHO avviene proprio come un vero eco: il destinatario restituisce al mittente lo stesso identico pacchetto ricevuto.
L'eco viene usato, per esempio, per individuare i sottomarini in immersione grazie ad uno strumento chiamato sonar che produce quel caratteristico rumore che si sente spesso nei film del tipo "Caccia ad Ottobre Rosso": ping. Non è quindi un caso che il comando da usare per inviare i pacchetti ICMP-ECHO ad un qualsiasi host sulla rete sia proprio ping:
>ping 10.253.175.44
Esecuzione di Ping 10.253.175.44 con 32 byte di dati:
Risposta da 10.253.175.44: byte=32 durata=4ms TTL=64
Risposta da 10.253.175.44: byte=32 durata=4ms TTL=64
Risposta da 10.253.175.44: byte=32 durata=4ms TTL=64
Risposta da 10.253.175.44: byte=32 durata=4ms TTL=64
Statistiche Ping per 10.253.175.44:
Pacchetti: Trasmessi = 4, Ricevuti = 4,
Persi = 0 (0% persi),
Tempo approssimativo percorsi andata/ritorno in millisecondi:
Minimo = 4ms, Massimo = 4ms, Medio = 4ms
Tutti i messaggi di notifica provengono dal server e sono inviati al client che li deve visualizzare nella GUI. Questi messaggi descrivono gli eventi del gioco e sono già stati ampiamente commentati in La classe base del gioco. Un messaggio inviato al remoto deve contenere come dati gli argomenti forniti al metodo di notifica.
Vi sono due messaggi di questo tipo:
Riassumendo, la tabella dei tipi di messaggio scambiati da Mastermind è la seguente:
| codice | categoria | metodo | field1 | field2 | field3 |
|---|---|---|---|---|---|
| 101 | service | echoRequest | timestamp | ||
| 102 | service | echoResponse | timestamp | ||
| 103 | service | presentation | nickname | version | |
| 202 | input | haveSolution | turn | solution | |
| 204 | input | haveGuess | turn | guess | |
| 301 | notify | newGame | params | ||
| 302 | notify | startGame | turn | ||
| 304 | notify | swapTurn | turn | tryCount | |
| 305 | notify | haveResults | turn | guess | results |
| 306 | notify | endGame | winner | resign | |
| 307 | notify | solution | turn | solution | |
| 308 | notify | byeBye | playAgain | ||
| 312 | notify | error | turn | errorCode | errorDescr |
La classe Message rappresenta un messaggio scambiato sulla rete dalla applicazione. Essa possiede i seguenti membri dati:
type è il codice del messaggio come da tabella precedente id un identificativo unico del messaggio; si tratta di un contatore progressivo che parte da 10 ed arriva a 99999; non ha molta importanza e, al momento attuale, il suo unico scopo è quello di marcare un messaggio come malformato se questo ID è negativo fields è una array di quattro elementi stringa che contengono le informazioni che dipendono dal tipo di messaggio, come descritto nella tabella di cui sopraLa classe Message possiede quattro campi di informazioni anche se, come vediamo dalla tabella, ne basterebbero tre: si tratta di una ridondanza che ho previsto, una specie di espansione per futuri utilizzi.
La classe possiede quattro costruttori:
Message(): il costruttore di default è protetto poichè viene usato dagli altri costruttori per inizializzare i membri dati interni: in effetti, un messaggio senza il suo codice non ha molto senso Message(int type): costruisce un messaggio del tipo specificato nell'argomento: vi è da osservare che sarà comunque necessario impostare i campi del messaggio con le informazioni specifiche per il tipo di messaggio come evidenziato nella tabella di cui sopra; fà eccezione a questa regola il tipo 101 (echo-request) : in questo caso il costruttore inserisce nel primo campo il timestamp in millisecondi rendendo di fatto il messaggio completo Message(int type, int id): costruisce un messaggio del tipo e con la ID specificati negli argomenti: anche in questo caso vale quanto scritto poc'anzi a proposito del tipo di messaggio; per quanto riguarda la ID, ogni messaggio viene costruito con una ID univoca ma questo costruttore può essere usato per impostare una ID specifica, se necessario Message(Message msg): questo particolare costruttore accetta come argomento un messaggio di tipo 101 (=echo-request) e costruisce un messaggio di tipo 102 (=echo-response)La classe Message possiede molti metodi per impostare ed ottenere informazioni dai campi del messaggio oltre a metodi di servizio.
I metodi di servizio sono i seguenti:
getID(): ritorna l'identificativo del messaggio getType(): ritorna il tipo di messaggio toString(): ritorna una stringa che descrive il messaggioCome accenntato in precedenza, per costruire un messaggio è necessario specificarne il tipo ma questo non basta per considerare il messaggio completo: è anche necessario impostare correttamente i suoi campi in modo che contengano le informazioni propeduetiche al tipo di messaggio.
Per impostare i campi del messaggio si usa il metodo setField che è sovrascritto in modo da accettare vari tipi di dato in input:
setField(int numField, boolean value) setField(int numField, int value) setField(int numField, String str)Per sempio, per costruire un messaggio di presentazione, si dovrà eseguire un codice simile a questo:
Il metodo setField può sollevare la eccezione IndexOutOfBoundsException se l'argomento numField è fuori dal range permesso che è tra 1 e 4.
I metodi getter consentono di ottenere le informazioni da un oggetto di classe Message; esistono molti metodi getter specializzati per ottenere un certo tipo di dato. Tutti i metodi getter accettano un solo argomento: l'indice del campo del messaggio da dove estrarre la informazione. Questo indice va da 1 a 4.
getField(int numField): questo è il metodo base che restituisce una stringa dal momento che ogni campo del messaggio è una stringa; questo è l'unico metodo getter che solleva una eccezione IndexOutOfBoundsException nel caso l'indice del campo è fuori range (1 .. 4) getSequence(int numField): è simile al metodo precedente in quanto ritorna la stringa della sequenza me solleva una eccezione MessageException in caso di errori ossia se l'indice del campo è fuori range (1 .. 4) getBoolean(int numField): restituisce un booleano leggendo il campo fornito come argomento getInt(int numField): restituisce un intero leggendo il campo fornito come argomento getParams(int numField): restituisce un oggetto di classe Params leggendo il campo fornito come argomento getResults(int numField): restituisce un oggetto di classe Results leggendo il campo fornito come argomento getVersion(int numField): restituisce un oggetto di classe Version leggendo il campo fornito come argomentoAd eccezione del primo metodo getter, tutti gli altri sollevano eccezioni di tipo MessageException in caso di errori; vi possono essere due cause di errore nella lettura di un messagio:
IndexOutOfBoundsException nel caso l'indice del campo da leggere sia fuori dal range permesso (1 .. 4) Params, Results o Version E' abbastanza ovvio che eccezioni di tipo MessageException non dovrebbero mai essere sollevate nel corso del programma: se dovessero presentarsi significa che c'è un bug nel codice dal momento che tutti i messaggi vengono creati dal codice Java e quindi un errore nel formato delle informazioni o un errore nel indicare il numero del campo dove ottenere l'informazione è sicuramente un bug.
Derivata da Player, la classe PlayerRemote rappresenta un giocatore remoto per la macchina locale: esso viene costruito con un unico argomento: il turno di gioco. Oltre ai membri dati definiti nella classe base (vedi I giocatori del Mastermind) questa classe aggiunge un solo membro dati:
che rappresenta l'oggetto connessione su cui il giocatore remoto si appoggia per l'inoltro dei messaggi di notifica provenienti dal server (vedi Il diagramma di flusso). Nella parte client, di contro, l'oggetto PlayerRemote non è chiamato in causa: in teoria potrebbe esserlo per l'input delle sequenze del giocatore lato client ma l'input viene gestito dalla GUI e non è affatto necessario passare per l'oggetto giocatore. La classe del giocatore remoto sovrascrive quasi tutti i metodi della classe base:
Dal momento che i metodi astratti devono essere obbligatoriamente sovrascritti, la classe PlayerRemote li sovrascrive tutti non essendo essa stessa una classe astratta:
getGuess e getSolution: le sequenze non vengono elaborate dalla classe PlayerRemote ma digitate dal giocatore umano che sta dalla altra parte della connessione; questi metodi ritornano semplicemente null. getType: ritorna Player.Type.REMOTE isLocal: ritorna sempre false isConnected: ritorna true solo se il membro dati connection non è null e il metodo Connection.isConnected ritorna true.getConnection: ritorna l'oggetto connessione collegato a questo giocatore remoto; l'oggetto connessione viene istanziato quando i due giocatori iniziano la prima partita e rimane in essere fino al termine della applicazione. setConnection: imposta l'oggetto connessione collegato a questo giocatore remoto; poichè l'oggetto connessione persiste per tutta la durata della applicazione questo metodo viene richiamato quando la connessione è stata stabilita con successo.Vi è da osservare che la connessione di rete può cadere e deve essere ristabilita. Tuttavia in questo caso l'oggetto connessione istanziato la prima volta non viene dereferenziato ma si usa sempre lo stesso oggetto per tentare la riconnessione con gli stessi parametri. In ogni caso, l'oggetto connessione passato a PlayerRemote è quello che ha stabilito (o ri-stabilito) una connessione con successo.
Tutti i metodi di notifica provenienti dal server di gioco vengono inviati alla macchina remota attraverso l'oggetto connessione il cui riferimento è memorizzato nel membro dati connection.
notifyEndGame notifyHaveGuess notifyHaveResults notifyHaveSolution notifyNewGame notifySolution notifyStartGame notifySwapTurn I metodi richiamano una delle versioni sovraccaricate del metodo di I/O sendNotify per eseguire la operazione. Per esempio, questo è il codice del metodo notifyNewGame che invia un messaggio di tipo 301 con i parametri di gioco nel primo campo della classe Message:
Notate che la prima istruzione del metodo è quella di richiamare la versione base del metodo; questo è assolutamnete necessario poichè nel codice del metodo della versione base vi sono importanti elaborazioni.
Poichè un messaggio può essere composto da uno, due o tre campi, a seconda del tipo di messaggio, esistono tre versioni sovraccaricate di questo metodo per pura convenienza:
void sendNotify(int type, String field1): invia un messaggio di notifica che contiene un solo campo significativo void sendNotify(int type, String field1, String field2): invia un messaggio di notifica che contiene due campi significativi void sendNotify(int type, String[] fields): invia un messaggio di notifica composto da tre o più campi significativiVi è da osservare che le prime due versioni del metodo sendNotify si appoggiano alla terza versione: essi allocano semplicemente una array di uno o due elementi che conterranno gli argomenti al metodo e poi richiamano la terza versione che crea una istanza della classe Message e la invia:
sendMessage Il metodo sendMessage è piuttosto semplice: non fa altro che verificare che l'argomento ed il membro dati connection non siano null e richiama il metodo Connection.sendMessage il quale provvederà all'effettivo invio del messaggio.
Noterete la assenza di una clausola throw nonostante sappiamo che le primitive di I/O possono sollevare eccezioni di cui una di tipo checked: la IOException.
Non è una dimenticanza ma una precisa scelta strategica: gli eventi che riguardano le operazioni di I/O sulla connessione vengono notificati attraverso un listener (vedi Il listener degli eventi). Il metodo sendMessage della connessione non esegue veramente l'invio del messaggio ma si limita ad accodare il messaggio nella coda di invio; una volta richiamato il metodo sendMessage ci si può disinteressare del suo esito; il messaggio sarà recapitato oppure saranno notificati degli errori al listener degli eventi.
Quello che conta è che il metodo sendMessage, eseguito nel EDT, ritorni immediatamente, in modo che la GUI non si blocchi.
readMessage Il metodo readMessage si comporta praticamente allo stesso modo. Non solleva eccezioni, questo è vero, ma allo stesso tempo ritorna sempre null poichè anche il metodo Connection.readMessage ritorna null.
Si tratta di una classe base astratta che dichiara i metodi propedeutici per compiere le operazioni necessarie alla connessione ed allo scambio dei messaggi tra le due copie della applicazione, quella locale e quella remota. Vi sono sostanzialmente sei operazioni da compiere ed ad esse sono dedicati sei metodi specifici:
connect: che avvia la connessione remota reconnect: che tenta la ri-connessione in caso la connect sia fallita recover: che tenta di chiudere e riaprire la connessione in caso di errori sul canale di trasmissione sendMessage: che invia il messaggio fornito come argomento readMessage: che legge un messaggio dal canale close: che chiude la connessioneNelle prossime sotto-sezioni analizzeremo tutti questi metodi.
I membri dati della classe Connection sono dichiarati in modo da dare accesso alle classi derivate:
state: lo stato della connessione; vedi Lo stato della connessione listener: il listener degli eventi che scaturiscono dalla connessione; vedi Il listener degli eventi remote: il giocatore al quale dovrà essere notificata la avvenuta connessione Il giocatore remoto workerConnect: il worker-thread entro il quale avviene la connessione I metodi di I/O della connessione sendQueue: la coda di invio dei messaggi; vedi La coda di invio recvQueue: la coda di ricezione dei messaggi; vedi La coda di ricezioneNella classe base astratta viene definito un enumeratore che rappresenta lo stato della connessione:
Concordo con voi che forse gli stati enumerati sono ridondanti; in sostanza quello che interessa davvero è solo se la connessione è attiva oppure no. Per esempio, che differenza c'è tra NOT_CONNECTED e TERMINATED: se vogliamo cercare il pelo nell'uovo si potrebbe intendere che
Nella classe Connection è definito un enumeratore che ha il compito di rappresentare quale operazione di I/O è in corso di esecuzione; viene usata per segnalare gli errori sul canale:
La classe Connection possiede un solo costruttore al quale devono essere passati tre argomenti:
ConnectionListener: il listener degli eventi che scaturiscono dalla connessione; il listener sarà la applicazione principale definita dalla classe Main08. Player il giocatore remoto; questo è necessario perchè quando la connessione sarà stabilita con successo, alla classe PlayerRemote và notificato quale oggetto connessione deve usare per l'inoltro delle notifiche provenienti dal server di gioco MMProperties: le proprietà della applicazione; esse contengono i parametri di rete necessari a stabilire la connessioneNel costruttore si verifica che l'oggetto passato come argomento player sia effettivamente una istanza di PlayerRemote. In difetto, sarebbe un bug enorme, inutile continuare il programma.
Oltre a quelli elencati di seguito, ci sono altri metodi astratti che però saranno commentati in seguito, quando affronteremo i quattro metodi di I/O dei quali ho accennato all'inizio di questa sezione.
getRemoteAddr: restituisce una stringa che descrive la macchina remota; questa stringa dipende dal protocollo usato nella connessione; per esempio, nel protocollo IP essa sarà l'indirizzo IP o il nomehost del computer remoto isServer: il metodo ritorna TRUE se questo oggetto è il lato server della connessione; in una connessione TCP/IP vi è sempre un lato server ed un lato client di una connessione. In altri protocolli di comunicazione potrebbe non esistere questa differenza ma sarà compito del programmatore stabilire comunque un lato server fittizio: questo è necessario per stabilire quale delle due istanze di Mastermind funge da server di giocoI metodi concreti dell'oggetto Connection gestiscono quegli aspetti non legati ad un particolare protocollo e sono, per lo più, quelli che restituiscono i dati interni della classe, i cosidetti metodi getter:
getState: ritorna l'enumerato già descritto in Lo stato della connessione che rappresenta lo stato della connessione isConnected: ritorna TRUE se lo stato della connessione è State.CONNECTED getRemotePlayer: ritorna l'oggetto di classe PlayerRemote associato a questo oggetto connessione getListener e setListener: questi metodi restituiscono ed impostano il listener degli eventi della connessione; questo aspetto verrà approfondito nella sezione Il listener degli eventiCome accennato in La classe Connection vi sono sei metodi che eseguono le operazioni di input e output vero e proprio in una connessione:
connect: che avvia la connessione remota sendMessage: che invia il messaggio fornito come argomento readMessage: che restituisce il messaggio ricevuto dal remoto close: che chiude la connessione reconnect: che tenta la ri-connessione recover: che prima di tentare una ri-connessione, chiude quella in corso: questo metodo richiama prima close e poi reconnect Il metodo reconnect differisce da connect perchè nel protocollo Mastermind alla prima connessione i due giocatori si scambiano un messaggio di presentazione. Tuttavia, la riconnessione è una operazione da fare quando la connessione viene persa durante il gioco: non è necessario presentarsi nuovamente anzi, è una operazione da evitare.
Tutti questi metodi di I/O eseguono le operazioni in un thread separato istanziando un oggetto di classe SwingWorker: nel metodo in background viene eseguita la operazione di I/O effettiva mentre nel metodo done viene richiamato il metodo di comodo del listener.
Prendiamo come esempio il metodo connect, ma tutti gli altri sono molto simili:
Come prima operazione il metodo connect verifica che non ci sia già in esecuzione un worker-thread che si occupa della connessione; dobbiamo evitare il proliferare dei worker-threads. Nel metodo eseguito in background, viene richiamato il metodo doConnect: questo è il metodo che esegue la vera e propria operazione di input/output sul canale di comunicazione. Questo aspetto sarà discusso in I metodi astratti di I/O.
Una volta conclusa la connessione, il metodo done richiama il listener degli eventi informando il thread principale che la connessione è stata stabilita con successo.
Il metodo sendMessage potrebbe essere scritto allo stesso modo: si crea un worker-thread separato nel cui metodo doInBackground si richiama il doSendMessage: facile come bere un bicchier d'acqua. Stessa cosa per il metodo readMessage, no? Ebbene, benchè questa soluzione sia davvero di facile implementazione è pessima: soffre di problemi piuttosto severi non facili da risolvere.
Possono essere risolti, questo è vero, ma al prezzo di introdurre complessità al codice che è quello che invece la soluzione dovrebbe evitare. Per questo motivo l'invio e la ricezione dei messaggi sono stati implementati con una tecnica totalmente diversa; le code. Nel capitolo Versione 0.8: le code di invio e ricezione descriverò questa soluzione che, al prezzo di un piccolo sforzo di codifica, permette di superare i problemi elegantemente ed efficacemente.
Avrete notato che i metodi di I/O della classe Connection NON eseguono la vera operazione ma richiamano un metodo astratto il cui nome è uguale al metodo di I/O preceduto da "do". Abbiamo quindi quattro metodi astratti definiti nella classe Connection:
doConnect doSendMessage doReadMessage doClose Questi quattro metodi astratti, che devono essere definiti in una classe derivata, sono i veri responsabili della effettiva operazione di input / output. Con questa organizzazione, è facile implementare protocolli di rete diversi: basta derivare da Connection ed implementare i quattro metodi astratti.
Ed è proprio quello che avviene in Mastermind: anche se per il momento viene implementato il solo protocollo TCP/IP, aggiungere altri protocolli risulta abbastanza facile.
Abbiamo imparato in Un piccolo telnet che la implementazione di una trasmssione e di un protcollo applicativo non è particolarmente difficile da scrivere. La applicazione telnet funziona piuttosto bene ma soffre di un problema: tutte le operazioni che riguardano l'input e output sono bloccanti: il server deve attendere un comando dal client per poter continuare la elaborazione e nella attesa non può fare nient'altro!
Dall'altro lato, il client deve attendere la risposta del server per poter continuare la elaborazione ed accettare un ulteriore comando dal utente. Per il telnet questo non costituisce un serio problema specialmente dal lato client: inutile impartire un altro comando se non abbiamo avuto la risposta di quello precedente.
Diverso il discorso per il lato server: in attesa di input sulla connessione, il server non può fare nulla, nemmeno accettare una nuova richiesta di connessione da un secondo client e questo, per un server degno di questo nome, sarebbe inaccettabile.
Ma torniamo a MasterMind: in un gioco basato sui turni, il fatto che lo scambio dei messaggi sia bloccante non è nemmeno così scandaloso: il giocatore che non è in turno non può fare altro che aspettare che l'avversario abbia inviato la sua guess, la ipotesi di soluzione, prima di poter continuare.
Tuttavia, questa è una applicazione GUI e non è accettabile per l'utente che la interfaccia si congeli e diventi non responsiva: egli potrebbe volersi arrendere anche se non è in turno quando capisce che oramai l'avversario è molto vicino alla soluzione.
Per questo motivo implementeremo una soluzione che nonostante il gioco sia basato sui turni, non blocchi del tutto la GUI e dia la possibilità al giocatore non in turno di interagire comunque con la interfaccia grafica. Ovviamente, egli non potrà inviare una guess al server se non è in turno ma potrebbe comunque prepararla clikkando sui bottoni di comando.
Per ottenere una comunicazione di rete non-bloccante (ma il discorso si applica a tutte le operazioni di input / output, non solo alle reti) vi sono sostanzialmente due strade:
SelectorProvider; quando i dati sono disponibili per la lettura e/o la scrittura il SelectorProvider "seleziona" il canale e ne richiama il codice che esegue la operazione Quale dei due approcci è il migliore? Come ribadito più e più volte nel corso di questo tutorial, non esiste l'approccio migliore in assoluto: entrambi hanno vantaggi e svantaggi e tutto dipende dal tipo di applicazione.
Il primo approccio ha il vantaggio di eseguire in un unico thread: prendiamo ad esempio un server web che può servire potenzialmente migliaia o decine di migliaia di connessioni che, per la natura del protocollo HTTP, durano pochissimo tempo: creare un thread separato per ogni connessione ha un costo non indifferente sia in termini di tempo che di consumo di memoria e quindi è conveniente usare i selettori in questo contesto. Lo svantaggio di questa soluzione è che la gestione dei selettori non è banale ed il codice necessario è piuttosto complesso; è molto più facile creare un thread separato, specialmente usando Java, ricordate? (vedi La classe SwingWorker).
Il secondo approccio ha il vantaggio della semplicità di gestione e della stesura del codice ma, di contro, è molto più avido di risorse. Per una applicazione che instaura una sola connessione di rete durante tutta la sua esecuzione e che scambia messaggi solo nel turno di gioco, il costo della creazione del thread di I/O è irrisorio. Quindi questa è la strategia migliore per MasterMind ed è quella che useremo.
In un approccio non-bloccante per le operazioni di I/O dobbiamo rivedere tutte le nostre strategie che abbiamo imparato nel capitolo precedente. Prendiamo come punto di partenza il codice del client telnet discusso in Il blocco try-catch, che, per comodità, riassumo brevemente di seguito:
la riga marcata "I/O sul canale" mostra l'operazione bloccante: quando il metodo rientra abbiamo due certezze, una alternativa all'altra:
Nel primo caso, il comando è stato inviato al server e la riga successiva attende e stampa la risposta del server: il metodo printResponse legge la risposta del server in modo bloccante: solo quando la risposta è completa essa viene stampata sul terminale.
In un approccio non-bloccante, invece, il metodo che esegue la operazione di I/O rientra immediatamente ma non è assolutamente possibile stabilire se l'operazione è andata a buon fine oppure no. Ma non solo: anche se il comando sarà inviato con successo al remoto non sappiamo quando l'operazione sarà conclusa nè sappiamo quando sarà disponibile una risposta.
Questo perchè la effettiva trasmissione del messaggio è posticipata ed affidata al thread separato la cui esecuzione è asincrona: non è possibile stabilire quando la operazione sarà completata. Tuttavia, la applicazione non può ignorare lo stato della trasmissione: e se ci sono stati errori?
La soluzione a questo problema è quella di dichiarare dei metodi che il thread separato di I/O richiami quando le operazioni sono concluse: questi metodi saranno eseguiti nel thread principale. In molti linguaggi simili metodi vengono chiamati callback methods (=metodi callback).
In quale classe dovranno essere definiti questi metodi? Nella app principale (la classe Main)? O forse nel oggetto game (la classe MasterMind)? Risposta: non dobbiamo deciderlo adesso anzi, lasciamo aperte tutte le porte: qualunque classe può gestire gli eventi asincroni di I/O purchè implementi la interfaccia ConnectionListener.
I metodi dichiarati dalla interfaccia sono questi:
connectionAborted: la connessione è stata interrotta prima ancora di essere stata stabilita con successo connectionError: le operazioni di I/O (connect, send e read) hanno sollevato una eccezione di tipo MMException la quale contiene come cause la effettiva eccezione sollevata dai metodi di I/O connectionEstablished: viene richiamato quando la connessione è stata stabilita con successo. connectionClosed: viene richiamato quando la connessione viene chiusa connectionStarted: viene richiamato all'inzio della operazione di connessione dai metodi connect(), reconnect() e recover() messageReceived: un messaggio completo è stato ricevuto sul canale messageSent: un messaggio è stato inviato con successo sul canale reconnected: viene richiamato quando la connessione è stata ristabilita con successo dal metodo reconnect e recover.I threads che eseguono le operazioni di I/O non richiamano direttamente i metodi della classe che implementa la interfaccia ma, invece, richiamano i metodi di comodo della classe Connection stessa; saranno questi metodi di comodo che richiameranno quelli della interfaccia; questo è necessario perchè la classe Connection deve aggiornare il suo stato interno in risposta agli eventi occorsi sul canale di trasmissione.
Pensiamo per esempio al evento connectionEstablished: oltre ad aggiornare il membro dati state ed impostarlo a CONNECTED, il metodo di comodo richiama il PlayerRemote.setConnection in modo che l'oggetto Player possa inoltrare i messaggi di notifica provenienti dal server di gioco alla connessione.
I metodi di comodo della classe Connection hanno lo stesso nome di quelli della interfaccia ConnectionListener. Di seguito è riportato il codice di uno dei metodo di comodo. Notate che, alla fine del metodo, viene richiamato l'omonimo metodo del listener degli eventi:
La classe che implementa la interfaccia Connectionlistener è la applicazione principale e cioè il Main08. Essa riceve i messaggi e gli eventi dal oggetto connessione e li smista al oggetto deputato ad elaborarli: il pannello dello stato di avanzamento se siamo nella fase di connessione e presentazione; l'oggetto game se siamo in una partita in corso.
I metodi sono di facile lettura, unica nota merita il metodo connectionEstablished il quale, oltre a visualizzare i messaggi informativi sul pannello dello stato di avanzamento della connessione provvede ad inviare il messaggio ECHO-REQUEST.
Anche il metodo connectionError merita un commento: esso visualizza l'errore nel pannello di stato e abilita il bottone "Retry" sul pannello stesso in modo che l'user abbia la possibilità di ritentare con gli stessi parametri scelti in precedenza.
A proposito di errori di connessione, essi possono accadere molto più frequentemente di quanto si possa immaginare. Prendiamo il caso in cui un client tenta di connettersi al lato server del gioco ma il giocatore server non ha ancora iniziato la propria parte di connessione; non essendoci alcun server in ascolto sulla porta specificata nei parametri di connessione, il client otterrà un messaggio di errore che sarà visualizzato nel pannello della connessione:
Connection refused: connect
Per risolvere l'empasse è sufficiente che il giocatore lato server inizi le operazioni di connessione clikkando il bottone Play. Ora che il server è in ascolto, il client potrà connettersi correttamente clikkando il bottone "Retry".
Se c'è una cosa che fà imbufalire gli user è quella di non sapere cosa sta facendo la applicazione; può accadere che per stabilire la connessione sia necessario un pò di tempo e non è piacevole per lo user vedere lo splashscreen o il pannello delle proprietà per troppo tempo ignaro del fatto che la applicazione sta tentando di connettersi al remoto. Molte applicazioni (specialmente quelle professionali) inviano un feedback allo user attraverso messaggi sulla barra di stato ma, nel Mastermind la barra di stato è disponibile solo quando una partita è cominciata.
Per ovviare a questo problema ho disegnato un pannello della connessione implementato dalla classe ConnectionPanel che viene visualizzato nel frame subito dopo che l'user clikka il bottone Play. In questo pannello saranno fornite le informazioni sullo stato di avanzamento della connessione. Questo è un esempio di output del pannello connessione in versione definitiva:
Connecting ... ConnectionTEST Connected to: ConnectionTEST Sending echo-request message Estimated latency: 155 ms Sending presentation message Remote nickname: ConnTEST Local version: 0.8.0 Remote version: 0.8.0
I messaggi visualizzati sul pannello della connessione sono di facile comprensione ma un breve commento lo meritano:
connectionStarted connectionEstablished Il pannello contiene tre bottoni di comando:
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
Argomento precedente - Argomento successivo - Indice Generale