MokaByte Numero 19 - Maggio 1998
 

 
Java Relay Chat
III parte
 
di
Michele
Sciabarrà
In questo numero finalmente completiamo il sistema. Una applet permetterà di fornire agli utenti del sito un semplice sistema di chat


 


Nei numeri precedenti ci siamo preoccupati di definire il protocollo e codificare il server. Abbiamo visto come, volendo, si possa "chattare" interagendo direttamente con tale programma. Tuttavia si tratta di una interazione piuttosto scomoda: il protocollo è progettato per essere utilizzato da un apposito programma client e non direttamente dall’utente finale. Quindi il lavoro non può dirsi completo senza implementare un client. L’obiettivo è consentire l’uso del chat agli utenti del World Wide Web, senza dover installare nessun programma. In questo caso la scelta è quasi obbligata: uno dei pochi linguaggi con cui oggi si possono scrivere applet è Java (anche se esiste un compilatore Ada che genera codice per la JVM). Naturalmente, dato che il protocollo è documentato e piuttosto semplice, nulla impedisce di scrivere un client JRC, per esempio, in Visual Basic




Interfacce grafiche in Java
Il programma server gira in background senza interagire direttamente con gli utenti e non ha quindi bisogno di una interfaccia grafica (anche se potrebbe eventualmente creare una finestra di log). Il client è un caso diverso: vogliamo che l’utente interagisca comodamente. L’uso di una GUI è quindi obbligatorio. In Java per costruire interfacce grafiche occorre utilizzare la libreria AWT. In realtà oggi questa non è più l’unica scelta. Per esempio Netscape ha sviluppato le IFC (Internet Foundation Classes), mentre Microsoft sta sviluppando le AFC (Application Foundation Classes). L’ultima novità è che le IFC e la AWT convergeranno nelle JFC (Java Foundation Classes) grazie alla collaborazione di Sun, Netscape e IBM. Mentre scrivo comunque i browser che supportano le nuove librerie di classi sono ancora in beta. Moltissimi browser installati non sono in grado di supportare né IFC né AFC né JFC, ma solo la AWT 1.0. Ragion per cui da oggi e per un certo periodo a questa parte (un paio di anni al massimo) per compatibilità con l’installato dovremo continuare a scrivere applet che utilizzino la prima versione della AWT. Abbiamo deciso quindi di rispettare questo vincolo per garantirci la compatibilità con il maggior numero di browser possibile.
La AWT 1.0 in verità non è particolarmente ricca né molto ben progettata. Programmare con questa libreria non è molto comodo e le interfacce prodotte sono abbastanza povere. In principio occorreva sviluppare a mano il codice che genera l’interfaccia grafica. Un programmatore Visual Basic può quindi pensare che Java sia un ritorno ai primi tempi di Windows, non l’ultima novità della tecnologia. Sono presto comparsi in commercio vari strumenti che consentono di disegnare l’interfaccia utente invece di codificarla. In pratica nessuno di essi svolge un buon lavoro. Il problema è che la AWT è stata chiaramente progettata pensando alla codifica manuale delle GUI. L’impostazione porta a mescolare il codice che descrive l’interfaccia con quello che gestisce gli eventi. Questo è abbastanza comodo quando il programmatore deve fare tutto a mano: non deve continuamente saltare da un file all’altro. Utilizzando invece uno strumento visuale, il risultato diviene contorto e poco chiaro: le parti codificate manualmente si confondono con codice generato automaticamente, che è generalmente complesso e poco leggibile.
Per fortuna, la AWT 1.1 pone rimedio a tutti i problemi che rendono difficoltoso usare ambienti di sviluppo visuali con la 1.0. Con la nuova AWT finalmente è efficace utilizzare gli strumenti visuali (infatti gestione eventi e codifica dell’interfaccia sono distinti), mentre la codifica manuale diviene più complessa.
 
 
 
 

