MinMax – a Java Distributed Application SISTEMI DISTRIBUITI Fabrizio G. Ventola CdL Magistrale Informatica 2012/2013 Introduzione Uno degli obiettivi dell’informatica moderna è quello di offrire servizi sempre più completi mediante la risoluzione di grandi istanze di problemi complessi nel minor tempo possibile. Per far fronte a questo problema si necessita di grandi risorse di calcolo che difficilmente possono risiedere su un unico elaboratore, di conseguenza si ricorre alla computazione distribuita mediante l’uso di sistemi distribuiti ove un algoritmo può essere eseguito e scomposto in più processi comunicanti dislocati su diverse macchine di calcolo. La condivisione di risorse dislocate su più elaboratori ha posto nuove sfide riguardo la comunicazione tra processi, la sincronizzazione tra loro e la sicurezza in questi sistemi. I moderni linguaggi di programmazione e relative piattaforme di sviluppo offrono oramai diversi strumenti che rendono più trasparente allo sviluppatore la gestione delle risorse distribuite facilitandogli in maniera consistente il compito. Originariamente i linguaggi di programmazione offrivano esclusivamente le socket per dare la possibilità allo sviluppatore di realizzare applicazioni di rete senza definire alcun protocollo di supporto per la comunicazione tra processi dislocati su vari elaboratori. In seguito sono state ideate le RPC – remote procedure call che forniscono un ulteriore livello di astrazione dando allo sviluppatore la capacità di chiamare delle procedure di calcolo dislocate quasi come se fossero presenti sull’elaboratore locale. Successivamente con l’ascesa del paradigma object-oriented si è sentito il bisogno di implementare qualcosa di simile al meccanismo RPC, adatto alla programmazione di tipo procedurale, portando la stessa idea su sistemi object-based. Da questo principio è nato il meccanismo RMI – remote method invocation che consente allo sviluppatore di invocare metodi di oggetti remoti, ossia dislocati fisicamente su diversi calcolatori, come se fossero metodi di oggetti presenti sulla macchina locale nello stesso address space del processo client. In questo elaborato si presenta lo sviluppo di una semplice applicazione distribuita object-based realizzata in Java 6.0 mediante l’utilizzo del meccanismo Java RMI messo a disposizione dal linguaggio stesso. L’applicazione consente l’invio da parte di un client di una lista di valori reali ad un oggetto remoto posto sul server per poi dare la possibilità all’utente di calcolare il valore minimo, massimo e l’ordinamento crescente della lista dei valori. Le computazioni sono eseguite sul server mediante l’invocazione di metodi remoti da parte del client per mettere in evidenza le potenzialità del meccanismo RMI messo a disposizione dal linguaggio di programmazione Java 6.0. Java RMI Il linguaggio di programmazione Java è un linguaggio object-oriented pseudo-compilato ovvero all’atto della compilazione del codice Java viene generato un linguaggio intermedio bytecode che sarà poi interpretato ed eseguito da una macchina virtuale JVM - Java Virtual Machine. Questo consente di sviluppare applicazioni platform-indipendent, portabili e di eseguire lo stesso codice su architetture diverse con l’unico requisito necessario di avere in esecuzione una macchina virtuale JVM. Un altro aspetto importante del linguaggio Java è dato dalla possibilità di realizzare le interfacce di classi di oggetti. L’interfaccia permette la netta separazione tra l’implementazione di una classe e le operazioni possibili sugli oggetti istanze della classe. Questo oltre a determinare i diversi vantaggi dell’information hiding e dell’incapsulamento consente di dislocare le interfacce e le implementazioni delle classi su diversi calcolatori venendo così incontro all’esigenza degli sviluppatori di sistemi distribuiti di avere le istanze della classe che implementa l’interfaccia sull’object server e l’interfaccia, che definisce le operazioni possibili sugli oggetti di quella classe, sull’object client. Di conseguenza il client mediante il meccanismo di RMI invoca i metodi degli oggetti a disposizione sull’object server e riceve i risultati. Come si è accennato le applicazioni distribuite realizzate mediante Java RMI presentano almeno due entità principali: l’object client e l’object server. L’object server si preoccupa di ospitare gli oggetti e metterli a disposizione del client mentre l’object client ha l’onere di localizzare gli oggetti remoti, instaurare una connessione con l’object server e invocare i metodi degli oggetti remoti ospitati dall’object server. In Java per realizzare una classe che dia la possibilità di istanziare oggetti remoti bisogna estendere l’interfaccia java.rmi.Remote e bisogna dotare ogni metodo dell’interfaccia della clausola di lancio dell’eccezione java.rmi.RemoteException importando i relativi Java package. L’architettura di un’applicazione distribuita realizzata mediante Java RMI presenta una struttura a pila formata da tre livelli principali (Figura 1): • Stub (client) / Skeleton (server) • Remote Reference Layer • Transport Layer Al livello più alto si trova lo stub presente sul client che ha il compito di svolgere da rappresentativa dell’oggetto remoto che vive sul server, mentre lo skeleton ha la funzione di trasformare la richiesta remota pervenuta dal client in una richiesta locale all’oggetto presente sul server. Date le funzioni di intermediazione tra le richieste locali e le richieste remote svolte dallo stub e dallo skeleton solitamente in letteratura sono denominati anche come proxy. Al livello successivo si pone il Remote Reference Layer che ha principalmente il compito di effettuare il marshalling e l’unmarshalling dei parametri. Per marshalling s’intende la codifica dei parametri dei metodi o dei risultati in un formato che consente l’invio tramite rete degli stessi (o il salvataggio su file) e l’unmarshalling, come suggerisce il nome, prevede la decodifica di suddetti parametri dei metodi o dei risultati. Dato che i parametri in Java possono essere o tipi primitivi o oggetti istanze di classi è necessario che tutti i tipi dei parametri implementino l’interfaccia java.io.Serializable che prevede dei metodi standard per la serializzazione degli oggetti. In Java i tipi primitivi e le classi fornite dalla libreria standard implementano tale interfaccia (escluso alcune classi platform-dependent). Nel livello più basso invece si posizione il Transport Layer che ha l’onere di instaurare e gestire una connessione “fisica” di rete tra il client e il server. Il sottosistema Java RMI rende trasparente allo sviluppatore la connessione stratificata tra client e server consentendogli di gestire in maniera più agile lo sviluppo dell’applicazione a oggetti distribuita fornendo implicitamente e in maniera trasparente meccanismi di streaming di oggetti e di set-up della connessione Client-Server. Figura 1: Architettura Java RMI MinMax - un’applicazione distribuita Per sviluppare un’applicazione distribuita mediante Java RMI occorre procedere per fasi per realizzare le varie componenti dell’applicazione. Innanzitutto è necessario realizzare l’interfaccia remota che definisce i metodi che il client potrà invocare sugli oggetti remoti, indicando il tipo dei parametri di input e il tipo dei dati dei restituiti dai metodi invocati. Successivamente è necessario implementare le classi che a loro volto implementano una o più interfacce remote. Le istanze di questi oggetti saranno ospitate sull’object server pronti per essere invocati dallo skeleton. In seguito bisogna realizzare la classe client e la classe server, compilarle mediante il compilatore Java, renderle disponibili in rete mediante il servizio RMI Registry, avviare il server ed infine eseguire il client. Componenti MinMax, come accennato precedentemente, è una semplice applicazione distribuita che fornito in input una lista di valori reali effettua il calcolo in remoto del valore minimo, del valore massimo e dell’ordinamento crescente della lista dei valori. L’applicazione è realizzata mediante quattro componenti (classi Java): • MinMax • MinMaxImplementation • Client • Server MinMax è l’interfaccia della relativa implementazione MinMaxImplementation e definisce i metodi pubblici invocabili dal client, mentre Client e Server sono le classi che, come suggerisce il nome, implementano le funzioni di client e server dell’applicazione distribuita. Di seguito è fornito il codice sorgente Java delle quattro componenti. Classe Server.java /** * @author Fabrizio Ventola */ import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Server { private void startServer(String host, int port){ try { System.setProperty("java.rmi.server.hostname", host); // create on selected port Registry registry = LocateRegistry.createRegistry(port); // create the service called minmax registry.rebind("minmax", new MinMaxImplementation()); } catch (Exception e) { System.out.println("Error raised: " + e.getMessage()); } System.out.println("RMI MinMax server is ready now!"); } public static void main(String[] args) { Server server = new Server(); // start the server server.startServer(args[0], Integer.parseInt(args[1])); } } Classe Client.java /** * @author Fabrizio Ventola */ import import import import import import java.io.BufferedReader; java.io.IOException; java.io.InputStreamReader; java.rmi.registry.LocateRegistry; java.rmi.registry.Registry; java.util.ArrayList; public class Client { private void startClient(String host, int port){ try { // connect to selected host and port Registry myRegistry = LocateRegistry.getRegistry(host, port); // look for the service MinMax minmaxImplementation = (MinMax) myRegistry.lookup("minmax"); boolean terminate = false; // ask info to the remote object System.out.println(minmaxImplementation.getInfo()); // show menu while(!terminate) { System.out.println("Select one option:"); System.out.println("1: Insert values"); System.out.println("2: Find min value"); System.out.println("3: Find max value"); System.out.println("4: Sort values"); System.out.println("5: Print values"); System.out.println("6: Terminate and quit the client"); System.out.print("Insert your command: "); // read input from the user InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader (reader); String comandInput; int command = 0; try { comandInput = input.readLine(); command = Integer.parseInt(comandInput); } catch(IOException e){ System.out.println ("Something wrong: " + e.getMessage()); } switch(command){ // insert values case 1:{ boolean endInsert = false; ArrayList<Double> list = new ArrayList(); while (!endInsert) { System.out.print("Insert a value, put T to finish: "); String value; try { value = input.readLine(); if ("T".equals(value) || "t".equals(value)) endInsert = true; else list.add(Double.parseDouble(value)); } catch (IOException e) { System.out.println ("Something wrong: " + e.getMessage()); } } } minmaxImplementation.input(list); System.out.println(); break; // compute min value case 2: { System.out.println("Min value is: " + minmaxImplementation.min()); System.out.println(); break; } // compute max value case 3: { System.out.println("Max value is: " + minmaxImplementation.max()); System.out.println(); break; } // compute sorted list case 4: { ArrayList<Double> sortedList = minmaxImplementation.sort(); System.out.println("Sorted list is:"); for (Double element : sortedList){ System.out.print(" " + element); } System.out.println(); System.out.println(); break; } // request values list case 5: { ArrayList<Double> listSent = minmaxImplementation.getList(); System.out.println("Inserted values are:"); for (Double element: listSent){ System.out.print(" " + element); } System.out.println(); System.out.println(); break; } // exit case 6: { System.out.println("Quitting..."); terminate = true; break; } } } } catch (Exception e) { System.out.println ("Something wrong: " + e.getMessage()); } } public static void main(String[] args) { Client client = new Client(); // start the client client.startClient(args[0], Integer.parseInt(args[1])); } } Classe MinMax.java /** * @author Fabrizio Ventola */ import java.rmi.Remote; import java.rmi.RemoteException; import java.util.ArrayList; // interface defines the public methods that client can invoke public interface MinMax extends Remote { // information about the distributed application String getInfo() throws RemoteException; // gives the values list as input void input(ArrayList<Double> list) throws RemoteException; // compute the min value Double min() throws RemoteException; // compute the max value Double max() throws RemoteException; // compute the sorted list ArrayList<Double> sort() throws RemoteException; // get the values list ArrayList<Double> getList() throws RemoteException; } Classe MinMaxImplementation.java /** * @author Fabrizio Ventola */ import import import import import java.rmi.RemoteException; java.rmi.server.ServerNotActiveException; java.rmi.server.UnicastRemoteObject; java.util.ArrayList; java.util.Collections; // Implementation of the MinMax interface // The UnicastRemoteObject class defines a non-replicated remote object whose references are valid only while the server process is alive. // The UnicastRemoteObject class provides support for point-to-point active object references (invocations, parameters, and results) using // TCP streams. public class MinMaxImplementation extends UnicastRemoteObject implements MinMax { private ArrayList<Double> list; public MinMaxImplementation() throws RemoteException { } // gives info about the application @Override public String getInfo() throws RemoteException { String infoMessage = new String("MinMax RMI implementation. MinMax it's a little application based on Java RMI that makes simple calculations."); try { System.out.println ("Info request from " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } return infoMessage; } // takes in input the values list @Override public void input(ArrayList<Double> list) throws RemoteException { this.list = list; try { System.out.println ("List of values received from " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } } // computes the min value @Override public Double min() throws RemoteException { Double min = Collections.min(list); try { System.out.println ("Min value calculation request from " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } return min; } // compute the max value @Override public Double max() throws RemoteException { Double max = Collections.max(list); try { System.out.println ("Max value calculation request from " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } return max; } // compute the sorted list of values @Override public ArrayList<Double> sort() throws RemoteException { //make a copy of the list of values ArrayList<Double> sortedList = new ArrayList<Double>(list.size()); for (Double number : list) { sortedList.add(new Double(number)); } try { System.out.println ("Sort calculation request from " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } //sort the list and return the sorted list; Collections.sort(sortedList); return sortedList; } // gives the original list of values taken in input @Override public ArrayList<Double> getList(){ try { System.out.println ("Sending values list to " + UnicastRemoteObject.getClientHost()); } catch (ServerNotActiveException e) { System.out.println ("Something wrong: " + e.getMessage()); } return this.list; } } Esecuzione dell’applicazione distribuita MinMax Si presentano di seguito alcuni screenshot durante l’esecuzione dell’applicazione distribuita MinMax, sia il server sia il client vengono lanciati sulla macchina locale all’indirizzo IP 127.0.0.1 associato all’alias localhost, sulla porta 8080. Si deve considerare che sia il client ed il server possono essere eseguiti su macchine separate connesse alla rete e che utilizzino lo stack TCP/IP. Figura 2: Avvio del server Figura 3: Avvio del client e invocazione metodi Figura 4: Client, invocazione di ulteriori metodi Figura 5: Server in esecuzione, arrivo delle richieste dal client Conclusioni Il sistema fornito da Java per effettuare l’invocazione remota di metodi solleva lo sviluppatore dall’incarico di realizzare un protocollo di comunicazione tra client e server e di realizzare il codice per effettuare le operazioni di marshalling e unmarshalling rendendo lo sviluppo di applicazioni a oggetti distribuite molto più semplice. L’alto livello di astrazione fornito però presenta degli svantaggi nel caso in cui sia necessario l’accesso concorrente di processi a più oggetti remoti replicati. Riferimenti 1. A. Tanenbaum, M. Van Steen Distributed Systems, Principles and Paradigms. PearsonPrentice Hall, 2nd edition, 2006 2. The Oracle Java RMI Tutorial: http://docs.oracle.com/javase/tutorial/rmi/ 3. Java SE 6 API Specification: http://docs.oracle.com/javase/6/docs/api/