После подписывания сборку можно поместить в GAC. Простейший вариант сделать это – использовать утилиту gacutil.exe, входящую в состав .NET Framework SDK. При использовании ключа /i сборка помещается в GAC, а ключ /u удаляет сборку из GAC:
gacutil /i ul.dll
Теперь сборка UL.dll помещена в GAC. Ее сильное имя (для ссылки в программах) имеет вид:
Компонентом сильного имени является версия сборки. Если программист желает указать версию, то для этого используется атрибут AssemblyVersion. Номер версии имеет формат Major.Minor.Build.Revision. Если номер версии задается, то часть Major является обязательной. Любая другая часть может быть опущена (в этом случае она полагается равной нулю). Часть Revision можно задать как *, тогда компилятор генерирует ее как количество секунд, прошедших с полуночи, деленное на два. Часть Build также можно задать как *. Для нее будет подставлено количество дней, прошедших с 1 февраля 2000 года. Пример использования атрибута AssemblyVersion:
using System;
using System.Reflection;
[assembly: AssemblyVersion("1.2.3.*")]
. . .
2.14. КОНФИГУРИРОВАНИЕ сборок
Необходимость конфигурирования сборок обычно возникает при развертывании приложений. Платформа .NET предлагает стандартизированный подход к конфигурированию, основанный на использовании конфигурационных XML-файлов. Специальное пространство имен System.Configuration отвечает за работу с файлами конфигураций.
Рассмотрим общую схему файла конфигурации. Корневым элементом файла всегда является элемент configuration. Некоторые подчиненные элементы описаны далее:
Платформа .NET Framework имеет один файл конфигурации компьютера с параметрами, относящимися к системе в целом. Этот файл называется machine.config. Любая сборка может иметь собственный конфигурационный файл. Он должен носить имя файла сборки (с расширением) с добавленным окончанием .config и располагаться в одном каталоге со сборкой. Таким образом, файл конфигурации для main.exe должен называться main.exe.config. В случае web-приложений файл конфигурации всегда называется web.config.
Разберем на примерах некоторые возможности конфигурирования. Рассмотрим подробнее структуру секции runtime:
Если параметр тэга равен "true", то среда исполнения помимо обычных каталогов пытается искать сборки в каталогах, указанных в переменной среды окружения DEVPATH.
<assemblyBinding>
Тэг служит для объединения группы вложенных элементов
<probing>
Прототип тэга:
<probing privatePath = "paths"/>
Тэг используется для указания подкаталогов, в которых производится поиск локальных сборок приложения. Например, пусть основное приложение AppMain.exe размещено в каталоге C:\Test, и оно использует сборку Add1.dll из каталога C:\Test\bin и сборку Add2.dll из каталога C:\Test\bin\bin2. Тогда для корректного запуска основного приложения файл AppMain.exe.config должен иметь следующий вид:
<configuration>
<runtime>
<assemblyBinding>
<probing privatePath = "bin;bin\bin2"/>
</assemblyBinding>
</runtime>
</configuration>
<publisherPolicy>
Прототип тэга:
<publisherPolicy apply = "yes" | "no"/>
Пусть приложение использует некую стороннюю библиотеку. Если в тэге <publisherPolicy> определен параметр "yes" (по умолчанию), то при установке новой версии библиотеки, приложение автоматически будет использовать эту новую версию. В случае параметра "no" приложение откажется работать с новой версией.
Этот тэг позволяет ассоциировать сокращенное имя сборки с полным строгим именем для удобства использования в программе. Рассмотрим следующий пример:
<configuration>
<runtime>
<assemblyBinding>
<qualifyAssembly
partialName = "Strong"
fullName = "Strong, version=1.2.0.0,
culture=neutral,
publicKeyToken=1234567812345678"/>
</assemblyBinding>
</runtime>
</configuration>
Конфигурационный файл данного примера позволит везде в программе вместо длинного строго имени использовать короткий псевдоним Strong.
<dependentAssembly>
Этот тэг позволяет настроить загрузку персонально для каждой сборки, связанной с основным приложением. Он сам является пустым и объединяет элементы <assemblyIdentity>, <bindingRedirect>, <codeBase>, <publisherPolicy>. Элемент должен быть указан для каждой сборки, политику загрузки которой мы собираемся настраивать.
<assemblyIdentity>
Прототип тэга:
<assemblyIdentity
name = "assembly Name" publicKeyToken = "public key token"
culture = "assembly culture"/>
Тэг идентифицирует сборку
<bindingRedirect>
Прототип тэга:
<bindingRedirect
old version = "old assembly version"
new version = "new assembly version"/>
Тэг позволяет подменить версию сборки, которая будет загружаться приложением. Рассмотрим пример:
<configuration>
<runtime>
<assemblyBinding>
<dependentAssembly>
<assemblyIdentity name = "myAssembly"
publicKeyToken="32ab4ba45e0a69a1"
culture = "neutral"/>
<bindingRedirect oldVersion = "1.0.0.0"
newVersion = "2.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Теперь, если основное приложение запросит сборку myAssembly версии 1.0.0.0, то, ничего не зная об этом, получит сборку версии 2.0.0.0.
<codeBase>
Прототип тэга:
<codeBase version="assembly version" href="URL of assembly"/>
Этот тэг позволяет задать произвольное местоположение зависимой сборки. Причем, в качестве параметра href может быть указан сетевой адрес. Продемонстрируем это на примере:
<configuration>
<runtime>
<assemblyBinding>
<dependentAssembly>
<assemblyIdentity name = "myAssembly"
publicKeyToken = "32ab4ba45e0a69a1"
culture = "neutral"/>
<codeBase version = "1.0.0.0"
href = "http://localhost/one/Strong.dll"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Этот конфигурационный файл предписывает загружать среде исполнения сборку Strong из Интернета по адресу http://localhost/one/Strong.dll.
<gcConcurrent>
Прототип тэга:
<gcConcurrent enabled = "true" | "false"/>
Если параметр enabled установлен в true, то «сборка мусора» происходит в параллельном потоке по отношению к основному приложению. Это поведение по умолчанию.
Рассмотрим задачу размещения собственных данных в конфигурационном файле сборки. В простейшем варианте для этого используется секция appSettings. Данная секция может содержать следующие элементы:
· <add key = "name" value = "the value"/> – добавляет новый ключ и значение в коллекцию конфигурационных элементов;
· <remove key = "name"/> – удаляет существующий ключ и значение из коллекции конфигурационных элементов;
Для работы с данными секции appSettings используется класс ConfigurationSettings из пространства имен Sistem.Configuration. Статическое свойство AppSettings представляет собой коллекцию, позволяющую получить строковое значение конфигурационного элемента по ключу.
Пусть имеется следующее консольное приложение main.exe:
Создадим для этого приложения конфигурационный файл main.exe.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="key1" value="Alex" />
<add key="key2" value="Volosevich" />
</appSettings>
</configuration>
Результат выполнения main.exe:
Alex
Volosevich
Все секции конфигурационного файла разбираются специальными классами-обработчиками (section handlers). Такой класс реализует интерфейс System.Configuration.IConfigurationSectionHandler. В составе .NET Framework имеется несколько готовых классов-обработчиков: NameValueSectionHandler, IgnoreSectionHandler, DictionarySectionHandler, SingleTagSectionHandler.
Чтобы описать в конфигурационном файле собственную секцию, следует использовать тэг configSections. Вложенные элементы section данного тэга имеют атрибут name – имя секции, и атрибут type – полное имя класса-обработчика. Рассмотрим следующий пример конфигурационного файла:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name = "mySection"
type = "System.Configuration.NameValueSectionHandler" />
</configSections>
<mySection>
<add key="key1" value="Alex" />
<add key="key2" value="Volosevich" />
</mySection>
</configuration>
Этот файл задает пользовательскую секцию с именем mySection и указывает, что для ее обработки следует использовать NameValueSectionHandler, то есть, содержимое секции будет доступно как NameValueCollection.
Для чтения информации из пользовательской секции применяется метод ConfigurationSettings.GetConfig(). В качестве параметра методу передается имя секции. Метод возвращает значение типа object, которое следует привести к типу класса-обработчика секции. Следующее приложение читает информацию из своего (описанного выше) конфигурационного файла:
Следует иметь в виду, что файл конфигурации читается и разбирается исполняемой средой один раз, при загрузке приложения. Поэтому все изменения, которые внесены в этот файл, не буду иметь эффекта до перезагрузки приложения. Если такое поведение является неприемлемым, то можно воспользоваться классом System.IO.FileSystemWatcher. Как следует из его названия, класс позволяет отследить изменения, происходящие с файлом (или несколькими файлами). Подробности работы с классом FileSystemWatcher можно найти в .NET Framework SDK.
3.1. ДОМЕНЫ ПРИЛОЖЕНИЙ
Любому запущенному приложению в операционной системе соответствует некий процесс. Процесс образует границы приложения, и для взаимодействия процессов требуется применять специальные средства. В .NET Framework процессы дополнительно подразделяются на домены приложений. Один домен может содержать несколько сборок. Различным доменам могут соответствовать различные политики безопасности, домены могут создаваться и уничтожаться в ходе работы в рамках одного приложения.
Рассмотрим работу с доменами на примерах. Пусть имеется следующая сборка, размещенная в динамической библиотеке Students.dll:
using System;
public class Student {
public string Name;
public int Age;
public double MeanScore;
public Student(string Name, int Age, double MeanScore) {
this.Name = Name;
this.Age = Age;
this.MeanScore = MeanScore;
log.print("New Student object created");
}
public void sayName() {
log.print("sayName() is called");
Console.WriteLine("My name is {0}", Name);
}
}
public class log {
public static void print(string s) {
Console.WriteLine("[{0}]: {1}",
AppDomain.CurrentDomain.FriendlyName,s);
}
}
Домены приложений инкапсулированы в объектах класса System.AppDomain. Любое приложение при запуске создает домен по умолчанию, в который загружается главная сборка приложения. Для создания нового домена используется статический метод AppDomain.CreateDomain(). Существует несколько перегруженных версий этого метода, которые обеспечивают применение к создаваемому домену определенных политик безопасности. В простейшем случае методу передается в качестве параметра только строка с дружественным именем домена, которое используется как идентификатор домена:
После создания домена в него можно загрузить сборки, используя метод Load()[7] (указывается слабое или сильное имя сборки):
ad2.Load("Students");
Отдельную сборку выгрузить из домена нельзя, но можно выгрузить домен целиком, используя его метод Unload():
AppDomain.Unload(ad2);
В любом домене можно создавать объекты различных типов. Данные действия выполняются при помощи методов CreateInstance(), CreateInstanceFrom(), CreateInstanceAndUnwrap(), CreateInstanceFromAndUnwrap(). Рассмотрим подробнее метод CreateInstance(). Существует несколько перегруженных версий этого метода. В простейшем случае в качестве параметров метода указывается имя сборки и полное имя типа. Самый полный вариант CreateInstance() имеет следующую сигнатуру:
public virtual ObjectHandle CreateInstance(
string assemblyName,
string typeName,
bool ignoreCase,
BindingFlags bindingAttr,
Binder binder,
object[] args,
CultureInfo culture,
object[] activationAttributes,
Evidence securityAttributes);
Опишем параметры метода CreateInstance(). Первый параметр указывает имя сборки, содержащей тип создаваемого объекта. Сборка может быть загружена в домен, хотя это и не обязательно. Второй параметр – строка с полным именем создаваемого типа. При помощи третьего параметра можно сделать поиск типа в сборке регистронезависимым. Четвертый параметр позволяет задать область поиска конструктора типа. Если данный параметр равен нулю, выполняется регистрозависимый поиск public-конструктора типа[8]. Параметр binder позволяет указать особый способ связывания и подстановки вызовов методов и полей. Обычно используется способ по умолчанию (значение параметра – null). Следующим указывается массив, содержащий параметры конструктора. Параметр culture указывает культуру создаваемого объекта (актуально для приложений, настраиваемый на локализацию). Предпоследний параметр позволяет задать массив объектов-атрибутов, участвующих в активации создаваемого объекта. Последний параметр управляет атрибутами безопасности создаваемого объекта.
Метод CreateInstanceFrom() практически идентичен CreateInstance(), но сборка загружается из указанного файла. В следующем фрагменте кода происходит создание нового домена приложения и создание объекта типа Student в этом домене:
ad2.CreateInstanceFrom(@"C:\Students.dll", // Имя файла сборки
"Student", // Имя класса
false, // Регистрозависимый поиск
// Указываем, что ищем конструктор
// Хотя с таким же успехом можно написать 0
BindingFlags.CreateInstance,
null, // Особой привязки не используем
// Параметры конструктора
new object[]{"Ivanov", 20, 4.5},
null, // Остальные параметры игнорируем
null, // или принимаем по умолчанию
null);
Приведенный фрагмент кода демонстрирует создание объекта в другом домене, однако дальнейшая работа с объектом невозможна – мы просто не имеем никакой ссылки на созданный объект. Технология передачи объектов через границы доменов (процессов) называется маршалинг. Пусть имеются два домена – D1 и D2. Предположим, что в домене D1 требуется производить работу с объектом Obj из домена D2. При маршалинге возможна передача объектов как по ссылке, так и по значению. В случае передачи по значению в домене D1 будет создана локальная копия объекта Obj из D2. Естественно, все изменения, внесенные в Obj из D1 не отразятся на Obj из D2.
В случае передачи объекта по ссылке среда исполнения использует специальный объект-прокси (посредник) Pr, который в домене D1 выглядит как объект Obj, но фактически перенаправляет все вызовы своих методов к объекту Obj в домене D2.
Простейший способ осуществить маршалинг объектов по значению – пометить тип как сериализуемый (атрибутом [Serializable]). Пусть таким образом помечен тип Student. В предыдущем примере значение, возвращаемое CreateInstance(), игнорировалось. В следующем фрагменте это значение приводится к типу Student, и демонстрируется работа с элементами созданного объекта:
Как видим, объект был создан в одном домене, но затем копия объекта была перемещена в другой домен, где с ней и происходила работа.
Обратите внимание: метод CreateInstanceFrom() возвращает объект специального класса ObjectHandle. Данный класс реализует «обертки» над реальными объектами, что позволяет не передавать через границы доменов метаданные. Для получения реального объекта используется экземплярный метод ObjectHandle.Unwrap(), который возвращает либо объект, либо прокси. Метод CreateInstanceFromAndUnwrap() выполняет «разворачивание» созданного объекта автоматически.
Чтобы передача объектов некоторого типа между границами доменов происходила по ссылке, требуется, чтобы тип был унаследован от типа MarshalByRefObject. Если допустить, что тип Student унаследован от этого типа, то предыдущий пример выведет на консоль следующее:
[New Domain]: New Student object created
[New Domain]: sayName() is called
My name is Ivanov
[New Domain]: sayName() is called
My name is Petrov
[New Domain]: sayName() is called
My name is Petrov
В завершение приведем таблицу, содержащую некоторые методы и свойства класса System.AppDomain.
Метод позволяет получить данные, сохраненные в домене под неким именем-ключом
Load()
Метод загружает сборки в домен
SetAppDomainPolicy()
Метод для установки политики безопасности домена
SetData()
Метод позволяет сохранить данные в домене под неким именем-ключом
3.2. Архитектура .NET Remoting
В настоящее время все большее распространение получают распределенные приложения. В распределенном приложении (distributed application) отдельные компоненты выполняются на различных компьютерах, которые связаны сетью передачи данных. Как правило, компонент распределенного приложения реализует некий сервис, иначе говоря, предоставляет определенные услуги, доступные путем вызова методов компонента. Сам компонент является объектом некоторого класса. Таким образом, создание распределенного приложения подразумевает возможность вызова на одном компьютере методов объекта, размещенного на другом компьютере.
Введем некоторые термины, которые используются в дальнейшем. Рассматривая распределенное приложение, будем выделять клиент и сервер. Сервер содержит удаленные компоненты, клиент пользуется данными компонентами. Компонентам соответствуют классы. Эти классы будем называть удаленными классами (по отношению к клиенту), а объекты удаленных классов – удаленными объектами.
Для вызова методов удаленных объектов и клиент и сервер могут использовать традиционные подходы сетевого программирования. А именно:
1.Клиент должен обеспечить соединение с сервером и передачу серверу в закодированном виде имени класса, имени метода и параметров метода.
2.Сервер организует прослушивание сообщений от клиентов и их обработку (желательно в отдельных потоках выполнения).
3.Обработка сообщений клиентов подразумевает декодирование имени метода и параметров, фактический вызов соответствующего метода, кодирование результата и отправку его клиенту.
4.Клиент принимает закодированные результаты, декодирует их и возвращает как результат вызова метода.
Предложенное решение обладает рядом существенных недостатков, главный из которых – отсутствие гибкости. Также отметим возможные проблемы, связанные со временем существования удаленных объектов, обработкой исключительных ситуаций и т. п.
Технологии, подобные .NET Remoting[9] (далее для краткости – Remoting), служат универсальным средством для организации работы с удаленными объектами. Remoting тесно интегрирована с исполняющей средой .NET Framework, а также предоставляет множество средств для тонкой настройки своей инфраструктуры.
Рассмотрим основные элементы Remoting, показанные на рисунке 6.
Рис. 6. Основные элементы архитектуры Remoting
Первый элемент архитектуры Remoting – это объект-заместитель или прокси-объект (proxy object). Прокси-объект выполняет следующие задачи. Во-первых, для клиентского кода он выглядит так же, как и любой объект локального класса, что упрощает клиентский код. Во-вторых, все вызовы методов прокси-объект превращает в специальные объекты-сообщения. Сообщение служит для описания метода. Оно, в частности, содержит имя метода, коллекцию входных и выходных параметров. Для того чтобы исполняющая среда (CLR) могла правильно создать прокси-объект, удаленный класс должен быть наследником (прямым или косвенным) класса System.MarshalByRefObject. В дальнейшем подобные классы будем для краткости называть MBR-классами. Кроме этого, клиентскому коду требуется для работы метаданные удаленного класса. Обычно для этого в удаленном классе выделяют интерфейс, который разделяют между сервером и клиентом.
Сообщение, сгенерированное прокси-объектом, попадает в канал (channel). Канал осуществляет коммуникацию между компьютерами по определенному протоколу. Стандартными каналами являются HTTP-канал и TCP-канал (входят в поставку Remoting). При необходимости можно реализовать собственный канал передачи.
С каналом могут быть связаны канальные приемники, при помощи которых осуществляется перехват сообщений. Обязательным элементом канала является форматер (formatter). Задача форматера – сериализовать сообщение, то есть представить его в виде потока данных. В составе Remoting имеется бинарный форматер и SOAP-форматер[10]. HTTP-канал использует по умолчанию SOAP-форматер, TCP-канал – бинарный форматер. При необходимости можно реализовать и собственный форматер данных и канальные приемники. Так как форматер выполняет сериализацию объекта-сообщения, то типы, представляющие входные и выходные параметры метода, должны быть сериализуемыми.
На стороне сервера форматер выполняет десериализацию потока данных из канала, а специальный диспетчер находит и вызывает требуемый метод с указанными фактическими параметрами. Затем диспетчер формирует сообщение с результатами работы метода и передает его в серверный канал. Далее это сообщение попадает в форматер на клиенте, десериализуется, а прокси-объект преобразует сообщение в выходное значение метода.
Такова в общих чертах архитектура Remoting. Отметим, что данная технология является расширяемой. В частности, как уже было сказано, пользователь при желании может реализовать собственные форматеры, каналы и канальные приемники, а также предоставлять собственные прокси-объекты вместо стандартных.
3.3. Активация удаленных объектов и их время жизни
Перед доступом к удаленному объекту он должен быть создан и инициализирован. Данный процесс называется активацией. В Remoting удаленные объекты поддерживают два вида активации: серверную активацию и клиентскую активацию[11].
При серверной активации инфраструктура Remoting регистрирует тип на сервере и назначает ему универсальный идентификатор ресурсов (Uniform Resource Identifier, URI). Так как каждому типу назначен некий известный URI, то такие типы получили название общеизвестных типов (well-known types). Объекты общеизвестных типов далее будут обозначаться как SAO – server activated objects.
В Remoting поддерживаются два режима серверной активации: режим Singleton и режим SingleCall. При использовании режима Singleton существует один объект для обслуживания всех вызовов клиентов. Этот объект создается инфраструктурой на стороне сервера при первой необходимости (при вызове клиентом метода объекта). Будучи активированным, Singleton-объект обслуживает вызовы своих методов от различных клиентов в течение некоторого периода времени (в течение своего времени жизни). Затем этот объект автоматически уничтожается. Singleton-объект может сохранять свое состояние между отдельными вызовами методов.
Следующий пример кода показывает конфигурирование типа на сервере в режиме Singleton:
В коде используется класс System.Runtime.Remoting.RemotingConfiguration для регистрации типа с именем SomeMBRType. Клиент также должен сконфигурировать тип SomeMBRType как общеизвестный в режиме Singleton: