Статьи:







Рекламка:

Предупреждение

Данный материал не может распространяться без письменного разрешения его правообладателя, за исключением публикации на Веб-страницах, при условии целостности содержания материала и указания ссылки на исходную страницу, с которой была скопирована эта статья.

Создание мобильных агентов .NET для взаимодействия по сети

Создание мобильных агентов .NET для взаимодействия по сети




Автор: Мэт Нили
Источник: здесь



Недавно я воспользовался возможностью вернуться к учебе и получить ученую степень. Это позволило мне понять две важные вещи: что в академических кругах обсуждаются кое-какие перспективные идеи, которые никогда не всплывали в среде практикующих разработчиков, и что академический мир в целом пока еще довольно плохо знаком с Microsoft .NET Framework. Так у меня появилась цель представить программистам малоизвестные идеи и в то же время ознакомить преподавателей и ученых с .NET Framework. Решающим фактором, подтолкнувшим меня к этому, стала парадигма распределенных вычислений, лежащая в основе мобильных агентов (mobile agents).

Термин "агент" возник в области искусственного интеллекта и описывает логическую сущность, которая обладает некоторой автономностью в своей среде или на своем хосте. Мобильный агент может вдобавок перемещаться между хостами. В компьютерном контексте под мобильным агентом понимают сущность, которая объединяет данные и код и способна перемещаться между разными средами выполнения. Благодаря этому мобильные агенты могут дать ряд преимуществ, таких как уменьшение передаваемого по сети трафика, децентрализация, высокая надежность и отказоустойчивость, а также легкость развертывания.

В этой вводной статье я создам пример системы управления мобильными агентами и разработаю несколько мобильных агентов, указывая на проблемы, с которыми я сталкивался по ходу дела, а также некоторые идеи по поводу их решения. В конце статьи я затрону ряд более сложных концепций, которые нужно учитывать при создании серьезных систем управления мобильными агентами.

Введение в разработку мобильных агентов


Есть три основных проектировочных шаблона мобильных агентов, причем эти шаблоны можно комбинировать. Каждый шаблон называется в соответствии с основным предназначением агента, который может быть перемещаемым (traveling agent), агентом задач (task agent) и агентом взаимодействия (interaction agent). Основная характеристика перемещаемого агента - изменение его местонахождения. Агент задач используется для обработки (выполнения работы или задач). Агент третьего типа взаимодействует с другими агентами.

Приложение с перемещаемым агентом может, например, контролировать операции, отправляя в локальную сеть агент со списком компьютеров, которые он должен посетить, чтобы собрать информацию об аппаратных компонентах и ПО (рис. 1). Примером приложения с агентом задач (рис. 2) может служить программа, выполняющая большой объем вычислений. Агент задач в этом случае мог бы определять, какие хосты доступны в сети, и распределять между ними задачи; по сути, мы получили бы приложение с параллельной обработкой и балансировкой нагрузки. Чтобы понять работу агента взаимодействия, представьте рынок. В такой среде агент-покупатель может взаимодействовать с разными агентами-продавцами, выясняя лучшую цену на какой-то товар (рис. 3). После этого он может приобрести товар по этой цене у соответствующего агента-продавца.


Рис. 1. Перемещаемый агент передается с компьютера на компьютер




Рис. 2. Агент задач распределяет задачи




Рис. 3. Агент взаимодействия



Что же .NET может предложить мобильным агентам? Ну, во-первых, объектно-ориентированная платформа делает создание приложений с мобильными агентами интуитивно понятным. Во-вторых, в .NET реализованы интегрированные сервисы, облегчающие разделение кода на компоненты и повышающие его мобильность - я имею в виду удаленное взаимодействие объектов и сериализацию. (Переслать объект по сети средствами .NET проще простого. Назовите этот объект Agent, и вы получите почти готовое приложение с мобильным агентом.) Кроме того, очень большое значение имеют поддержка многопоточности и синхронизация, так как каждый агент, по идее, должен быть автономным. Наконец, в среде мобильных агентов определенно пригодятся реализованные в .NET Framework средства защиты. Как вы увидите, скачивать из Интернета и выполнять неизвестный код - дело весьма опасное.

Простая система управления мобильными агентами


Прежде всего должен сказать, что архитектура моей системы управления мобильными агентами не является ни стандартной, ни идеальной - это просто один вариант из многих. Работу над этой системой я начал с создания решения Visual Studio, включающего три проекта: консольное приложение серверного процесса, консольное приложение клиентского процесса и общую для этих двух проектов библиотеку классов. В этой библиотеке я создал два класса: Agent и AgentHost.

