Режим серверной активации SingleCall предназначен для реализации концепции объектов без сохранения состояния. Если некий тип сконфигурирован в режиме SingleCall, инфраструктура Remoting создает объект типа для каждого вызова метода типа. После вызова метода объект уничтожается. Следующий пример демонстрирует конфигурирование типа в режиме SingleCall:
С точностью до последнего параметра этот фрагмент кода идентичен коду для случая режима Singleton. Клиентский код регистрации полностью совпадает для двух режимов.
В некоторых программных сценариях требуется, чтобы каждый клиент работал со своей копией удаленного объекта. В этом случае следует использовать клиентскую активацию. Объекты типов с клиентской активацией (далее – CAO, client activated objects) сохраняют свое состояние между вызовами методов. Такие объекты имеют определенное время жизни, после которого автоматически уничтожаются.
Приведем пример кода, конфигурирующего тип на сервере как тип для CAO:
Более детально типы с клиентской активацией будут рассмотрены ниже.
Рассмотрим некоторые вопросы, связанные с отслеживанием временим жизни удаленных объектов. Эта проблема актуальна для CAO и SAO Singleton. В Remoting для управления временем жизни таких объектов используется механизм на основе лицензий и спонсоров.
Лицензия (lease) – это объект, инкапсулирующий несколько значений типа TimeSpan[12]. В Remoting для описания лицензий используется интерфейс ILease (пространство имен System.Runtime.Remoting.Lifetime). Интерфейс ILease определяет несколько свойств, связанных с расчетом времени жизни объекта:
· InitialLeaseTime
· RenewOnCallTime
· SponsorshipTimeout
· CurrentLeaseTime
Свойство только для чтения CurrentLeaseTime содержит время, оставшееся до истечения срока действия лицензии. Свойство InitialLeaseTime указывает на первоначальный срок лицензии. При выдаче лицензии CurrentLeaseTime устанавливается равным InitialLeaseTime. Если InitialLeaseTime равно 0, то срок лицензии никогда не заканчивается. При вызове клиентом метода удаленного объекта инфраструктура Remoting определяет время, оставшееся до истечения лицензии. Если это время меньше, чем RenewOnCallTime, то лицензия продлевается на интервал, равный RenewOnCallTime. Свойство SponsorshipTimeout определяет, как долго инфраструктура Remoting будет ожидать ответа спонсора лицензии. Все перечисленные свойства имеют тип TimeSpan.
Когда создается объект с клиентской активацией или объект с серверной активацией в режиме Singleton, исполняющая среда запрашивает у объекта лицензию, вызвав его метод InitializeLifetimeServices(). Это виртуальный метод класса MarshalByRefObject. Для реализации лицензии с нестандартными параметрами метод можно перекрыть.
В каждом домене приложения имеется специальный диспетчер лицензий, который управляет лицензиями экземпляров типов, зарегистрированных в домене[13]. После активации удаленного объекта связанная с ним лицензия передается диспетчеру. Диспетчер содержит таблицу, в которой сопоставлены лицензии и момент окончания их действия. Диспетчер периодически просматривает данную таблицу[14]. Если момент окончания действия лицензии наступил, лицензия уведомляется об этом.
Истекшие лицензии пытаются продлить себя, опросив своих спонсоров. Если у лицензии нет спонсоров или никто из спонсоров не продлил ее, то лицензия уведомляет диспетчер о том, что ее следует удалить из таблицы лицензий, и связанный с лицензией удаленный объект уничтожается.
Спонсор – это объект, который может продлить лицензию. Класс спонсора должен реализовывать System.Runtime.Remoting.Lifetime.ISponsor. Спонсоры могут размещаться как на клиенте, так и на сервере, а значит, класс спонсора должен быть MBR-типом. Созданного спонсора разрешается связать с лицензией, вызвав метод ILease.Register(). У лицензии может быть несколько спонсоров.
Рис. 7. Спонсоры и лицензии
В пространстве имен System.Runtime.Remoting.Lifetime содержится класс ClientSponsor. Это MBR-тип и он реализует интерфейс ISponsor. Класс ClientSponsor позволяет регистрировать ссылки на удаленные объекты, которые предполагается спонсировать. Когда методу ClientSponsor.Register() передается ссылка на удаленный объект, этот метод регистрирует экземпляр ClientSponsor в качестве спонсора лицензии удаленного объекта и запоминает ссылку на лицензию удаленного объекта во внутренней хэш-таблице. Интервал времени, на который спонсор продлит лицензию, задается свойством ClientSponsor.RenewalTime. Ниже показан пример использования ClientSponsor:
ClientSponsor cp = new ClientSponsor(TimeSpan.FromMinutes(5));
cp.Register(someMBR);
3.4. Программная настройка Remoting
Разберем настройку и использование Remoting на следующем примере. Предположим, что планируется реализовать в виде удаленного сервиса калькулятор, который производит четыре арифметических действия. На первом этапе выделим функционал сервиса и оформим его описание в виде интерфейса:
namespace BSUIR {
public interface ICalc {
double Add(double x, double y);
double Sub(double x, double y);
double Mult(double x, double y);
double Div(double x, double y);
}
}
Скомпилируем интерфейс в динамическую библиотеку ICalc.dll.
В качестве непосредственной реализации интерфейса BSUIR.ICalc рассмотрим класс BSUIR.Calculator:
using System;
namespace BSUIR {
public class Calculator: ICalc {
public Calculator() {
log("Calculator constructor");
}
public double Add(double x, double y) {
log("Add " + x + " + " + y);
return x+y;
}
public double Sub(double x, double y) {
log("Sub " + x + " - " + y);
return x-y;
}
public double Mult(double x, double y) {
log("Mult " + x + " * " + y);
return x*y;
}
public double Div(double x, double y) {
log("Div " + x + " / " + y);
return x/y;
}
public static void log(string s) {
Console.WriteLine("[{0}]: {1}",
AppDomain.CurrentDomain.FriendlyName, s);
}
}
}
Класс Calculator скомпилирован в динамическую библиотеку с именем calc.dll. Для компиляции класса необходимо установить ссылку на сборку ICalc.dll.
Итак, на данном этапе у нас имеются:
1. интерфейс BSUIR.ICalc, размещенный в отдельной динамической библиотеке ICalc.dll;
2. класс BSUIR.Calculator, который реализует интерфейс BSUIR.ICalc и размещается в динамической библиотеке calc.dll.
Что дает подобное разделение на интерфейс и реализующий его класс? Преимущества подхода заключаются в следующем: для клиентов необходим только интерфейс BSUIR.ICalc, без его реализации. Мы можем менять на серверной части нашего распределенного приложения класс BSUIR.Calculator совершенно «прозрачно» для клиентов, пока класс поддерживает интерфейс BSUIR.ICalc. Если бы разделение не выполнялось, то клиенту понадобился бы класс BSUIR.Calculator на локальной машине, хотя и предполагалось бы удаленное использование объектов данного класса.
Построим сервер для нашего распределенного приложения. Прежде всего, нам необходимо внести некоторые изменения в класс BSUIR.Calculator.
using System;
namespace BSUIR {
public class Calculator: MarshalByRefObject, ICalc {
// далее по коду изменений нет
. . .
}
}
Напомним, что наследование от MarshalByRefObject – это обязательное условие для удаленных типов.
Код настройки сервера будет размещаться в методе Main() консольного приложения (собственно, данное приложение и будет сервером[15]). Во-первых, необходимо создать объект, описывающий канал. Воспользуемся классом HttpChannel (пространство имен System.Runtime.Remoting.Channels.Http) для создания стандартного канала на основе протокола HTTP:
HttpChannel chan = new HttpChannel(6000);
Параметром конструктора является номер порта, с которым связан канал (канал прослушивает данный порт).
Созданный канал должен быть зарегистрирован в инфраструктуре. Для этого используется статический метод RegisterChannel() класса ChannelServices из пространства имен System.Runtime.Remoting.Channels:
ChannelServices.RegisterChannel(chan);
После создания и регистрации канала требуется зарегистрировать класс, объекты которого предполагается использовать удаленно. Для регистрации класса используются статические методы класса RemotingConfiguration. В нашем примере будут использоваться объекты с серверной активацией в режиме SingleCall:
Параметры метода: тип регистрируемого класса; строка, представляющая концевую точку для типа; элемент перечисления WellKnownObjectMode, указывающий на режим использования серверных объектов (Singleton или SingleCall).
После регистрации канала и удаленного типа сервер переводится в режим ожидания. Полный текст сервера приведен ниже:
При компиляции сервера необходимо установить ссылку на сборку calc.dll и сборку ICalc.dll.
Приступим к написанию клиента. Как и сервер, клиент будет консольным приложением, логика работы которого сосредоточена в методе Main().
Клиент должен зарегистрировать канал. При вызове конструктора канала на клиенте можно использовать 0 в качестве параметра или применить конструктор без параметров. В последнем случае клиент не сможет принимать вызовы сервера. Это не всегда приемлемо (например, при работе со спонсорами, асинхронных вызовов методов и т. п.).
HttpChannel chan = new HttpChannel(0);
ChannelServices.RegisterChannel(chan);
Для соединения с сервером и получения ссылки на удаленный объект можно использовать статический метод Connect() класса RemotingServices. В качестве параметров методу передается тип запрашиваемого объекта и универсальный идентификатор ресурсов, указывающий на концевую точку, связанную с серверным объектом. Типом запрашиваемого объекта в нашем случае будет интерфейс ICalc, так как работа будет вестись через этот интерфейс:
Для программной настройки инфраструктуры Remoting применялся класс RemotingConfiguration. Подробное описание свойств и методов этого класса приведено в таблице. Все свойств и методы являются статическими.
Таблица 18
Статические элементы класса RemotingConfiguration
Имя элемента
Описание
ApplicationId
Строка, содержащая GUID для приложения
ApplicationName
Строка с именем приложения. Является частью URI удаленного CAO-типа для клиентов
Configure
Метод используется в случае, когда настройки Remoting хранятся в конфигурационном файле
Набор методов для регистрации типов различных видов на клиенте и на сервере
3.5. УДАЛЕННЫЕ Объекты с клиентской активацией
Рассмотрим пример распределенного приложения, в котором используются типы с клиентской активацией. Также опишем более подробно работу с лицензиями и спонсорами.
Пусть требуется написать сервер, который предоставляет клиентам услугу по вычислению суммы двух целых чисел. Для получения данной услуги клиент должен пройти процедуру регистрации на сервере. Кроме этого, сервер хранит для каждого клиента последний рассчитанный результат. В приложении будет задействован механизм лицензий (нестандартное время для параметров лицензии) и спонсоров.
Начнем решение задачи с разработки класса для представления удаленного объекта. Первая «заготовка» класса может выглядеть следующим образом (сборка RemoteCAO.dll):
Обратите внимание на следующие моменты. Класс UserCalculator способен генерировать исключительные ситуации[16]. Клиент сможет корректно обработать исключительную ситуацию, которая возникла в удаленном объекте. В процессе работы с удаленным объектом мы будем совершенно свободно манипулировать его полем (не используя методы). В реальном приложении процесс авторизации обычно подразумевает работу с базой данных, в нашем случае применен упрощенный подход.
Класс UserCalculator, как и любой удаленный тип, является наследником класса MarshalByRefObject. В таблице 19 приведено описание методов класса MarshalByRefObject.
Таблица 19
Методы класса MarshalByRefObject
Имя метода
Описание
CreateObjRef()
Виртуальный метод, возвращающий объект класса System.Runtime.Remoting.ObjRef. Такой объект необходим клиентскому приложению, чтобы настроить прокси-объект. Метод можно переписать, если требуется собственная реализация ObjRef.
GetLifetimeService()
Функция возвращает объект, реализующий интерфейс ILease, то есть лицензию, связанную с объектом.
InitializeLifetimeService()
Данный виртуальный метод вызывается инфраструктурой Remoting при создании объекта и должен возвращать лицензию объекта. При необходимости метод можно переписать.
В нашем примере мы собираемся использовать нестандартные времена для лицензии объектов класса UserCalculator. Следовательно, нам требуется переписать виртуальный метод InitializeLifetimeService():
namespace RemoteCAO {
public class UserCalculator: MarshalByRefObject {
. . .
public override object InitializeLifetimeService() {
В начале работы метода InitializeLifetimeService() вызывается метод базового класса, который возвращает стандартную лицензию. Параметры можно устанавливать у лицензии, которая не была помещена в диспетчер лицензий. В методе выполняется такая проверка (проверяется текущее состояние лицензии). В случае успеха устанавливаются требуемые значения свойств лицензии. Если метод InitializeLifetimeService() переписан так, что возвращает значение null, то лицензия объекта никогда не заканчивается.
Перейдем к реализации сервера. Как и в примере из предыдущего параграфа, будем использовать в качестве сервера консольное приложение:
В сервере регистрируется канал и удаленный тип с клиентской активацией. Задается имя приложения ("MyServer"). Оно будет являться частью URL при доступе к удаленному объекту[17].
В клиенте требуется обратить внимание на следующие особенности. При создании канала конструктору передан параметр 0, так как планируется принимать обратные вызовы от сервера. При регистрации типа указано (как часть URL) имя приложения-клиента. После того как на клиенте зарегистрирован удаленный тип с клиентской активацией, для создания удаленных объектов используется оператор new. В клиенте мы можем обрабатывать исключительные ситуации, которые возникли на сервере и работать с полем удаленного объекта.
Реализуем спонсора, который сможет продлить время жизни удаленного объекта. Будем использовать клиентский спонсор, то есть объект-спонсор разместим на клиенте. Напомним, что класс для спонсоров должен реализовывать интерфейс ISponsor, а также быть MBR-типом:
using System.Runtime.Remoting.Lifetime;
. . .
public class FatSponsor: MarshalByRefObject, ISponsor {
public TimeSpan Renewal(ILease lease) {
Console.WriteLine("Renewal called....");
return TimeSpan.FromMinutes(1);
}
}
class CalcClient {. . .}
Наш спонсор прост. Он реализует метод ISponsor.Renewal(), который возвращает (независимо от лицензии) время, на которое продлевается лицензия объекта (1 минута).
Объект-спонсор требуется создать и зарегистрировать. Для этого у удаленного объекта запрашивается лицензия (методом GetLifetimeService()), а затем при помощи метода ILease.Register() регистрируется спонсор:
Метод ILease.Unregister() используется для отмены регистрации определенного спонсора.
Приведенный код имеет следующую особенность. Он работает только с Microsoft Framework 1.0. В версии 1.1 и выше данный код работать не будет. В целях безопасности в этих версиях форматерам запрещено передавать вызовы от сервера клиенту без дополнительных настроек. Если проводится настройка Remoting с помощью конфигурационных файлов, то в параметрах форматера (и на клиенте, и на сервере) требуется поместить такой текст (выделен жирным шрифтом):
<channels>
<channel ref="http" port="0" >
<serverProviders>
<formatter ref="soap" typeFilterLevel="Full" />
<formatter ref="binary" typeFilterLevel="Full" />
</serverProviders>
</channel>
</channels>
3.6. Настройка Remoting при помощи конфигурационных файлов
В предыдущих примерах с использованием Remoting применялось программное конфигурирование инфраструктуры. Такой подход не всегда удобен, особенно если настройки требуется часто изменять. Наряду с программным конфигурированием Remoting допускает настройку с использованием стандартных конфигурационных файлов. Вместо написания такого кода на сервере
допускается использование следующего конфигурационного файла, обеспечивающего аналогичные настройки:
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="1234" />
</channels>
<service>
<wellknown mode="Singleton"
type="Server.CustomerManager, Server"
objectUri="CustomerManager.soap" />
</service>
</application>
</system.runtime.remoting>
</configuration>
Для применения настроек конфигурационного файла в вашем приложении достаточно вызвать метод RemotingConfiguration.Configure() и передать ему в качестве параметра имя config-файла:
String filename = "server.exe.config";
RemotingConfiguration.Configure(filename);
Ниже рассмотрены правила создания конфигурационных файлов. Файл с конфигурацией Remoting имеет следующую структуру:
<configuration>
<system.runtime.remoting>
<application>
<lifetime />
<channels />
<service />
<client />
</application>
</system.runtime.remoting>
</configuration>
<lifetime />
Данный тэг используется для изменения параметров лицензии, используемых инфраструктурой по умолчанию. Тэг может иметь следующие атрибуты:
· leaseTime – начальное время жизни (time to live, TTL) объекта (по умолчанию – 5 минут);
· sponsorshipTimeout – время ожидания ответа от спонсора (по умолчанию – 2 минуты);
· renewOnCallTime – время, добавляемое к TTL объекта, когда вызывается его метод (по умолчанию – 2 минуты);
Все атрибуты являются необязательными и могут быть заданы в различных временных единицах. При этом используется обозначение D для дней, H для часов, M для минут, S для секунд и MS для миллисекунд. Комбинации вида 1H5M не поддерживаются.
Пример секции <lifetime>:
<lifetime
leaseTime="90MS"
renewOnCallTime="90MS"
leaseManagerPollTime="100MS"
/>
<channels />
Этот тэг служит для группировки тэгов, описывающих отдельные каналы. Сам он не имеет каких-либо атрибутов.
<channel />
Тэг позволяет указать номер порта для канала на стороне сервера, сослаться на нестандартный пользовательский канал, а также провести дополнительную настройку канала. Если планируется использовать стандартные TCP или HTTP каналы, этот тэг не требуется указывать на клиенте, так как стандартные каналы регистрируются .NET Framework автоматически. На стороне сервера требуется указать, по крайней мере, номер порта для канала.
Для ссылки на канал можно использовать два способа: указать короткую именную ссылку для предварительно зарегистрированных каналов или использовать точное имя типа (пространство имен, имя класса, имя сборки), реализующего канал.
Тэг <channel> может иметь следующие атрибуты:
· ref – ссылка на стандартный канал ("tcp" или "http") или ссылка на канал, предварительно описанный в конфигурационном файле;
· displayName – используется .NET Framework Configuration Tool;
· type – главный атрибут, если не задан атрибут ref. Содержит точное имя типа канала, с указанием пространства имен и сборки. Для глобальных сборок требуется указывать сильное имя. В качестве примера использования рассмотрите описание HTTP-канала в файле machine.config;
· port – Номер порта канала на сервере. Если клиент планирует получать от сервера сообщения обратного вызова, то в качестве номера порта на клиенте требуется указать 0.
В дополнение к описанным атрибутам, HTTP-канал поддерживает следующие дополнительные атрибуты (для некоторых атрибутов указано, где их требуется задавать – на клиенте (К) или на сервере (С)):
· name – имя канала (по умолчанию "http"). При регистрации нескольких каналов должно быть уникальным, или требуется указать пустую строку (""). Значение данного атрибута можно использовать при вызове функции ChannelServices.GetChannel();
· priority – индикатор вероятности того, что исполняющая среда выберет данный канал для пересылки данных (по умолчанию параметр равен 1). Чем больше указанное целое число, тем выше вероятность;
· clientConnectionLimit – число соединений, которые клиент может одновременно открыть к заданному серверу (по умолчанию – 2) (К);
· proxyName – имя (адрес) прокси-сервера (К);
· proxyPort – номер порта прокси-сервера (К);
· suppressChannelData – атрибут указывает, будут ли данные о канале присутствовать в структуре ChannelData, используемой при создании объекта ObjRef (по умолчанию – "false") (С);