Сообщений 8    Оценка 306 [+1/-0]         Оценить  
Система Orphus

.Net. Использование Remoting в multitier приложениях

Автор: Андрей Корявченко
The RSDN Group

Источник: RSDN Magazine #1
Опубликовано: 03.12.2002
Исправлено: 29.08.2005
Версия текста: 1.0

Что такое Remoting и с чем его едят
Протоколы, используемые Remoting
Передача объектов клиенту
Активация объектов
Активируемые сервером объекты (server activated)
Активируемые клиентом объекты (client activated)
Remoting-сервер
Использование Remoting в многоуровневых приложениях
Использование DataSet
Универсальный клиент

Демонстрационный проект (C#)

Что такое Remoting и с чем его едят

Технология Remoting была разработана для создания распределенных приложений. С ее помощью можно обращаться к экземплярам классов .Net, находящимся за пределами собственного домена (application domain). Это может быть другое приложение внутри одного процесса (например, обращение из одного asp.net приложения к объектам другого), другой процесс на той же машине, или процесс на другой машине (в том числе подключенной через Internet).

Механизм Remoting работает следующим образом.


Сервер при запуске настраивает Remoting для использования определенного протокола. При этом указывается транспортный протокол и протокол доступа. Затем сервер регистрирует все классы, к которым он предоставляет доступ.

Клиент при необходимости доступа к удаленным объектам также указывает протоколы доступа и посылает запрос на сервер. Сервер в ответ на этот запрос создает у себя затребованный объект и передает его идентификатор клиенту. Клиент создает у себя специальный прокси-класс, который затем и использует, как если бы это был объект в его собственном домене.

Протоколы, используемые Remoting

Для своей работы Remoting использует два вида протоколов: транспортный и доступа к объектам. На данный момент в .Net Framework реализовано два первых и два вторых.

Первый транспортный протокол (Channel в терминах .Net), реализованный в Remoting – это TCP. В качестве протокола доступа (formatter в .Net) TcpChannel использует собственный бинарный формат, но при необходимости можно использовать любой другой.

Второй протокол – HTTP. Для доступа он использует SOAP (Simple Object Access Protocol) – протокол доступа, основанный на XML. Основные его преимущества – SOAP стандартизован, так что теоретически возможно использование из клиентов, созданных без использования .Net Remoting, возможен доступ через стандартно настроенные прокси-серверы, не создается постоянное соединение. Основной недостаток – протокол текстовый, имеет большую избыточность.

СОВЕТ

Для выбора протокола можно пользоваться следующим алгоритмом. Если:

1) необходим доступ через прокси-сервер, настроить который на определенный порт нельзя;

2) возможно использование из клиентов, созданных без использования .Net Remoting;

3) необходимо контролировать содержимое обмена –

то следует использовать HttpChannel, иначе TcpChannel.

TcpChannel и HttpChannel – это двунаправленные каналы. В случае, если приложение имеет чисто клиент-серверную архитектуру, необходимость использования именно двунаправленных каналов отсутствует. Более того, если между клиентом и сервером находится прокси-сервер, функционирование двунаправленных каналов на клиенте невозможно. Для подобных случаев существуют две разновидности однонаправленных каналов. К примеру, для протокола HTTP это HttpClientChannel и HttpServerChannel.

Однако число протоколов не ограничивается использованием Http+SOAP и Tcp+бинарный формат. Вы можете написать свой собственный канал и свой собственный форматер, реализовав IChannel и IRemotingFormatter соответственно. Можно также использовать BinaryFormatter совместно с HttpChannel, и SoapFormatter с TcpChannel.

Передача объектов клиенту

В Remoting объекты передаются по значению (by value), либо по ссылке (by reference).

ПРИМЕЧАНИЕ

Обратите внимание на то, что параметры методов и возвращаемое значение, как правило, передаются по значению. Создавая классы, экземпляры которых предполагается передавать (возвращать) в методы удаленных объектов по значению, необходимо их пометить атрибутом [Serializable] или реализовать в них интерфейс ISerializable.

При передаче по ссылке передается только идентификатор (точнее, специальный объект, ObjRef). Клиент создает специальный прокси-класс, который перенаправляет вызовы методов на сервер и получает возвращаемые значения. Объект, передаваемый по ссылке, должен наследоваться от MarshalByRefObject. На самом деле создаются два прокси-класса – «настоящий» прокси (real proxy) и «прозрачный» прокси (transparent proxy). Клиент общается со вторым, который в свою очередь, создается и управляется первым. «Прозрачный» прокси имеет такой же набор методов, как и удаленный класс. При его использовании у клиента создается впечатление, что класс, с которым он работает, находится в адресном пространстве клиента, но на самом деле вызовы методов сериализуются и передаются по каналам связи экземпляру удаленного объекта.

