Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Version 0.9: la ricerca dei giocatori in rete

Introduzione

In questo capitolo il lettore impererà l'uso di un protocollo, basato su IP, alternativo al TCP: il protocollo UDP (User Datagram Protocol). Oltre a questo, verrà affrontato il tema della gestione dei risultati intermedi in un wroker-thread poichè, in alcune situazioni particolari (come questa) essa non è affatto banale.

In una partita tra giocatori remoti usando il protocollo TCP/IP vi è il problema per il lato client della connessione di indicare nei parametri di rete l'indirizzo IP o il nome della macchina che funge da server di gioco. E' un problema che si risolve facilemente usando il comando ipconfig dal prompt di MS-DOS:

>ipconfig
   Indirizzo IPv6 locale rispetto al collegamento . : fe80::d879:5dc6:dbeb:dadb%10
   Indirizzo IPv4. . . . . . . . . . . . : 10.117.242.114
   Subnet mask . . . . . . . . . . . . . : 255.255.255.0
   Gateway predefinito . . . . . . . . . : 10.117.242.7

ma gli user sono pigri e, molti di essi, analfabeti in senso informatico, e la maggior parte di essi ignora persino l'esistenza del comando ipconfig. Dobbiamo implementare un feature che faciliti la ricerca dei giocatori online. La soluzione più professionale sarebbe quella di implementare un server centralizzato a cui i giocatori si connettono; in questo modo tutti i giocatori sarebbero dei client e l'indirizzo IP sarebbe conosciuto.
Se il server fosse su un dominio specifico come per esempio mastermind.net non sarebbe nemmeno necessario conoscere il suo indirizzo IP: ci pensa il DNS (Domain Name System = il sistema dei nomi a dominio) a risolvere il nome del host in un indirizzo IP. Il nome host potrebbe addirittura essere hardcoded nel codice del pannello delle proprietà.

Mi sembra uno spreco di denaro comprare un dominio per un giochino per bambini. Quello che basta ai giocatori di Mastermind è trovare un avversario nei paraggi per esempio un familiare nella stanza accanto oppure un collega dell'ufficio accanto al nostro o al limite al piano di sopra della sede aziendale.
E il protocollo UDP è proprio quello che risolve questa situazione (a gratis).

Il protocollo UDP

Come il TCP/IP, il protocollo UDP si basa su IP ed è un protocollo di trasporto che si trova al quarto livello della catena ISO/OSI (vedi Il modello ISO/OSI). A differenza di TCP, che è connection oriented (=orientato alla connessione) il protocollo UDP è connectionless (=senza connessione).
Se TCP somiglia ad una telefonata (vedi Il protocollo TCP/IP) UDP assomiglia alle cartoline postali: il mittente scrive un pacchetto UDP specificando il mittente ed il destinatario. Successivamente, invia il pacchetto UDP sulla connessione di rete: il mittente non ha alcuna garanzia che il pacchetto sia stato letto; non si è nemmeno certi che sia arrivato, o che sia arrivato integro, o che il destinatario sia davvero connesso; potrebbe anche aver disabilitato l'interfaccia di rete (p.es. la modalità aereo nei computer portatili).