La AWT ed il disegno dell’interfaccia...
Vediamo come si programma utilizzando la AWT 1.0. In realtà l’obiettivo è scrivere una applet con interfaccia grafica. Poiché utilizzeremo la AWT per la GUI delle applet, lo studio di questa libreria è un passaggio obbligato. Coprire tutti gli aspetti sarebbe abbastanza lungo, quindi ci limiteremo a trattare gli argomenti principali (quanto basta per capire il codice del client).
Trattandosi di un linguaggio OOP, la libreria AWT è una gerarchia di classi. Alcune classi sono più importanti di altre perché sono le radici di grossi sottoalberi.
Da Object è derivato Component e da questo Container. Queste ultime due classi sono fondamentali: servono per suddividere le classi della AWT in due importanti categorie: i componenti di interfaccia veri e propri (per esempio i bottoni) e i contenitori di componenti (per esempio le finestre). In realtà la distinzione tra componenti e contenitori non è così netta: un Container e tutti i componenti contenuti, può infatti essere trattato come se fosse un singolo componente. Questo comunque non è vero sempre: per esempio una Window non può contenere altre Window. Pensando alla MDI di Windows si potrebbe forse pensare che sia lecito. In realtà la MDI è specifica di Windows e un programma Java che ne faccia uso non potrebbe girare, per esempio, sul Mac. Comunque ci sono dei contenitori (in particolare i Panel ) che non generano una finestra separata e possono effettivamente essere utilizzati come componenti (quindi composti da sottocomponenti). I Panel possono effettivamente essere aggiunti ad altri container come se fossero singoli componenti. In Tabella 1 sono riassunti i componenti disponibili, mentre nella Tabella 2 sono riassunti i contenitori.

Tabella 1


Button Bottone
Canvas Compoonente per disegnare
Checkbox Selezione on/off
CheckboxMenuItem MenuItem a selezione on/off
Choice Lista di selezione a discesa
Label Etichetta
List Lista di selezione
Menu Un menù contenente MenuItem
MenuBar La barra dei menù
MenuItem Una voce di menù
Scrollbar Barre di scorrimento
TextArea Campo di testo multilinea
TextField Campo di testo a linea singola

 
 

Tabella 2


Dialog Finestra di Dialogo
FileDialog Seleziona un file da aprire
Frame Finestra con menù
Panel Contenitore non finestra
Window Finestra semplice

 
 

L’uso è semplice: si crea un contenitore e si aggiungono dei componenti al contenitore. Il problema si pone nel posizionamento degli elementi nel contenitore. Si potrebbe pensare che quando si aggiunge un componente occorra anche specificare le sue coordinate nel container. Questo approccio effettivamente è previsto ed è utilizzabile. Comunque c’è un problema: codificando in questo modo si ha una dipendenza dell’interfaccia dalla risoluzione. Infatti, data la varietà delle macchine che accedono ad Internet, quello che si vede bene a 640x480 sembrerebbe un francobollo a 1024x768. La soluzione usata dalla AWT (e ripresa dal linguaggio Tcl/Tk) è utilizzare un posizionamento "logico" nel container: non si specificano le coordinate, bensì si dà una indicazione di come debbano essere posizionati gli elementi: per esempio Nord, Centro, Est. Il meccanismo si basa sull’uso dei cosiddetti LayoutManager. In Tabella 3 sono elencati i layout manager disponibili nella AWT 1.0, ma è possibile definirne di nuovi. Innanzitutto ogni contenitore può impostare il layout manager corrente utilizzando setLayout. Quando verrà aggiunto un nuovo elemento, il contenitore affiderà il posizionamento dell’elemento al layout manager.
 
 

Tabella 3


BorderLayout A croce
CardLayout A "tabbed dialog"
FlowLayout A flusso orizzontale
GridBagLayout A griglia con vincoli
GridLayout A griglia

 

È importante notare che il posizionamento è dinamico e dipendente sia dalla dimensione del contenitore che dalla dimensione dei componenti. Utilizzando questo meccanismo si creano interfacce dinamiche facilmente ridimensionabili che mantengono la loro struttura.
Per esempio consideriamo il caso di una finestra che contiene una Label in alto (la stringa "Hello") e il bottone "OK" in basso.
 
 

   class MioFrame extends Frame {
        Button b = new Button("OK");
        MioFrame() {
            setLayout(new BorderLayout());
            add("North",new Label("Hello"));
            add("South", b);
        }
    }
 
 
 
 