ПРИМЕЧАНИЕ

Прокси создаются автоматически на базе информации о типах объектов (reflection), но существует возможность реализовать свой собственный прокси.

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

При передаче по ссылке вполне достаточно наличия описания интерфейса (сборки, содержащей описание интерфейса без его реализации).

Активация объектов

Для объектов, передаваемых по ссылке, существует два механизма создания объектов: на сервере (server activated) и на клиенте (client activated).

Активируемые сервером объекты (server activated)

Активируемые сервером объекты не создаются на сервере в момент создания клиентом прокси-класса. Вместо этого они создаются в момент первого вызова, или при каждом вызове методов. При этом существуют два типа подобных объектов – Singleton и SingleCall. Первые гарантируют, что реальный экземпляр будет всегда один для всех клиентов. Вторые создают новый объект для каждого вызова метода (модель, похожая на Web Services).

ПРИМЕЧАНИЕ

В случае активируемых сервером объектов по умолчанию нет никакой гарантии, что одной ссылке на клиенте будет соответствовать один и тот же экземпляр класса на сервере. Даже Singleton, если к нему долгое время не обращались, может быть сервером уничтожен, несмотря на то, что клиент по прежнему сохраняет на него ссылку. Если нужно создать настоящий синглтон, то есть экземпляр, единый на все приложение, то следует перекрыть метод InitializeLifetimeService и вернуть из него null. Это гарантирует, что сервер никогда этот экземпляр не уничтожит.

Активируемые клиентом объекты (client activated)

Активируемые клиентом объекты, в противоположность активируемым сервером, управляются клиентом. Именно клиент определяет время жизни того или иного экземпляра.

Существует проблема удаления объектов, управляемых клиентом, связанная с тем, что связь сервер-клиент принципиально ненадежна. Объекты же должны быть удалены со 100% гарантией. Ситуация особенно обостряется в случае протоколов, не устанавливающих постоянного соединения, таких, как HTTP. Классический подход – клиент периодически отсылает на сервер уведомления о том, что клиент жив. Remoting использует иной подход. Каждому экземпляру назначается время жизни. По истечению этого времени сервер посылает клиенту запрос. Если клиент разрешает удалить экземпляр, или если ответа от клиента нет, сервер удаляет объект.

Remoting-сервер

