giovedì 7 ottobre 2010

Applicazioni client/server con Java

La comunicazione e lo scambio dell'informazione sono da sempre alla base di Internet. Anche Java, nato inizialmente con lo stesso obiettivo, supporta la programmazione di rete. Il package java.net raccoglie le interfacce, le classi e le possibili eccezioni utili proprio alla programmazione per questo tipo di applicazioni, spesse volte dette anche applicazioni client/server.

Queste applicazioni osservano un noto modello di programmazione che è appunto il modello client/server. Durante lo scambio dell'informazione il suddetto modello assegna ai partecipanti dei ruoli ben precisi: il client è l'applicazione che, interfacciandosi attraverso la rete al server, richiede un preciso servizio; il server è invece quell'applicazione che soddisfa, attraverso la rete, la richiesta del client.

La comunicazione fra client e server si svolge inevitabilmente attraverso la rete, grazie ai protocolli di rete. A livello di trasporto essa può allora avvenire attraverso due dei principali protocolli di trasporto: TCP o UDP. Il protocollo di rete TCP realizza un canale logico per la connessione affidabile e orientato alla connessione, si impegna a fare del suo meglio per garantire l'arrivo dei pacchetti e l'ordine con cui essi vengono spediti. Il protocollo di rete UDP è assai più sbrigativo, non prevede una connessione e non garantisce ne l'arrivo ne l'ordine dei pacchetti. UDP viene usato, ad esempio, per lo streaming dei flusi audio e/o video. La scelta del protocollo per il trasporto può quindi variare in base al tipo di applicazione che abbiamo in mente.

Tale scelta va fatta ancora prima di mettersi a scrivere il codice dell'applicazione. Java implementa nel package jav.net diverse classi, alcune di queste sono orientate al protocollo TCP, altre invece seguono il protocollo UDP. La comunicazione fra client e server avviene attraverso le socket (presa o spina). Ogni applicazione si può dotare di una socket, la finestra sulla rete.

La classe Socket implementa uno dei due lati della connessione di rete (end-point) ed è orientata al protocollo TCP, tipicamente quella di un'applicazione client. La classe ServerSocket, invece, realizza la socket per l'applicazione server. Tale socket viene usata dai server per rimanere in ascolto e accettare le richieste di connessione dei client. La classe DatagramSocket realizza l'astrazione necessaria all'invio e alla ricezione dei dati attraverso il protocollo UDP.

In questo articolo mi occuperò esclusivamente delle classi Socket e ServerSocket. E' evidente che l'applicazione server deve trovarsi già in esecuzione prima di ricevere richieste dai client che altrimenti si perdono nella rete. Osserveremo allora prima la struttura di un'applicazione server. L'istanza di una socket per il server avviene attraverso il costruttore della classe ServerSocket:

public ServerSocket(int port) throws IOException

dove port rappresenta il numero di porta usato dal server, presso il proprio indirizzo di rete, per l'ascolto. Alcuni numeri di porta sono assegnati di default e spettano a vari servizi di rete (sono cioè usati da altri protocolli per applicazioni). Se l'istanza di una ServerSocket non riesce vi consiglio di cambiare il numero della porta usato nel costruttore, potrebbe infatti essere già impegnato! Il costruttore va poi eseguito all'interno di un blocco try/catch per intercettare proprio l'errore che vi descrivevo poco fa. Esistono altri tipi di cotruttori, date come sempre uno sguardo alla documentazione della classe:

server=new ServerSocket(10000);

Se l'istanza della socket riesce possiamo allora collocare la stessa in uno stato di attesa. Il metodo accept() blocca l'esecuzione del programma e lo pone in attesa di richieste di connessione. Il metodo si sblocca alla prima richiesta del client, ritornando l'oggetto socket usato dallo stesso! Da questo oggetto il server, se vuole, può estrarre l'indirizzo IP con il metodo getInetAddress():

client=server.accept();

