Il
canale
Il
meccanismo più semplice è quello di comunicare
con l'applicazione attraverso il canale dei socket.
Naturalmente questo implica che l'applicazione "guidata",
o meglio un suo modulo, stia ad ascoltare una porta
e, ricevuto un comando, lo esegua.
Ecco
allora già ben definito il nostro primo punto
di arrivo: costruire un piccolo modulo che sia in grado
di ascoltare su di un canale e trasmettere gli eventuali
comandi ricevuti ad un'applicazione.
Naturalmente,
l'ascoltare un canale non deve interferire con la normale
interazione che l'applicazione ha con l'utente finale.
Questo ci costringe a pensare il modulo di ascolto come
un thread che assolva questo compito specifico.
Per
quanto riguarda la parte di comunicazione tra il modulo
socket e l'applicazione gui, la cosa più naturale
è quella di estendere il meccanismo dei listener.
In pratica così come avviene per i normali componenti
grafici, sui quali è possibile aggiungere degli
ascoltatori per un componente specifico,così
una qualsiasi classe può essere in grado di ricevere
comandi via socket implementando i metodi della corrispettiva
interfaccia.
E
l'applicazione cosa fa?
Naturalmente
l'applicazione socket-comandata è libera di fare
quello che più vi piace, ma per il nostro esempio
qualcosa si doveva pur scegliere e quindi abbiamo deciso
di costruire un'applicazione che fosse in grado di eseguire
comandi batch associati ad un nome logico. In pratica
da una lista sarà possibile selezionare un elemento
al quale corrisponde l'esecuzione di un processo. Con
la pressione di un tasto la procedura verrà eseguita.
L'unione
fa la forza!
Unendo
queste due caratteristiche riusciremo ad ottenere qualcosa
di veramente carino. Non va dimenticato, infatti, che
l'aver scelto il socket come canale di comunicazione
ci permette di pilotare l'applicazione target da un
punto della rete qualsiasi. In pratica ciò significa,
che se riusciremo nel nostro intento, avremo costruito
un'applicazione in grado di eseguire processi batch
su un server a partire da un qualsivoglia client; costruiremo
un "remote Commander"!
Il
fascino della TeleInterazione
Alzi
la mano chi non è rimasto affascinato da strumenti
quali il "Terminal Server", oppure non è
rimasto a bocca aperta davanti allo schermo della sua
macchina, mentre un assistente in remoto, configurava
qualche parte del sistema. E' il fascino della tele
interazione che può raggiungere i livelli più
alti quando i tecnici della Nasa riescono a guidare
il robottino su marte direttamente dalla terra.
Attuatori
ed informazione veloce
Anche
se internet ha permesso, rispetto alla tele presenza
ed alla tele interazione, di sviluppare applicazioni
stupefacenti ed ora di uso molto comune come ad esempio
l'istant messagging o i giochi in rete, nell'era pre-internettautica
lo strumento più tele interattivo in assoluto
è stato senza dubbio il telefono, mentre un'altra
applicazione diffusissima che costituisce in pratica,
una sorta di tele trasporto cartaceo è il fax.
Pensando
un po' in astratto a queste applicazioni, possiamo identificare
due meta elementi che le caratterizzano:
Iniziamo
dal secondo elemento, il canale, che è la via
fisica attraverso la quale una sorta di informazione
corre molto velocemente da un punto ad un altro e viceversa.
Agli estremi di questa via si trovano due attuatori
che sono, generalmente, una qualsiasi sorta di apparecchiaitura
tecnologica in grado di interpretare l'informazione
che corre nel canale e trasformarla in informazioni
che una persona può capire e con la quale può
interagire. L'attuatore deve essere in grado di compiere
anche il processo inverso, cioè riimodulare il
segnale che l'utente genera per immetterlo nuovamente
nel canale e spedirlo verso l'altro punto del sistema.
Attuatori
sistemati in posti strani...
La
cosa più interessante di questi attuatori è
che possono essere sistemati nei posti più strani,
ho sentito di un paio di pianoforti, uno sistemato a
New York, l'altro in Giappone. Un pianista suonava in
America ed il pubblico giapponese ascoltava la musica
come se il pianista stesse utilizzando la tastiera del
pianoforte giapponese. Attuatori un po' più alla
portata di tutti sono gli ultimi modelli di WebCam con
scheda telefonica incorporata che possono spedire suoni
ed immagini ad un telefonino. Ancora più interessanti
sono qquegli attuatori che non sono posti in mondi reali,
ma in mondi virtuali.
Ed
ora è come se la storia volesse farci uno scherzetto!
Se tutto è partito dal telefono al telefono sembra
essere ritornato, infatti una delle applicazioni più
diffuse e promettenti in quanto a velocità di
espanzione ed utilizzo è proprio il VOIP (Voice
Over IP).
Il
disegno
Dopo
queste brevi riflessioni torniamo a quello che era il
nostro problema tecnico di partenza: costruire una semplice
architettura in grado di inviare comandi da un PC all'altro
all'interno di una rete.
Per
i più distratti occorre sottolineare che quella
che cercheremo di costruire è un'applicazione
Client/Server. Questo significa che nel disegno dell'applicazione
dovremmo distinguere le due parti: il Client ed il Server
appunto...
Il
server
Iniziamo
dalla parte più complessa, il lato server. L'applicazione
lato server è costituita da due modoli di facile
identificazione: il modulo esecutore che ha il compito
di lanciare una procedura batch che l'utente è
in grado di scegliere attraverso un nome logico; ed
il modulo Socket il cui compito è ascoltare un
determinato canale in attesa che arrivino dei comandi
da eseguire. Naturalmente, una volta arrivato un comando
valido, il modulo stesso deve essere in grado di trasmetterlo
all'applicazione che lo eseguirà come si trattasse
di un normale comando fornito dall'utente attraverso
l'uso del mouse o della tastiera.
Il
client
Molto
più semplice è il compito della parte
client: si tratta di un programmino che deve essere
in grado, aprendo una comunicazione socket, di inviare
un comando all'applicazione bersaglio.
Eseguire processi Batch in java
Per
eseguire un processo batch in java, la prima cosa da
effettuare è quella di recuperare l'ambiente
di runtime con il metodo getRuntime() della classe Runtime.
Il metodo exec() è, infine, quello che esegue
il comando batch. Questo metodo prevede tre parametri:
-
il comando da lanciare;
- un
array di stringhe che nella forma chiave=valore, rappresentano
variabili d'ambiente utilizzate dal processo che si
intende lanciare;
- la
directory di lavoro che è la directory nella
quale il processo lanciato lavorerà.
Per
semplificare la gestione di questi parametri, abbiamo
costruito un piccolo oggetto che li contiene tutti.
package
org.illeva.remcom;
import
java.util.*;
import java.io.*;
/**
* E' la classe che esegue i processi batch.
*/
public class OsRunner
{
// Data members
/** Nome logico del processo da eseguire */
private String dName;
/**
* Nome fisico del processo da eseguire. Si tratta di
un vettore di stringhe perchè il nome fisico
del processo
* può essere la composizione di più stringhe
*/
private Vector dCmd=new Vector();
/**
* E' la collezione di coppie chiave/valore che costituiscono
* le variabili d'ambiente con le quali
* il processo in eseguzione può interagire.
*/
private HashMap dEnv=new HashMap();
/** E' la directory di lavoro del processo. */
private File dWorkDir;
[...]
} // end class
Di
seguito, sempre dalla medesima classe, riportiamo il
metodo che esegue il processo batch:
public
void run()
{
String[] vCmd=null;
String[] vEnv=null;
/* Trasforma il vettore di stringhe che rappresenta
il comando da lanciare
in un array di stringhe. */
if(dCmd.size()>0)
{
vCmd=new String[dCmd.size()];
for(int i=0; i0)
{
vEnv=new String[dEnv.size()];
Iterator i=dEnv.keySet().iterator();
int j=0;
while(i.hasNext())
{
String vTemp=(String)(i.next());
vEnv[j++]=vTemp+"="+((String)(dEnv.get(vTemp)));
} // end while
} // end if
try
{
Process vChild=Runtime.getRuntime().exec(vCmd,vEnv,dWorkDir);
}
catch(IOException anEx)
{
anEx.printStackTrace(System.err);
} // end catch
} // end method
La
GUI del server
L'interfaccia
dell'applicazione server è molto semplice: una
lista di comandi batch che possono essere eseguiti,
e due bottoni; uno per l'esecuzione del comando scelto
dalla lista e l'altro bottone per abilitare o disabilitare
il modulo di ascolto remoto che permette di eseguire
gli stessi comandi ma da un altro PC.
Per
quanto riguarda il funzionamento di questa parte di
codice non vi è nulla di sostanziale da sottolineare
o su cui valga la pena porre la nostra attenzione. Rimandiamo
i nostri lettori alla lettura diretta del codice sorgenteper
chi volesse approfondire questa parte il cui funzionamento
è quello classico delle "vecchie" interfacce
grafiche implementate con AWT.
Il
file di configurazione
Spendiamo
invece due parole sulla struttura del file di configurazione,
per tutte quelle persone che volessero utilizzare realmente
l'applicazione. Nella directory "config" è
presente un file nominato "process.txt", dove
vengono destritti i processi batch che è possibile
lanciare:
#
# File process definition
#
#
# e' il nome logico del processo. Quello che compare
nella lista dei processi che
# l'utente può scegliere di eseguire.
#
ProcName=Test
#
# e' la stringa di comando che costituisce l'esecuzione
vera e propria del
# processo batch.
#
CmdList=cmd.exe,/C,start,funny.bat
#
# E' la directory di lavoro nella quale il batch viene
eseguito
#
WorkDir=D:/Batch
ProcName=Prova
CmdList=cmd.exe,/C,start,prova.bat
#
# E' la lista di variabili d'ambiente che il processo
batch può utilizzare durante la sua esecuzione.
#
EnvList=JAVA_HOME=D:/src,CLASSPATH=D:/classes
WorkDir=D:/Batch
In
un secondo file denominato "MainSocket.properties"
possiamo impostare la porta sulla quale il socket server
ascolta le richieste che possono pervenire da vari client
ed il tempo in millisecondi nel quale il thread resta
in attesa dei comandi prima di dare un po' di ascolto
anche all'interfaccia GUI che lo ha generato perchè
da questa potrebbero arrivare ordini del tipo: "Mi
sto chiudendo, chiudi tutto anche tu..." oppure
"Per il momento smetti di ascoltare la porta socket..."
#
MainSocket configuration files
# Porta sulla quale ascolta il socket
port=1304
# E' il numero massimo di richieste che possono essere
accodate prima di essere servite.
backlog=2
# Tempo di attesa, in millisecondi, per una richiesta
sul canale socket
sleep=3000
Il
parametro backlog è quello che permette di difinire
quante richieste, al massimo, possono essere accodate
in attesa di essere servite.
Il
modulo d'ascolto
Cuore
del modulo d'ascolto che abbiamo racchiuso nel package
cmdsock per rendere più agevole il lavoro a chi
volesse estrapolarlo da questa applicazione e usarlo
in altri contesti, e la classe MainSocket le cui caratteristiche
principali sono due: è un thread e ha al suo
interno un serverSocket che ha il compito di smistare
le richieste in arrivo dai vari client:
package
org.illeva.remcom.cmdsock;
import
java.io.*;
import java.util.*;
import java.net.*;
/**
* E' il thread che rimane in ascolto su di una porta
per ricevere i comandi che verranno trasmessi
* all'applicazione target.
*/
public class MainSocket extends Thread
{
// Data members
/** E' il flag per gestire l'esistenza del thread. */
private boolean dExists=true;
/** E' il flag che attiva il thread oppure ne sospende
le attività. */
private boolean dActive=true;
/** E' il socket che ascolta una porta in attesa di
comandi. */
private ServerSocket dSock=null;
/** File di configurazione per la configurazione dell'applicazione.
*/
private Properties dProp=new Properties();
/** E' il tempo di sleep tra l'attesa di un comando
ed il successivo. */
long dSleep=3000;
/** E' l'applicazione target a cui i comandi andranno
inviati. */
private SocketListener dApp=null;
[...]
} // end class
Molto
importante è la dichiarazione dell'ultimo dato
membro, che altro non è che l'applicazione GUI
alla quale andranno inviati i comandi ricevuti dal canale
socket. Abbiamo scelto, per una migliore libertà
di progettazione, di usare una interfaccia che stabilisca
un canale di comunicazione con il modulo socket.
In
effetti l'interfaccia SocketListener è molto
semplice e definisce due metodi che permettono di inviare
all'applicazione in ascolto i comandi che vengono ricevuti
via socket:
package
org.illeva.remcom.cmdsock;
/**
* E' l'interfaccia che l'applicazione target deve implementare
per eseguire i comandi ricevuti via socket.
*/
public interface SocketListener
{
/** E' il metodo per l'esecuzione dei comandi GUI, cioè
quelli che in qualche modo
riguardano l'aspetto grafico. */
public void processWinCommand(SocketEvent anEvent);
/** Esegue i comandi di tipo applicativo. */
public String processApplicationCommand(SocketEvent
anEvent);
} // end interface
Abbiamo
costruito una classe specifica per gestire gli eventi
che possono arrivare via socket:
package
org.illeva.remcom.cmdsock;
import
java.util.EventObject;
/**
* E' l'evento che lapplicazione target deve gestire.
*/
public class SocketEvent extends EventObject
{
/** E' un evento di tipo Window che in qualche modo
riguarda la GUI dell'applicazione target */
public static final int WIN_CMD=1;
/** E' un evento di tipo applicativo. */
public static final int APP_CMD=2;
public static final int WIN_CLOSE=10;
public static final int WIN_HIDE=11;
public static final int WIN_SHOW=12;
public static final int APP_PARAM=100;
public static final int APP_CMDLIST=101;
// Data members
/** E' il tipo di evento da gestire */
private int dType=0;
/** E' il parametro dell'evento */
private String dParam;
/** E' il comando che va eseguito */
private int dCommand=0;
/** Ritorna il tipo di comando. */
public int getType() {return dType;}
/** Ritorna il comando da eseguire. */
public int getCommand() {return dCommand;}
/** Ritorna il parametro del comando. */
public String getParam() {return dParam;}
/**
* Costruttore dell'evento.
*/
public SocketEvent(Object aSource,int aType,int aCommand,String
aParam)
{
super(aSource);
dType=aType;
dCommand=aCommand;
dParam=aParam;
} // end constructor
/**
* Costruttore dell'evento con parametro nullo.
*/
public SocketEvent(Object aSource,int aType,int aCommand)
{
this(aSource,aType,aCommand,null);
} // end constructor
} // end class
Torniamo
per un attimo alla classe MainSocket per dare un'occhiata
al metodo principale della classe stessa, il metodo
run il cui compito è quello di smistare le chiamate
in arrivo facendo in modo che ogni chiamata continui
il suo dialogo su di un canale privato per liberare
il più presto possibile il canale principale
in attesa di nuove richieste:
/**
* E' il metodo vero e proprio del thread. Non appena
viene ricevuto un comando, un socket viene associato
* alla richiesta ed inizia il colloquio vero e proprio
fra Client e Server.
*/
public void run()
{
/* Ciclo che regola l'sistenza del thread */
while(dExists)
{
/* Ciclo che regola l'ascolto o meno sul canale del
thread stesso */
while(dActive)
{
Socket vSock=null;
try
{
// System.out.println("Prima della accept...");
vSock=dSock.accept();
new ChildSocket(vSock,dApp).start();
System.out.println("Ho creato un Child...");
} // end try
catch(SocketTimeoutException anEx)
{
// System.out.print(".");
} // end catch
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end while
/* Se l'ascolto sul canale è disattivato il thread
dorme per un po' di tempo */
try
{
// System.out.println("Sto dormendo...");
sleep(dSleep);
} // end try
catch(InterruptedException anEx)
{
System.out.println("MainSocket wake up...");
} // end catch
} // end while
} // end method
Una
volta che la richiesta arriva al ServerSocket il dialogo
vero e proprio tra client e server viene gestito dalla
classe ChildSocket. Essendo praticamente possibile che
più richieste si sovrappongano, ciascuna classe
che gestisce il dialogo tra server e client deve essere
un thread in modo da riuscire a parallelizzare il lavoro.
package
org.illeva.remcom.cmdsock;
import
java.net.*;
import java.io.*;
import java.awt.event.*;
/**
* E' il socket lato server che viene creato ad ogni
richiesta che il ServerSocket riceve.
*/
public class ChildSocket extends Thread
{
// Data members
/** E' il canale di comunicazione che si apre fra i
due socket. */
private Socket dSock=null;
/** E' l'applicazione target. */
private SocketListener dApp=null;
/** E' la Stringa da spedire. */
private String dSend;
[...]
} // end class
Anche
per questa classe il cuore di tutto è il metodo
run dove, con una semplicissima macchina a stati, si
gestisce il dialogo tra client e server:
/**
* Gestisce il protocollo di comunicazione, lato server,
con il client.
*/
public void run()
{
try
{
PrintWriter vOs=new PrintWriter(dSock.getOutputStream(),true);
BufferedReader vIs=new BufferedReader(new InputStreamReader(dSock.getInputStream()));
int vStatus=1;
dSend="Hy rcClient";
String vReceive="";
String vCmd=null;
while(vStatus>0)
{
vOs.println(dSend);
vReceive=vIs.readLine();
if(vReceive.equals("Bye"))
vStatus=0;
else
switch(vStatus)
{
case 1:
if(vReceive.startsWith("Hy rcServer, i'm"))
{
vStatus=2;
dSend="Sends command";
} // end if
else
vStatus=0;
break;
case 2:
if(vReceive.equals("WinCmd(Close)"))
vCmd=vReceive;
else
executeCommand(vReceive);
// vSend="Executed command "+vReceive;
vStatus=3;
break;
default:
vStatus=0;
} // end switch
} // end while
vOs.close();
dSock.close();
/* Se alla fine del dialogo è stato ricevuto
il comando, allora
lo trasmette all'applicazione.
Questo perchè c'è almeno un comando che
non può essere eseguito immediatamente,
quello di chiusura dell'applicazione. */
if(vCmd!=null)
executeCommand(vCmd);
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end method
In
fase di testing si è scoperto che esiste almeno
un comando che non può essere inviato immediatamente
all'applicazione, cioè il comando che chiude
l'applicazione stessa. Se questo viene eseguito immediatamente,
il dialogo fra client e server si interrompe ed il client
riceve un'eccezzione di IO. Questo è il motivo
per il quale, una volta chiuso il canale di comunicazione
controlliamo se c'è un comando che deve essere
eseguito.
Il
programma client
Paassiamo
ora ad analizzare la parte client di tutto il progetto,
e cominciamo subito dalla comunicazione via socket che
avevamo appena lasciato dal lato server.
Il
dialogo lato client
Il
dialogo sul lato del fiume che è abitato dai
clients è gestito dalla semplice classe che con
un enorme sforzo di fantasia creativa abbiamo chiamato
"Client". Glissando su questo fatto che da
solo basterebbe a farci radiare dall'albo dei programmatori,
diamo un'occhiata alla sua struttura:
package
org.illeva.remcom.cmdsock;
import
java.net.*;
import java.io.*;
/**
* E' la classe che gestisce il protocollo di comunicazione,
lato client. In pratica invia i comandi all'applicazione
target.
*/
public class Client
{
// Data members
/** E' il canale di comunicazione */
private Socket dSock=null;
/** E' il comando da inviare */
String dCmd;
[...]
} // end class
Al
costruttore di questa classe viene passato un IP e la
porta da usare per la chiamata al server:
/**
* E' il costruttore della classe.
*/
public Client(String aHost,int aPort,String aCmd)
{
dCmd=aCmd;
try
{
dSock=new Socket(aHost,aPort);
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end method
Come
al solito, comunque, il metodo run è quello che
racchiude il lavoro vero e proprio che la classe esegue:
public
String run()
{
String vRc="";
try
{
PrintWriter vOs=new PrintWriter(dSock.getOutputStream(),true);
BufferedReader vIs=new BufferedReader(new InputStreamReader(dSock.getInputStream()));
int vStatus=1;
String vReceive="";
String vSend="";
while(vStatus>0)
{
vReceive=vIs.readLine();
System.out.println("[Rec] "+vReceive);
switch(vStatus)
{
case 1:
if(vReceive.startsWith("Hy rcClient"))
{
vSend="Hy rcServer, i'm user,password";
vStatus=2;
} // end if
else
{
vSend="Bye";
vStatus=0;
} // end else
break;
case 2:
if(vReceive.equals("Sends command"))
{
vSend=dCmd;
vStatus=3;
} // end if
else
{
vSend="Bye";
vStatus=0;
} // end else
break;
case 3:
if(vReceive.startsWith("Executed command"))
{
vSend="Bye";
vStatus=0;
} // end if
else
{
vRc=vReceive;
vSend="Bye";
vStatus=0;
} // end else
break;
default:
vSend="Bye";
vStatus=0;
} // end switch
vOs.println(vSend);
System.out.println("[Send] "+vSend);
} // end while
vOs.close();
dSock.close();
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
return vRc;
} // end method
La
stringa che il metodo restituisce è la risposta
che il server da alla chiamata effettuata. Nella maggior
parte dei casi questa risposta conferma l'avvenuta esecuzione
del comando richiesto. Vi è però una richiesta
particolare alla quale il server risponde con la lista
di tutti i comandi che possono essere eseguiti su quella
macchina.
Un
dialogo di esempio
A
titolo di esempio riportiamo un dialogo tra client e
server per cercare di capire quali sono le informazioni
che vengono scambiate.
[Rec]
Hy rcClient
[Send] Hy rcServer, i'm user,password
[Rec] Sends command
[Send] App(CmdList)
[Rec] CmdList(Win(Close),Win(Hide),Win(Show),App(Batch1),App(Batch2))
[Send] Bye
Il
dialogo sopra riportato è quello che si instaura
quando un client con interfaccia grafica, all'atto della
partenza chiede al server quali sono i comandi che possono
essere eseguiti.
La
GUI del client
L'interfaccia
del programma client è molto semplice: una combobox
dove sono presenti tutti gli hosts con relativa porta
ai quali è è possibile inviare un comando.
Una volta scelto l'host, la lista sotto la combo viene
riempita con i comandi che è possibile eseguire
sull'ost selezionato. Scelto un comando dalla lista,
la pressione del bottone di esecuzione invia il comando
al PC target.
Come
per la parte server, anche il programma client per funzionare
correttamente ha bisogno di un file di configurazione
che riporta la lista degli host che possono essere contatttati
con la rispettiva porta alla quale rispondono. Il file
che si trova nella directory config si chiama hosts.txt:
#
# Lista dei Server
#
localhost(1304)
# test(1305)
Un'occhiata
al sorgente che esegue il programma client ci permette
di scoprire una caratteristica interessante:
package
org.illeva.remcom;
import
org.illeva.remcom.cmdsock.Client;
import org.illeva.remcom.gui.ClientWin;
public
class MainClient
{
public static void main(String[] argv)
{
if(argv.length==3)
{
Client vObj=new Client(argv[0],new Integer(argv[1]).intValue(),argv[2]);
System.out.println("Return: "+vObj.run());
} // end if
else
{
ClientWin vObj=new ClientWin();
vObj.show();
vObj.pack();
} // end else
} // end main
} // end class
Il
programma lanciato con i parametri che rappresentano
un host, una porta sulla quale operare ed un comando
da lanciare, invia direttamente il comando senza aprire
la GUI. Questo è utile per chi volesse costruirsi
un comando batch da inserire direttamente sul desktop
del computer. Richiamato senza parametri, il programma
parte mostrando la GUI attraverso la quale l'utente
può operare normalmente.
Implemetazioni
possibili
Ci
preme sottolineare che pur trattandosi di un programma
dimostrativo, può essere utilizzato in un contesto
reale. Se si dovesse comunque utilizzare l'architettura
in un ambiente professionale, personalmente riorganizzerei
tutto il disegno per rendere possibile il poter pilotare
più applicazioni diverse utilizzando un solo
modulo di comando socket. Così com'è strutturato,
se si devono pilotare due applicazioni diverse sullo
stesso server, occorrono due istanze del programma che
utilizzino due porte diverse.
Un'altra
pecca del progetto è che, una volta lanciato
il comando, il server non rinvia al client nessun tipo
di informazione sul batch in esecuzione (potrebbe ad
esempio rinviare l'output del processo lanciato). Personalmente
ho, in parte, ovviato a questo problema utilizzando
il comando net send di windows, comando che permette
di visualizzare messaggi su di una determinata macchina.
Per
chi volesse mettere le mani in pasta, questi sono i
due principali filoni di sviluppo, oltre, magari, a
riscrivere la GUI utilizzando le swing, che risultano
essere un po' più eleganti della AWT che abbiamo
utilizzato per semplicità.
Per
chi volesse dare un'occhiata alla documentazione generata
con javadocs, abbiamo cercato di commentare tutte le
parti del codice sorgente in modo da rendere più
chiaro possibile quale sia il disegno di questa piccola
applicazione. L'applicazione completa può essere
scaricata da questo stesso
link.
|