Класс Agent - это, по сути, интерфейс, которому должны соответствовать все агенты, чтобы хост знал, как их выполнять. Я хочу, чтобы все подклассы агентов поддерживали некую общую функциональность, поэтому я сделал Agent абстрактным классом, а не просто интерфейсом. Класс Agent представляет мобильный агент, а потому он должен поддерживать минимум две области функциональности: выполнение кода и перемещение. Ну а раз так, я включил в класс Agent два метода с именами Run и Move. Метод Run я сделал абстрактным, поэтому он должен быть переопределен в подклассах агентов. Этот метод будет однократно вызываться объектом AgentHost, когда агент будет прибывать на хост. Второй метод, Move, содержит внутренний коммуникационный код системы, который мы обсудим позднее.

Чтобы класс AgentHost мог быть хостом класса Agent, ему для начала нужен только один метод. Я назвал его просто HostAgent. Он принимает один параметр типа Agent. При вызове этот метод вызывает метод Run класса Agent.
public class AgentHost : MarshalByRefObject
{
    public static void Initialize(int port, string objectUri)
    {
        // Создаем новый TCP-канал для конкретного порта
        // и регистрируем его в исполняющей среде
        TcpServerChannel c =
            new TcpServerChannel("MyAgentHostChannel", port);
        ChannelServices.RegisterChannel(c);

        // Регистрируем тип хоста агента в удаленной
        // исполняющей среде
        RemotingConfiguration.RegisterWellKnownServiceType(
            typeof(AgentHost), objectUri,
            WellKnownObjectMode.SingleCall);
    }

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


В качестве коммуникационного механизма я использую .NET Remoting. Помните, однако, что систему управления мобильными агентами можно создать на основе любой коммуникационной технологии. Уже доступна бета-версия платформы Windows Communication Foundation (ранее известной под кодовым названием "Indigo"), и вы можете выбрать ее.

Для реализации удаленного взаимодействия я добавил в класс AgentHost метод Initialize, который создает TCP-канал для конкретного порта с конкретным универсальным идентификатором ресурса (uniform resource identifier, URI). Затем этот метод регистрирует тип AgentHost в исполняющей среде .NET Remoting, предоставляя доступ к хосту внешним сущностям.

Клиентский код коммуникаций (метод Move класса Agent) оказался еще проще благодаря типу Activator. Этот тип имеет метод GetObject, одна из перегруженных версий которого принимает URL. Данный метод возвращает серверному объекту AgentHost прокси. Объект Agent использует прокси для вызова метода HostAgent, передавая ему в качестве параметра Agent самого себя.
[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);
    }
}


Полный код серверного процесса, служащего для хостинга класса AgentHost, показан на листинге 1, а парой строк ниже приведен соответствующий код клиентского процесса. В этом фрагменте создается объект еще не рассмотренного нами подкласса агента и отправляется хосту по заданной конечной точке удаленного взаимодействия:
MyFirstAgent agent = new MyFirstAgent();
agent.Move("tcp://localhost:10000/MyAgentSample");


Листинг 1. Серверный процесс агента
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();
    }
}


Теперь нам не хватает только фактической реализации агента. Очень простой вариант мобильного агента реализован в классе MyFirstAgent. При создании объект MyFirstAgent просто записывает имя процесса, где он создается. При вызове метода Run он узнает имя процесса, в котором он работает в текущий момент, и выводит имена обоих процессов на консоль. Обратите внимание, что любой подкласс агента должен быть отмечен атрибутом Serializable, чтобы исполняющая среда могла его корректно сериализовать.
[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);
    }
}


Чтобы привести эту систему в действие, запустите хост посредством исполняемого файла серверного процесса. При этом будет сформирован коммуникационный канал системы с внешним миром, и она сможет начать принимать мобильные агенты для их хостинга. Как только хост будет готов, запустится клиентский процесс, создающий экземпляр MyFirstAgent и перемещающий его на хост.

Разрешение сборок


Если вы попытаетесь запустить этот, казалось бы, законченный пример, возникнет исключение при загрузке сборки. Проблема в том, что, хотя для компиляции класса AgentHost нужен лишь базовый класс Agent (находящийся в той же сборке), в период выполнения классу AgentHost требуется доступ и к сборке производных типов. Без этой информации система не может корректно разместить типы в памяти, и наследование не работает.

Таким образом, проблема сводится к тому, как на стороне хоста получить информацию о клиентской сборке агента для корректного разрешения .NET-сборок. Ее можно разделить на две части: передачу сборки и управление сборкой. Первая часть охватывает перенос сборок, в которых определены подклассы агентов, в домен приложения хоста. Решение этой задачи зависит от масштаба вашей системы управления мобильными агентами и имеющихся у вас средств. Например, система может загружать сборки из общего хранилища файлов, а также с Web- или FTP-сервера. Если общеизвестного ресурса, служащего для обмена сборками, нет, клиент должен будет отправлять данные непосредственно серверу. Для нашего примера я выбрал второй подход, чтобы свести к минимуму любые другие зависимости и проблемы при развертывании. Однако из-за этого возникают кое-какие проблемы с безопасностью, которые мы обсудим позже.

