MokaByte Numero 13 - Novembre 1997      

La programmazione distribuita in Java

In questo articolo vedremo alcuni paradigmi per la programmazione distribuita (in particolare per il codice mobile), e le caratteristiche che Java mette a disposizione per implementarli.

    di Lorenzo Bettini   

   

Introduzione

L’avvento di Internet e la sua ormai affermata popolarità ha reso le applicazioni distribuite alla "portata di tutti". Molti sono gli esempi di applicazioni distribuite che tutti i giorni abbiamo modo di utilizzare, magari a nostra insaputa. Il World Wide Web è un esempio, forse uno fra i più utilizzati, di applicazione distribuita: le informazioni sono distribuite su vari processori, sparsi in tutto il mondo, e quando si vuole visitare una pagina web, avvengono una serie di operazioni a noi invisibili: il browser che stiamo utilizzando effettua una richiesta al server web all’indirizzo richiesto, il server web cerca sul proprio hard disk il documento e provvede ad inviarlo al computer che ha effettuato la richiesta, a questo punto il browser è capace di interpretare il documento e a presentarcelo in forma leggibile (le operazioni che vengono effettuate sono in realtà di più, ma non è importante in questa sede). Questo è un classico esempio di Client/Server, cioè un esempio di applicazione distribuita, in cui è richiesta la collaborazione di più programmi in esecuzione su computer diversi. Nell’esempio precedente vengono spediti dati fra i vari computer, ma può essere spedito anche del codice da eseguire. Internet può quindi essere considerato il più grande sistema distribuito mai costruito.

Per evidenziare ancora di più il senso di globalità si parla anche di Computazione Globale (Global Computation [7]). Oltre all’esempio dell’ipertesto globale del World-Wide-Web, vale la pena ricordare che il protocollo Ftp ci mette a disposizione un file system globale, nel senso che possiamo accedere a tutti gli hard disk di tutti i computer collegati ad Internet (ai quali abbiamo l’accesso, ovviamente), e col protocollo Telnet possiamo utilizzare computer sparsi per il mondo come se fossero a casa nostra. Ovviamente non si sono considerati i problemi di connessione e quindi il tempo necessario per portare a termine operazioni che fanno uso della rete. Diventa quindi inutile avere computer potenti, quando si usano connessioni di rete poco veloci: nelle applicazioni distribuite i fattori da prendere in considerazione sono la latenza e la bandwidth, e non tanto la velocità del processore e la quantità di memoria (purché questi non raggiungano valori talmente bassi da diventare a loro volta fattori importanti). Questo non deve stupire: anche senza utilizzare la rete, è praticamente inutile avere un processore molto veloce, e talmente poca memoria a disposizione da rendere necessario continuamente l’utilizzo dell’hard disk, col rallentamento che ne consegue. Può cambiare il contesto di esecuzione, ma la risorsa che gioca il ruolo più critico è sempre la più lenta!

Ovviamente ci sono tecnologie specifiche per scrivere applicazioni distribuite; del resto adesso si ha una dimensione in più, e questo non è poco. L’interazione fra i vari componenti dovrebbe essere indipendente dalla loro locazione fisica, e comunque dovrebbe essere trasparente all’utente.

I linguaggi per applicazioni distribuite dovrebbero mettere a disposizione costrutti semplici per la comunicazione in rete; questi comunque erano già presenti sotto forma di librerie in molti linguaggi tradizionali. I costrutti più interessanti invece sono quelli messi a disposizione per gestire il cosiddetto Codice Mobile (Mobile Code) [1] [2] [6]. Come si è detto infatti, nella rete, non sono trasmessi solo dati, ma anche pezzi di codice, da essere eseguito sulla macchina destinataria.

Prendiamo in esame alcuni dei più famosi paradigmi per il codice mobile:

