Premessa


Questo documento vuole essere una guida pratica alla costruzione di agenti mobili per SOMA (Secure and Open Mobile Agent programming framework), il sistema ad agenti mobili messo a disposizione dal Dipartimento di Elettronica, Informatica e Sistemistica (DEIS) della Università di Bologna.

Questa guida, che ripercorre il sentiero da me personalmente seguito nell'imparare a scrivere agenti mobili per SOMA, si limita a trattare alcuni temi basilari riguardanti il sistema e gli agenti costruiti per vivere in esso: la creazione di agenti; la migrazione di agenti da un place all'altro; l'interazione degli agenti con il sistema e l'ambiente sottostante da un lato, e con l'utente dall'altro; l'uso dei meccanismi di comunicazione e coordinamento messi a disposizione dal sistema; lo sfruttamento della libreria grafica per dotare gli agenti di una GUI. Esula dagli scopi di questa guida spiegare le tematiche più avanzate di SOMA, quali sicurezza, Quality of Service (QoS), internazionalizzazione delle interfacce, e tutti quei temi che tendono verso il livello applicativo. Lo scopo di questa guida consiste nello spiegare come sfruttare i meccanismi primitivi messi a disposizione da SOMA, ed essa è caratterizzata dall'avvalersi soprattutto del codice di alcuni agenti di esempio per portare a termine il suo compito.

Ad ottobre 2002, infatti, le principali fonti di informazione su SOMA si dividono in due categorie: da un lato, troviamo le tesi di laurea, che spiegano soprattutto i concetti e le astrazioni che hanno trovato concretizzazione ed implementazione in SOMA, senza però soffermarsi troppo su come effettivamente scrivere codice per costruire agenti mobili che vivano nel sistema; dall'altro, troviamo il codice dei progetti per l'esame di Reti di Calcolatori, il codice degli agenti allegati alla distribuzione di SOMA, e il codice di SOMA stesso, fonti ultime per comprendere la realizzazione concreta di entità mobili per il sistema, ma spesso troppo poco commentate, slegate dai concetti, e nelle quali il codice relativo alle applicazioni ad agenti si confonde con il codice relativo alle funzionalità primitive messe a disposizione da SOMA. Questa guida vuole essere un primo timido passo verso l'incontro e la fusione delle due categorie appena illustrate: la speranza è quella di semplificare la vita agli studenti che vogliono utilizzare SOMA nei loro progetti, cercando di rendere almeno inizialmente più dolce la finora ripida curva di apprendimento del sistema, così che più persone possano utilizzare, supportare e contribuire al progetto di ricerca portato avanti dalla nostra Università.

Questa guida presuppone la lettura di alcuni documenti, riportati in bibliografia, atti a spiegare le logiche di progettazione ed i meccanismi messi a disposizione da SOMA. Si suppone inoltre che il lettore sia a conoscenza della teoria riguardante il paradigma ad agenti mobili.

 


Il primo agente

Il codice seguente illustra il primo agente che andremo a costruire. Esso non fa nulla di particolare (non si muove, non comunica, non crea nuovi agenti e via dicendo) ma si limita a mostrare nella finestra del place di creazione una frase diversa dal solito saluto al mondo (ciatata da Puck in A Midsummer Night's Dream, di William Shakespeare, Act 3, Scene 2).
import SOMA.agent.*;

/**
 * Just a try to build an agent and let it say a shakespearian quotation.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Sunday 10th February, 2002
 */
public class TryAgent extends Agent {
	
	public void run() {
		
		agentSystem.getOut().println("Lord, what fools these mortals be\n");
	
	}
	
} // end TryAgent
Tutti gli agenti di SOMA devono estendere la classe astratta SOMA.agent.Agent. L'unico metodo astratto che questa classe presenta è il metodo run(), che andrà quindi implementato in ogni agente, e che sarà il metodo a partire dal quale l'agente verrà messo in esecuzione. La classe Agent contiene infatti un campo stringa che identifica il nome del metodo da cui far partire l'agente, ed esso viene settato automaticamente al valore "run" all'atto di creazione di un agente.
/**
 * @serial
 * Metodo che verra' eseguito alla prossima attivazione dell'agente.
 */
public String start = "run";
Che cosa fa il nostro agente? Usa un riferimento ad un oggetto di tipo SOMA.agent.AgentSystem mantenuto internamente alla classe Agent per recuperare lo stream di output dell'environment corrente e stampare su di esso la citazione shakespeariana che abbiamo prescelto.

La classe AgentSystem funge da interfaccia tra l'agente ed il sistema sottostante: al momento della attivazione di un agente in un place, il riferimento ad un AgentSystem è l'unica possibilità che ha l'agente di interagire con l'infrastruttura ad agenti mobili. L'interazione tra agente e sistema tramite un oggetto di tipo AgentSystem avviene tipicamente attraverso i seguenti metodi:
public PlaceID getPlaceID()
public Environment getEnvironment();

public InputStream getIn() {
	return getEnvironment().in;
}

public PrintStream getOut() {
	return getEnvironment().out;
}

public PrintStream getErr() {
	return getEnvironment().err;
}


/**
 * Restituisce l'elenco degli identificatori dei place di questo dominio.
 */
public PlaceID[] getPlaces();

/**
 * Restituisce l'elenco degli identificatori dei domini, o
 * un array vuoto se non e' presente un Domain Name Service, perche'
 * non siamo in un default place.
 */
public PlaceID[] getDomains();

/**
 * Restituisce il numero di worker e quindi di agenti del place.
 */
public int agentsNumber()
Tra essi, vediamo anche il metodo getOut() che abbiamo appena utilizzato. Esso ci restituisce un riferimento ad un oggetto di tipo java.io.PrintStream che useremo tramite il metodo println() per mostrare a video la nostra citazione. Dopo aver compilato l'agente, averlo copiato nella directory agents relativa alla installazione di Soma 4.0, ed aver aperto, tramite GUI, una finestra di place relativa ad un dominio di prova precedentemente costruito, possiamo lanciare il nostro agente TryAgent e vederne a video gli effetti sulla finestra di place.

Un agente di prova al lavoro
Figura 1. Un agente di prova al lavoro.

 

 

Interagire con l'Environment: i Place e i Domini


Oltre a fornire uno stream di output come abbiamo appena visto, la classe Environment mette a disposizione diversi metodi per interagire con le facilities disponibili su ogni place, come ad esempio il Domain Name System ed il Place Name System.

Il DNS è una tabella che contiene il sottoalbero gerarchico dei domini relativo al Default Place su cui ci troviamo. Dato che i domini di SOMA sono inseriti in un albero gerarchico per permettere una elevata scalabilità del sistema, al momento della creazione di un Default Place occorre specificare qual è il suo Default Place genitore, a meno che non si stia creando quello che diventerà la radice dell'albero. Ogni Default Place ha quindi una tabella di DNS che contiene necessariamente il nome del suo genitore e dei suoi figli. Può inoltre contenere altri nomi che possono essere utili, ma non è richiesto che in ogni tabella siano memorizzati i nomi di tutti i Default Place.

Il PNS è una tabella in cui sono contenuti tutti i nomi dei Place appartenenti ad un certo dominio, in quanto, per come SOMA è strutturato, tutti i Place che appartengono ad uno stesso dominio si devono conoscere l'un l'altro. Il proprietario di questa tabella è però il Default Place, che provvede a copiarla ai nuovi Place al momento della loro creazione.

Vediamo quindi un esempio di un agente che interagisce con le tabelle di DNS e PNS gestite dall'environment.
import SOMA.agent.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * A try to interact with the Environment and the Domain Name Service.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Sunday 10th February, 2002
 */
public class EnvAgent extends Agent {
	
	private Vector placesToVisit = new Vector();
	
	public void run() {
		
		PlaceID home = agentSystem.getPlaceID();
		
		if (home.isDomain()) {
			// Get children domains
			DomainNameService dns = agentSystem.getEnvironment().domainNameService;
			Vector children = dns.getChildrenDNS(); // a Vector of PlaceIDs
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				placesToVisit.add(place);
			}
			// Get places in this domain
			Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
			places.remove(home);
			for (int index = places.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) places.get(index);
				placesToVisit.add(place);
			}
		} else
			placesToVisit.add(home);
		
		// Print results
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) placesToVisit.get(index);
			agentSystem.getOut().println(place);
		}	
		
	}
	
} // end EnvAgent
Salta subito agli occhi che la classe SOMA.naming.domain.DomainNameService, rappresentante il gestore del Domain Name System, è un membro pubblico della classe SOMA.Environment, e che tra i metodi di accesso alle informazioni in esso contenute spicca getChildrenDNS(). Esso restituisce un java.util.Vector contenente i PlaceID di tutti i figli del Default Place corrente, e nell'esempio viene difatti richiamato solo se il Place corrente è un Default Place e qunidi effettivamente possiede una tabella di DNS (il controllo è effettuato dal predicato isDomain() della classe SOMA.naming.PlaceID).