Чтобы облегчить передачу сборок на стороне хоста, я добавил в класс AgentHost два метода: IsAssemblyInstalled и UploadAssembly. Первый из них принимает полное имя сборки ("mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") и возвращает булево значение, показывающее, установлена ли в системе эта сборка. Второй метод сохраняет данные сборки в уникальном месте, чтобы их можно было легко загрузить. Оба метода тесно связаны с управлением сборками.

На клиентской стороне коммуникационного канала я так изменил метод Move агента, чтобы он собирал информацию о ссылках, имеющихся в сборке агента. Если сборка агента не установлена на хосте (что определяется вызовом метода IsAssemblyInstalled хоста), код рекурсивно обходит граф зависимостей сборки агента и узнает, какие сборки нужно передать хосту. Для каждой обнаруженной сборки вызывается метод IsAssemblyInstalled. Если он возвращает false, файл сборки передается хосту (методом UploadAssembly хоста). Как видите, благодаря объединению возможностей пространств имен System.Reflection и System.IO все получилось совсем несложным. (Если предъявляются повышенные требования к надежности системы, исключите загрузку из сети разных версий важнейших файлов исполняющей среды .NET. Так, если подкласс агента зависит от исполняющей среды Visual Studio 2005, вам, вероятно, не следует загружать из сети и устанавливать сборки Visual Studio 2005, используя этот механизм.)

Листинг 2. Обновленный агент, использующий отражение
[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); // перемещение на хост
    }

    private void TransferAgentAssembliesToHost(AgentHost h)
    {
        // Получаем полное имя сборки, где определен этот агент
        AssemblyName thisAssemblyName =
            GetType().Assembly.GetName();

        // Если эта сборка еще не установлена, обходим граф
        // ее зависимостей, чтобы узнать, какие сборки нужны
        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)
    {
        // Не встречалась ли нам уже эта сборка?
        if (!assembliesFound.ContainsKey(
            assemblyToSearch.FullName))
        {
            assembliesFound.Add(assemblyToSearch.FullName,
                assemblyToSearch);
            AssemblyName[] references = Assembly.Load(
                assemblyToSearch).GetReferencedAssemblies();
            foreach (AssemblyName reference in references)
                FindReferencedAssemblies(assembliesFound,
                    reference);
        }
    }
    ...
}


Заметьте, что этот механизм не позволяет загружать посредством отражения сборки, находящиеся в памяти, или сборки, на которые нет ссылок, поэтому не создавайте такие сборки при реализации агентов. На самом деле по причинам, связанным с безопасностью и надежностью, я советую заблокировать для агентов большинство API-интерфейсов отражения. Нюансы обеспечения безопасности мы обсудим потом.

Как уже говорилось, вторым аспектом проблемы разрешения сборок является управление сборками. Теперь у класса AgentHost есть нужные данные сборок, но что с ними делать? Опять же, это зависит от реализации. Если ваша система требует, чтобы вы принимали только сборки со строгими именами, можете сохранить сборки на диске и установить их в кэш глобальных сборок (Global Assembly Cache, GAC). Тем не менее, иногда делать это нежелательно - например, если вам нужно использовать сборки с нестрогими именами для получения большего контроля и гибкости. Кроме того, вам, возможно, захочется полностью отделить полученные сборки от остальных компонентов системы, тогда как сборки, помещенные в GAC, доступны глобально. Это ставит под угрозу надежность и безопасность системы.

Вы также можете держать сборки полностью в памяти. Это ускорит запуск приложения и усилит защиту (ведь на диске ничто не хранится), но как только домен приложений будет выгружен, вместе с ним исчезнут и все сборки.

Если вы решите создать собственную инфраструктуру управления сборками, вам придется найти место для их хранения. Кроме того, вы должны будете перенаправлять неудавшиеся запросы загрузки сборок к своему хранилищу. Сам я для хранения сборок выбрал каталог, уникальный для моего приложения. Для получения базового каталога хранилища я вызываю метод Environment.SpecialFolders, передавая ему в качестве параметра значение Environment.SpecialFolders.CommonApplicationData. После этого создаю подкаталоги с именами, соответствующими названию компании (в данном примере - "MSDN"), имени приложения и его версии. Это гарантирует, что в моем распоряжении будет уникальный сегмент файловой системы.

Создав уникальный каталог для хранения сборок, я должен решить другую проблему, которая состоит в том, что Windows и .NET Framework по-разному "понимают" уникальность. В каталоге может храниться лишь один файл с конкретным именем, тогда как уникальность сборки определяется по четырем элементам: имени, версии, культуре и открытому ключу. То есть вы должны будете хранить свои сборки так, чтобы гарантировать их уникальность. Так как вы, по сути, создаете кэш локальных сборок, имеет смысл узнать, что делает GAC. А он создает файловую структуру на основе четырех элементов, уникально идентифицирующих сборку. Как показано на рис. 4, я просто воспроизвел эту структуру. Теперь каждая сборка будет храниться в уникальном месте, найти которое будет совсем несложно.


Рис. 4. Иерархия каталогов, служащих для хранения сборок



