MokaByte Numero 19 - Maggio 1998  

Agenti mobili in Java
(I parte)

di
Lorenzo Bettini

  Iniziamo a vedere questo mese una semplice implementazione degli agenti mobili in Java.


Introduzione

Delle ragioni per l'utilizzo del codice mobile si è già parlato nello scorso articolo [1]. Un caso particolare di codice mobile è rappresentato proprio dagli agenti mobili. Gli agenti mobili sono già stati trattati in [2]. I vantaggi degli agenti mobili sono molteplici, ma quelli principali sono il basso utilizzo delle comunicazioni in rete (tipico del codice mobile in generale), e l'autonomia di tali agenti che permette di slegare il mittente dall'agente stesso. L'idea principale è quella di utilizzare gli agenti mobili soprattutto nel commercio elettronico, in modo da effettuare non solo acquisti in Internet, ma vere e proprie ricerche: una volta forniti gli estremi all'agente, sarà quest'ultimo a visitare i vasi siti in Internet e ad effettuare le ricerche; alla fine riporterà i risultati al "padrone". Con questa idea è stato sviluppato il linguaggio Telescript [3] dalla General Magic.

In questo articolo non si avrà la pretesa di sviluppare un vero e proprio framework per la gestione degli agenti mobili in Java; tra l'altro in rete si possono trovare molte implementazioni, fra le quali spicca, forse anche perché è stata una delle prime, quella dell'IBM: la libreria degli Aglets [2] [4]. Invece si vuole dare un'idea di una possibile implementazione degli agenti mobili utilizzando le caratteristiche messe a disposizione da Java, che si è detto già molte volte, lo rendono un linguaggio molto adatto per lo sviluppo di applicazioni distribuite con codice mobile.

Classi

Come sempre sarà presentato un package, Agent, che conterrà le varie classi del framework. Si è prestata particolare attenzione alla estendibilità (e quindi alla possibilità di personalizzazione delle varie classi). Inoltre la situazione si presta molto bene ad essere implementata con il paradigma del Client-Server [5].

L'AgentServer

Questa class costituisce il server, cioè "accoglie" i vari agenti che provengono dalla rete. Anche in questo caso si tratta di un server multithreaded (in ascolto su una determinata porta, che può essere specificata da linea di comando), che è perennemente in ascolto di richieste di connessione, e per ogni richiesta manda in esecuzione un nuovo thread per gestire quella particolare connessione (in questo caso quel particolare agente).

  public void run() {
    Socket socket ;

    try {
      while( true ) {
        socket = serversocket.accept() ;
        Print( "Nuova connessione da " + 
           socket.getInetAddress().getHostAddress() +
           ":" + socket.getPort() ) ;
        newAgentHandler( this, socket ) ;
      }
    } catch ( IOException e ) {
      System.err.println( e ) ;
    }
  }

  protected void newAgentHandler( AgentServer server, Socket socket ) {
    AgentHandler Ahandler ;
    Ahandler = new AgentHandler( server, socket ) ;
    Ahandler.start() ;
  }

Per semplicità si sono riportati solo i metodi principali di tale classe.

L'AgentHandler

Questa classe si occupa di gestire la singola connessione col client; in questo caso particolare si occupa di gestire l'agente appena inviato in rete:

  public void run() {
    startAgentLoader() ;
  }

  protected void startAgentLoader() {
    try {
      AgentClassLoader loader = new AgentClassLoader() ;
      loader.setMessages( true ) ;
      String className = new String("Agent.DefaultAgentLoader") ;
      loader.addClassBytes( className, ClassBytesLoader.loadClassBytes(
        className ) ) ;
      Class agentLoaderClass = loader.forceLoadClass( className ) ;
      AgentLoader agentLoader = (AgentLoader)agentLoaderClass.newInstance() ;
      agentLoader.setServer( server ) ;
      agentLoader.setInputStream( socket.getInputStream() ) ;
      agentLoader.setOutputStream( socket.getOutputStream() ) ;
      agentLoader.start() ;
    } catch ( Exception e ) {
      e.printStackTrace() ; 
    }
  }