Il contenuto della tabella del Place Name System è invece restituito dal metodo getPlaces() della classe AgentSystem, che in realtà non è altro che un wrapper per il metodo getPlacesArray() della classe SOMA.naming.place.PlaceNameService. Parallelamente, anche nella classe DomainNameService esiste un metodo getDomainsArray(), il quale restituisce tutti gli ID dei domini contenuti in una tabella di DNS, compresi dunque l'eventuale Default Place che svolge il ruolo di parent per il Default Place corrente, ed il Default Place corrente stesso.

 

 

Muovere un agente nel sistema


I due agenti realizzati finora non facevano altro che interagire con il sistema in maniera locale al Place su cui erano stati creati. Vediamo ora com'è possibile spostare un agente da un Place all'altro.

Il sistema SOMA mette a disposizione il metodo
public void go(PlaceID destination, String method)
appartenente alla classe Agent, come primitiva per realizzare il movimento di un agente attraverso il sistema. Il primo parametro rappresenta il Place verso cui l'agente vuole migrare, ed il secondo parametro è il nome del metodo pubblico che l'agente desidera eseguire una volta arrivato sul Place di destinazione.

La spiegazione della particolare signature del metodo go() risiede nei limiti del linguaggio Java, che permette solamente la realizzazione di un particolare tipo di mobilità di codice chiamato mobilità debole. Al contrario di ciò che avviene nel modello forte di mobilità, in cui si realizza la migrazione del codice e del suo stato di esecuzione, il modello di mobilità debole prevede solo la migrazione del codice. La piattaforma Java non permette l'accesso allo stato di esecuzione dei thread, in particolar modo allo stack: non è possibile quindi catturare lo stato di esecuzione di un agente, nè tantomeno ripristinarlo.

La cosa più simile alla mobilità forte che si può ottenere con un modello a mobilità debole è permettere alla applicazione di riprendere la propria esecuzione in un punto staticamente determinato, come ad esempio il punto di ingresso di un metodo appartenente alla classe dell'agente. L'applicazione decide quindi dove e quando migrare e, pur non potendo riprendere la propria esecuzione dal punto esatto in cui essa è stata interrotta, è quantomeno in grado di scegliere da che metodo ripartire, una volta ripristinata sul nodo destinazione della sua migrazione.

Si noti che il trasferimento dell'agente da un place ad un altro, come conseguenza della invocazione del metodo go(), avviene in realtà tramite una clonazione (una copia) dell'agente stesso sul place di destinazione. L'esecuzione dell'agente originale, situato ancora sul place di partenza, non viene automaticamente bloccata nè dalla Java Virtual Machine, nè dal sistema SOMA: dopo la chiamata al metodo go(), l'agente originale continua la sua esecuzione. Avere nel sistema due agenti con lo stesso nome contemporaneamente in esecuzione può portare a problemi o inconvenienti, ed è perciò buona norma fare in modo che l'esecuzione dell'agente originale termini in modo naturale dopo l'invocazione del metodo go(), inserendo questa come ultima chiamata nel metodo che la contiene.

Un esempio di un agente che si muove all'interno del dominio nel quale è stato generato può essere sintetizzato nel seguente codice.
import SOMA.agent.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * A try to move an agent around the system.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Monday 11th February, 2002
 */
public class RegionTourAgent extends Agent {
	
	// Serializable members
	private Vector placesToVisit = new Vector();
	
	public void run() {
		
		list();
		
		// Move agent on the other domains or places just listed
		if (placesToVisit.size() > 0) {
			PlaceID placeToGo = (PlaceID) placesToVisit.get(0);
			placesToVisit.remove(placeToGo);
			try {
				go(placeToGo, "run");
			} catch (CantGoException e) {
				e.printStackTrace();
			}
		} else
			agentSystem.getOut().println("Tour finished!");
	}
	