...e la gestione degli eventi
Una volta disegnata l’interfaccia utente, si pone il problema della gestione degli eventi: ovvero vorremo fare in modo che l’interfaccia risponda al click di un bottone, alla selezione di una voce di menu o altro. Ogni componente in Java è sensibile agli eventi tramite il metodo handleEvent.
Quest’ultimo metodo ereditato "così com’è" non fa nulla: per gestire gli eventi occorre ereditare da un componente una nuova classe che lo ridefinisca opportunamente. Per esempio, supponiamo di voler dotare la classe MioFrame di un gestore di eventi che nasconda la finestra quando si preme un bottone. Occorre aggiungere il seguente metodo:
 
 

    public boolean  handleEvent(Event evt) {
        if(evt.target==b) {
            hide();
            return true;
        }
    return
    super.handleEvent(evt);
    }
 
 

Cliccando su OK innanzitutto il sistema tenterà di smistare l’evento alla handleEvent del bottone. Se il bottone sarà in grado di gestire l’evento, la handleEvent ritornerà true e la gestione terminerà.
Nel nostro caso non andrà così perché la Button.handleEvent non farà nulla e ritornerà false. L’evento verrà allora smistato al contenitore dei componenti, nel nostro caso alla finestra MioFrame. Questa volta l’evento verrà effettivamente gestito e la gestione terminerà grazie al "return true". È da notare l’uso della super.handleEvent(): è utilizzata per smistare l’evento ad una eventuale superclasse qualora l’evento non dovesse essere gestito localmente.
Infine ricordiamo che non sempre è necessario ridefinire la handleEvent, ma si possono utilizzare degli altri metodi più specifici. Ridefinire la handleEvent comporta che vengano intercettati tutti gli eventi. Tuttavia la finestra Component.handleEvent (la finestra principale) chiama alcuni metodi in corrispondenza a particolari eventi: per esempio mouseDown o keyUp. Se si vogliono soltanto gestire i click del mouse o la tastiera è sufficiente ridefinire questi metodi invece di ridefinire completamente la handleEvent. Infatti, quest’ultima potrebbe essere una scelta inefficiente: handleEvent si occupa di gestire anche eventi di sistema come i repaint. Un buon compromesso è ridefinire action che intercetta tutte le azioni dell’utente, tralasciando quelle di sistema.
 
 
 

La classe Applet
Le applet delle pagine Web devono essere definite estendendo la classe java.applet.Applet. Questa mette a disposizione alcuni metodi necessari per interagire con il browser. Normalmente l’esecuzione di un programma in Java parte dal metodo main statico della classe indicata come la classe principale del programma. Le applet invece vengono invocate direttamene dal browser che ha una sua main. In pratica quello che si deve fare è ridefinire alcuni o tutti dei seguenti metodi:
 
 

init(), per l’inizializzazione di una nuova istanza della applet (chiamata quando il browser carica l’applet);
start(), per l'avvio dell’applet (chiamata quando il browser mostra la pagina);
stop(), per fermare la applet (chiamata quando il browser nasconde la pagina);
destroy(), per liberare eventuali risorse occupate dall’applet (chiamata quando il browser elimina dalla memoria l’applet).
Bisogna tenere conto del fatto che la init viene chiamata una volta sola, al caricamento della pagina, mentre stop e start possono essere chiamati più volte. Per esempio se l’utente carica una pagina, vengono chiamate prima init e poi start; se l’utente cambia pagina, viene chiamato stop. Se l’utente però torna indietro alla pagina in cache, non verrà ricaricata l’applet, ma solo fatta ripartire con start.
 
 
 

Modifiche al server
Abbiamo innanzitutto previsto lo switch -w che consente di aprire esplicitamente una finestra di logging. Senza questo switch, il log va nello standard error. Il motivo di questa modifica è contingente: infatti dispongo di un server Linux senza interfaccia grafica. Si tratta di un vecchio 486 con soli 8 mega di RAM e far girare XWindows è "pesante". Ciò nonostante, il JDK per Linux gira senza problemi, a patto ovviamente di non usare la GUI. Ragion per cui ho previsto la possibilità di logging nello standard error.

La seconda modifica è l’aggiunta del comando WHO per "listare" chi c’è. Questa modifica serve per consentire di scegliere il destinatario dei messaggi. Senza questo comando non è possibile tenere traccia di chi è in chat e quindi scegliere il destinatario di una comunicazione riservata.
 
 
 