Client - Server : Di questo paradigma si è già parlato all’inizio dell’articolo. Del resto si tratta di una filosofia di programmazione molto diffusa, che viene utilizzata anche in assenza di connessioni di rete: il client richiede dei servizi messi a disposizione da un server. Il server ha a disposizione tutte le risorse e tutti i mezzi per adempiere alla richiesta del client e fornirgli i risultati. Questo almeno è quello che appare al client: il server a sua volta per portare a termine il suo compito, può richiedere alcuni servizi ad un altro server, agendo così anche come client. Ovviamente non è necessario che client e server risiedano su computer diversi. Come si è detto si tratta di un’astrazione per "distribuire" vari compiti tra vari componenti, che possono essere threads di esecuzione diversi su una stessa macchina, oppure programmi che risiedono su macchine diverse, che si trovano geograficamente lontane. Se si ha davvero l’indipendenza dalla locazione fisica, la distribuzione diventa completamente trasparente per il client e per il server. Il sistema X-Windows adotta il paradigma Client-Server: il server gestisce un display fisico ed i vari client (le varie applicazioni) non accedono direttamente al display, ma vi accedono tramite la richiesta di servizi al server; una piacevole conseguenza di questo sistema è che il client può risiedere su una macchina diversa ed ottenere comunque i servizi grafici messi a disposizione di un server remoto.

Remote Evaluation : un programma X per portare a termine un certo compito può avere bisogno di risorse che non sono presenti nella macchina in cui sta eseguendo. Può appoggiarsi allora ad un altro calcolatore dotato di tali risorse. E’ compito del programma X fornire le istruzioni necessarie per utilizzare tali risorse. Sul computer remoto sarà presente un altro programma Y che è in ascolto di richieste in arrivo. Tale programma mette a disposizione le risorse della propria macchina (come del resto faceva il server nel paradigma Client-Server), purché il programma X gli fornisca del codice da eseguire; questo codice sarà mandato in esecuzione sul computer dove è in esecuzione Y. Questo paradigma è in effetti più flessibile del precedente: il precedente mette a disposizione solo un numero limitato di servizi che il client può richiedere. In questo paradigma invece l’esecutore (il programma Y) offre un servizio programmabile, poiché esegue codice fornito da altri programmi. Non c’è bisogno di riprogrammare il server per ottenere nuovi servizi. Del resto quando un programma utilizza una periferica, è perché vuole utilizzare le sue risorse, ma è lui stesso che fornisce i comandi necessari: un programma di videoscrittura stamperà un documento sfruttando la stampante, ed inviandole i comandi necessari per ottenere l’output desiderato.

Code on Demand : questo è il caso esattamente simmetrico del precedente: il programma X ha le risorse, ma non sa come utilizzarle, richiede quindi delle istruzioni (codice) ad un altro programma Y, che risiederà su un altro computer. Una volta ottenute le istruzioni, sarà il programma X ad eseguirle sul proprio computer e sulle proprie risorse. All’inizio dell’articolo si è parlato delle operazioni che un browser web effettua per mostrare una pagina web all’utente: il browser ha a disposizione tutte le risorse (monitor, primitive grafiche), ma è il server web a cui viene spedita la richiesta che spedisce il documento: codice HTML, quindi codice, da essere eseguito (interpretato) per mostrare all’utente le informazioni richieste. Parlare di perfetta simmetria è comunque scorretto il server web in questo caso spedisce il codice HTML perché deve esaudire una richiesta, mentre il programma di video scrittura spedisce le istruzioni alla stampante perché ha la necessità di effettuare una certa operazione. La sottile differenza sta quindi nell’intenzione che ha portato alla spedizione del codice.

Come si è visto nel paradigma Client - Server non viene trasmesso codice, ma solo dati che descrivono la richiesta di un particolare servizio. Gli altri due paradigmi sono molto più potenti del primo: viene spedito codice eseguibile, e questo rende indipendenti i programmi che comunicano, in quanto non è necessario modificare l’esecutore, se vogliamo che esegua operazioni nuove: basta modificare il programma che spedisce il codice, anzi, basta modificare solo il codice spedito.

Esiste comunque un altro paradigma che estremizza il concetto di codice mobile, ed è quello degli

