Un po' di teoria Ricordiamo che un'applicazione distribuita è un'applicazione composta da più programmi (almeno 2) posti in esecuzione su macchine diverse all'interno di una rete di calcolatori e che utilizzano un protocollo di comunicazione comune. I pc connessi a Internet comunicano tra loro usando diversi protocolli tra cui il più famoso è forse il TCP/IP (Transmission Control Protocol/Internet Protocol) ben supportato da Java. Un altro protocollo è l'udp (User Datagram Protocol). Il protocollo TCP/IP viene definito protocollo connection-oriented, orientato alla connessione, cioè garantisce che i pacchetti inviati arrivino a destinazione (se vi è un qualche errore il destinatario chiede la ritrasmissione del pacchetto), mentre l'udp non garantisce l'arrivo di tutti i pacchetti e viene definito connectionless. E' preferito in genere in tutti quei casi in cui perdite contenute di informazione durante la trasmissione sono trascurabili (come nelle trasmissioni audio-video) e quello che conta invece è la velocità di trasmissione. Java utilizza per la comunicazione in rete il paradigma di programmazione basato sul concetto di socket, progettato presso l'università di Berkley (California). Il socket può essere definito come un punto terminale o di aggancio di una linea di comunicazione da cui un programma può inviare i dati in rete e può ricevere i dati dalla rete. Fig. 1 Definizione di socket Il socket (la traduzione è presa di corrente) è un'astrazione software che è in qualche modo l'equivalente della presa elettrica per gli apparecchi elettrici. La realizzazione di applicazioni client/server in Java si basa sulle classi contenute nel package java.net e in particolare sulle classi InetAddress, ServerSocket e Socket. Ricordiamo brevemente che un programma è detto client se richiede un servizio a un programma detto server che fornisce il servizio richiesto e poi si rimette in ascolto in attesa di ulteriori richieste da parte di altri client. Può accadere frequentemente che su un server vi siano in esecuzione servizi diversi per cui, per distinguerli, si usa un numero identificativo del servizio, la porta appunto. Per esempio su una macchina server, potrebbero girare contemporaneamente un server web, un server Tomcat (un server web basato su Java) e un server MySQL. Le porte impiegate sono la porta 80 per il server web, la porta 8080 per il server Tomcat e la porta 3306 per il database server MySQL. Le porte dalla 0 alla 1023 sono riservate ai principali servizi di rete conosciuti e sono dette anche well known ports. Inoltre un server singlethread (o server unicast) è un server che comunica con un solo client alla volta, mentre se il server è multithread (o server multicast) al server possono connettersi contemporaneamente più client (per ogni client, il server attiva un thread distinto). In quest'ultimo caso il server sposta la richiesta dalla porta sulla quale è stata effettuata la connessione su una nuova porta. Altra considerazione importante è che, una volta stabilita la connessione, la comunicazione tra client e server è basata sulla comunicazione tra flussi (stream), cioè l'invio e la ricezione dei dati sono gestiti in maniera simile alla gestione dei file. L'applicazione L'applicazione che presentiamo consiste di 3 classi: ClientMax, che rappresenta il client, ServerMax, che rappresenta il server e ThreadMax che rappresenta il thread del servizio richiesto dal client (in questo caso, il servizio richiesto è banalmente il massimo tra due numeri interi) e ingloba la logica dell'applicativo. Il funzionamento è il seguente: ServerMax consiste di un ciclo infinito di attesa di una qualche connessione sulla porta 10000 (un valore scelto a caso, qualsiasi altro valore maggiore di 1024 sarebbe andato altrettanto bene) mediante un oggetto di classe ServerSocket. Una volta stabilita la connessione col client, rappresentata da un oggetto di classe Socket, istanzia un thread di servizio per quel client. Poi torna a mettersi in ascolto sulla porta 10000. Il client ClientMax crea una connessione col server all'indirizzo IP e sulla porta indicati applicazioni_distribuite.pdf Pag. 1/8 Cozzetto
nell'oggetto di classe Socket. Poi genera due valori interi casuali e li invia, mediante lo stream di output associato al socket, al server. Il server provvede ad elaborare i dati e li rinvia al client che li riceve sullo stream di input. Il thread ThreadMax provvede a ricevere sullo stream di input associato i due valori, ne calcola il risultato e provvede a inviarlo al client attraverso lo stream di output. Tutto questo in maniera concorrente con altri thread. Fig. 2 Diagramma UML delle componenti software dell'applicativo Il codice L'applicativo è stato sviluppato con Eclipse (Ganymede), un potente IDE altrettanto utilizzato in ambiente professionale al pari di NetBeans, liberamente scaricabile da http://www.eclipse.org/downloads/. Consigliamo questa versione di Eclipse in quanto consente anche di progettare applicativi web, quindi pagine jsp, jsf ecc e in generale applicativi per la grande impresa. /* * ServerMax.java * Created on 20 marzo 2007, 9.10 package clientserverpkg; import java.io.*; import java.net.*; /** * @author maurizio public class ServerMax { final static int port = 10000; static ServerSocket serversocket = null; static Socket clientsocket = null; static String threadname = ""; public ServerMax() { applicazioni_distribuite.pdf Pag. 2/8 Cozzetto
public static void main(string[] args) { Thread.currentThread().setName("ServerMax"); threadname = Thread.currentThread().getName()+" ==> "; try { System.out.println(threadName+" Tento di creare l'oggetto di classe ServerSocket "); serversocket = new ServerSocket(port); System.out.println(threadName+" Oggetto di classe ServerSocket creato"); while (true) { // ciclo infinito di attesa delle connessioni dei client System.out.println(threadName+" Server attivo e in ascolto sulla porta "+port+"..."); // il server si sospende e rimane in attesa della connessione clientsocket = serversocket.accept(); String addr = clientsocket.getinetaddress().tostring(); int clientport = clientsocket.getport(); int localport = clientsocket.getlocalport(); System.out.println(threadName+" Ricevuto nuovo client con indirizzo: "+addr); System.out.println(threadName+" Porta del client: "+clientport); System.out.println(threadName+" Porta locale: "+localport); // Le tre istruzioni precedenti possono essere sintetizzate scrivendo: //System.out.println("Ricevuto nuovo client "+clientsocket+" "+threadname); ThreadMax max = new ThreadMax(clientSocket); System.out.println(threadName+"Creato nuovo thread max per servire il client"); max.start(); System.out.println(threadName+"Thread max avviato per il nuovo client"); // fine ciclo infinito catch (IOException e) { System.out.println(threadName+" Errore "+e.getmessage()); // fine try-catch // fine metodo main // fine classe ServerMax applicazioni_distribuite.pdf Pag. 3/8 Cozzetto
/* * ClientMax.java * * Created on 21 marzo 2007, 8.31 package clientserverpkg; import java.net.*; import java.io.*; /** * @author maurizio public class ClientMax { static Socket socket = null; static final int port = 10000; static BufferedReader in = null; static PrintWriter out = null; static String threadname = ""; public ClientMax() { public static void main(string[] args) { Thread.currentThread().setName("ClientMax"); threadname = Thread.currentThread().getName()+" ==> "; try{ socket = new Socket("localhost",port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(),true); // true abilita il flush System.out.println(threadName+"Genero n1"); int n1 = (int)(math.random()*(99)); System.out.println(threadName+"n1="+n1); System.out.println(threadName+"Genero n2 "); int n2 = (int)(math.random()*(99)); System.out.println(threadName+"n2="+n2); // il client invia al server i due valori out.println(n1); out.println(n2); // il server elabora il calcolo (in questo caso il massimo tra due numeri) // creando un thread apposito // il client riceve il risultato del calcolo String maxstr = in.readline(); System.out.println(threadName+"Il massimo è: "+maxstr); catch(exception e){ System.out.println(threadName+"Errore: "+e.getmessage()); // fine blocco try-catch // fine metodo main // fine classe ClientMax applicazioni_distribuite.pdf Pag. 4/8 Cozzetto
/* * ThreadMax.java * * Created on 20 marzo 2007, 9.34 * package clientserverpkg; import java.io.*; import java.net.*; /** * * @author maurizio public class ThreadMax extends Thread { Socket clientsocket = null; BufferedReader in = null; PrintWriter out = null; String threadname = ""; /** Creates a new instance of Max public ThreadMax(Socket clientsocket) { Thread.currentThread().setName("Max"); threadname = Thread.currentThread().getName()+" ==> "; this.clientsocket = clientsocket; try { in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); out = new PrintWriter(clientSocket.getOutputStream(),true); catch (IOException e) { System.out.println(threadName+"Errore: "+e.getmessage()); // fine costruttore @Override public void run() { System.out.println(threadName+"Thread Max partito. Calcolo del massimo tra due numeri"); try{ int n1= Integer.parseInt(in.readLine()); System.out.println(threadName+"Ho letto n1="+n1); int n2= Integer.parseInt(in.readLine()); System.out.println(threadName+"Ho letto n2="+n2); System.out.println(threadName+"Calcolo il max "); // qui si concentra la logica int max = Math.max(n1,n2); System.out.println(threadName+"Il max vale "+max); out.println(max); out.flush(); applicazioni_distribuite.pdf Pag. 5/8 Cozzetto
catch(exception e){ System.out.println(threadName+"Eccezione: "+e.getmessage()); try{ System.out.println(threadName+"Chiudo la connessione col client"); clientsocket.close(); catch(ioexception e){ System.out.println(threadName+"Errore: "+e.getmessage()); // fine metodo run // fine classe ThreadMax La prova L'applicativo è stato sviluppato e testato dapprima in locale. Successivamente, chiedendo ad un amico (Francesco Sarasini, ex studente dell'itis), è stato testato via Internet. Per poter effettuare la prova, ho aperto sul router la porta 10000 associandola all'indirizzo privato 192.168.1.100 del pc (la mia lan domestica è costituita da due pc, un vecchio ma ancora glorioso imac e un portatile HP). Fig. 3 Associazione della porta 10000 all'indirizzo 192.168.1.100 (redirect) Successivamente ho lanciato il componente ServerMax, mentre Francesco ha leggermente modificato il codice della classe ClientMax inserendo al posto di localhost dell'istruzione applicazioni_distribuite.pdf Pag. 6/8 Cozzetto
l'indirizzo IP del mio computer 151.47.93.38. socket = new Socket("localhost",port); Fig. 4 Il client di Francesco Poi ha lanciato il ClientMax. Vediamo cosa è apparso invece sulla mia macchina. applicazioni_distribuite.pdf Pag. 7/8 Cozzetto
Fig. 5 Ecco la risposta del mio server Notiamo che l'applicativo riporta diverse informazioni interessanti: l'indirizzo IP del client (87.8.24.51), la nuova porta aperta (50929) e la porta locale 10000 del mio server. Ovviamente occorrerebbe testare l'applicativo con più utenti che fanno contemporaneamente più richieste al server e osservarne il comportamento. applicazioni_distribuite.pdf Pag. 8/8 Cozzetto