Come funziona il client
Il client è una applet che viene caricata dinamicamente quando viene consultata la pagina Web che la contiene. Il JRC è utile per offrire un servizio di chat, seppur molto semplice agli utenti del sito. All’avvio appare una finestra che consente di entrare (Figura 1).
 
 

Figura 1

 

Viene richiesto il nostro nickname: se è ammissibile (ovvero non ci sono altri utenti con lo stesso nickname), viene attivata la connessione. Appare quindi la finestra di chat vera e propria, visibile in Figura 2.
 
 
 
 

Figura 2

 

Si tratta di una finestra indipendente, aperta e gestita dalla JVM. Si possono distinguere, in alto, la parte dove appaiono gli interventi; al centro abbiamo un campo di testo per digitare il proprio intervento. In basso ci sono dei bottoni: Talk per inviare l’intervento a tutti, Who per listare chi è correntemente presente nella stanza di conversazione e infine Send per inviare una comunicazione riservata ad un solo utente. Segue infatti un altro campo di testo per scrivere il nickname di destinatari di conversazioni riservate. Infine abbiamo un bottone Clear per svuotare il buffer qualora si riempisse troppo e infine il bottone Quit, che chiude la finestra di chat. Si ritorna così alla finestra principale e volendo si può rientrare nuovamente in chat digitando un nuovo nickname.
 
 
 

Classe AppClient
La programmazione orientata agli oggetti è nata allo scopo di rinforzare la riusabilità del codice. Nonostante questa semplice verità, vedendo come programmano alcuni sviluppatori C++ ci si rende conto che forse lo scopo non è stato raggiunto. È vero, la OOP offre altri vantaggi come la possibilità di ottenere una struttura del codice più ordinata che semplifica la manutenzione. Comunque il vero salto di qualità si ha quando si riesce a riutilizzare del codice. Il problema è che avendo a disposizione delle completissime librerie come le MFC per il Visual C++ o anche la stessa libreria di Java, il programmatore si sente scoraggiato a farsi una nuova libreria, perché gli sembra di "reinventare l’acqua calda". Il fatto è che le librerie di classi sono di solito generaliste e non offrono soluzioni pronte all’uso per il particolare dominio applicativo del programmatore. Di solito invece i programmatori tendono a fare programmi abbastanza simili tra di loro (anche per motivi commerciali o aziendali) legati ad un particolare dominio applicativo. Quindi in ogni caso vale la pena crearsi una piccola libreria. Non si tratta di "perdere tempo", ma di prendere l’abitudine di codificare scrivendo piccole e semplici classi riutilizzabili. C’è comunque un prezzo da pagare: bisogna imparare a separare i concetti generali da quelli specifici delle applicazioni. Consideriamo per esempio la classe JRCClient che scriveremo più avanti e che implementa il protocollo JRC. Se scriviamo una unica classe monolitica le possibilità di riuso, a meno di non scrivere altri client JRC, sono abbastanza basse. Tuttavia c’è un aspetto di questa classe abbastanza generale: il fatto che si tratta di un client di protocollo. Alcuni elementi dunque sono comuni a vari client. Per esempio, possiamo immaginare facilmente la seguente classe:
 

class AppClient {
    void open(String host, int port);
    void close();
    void putLine();
    void getLine();
    String[] split(String s);
}


Questa è una classe estremamente semplice, come vedete. Eppure il suo uso è chiaramente frequente. Il trucco è quindi imparare a separare il caso specifico dell’applicazione dai casi più generali. Scrivere le due classi JRCClient e AppClient può prendere pochi minuti in più rispetto alla sola classe JRCClient. Però il codice di AppClient può essere facilmente riutilizzato. Ogni riuso è tempo risparmiato, non tanto forse in termini di codifica, quanto soprattutto di testing, di analisi del problema e di consultazione di manuali ed help on-line. Quella classe rappresenta un problema risolto che non deve essere rianalizzato, per di più già testata nel contesto di un progetto concreto. La classe AppClient può valere un’oretta di tempo. Saper rifare la stessa cosa sistematicamente significa produttività. Tanto per fare un esempio dell’importanza di questo approccio, almeno metà del codice del server JRC è codice riutilizzato da altri progetti.
 
 

