Россия - русский

Argentina
(Español)

Australia
(English)

Brasil
(Português)

Canada
(English)

Canada
(Français)

中国
(简体中文)

Colombia
(Español)

Deutschland
(Deutsch)

España
(Español)

France
(Français)

India
(English)

Italia
(Italiano)

日本
(日本語)

México
(Español)

Perú
(Español)

Россия
(Pусский)

United
Kingdom (English)

United
States (English)

Дополнительно…

Microsoft.com

Добро пожаловать Вход

MSDN Magazine

Home

Subscribe

Reader
Services

Community

Partner
Resources

Write Mobile Agents In .NET To Roam And
Interac...

We were unable to locate this content in
ru-ru.

Here is the same content in en-us.

Wandering Code

Write Mobile Agents In .NET To Roam And
Interact On Your Network

Matt Neely

This article discusses:

Introduction to mobile agents

Creating a simple mobile agent system

Mobile agent development patterns

Security implications and solutions

This article uses the following
technologies:
.NET Framework

Code
download available at: MobileAgents.exe
(145 KB)
Browse
the Code Online

Contents

Introduction
to Mobile Agents
A
Simple Mobile Agent System
Assembly
Resolution
The
Traveling Pattern
The
Task Pattern
The
Interactive Pattern
Advanced
Topics
Security
and Mobile Agents
Conclusion

Recently I had the opportunity to return to
school to complete a graduate degree. This experience made me realize two
important things: there are some cool ideas in academia that seem to never
find the light of day in the professional setting, and the academic world
at large is not yet very familiar with the Microsoft® .NET Framework. Thus was born my goal of
introducing lesser-known ideas to the professional masses while
introducing the .NET Framework to the academic world. One particular
concept has become the pivot point of this goal: mobile agents, a
distributed computing paradigm.

The term agent originates in artificial
intelligence and describes a logical entity that has some level of
autonomy within its environment or host. A mobile agent has the added
capability to move between hosts. In a computing context, a mobile agent
is a combined unit of data and code that can move between different
execution environments. This mobility gives mobile agents several
potential advantages: reduced network traffic, decentralization, increased
robustness and fault-tolerance, and an easy deployment scenario.

In this introduction to mobile agents, I'll
create an example mobile agent system and develop several mobile agent
applications, highlighting some of the problems that I encountered along
the way as well as several ideas for overcoming them. Lastly, I'll touch
on some advanced concepts that need to be addressed when creating a
serious mobile agent system.

Introduction to Mobile Agents

Figure 1 Traveling Agent
Hops Between Machines

There are three main design patterns for
mobile agents which can be combined for any implementation. These patterns
are named after the agent's primary purpose: traveling, task, and
interaction. A traveling agent is mainly concerned with location. A task
agent is used for processing (doing work or tasks). An interaction agent
collaborates or interacts with other agents

Figure 2 Task Agent
Allocates Tasks

An example of a traveling agent app could
perform operations control. An agent is sent out with a list of machines
on the local network it should traverse to inventory hardware and software
(see Figure 1). A task agent application (see
Figure 2) could be one that performs massive scientific
calculations. An agent can determine what hosts are available on the
network and subdivide its tasks accordingly, which essentially creates a
load-balancing, parallel-processing application. To understand an
interaction agent, think of a marketplace. In this environment, a buying
agent can interact with various selling agents to determine the best price
for some item (see Figure 3). The buying agent can then
purchase from the chosen selling agent.

Figure 3 Interaction
Agent

So what does .NET have to offer mobile
agents? First, having an object-oriented framework makes construction of a
mobile agent application much more intuitive. Next, there are built-in
services that facilitate the componentization and mobility of code, namely
object remoting and serialization. (Sending an object over the wire is a
snap with .NET. Name that object Agent and you're close to having a mobile
agent application.) In addition, threading and synchronization are
crucial, as each agent is supposedly autonomous. And finally, the security
features of the .NET Framework will be a great boon to the mobile agent
environment. As I'll discuss later, downloading and running unknown code
is far from a secure thing to do.

A Simple Mobile Agent System

Let me first say that my particular design of
a mobile agent system is not a standard, absolute, or ideal version. It is
merely one implementation of many. That disclaimer aside, I began my
mobile agent application as a solution in Visual Studio® with three projects: a server process console
application, a client process console application, and a common class
library to be used by the first two projects. In the class library, I
start out with two classes: Agent and AgentHost.

The Agent class is essentially going to be
the interface that all agents must adhere to so that the host knows how to
run them. Because I want to include some actual common functionality to
all agent subclasses, I'm making the Agent class an abstract class and not
merely an interface. The Agent class represents a mobile agent and so must
have at least two areas of functionality: executing code and moving. As
such, I gave the Agent class two brilliantly named methods: Run and Move.
The Run method is marked abstract and therefore must be overridden in
agent subclasses. Run will be invoked by an AgentHost once an Agent
arrives there. The second method, Move, contains the internal
communication code for this system, which I'll discuss later.

The AgentHost class (shown in Figure
4) needs only one method to start with to host an Agent class.
I've called it simply "HostAgent." It has a single parameter, agent, of
type Agent. When this method is invoked, it will call the Run method on
the Agent class.

Figure 4 AgentHost Class

Копировать код