Но остался один подвох. Если вы допускаете применение сборок с нестрогими именами, возможны проблемы с их идентификацией. Это объясняется тем, что сборки с нестрогими именами не имеют открытого ключа, т. е. их уникально идентифицируют только три атрибута (имя, версия, культура). Если учесть, что все сборки, которые будет получать хост, содержат реализации агентов, резонно предположить, что вы будете иметь дело с большим числом сборок, имеющих имена наподобие MyAgent.dll, нейтральную культуру и версию 1.0.0.0. Вытекающий из этого конфликт имен (а также проблемы с безопасностью, которые мы обсудим позднее) - веский довод в пользу отказа от поддержки сборок с нестрогими именами.

Чтобы облегчить управление сборками, я создал вспомогательный класс AssemblyManager. В его статическом конструкторе я подписываюсь на получение события AssemblyResolve текущего домена приложения. Это событие будет генерироваться всякий раз, когда исполняющая среда .NET не сможет загрузить сборку при помощи заданных методов. О том, как общеязыковая исполняющая среда (common language runtime, CLR) выполняет разрешение сборок, см. по ссылке. Вторым параметром обработчика события является класс ResolveEventArgs (первый параметр - объект AppDomain, который не смог выполнить разрешение сборки). Свойство Name этого класса позволяет узнать полное имя сборки, которую исполняющая среда не сумела найти. Используя конструктор AssemblyName(string), я могу получить четыре идентифицирующих сборку элемента, о которых мы уже говорили: имя, версию, культуру и маркер открытого ключа. Эта информация позволяет мне сформировать путь к конкретной сборке и загрузить ее.

Листинг 3. Класс AssemblyManager
public class AssemblyManager
{
    private static string _assemblyStoreBaseDir = string.Empty;

    static AssemblyManager()
    {
        // Создаем корневой каталог для сборок агентов
        _assemblyStoreBaseDir = Path.Combine(
            Environment.GetFolderPath(
            Environment.SpecialFolder.CommonApplicationData),
            @"MSDN\MobileAgentsSample\v1.0");
        if (!Directory.Exists(_assemblyStoreBaseDir))
            Directory.CreateDirectory(_assemblyStoreBaseDir);

        // Подключаем обработчик событий, уведомляющих
        // о неудачном разрешении сборок в домене приложения
        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;

        // Получаем элементы, идентифицирующие сборку
        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)
    {
        // Определяем каталог, где должна быть сохранена
        // данная сборка
        string fileName, dirPath;
        GetDirectoryAndFileForAssembly(assemblyFullName,
            out dirPath, out fileName);

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

        // Если файл сборки не существует, создаем его
        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;
    }
}


Класс AssemblyManager также включает два внутренних метода, помогающих классу AgentHost передавать сборки: IsAssemblyInstalled и SaveAssemblyBits. Метод IsAssemblyInstalled просто пытается загрузить сборку по ее полному имени, передаваемому в этот метод в качестве его единственного параметра. Если загрузка завершается неудачей, значит, сборка еще не установлена. Второй метод делит полное имя сборки на составные части и сохраняет данные сборки в подходящем месте.

Перемещаемый агент


Чтобы продемонстрировать использование перемещаемого агента, я разработал простой сценарий, в котором агент посещает каждый хост, приведенный в указанном ему списке. Прибывая на каждый хост, он просто выводит на консоль процесса хоста сообщение, извещающее о его прибытии, и перемещается на следующий хост. Реализация этого сценария проста. Я создал класс MyTravelingAgent, производный от Agent, и включил в него закрытый объект _destinations типа System.Collections.Queue<string>, содержащий информацию о хостах, которые агенту следует посетить. Затем реализовал в классе метод AddDestination, добавляющий URL хоста в очередь. В переопределенном методе Run агент просто выводит сообщение на консоль, приостанавливает работу на некоторое время и отправляется к следующему хосту, извлекая перед этим из очереди его URL.

Листинг 4. Класс 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);
        }
    }
}


Когда агент передается в новое место, в его распоряжении появляется синхронное соединение с удаленным хостом, которое не освобождается до завершения выполнения Agent. Одна из операций в процессе работы перемещаемого агента заключается в его перемещении для выполнения на новом хосте. Эта операция тоже выполняется синхронно. Таким образом, первоначальный вызов метода Agent.Move ожидает, пока перемещаемый агент не попадет в место назначения и не завершит выполнение. Очевидно, это неудачный вариант. Можно легко представить агент, который постоянно перемещается между хостами, выполняя полезную работу, но не имеет финального пункта назначения.

Это можно исправить при помощи .NET Remoting, но я хочу, чтобы решение не зависело от протокола, поэтому воспользуюсь классом ThreadPool из пространства имен System.Threading. Вызвав статический метод ThreadPool.QueueUserWorkItem, я пересылаю входящий Agent в другой поток, в котором он и выполняется. Это позволяет вызвавшему данный метод коду немедленно вернуть управление:
public void HostAgent(Agent agent)
{
    ThreadPool.QueueUserWorkItem(this.RunAgent, agent);
}
private void RunAgent(object context)
{
    ((Agent)context).Run();
}


