MokaByte Numero 14 - Dicembre 1997
|
La volta scorsa [1] avevamo visto una panoramica di alcuni paradigmi per le applicazioni distribuite, ed in particolare i mezzi che Java mette a disposizione per la realizzazione di questi paradigmi. Questa volta iniziamo ad vedere limplementazione del più semplice di questi: il Client - Server.
Ricordiamo brevemente che in questo paradigma un programma chiamato Client richiede dei servizi ad un altro programma chiamato Server. Questultimo è in ascolto di richieste da parte dei client, esegue tali richieste con le risorse che ha a disposizione, e rispedisce, se richiesto i risultati al client. Tali programmi possono risiedere anche su computer diversi: tutto questo è trasparente ad entrambi.
Sebbene questo paradigma sia abbastanza datato, ed in confronto agli altri meno potente, esistono tuttora molte applicazioni, soprattutto in Internet, che fanno uso di questo paradigma:
Lastrazione dalla locazione fisica di tali computer è ottenuta tramite il paradigma delle Socket, presentato per la prima volta nella Berkeley Software Distribution (BSD) della University of California a Berkeley. Una socket è simile ad una presa elettrica; tutto ciò che è in grado di comunicare tramite il protocollo TCP/IP può collegarsi ad una socket e comunicare tramite queste porta di comunicazione. Ovviamente Java mette a disposizione alcune classi per lutilizzo delle socket (si veda a proposito larticolo introduttivo [2]), tra cui la classe Socket. Usando questa classe un client può stabilire un canale di comunicazione con un host remoto. Si potrà comunicare attraverso questo canale utilizzando stream particolari, specializzati per le socket.
Quindi un client, per comunicare con un host remoto usando il protocollo TCP/IP, dovrà creare per prima cosa un oggetto Socket con tale host. Si dovrà specificare lindirizzo IP dellhost, e il numero di porta. Sullhost remoto dovrà esserci un server che è in "ascolto" su tale porta. La classe Socket mette a disposizione due costruttori:
Socket( String host, int port ) throws IOException
Socket( InetAddress address, int port ) throws IOException
InetAddress è una classe di utilità per la gestione di indirizzi Internet (ad esempio ottenere lindirizzo IP numerico, dato un indirizzo sotto forma di stringa).
Una volta creato un oggetto Socket, si possono ottenere gli stream ad esso associati tramite i metodi della classe Socket:
InputOutputStream getInputStream() throws IOException
OutputOutputStream getOutputStream() throws IOException
A questo punto la comunicazione può avere inizio: il client può scrivere sulloutputstream, come si fa con un normale stream, e ricevere dati dal server leggendo dallinputsream.
Un semplice programma Client
Vediamo adesso un semplice programma Client: si tratta di un client HTTP che dato un URL richiede un file al server HTTP di quellhost.
/* HHTPClient * * Esempio di Client HTTP */ import java.net.*; import java.io.*; public class HTTPClient { public HTTPClient (String textURL) throws IOException { Socket socket = null; dissectURL (textURL); socket = connect (); try { getPage (); } finally { socket.close (); } } protected String host, file; protected int port; protected void dissectURL (String textURL) throws MalformedURLException { URL url = new URL (textURL); host = url.getHost (); port = url.getPort (); if (port == -1) port = 80; file = url.getFile (); } protected DataInputStream in; protected DataOutputStream out; protected Socket connect () throws IOException { System.err.println ("Connessione a " + host + ":" + port + "..."); Socket socket = new Socket (host, port); System.err.println ("Connessione avvenuta."); BufferedOutputStream buffOut = new BufferedOutputStream (socket.getOutputStream ()); out = new DataOutputStream (buffOut); in = new DataInputStream (socket.getInputStream ()); return socket; } protected void getPage () throws IOException { System.err.println ("Richiesta del file " + file + " inviata..."); out.writeBytes ("GET " + file + " HTTP/1.0\r\n\r\n"); out.flush (); System.err.println ("Ricezione dati..."); String input ; while ((input = in.readLine ()) != null) System.out.println (input); } public static void main (String args[]) throws IOException { if( args.length < 1 ) throw new IOException( "Sintassi : HTTPClient URL" ) ; try { new HTTPClient (args[0]); } catch (IOException ex) { ex.printStackTrace (); } System.out.println ("exit"); } }
Il funzionamento del programma è molto semplice: sulla riga di comando si passa un normale URL. Tale URL viene passato al costruttore della classe HTTPClient. Nel metodo dissectURL viene utilizzata una comoda classe messa a disposizione da Java, URL: passata al costruttore una stringa rappresentante lurl (ad esempio "http://www.myhost.com:80/mydir/myfile.html), è possibile ottenere lhost, la porta ed il nome del file dallurl, rispettivamente coi metodi: getHost, getPort, getFile. Come si vede se non viene specificata una porta si assume quella di default per i server web: la porta 80.
Nel metodo connect effettuiamo la connessione vera e propria aprendo una socket con lhost e sulla porta specificati:
Socket socket = new Socket (host, port);
Effettuata la connessione possiamo ottenere gli stream associati con i metodi della classe Socket getOutputStream e getInputStream. Creiamo poi un DataOutputStream ed un DataInputStream su tali stream ottenuti (effettivamente per ottimizzare la comunicazione in rete prima viene creato uno stream bufferizzato sullo stream di output, ma questi dettagli al momento non ci interessano).
A questo punto si deve richiedere il file al server web e quindi si spedisce tale richiesta tramite lo stream di output:
out.writeBytes ("GET " + file + " HTTP/1.0\r\n\r\n");
A questo punto non resta che mettersi in attesa, sullo stream di input, dellinvio dei dati da parte del server:
while ((input = in.readLine ()) != null)
Il contenuto del file viene stampato sullo schermo una riga alla volta.
Questo semplice programma illustra un esempio di client che invia al server una richiesta, e riceve dal server i dati richiesti. E questo che avviene quando tramite il nostro browser quando visitiamo una pagina web, anche se in modo senzaltro più complesso!
Lo "spirito" però rimane questo: il client apre una connessione col server, comunica al server quello che desidera tramite un protocollo di comunicazione (nellesempio lHTTP), ed attende la risposta del server (comunicata sempre tramite lo stesso protocollo). Il comando GET infatti fa parte del protocollo HTTP.
Per testare il programma non è necessaria una connessione ad Internet, basta avere un web server installato ed attivo e lanciare il programma in questo modo
java HTTPClient http://localhost/index.html
E sullo schermo verrà stampato il contenuto dellintero file index.html (se il file viene trovato, ovviamente, altrimenti si otterrà il tipico errore di file non trovato a cui ormai la navigazione web ci ha abituati).
Un semplice programma Server
Vediamo adesso un esempio di programma Server. Un server rimane in attesa di connessioni su una certa porta, ed ogni volta che un client si connette a tale porta, il server ottiene una socket, tramite la quale può comunicare col client. Il meccanismo messo a disposizione da Java per questo è la classe ServerSocket, tramite la quale il server può appunto accettare connessioni dai client attraverso la rete.I passi tipici di un server saranno quindi:
Infatti il metodo accept della classe ServerSocket crea un oggetto Socket per ogni connessione. Il server potrà poi comunicare, come fa un client: estraendo gli stream di input ed output dalla socket.
I costruttore della classe ServerSocket sono:
ServerSocket(int port) throws IOException
ServerSocket(int port, int count) throws IOException
A parte il parametro che specifica la porta (già spiegato), il parametro count permette di specificare il numero di richieste di connessioni messe in coda dal sistema operativo (il default per questo parametro è 50). Infatti dopo che il ServerSocket è stato creato il sistema operativo si mette subito in attesa di connessioni; queste richieste saranno messe in una coda e saranno rimosse una per volta, ad ogni chiamata del metodo
Socket accept() throws IOException
Quindi count non limita il numero di connessioni che un server è in grado di mantenere, ma il numero di richieste di connessioni che verranno messe in coda, se il server ci mette molto per accettare nuove connessioni. Infine se come numero di porta viene specificato 0 il sistema operativo seleziona un numero di porta valido e non occupato. E comunque possibile ottenere il numero di porta sul quale il ServerSocket è in ascolto di connessioni utilizzando il metodo
int getLocalPort()
Lultimo metodo della classe ServerSocket è
void close() throws IOException
Che chiude il ServerSocket, ma non le connessioni che sono stare stabilite (queste dovranno essere chiuse chiamando il metodo close della classe Socket). Tipicamente quando una socket viene chiusa da un lato, sullaltro lato si avrà una IOException.
Segue un semplice esempio di Server (non a caso chiamato SimpleServer)
/* SimpleServer.java */ import java.net.*; import java.io.*; public class SimpleServer { public static void main (String args[]) throws IOException { int port = 0 ; if (args.length == 1) port = Integer.parseInt( args[0] ) ; System.out.println ("Server in partenza sulla porta " + port); ServerSocket server = new ServerSocket (port); System.out.println ("Server partito sulla porta " + server.getLocalPort() ) ; System.out.println ("In attesa di connessioni..."); Socket client = server.accept (); System.out.println ("Richiesta di connessione da " + client.getInetAddress ()); /* il server viene chiuso, ma la connessione col client rimane attiva */ server.close (); try { InputStream i = client.getInputStream (); OutputStream o = client.getOutputStream (); PrintStream p = new PrintStream (o) ; p.println("BENVENUTI."); p.println("Questo e' il SimpleServer :-)") ; p.println () ; p.println("digitare HELP per la lista di servizi disponibili" ) ; int x; ByteArrayOutputStream command = new ByteArrayOutputStream() ; String HelpCommand = new String( "HELP" ) ; String QuitCommand = new String( "QUIT" ) ; while ((x = i.read ()) > -1) { o.write (x); if( x == 13 ) { /* newline */ p.println() ; if( HelpCommand.equalsIgnoreCase( command.toString() ) ) { p.println( "Il solo servizio disponibile e' l'help," ) ; p.println( "e QUIT per uscire." ) ; p.println( "Altrimenti che SimpleServer sarebbe ;-)" ) ; } else if ( QuitCommand.equalsIgnoreCase( command.toString() ) ) { p.println( "Grazie per aver usato SimpleServer ;-)" ) ; p.println( "Alla prossima. BYE" ) ; try { Thread.sleep( 1000 ) ; } finally { break ; } } else { p.println( "Comando non disponibile |-(" ) ; p.println( "Digitare HELP per la lista dei servizi" ) ; } command.reset() ; } else if ( x != 10 ) /* carriage return */ command.write(x) ; } } finally { System.out.println ("Connessione chiusa"); client.close (); } } }
Lesempio mostra lutilizzo di tutti i metodi suddetti della classe ServerSocket. In particolare accetta da linea di comando un parametro che specifica la porta su cui mettersi in ascolto di richieste di connessioni. Se non viene passato nessun argomento si userà la porta scelta dal Sistema Operativo. Dopo la creazione delloggetto ServerSocket ci si mette in ascolto di connessioni, e appena se ne riceve una, si chiude il ServerSocket (cioè si accetta una sola connessione). Tramite loggetto Socket restituito dal metodo accept si ottengono i due stream per comunicare col client. Si attende poi che il client invii dei comandi: ogni volta che viene letto un carattere, questo viene rispedito al client, in modo che questultimo possa vedere quello che sta inviando. Appena viene digitato un newline (cioè un INVIO o ENTER) si controlla se il servizio richiesto (memorizzato via via in un buffer) è disponibile, e si risponde in modo opportuno. Si noti come tutte le comunicazioni fra il server ed il client siano racchiuse in un blocco try...finally: se nel frattempo avviene uneccezione si è comunque sicuri che la connessione verrà chiusa (cioè non rimane pendente). Leccezione in questione è tipicamente una IOException dovuta alla disconnessione da parte del client.
A proposito di client: il client dovè in questo esempio? Come nellaltro esempio avevamo usato un server già esistente (web server) per testare il nostro client, questa volta per testare il nostro server utilizzeremo un client classico: il telnet.
Quindi se si è lanciato il server con la seguente riga di comando
java SimpleServer 9999
Basterà utilizzare da unaltro terminale (ad esempio unaltra shell del DOS in Windows 95, o un altro xterm sotto Linux) il seguente comando
telnet localhost 9999
Nella seguente figura si può vedere una sessione di esempio del programma.
Come detto il server dellesempio precedente chiude il ServerSocket, appena ha ricevuto una richiesta di connessione, quindi tale server funziona una sola volta! Un server che "si rispetti", invece deve essere in grado di accettare più connessioni, ed inoltre dovrebbe essere in grado di soddisfare più richieste contemporaneamente. Per risolvere questo problema si deve ricorrere al multithreading, per il quale Java offre diversi strumenti. Il programma sarà così modificato:
In effetti è questo quello che avviene nei server di cui si è già parlato. Se si prende un programma scritto in C che utilizza le socket si potrà vedere che appena viene ricevuta una richiesta di connessione, il programma si duplica (esegue una fork()), ed il programma figlio lancia un programma che si occuperà di gestire la connessione appena ricevuta. Nel nostro caso basterà creare un nuovo thread e passargli la socket della nuova connessione.
Segue il programma modificato per trattare più connessioni contemporaneamente:
/* SimpleMTServer.java */ import java.net.*; import java.io.*; public class SimpleMTServer extends Thread { protected Socket client ; public SimpleMTServer( Socket socket ) { System.out.println( "Arrivato un nuovo client da " + socket.getInetAddress ()) ; client = socket ; } public void run() { try { InputStream i = client.getInputStream (); OutputStream o = client.getOutputStream (); . . . <come sopra> . . . } } catch (IOException e) { e.printStackTrace() ; } finally { System.out.println ("Connessione chiusa con " + client.getInetAddress ()); try { client.close (); } catch (IOException e) { e.printStackTrace() ; } } } public static void main (String args[]) throws IOException { int port = 0 ; Socket client ; if (args.length == 1) port = Integer.parseInt( args[0] ) ; System.out.println ("Server in partenza sulla porta " + port); ServerSocket server = new ServerSocket (port); System.out.println ("Server partito sulla porta " + server.getLocalPort() ) ; while( true ) { System.out.println ("In attesa di connessioni..."); client = server.accept() ; System.out.println ("Richiesta di connessione da " + client.getInetAddress ()); (new SimpleMTServer( client )).start() ; } } }
Come si vede si sono dovuti fare alcuni cambiamenti:
Notare che poiché il metodo run della classe Thread, che viene ridefinito dalla nostra classe, non lancia nessuna eccezione, dobbiamo intercettare tutte le eccezione allinterno del metodo: in questo caso leccezione in questione è IOException che può essere lanciata anche quando si cerca di chiudere la comunicazione.
Un esempio un po più pratico
Veniamo adesso ad un esempio un po più interessante, senzaltro più utile: un sistema per "chattare". Si vuole costruire sia il Server Chat, che il Client Chat. In questo caso si capisce chiaramente che il server dovrà essere multithreaded (chattare da soli non ha molto senso, no?). Per motivi di spazio non sono stati inseriti i listati di quest'ultimo esempio direttamente nell'articolo, ma sono comunque scaricabili. Il server (classe ChatServer) segue lo schema dellesempio precedente, e anche il client (ChatClient) non si discosta molto dal primo esempio di client visto allinizio. Nella classe del server è presente un vettore condiviso da tutti i thread (infatti si tratta di un membro statico) in cui vengono registrati tutti i thread attivati per gestire le connessioni. Ovviamente tutte le modifiche che si faranno a tale vettore dovranno avvenire in modo sincronizzato (tramite un blocco synchronized). In questo modo quando arriva un messaggio dal client, il thread che se ne occupa ha la possibilità, scorrendo tale vettore, di diffondere il messaggio a tutti gli altri thread, che a questo punto spediranno il messaggio ai client che gestiscono (non ha caso il metodo che si occupa di questo si chiama broadcast).
In questo esempio viene fatto uso della serializzazione ([3]), cioè vengono creati degli ObjectInputStream e ObjectOutputStream sui rispettivi stream delle socket, ovviamente sia dal client che dal server (altrimenti la comunicazione non potrebbe essere effettuata).
Lesempio è molto semplice, però è funzionante: basta lanciare il server, specificando la porta su cui si metterà in ascolto:
java ChatServer 9999
e poi ci si può collegare a tale server col ChatClient da qualsiasi macchina
java ChatClient localhost 9999 (in locale)
java ChatClient <nome host> 9999 (ad un server remoto)
Comparirà una finestra con unarea di testo, dove verranno visualizzati via via i messaggi, ed una casella di input dove è possibile scrivere i propri messaggi. Nel momento in cui si preme invio, il messaggio viene spedito (anche il mittente riceve il messaggio che ha scritto). Non sono presenti, sia nel server, che nel client, tutti i servizi messi a disposizione dai client e server chat professionali, ma è anche vero che queste classi sono solo uno scheletro, che può essere personalizzato a piacere: basterà derivare dalle due suddette classi e anche da ChatFrame, il frame che si occupa della visualizzazione.
Nella seguente figura è visualizzato un possibile output dei due programmi (eseguiti in locale)
Conclusioni
Questa volta abbiamo iniziato a vedere alcune semplici implementazione dei paradigmi presentati in [1]. Come si può vedere i programmi scritti sono molto piccoli e compatti; è vero che non fanno molto, però è anche vero che scritti in un altro linguaggio (vedi C) sarebbero stati molto più lunghi e complessi. Senza contare che per utilizzare il multithreading e la sincronizzazione si sarebbe dovuti ricorrere a chiamate di libreria (che il più delle volte sono fortemente dipendenti dal sistema operativo), mentre qui le abbiamo a disposizione a livello di linguaggio.
La prossima volta continueremo a vedere alcune implementazioni degli altri paradigmi.
Stay tuned ;-)
HTTPClient.java
SimpleServer.java
SimpleMTServer.java
ChatClient.java
ChatServer.java
[1]
L.Bettini "Lunione fa la forza - La programmazione
distribuita in Java" MokaByte - Novembre 97
[2] L.Bertoncello "Introduzione alla programmazione in rete
- Sockets" MokaByte - Ottobre 96
[3] M.Carli "La Serializzazione" MokaByte - Aprile
97