	/** Very similar to the run() method in EnvAgent */
	public void list() {
		
		PlaceID home = agentSystem.getPlaceID();
		
		if (home.isDomain()) {
			// Get children domains
			DomainNameService dns = agentSystem.getEnvironment().domainNameService;
			Vector children = dns.getChildrenDNS(); // a Vector of PlaceIDs
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				placesToVisit.add(place);
			}
			// Get places in this domain
			Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
			places.remove(home);
			for (int index = places.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) places.get(index);
				placesToVisit.add(place);
			}
		}
		
		// Print results
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) placesToVisit.get(index);
			agentSystem.getOut().println(place);
		}
	}

} // end RegionTourAgent
Si noti come l'invocazione del metodo go() svolga il ruolo di ultima chiamata all'interno del metodo run() in cui è contenuta, almeno per quel particolare flusso di esecuzione che entra all'interno della clausola if. In buona sostanza, nello scrivere il codice di ogni agente, si deve cercare di fare in modo che nessuna altra chiamata segua il metodo go(), cosicché l'esecuzione dell'agente possa terminare subito dopo aver effettuato la migrazione.

Se il place di destinazione per la migrazione è caduto o non è più raggiungibile, il metodo go() genera una eccezione di classe SOMA.agent.CantGoException. Ecco il motivo per cui l'invocazione del metodo go() è racchiusa in un blocco try/catch, così da poter catturare l'eccezione generata e gestire la condizione verificatasi in maniera adeguata (in modo magari più evoluto rispetto alla semplice stampa della stack trace come accade nell'esempio).

Esiste però un caso in cui una migrazione che non ha avuto successo non porta alla generazione della relativa CantGoException. Supponiamo infatti che il place di destinazione non sia conosciuto dal place in cui correntemente si trova l'agente, cioè che il nome del place di destinazione non sia contenuto nella sua tabella di PNS. In questo caso, la migrazione avviene verso il Default Place, dove si cercherà di risolvere l'associazione col nome logico controllando la tabella del Domain Name Service. Ma se neppure questo tentativo ha successo, la generazione di una CantGoException non potrà avvenire, a causa della perdita dello stato di esecuzione caratteristico di Java (lo stack) dovuta alla migrazione appena verificatasi.

Per evitare errori causati da un agente che riprenda la sua esecuzione su un place diverso da quello in cui si aspettava di ritrovarsi, è consigliabile controllare che, a seguito di una richiesta di migrazione, l'agente si trovi effettivamente sul place che desiderava raggiungere. Ha quindi senso scrivere il seguente codice, qui riportato come esempio generico.
protected PlaceID destination;

public void run() {
	try {
		// save the destination for later checking
		destination = placeToGo;
		go(placeToGo, "startMethod");
	} catch (CantGoException e) {
		// manage the exception
	}
}

public void startMethod() {
	// get the current place
	PlaceID currentPlace = agentSystem.getPlaceID();
	if (currrentPlace.equals(destination)) {
		// I am where I am supposed to be
	} else {
		// I did not reach my destination!
	}
}
Nel caso il dominio sia definito in maniera particolare (ad esempio sia costituito solo da Defaul Place situati su diverse macchine e sia completamente noto ad ogni nodo) è possibile semplificare il codice dei propri agenti evitando questo ulteriore controllo.

 

Creare nuovi agenti


Un agente non è un thread, in quanto Java non ne permetterebbe la serializzazione, bensì un oggetto passivo: chi si occupa materialmente del flusso di esecuzione di un agente è una entità chiamata worker. Nel momento in cui un agente viene creato o arriva su un place attraverso la rete, il sistema crea un worker a cui l'agente viene affidato: il worker invoca il metodo di lancio dell'agente, per poi attendere che l'agente termini la propria esecuzione.
transient AgentWorker worker = null;
Il worker è definito transient poichè, essendo in realtà un thread, non può migrare insieme all'agente. Piuttosto, quello che avviene è la creazione di un nuovo worker in ogni Place in cui l'agente arriva.

Per creare un agente e metterlo in esecuzione è quindi innanzitutto necessario associargli un worker. Questa operazione si effettua attraverso il metodo createAgent() della classe SOMA.agent.mobility.AgentManager.
/**
 * Creazione di un agente.
 * @param agentName Nome dell'agente.
 * @param argument Parametro di inizializzazione, vedi
 * {@link SOMA.agent.Agent#putArgument(Object obj)}.
 * @param isSystemAgent Se a true si forza l'utilizzo del classloader di sistema
 * @param traceable Se a true l'agente è traceable ed ha una mailbox.
 */
public AgentWorker createAgent(String agentName, Object argument, boolean isSystemAgent, boolean traceable) 
{
	AgentWorker worker = null;

	try
	{
		AgentID newID = newAgentID();
		
		ClassLoader classLoader;
		
		if (isSystemAgent)
			// Lo stesso ClassLoader della classe attuale!
			classLoader = getClass().getClassLoader();
		else
			classLoader = new AgentClassLoader(env, agentName, newID);
		
		Agent agent = (Agent) classLoader.loadClass(agentName).newInstance();
		agent.setID(newID);
		agent.putArgument(argument);
		agent.setTraceable(traceable);
		
		worker = createWorker(agent);
	}
	catch( Exception e )
	{
		e.printStackTrace(env.err);
	}
	
	return worker;
}
Il metodo cerca la classe dal nome agentName, la carica in memoria tramite il Class Loader (usando il caricatore di classi del sistema se l'argomento isSystemAgent è impostato a true), e ne effettua il cast a SOMA.agent.Agent. Dopodichè viene creato un nuovo thread worker a cui viene assegnato l'agente appena creato. Il worker non viene però fatto partire, bensì ne viene restituito un riferimento a chi ha invocato createAgent(). Se il riferimento è null, ciò significa che non è stato possibile creare l'agente. In caso la creazione abbia successo, per lanciare il worker (e quindi porre in esecuzione l'agente) è sufficiente invocarne il metodo start().

Il secondo argomento del metodo createAgent() rappresenta una struttura contenente parametri utili alla inizializzazione dell'agente. L'ultimo argomento indica la tracciabilità di un agente all'interno del sistema: se un agente possiede o meno una mailbox (discussa più avanti in questo documento) risulta rintracciabile o meno da parte del sistema SOMA.

Un esempio della creazione di nuovi agenti può essere costituito da una coppia di classi: la prima rappresenta un agente con il ruolo di base, atto a creare altri agenti e a recuperare i place su cui farli migrare; la seconda rappresenta un agente che, creato dall'agente base, effettua un singolo spostamento.
import SOMA.agent.*;
import SOMA.agent.mobility.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * This class represents the base agent, and it creates LurkerTryAgent(s) in order
 * to explore the first-level domains and places. Note that the base agent is fixed
 * (i.e. it does not move itself around the domains).
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class BaseTryAgent extends Agent {
	
	public void run() {
		
		Vector placesToVisit = new Vector();
		PlaceID home = agentSystem.getPlaceID();
		SOMA.Environment env = agentSystem.getEnvironment();
		
		if (home.isDomain()) {
			// Get children domains
			Vector children = env.domainNameService.getChildrenDNS();
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
			// Get places in this domain
			Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
			places.remove(home);
			for (int index = places.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) places.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
		}
		
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			PlaceID placeToGo = (PlaceID) placesToVisit.get(index);
			Vector v = new Vector();
			v.add(home);
			v.add(placeToGo);
			AgentWorker lurker = env.agentManager.createAgent("LurkerTryAgent", v,
									false, true);
			if (lurker != null)
				try {
					lurker.start();
				} catch (AgentWorker.AgentWorkerException awe) {
					awe.printStackTrace();
				}
		}
	}
} // end BaseTryAgent
Si notino le ultime righe del metodo run(): in esse si crea un java.util.Vector che contiene alcuni parametri passati dall'agente base agli agenti lurker. Come abbiamo già detto, il secondo argomento del metodo createAgent() appena analizzato rappresenta utili informazioni di inizializzazione per gli agenti creati, passate ad essi sotto forma di java.lang.Object. In questo esempio, si passano ad un LurkerTryAgent il place su cui esso viene creato ed il place di destinazione su cui esso dovrà migrare.