public class AgentHost : MarshalByRefObject
{
public static void Initialize(int port, string objectUri)
{
//Create a new TcpChannel on the given port and register it
// with the runtime
TcpServerChannel c =
new TcpServerChannel("MyAgentHostChannel", port);
ChannelServices.RegisterChannel(c);

//Register the agent host type with the remoting runtime
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(AgentHost), objectUri, WellKnownObjectMode.SingleCall);
}

public void HostAgent(Agent agent)
{
agent.Run();
}
}

The communication mechanism that I'm using is
.NET remoting. However, it is important to keep in mind that a mobile
agent system can be created on top of any communication layer. Now that
the Windows Communication Foundation (formerly code-named "Indigo") is
available in beta, you might consider using that instead.

In order to implement the remoting
capability, I added a method to the AgentHost class called Initialize,
which will properly set up a TcpChannel on the given port with the given
uniform resource identifier (URI). It will then register the AgentHost
type with the remoting runtime so that the host will be externally
available.

The client-side code for communication (which
resides within the Move method of the Agent class) is even easier, thanks
to the Activator type. Activator has a GetObject method overload that
takes a URL. This method will return a proxy to an AgentHost object on the
server. The Agent uses the proxy to call the HostAgent method, giving
itself as the Agent parameter (see Figure 5).

Figure 5 Agent Class

Копировать код

[Serializable]
public abstract class Agent
{
public event System.EventHandler AgentMoved;

public Agent() { }

public abstract void Run();

public void Move(string urlOfHostToMoveTo)
{
AgentHost h = (AgentHost)Activator.GetObject(
typeof(AgentHost), urlOfHostToMoveTo);
h.HostAgent(this);
}
}

You can find the complete code for the server
process that will host the AgentHost class in Figure 6.
Also, the following code snippet shows the relevant client process code,
which references a yet-to-be-discussed agent subclass and sends it to a
host at a given remoting endpoint:

Копировать код

MyFirstAgent agent = new MyFirstAgent();
agent.Move("tcp://localhost:10000/MyAgentSample");

Figure 6 Agent Server Process

Копировать код

class AgentServer
{
static void Main(string[] args)
{
int port = 10000;
if (args.Length > 0) port = int.Parse(args[0]);

MobileAgents.AgentHost.Initialize(port, "MyAgentSample");
Console.Out.WriteLine("Press enter to stop...");
Console.In.ReadLine();
}
}

Now, the only thing that is missing is an
actual Agent implementation. The MyFirstAgent class (see Figure
7) gives a very simple implementation of a mobile agent. On
creation, it merely records the name of the process that it was created
in. When it runs, it gets the name of the process it now runs in. It
outputs the two of them to the console in a formatted string. Note that
any Agent subclass should also have the Serializable attribute in order
for the runtime to properly serialize it.

Figure 7 MyFirstAgent Class

Копировать код

[Serializable]
class MyFirstAgent : MobileAgents.Agent
{
string _startingProcess = string.Empty;

public MyFirstAgent()
{
using(Process p = Process.GetCurrentProcess())
_startingProcess = p.ProcessName;
}

public override void Run()
{
using(Process p = Process.GetCurrentProcess())
Console.WriteLine(
"I started in '{0}' but now am in '{1}'!",
_startingProcess, p.ProcessName);
}
}

In order to run this mobile agent system, you
need to start a host with the server process executable. This will
establish the system's communication to the outside world so that it can
begin to accept mobile agents for hosting. Once the host is ready, the
client process is run to create a MyFirstAgent instance and then move it
to the host.

Assembly Resolution

If you try to run this seemingly complete
example, you get an assembly load exception. The problem is that even
though the AgentHost only needs the Agent base class for compilation (and
that is in the same assembly), at run time it will also need to access the
assembly of the derived base types. Without this information, types can
not be correctly laid out in memory and any inheritance functionality
would break.

So the problem becomes, "how do I get the
client-side, custom-defined agent assembly's bits to the host in order for
correct .NET assembly resolution?" This problem can be broken into two
parts, assembly transfer and assembly management. Assembly transfer deals
with how you get the assemblies that define Agent subclasses into the
realm of the hosting AppDomain. The solution depends on the scope of your
mobile agent system and what you can assume. For example, your system may
be architected such that it knows of a file share, Web, or FTP site from
which to download the assemblies. Precluding the existence of a commonly
known area for assembly download, the client will have to hand over the
bits directly to the server. This is the approach I took in this example
as it minimizes any other dependency or deployment headache. However, it
also presents some security concerns, to be discussed later on in this
article.

To facilitate the transfer of assemblies on
the host side, I added two methods to the AgentHost class:
IsAssemblyInstalled and UploadAssembly. The first method takes an
assembly's full name ("mscorlib, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089") and returns a Boolean indicating whether
the assembly is installed on the system. The second method will actually
save the assembly's bits in a unique location for easy loading. These two
methods are closely linked to assembly management.

On the client side of the communication
channel, I altered the Agent's Move method to gather information about the
agent assembly's references (see Figure 8). If the agent
assembly is not installed on the host (determined by a call to the host's
IsAssemblyInstalled method) then the code will recursively walk the agent
assembly's dependency graph and track which assemblies need to be uploaded
to the host. For each assembly found, a call to IsAssemblyInstalled is
made. The assembly file will be uploaded (via the host's UploadAssembly
method) if IsAssemblyInstalled returns false. This process is simple with
the combined power of the System.Reflection and System.IO namespaces. (In
a robust system, care should be taken to not upload different versions of
core .NET runtime files. For example, if the Agent subclass depends on the
Visual Studio 2005 runtime, you should probably not try to upload and
install Visual Studio 2005 assemblies via this mechanism.)

Figure 8 Updated Agent with Reflection

Копировать код

[Serializable]
public abstract class Agent
{
public event System.EventHandler AgentMoved;

public Agent() { }
public abstract void Run();
public void Move(string urlOfHostToMoveTo)
{
AgentHost h = (AgentHost)Activator.GetObject (
typeof(AgentHost), urlOfHostToMoveTo);
TransferAgentAssembliesToHost(h);
h.HostAgent(this); // move to the host
}

private void TransferAgentAssembliesToHost(AgentHost h)
{
//Get the full name of the assembly this agent is defined in.
AssemblyName thisAssemblyName = GetType().Assembly.GetName();

//If this hasn't been installed yet, walk the
//assembly dependency graph to see what assemblies are needed
if (!h.IsAssemblyInstalled(thisAssemblyName.FullName))
{
Dictionary<string, AssemblyName> assembliesFound =
new Dictionary<string, AssemblyName>();
FindReferencedAssemblies(assembliesFound,
GetType().Assembly.GetName());
foreach (AssemblyName an in assembliesFound.Values)
{
if (!h.IsAssemblyInstalled(an.FullName))
h.UploadAssembly(an.FullName,
GetRawAssembly(Assembly.Load(an)));
}
}
}

private void FindReferencedAssemblies(
Dictionary<string, AssemblyName> assembliesFound,
AssemblyName assemblyToSearch)
{
//Check to see that we haven't encountered this assembly yet.
if (!assembliesFound.ContainsKey(assemblyToSearch.FullName))
{
assembliesFound.Add(assemblyToSearch.FullName,
assemblyToSearch);
AssemblyName[] references = Assembly.Load(
assemblyToSearch).GetReferencedAssemblies();
foreach (AssemblyName reference in references)
FindReferencedAssemblies(assembliesFound, reference);
}
}

...
}

Another thing to note is that this mechanism
does not allow for in-memory assemblies or unreferenced assemblies loaded
via reflection. Therefore, avoid such programming constructs in an agent
implementation. In fact, I would recommend denying agents the ability to
use most reflection APIs for security and robustness reasons. I'll discuss
security later in this article.

As mentioned before, assembly management is
the one sub-problem of assembly resolution. Now that the AgentHost has
been given the bits of the assemblies, what does it do with them? Again,
this depends on your implementation. If your system requires that you only
accept strong-named assemblies, then you can save the bits to disk and
install them into the Global Assembly Cache (GAC). However, there may be
circumstances where you do not want to do this. For example, one such
circumstance is if you need to use weakly named assemblies or else just
want greater control and flexibility. You might also want to totally
separate the downloaded assemblies from the rest of the system. Putting an
assembly in the GAC makes it globally accessible. This has considerable
robustness and security implications.

Another management possibility is to keep the
assemblies totally in memory. This can be a benefit to startup performance
and security (nothing is directly stored or accessed to disk) but once the
Application Domain is unloaded, all assemblies go with it.

If you create your own assembly management
infrastructure you will need to find a place to store assemblies. You will
also need to be able to resolve failed assembly loads to your assembly
store. To find a place to store the assemblies, I chose a directory
location that would be unique to my application. The base directory is
returned by a call to Environment.SpecialFolders with the
Environment.SpecialFolders.CommonApplicationData enum value as the
parameter. Subdirectories are then created with the company ("MSDN" in
this example), the application name, and the application version. This
should ensure that I'll have a unique segment of the file system to call
my own.

Now that I have a unique spot for storing
assemblies, there's another problem I need to address—the Windows® operating system and the .NET Framework
identify uniqueness differently. You can only have one file of any given
name in a directory. However, assembly uniqueness has four parts: name,
version, culture, and public key. This means that you'll have to store
your assemblies in a way that ensures this uniqueness. Since you're
essentially creating a local assembly cache, it makes sense to look at
what the GAC does. The GAC creates a file structure based around the four
uniquely identifying parts of the assembly. I merely copied this
structure, as shown in Figure 9. Now each assembly should
be located in a unique place that is easily discovered.

Figure 9 File Hierarchy
for an Assembly

There is still one last caveat, however. If
you do allow weakly named assemblies, it is still possible for there to be
identification problems. This is because weakly named assemblies do not
have a public key and therefore only have three attributes (name, version,
culture) to uniquely identify them. Given that the assemblies that the
host will be receiving all contain Agent implementations, it is a good
assumption that you'll see lots of assemblies named MyAgent.dll or the
like, all with the neutral culture and version 1.0.0.0. This name
collision (in addition to security issues, discussed later) is a good
reason to not support weakly named assemblies in a robust system.

In order to facilitate this assembly
management, I created a helper class appropriately called AssemblyManager
(see Figure 10). In its static constructor, it hooks into
the current AppDomain's AssemblyResolve event. This event will be fired
any time the .NET runtime is unable to load an assembly via established
methods. See How
the Runtime Locates Assemblies for information on how the common
language runtime (CLR) resolves assemblies. The ResolveEventArgs class is
the second parameter to this method (the first being the AppDomain object
that was unable to resolve the assembly). This class has a Name property
that gives the full name of the assembly the runtime couldn't find. Using
the AssemblyName(string) constructor, I can obtain the four parts I
mentioned earlier—name, version, culture, and public key token. Knowing
these allows me to construct a path to the given assembly and then load
it.

Figure 10 AssemblyManager

Копировать код

public class AssemblyManager
{
private static string _assemblyStoreBaseDir = string.Empty;

static AssemblyManager()
{
//Create root directory for agent assemblies
_assemblyStoreBaseDir = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.CommonApplicationData),
@"MSDN\MobileAgentsSample\v1.0");
if (!Directory.Exists(_assemblyStoreBaseDir))
Directory.CreateDirectory(_assemblyStoreBaseDir);

//Hook AppDomain for assembly resolution failures
AppDomain.CurrentDomain.AssemblyResolve +=
new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

private static Assembly CurrentDomain_AssemblyResolve(
object sender, ResolveEventArgs args)
{
string fileName, dirPath;
GetDirectoryAndFileForAssembly(args.Name,
out dirPath, out fileName);
string fullPath = Path.Combine(dirPath, fileName);
if (File.Exists(fullPath)) return Assembly.LoadFrom(fullPath);
return null;
}

private static void GetDirectoryAndFileForAssembly(
string assemblyFullName, out string directoryName,
out string fileName)
{
fileName = string.Empty;
directoryName = string.Empty;

//Parse the assembly name
string name, version, culture, publicKeyToken;
ParseAssemblyFullName(assemblyFullName, out name, out version,
out culture, out publicKeyToken);

fileName = name + ".dll";
string relPath = string.Format(@"{0}\{1}\{2}\{3}", name,
culture, version, publicKeyToken);
directoryName = Path.Combine(_assemblyStoreBaseDir, relPath);
}

private static void ParseAssemblyFullName(string assemblyFullName,
out string name, out string version, out string culture,
out string publicKeyToken)
{
name = version = culture = publicKeyToken = string.Empty;
AssemblyName an = new AssemblyName(assemblyFullName);
name = an.Name;
version = an.Version.ToString();
culture = an.CultureInfo.NativeName;
publicKeyToken = ConvertBytesToHexString(an.GetPublicKeyToken());
}

private static string ConvertBytesToHexString(byte[] bits)
{
StringBuilder sb = new StringBuilder(bits.Length*2);
foreach (byte b in bits) sb.AppendFormat("{0:X2}", b);
return sb.ToString();
}

internal static void SaveAssemblyBits(string assemblyFullName,
byte[] assemblyBits)
{
//Get the directory this assembly should go in.
string fileName, dirPath;
GetDirectoryAndFileForAssembly(assemblyFullName, out dirPath,
out fileName);

if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);

//Create the assembly file if it doesn't already exist.
string filePath = Path.Combine(dirPath, fileName);
if (!File.Exists(filePath))
{
using(FileStream fs = File.OpenWrite(filePath))
fs.Write(assemblyBits, 0, assemblyBits.Length);
}
}

internal static bool IsAssemblyInstalled(string assemblyFullName)
{
try
{
AssemblyName an = new AssemblyName(assemblyFullName);
Assembly asm = Assembly.Load(an);
if (asm != null) return true;
}
catch (FileNotFoundException) { }
return false;
}
}

The AssemblyManager class also has two
internal methods that are called by the AgentHost class to facilitate
assembly transfer: IsAssemblyInstalled and SaveAssemblyBits. The
IsAssemblyInstalled method will simply attempt to load the assembly given
the full name passed in as the single parameter. If a failure occurs, the
assembly must not be installed yet. The second method will save the
assembly's bits to the appropriate location by again parsing the full name
of the assembly.

The Traveling Pattern

To exemplify a traveling agent, I wrote a
simple scenario where an agent would have an itinerary and would go to
each host listed in it. Once at each host, it will merely output a message
on the host process' console saying that it has arrived (in lieu of actual
work) and then move to the next host. The implementation is straight-
forward. I create a class MyTravelingAgent (see Figure
11) derived from Agent. I give it a private
System.Collections.Queue<string> class, _destinations, for its
itinerary. I then add another method, AddDestination, which will add a URL
to the queue. The implementation of the Run override method merely outputs
to the console, sleeps a bit, dequeues the next URL from the queue, and
then moves there.

Figure 11 MyTravelingAgent

Копировать код

[Serializable]
class MyTravelingAgent : MobileAgents.Agent
{
Queue<string> _destinations = new Queue<string>();
public void AddDestination(string url) { _destinations.Enqueue(url); }

protected override void Run()
{
Console.WriteLine("I'm here now: {0}", DateTime.Now);
System.Threading.Thread.Sleep(5000);
if (_destinations.Count > 0)
{
string nextDestination = _destinations.Dequeue();
this.Move(nextDestination);
}
}
}