Agenti Mobili : [3] [4] Un programma X invece di spedire del codice ad un altro programma che si trova su un computer remoto, si "trasferisce" su tale computer, insieme ai suoi dati, e continua la sua esecuzione lì. Questa esigenza può nascere nel caso in cui il programma X abbia dei dati e delle operazioni da eseguire su questi dati, però non ha alcune risorse che sono su un altro computer, ma che comunque non possono essere trasferite tramite la rete. Ad esempio può essere necessaria una notevole potenza di calcolo, e questa (purtroppo) non può essere trasferita in rete, ed allora per sfruttarla X dovrà trasferirsi su questo potente computer, effettuare lì le operazioni sui propri dati, e ritornare sul computer da cui era "partito" per riportare i risultati all’utente che aveva lanciato il programma. Col paradigma degli agenti mobili si cerca di ridurre l’utilizzo della rete per la comunicazione fra più applicazioni: col Client - Server, per ogni richiesta di servizio si ha la spedizione di un pacchetto in rete, e si è già detto che l’utilizzo della rete è il collo di bottiglia delle applicazioni distribuite. Se quindi il client ha bisogno di effettuare una certa operazione tramite la chiamata dei servizi del server, si avranno tante comunicazioni in rete quante richieste di servizi sono necessarie per portare a termine tale operazione. L’idea degli agenti mobili è invece quella di svolgere tale operazione direttamente spostandosi (spostando il proprio codice e i dati) sul computer del server, ed interagire col server in locale. I risultati delle operazioni saranno poi riportati al sito di origine. E’ chiaro che in questo caso solo due comunicazioni in rete sono necessarie: una per la spedizione dell’agente e l’altra per il "ritorno a casa" dell’agente (o comunque per la spedizione del risultato).

Un computer con un sistema operativo costituisce una piattaforma su cui gli sviluppatori possono costruire applicazioni; il paradigma di comunicazione degli agenti mobili vuole rendere la rete una piattaforma per sviluppare applicazioni!

Cosa offre Java?

Vediamo adesso cosa offre Java per le applicazioni distribuite, ed in particolare come, se è possibile, sfruttare le features di questo linguaggio per implementare i suddetti paradigmi di comunicazione.

Senz’altro l’utilizzo delle sockets rende possibile l’implementazione di programmi che comunicano tramite il paradigma Client - Server. I sockets non sono di certo una novità, tuttavia l’implementazione che Java fornisce a livello di classi utilizzabili dall’utente, rende la scrittura di un’applicazione Server e di una Client molto semplice. Lo stesso non si può dire delle API sockets che si trovano nelle librerie di altri linguaggi (vedi C fra tutti). Da una parte il server creerà un oggetto di classe ServerSocket specificando semplicemente il numero della porta su cui si metterà in ascolto col metodo accept (sempre della suddetta classe). Questo metodo, appena sarà ricevuta una richiesta di connessione, restituirà un oggetto della classe Socket. Usando i due stream (di input e di output) messi a disposizione da tale oggetto sarà possibile iniziare la comunicazione. Dall’altra parte il client dovrà creare un oggetto della classe Socket specificando nel costruttore l’indirizzo dell’host e il numero di porta; se la connessione viene stabilita anche il client userà i due stream per la comunicazione col server. Ovviamente tali stream di input e output derivano rispettivamente dalle classi basi astratte InputStream e OutputStream, quindi qualsiasi classe che usi degli stream ai quali si riferisce tramite variabili (riferimenti) di tipo InputStream e OutputStream, potrà comunicare in rete in modo completamente trasparente.

Nella release 1.1 del jdk è inoltre presente un nuovo framework, Remote Method Invocation [9], abbreviato spesso con RMI, che permette ad oggetti che si trovano anche su computer diversi di comunicare tramite normali invocazioni di metodi. In questo modo si può ottenere un riferimento ad un oggeto remoto, e chiamarne i metodi, come se fosse un oggetto locale. Questo non è altro che il meccanismo delle Remote Procedure Calls (RPC). In questo modo il client non dovrà richiedere dei servizi al server, ma una volta ottenuto un riferimento ad un oggetto server, ne chiamerà direttamente i metodi.