Una volta passati, come vengono recuperati questi parametri? Diamo una occhiata al codice della classe LurkerTryAgent.
import SOMA.agent.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

/**
 * Coupled with BaseTryAgent, this agent realizes a first try to create and
 * move new agents from an initial place.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class LurkerTryAgent extends Agent {
	
	// Serializable fields
	private PlaceID myHome; // the place where the lurker was created
	private PlaceID myNextPlace; // the place where the lurker was said to go
	
	public void putArgument(Object obj) {
		// the object is a Vector
		java.util.Vector v = (java.util.Vector) obj;
		myHome = (PlaceID) v.get(0);
		myNextPlace = (PlaceID) v.get(1);
	}
	
	public void run() {
		try {
			go(myNextPlace, "greetings");
		} catch (CantGoException cge) {
			cge.printStackTrace();
		}
	}
	
	public void greetings() {
		// get the current place
		PlaceID currentPlace = agentSystem.getPlaceID();
		if (currrentPlace.equals(myNextPlace))
			agentSystem.getOut().println("I come from " + myHome + "- I was said to go to "
					+ myNextPlace + " and I am now in " + currentPlace);
		else
			agentSystem.getOut().println("I did not reach my destination!");
	}
	
} // end LurkerTryAgent
Le informazioni contenute nella struttura passata ad un agente al momento della sua creazione vengono trasmesse al metodo putArgument(). Esso è il primo metodo che il sistema chiama per ogni agente creato su un qualsiasi place, ma la sua implementazione nella classe astratta Agent è vuota.
/**
 * Permette di definire lo stato iniziale dell'agente.
 * Questo metodo è vuoto nella classe Agent e deve essere
 * ridefinito dalle sottoclassi che implementano agenti, in
 * maniera analoga a {@link #run()}.
 * @param obj Un oggetto contenente informazioni di inizializzazione.
 * Ovviamente l'oggetto può anche contenere una struttura dati complessa.
 */
public void putArgument(Object obj) {}
Questo significa che se il programmatore non ridefinisce il metodo con la propria implementazione, l'effetto della sua chiamata da parte del sistema sarà nullo. Ma nel caso si vogliano passare dei parametri di inizializzazione ad un agente, essi si potranno accedere tramite l'argomento del metodo putArgument(), che dovrà qunidi essere ridefinito in maniera appropriata, a seconda del tipo di struttura usata per contenere le informazioni, e a seconda del tipo di informazioni passate.

Si noti poi che, al momento della chiamata di putArgument() nel metodo createAgent() della classe AgentManager, il membro agentSystem interno alla classe dell'agente ancora non esiste: esso viene infatti settato solo all'interno del metodo createWorker(), che il metodo createAgent() chiama solo dopo aver chiamato putArgument().
public AgentWorker createWorker(Agent agent)
{
	AgentWorker worker = null;
	try
	{
		// Se non c'è il security manager, non effettuo neanche i controlli.
		
		if( System.getSecurityManager() == null ||
		agent.getClass().getProtectionDomain().implies(new PlaceAccessPermission(env.placeID)))
		{
			agent.agentSystem = agentSystem;
			// ...
		}
		// ...
	} 
	catch( Exception e )
	{
		e.printStackTrace(env.err);
	}
	return worker;
}
Ciò implica che nè l'AgentSystem stesso, nè tutte le altre strutture a cui si accede attraverso l'AgentSystem (come ad esempio gli Shared Objects trattati nel capitolo seguente), possono essere utilizzate all'interno del metodo putArgument(). La funzione per la quale esso è stato pensato, cioè passare agli agenti appena creati una struttura contenente parametri di inizializzazione, è anche l'unica funzione utile che esso può svolgere.

 

Comunicazione e coordinamento: gli Shared Objects


In termini generali, si possono distinguere due forme di comunicazione tra agenti:
  • una forma di interazione stretta, usata quando si vuole avere una forte collaborazione tra agenti, e realizzata sotto forma di oggetti condivisi o tramite l'invocazione diretta dei metodi di un altro agente. Essendo necessaria una conoscenza statica della interazione, questa forma risulta poco flessibile e limitata.

  • una forma di interazione lasca, in cui non è richiesta una conoscenza esplicita degli agenti che vogliono comunicare. Può essere realizzata tramite message passing, in modo che si debba conoscere solo l'identificatore unico del destinatario, lasciando al sistema sottostante i compiti di ricerca dell'agente ricevente e di spedizione del messaggio. Si può anche pensare a forme di comunicazione anonima utilizzando una blackboard o un tuple space.

Per quel che riguarda le forme di interazione stretta, il sistema SOMA non permette ad un agente di crearne un altro e di riferirsi direttamente ad esso, poiché quando un agente crea un altro agente ciò che avviene è in realtà, come abbiamo già rilevato, la creazione di un worker, a cui viene demandato il compito di mandare in esecuzione il nuovo agente. Ciò rende impossibile per un agente richiamare direttamente i metodi di un altro agente.

SOMA mette dunque unicamente a disposizione una tabella hash di oggetti condivisi, riferita all'interno di AgentSystem con il nome di sharedObjects. In essa è possibile memorizzare delle coppie {chiave, valore} in cui la chiave è un identificatore intero, ed il valore un qualsiasi oggetto Java. Un agente che deposita una coppia in sharedObjects renderà visibile il valore a tutti gli agenti che ne conoscano la chiave corrispondente.