When an agent moves to a new location, it has
a synchronous connection to the remote host and won't relinquish it until
the Agent is moved and finishes executing. In a traveling agent pattern,
part of the execution involves moving to another host to execute there.
This operation also performs synchronously. Thus, the originating call to
Agent.Move waits for the traveling agent to move to its final destination
and then finishes executing. Needless to say, this is not a good design.
One could easily imagine a traveling agent that has no final destination
but has a continual route that it travels performing useful work.

Although there are ways to address this via
Remoting, I want a protocol-agnostic solution. The System.Threading
namespace offers an easy workaround in the form of ThreadPool. I'll use
the ThreadPool.QueueUserWorkItem static method to forward the incoming
Agent to another thread which will then execute it. This action allows the
caller to return immediately:

Копировать код

public void HostAgent(Agent agent)
{
ThreadPool.QueueUserWorkItem(this.RunAgent, agent);
}

private void RunAgent(object context)
{
((Agent)context).Run();
}

The Task Pattern

The task pattern-based agent application will
use a type of task pattern known as the parent-child pattern. In this
scenario, one agent is the parent or controller agent and it spawns or
communicates with child agents that do its bidding. My implementation of
this pattern merely creates a parent agent and tells it on which hosts to
perform work. When run, the parent agent will create a child agent for
each host to visit and then send the child agent there. Each child agent
will then perform some valuable work, like enforcing strong access control
lists (ACLs) on all file shares or clearing the Recycle Bin. My child
agent will merely list the system's logical drives (with a call to
Directory.GetLogicalDrives) and again output the results to the host's
console (see Figure 12).

Figure 12 Creating Parent and Child Agents

Копировать код

[Serializable]
class MyParentTaskAgent : MobileAgents.Agent
{
List<string> _hostsToCheck = new List<string>();

public void AddHost(string url) { _hostsToCheck.Add(url); }

protected override void Run()
{
foreach (string hostUrl in _hostsToCheck)
{
AgentProxy proxy = Agent.CreateAgent(
typeof(MyChildTaskAgent));
proxy.Move(hostUrl);
}
}
}

[Serializable]
class MyChildTaskAgent : MobileAgents.Agent
{
protected override void Run()
{
foreach (string drive in Directory.GetLogicalDrives())
{
Console.WriteLine("Found drive: '{0}'", drive);
}
}
}

The Interactive Pattern

The interactive agent application consists of
selling and buying agents. Sellers will set up shop on a host. The buying
agent will then visit the shops that it knows about, looking for the best
deal for some particular item. For this application I created two Agent
subclasses: MySellingAgent and MyBuyingAgent.

MySellingAgent is not too complex. When a
selling agent gets to a host to set up shop, it will register itself with
a "registry," so to speak. This registry is really a static dictionary
that will store the agent's ID (a simple integer in this scenario) mapping
to the selling agent instance itself. MySellingAgent has a static
dictionary, _sellers, a static constructor that initializes the
dictionary, and two public static methods for buyers to find seller
instances: GetSellersOnHost and GetSellerById. The first will return an
array of MySellingAgent objects, the second will return a single instance
of a MySellingAgent object.Generic Interaction Mechanisms

I'm sure that many of you see another
shortcoming of the interactive buyer-seller scenario. That is, the buyer
needs compile-time knowledge of the seller. Unless you code all agents
involved in an interaction, you can't control this. If you can't control
it, a generic mechanism needs to exist to allow for discovery and use of
other agents' services. The AgentProxy class I described gets a step
closer as you can invoke methods without compile-time knowledge. However,
there is no discovery mechanism to see what services or functionality an
agent provides.

Although it is fun to reinvent the wheel, you
can take advantage of some protocols that currently exist for this
functionality, for example, Universal Description, Discovery, and
Integration (UDDI), Web Services Description Language (WSDL), and SOAP
(see http://www.w3c.org/ for
descriptions of these protocols). I can envision a world where each host
acts as a UDDI server giving agents the ability to find other agents to
use their services and communicate. Once an agent is found, WSDL can be
used to dynamically discover the communication protocols and SOAP can be
used for the actual communication. This essentially makes these mobile
agents mobile Web services. That is a powerful, albeit complex,
paradigm.

On the instance level, the selling agent has
an ID (again, merely an integer) and a dictionary of items for sale. The
dictionary maps the item's name to an internal class ItemForSale, which
holds the item's name and price:

Копировать код

[Serializable]
public class ItemForSale
{
public ItemForSale(string name, int price)
{
this.ItemName = name;
this.ItemPrice = price;
}

public string ItemName = string.Empty;
public int ItemPrice = 0;
}

The selling agent's instance constructor
merely copies the parameters to member variables. Three public methods
exist for interaction with a buyer: IsItemForSale, GetItemPrice, and
BuyItem. In the overridden Run method, the seller registers itself on the
host by adding itself to the static _sellers dictionary. It then waits to
be called by a buying agent.

MyBuyingAgent, shown in Figure
13, is a bit more complex as it needs to maintain more state. The
buying agent is given the name of an item to buy, a value for the maximum
amount it can spend for that item, and an array of host URLs that the
agent needs to visit in order to find the item at the lowest
price.

Figure 13 MyBuyingAgent

Копировать код

