Java RMI: Esempio Completo di un Applicazione Distribuita Il Problema Produttore/Consumatore in Ambiente Distribuito* *a cura del Prof. L. Nigro, Università della Calabria
Java RMI (Remote Method Invocation) Introduce uno strato di trasporto basato su TCP, per la chiamata e trasmissione di dati (opportunamente espressi in un formato di rete; Java usa la serializzazione come tecnica di marshalling) ) ad un oggetto remoto Un oggetto remoto è rappresentato presso il client da uno stub (proxy), che implementa la stessa interfaccia dell oggetto remoto.. Lo stub riceve la richiesta di invocazione di un metodo, prepara un blocco di informazioni (messaggio)) e lo spedisce alla macchina server dove risiede l oggetto remoto (oggetto effettivo), utilizzando i servizi di TCP Presso il server è presente lo skeleton che si interfaccia con la rete, converte le informazioni ricevute dallo stub, chiama il metodo effettivo sull oggetto remoto e, alla fine, costruisce un messaggio col risultato che è inviato allo stub e quindi al client Lo schema complessivo mira a rendere indistinguibile la chiamata di un metodo su un oggetto locale da quella relativa ad un oggetto remoto (semantica sincrona o bloccante) RMI richiede la mobilità del codice di classi Java e utilizza un meccanismo di garbage collection distribuita degli oggetti remoti basata su reference-count count IS_7 - L. Nigro 51
Architettura IS_7 - L. Nigro 52
Osservazioni Le prime versioni di Java effettivamente generavano stub e skeleton di un oggetto remoto Java 1.4 si limita a generare solo la componente stub Le ultime versioni (Java 5.0 o superiore) ) non generano più esplicitamente stub e skeleton dal momento che il linguaggio ne fornisce direttamente il supporto (basato sulla reflection e i proxy dinamici) indipendentemente dal particolare oggetto remoto implementato Ottenere un applicazione distribuita funzionante basata su Java RMI può essere non agevole dal momento che occorre padroneggiare tutta una serie di dettagli.. In quanto segue si mostrerà l uso di RMI attraverso lo sviluppo concreto di esempio non banale, rappresentativo della situazione generale I concetti base sono: progetto interfacce remote (da( collocare identicamente lato server e lato client) implementazioni delle interfacce lato server (UnicastRemoteObject( UnicastRemoteObject) pubblicazione di uno o più oggetti remoti su rmiregstry implementazioni delle applicazioni client con, es., lookup di oggetto remoti su rmiregistry predisposizione di un web server per il download di classi a runtime. IS_7 - L. Nigro 53
Produttore/consumatore/buffer limitato in ambiente distribuito Si tratta di un problema classico di programmazione concorrente Un certo numero di processi produttori e consumatori comunicano tra loro scambiandosi messaggi di un tipo generico, attraverso la mediazione di un buffer limitato (mailbox) Il buffer va realizzato in veste thread-safe mediante una scelta opportuna del meccanismo di controllo della concorrenza Il buffer costituisce un oggetto remoto condiviso tra i produttori e consumatori dispersi su varie macchine (dunque il problema coinvolge il vero parallelismo) Come meccanismi di controllo della concorrenza si considerano: semafori, lock/condition, monitor nativo di Java Si desidera che i risvegli dei processi avvengano in ordine FIFO Il buffer rappresenta il server, i processi produttori/consumatori i client IS_7 - L. Nigro 54
Impostazione del progetto Il progetto viene sviluppato su singola macchina ma è pensato esplicitamente per il distribuito. Tutto ciò si riflette,, ad esempio, nel controllo degli accessi alle classi che in una JVM è mediato dal classpath Sviluppare secondo RMI significa comprendere esattamente che il sistema software deve poter girare indipendentemente da ogni agevolazione di classpath Concretamente, è utile suddividere il lavoro in tre progetti associati rispettivamente al lato server, al lato client, agli aspetti comuni (common). Tali progetti possono essere sviluppati in ambiente Eclipse (dopo( aver scaricato dalla rete ed installato il plug-in di Java RMI) o direttamente utilizzando gli strumenti Java (il( metodo preferito di seguito) Siano bb_server, bb_client e bb_common le tre directory (es( es. di c:\) dei progetti. Siccome le varie componenti dell applicazione distribuita gireranno su macchine (e JVM) diverse, l utilizzo del packaging deve avvenire rigorosamente in modo congruente nei vari sotto progetti Si sottolinea ancora che le classi interne a bb_server o bb_client o bb_common non devono basarsi sul classpath per ottenere l accesso a classi degli altri progetti.. A tale proposito, si ammette che sulla macchina di sviluppo locale il classpath non contenga gli entry point di bb_server, bb_client e bb_common IS_7 - L. Nigro 55
Le interfacce Il punto di partenza è la definizione di opportune interfacce lato server Siccome le specifiche richiedono di poter lavorare su bounded buffer basati su diversi metodi di concorrenza, si è deciso di introdurre un factory che ottiene, a partire da informazioni di configurazione,, la dimensione del buffer e la specifica del tipo di controllo della concorrenza L oggetto factory è di fatto il primo oggetto remoto da realizzare. Altri oggetti remoti sono poi i vari tipi di buffer Le interfacce BoundeBuffer e BoundedBufferFactory rappresentano aspetti comuni dell intero progetto. Esse vengono sviluppate in bb_common e poi replicate identicamente (rispettando la struttura a package) in bb_server e bb_client Tanto BoundedBuffer quanto BoundedBufferFactory estendono l interfaccia Remote e i loro metodi devono dichiarare che possono sollevare l eccezione RemoteException (di tipo checked) Ogni (sotto) progetto ha package del tipo is.sottodirectory, il che significa che una corrispondente gerarchia di directory va realizzata in bb_common,, o bb_server etc. Ad esempio, siccome si desidera il package is.boundedbuffer per le interfacce BoundedBuffer e BoundedBufferFactory,, in bb_common si crea la directory is e al suo interno boundedbuffer etc. IS_7 - L. Nigro 56
Le interfacce di is.boundedbuffer package is.boundedbuffer; import java.rmi.*; public interface BoundedBuffer extends Remote{ public void put( Message m ) throws RemoteException; public Message get() throws RemoteException; //BoundedBuffer package is.boundedbuffer; import java.rmi.*; public interface BoundedBufferFactory extends Remote{ public BoundedBuffer getbuffer() throws RemoteException; //BoundedBufferFactory IS_7 - L. Nigro 57
Le interfacce di is.boundedbuffer In realtà, il package is.boundedbuffer dichiara anche l interfaccia Message che descrive la tipologia generica dei messaggi scambiati Message non è un interfaccia remota,, ma estende Serializable. Metodi esistono per fissare/ottenere il dato di un messaggio,, e per fissare/ottenere il tempo di permanenza del messaggio nel buffer package is.boundedbuffer; import java.io.*; public interface Message extends Serializable{ public void setdata( Object dato ); public Object getdata(); public long getbuffertime(); public void setbuffertime( long time ); //Message IS_7 - L. Nigro 58
Problemi di compilazione Dopo aver creato e compilato in bb_common le classi di is.boundedbuffer, si copia la gerarchia di directory is/boundedbuffer all interno di bb_server e bb_client Attenzione: l impostazione del lavoro richiede che per compilare correttamente es.. le interfacce di is.boundedbuffer, ci si debba collocare col prompt di sistema (uso di javac a riga di comando) ) in bb_common. Da dentro Eclipse, ovviamente,, le cose risulterebbero semplificate dallo strumento Utile è un file batch (es( es. jc.bat) posto in bb_common, col seguente contenuto: javac is/boundedbuffer boundedbuffer/*.java IS_7 - L. Nigro 59
Implementazioni lato server In bb_server si crea la sottodirectory di is es. denominata boundedbuffer_server, che si affianca alla sottodirectory boundedbuffer copiata da bb_common In boundedbuffer_server si programma l oggetto BoundedBufferFactoryImpl implementazione di BoundedBufferFactory ed erede di UnicastRemoteObject.. In questo modo l oggetto remoto (factory) si esporta ad RMI. L esportazione significa che RMI si fa poi carico di sentire richieste di connessioni dall esterno esterno, su una certa porta, dirette all oggetto remoto Nella stessa directory si implementano le classi bounded buffer concrete IS_7 - L. Nigro 60
BoundedBufferFactoryImpl Scopo di questa classe è consultare un file di properties (buffer.properties( buffer.properties) ) ed ottenere da questo le informazioni sulla buffer size ed il tipo di controllo della concorrenza. Esempio di contenuto di buffer.properties: BufferSize=5 ConcurrencyMethod=Semaphore Gli altri metodi di concorrenza sono indicati dai valori Lock e Native di ConcurrentMethod Se viene sollevata qualche eccezione (es. il file di properties non viene trovato) ) la classe ipotizza il controllo di concorrenza Native con una buffer size di default di 5 slot IS_7 - L. Nigro 61
BoundedBufferFactoryImpl package is.boundedbuffer_server; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; import java.util.properties; import java.io.*; public class BoundedBufferFactoryImpl extends UnicastRemoteObject implements BoundedBufferFactory{ private BoundedBuffer bb=null; public BoundedBufferFactoryImpl() throws RemoteException{ public BoundedBuffer getbuffer() throws RemoteException{ if( bb!=null ) return bb; Properties p=new Properties(); int n=5;//dimensione di default IS_7 - L. Nigro 62
BoundedBufferFactoryImpl try{ FileInputStream in= new FileInputStream("c:\\bb_server\\is\\boundedbuffer_server\\buffer.properties"); p.load( in ); String size=p.getproperty("buffersize"); n=integer.parseint(size); System.out.println("buffer size="+n); String conctype=p.getproperty("concurrencymethod"); if( conctype.equalsignorecase("semaphore") ){ System.out.println("Semaphore"); bb=new BoundedBufferSemaphoreImpl(n); else if( conctype.equalsignorecase("lock") ){ System.out.println("Lock/Condition"); bb=new BoundedBufferLockImpl(n); else{//monitor nativo di default System.out.println("Java Native Monitor"); bb=new BoundedBufferSynchronizedImpl(n); catch( IOException e ){ e.printstacktrace(); System.out.println("Java Native Monitor"); bb=new BoundedBufferSynchronizedImpl(n); return bb; //getbuffer //BoundedBufferFactoryImpl IS_7 - L. Nigro 63
Le classi degli oggetti remoti bounded buffer Per facilitare lo sviluppo si è deciso di realizzare preliminarmente la classe BoundedBufferImpl che implementa BoundedBuffer ma senza alcun controllo della concorrenza L uso di questa classe da parte di più thread è unsafe Le classi eredi aggiungono il controllo della concorrenza,, ma si disinteressano della gestione del buffer IS_7 - L. Nigro 64
BoundedBufferImpl package is.boundedbuffer_server; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; public class BoundedBufferImpl extends UnicastRemoteObject implements BoundedBuffer{//bounded buffer non sincronizzato private Message[] buffer; private int n, in, out, count; public BoundedBufferImpl( int n ) throws RemoteException{ if( n<=0 ) throw new RemoteException ("La dimensione del buffer deve essere positiva!"); this.n=n; buffer=new Message[n]; in=0; out=0; count=0; //costruttore IS_7 - L. Nigro 65
BoundedBufferImpl public void put( Message x ) throws RemoteException{ buffer[in]=x; in=(in+1)%n; count++; System.out.println("Buffering "+x+"..."); x.setbuffertime( System.currentTimeMillis() ); //put public Message get() throws RemoteException{ Message x=buffer[out]; out=(out+1)%n; count--; x.setbuffertime( System.currentTimeMillis()-x.getBufferTime() ); System.out.println("Unbuffering "+x+"..."); return x; //get protected boolean empty(){ return count==0; protected boolean full(){ return count==n; //BoundedBufferImpl IS_7 - L. Nigro 66
BoundedBufferSemaphoreImpl package is.boundedbuffer_server; import java.util.concurrent.*; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; public class BoundedBufferSemaphoreImpl extends BoundedBufferImpl{ private Semaphore mutex=new Semaphore(1,true); private Semaphore empty, full; public BoundedBufferSemaphoreImpl( int n ) throws RemoteException{ super(n); empty=new Semaphore(n,true); full=new Semaphore(0,true); public void put( Message x ) throws RemoteException{ empty.acquireuninterruptibly(); mutex.acquireuninterruptibly(); super.put(x); mutex.release(); full.release(); //put public Message get() throws RemoteException{ full.acquireuninterruptibly(); mutex.acquireuninterruptibly(); Message x=super.get(); mutex.release(); empty.release(); return x; //get //BoundedBufferSemaphoreImpl IS_7 - L. Nigro 67
BoundedBufferLockImpl package is.boundedbuffer_server; import java.util.concurrent.locks.*; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; import java.util.*; public class BoundedBufferLockImpl extends BoundedBufferImpl{ private Lock lock=new ReentrantLock(); private Condition empty=lock.newcondition(); private Condition full=lock.newcondition(); private List<Thread> listaproduttori=new ArrayList<Thread>(); private List<Thread> listaconsumatori=new ArrayList<Thread>(); private boolean produttoredevedormire(){ if( super.full() listaproduttori.get(0)!=thread.currentthread() ) return true; return false; //produttoredevedormire private boolean consumatoredevedormire(){ if( super.empty() listaconsumatori.get(0)!=thread.currentthread() ) return true; return false; //consumatoredevedormire public BoundedBufferLockImpl( int n ) throws RemoteException{ super(n); //costruttore IS_7 - L. Nigro 68
BoundedBufferLockImpl public void put( Message x ) throws RemoteException{ lock.lock(); try{ listaproduttori.add( Thread.currentThread() ); while( produttoredevedormire() ){ full.awaituninterruptibly(); listaproduttori.remove(0); super.put(x); empty.signalall(); finally{ lock.unlock(); //put public Message get() throws RemoteException{ Message x; lock.lock(); try{ listaconsumatori.add( Thread.currentThread() ); while( consumatoredevedormire() ){ empty.awaituninterruptibly(); listaconsumatori.remove(0); x=super.get(); full.signalall(); finally{ lock.unlock(); return x; //get //BoundedBufferLockImpl IS_7 - L. Nigro 69
BoundedBufferSynchronizedImpl package is.boundedbuffer_server; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; import java.util.*; public class BoundedBufferSynchronizedImpl extends BoundedBufferImpl{ private List<Thread> listaproduttori=new ArrayList<Thread>(); private List<Thread> listaconsumatori=new ArrayList<Thread>(); private boolean produttoredevedormire(){ if( super.full() listaproduttori.get(0)!=thread.currentthread() ) return true; return false; //produttoredevedormire private boolean consumatoredevedormire(){ if( super.empty() listaconsumatori.get(0)!=thread.currentthread() ) return true; return false; //consumatoredevedormire public BoundedBufferSynchronizedImpl( int n ) throws RemoteException{ super(n); //costruttore IS_7 - L. Nigro 70
BoundedBufferSynchronizedImpl public synchronized void put( Message x ) throws RemoteException{ listaproduttori.add( Thread.currentThread() ); while( produttoredevedormire() ){ try{ wait(); catch( InterruptedException e ){ listaproduttori.remove(0); super.put(x); notifyall(); //put public synchronized Message get() throws RemoteException{ listaconsumatori.add( Thread.currentThread() ); while( this.consumatoredevedormire() ){ try{ wait(); catch( InterruptedException e ){ listaconsumatori.remove(0); Message x=super.get(); notifyall(); return x; //get //BoundedBufferSynchronizedImpl IS_7 - L. Nigro 71
Implementazione del server package is.boundedbuffer_server; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; import javax.naming.*; public class BoundedBufferServer{ public static void main( String[] args ){ System.setProperty("java.security.policy", "c:\\bb_server\\is\\boundedbuffer_server\\server.policy"); System.setSecurityManager( new RMISecurityManager() ); try{ System.out.println("Costruzione factory..."); BoundedBufferFactory bbf=new BoundedBufferFactoryImpl(); System.out.println("Pubblicazione factory..."); Context naming=new InitialContext(); naming.rebind("rmi:boundedbufferfactory",bbf); System.out.println("In attesa di clienti..."); catch( Exception e ){ e.printstacktrace(); //main //BoundedBufferServer IS_7 - L. Nigro 72
Commenti Un programma server possiede il main ed il suo scopo è creare e pubblicare uno o più oggetti remoti di cui i client potranno scaricare lo stub su richiesta La pubblicazione sfrutta il servizio rmiregistry fornito da Java, che memorizza coppie <nome,riferimento> di oggetti remoti L utilizzo del servizio rmiregistry avviene mediante la classe Context di cui si crea un oggetto naming. Il metodo rebind inserisce una nuova coppia ed eventualmente aggiorna l oggetto se il nome è già presente.. I nomi sono del tipo rmi:nome_oggetto È importante assicurare nomi unici globali per gli oggetti remoti Nell esempio esempio,, solo l oggetto factory è creato e pubblicato sotto il nome rmi:boundedbufferfactory.. Un oggetto bounded buffer non viene pubblicato ma egualmente potrà essere ottenuto da un client invocando getbuffer() sul factory, che cosi restituisce lo stub del bounded buffer. Tutto ciò, tra l altro, assicura che i vari client ottengano e cooperino utilizzando uno stesso oggetto remoto bounded buffer IS_7 - L. Nigro 73
Commenti La compilazione di BoundedBufferServer implicitamente genera lo stub associato (in realtà Java 5.0 o superiore non lo genera più) Utilizzando una versione di Java sino alla 1.4, lo stub andrebbe creato esplicitamente col compilatore rmic come segue: >rmic is.boundedbuffer_server.boundedbufferserver invocato da dentro bb_server e specificando esplicitamente il package. Si otterrebbe il file BoundedBufferServer_Stub.class rmiregistry, una volta contattato, fornisce ad un client lo stub di un oggetto remoto pubblicato.. A questo scopo lo stub dovrebbe essere disponibile come una risorsa comune (si veda più avanti) ) cui rmiregistry può attingere on-demand IS_7 - L. Nigro 74
Implementazioni lato client Si presentano due processi client: uno con funzioni di produttore di messaggi,, un altro come consumatore.. Le relative classi (provviste del main) sono parte delle sotto directory boundedbuffer_producer e boundedbuffer_consumer di is di bb_client Il produttore ottiene un messaggio alla volta da tastiera e lo scrive sul buffer remoto Il consumatore, ogni qualvolta l utente digita Invio, chiede un messaggio al buffer e lo consuma, nel senso che ne mostra il contenuto su video unitamente al tempo di stazionamento del messaggio nel buffer Ma come è fatto un messaggio? È responsabilità del producer definire la tipologia dei messaggi scambiati In concreto, nella sotto directory boundedbuffer_producer si introduce la classe PCMessage che al momento risulta sconosciuta tanto al server (bounded buffer) quanto ai client consumer. La classe PCMessage verrà ottenuta dinamicamente dal server e dai consumatori sfruttando la mobilità del codice (si veda più avanti) presente in RMI IS_7 - L. Nigro 75
Classe PCMessage package is.boundedbuffer_producer; import java.io.*; import is.boundedbuffer.*; public class PCMessage implements Message, Serializable{ private long waittime; String content; public PCMessage( String s ){ content=s; public void setdata( Object s ){ content=(string)s; public Object getdata(){ return content; public void setbuffertime( long time ){ waittime=time; public long getbuffertime(){ return waittime; public String tostring(){ return content; //tostring //PCMessage IS_7 - L. Nigro 76
ProducerClient package is.boundedbuffer_producer; import is.boundedbuffer.*; import java.rmi.*; import java.rmi.server.*; import javax.naming.*; import java.util.*; public class ProducerClient{ public static void main( String[] args ){ System.setProperty("java.security.policy", "c:\\bb_client\\is\\boundedbuffer_producer\\client.policy"); System.setSecurityManager( new RMISecurityManager() ); String url="rmi://localhost/"; //prefisso url della macchina remota del server BoundedBufferFactory factory=null; BoundedBuffer bb=null; try{ Context naming=new InitialContext(); factory=(boundedbufferfactory)naming.lookup(url+"boundedbufferfactory"); bb=factory.getbuffer(); catch( Exception e ){ e.printstacktrace(); Scanner sc=new Scanner( System.in ); for(;;){ System.out.print("msg> "); String msg=sc.nextline(); try{ bb.put( new PCMessage(msg) ); catch( RemoteException e ){ e.printstacktrace(); //ProducerClient IS_7 - L. Nigro 77
ConsumerClient package is.boundedbuffer_consumer; import is.boundedbuffer.*; import java.util.scanner; import java.rmi.*; import java.rmi.server.*; import javax.naming.*; public class ConsumerClient{ public static void main( String[] args ){ System.setProperty("java.security.policy", "c:\\bb_client\\is\\boundedbuffer_consumer\\client.policy"); System.setSecurityManager( new RMISecurityManager() ); String url="rmi://localhost/"; BoundedBufferFactory factory=null; BoundedBuffer bb=null; try{ Context naming=new InitialContext(); factory=(boundedbufferfactory)naming.lookup( url+"boundedbufferfactory ); bb=factory.getbuffer(); catch( Exception e ){ e.printstacktrace(); Scanner sc=new Scanner( System.in ); String answer=null; Message msg=null; IS_7 - L. Nigro 78
ConsumerClient for(;;){ System.out.print("Press Invio to receive "); answer=sc.nextline(); try{ msg=(message)bb.get(); System.out.println("received msg "+msg+ " with buffer time="+msg.getbuffertime()+" ms"); catch( RemoteException e ){ e.printstacktrace(); //ConsumerClient IS_7 - L. Nigro 79