Classe JRCClient
Questa classe è al centro del client: infatti si tratta della classe che interagisce col server utilizzando il protocollo. Per aumentare l’astrazione, incapsula completamente la gestione del server, l’apertura e la chiusura di connessioni di rete, l’invio e la ricezione di stringhe dai Socket. La definizione "astratta" di questa classe (senza l’implementazione dei metodi e senza altri dettagli) è la seguente:
 

class JRCClient  extends AppClient {
    JRCClient();
    void nick();
    void send();
    void talk();
    void list();
    void who();
}

 

Questa classe incapsula completamente la gestione del protocollo. L’utilizzatore crea un nuovo JRCClient e interagisce con esso come se si trattasse di una classe locale, senza sapere nulla del server e del protocollo. Per l’implementazione sfruttiamo i metodi ereditati da AppClient, per aprire e chiudere connessioni e leggere e scrivere nei socket. Abbiamo anche definito due metodi privati che rendono semplice l’implementazione dei metodi pubblici corrispondenti ai comandi: check() e data().
Nella interazione con il server sono infatti possibili due casi: comandi che ritornano solo codici di successo o di errore e comandi che ritornano dati. La check() semplicemente controlla se l’ultimo comando sia stato correttamente inserito. Invece data() si aspetta dei dati nel formato fornito dal protocollo e li ritorna come un array di stringhe (oppure ritorna null se ci sono errori). In questo modo l’implementazione ad esempio del metodo nick (che usa check) è semplicemente:

    public synchronized   boolean nick(String name)   throws IOException {
        putLine("nick "+name+"\n");
        return check();
    }
 
 

mentre l’implementazione di read (che usa data) è
 

    public synchronized String[] read() throws IOException {
        putLine("read\n");
        return data();
    }
 

Classe JRCApplet
Questa classe mostra l’interfaccia che si presenta all’utente prima di entrare in chat. Il metodo init costruisce l’interfaccia (vedere Figura 1), che presenta un campo di input per il nickname, un bottone e una label che serve da barra di stato che mostra messaggi di errore. Per esempio tentando di entrare con un nickname già esistente oppure tentando di entrare in chat quando la finestra è già aperta si ottengono corrispondenti messaggi di errore in basso. La classe JRCApplet mette a disposizione anche due metodi, lock e unlock, che consentono alla finestra di chat di bloccare l’apertura di nuove finestre e di sbloccarla. Per il resto non ci sono altri particolari elementi di rilievo.
 

Classe JRCChat
Questa classe costruisce e gestisce l’interfaccia che si presenta all’utente quando si apre la finestra di chat. Si tratta della classe che implementa la parte grafica del client (la classe JRCClient implementava la parte non grafica). Mostriamo, nel Listato 1, il costruttore di JRCChat in modo da esemplificare i concetti di AWT di cui abbiamo parlato prima.
 

Listato 1
class JRCChat extends Frame implements Runnable {
    public JRCChat(JRCApplet applet, String nick) throws Exception {
       applet.lock();
       this.applet = applet;
       if(!client.open(applet.getDocumentBase().getHost(), 6789))
            throw new Exception("Cannot open the JRC server");
      area.setEditable(false);
      setTitle("JRCClient - ["+nick+"]");
      setResizable(false);
      Panel pn = new Panel();
      pn.setLayout(new BorderLayout());
      Panel ps = new Panel();
      ps.setLayout(new BorderLayout());
      Panel psn = new Panel();
      psn.setLayout(new FlowLayout());
      Panel pss = new Panel();
      pss.setLayout(new FlowLayout());
      psn.add(new Label("Input:"));
      psn.add(text);
      pss.add(talk);
      pss.add(who);
      pss.add(send);
      pss.add(new Label("To:"));
      pss.add(choice);
      pss.add(new Label("    "));
      pss.add(clear);
      pss.add(quit);
      pn.add("Center", area);
      ps.add("North", psn);
      ps.add("South", pss);
      add("North", pn);
      add("South", ps);
      pack();
      thread = new Thread(this);
      thread.start();
      if(!client.nick(nick)) {
        exit();
        throw new Exception("Nickname already exist");
        }
    }
}
 
 
 