Агент задач


В приложении с агентом задач я реализовал шаблон "родитель-потомки". В этом сценарии один агент является родительским (или агентом-контроллером), который порождает или взаимодействует с дочерними агентами, выполняющими свою работу. В своей реализации я просто создаю родительский агент и указываю ему, на каких хостах нужно выполнить работу. При запуске родительский агент создает и отправляет дочерний агент на каждый из этих хостов. Каждый дочерний агент может выполнять что-то полезное, например, применять списки управления доступом (access control lists, ACL) с более жесткими ограничениями ко всем общим хранилищам файлов или очищать "Корзину". Мой дочерний агент просто составляет список логических дисков системы (вызывая метод Directory.GetLogicalDrives) и выводит результаты на консоль хоста.

Листинг 5. Создание родительского и дочернего агентов
[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);
        }
    }
}


Агент взаимодействия


Мое приложение, основанное на этом шаблоне, включает агенты-продавцы и агенты-покупатели. Первые создают на хосте "магазин". Агент-покупатель посещает известные ему магазины в поисках лучшей цены на конкретный товар. Для этого приложения я унаследовал от класса Agent два подкласса: MySellingAgent и MyBuyingAgent.

Класс MySellingAgent довольно прост. Когда агент-продавец прибывает на хост для создания магазина, он регистрирует себя в "реестре". Этот реестр - на самом деле статический словарь, где хранится идентификатор агента (в данном сценарии простое целое число), сопоставленный с экземпляром агента-продавца. Класс MySellingAgent включает статический словарь _sellers, статический конструктор, инициализирующий этот словарь, и два открытых статических метода, позволяющих покупателям находить продавцов: GetSellersOnHost и GetSellerById. Первый из них возвращает массив объектов MySellingAgent, а второй - единственный экземпляр MySellingAgent.

На уровне экземпляра у агента есть идентификатор (также простое целое число) и словарь, определяющий продаваемые товары. Словарь сопоставляет название товара с внутренним классом ItemForSale, который содержит название товара и его цену:
[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;
}


Конструктор экземпляра агента-продавца просто копирует параметры в переменные-члены. Для взаимодействия с покупателем предназначены три открытых метода: IsItemForSale, GetItemPrice и BuyItem. В переопределенном методе Run продавец регистрируется на хосте, добавляя себя в статический словарь _sellers, и начинает ждать, когда его вызовет агент-покупатель.

Класс MyBuyingAgent чуть сложнее, так как он должен хранить больше данных о состоянии. Агенту-покупателю указывается название товара, который нужно купить, максимальная сумма, которую можно на это потратить, и массив адресов хостов, которые агенту следует посетить для поиска наименьшей цены на товар.

Листинг 6. Класс 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);
    }
}


Перемещаясь с хоста на хост, агент-покупатель должен перебрать всех продавцов на каждом хосте, определить, есть ли у продавца нужный товар, и узнать его цену. Так как агенту нужно найти самую выгодную цену, он должен отслеживать, у какого продавца он собирается купить товар. Такого продавца я называю продавцом-победителем (winning seller). Агент запоминает его местонахождение, идентификатор и предлагаемую им цену на нужный товар.

Наконец, агенту-покупателю нужен еще один элемент данных - его текущее местонахождение. Так как класс AgentHost пока не позволяет получить эту информацию, я был вынужден схитрить. Вместо прямого вызова метода Move базового класса я создал функцию-оболочку GoToMarketplace. Она присваивает переменной-члену URL хоста, на который собирается переместиться агент, и вызывает Move:
public void GoToMarketplace(string hostUrl)
{
    this._currentHost = hostUrl;
    this.Move(this._currentHost);
}


Когда агент появится на хосте назначения, переменная _currentHost будет содержать адрес этого хоста. Переопределенный метод Run работает следующим образом. Если текущий хост совпадает с хостом-победителем, значит, вы уже приняли решение купить товар здесь. Тогда продавец вызывает статический метод MySellingAgent.GetSellerById. После этого можно вызвать метод BuyItem возвращенного объекта. Если текущий хост не является победителем, агент попытается найти победителя на локальном хосте. После этого я проверяю, есть ли еще адреса хостов, которые следует посетить. Нет - агент перемещается на хост-победитель и приобретает товар, а иначе он просто перемещается на очередной хост.

Более сложные нюансы


Разработчикам, создающим систему управления мобильными агентами, нужно как-то управлять ссылками на локальные объекты агентов. Думаю, многие из вас заметили, что при простом вызове метода Move никакие операции над локальным объектом агента не выполняются. И все же он существует. Его можно активировать и запустить без согласия его клона, который вы только что отправили по сети, и без знаний об этом клоне. Просто храня ссылку на объект, вы сможете держать его в памяти и предотвратите его уничтожение сборщиком мусора. Автономный агент должен быть благодарен каждому, кто хранит ссылку на него.