Poiché si riceve del codice dalla rete, e cioè si dovranno utilizzare classi che non necessariamente sono presenti sul server, si ha la necessità di scrivere un class loader personalizzato, per scaricare le classi di cui si necessita dalla rete, insieme all'agente. Per quanto riguarda il funzionamento del class loader si veda [6] e per un esempio di class loader personalizzato che scarica i dati di una classe dalla rete si veda [1].

L'AgentHandler non gestisce personalmente l'agente, ma lascia questa gestione ad un AgentLoader; vale la pena notare che tale AgentLoader viene caricato col class loader personalizzato: in questo modo, qualsiasi classe che sarà necessaria a quest'ultimo durante la sua esecuzione sarà caricato con tale class loader automaticamente.

Per quanto già detto in [6], non si può effettuare il casting di un oggetto istanziato tramite una classe caricata col proprio class loader, in quanto si otterrebbe un errore: solo il class loader è a conoscenza della nuova classe (si veda per una spiegazione più dettagliata l'articolo citato). Si deve quindi utilizzare una classe (o un'interfaccia) che sia già caricata col class loader primordiale, cioè una classe comune anche al programma in esecuzione, da cui la classe caricata col class loader derivi (o nel caso di un'interfaccia, che sia implementata da tale classe). In questo caso si è scelto la soluzione di un'interfaccia (AgentLoader) implementata dalla classe effettivamente caricata col class loader personalizzato (DefaultAgentLoader).

ClassBytesLoader è una classe di utilità che fornisce un metodo (statico) per effettuare il caricamento dei dati di un file .class presente in una directory specificata nel CLASSPATH; tale classe non viene presentata esplicitamente.

L'AgentClassLoader

Non ci dilungheremo più di tanto sull'implementazione del class loader in quanto le tecniche di personalizzazione sono già state trattate negli articoli precedenti.

Questo tipo di class loader, a differenza di quello presentato in [1] non carica le classi direttamente dalla rete: le classi, quando necessarie vengono cercate in una tabella interna al class loader; si tratta di una tabella hash, in cui la chiave è il nome della classe, ed il valore è un array di byte. Saranno le classi che utilizzano tale class loader ad inserire i dati all'interno di tale tabella tramite il metodo addClassBytes (si veda a tal proposito il comportamento dell'AgentHandler).