Il protocollo TCP connette due endpoints tramite un socket che è unico in ambito Internet (vedi Cos'è un socket?); in pratica TCP connette due, e solo due, dispositivi. UDP, al contrario, consente di inviare i messaggi a molti destinatari senza conoscerne l'indirizzo IP specifico ma usando un indirizzo IP speciale chiamato

  • indirizzo di broadcast: inviato a tutti i dispositivi
  • indirizzo multicast: inviato a uno o più destinatari che fanno parte di un gruppo

L'indirizzo di broadcast

L'indirizzo di broadcast è specifico di una rete locale e si ottiene eseguendo la operazione logica OR tra l'indirizzo di rete e l'inverso della sua netmask. L'indirizzo di rete si ottiene eseguendo la operazione logica AND tra l'indirizzo IP del proprio dispositivo e la netmask.

indirizzo host       10.117.202.114
AND netmask         255.255.255.0
                    ---------------
indirizzo di rete    10.117.202.0
OR netmask inversa    0.  0.  0.255
                     --------------
indirizzo broadcast  10.117.202.255

Pertanto, se io invio un messaggio all'indirizzo IP 10.117.202.255 esso raggiungerà TUTTI i dispositivi collegati alla mia stessa rete locale. E per raggiungere le altre reti? Non si può: i pacchetti UDP broadcast e multicast non possono superare il gateway, cioè il nostro router.

L'indirizzo multicast

L'indirizzo di broadcast viene usato raramente e solo per scopi specifici come per esempio nel procollo ARP (Adress Resolution Protocol) che è un protocollo di secondo livello del modello ISO/OSI (livello data-link). ARP è usato, per esempio, nelle reti ethernet per risolvere un indirizzo IP in un MAC address, l'indirizzo fisico della scheda ehternet.
Nelle applicazioni si preferisce inviare i pacchetti destinati a più macchine al cosidetto indirizzo multicast (=destinazione multipla) poichè con il multicast è possibile far giungere i pacchetti solo ai destinatari che fanno parte dello stesso "gruppo" cioè solo a coloro che si registrano con lo stesso indirizzo multicast.
Gli indirizzo multicast sono definiti ed assegnati da IANA (=Internet Assigned Numbers Authority = autorità per l'assegnazione dei numeri Internet) e si trovano nel range che va da 224.0.0.0 fino a 239.255.255.255

I pacchetti UDP in Mastermind

Come accennato, se io invio un pacchetto UDP ad un indirizzo broadcast o multicast esso raggiunge molti destinatari anche se il mittente non sa nulla di quanti destinatari abbia raggiunto nè se il messaggio, giunto a destinazione regolarmente, sia stato effettivamente letto da una qualche applicazione.

La strategia implementata in mastermind per cercare giocatori in rete è la seguente:

  • il server di gioco inizia la connessione mettendosi in ascolto sulla porta specifica di Mastermind e, contemporaneamente, invia ad intervalli regolari un pacchetto UDP multicast che contiene il proprio indirizzo IP ed il nome del giocatore; questo è il lato "sender" del protocollo UDP
  • nel pannello delle proprietà il client Mastermind è in attesa che l'user specifichi l'indirizzo IP del server a cui connettersi per giocare
  • quando è visibile il pannello delle proprietà il lato client della connessione deve eseguire un thread che si registra allo stesso indirizzo multicast del server; questo è il lato "receiver" del protocollo UDP
  • quando viene ricevuto il pacchetto UDP dal server, il client estrae le informazioni in esso contenute: l'indirizzo IP ed il nome del giocatore. Con queste informazioni prepara una stringa nel formato nome_giocatore@hostname/indirizzo_IP:porta
  • il componente "serverAddr" del pannello delle proprietà diventa una combo-box nella quale si possono aggiungere le stringhe ricevute dal sender
  • l'user non deve più specificare un indirizzo IP nel campo di testo "server address" ma può semplicemente selezionarlo dalla lista a discesa della combo-box

Per chi vuole rispolverare le caratteristiche principali delle combo-box ricordo che ne ho già scritto in Il componente JComboBox.

Le interfacce di rete

Per inviare i pacchetti UDP ad un indirizzo multicast è necessario registrarsi ad uno specifico "gruppo" in modo che i pacchetti UDP inviati al gruppo raggiungano tutti i membri del gruppo stesso. Il gruppo viene definito con due informazioni

  • l'indirizzo multicast che per default è il 230.18.8.0 in Mastermind essere cambiato nelle proprietà della applicazione.
  • l'interfaccia di rete su cui inviare / da cui ricevere i pacchetti UDP

Ogni macchina connessa ad una rete possiede almeno due interfacce di rete:

  • la scheda di rete, per esempio Ethernet o wireless a cui viene assegnato un indirizzo IP specifico
  • la interfaccia di loopback, cioè la macchina stessa, il cui hostname è localhost ed il cui indirizzo IP è il 127.0.0.1

La strategia di Mastermind è quella di inviare e ricevere i pacchetti UDP su tutte / da tutte le interfaccie di rete attive ed abilitate al multicasting: in questo modo il giocatore può connettersi ad un qualsiasi remoto su diverse reti. Per esempio una macchina potrebbe essere collegata ad una rete Ethernet ed anche ad una wireless: nel pannello delle proprietà la lista dei server Mastermind conterrà sia i server di gioco presenti sulla LAN cablata che su quella senza fili.

Le classi Java

Il JRE (Jave Runtime Environment) mette a disposizione del programmatore una nutrita serie di classi per gestire i protocollo UDP e per gli indirizzamenti multicast. Queste classi fanno parte del package java.net. Le classi Java usate nel progetto mastermind sono:

  • NetworkInterface: rappresenta una interfaccia di rete
  • MulticastSocket: il socket associato ad un indirizzo multicast ed ad una specifica interfaccia
  • DatagramPacket: il pacchetto UDP inviato / ricevuto attraverso il socket
  • InetAddress: rappresenta un indirizzo IP, nel caso di Mastermind è un indirizzo IP multicast
  • InetSocketAddress: rappresenta un endpoint di un socket e quindi contiene, oltre al InetAddress anche la porta a cui ci si collega

La classe NetworkInterface

Questa classe Java rappresenta una interfaccia di rete che può essere attiva oppure no. La classe non possiede costruttori: per ottenere un oggetto di questa classe è necessario richiamare uno dei suoi metodi statici quali:

  • getByInetAddress: che ritorna la interfaccia di rete che ha uno specifico indrizzo IP
  • getByName: che ritorna la interfaccia di rete che ha uno specifico nome come per esempio "wireless" o "ethernet"
  • getNetworkInterfaces: che ritorna un enumerato contenente tutte le interfacce di rete presenti sulla macchina

Una volta ottenuto un oggetto di classe NetworkInterface è possibile richiamare i suoi metodi concreti per ottenere una serie di informazioni sulla interfaccia di rete:

  • isLoopback(): ritorna TRUE se la interfaccia è quella di loopback
  • isUp(): ritorna TRUE se la interfaccia è attiva
  • isVirtual(): ritorna TRUE se la interfaccia è virtuale
  • supportsMulticast(): ritorna TRUE se la interfaccia supporta il multicasting

Ovviamente, il secondo e l'ultimo metodo concreto sono proprio quelli che interessano noi per la ricerca dei giocatori in rete nel progetto Mastermind.

La classe MulticastSocket

Questa classe rappresenta il socket multicast sul quale si inviano e si ricevono i pacchetti UDP. I metodi predisposti allo scopo sono i soliti: send per inviare il pacchetto e receive per ricevere un pacchetto. Un altro metodo definito dalla classe e che ci interessa da vicino è il

void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf)

il quale, come accennato in precedenza, "registra" questo socket nel "gruppo" multicast. Il primo argomento da fornire al metodo è l'indirizzo multicast del gruppo (nel caso del Mastermind è il "230.18.8.0"); il secondo argomento al metodo è la interfaccia di rete che deve fare parte del gruppo.
Da notare che possono far parte dello stesso gruppo multicast anche due o più interfacce di rete ed infatti è ciò che accade in Mastermind: si cercano giocatori su tutti i collegamenti in rete, su qualaiasi interfaccia.

La classe DatagramPacket

Un pacchetto UDP è rappresentato dalla classe DatagramPacket che possiede diversi costruttori ma tutti condividono almeno un argomento: una array di byte che costituisce il messaggio vero e proprio che viene chiamato in gergo payload.
Vi è da osservare che l'input/output del protocollo UDP è ben diverso da quello che abbiamo visto per il TCP/IP (vedi JavaNIO (Java New IO)): in UDP non ci sono i canali o i buffers e nemmeno gli streams che abbiamo usato in Un piccolo telnet.
Quello che abbiamo nel protocollo UDP è un buffer di bytes che viene inviato e ricevuto in modalità bloccante: Nella classe MulticastSocket abbiamo solo due metodi di I/O e tutti e due accettano come argomento solo un oggetto di questa classe:

public void send(DatagramPacket p) throws IOException
public void receive(DatagramPacket p) throws IOException

La classe InetSocketAddress

Useremo una istanza di questa classe per creare un oggetto di classe SocketAddress contenete l'indirizzo IP multicast col quale possiamo "registrare" il socket nel gruppo mastermind:

try {
InetAddress mcastaddr = InetAddress.getByName( "230.18.8.0" );
InetSocketAddress group = new InetSocketAddress( mcastaddr, 3963 );
socket = new MulticastSocket( port );
socket.joinGroup( group, networkInterface );
}
catch( IOException | SecurityException | IllegalArgumentException ex )
{ ... }

La classe SocketAddress è una classe base astratta non legata ad un particolare protocollo di rete. Derivata da essa vi è la InetSocketAddress che possiede dei costruttori e che rappresenta un endpoint di un socket e cioè l'indirizzo IP ed il numero di porta. I suoi due metodi getter principali consentono di ottenere i due dati di cui sopra.
In particolare, il metodo getAddress restituisce un oggetto di classe InetAddress che contiene l'indirizzo IP del socket-address.

Una ultima nota: la classe InetAddress rappresenta un indirizzo del protocollo IP (=Internet Protocol) ma non è istanziabile poichè vi sono due standard IP:

  • IPv4: la prima versione, che usa 32 bits per memorizzare un indirizzo IP: indirizzi IPv4 vengono rappresentati dalla classe derivata Inet4Address
  • IPv6: la seconda versione, che usa 128 bits per memorizzare un indirizzo IP: indirizzi IPv6 vengono rappresentati dalla classe derivata Inet6Address

Implementazione in Mastermind

I files sorgente

I files sorgente che implementano la ricerca dei giocatori in rete sono contenuti nel package mastermind.net.udp:

nome file descrizione
ListInterfaces.java applicazione CLI di test delle interfacce
MCInterface.java una interfaccia di rete attiva ed abilitata al multicast
MulticastWorker.java la classe base astratta del worker-thread di invio/ricezione
MulticastReceiver.java la classe specializzata nella ricezione dei pacchetti UDP
MulticastSender.java la classe specializzata nel invio dei pacchetti UDP
UDPListener.java il listener degli eventi UDP
PropertyPanel09.java il nuovo pannello delle proprietà della app
ConnectionPanel09.java il nuovo pannello delle connessione remota
EmptyListException.java eccezione per lista interfacce vuota
MalformedItemException.java eccezione per formato non valido

Le proprietà della app

La ricerca dei giocatori in rete da parte di Mastermind viene affidata alle classi elencate più sopra le quali possono essere configurate attraverso le seguenti proprietà della applicazione:

  • mcInterval: intervallo di tempo, in millisecondi, tra un invio e l'altro da parte del sender dei pacchetti UDP; il default è 1000 ms; se questa proprietà ha un valore negativo l'invio e la ricezione dei pacchetti UDP sono disabilitati
  • mcTimeour: intervallo di tempo, in millisecondi per il receiver scaduto il quale la primitiva di I/O receive deve rientrare anche se nessun pacchetto UDP è stato ricevuto; il default è 500 ms
  • mcInterface: può contenere un elenco di interfacce di rete specifiche, separate da whitespaces su cui inviare / da cui ricevere i pacchetti UDP multicast; se il valore di questa proprietà è null i pacchetti UDP saranno inviati 7 ricevuti da tutte le interfacce di rete attive ed abilitate al multicast; il valore di default è null
  • mcAddress: rappresenta l'indirizzo IP multicast del gruppo; il valore di default è "230.18.8.0"
  • mcPort: la porta UDP del datagram-socket di invio / ricezione dei pacchetti UDP per il multicasting in Mastermind; il valore di default è "3963"

Le interfacce di rete

Il file sorgente ListInterfaces.java è una piccola applicazione CLI che potete usare per sperimentare un pò le interfacce di rete presenti sul vostro computer. Potrebbero essercene molte di più di quelle fisiche: nel mio computer ci sono tre interfacce di rete fisiche: il loopback, la scheda Ethernet e la scheda Wireless.
Ma l'elenco ottenuto con la applicazione è molto più lungo:

>java -ea mastermind.net.udp.ListInterfaces -n
TEST INTERFACES
Interface: ethernet_0
Interface: ethernet_1
Interface: ethernet_2
Interface: ethernet_3

... omissis ...

Interface: ppp_32768
Interface: loopback_0
Interface: wireless_0
Interface: wireless_1

... omissis ...

Quindi Mastermind invierà pacchetti UDP su tutte quelle interfacce anche se non sono davvero esistenti? Certo che no. Possiamo ottenere informazioni dettagliate sulla interfaccia di rete specificandone il nome come argomento sulla command-line. Per esempio.

>java -ea mastermind.net.udp.ListInterfaces ethernet_2

Information on interface: ethernet_2
Display name: Intel(R) Ethernet Connection (2) I219-LM-WFP 802.3 MAC Layer LightWeight Filter-0000
isLoopback? false
isUp? false
isVirtual? false
supportsMulticast? true
Interface Addresses:

Notiamo subito che la interfaccia non è attiva (vedi IsUp()=false) e che ad essa non è associato alcun indirizzo IP. Ovviamente, Mastermind invierà i pacchetti UDP solo sulle interfaccie attive e, fra queste, solo quelle che supportano il multicasting. Per una interfaccia di rete attiva otteniamo molte più informazioni:

>java -ea mastermind.net.udp.ListInterfaces wireless_32768

Information on interface: wireless_32768
Display name: Realtek RTL8188ETV Wireless LAN 802.11n USB 2.0 Network Adapter
isLoopback? false
isUp? true
isVirtual? false
supportsMulticast? true
Interface Addresses:
  Address: /fe80:0:0:0:d879:5dc6:dbeb:dadb%wireless_32768
  Raw address length: 16
  Broadcast addr: N/A
  Network prefix length: 64
  Address: /10.117.242.114
  Raw address length: 4
  Broadcast addr: /10.117.242.255
  Network prefix length: 24

Notiamo anche che alla interfaccia wireless_32768 sono associati due indirizzi IP

  • un indirizzo IPV4: 10.117.242.114
  • un indirizzo IPV6: fe80:0:0:0:d879:5dc6:dbeb:dadb

La classe MCInterface

Questa classe viene implementata dal sorgente MCInterface.java e rappresenta una singola interfaccia di rete attiva ed abilitata al multicasting. Il worker-thread che invia / riceve i pacchetti UDP mantiene una lista di oggetti di classe MCInterface poichè sia il sender che il receiver iterano sulla lista per inviare ad ogni interfaccia di rete il pacchetto UDP. Oggetti di questa classe vengono istanziati passando un solo argomento al costruttore: l'interfaccia di rete, un oggetto di classe NetworkInterface.
I metodi più importanti di questa classe sono:

  • isUpEnabled; questo metodo ritorna TRUE se la interfaccia di rete specifica
    è attiva (up) ed abilitata al multicasting (enabled); come già detto, solo le interfacce che hanno queste due caratteristiche saranno inserite nella lista del worker-thread
  • createSocket: il metodo crea il DatagramSocket sul quale saranno inviati / ricevuti i pacchetti UDP multicast
  • close: chiude il socket, quando le operazioni saranno concluse
  • receive: riceve un DatagramPacket dal socket creato con il metodo precedente
  • send: invia un DatagramPacket sul socket creato con il metodo precedente
  • getErrorCount: ritorna il numero di errori di I/O occorsi sul socket, vedi Errori di invio/ricezione per ulteriori informazioni

MulticastWorker: la classe base

La classe MulticastWorker è la classe base astratta per il sender ed il receiver; essa si occupa di creare e mantenere la lista delle interfacce di rete attive, rappresentate dalla classe MCInterface. Da questa classe derivano le due classi specializzate:

  • MulticastSender che si occupa dell'invio dei pacchetti UDP
  • MulticastReceiver che si occupa della ricezione dei pacchetti UDP

MulticastWorker: I costruttori

La classe MulticastWorker possiede due costruttori pubblici. Il primo di essi accetta tre argomenti di classe:

  • UDPListener: il listener degli eventi UDP, vedi Il listener degli eventi UDP
  • MMProperties: le proprietà della applicazione da cui si estraggono i parametri per l'invio / ricezione dei pacchetti; vedi Le proprietà della app
  • String: un elenco di nomi di interfacce di rete che costituiscono la lista delle interfacce usate nel worker-thread; questa stringa contiene normalmente il valore "all" che significa tutte le interfacce di rete attive ed abilitate al multicasting

Il secondo costruttore pubblico di MulticastWorker accetta gli stessi primi due argomenti ma il terzo non è una stringa di nomi di interfacce ma una lista di oggetti MCInterface che rappresenta comunque la lista delle interfacce di rete da usare. Questo costruttore non viene usato nella applicazione Mastermind ma nei test che vengono descritti nella documentazione javadoc del package specifico, vedi il package mastermind.test.udp.

La lista di interfacce di rete da usare nel worker-thread viene creata nel costruttore: questo perchè le interfacce di rete presenti nel computer non cambia nel tempo quindi può essere creata una sola volta nel corso della applicazione.

MulticastWorker: I metodi

La classe MulticastWorker è una classe base astratta. I due metodi astratti sono:

  • start che fà partire il worker-thread di invio o ricezione dei pacchetti UDP; il metodo è specializzato per le due derivate: il sender ed il receiver
  • stop che ferma il worker-thread ma non chiude i sockets: le operazioni possono essere riprese richiamando nuovamente start

I seguenti metodi sono responsabili di gestire la lista delle interfacce di rete:

  • createInterfacesFromIfaces: crea la lista delle interfacce di rete ed apre i socket datagram per ognuna di esse; il metodo verifica anche che le interfacce passate come argomento siano attive ed abilitate al multicast
  • createInterfacesFromStrings crea la lista delle interfacce di rete ed apre i socket datagram per ognuna di esse; il metodo verifica anche che le interfacce passate come argomento siano attive ed abilitate al multicast
  • getInterfaceCount: ritorna il numero di interfacce di rete presenti nella lista
  • getInterfaces: ritorna la lista di interfacce di rete attive ed abilitate
  • close: ferma il worker-thread e chiude i sockets datagram

Errori di invio/ricezione

Come tutte le operazioni di Input/Output, anche l'invio dei pacchetti UDP può non andare a buon fine a causa di errori di trasmissione. I prototipi dei metodi dedicati a queste operazioni parlano chiaro:

public void send(DatagramPacket p) throws IOException
public void receive(DatagramPacket p) throws IOException

A differenza del protocollo TCP, che è affidabile, UDP è inaffidabile e gli errori di trasmissione sono molto più frequenti. Ma un errore su una inetrfaccia di rete non significa nulla ed è per questo che il sender invia molte copie del messaggio ad intervalli regolari; anche se può capitare un errore nell'invio di uno di essi, le altre copie raggiungeranno la destinazione.
Ovviamente, non è pratico continuare ad inviare pacchetti su una interfaccia che è sempre in errore: la strategia adottata dal sender e dal receiver di Mastermind è quella di mantenere un contatore degli errori di I/O nella classe MCInterface: dopo cinque errori consecutivi, la interfaccia sarà rimossa dalla lista delle interfacce di rete da usare nel worker-thread.
Se tutte le interfacce di rete della lista vengono rimosse, la lista rimarrà vuota: in questo scenario, non ha più senso tenere in vita il worker-thread il quale ritorna dal metodo eseguito in background e termina.

Pubblicare i risultati

Le operazioni di invio e ricezione dei pacchetti UDP vengono eseguite nel metodo doInBackground del worker-thread in modo da non bloccare la GUI. Abbiamo già avuto a che fare con i threads, ricordate? In due occasioni:

Benchè trattasi di worker-threads in entrambi i casi, la organizzazione del metodo in background è profondamente diversa tra i due: nel primo caso il thread viene eseguito per completare una singola operazione, l'invio della guess, e poi termina. Nel secondo caso, invece, il thread esegue continuamente la operazione ad esso assegnata e non termina mai, salvo gli eventi eccezionali come per esempio la perdita della connessione: inutile tenere vivo un thread che trasmette messaggi su un canale di comunicazione quando il canale si è interrotto.
Abbiamo quindi imparato a pubblicare i risultati intermedi nel caso delle code dei messaggi: ogni volta che un messaggio viene inviato e/o ricevuto con successo, si richiama il metodo publish fornendo come dato da pubblicare un oggetto Message che rappresenta il messaggio inviato o ricevuto.

Ma questo caso, quello dei pacchetti UDP, è diverso ancora: come detto in precedenza, una eccezione di I/O su una interfaccia di rete non pregiudica affatto il completamento delle operazioni: a parte il fatto che dobbiamo tentare nuovamente, almeno cinque volte, la operazione stessa, ci sono altre interfacce di rete nella lista su cui inviare o da cui ricevere i messaggi.
Pertanto, non possiamo sollevare la eccezione nel metodo doInBackground perchè questo evento fermerebbe il thread e sarebbe poi richiamato il suo metodo done: una volta richiamato done, il thread non può più essere ri-eseguito, nemmeno richiamandone il metodo execute. Da notare che questo è un limite della classe SwingWorker, non dei threads in generale implementati dalla classe java.lang.Thread.

Anche se non possiamo sollevare la eccezione nel meodo in background, dobbiamo pure segnalare l'evento al listener degli eventi in modo da darne notizia allo user o comunque alla applicazione e non solo: ci sono altri due eventi degni di nota che dobbiamo segnalare (ma senza sollevare eccezioni): il fatto che una interfaccia di rete è stata rimossa dalla lista per troppi errori di I/O ed il fatto che la lista delle interfacce di rete è vuota perchè tutte sono state rimosse per troppi errori.
Non sembra particolarmene difficile da scrivere, vero?

@Override
protected Void doInBackground() throws InterruptedException
{
while( !isCancelled()) {
if ( interfaces.size() <= 0 ) {
// lista delle interfacce vuota, ritorno e termino
return null;
}
Iterator<MCInterface> iter = interfaces.iterator();
while ( iter.hasNext()) {
MCInterface item = iter.next();
// controlla se questa interfaccia ha accumulato troppi errori,
// nel caso positivo, rimuove la interfaccia dalla lista
if ( item.getErrorCount() >= mcErrorLimit ) {
iter.remove();
// notifica il listener degli eventi che la interfaccia
// in elaborazione è stata rimossa dalla lista delle interfacce
listener.removedForErrors( "sender", item );
continue;
}
... omissis ...
}

Anche se dal punto di vista logico il codice sembrerebbe corretto, esso non funziona. La causa del mancato funzionamento non è nella logica di programmazione ma nelle costrizioni della libreria Java Swing che non permette di accedere ai componenti GUI da un thread diverso dal EDT.
Pertanto, l'aver richiamato il metodo removedForErrors del listener non ci aiuta molto se non possiamo dare alcun feedback visivo allo user sul fatto che una interfaccia è stata rimossa dalla lista delle interfacce. Come risolviamo la faccenda? Ma è ovvio, pubblicando un risultato intermedio! Ricordate? I metodi doInBackground e publish vengono eseguiti nel thread secondario mentre i metodi done e process vengono eseguiti nel EDT.

Abbiamo un altro problema. I dati da pubblicare come risultati intermedi cominciano ad essere troppi e, manco a dirlo, la classe SwingWorker ne accetta uno solo, il tipo "V":

public abstract class SwingWorker<T,V> extends Object
implements RunnableFuture<T>

ma a noi ne servono molti di più. La soluzione a questa empasse è quella di definire un nuovo tipo di dato che raggruppa queste inofromazioni e che sarà il tipo di dato da pubblicare come risultato intermedio. Potrebbe essere una classe ma, considerato che sarà immutabile e che i suoi membri dati saranno final, un record è il costrutto ideale:

// File: MulticastWorker.java
public class MulticastWorker
{
protected static record UDPResult( MCInterface iface,
int progr,
DatagramPacket packet,
String payload,
CommException except )
... omissis ...

Gli argomenti al costruttore hanno significati specifici in linea con le informazioni che devono fornire:

  • iface la interfaccia di rete che è stata elaborata nel metodo in background
  • progr il progressivo del messaggio inviato / ricevuto: se questo valore è negativo significa che l'interfaccia iface è stata rimossa dalla lista delle interfacce di rete; in questo caso, tutti gli altri argomenti saranno null
  • payload il messaggio inviato dal sender; questo argomento sarà null per il receiver o in caso di eccezioni
  • packet il pacchetto datagram ricevuto dal receiver; questo argomento sarà null per il sender o in caso di eccezioni
  • except la eccezione sollevata dalla interfaccia di rete iface nelle operazioni di I/O, se la operazione si è conclusa con successo, questo argomento sarà null

MulticastSender: invio dei pacchetti UDP

Questa classe, derivata da MulticastWorker, sovrascrive i due metodi astratti della classe base: start e stop. Il primo fà partire l'invio dei pacchetti UDP mentre il secondo, come facilmente intuibile, lo ferma. Il metodo start fà partire un thread secondario, una classe anonima derivata da SwingWorker: il thread secondario è infinito nel senso che il suo metodo doInBavkground non termina mai salvo la cancellazione del thread.

Lavorando in background, il MulticastSender itera su tutte le interfacce di rete attive ed abilitate al multicast richiamndone il metodo send al quale viene passato il messaggio da inserire nel pacchetto UDP:

// File: MulticastSender.java
public void start( String payload )
{
... omissis ...
worker = new SwingWorker<Void, UDPResult>()
{
@Override
protected Void doInBackground() throws InterruptedException
{
... omissis ...
Iterator<MCInterface> iter = interfaces.iterator();
while ( iter.hasNext()) {
... omissis ...
// pubblica la rimozione della interfaccia in elaborazione
// come risultato intermedio
publish( new UDPResult( item, -1, null, null, null ));
// pubblica l'invio del payload eseguito con successo
// come risultato intermedio
publish( new UDPResult( item, counter, null, payload, null ));
counter++;
}
catch( CommException ex )
{
// pubblica il sollevamento di una eccezione di I/O
// come risultato intermedio
publish( new UDPResult( item, counter, null, payload, ex ));
}
... omissis ...

L'invio avviene ad intervalli regolari il cui lasso di tempo è specificato dal parametro mcInterval.

MulticastReceiver: ricezione dei pacchetti UDP

Il multicast-receiver è molto simile al sender: sovrascrive i due metodi astratti start e stop il primo dei quali fà partire la ricezione dei pacchetti UDP mentre il secondo la ferma. Anche il metodo doInBackground è praticamente lo stesso: in un ciclo infinito (salvo cancellazione del thread) il metodo itera su tutte le interfacce attive ed abilitate al multicasting e per ognuna di esse ne richiama il metodo receive che ritorna:

  • un oggetto di classe DatagramPacket se un pacchetto UDP è stato letto dal socket
  • null se il timeout di ricezione è scaduto e nessun pacchetto UDP è stato letto

Anche nel receiver, i risultati delle operazioni vengono pubblicati come risultati intermedi di qualsiasi natura essi siano.

Il listener degli eventi UDP

Quali classi di Mastermind sono interessate agli eventi che scaturiscono dalle classi di questo package dedicato al protocollo UDP? Dobbiamo distinguere due casi: il lato server della applicazione ed il lato client

Il lato server di Mastermind

Dal lato del server di gioco, i pacchetti UDP devono essere inviati quando il server è pronto ad accettare connessioni e cioè quando esso è in ascolto sulla porta dedicata al servizio. Quindi la classe inetressata agli eventi è la ConnectionPanel che visualizza il pannello dello stato di avanzamento della connessione che si trova nella fase in cui il server è in attesa di richieste di connessione. Per quanto riguarda gli eventi ai quali il server è interessato possiamo escludere l'invio dei singoli pacchetti UDP: dovrebbe invece essere dato un feedback all'utente se il multicasting è stato attivato oppure no:

Connecting ... port: 18862 (server mode)
Multicast sender started

La classe ConnectionPanel deve essere modificata per implementare la nuova feature (=funzionalità). In linea con la strategia di versioning dei sorgenti, deriviamo una nuova classe, che chiamiamo Connectionpanel09 ed implementiamo solo i metodi che dovrebbero essere modificati: tutti gli altri metodi sarano ereditati dalla classe base.
La nuova classe ConnectionPanel09 farà parte del package mastermind.net.udp in linea con la politica di packaging che stabilisce che una classe deve essere inserita nel package dove vi trovano le dipendenze da altre classi e non nel package che ne determina l'uso.

Il lato client di Mastermind

Dal lato client la classe interessata agli eventi UDP è la PropertyPanel ossia il pannello delle proprietà in cui l'user deve inserire l'indirizzo IP del server di gioco. La ricezione di un pacchetto UDP contenente le informazioni sul server di gioco deve essere notificata al pannello delle proprietà in modo che esso aggiorni la combo-box della lista dei server disponibili e dalla quale l'user può selezionare un server a cui connettersi.
Come per il pannello della connessione scriveremo una nuova classe, che chiameremo PropertyPanel09, derivata da PropertyPanel ed implementeremo solo i metodi che dovrebbero essere modificati e quelli nuovi. Tra i metodi nuovi troviamo, ovviamente, quelli che implementano la interfaccia UDPListener che è il listener degli eventi UDP e che elaborano i pacchetti UDP contenenti la stringa di descrizione dei server di gioco Mastermind.

I metodi della interfaccia

Poichè ci sono almeno due classi interessate agli eventi UDP, la strategia migliore è quella di definire una interfaccia: qualsiasi classe che la implementi può essere passata come argomento listener al costruttore di MulticastWorker. Questi sono i metodi definiti nella inetrfaccia UDPListener:

  • udpErrorReceive: errore nella ricezione del messaggi UDP. Gli argomenti al metodo sono la inetrfaccia di rete che ha causato l'errore e l'errore stesso
  • udpErrorSend: errore nell'invio di messaggi UDP. Gli argomenti al metodo sono la inetrfaccia di rete che ha causato l'errore e l'errore stesso
  • udpMessageReceived: messaggio UDP ricevuto con successo. Gli argomenti al metodo sono: la interfaccia di rete che ha ricevuto il pacchetto UDP, l'indirizzo del mittente (il server di gioco) ed il messaggio stesso, il cosidetto payload nei pacchetti UDP
  • udpMessageSent: messaggio UDP inviato con successo. Gli argomenti al metodo sono: il progressivo del messaggio e la interfaccia di rete che lo ha inviato
  • udpReceiverStarted: Worker-thread di ricezione UDP eseguito; non ci sono argomenti
  • udpReceiverStopped: Worker-thread di ricezione UDP cancellato; non ci sono argometi
  • udpSenderStarted: Worker-thread di invio UDP eseguito; non ci sono argomenti
  • udpSenderStopped: Worker-thread di invio UDP cancellato; non ci sono argomenti

Curiosità

In questa sezione descrivo alcune particolarità del protocollo IP e della libreria java Swing. La sezione non insegna nulla sul linguaggio Java quindi il lettore può anche saltare alla prossima sezione senza pregiudicare la comprensione di questo tutorial. Tuttavia, se egli ha un pò di tempo da sprecare, consigli di continuare a leggere.

Il metodo udpMessageReceived

Chi ha osservato da vicino la interfaccia UDPListener sarà rimasto un pò sorpreso nella definizione del metodo:

void udpMessageReceived(String iface, String ipaddr, String payload)

nel quale viene passato come secondo argomento l'indirizzo IP del server di gioco che ha inviato il pacchetto UDP. in effetti, l'indirizzo IP del server è contenuto nel payload che ha il formato:

    nickname@hostname/ip_address:port

dove:

  • nickname è il nome del giocatore remoto, quello che funge da server
  • hostname è il nome della macchina server; a questo proposito và detto che questa informazione è disponibile solo se la libreria Java ha risolto un nome macchina in un indirizzo IP, nessun reverse lookup (=visura rovesciata) viene eseguito da Java per risolvere un indirizzo IP in un nome host
  • ip_address questo è l'indirizzo IP che il sender ottiene dalla interfaccia di rete usata per inviare il pacchetto UDP; in questa versione di Mastermind vengono estratti solo gli indirizzi IPV4.
  • port è la porta TCP/IP su cui il server è in ascolto per le richieste di connessione

Quindi la domanda che vi starete ponendo sarà: perchè l'indirizzo IP del server viene passato come argomento al metodo considerato che è contenuto nel payload? Ebbene, l'indirizzo IP contenuto nel payload viene inviato dalla macchina remota che però è sconosciuta, in gergo si dice untrusted (=non ci si può fidare).
Chi ci assicura che quell'indirizzo è quello reale? Nessuno. Invece, l'indirizzo IP passato come argomento al metodo è stato ottenuto dal pacchetto UDP ricevuto e quello non si può falsificare se non usando strumenti piuttosto sofisticati.
E' un pò come se in una chiamata con uno sconosciuto gli chiediamo il suo numero di telefono ed egli ce lo comunica a voce durante la chiamata: sarà quello giusto? Ma se invece lo otteniamo dal display del nostro telefono come "numero chiamante" allora possiamo fidarci un pò di più.

I limiti di Java Swing

Nella sezione Pubblicare i risultati avete imparato che non è possibile richiamare i metodi che interagiscono con i componenti GUI Swing da un thread diverso dal EDT. In altre parole, all'interno del metodo doInBackground non posso richiamare, direttamente o indirettamente, il metodo MasterMind.setStatusbarMessage (per fare un esempio) poichè questo metodo aggiorna il componente Swing JLabel che fà parte della statusbar di Mastermind.
So che per voi è una delusione e che siete quasi tentati di passare ad un altro linguaggio/ambiente: se Java Swing è così limitato, perchè dovrei imparare ad usarlo? Ebbene, quello descritto non è un limite di Java Swing ma del sistema operativo stesso: sia Windows™ che gli ambienti grafici basati su Linux hanno questo limite quindi passare ad un altro linguaggio/ambiente non risolve il problema.
Non sò se Mac abbia lo stesso limite, non ho mai programmato nè mai posseduto un computer della mela; se qualcuno vorrà modificare questa mia opera ed è a conoscenza di questa informazione è il benvenuto.

Perchè usare UDP?

A dispetto della sua inaffidabilità e propensione agli errori di trasmissione, il protocollo UDP è usato moltissimo nelle applicazioni reali specialmente in quelle real-time. La controparte di UDP è il TCP (Trasmission Control protocol), un protocollo affidabile che garantisce il flusso di dati: ciò che viene trasmesso dal mittente raggiunge il destinatario nello stesso ordine in cui è stato trasmesso, salvo errori di connessione, ovviamente.
Ma entrambi si basano su IP, un protocllo inaffidabile, quindi come viene realizzata la affidabilità del TCP se si basa su IP? Ebbene, le regole per raggiungere la affidabilità del TCP sono, sostanzialmente, le seguenti:

  • ogni messaggio viene diviso in pacchetti IP che vengono inviati sulla rete senza alcuna garanzia di raggiungere il destinatario
  • tutti i pacchetti vengono numerati e contati: per esempio uno dei pacchetti potrebbe essere il numero 12 di 30
  • il destinatario riceve un pacchetto IP e lo conserva fino a che non saranno arrivati tutti i 30 pacchetti
  • il destinatario invia un pacchetto ACK (Acknoledge = ricevuto) al mittente informandolo che il pacchetto 12 di 30 è stato ricevuto
  • il mittente prende nota di tutti i pacchetti ACK ricevuti e quando avrà ricevuti tutti i 30 ACK considera il messaggio inviato correttamente
  • poichè anche i pacchetti ACK possono perdersi per strada il mittente, passato un certo periodo di tempo, provvede a reinviare tutti i pacchetti IP per i quali non ha ricevuto una ACK
  • quando il destinatario ha ricevuto tutti i 30 pacchetti, ricostruisce il messaggio originale mettendo in sequenza i pacchetti ricevuti

Come potete immaginare, queste operazioni generano una certa latenza che dipende in gran parte dalla qualità della rete ma non solo, molto dipende anche dal caso. Prendiamo ad esempio una telecamera che invia il video di ciò che riprende. Ogni frame del video può essere considerato un messaggio da inviare al computer di sorveglianza; il messaggio è composto da parecchi pacchetti IP poichè un frame a colori di 10 o 20 megapixels pesa parecchio, nonostante la compressione.

Ora supponiamo di usare TCP per avere una connessione affidabile: la telecamera invia un frame composto da qualche centinaio di pacchetti ma uno di essi viene perso per strada costringendo il mittente a ritrasmetterlo. Poichè il TCP aspetta fino all'ultimo pacchetto prima di inoltrare il messaggio (il frame) al computer di sorveglianza otteniamo una certa latenza che potrebbe essere critica.
Usando il protocollo UDP, invece, il computer di sorveglianza può elaborare i pacchetti in tempo reale, nel momento stesso in cui li riceve. Certo, nel frame visualizzato sul computer di sorveglianza mancherà qualche pixel, dovuto a qualche pacchetto perso per strada, ma è molto meglio perdere qualche pixel piuttosto che dover attendere l'intero frame, specialmente se il fattore tempo è un aspetto critico della applicazione.

Ulteriore documentazione

La documentazione completa del package descritto in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc del progetto: The MasterMind Project Version 0.9

Argomento precedente - Argomento successivo - Indice Generale