[Serializable]
class MyBuyingAgent : MobileAgents.Agent
{
...

public MyBuyingAgent(
string itemToBuy, int maximumPriceForItem, string[] urlsToVisit)
{
this._itemToBuy = itemToBuy;
this._maxPriceForItem = maximumPriceForItem;
foreach (string url in urlsToVisit) _sitesToVisit.Enqueue(url);
}

protected override void Run()
{
Console.WriteLine("Buying agent is here.");
System.Threading.Thread.Sleep(2000);

if (this._currentHost == this._winnerHost) BuyFromWinner();
else
{
FindItem();
if (_sitesToVisit.Count == 0)
this.GoToMarketplace(this._winnerHost);
else
{
string nextHost = this._sitesToVisit.Dequeue();
this.GoToMarketplace(nextHost);
}
}
}

private void BuyFromWinner()
{
MySellingAgent seller =
MySellingAgent.GetSellerById(this._winnerId);
if (seller != null)
{
seller.BuyItem(this._itemToBuy);
Console.WriteLine(
"We bought '{0}' from seller {1} at {2} for ${3}!",
_itemToBuy, _winnerId, _winnerHost, _winnerPrice);
}
}

private void FindItem()
{
MySellingAgent[] sellers = MySellingAgent.GetSellersOnHost();
foreach (MySellingAgent seller in sellers)
{
if (seller.IsItemForSale(this._itemToBuy))
{
int sellersPrice = seller.GetItemPrice(this._itemToBuy);
if (sellersPrice <= this._maxPriceForItem)
{
if (_winnerId == -1 || sellersPrice < _winnerPrice)
{
this._winnerHost = this._currentHost;
this._winnerId = seller.Id;
this._winnerPrice = sellersPrice;
}
}
}
}
}

public void GoToMarketplace(string hostUrl)
{
this._currentHost = hostUrl;
this.Move(this._currentHost);
}
}

As the buying agent travels, it will need to
enumerate each seller on each host, determine if that seller has the item
desired, and then find the item's price. Since the agent wants to find the
best price, it will have to track which seller it is going to buy from. I
call this the winning seller. The agent will have to store the location of
the winning seller, the ID of the winning seller, and the price the
winning seller is offering for the desired item.

Finally, the buying agent will need one more
piece of information—its current location. Since there is currently no
facility in the AgentHost to give this information, I'll have to cheat.
Instead of directly calling the base class's Move method directly, I
created a wrapper function called GoToMarketplace. This will set a member
variable to the URL of the host that it is about to move to and then it
will call Move:

Копировать код

public void GoToMarketplace(string hostUrl)
{
this._currentHost = hostUrl;
this.Move(this._currentHost);
}

When the agent appears at the destination host,
it will have its current host member set to where it is located.

The logic in the overridden Run method is as
follows. If the current host is the same as the winning host, you have
already determined that you are going to buy from a seller here. If so,
get that seller by calling the static MySellingAgent.GetSellerById method.
The object instance returned can now be invoked, calling its BuyItem
method. If the current host is not the winning host, the agent will try
looking for it on the local host. After this I check for any more host
URLs to move to. If there are none, then the agent will move to the
winning host to purchase the item. If there are more host URLs, the agent
simply moves to the next host and continues.

Advanced Topics

Developers creating a mobile agent system
need to manage local agent object references. As I'm sure some of you have
noticed, merely calling the Move method doesn't do anything to the local
object instance. It still exists. It can be invoked and run without the
knowledge or consent of its clone that you just sent across the network.
Just keeping a reference to the object will keep it in memory and prevent
it from being garbage collected. In effect, the supposedly autonomous
agent is at the mercy of anyone who has a reference to it.

Most agent implementations (especially in
managed environments like that provided by the .NET Framework) use an
agent proxy and never give out direct references to the agents themselves.
A possible way to create such a proxy system is to implement an agent
factory pattern. Instead of returning an agent, it will return a proxy to
the agent. By using a basic eventing mechanism, all proxies to an agent
can be notified when that agent moves. Once notified, they can ensure that
no one can use the agent from that point on. Furthermore, the class
implementing the proxy can use a WeakReference object to allow for greater
agent object independence. (See WeakReference
Class for a description of the WeakReference class.) Figure
14 shows the simple AgentProxy implementation based on the code
download available from the MSDN®Magazine Web site. The creation and use
of an agent would then look like this:

Копировать код

AgentProxy proxy = Agent.CreateAgent(typeof(MyTravelingAgent));
proxy.InvokeMethod("AddDestination",
"tcp://localhost:10000/MyAgentSample");
proxy.InvokeMethod("AddDestination",
"tcp://localhost:10002/MyAgentSample");
proxy.Move("tcp://localhost:10001/MyAgentSample");

Figure 14 AgentProxy Class

Копировать код

[Serializable]
public class AgentProxy
{
private WeakReference _agent = null;
private bool _agentLeft = false;

internal AgentProxy(Agent agentToProxy)
{
_agent = new WeakReference(agentToProxy);
agentToProxy.AgentMoved += agentToProxy_AgentMoved;
}

public void Move(string hostUrl)
{
if (_agent.IsAlive && !_agentLeft)
{
Agent agent = (Agent)_agent.Target;
agent.Move(hostUrl);
}
else throw new Exception("The agent no longer exists here.");
}

public object InvokeMethod(string methodName,
params object[] methodArguments)
{
if (_agent.IsAlive && !_agentLeft)
{
Agent a = (Agent)_agent.Target;
Type agentType = a.GetType();
object retVal = agentType.InvokeMember(methodName,
BindingFlags.Public | BindingFlags.InvokeMethod |
BindingFlags.Instance, null, a, methodArguments);
return retVal;
}
else throw new Exception("The agent no longer exists here.");
}

private void agentToProxy_AgentMoved(object sender, EventArgs e)
{
_agentLeft = true;
_agent.Target = null;
}
}