Un possibile esempio dell'uso degli sharedObjects può essere strutturato secondo lo schema comprendente un agente base e più agenti lurker già visto in precedenza. In questo caso, l'agente base depositerà nella tabella hash informazioni che gli agenti lurker provvederanno a manipolare. Infine, sempre l'agente base si occuperà di visualizzare il risultato del lavoro degli agenti lurker.

Il codice dell'agente base è il seguente.
import SOMA.agent.*;
import SOMA.agent.mobility.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * An example of a base agent using shared objects to coordinate its work
 * with other lurker agents.
 * Note that the base agent does not die, but waits for the lurker agent(s)
 * to return, and must be terminated manually, since it uses the idle() method
 * to suspend itself.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class SObjBaseAgent extends Agent {
	
	public void run() {		
		Vector placesToVisit = new Vector();
		PlaceID home = agentSystem.getPlaceID();
		SOMA.Environment env = agentSystem.getEnvironment();
		
		if (home.isDomain()) {
			// Get children domains
			Vector children = env.domainNameService.getChildrenDNS();
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
			// Get places in this domain
			Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
			places.remove(home);
			for (int index = places.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) places.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
		}
		
		// Initialize the shared objects relative to all children domains and places
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) placesToVisit.get(index);
			agentSystem.sharedObjects.put(index, place);
		}
		
		// Create lurkers which will move on first level domains and places, access shared
		// objects and somehow manipulate them
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			Vector v = new Vector();
			v.add(home);
			v.add(new Integer(index));
			AgentWorker lurker = env.agentManager.createAgent("SObjLurkerAgent", v,
									false, true);
			try {
				lurker.start();
			} catch (AgentWorker.AgentWorkerException awe) {
				awe.printStackTrace();
			}
		}
		
		idle("printSharedObjects"); // wait
	}
	
	public void printSharedObjects() {
		for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--)
			agentSystem.getOut().println(agentSystem.sharedObjects.get(index).toString());
	}
	
} // end SObjBaseAgent
Si noti che le modalità di accesso agli sharedObjects sono quasi identiche a quelle di una qualsiasi java.util.Hashtable, con la differenza che in questo caso gli indici sono numerici invece di poter essere un qualsiasi oggetto Java. La struttura degli sharedObjects deriva in realtà dalla classe SOMA.utility.IndexHashtable che estende direttamente java.util.Hashtable, pur apportando alla sua interfaccia le modifiche appena segnalate.

L'agente base aspetta sul metodo idle() che gli agenti lurker abbiano terminato le loro manipolazioni, entrando quindi nel cosiddetto stato Idle. Lo stato Idle, pur essendo uno stato di attesa di messaggi, differisce molto, ad esempio, dallo stato in cui l'agente si trova quando si mette in attesa di messaggi nella sua mailbox con la relativa primitiva (si veda il capitolo seguente per i dettagli sull'uso delle mailbox). Infatti, mentre nel secondo caso l'agente è attivo ma bloccato, quando si trova nello stato Idle l'agente è disattivo. Per illustrare la differenza tra questi due stati, basti pensare che, attendendo messaggi nella propria mailbox, un agente potrebbe sbloccarsi autonomamente allo scadere di un certo timeout prefissato pur non avendo ricevuto messaggi, mentre è invece concettualmente impossibile, per un agente, uscire dallo stato Idle in maniera autonoma. Una volta nello stato Idle, un agente viene congelato e affidato al sistema, e dovrà essere il sistema stesso a risvegliarlo al momento opportuno.

Non usando una mailbox nel nostro esempio, l'unico modo di far attendere l'agente base per un tempo indefinito consiste nel farlo entrare nello stato Idle, da cui però non potrà risvegliarsi da solo: sarà necessario l'intervento manuale dell'utente per riattivare l'agente base e fargli stampare a video gli sharedObjects manipolati dagli agenti lurker.

Tabella di manipolazione degli agenti su un place
Figura 2. Premendo il pulsante Start, l'agente selezionato esce dallo stato Idle e si riattiva.

Il codice relativo alla classe degli agenti lurker è il seguente. Si noti che, per permettere al lettore di concentrarsi sull'argomento del presente capitolo, sono stati omessi dal codice i controlli che ogni agente mobile dovrebbe effettuare per verificare di essere arrivato nel posto giusto dopo aver eseguito una migrazione.
import SOMA.agent.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * An example of a lurker agent coordinating with a base agent thanks to
 * the use of shared objects.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class SObjLurkerAgent extends Agent {
	
	private PlaceID myHome; // the PlaceID the agent come from
	
	// myIndex really indicates the particular shared object property of the lurker agent,
	// i.e. the one and only object a single lurker agent will modify
	private int myIndex;
	private String manipulation = "";
	
	public void putArgument(Object obj) {
		Vector v = (Vector) obj;
		myHome = (PlaceID) v.get(0);
		Integer temp = (Integer) v.get(1);
		// the agent destination place's index in the shared objects table
		myIndex = temp.intValue();
	}
	
	public void run() {
		try {
			go((PlaceID) agentSystem.sharedObjects.get(myIndex), "buildString");
		} catch (CantGoException cge) {
			cge.printStackTrace();
		}
	}
	
	/**
	 * Builds the String which will substitute the destination PlaceID entry in the
	 * shared objects table.
	 */
	public void buildString() {
		// Get children domains
		if (agentSystem.getPlaceID().isDomain()) {
			SOMA.Environment env = agentSystem.getEnvironment();		
			Vector children = env.domainNameService.getChildrenDNS();
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				manipulation += place.toString();
			}
		}
		// Get places in this domain
		Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
		places.remove(agentSystem.getPlaceID());
		for (int index = places.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) places.get(index);
			manipulation += " " + place.toString();
		}
		
		try {
			go(myHome, "manipulate");
		} catch (CantGoException cge) {
			cge.printStackTrace();
		}
	}
	
	public void manipulate() {
		// Substitute the old object (a PlaceID) with the String previously built
		agentSystem.sharedObjects.remove(myIndex);
		agentSystem.sharedObjects.put(myIndex, manipulation);
		// Print all the shared objects except the one just modified
		agentSystem.getOut().println("---" + getID() + "---" + myIndex + "---\n");
		for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--)
			if (index != myIndex)
				agentSystem.getOut().println(
					agentSystem.sharedObjects.get(index).toString());
	}
	
} // end SObjLurkerAgent
Va sottolineato come gli Shared Objects siano una facility pensata per consentire lo scambio di oggetti tra agenti che risiedono seppur momentaneamente sullo stesso place, ma che non si conoscono l'un l'altro. Nel caso si desideri usare gli Shared Objects per altri fini, ci si dovrebbe domandare se non esista una maniera concettualmente più corretta di concretizzare le proprie intenzioni. Per fare un esempio, sebbene sia possibile inserire nella tabella hash oggetti che rappresentino parametri di inizializzazione che un agente appena creato legga al momento della esecuzione del suo metodo run(), questo tipo di uso degli Shared Objects non rispetta lo spirito con il quale lo strumento è stato creato, ed il sistema SOMA, come abbiamo visto parlando del metodo createAgent(), offre al problema della inizializzazione una soluzione più efficace, elegante, e soprattutto adeguata.

 