In Java le classi vengono caricate a run-time solo quando sono necessarie, tramite un ClassLoader. Il ClassLoader può recuperare i dati delle varie classi non solo dal file system locale, ma anche dalla rete. Del resto quando apriamo una pagina web in cui è presente un’applet Java, è il class loader che si preoccupa di trasferire dalla rete tutti i dati necessari per le classi usate dall’applet; anzi solo delle classi che effettivamente vengono usate dall’applet. Non basterà la dichiarazione di una variabile di tipo X perché il ClassLoader cerchi di scaricare dalla rete i dati del file X.class, ma sarà necessaria un’istruzione del tipo new X(...). Quindi i dati della classe saranno scaricati solo quando viene creato un oggetto di tale classe (la questione non è così semplice: esistono altri casi che rendono necessario il downloading, ma l’argomento sarà approfondito in un articolo specifico in futuro). Ovviamente questo meccanismo viene utilizzato anche in locale, ma forse in quel caso non viene notata una grossa differenza. I motivi di tale ottimizzazione risiedono ovviamente nel cercare di ridurre il più possibile le comunicazioni in rete.

Il downloading dinamico delle classi di un’applet non è altro che un esempio di Code On Demand.

Un’altra feature molto importante introdotta nel jdk 1.1 è la Serializzazione [8]. Tramite questa è possibile scrivere in uno stream (ObjectOutputStream e ObjectInputStream) un qualsiasi oggetto; quest’ultimo deve appartenere ad una classe che implementi l’interfaccia java.io.Serializable. Si noti che basta dichiarare che una classe implementa tale interfaccia, senza dover definire metodi particolari: tutti i dettagli di tale serializzazione sono "risparmiati" al programmatore, e questo non è poco! Ovviamente le classi più usate implementano tale interfaccia. Siccome si è visto che le comunicazioni tramite le socket avvengono usando gli stream, si capisce subito che tramite la serializzazione si possono spedire interi oggetti in rete. In particolare anche i threads possono essere spediti in rete tramite la serializzazione (la classe Thread non implementa l’interfaccia richiesta, ma basta derivare da Thread ed implementare tale interfaccia nella classe derivata), quindi se sul computer destinatario è in ascolto un programma per ricevere threads dalla rete e mandarli in esecuzione, si ha un esempio di Remote Evaluation!

A questo punto il passaggio agli agenti mobili sembra scontato: dovrebbe bastare modificare tale programma sul computer di destinazione, in modo che invece di mandare in esecuzione il thread letto dalla rete, lo faccia semplicemente continuare ad eseguire dal punto in cui era stato interrotto. Purtroppo questo non è possibile: la Java Virtual Machine non permette di modificare direttamente il Program Counter (ovviamente per motivi di sicurezza tra le altre cose), e comunque lo stato di esecuzione di un thread non viene serializzato. Un’implementazione corretta degli agenti mobili, richiede invece che sia possibile proprio questa caratteristica; se si vuole, questa è una di quelle caratteristiche che differenzia il paradigma degli agenti mobili da quello della Remote Evaluation. Si vedrà in un successivo articolo come sia possibile fornire un’implementazione che si avvicina abbastanza a quella richiesta dal paradigma degli agenti mobili (del resto il Tokyo Research Laboratory dell’IBM ha sviluppato il progetto degli Aglets, per programmare con - pseudo - agenti mobili in Java [5]).

Se con mobilità di codice, si intende la possibilità di permettere ad un processo, o ad un thread, di spostare il proprio codice ed il proprio stato di esecuzione, su un altro computer, e lì proseguire ad eseguire, allora NON possiamo dire che Java permette la mobilità di codice: permette piuttosto ad un thread su un certo computer di collegarsi (il termine inglese è linked oppure bounded) dinamicamente con del codice proveniente da un altro computer. Secondo [1] Java sarebbe un linguaggio che permette la mobilità debole (weak mobility), che è contrapposta al primo tipo di mobilità: la mobilità forte (strong mobility).

