Chapter 3: The Example Application
The idea of intelligent computing agents has been around for so long that, like Object-Orientation, it now means different things to different people. For the purposes of this book, an agent is an intelligent computing entity, that propagates itself across the network with little to no assistance from the user who dispatched it, executing its function on every machine it encounters, and reporting its results back to the user ... executing on multiple machines in parallel and doing it all without requiring a continuous connection to the user.
The original implementation of online computing was a wildly exaggerated client-server model: a dumb terminal logged onto a multi-user server. All the meaningful computing activity took place on the server. It had to be that way, since dumb terminals were incapable of doing any computing for themselves. If we compare that model with what happens on the Web today, they’re not all that different. The user ‘logs on’ to a server, and his ‘terminal’ displays whatever the site tells it to. We can switch servers very easily, via hyperlinks, and occasionally the static text will be livened up by a form, an audio clip, or even a cgi-bin script, but the meaningful computing activity still occurs almost exclusively on the server. Were we still using dumb terminals, this is all we could hope for, but in reality, this model vastly underutilizes the computing resources available on any Web-capable PC.
The first crop of Java applets hasn’t improved much on that model. The central server sends a Java class to the workstation, where it then executes. At that point, out of all the interconnected machines on the net, you have two of them working efficiently together, in parallel, to satisfy the needs of a single user. An improvement, to be sure, but not much.
What about the hundreds of thousands of other machines that are connected to the net? Why aren’t they helping with the task? In truth, there’s only one reason - lack of a viable distributed operating system. While it would be a gross overstatement to call the agent system a distributed operating system, it does aim to accomplish some of the same goals - to replicate a task across multiple CPUs in parallel in order to gain better performance. In our case, the better performance is not better speed (the goal of most distributed OSes) but MORE results, for example, more hits on a file search.
Searching for a file is actually a perfect example of the limitations of the one-to-one multiprocessing available via the Web today. Many sites that serve files via ftp allow you to search for a file. If you have some idea which site serves the file, you’re very likely to find it without much ado, by hitting that server and doing a one-to-one query there. But what if you hit server A and the file is really on server B. A and B are physically and logically connected via the net, but the query on server A will not find the file on server B because there is no mechanism to move the query from A to B.
That is the basic premise behind the agent system: to get that query from server A to server B. The user, via his Web browser, picks an agent from a list of available agents, configures it with some set of arguments and dispatches it to do its work on each machine in a limitless network of interconnected machines. The work that the agent does isn’t really important to us. It could be anything from a simple, and free, file search, to a complicated, and expensive commercial database query. We don’t really care. The point of the agent system is to provide a mechanism through which agents of unlimited complexity can traverse the net doing their jobs.
Given that functional description, our system splits easily into three components, or classes: the user interface (class AgentLauncher) that runs on the user's Web browser allowing him to pick an agent, the server (class AgentServer) which enables the agent to run on a particular machine, and the agent itself (class Agent) which must travel over the network and execute on all the different servers.
The AgentLauncher, an applet that runs within a users Web browser, is the user interface to the agent system. The AgentLauncher has the following tasks to perform:
Choose an agent from the list of Agents.
Dispatch that Agent onto the network.
Display the results of that Agent’s work.
Let’s start at the beginning and look at how the AgentLauncher is designed to perform these tasks. Figure 3.1 shows an AgentLauncher that has just started up.
Figure 3.1: An AgentLauncher with no running agents.
<ch3_fig1.tif>
We supply buttons that correspond rougly to these tasks - Pick, Dispatch, Stop, Kill and Results. We’ll talk more about Stop and Kill later. For now, let’s concentrate on the other three.
Choosing an Agent
To select an Agent to dispatch, the user pushes the Pick button. This brings up a dialog like the one in Figure 3.2.
Figure 3.2: The AgentLauncher displaying the list of available Agents.
<ch3_fig2.tif>
For each Agent, we display the class name of the Agent, and a descriptive comment. The user picks an Agent from the list box, and hits OK. We’ll talk more about how to build and use dialog boxes in Chapter 5. For now, it will suffice to say that displaying a dialog box is a fairly straightforward task for an Applet. From a design viewpoint, the really interesting question is “Where does this list of agents come from?”
Because the AgentLauncher must run within a Web browser, it suffers from two key limitations that shape its design: lack of file IO and severely limited network communication. First, some browsers[1] do not allow file IO from applets. Currently, there is no way around this. Second, some browsers only allow applets to open socket connections to the server from which the applet was originally loaded.
At first, this prohibition of file IO might not seem to be that big a deal. That’s because file IO is so pervasive in standalone applications that we don’t even recall using it. Where this becomes problematic for the agent system is in dispatching agents, and viewing results.
In order to dispatch, we need to pick one Agent from an existing, persistent list of Agents, and dispatch it to each AgentServer in an existing, persistent list of AgentServers.
Though we’d like to keep the list of Agents locally where they ultimately belong, it can’t be done because of the file I/O restriction. Thus, we need to keep the Agent list (and the Agents themselves) on a server, and have the server send us the list in a message (see AgentListMessage in Chapter 7).
The AgentLauncher requests the message with a QueryAgentListMessage (see Chapter 7) and the dispatching AgentServer responds with an AgentListMessage (see Chapter 7) which contains all the information about the Agents that can be dispatched from this server. This is the only instance of a query/response message algorithm in the agent system. All the other messages in the system are “thrown out there” with no requirement that anyone respond to them.
Having picked the Agent, the next step is to configure the Agent. In order to keep things simple, for now, we’ll save configuration for a little later, and jump straight to dispatching.
Dispatching an Agent
Once we’ve picked and configured the Agent, the next step is to dispatch it onto the network via the Dispatch button. From the AgentLauncher’s standpoint, all dispatching amounts to is sending a message to the AgentServer that contains the Agent name and some configuration information. The Agent class file itself resides on the AgentServer, again because of the Applet restriction on file I/O. The AgentServer that provides the list of available Agents, and receives the DispatchMessage is designated as the dispatching AgentServer. Dispatching from the AgentServer’s perspective is discussed in more detail later on.
Viewing Results
Our preferred communication solution would be to have all the agents dispatched by a user communicate their results directly back to that users machine, as in Figure 3.3.
Figure 3.3: Three instances of an Agent communicating directly with the user.
<ch3_fig3.tif>
Unfortunately, browsers often restrict applets network I/O in such a way that they can only open socket connections back to the server they were loaded from. What this does is force us to run all the Agent-to-AgentLauncher messages back through a server (the dispatching AgentServer) as in figure 3.4. This is inconvenient, to say the least, but not fatal.
Figure 3.4: Three instances of an Agent communicating with the user through a server.
<ch3_fig4.tif>
The AgentLauncher opens only one socket connection, to the AgentServer running on the same machine the applet was served from (the dispatching AgentServer). This is the only connection the browser allows us. We’ll talk about agent results in more detail later. For now, all we need to know is that Agents report their results in messages back to the AgentLauncher through the dispatching AgentServer.
Stopping an Agent
At some point, our dispatched Agent is no longer needed. While we will insert some simple, automatic mechanisms to prevent Agents from running forever, we also let the user stop an agent run manually by hitting the Stop button. This sends a message (KillMessage, see Chapter 7) out onto the network that says “Kill any instance of this Agent”.
Killing the AgentLauncher
For reasons we’ll discuss at more length in Chapters 4 and 6, once you start the AgentLauncher, it runs in the background forever, or until you exit the browser. For this reason, we provide a Kill button that terminates any AgentLauncher threads that may be running.
The agents we dispatch onto the net need a platform to run on. You can’t just connect to a random socket on the net, dump a Java class into it and expect it to run. This is where the AgentServer comes in. The AgentServer has one purpose in life - to run Agents. What this amounts to is receiving Java class files and configuration information over a network socket connection, instantiating the class and providing various ‘services’ to the new object (the instance of the received class). Because of the AgentLauncher file I/O restrictions we’ve discussed above, the dispatching AgentServer has another small task to perform - serving the list of Agents to interested AgentLaunchers. Thus, we can summarize the AgentServers responsibilities as:
* Dispatching Agents.
* Running Agents.
* Serving the list of dispatchable Agents to the AgentLauncher.
We’ll talk, in detail, about how AgentServers perform these tasks, but first, let’s look briefly at what kind of program an AgentServer is.
Application vs. Applet
The AgentServer is a standalone application. While standalone applications are not the focus of this book, we’ll talk about the AgentServer enough that writing standalone apps should be fairly easy to pick up. For design purposes, the main difference between standalone application development and applet development is the unrestricted access to network and file IO.
We already know that Applets must run within a browser. Standalone applications (as the name implies) can run without a browser. All a Java application needs is a Java interpreter. That interpreter is a program named java. The following command runs our standalone AgentServer application:
java -classpath \agent\classees\rel;\jdk\java\lib\classes.zip agent/Server/AgentServer
Figure 3.5 shows an AgentServer running.
<ch3_fig5.tif>
Notice how similar the AgentServer is in look and feel to Sun’s appletviewer. Notice also how limited a user interface the AgentServer has. In fact, the only user interaction it allows (besides the load-test function from Chapter 9) is the Exit function. We’ll look at little more closely at the unique requirements of standalone applications as we go along.
Dispatching Agents: The Agent Network
A key part of dispatching an Agent is knowing where to dispatch it to. We need to maintain a list of AgentServers to which Agents may be dispatched. The effect we want to achieve is to have an Agent started anywhere on the network eventually cover the whole network. What we come up with is a simple command the Agent can give an AgentServer that says “Redispatch me to all the AgentServers on your list”.
Thus, we can define an Agent Network as any set of AgentServers that have each other in their server lists. There is no system as to how AgentServers link to each other via their server lists. One AgentServer may appear in many different lists. While this makes list maintenance fairly easy, it also means AgentServers must take special precautions to make sure that they don’t run the same agent instance twice. Figure 3.6 shows the interlocking server lists of a simple agent network.
Figure 3.6 a simple agent network.
<ch3_fig6tif>
We have four AgentServers each with different, occasionally overlapping, server lists. Figure 3.7 shows how an Agent started at one of these servers would traverse the network.
Figure 3.7: An Agent traversing the network.
<ch3_fig7.tif>
The Agent starts at Katie and dispatches to Ginger and Rafi. From Ginger, it re-dispatches to Rafi, but Rafi recognizes it as one that's already run and dumps it. From Rafi, the Agent dispatched from Katie re-dispatches to Dudley. Once on Dudley, it redispatches back to Katie, which recognizes it as a duplicate and dumps it.
Notice that the Agent tries to load twice on both Katie and Rafi - an inefficency that a more sophisticated propagation strategy would hopefully. This is a very simplistic agent propagation strategy that requires hand-building and maintenance of the server lists. For our purposes, though, it will suffice.
Running Agents: The AgentContext Interface
The AgentServer has two pieces: the tasks it needs to perform simply to exist, and the environment it provides for Agents to run in. In this way, the AgentServer is very much like a Web browser. The browser does many tasks on its own, such as displaying HTML text, but it also provides an environment for applets to run in, a way for applets to talk to the browser, via the AppletContext interface (which we discuss at length in the next chapter). The AgentServer provides agents with similar context through the AgentContext interface.
Listing 3-1: The AgentContext interface.
package agent.Server;
import agent.*;
import java.lang.*;
import java.net.*;
/** The Agents interface into the environment in which it is
running. This is implemented only by the AgentServer. An Agent should
never run any server side functions unless it has a valid AgentContext.
@see agent.Agent
@version 1.0
@author John Rodley 12/1/1995
*/
public interface AgentContext {
/** This is called by the Agent to re-dispatch this Agent to all the servers in
the Servers list. The Agent can call this method ONLY ONCE! The server
has the option of whether or not to do anything. For security reasons,
some servers may not dispatch at all. The Agent can call dispatch at
any time. Some Agents may want to dispatch first, then run, others may
want to run first and then decide whether or not to dispatch based on
results.
@return true if the Agent was dispatched to ANY servers, false otherwise
*/
public boolean dispatch();
/** This is called by the Agent to write a String of result text to the
results HTML file. The Agent is responsible for creating syntactically
correct HTML. The server may, or may not, choose to actually write the
string to the file. Servers will be expected to implement checks on
result file size, and Agent run-time that will affect whether or not
they actually write the string.
@param HTMLString This is a string of text in HTML that will appear in
the results page for this Agent run.
@return true if the string was written to the output file, false
otherwise.
*/
public boolean writeOutput( String HTMLString );
/** This is called by the Agent to write a String of result text to the
results HTML file. The Agent is responsible for creating syntactically
correct HTML. The server may, or may not, choose to actually write the
string to the file. Servers will be expected to implement checks on
result file size, and Agent run-time that will affect whether or not
they actually write the string.
@param HTMLString This is a string of text in HTML that will appear in
the results page for this Agent run.
@return true if the string was written to the output file, false
otherwise.
*/
public boolean writeOutput( byte b[] );
/** Called by the Agent to get the URL of the file that writeOutput has
been writing our result strings to. Agent uses this to pass to
reportFinish.
@see reportFinish
@return the URL of the results file as a String. Result files must have Web-accessible
URLs.
*/
public String getResultsURL(String AgentID);
/** Called by the Agent to make the AgentServer tell the dispatching
AgentServer that this Agent has begun working. Receipt of this message
by the AgentLauncher should cause it to create an entry in its agent
list for this agent.
@param AgentID The ID of this agent
@see agent.Launcher
@see agent.Server
*/
public void reportStart( String AgentID );
/** Called by the Agent to make the AgentServer tell the dispatching
AgentServer that this Agent has finished work and has created the
following result file. Receipt of this message
by the AgentLauncher should cause it to update this agents entry in the agent
list.
@param AgentID The ID of this agent
@param url The URL of the results file. If null, there were no results.
@param price The price this AgentServer will charge to view the results
file.
@param comment Any comment the Agent might wish to make about this
result - including its size, running time ...
@see agent.Launcher
@see agent.Server
*/
public void reportFinish( String AgentID, String url, int price, String comment );
}
Through the AgentContext interface, an Agent can request the following services from an AgentServer:
* Report that it has started work back to its dispatching AgentServer (which passes it on to the AgentLauncher). This allows the AgentLauncher to maintain the viewable list of working Agents. Returns true if successful, false otherwise.
* Report that it has finished work back to its dispatching AgentServer (which passes it on to the AgentLauncher). This message includes the URL of the results file and allows the AgentLauncher to update its viewable list of working Agents. Clicking on this Agent will now point the browser at the results file URL.
* Write a string to the results file on the AgentServer host. The supplied string will appear in the results file that the AgentServer has created especially for THIS run of THIS agent. Returns false if the AgentServer chooses NOT to write the string to the results file.
* Get the URL that corresponds to the results file. Returns a URL corresponding to the file where all the strings sent to writeOutput were written.
* Re-dispatch this Agent to all the servers in the agent server list. Returns true if the Agent was dispatched to any other servers, false otherwise. This is how Agents hop across the Agent system network - by telling the server they’re running on to re-dispatch them.
Notice that we’ve eliminated all the communication functions from the Agent. All the communications back to the user to report status and give the URL of the results file are handled via calls to the AgentServer through the AgentContext interface. This allows, but does not require, us to implement a security system within the AgentServer that restricts Agents (whom we might not trust) from making network connections while preserving the ability of AgentServers (whom we do trust) to make those same connections.
Notice also that Agents access to the file system can be severely restricted. The AgentServer is only required to allow the Agent write access to a single file into which it can write its results. The Agents only access to that file is through the writeOutput calls, which allow the Agent to write strings, or byte arrays to a file on the AgentServer. The AgentServer, though, retains complete control over that file. The AgentServer can choose whether or not to actually write that string/byte array to the file. If the file is too big, or the Agent’s been running too long, or for whatever reason, the AgentServer can simply stop that output. This way, within the AgentServer’s SecurityManager we can eliminate entirely the Agent’s file I/O ability without crippling the AgentServer’s file I/O ability - something many system administrators might find attractive.
The final call available to Agents, dispatch, allows Agents to control when and whether they get re-dispatched to the servers in the current AgentServers list. One place where this control might be useful is in our file finding example. If an Agent finds the file on a particular server, there’s no reason to keep dispatching to other servers. After all, how many copies of that file do you need?
Of all the AgentServers in an Agent system, for any particular Agent run, there will be one server designated as the dispatching AgentServer. This AgentServer has the only direct connection to the user, and all messages from Agents in the field must go through this server. The dispatching AgentServer runs the exact same code as all the other AgentServers.
The reason we need a special dispatching AgentServer is because an AgentLauncher applet can only open network connections to the host from which it was loaded. If you load the HTML page identified by the URL:
http://www.channel1.com/users/ajrodley/index.html
which contains this applet tag:
<applet code=animator/JohnImage.class height=100 width=200>
then the AgentLauncher will only be able to make a network connection back to www.channel1.com. Thus, in order to talk to the network of AgentServers, an AgentLauncher loaded from www.channel1.com must funnel all communications through a server running on www.channel1.com. The AgentServer running on www.channel1.com will be the one that starts dispatching any agents chosen from this AgentLauncher. Thus, we designate the server running on www.channel1.com as the dispatching AgentServer.
The dispatching AgentServer deals with two message types (from the AgentLauncher) that other AgentServers do not:
Dispatch the class named <xxx> configured with <xxx> arguments from this dispatching AgentServer to all the AgentServers in the server list.
Get the list of Agents that are available for dispatching from this AgentServer.
When an Agent is dispatched throughout the network, the address <machine name:port number> of the dispatching AgentServer is included in the message. Other AgentServers then know where to connect to when they report results.
The dispatching AgentServer and the AgentLauncher keep an open connection. Should that connection disappear, then the dispatching AgentServer knows to cease serving that run of that Agent.
From a user’s perspective, the Agent is where the real utility of this system is embedded. Say, for instance, a user wishes to search the network for a file with a particular name. What has to happen is that someone needs to write a Java class that runs that search on one machine. The agent system takes care of getting that class onto all the machines on the network.
Running agents do not communicate with each other. The AgentLauncher configures the Agent, the dispatching AgentServer sends it out onto the network, and, via the AgentContext interface, the running Agents all report directly back to the dispatching AgentServer. Inter-agent communication is not prohibited by design. It is simply beyond the scope of what we’re trying to accomplish.
An Agent is neither an applet nor an application. It is simply a class that embodies all our knowledge about a particular task. In our file find example, for instance, all the information about searching for files - how to deal with directory structures and filename conventions - is embedded in the Agent. This also means that the Agent, rather than the AgentLauncher must get its configuration information from the user.
In order to write an Agent class, we have to answer the question: what generalizations can we make about agents? Surprisingly few. Any two agents are only guaranteed to have three things in common:
* They sub-class Object, as all Java classes do.
* They need to be configured.
* They run.
These requirements guide us pretty quickly to the design of a base class for Agents shown in Listing 3.2.
Listing 3.2: The Agent class.
package agent.Agent;
import java.lang.*;
import java.util.*;
import java.awt.*;
import agent.Agent.*;
// To catch the definition of AgentContext
import agent.Server.AgentContext;
/** The base class for all Agents in the agent system. An
almost purely abstract class, with the only bit of
implementation embodied in the setAgentContext method.
@version 1.0 12/1/1995
@author John Rodley
@see agent.AgentContext
@see agent.AgentServer
*/
public abstract class Agent extends Thread {
/** The Agent's interface to the AgentServer. If the Agent is
loaded on an AgentLauncher, as it will be when it is being
configured, this will be null.
*/
public AgentContext ac;
/** Called by the AgentServer to set an AgentContext for this
agent to use when reporting status or creating/reporting
results.
@param agentContext An object implementing the AgentContext
interface. Usually the AgentServer itself.
*/
public void setAgentContext(AgentContext a) { ac = a; }
/** Called by the AgentLauncher to get the arguments needed
for this run of this agent. Usually creates a Dialog to get
input from the browser user.
@param frame A Frame object, usually the Frame of the browser.
*/
public abstract void configure( Frame frame );
/** Called by the AgentLauncher to get the arguments that were
set via configure.
@return A Vector of Strings that will be passed back to the
Agent when the AgentServer instantiates it on a server. The
Strings can contain any data that might be meaningful to the
Agent. There is no limit on the number of arguments.
@see awt.Dialog
*/
public abstract Vector getArguments();
/** Called by the AgentServer to set the arguments this run of
this agent will use. The arguments obtained from the browser
user are stuffed into a portion of the agent load message. The
AgentServer extracts the args from the message, creates a
Vector of Strings and passes that Vector to the agent via this
call.
@param args A Vector of Strings that are exactly what the
AgentLauncher returned to the AgentLauncher from getArguments.
*/
public abstract void setArguments( Vector args );
}
Sub-classing Object obviously requires no work on our part.
The requirement to be runnable could have been satisfied either of two ways - implementing the Runnable interface, or sub-classing Thread. We choose to sub-class Thread, but could just as easily have done it the other way. The only reason to sub-class Thread is that we DON’T need to sub-class anything else. If we needed to sub-class anything else, also sub-classing Thread would not be possible because of Java’s single class inheritance (no multiple inheritance). In that case, we would have implemented Runnable.
The need to be configured is more difficult to satisfy, because it means that we must instantiate the Agent within the AgentLauncher. Whenever we load a class in Java, the class loader tries to resolve every class/interface reference in that class. Agent ‘references’ the AgentContext interface in the variable ac, and the method setAgentContext. Thus, the AgentContext interface definition (agent/Server/AgentContext.class) must be available to the AgentLauncher, even though an Agent instantiated under the AgentLauncher will never use it.
In actuality, an Agent instantiated under the AgentLauncher never gets started to run in its own thread because it doesn’t need to run asynchronously. We only instantiate Agent under AgentLauncher so we can get the list of arguments it needs to run via the configure and getArguments methods.
Consider a file-find agent that allows you to search for several files in one run. When the user picks this agent from the list, the AgentLauncher calls Agent.configure which can do anything it wants to get the arguments from the user. In most cases, we’d expect it to put up a dialog box that the user can fill out.
Our file-find example (detailed in chapter 9) puts up the dialog box of Figure 3.8.
Figure 3.8: The FileFinder Agent’s configuration dialog box.
<ch3_fig8.tif>
This dialog allows the user to enter 7 filenames for which to search. This dialog is constructed, displayed, and processed by an instance of Agent created by the AgentLauncher. It can be as complex, or as simple, as the particular Agent requires. If we did not embed this configuration method within the Agent itself, we’d have to do one of the following:
Modify the AgentLauncher to have individualized configuration dialogs for each of the available Agents.
or
Implement a generic configuration dialog through which all arguments for all agents had to be entered.
The first of these would make adding new Agents a nightmare, as the AgentLauncher would have to be modified for each new Agent, and then kept in sync with any Agent revisions. The second option would severely limit the descriptiveness of the configuration dialog, making the whole system seem vague and confusing. Encapsulating the knowledge of a task within a single class is one of the goals of O-O design. Spreading this knowledge among various classes as those two alternatives propose would amount to O-O heresy.
The ultimate result of configuring an Agent is a Vector (growable array) of Java Strings. The Agent can construct this Vector however it wants. Whatever the Vector looks like when the Agent gives it to the AgentLauncher (when the AgentLauncher invokes Agent.getArguments) at configuration time is exactly how it will look when the AgentServer passes it into the Agent at run-time (via Agent.setArguments). The AgentLauncher stores those arguments to be packaged with the agent’s class file when the agent is dispatched to the AgentServers.
It might seem that the Agent class really doesn’t do anything at all. That’s true. The Agent class is almost purely abstract. An Agent writer has to subclass it and add worthwhile content to make the Agent useful. The theory is that the agent system - AgentLauncher, AgentServers and Agent superclass - will be a medium in which agents can operate. This allows coders writing individual agents to concentrate on what the agent needs to accomplish on the target machine, without worrying about how the agent gets onto the target machine, or how to transport the results back to the user.
How Does An Agent Report Results?
Any particular agent will have one job to do, for example, running a database query. But how does the user know what the agent found, what the results of the query were? The agent could send every byte of information it finds back over the network connection to the AgentLauncher, where the AgentLauncher would format it for display to the user. This would work, but it could mean a huge amount of communication between AgentLauncher and Agent. We want to restrict that communication as much as possible.
It would also mean a great deal of work on the AgentLauncher to present the information in an attractive manner. In most cases, this would not be feasible, since, by design, the AgentLauncher knows absolutely nothing about the content of the data. The AgentLauncher doesn’t know whether the Agent is running an SQL query, searching for a file, or querying every system for users named Joe. All the knowledge of the Agent’s job is contained in the Agent itself, including how to best format the results.
The Web provides a perfect solution to this problem. As the Agent performs its task, it creates an HTML page describing the results. When it finishes, it sends a single message back to the AgentLauncher giving the URL of the results page. If the user wishes to see those results, he merely points the browser at that URL. Figure 3.10 shows the results HTML file created when our sample FileFinder agent searched for *.class on one of the AgentServers in my local network.
Figure 3.9: A FileFinder agent results file viewed with Netscape.
<ch3_fig9.tif>
The trick of result-reporting is how, within the AgentLauncher, to get the URL of the Agent’s results file to appear as a hyper-link in the users current HTML document. We take two approaches to this. One of them is to represent each running Agent with an animated icon. Clicking on the icon takes you to the results file for that Agent. The AgentServer to which the AgentLauncher is connected also creates its own results file that is simply a list of all the result URLs that have come back from the running Agents. Clicking on a “Results” button in the AgentLauncher takes the user to this page. From there he can also get to the various individual result pages by clicking on one of those links.
Now that we’ve looked at the system design, we can take a look at how one run, dispatching one agent to do one job, might work from the user’s point of view:
* user starts browser
* user points browser at HTML page containing the AgentLauncher applet
* the AgentLauncher connects to the AgentServer and gets the Agent list
* user chooses an Agent from the Agent list via the AgentLauncher
* user configures the Agent via the AgentLauncher
* user dispatches the Agent via the AgentLauncher
* the AgentLauncher sends the Agent name and configuration info to the dispatching AgentServer, which dispatches it to all the AgentServers that it knows about. Each AgentServer that runs the Agent does the same, thus covering the whole network.
* each of the AgentServers creates an instance of the Agent and start it running.
the user watches the results display as the Agent begins to run on all the different servers
* as the Agents report results, the user clicks on a particular result HTML page to view
* the result page HTML page appears on the browser
* ...
The user can switch back and forth between result pages and the AgentLauncher, while the AgentLauncher continues to run in the background, collecting new result URLs.
The agent system does not ship objects around the net. An object is an instance of a class, and the agent system ships classes. This is an important distinction.
The main reason for this is that there is no practical way to turn a Java object into a lump of bytes that can be pushed across a network connection. In C/C++ this would be trivial. Just cast a pointer to the object to char * and write it to the socket. You can’t do that in Java.
More difficult than the problem of how to “package” an object for shipment is the problem of translating context from the machine that the object was instantiated on, to the machine it intends to run on. In order to run an object (rather than a class) that was shipped across the net, Java would have to replicate the conditions that existed on the machine that instantiated the object. This would be wildly expensive, if possible at all. This is a pretty advanced topic that we’re glossing over lightly. Suffice to say that shipping objects rather than classes presents virtually insurmountable problems within Java.
The practical implications of shipping classes rather than objects appear in the communications protocol. Were we shipping objects around the net, we could configure the agent locally, then dispatch the whole object as one lump of bytes. As it is, we have to ship both the configuration data and the class file as distinct lumps of data and the configuration data has to be applied to the Agent every time it starts up on an AgentServer. It’s as if, instead of telling our plumber what we want done, and then sending him to the building, we’re sending him out to the site with sealed instructions that he can only open once he gets there.
It would not be possible to write an application like this without something like Java. Java’s simple network communication package makes it easy to implement our fairly simple protocol (see Chapter 7). Its simple class-loading mechanism allows us to load the classes we’ve passed over the net and run them as easily as we run local classes (see Chapter 9). The combination of Java’s Applet class and a Web browser provides as slick a user interface as you could possibly want.
But most important of all, Java provides portability. With Java we can write ONE AgentServer that will run on any machine. We can write ONE AgentLauncher that will run under any browser. And we can write ONE Agent that will run on any AgentServer machine. Once these three pieces are in place, anyone who wants to write a new Agent need only concentrate on what the Agent needs to do without worrying about how Agents traverse the net, or how they get their results to the user.
In this chapter, we’ve seen how the agent system splits into three logical pieces - the user interface, server and agent. We saw how those three pieces each correspond to a different type of Java class:
* A standalone Java application - the AgentServer.
* A Java applet - the AgentLauncher.
* A Java class that can operate as part of either an applet or application - the Agent.
We looked at the strengths and weaknesses of each piece and saw how the security restrictions imposed on applets complicate the design of the system. We saw how to take advantage of the Web by storing our results in hyper-linked HTML documents. Along the way, we saw some of the risks involved with such a system:
* Cancerous agent multiplication
* Malicious agents attacking system resources
* Buggy agents eating CPU cycles
and how the agent system tries to mitigate them. We explored the difference between object and classes, and why the agent system transports classes, rather than objects, over the network. Finally, we discussed the unique combination of features in Java - Web access, network communication, easy class-loading and portability - that make this type of system possible.
1. Can you think of any ‘business’ reason why a browser would disallow file IO access to Java applets?
2. How are agents similar to viruses? How do they differ?
3. Why don’t running Agents connect directly back to the user who dispatched them?
4. What prevents a running Agent from wiping out a servers file-system?
5. What is the difference between an object and a class? Why does the agent system pass classes instead of objects?
6. If the administrator of a server wanted to charge users a fee for viewing the results of Agent runs on its machine, how would he do it?
7. Can agents communicate with each other? What might prevent them?
8. Where do the results of an agents work appear? What physical system do these files reside on?
9. Discuss the strengths and weaknesses of the AgentServers server list scheme? How would you change it?
10. Assume an agent network with four servers. Using the server list scheme, what is the maximum number of times an Agent can be loaded across the network from one server to another? How many of these loads are redundant?