Affinchè il server possa realmente mandare al client dei byte (testo o informazioni varie) deve necessariamente estrarre dalla socket del client gli stream per l'input:

input=new BufferedReader(new InputStreamReader(client.getInputStream()));
e per l'output:
output=new PrintWriter(client.getOutputStream(),true);
I metodi getInputStream() e getOutputStream() ritornano rispettivamente InputStream e OutputStream. La comunicazione adesso è davvero semplice, se il server intende leggere cosa ha scritto il client non deve fare altro che usare lo stream input. Nell'esempio ho appositamente convogliato lo stream di input in un oggetto BufferedReader che semplifica la lettura con il metodo readLine() (che ritorna una stringa). Per leggere, ad esempio, si potrebbe avere questa istruzione:
String clientMessage=input.readLine();
Se il server intende scrivere qualcosa al client userà allora lo stream di output:
output.println("Ciao!");
Prestate attenzione alla chiusura dell'applicazione e liberate sempre le risorse impegnate. Chiudete prima gli stream di input e di output, quindi la socket del client e infine quella del server:
input.close();
output.close();
client.close();
server.close();
L'applicazione client opera allo stesso modo di quella server, la prima operazione da fare è la connessione al server (che sarà in attesa, bloccato sul metodo accept()). Il client può allora usare la classe Socket in questo modo:
client=new Socket(ip,10000);
dove ip è la stringa con l'indirizzo ip del server e 10000 il numero di porta del servizio. Se la connessione riesce senza eccezzioni (anche questa operazione, così come le altre, va sempre innescata all'interno di un blocco try/catch) il client può allora ricavare dalla socket (ora connessa al client) gli stream per l'input e per l'output. Per l'input, come visto prima, ad esempio:
input=new BufferedReader(new InputStreamReader(client.getInputStream()));
per l'output, analogamente:
output=new PrintWriter(client.getOutputStream(),true);
Così come fa il server, anche il client può usare gli strem per scrivere al server e per leggerne i messaggi inviati dallo stesso, il protocollo TCP realizza infatti un canale bidirezionale. Le applicazioni client e server dovranno in ogni modo accordarsi su quando e come scriversi, dovranno cioè osservare un protocollo per lo scambio dei messaggi (un compito è lasciato alla fantasia del programmatore). Non dimenticate di chiudere nell'ordine esatto le risorse impegnate anche nell'applicazione client.
Qui potete scaricare l'archivio jar di un'applicazione server, a questo indirizzo invece trovate il client. Lanciate prima il server (con il comando "java -jar ServerSocket.jar", nella cartella che ospita il file scaricato) e successivamente il client (con "java -jar ClientSocket.jar", sempre nella cartella che ospita il file scaricato) se volete utilizzare l'applicazione nella vostra rete locale (con indirizzo ip di loopback 127.0.0.1). Altrimenti eseguite le due applicazioni su due computer con indirizzo ip differente. L'esecuzione del client inizia proprio con la richiesta dell'indirizzo ip del server (la porta usata è di default la 10000), se non conoscete il vostro indirizzo ip potete ricavarlo sfruttandore ad esempio i tanti servizi di rete, ad esempio qui. Qui potete vedere la parte server in esecuzione (su un computer con sistema operativo Linux, il mio Aspire One!):

Qui invece la parte client (in esecuzione su un PC con sistema operativo Windows XP):

L'applicazione permette lo scambio di messaggi fra client e server, dopo aver effettuato la connessione al server il client inizia a scrivere al server (che si mette quindi in attesa di un messaggio). Subito dopo è il client che si mette in attesa di un messaggio di risposta da parte del server. La comunicazione si svolge quindi su turni. Va inoltre osservata la capacità del server di rispondere a una sola richiesta di connessionne del client. Per implementare un server che accetta più connessioni quest'ultimo deve necessariamente usare una socket per l'ascolto e generare ad ogni connessione una socket per il client.

Nessun commento:

Posta un commento