Comunicazione e coordinamento: la Mailbox


Per quel che riguarda le forme di interazione stretta, illustrata all'inizio del precedente capitolo, ogni agente si porta dietro una propria mailbox, una rappresentazione della astrazione casella di posta nella quale vengono memorizzati i messaggi che arrivano in modo asincrono spediti dagli altri agenti. La mailbox mette a disposizione i seguenti metodi per inserire e prelevare messaggi:
public synchronized void storeMessage(Message message)
public syncrhonized Message getMessage()
public synchronized boolean isMessage()
La creazione dei messaggi da inviare ad altri agenti si effettua tramite il costruttore della apposita classe:
public Message(Object message, AgentID from, AgentID to)
Ogni messaggio porta quindi con sè informazioni relative al mittente e al destinatario, ed il suo contenuto informativo è rappresentato da un qualsiasi oggetto Java. Si noti che, come già detto, per spedire un messaggio, un agente deve conoscere il nome (l'identificatore unico) dell'agente destinatario. Compito del sistema è rintracciare l'agente destinatario e consegnargli il messaggio. Una volta creato il messaggio, per spedirlo bisognerà dunque consegnarlo al sistema attraverso il place in cui l'agente si trova, tramite il metodo sendMessage() di AgentSystem.

Si notino poi le caratteristiche della azione di spedizione di un messaggio: essa è trasparente alla locazione, in quanto si deve specificare solo il nome dell'agente destinatario, ma non il place in cui esso si trova; essa è inoltre semanticamente asincrona, nel senso che il sistema si occupa della consegna senza notificare l'agente del successo o di un eventuale fallimento della operazione, e quindi senza dare garanzie sul fatto che il messaggio arrivi a destinazione; ma è operativamente bloccante, nel senso che, sebbene la spedizione venga effettuata dal sistema in modo che l'agente non debba attendere e possa proseguire la sua esecuzione successiva alla chiamata di sendMessage(), l'agente dovrà comunque attendere il tempo necessario a copiare il messaggio in una apposita area di sistema.

Infine, si noti come anche la ricezione di un messaggio sia una operazione bloccante: nel momento in cui l'agente chiede la lettura di un messaggio, nel caso di mailbox vuota esso rimarrà bloccato fino a quando non ne arriverà uno. Questo comportamento è testimoniato dal codice del metodo getMessage() della classe SOMA.agent.Mailbox riportato qui di seguito.
/**
 * Restituisce il primo messaggio in mailbox. La chiamata è sospensiva
 * ma esiste la possibilità di verificare se la Mailbox è piena.
 */
public synchronized Message getMessage()
{
	try
	{
		while (messages.size() == 0)
			wait();
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
	return (Message) messages.removeFirst();
}
La possibilità, citata nel commento JavaDoc, di verificare se la mailbox è piena, viene messa a disposizione dal metodo isMessage().

Possiamo creare un piccolo esempio in cui più agenti si coordinano attraverso una maibox, sfruttando il codice già scritto per l'esempio precedente concernente l'uso degli Shared Objects. Mentre prima era necessario un intervento esterno per consentire all'agente base di risvegliarsi e leggere il contenuto della tabella hash modificata dagli agenti lurker, ora il coordinamento verrà realizzato attraverso la mailbox. L'agente base manterrà in una variabile il numero di agenti lurker che ha creato; dopodichè, una volta che un agente lurker avrà finito il proprio lavoro, spedirà un particolare messaggio all'agente base; dopo aver ricevuto un numero di messaggi di un certo tipo pari al numero di lurker creati, l'agente base provvederà a presentare a video il contenuto degli Shared Objects.

Il codice dell'agente base è il seguente.
import SOMA.agent.*;
import SOMA.agent.mobility.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * An example of a base agent using its mailbox to coordinate its work
 * with other lurker agents.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class MailboxBaseAgent extends Agent {
	
	private int numAgents = 0;
	private int numMessage = 0;
	
	public void run() {
		Vector placesToVisit = new Vector();
		PlaceID home = agentSystem.getPlaceID();
		SOMA.Environment env = agentSystem.getEnvironment();
		
		if (home.isDomain()) {
			// Get children domains
			Vector children = env.domainNameService.getChildrenDNS();
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
			// Get places in this domain
			Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
			places.remove(home);
			for (int index = places.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) places.get(index);
				// update the list of places to visit
				placesToVisit.add(place);
			}
		}
		
		// Initialize the shared objects relative to all children domains and places
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) placesToVisit.get(index);
			agentSystem.sharedObjects.put(index, place);
		}
		
		// Create lurkers which will move on first level domains and places, access shared
		// objects and somehow manipulate them
		for (int index = placesToVisit.size() - 1; index >= 0; index--) {
			Vector v = new Vector();
			v.add(home);
			v.add(new Integer(index));
			v.add(getID()); // the base agent ID
			AgentWorker lurker = env.agentManager.createAgent("MailboxLurkerAgent", v,
									false, true);
			try {
				lurker.start();
			} catch (AgentWorker.AgentWorkerException awe) {
				awe.printStackTrace();
			}
			numAgents++;
		}
		
		// if the agent is not traceable, there is no mailbox
		if (mailbox != null)
			mailbox.mailListener = new Mailbox.MailListener() {
				
				public void run() {
					Message mex = mailbox.getMessage();
					agentSystem.getOut().println(
						"Message received by " + mex.from);
					if (mex.message.toString().equals("lurker_agent_done"))
						numMessage++;
					agentSystem.getOut().println(
						"" + numMessage + " message(s) arrived");
				}
				
			};
		agentSystem.getOut().println("" + getID() + ": I'm going idle...");
		idle("idleCycle"); // wait
	}
	
	public void idleCycle() {
		if (numMessage != numAgents)
			idle("idleCycle");
		else
			printSharedObjects();
	}
	
	public void printSharedObjects() {
		for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--)
			agentSystem.getOut().println(agentSystem.sharedObjects.get(index).toString());
	}
	
} // end MailboxBaseAgent
L'interfaccia MailListener è definita all'interno della classe Mailbox e dichiara l'unico metodo run() che è stato implementato nel nostro esempio. In esso, l'agente richiede alla propria mailbox un messaggio, ed incrementa un contatore nel caso esso sia stato mandato da uno degli agenti lurker che sono stati precedentemente creati. Già da qui si può intuire il formato del messaggio: una semplice java.lang.String contenente una particolare sequenza di caratteri ("lurker_agent_done").