For bonus points, some implementations of
agent systems also use proxies as a location-agnostic way of interfacing
with agents. With this design, if some object has a proxy to agent A on
machine X and A moves to machine Y, the user of the proxy doesn't care. It
can call to agent A's proxy as before and the proxy takes care of all the
details, making sure that A (now on Y) gets the message.

Unfortunately, this object reference problem
is not merely due to external handles to the agent. One of the
shortcomings of building an agent system on the .NET Framework is the
inability to stop thread execution, store stack frames and registers, and
then truly resume execution on the destination host. Because of this,
there still exists the potential for an agent to misuse itself. If an
agent makes a call to this.Move but then continues processing, it is
violating the intention for one agent to exist in only one place at one
time. Therefore, agent implementers need to think of a call to this.Move
as if it is the final word in processing (exempting object cleanup, of
course).

Agents should also have some internal
mechanism to make sure that during race conditions they are not moved
twice by accident. This goes mostly for the host side. By giving each
agent instance a unique identifier, a host can add rules to make sure
agents are not moved to it multiple times.

Another topic for advanced systems is the
support given by the host. As seen in the buying agent example, there was
no means for an agent to know where it was unless I engineered an ugly
solution. Shouldn't the host itself provide this information? What
services should the host give executing agents? Maybe the host will
allocate file or database space. Perhaps the host will provide persistence
services for long-running agents. The more services a host provides, the
more interaction between agent and host, the more complexities arise. A
production system will need more careful design than the samples given
here.

