Upload
doanphuc
View
221
Download
0
Embed Size (px)
Citation preview
Università degli Studi della Calabria FACOLTÀ DI INGEGNERIA
Corso di Diploma in Ingegneria Informatica
TESI DI DIPLOMA
UNA IMPLEMENTAZIONE DISTRIBUITA DELLA
PROGRAMMAZIONE GENETICA IN AMBIENTE PEER-TO-PEER
RELATORI CANDIDATI
Prof. Giandomenico Spezzano Castelfranco Antonino
Toscano Cosma Christian
_________________________________________________
ANNO ACCADEMICO 2002/2003
7.1
7.2
7.3
7.4 Introduzione
L’esigenza sempre più crescente di potenza di calcolo ha portato alla realizzazione di reti
informatiche, per risolvere il problema di risorse limitate, che vengono fornite da macchine
parallele, comunque sempre più prestanti ed economiche. Negli ultimi anni si sono fatte
strada, il grid computing e l’architettura peer-to-peer, che inizialmente si sono evolute in
maniera indipendente e che adesso sembrano trovare un naturale punto di convergenza: la
condivisione delle risorse di calcolo. Da qui nasce l’esigenza di parallelizzare o distribuire il
calcolo, creando applicazioni che abbiano tali caratteristiche. Un paradigma che ha in se il
concetto di parallelismo è rappresentato dagli algoritmi evolutivi e, nel caso che andremo
esaminare, la programmazione genetica.
Il nostro obiettivo è quello di realizzare un’implementazione distribuita della
programmazione genetica in ambiente peer-to-peer, ispirandoci a CAGE che ne è
un’implementazione parallela.
Dopo aver descritto la programmazione genetica e messo in evidenza le sue caratteristiche
nel capitolo 1, descriveremo la filosofia peer-to-peer cercando una diretta correlazione con
essa (capitolo 2).
Nel terzo capitolo analizzeremo la piattaforma JXTA che fornisce gli strumenti per
sviluppare applicazioni peer-to-peer e vedremo i dettagli implementativi del nostro prototipo,
nel quarto e nel quinto capitolo.
7.5
7.6
7.7
7.8
7.9
7.10
7.11 Capitolo 1
Programmazione genetica
1.1 L’Intelligenza Artificiale
"L’intelligenza artificiale è la capacita di un computer o di un dispositivo controllato da
computer di eseguire compiti comunemente associati con i più elevati processi intellettuali,
caratteristici dell’uomo, quali l’abilità di ragionare, scoprire significati, generalizzare o
imparare dalla passata esperienza."
L'intelligenza artificiale, inoltre, si occupa, fra l'altro, di risolvere problemi utilizzando
algoritmi che imitano o si ispirano all'intelligenza umana.
L'utilizzo di metodologie di AI(Artificial Intelligence) consente di affrontare problemi
non facilmente risolvibili con le tecniche tradizionali.
1.2 Gli algoritmi evolutivi
Gli algoritmi evolutivi sono strategie euristiche, che si ispirano all'evoluzione naturale
teorizzata da Darwin nel suo libro sull'evoluzione della specie, per risolvere problemi, trattati
dall’Intelligenza Artificiale, che ricercano un minimo o un massimo globale in un dominio
limitato.
Definizione 1.2.1: consideriamo un elemento D x ∈ , dove e D è un particolare
dominio, spesso chiamato spazio di ricerca. Se consideriamo D come uno spazio
cartesiano, allora la cardinalità di D sarà uguale a n e x sarà un vettore. Data una
funzione f : D�R detta funzione obiettivo, allora la ricerca dell'ottimo globale
corrisponde a trovare un x* che massimizza tale funzione, cioè: D *x ∈ e
f(x*) f(x) :D x ≤∈∀ .
La ricerca diventa molto difficoltosa, se la funzione presenta più punti di massimo locale,
vincoli sul dominio D, non linearità, ecc.
L’utilizzo degli algoritmi evolutivi permette di ottimizzare i tempi di ricerca, che altrimenti
non sarebbero accettabili se si risolvessero i problemi attraverso l’uso di tecniche esatte,
malgrado essi risolvano il problema con un certo grado d'incertezza, oppure non assicurino la
convergenza della ricerca alla soluzione se non in casi particolari. Essi sono classificati fra i
metodi di ricerca "deboli", chiamati così perchè riescono a risolvere una grande varietà di
problemi utilizzando poche informazioni sul domino particolare, in contrapposizione a quelli
"forti" che sfruttano la maggior parte delle conoscenze del dominio applicativo.
I metodi di ricerca "deboli" sono stati spesso ispirati dall'imitazione di metodi di
risoluzione utilizzati dall'uomo o da analogie con il mondo naturale. Fra i più conosciuti si
ricordano hill-climbing, depth-first e breadth-first-search.
Angeline, ha introdotto una nuova tipologia di metodi di ricerca, i metodi deboli
evolutivi, che meglio permette di classificare questo tipo di algoritmi.
Essi sono, infatti, metodi che inizialmente hanno poca conoscenza del dominio, ma durante
la loro evoluzione acquistano più consapevolezza del problema, diventando in tal modo
metodi forti. Si parla in questo caso di "intelligenza emergente", cioè si ha una comprensione
del dominio che emerge durante 1'evoluzione stessa, per cui si riescono a risolvere in modo
efficace anche problemi con domini particolari.
Come nella teoria dell’evoluzione naturale concepita da Darwin, nel suo libro “On the
Origin of Species by Means of Natural Selection” del 1859 sull’evoluzione della specie, gli
algoritmi evolutivi orientano la ricerca verso gli elementi piu "adatti", che hanno maggiore
possibilità di sopravvivere e di trasmettere le loro caratteristiche ai successori: in pratica, si ha
una popolazione di individui che evolvono di generazione in generazione attraverso
meccanismi simili alla riproduzione sessuale e alla mutazione dei geni.
Gli algoritmi evolutivi sono tecniche che vengono definite euristiche, perché conducono ad
una ricerca che privilegia le zone del dominio della funzione obiettivo, dove maggiormente è
possibile trovare soluzioni migliori, non trascurando altre zone a più bassa probabilità di
successo in cui saranno impiegate un minor numero di risorse.
Gli algoritmi evolutivi si possono classificare in algoritmi genetici e programmazione
genetica: entrambi hanno avuto un’importante diffusione nella comunità scientifica, nelle
strategie evolutive e nella programmazione evolutiva.
1.3 Gli algoritmi genetici
Nel 1975 John Holland propone la pionieristica formulazione matematica degli Algoritmi
Genetici, nel seguito indicati come AG: si tratta di un semplice modello computazionale
dell’evoluzione naturale e del meccanismo dell’ereditarietà basato su una popolazione di
stringhe binarie che, in analogia con il DNA biologico, codificano convenientemente la
struttura e le caratteristiche di una possibile soluzione per il problema considerato.
Si utilizzano per semplicità sequenze di bit di lunghezza prestabilita e su queste si
definiscono le operazioni genetiche di mutazione (riproduzione asessuale) e ricombinazione
o crossover (riproduzione sessuale).
In termini biologici, ogni stringa rappresenta un genotipo, cioè la formula basilare per la
costruzione dell’organismo manipolata dai processi genetici:
il genotipo è quindi decodificato per formare il fenotipo dell’individuo, corrispondente alla
possibile soluzione che viene valutata per determinarne la distanza dall’ottimo cercato, cioè il
suo grado di adattamento all’ambiente indicato solitamente con il termine fitness.
Considerato un generico problema da risolvere, un AG è costituito da cinque componenti
basilari:
� una codifica delle possibili soluzioni, la popolazione, tale da essere facilmente
manipolabile dagli operatori genetici: il metodo classico è l’uso di una stringa binaria
di lunghezza fissata M;
� una funzione di adattamento o fitness, in grado di assegnare ad ogni possibile
elemento dello spazio di ricerca un valore numerico corrispondente alla distanza dalla
soluzione desiderata;
� un insieme di operatori genetici che forniscano un modo per generare stocasticamente
i nuovi individui, a partire da uno o più genitori selezionati nella popolazione
esistente: si utilizzano in genere le sole operazioni di mutazione e crossover;
� un insieme di parametri di sistema che caratterizzano il comportamento dell’algoritmo
durante l’esecuzione: ad esempio la dimensione della popolazione, o le percentuali di
utilizzo degli operatori genetici durante la fase di riproduzione;
� un criterio di terminazione, che stabilisce quando l'algoritmo si può fermare.
Figura 1.3.1: Uno schema classico di algoritmo genetico
In figura 1.3 è rappresentato un semplice schema di funzionamento di un algoritmo
genetico:
1. si crea in maniera casuale una popolazione di individui, dove ogni individuo è
rappresentato da una stringa di lunghezza prefissata, tipicamente binaria;
2. si valuta la fitness;
3. si verifica se è soddisfatto il criterio di terminazione e in caso contrario si passa alla
nuova generazione.
Figura 1.3.2: pseudo codice per i passi fondamentali di un AG
La nuova popolazione sarà costruita, applicando alla vecchia i principali operatori genetici:
� il crossover che, dati due elementi selezionati nella popolazione, detti "genitori",
genera due "figli", cioè due individui con caratteristiche ereditate da entrambi i
parenti;
� la mutazione che altera un singolo "gene" (bit) di un individuo;
� la riproduzione che copia un individuo nella nuova popolazione.
Questi operatori che, insieme al numero di generazioni massime e alla dimensione della
popolazione, costituiscono i parametri fondamentali dell'algoritmo, sono applicati con diverse
probabilità fino a che la nuova popolazione non ha raggiunto la dimensione desiderata.
1.3.1 La popolazione
Un elemento molto importante degli algoritmi genetici è la popolazione, che è costituita da
un numero prefissato di individui ed è generata in modo casuale all'inizio dell'algoritmo.
Esistono diverse varianti di algoritmo genetico, le cui caratteristiche dipendono dalla
gestione della popolazione:
� si parla di popolazione generazionale se 1'intera popolazione è sostituita interamente
dai nuovi elementi;
� in caso contrario si ha 1'algoritmo genetico steady-state (a stato fissato);
� si ha un modello elitarista se 1'elemento o gli elementi migliori sono conservati
durante 1'evoluzione della popolazione.
� si ha un modello a isole, invece, se si hanno una serie di popolazioni che evolvono in
maniera autonoma, con occasionali migrazioni di individui da una "isola" all'altra.
Ogni individuo della popolazione è rappresentato, in genere, da una stringa di lunghezza
prefissata di bit.
Nel caso più diretto, tali stringhe rappresentano la concatenazione delle rappresentazioni
binarie dei parametri numerici di una funzione da minimizzare o di una funzione che codifica
la soluzione ad un problema in termini numerici.
La scelta di utilizzare prevalentemente un alfabeto binario è dovuto ad alcuni importanti
risultati teorici, quali il teorema degli schemi di Holland, che indica questa come una
rappresentazione molto vicina all'ottimo. Recenti studi indicano, però, in questa scelta alcuni
svantaggi, quali aggiungere multimodalità e complessità alla funzione obiettivo, il che
rende il problema più difficile da affrontare.
In generale, data l'universalità della rappresentazione binaria, il genoma può comunque
rappresentare virtualmente qualunque tipo di informazione (segnali, immagini, forme,
relazioni di tipo strutturale o topologico, ecc.).
Per impostare un esperimento con un algoritmo genetico è quindi necessario definire in
modo esatto la struttura del genoma, stabilendone quindi la lunghezza ed il significato dei
singoli bit.
1.3.2. Gli operatori genetici
Negli algoritmi genetici si utilizzano principalmente tre operatori: la riproduzione, 1a
mutazione e il crossover. I primi due metodi si applicano ad un solo individuo, mentre il
crossover ha bisogno di due individui. Prima di applicare uno di questi operatori è necessario
selezionare uno o due individui della popolazione, a seconda del caso.
I più popolari metodi di selezione sono:
� il fitness proporzionate, che assegna ad ogni elemento della popolazione una
probabilità di essere scelto, proporzionata al valore della sua fitness;
� il K-tournament, che sceglie K elementi a caso nella popolazione, indice un torneo
fra questi individui e quello che risulta vincente sarà quello selezionato.
Ovviamente, con entrambi i metodi, gli individui con fitness migliore hanno maggiori
probabilità di essere selezionati e, quindi, di trasmettere i propri geni alla generazione
successiva, rispettando i meccanismi evolutivi di sopravvivenza dei più adatti.
7.11.1 Figura 1.3.3: Due semplici operatori genetici
La riproduzione ricopia semplicemente 1'individuo nella nuova popolazione, lasciando
intatto tutto il suo patrimonio genetico.
La mutazione inverte un bit, scelto casualmente con una distribuzione uniforme, del
genotipo dell'individuo e inserisce in tal modo diversità nella popolazione, portando la ricerca
verso nuovi spazi o recuperando alleli che erano andati perduti in precedenza.
Il crossover combina i patrimoni genetici dei due genitori in modo da costruire due "figli",
che possiedono parte dei geni di uno e parte dell'altro. Esistono diverse tipologie di crossover:
� il crossover ad un punto, in cui le stringhe che codificano i due genitori vengono
"tagliate" in uno stesso punto. Si opera poi uno scambio della parte destra (o sinistra)
delle stringhe (figura 1.3.3), per ottenere due figli in cui il genotipo del primo è
costituito dalla concatenazione della parte destra del genotipo del primo genitore con
la parte sinistra (destra) di quello del secondo, mentre il genotipo del secondo figlio è
costituito dalla concatenazione della parte destra (sinistra) del genotipo del secondo
genitore con la parte sinistra (destra) di quello del primo;
� nel crossover a due punti, la stringa è considerata "circolare" (l'ultimo bit si
immagina concatenato al primo) (figura 1.3.4) e quindi il genotipo, "tagliato" in due
punti, viene suddiviso in due parti che, nella rappresentazione lineare della stringa,
corrispondono alla sottostringa interna ai due tagli e alle due sottostringhe esterne ad
essi che, tuttavia, come detto, si immaginano concatenate fra loro in una unica stringa.
Lo scambio avviene quindi in modo del tutto analogo al crossover singolo punto;
Figura 1.3.4: Crossover a due punti
� il crossover uniforme, invece, prevede che, per ogni posizione all'interno della stringa
(figura 1.3.5), i bit corrispondenti dei due genitori vengano assegnati uno ad un figlio
e l'altro all'altro figlio in modo casuale.
Figura 1.3.5: Crossover uniforme
Il crossover è la forza trainante dell'algoritmo genetico ed è 1'operatore che maggiormente
influenza la convergenza, anche se un suo utilizzo troppo spregiudicato potrebbe portare ad
una convergenza prematura della ricerca su qualche massimo locale.
I parametri che regolano la minore o maggiore influenza di un operatore su un altro sono
identificati come la probabilità di crossover, di mutazione e di riproduzione; la cui somma
deve essere uguale ad uno; bisogna comunque aggiungere che esistono varianti di algoritmi
genetici, che realizzano in ogni caso la mutazione anche dopo il crossover, per mantenere
maggiore diversità nella popolazione. In realtà, la scelta dei parametri e del tipo particolare di
operatore usato dipendono fortemente dal dominio del problema, perciò non è possibile, a
priori, stabilire le specifiche di un algoritmo genetico. Addirittura sono stati sviluppati con
buon successo algoritmi ibridi the sostituiscono la mutazione con un algoritmo fortemente
dipendente dal dominio.
1.3.3 Alcuni cenni teorici
Esiste pochissima teoria sul funzionamento effettivo degli algoritmi genetici; in
particolare, Holland ha tentato di spiegare la distribuzione delle risorse nello spazio di ricerca
con il suo famoso teorema degli schemi. Tale teorema è la prima rigorosa spiegazione del
perché gli algoritmi genetici funzionino.
Uno schema è un modello di valori del gene che possono essere rappresentati, nella
codifica binaria, da una stringa di caratteri dell'alfabeto {0,1,*}. Un cromosoma contiene gli
schemi ottenuti sostituendo col simbolo "*" uno o più dei suoi bit.
Per esempio, il cromosoma"1010", contiene tra gli altri gli schemi 10**, *0*0, **10 e
**1*. L'ordine di uno schema è il numero di simboli diversi da "*" che contiene e la
lunghezza definita è la distanza tra i simboli diversi da "*" più esterni (nell'esempio 2,3,1,3,
rispettivamente).
Il teorema degli schemi spiega la potenza di un GA in termini di quanti schemi sono
processati. Agli individui della popolazione viene data la possibilità di riprodursi, spesso
chiamata prove di riproduzione (reproduce trials), e producono figli. Il numero di opportunità
che ogni individuo riceve è in proporzione al suo fitness (selezione di tipo fitness-
proportionate), quindi i migliori individui contribuiscono maggiormente ai geni della
generazione successiva. Si presuppone che un alto valore di fitness sia dovuto al fatto che
l'individuo possiede buoni schemi. Passando i migliori schemi alla generazione successiva,
aumenta la probabilità di trovare soluzioni migliori. Holland ha dimostrato che la cosa
migliore è assegnare prove di riproduzione in numero sempre maggiore agli individui che
hanno il fitness più elevato rispetto al resto della popolazione, in modo che gli schemi buoni
abbiano un numero di prove crescente in modo esponenziale nelle generazioni successive
(teorema degli schemi). Ha mostrato inoltre che poichè ogni individuo contiene un gran
numero di schemi diversi, il numero degli schemi che devono essere effettivamente processati
è dell'ordine di n3, dove n è il numero di individui. Questa proprietà è detta parallelismo
implicito ed è una delle motivazioni del buon funzionamento dei GA.
Il crossover e la mutazione alterano gli schemi definiti e possono introdurne di nuovi.
Ricapitolando, il teorema degli schemi dimostra che, utilizzando una selezione di tipo
fitness-proportionate, la distribuzione degli schemi di ricerca, cioè l'aumento o la diminuzione
di un particolare schema, avviene in modo molto vicino all'ottimo matematico ed è
indipendente dal problema.
Numerose critiche sono state mosse a questo teorema, alcune delle quali sulla reale
applicabilità in casi pratici; sono stati sviluppati, infatti, GA (Genetic Algorithm) che non
soddisfano le condizioni del teorema, ma ottengono risultati simili all'algoritmo classico.
Altre critiche sono state mosse sugli aspetti teorici, in particolare si è notato che il teorema
degli schemi non esplica le correlazioni esistenti fra i padri e i figli.
Il teorema di Price spiega meglio queste relazioni ed asserisce che il funzionamento e
1'efficienza di un GA dipende maggiormente dalle correlazioni fra genitore a figlio che non
dagli schemi stessi.
1.4 Le strategie e la programmazione evolutiva
Le strategie evolutive sono state sviluppate all'Università di Berlino da Rechenberg,
Bienert, Schwefel, Bäck, Hoffmeister.
Diversamente dagli algoritmi genetici, la popolazione è costituita da vettori di numeri reali
di lunghezza prefissata e la popolazione iniziale è generata in modo casuale in un certo
intervallo e utilizzando una distribuzione uniforme.
Una nuova popolazione è costituita dai migliori M individui di quella precedente e i
rimanenti sono scelti applicando dei particolari operatori.
I principali sono:
� la ricombinazione discreta, che genera i figli con distribuzione uniforme rispetto ai
genitori;
� la ricombinazione intermedia, che fa la media dei valori dei due genitori;
� la ricombinazione intermedia casuale, che determina in modo casuale i pesi da
attribuire ai genitori;
� la mutazione che aggiunge un valore casuale, preso da una distribuzione gaussiana
con varianza scelta dal programmatore in modo appropriato a ogni elemento del
vettore.
Il principale vantaggio derivante dall'usare queste strategie è che elementi della
popolazione con piccole diversità, dovrebbero presentare poca differenza nei comportamenti.
Questo è vero, ma non sempre si riescono a generare figli con caratteristiche sufficientemente
simili a quelle dei genitori.
La programmazione evolutiva è stata introdotta da Fogel, Owens e Walsh.
Ogni individuo della popolazione costituisce una FSM (macchina a stati finiti), ed è
formato da una serie di stati interni facenti parte di un alfabeto finito.
La caratteristica principale di una FSM è che riceve in input una serie di simboli e
restituisce in output una serie di stati, basandosi solo sugli stati correnti e 1'input.
Ebbene, 1'obiettivo della programmazione evolutiva è predire la prossima configurazione
del sistema.
L'operatore usato è quello di mutazione, che altera lo stato iniziale, modifica la transizione
o cambia uno stato interno.
La caratteristica fondamentale di questo tipo di algoritmi è che i figli hanno un
comportamento simile a quello dei genitori.
1.5 La programmazione genetica
La programmazione genetica (PG) è un’estensione degli AG, in cui gli individui che
compongono la popolazione sono costituiti non più da strutture, che codificano le
caratteristiche delle possibili soluzioni, ma da veri e propri programmi, che una volta eseguiti
calcolano tali soluzioni.
Diversi tentativi sono stati fatti per adattare gli AG all’utilizzo di un linguaggio di
programmazione tradizionale, ma è con John Koza che arriviamo ad un paradigma applicato
con successo ad una grande varietà di problemi: l’algoritmo da lui definito è quello che
chiameremo canonico, per distinguerlo dalle numerose varianti che sono state proposte
successivamente.
I programmi utilizzati possono variare dinamicamente la loro struttura e vengono
rappresentati tramite alberi di derivazione o di parsing (Figura 1.5.1), che esplicitano
l’ordine di valutazione delle primitive del linguaggio implementato.
Eseguire il programma significa quindi partire dall’operatore presente nella radice e
valutare gli argomenti che questo utilizza, cioè i nodi figli, i quali a loro volta richiederanno il
calcolo dei propri argomenti, e così via, in un processo ricorsivo che provoca una visita
anticipata dell’albero.
L’uso della sintassi del linguaggio LISP (Figura 1.5.1), dovuta all’implementazione
originaria di Koza, pur non essendo necessaria, risulta però conveniente, sia per rappresentare
gli alberi in modo lineare attraverso la notazione prefissa e le parentesi, sia per il requisito
fondamentale di chiusura delle funzioni:
Figura 1.5.1: Notazione prefissa in stile LISP ed albero di derivazione
tutte le primitive del linguaggio definito devono restituire un valore del medesimo tipo, in
modo che, generando combinazioni casuali di codice, sia sempre garantita la correttezza
sintattica del programma. Stabilito che ad ogni nodo deve sempre corrispondere un numero di
figli pari a quello dei parametri usati dall’operatore corrispondente, il requisito di chiusura
implica che si possono utilizzare come argomenti i valori restituiti da qualsiasi altra primitiva,
consentendo quindi l’inserimento di sottoalberi in un punto qualunque del programma senza
alcuna verifica di correttezza.
Lo spazio di ricerca dell’algoritmo è quindi dato dai soli alberi di derivazione col corretto
numero di figli per nodo, ed ha una dimensione finita poiché si impone a questi una
profondità massima prestabilita dai parametri di sistema: in questo modo si evita la crescita
illimitata della lunghezza dei programmi, e quindi anche della rispettiva complessità, per
evitare ovvi problemi computazionali.
La popolazione dei programmi evolve utilizzando una funzione di fitness, che definisce la
bontà di un programma, e gli operatori genetici di crossover e mutazione adattati alla
rappresentazione ad albero.
1.5.1 I parametri principali
La programmazione genetica si presenta strutturalmente più complessa rispetto agli
algoritmi genetici, dal momento che ci sono molti più parametri da considerare nella
progettazione.
Le principali scelte che devono essere valutate sono:
� la generazione della popolazione iniziale;
� l'insieme di funzioni e terminali di base;
� il tipo di selezione;
� la dimensione della popolazione e il numero massimo di generazioni;
� il criterio di terminazione.
La procedura di creazione della popolazione iniziale richiede ora di generare delle strutture
ad albero in modo casuale, con il vincolo di introdurre tanti figli per nodo quanti sono gli
argomenti dell’operatore del linguaggio a questo associato: vengono quindi introdotti nuovi
parametri di sistema che specificano la profondità minima e massima degli alberi nella
generazione iniziale ed il metodo utilizzato per la loro costruzione.
Esistono principalmente tre metodi di generazione che danno luogo ad alberi di dimensione
e forma differenti:
� utilizzando il metodo full si creano solo alberi perfettamente bilanciati dalla
profondità massima prestabilita, cioè tali che ogni ramo dello stesso livello abbia
sempre la stessa lunghezza;
� nel metodo grow si utilizza il solo vincolo delle profondità minima e massima e si
generano casualmente alberi anche sbilanciati;
� infine, si possono combinare queste due procedure in modo da creare metà
popolazione col primo metodo e l’altra metà con il secondo, facendo in modo che gli
alberi risultino anche equidistribuiti rispetto alla profondità, da quella minima a quella
massima, garantendo così un’alta diversità nella loro struttura (metodo ramped-half-
and-half).
Usando questa tecnica si riesce a creare una varietà di alberi per forma e dimensione,
instaurando un buon grado di diversità nella popolazione.
Infatti, sperimentalmente, in un gran numero di casi, con questo metodo di generazione si
sono ottenuti i risultati migliori che con gli altri due.
Un'operazione aggiuntiva utile per aumentare la diversità nella popolazione è quella di
controllare, durante la creazione, se un individuo esiste già e in caso affermativo sostituirlo
con uno nuovo.
Un programma per computer è costituito da funzioni a cui sono passati argomenti che
possono essere altre funzioni o simboli terminali (costanti, variabili, numeri random, ecc..).
Nella programmazione genetica la fase iniziale prevede la definizione dell'insieme di base
di funzioni e terminali, da cui creare la popolazione iniziale.
La scelta di questi insiemi dipende fortemente dal dominio particolare del problema; per
esempio se si vuole ricercare un'espressione matematica intera, i simboli di funzione
potrebbero essere {+, *, - , \} e i terminali {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}.
Le funzioni scelte devono soddisfare la proprietà di chiusura, già descritto, e inoltre, avere
la proprietà di sufficienza, cioè è necessario che le funzioni, unite ai simboli terminali, siano
in grado di generare una soluzione del problema.
Perciò, la scelta di un insieme di funzioni e terminali, appropriate ad un particolare
dominio, risulta essere un punto critico nella riuscita di un algoritmo di programmazione
genetica; infatti, non sempre si conosce 1'insieme di funzioni sufficienti alla risoluzione di un
problema e, del resto, sceglierne un insieme ridondante può degradare in maniera significativa
le prestazioni del sistema.
La selezione degli elementi, su cui applicare gli operatori, influenza la convergenza
dell'algoritmo; infatti, una maggiore pressione selettiva comporta una più veloce convergenza
dell'algoritmo, però può portare ad una perdita della diversità della popolazione e la soluzione
ottima potrebbe non essere mai raggiunta.
I metodi più usati sono:
� fitness proportionate selection, in cui la probabilità che ogni individuo sia
selezionato è pari alla sua fitness normalizzata;
� rank selection, in cui gli elementi sono ordinati e scelti in base alla loro fitness
relativa e non assoluta;
� tournament selection, in cui un numero fissato di elementi è estratto in modo casuale
dalla popolazione, e quello fra di essi che possiede la fitness migliore viene
selezionato.
Fra questi metodi, il primo è quello che, generalmente, comporta una minor pressione
selettiva.
La dimensione della popolazione è un altro dei parametri che influenzano la convergenza
dell'algoritmo, insieme al numero massimo di generazioni; infatti un sottodimensionamento
potrebbe portare al non raggiungimento dell'ottimo cercato e del resto, al loro aumentare,
1'algoritmo diventa computazionalmente troppo espansivo.
1.5.2 Mutazione, crossover ed altri operatori
Il passaggio da una generazione all'altra avviene attraverso 1'applicazione di definiti
operatori unari o binari ad elementi della popolazione selezionati con i metodi descritti nel
precedente paragrafo.
I suddetti operatori sono applicati in modo esclusivo con una certa probabilità; ovviamente
la somma delle probabilità sarà uguale ad uno.
8
9 FIGURA 1.5.2: UN ESEMPIO DI CROSSOVER
Nella programmazione genetica la forza trainante dell'algoritmo è costituita dall'operatore
di crossover, di gran lunga più usato rispetto agli altri operatori quali mutazione,
permutazione, editing, incapsulamento e decimazione.
L’operazione genetica di crossover (Figura 1.5.2) consiste nello scambio incrociato di
interi sottoalberi appartenenti a due programmi: selezionati i genitori, su entrambi si sceglie
un nodo con procedura casuale, quindi i relativi sottoalberi vengono scambiati tra loro
ottenendo due nuovi individui.
È importante notare che se i due genitori sono identici, i figli ottenuti tramite crossover
risultano solitamente differenti da questi, grazie al fatto che i nodi per lo scambio dei
sottoalberi sono presi in modo casuale su entrambi gli alberi.
La scelta del punto di crossover avviene con una preferenza per i nodi con figli stabilita da
un parametro di sistema, in modo da evitare il rischio di effettuare troppo spesso semplici
scambi di foglie, presumibilmente poco significativi, e favorire modifiche più incisive nella
struttura dell’albero: lo standard utilizzato prevede una preferenza del 90% sui nodi interni.
Un secondo parametro, di importanza molto minore, stabilisce il comportamento del
sistema quando gli alberi generati superano il limite di profondità massima stabilito: si può
riprovare a scegliere i punti di crossover o ripartire da una nuova selezione, o ancora si
possono restituire le copie immutate degli stessi genitori.
Di norma è utilizzato quest’ultimo comportamento, sia perché non richiede un ulteriore
peso computazionale, e sia perché la ripetizione del tentativo non porta in generale a sensibili
differenze nelle prestazioni, dal momento che può comunque capitare che il crossover
produca delle copie dei genitori, il che avviene quando sono scelti per lo scambio sottoalberi
tra loro identici.
Figura 1.5.3: Un esempio di mutazione
L’operatore genetico di mutazione (Figura 1.5.3) consiste nell’inserire del nuovo codice
nel programma: selezionato il genitore, si sceglie in modo completamente casuale un suo
nodo, quindi il relativo sottoalbero viene sostituito da un altro generato sempre con procedura
casuale. La mutazione può essere implementata in modo che, nel caso il nuovo albero superi
il limite massimo di profondità, venga restituita una copia del genitore oppure sia ripetuta da
capo l’operazione; un ulteriore parametro specifica la profondità massima del nuovo
sottoalbero da inserire, il quale verrà creato con la stessa procedura casuale utilizzata per la
generazione iniziale. In generale si può considerare la mutazione come una specializzazione
del crossover per cui solitamente non è usata nell’algoritmo canonico: a differenza del
comportamento osservato per il crossover negli AG, la sua implementazione sugli alberi non è
legata alla perdita di diversità, ed in particolare è assai poco probabile che una primitiva
sparisca completamente e necessiti di essere reintrodotta tramite mutazione. In realtà, però, la
dinamica evolutiva può portare comunque a vistose cadute del tasso di diversità e l’uso della
mutazione, in una delle tante varianti proposte, è un possibile metodo per attenuare questo
processo.
La permutazione è attuata scegliendo un nodo interno (una funzione) a caso dell'albero; se
la funzione ha k argomenti, è estratta una delle k! permutazioni e gli argomenti sono
arrangiati di conseguenza. Ovviamente, se la funzione è commutativa l'operatore non ha
nessun effetto.
L'editing permette di semplificare un sottoalbero valutando 1'espressione risultante: ad
esempio, (OR X X) può essere sostituita con X.
L'operazione può essere effettuata durante la fase di run con una certa frequenza (ogni
generazione o più raramente) o sul risultato finale.
Nel primo caso, il suo utilizzo potrebbe causare una prematura convergenza dato che le
espressioni semplificate sono più soggette ad un effetto distruttivo del crossover.
Con 1'incapsulamento, un sottoalbero, che parte da un nodo interno di un albero
selezionato, è scelto e compilato; quindi tale sottoalbero va a far parte dei terminali e non è
soggetto perciò all'effetto distruttivo del crossover. In tal modo sono salvate le funzioni più
interessanti.
La decimazione seleziona una certa percentuale (ad esempio il 20%) della popolazione in
una determinata generazione, in base ai valori della fitness, senza permettere duplicati; il resto
della popolazione è cancellata.
Questo risulta utile per mantenere la diversità in quei casi dove pochi elementi hanno una
fitness molto più elevata degli altri e questi avrebbero il sopravvento in poche generazioni.
Come accennato in precedenza, fra i vari operatori, il più usato è senz'altro il crossover
che, diversamente dagli algoritmi genetici, basta da solo a garantire il mantenimento della
diversità; in genere è usato in congiunzione con la mutazione che ha una probabilità minore;
gli altri operatori sono usati solo in casi particolari, nei quali è possibile sfruttare uno dei
vantaggi descritti in precedenza.
Gli operatori sopracitati garantiscono la correttezza sintattica degli alberi generati, grazie
alla proprietà di chiusura, ma non la correttezza semantica. Sarà la fitness, penalizzando gli
alberi che non rispettano le regole semantiche, a favorire la crescita di alberi semanticamente
corretti.
1.5.3 L’aumento della complessità nella programmazione genetica
La complessità di un programma generato cresce con il passare delle generazioni, se non si
adottano strategie per limitarla, producendo effetti negativi, in quanto il programma risulta di
difficile interpretazione e non generalizza bene, in accordo al principio del rasoio di Occam,
che sostiene che le soluzioni più semplici sono da preferire in quanto evitano 1'aggiunta del
superfluo e, quindi, sono applicabili ad una casistica più generale.
Il fenomeno del bloating è una delle cause dell’aumento di complessità dei programmi;
esso indica 1'utilizzo di una quantità di simboli in eccesso rispetto a quelli effettivamente
necessari a esprimere un concetto,ad esempio:
log e + log e + log e al posto della semplice espressione 3 * log e.
Fra le cause scatenanti del bloating hanno particolare rilievo i cosiddetti introns, che
prendono il nome da quella parte del DNA che non viene trascritta nelle proteine.
In GP, gli introns sono definiti come una parte dell'albero del programma che non altera il
fenotipo, cioè non è utilizzato nel calcolo della fitness, come ad esempio i1 blocco (X AND
X) che non modifica la semantica del risultato.
Eliminare questa parte superflua del codice potrebbe sembrare la soluzione migliore per
ovviare ai problemi sopracitati, ma ciò potrebbe comportare effetti collaterali negativi. Vari
studi ed esperimenti hanno evidenziato un effetto positivo di questo fenomeno, nel preservare
soluzioni interessanti dagli effetti distruttivi del crossover.
Infatti, se un programma contiene parti non utilizzabili per il calcolo della fitnesss, sarà più
probabile che il crossover non alteri la fitness di tale programma. Perciò, 1'eliminazione o
meno degli introns deve essere attentamente considerata nel ridurre la complessità del
sistema.
In genere, per ottenere soluzioni più semplici, si agisce sui parametri del sistema fissando
la profondità massima degli alberi o si aggiunge alla fitness una funzione di parsimonia che
favorisca gli alberi più corti o, ancora, si modificano gli operatori genetici in modo da ridurre
le ridondanze. Limitare la profondità massima può essere difficoltoso se non si conosce la
dimensione della soluzione del problema, rischiando così di escluderla perché fuori dal limite
accettato. Ancora, aggiungere la funzione di penalizzazione alla fitness (parsimonia) non
favorisce lo sviluppo naturale del sistema e può penalizzare la ricerca della soluzione ottima.
In definitiva, la scelta di utilizzare alberi più o meno complessi resta un problema aperto,
dal momento che la riduzione dell'albero se da una parte genera soluzioni più compatte e
generali, dall'altra può portare ad una prematura convergenza e al non raggiungimento della
soluzione ottimale, cosa che non può prescindere dal dominio particolare.
1.5.4 Differenze e analogie fra GP e GA
Il genotipo degli algoritmi genetici è, in genere, rappresentato con una stringa di lunghezza
prefissata di bit, mentre nella programmazione genetica esso è costituito da un albero formato
da funzioni e terminali, di lunghezza variabile, anche se, solitamente, è fissata una profondità
massima che ne limita la dimensione. Niente vieterebbe, quindi, di rappresentare un albero di
GP, sicuramente limitato in dimensione da una profondità massima o da necessità di
implementazione, con una stringa di bit di lunghezza fissata pari a quella massima dell'albero,
cioè di utilizzare un algoritmo genetico.
Necessità di ordine pratico rendono più facile e naturale usare GP per far evolvere
programmi per computer che non GA. Da non sottovalutare il fascino naturale, che far
evolvere programmi per computer, invece che innaturali stringhe di bit senza significato,
esercita sull'uomo e rende più spontaneo la scelta della programmazione genetica.
L'operatore prevalente è, in entrambi i casi, quello di crossover, ma mentre in GP esso è
sufficiente da solo a mantenere la diversità nella popolazione, in GA è la mutazione che si
sobbarca questo compito, in modo da evitare una prematura convergenza, con 1'introduzione
di nuovi elementi per la ricerca o il recupero di elementi precedentemente persi. Nella
programmazione genetica, il crossover fra più alberi può introdurre una nuova semantica negli
alberi generati, spostando la ricerca verso un'altra zona, più di quanto possa fare l'analogo
operatore degli algoritmi genetici rendendo superflua la mutazione.
Un altro fattore importante di differenza fra i due metodi è la sensibilità ai parametri che
sono spesso di fondamentale importanza per la convergenza di un algoritmo genetico, mentre
non influenzano di molto il raggiungimento di una soluzione ottima per 1a GP; per essa 1a
fase di tuning dei parametri risulta più semplice e non necessita di molto sforzo.
Un altro punto da tenere in considerazione è che 1'interpretazione di un allele dipende
fortemente dalla sua posizione nella stringa di bit, mentre un istruzione per computer ha lo
stesso significato indipendentemente dalla posizione anche se, ovviamente, può generare
comportamenti semantici diversi; perciò questo aspetto facilita l'emergere di dinamiche
naturali insite nel processo evolutivo della programmazione genetica.
1.6 I modelli per la programmazione genetica
Prima di cominciare a descrivere la programmazione genetica, vediamo alcune definizioni
che possono tornare utili allo scopo:
Definizione 1.6.1: Lo speedup di un algoritmo parallelo è il rapporto fra il tempo
ottenuto dal miglior algoritmo sequenziale e quello impiegato dall'algoritmo parallelo
fatto girare su p processori; il caso ideale è quello di speedup lineare.
Definizione 1.6.2: L'efficienza è data dal rapporto fra lo speedup e il numero di
processori ed è un indice della frazione di tempo utilmente speso dai processori.
Definizione 1.6.3: Un sistema parallelo si dice scalabile se 1'efficienza può essere
mantenuta costante, all'aumentare dei processori, aumentando anche la dimensione del
problema. Se variando in modo lineare il numero di processori, basta variare in modo
lineare anche la dimensione del problema e 1'efficienza rimane costante, allora il sistema
si dice altamente scalabile.
La programmazione genetica e gli algoritmi evolutivi in genere hanno bisogno di grandi
popolazioni per ottenere una buona convergenza; questo aspetto, unito al fatto che calcolare la
fitness di un singolo individuo richiede la valutazione su un certo training set, porta ad uno
sforzo computazionale notevole.
Inoltre, nel caso particolare della programmazione genetica, l'albero del programma può
anche essere molto complesso e può quindi dare problemi di complessità spaziale, oltre a
quelli di complessità temporale, in quanto può superare la capacità di memorizzazione di una
singola macchina.
Una soluzione al problema della memoria insufficiente è quella di realizzare
un’implementazione parallela di questo tipo di algoritmi.
Questo compito risulta facilitato dal fatto che la programmazione genetica è
implicitamente parallela, in quanto la fase più dispendiosa dell’algoritmo, che è la valutazione
della fitness di ogni individuo, è indipendente e può avvenire su processori diversi. Il
problema, invece, è la selezione degli individui più adatti all’interno della popolazione, la
quale ha bisogno di accessi remoti che riducono la possibilità di avere speed-up lineari
dell’algoritmo parallelo.
I metodi per parallelizzare un algoritmo evolutivo, possono essere classificati in tre
tipologie: modello globale , modello a isole e modello diffusivo o cellulare.
� Il modello globale, in cui si ha sempre una singola popolazione, come nella versione
sequenziale, ma la valutazione della fitness degli individui è eseguita in parallelo,
dividendo il carico fra i processori disponibili, mentre il crossover e gli altri operatori
sono applicati in sequenziale. La selezione è quella classica che agisce su tutta la
popolazione, da qui il nome globale. Questo tipo di modello ha il vantaggio di
preservare il comportamento dell'algoritmo sequenziale anche in parallelo, ma la fase
di selezione richiede frequenti accessi all'intera popolazione e il conseguente overhead
di comunicazione risulta inaccettabile a meno che non ci siano funzioni di fitness
particolarmente pesanti da valutare.
� Un'alternativa al modello precedente è il modello ad isole, che utilizza un diverso
meccanismo di selezione limitato agli individui posti su un unico processore; la
popolazione è suddivisa in sottopopolazioni, dette isole, che sono distribuite, di solito,
una per processore. L'implementazione di tale modello prevede la creazione di
sottopopolazioni random all'interno di ogni singolo nodo; i vari operatori genetici e la
valutazione della fitness vengono eseguiti a livello locale. In tal modo l'algoritmo
presenta una scalabilità quasi lineare e risulta di facile implementazione: infatti, è
sufficiente lanciare un processo dell'algoritmo per ogni nodo. Una variante di tale
modello prevede la migrazione di elementi da una sottopopolazione all'altra, affinché
si introduca diversità nelle isole e si garantisca una convergenza migliore. Un aspetto
ancora non ben compreso di questo modello è se, in effetti, riesca a mantenere la
stessa convergenza di quello sequenziale classico.
� Nel modello diffusivo, ogni individuo k è sostituito, nella successiva generazione, da
un nuovo elemento generato applicando l'operatore di crossover o mutazione ad
elementi appartenenti al vicinato di k stesso. La selezione ristretta ad un vicinato
locale rende l'implementazione parallela molto efficiente, dal momento che le
comunicazioni sono limitate solo ai vicini e, di conseguenza, non c'è un overhead
eccessivo. Inoltre, soluzioni sub-ottime non si diffondono velocemente all'interno
dell'intera popolazione, favorendo lo svilupparsi di nicchie in cui si esplorano diverse
porzioni dello spazio di ricerca e viene evitata una prematura convergenza. Inoltre,
rispetto al modello ad isole, si può notare una maggiore stabilità riguardo ai parametri
di progettazione.
Dal momento che un modello di questo tipo è equivalente ad un automa cellulare,
esso è detto anche modello cellulare.
1.6.1 L'automa cellulare
Dalla sezione precedente si evince che 1'automa cellulare si presenta come il modello
ristretto più adatto per 1'implementazione di un modello diffusivo.
Gli automi cellulari sono stati introdotti da John Von Neumann e Stan Ulam, nel 1950 e,
in origine, sono stati utilizzati per lo studio di sistemi di autoriproduzione.
Un automa cellulare è formato da un insieme di elementi, detti celle, che sono disposti in
una qualche forma geometrica, a una, due o tre dimensioni.
Ad ogni cella è associato uno stato che possiede un valore compreso in un insieme finito.
Ogni cella evolve, secondo istanti discreti di tempo, da un certo valore dello stato ad un altro,
secondo una regola locale, detta funzione di transizione.
Il valore dello stato di una singola cella, al tempo T+1, dipende solamente dai valori dello
stato delle adiacenti e della cella stessa, al tempo T.
9.1.1 Figura 1.6.1: Evoluzione di un semplice automa cellulare monodimensionale
L'insieme delle celle che influenzano lo stato di una cella c vengono dette vicinato della
cella c.
La figura 1.6.1 esemplifica il funzionamento di un automa cellulare.
Quello visibile in figura è monodimensionale, come si può vedere, e le adiacenti di ogni
cella sono le due più vicine.
Lo stato di una cella è un intero e viene aggiornato facendo la somma del valore della cella
stessa e delle adiacenti, come si può notare dall'illustrazione.
La topologia è toroidale, cioè 1'adiacente della prima cella è 1'ultima e viceversa.
Gli automi cellulari sono usati per simulare fenomeni fisici e lo stato di una cella
rappresenta alcune caratteristiche (per esempio temperatura, pressione, ecc..) della zona di
spazio corrispondente alla cella stessa.
Diamo, di seguito, alcune definizioni formali di automa cellulare.
Definizione 1.6.1: Un automa cellulare è una quadrupla A = (E n, X, S, f) dove:
� E n è l' insieme delle celle appartenenti a uno spazio euclideo a n dimensioni e
ciascuna di esse può essere rappresentata come un punto a coordinate intere.
� X è l'indice di vicinato, cioè un insieme finito di vettori a n dimensioni che
rappresentano le coordinate di una cella; esso permette di definire l'insieme
N(X,i) dei vicini della cella i = < i1, i2, i3, … , in > in questo modo: sia X = { x1, x2,
x3 , … xm-1 } dove m è la cardinalità di X; allora N(X,i) = { i + x0 , i + x1 , i + x2 ,
... , i + xm-1) e x0 è il vettore nullo che permette di definire la cella stessa.
� S è l' insieme degli stati dell'automa: esso è costituito dal prodotto cartesiano S1 x
S2 x S3 x ... x Sk dove ciascun Si è l'insieme dei valori che ciascun sottostato può
assumere.
� f: Sm � S è la funzione di transizione dell'automa.
Definizione 1.6.2: La funzione ct : En � S, dove c(i) è lo stato della cella i, è detta
configurazione dell'automa cellulare.
La particolare struttura dell'automa cellulare consente un'efficiente implementazione
parallela ed è perciò stato scelto come modello per la realizzazione dell'ambiente parallelo di
programmazione genetica.
Nel caso della programmazione genetica, ogni cella dell'automa cellulare corrisponde ad
un individuo della popolazione.
La funzione di transizione di una cella applica uno dei classici operatori genetici agli
individui della popolazione nei modi visti in precedenza; gli elementi selezionati sono, però,
la cella stessa e (nel caso di operatori binari) uno dei suoi vicini, scelto secondo un criterio
opportuno. Inoltre restituisce 1'individuo prodotto dall'operatore (o uno dei due individui
prodotti nel caso di operatore binario) che, quindi, diventa il sostituito alla cella stessa nella
nuova popolazione.
1.7 CAGE ( CellulAr GEnetic programming tool )
CAGE è un’applicazione parallela per lo sviluppo della programmazione genetica basata sul
modello cellulare.
La popolazione degli individui è organizzata in una matrice a due dimensioni ed è suddivisa
tra i vari processori (Figura 1.7.1) in sezioni longitudinali, in modo che il carico di lavoro sia
bilanciato tra le parti. Inoltre, ogni processore ha due bordi, contenenti un numero di individui
pari alla larghezza della matrice, che conterranno il vicinato sinistro e destro del processore.
Figura 1.7.1: La griglia della popolazione divisa tra tre processi
La scelta di questa suddivisione è dettata da problemi di efficienza, dal momento che, su
calcolatori reali paralleli, con elevati tempi di startup delle comunicazioni, è quella che
garantisce maggiore scalabilità. La creazione degli individui, l’applicazione degli operatori
genetici e la valutazione della fitness avviene localmente su ogni processore, relativamente
alla porzione di popolazione presente in esso.
1.7.1 Architettura software di CAGE
L’architettura dell’ambiente è formata da una serie di processori disposti logicamente ad
anello (Figura 1.7.2), in cui le comunicazioni sono ristrette ai processori vicini localmente e
l’ultimo processore costituisce il vicino del primo; quindi tale struttura è adatta ad essere
gestita come una topologia ad anello, che, comunque, è abbastanza efficiente su un fat-tree.
Figura 1.7.2: Architettura software di CAGE
Ogni processore è uno slave ( valuta la fitness della sua porzione di popolazione, applica i
vari operatori, ecc.), mentre uno di essi, detto master, oltre a queste funzioni ne deve svolgere
altre aggiuntive ( scrive i report per ogni generazione e il report finale,manda in output la
messaggistica di errore, ecc.).
Lo schema di funzionamento dell’algoritmo parallelo è il seguente:
1. Ogni processore legge i parametri dell'algoritmo da file e da riga di comando.
2. Ogni processore alloca e inizializza le strutture di base, quindi crea la propria porzione
di popolazione secondo i parametri specificati e ne valuta la fitness iniziale.
3. Per ogni generazione fino alla finale o al soddisfacimento del criterio di terminazione
sono eseguite le seguenti operazioni da ogni processore (se non specificato altrimenti):
� sono caricati e scambiati i bordi dei processori vicini, in modo che la fase di
selezione locale possa avvenire senza ulteriori comunicazioni, anche negli
individui appartenenti alla striscia verticale di confine fra i processori;
� ad ogni individuo appartenente al processore, a secondo della probabilità
specificata è applicato un determinato operatore selezionando, nel caso di operatori
binari, anche un altro individuo, scelto fra quelli appartenenti al vicinato di Moore
(quello che presenta la fitness migliore);
� l'elemento risultante dall'applicazione dell'operatore è inserito nella nuova
popolazione; nel caso del crossover, che genera due figli, la scelta dell'elemento da
sostituire dipende dal parametro specificato a verrà descritto dopo;
� viene caricata la nuova popolazione;
� è valutata la fitness della nuova popolazione;
� gli slave valutano 1'elemento della popolazione avente fitness minima e il master
riceve l'elemento minimo di tutti i processori a stampa il report della generazione.
4. Il master genera il report dell'intero run dell'algoritmo, quindi vengono liberate tutte le
strutture dati.
CAGE si basa su SGPC (Simple Genetic Programming in C), uno dei migliori tool
relativi alla programmazione, in quanto ad efficienza, portabilità, leggibilità del codice e
facilità d’uso. SGPC è stato scritto da Aviram Carmi e Walter Alden Tackett in linguaggio C,
il che rende l’ambiente altamente compatibile ed efficiente e permette di definire le funzioni e
i terminali della programmazione genetica in un file denominato setup.c, mentre in un altro
file, chiamato fitness.c, è possibile definire la funzione di fitness dell’algoritmo e un
eventuale criterio di terminazione prima del raggiungimento del numero massimo di
generazioni. Il makefile rende possibile il linkaggio di tutti i file e costruisce l’eseguibile. I
parametri, se diversi da quelli indicati per default, possono essere scritti in un file definito
dall’utente, specificato a tempo di esecuzione. L’ambiente supporta la maggior parte dei
parametri utilizzati nella programmazione genetica classica, in particolare segnaliamo i
seguenti:
� seed (il seme random utilizzato per generare i numeri casuali, laddove sia
necessario);
� population_size (la dimensione della popolazione);
� steady-state (introduce la possibilità di avere un algoritmo a stato fissato);
� max_depth_for_new_tree (la profondità massima dei nuovi alberi creati);
� max_depth_after_crossover (la profondità massima degli alberi generati dal
crossover);
� max_mutant_depth (la profondità massima degli sottoalberi creati per l’operazione
di mutazione);
� grow_method (il metodo di creazione della popolazione: sono supportati il GROW, il
RAMPED e il FULL);
� selection_method (i metodi per la selezione della popolazione: è possibile utilizzare
FITNESSPROP oppure TOURNAMENT);
� tournament_k (il numero degli individui che partecipano al torneo, nel caso il
metodo di selezione sia il k-tournament);
� crossover_func_pt_fraction ( la probabilità di crossover che sceglie solo punti
interni degli alberi) ;
� crossover_func_any_fraction ( la probabilità di crossover che sceglie fra qualsiasi
punto degli alberi, anche un terminale);
� fitness_prop_repro_fraction ( la probabilità di riproduzione);
� parsimony_factor (un fattore di parsimonia che è aggiunto, moltiplicato per la
dimensione dell’albero soluzione, alla funzione di fitness definita dall’utente, in modo
da facilitare l’evoluzione di soluzioni più semplici).
E’ possibile specificare il numero massimo di generazioni eseguite e il numero di isole;
infatti SGPC consente di simulare il funzionamento di un algoritmo di GP distribuito secondo
il metodo ad isole, però su di un singolo processore. Inoltre, il tool rende possibile l’utilizzo di
costanti random secondo quanto descritto da Koza e consente di definire oltre alle normali
funzioni, anche macro, il che rende il codice molto efficiente.
CAGE conserva tutte le caratteristiche di SGPC, con l’aggiunta di alcune modifiche. Il tool
CAGE è basato su una griglia a due dimensioni. Inoltre, per ogni computazione locale vi sono
due buffer in più e le comunicazioni sono state implementate tramite le librerie di MPI. Per di
più CAGE supporta una differente strategia di selezione e sostituzione, specificate nel
parametro selection_method. Per l’operatore di crossover ci sono le seguenti strategie:
� EVERCHANGE seleziona il miglior individuo del vicinato di Moore e sostituisce
sempre il vecchio individuo con uno dei figli a caso.
� GREEDY sostituisce il vecchio individuo solo se il migliore dei due figli presenta una
fitness migliore del padre.
� SA adotta una strategia di sostituzione di tipo simulated annealing, tuttora non
implementata.
E’ possibile poi specificare i parametri width e height, che rappresentano rispettivamente la
larghezza e l’altezza della matrice dell’automa sotto il vincolo che il loro prodotto sia pari alla
dimensione della popolazione.
1.7.3 Dettagli dell’implementazione
La popolazione dell’automa è implementata come una matrice bidimensionale di alberi, ed
è divisa fra i processori secondo la dimensione longitudinale come illustrato in Figura 1.7.2.
Questa scelta, alternativa a quella di implementare l’automa come un vettore, rende più
naturale la programmazione delle varie funzioni, dal momento che rende l’accesso ad un
determinato elemento della popolazione immediato.
Si è scelto di decomporre la popolazione in una dimensione per motivi di efficienza.
Infatti, con una decomposizione a due dimensioni si hanno un numero di messaggi inviati alto
con una dimensione piccola, mentre con una decomposizione a una dimensione si hanno un
numero di messaggi piccolo con dimensione grande. Considerando che il tempo di startup è
più grande del tempo di trasferimento, il secondo approccio è più efficiente rispetto al primo.
L’operazione cruciale dell’implementazione parallela è la selezione dell’elemento su cui
applicare l’operazione di crossover che avviene a livello locale, in modo da minimizzare le
comunicazioni tra i processori; in pratica tale elemento viene scelto fra i vicini di raggio
unitario della cella stessa. Se la cella è interna alla porzione di automa contenuta nel singolo
nodo non vi sono problemi, ma se essa si trova sul bordo ha bisogno di accedere ad individui
che si trovano su altri nodi. Si è scelto, allora, di aggiungere alla matrice una coppia di bordi
(destro e sinistro) che contenga i vicini delle celle situate ai confini con gli altri nodi. Allora,
all’inizio di ogni generazione, in maniera sincrona, i bordi sono riempiti con gli individui
provenienti dai processori vicini. In questo modovi è un aumento, anche se limitato, della
memoria necessaria alle strutture dati, ma si riducono notevolmente gli overhead temporali
delle comunicazioni, dal momento che si scambiano tutti gli individui in una sola
comunicazione minimizzando il tempo di startup.
CAGE è basato su SPMD (Single Program Multiple Data). SPMD è lo stile dominante
dei programmi paralleli,dove ogni processore usa lo stesso programma benché ciascuno abbia
i propri relativi dati. Per un’effettiva implementazione, i dati sono partizionati in modo che le
comunicazioni siano locali e il carico computazionale sia condiviso tra gli elementi
processanti con una determinata strategia di bilanciamento. In questo approccio non si
assegna solo un individuo ad un processore, ma gli individui sono raggruppati in “fette” e
viene assegnata una “fetta” (slice) della popolazione a un nodo.
Il programma concorrente che implementa l’architettura di CAGE è composto da un set di
identici processi slice. Non è richiesta nessuna coordinazione perché il modello di
computazione è completamente decentralizzato. Ogni processo slice gira su una singola unità
elaborativi della macchina parallela.
La dimensione della sottopopolazione di ogni processo slice è calcolata dividendo la
popolazione per il numero di processori su cui CAGE è eseguito. Le variabili weight e height
definiscono la grandezza dei bordi della sottogriglia 2D.
Le comunicazioni sono state implementate adoperando le primitive della libreria MPI, che
non prevedono la possibilità di utilizzare strutture predefinite a puntatori.
Perciò, per inviare i bordi, che sono costituiti da una serie di alberi, si possono seguire due
strade alternative:
� Creare una particolare struttura, che preveda di mandare i puntatori in forma di
numeri e poi ricostruirla in fase di ricezione.
� Trasformare opportunamente un albero in un array di un tipo predefinito.
Si è scelto di seguire questa seconda strada, realizzando un pack dell’albero prima della
spedizione e un unpack a ricezione avvenuta.
Si è scelta questa seconda possibilità perché non si introduce ridondanza nell’informazione
da trasferire e inoltre i tempi per realizzare il pack e l’unpack sono trascurabili. L’operazione
di pack viene realizzata visitando l’albero in ampiezza e assegnando a locazioni successive
del vettore un carattere corrispondente ad una particolare funzione o terminale; gli alberi sono
posti in un unico array di caratteri, uno di seguito all’altro senza buchi, con associato un altro
array, che memorizza le lunghezza in sequenza degli alberi,in modo che sia possibile
ricostruire la sequenza originaria degli alberi. In questo modo si riesce, con due sole
comunicazioni, a trasferire un bordo dell’automa, minimizzando i tempi di startup e non
introducendo un eccessivo aumento di informazione. I processori sono connessi in accordo
all’architettura ad anello: ogni processo ha due buffers per inviare (SRbuf, SLbuf) e due
buffers per ricevere (RRbuf, RLbuf). Lo scambio dei dati avviene con due operazioni di invio
asincrone seguiti da due operazioni di ricevimento asincrone da destra a sinistra tra processi
vicini. L’esecuzione di un programma parallelo è composto da due fasi: computazione e
comunicazione; durante la computazione vengono manipolati i dati locali da parte dei
processi, questi dati possono essere variabili locali o dati ricevuti dai bordi vicini; durante la
computazione non sono scambiati dati.
Per poter eseguire una distribuzione computazionale equilibrata tra i nodi in CAGE è
introdotta una intelligente partizione della griglia. La strategia di partizione è basata su una
decomposizione block-cycling. L’idea è di dividere la griglia virtuale in un numero di campi
e assegnare uguali parti di ogni campo a ognuno dei processi (come illustrato in Figura
1.7.3).
Figura 1.7.3: Strategia di bilanciamento del carico
1.7.4 Esempi di programmazione genetica applicati al modello cellulare
Per verificare la bontà del modello cellulare per la programmazione genetica e per studiare
la scalabilità del sistema con problemi di diversa complessità, utilizzeremo tre esempi: la
regressione simbolica, il cosiddetto even-4-parity e la classificazione, descritti in Koza.
Il problema della regressione simbolica, ha come obiettivo la produzione di una funzione
che si adatti ad un insieme di punti.
Cioè, dato un insieme di punti del tipo (x,y), bisogna trovare una funzione che, per ogni
valore di x, restituisca il valore di y appropriato.
Nel nostro esempio, abbiamo utilizzato un insieme di 20 punti scelti in modo casuale
nell'intervallo [-1,1], come ascissa, e come ordinata abbiamo preso il valore della funzione y =
2.178 x2 + 3.1416 x applicata ai punti dell'intervallo:
� come insieme di funzioni della programmazione genetica abbiamo preso le funzioni
classiche {+,*,-,%}, dove % sta per la divisione protetta, cioè una divisione fra due
argomenti che ritorna il valore 1 se il denominatore è zero, per rispettare la proprietà
di chiusura;
� i terminali sono costituiti dalla variabile x e da una costante random, che permette di
generare i coefficienti che approssimano 1'equazione da ricavare;
� la funzione di fitness è data dalla somma delle differenze fra il valore dato dalla
funzione generata e quello della funzione esatta, calcolato su tutti i 20 punti scelti.
La even-4-parity è una funzione di tipo booleano che riceve in input quattro argomenti
(d0, dl, d2, d3) e ritorna un valore di verità solo se un numero pari di questi argomenti è vero,
altrimenti restituisce un valore di falsità.
Per addestrare il nostro algoritmo utilizziamo tutte le 16 combinazioni vero, falso possibili
con i 4 argomenti suddetti e la soluzione esatta dovrebbe verificare tutte le combinazioni:
� l'insieme delle funzioni è dato da {AND, OR, NAND, NOR} che consente di
rappresentare qualsiasi funzione booleana esistente;
� il set di terminali è dato, ovviamente, dagli argomenti di input { d0, d1, d2, d3};
� la funzione di fitness è data dalla somma, su tutte le combinazioni, della distanza di
Hamming fra il programma generato a il valore booleano corretto.
Il processo della classificazione consiste nell'assegnare differenti oggetti (tuple) di una
base di dati ad alcuni gruppi predeterminati, detti classi, a seconda del valore degli attributi.
Uno dei metodi per realizzare questo compito è quello di costruire alberi di decisione, che
permettano di assegnare un oggetto ad una determinata classe, semplicemente visitando
1'albero stesso.
Con la programmazione genetica è possibile costruire questo tipo di alberi, utilizzando
come:
� funzioni di base gli attributi degli oggetti;
� come simboli terminali le classi predefinite;
� la fitness sarà data dalla somma del numero di tuple non classificate correttamente su
un insieme di esempi, detto training set, di cui si conosce già la classificazione
corretta.
PARAMETRI DI PCGP 9.1.1.1 PROBLEMA 9.1.1.1.1 REG 9.1.1.1.2 EVEN-4-
9.2 CLASSIFICAZISimboli terminali {X, R} {d0, dl, d2, d3} classi Funzioni {+, - , * , %} {AND, OR, NAND, attributi Population_size 800 800 800 Width 40 40 40 Height 20 20 20 Max_depth_for_new_tree 5 5 5 Max_depth_after_crossove 6 6 6 Grow_method RAMPED RAMPED RAMPED selection_method EVERCHANG EVERCHANGE EVERCHANGE Crossover_func_pt_fractio 0.1 0.1 0.1 Crossover_any_pt_fraction 0.8 0.8 0.8 Fitness_prop_repro_fractio 0.1 0.1 0.1 Parsimony_factor 0.0 0.0 0.0
9.2.1 Tabella 1.7.1: I parametri usati negli esperimenti
Gli esperimenti sono stati condotti usando i parametri descritti nella tabella 1.7.1 e facendo
eseguire 1'algoritmo per 200 generazioni. In tabella 1.7.2 sono riportati i tempi di esecuzione
per gli esperimenti descritti sopra, variando il numero di processori (l, 2, 4, 8, 10) e mediando
su 5 prove, con seme random diverso.
La terminazione dell'algoritmo con un numero di generazioni fissate e non con il
raggiungimento di una soluzione ottima o subottima, ci permette di calcolare la scalabilità del
sistema in maniera indipendente da variazioni casuali che si avrebbero se le prove
raggiungessero la soluzione in un numero di generazioni differenti l’una dall'altra.
NUMERO DI PROBLEMA PROCESSORI REGRESSIONE EVEN-4-PARITY CLASSIFICAZIONE Tempo di esecuzione
1 179.37 182.24 1216.9 2 94.35 93.62 652.6 4 47.87 49.90 334.3 8 24.75 26.70 174.1 10 20.18 19.27 131.5
Tabella 1.7.2: Tempi di esecuzione dei tre esperimenti
I risultati sperimentali, effettuati su questi tre esempi hanno mostrato speed-up quasi lineari
che confermano l'elevata scalabilità di questo tipo di implementazione.
1 I test sono stati svolti su macchina parallela CS-2, di tipo MIMD, costituita da 6 nodi; ciascun nodo è formato da due processori
HyperSparc a 200 Mhz e una memoria RAM da 256 Mbyte. La rete di interconnessione è un fat-tree con larghezza di banda di 50 Mbytes/s
per ogni direzione.
Uno studio teorico della scalabilità e della complessità del sistema è stato portato avanti,
applicando il metodo della funzione di isoefficienza di Kumar ad un calcolatore reale,
sfruttando le misure e i modelli ricavati per le primitive MPI sulla CS-2 e si è ottenuta
un'espressione dello speed-up e dell'efficienza in funzione della dimensione del problema e
del numero di processori. L'utilizzo di tale modello permette di stabilire a priori il numero di
processori realmente utilizzabili per un'applicazione e verificarne l'efficienza, soltanto
stimando empiricamente alcuni parametri del sistema.
1.8 Un’ implementazione distribuita della programmazione genetica
Come abbiamo già notato gli algoritmi di programmazione genetica godono delle seguenti
proprietà:
� parallelismo implicito: il carico della computazione può essere suddiviso fra vari
processori, che fanno evolvere la popolazione autonomamente, introducendo diversità
più o meno frequentemente, per non avere una convergenza precoce, a seconda che si
usi il modello globale, a isole o diffusivo;
� efficienza: i modelli di parallelizzazione dell’elaborazione, danno enormi vantaggi per
quanto riguarda i tempi di esecuzione, rispetto a quelli sequenziali;
� scalabilità: l’efficienza rimane costante all’aumentare dei processori e della
dimensione del problema.
Abbiamo anche visto che un’implementazione parallela della programmazione genetica,
oltre che dividere il notevole sforzo computazionale fra diversi processori, risolve il problema
della memorizzazione degli alberi di programma, che possono essere anche molto complessi e
quindi richiedere una capacità di memorizzazione che va oltre quella di una singola macchina.
Tuttavia, i modelli di parallelizzazione possono essere utilizzati anche su architetture
distribuite, cioè nel caso in cui l’elaborazione della fitness avviene su macchine diverse
collegate in rete; in questo caso, però, si accentua ancora di più il problema dell’overhead sul
tempo di elaborazione, dovuto ai tempi di latenza delle comunicazioni lungo la rete.
Per questo occorre fare una scelta oculata del modello di elaborazione, in modo da rendere
trascurabile il problema.
Per rendere ininfluenti tali ritardi nell’elaborazione, e nello stesso tempo mantenere le
caratteristiche di parallelismo, efficienza e scalabilità, si deve scegliere un’architettura che sia
esente da sincronizzazione2, non è necessario quindi che sia centralizzata come,ad esempio, il
client-server, e che sia robusta, cioè resistente ai cambiamenti che sono frequenti in una rete
come Internet, per garantire che l’elaborazione sia portata a termine con successo.
Da qui nasce l’idea di un’implementazione distribuita peer to peer della
programmazione genetica, di cui noi vogliamo mostrare i vantaggi.
2 La sincronizzazione va bene nel caso di un’architettura parallela, ma è inaccettabile nel caso di architetture distribuite.
CAPITOLO 2
IL PEER-TO-PEER:
UNA NATURALE SOLUZIONE PER LA PROGRAMMAZIONE GENETICA
2.1 Cos'è il peer-to-peer?
E' l'acronimo di Peer-to-Peer, dove i peer non sono altro che normali PC usati da normali
utenti per navigare su Internet, inviare e ricevere E-mail, scrivere documenti, ecc.
L'architettura peer-to-peer e' una architettura nella quale i computer connessi ad Internet
possono condividere capacità di calcolo, spazio su disco e ogni tipo di risorsa in genere,
senza l'utilizzo di un server centrale. Proprio l'assenza di un server centrale che amministri
le connessioni e le risorse è la chiave di volta. La tecnologia Peer-to-Peer evolve l'ambiente di
elaborazione centralizzato pre-esistente ad un nuovo livello di infrastruttura software, creando
una rete "point-to-multipoint" tra host equivalenti (livello utente/applicazione) che si
comportano, a seconda della situazione, da server o da client. Riflettiamo, ad esempio, sulle
possibilità offerte alle funzioni di immagazzinamento e/o di condivisione dati di un sistema o
di una rete. In una architettura tradizionale, un server può fornire le informazioni presenti sui
propri hard-disk.
Di quanto stiamo parlando? Cinquanta dischi da cento gigabyte? Qualunque sia l'effettiva
capacità di immagazzinamento gestita da un server, non sarà mai paragonabile alle possibilità
offerte da una architettura p2p, nella quale ogni PC domestico condivide tutto o parte del suo
hard-disk.
Centinaia e centinaia di terabyte accessibili da tutti o da solo chi ne abbia i giusti privilegi;
senza i ritardi causati dalle migliaia di connessioni al minuto che un server deve gestire; senza
un luogo centralizzato che tenga traccia delle nostre connessioni e con la possibilità di
eseguire ricerche sugli hard-disk di altri peer come se le facessimo sul nostro; con
l'opportunità di avere la più recente versione di un prodotto prelevandola direttamente dal pc
del realizzatore e sfruttando al meglio la banda disponibile nella connessione tra i due peer.
Napster, con la sua architettura p2p ibrida (ovvero basata su un server centrale che
indirizza le connessioni, e il download tra i PC che avviene in modalita p2p), è uno degli
esempi più noti di filesharing. Grazie alla sua straordinaria facilità d'uso, alla semplicità
nell'effettuare ricerche ed all'efficacia nel determinare i risultati, ha iniziato una vera e propria
rivoluzione su Internet. Ecco, per quanto riguarda l'aspetto dell'architettura p2p che permette
la condivisione di risorse e nell'ottica di avere un quadro completo delle potenzialità, si
immagini un "fenomeno Napster" a 360 gradi.
Ma non si pensi che il p2p permetta solo una efficiente gestione delle risorse condivise. I
settori che traggono vantaggio dalle possibilità che questa architettura offre sono realmente
molteplici. Si pensi ai milioni di peer nel mondo che contribuiscono alla ricerca di
intelligenze extraterrestri con il progetto S.E.T.I. o ai milioni di utenti di portali che utilizzano
la comunicazione realtime.
2.2 L’essenza del peer to peer
In una rete peer to peer, si possono distinguere i seguenti punti cardine:
� Il sistema è basato sull’interazione fra peer;
� Il sistema non ha servizi o risorse operative centralizzate;
� Si possono verificare cambiamenti radicali sulla composizione della rete;
� Si va verso una topologia non deterministica;
� Può esservi un massiccio uso concorrente delle risorse.
Per comprendere meglio, cerchiamo di analizzare singolarmente i punti su riportati.
2.1.1 Sistema basato sull’interazione fra i peer
Non c’è nulla in comune fra i mega-siti su internet e l’architettura client-server, queste
dipendono strettamente da un robusto server che deve gestire tutti i loro utenti.
In figura è riportato un esempio di architettura centralizzata.
Figura 2.1.1: Un sistema centralizzato
Anche se il clustering o le altre tecnologie locali distribuite sono utilizzate al posto dei
server, c’è ancora un gruppo di server centralizzati che devono essere avviati per far lavorare
il sistema. Il client, malgrado tutto, leggero o pesante che sia, non è capace di svolgere il
lavoro richiesto senza connettersi al server centralizzato.
Tipicamente, il cuore della rete consiste di un mega-server e di un sofisticato routing e di
infrastrutture per il caching che assicurano che la parte più esterna della rete (client, web
browser) possa accedere a queste informazioni centralizzate nella maniera più efficiente.
Questa è la più esplicativa raffigurazione di internet dei nostri giorni, senza il peer to peer.
Il comportamento di questa sorta di sistemi è ben conosciuto. Con l’aumentare degli
utilizzatori, la potenza computazionale, la risorse di memorizzazione, e la larghezza di banda
associata ai server centralizzati, devono aumentare proporzionalmente. Contemporaneamente
il software e l’hardware devono essere capaci di “scalare” per supportare l’incremento di
risorsa richiesta all’aumentare degli utilizzatori. Normalmente, così come la scalabilità
permette di gestire decine di migliaia di utenti, i costi raggiungono livelli stratosferici.
Un sistema peer-to-peer si poggia unicamente sull’interazione fra peer. In figura è
rappresentato un sistema centralizzato che mette in evidenza il motivo per cui questo sistema,
è conosciuto come sistema con azione “ai bordi della rete”.
Figura 2.2.2: Sistema centralizzato in cui è evidente l’azione sul bordo rete
In figura si può notare come il peer-to-peer è basato sull’interazione client-client, senza il
server centrale. Quindi ogni peer interagisce direttamente con un altro peer; se vogliamo però
continuare a vedere l’architettura di una rete con la vecchia visione, vorrà dire che ogni peer
dovrà assumere contemporaneamente sia la funzione di client che di server.
In questa maniera ogni peer può gestire una porzione della rete, svolgendo quindi una parte
del lavoro che nei sistemi centralizzati, viene svolto dal server, in questo modo in molte
applicazioni, è possibile ridurre il carico di lavoro legato ad una gestione centralizzata, inoltre
è possibile gestire la rete con i peer correntemente connessi. Questo rappresenta il sogno di
molti professionisti del calcolo distribuito e parallelo.
2.3 Nessun appoggio su server o risorse centralizzate
Un sistema interamente basato sul peer-to-peer non necessita dell’esistenza di alcun server
o risorsa centralizzata. Quindi, un sistema puramente basato su tecnologia peer-to-peer non
deve contare su sistema di naming o indirizzamento centralizzato. Ciò significa che il nostro
risolutore di indirizzi predefinito, DNS, non è indicato a modellare il peer-to-peer. Mentre la
tabella DNS è replicata in molti server distribuiti, l’unica gerarchia centrale dei nomi è gestita,
dalla sua radice, da una autority centrale. Le esperienze fatte, insegnano che quando i name
server per un dominio non sono attivi, non vi è modo di accedere ad un sito web attraverso il
suo nome. Questo è sicuramente un problema che un sistema peer-to-peer evita – la caduta di
alcuni peer effettivamente non compromette il buon funzionamento dell’intero sistema o rete.
Un sistema di indirizzamento peer-to-peer, invece, deve essere completamente
decentralizzato. Se ci sono centinaia di migliaia di peer in una rete peer-to-peer, ognuno di
essi deve essere capace di trovare ogni altro peer connesso e stabilire una comunicazione.
Anche nel caso di una quasi completa distruzione di una rete, e nel caso in cui solamente due
peer rimangano connessi, esso sono ancora capaci di comunicare fra di loro.
2.4 Resistenza ai cambiamenti profondi nella composizione di una rete
Nel precedente paragrafo si è evidenziata la necessità di adattamento di un sistema peer-
to-peer ai cambiamenti profondi della composizione di una rete. Essenzialmente, un sistema
peer-to-peer progettato in modo coerente dovrebbe permettere la comunicazione fra due peer
per tutto il tempo in cui vengono spedite informazioni fra essi. In una rete completamente
connessa di peer, ci dovrebbero essere molte scelte durante la selezione del protocollo di rete
e mezzo di comunicazione fra peer. Un buon sistema peer-to-peer implementa alcune
ottimizzazioni per trarre vantaggio da ciò, e sceglie la più efficiente.
2.5 Una rete con topologia non deterministica
In un sistema peer-to-peer, i primi “clients” formano la rete e svolgono tutto il lavoro. Una
macchina client, come sappiamo, non è tipicamente una macchina super robusta o sempre in
attività. Ciò significa che le macchine possono essere accese o spente dall’utente in qualunque
momento, possono avere malfunzionamenti e quindi non riapparire più sulla rete, i loro dischi
e memoria non danno garanzia su backup o su protezioni, i loro contenuti dovrebbero essere
considerati volatili. In qualunque istante la topologia della rete peer-to-peer è imprevedibile. I
nodi che formano la rete sono costantemente variabili in base a quando un utente decide di
accedervi o uscirne. Proprio su questa variabilità si fonda la robustezza del peer-to-peer, che
riesce ad adattarsi a qualunque situazione.
2.6 Ottima scalabilità
L’essere completamente decentralizzata, permette alle reti peer-to-peer di avere un’ottima
scalabilità. Infatti, attraverso applicazioni che distribuiscono i processi lavorativi sui peer, un
buon sistema peer-to-peer dovrebbe avere, come unico limite al numero di utenti che possono
farvi parte, la larghezza di banda esistente fra i peer. Se pensiamo al fatto che comunque la
larghezza di banda va verso un incremento nel prossimo futuro, non dovrebbero addirittura
esservi limitazioni.
In effetti contrariamente a ciò che ci si potrebbe aspettare, le prestazioni della rete
aumentano con l’aumentare del numero di utenti, che è certamente una buona qualità.
2.7 Il peer to peer e la programmazione genetica: connubio perfetto
Nei precedenti paragrafi abbiamo potuto vedere le qualità del peer to peer. Riassumendo,
possiamo dire che il peer to peer è:
� Un sistema completamente decentralizzato;
� Caratterizzato da topologia non deterministica;
� Capace di resistere a cambiamenti radicali della rete;
� Ottimamente scalabile.
L’insieme di queste caratteristiche permettono l’individuazione di altri aspetti molto
importanti quali robustezza, parallelismo e asincronia.
La robustezza è intesa come la capacità da parte delle reti peer to peer di resistere a
cambiamenti improvvisi e radicali della rete dovuti ad esempio a crash di un sistema o
semplicemente alla disconnessione di un nodo. Questa qualità rende possibile la realizzazioni
di applicazioni che non sono strettamente legate alla composizione della rete. Immaginiamo
per esempio dei processi stocastici per cui non è necessario sapere da chi provengano i dati di
input da elaborare.
L’aspetto successivo, il parallelismo, è una proprietà intrinseca poiché ogni nodo è
completamente indipendente dagli altri. Le reti peer to peer, permettono di elaborare problemi
che necessitano di grande potenza di calcolo, l’osservazione è giustificata dalle risorse che il
peer to peer offre. Un algoritmo, che serva per la risoluzione di un dato problema, può essere
suddiviso in tanti sotto problemi, che a loro volta vengono affidati ai nodi della rete, ogni
nodo è così capace di procedere indipendentemente dagli altri durante l’elaborazione..
L’ultimo dei tre aspetti, l’asincronia, è comprensibile se si consideriamo i nodi della rete in
base alla loro diversità: differente potenza di calcolo e differenti tempi di elaborazione. La
possibilità di realizzare comunicazioni tramite primitive non bloccanti, permette alle
applicazioni a cui non necessiti scambiare obbligatoriamente dati, di lavorare autonomamente
e non essere influenzate da altri nodi che, disponendo di una potenza di calcolo minore,
richiedano un tempo di elaborazione maggiore.
A questo punto, se pensiamo alle conclusioni fatte nel precedente capitolo, si capisce
chiaramente come l’architettura peer to peer, sia una scelta naturale per l’implementazione
distribuita di algoritmi di programmazione genetica.
2.8 Java per il supporto peer to peer
L’ambiente che si è scelto per la realizzazione della piattaforma supporta applicazioni che:
� Sono massicciamente parallelizzabili;
� Sono asincrone;
� Hanno poche comunicazioni fra sottoprocessi;
� Richiedono molte risorse;
� Sono robuste.
Si potrebbe a questo punto pensare che la lista su menzionata sia molto restrittiva, ma vi
sono diverse applicazioni interessanti che si adattano a questa descrizione.
Buoni esempi sono rappresentati da processi indipendenti con carichi bilanciati, modelli ad
isole nella programmazione genetica, ottimizzazione euristica, e così via.
Le assunzioni su riportate permettono di dare minore importanza al requisito di affidabilità, al
requisito di sincronia e di controllo dei sottoprocessi. In altre parole sotto questi presupposti è
possibile applicare le tecnologie peer to peer. L’approccio peer to peer permette di avere
grande potenza di calcolo poiché è realizzato su reti come le WAN, inoltre come già
ampiamente evidenziato, un ulteriore vantaggio è la scalabilità quindi l’adattabilità in base
alle risorse presenti sulla rete senza ulteriori investimenti.
Nella piattaforma Java troviamo una naturale possibilità di distribuire i processi attraverso
in linkaggio dinamico del codice eseguibile di un’applicazione. Java fornisce sicurezza e una
completa indipendenza della piattaforma che la rendono un’ovvia scelta per la nostra
applicazione.
In altre parole essa può essere vista come una macchina distribuita virtuale fatta di
computer sparsi su internet. La configurazione delle macchine può costantemente cambiare e
crescere verso un limite teorico molto ampio. Ognuno che abbia accesso a Internet può unirsi
al gruppo di lavoro ed offrire la propria risorsa o utilizzare le risorse offerte per risolvere il
proprio problema.
10 CAPITOLO 3
DESCRIZIONE DELLA PIATTAFORMA JXTA
3.1 Architettura e servizi JXTA
Project JXTA ha definito una serie di sei protocolli, il cui scopo è quello di creare una rete
“virtuale” che nasconda le reti esistenti in particolare internet. Al di sopra di questa rete
possono essere creati servizi e applicazioni in modo semplice grazie alle primitive fornite. Le
specifiche dei protocolli descrivono come i peer comunicano e interagiscono, senza dire nulla
su come sono poi implementati o su come scrivere un’applicazione Peer-to-Peer.
L’uso dei protocolli JXTA permette ai peer di collaborare nella formazione di una
comunità auto-configurante, che non ha dipendenze dalla loro posizione sulla rete e non
necessita di particolari infrastrutture di supporto.
Nell’architettura JXTA, si è cercato di creare un set di protocolli con il minor overhead
possibile, senza fare delle assunzioni circa i meccanismi di trasporto propri di ogni rete e i
bisogni di un ambiente paritario. Attraverso un approccio di questo genere è possibile
realizzare molteplici servizi e applicazioni peer-to-peer, anche in presenza di ambienti di rete
fortemente mutevole e non affidabile.
L’uso dei protocolli JXTA permettono ai peer di propagare informazioni sulle loro risorse, di
scoprire le risorse di rete (servizi, pipe, ecc) che altri peer mettono a disposizione.
Un aspetto importante di questa architettura è la possibilità da parte dei peer di formare
PeerGroup in cui effettuare il routing dei messaggi e intrattenere speciali relazioni fra di
loro. Ogni messaggio porta con se una lista ordinata completa o parziale di gateway peer
attraverso i quali il messaggio può essere indirizzato.
Nell’eventualità che un’informazione di routing sia scorretta, è possibile trovare
dinamicamente un’altra strada che permetta di consegnare a destinazione le informazioni.
L’architettura JXTA è composta da sei protocolli che lavorano insieme per permettere: la
scoperta, l’organizzazione, il monitoraggio e la comunicazione tra peer. Tutti questi
protocolli sono implementati utilizzando un comune strato di messaggi. Questo strato di
messaggi è quello che lega i protocolli JXTA alle varie reti di trasporto. Ogni protocollo
JXTA è indipendente dagli altri, e un peer può non implementare tutti e sei i protocolli.
Per esempio, se un dispositivo ha tutti gli advertisement che usa pre-memorizzati, allora
non c’è bisogno che implementi il Peer Discovery Protocol.
Un peer può usare un set pre-configurato di peer router per instradare a tutti i suoi
messaggi, quindi non deve implementare il Peer Revolver Protocol e così via per gli altri
protocolli.
Nella seguente figura è riportato lo stack di protocolli JXTA:
Fig. 3.1.1: I protocolli JXTA
I protocolli JXTA non richiedono messaggi di alcun genere che ad alcun livello siano
mandati per la rete. Per esempio, JXTA non richiede il polling periodico, il controllo sullo
stato dei link, o il segnale per la scoperta dei vicini.
Un peer può decidere se memorizzare in cache gli advertisement scoperti via Peer
Discovery Protocol per un uso successivo. E’ importante segnalare che il caching non è
richiesto dall’architettura JXTA, ma che tuttavia può essere un’importante via di
ottimizzazione.
Il caching di advertisement da parte di un peer evita di dover effettuare una nuova scoperta
ogni volta che un peer accede a una risorsa di rete. In un ambiente altamente transitorio,
compiere la scoperta è la sola soluzione possibile, mentre in ambiente statico, il caching
risulta molto più efficiente. I peer possono ottenere informazioni dai peer vicini che hanno
memorizzato informazioni nella cache.
Ogni peer diviene quindi un provider per tutti gli altri.
3.2 Perché utilizzare JXTA?
Il progetto JXTA è una piattaforma open source progettata per il peer-to-peer computing.
Il protocollo JXTA standardizza il modo in cui i peer:
� scoprono altri peer;
� si auto-organizzano in un PeerGroup;
� annunciano e scoprono le risorse;
� comunicano con gli altri peer;
� tengono sotto controllo ogni altro peer.
Il protocollo JXTA invece non richiede:
� l’uso di un particolare linguaggio di programmazione o sistema operativo;
� l’uso di una particolare topologia di rete o di un particolare meccanismo di trasporto;
� l’utilizzo di un particolare metodo di autenticazione o di sicurezza.
L’architettura JXTA utilizza il paradigma del peer-to-peer, da inoltre la possibilità al
programmatore di creare e usare applicazioni e servizi interoperabili. Quest’architettura
fornisce una semplice e generica piattaforma peer to peer per ospitare ogni tipo di servizio di
rete.
In particolare l’architettura JXTA:
� è definita da un piccolo numero di protocolli. Ogni protocollo è semplice da
implementare e da integrare nelle applicazioni e nei servizi peer to peer. Così i servizi
offerti da un particolare utente possono essere usati in maniera trasparente da un
utente di un’altra comunità;
� è definita indipendentemente dal linguaggio di programmazione. Infatti può essere
implementato in C++, Java, Perl, ecc. Dispositivi eterogenei con uno stack di software
completamente differenti possono interoperare con i protocolli JXTA;
� è stata progettata indipendentemente dal protocollo di trasporto. Infatti può essere
implementato sullo stack di protocolli TCP/IP,http, Bluetooth, HomePNA, e molti altri
protocolli.
3.3 Il progetto JXTA
Project JXTA è una piattaforma per lo sviluppo di applicazioni di networking rivolta al
peer-to-peer. Jxta è nato inizialmente all’interno di Sun Microsystem, come piccolo progetto
di ricerca per cercare di analizzare e sfruttare le enormi potenzialità del peer-to-peer.
L’architettura che lo caratterizza è mostrata in figura.
Fig. 3.3.1: Architettura di Jxta
Il più basso livello, quello di Core, contiene le funzioni essenziali per ogni soluzione p2p,
cioè quelle di comunicazione, quelle di discovery, sia dei peer che delle altre risorse e di altre
funzioni di basso livello come ad esempio il routing dei messaggi.
Il livello intermedio contiene invece servizi di più alto livello e che sono desiderabili mo
non necessari ad ogni applicazione p2p, come ad esempio la ricerca e la condivisione di file,
l’autenticazione dei peer, o altri servizi implementati dalla comunità di programmatori JXTA.
Al livello più in alto infine troviamo le applicazioni peer-to-peer che sfruttano i livelli
sottostanti, ad esempio applicazioni Istant Messaging e di Auction-on-line.
JXTA è quindi un insieme di protocolli generici per il peer-to-peer che consente a qualsiasi
dispositivo connesso in qualche modo ad una rete(cioè dal cellulare ad un palmare) da un PC
ad un server, di comunicare e collaborare in modo paritario. I protocolli JXTA sono
indipendenti sia dall’ambiente hardware che dal linguaggio di programmazione, esistono
infatti implementazioni diverse, chiamate bindings. Oltre a Java, progetto originario ed
attualmente più completo, esistono anche quelle in linguaggio C, sia sotto linux che sotto
Windows, e ci sono progetti in corso per il porting in Perl, Objective C, Ada ecc.
3.4 Gli elementi di JXTA
3.4.1 Peer
Un peer è ogni dispositivo di rete (sensore, telefono, PDA, PC, server, supercomputer,
etc.) che implementi uno o più protocolli JXTA. Ogni peer opera indipendentemente e in
maniera asincrona dagli altri peer, ed è univocamante identificato da un Peer ID.
Un peer pubblica una o più interfacce di rete per utilizzarle con il protocollo JXTA. Ogni
interfaccia pubblicata è notificata come un peer endpoint che identifica univocamente
l’interfaccia di rete. I peer endpoint sono utilizzati dai peer per stabilire connessioni punto a
punto fra essi. I peer non devono essere necessariamente connessi direttamente per
intraprendere una comunicazione. Quando due peer devono comunicare fra loro e non sono
commessi direttamente, i peer che sono collocati fra di esse, fungono da intermediari, per
inoltrare sulla rete i messaggi. I peer scoprono spontaneamente gli altri peer presenti sulla rete
per formare eventualmente dei PeerGroup. I peer tipicamente interagiscono con un limitato
numero di peer (detti peer vicini o amici).
3.4.2 PeerGroup
Un PeerGroup è una collezione di peer che svolgono un un insieme di servizi in comune.
Ogni PeerGroup è univocamente identificato da un PeerGroupId, i peer si organizzano
automaticamente al suo interno. Ogni peer group può stabilire la propria politica di
appartenenza, (membership policy) da un accesso libero, ad un accesso con identificazione.
Un peer può appartenere a più peer group simultaneamente. Inizialmente, il primo gruppo
istanziato è il Net Peer Group.
Il protocollo JXTA non impone, come, quando, e perché un PeerGroup debba essere creato.
JXTA descrive solamente come un peer possa pubblicare, scoprire, fare il “join”, e monitorare
un PeerGroup.
JXTA riconosce tre motivazioni principali per creare un PeerGroup:
� Per creare un ambiente sicuro. All’interno di tale gruppo, viene stabilita una politica
di appartenenza che va da una semplice username/password, a chiavi criptate. I
partecipanti al PeerGroup permettono ai membri di accedere e pubblicare contenuti
protetti. Un PeerGroup forma uno spazio logico in cui i limitanti limitano l’accesso
alle risorse del gruppo.
� Per creare un ambiente visibile. Un PeerGroup è tipicamente formato e auto-
organizzato sopra i mutevoli interessi dei peer. Non sono imposte particolari regole su
come formare un PeerGroup, poiché i peer con gli stessi interessi tendono ad unirsi
allo stesso PeerGroup. I PeerGroup servono a suddividere la rete in regioni astratte
provvedendo ad un esplicito meccanismo di visibilità. I partecipanti al PeerGroup
definiscono l’ambito di visibilità delle ricerche dove ricercare gli argomenti del
gruppo.
� Per creare un ambiente monitorato. Un PeerGroup permette ai peer di monitorare
un set di peer per qualche speciale scopo (controllo del traffico, etc.). Un PeerGroup
fornisce un set di servizi chiamati PeerGroup services. JXTA definisce un set base di
servizi. Il protocollo JXTA specifica il formato guida per questi servizi di base.
Servizi aggiuntivi possono essere sviluppati per far fronte a specifiche esigenze.
In un peer gruop si hanno una serie di servizi chiamati peer group service. JXTA definisce
un nucleo di servizi di peer group. I servizi di questo nucleo sono i seguenti:
� Discovery Service: questo servizio è utilizzato dai peer per cercare le risorse del peer
group, peer, peer group, pipes e servizi.
� Membership Service: il servizio di membership è usato dai membri correnti per
accettare o respingere una nuova applicazione. I peer che desiderano unirsi al peer
group devono prima ricercare un membro è quindi richiedere l’accesso.
� Access Service: il servizio di accesso è usato per convalidare le richieste fatte da un
peer ad un altro. Il peer riceve la richiesta, ottiene le credenziali del peer che fa la
richiesta, inoltra informazioni circa la richiesta e infine determina se l’accesso al
servizio richiesto è permesso. Non tutte le azioni di un PeerGroup devono essere
controllate con l’Access Service, solo quelle azioni in cui a non tutti i peer è concesso
l’uso.
� Pipe Service: il servizio di pipe è usato per gestire e creare connessioni pipe tra
differenti membri di un PeerGroup.
� Revolver Service: questo servizio è usato per spedire query ai peer per cercare
informazioni su di un peer, un peer-group, un servizio o una pipe.
� Monitoring Service: il servizio di monitoraggio è usato per permettere ad un peer di
monitorare altri membri dello stesso PeerGroup.
Non tutti questi servizi devono essere necessariamente implementati da un PeerGroup.
Ogni servizio può implementare uno o più protocolli JXTA. Un servizio tipicamente
implementa un protocollo per ragioni di semplicità e modularità, ma un servizio può anche
non implementare alcun protocollo.
3.4.3 Network Services
I peer cooperano e comunicano per pubblicare, scoprire e invocare servizi di rete. I peer
scoprono i vari servizi via Peer Discovery Protocol.
Il protocollo JXTA riconosce due livelli di servizi di rete:
� Peer Services. Un peer service è accessibile unicamente dal peer che pubblica questo
servizio. Se il peer cade, cadrà con esso anche il servizio. Istanze multiple del
medesimo servizio, possono essere avviate su differenti peer, ma ogni istanza del
servizio pubblica il proprio dvertisement.
� PeerGroup Services. Un peer group service è composto di una collezione di istanze
che girano sui membri multipli del peer group. Se un qualunque peer cade il servizio
del peer group non cessa.
3.4.4 Pipe JXTA
I peer JXTA utilizzano le pipe per inoltrare messaggi ad altri peer. Le pipe sono asincrone
e unidirezionali, sono meccanismi per il trasferimento di messaggi utilizzate per il servizio di
comunicazione.
Le pipe non sono tipizzate; esse supportano il trasferimento di ogni oggetto, incluso
codice binario, stringhe, e qualunque oggetto basato su tecnologia Java.
Le pipe si differenziano in input pipe, su cui ricevere messaggi, e output pipe, su cui inviare
messaggi.
I peer endpoint, vengono collegati dinamicamente per poter ricercare a tempo di
esecuzione altri peer endpoint. Gli endpoint corrispondono ai canali disponibili su di un peer,
che possono essere usati per ricevere ed inviare messaggi da un altro peer. Come già è stato
detto in precedenza, per intraprendere una comunicazione, i peer non devono necessariamente
essere connessi direttamente; i peer endpoint intermediari, sono utilizzati per propagare il
messaggio.
Le Pipe offrono fondamentalmente due differenti modi per comunicare, poin-to-poit e
propagate, inoltre il nucleo di JXTA offre pipe secure unicast una variante delle point-to-poit
pipe.
� Point to Point. Una pipe point to point connette esattamente due pipe endpoint
insieme, una pipe di input che riceve messaggi da una pipe di output. Non sono
supportati messaggi di acknoledgement o verifica. Informazioni addizionali nel
payload del messaggio, come un id univoco, sono richieste qualora i messaggi
vengano inviati in sequenza. Il payload del messaggio può anche contenere un pipe
advertisement, che può essere usato per aprire una pipe di risposta al mittente.
� Propagate Pipe. Una Propagate Pipe connette insieme una pipe di output con più pipe
di input. Il flusso di messaggi nella pipe di input dalla pipe di output. Un propagate
message è mandato a tutte le input pipe in attesa. Questo processo può creare più copie
del messaggio che deve essere inviato. Sul TCP/IP,l’IP multicast può essere usato
come una implementazione per propagare. La propagazione può essere implementata
usando comunicazioni punto a punto su meccanismi di trasporto che non prevedono il
multicast come ad esempio HTTP.
Fig. 3.4.1: Le pipe Jxta
Come mostrato nella figura precedente le pipe possono connettere due peer che non hanno
una connessione fisica diretta. Uno dei molti peer endpoint intermediari è usato per effettuare
il routing del messaggio tra i due pipe endpoint.
3.4.5 Message JXTA
Un messaggio è l’unità di base di dati che viene scambiata fra due peer. I messaggi
vengono spediti e ricevuti attraverso il Pipe-Service e dall’Endpoint Service. Tipicamente
questi servizi vengono utilizzati da applicazioni per creare, spedire e ricevere messaggi.
Un messaggio è identificato da una sequenza ordinata di nomi e contenuti tipizzati
chiamati elementi del messaggio. Così un messaggio è essenzialmente un insieme di coppie
nomi/valori. Il contenuto può essere un tipo arbitrario. Vi sono due possibili rappresentazioni
per i messaggi: XML e binario.
Il protocollo JXTA è specificato come un set di messaggi XML scambiati tra peer. Ogni
piattaforma software descrive come un messaggio è convertito, per e da una struttura dati
nativa, come un oggetto Java o una struct C. L’uso di messaggi XML per definire protocolli
permette a differenti tipi di peer di comunicare tra di loro. Ogni peer è libero di implementare
il protocollo in una maniera meglio adatta al suo scopo.
3.4.6 Advertisement JXTA
Tutte le risorse di rete, come peer, PeerGroup, pipe e servizi, sono rappresentate degli
advertisement. Gli advertisement sono strutture di metadati rappresentati da un documento
XML. Il protocollo JXTA, utilizza gli advertisement per la descrizione e la pubblicazione
delle risorse disponibili. Inoltre i peer scoprono le risorse appunto ricercando gli
advertisement corrispondenti, e questo advertisement può essere depositato nella cahce locale
del peer. Ogni advertisement possiede un tempo di vita che specifica la durata della risorsa
associato. In questa maniera gli advertisement obsoleti possono essere eliminati senza nessun
controllo centralizzato. Per estendere la durata di un servizio, un advvertisement può essere
ripubblicato.
L’architettura JXTA definisce i seguenti tipi di advertisement:
� Peer Advertisement;
� PeerGroup Advertisement;
� Pipe Advertisemen;
� Service Advertisement;
� Content Advertisement;
� Endpoint Advertisement;
Peer Advertisement
Un Peer Advertisement descrive le risorse di un peer.
L’utilizzo principale di questo advertisement è quello di fornire informazioni sul peer,
come nome, peer id, servizi disponibili ed endpoint attivi.
Di seguito viene mostrata la struttura di un Peer Advertisement:
Un peer
advertisemet
contiene
quindi i
seguenti
campi:
� Name
:
nome
del
peer.
� Keywords: è un stringa opzionale da utilizzare per indicizzare e per effetture ricerche
sul peer.
� Peer id: l’identificatore che univocamente identifica un peer sulla rete.
� Service: sono dei Service Advertisement, uno per ogni servizio che il peer mette a
disposizione.
� Endpoint: gli endpoint attivi del peer sono degli URI del tipo http://127.0.0.1:9789.
� Initial App: applicazione opzionale che viene avviata al boot del peer.
PeerGroup Advertisement
���������� �������� ���� ����������� �
����� !������"���� "� �
�#$��� � ��������%���&#$��� �
�'�()��� � %$���� *"���++$"�� � � �$�� ��� ����$�
�&'�()��� �
�!��� ��������%����&!��� �
�,������ �
�� � ����� ,���������"���� "� �
������������������
�&����� ,���������"���� "� �
�&,������ �
�- �%�� "� �
Un PeerGroup advertisement descrive le risorse specifiche di un peer group, inoltre
raccoglie informazioni quali nome, peer group ID, descrizione, specifiche, e parametri di
servizio.
Analizziamo la struttura di un PeerGroup advertisement:
Un
PeerGroup
advertisem
ent
contiene
quindi i
seguenti
campi :
� Name : nome del PeerGroup.
� Keywords : è un stringa opzionale da utilizzare per indicizzare e per effetture ricerche
sul PeerGroup.
� PeerGroup id: l’identificatore che univocamente identifica un peer sulla rete.
� Service: sono dei Service Advertisement, uno per ogni servizio che il PeerGroup
mette a disposizione.
� Initial App: applicazione opzionale che viene avviata quando un nuovo peer effettua
il join al gruppo.
Pipe Advertisement
Un Pipe Advertisement, Descrive il canale di comunicazione di una pipe, utilizzato dal
pipe service per la creazione dell’endpoint associato alla pipe di input e a quella di output.
���������� �.���.�� ���� ��.�����.�� �
����� !��/ �*%����"���� "� �
�#$��� � ���������*%%���&#$��� �
�'�()��� �%$����*"���++$"��� ��$���������$��&'�()��� �
�/ ��� �!��/ �*%�0���&/ ��� �
�,������ �
����� ,���������"���� "� �
� � ������������������
�&���� ,���������"���� "� �
Ogni pipe advertisement contiene un ID simbolico opzionale, specifica il tipo della pipe, ed
un pipe ID univoco.
Vediamo la struttura di un pipe advertisement:
Analizziamo i campi della struttura:
� Name: E’ un elemento opzionale che può essere associato alla pipe. Il nome non deve
per forza essere univoco;
� Pipe Id: E’ un campo assolutamente indispensabile in quanto identifica univocamente
una pipe;
� Type: E’ un campo opzionale che specifica il tipo di pipe. Sono disponibili i seguenti
tipi:
1. RELIABLE (garantisce l’ordine di invio e che il messaggio sia inviato una sola volta).
2. UNRELIABLE (può non arrivare a destinazione o se si tratta di più messaggi possono
arrivare in un ordine diverso)
���������� �.���.�� ���� ��.�����.�� �
����� !�%�����"���� "� �
�#$��� � ��������$�%�%��&#$��� �
�0�� �!�%��0���&0�� �
3. SECURE (combina le funzionalità della pipe di tipo reliable e in più utilizza la
crittografia dei messaggi).
Service Advertisement
Il service advertisement contiene la descrizione di come creare ed invocare un servizio.
L’associazione del flusso dei messaggi del servizio con le relative pipe, è compito dei campi
pipe advertisement e metodo di accesso. Un service advertisement può avere più tipologie di
metodi di invocazione ognuna delle quali deve essere specificata.
Vediamo la struttura di un service advertisement:
Analizzi
amo i
campi della
struttura:
� Na
me:
è un campo opzionale che può essere associato al servizio e non deve per forza essere
univoco;
� Keywords: una stringa che può essere utilizzata per indicizzare il servizio in modo da
ritrovarlo in caso di ricerca. Questa stringa non è garantito sia unica, infatti più servizi
posso avere la stessa keyword;
� Service Id: questo è il campo che identifica univocamente il servizio;
� Version: questo campo identifica la versione del servizio;
� Provider: questo campo da informazioni sul fornitore del servizio;
���������� �.���.�� ���� ��.�����.�� �
����� ,���������"���� "� �
�#$��� � �����������+���&#$��� �
�1��� � �0���1��� ���&1��� � �
�'�()��� �%$����*"���++$"��� ��$���������$��&'�()��� �
�0�� �0���,���+����&0�� �
�!�%�� �!�%��$���"���� "�&!�%�� �
�!$$�� �%$$��"������� 2��*$+�� ���������+����&!$$�� �
� Pipe: questo è un campo opzionale che permette di creare un pipe di output da
associare al servizio. Non è detto che tutti i servizi utilizzino le pipe;
� Params: questo campo specifica i parametri di ingresso al servizio, in particolare si
tratta di una lista di stringhe;
� URI: questo campo opzionale specifica la posizione del codice che implementa il
servizio;
� Access Methods: almeno un metodo di accesso è richiesto per poter utilizzare il
servizio. Per esempio ilmetodo di accesso potrebbe essere specificato in un documento
WSDL che appunto specifica che il metodo di accesso è quello dei web service.
Content Advertisement
Un content advertisement descrive un come un contenuto (file o lo stato di un processo)
che può essere condiviso in un gruppo. Non ci sono restrizioni sui tipi di contenuto che
possono essere condivisi.
Vediamo la struttura di un content advertisment:
Analizziamo i campi della struttura:
� Mimetype: Il mime type del contenuto, può anche essere sconosciuto;
���������� �.���.�� ���� ��.�����.�� �
����� 3� "� "����"���� "� �
�4 ���"(%�� � ��������$�%�%��&4 ���"(%�� �
�,�+�� ��* �5�++$�&,�+�� �
�- ���� �� �"�%���������2��$��&- ���� �� �
� Id: E’ un campo richiesto che identifica univocamente un contenuto;
� Size: Le dimensioni totali del contenuto. Se il campo contiene il valore –1 allora
significa che non è nota la dimensione;
� Encoding: Specifica il tipo di codifica usata;
� RefId: Se il contenuto è stato propagato questo campo specifica chi originariamente
aveva il contenuto.
EndPoint Advertisement
Un Endpoint Advertisement descrive un protocollo di trasporto utilizzabile da un peer. Un
peer tipicamente può utilizzare più protocolli di trasporto. Solitamente si ha bisogno di un
endpoint per ogni tipo di protocollo di trasporto utilizzato (TCP/IP, HTTP, ecc). Un peer può
avere più interfacce di rete. Ogni interfaccia può avere un suo set di endpoint. L’endpoint
advertisement è presente nel peer advertisement per descrivere gli endpoint raggiungibili su di
un peer. Gli endpoint sono rappresentati come indirizzi virtuali che permettono di creare
dinamicamente un canale di comunicazione.
Per esempio “tcp://123.123.20.20:1002” o “http://133.125.23.10:6002” sono degli URI che
rappresentano indirizzi di endpoint.
Vediamo la struttura di un endpoint advertisement:
Analizziamo i campi della struttura:
<?xml version="1.0" encoding="UTF-8"?>
<JXTA:EndpointAdvertisement>
<Name> nome dell’endpoint</Name>
<Keywords> keywords</Keywords>
� Name: E’ un campo opzionale che può essere associato all’endpoint;
� Keywords: E’ una stringa opzionale che permette di indicizzare l’endpoint e di
ritrovarlo agevolmente in caso venga ricercato;
� Address: E’ un URI che indica la posizione globale dell’endpoint.
3.5 JXTA Credentials
Le reti P2P dinamiche come la rete JXTA, hanno la necessità di supportare differenti
livelli di accesso alle risorse. Ciò fa si che i peer JXTA operano in politiche di trusting, in cui
un peer individuale agisce sotto l’autorità concessagli da un altro peer sottoposto alla
medesima politica per realizzare una particolare operazione.
Le relazioni tra i peer, come si può immaginare, possono cambiare molto rapidamente, per
questo motivo l’architettura JXTA ha bisogno di meccanismi di accesso alle risorse, che
rapidamente concedano o neghino l’accesso ad un servizio.
A tale scopo esistono quattro tipologie di accesso alle risorse:
� Confidentiality: garantisce che il contenuto di un particolare messaggio non sia
intercettato da individui non autorizzati;
� Authorization: garantisce che il mittente sia autorizzato a spedire un messaggio;
� Data Integrity: garantisce che il messaggio non sia stato modificato accidentalmente
o deliberatamente durante la trasmissione;
� Refutability: garantisce che il messaggio sia stato trasmesso da un mittente
propriamente identificato e che non sia stato già spedito in precedenza.
I messaggi XML permettono di aggiungere una varietà di informazioni sotto forma di
metadati come ad esempio: credenziali, certificati digitali, chiavi pubbliche, etc. Le
credenziali sono costituite da un token che è presente nel body di un messaggio per
identificare il mittente, e che possono essere utilizzate per verificare che il messaggio sia
indirizzato verso il giusto endpoint.
Quindi le credenziali rappresentate da questo token sono trasparenti all’utente e devono
essere presenti in ogni messaggio trasmesso. Inoltre i protocolli JXTA hanno come obbiettivo,
l’assoluta compatibilità con gli odierni meccanismi di trasporto sicuro come IPSec o SSL.
3.6 ID JXTA
Gli ID sono un sistema canonico che permette di identificare univocamente tutte le risorse
di JXTA. Correntemente, esistono sei tipi di entità JXTA che hanno un ID JXTA definito:
peer, peer group, pipes, contenuti, moduli, specificazioni.
Gli URNs sono utilizzati per esprimere gli ID JXTA.
Un esempio di JXTA peer ID è il seguente:
urn:jxta:uuid-59616261646162614A78746150325033F3BC76FF13C2414CBC0AB663666DA53903
3.7 L’Architettura di una rete JXTA
Una rete JXTA consiste in una serie di peer connessi tra di loro. Le connessioni possono
essere fortemente transitorie, e il routing dei messaggi attraverso i peer è non deterministico.
Un peer può entrare ed uscire dalla rete in ogni momento e il routing tra due peer può
cambiare molto frequentemente.
L’organizzazione della rete non è affidata alla struttura di JXTA, ma in pratica sono utilizzati
3 tipi di peer:
� Minimal peer: Sono quei peer che mandano e ricevono messaggi, ma non
mantengono in cache advertisement relativi ad altri peer, ne tanto meno messaggi di
routing. Un minimal peer è quindi un peer con risorse molto limitate come ad esempio
un PDA o un cellulare;
� Simple peer: Un simple peer può mandare e ricevere messaggi e tipicamente
mantiene nella cache degli advertisement. Un simple peer risponde a richieste di
scoperta provenienti da altri peer, ma non inoltra le richieste di scoperta. La maggior
parte dei peer sono simple peer;
� RendezVous peer: Un RendezVous peer è un peer come gli altri, solo che mantiene
nella cache degli advertisement. Inoltre un RendezVous peer può anche inoltrare le
richieste che gli arrivano da altri RendezVous peer per aiutare altri peer nella scoperta
di risorse. Ogni simple peer può configurare se stesso come RendezVous peer o può
inizializzare una lista di RendezVous peer da utilizzare. Un RendezVous peer inoltre
mantiene una lista di tutti i peer che attualmente sono connessi a lui, per richiedere
informazioni su particolari risorse sullo stato del gruppo per ciò che riguarda le
presenze. I peer inviano le richieste di scoperta ai RendezVous peer, che a loro volta
inoltrano la richiesta ad altri RendezVous peer che conoscono. Il processo di inoltro
delle richieste continua fino a che il campo TTL del messaggio di scoperta non
raggiunge il valore zero. Tutti i messaggi di scoperta hanno come valore del TTL 7. I
cicli sono eliminati facendo si che venga mantenuta una lista di tutti i peer a cui è stato
già mandato il messaggio. Un esempio di utilizzo dei RendezVous peer è mostrato
nella seguente figura:
Fig. 3.7.1:
Scoperta
tramite RendezVous
� Relay peer: Un relay peer mantiene informazioni di routing verso altri peer ed inoltra
i messaggi verso questi peer. Un peer controlla prima nella sua cache se possiede già
le informazioni di routing e in caso non le possieda si rivolge al relay peer. I relay peer
servono anche nel caso su una rete siano presenti NAT o Firewall.
Nella seguente figura è mostrata la consegna di un messaggio ad un peer che è
posto in una rete con firewall:
Fig. 3.7.2: Consegna di messaggi attraverso i relay peer
10.1 3.8 Protocolli di JXTA
JXTA definisce una serie di formati di messaggi XML, o protocolli, per comunicazione tra
peer. I peer usano questi protocolli per scoprire altri peer, inviare advertisement e scoprire
risorse di rete, comunicazione e messaggi di routing.
Ci sono sei protocolli di JXTA:
� Peer Discovery Protocol (PDP): usato dai peer per rendere note le loro proprie
risorse (p.e., peer, peer groups, pipe, o servizi) e scopre risorse da altri peer;
� Peer Information Protocol (PIP): usato dai peer ottenere informazioni sullo stato
(uptime, state, recent traffic, ecc.) dagli altri peer;
� Peer Revolver Protocol (PRP): abilita i peer a spedire query generiche a uno o più i
peer e riceve una risposta (o risposte multiple) ad esse. Diversamente da PDP e PIP,
che sono usati per richiedere informazioni specifiche predefinite, questo protocollo
permette ai peer services di definire e scambiare tutte le informazioni di cui hanno
bisogno;
� Pipe Binding Protocol (PBP): usato dai peer per stabilire un canale di
comunicazione virtuale, o pipe, tra uno o più i peer;
� Endpoint Routing Protocol (ERP): usato dai peer per cercare routes(percorsi) verso
le porte di destinazione su altri peer. Le informazioni sui percorsi includono una
sequenza ordinata di relay peer IDs che possono essere usati per spedire un messaggio
a destinazione. (Per esempio, il messaggio può essere consegnato tramite la sua
spedizione ad un peer A che lo invia ad un peer B che lo inoltra alla destinazione
finale);
� Rendezvous Protocol (RVP): usato dai peer per propagare messaggi in un peer
group.
Tutti i protocolli JXTA sono asincroni, e sono basati su un modello query/response. Un
peer JXTA usa uno dei protocolli per spedire una query ad uno o più peer nel suo peer group.
Esso può ricevere zero, uno, o più risposte alla sua query.
Per esempio, un peer può usare PDP per spedire una discovery query che chiede tutti i peer
noti in una Net Peer Group di default. In questo caso, peer multipli risponderanno
probabilmente con una discovery response. In un altro esempio, un peer può spedire una
discovery request che chiede una pipe specifica chiamata “l'aardvark”. Se questa pipe non è
trovata, allora non verrà spedita in risposta alcuna discovery response.
Non è necessario che nei peer JXTA siano implementati tutti e sei i protocolli; è necessario
implementare solamente i protocolli che verranno usati. La corrente piattaforma Project
JXTA J2SE supporta tutti i sei protocolli di JXTA. Le API del linguaggio di programmazione
Java sono usate per accedere alle operazioni supportate da questi protocolli, come scoperta
dei peer o congiunzione di peer group.
3.9 Peer Discovery Protocol
Il Peer Discovery Protocol (PDP) è usato per scoprire tutte le risorse pubblicate dai peer.
Le risorse sono rappresentate come advertisement. Una risorsa può essere un peer, peer
group, pipe, servizi, o qualsiasi altra risorsa che ha un advertisement. PDP consente a un peer
di cercare advertisement su altri peer. Il PDP è il protocollo di scoperta di default per tutti gli
utenti definiti peer group e la net peer group di default.
I discovery service utilizzati possono scegliere di influenzare il PDP. Se un peer group non
ha il proprio discovery service, il PDP è usato per esaminare i peer alla ricerca di
advertisement. Ci sono modi multipli per scoprire informazioni distribuite.
La corrente piattaforma ProjectJXTA J2SE usa una combinazione fra indirizzi IP multicast
e l'uso di rendezvous peers all’interno della subnet locale, una tecnica basata sul
networkcrawling.
I rendezvous peers forniscono il meccanismo per spedire richieste da un peer noto al
prossimo (“strisciando” per la rete), per scoprire dinamicamente informazioni. Un peer può
essere pre-configurato con un set predefinito di rendezvous peers. Un peer può così scegliere
di avviare se stesso localizzando dinamicamente i rendezvous peers o di risorse di rete nella
prossimità del suo ambiente.
Altre tecniche, come le Content Addressable Networks (CANs), potrebbero essere
aggiunte per migliorare la scoperta delle risorse.
I peer generano messaggi di richiesta di tipo discovery query per scoprire advertisement
all’interno di un peer group. Questi messaggi contengono le credenziali del peer analizzato e
lo identificano al destinatario della comunicazione.
I messaggi possono essere spediti a qualsiasi peer all’interno di una regione o a un
rendezvous peer. Un peer può ricevere zero, uno, o più risposte a una richiesta di tipo
discovery query. Il messaggio di risposta restituisce uno o più advertisement.
3.10 Peer Information Protocol
Una volta localizzato un peer, possono essere consultate le sue risorse.
Il Peer Information Protocol (PIP) fornisce un set di messaggi per ottenere informazioni
sullo stato del peer. Queste informazioni possono essere usate per sviluppi interni o
commerciali delle applicazioni JXTA. Per esempio negli sviluppi commerciali le
informazioni possono essere usate per quantificare l'uso di un peer service e accreditare ai
consumatori l’uso del servizio.
All’interno di uno sviluppo IT, le informazioni possono essere usate dal reparto IT per
monitorare il comportamento di un nodo e dirottare il traffico della rete peer migliorare le
prestazioni complessivo. Questi ganci possono essere estesi per fornire al reparto IT il
controllo del peer node in aggiunta alle informazioni sullo stato.
Il messaggio ping di PIP è spedito ad un peer per controllare se il peer è attivo e ottenere
informazioni su di esso. Il messaggio ping specifica se deve essere restituita un’intera risposta
(peer advertisement) o un semplice acknoledgment (riconoscimento: alive ed uptime).
Il messaggio di peer info è usato per spedire un messaggio in risposta ad un ping.
Esso contiene le generalità del mittente, il peer ID del mittente e il peer ID del destinatario,
uptime, e peer advertisement.
3.11 Peer Revolver Protocol
Il Peer Revolver Protocol (PRP) consente ai peer di spedire query request generiche ad
altri peer. Le query request possono essere spedite ad un peer specifico, o possono essere
propagate all’interno di un peer group tramite i rendezvous services. Il PRP è un foundation
protocol che supporta query request generiche. Sia il PIP che il PDD sono fatti usando PRP, e
forniscono specifiche query/requests: il PIP è usato per consultare specifiche informazioni
sullo stato e PDP è usato per scoprire le peer resources. Il PRP può essere usato per ogni
query generica di cui un’applicazione può aver bisogno.
Per esempio, il PRP permette ai peer di definire e scambiare query per trovare o cercare
informazioni sullo stato del servizio, lo stato di un pipe endpoint, ecc. Il messaggio di
revolver query è usato per spedire una richiedere un servizio ad un altro membro di un peer
group. Il messaggio di resolver query contiene l’identificazione del mittente, un query ID
unico, uno specifico service handler e la query.
Ciascun servizio può registrare un handler nel resolver service del peer group che processa
la specifica richiesta di revolver query e genera le risposte. Il messaggio resolver response è
usato per spedire una risposta ad un messaggio di revolver query; esso contiene le generalità
del mittente, un query ID unico, uno specifico service handler e la risposta. Possono essere
spediti messaggi di revolver query multipli. Un peer può ricevere zero, uno, o più risposte a
una query request.
3.12 Pipe Binding Protocol
Il Pipe Binding Protocol (PBP) è usato dai membri del peer group per associare un pipe
advertisement ad un pipe endpoint.
Il pipe virtual link (percorso virtuale) può essere stratificato su qualsiasi numero di link di
trasporto di reti fisiche come TCP/IP. Ciascuna parte finale di una pipe lavora per mantenere
il link virtuale e ristabilirli, se necessario, legando o trovando i pipe endpoints che si trovano
attualmente sul confine.
Una pipe può essere vista come una coda astratta di messaggi e che supporta la creazione,
open/resolve (bind), chiusura (unbind), cancellazione, spedizione, e ricezione delle
operazioni. Le implementazioni attuali delle pipe possono differire, ma tutte le
implementazioni conformi al protocollo usano PBP per legare le pipe ad un endpoint.
Durante l’operazione astratta di creazione, un peer locale lega un pipe endpoint ad un pipe
transport.
La query PBP viene spedita attraverso la pipe endpoint del peer per trovare una pipe
endpoint di confine sullo stesso pipe advertisement. La query può richiedere informazioni
non ottenute dal cache. Questo è usato per ottenere le informazioni più recenti da un peer. La
query può contenere anche un peer ID opzionale che, se presente, indica che solamente il peer
specificato potrebbe rispondere. La richiesta del PBP è rispedita al peer richiedente attraverso
ogni peer di confine sulla pipe.
Il messaggio contiene il Pipe ID, il peer dove è stata creata la corrispondente InputPipe e
un valore boolean che indica se esiste l’InputPipe sul peer specificato.
3.13 Endpoint Routing Protocol
L'Endpoint Routing Protocol (ERP) definisce un set di messaggi request/query che sono
usati trovare informazioni di routing. Queste informazioni di instradamento sono mecessarie
per spedire un messaggio da un peer (sorgente) ad un altro (destinazione).
Quando a un peer è richiesto di spedire un messaggio ad un determinato endpoint address,
per prima cosa guarda nella sua cache locale per determinare se esiste una strada per arrivare
al peer. Se non trova un percorso, spedisce una route resolver query ai peer ad esso collegati
per ottenere informazioni di risoluzione del percoso. Quando un relay peer riceve una route
query, controlla se conosce il percorso; se non lo conosce restituisce le informazioni di route
come un’enumerazione di salti.
Qualsiasi peer può consultare un relay peer per ottenere informazioni di routing, ed ogni
peer può diventare un relay all’interno di un peer group. I relay peer tipicamente conservano
nella cache informazioni di routing; esse includono il peer ID del mittente, il peer ID del
destinatario, un campo TTL per il routing, e una sequenza ordinata di peer ID di gateway.
La sequenza di peer ID potrebbe non essere completo, ma deve contenere almeno il primo
relay. Le richieste di instradamento sono spedite da un peer ad un peer relay per richiedere
informazioni di routing.
La query può indicare una preferenza per aggirare il contenuto della del router e cercare
dinamicamente un nuovo percorso. Le query di routing sono spedite da un relay peer in
risposta ad una richieste di informazioni di routing. Questo messaggio contiene il peer ID
della destinazione, il peer ID e il peer advertisement di un router che conosce il percorso per
raggiungere la destinazione, e una sequenza ordinata di uno o più relay.
3.14 Rendezvous Protocol
Il Rendezvous Protocol (RVP) è il responsabile della propagazione di messaggi in un peer
group. Mentre differenti peer groups possono avere differenti metodi per propagare
comunicazioni, l’RVP definisce un semplice protocollo che permette:
� di connettere peers a server (essere capaci di propagare e ricevere messaggi) ;
� di controllare la propagazione dei messaggi (TTL, ricerca del loopback ecc.).
L’RPV è usato dal PRP e dal PBP per propagare messaggi.
10.2 3.15 Un semplice esempio che avvia una piattaforma JXTA
Questo paragrafo discute i passi necessari per far girare un semplice esempio includendo:
� Requisiti del sistema ;
� Accesso alla documentazione on-line;
� Downloading dei file binari del Progetto JXTA;
� Compilazione del codice della tecnologia JXTA;
� Esecuzione delle applicazioni della tecnologia JXTA;
� Configurazione dell'ambiente JXTA ;
3.15.1 Requisiti del sistema
Il seguente Project JXTA J2SE Platform Binding richiede una piattaforma che supporta la
JRE di Java o il JDK 1.3.1 e successive. Questo ambiente è attualmente disponibile per
Solaris, Microsoft Windows 95/98/2000/ME/NT 4.0, Linux, e Macintosh.
3.15.2 Esempio HelloWorld
Questo esempio illustra come un’applicazione può avviare la piattaforma di JXTA.
L'applicazione istanzia la piattaforma JXTA e poi stampa un messaggio visualizzando:
� il nome del peer group;
� il peer group ID;
� il nome del peer;
� il peer ID.
Figura 3.15.1: Output: SimpleJxtaApp
3.15.3 La classe SimpleJxtaApp
Definiamo una sola classe, SimpleJxtaApp con una variabile di classe:
� PeerGroup netPeerGroup // il nostro peerGroup (il peerGroup di default)
e due metodi:
� public static void main( ) //metodo principale: stampa informazioni sul peer e il
PeerGroup;
� public void startJxta( ) //inizializza la piattaforma JXTA e crea la net peer group
3.15.4 startJxta ( )
Il metodo startJxta( ) usa una sola chiamata per instanziare la piattaforma JXTA [linea
35]:
netPeerGroup = PeerGroupFactory.newNetPeerGroup( );
Questo chiamata istanzia l'oggetto “piattaforma di default”, quindi crea e restituisce un
oggetto PeerGroup contenente il net peer group di default. Questo oggetto contiene le
realizzazioni delle referenze di default dei vari servizi di JXTA (DiscoveryService,
MembershipService, RendezvousService ecc.). Inoltre contiene il peer group ID e il nome del
peer group, così come il nome e l’ID del peer sul quale gira l’applicazione.
3.15.5 main( )
Questo metodo innanzitutto chiama startJxta() per instanziare la piattaforma JXTA.
Successivamente, questo metodo stampa varie informazioni sul nostro netPeerGroup:
� Group name: il nome del net group di default, NetPeerGroup [linea 19]:
System.out.println ("Hello from JXTA group " +
netPeerGroup.getPeerGroupName( ) );
� Peer Group ID: l’ID del peer group del net peer group di default [linea 21]:
System.out.println (" Group ID = " + netPeerGroup.getPeerGroupID(
).toString());
� Peer Name: il nostro nome; tutto ciò che noi abbiamo fornito durante la
configurazione di base del JXTA[linea 23]:
System.out.println (" Peer name = " + netPeerGroup.getPeerName( ));
� Peer ID: il peer ID univoco che è stato assegnato al nostro peer JXTA quando
abbiamo avviato l’applicazione [linea 25]:
System.out.println (" Peer ID = " + netPeerGroup.getPeerID( ).toString( ));
Dopo avere stampato queste informazioni, la nostra applicazione termina.
3.15.6 Esecuzione di “Hello World”
La prima volta che SimpleJxtaApp è avviato, è visualizzato il tool di autoconfigurazione.
Dopo aver inserito le informazioni di configurazione e cliccato OK, l’applicazione continua a
stampare informazioni circa il peer JXTA e il peer group. Quando l’applicazione ha finito, si
può esaminare i vari file e subdirectories che sono stati creati nella directory corrente:
� PlatformConfig: il file di configurazione creato dal tool di autoconfigurazione;
� cm: la directory di cache locale; essa contiene subdirectories per ciascuno gruppo che
è stato individuato. Nel nostro esempio, noi dovremmo vedere la subdirectory
chiamata jxta-NetGroup;
� cm\jxta-NetPeerGroup\Peers: subdirectory che contiene advertisement (documenti
XML) per ogni peer che è stato individuato nel NetPeerGroup. Nel nostro esempio,
noi abbiamo creato un singolo peer JXTA, e dovremmo quindi vedere in questa
directory un solo file. Un esempio di peer advertisement è mostrato nella figura in
basso;
� pse: subdirectory che contiene i nostri peer certificati (usati per la sicurezza).
3.16 Programmare con JXTA
Questo parte del capitolo presenta diversi esempi di programmazione JXTA che eseguono
task comuni come scoperta di peer e peer group, creare e pubblicare advertisement, creare e
legare un peer group e usare pipe.
3.17 Scoperta di un peer ( Peer Discovery )
Il seguente esempio di programmazione illustra come scoprire altri peer JXTA nella rete.
L’applicazione istanzia una piattaforma JXTA, quindi spedisce una Discovery Query al net
peer group di default, cercando ogni peer JXTA. Per ogni Discovery Response3 ricevuto,
l’applicazione stampa il nome del peer che ha spedito la risposta e anche il nome di ogni peer
che è stato scoperto.
Figura 3.17.1: Esempio di output: esempio di scoperta di peer
3.17.1 Il servizio di scoperta ( Discovery Service)
Il JXTA Discovery Service fornisce un meccanismo sincrono per scoprire peer, peer
group, pipe e il servizio advertisement. Gli advertisement sono memorizzati in una cache
locale persistente (la directory cm).
3 Nota: Poiché le Discovery Responses sono spedite in modo asincrono è necessario aspettare che siano spedite molte
Discovery Request prima di ricevere qualche risposta.
Se non si riceve alcuna Discovery Response, quando si avvia questa applicazione, significa che JXTA non è stato configurato
correttamente. Si dovrà specificare, tipicamente, almeno un rendezvous peer. Se il peer è localizzato dietro un firewall o un NAT, sarà
necessario specificare un relay peer. Rimuovere il file Platform Config che era stato creato nella directory corrente e riavviare l’applicazione.
Quando appare il configuratore JXTA inserire la configurazione corretta.
Quando un peer si avvia, la stessa cache è referenziata. Un peer può usare il metodo
getLocalAdvertisements( ) per recuperare advertisement che si trovano nella sua cache
locale. Se esso vuole scoprire altri advertisement può usare getRemotedAdvertisements( )
per spedire una Discovery Query ad altri peer.
La Discovery Query può essere spedita ad un peer specifico o propagato sulla rete JXTA.
Nella piattaforma J2SEE le Discovery Query, non intesi per uno specifico peer, sono
propagati nella sottorete locale, utilizzando l’IP multicast, ed inoltre propagato ai peer
rendezvous configurati. Un peer include il proprio advertisement in una Discovery Query,
eseguendo una notifica o un meccanismo di scoperta automatico.
Ci sono due modi per ricevere una Discovery Response. Ci si può aspettare che uno o più
peer rispondano ad una Discovery Response, e che facciano una chiamata al metodo
getLocalAdvertisements( ) per ricevere il risultato che è stato trovato e aggiunto alla cache
locale. Alternativamente, notifiche asincrone di scoperta dei peer possono essere completate
aggiungendo un Discovery Listener, che richiama il metodo discoveryEvent( ) ,quando è
ricevuto un evento di scoperta.
Se si sceglie di aggiungere un Discovery Listener si hanno due opzioni:
� si può invocare addDiscoveryListener( ) per registrare un listener;
� si può passare un listener come argomento al metodo getRemotedAdvertisements( ).
Il DiscoveryService è così usato per pubblicare advertisements. Tutto ciò è discusso in
maniera più dettagliata nel paragrafo “Creazione di peer group e pubblicazione di
advertisements”.
In tale esempio sono utilizzate le seguenti classi:
� net.jxta.discovery.DiscoveryService: meccanismo asincrono per scoprire peer ,peer
group, pipe e service advertisements e pubblicare advertisements;
� net.jxta.discovery.DiscoveryListener: l’interfaccia che descrive il listener, che serve
ad ascoltare gli eventi DiscoveryService;
� net.jxta.DiscoveryEvent: contiene le tipologie di Discovery Response;
� net.jxta.protocol.DiscoveryResponseMsg: definisce le Discovery Service
“response”.
3.17.2 Discovery Demo
Questo esempio usa l’interfaccia DiscoveryListener per ricevere eventi di notifica
asincroni.
Si definisce una singola classe, chiamata DiscoveryDemo, che implementa l’interfaccia
DiscoveryListener. Così si definisce una variabile di classe:
� PeerGroup netPeerGroup: il nostro peer group (peer group di default) [linea 27]
e quattro metodi:
� public void startJxta( ): inizializza la piattaforma JXTA [linea 35];
� public void esegui( ): thread per spedire una DiscoveryRequest [linea 58];
� public void discoveryEvent(DiscoveryEvent ev): gestisce le DiscoveryResponse
ricevute [linea 96];
� public static void main( ): metodo principale [linea 150]
3.17.2.1 Metodo startJxta( )
Il metodo startJxta( ) istanzia la piattaforma JXTA e crea il net peer group di default
[linea 37]:
gruppo = PeerGroupFactory.newNetPeerGroup( );
Successivamente, il nostro servizio di scoperta è ricevuto dal nostro peer group, il
netPeerGroup [linea 50]:
servizioDiScoperta = gruppo.getDiscoveryService( );
Questo servizio di scoperta sarà utilizzato successivamente per aggiungere il nostro peer
come DiscoveryListener per un evento di DiscoveryResponse e per spedire
DiscoveryRequest.
3.17.2.2 Metodo esegui( )
Il metodo esegui( ) aggiunge, per prima cosa, l’oggetto chiamante come un
DiscoveryListener per eventi DiscoveryResponse [linea 66]:
servizioDiScoperta.addDiscoveryListener(this);
Ora, quando è ricevuta una Discovery Response, il metodo discoveryEvent( ) sarà
chiamato su questo oggetto. Questo abilita la nostra applicazione a essere notificata
asincronamente ogni volta che questo peer riceve una Discovery Response.
Successivamente il metodo esegui( ) cicla all’infinito, spedendo Discovery Request
attraverso il metodo getRemoteAdvertisements( ).
Quest’ultimo riceve sei argomenti:
� java.lang.String peerID: ID del peer a cui spedire la query; se è null propaga una
query request.
� Int type: DiscoveryService.PEER, DiscoveryService.GROUP,
DiscoveryService.ADV.
� java.lang.String attribute: il nome dell’attributo su cui restringere la ricerca.
� java.lang.String value: valore dell’attributo per delimitare il campo di ricerca.
� int threshold: il limite superiore di risposte che devono giungere da un peer.
� net.jxta.discovery.DiscoveryListener listener: servizio di Discovery Listener.
Ci sono due principali modi per inviare una Discovery Request attraverso il Discovery
Service.
1. Se è specificato un peer ID nell’invocazione del metodo getRemoteAdvertisements(
), il messaggio viene spedito solo a tale peer. In questo caso l’endpoint router prova a
risolvere localmente l’endpoint del peer di destinazione.
2. Se è specificato un peer ID null, il messaggio è propagato nella sottorete locale
utilizzando l’IP multicast, e viene anche propagato ai rendezvous peer. Risponderanno
a tale richiesta solo i peer appartenenti allo stesso peer group.
Il parametro type specifica quali tipi di advertisement bisogna vagliare.
La classe DiscoveryService definisce tre costanti:
� DiscoveryService.PEER: cerca i peer advertisements.
� DiscoveryService.GROUP: cerca un group advertisement.
� DiscoveryService.ADV: cerca tutti gli altri tipi di advertisement, come i pipe
advertisement e module class advertisement.
L’area di ricerca può essere ristretta specificando la coppia attribute e value; solo gli
advertisement che corrispondono a tali valori verranno restituiti. La variabile attribute deve
avere un valore uguale ad un elemento nel documento XML associato. La stringa value usa
caratteri jolly (ad esempio, *) per effetturare il confronto. Per esempio le seguenti chiamate
limiteranno la ricerca ai peer, il cui nome contiene esattamente la stringa “test1”:
discovery.getRemoteAdvertisements (null, DiscoveryService.PEER, “Name”, “test1”, 5, null);
Un secondo esempio è quello per cui la chiamata del metodo getRemoteAdvertisements(
) restituisca ogni peer il cui nome contiene la stringa “test”:
discovery.getRemoteAdvertisements (null, DiscoveryService.PEER, “Name”, “*test*”, 5, null);
La ricerca può così essere limitata specificando il valore della variabile threshold, che
indica il limite superiore di risposte da un peer. Nel nostro esempio [linea 73], abbiamo
spedito una Discovery Request alla sottorete locale e ai rendezvous peer, alla ricerca di ogni
peer. Dando alla variabile threshold il valore 5, il nostro peer riceverà un massimo di cinque
risposte (peer advertisements) in ogni Recovery Response.
Se il peer ha più risultati rispetto al numero specificato, il numero esatto di risposte sarà
selezionato casualmente. Poiché il nostro peer è già un DiscoveryListener è stato specificato il
valore null per il parametro finale (DiscoveryListener).
discovery.getRemoteAdvertisements (null, DiscoveryService.PEER, null, null, 5, null);
Non c’è alcuna garanzia che vi sarà risposta ad una Discovery Request, in altre parole un
peer potrebbe ricevere nessuna, una o più risposte.
3.17.2.3 Metodo discoveryEvent( )
Poiché le nostre classi implementano l’interfaccia DiscoveryListener, si deve avere un
metodo discoveryEvent( ) [linea 96]:
public void discoveryEvent(DiscoveryEvent ev)
Il Discovery Service invoca questo metodo ogni volta che è ricevuto una Discovery
Response. I peer scoperti sono addizionati automaticamente alla cache locale (./cm/jxta-
NetGroup/peers). La prima parte di questo metodo stampa un messaggio che contiene il peer
che ci invia la risposta. Al metodo discoveryEvent( ) è passato un singolo argomento di
classe DiscoveryEvent.
Il metodo getResponse( ) restituisce la risposta associata a questo evento e lo memorizza
nella variabile risposta di classe DiscoveryResponseMsg [linea 98]:
DiscoveryResponseMsg risposta = ev.getResponse( );
Ogni oggetto DiscoveryResponseMsg contiene il peer advertisements del peer che ci invia
la risposta, un contatore del numero di risposte restituite, ed un elenco di peer
advertisement(uno per ogni peer scoperto).
Il nostro esempio riceve l’advertisement del peer che risponde al messaggio [linea 102]:
String testoAdvertisement = risposta.getPeerAdv( );
e usa tale stringa contenente l’advertisement del peer per costruire un PeerAdvertisement.
Il metodo statico AdvertisementFactory.newAdvertisement( ) è utilizzato per creare
advertisements [linea 108]:
InputStream inputStream = new ByteArrayInputStream( testoAdvertisement.getBytes( ) );
advertisement = (PeerAdvertisement) AdvertisementFactory.newAdvertisement (new MimeMediaType(
"text/xml" ), inputStream);
I due argomenti passati al metodo AdvertisementFactory.newAdvertisement( ) sono un
oggetto di classe MimeMediaType nel quale si riversano i byte del flusso di input
inputStream di classe ByteArrayInputStream, ottenuto dalla stringa testoAdvertisement
tramite il metodo getBytes( ).
Ottenuto così il peer advertisement, si può stampare una statistica delle risposte ricevute ed
il nome dei peer che hanno risposto [linea 112]:
System.out.println("[Ho ricevuto una Discovery Response che mi informa della presenza di [" +
risposta.getResponseCount() + " peer nella rete] dal peer : " + advertisement.getName()+ " ]" );
La seconda parte di questo metodo stampa il nome di ogni peer scoperto.Le risposte sono
restituite sottoforma di elenco, e possono essere ricevute dal DiscoveryResponseMsg [linea
124]:
Enumeration elenco = risposta.getResponses( );
Per ogni elemento dell’elenco noi creiamo un peer advertisement, quindi riceviamo e
stampiamo il nome del peer [linea 116]:
nuovoAdvertisement = (PeerAdvertisement) AdvertisementFactory.newAdvertisement (new
MimeMediaType( "text/xml" ), new ByteArrayInputStream (stringa.getBytes( )));
System.out.println( " Peer name = " + nuovoAdvertisement.getName());
3.17.2.4 Metodo main( )
Il metodo main( ) [linea 150] crea innanzitutto un nuovo oggetto di classe
DiscoveryDemo. Successivamente invoca il metodo startJxta( ) [linea 152] che istanzia la
piattaforma Jxta. Infine, invoca il metodo esegui( ) [linea 153] che cicla continuamente
spedendo Discovery Request.
3.18 La scoperta di un peer group (Peer Group Discovery)
Questo è molto simile a quello precedendente; la principale differenza è che invece di
spedire DiscoveryRequest per cercare peer, vengono spedite DiscoveryRequest per cercare
peer groups. Ogni DiscoveryRequest ricevuta contiene peer group advertisements piuttosto
che peer advertisements.
Come l’esempio precedente, viene istanziata una piattaforma JXTA e quindi spedita una
DiscoveryRequest al netPeerGroup di default alla ricerca di peer group. Per ogni
DiscoveryResponse ricevuta, l’applicazione stampa il nome del peer che spedisce la risposta,
così come ogni peer group scoperto.
Figura 3.18.1: Esempio di output: esempio di peer group discovery
3.18.1 Il metodo run( )
L’unica differenza, rispetto all’esempio precedente, è che si spedisce la DiscoveryRequest
alla ricerca di peer groups piuttosto che peers [linea 68]:
servizioDiScoperta.getRemoteAdvertisements (null, DiscoveryService.GROUP, null, null, 5, null);
3.18.2 Il metodo discoveryEvent( )
La prima parte di questo metodo è identica all’esempio precedente: noi riceviamo un
DiscoveryResponseMsg, estraiamo gli advertisement dei peer che rispondono (una stringa),
successivamente crea un oggetto peer advetisement usando la stringa.
Una volta che si è in possesso del peer advertisement, si può stampare il messaggio
contenente il nome del peer rispondente ed il numero di risposte. Le differenze si trovano
nella seconda parte del metodo, che stampa il nome di ogni peer group scoperto.
Come l’esempio di Peer Discovery, le risposte sono restituite sottoforma di elenco e sono
ricevute da DiscoveryResponseMsg [linea 92]:
Enumeration elenco = risposta.getResponses( );
Ora, invece di creare peer advertisements, noi creiamo peer group advertisements [linea
128]:
newAdv = (PeerGroupAdvertisement) AdvertisementFactory.newAdvertisement (new MimeMediaType (
"text/xml" ), new ByteArrayInputStream (str.getBytes( )));
System.out.println( " Peer Group = " + newAdv.getName());
3.19 Creare Peer Groups e pubblicare advertisements
Questo esempio crea un nuovo peer group e stampa il suo nome e il suo ID, quindi
pubblica il suo advertisement.
In figura è riportato l’output dell’applicazione:
Figura 3.19.1: Esempio output: creazione e pubblicazione di peergroup
Dopo il completamento di questo programma, si può verificare che l’ advertisement del
peer group sia stato aggiunto alla cache locale ./cm. L’advertisement del nuovo gruppo è
aggiunto al gruppo genitore, il mioGruppo creato dal nostro esempio: (jxta-
NetGroup/Groups directory). Inoltre, viene creata una nuova directory che ha lo stesso nome
del peer group ID, che contiene sottodirectory destinate a contenere peer, peer groups e altri
advertisement scoperti nella rete.
Un advertisement per il nostro peer è aggiunto alla sottodirectory corrispondente, dove
saranno aggiunti gli advertisement per ogni peer addizionale scoperto nel nuovo peer group.
� .cm/jxta-NetGroup/Groups/1D5E451AF1B243C1AD49B9D331AE858C02:
advertisement per il nuovo gruppo.
� .cm/1D5E451AF1B243C1AD49B9D331AE858C02: directory per il nuovo peer
group.
� .cm/1D5E451AF1B243C1AD49B9D331AE858C02/Peers/<peer id>: il nostro peer
advertisement.
3.19.1 Il metodo main( )
Questo metodo invoca startJxta( ) per avviare la piattaforma JXTA e creare un
mioGruppo di default. Inoltre invoca creaGruppo( ), per creare un nuovo peer group e
pubblicare il suo advertisement.
3.19.2 Il metodo startJxta( )
Questo metodo è identico agli esempi precedenti. Esso istanzia la piattaforma JXTA ed
estrae informazioni necessarie nel resto dell’applicazione:
� istanzia la piattaforma JXTA e crea il net peer group di default [linea 43];
mioGruppo = PeerGroupFactory.newNetPeerGroup();
� estrae il discovery service dal peer group; questo è usato successivamente per
pubblicare l’advertisement del nuovo gruppo [linea 58]
servizioDiScoperta = mioGruppo.getDiscoveryService();
3.19.3 createGroup( )
Questo metodo è utilizzato per creare un nuovo peer group è pubblicare i suoi
advertisement. La prima parte di questo metodo[linea 73] crea il nuovo peer group.
Inizialmente, si invoca getAllPurposePeerGroupImpleAdvertisement( ) per creare un
ModuleImplAdvertisement, che contiene voci per tutti i peer group services del core [linea
71]
ModuleImplAdvertisement implAdv = mioGroup.getAllPurposePeerGroupImplAdvertisement( );
Successivamente utilizziamo newGroup( ) per creare un nuovo peer group [linea 73].
PeerGroup nuovoPeerGroup = mioGruppo.newGroup(null, implAdv, "Gruppo Tesi", "Gruppo per la prova
della creazione di nuovi peer group");
Passiamo quattro argomenti al metodo newGroup( ):
� PeerGroupID gid: il peer group ID del gruppo che verrà creato; se è null, viene
creato un nuovo peer group ID.
� Advertisement implAdv: l’implementazione dell’advertisement.
� String name: il nome del nuovo gruppo.
� String description: una descrizione del gruppo.
Quando viene creato un nuovo gruppo tramite newGroup( )4, il suo advertisement è
sempre aggiunto alla cache locale. Esso usa il valore di default per la scadenza
4 Dal momento in cui il metodo newGroup( ) crea un nuovo gruppo esso è anche pubblicato, non è quindi necessario
invocare esplicitamente il metodo DiscoveryService.publish( )
dell’advertisement; il tempo di vita locale è di 365 giorni (il tempo di vita dell’advertisement
è memorizzato localmente dal peer che lo genera), ed un tempo di vita remoto di due ore (il
tempo dell’advetisement è mantenuto nella cache del peer che ha richiesto e ricevuto
l’advertisement). Dopo la creazione del gruppo, stampiamo il nome del gruppo è il suo peer
group ID.
La seconda parte del metodo pubblica in remoto il nuovo peer group advertisement [linea
102].
servizioDiScoperta.remotePublish(adv, DiscoveryService.GROUP);
Questo metodo riceve due argomenti: l’advertisement da pubblicare e il tipo di
advertisement; utilizza il tempo di scadenza di default.
La sua invocazione usa il discovery service per spedire messaggi alla sottorete locale e così
a tutti i rendezvous peers.
3.20 Unire un peer group
Questo esempio crea e pubblica un nuovo peer, unisce il peer group, e stampa le sue
credenziali di autorizzazione.
In figura è mostrato l’output di quando l’applicazione è in esecuzione:
Figura 3.20.1: Esempio di output: creare e unire un peer group
Questo esempio si costruisce sul precedente esempio che crea e pubblica un nuovo gruppo.
Il nuovo codice in questo esempio e nel metodo joinGroup( ), che illustra come fare per
appartenere ad un gruppo e quindi unirlo.
Questo esempio utilizza il meccanismo di default per unire un gruppo.
3.20.1 Servizio di appartenenza ad un gruppo (Membership Service)
In JXTA, il Membership Service è usato per fare in modo di appartenere ad un gruppo,
unire un peer group e abbandonare un peer group.
Il Membership Service permette ad un peer di assumere un’identità in un peer group.
Quando è stata stabilita la sua identità, sono disponibili delle credenziali che permettono al
peer di essere stato identificato correttamente. Le identità sono utilizzate dai servizi per
determinare i permessi che possono essere concessi al peer. Quando un peer group è
istanziato su un peer, il Membership Service per questo peer group assegna al peer un’identità
temporanea di default. Quest’identità temporanea , per convenzione, consente solo al peer di
stabilire la sua vera identità.
La sequenza per l’assegnazione dell’identità ad un peer in un peer group è la seguente:
� Apply: Il peer fornisce al Membership Servise le credenziali iniziali che possono
essere utilizzate dal servizio, per determinare quale metodo di autenticazione deve
essere usato per determinare l’identità di questo peer. Se il servizio concede
l’autenticazione usando il meccanismo richiesto, allora è restituito un’appropriato
oggetto autenticatore. Si suppone che il peer group sappia come interagire con
l’oggetto autenticatore (si ricordi che prima di applicare il processo è richiesto il
metodo di autenticazione).
� Join: l’autenticazione completa è restituita al Membership Service e l’identità di
questo peer è adattata in base alle credenziali fornite dall’autenticatore. L’identità del
peer rimane così come era, fino a quando non viene completata l’operazione di Join.
� Resign. È scartata qualsiasi identità esistente stabilita per questo peer, e l’identità
corrente ritorna all’identità nobody.
Le credenziali di autenticazione sono utilizzate dai servizi Membership Service come base
per le applicazioni per l’apparteneza al peer group.
Le AutenticationCredential forniscono due importanti pezzi di informazione: il metodo
di autenticazione richiesto e le informazioni di identità che saranno fornite a questo metodo di
autenticazione.
Non tutti i metodi di autenticazione utilizzano le informazioni sull’identità.
3.20.2 main( )
Questo metodo chiama i seguenti tre metodi:
� startJxta( ): per istanziare la piattaforma e creare il net peer group di default( ) [linea
35];
� createGroup( ): per creare e pubblicare un nuovo peer group [linea 36];
� joinGroup( ): per unire il nuovo gruppo [linea 38].
3.20.3 startJxta( )
Questo metodo è uguale a quello dell’esempio precedente.
3.20.4 createGroup( )
Questo metodo [linea 66] è quasi identico a quello dell’esempio precedente. Esso è usato
un nuovo peer group e pubblicare il suo advertisement. La sola differenza significativa e che
se il gruppo è creato con successo esso restituisce il nuovo peer group. Se si verifica un errore
durante la creazione del nuovo peer group il metodo restituisce null.
3.20.5 joinGroup( )
Il metodo è utilizzato per unire il peer group che è passato come argomento [linea 108]:
private void joinGroup(PeerGroup gruppo)
Nel codice di esempio il metodo joinGroup( ) inizialmente genera le credenziali di
autenticazione per il peer nel peer group specificato [linea 116]:
AuthenticationCredential authCred = AuthenticationCredential(gruppo, null, credenziali);
Questo costruttore prende tre argomenti:
� PeerGroup peerGroup: il peer group in cui vengono create queste credenziali di
autenticazione (il peer group che vogliamo unire).
� java.lang.String metodo: il metodo di autenticazione che sarà richiesto quando le
credenziali di autenticazione è fornita al servizio di MemberShipService del peer
group.
� Element identifyInfo: informazione aggiuntiva opzionale riguardante l’identità
richiesta, utilizzata dal metodo di autenticazione. Quest’informazione è passata al
metodo di autenticazione durante l’operazione di apply( ) del Membership Service.
Le credenziali di autenticazione sono create nel contesto di un peer group. Tuttavia, sono
generalmente indipendenti dal peer group. L’intenzione è di passare le credenziali di
autenticazione al Membership Service dello stesso peer group. Successivamente il nostro
esempio estrae il MemberShip Service dal peer group che noi vogliamo unire [linea 121]:
MembershipService appartenenza = gruppo.getMembershipService( );
e usa il metodo MembershipService.apply( ) per applicare l’appartenenza al gruppo [linea
125]:
Authenticator autenticatore = appartenenza.apply( authCred );
Le credenziali di autenticazione create precedentemente sono passate al metodo apply( ).
Nelle credenziali è inclusa l’informazione riguardante il nostro peer group ID, il nostro
peer ID e la nostra identità da utilizzare quando uniamo questo gruppo.
Il metodo apply( ) restituisce un oggetto Authenticator, che è utilizzato per verificare che
l’autenticazione sia stata completata correttamente. Il meccanismo per completare l’oggetto di
autenticazione è unico per ogni metodo di autenticazione. La sola operazione comune è
isReadyForJoin( ), che fornisce informazioni se sia stato o meno completato correttamente il
processo di autenticazione.
Dopo essere riusciti ad applicare l’appartenenza, il passo successivo è l’unione del gruppo.
Per prima cosa, il metdo Authenticator.isReadyForJoin( ) è invocato per verificare il
processo di autenticazione. Questo metodo restituisce true se l’oggetto autenticatore è
completo e pronto ad essere sottoposto al Memebership Service per l’unione; altrimenti esso
restituisce false. Se tutto è andato a buon fine nell’unire il group, il metodo
MembershipService.join( ) è invocato per unire il gruppo [linea 129]:
if (autenticatore.isReadyForJoin( )){
Credential miaCredenziale = appartenenza.join(autenticatore);
Il metodo MembershipService.join( ) restituisce un oggetto Credential.
3.21 Scambio di messaggi fra due peer
Questo esempio illustra come usare le pipe per spedire messaggi tra due peer JXTA.
In questo esempio sono usate due applicazioni separate:
� PipeListener: legge in un pipe advertisement contenuto in un file (examplepipe.adv),
crea un input pipe e ascolta i messaggi su di essa.
� PipeExample: legge un pipe advertisement contenuto in un file (examplePipe.adv),
crea un outputPipe e manda un messaggio su di essa.
Le figure in basso mostrano l’output quando sono avviate le applicazioni PipeListener e
PipeExample5:
Figura 3.21.1: Esempio di output: PipeListener
5 se si avviano entrambe le applicazioni sullo stesso sistema, sarà necessario avviare ogni applicazione da una sottodirectory
separata, cosicché può essere configurate ad usare orte separate.
Figura 3.21.2: Esempio di output: PipeExample
La seguente sezione fornisce le informazioni di fondo su pipe service, input pipe e output
pipe.
3.21.1 JXTA Pipe Service
La classe PipeService definisce un set di interfacce per creare e accedere alle pipe in un
peer group. Le pipe sono il cuore del meccanismo di scambio messaggi fra due applicazioni o
servizi JXTA. Le pipe forniscono un canale di comunicazione fra due peer semplice,
unidirezionale e asincrono. I messaggi sono scambiati fra input pipe e output pipe.
Un’applicazione, che vuole aprire una comunicazione in ricezione con altri peer, crea un
input pipe e la lega ad un pipe advertisement specifico. L’applicazione quindi pubblica il pipe
advertisement, cosicché le altri applicazioni o servizi possono ottenere l’advertisement e
creare output pipes corrispondenti per spedire messaggi all’input pipe. Le pipe sono
univocamente identificate in tutto l’ambiente JXTA da un PipeId (UUID) racchiuso in un pipe
advertisement. Questo PipeId unico è utilizzato per creare l’associazione fra input e output
pipes.
Le pipe sono canali di comunicazione non localizzati che non sono limitate ad un peer
specifico. Questa è una caratteristica unica delle pipe JXTA. Il meccanismo per risolvere la
locazione delle pipe ad un peer fisico è fatto in una maniera completamente decentralizzata,
attraverso il JXTA Pipe Binding Protocol
Il PBP non si poggia su un protocollo centralizzato come un DNS per legare un pipe
advertisement (nome simbolico) ad un’istanza di una pipe in un peer fisico (indirizzo IP).
Invece il protocollo di risoluzione utilizza un meccanismo di ricerca dinamico e adattabile
che tenta sempre di trovare i peer dove è in esecuzione un’istanza di questa pipe.
Le seguenti classi sono usate nelle applicazioni PipeListener e PipeExample:
� net.jxta.pipe.PipeService: definisce le api del pipe service.
� net.jxta.pipe.InputPipe: definisce l’interfaccia per ricevere messaggi da un pipe
service. Un’applicazione che desideri recevere un messaggio da un pipe creerà un
input pipe. Un’ InputPipe è creata e restituita dal pipe service.
� net.jxta.pipe.PipeMsgListener: l’interfaccia ascoltatore per ricevere PipeMsgEvent.
� net.jxta.pipe.PipeMsgEvent: contiene gli eventi ricevuti da una pipe.
� net.jxta.pipe.OutputPipe: definisce l’interfaccia per spedire messaggi da un
PipeService. Le applicazioni che vogliono spedire messaggi su una pipe devono
innanzitutto ottenere una OutputPipe dal PipeService.
� net.jxta.pipe.OutputPipeListener: l’interfaccia ascoltatore per ricevere eventi di
risoluzione OutputPipe.
� net.jxta.pipe.OutputPipeEvent: contiene gli eventi ricevuti quando è risolta una
OutputPipe.
� net.jxta.endpoint.Message: definisce l’interfaccia di messaggi spediti a pipe o
ricevuti da pipe usando le API PipeService. Un messaggio contiene un set di
MessageElements.
3.21.2 Pipe listener
Questa applicazione crea un ascoltatore per i messaggi su una input pipe. Definisce una
singola classe, PipeListener, che implementa le interfacce Runnable e PipeMsgListener.
Due costanti di classe contengono informazioni sulle pipe da creare:
� String FILENAME: il file XML contenente la rappresentazione testuale del nostro
pipe advertisement (questo file deve esistere, e deve contenere un pipe advertisement
valido, affinché la nostra applicazione venga eseguita correttamente).
� String TAG: il nome del messaggio, o tag che ci aspettiamo in ogni messaggio che
riceviamo.
Definiamo quindi quattro variabili:
� PeerGroup netPeerGroup: il nostro peer group, quello di default.
� PipeService pipeService: il pipe service che usiamo per creare le input pipe ed
ascoltare se arrivano messaggi.
� PipeAdvertisement pipeAdvertisement: il pipe advertisement che usiamo per creare
le nostre input pipe.
� InputPipe pipeIn: l’input pipe che creiamo.
3.21.2.1 main( )
Il metodo [linea 39] crea un nuovo oggetto PipeListener, invoca startJxta( ) per istanziare
la piattaforma JXTA, crea il net peer group di default ed invoca il metdo run( ) che crea
l’input pipe e registra questo oggetto come un PipeMsgListener (quest’applicazione non
termina mai, a causa di un thread Java “invisibile” che agisce inviando input pipe event).
3.21.2.2 startJxta( )
Questo metodo istanzia la piattaforma JXTA e crea il net peer group di default [linea 53]]:
netPeerGroup = PeerGroupFactory.newNetPeerGroup( );
A questo punto riceve il PipeService dal net peer group di default [linea 65]. Questo
servizio è utilizzato successivamente quando creiamo una input pipe:
pipeService = netPeerGroup.getPipeService( );
Successivamente, creiamo un pipe advertisement leggendolo da un file esistente
examplepipe.adv [linea 69]:
FileInputStream fileInputStream = new FileInputStream(FILENAME);
Il file examplepipe.adv deve esistere e deve essere un documento XML valido, contenente
un pipe advertisement, altrimenti viene sollevata un eccezione dalla piattaforma Jxta.
Questa applicazione (che crea una input pipe) e l’applicazione associata (che crea l’output
pipe) leggono il loro pipe advertisement dallo stesso file. Il contenuto del file
examplepipe.adv è presentato in figura 4.5.4.1. Il metodo
AdvertisementFactory.newAdvertisement( ) è invocato per creare un nuovo pipe
advertisement [linea 70]:
pipeAdvertisement = (PipeAdvertisement) AdvertisementFactory.newAdvertisement(new
MimeMediaType("text/xml"),
fileInputStream);
I due argomenti di questo metodo sono il MIME type (“text/xml” in questo esempio), da
associare al documento strutturato risultante (per esempio l’advertisement) e l’InputStream
contenente il corpo (body) dell’advertisement. Il tipo dell’advertisement è determinato dalla
lettura dell’InputStream.
Infine viene creato il pipe advertisement, viene chiuso l’InputStream [linea 73] e il metodo
termina:
fileInputStream.close( ).
3.21.2.3 run( )
Questo metodo utilizza il metodo PipeService.createInputPipe( ) per creare una nuova
input pipe per la nostra applicazione [linea 91]:
pipeIn = pipeService.createInputPipe(pipeAdvertisement, this);
Poiché vogliamo leggere un evento da una input pipe, invochiamo createInputPipe( ) con
due argomenti:
� PipeAdvertisement adv: l’advertisement della pipe che è stata creata;
� PipeMsgListener listener: l’oggetto che riceverà gli eventi generati dai messaggi
sulla input pipe;
Registrando il nostro oggetto come ascoltatore, quando noi creiamo l’input pipe, il nostro
metodo pipeMsgEvent( ) sarà invocato asincronamente ogni volta che si verifica su questa
pipe un PipeMsgEvent (per esempio: ogni volta che viene ricevuto un messaggio).
3.21.2.4 pipeMsgEvent( )
Questo metodo [linea 111] è invocato asincronamente ogni volta che si verifica un pipe
event sulla nostra input pipe.
A questo metodo viene passato un argomento:
� PipeMsgEvent event: l’evento che si verifica sulla pipe.
Inizialmente il nostro metodo invoca PipeMsgEvent.getMessage( ) per ricevere il
messaggio associato con l’evento [linea 116]:
messaggio = event.getMessage( );
Ogni messaggio contiene zero o un elemento, ognuno con un nome di elemento associato
(tag) e una stringa corrispondente. Il nostro metodo invoca messaggio.getString( ) per estrarre
la stringa corrispondente al messaggio identificato dal tag passato come argomento [linea
128]:
String testoMessaggio = messaggio.getString(TAG);
Se il tag non è presente nel messaggio, il metodo restituisce null.
Si ricordi che sia l’input pipe che l’output pipe devono essere in accordo con il tag
utilizzato nel messaggio. Nel nostro esempio, noi impostiamo nella classe PipeListener una
costante per riferirci al tag [linea 32]:
private final static String TAG = "Ascoltatore di messaggi sulla pipe";
Infine, il nostro metodo stampa il messaggio che ha ricevuto [linea 133]:
System.out.println("Il messaggio ricevuto è: " + testoMessaggio);
3.21.3 PipeExample
Questo esempio crea una output pipe sulla quale spedisce un messaggio. Definisce una
singola classe, PipeExample, che implementa le interfacce Runnable e OutputPipeListener.
Come la classe corrispondente, PipeListener, definisce due costanti per contenere
informazioni sulle pipe da creare:
� String FILENAME: il file XML contenente la rappresentazione testuale del nostro
PipeAdvertisement.
� String TAG: il nome del tag, che vogliamo includere in ogni messaggio che
spediamo.
3.21.3.1 main( )
Questo metodo [linea 36] crea un nuovo oggetto PipeExample, invoca startJxta( ) per
istanziare la piattaforma Jxta e crea il net peer group di default, e quindi chiama il metodo
run( ), che crea una output pipe.
3.21.3.2 startJxta( )
Questo metodo istanzia la piattaforma Jxta e crea il netPeerGroup di default [linea 103]:
netPeerGroup = PeerGroupFactory.newNetPeerGroup( );
Quindi ottiene il PipeService e il DiscoveryService dal net peer group di default [linea
118]. Questi servizi sono utilizzati successivamente, quando creeremo una input pipe:
pipeService = netPeerGroup.getPipeService( );
discoveryService = netPeerGroup.getDiscoveryService( );
Successivamente, creiamo un pipe advertisement, leggendolo da un file XML esistente
[linea 123]:
FileInputStream fileInputStream = new FileInputStream(FILENAME);
Il file examplepipe.adv deve esistere e deve essere un documento XML valido, contenente
un pipe advertisement, o sarà generata un’eccezione dalla piattaforma Jxta. Si ricordi che
l’applicazione che crea l’input pipe, legge il suo pipe advertisement dallo stesso file. Il
contenuto di examplepipe.adv si può vedere in figura 3.22.1.
Come nel precedente esempio, PipeListener, il metodo
AdvertisementFactory.newAdvertisement( ) è invocato per creare un nuovo pipe
advertisement [linea 124]:
pipeAdv = (PipeAdvertisement) AdvertisementFactory.newAdvertisement (new
MimeMediaType("text/xml"),
fileInputStream);
Poi viene creato il pipe advertisement, viene chiuso l’input stream [linea 127] e il metodo
termina:
fileInputStream.close( );
3.21.3.3 run( )
Questo metodo utilizza PipeService.createOutputPipe( ) per creare una nuova output pipe
per l’applicazione [linea 58]:
pipeService.createOutputPipe(pipeAdv, this);
Poichè noi vogliamo essere notificati quando i pipe endpoint sono risolti, invochiamo
createOuputPipe( ) passando due argomenti:
� PipeAdvertisement adv: l’advertisement della pipe da creare;
� OutputPipeListener listener: l’ascoltatore che deve essere invocato quando viene
determinata la pipe.
Registrando il nostro oggetto come ascoltatore, quando creiamo una output pipe, il nostro
metodo outputPipeEvent( ) sarà invocato asincronamente quando viene determinato il pipe
endpoint.
3.21.3.4 outputPipeEvent( )
Poiché abbiamo implementato l’interfaccia OutputPipeListener, dobbiamo definire il
metodo OutputPipe( ). uesto metodo [linea 74] è invocato asincronamente dalla piattaforma
Jxta, quando viene determinato il nostro pipe endpoint.
A questo metodo è passato un argomento:
� OutputPipeEvent event: l’evento che avviene su questa pipe;
Il nostro metodo per prima cosa chiama OuputPipeEvent.getOutputPipe( ) per creare
l’output pipe , ottenendola dall’evento [linea 77]:
OutputPipe op = event.getOutputPipe( );
Dopo, invochiamo PipeService.createMessage( ) per creare un nuovo messaggio [linea 82]:
Message msg = pipeService.createMessage( );
Ogni messaggio zero o un elemento, ognuno dei quali ha un tag associato e una stringa
corrispondente. ia l’input pipe che l’outpup pipe devono accettare lo stesso tag usato nel
messaggio. Si ricordi che settiamo una costante in entrambe le classi, PipeListener e
PipeExample, per contenere il TAG [linea 30]:
private final static String TAG = "Ascoltatore di messaggi sulla pipe";
Noi usiamo il metodo msg.setString( ) per aggiungere questo nuovo elemento al messaggio
[linea 83]:
msg.setString(TAG, myMsg);
Adesso che è stato creato il messaggio e contiene il testo, lo spediamo sulla output pipe
con l’invocazione di op.send(msg) [linea 84]. Dopo la spedizione di questo messaggio,
chiudiamo l’output pipe e terminiamo questo metodo.
3.21.4 Pipe Advertisement: il file examplepipe.adv
Il file XML contenente il pipe advertisement, example.adv, è riportato in figura. Questo
file è letto da entrambe le classi PipeListener e PipeExample6, per creare l’input pipe e la
output pipe. Entrambe le classi devono usare lo stesso pipe-id, in modo da comunicare tra di
loro.
Figura 3.21.1: il file example.adv
CAPITOLO 4
6 Entrambe le classi leggono dalla stesso file che si trova nella directory corrente. Se questo file non esiste, o contiene un piep advertisement non valido, l’applicazione genera un’eccezione
CAGE PEER–TO-PEER: L’IMPLEMENTAZIONE
Introduzione
CAGE peer to peer è un’applicazione distribuita su una rete di nodi, ognuno di essi è
completamente indipendente ed equivalente. Nessun nodo possiede informazioni o funzioni
speciali; in altre parole non esistono server. Inoltre ogni nodo deve possedere sufficienti
informazioni sul resto della rete, in modo da restare connesso e a sua volta fornire
informazioni.
Ogni nodo possiede una cache locale che contiene informazioni sui peer facenti parte della
rete, questa cache viene aggiornata periodicamente.
Poiché le caratteristiche della programmazione genetica si adattano in modo naturale alla
filosofia peer-to-peer, si è assunto che ogni applicazione avviata su ciascun nodo, sia formata
da un certo numero di task che possano lavorare in modo del tutto indipendente rispetto ai
task su altri nodi, con velocità differenti che dipendono dall’architettura della macchina su cui
sono eseguiti. Le uniche interazioni che si verificano fra i nodi sono le comunicazioni che, di
tanto in tanto, servono per diversificare le popolazioni elaborate su ciascun nodo.
L’approccio che è stato utilizzato per la realizzazione dell’applicazione si è ispirato al
modello a isole e a quello cellulare.
La piattaforma utilizzata per la realizzazione del software è JXTA, che abbiamo visto in
dettaglio nel capitolo 3. L’interfaccia è realizzata in modo da far si che possa gestire l’accesso
di tre differenti tipologie di utenti (figura 4.1):
Fig. 4.1: Architettura di CAGE peer to peer
� Utente A: utente che non desidera utilizzare l’applicativo per eseguire calcoli, ma
vuole semplicemente mettere a disposizione la potenza di calcolo della propria CPU;
� Utente B: questa tipologia di utenti comprende coloro che pur non essendo
programmatori, hanno intenzione di utilizzare l’applicazione per condurre esperienze
che sono già a disposizione;
� Utente C: utenti capaci di programmare la piattaforma e quindi eseguire esperimenti
specifici.
4.1 Le classi della nostra applicazione
Sono state sviluppate le seguenti classi:
� CanaleDiInput: implementa un canale di input unidirezionale asincrono;
� CanaleDiOutput: implementa un canale di output unidirezionale asincrono;
� Esploratore: fornisce i servizi per la ricerca dei vicini e per la configurazione
dell’anello;
� GruppoProgrammazioneGenetica: classe per la formazione del gruppo di lavoro per
la programmazione genetica;
� Peer: implementa un nodo e i suoi servizi;
� Utilità: fornisce le utilità per l’utilizzo degli advertisement.
4.2 La classe CanaleDiInput
Questa classe implementa l’interfaccia PipeMsgListener, che serve per l’ascolto degli
eventi generati dai messaggi ricevuti sulla pipe di input. Gli attributi della classe sono i
seguenti:
� private final static String TAG: contiene il nome del tag in cui è contenuto il
messaggio ricevuto;
� private Utility util;
� private PeerGroup gruppo: serve a memorizzare il gruppo di lavoro;
� private PipeService pipeService: variabile utilizzata per memorizzare il servizio per
l’utilizzo delle pipe;
� private Message message: buffer che contiene il messaggio momentaneamente
ricevuto;
� InputPipe pipeIn: variabile su cui verrà aperta la pipe di input.
Questa classe è fornita di un costruttore, che prende come parametro il gruppo di lavoro, e
dei seguenti metodi:
� public byte[] restituisciMessaggio(): restituisce il messaggio, codificato come array
di byte;
� public byte[] ricevibile(String nomeFile): restituisce il contenuto del file, il cui
nome è passato come argomento, codificato come array di byte;
� public void ascoltaSu(PeerID pID): apre la pipe di input per ricevere i messaggi dal
peer specificato;
� public void chiudi(): chiude la pipe di input;
� public void pipeMsgEvent(PipeMsgEvent event): ascoltatore degli eventi generati
dalla ricezione dei messaggi.
4.3 La classe CanaleDiOutput
Questa classe implementa l’interfaccia OutputPipeListener, che serve per l’ascolto degli
generati dai messaggi inviati sulla pipe di output. Gli attributi della classe sono i seguenti:
� private final static String TAG: contiene il nome del tag in cui è contenuto il
messaggio ricevuto;
� private Utility util;
� private PeerGroup gruppo: serve a memorizzare il gruppo di lavoro;
� private PipeService pipeService: variabile utilizzata per memorizzare il servizio per
l’utilizzo delle pipe;
� private Message message: buffer che contiene il messaggio momentaneamente
ricevuto;
� OutputPipe pipeOut: variabile su cui verrà aperta la pipe di output.
Questa classe è fornita di un costruttore, che prende come parametro il gruppo di lavoro, e
dei seguenti metodi:
� public void apri(PeerID pID): crea una pipe di output per inviare il messaggio al
peer specificato;
� public void invia(byte[] msg): questo metodo invia il messaggio specificato al peer
verso cui è stata aperta la pipe di output;
� public void inviaFile(String nomeFile, byte[] msg): questo metodo invia il
contenuto del file specificato nella lista dei parametri;
� public void chiudi(): chiude la pipe di output;
� public void outputPipeEvent(OutputPipeEvent event): ascoltatore degli eventi
generati dalla spedizione dei messaggi.
4.4 La classe Esploratore
Questa classe implementa l’interfaccia DiscoveryListener, che serve per l’utilizzo dei
servizi relativi alla scoperta dei peer sulla rete. Gli attributi della classe sono i seguenti:
� public static PeerID peerDestro;
� public static PeerID peerSinistro;
� public static PeerID auxDestro;
� public static PeerID auxSinistro;
� public static PeerID peerR;
� public static PeerID peerL.
Queste variabili contengono l’id dei peer vicini, con cui si dobrà creare il link logico per
formare l’anello.
� private PeerGroup gruppo: serve a memorizzare il gruppo di lavoro;
� private DiscoveryServiceservizioDiScoperta: serve a memorizzare il servizio di
scoperta per il gruppo di lavoro.
Questa classe è fornita di un costruttore, che prende come parametro il gruppo di lavoro, e
dei seguenti metodi:
� public void aggiornaCacheLocale(): è un metodo che viene utilizzato per
l’aggiornamento della cache locale di ogni peer;
� private Vector ordinaCacheLocale(): il metodo viene utilizzato per ordinare in
modo crescente le voci della cache locale, in base al peerID.
� public void individuaVicini(): la funzione di questo metodo è l’individuazione dei
peer destro e sinistro con cui si effettuerà lo scambio delle informazioni.
4.5 La classe GruppoProgrammazioneGenetica
Questa classe modella il gruppo di lavoro di cui tutti i peer dovranno far parte, per
partecipare all’elaborazione degli algoritmi di programmazione genetica. Gli attributi della
classe sono i seguenti:
� private final static String NOME: contiene il nome del gruppo di lavoro;
� private final static String DESCRIZIONE: contiene informazioni sul gruppo di
lavoro;
� private static PeerGroup netPeerGroup: memorizza il netPeerGroup di default, a
cui appartengono tutti i peer JXTA mondiali;
� private static PeerGroup gruppoProgrammazioneGenetica: serve per
memorizzare il peerGroup creato con i servizi all’interno di questa classe;
� private PeerGroupID PGID: la variabile contiene l’identificativo in formato
esadecimale del gruppo creato.
Questa classe è fornita di un costruttore, che prende come parametro il gruppo di lavoro, e
dei seguenti metodi:
� private void creaGruppo(): crea il gruppo di lavoro;
� private void uniscimiAlGruppo(): effettua il join del peer richiedente al gruppo di
lavoro;
� public PeerGroup get(): restituisce il gruppo di lavoro.
4.6 La classe Peer
Questa è la classe base che utilizza tutti i servizi offerti dalla piattaforma; in essa viene
avviata la piattaforma JXTA per il peer in questione, viene effettuato il join con il gruppo di
lavoro ed estratti il DiscoveryService.
A questo punto viene inizializzata la cache per eliminare eventuali voci inconsistenti e
vengono inizializzate tutti gli attributi della classe, che sono:
� private GruppoProgrammazioneGenetica gruppo: contiene il gruppo di lavoro;
� private Esploratore explorer: entità necessaria per la scoperta dei vicini e la
configurazione dell’anello;
� private DiscoveryService servizioDiScoperta: memorizza il servizio di scoperta
estratto per il gruppo di programmazione genetica;
� public Utility util: utilità di gestione degli advertisement;
� private CanaleDiInput inR: canale di input per la ricezione dei messaggi dal vicino
di destra;
� private CanaleDiInput inL: canale di input per la ricezione dei messaggi dal vicino
di sinistra;
� private CanaleDiOutput outR: canale di output per l’invio dei messaggi al vicino di
destra;
� private CanaleDiOutput outL: canale di output per l’invio dei messaggi al vicino di
sinistra;
� private Thread inviaADestra: thread che permette di inviare messaggi al peer di
destra, parallelamente all’esecuzione del resto del programma;
� private Thread inviaASinistra: thread che permette di inviare messaggi al peer di
sinistra, parallelamente all’esecuzione del resto del programma;
� private String arrayParametri[]: questo array è necessario per riceverei parametri
da passare al processo C che realizza l’infrastruttura per l’esecuzione di algoritmi di
programmazione genetica;
� private byte[] bufferDiTrasmissioneDestro: variabile che memorizza l’albero da
spedire al peer di destra;
� private byte[] bufferDiTrasmissioneSinistro: variabile che memorizza l’albero da
spedire al peer di sinistra;
� private byte[] bufferDiRicezioneDestro: variabile che memorizza l’albero ricevuto
dal peer di destra;
� private byte[] bufferDiRicezioneSinistro: variabile che memorizza l’albero ricevuto
dal peer di sinistra;
� private MetodiNativi m: entità che fornisce i servizi per invocare i metodi nativi;
� public boolean pronto[]: array di booleani per memorizzare lo stato di elaborazione,
per la sincronizzazione della fase di configurazione dell’anello;
� private boolean inviatoSx e private boolean inviatoDx: booleani che memorizzano
lo stato di ricezione dei messaggi;
Questa classe è fornita di un costruttore, che non riceve alcun parametro e avvia la
piattaforma:
� public void startJxta(): effettua le azioni descritte all’inizio del paragrafo;
� private inizializzaCache: inizializza la cache del peer;
� public PeerGroup getGruppo(): restituisce il gruppo di lavoro;
� public void ConfiguraAnello(): metodo che realizza la configurazione dell’anello.
� public void attendi(): realizza l’attesa dell’avvio dell’elaborazione, da parte di un
qualunque altro peer appartenente al gruppo di lavoro;
� public void estraiParametri(): metodo che carica nell’arrayParametri, i valori da
passare al processo C.
� public void ricevi(): realizza la ricezione dei file di configurazione dell’algoritmo di
programmazione genetica, inviati dal peer che ha avviato l’elaborazione.
� public void run(): azioni eseguite da ogni peer;
� private void riceviR(): riceve il messaggio inviato dal peer di destra;
� private void riceviL(): riceve il messaggio inviato dal peer di sinistra.
4.7 La classe Utility
Questa classe fornisce tutti i servizi per la gestione degli advertisement.Gli attributi della
classe sono i seguenti:
� private static PeerGroup gruppo memorizza il gruppo di lavoro;
� private DiscoveryService servizioDiScoperta: memorizza il servizio di scoperta
estratto per il gruppo di programmazione genetica.
Questa classe è fornita di un costruttore, che prende come parametro il gruppo di lavoro, e
dei seguenti metodi:
� public void creaPipeAdvertisement(String nome, String tipo): crea un pipe
advertisement per canali del tipo passato come argomento;
� public void creaPeerGroupAdvertisement(String nome): crea un peer group
advertisement con il nome specificato;
� public void creaConfigurationAdvertisement(): crea un advertisement che viene
utilizzato durante la configurazione dell’anello;
� public void token(String mioID, String peerID): realizza il token che percorre
l’anello a fine elaborazione, per la raccolta dei risultati;
� public int contaAdvertisement(int tipo, String tag, String val): metodo che conta
gli advertisement nella cache locale, che corrispondono ai valori passati come
parametro;
� public PipeAdvertisement estraiPipeAdvertisement(String nome, int tipo):
metodo che viene utilizzato per l’estrazione di un pipe advertisement dalla cache
locale, con il nome e il tipo specificato;
� public PipeAdvertisement estraiFileAdvertisement(String nome): estrae
l’advertisement che ha il contenuto del file di configurazione della programmazione
genetica, spedito dal peer che ha avviato l’elaborazione.
4.8 La realizzazione dei canali di comunicazione
JXTA fornisce l’implementazione di canali di comunicazione, di input e di output,
unidirezionali e asincroni, nella classe PipeService. Considerata la scelta della topologia ad
anello, per la realizzazione di comunicazioni veloci e che non appesantiscano la rete, abbiamo
realizzato un servizio che costruisce un canale bidirezionale, unendo in una sola entità i
canali di input e di output.
Visto che per aprire una pipe, bisogna creare un pipe advertisement con lo stesso nome su
entrambi i peer che intendono comunicare, abbiamo creato un naming ad hoc, che garantisce
la dinamicità della creazione e della distruzione dei canali di comunicazione e che mantenga
decentralizzata l’assegnazione dei nomi, in linea con la filosofia peer-to-peer.
Ogni peer durante la configurazione dell’anello, crea due pipe advertisement per il peer di
destra e due per il peer di sinistra, rispettivamente per l’apertura dei canali di input e di
output, con i nomi che rispettano la sintassi riportata nella tabella 4.8.1.
Tipo di canale Nome PipeAdvertisement
Canale di input per ricevere dal peer PeerIdDestro.in
Canale di output per inviare al peer destro PeerIdDestro.out
Canale di input per ricevere dal peer MyPeerId.out
Canale di output per ricevere dal peer MyPeerId.in
Tabella 4.8.1: Naming degli advertisement per l’apertura delle pipe
L’idea è simile a quella di un cavo incrociato, utilizzato per la connessione di due
computer in rete. A destra i canali di input e di output si aprono sui PipeAdvertisement
corrispondenti (.in per il canale di input, .out per il canale di output), mentre a sinistra il
matching è invertito.
CAPITOLO 5
LA JAVA NATIVE INTERFACE
5.1 Cos'è JNI e quando serve
In generale, Java fornisce allo sviluppatore tutto quello di cui ha bisogno nei vari settori, e
in alcuni di questi (programmazione distribuita, applicazioni per Internet, etc.) permette di
ottenere ottimi risultati in tempo significativamente inferiore ad altri linguaggi. Tuttavia,
esistono casi in cui è indispensabile che un’applicazione Java interagisca a basso livello con il
sistema in cui viene eseguita, cioè con la sua parte nativa. Per questo l’ambiente di sviluppo di
Java (cioè il JDK in una sua qualunque versione) contiene la libreria JNI, o Java Native
Interface.
Si tratta di una collezione di librerie che permettono l’interazione e lo scambio di dati tra
una qualunque macchina virtuale Java (JVM) e il sistema in cui essa è installata.
Tipicamente, l’interazione con le parti a basso livello del sistema operativo è realizzata con
linguaggi che la consentano, e tra questi primeggiano sicuramente il C e il C++.
L’interazione a basso livello con il sistema, come regola, dovrebbe essere evitata,
soprattutto nello sviluppo di nuove applicazioni, che possono essere disegnate da zero con
certi criteri progettuali che tengano conto di tali problematiche; tuttavia, esistono casi dove
essa può rendersi estremamente utile.
Fra i più comuni, possiamo annoverare:
� interfacciamento di nuove applicazioni Java con software già esistente e non
convertibile per le più disparate ragioni, quali fattibilità, costi, vincoli di carattere
tecnico (es. sistemi di acquisizione dati sul campo);
� aggiunta ai propri programmi di funzionalità che non sono messe a disposizione da
Java;
� necessità di disporre di particolari performance in alcune funzionalità che Java,
malgrado i recenti ed importanti progressi, non è in grado di fornire.
La principale conseguenza dell’uso di JNI in un’applicazione è che questa, ovviamente,
non può più definirsi “100% Pure Java”, per cui viene naturale chiedersi se è consigliabile o
meno utilizzare la Java Native Interface.
Per dare una risposta a questa domanda consideriamo la seguente figura (5.1.1):
Figura 5.1.1: interazione di applicazioni Java con l'ambiente in cui operano
In figura, le frecce bidirezionali più scure rappresentano le normali interazioni di visibilità
che si realizzano fra i diversi strati di un sistema software; quelle più chiare, invece,
rappresentano le interazioni che si potrebbero realizzare tra un’applicazione scritta in Java nei
confronti delle parti di sistema più a basso livello o di software già esistente in casi come
quelli elencati poco sopra. Tali interazioni sono possibili proprio grazie all’utilizzo della
libreria JNI.
In certi casi di tali interazioni non si può fare a meno, e quindi si mina la portabilità delle
applicazioni. Ci sono due possibilità, a seconda dell’obiettivo che ci si prefigga:
� La nuova applicazione è stata scritta in Java per sfruttare le sue caratteristiche e il suo
particolare approccio ai concetti di OOP/OOD, ma deve funzionare solo su una certa
piattaforma;
� La nostra nuova applicazione è stata scritta in Java non solo per quanto detto sopra,
ma deve anche funzionare su piattaforme diverse e ovviamente fornire le stesse
funzionalità.
Nel primo caso non c’è alcun problema, mentre nel secondo, l’unico modo per assicurare
la portabilità di una applicazione è quello di sviluppare la parte di software (in particolare
modo una libreria condivisa) che interagisce a basso livello, per tutti i sistemi operativi su
cui intendiamo eseguire il nostro applicativo. In questo modo, anche se non potremo più
affermare di aver scritto un’applicazione solo in Java, di esso potremmo comunque sfruttare
tutti i vantaggi, e la portabilità è garantita.
Nel caso della nostra implementazione, abbiamo sfruttato la parte implementata della
versione parallela di CAGE, privata della parte delle primitive MPI per la comunicazione fra
processori; in questo modo si è ottenuto un’algoritmo sequenziale da utilizzare su ogni peer.
A questo punto per unire l’architettura di comunicazione, sviluppata interamente in Java
utilizzando la piattaforma JXTA, si è utilizzata la Java Native Interface: per mantenere la
portabilità si compilerà la libreria libgcp.so per ogni sistema operativo.
5.2 Struttura di JNI: vista d'insieme delle funzionalità
Come si è visto nel paragrafo precedente, JNI è un po’ il tramite tra i due “mondi” del
codice nativo e di una qualunque JVM (figura 5.1.2).; tale interazione è in qualche modo
speculare. Infatti è possibile chiamare e interagire con la parte a basso livello, implementando
in codice nativo metodi di classi Java (§ 5.1), e fare esattamente il cammino inverso, creando
una JVM e invocarne i metodi di classi di sistema o anche definite dall’utente.
Figura 5.2.1: Dettaglio del rapporto tra Java e il mondo nativo attraverso JNI
I metodi forniti da JNI consentono una grande flessibilità, in quanto è possibile:
� avere completo accesso alle classi Java, sia di sistema, sia definite dall’utente,
esaminandone gli attributi, chiamandone i metodi, etc;
� definire e dichiarare intere classi con tutte le loro caratteristiche, in maniera analoga a
quanto è possibile in Java puro con la Reflection API;
� creare nuovi oggetti di cui è stata già data completa definizione in Java attraverso uno
dei costruttori;
� creare array e stringhe di tipi primitivi o di qualsiasi altro oggetto;
� creare e sollevare eccezioni.
Java e il C/C++ sono linguaggi che, come si sa, hanno una matrice in comune. Siccome in
Java il trattamento dei tipi base (interi, booleani, numeri in virgola mobile, etc.) non è
altrettanto flessibile che in C/C++, esiste una corrispondenza standard di tali tipi nel passaggio
fra i due ambienti. Ad esempio, gli interi in Java sono soltanto con segno, e di questo bisogna
tenere conto (ad es. un numero intero a 32 bit in C/C++ senza segno maggiore di 231-1
(2147483647), se passato ad un programma Java, dev’essere contenuto in un intero lungo a 64
bit).
In tabella 5.2.1 e figura 5.2.2 sono riportate rispettivamente le mappature tra i tipi primitivi
e quelli complessi tra Java e C/C++.
Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A
Tabella 5.2.1: Tipi primitivi tra Java e C/C++
Figura 5.2.2: I tipi complessi di Java nel mondo del codice nativo
Nel caso dei tipi rappresentati in Figura 5 se una procedura Java ad esempio ha come
parametro una String, nella corrispondente implementazione nativa quel parametro sarà
dichiarato di tipo jstring e così via.
Un’altra cosa importante sono le cosiddette type signatures (“firme”), riportate in tabella
5.2.2, che indicano con un codice convenzionale il tipo di parametri e il tipo di ritorno di una
qualunque funzione nella sua controparte Java; tali firme servono alle istruzioni in C/C++ per
attribuire alla controparte nativa di un oggetto Java il suo tipo corretto, ed inoltre di esplorare
le classi definite in Java in modo analogo a quanto avviene grazie al package
java.lang.reflection.
Tabell
a 5.2.2:
“Firme”
dei tipi di
Java
Per
esempio,
supponia
mo di
avere due metodi di una classe così definiti:
1. void pippo(int x, String s, double y);
2. byte[] topolino(com.foo.bar.Minni x, boolean b)
Le due firme diventano rispettivamente:
1. (Iljava/lang/String;D)V;
2. (Lcom/foo/bar/Minni;Z)[B;
il tipo restituito da una funzione va in fondo a tutto, mentre i parametri formali vanno
inseriti tra parentesi e in sequenza senza alcun separatore (es. spazio) interposto.
5.3 L’uso della JNI nella nostra applicazione
7 Dopo la L va inserito l’intero classpath della classe (es. String – Ljava/lang/String)
Field Descriptor Java Language
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Lclass7 class
[type8 type[]
([type] || [Lclass] || [type) [type] || [Lclass] || [type method
([type] || [Lclass] || [type)V constructor
Nei paragrafi precedenti ci siamo limitati a descrivere gli aspetti principali e la simbologia
della JNI, che risultano fondamentali per capire la filosofia che sta dietro a tutto questo
discorso, nonché per rendere immediatamente comprensibile la nostra implementazione.
Il meccanismo per l’utilizzo della libreria nativa libgpc.so, che contiene i metodi che
implementano i servizi per gli algoritmi di programmazione genetica, è abbastanza semplice:
la classe Java MetodiNativi definisce i metodi detti nativi, che non vengono implementati.
Nella stessa classe viene introdotta una particolare istruzione in cui si specifica dove la JVM
può trovare a runtime l’implementazione di questi metodi che è realizzata a basso livello
tramite la libreria di sistema libgpc.so, detta shared library. Basta compilare questa libreria
per ogni sistema operativo, ospitante la nostra applicazione, per renderla indipendente da
esso, mantenendo così la portabilità.
5.3.1 I metodi nativi
I metodi della classe MetodiNativi che non sono implementati, ma sono già stati
implementati per la versione parallela di CAGE, devono avere nell’intestazione la parola
chiave native9. La struttura sintattica di un metodo nativo ricorda molto da vicino quella
seguita dai metodi definiti nelle interfacce: vengono dichiarate solo le intestazioni dei metodi,
mentre l’implementazione viene destinata alle classi che le implementano. Il compilatore
controlla se all’interno di queste sono implementati tutti i metodi dell’interfaccia, segnalando
un errore in caso contrario.
Allo stesso modo, in una classe vengono implementate soltanto le intestazioni dei metodi
nativi; la parola chiave native informa il compilatore Java che non deve cercare all’interno
8 Al posto di type viene inserita la firma del tipo (es. int – I ) 9 Nel listato 5.3.1 prendiamo in considerazione le righe di codice 5-10
della classe l’implementazione di tale metodo, ma che essa verrà effettuata dal compilatore di
un altro linguaggio di programmazione, che nel nostro caso è il C.
Listato 5.3.1 – L’implementazione della classe metodi nativi
5.3.2 Shared library
Solo al momento dell’esecuzione del programma verrà caricata l’implementazione del
metodo all’interno della classe. Per fare ciò, il sistema andrà a cercare tale implementazione
dentro una libreria condivisa esterna, che nel nostro caso è libgpc.so. L’estensione *.so indica
che è una libreria condivisa per il s.o. Linux, mentre per Windows abbiamo l’estensione *.dll.
Questa libreria è segnalata mediante la classe stessa, utilizzando il metodo
System.loadLibrary(String)10 inserito dentro un blocco di codice statico: durante il
caricamento della classe a runtime, la Java Virtual Machine utilizzerà le istruzioni contenute
nel blocco statico per effettuare l’inizializzazione e se non dovesse trovare alcun riferimento
alla shared library da utilizzare, genererà un’eccezione di tipo
java.lang.UnsatisfiedLinkError.
5.3.3 Compilazione
Appena la classe Java è pronta è possibile passare alla fase successiva, la compilazione del
codice. Questa fase è in realtà composta da due passi:
� Nel primo bisogna convertire il sorgente in bytecode mediante una compilazione
classica usando il comando javac;
� Nel secondo passo si procede alla generazione dell’header file(con estensione *.h),
necessario per la costruzione della shared library, mediante l’esecuzione del comando
javah:
javah –jni NomeClasse
10 Il nome da passare al metodo loadLibrary() deve essere il nome della libreria escluso il prefisso 'lib' e l'estensione '.so'
L’esecuzione del comando genererà il file MetodiNativi.h; è possibile assegnare
un nome diverso da quello del bytecode processato, specificandolo mediante l’opzione
“-o” nel comando di generazione; per lanciare javah senza parametri per avere
l’elenco delle opzioni possibili.
5.3.4 Header file
Nel codice dell’header file così ottenuto troviamo un’avvertenza sotto forma di commento,
con la quale veniamo avvertiti di non modificare in alcun modo il sorgente.
Questo file contiene la traduzione in linguaggio C dell’intestazione dei metodi nativi Java
(listato 5.3.2): non avrebbe senso, quindi, alterarlo, perché altrimenti si perderebbe il reale
significato delle intestazioni.
Listato 5.3.2: Il file header contenente le intestazioni dei metodi nativi
La conversione dell’intestazione dei metodi nativi seguono un preciso standard: la
funzione nel linguaggio nativo avrà un nome composto dal prefisso Java, seguito dal nome
del package contenente la classe Java, dal nome della classe Java, dal nome della classe ed
infine dal nome del metodo nativo, ognuno intervallato dal carattere underscore ‘_’.
Prendiamo ad esempio il metodo Java startup(String[ ]) (listato 5.3.1): seguendo la
convenzione e tenendo conto che la classe non è stata definita dentro un particolare package,
il nome della corrispondente funzione sarà quindi Java_MetodiNativi_startup (listato 5.3.2).
5.3.5 Parametri aggiuntivi
Osservando il codice precedente si nota che tra i parametri di entrambe le funzioni, oltre a
quelli dei metodi nativi opportunamente convertiti, ne troviamo altri due di tipo particolare. Il
primo parametro è un puntatore i tipo JNIEnv (listato 5.3.2): tramite esso è possibile accedere
ad una serie di informazioni, come ad esempio i parametri e gli oggetti contenuti nell’istanza
della classe Java. Il secondo parametro è un jobject: esso può assumere diversi significati. Se
è un metodo di istanze esso assume lo stesso significato della variabile “this” in Java, cioè
corrisponde al riferimento all’istanza dell’oggetto. Se il metodo è invece di classe, il jobject
corrisponderà al riferimento al metodo Java della classe. Ad esempio, tramite il puntatore
JNIEnv* env e il parametro jobject obj è possibile ottenere la classe Java dell’istanza su cui è
stato chiamato il metodo nativo:
jclass cls = (*env)field = (*env)�GetObjectClass(env,obj)
5.3.6 L’implementazione
L’header file è utilizzato per costruire il sorgente C (o C++), da cui ricavare tramite
compilazione la shared library finale, o è importato allorquando il codice C è già esistente,
come nel caso della nostra applicazione.
I metodi utilizzati nelle modifiche ai metodi dell’implementazione parallela di CAGE,
opportunamente sequenzializzata, sono i seguenti:
� (*env)����NewByteArray(JNIEnv,jint): questo metodo, come ogni altro metodo della
JNI, esistente per tutti i tipi predefiniti di java, compreso Object, inizializza un’array
del tipo corrispondente, in questo caso jbyte11: al metodo vanno passati l’oggetto env e
la lunghezza dell’array.
� (*env)����SetByteArrayRegion(JNIEnv, jbyte*, jint, jint, jbyte*): questo metodo
permette di riempire l’array passato come secondo parametro, con gli elementi
dell’array passato come ultimo parametro. Al metodo vanno inoltre passati, l’oggetto
env e gli indici di inizio e fine array;
� (*env)����GetArrayLenght(JNIEnv,jbyte*): questo metodo restituisce la dimensione
l’array passato come argomento;
� (*env)����GetObjectArrayElement(JNIEnv,jbyte*,jint): questo metodo estrae
l’oggetto corrente, identificato dall’indice passato come parametro, dall’array di
oggetti generici;
11 I tipi, corrispondenti ai tipi java, nelle implementazioni C/C++ conservano lo stesso nome preceduto dal carattere j
� (*env)����GetByteArrayElements(JNIEnv,jbyte*,jint): questo metodo estrae gli
elementi dell’array passato come parametro, dall’indice jint.
� (*env)�GetStringUTFChars(JNIEnv, jobject, jint): con questo metodo si converte
l’oggetto passato come parametro, in un array di caratteri;
� (*env)�ReleaseStringUTFChars(JNIEnv, jobject, jbyte*): Rilascia la memoria
per l’oggetto passato come argomento.
Bisogna sempre ricordarsi di includere in ogni file che implementa i metodi nativi, sia
l’header file generato che il file jni.h contenente l’API JNI di Java. Questo file, insieme ad
altri ad esso collegati, si trovano nella directory include presente nel JDK distribuito dalla
SUN: affinché il compilatore trovi questo file bisogna specificare il percorso da seguire per
trovare questa directory.
5.3.7 Compilare i sorgenti
Implementate tutte le funzioni dichiarate nell’header file, non rimane altro che compilare i
sorgenti per ottenere la shared library.
Su sistemi Linux la libreria si costruisce nel seguente modo:
gcc <nomefile> -o libgpc.so -shared -I$DIR_JDK+"include" -I$DIR_JDK+"include/linux"
mentre su Windows, usando Microsoft Visual C++, la sintassi è la seguente:
cl –Ic:\java\include –Ic:\java\include\win32 –LD <nomefile> -Felibgpc.dll
10.4
10.5
10.6
10.7 Conclusioni e sviluppi futuri
Abbiamo visto come il peer-to-peer e in particolar modo JXTA, la piattaforma che Sun
Microsystem ha proposto nel tentativo di creare uno standard per lo sviluppo di questo tipo di
applicazioni distribuite, si adattano perfettamente all’implementazione di applicazioni di
programmazione genetica. L’obiettivo di sviluppare un’implementazione distribuita di CAGE
è stato raggiunto, costruendo attraverso l’uso di JXTA una topologia ad anello per la
comunicazione fra i peer, utilizzando primitive asincrone.
La scelta è stata orientata su JXTA perché attraverso i suoi servizi, si è in grado di costruire
applicazioni, robuste, scalabili e altamente parallelizzabili, proprietà fondamentali per la
programmazione genetica.
Il nostro prototipo si è ispirato, per l’evoluzione della popolazione, ai modelli a isole e
diffusivo, nel tentativo di dirigere l’attenzione verso lo studio di un modello ibrido, che
promuova l’autonomia di evoluzione delle isole e che nello stesso tempo permetta di
scambiare poche, ma necessarie, informazioni per evitare una prematura convergenza della
fitness.
Gli sviluppi di questo applicativo potrebbero essere orientati verso:
� La creazione di un gruppo per ogni tipo di elaborazione, in modo da permettere il
contemporaneo uso del peer, come risorsa disponibile ad eseguire problemi lanciati da
altri, che come mezzo per risolverne uno proprio.
� La visualizzazione dei processi attivi e delle risorse allocate.
� Il bilanciamento del carico computazione, in quanto in una rete JXTA si possono
trovare dispositivi eterogenei (workstation, cluster, pc, pda, cellulari).
� Diritti di computazione: accettare o meno la cessione della propria risorsa.
� Sicurezza: gruppi con username e password (gestione di una eventuale registrazione).
� Creazione di advertisement di servizio XML personalizzati (dovrebbe essere
disponibile la funzionalità dalla versione 2.1).
� Inserimento dei grafici per visualizzare l’andamento della fitness
� I file .cfg e default.in verranno implementati come file XML.
� Rendere disponibili gli editor del sistema operativo, su cui viene installata
l’applicazione, costruendo una funzione simile ad apri con… di Windows
� Gestione di eventuali crash dei nodi, e di nuovi ingressi per incrementare la potenza di
calcolo.
� Migliorare la politica di ricerca dei vicini, spostandola a più basso livello.
Abbiamo elencato solo una parte dei possibili sviluppi dell’applicazione; inoltre la
piattaforma ha dimostrato grandi potenzialità ed è adatta a diversi scopi, che vanno oltre il
semplice scambio file.
10.8
10.9
10.10
10.11
10.12
10.13
10.14 Appendice A
Tool di configurazione JXTA
11
12
13 INTRODUZIONE
La prima volta che un’applicazione è avviata sul sistema, un tool di auto-configurazione
(Jxta Configurator) è visualizzato per configurare la piattaforma JXTA per la rete.
Questo tool è utilizzato per specificare informazioni sulla configurazione del peer JXTA in
esame, configurazione TCP/IP e HTTP, rendezvous e relay peer, informazioni di sicurezza.
13.1 A.1 Processo di configurazione
Due file di configurazione sono usati durante il processo di configurazione JXTA,
PlatformConfig e reconf:
� PlatformConfig: informazioni specifiche sul peer. Contiene tutte le informazioni di
configurazione necessarie per configurare la piattaforma JXTA ed avviare il
NetPeerGroup di default.
� reconf: se questo file esiste, sarà avviata la schermata di configurazione, inoltre dovrà
esistere contemporaneamente il file PlatformConfig.
All’avvio, la piattaforma JXTA cerca il file PlatformConfig nella directory corrente
� se esso esiste i suoi parametri verranno utilizzati per configurare la piattaforma.
� se non esiste viene visualizzato il tool di configurazione JXTA e l’utente può inserire
le informazioni di configurazione per il peer JXTA.
Quando l’utente e chiude il tool di configurazione, esso crea nella directory corrente le
seguenti directory e file di configurazione.
� PlatformConfig;
� cm (directory): contiene la cache locale degli advertisement JXTA;
� pse (directory): memorizza username e password.
Se la directory pse non esiste, quando si avvia l’applicazione JXTA, viene visualizzato il
configuratore JXTA. Le informazioni di configurazione dal file PlatformConfig saranno
reinseriti sui vari pannelli. L’utente può selezionare il pannello Security per inserire una
nuova username e password.
Un esempio del file PlatformConfig è visualizzato nella seguente figura: questo file è un
documento XML contenente informazioni riguardo il PeerID, il nome del peer e qualche
informazione sulla configurazione di TCP/IP e HTTP per il peer in questione.
13.1.1 Figura A.1.1: Esempio file PlatformConfig
13.2 A.2 Descrizione del tool
Quando il configuratore di JXTA viene visualizzato per la prima volta, invita l’utente ad
inserire i dati nel pannello Basics. Pannelli addizionali possono essere visualizzati,
selezionando advanced, Rendezvous/Router e Security nella parte superiore della finestra. In
molti casi solo i settaggi nel pannello Basics e Security necessitano di modifiche. Bisogna
specificare almeno il Peer Name (nel primo), username e password (nel secondo).
13.2.1.1.1 A.3 Basic Settings
Il pannello Basic Settings è visualizzato come primo pannello dal configuratore JXTA. Il
campo Peer Name può accogliere qualsiasi stringa. Se il peer JXTA è dietro un firewall,
bisogna selezionare la casella “Use a proxy server” e inserire il nome host e il numero di
porta per il proprio proxy server.
Se non si inseriscono nessuna delle impostazioni avanzate o di quelle rendezvous/relay,
può essere cliccare il tasto OK in basso alla finestra per continuare la configurazione.
Se è necessario specificare impostazioni di configurazione addizionali si deve cliccare
advanced o Rendezvous/Router.
13.2.1.2 Figura A.3.1: Configuratore JXTA: pannello Basic
13.2.1.2.1
13.2.1.2.2 A.4 Advanced settings
Questo pannello permette di specificare il Trace Level, le impostazioni TCP/IP e le
impostazioni HTTP (vedi figura A.4.1).
Di default Trace Level è settato a “error” e sia TCP/IP che HTTP sono abilitati.
Se il peer è dietro un firewall o NAT, esso deve usare HTTP; inoltre può essere anche
usato il TCP/IP.
Viceversa HTTP non è obbligatorio.
13.2.1.3 Figura A.4.1: Configuratore JXTA: Advanced settings
13.2.1.3.1 A.5 Trace level
Il trace level può essere settato in base alle opzioni presenti nel menu:
� error: visualizza solo gli errori JXTA;
� warn: visualizza gli errori JXTA e gli avvisi;
� info: visualizza messaggi di informazione addizionali;
� debug: visualizza lunghi messaggi di aiuto durante il debugging delle applicazioni
JXTA;
� user default: visualizza messaggi definiti dall’utente (da usare con log4j e event
tracing).
A.6 TCP settings
Il TCP è abilitato di default sulla porta 9701.
Quando TCP è abilitato, ogni istanza della piattaforma JXTA è limitata ad uno specifico
numero di porte TCP di un dato peer.
� Network interface: un peer può avere interfacce di rete multiple. Si può selezionare
quale interfaccia di rete deve essere usata per JXTA, scegliendo l’interfaccia
desiderata dal menu;
� Port number: si può cambiare la porta usata dal TCP/IP inserendo un valore
differente nel campo destro dell’indirizzo IP, di default 9701.
Nota: istanze multiple della piattaforma JXTA possono essere avviate da un singolo peer
cambiando il numero del TCP e legando ogni istanza ad una differerente porta.
Affinché le impostazioni di configurazione siano memorizzate nella directory locale,
ogni istanza deve essere avviata in una directory specifica.
� Public NAT address: se il peer è localizzato dietro un NAT si deve specificare il
public nat address per questo nodo.
� Manual configuration: se si seleziona l’opzione manual, sarà visualizzato un box
addizionale, always manual. Se si seleziona questo box, la schermata di
configurazione sarà visualizzata e l’utente dovrà selezionare manualmente
l’interfaccia di rete per il TCP, ogni volta che il peer sarà avviato. Questa opzione è
necessaria per nodi che usano DHCP, quando l’indirizzo IP può cambiarte. Se il box
always manual non è selezionato, il valore selezionato sarà salvato nel file di
configurazione e l’utente non sarà invitato a selezionare l’interfaccia di rete la volta
successiva.
13.2.1.3.2 A.7 HTTP settings
Di default, HTTP è abilitato ed è configurato sulla porta 9700. HTTP deve essere abilitato
se il peer si trova dietro un firewall o un NAT. Se si vuole usare una porta differente, bisogna
selezionare dal menu l’interfaccia desiderata. Si può cambiare la porta usata da HTTP
fornendo un valore diverso nel campo a destra dell’indirizzo IP. Se un peer ha più di una
interfaccia di rete si può cliccare sul box manual per esplicitare le specifiche che l’interfaccia
di rete dovrebbe usare per l’HTTP. Se si seleziona l’opzione manual, sarà visualizzato un box
addizionale, always manual. Se si seleziona questo box, la schermata di configurazione sarà
visualizzata e l’utente dovrà selezionare manualmente l’interfaccia di rete per il HTTP, ogni
volta che il peer sarà avviato.
13.2.1.3.3 A.8 Rendezvous and Relay Settings
Il pannello Rendezvous/Router setting permette di specificare le impostazioni di
Rendezvous e HTTP (vedi figura A.8.1).
Di default il peer JXTA non agisce come un nodo rendezvous e nemmeno come relay, ma
è configurare un relay peer.
Figura A.8: Configuratore JXTA: impostazioni Rendezvous/Router
A.9 Downloading rendezvous e relay peer
Si può scaricare una lista di relay e rendezvous peers attraverso la selezione del tasto
Download relay and rendezvous list posto in basso alla finestra.
Verrà visualizzata la nuova finestra (figura A.9.1).
13.2.1.4 Figura A.9.1: Configuratore JXTA: finestra “Load list from URL”
Cliccando sul tasto Load verranno scaricati uno più rendezvous e relay peer.
Una volta terminato si clicca Dismiss per chiudere questa finestra e ritornare al pannello
Rendezvous/Router.
13.2.1.5 Figura A.9.2 – Configuratore JXTA: aggiornamento rendezvous router settings
A.10 Rendezvous Settings
È possibile aggiugere un peer rendezvous fornendo l’indirizzo IP e il numero di porta, e
selezionando il tasto “+”. Per rimuovere un rendezvous peer, selezionare il suo indirizzo e
premere il tasto “-“. Se si desidera che il peer in questione agisca come rendezvous,
selezionare il box Act as a rendezvous.
A.11 HTTP Relay Settings
Il box Use a relay è selezionato di default. Si può addizionare un relay peer fornendo
l’indirizzo IP e il numero di porta, e ciccando il tasto”+”. Per rimuovere un relay peer bisogna
selezionare l’indirizzo e premere il tasto “-”. Se si desidera che il peer agisca come relay
selezionare il box Act as a relay.
Sia TCP che HTTP devono essere abilitati prima di agire come un relay.
Inoltre, deve essere specificato l’indirizzo pubblico (Static Nat Address).
A.12 HTTP Security Settings
La piattaforma JXTA richiede che siano create username e password la prima volta quando
viene configurata. Nei successivi avvii della piattaforma verranno richieste username e
password. Il pannello Security Settings invita l’utilizzatore a fornire username e password.
La password deve essere lunga almeno 8 caratteri.
13.2.1.6 Figura A.12.1: Configuratore JXTA: Security Settings
Le applicazioni possono saltare la finestra sulla sicurezza settando le seguenti System
Properties:
� net.jxta.tls.principal;
� net.jxta.tls.password;
Indice
Introduzione ......................................................................................................... I
Capitoto 1............................................................................................................ 1
1.1 L’Intelligenza Artificiale ......................................................................... 1
1.2 Gli algoritmi evolutivi.............................................................................. 2
1.3 Gli algoritmi genetici ............................................................................... 4
1.3.1 La popolazione.................................................................................... 8
1.3.2. Gli operatori genetici ......................................................................... 9
1.3.3 Alcuni cenni teorici ........................................................................... 13
1.4 Le strategie e la programmazione evolutiva ......................................... 15
1.5 La programmazione genetica ................................................................ 17
1.5.1 I parametri principali........................................................................ 19
1.5.2 Mutazione, crossover ed altri operatori............................................. 22
1.5.3 L’aumento della complessità nella programmazione genetica ........... 27
1.5.4 Differenze e analogie fra GP e GA .................................................... 28
1.6 I modelli per la programmazione genetica ........................................... 30
1.6.1 L'automa cellulare ............................................................................ 33
1.7 CAGE ( CellulAr GEnetic programming tool ) ................................... 36
1.7.1 Architettura software di CAGE ......................................................... 37
1.7.3 Dettagli dell’implementazione........................................................... 42
1.7.4 Esempi di programmazione genetica applicati al modello cellulare .. 46
1.8 Un’ implementazione distribuita della programmazione genetica ...... 50
Capitolo 2.......................................................................................................... 53
2.1 Cos'è il peer-to-peer? ............................................................................. 53
2.2 L’essenza del peer to peer...................................................................... 55
2.1.1 Sistema basato sull’interazione fra i peer ......................................... 55
2.3 Nessun appoggio su server o risorse centralizzate ................................ 59
2.4 Resistenza ai cambiamenti profondi nella composizione di una rete... 60
2.5 Una rete con topologia non deterministica ........................................... 60
2.6 Ottima scalabilità................................................................................... 61
2.7 Il peer to peer e la programmazione genetica: connubio perfetto ....... 61
2.8 Java per il supporto peer to peer........................................................... 63
Capitolo 3.......................................................................................................... 65
3.1 Architettura e servizi JXTA .................................................................. 65
3.2 Perché utilizzare JXTA?........................................................................ 68
3.3 Il progetto JXTA.................................................................................... 70
3.4 Gli elementi di JXTA ............................................................................. 71
3.4.1 Peer .................................................................................................. 71
3.4.2 PeerGroup ........................................................................................ 72
3.4.3 Network Services.............................................................................. 75
3.4.4 Pipe JXTA ......................................................................................... 75
3.4.5 Message JXTA .................................................................................. 77
3.4.6 Advertisement JXTA .......................................................................... 78
3.5 JXTA Credentials .................................................................................. 87
3.6 ID JXTA ................................................................................................. 88
3.7 L’Architettura di una rete JXTA .......................................................... 89
3.8 Protocolli di JXTA ................................................................................. 91
3.9 Peer Discovery Protocol......................................................................... 93
3.10 Peer Information Protocol................................................................... 94
3.11 Peer Revolver Protocol ........................................................................ 95
3.12 Pipe Binding Protocol .......................................................................... 96
3.13 Endpoint Routing Protocol.................................................................. 97
3.14 Rendezvous Protocol............................................................................ 98
3.15 Un semplice esempio che avvia una piattaforma JXTA ..................... 99
3.15.1 Requisiti del sistema........................................................................ 99
3.15.2 Esempio HelloWorld ......................................................................100
3.15.3 La classe SimpleJxtaApp ................................................................100
3.15.4 startJxta ( ).....................................................................................101
3.15.5 main( ) ...........................................................................................101
3.15.6 Esecuzione di “Hello World” .........................................................102
3.15.7 Codice sorgente: SimpleJxtaApp ....................................................105
3.16 Programmare con JXTA ....................................................................105
3.17 Scoperta di un peer ( Peer Discovery ) ...............................................106
3.17.1 Il servizio di scoperta ( Discovery Service).....................................106
3.17.2 Discovery Demo.............................................................................108
3.18 La scoperta di un peer group (Peer Group Discovery) .....................116
3.18.1 Il metodo run( ) ..............................................................................117
3.18.2 Il metodo discoveryEvent( ) ............................................................117
3.19 Creare Peer Groups e pubblicare advertisements .............................118
3.19.1 Il metodo main( )............................................................................119
3.19.2 Il metodo startJxta( ) ......................................................................120
3.19.3 createGroup( ) ...............................................................................120
3.20 Unire un peer group............................................................................122
3.20.1 Servizio di appartenenza ad un gruppo (Membership Service)........123
3.20.2 main( ) ...........................................................................................125
3.20.3 startJxta( )......................................................................................125
3.20.4 createGroup( ) ...............................................................................125
3.20.5 joinGroup( ) ...................................................................................126
3.21 Scambio di messaggi fra due peer ......................................................128
3.21.1 JXTA Pipe Service..........................................................................130
3.21.2 Pipe listener ...................................................................................132
3.21.3 PipeExample ..................................................................................137
3.21.4 Pipe Advertisement: il file examplepipe.adv ...................................142
Capitolo 4.........................................................................................................144
Introduzione................................................................................................144
4.1 Le classi della nostra applicazione .......................................................146
4.2 La classe CanaleDiInput.......................................................................146
4.3 La classe CanaleDiOutput ....................................................................147
4.4 La classe Esploratore ............................................................................149
4.5 La classe GruppoProgrammazioneGenetica .......................................150
4.6 La classe Peer ........................................................................................151
4.7 La classe Utility .....................................................................................154
4.8 La realizzazione dei canali di comunicazione........................................155
Capitolo 5.........................................................................................................157
5.1 Cos'è JNI e quando serve......................................................................157
5.2 Struttura di JNI: vista d'insieme delle funzionalità ............................160
5.3 L’uso della JNI nella nostra applicazione ............................................165
5.3.1 I metodi nativi ..................................................................................166
5.3.2 Shared library ..................................................................................168
5.3.3 Compilazione ...................................................................................168
5.3.4 Header file .......................................................................................169
5.3.5 Parametri aggiuntivi ........................................................................171
5.3.6 L’implementazione ...........................................................................171
5.3.7 Compilare i sorgenti ........................................................................173
Conclusioni e sviluppi futuri.............................................................................174
Appendice A ....................................................................................................177
Introduzione................................................................................................177
A.1 Processo di configurazione...................................................................178
A.2 Descrizione del tool ..............................................................................180
A.3 Basic Settings........................................................................................180
A.4 Advanced settings.................................................................................181
A.5 Trace level ............................................................................................182
A.6 TCP settings .........................................................................................183
A.7 HTTP settings ......................................................................................184
A.8 Rendezvous and Relay Settings ...........................................................185
A.9 Downloading rendezvous e relay peer.................................................185
A.10 Rendezvous Settings...........................................................................187
A.11 HTTP Relay Settings..........................................................................187
A.12 HTTP Security Settings …………………………………………………...187
Bibliografia…………………………………………………………...………………..189
Bibliografia
A. David e J. R. Koza, “Exploiting the fruits of parallelism”: An implementation of
parallel genetic programming that achieves super-linear performance”, Information science
Jurnal, Elsevier, 1997.
P. J. Angeline, “Genetic Programming and Emergent Intelligent”, Advances in Genetic
Programming, K. Kinnear, Cambridge MA: MIT Press, pp. 75-96.
E. Cantù-Paz, “Designing Efficient Master-Slave Parallel Genetic Algorithms”, Technical
Report IlliGAI 9704, Dept. Comp. Science, University of Illinois at Urbana-Champaign,
1997.
E. Cantù-Paz, “Migration Policies and Takeover Times in Parallel Genetic Algorithms” ,
GECC-99: Proceedings of the Genetic and Evolutionary Competition Conference, San
Francisco, July 1999.
E. Cantù-Paz, “Migration policies, selection pressure, and parallel evolutionary
algorithms” , Technical Report IlliGAI 99015, Dept. Comp. Sciencs, University of Illinois at
Urbana-Champnign, 1999.
A. G. Deakin e D. F. Yates, “Genetic Programming tool Avaible on the Web: A First
Encounter” , Genetic Programming 1996, Proceedings of the First Annual Conference, MIT
Press, Stanford University, July 1996.
Enrique Alba, Marco Tomassini, “Parallelism and Evolutionary Algorithms”, IEEE
Transaction on evolutionary computation, vol. 6, n°5, Ottobre 2002
G. Folino, C. Pizzuti, G. Spezzano, “A tool for Parallel Genetic programming
Applications” , Proc. Of the European Genetic Programming Conference EuroGP01, Como,
15-16 April, to appears.
J. Holland, “Adaptation in Natural and Artificial Systems”, University of Michigan Press,
1975.
J. R. Koza, “Genetic Programming: On the Programming of Computers by means of
Natural Selection” , MIT Press, Cambridge, 1992.
C. E. Leiserson, “Fat-Trees: Universal Networks for Hardware-Efficient Supercomputing”
, IEEE Transaction on Computers, Vol. C-34, N. 10, October 1985.
P. Nordin e W. Banzhaf, “Complexity Compression and Evolution” , Proceedings of the
Fourth International Conference on Genetic Algorithms, Morgan Kaufmann Publishars Inc.,
1995.
P. Nordin, F. Francone e W. Banzhaf, “Explictly Defined Intros and Destructive Crossover
in Genetic Programming” Advances in Genetic Programming: Volume 2, P. Angeline and K.
Kinnear, MIT Press, Cambridge, 1996, pp. 111-134.
M. Oussaiden, B. Chopard, O. Picted and M. Tommasini, “Parallel Genetic Programming
and its Application to Trading Model Induction”, Parallel Computing....
C. C. Pettey, “Diffusion (cellular) models”, Handbook of Evolutionary Computation,
Institute of Phisics Publishing and Oxford University Press,1997, C 6.4.
E. Rich, “Artificial Intelligence”, McGraw-Hill, NewYork, 1983.
M. Snipper, “The Emergence of cellular Computing”, Computer, IEEE Computer Society,
July 1999.
Stephen Wolfram, “Cellular Automata and Complexity”, Addison Wesley Publishing
Company, 1994.
Antonio Pelleriti, “JXTA: il vero peer-to-peer”, Io Programmo Dicembre 2002
Antonino Panella, “JNI: dare accesso alle funzionalità del sistema operativo”, Io
Programmo Dicembre 2002
M.G. Arenas, Pierre Collèt, A.E. Eiben, Mark Jelasity, J.J. Merelo, Ben Paechter, Mike
Preu�, Marc Schoenauer, “A framework for distributed evolutionary algorithms”
Mark Jelasity, Mike Preu�, Ben Paetcher, “A scalable and robust framework for
distributed applications”
Daniel Brookshier, Darren Govoni, Navaneeth Krishnan, “JXTA: Java P2P
Programming”, SAMS Publishing, 2002