Va evidenziato l'uso del metodo idle() all'interno del metodo idleCycle(), richiamato la prima volta subito dopo aver costruito un listener per la mailbox, e poi più volte fino a quando tutti gli agenti lurker non hanno concluso il proprio lavoro e spedito un messaggio all'agente base. Si deve fare in modo che l'agente base non muoia subito dopo aver creato il MailListener e che rimanga sospeso nel sistema fino a quando tutti i messaggi spediti dai lurker non gli siano arrivati. Per ottenere questo comportamento è necessario che il sistema prenda in consegna l'agente e lo sospenda: esattamente ciò che accade con l'utilizzo del metodo idle().

Ma non si era forse sottolineata in precedenza la necessità di un intervento esterno al fine di risvegliare un agente messo nello stato Idle? L'uso della sospensione dell'agente sulla mailbox ci permette di non avere più bisogno di un intervento esterno operato da un utente; piuttosto, l'agente viene riavviato dal sistema non appena gli viene consegnato un messaggio. Il metodo sendMessage() della classe SOMA.agent.mobility.AgentManager, che è quello che alla fine si occupa della trasmissione vera e propria del messaggio, illustra molto bene questo caso particolare in una porzione del suo codice riportata qui di seguito.
public synchronized void sendMessage(Message message, int attemptsCount)
{
	AgentWorker destinationWorker =
		env.agentManager.agentWorkerStore.getWorker(message.to);
	
	if (destinationWorker != null &&
	destinationWorker.getStatus() != AgentWorker.GONE) // Ho già trovato l'agente!
	{
		destinationWorker.agent.mailbox.storeMessage(message);
		
		// ATTENZIONE: Agenti in idle ==> riavviati.
		if (destinationWorker.getStatus() == AgentWorker.OFF ||
		destinationWorker.getStatus() == AgentWorker.IDLE ||
		destinationWorker.getStatus() == AgentWorker.STOPPED)
		{
			env.out.println("Waking up agent " + message.to);
			try
			{
				destinationWorker.start();
			}
			catch (AgentWorker.AgentWorkerException e)
			{
				e.printStackTrace();
			}
		}
	}
	// ...
}
Si nota infatti subito che, se l'agente destinatario di un messaggio è nello stato Idle, viene risvegliato, per poi potergli recapitare il messaggio e fare in modo che possa eventualmente reagire di conseguenza. Nel nostro esempio, fino a quando non sono arrivati un numero di messaggi pari al numero di lurker creati, l'agente base non potrà stampare i risultati del lavoro effettuato dai lurker, e quindi dovrà ripetutamente rientrare nello stato Idle fino a quando la condizione appena detta per il proseguimento della sua esecuzione non si sarà verificata.

Il codice della classe relativa agli agenti lurker di questo esempio è il seguente. Questa volta i lurker non si limitano a manipolare un oggetto trovato nella tabella hash degli Shared Objects, ma costruiscono un vettore di 10.000 interi, in modo da perdere un po' di tempo e realizzare, su una singola macchina, una sequenza di arrivi cronologicamente simile a quella che si verifica in una rete locale.
import SOMA.agent.*;
import SOMA.naming.*;
import SOMA.naming.domain.*;

import java.util.*;

/**
 * An example of a lurker agent coordinating with a base agent thanks to
 * the use of a mailbox.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Wednesday 13th February, 2002
 */
public class MailboxLurkerAgent extends Agent {
	
	private PlaceID myHome; // the PlaceID the agent come from
	
	// myIndex really indicates the particular shared object property of the lurker agent,
	// i.e. the one and only object a single lurker agent will modify
	private int myIndex;
	private AgentID parent;
	private String manipulation = "";
	
	public void putArgument(Object obj) {
		
		Vector v = (Vector) obj;
		myHome = (PlaceID) v.get(0);
		Integer temp = (Integer) v.get(1);
		// the agent destination place's index in the shared objects table
		myIndex = temp.intValue();
		parent = (AgentID) v.get(2);
		
	}
	
	public void run() {
		try {
			go((PlaceID) agentSystem.sharedObjects.get(myIndex), "buildString");
		} catch (CantGoException cge) {
			cge.printStackTrace();
		}
	}
	
	/**
	 * Builds the String which will substitute the destination PlaceID entry in the
	 * shared objects table.
	 */
	public void buildString() {
		// Get children domains
		if (agentSystem.getPlaceID().isDomain()) {
			SOMA.Environment env = agentSystem.getEnvironment();
			Vector children = env.domainNameService.getChildrenDNS();
			for (int index = children.size() - 1; index >= 0; index--) {
				PlaceID place = (PlaceID) children.get(index);
				manipulation += place.toString();
			}
		}
		// Get places in this domain
		Vector places = new Vector(Arrays.asList(agentSystem.getPlaces()));
		places.remove(agentSystem.getPlaceID());
		for (int index = places.size() - 1; index >= 0; index--) {
			PlaceID place = (PlaceID) places.get(index);
			manipulation += " " + place.toString();
		}
		
		// Build a huge vector - make them being late!
		Vector v = new Vector();
		for (int i = 0; i < 10000; i++)
			v.add(new Integer(i));
		
		try {
			go(myHome, "manipulate");
		} catch (CantGoException cge) {
			cge.printStackTrace();
		}
	}
	
	public void manipulate() {
		// Substitute the old object (a PlaceID) with the String previously built
		agentSystem.sharedObjects.remove(myIndex);
		agentSystem.sharedObjects.put(myIndex, manipulation);
		// the agent has done, and notifies it to its parent
		Message mex = new Message("lurker_agent_done", getID(), parent);
		agentSystem.sendMessage(mex);
	}
	
} // end MailboxLurkerAgent
Si noti che, ancora una volta, per permettere al lettore di concentrarsi sull'argomento del presente capitolo, sono stati omessi dal codice i controlli che ogni agente mobile dovrebbe effettuare per verificare di essere arrivato nel posto giusto dopo aver eseguito una migrazione.

 

Agenti con GUI personalizzate


Gli strumenti trattati in [Ant99] non solo permettono l'accesso al sistema SOMA tramite una comoda interfaccia grafica, ma consentono anche di dotare i propri agenti di GUI attraverso le quali settare opzioni, originare comandi tramite pulsanti, e via dicendo. Un minimale agente che si presenti attraverso una finestra grafica appare come nella seguente figura.