Comunque a Java manca proprio poco per fregiarsi di tale qualità: il punto debole è proprio quello di non permettere il salvataggio ed il ripristino dello stato di esecuzione, ma tramite piccoli stratagemmi e regole a cui il programmatore che vuole usare gli agenti mobili in Java deve sottostare (quelle adottate negli Aglets), si può dire che gli agenti mobili sono implementabili in Java, e vedremo come fare in un futuro articolo.

Del resto non ci dimentichiamo che programmazione distribuita implica programmazione concorrente, che a sua volta implica necessità di sincronizzare processi e risorse, ed in questo non possiamo avere niente da ridire su quello che Java mette a disposizione: sincronizzazione a livello di linguaggio, quindi a livello di sintassi, e non di libreria! Questo vuole dire poter scrivere sezioni critiche in modo pulito, direttamente con istruzioni specifiche: l’istruzione synchronized permette l’accesso ad un thread alla volta al blocco di codice che racchiude, o ad alcuni metodi di una classe. Ovviamente è sempre compito del programmatore sincronizzare in modo opportuno risorse e threads, ma in questo modo si ha la possibilità di farlo in modo semplice ed elegante (e spesso questo ha come piacevole conseguenza la leggibilità di codice!).

Inoltre un problema legato alla programmazione distribuita è la forte dipendenza dall’architettura del computer sul quale sarà eseguito il codice. Il paradigma Client - Server non risente molto di questo problema, perché non viene spostato del codice, ma solo dei dati (tutt’al più si avranno da risolvere i problemi delle rappresentazioni dei dati nelle varie piattaforme). E’ anche vero, però, che i paradigmi più interessanti e flessibili sono proprio quelli che basano la propria forza sulla spedizione di codice in rete. Già sarebbe tanto richiedere che codice compilato per un sistema operativo su una macchina Intel venga eseguito correttamente e senza problemi su un computer sempre Intel, ma con un diverso sistema operativo, figuriamoci quando oltre al sistema operativo, cambia anche il tipo di processore! Java per l’ormai suo famoso byte code non presenta assolutamente (be’ non esageriamo tanto ;-) ) questo problema.

Quindi non penso proprio sia azzardato definire Java un linguaggio per creare applicazioni distribuite, e forse chi è ancora restio a farlo conosce Java solo come linguaggio per creare simpatiche (quanto spesso inutili) animazioni ed effetti speciali sulle pagine web!

Nei prossimi articoli vedremo come sfruttare le caratteristiche del linguaggio Java per creare piccole applicazioni distribuite, implementando i suddetti paradigmi, e soprattutto vedremo un esempio di implementazione di agenti mobili in Java (senza nessuna pretesa di concorrere con l’IBM ;-).

 

Riferimenti bibliografici

[1] A.Carzaniga, G.P.Picco, G.Vigna "Designing Distributed Applications with Mobile Code Paradigms"
[2] C.Ghezzi, G.Vigna, "Mobile Code Paradigms and Technologies: a Case Study"
[3] J.White (General Magic) "Mobile Agents White Paper" http://www.genmagic.com/agents/Whitepaper/whitepaper.html
[4] F.Giudici "Teoria degli Agenti Mobili" Mokabyte - Gennaio ’97
[5] F.Giudici "Agenti Mobili (Aglets IBM)" Mokabyte - Febbraio ’97
[6] L.Cardelli "Mobile Computation" http://www.research.digital.com/SRC/personal/Luca_Cardelli/home.htm
[7] L.Cardelli "Global Computation" http://www.research.digital.com/SRC/personal/Luca_Cardelli/home.htm
[8] M.Carli "La Serializzazione" MokaByte - Aprile ’97
[9] F.Giudici "La Remote Method Invocation API" MokaByte - Aprile ’97