MokaByte
Numero 19 - Maggio 1998
|
|||
|
(I parte) |
||
Lorenzo Bettini |
|
||
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() {Per semplicità si sono riportati solo i metodi principali di tale classe.
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() ;
}
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() {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].
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() ;
}
}
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 {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.
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 ) ;
}
L'AgentLoader
L'interfaccia
AgentLoader
definisce i seguenti metodi:
public interface AgentLoader {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.
public void start() ;
public void setServer( AgentServer server ) ;
public void setInputStream( InputStream istream ) ;
public void setOutputStream( OutputStream ostream ) ;
}
Vediamo adesso cosa esegue il DefaultAgentLoader:
public void start() {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):
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() ;
}
}
public class AgentPacket implements java.io.Serializable {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.
public String className ; // nome della classe
public byte ClassBytes[] ; // bytes della classe
public byte AgentBytes[] ; // contenuto dell'agentepublic 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() ;
}
}
...
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 )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.
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() ;
}
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() {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.
return getClassBytes( getClass().getName() ) ;
}final public byte[] getClassBytes( String className ) {
AgentClassLoader classLoader =
(AgentClassLoader)(getClass().getClassLoader() ) ;if ( classLoader != null ) {
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 ;
}
Un esempio
Vediamo adesso
un semplice esempio di agente mobile, che deriva dalla classe Agent:
public class TestAgent extends Agent {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!
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 ) ;
}
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)
Sorgenti
clsrc.zip
[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 .
MokaByte Web 1998 - www.mokabyte.it MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it |