Un minimale agente con GUI
Figura 3. Un minimale agente dotato di GUI.

Come si vede, la finestra con cui ogni agente si presenta contiene almeno: una area di testo su cui scrivere messaggi, in un certo modo simile all'area di testo presente nella finestra di ogni Place; un campo di testo da usare come se fosse una linea di comando, per impartire istruzioni a SOMA e all'agente; un pulsante Clear, per pulire l'area di testo contenuta nella finestra.

Il codice di questo agente è riportato qui di seguito.
import SOMA.agent.*;
import SOMA.gui.*;
import SOMA.utility.*;

import java.awt.*;
import javax.swing.*;

/**
 * The minimal windowed agent - a mobile agent controlled by a window
 * built up using the SOMA GUI facilities.
 *
 * @author	Giulio Piancastelli
 * @version	1.0 - Monday 18th February, 2002
 */
public class WinSkeletonAgent extends Agent {
	
	/* Transient members */
	
	// The agent's output window
	private transient OutputFrame2 window;
	// Wait on the semaphore before exiting - to synchronize the main thread (waiting) with
	// actions from the menu and buttons
	private transient WaitAndTimeout exitSemaphore;
	
	public void run() {
		agentSystem.getOut().println("Agent [" + getID() + "] running...");
		buildWindow(); // create the agent's window
		
		exitSemaphore = new WaitAndTimeout(0, "<EXIT>", window.out);
		startMethod();
	}
	
	public void startMethod() {
		agentSystem.getOut().println("Agent [" + getID() + "] executing startMethod...");
		
		exitSemaphore.Wait(); // waiting here...
		// restarting with Exit, Go or Idle commands
		window.dispose();
		agentSystem.getOut().println("Agent [" + getID() + "] exiting from main thread...");
	}
	
	private void buildWindow() {
		window = new OutputFrame2("Agent [" + getID() + "]", WinSkeletonAgent.class.getName());
		
		// what to do on exit
		window.onExitCommand = new OutputFrame2.Listener() {
			
			public void run() {		
				// show this message in the agent window when exiting
				window.out.println("Agent [" + getID() + "] exiting...");
				exitSemaphore.Done();
			}
			
		};
		window.pack();
		window.setVisible(true);
	}
	
} // end WinAgent
Si noti innanzitutto che la finestra dell'agente è una istanza della classe SOMA.gui.OutputFrame2. Essa estende javax.swing.JFrame, e può dunque essere trattata come tale, il che significa ad esempio che ad essa si possono aggiungere menu, oltre ad effettuare tutte le manipolazioni che possono essere fatte su un normale JFrame. Inoltre, OutputFrame2 mette a disposizione del programmatore un pannello inizialmente vuoto in cui poter inserire i controlli di cui l'agente ha bisogno: tipicamente pulsanti appartenenti alla classe javax.swing.JButton, ma anche javax.swing.JComboBox e più in generale qualsiasi componente Swing si ritenga necessario.
/**
 * Pannello vuoto, inserito prima del pannello coi bottoni di default.
 * Utile per inserirvi i propri bottoni.
 * Questo pannello viene CREATO E INSERITO NEL FRAME DA QUESTA CLASSE.
 * Dopo la costruzione lo si può riempire con ciò che si vuole, ma bisogna
 * farlo sempre PRIMA di chiamare il metodo setVisible() sulla finestra!
 */
public JPanel prePanBottoni;
Il pannello verrà visualizzato accanto al pulsante Clear, come si può vedere nella seguente immagine, raffigurante un agente di esempio discusso in [Ant99] e recuperabile nella directory agents della distribuzione di SOMA.

Un agente evoluto con GUI
Figura 4. Un agente dotato di GUI con una serie di pulsanti ed un menu.

Altri membri interessanti della classe OutputStream2 sono:
public transient PrintStream out;
public Listener onExitCommand;

Il primo è lo stream di output collegato alla area di testo della finestra, su cui si scrivono informazioni come sulla area di testo della finestra di Place: nei nostri esempi, nel primo caso si è usato window.out.println(), nel secondo caso si è usato agentSystem.getOut().println(). Il secondo è un listener al cui metodo run() deve essere assegnato il lavoro da fare prima di chiudere la finestra e di lasciare quindi che l'agente muoia.

Nel codice del nostro esempio va sottolineato l'uso dell'oggetto di sincronizzazione SOMA.utility.WaitAndTimeout. Esso serve in generale per l'attesa di un evento coadiuvata da un timeout, e nel presente caso di costruzione di una GUI per un agente, funziona da semaforo su cui attendere prima di uscire, in modo da sincronizzare il thread principale in attesa con le azioni dei bottoni o del menu.

Non per tutti gli agenti ha senso l'uso di una GUI: nell'esempio del capitolo precedente, infatti, l'agente base potrebbe aver bisogno di una interfaccia grafica in modo che l'utente possa decidere quando far partire i lurker, oppure per un tipo di presentazione dei dati più raffinato rispetto alle possibilità offerte dalle finestre dei Place. Può avere meno senso l'idea di fornire una GUI anche ai lurker, che somigliano piuttosto ad elementi di pura computazione e comunicazione, senza che vi sia da parte loro un interesse per la parte di presentazione necessaria in una qualsiasi applicazione.

Infine, si noti che un vantaggio indiretto nell'uso di una GUI, per quegli agenti che ne possono essere ragionevolmente dotati, consiste nel non doversi più preoccupare del rischio che un agente muoia prima di potersi coordinare efficacemente con altri agenti, magari da lui stesso creati, come nelle combinazioni tra base e lurker viste finora. Infatti, riprendendo l'esempio visto nell'uso di mailbox o Shared Objects, nel caso l'agente base sia dotato di GUI, le chiamate al metodo idle() non sono più necessarie per il corretto funzionamento della applicazione.

 


Bibliografia
[Ant99] Luigi Antenucci, "Interfacce di controllo per sistemi ad agenti mobili in ambienti eterogenei", Tesi di Laurea presso l'Università di Bologna, Facoltà di Ingegneria, a.a. 1998/1999
[FPV98] Alfonso Fuggetta, Gian Pietro Picco, Giovanni Vigna, "Understanding Code Mobility", IEEE Transaction On Software Engineering, May 1998
[KT98] Neeran Kamik, Anand Tripathi, "Design Issues in Mobile-Agent Programming Systems", IEEE Concurrency, July-September 1998
[Pic01] Gian Pietro Picco, "Mobile Agents: An Introduction", Microprocessors And Microsystems, April 2001
[Pro99] Livio Profiri, "", Tesi di Laurea presso l'Università di Bologna, Facoltà di Ingegneria, a.a. 1998/1999