Quindi il class loader per effettuare il caricamento delle classi, le cercherà all'interno di tale tabella.

  public Class loadClass(String className) throws ClassNotFoundException {
    return (loadClass(className, true));
  }

  protected synchronized Class loadClass(String className, boolean resolveIt)
    throws ClassNotFoundException {
      return loadClass( className, resolveIt, false ) ;
  }

  protected synchronized Class loadClass(String className, boolean resolveIt,
        boolean force )
    throws ClassNotFoundException {
      Class result;
      byte  classData[];

      PrintMessage("--- caricamento : "" + className );

      /* vediamo prima se è nella cache */
      result = findLoadedClass(className);
      if (result != null) {
	      PrintMessage("        --- recuperata dalla cache." );
        return result;
      }

      if ( ! force ) {
        try {
          result = super.findSystemClass(className);
          PrintMessage("        --- caricata dal file system locale");
          return result;
        } catch (ClassNotFoundException e) {
          PrintMessage("        --- non e' una classe di sistema");
        }
      }

      if ( className.startsWith( "java." ) ) {
        // this is dangerous
        throw new SecurityException( className ) ;
      }

      /* proviamo a controllare nella nostra tabella */
      classData = getClassBytes(className);
      if (classData != null) {
        PrintMessage("        --- recupero dei bytes di " + className );
      } else {
        ClassNotFoundException e = new ClassNotFoundException( className ) ;
        e.printStackTrace() ;
        throw e ;
      }

      /* parsing */
      result = defineClass(className, classData, 0, classData.length);
      if (result == null) {
        throw new ClassFormatError();
      }

      if (resolveIt) {
        resolveClass(result);
      }

      PrintMessage("            --- nuova classe caricata : " + className );
      return result;
  }

  public synchronized Class forceLoadClass(String className)
    throws ClassNotFoundException {
      return loadClass( className, true, true ) ;
  }

In effetti l'AgentHandler utilizza il metodo forceLoadClass in quanto se si cercasse la classe DefaultAgentLoader nel file system locale, la ricerca avrebbe successo, ed effettivamente tale classe risulterebbe caricata tramite il class loader primordiale, e non tramite quello personalizzato, che non è proprio quello che vogliamo noi. Per il resto l'implementazione non si discosta molto da quelle viste nei precedenti articoli.

Quindi per caricare una classe con questo class loader si deve:

  1. aggiornare la tabella del classe loader
  2. richiedere il caricamento della classe (eventualmente forzandolo tramite il metodo forceLoadClass, invece di utilizzare il classico loadClass)

L'AgentLoader

L'interfaccia AgentLoader definisce i seguenti metodi:

public interface AgentLoader {
  public void start() ;
  public void setServer( AgentServer server ) ;
  public void setInputStream( InputStream istream ) ;
  public void setOutputStream( OutputStream ostream ) ;
}

Tramite questi metodi (a parte la possibilità di mandare in esecuzione l'AgentLoader che implementerà questa interfaccia) si potranno settare alcuni dati membro, che saranno presenti nella classe che implementerà l'interfaccia. Un'alternativa poteva essere quella di passare tali parametri direttamente al costruttore di una classe base (invece di un'interfaccia), sfruttando gli oggetti Constructor presenti nelle reflection API, ma in questo modo si lascia più libertà nell'implementazione.

In particolare all'AgentLoader vengono passati oltre ad un riferimento al server (non utilizzato in questa implementazione), i due stream da cui leggere l'agente. Notare che ancora una volta si tratta di stream generici, il che non vincola il loader a dover leggere l'agente direttamente dalla rete, ma da qualsiasi altro dispositivo gestibile tramite stream (ad esempio un file in cui è stato memorizzato temporaneamente un agente).

Vediamo adesso cosa esegue il DefaultAgentLoader:

  public void start() {
    try {
      ObjectInputStream objIStream = new ObjectInputStream( iStream ) ;
      ObjectOutputStream objOStream = new ObjectOutputStream( oStream ) ;
      AgentClassLoader cl = (AgentClassLoader)(getClass().getClassLoader()) ;
      // recupera l'AgentPack
      AgentPacket pack = (AgentPacket)(objIStream.readObject()) ;
      objOStream.writeObject( new Boolean( true ) ) ;
      System.out.println( "Ricevuto agente." ) ;
      // aggiorna il database del class loader
      cl.addClassBytes( pack.className, pack.ClassBytes ) ;
      startAgent(pack) ;
    } catch ( ClassNotFoundException e ) {
      e.printStackTrace() ;
    } catch ( IOException ioe ) {
      ioe.printStackTrace() ;
    } catch ( AgentException ae ) {
      ae.printStackTrace() ;
    }
  }

Dopo aver costruito degli ObjectStream sugli stream passati (viene utilizzata la serializzazione per inviare e ricevere un agente), viene recuperato il "pacchetto" contenente l'agente. Questo pacchetto è rappresentato da una classe (fondamentalmente una struttura):

public class AgentPacket implements java.io.Serializable {
    public String className ; // nome della classe
    public byte ClassBytes[] ; // bytes della classe
    public byte AgentBytes[] ; // contenuto dell'agente

    public AgentPacket( Agent P, byte[] clBytes ) {
      className = P.getClass().getName() ;
      ClassBytes = clBytes ;
      try {
        ByteArrayOutputStream byteOStream = new ByteArrayOutputStream();
        ObjectOutputStream objOStream = new ObjectOutputStream( byteOStream );
        objOStream.writeObject( P ) ;
        AgentBytes = byteOStream.toByteArray() ;
      } catch ( IOException e ) {
        e.printStackTrace() ;
      }
    }
...

Come si vede l'agente stesso viene trasformato in un array di byte e memorizzato in questa struttura insieme alla classe che lo rappresenta. E' stato necessario memorizzare l'agente sotto forma di array di byte (si sarebbe potuto benissimo serializzare l'agente stesso) in quanto appena il pacchetto viene deserializzato sarebbe cercata subito la classe dell'agente che ancora non è stata ottenuta, in quanto anche lei presente nel pacchetto, e quindi si otterrebbe una bella ClassNotFoundException. Essendo invece contenuto in un array di byte, durante la deserializzazione non è necessaria nessuna informazione sulla classe dell'agente, anche perché è visto semplicemente come una sequenza di bit.

In questo modo l'AgentLoader può deserializzare tranquillamente il pacchetto ed ottenere il contenuto della classe dell'agente (membro ClassBytes). In questo modo la tabella del class loader può essere aggiornata coi dati opportuni. Appena letto il pacchetto si spedisce un segnale di "agente ricevuto" al mittente, per confermare che tutto è andato a buon fine.

Vediamo come viene effettivamente fatto partire l'agente ricevuto:

  protected void startAgent( AgentPacket pack ) 
      throws ClassNotFoundException, IOException, AgentException  {
    ByteArrayInputStream byteIStream = new ByteArrayInputStream( pack.AgentBytes ) ;
    ObjectInputStream objIStream = new ObjectInputStream( byteIStream ) ;
    // questo provocherà il caricamento della classe dell'agente
    Agent agent = (Agent)objIStream.readObject() ;
    System.out.println( "Esecuzione agente : "" + agent.AgentName() ) ;
    agent.onArrival() ;
  }

Viene semplicemente deserializzato l'agente: l'array di byte (in cui era stato memorizzato l'agente) viene riconvertito nell'agente stesso. Notare come questa operazione è svolta con una semplicità estrema grazie alla serializzazione e agli stream: chi l'ha detto che si deve per forza deserializzare da un file o dalla rete? si può tranquillamente attaccare un ObjectStream ad un ByteArrayStream.

Al momento di questa deserializzazione avverrà il caricamento della classe dell'agente:

  1. Il deserializzatore si accorge che è necessario caricare in memoria la classe dell'oggetto che viene deserializzato.
  2. Viene invocato automaticamente il metodo loadClass del class loader con cui è stata caricata la classe dell'oggetto attualmente in esecuzione (l'AgentLoader) cioè il nostro AgentClassLoader.
  3. Il class loader riuscirà a caricare la classe dell'agente in quanto la troverà nella propria tabella (aggiornata dall'AgentLoader).

Notare, a parte la semplicità della soluzione, che così non è necessario richiamare manualmente il metodo loadClass del class loader che tra l'altro avrebbe richiesto l'istanziamento manuale di un nuovo agente. In questo modo invece l'agente viene semplicemente ricostruito da quanto è stato spedito in rete, insieme a tutti i suoi dati, proprio come richiesto dalle definizioni standard di agenti mobili!

L'agente verrà in un certo senso "risvegliato" dall'ibernazione a cui era stato sottoposto quando era stato spedito in rete.

Purtroppo però Java non permette di salvare lo stato di un thread, nel senso che non è possibile, ovviamente per motivi di sicurezza, salvare il valore del program counter e lo stack delle chiamate di un thread. Quindi non è possibile far riprendere l'esecuzione all'agente direttamente da dove era stata interrotta. Siamo quindi in presenza di mobilità debole [7], che è l'unica messa a disposizione da Java.

Comunque si può ottenere un effetto simile stabilendo la convenzione che quando un agente viene riavviato sul sito remoto viene chiamato un metodo specifico, ad esempio onArrival della classe base degli agenti (questa è la soluzione utilizzata negli Aglets [4]).

La classe Agent

Come si sarà intuito questa rappresenta la classe base per gli agenti; si tratta di una classe astratta in quanto dovranno essere implementati alcuni metodi

// da definire nelle classi derivate
abstract protected void execute() throws AgentException ;
// chiamato all'arrivo su un sito remoto
abstract public void onArrival() throws AgentException ;

Nel package viene definita anche un classe base per le eccezioni generate dalle classi del package stesso. Non viene derivata nessuna particolare eccezione, in questa implementazione.

Vediamo come viene gestita la migrazione di un agente su un sito remoto:

	protected void migrate( String host, int port ) throws AgentException {
	  Socket socket ;
	  try {
	    PrintMessage( "Trasferimento su " + host + ":" + port ) ;
	    socket = new Socket( host, port ) ;
	    sendAgent( socket ) ;	    
	  } catch ( IOException e ) {
	    e.printStackTrace() ;
	  }
	}
	
	protected void sendAgent( Socket socket ) 
	    throws IOException, AgentException {
	  ObjectOutputStream objOStream = 
	    new ObjectOutputStream( socket.getOutputStream() ) ;
	  ObjectInputStream objIStream =
	    new ObjectInputStream( socket.getInputStream() ) ;
	  Boolean ok ;
	  try {
  	    objOStream.writeObject( new AgentPacket( this, getClassBytes() ) ) ;
  	    ok = (Boolean)(objIStream.readObject()) ;
  	    if ( ! ok.booleanValue() ) {
  	      throw new AgentException( "Migrazione fallita" ) ;
  	    }
	  } catch ( ClassNotFoundException e ) {
	    e.printStackTrace() ;
	  }
	}

Dopo aver stabilito una connessione col server, si spedisce l'AgentPacket contenente le informazioni sull'agente (contenuto dell'agente e i dati binari della classe dell'agente). vediamo come vengono recuperate le informazioni sulla classe dell'agente:

  final public byte[] getClassBytes() {
    return getClassBytes( getClass().getName() ) ;
  }
	
  final public byte[] getClassBytes( String className ) {
    ClassLoader defaultLoader=this.getClass().getClassLoader();
    AgentClassLoader classLoader;

    if ( ( defaultLoader != null ) && 
         (defaultLoader instanceof AgentClassLoader) )  {
      //cioè non sto utilizzando il class loader di default
    
      classLoader = (AgentClassLoader)(defaultLoader) ;
      byte[] ClassBytes = classLoader.getClassBytes( className ) ;
      if ( ClassBytes != null )
        return ClassBytes ;
    }
    
    try {			
      byte result[] = ClassBytesLoader.loadClassBytes(className) ;
      return result ;
    } catch ( FileNotFoundException n'of ) {
      nof.printStackTrace() ;
      return null ;
    } catch ( Exception e ) {
      e.printStackTrace() ;
    }
    return null ;
  }  

Si noterà che prima di cercare la classe nel file system locale, la si cerca nella tabella del class loader. E' proprio necessario? Certo che lo è: supponete di spedire un agente sul sito A, e che dopo aver eseguito alcune operazioni l'agente, autonomamente migri dal sito A al sito B; quando l'agente è sul sito A non troverà le informazioni sulla sua classe sul file system di A, ma le troverà nella tabella del class loader. Quando invece l'agente viene spedito per la prima volta sul sito A, il class loader sarà quello primordiale, quindi la ricerca andrà effettuata nel file system locale.

NOTA aggiunta in Agosto 2001: A tal proposito è importante notare che richiamando getClass().getClassLoader() dalla versione 1.2 di Java non si ottiene più null nel caso il class loader sia quello primordiale. E' quindi necessario effettuare un test con instanceof. Nella prima versione di questo articolo il test si limitava a testare che il class loader restituito fosse differente da null, e questo ovviamente causava una ClassCastException successiva quando eseguito con una JVM versione 1.2 o successiva. Ringrazio a tal proposito le varie persone che mi hanno fatto notare il problema e mi scuso per il ritardo con cui ho provveduto ad effettuare le dovute modifiche.
 

Un esempio

Vediamo adesso un semplice esempio di agente mobile, che deriva dalla classe Agent:

public class TestAgent extends Agent {
  protected String host ;
  protected int port ;
  // dati di prova che saranno spediti insieme all'agente
  protected String s1 ;
  protected int n1 ;

  protected void execute() throws AgentException {
    System.out.println( "Salve io l'agente " + AgentName() ) ;
    s1 = new String( "Stringa di prova" ) ;
    n1 = 100 ;
    System.out.println( "Queste sono due mie variabili : " +
      s1 + ", "" + n1 ) ;
    System.out.println( "Adesso migro su " + host + ":" + port ) ;
    migrate( host, port ) ;
    System.out.println( "Agente migrato" ) ;
  }
  
  public void onArrival() throws AgentException {
    System.out.println( "Salve, sono l'agente " + AgentName() + 
      " appena arrivato..." ) ;
    System.out.println( "Queste sono due mie variabili : " +
      s1 + ", "" + n1 ) ;
  }

Dopo che l'agente sarà migrato, ed una volta a destinazione, si potrà constatare effettivamente che i dati dell'agente conterranno gli stessi valori: l'agente è diventato mobile!

Per testare l'esempio basterà lanciare il server su un terminale, e lanciare quest'esempio su un altro terminale, possibilmente facendo sì che il server non riesca ad accedere al file TestAgent.class dal file system locale, per convincersi che effettivamente viene utilizzata la classe recuperata dalla rete (ad esempio si può mettere tale file in una directory non contenuta nel CLASSPATH).

Il server come sempre prende come parametro il numero di porta su cui rimarrà in ascolto di agenti in arrivo.

Conclusioni

Il package presentato non ha la pretesa di essere immediatamente utilizzabile per scopi professionali, ma può essere personalizzato per ottenere un framework effettivamente utilizzabile, e che magari si può avvicinare notevolmente a quello proposto dagli Aglets. Fondamentalmente il package è carente di meccanismi di sicurezza (che comunque possono essere facilmente aggiunti, come probabilmente vedremo in un prossimo articolo).

Il materiale qui presentato del resto si basa su una parte del framework realizzato in [8].

Nel prossimo articolo estenderemo tale package, anche perché così com'è è utilizzabile solo con agenti molto semplici. Il prossimo mese vedremo una soluzione su come risolvere questo inconveniente; quale inconveniente? Vi invito a scoprirlo :-) (attendo responsi)

A presto :-)

Lorenzo Bettini

 

Sorgenti (modificati in Agosto 2001)

agsrc.zip

 



Bibliografia

[1] Donato Cappetta, Lorenzo Bettini, Un NetworkClassLoader in Java, MokaByte Aprile 1998
[2] Fabrizio Giudici, Agenti mobili, Mokabyte Gennaio 1997 e Febbraio 1997
[3] General Magic, Telescript, http://www.genmagic.com/Telescript .
[4] IBM Aglets Workbench http://www.trl.ibm.co.jp/aglets .
[5] Lorenzo Bettini, Client Server in Java, MokaByte Dicembre 1997
[6] Lorenzo Bettini, Il class loader, MokaByte Marzo 1998
[7] Lorenzo Bettini, La programmazione distribuita in Java, MokaByte Novembre 1997
[8] Lorenzo Bettini, Progetto e realizzazione di un linguaggio di programmazione per codice mobile, tesi di Laurea in Scienze dell'Informazione, Aprile 1998, http://rap.dsi.unifi.it .