Большинство реализаций агентов (особенно в управляемых средах вроде .NET Framework) используют прокси агента и никогда никому не предоставляет прямые ссылки на сами агенты. Один из способов создания такой системы прокси - реализация шаблона фабрики агентов, возвращающей не агент, а соответствующий ему прокси. Используя простой механизм событий, можно уведомлять все прокси агента о его перемещении. Получив уведомление, они могут гарантировать, что с этого момента больше никто не сможет обращаться к агенту. Для большей независимости объектов агентов в классе прокси можно воспользоваться объектом WeakReference (описание этого класса см. по ссылке). На листинге показан простой вариант реализации класса AgentProxy, основанный на коде, который можно скачать с сайта MSDN Magazine. Теперь создать и использовать агент можно так:
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");


Листинг 7. Класс AgentProxy
[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;
    }
}


В некоторых реализациях систем управления агентами прокси используются для взаимодействия с агентами без знания об их расположении. Если при такой архитектуре некий объект имеет на компьютере X прокси, соответствующий агенту A, и этот агент перемещается на компьютер Y, то пользователя прокси это не волнует. Он может вызывать прокси агента A, как и раньше, а прокси заботится обо всем сам, гарантируя, что агент A (находящийся уже на компьютере Y) получит сообщение.

К сожалению, проблема, о которой идет речь, связана не только с внешними ссылками на агент. Одно из следствий создания системы управления агентами на платформе .NET Framework - невозможность останавливать выполнение потоков, хранить фреймы стека и регистры и по-настоящему возобновлять выполнение на хосте назначения. Из-за этого сохраняется вероятность, что агент сделает что-то не то. Если агент вызовет метод this.Move и продолжит обработку, он нарушит принцип "в одном месте только один агент". Таким образом, разработчики агента должны рассматривать вызов this.Move как заключительный аккорд обработки (за исключением очистки объекта, конечно же).

Агенты также должны располагать каким-то внутренним механизмом, гарантирующим, что при гонках (races) они не будут случайно перемещены дважды. Это обеспечивается большей частью на стороне хоста. Назначив каждому экземпляру агента уникальный идентификатор, хост может определить правила, гарантирующие, что агент не прибудет на хост несколько раз.

Другой нюанс реализации более сложных систем связан с поддержкой агентов хостами. В примере с агентом-покупателем у агента не было никакой возможности узнать свое местонахождение, пока я не решил эту проблему, правда, весьма неуклюжим способом. Не должен ли хост сам предоставлять эту информацию? Какие сервисы хосту следует предлагать агентам? Возможно, хост должен выделять агенту файл или место в базе данных. Или предлагать сервисы хранения длительно выполняемым агентам. Чем больше сервисов предлагает хост, тем интенсивнее взаимодействие между ним и агентом, а вместе с этим растет и сложность системы. В отличие от моих примеров на практике нужно уделять больше внимания проектированию.

Код, который можно скачать к этой статье, включает простой класс контекста, HostContext. Каждый агент может использовать этот класс, просто обращаясь к его статическому свойству HostContext.Current. Класс HostContext позволяет узнать только местонахождение текущего хоста, но ничто не мешает реализовать в нем другие сервисы, обеспечивающие агентам дополнительную функциональность (о применении класса AgentProxy для вызова методов без наличия информации о них на этапе компиляции см. во врезке "Универсальные механизмы взаимодействия").

Мобильные агенты и безопасность


Когда я объясняю людям парадигму мобильных агентов, им, как правило, сначала кажется, что все это очень напоминает вирусы. Конечно, возможность скачать из сети, установить и выполнить неизвестный код создает угрозу безопасности. Таким образом, при разработке любой реальной системы необходимо глубоко разбираться в проблемах безопасности и создать исчерпывающую модель угроз.

Давайте вкратце обсудим некоторые угрозы, уникальные для систем управления мобильными агентами, и то, как средства .NET позволяют ослабить эти факторы риска. Но сначала я хочу сказать, что здесь описаны не все возможные угрозы и что описанные относятся не ко всем системам - это зависит от функциональности конкретной системы управления мобильными агентами. Так, факторы риска в общедоступной системе управления агентами взаимодействия существенно отличаются от факторов риска, характерных для внутренней системы управления агентами задач. Не забывайте о модели угроз. Для определения угроз, присущих системе управления мобильными агентами, нужно обратить внимание на их источники. Иными словами, систему нужно защищать от злонамеренных клиентов, агентов и хостов (в исходном коде, который можно скачать к этой статье, реализовано несколько усовершенствований системы безопасности).