Итак, попробуем написать простейший сервер. Пусть это будет некое подобие time-сервера. Для начала создадим интерфейс удаленного класса. (Здесь и далее примеры написаны на C#).

using System;

public interface IRemoteTime 
{
  DateTime CurrentTime {get;}
}

Скомпилируем его в отдельную сборку. Она нам понадобится как на сервере, так и на клиенте.

ПРИМЕЧАНИЕ

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

СОВЕТ

Невозможно напрямую создать активируемые клиентом объекты, не имея кода самого объекта, а только его интерфейс. Однако можно затребовать у активируемого сервером объекта его активатор, который и будет возвращать ссылку. Используя такой подход, можно работать только с объектами, передаваемыми по ссылке.

Кроме интерфейса, можно также использовать класс с совпадающими публичными пустыми методами, унаследованный от MarshalByRefObject. Создать такой класс можно утилитой soapsuds с ключом –nowp.

В этом случае для создания удаленных объектов можно использовать оператор new.

Реализуем интерфейс. Будем использовать передачу по ссылке.

ПРИМЕЧАНИЕ

Обратите внимание на то, что DateTime помечен атрибутом [Serializable]. Если бы это было не так - использовать его в качестве возвращаемого типа было бы нельзя.

using System;

public class RemoteTime : MarshalByRefObject, IRemoteTime 
{
  public DateTime CurrentTime 
  {
    get { return DateTime.Now; }
  }
}

Теперь реализуем сервер.

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

public class SimpleServer 
{

  static void Main() 
  {
    ChannelServices.RegisterChannel(new HttpChannel(888)); //1

    RemotingConfiguration.RegisterWellKnownServiceType(
      typeof(RemoteTime), 
      "RemoteTimeHost/rt",
      WellKnownObjectMode.Singleton); //2

    Console.WriteLine("Remote time host is running.");
    Console.WriteLine("Press ENTER to exit");
    Console.ReadLine();
  }
}

Рассмотрим код подробнее.

Строка, помеченная единицей регистрирует HttpChannel. 888 – это номер http-порта, который будет прослушиваться сервером.

Строка, помеченная двойкой, говорит серверу зарегистрировать RemoteTime как server activated Singleton-объект, и указывает url, по которому он будет доступен. Полный url объекта будет таким: http://server_name:888/RemoteTimeHost/rt.

ПРИМЕЧАНИЕ

Помимо создания собственных хост-приложений в качестве такового можно использовать Internet Information Server с установленным ASP.NET.

Существует еще один способ создания канала и регистрации объектов. Вместо динамической настройки параметров в коде приложения можно дописать секцию в файл конфигурации (application.exe.config или web.config):

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown 
          mode="Singleton" 
          type="ServiceClass, ServiceClassAssemblyName"
          objectUri="ServiceClass.rem"
        />
      </service>
      <channels>
        <channel 
          name="MyChannel" 
          priority="100" 
          ref="http"
        />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

При этом objectUri должен оканчиваться на .rem или .soap, чтобы IIS понял (в случае его использования), что надо запустить фильтр ASP.NET. При обращении к страницам с расширением rem или soap фильтр передаст их механизму Remoting.

ПРЕДУПРЕЖДЕНИЕ

Для работы приложения, конфигурирующего Remoting, необходимо, чтобы в SecurityPermission был установлен флаг RemotingConfiguration. Будьте внимательны – в .Net Framework redistributable этот флажок по умолчанию не установлен. Настройки защиты .NET приложений можно также задать с помощью «Microsoft .NET Framework Wizards» который можно найти в «Programs\Administrative Tools».

Ну и, наконец, реализация клиента.

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

public class SingleClient 
{

  static void Main() 
  {

    IRemoteTime rt = (IRemoteTime)Activator.GetObject(
      typeof(IRemoteTime),
      "http://server_name:888/RemoteTimeHost/rt");
    Console.WriteLine("Current time is : {0}",rt.CurrentTime);
  }
}

На клиенте при помощи статического метода GetObject класса Activator создаем прокси удаленного объекта, вызываем его метод и выводим результат на консоль.

Использование Remoting в многоуровневых приложениях

Remoting является классической ORPC-подсистемой, цель которой – осуществлять взаимодействие клиента и сервера с помощью удаленного вызова методов. Поэтому программист может использовать практически неограниченный набор приемов и подходов при построении многоуровневых приложений. Каждая архитектура пытается предоставить некоторые средства для упрощения разработки. Так, Java поддерживает идеологию Entity EJB-компонентов, которые позволяют упростить работу с корпоративными данными с помощью отображения их на Entity-объекты. Microsoft не реализовал такой функциональности, зато предложил использовать так называемые DataSet (ранее – отключенные Recordset). Это своего рода динамический массив, позволяющий использовать для любой своей колонки произвольный тип данных. Это можно рассматривать как аналог курсора в базах данных, только отключенный от источника данных. Использование DataSet позволяет значительно упростить процесс разработки корпоративного ПО. Он не так строен и красив, как Entity-компоненты в EJB, зато куда более прагматичен – быстрее и проще в освоении – и близок к жизни.

Использование DataSet

Microsoft пропагандирует использование DataSet в .Net Framework как главный способ работы с корпоративными данными в многоуровневых приложениях. Суть использования DataSet заключается в следующем: по запросу клиента сервер подготавливает экземпляр DataSet (загружает в него данные из разных источников и обрабатывает их) и передает его клиенту. Клиент использует полученный DataSet для отображения и редактирования данных, а затем отсылает изменения на сервер, который вносит полученные изменения в исходные источники данных.

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

using System;
using System.Data;
using System.Xml;

public class ServerApplication : MarshalByRefObject 
{

  private static XmlDataDocument xdd = null;
  private static XmlDataDocument DataDocument 
  {
    get
    {
      if(xdd == null) 
      {
        XmlDataDocument doc = new XmlDataDocument();

        doc.DataSet.ReadXmlSchema("schema.xsd");

        doc.Load("data.xml");
        doc.DataSet.AcceptChanges();

        xdd = doc;
      }
      return xdd;
    }
  }

  public ServerApplication() 
  {
    Console.WriteLine("Activated");
  } 

  public DataSet GetPreparedDataSet()
  {
    return DataDocument.DataSet;
  }

  public void Save(DataSet cds) 
  {
    xdd.DataSet.Merge(cds);
    xdd.Save("data.xml");
  }
}

Класс ServerApplication унаследуем от MarshalByRefObject, он будет передаваться по ссылке. Свойство DataDocument хранит ссылку на экземпляр XmlDataDocument. В реальном приложении доступ к данным будет производиться, скорее всего, через ADO.NET, однако в примере, чтобы упростить развертывание, в качестве источника данных мы будем использовать xml-файл и его схему. Xml-файл с данными имеет следующее содержимое:

<?xml version="1.0" encoding="Windows-1251"?>
<phone-book>
  <record>
    <id>1</id>
    <name>Иванов Иван Иванович</name>
    <phone>123-45-67</phone>
  </record>
  <record>
    <id>2</id>
    <name>Петров Петр Петрович</name>
    <phone>765-43-21</phone>
  </record>
  <record>
    <id>3</id>
    <name>Сидоров Сидор Сидорович</name>
    <phone>234-22-46</phone>
  </record>
</phone-book>

Его схему можно создать автоматически при помощи утилиты xsd.

<xs:schema id="phone-book" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="phone-book" msdata:IsDataSet="true" msdata:Locale="ru-RU">
    <xs:complexType>
      <xs:choice maxOccurs="unbounded">
        <xs:element name="record">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="id" type="xs:string" minOccurs="0" />
              <xs:element name="name" type="xs:string" minOccurs="0" />
              <xs:element name="phone" type="xs:string" minOccurs="0" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

Метод GetPreparedDataSet() возвращает DataSet, содержащий данные из xml-файла. Метод Save() объединяет изменения, передаваемые в него в виде DataSet, с основным DataSet, и сохраняет содержимое последнего в xml-файле.

Ниже приведена реализация основного класса сервера. В его методе Main() происходит регистрация канала и активируемого клиентом класса.

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

public class SimpleServer
{

  static void Main()
  {
    ChannelServices.RegisterChannel(new HttpServerChannel(888));

    RemotingConfiguration.RegisterActivatedServiceType(typeof(ServerApplication));

    Console.WriteLine("Application server is running.");
    Console.WriteLine("Press ENTER to exit");
    Console.ReadLine();
  }
}

Клиентский класс получает ссылку на удаленный экземпляр ServerApplication, вызывает метод GetPreparedDataSet() и показывает полученный DataSet в таблице, размещенной на форме. Если пользователь добавил новые записи, то при нажатии кнопки Save или закрытии формы они отсылаются на сервер. DataSet при передаче копируется, и клиент работает с его копией.

using System;
using System.Data;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Activation;
using System.Drawing;
using System.Windows.Forms;

public class Client 
{
  
  static void Main()
  {
    RemotingConfiguration.RegisterActivatedClientType(
      typeof(ServerApplication),"http://localhost:888");

    ServerApplication sa = new ServerApplication();

    ClientForm cf = new ClientForm(sa);

    Application.Run(cf);
  }

}

public class ClientForm : Form
{

  private ServerApplication srvapp;

  public ClientForm(ServerApplication sa)
  {
    Text = "Simple multitier application sample";
    Size = new Size(400,500);
    srvapp = sa;
    InitControls();
  }

  private DataGrid dg = new DataGrid();

  private void InitControls()
  {
    dg.Dock = DockStyle.Fill;
    dg.DataSource = srvapp.GetPreparedDataSet();
    dg.DataMember = "record";

    Panel pnl = new Panel();
    pnl.Dock = DockStyle.Bottom;
    pnl.Height = 40;

    Button sb = new Button();
    sb.Location = new Point(20,8);
    sb.Text = "&Save";
    sb.Click += new EventHandler(SaveClick);

    pnl.Controls.AddRange(new Control[]{sb});

    Controls.AddRange(new Control[]{dg,pnl});
  }

  protected override void OnClosed(EventArgs e)
  {
    SaveChanges();
  }

  private void SaveClick(object sender, EventArgs ea)
  {
    SaveChanges();
  }

  private void SaveChanges()
  {
    DataSet cds = ((DataSet)dg.DataSource).GetChanges(DataRowState.Added);
    if(cds != null)
    {
      srvapp.Save(cds);
      ((DataSet)dg.DataSource).AcceptChanges();
    }
  }
}

Универсальный клиент

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

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

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


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь
    Сообщений 8    Оценка 306 [+1/-0]         Оценить