Notiamo come il costruttore utilizzi le tecniche dei LayoutManager: il posizionamento non è basato su coordinate assolute ma relative. Riferendosi alla Figura 2, vediamo le tecniche usate per costruire questa interfaccia. Innanzitutto l’intera finestra utilizza un BorderLayout: al nord abbiamo posto la TextArea che mostra le conversazioni, mentre al centro e al sud abbiamo posto due pannelli. Abbiamo quindi utilizzato un contenitore come componente che contiene al suo interno altri componenti. Questa tecnica di nidificazione dei componenti si rivela molto utile in pratica, come si può vedere in questo caso. I pannelli utilizzano il FlowLayout per posizionare i loro elementi orizzontalmente e contengono i bottoni e i campi di testo che completano l’interfaccia utente. Tra le altre azioni svolte dal costruttore, abbiamo il lock della applet (per non consentire l’apertura di altre finestre di chat finché la presente è attiva), la creazione di una nuova istanza di JRCClient che gestirà l’interazione con il server, ed infine l’avvio di un thread autonomo per l’aggiornamento automatico della finestra di chat. Infatti nella finestra di chat devono essere mostrati anche gli interventi degli altri utenti. Poiché questa operazione non è automatica ma occorre esplicitamente eseguire una read, abbiamo risolto il problema creando un thread che ogni mezzo secondo controlla se ci sono stati altri interventi. L’aggiornamento vero e proprio è effettuato dal metodo update, invocato periodicamente dal thread che esegue il metodo run.
La gestione eventi infine si occupa di completare il quadro. Per gestire gli eventi abbiamo ridefinito il metodo action. Ridefinire il metodo handleEvent, oltre ad essere meno efficiente, riserva anche qualche brutta sorpresa con il Netscape Navigator.
Cliccando su Talk, viene invocato il metodo JRCClient.talk con il valore letto dal campo di input come parametro. Cliccando su Send, viene invocato il metodo JRCClient.send; i parametri sono presi sia dal campo di input che dal campo del destinatario. Gli altri casi sono abbastanza semplici: Who stampa nell’area di conversazione il risultato di JRCClient.who(), Clear svuota l’area di conversazione con area.setText(""). Le operazioni di Quit sono leggermente più complesse: occorre chiudere la connessione, fermare il thread della update, nascondere la finestra di chat e infine sbloccare la applet per consentire la creazione di una nuova finestra di chat.
 
 

Conclusioni
Con questa puntata abbiamo completato una prima implementazione del sistema. È ora disponibile per i lettori un completo chat system che, volendo, può essere ampliato e modificato. Il chat gira sia utilizzando il Netscape Navigator 3.0 che Microsoft Internet Explorer 3.0. In Figura 2 infatti sono visibili le finestre di chat aperte da due browser in esecuzione sulla stessa macchina. Questo è già un buon risultato: Java mette d’accordo due acerrimi rivali come Netscape e Microsoft. In realtà c’è da ricordare che il server, pur essendo stato sviluppato sotto Windows 95, in realtà gira su una macchina Linux. Questo è un risultato ancora più eclatante, che mostra chiaramente come Java consenta di raggiungere concretamente quel sogno di interoperabilità multipiattaforma vagheggiato per decenni e che solo ora, finalmente, è realtà concreta e tangibile.
 
 

Bibliografia

[1] Michele Sciabarrà "Lezioni di Java" Computer Programming 53, 54, 55, 56, 57, 58 Edizioni Infomedia 1996/1997
[2] Michele Sciabarrà "Cos’è Java" e "Dissolvenze in Java" , Computer Programming 48, Edizioni Infomedia 1996
[3] Yuri Bettini "OOP e Java" Dev 38, 39, Edizioni Infomedia 1997
[4] James Gosling "The Java Programming Language", Addison-Wesley 1996
[5] Gary Cornell "Core Java", Addison-Wesley 1996
[6] David Flanagan "Java in a Nutshell", O’Reilly & Associates 1996
[7] Michael Morrison et al. "Java Unleashed", Second Edition, Sams.net 1997

 
 

Michele Sciabarrà è consulente informatico e scrittore tecnico, specializzato in sistemi e applicazioni Internet ed Intranet. Si occupa di consulenza, amministrazione e sviluppo software per ambienti di rete e siti Web.

  

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it