Первая угроза состоит в том, что клиент может попытаться причинить вред хосту. В данном случае под клиентом я понимаю любую программу, которая взаимодействует с хостом через внешний интерфейс. Способ ослабления этой угрозы во многом зависит от вашего коммуникационного протокола. Допускаете ли вы анонимные вызовы? Выполняете ли аутентификацию? Как? Защиту коммуникационного протокола мы рассматривать не будем, но этот аспект очень важен, и его нужно тщательно анализировать на этапе проектирования системы. Я рекомендую выбирать протокол, позволяющий и аутентифицировать, и шифровать запросы, - так вы сможете защитить хост от атак многих типов. При решении этой задачи очень полезной может оказаться Windows Communication Foundation или другая подобная платформа.

Что касается самого хоста, то одно хорошее правило разработки любых средств безопасности гласит, что "поверхность атаки" должна быть как можно меньшей, поэтому к выбору функциональности, предоставляемой внешнему миру, нужно подходить с большой осторожностью (подробнее об уменьшении "поверхности атаки" см. по ссылке). В .NET Framework 2.0 реализована новая технология, которая называется прозрачностью (Transparency) и позволяет сделать так, чтобы доступный извне код всегда выполнялся с разрешениями вызвавшей его сущности.

Представленный здесь класс AgentHost включает три доступных извне метода: IsAssemblyInstalled, UploadAssembly и HostAgent. Метод IsAssemblyInstalled кажется безобидным, но на деле он может создать уязвимость, предоставив информацию потенциальному хакеру. Например, если в защите конкретной сборки .NET есть известная брешь, хакер может просто узнать у хоста, есть ли на нем эта сборка (и, следовательно, уязвимость). Кроме того, текущий вариант системы проверяет наличие сборки, просто загружая ее. А значит, сборка с брешью в защите загружается в домен приложения хоста. Без всякой изоляции. В коде, который можно скачать к этой статье, реализовано несколько иное решение: там хост создает отдельный AppDomain исключительно для загрузки сборок при вызове метода IsAssemblyInstalled.

Метод UploadAssembly требует внимания потому, что именно им воспользовался бы хакер для загрузки атакующего кода на хост. В системе, представленной в данной статье, сборка сохраняется на диске в известном месте и позднее загружается, если разрешение сборок завершается неудачей. Так как сборки не загружаются прямо в память, вы можете отсрочить решение проблемы загрузки агентов злоумышленника. Точнее говоря, вы можете отсрочить решение этой проблемы только до вызова HostAgent. Но как только сборка загружена на хост и сохранена, ее можно изучить на предмет безопасности. Вы, например, можете потребовать, чтобы у сборки было строгое имя. Такие сборки устраняют некоторые факторы риска, например хакер не сможет подменить сборку модифицированной версией.

Универсальные механизмы взаимодействия


Уверен, многие из вас заметили, что в сценарии "покупатель-продавец" у покупателя должна быть информация периода компиляции, относящаяся к продавцу. И такой информации у вас нет, если только вы сами не пишете все взаимодействующие агенты. А раз так, должен существовать какой-то универсальный механизм, позволяющий обнаруживать и использовать сервисы других агентов. Рассмотренный мной класс AgentProxy чуть ближе к цели, потому что вы можете вызывать методы, не имея сведений о них при компиляции, однако никакой механизм обнаружения предлагаемых агентом сервисов или функций в нем не реализован.

Изобретать колесо интересно, но все же для решения этой задачи можно воспользоваться уже существующими протоколами, например UDDI (Universal Description, Discovery, and Integration), WSDL (Web Services Description Language) и SOAP (описания этих протоколов см. на сайте www.w3c.org). На их основе вполне можно реализовать среду, где каждый хост выполнял бы функции сервера UDDI, позволяя агентам находить другие агенты для взаимодействия с ними и использования их сервисов. После обнаружения агента можно было бы при помощи WSDL динамически распознавать коммуникационные протоколы, а фактическое взаимодействие осуществлять по протоколу SOAP. По сути, такие мобильные агенты были бы мобильными Web-сервисами. Это очень мощная, хотя и сложная, парадигма.

Конечно, вы не должны полностью доверять создателю сборки, т. е. не доверяйте сборке только потому, что у нее строгое имя. И помните, что сборки, загружаемые с локального жесткого диска, по умолчанию пользуются полным доверием. Чуть позже мы к этому вернемся. В методе HostAgent начинается выполнение загруженного на хост агента, что приводит нас к обсуждению угроз, связанных с хостингом агента. Когда агент загружается на хост в первый раз, CLR ищет сборку агента. Я храню сборки в потайном месте, поэтому разрешение сборок, выполняемое по умолчанию, завершается неудачей. После этого нужную сборку загружает обработчик события AppDomain.AssemblyResolve. Было бы очень разумно поместить эти сборки перед загрузкой и применением в изолированную среду.