The code samples available in the download
provide a simple context class, HostContext, that each agent can utilize
by simply accessing the static property HostContext.Current. The
HostContext in the code exposes only the current host location, but other
services can be added here to give agents a more functional playground in
which to execute. (For details on using the AgentProxy class to invoke
methods without compile-time knowledge, see the sidebar "Generic
Interaction Mechanisms.")

Security and Mobile Agents

When I describe the mobile agent paradigm to
people, their first reaction is usually that it sounds like a virus.
Surely, allowing the download, installation, and execution of unknown code
has significant security implications. A thorough understanding of
security issues and a comprehensive threat model should be a minimum
requirement in any production system.

I'll briefly discuss some different threats
unique to a mobile agent system and how .NET facilities can help mitigate
the risks. First though, I must make the disclaimer that the threats
described here may or may not apply (nor are the threats I describe the
only ones that exist), based on the particular function of the mobile
agent system designed. For example, the threats for a publicly available
interactive mobile agent system greatly differ from an internal,
task-based mobile agent system. Don't forget that threat model. To
determine the threats inherent to a mobile agent system, you need to look
at the protagonists. As such, the system needs to be secure from malicious
clients, agents, and hosts. (The downloadable code implements several
security enhancements.)

The first possible threat is a malicious
client attempting to harm an established host. Here I am defining a client
as any program that communicates with a host via its outward-facing
interface. A lot of the mitigation for this threat depends on your
communication protocol. Will you allow anonymous calls? Will you
authenticate? How? Securing the communication protocol is beyond the scope
of this article, but it is very important and should be thoroughly
analyzed during the design phase. I recommend a protocol that can both
authenticate and encrypt the requests. Doing so will hinder many types of
attacks on the host. This is where a framework like that provided by
Windows Communication Foundation comes in extremely handy.

As for the host itself, a good rule of thumb
in any security design is to reduce the attack surface, so be very careful
of what functionality you choose to expose to the outside world (see Attack
Surface: Mitigate Security Risks by Minimizing the Code You Expose to
Untrusted Users for more information about attack surface reduction).
The .NET Framework 2.0 has a new facility called Transparency, which can
be used in external-facing code to always run with the caller's
permissions (see Are
You in the Know? Find Out What's New with Code Access Security in the .NET
Framework 2.0).

The AgentHost class presented here has three
externally visible methods: IsAssemblyInstalled, UploadAssembly, and
HostAgent. Although it might seem innocuous, IsAssemblyInstalled could
pose a security risk if it gives information away to a potential hacker.
For example, if there is a known security issue in a particular version of
a core .NET assembly, malicious code merely has to query the host to see
if that assembly (and thus, vulnerability) is available for attack.
Furthermore, the current implementation attempts to see if an assembly
exists by loading it. If an assembly that poses a security risk is loaded,
it is loaded into the host's application domain. So much for isolation.
The code download contains a slightly different solution where the host
creates a separate AppDomain for the sole purpose of loading assemblies
during the call to IsAssemblyInstalled.

UploadAssembly needs caution because it is
how a malicious user would upload malicious code onto the host machine. In
the mobile agent system this article presents, the assembly bits are saved
to disk at a known location and then loaded later if assembly resolution
fails. Because the assemblies are not loaded directly into memory, you can
delay the problem of uploading malicious agents. That is, you can delay
this problem only until the call to HostAgent. However, once an assembly
is uploaded and saved to storage, it is possible to inspect it for
security purposes. Here you can enforce such rules as requiring a strong
name. Strong-named assemblies help reduce risk in several ways, such as
proving that the assembly hasn't be tampered with post build.

Of course, you should only trust the builder
within certain limitations—you shouldn't trust an assembly just because it
has a strong name. Finally, remember that assemblies that are loaded from
a local hard disk are full-trust assemblies by default. I'll address that
in a moment.

The HostAgent method is where execution of
the uploaded agent code will begin, which brings me to a discussion of
agent-host threats. When an agent is to be hosted for the first time, the
CLR looks for the agent's assembly. Because I store the assemblies in an
obscure location, default assembly resolution will fail. It is then up to
the AppDomain.AssemblyResolve event handler to load the appropriate
assembly. It would be a very good idea to place those assemblies in a
sandboxed environment before you load and use them.

In the code download, I have bolstered the
security of the system by creating separate application domains to
separate agents from the host domain and other agents. I chose to allow
the agent to specify the name of the application it is a part of. Each
application will have its own app domain. This architecture is more
suitable for interactive agent scenarios where agents from different
sources can mingle more freely. You can segregate agents however you want.
Your scenario may prompt you to launch an application domain for each
agent type or even each agent instance that is hosted. The built-in
separation given with the AppDomain construct allows you to unload these
assemblies if you either determine they are a threat or if activity wanes
and you need to reclaim some memory.

For creating an AppDomain, the .NET Framework
2.0 has some new APIs that allow you to specify the code access security
(CAS) policy that you desire. New AppDomain.CreateDomain overloads allow
you to pass in the default Evidence, PermissionSet, and the names of the
full-trust assemblies for that new application domain. If you choose to go
this route, I recommend you read Shawn Farkas's blog at blogs.msdn.com/shawnfa on
creating custom AppDomainManagers and HostSecurityManager subclasses as
well as his recent MSDN Magazine article concerning hosting
add-ins at Do
You Trust It? Discover Techniques for Safely Hosting Untrusted Add-Ins
with the .NET Framework 2.0.

In the download code, I took another
approach. My approach alters the CAS policy for the local machine. Using
the SecurityManager class, I added a new CodeGroup with a restricted
permission set (derived from the Internet-named permission set, then
altered). The membership condition for this CodeGroup is a URL condition
that points to the base file location where the assemblies were saved.
This is set up and checked every time an agent AppDomain is created. And
the best part is that the settings are now machine-wide. If any other
process loads these assemblies they will have the same restrictive
permission set. (See Code
Access Security for more information on CAS in the CLR.)

Finally, I'll touch on the threat of a
malicious host towards your agent. If you send data in the form of an
agent to a host, that host has full control and use of the agent. So I'd
recommend not storing things like credit card or social security numbers
in them. Since you can't control what a malicious host might do to your
agent, the best course of action would be to prevent your agent from going
to such a host. Again, your communication protocol could take this on with
mutual authentication. Remember, security is based on trust. If you don't
trust a host, stay away from it.

Conclusion

There are many ways to engineer a networked
application. Mobile agents have their uses and their pros and cons. The
autonomous and mobile nature of mobile agents can lead to reduced network
traffic, decentralization, increased robustness and fault-tolerance, and
easy deployment. Now that you know a bit of what mobile agents can do,
let's look at these advantages.

A mobile agent system has the capability of
reducing network traffic by taking the processing local to the resource
being used. If you are doing a text search of files across the network,
you can dramatically improve performance by doing the search locally and
then returning only the results.

A mobile agent system can be decentralized
for increased robustness. Since the mobile agent contains code, you can
implant into it any fault-tolerance and error mechanisms you want. Think
of searching text files across the network again. If the machine that
originally dispatched the mobile agents to the remote hosts crashes,
processing can continue. The mobile agents can even camp out on remote
hosts until they can determine that it is safe to return. It is also
possible to survive a denial-of-service attack this way.

Deployment is made easier since mobile agents
essentially deploy themselves. The only prerequisite is that an agent host
is installed and configured on that machine. Once that condition is met,
you're open for business.

Unfortunately, mobile agents do have a
downside. They present a paradigm that is different from other development
paradigms and can be more difficult to comprehend. Also, security is a
much bigger issue with the download and execution of code. Search for more
information about mobile agents on MSN® and
look at the various implementations that currently exist. Then look at how
many even attempt to tackle the issue of security—most don't. Downloading
and executing unknown code is not a good way to control security.

I believe that mobile agents are a very
useful paradigm that should receive more attention than they have been
getting in the realm of academia. The features that are provided by the
.NET Framework should allow mobile agents to proliferate to the full
extent of their capabilities.

Matt Neely is a Software Design
Engineer at Microsoft where he spends most of his time looking busy when
the manager walks by. He enjoys playing the guitar and spending time with
his wife, four children, three cats, and a
puppy.

Управление
профилем | Юридическая
информация | Бюллетень
MSDN | Свяжитесь
с нами
© 2009
Microsoft Corporation. Все права защищены. Условия
использования | Товарные
знаки | Конфиденциальность

MSDN
Magazine Home
Current Issue Back
Issues
Topics
Columns Code
Downloads HTML
Help Files
RSS