MokaByteNumero
16 - Febbraio 1998
|
|||
|
|
||
Michele Sciabarrà |
|
||
Java è
un linguaggio chiaro ed elegante, semplice ma potente, portabile e innovativo.
Un "vero" programmatore, avendo tra le mani un simile strumento, non può
certo evitare di scrivere un bel programma.
Scherzi a parte,
nella precedente serie di articoli sul Perl (apparsa in Computer Programming
n.d.r.) ho mantenuto un approccio generalista, passando da un esempio all'altro
senza far riferimento ad un ordine particolare, ma cercando di proporre
programmi significativi. L'obiettivo era esaminare ogni volta un aspetto
differente. In questa nuova serie seguirò un approccio diverso e
scriverò un programma di una certa complessità raccontando
le varie peripezie e avventure.
In questo modo
potremo imparare ad utilizzare Java per fare qualcosa di più complesso
di una semplice applet. L'idea per l'impostazione di questa serie mi è
venuta seguendo la column "Chaos Manor" di Jerry Pournelle sulla
rivista americana Byte. Jerry racconta mese per mese le sue peripezie e
ho sempre trovato gustosissimo quello che gli succede e i disastri indotti
semplicemente dal cambiare un mouse (cose che in verità accadono
tutti giorni anche a me). Tuttavia l'aspetto più importante che
voglio riportare di quella column è quello istruttivo. Infatti,
mi è capitato spesso di ritrovare trucchi e informazioni che poi
mi sono stati utili anche nella pratica.
Un'altra chiave
di lettura che voglio offrire è quella introduttiva. Descrivendo
passo dopo passo la realizzazione di un programma mi soffermerò
sui dettagli della libreria; sarà interessante, inoltre, se qualche
lettore supporterà il mio lavoro fornendomi critiche, consigli,
modifiche, aggiunte e test. Comunque sia, intendo mantenere il programma
nei limiti consentiti dalla ragionevolezza e dalla semplicità, evitando
di aggiungere funzioni che avrebbero bisogno di molte righe di codice e
molto tempo per essere adeguatamente testate. Detto ciò, possiamo
iniziare.
Un
Sistema di chat multiutente in Java
Andando in giro
per la rete avrete forse visto qualche sistema di chat sviluppato in Java
(per esempio www.talk.com). Si tratta infatti di una applicazione
molto interessante che va oltre le semplici applet che abbelliscono le
pagine web. Il programma che svilupperemo non rappresenta dunque una novità,
ma vederne la realizzazione pratica dovrebbe essere di un certo interesse,
sia come base per fini didattici, sia come base per ulteriori modifiche
e raffinamenti da parte dei lettori.
Inoltre la realizzazione
è sufficientemente complessa da giustificare numerosi articoli che
coprano moltissimi aspetti del linguaggio. Un sistema di chat, è
per necessità di cose, un sistema client/server. Supponiamo infatti
di voler fare un programma singolo. Una prima possibilità è
collegarsi direttamente con gli utenti con i quali desideriamo comunicare,
utilizzando un sistema a rotazione (per esempio il primo comunica col secondo
che comunica col terzo e così via). Ma questo approccio ha delle
serie difficoltà. Per esempio, come si fa a sapere l'indirizzo degli
altri utenti? Ammettendo che questo venga comunicato in qualche modo, per
esempio via e-mail, rimane un'altra seria difficoltà tecnica. Java
è famoso per la sua capacità di incorporare programmi nelle
pagine Web e naturalmente vorremmo che il programma potesse girare come
applet. In questo caso dobbiamo scontrarci con la seguente limitazione:
le applet possono collegarsi soltanto ad un IP, ovvero solo alla
macchina da cui sono state scaricate. In queste condizioni, non è
possibile in nessun modo comunicare con altri, ammessa l'esistenza di un
modo per ottenere l'indirizzo automaticamente.
Quindi, l'unico
approccio praticabile è quello di avere un programma server,
che accetti delle connessioni da varie istanze di un altro programma client.
Quindi si devono scrivere due programmi differenti e complementari.
In pratica i client si connettono ad un server che funge da concentratore.
I client saranno delle applet mentre il server girerà come applicazione.
Quest'ultima condizione è necessaria perché solo così
il server potrà evitare le limitazioni imposte dai browser, in particolare
le limitazioni sugli IP.
Il meccanismo
in teoria è molto semplice: ogni client invia messaggi al server
che li ritrasmette a tutti gli altri client. Comunque ci sono diverse altre
funzionalità che possono essere richieste: per esempio i "canali"
di conversazione, le conversazioni private, la gestione dei nickname,
la possibilità di collegare tra di loro a catena più server,
ecc. Onestamente non so quante e quali di queste funzioni saranno implementate.
Tuttavia rilascerò il codice in modalità freeware,
protetto dalla famosa GPL (GNU Public License) in maniera tale che
sia liberamente ampliabile ( il codice modificato dovrà comunque
essere rilasciato freeware).
Funzioni
di base
Chiameremo il
programma JRC (Java Relay Chat), per similitudine al ben
noto sistema di chat Internet IRC, col quale però il sistema
non ha niente a che fare se non il fatto di svolgere funzioni molto simili.
JRC è
un sistema di chat multiutente client/server, composto da due distinti
programmi (un client e un server, appunto) implementati interamente in
Java. La versione 0.1 sarà abbastanza limitata e avrà
soltanto le funzioni base.
Il server ha
la funzione di accettare connessioni dai client; ogni client invia
al server gli interventi dell'utente che lo ha attivato e legge dal server
gli interventi di tutti gli altri. In questa versione ci sarà un
solo canale, e non ci sarà la gestione di nickname o di autorizzazioni
di ingresso: semplicemente il server accetterà gli interventi da
chiunque voglia connettersi, reinviandoli a tutti gli altri client, utilizzando
un solo canale di conversazione. Canali multipli, reti di server, gestione
dei nickname eccetera sono rimandati alle prossime versioni.
In questo articolo
cominceremo ad implementare il server. Per il momento avremo soltanto un
processo che attende connessioni; quando ne riceve una viene attivato un
altro processo esecutore di comandi per gestirla. L'esecutore di comandi
si limiterà semplicemente ad accettare il comando quit e
a ritornare inalterato l'input che riceve. Sembra una cosa elementare,
ma dobbiamo comunque far attenzione a parecchie cose: innanzitutto alle
basi per la gestione dell'I/O e dei socket, poi all'implementazione di
una interfaccia per la registrazione delle attività, infine al server
principale e l'esecutore dei comandi, dal quale poi deriveremo delle classi
che implementeranno il protocollo.
Gestione
dei Socket
Un socket
è
un canale di comunicazione di rete con altre macchine; in un certo senso
è come un file, con la differenza che scrivendo in questo canale,
qualcun altro "dall'altra parte" potrà leggere quello che si è
scritto; inoltre il canale è bidirezionale, cioè chi scrive
può anche leggere. I socket sono nati in ambiente Unix, ma sono
stati implementati in (praticamente) tutti i sistemi operativi. I socket
sono alla base dei programmi per Internet, come i browser o i programmi
di posta elettronica. Java comprende nella sua libreria standard delle
classi che consentono di utilizzarli con molta facilità. Il package
che comprende le funzioni per l'utilizzo dei socket è java.net,
ma fa uso di alcune classi del package java.io.
Nella tabella
1 abbiamo riassunto le classi e i metodi principali per l'uso dei socket,
mentre nella tabella 2 abbiamo quelle per l'uso dell'I/O.
|
|
Poiché
utilizzeremo queste classi, ci soffermiamo a discuterle.
Le classi mostrate
in tabella 1 sono quelle necessarie per usare i socket; consideriamo
la classe Socket che serve per aprire una connessione.
I costruttori
consentono di costruirne uno, data una porta e un indirizzo IP (che va
costruito creando una apposita istanza della classe InetAddress),
oppure specificando una stringa nome di un host (quindi sarà
il costruttore a ricavare l'indirizzo IP risolvendo il nome). Il socket
può essere chiuso, può essere esaminato per conoscere le
porte e gli indirizzi dei due capi della connessione, ed è possibile
stamparlo (utilizzando l'universale metodo toString di Java), in
modo da vederne la configurazione attuale. La cosa importante è
comunque vedere come si fa per leggere o scrivere. A questo scopo si utilizzano
due stream, uno di input e l'altro di output che possono essere
letti con gli appositi metodi. Dobbiamo quindi esaminare gli stream, ma
prima terminiamo la tab esaminando il ServerSocket: questo serve
appunto per creare un server che sta in attesa. Il costruttore richiede
una porta e si pone in attesa. Per accettare una connessione si utilizza
il metodo accept() che rimane sospeso finché qualcuno non
richiede una connessione client. A questo punto viene ritornato un Socket
già aperto, del quale si possono ricavare gli stream di I/O con
geInputStream()
e getOutputStream(). Anche il ServerSocket
naturalmente
può essere chiuso, esaminato e stampato.
Gli stream sono
il modo in cui viene implementato l'I/O in Java.
In tabella
1 sono riassunti i principali metodi delle classi InputStream e
OutputStream
che sono i tipi degli oggetti ritornati dai metodi che ricavano gli stream
dai Socket e che devono essere utilizzate per l'I/O di rete. Sia gli stream
di input (InputStream) che di output (OutputStream) hanno
un costruttore e un metodo per chiuderli (close). Gli stream di
input possono essere letti, utilizzando il metodo read, prelevando
un singolo byte alla volta oppure un array di byte (il numero di byte da
leggere è determinato dalla lunghezza dell'array). Un aspetto tipico
dell'I/O è il fatto che i dati sono spesso trasferiti a blocchi.
Questo perché il trasferimento di dati tipicamente impiega molto
tempo, e il tempo necessario per trasferire un singolo byte è spesso
uguale a quello necessario per trasferire un blocco di un kilobyte. Pensiamo
ai dischi: il tempo impiegato a scrivere dei dati è determinato
in misura preponderante dal tempo necessario per posizionarsi della testina
del disco su un settore; a quel punto la differenza tra scrivere un byte
o scriverne un migliaio è irrilevante. Questo fatto è vero
anche per le connessioni di rete, in quanto i dati vengono normalmente
trasferiti a pacchetti. Per questo motivo i sistemi di I/O normalmente
raccolgono blocchi di input e li inviano tutti insieme, cioè bufferizzano
l'I/O.
Viene richiesto
un intero blocco e poi si possono leggere i dati nel buffer di input. Per
questo motivo, l'input può procedere finché il buffer non
è vuoto.
La lettura può
essere eseguita con InputStream.read.
Poiché
l'input può bloccarsi, InputStream.available() consente di
conoscere quanti byte possono essere letti senza che una lettura si blocchi.
Un discorso analogo vale per l'output, che viene bufferizzato finché
il buffer non è pieno. Con OutputStream.write() si può
scrivere, un byte o un array di byte per volta. Il metodo OutputStream.flush()
obbliga
lo stream a svuotare il buffer, inviando a destinazione l'output nel buffer.
Logger
e derivati
Una operazione
effettuata comunemente dai server è la registrazione delle attività.
Questo perché un server normalmente opera senza che nessuno ne controlli
le attività ma in generale è necessario sapere chi ha usato
il server e che cosa ha fatto.
Questa operazione
viene normalmente chiamata logging. Per questo motivo il server
utilizzerà un Logger, ovvero un oggetto che si occuperà
di registrare le operazioni effettuate.
Il logger è
molto utile (praticamente indispensabile) nelle attività di debugging,
quindi ci occuperemo immediatamente di costruirne uno. In generale il logging
può andare sulla console, su un file oppure su una finestra: quindi
definiremo una base comune, una semplicissima interfaccia, e poi implementeremo
diversi logger.
I server si
aspetteranno semplicemente un oggetto di classe Logger. L'interfaccia
Logger
è la seguente:
AppServer
e AppServerExec
Possiamo adesso
implementare il server. In realtà avremo due diverse classi: il
server vero e proprio, AppServer, e l'esecutore di comandi, AppServerExec.
In tabella 3 sono riassunti i metodi di queste due classi. Le due
classi sono pensate per essere usate insieme.
|
L'AppServer
ha
il compito di accettare nuove connessioni e di mandare in esecuzione un
nuovo AppServerExec per gestirle.
Quando l'AppServer
viene
costruito, occorre fornire la porta in cui ascolta, un logger
e
un esecutore, ovvero un elemento di classe AppServerExec.
I metodi pubblici
di AppServerExec sono quelli che servono per interagire con AppServer:
il costruttore fornisce una istanza, mentre il metodo detach viene
invocato per attivare una nuova istanza. Il metodo run è
fornito perché l'esecutore implementa l'interfaccia Runnable
che consente di avviare un thread differente per ogni esecutore attivato.
Per comprendere meglio, osserviamo che il JRC.main è semplicemente:
|
L'obiettivo della progettazione è di fare in modo che si possa definire un nuovo esecutore semplicemente ridefinendo il metodo run ed ereditando tutto il necessario per interagire facilmente con il socket: abbiamo infatti getLine e putLine per leggere e scrivere linea per linea. Sfortunatamente ci sono altri due aspetti da tenere presenti: innanzitutto occorre implementare esplicitamente il metodo detach (se non lo si fa, il detach attiverà un nuovo thread che esegue AppServerExec.run()) e poi occorre ricordarsi che il socket deve essere chiuso esplicitamente dal metodo run() prima di terminare. Per il resto si può operare in maniera abbastanza semplice e naturale, leggendo con geLine e scrivendo con puLine.
Conclusioni
Abbiamo appena
cominciato. Comunque abbiamo già analizzato alcuni elementi interessanti
come i socket e il multithreading. Nella prossima puntata renderemo utilizzabile
il server, definendo il protocollo per il chat e cominceremo ad implementarlo.
Bibliografia
[1] Michele
Sciabarrà "Lezioni di Java" Computer Programming 53, 54,
55, 56, 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, 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
|
||
MokaByte ricerca
nuovi collaboratori.
|
||
|