В своем коде я обеспечиваю более надежную защиту системы, создавая отдельные домены приложения для изоляции агентов от домена хоста и других агентов. Я решил позволить агенту указать имя приложения, частью которого он является. Каждому приложению будет соответствовать отдельный AppDomain. Эта архитектура лучше соответствует сценариям применения агентов взаимодействия, в которых агенты, прибывшие из разных источников, могут более свободно взаимодействовать друг с другом. Вы можете разделить агенты как угодно. Ваш сценарий может подтолкнуть вас к созданию домена приложения для каждого типа агентов или даже для каждого экземпляра, размещаемого на хосте. Встроенный механизм разделения агентов при помощи доменов приложения позволяет выгружать сборки, если вы определите, что они ставят под угрозу безопасность, или если вам потребуется освободить часть памяти.

.NET Framework 2.0 включает ряд новых API, позволяющих задать политику защиты по правам доступа кода (code access security, CAS) для создаваемого домена приложения. Новые перегруженные версии метода AppDomain.CreateDomain позволяют передать в них используемые по умолчанию объекты Evidence и PermissionSet и указать имена полностью доверяемых сборок для нового домена приложения. Если вы решите задействовать эти средства, советую почитать в онлайновом дневнике Шона Фаркаса (Shawn Farkas) о создании собственных подклассов AppDomainManagers и HostSecurityManager, а также прочитать в недавнем номере "MSDN Magazine" его статью, посвященную хостингу надстроек.

В коде, который можно скачать к этой статье, я реализовал другой подход, основанный на изменении политики CAS для локального компьютера. Используя класс SecurityManager, я добавил новую группу кода (CodeGroup) с ограниченным набором разрешений (унаследованным от набора разрешений для Интернета и затем измененным). Условие членства в данной группе кода определяется адресом URL, указывающим на базовое место хранения сборок. Это условие формируется и проверяется при создании каждого домена приложения для агента. А лучше всего то, что эти параметры теперь распространяются на всю систему. Если какой-то другой процесс загрузит эти сборки, они будут обладать тем же ограниченным набором разрешений (подробнее о CAS в CLR см. по ссылке).

Наконец, скажу пару слов об угрозе, которую может представлять для вашего агента злонамеренный хост. Если вы отправляете хосту данные в форме агента, хост получает полный контроль над этим агентом. Таким образом, я не рекомендую хранить в агентах номера кредитных карт и другую подобную информацию. Вы не имеете контроля над тем, что хост может сделать с вашим агентом, поэтому лучше всего было бы предотвратить попадание агента на подозрительные хосты. Для этого можно воспользоваться коммуникационным протоколом, поддерживающим взаимную аутентификацию. Помните, что безопасность основана на доверии. Если вы не доверяете хосту, держитесь от него подальше.

Заключение


Есть много способов разработки сетевых приложений. Мобильные агенты имеют свои области применения и обладают специфическими достоинствами и недостатками. Автономная и мобильная природа этих агентов помогает сократить сетевой трафик, обеспечивает децентрализацию, повышенную надежность и отказоустойчивость, а также легкость развертывания. Остановимся на этих преимуществах.

Система управления мобильными агентами позволяет уменьшить сетевой трафик за счет перемещения средств обработки данных к используемым ресурсам. Если вы ищете распределенные по сети файлы, вы можете заметно повысить производительность, выполняя поиск локально и возвращая лишь результаты.

Возможность децентрализации системы управления мобильными агентами позволяет повысить надежность. Так как мобильный агент содержит код, вы можете интегрировать в него любые механизмы обеспечения отказоустойчивости и защиты от ошибок. Подумайте еще раз о поиске файлов в сети. Если компьютер, первоначально отправивший мобильные агенты удаленным хостам, выходит из строя, обработка может быть продолжена. Мобильные агенты могут даже переждать опасное время на удаленных хостах, пока не решат, что можно возвращаться. Это позволяет ослабить последствия атак типа "отказ в обслуживании".

Развертывание системы облегчается потому, что мобильные агенты, по сути, развертывают себя сами. Для этого лишь нужно, чтобы на целевом компьютере был установлен и сконфигурирован хост агента. Как только это сделано, можно приступать к работе.

К сожалению, у мобильных агентов есть и недостатки. Лежащий в их основе подход отличается от других парадигм программирования, и не всем будет легко понять его. Из-за скачивания и выполнения кода гораздо большую важность приобретает фактор безопасности. Поищите дополнительную информацию о мобильных агентах в MSN и взгляните на их реализации. После этого посмотрите, в каком количестве агентов предприняты хотя бы попытки реализации адекватной защиты, и вы придете к неутешительному выводу. Скачивание неизвестного кода из сети и его выполнение - не лучший способ контроля безопасности.

Я считаю, что мобильные агенты - очень полезная парадигма, заслуживающая большего внимания, а возможности .NET Framework позволяют мобильным агентам продемонстрировать все, на что они способны.
Идентификатор статьи: 75
Дата занесения/обновления: 15.03.2008 19:16

Комментарии к статье

Ваш комментарий будет первым!
Имя:
Текст:
  mml?
Пожалуйста, подтвердите, что вы человек, введя значение выражения 103+90=