|
||||||||||||||||||||||||||||
|
Главa 23Создание распределенных приложений с помощью .NET Remoting В главе 17 были рассмотрены службы Web, которые позволяют вызывать объекты на удаленном сервере. Использование сервера Web и протокола SOAP не всегда достаточно эффективно для приложений интранет. Протокол SOAP означает большие накладные расходы при пересылке большого объема данных. Для быстрых решений интранет можно использовать просто сокеты, как это делалось в предыдущей главе. В "старом мире", как известно, программы писали с использованием DCOM. С помощью DCOM можно вызывать методы на объектах, выполняющихся на сервере. Программная модель всегда является одной и той же, если объекты применяются на сервере или на клиенте. Без DCOM мы вынуждены иметь дело с портами и сокетами, уделяя внимание целевым платформам в связи с возможным различным представлением данных, и создавать специальные протоколы, в которых сообщения посылаются сокету, чтобы, в конце концов, вызвать некоторые методы. DCOM обрабатывает все эти вопросы для программиста. Заменой DCOM является .NET Remoting. В противоположность DCOM .NET Remoting может использоваться также в решениях Интернета, а для них DCOM недостаточно гибок и эффективен. С помощью .NET Remoting можно адаптировать и расширить любую часть архитектуры, поэтому подходит практически для любого удаленного сценария. В этой главе будут рассмотрены: □ Архитектура .NET Remoting □ Каналы, сообщения, приемники □ Создание клиентов и серверов □ Удаленные свойства с конфигурационными файлами □ Возможности расширения рабочей среды □ Использование возможностей удаленного управления в приложениях ASP.NET Прежде всего выясним, что такое .NET Remoting. Что такое .NET RemotingДва выражения могут описать .NET Remoting: Web Services Anywhere и CLR Object Remoting. Рассмотрим, что это означает. Web Services AnywhereВыражение Web Services Anywhere используется в .NET Remoting и означает, что с помощью .NET Remoting службы Web могут применяться в любом приложении с помощью любого транспорта, используя какое угодно кодирование полезной нагрузки. .NET Remoting является предельно гибкой архитектурой. Совместное использование SOAP и HTTP — только способ вызова удаленных объектов. Транспортный канал является подключаемым и может заменяться. Мы получаем каналы HTTP и TCP, представленные классами HttpChannelи TcpChannel. Мы вправе создать транспортные каналы для использования UDP, IPX или механизма общей памяти — выбор полностью зависит от программиста. Кодирование полезной нагрузки также можно заменить. Компания Microsoft предоставляет SOAP и механизмы двоичного кодирования. Можно использовать средство форматирования (форматтер) SOAP с помощью канала НТTР, но и использовать HTTP, применяя двоичный форматтер. Конечно оба эти форматтера можно использоватъ также с каналом TCP. .NET Remoting не только делает возможным использование служб Web в каждом приложении .NET, но позволяет также предложить возможности службы Web в каждом приложении. Не имеет значения, создается ли консольное приложение или приложение для Windows, Windows Service или компонент COM+ — службы Web могут использоваться везде.
CLR Object RemotingCLR Object Remoting везде располагается поверх служб Web. CLR Object Remoting облегчает использование служб Web. Все конструкции языка, такие как конструкторы, делегаты, интерфейсы, методы, свойства и поля, могут использоваться с удаленными объектами. CLR Object Remoting имеет дело с активацией, распределенной идентификацией, временем жизни и контекстами вызова. Обзор .NET Remoting.NET Remoting может использоваться для доступа к объектам в домене другого приложения независимо от того, находятся ли два объекта внутри одного процесса, в разных процессах или на разных системах. Удаленные сборки можно сконфигурировать для локальной работы в домене приложения или как часть удаленного приложения. Если сборка является частью удаленного приложения, то клиент получает для общения прокси вместо реального объекта. Прокси посылает сообщение в канал. Приложения .NET работают внутри домена приложения. Домен приложения можно рассматривать как подпроцесс внутри процесса, Традиционно процессы используются как изолирующая граница. Приложение, выполняющееся в одном процессе, не может получить доступ и разрушить память в другом процессе. Чтобы приложения общались друг с другом, требуется межпроцессная коммуникация. При использовании .NET домен приложения выступает новой границей безопасности внутри процесса, так как код CIL является проверяемым и обеспечивает безопасность типов данных. Различные приложения могут выполняться внутри одного процесса, но внутри различных доменов приложений. Объекты внутри одного домена приложений могут взаимодействовать напрямую. Чтобы получить доступ к объектам в другом домене приложений, требуется прокси.
Прежде чем перейти к внутренней функциональности .NET Remoting, давайте рассмотрим основные элементы архитектуры: □ Удаленный объект является объектом, который выполняется на сервере. Клиент не вызывает методы на этом объекте напрямую, а использует для этого прокси. С помощью .NET легко отличить удаленные объекты от локальных: каждый класс, производный из MarshalByValueObject, никогда не покидает свой домен приложений. Клиент может вызывать методы на удаленном объекте через прокси. □ Канал используется для коммуникации между клиентом и сервером. Существует клиентская и серверная часть канала. С помощью .NET Framework мы получаем два типа каналов, которые общаются через TCP или HTTP. Можно также создать специальный канал, который поддерживает связь с помощью другого протокола. □ Сообщения посылаются в канал. Они создаются для коммуникации между клиентом и сервером и хранят информацию об удаленных объектах, именах вызванных методов и всех аргументах. □ Форматтер определяет, как сообщения передаются в канал. Вместе с .NET Framework мы получаем форматтеры SOAP и двоичный. Форматтер SOAP можно использовать для коммуникации со службами Web, которые не основываются на .NET Framework. Двоичные форматтеры действуют значительно быстрее и могут эффективно использоваться в среде интранет. Конечно, имеется возможность создать специальный форматтер. □ Провайдер форматтера используется для соединения форматтера с каналом. Создавая канал, можно выбрать провайдер форматтера, и этот выбор в свою очередь, определяет форматтер, который будет использоваться для передачи данных в канал. □ Клиент вызывает методы на прокси, а не на удаленном объекте. Существует два типа прокси: прозрачный прокси и реальный прокси. Прозрачный прокси выглядит для клиента как удаленный объект. Клиент может вызывать методы, реализуемые удаленным объектом на прозрачном прокси. В свою очередь, прозрачный прокси вызывает метод Invoke()на реальном прокси. Метод Invoke()использует приемник сообщений для передачи сообщения в канал. □ Приемник сообщений является объектом-перехватчиком. Такие перехватчики имеются как на клиенте, так и на сервере. Приемник ассоциируется с каналом. Реальный прокси использует его для передачи сообщения в канал, поэтому приемник осуществляет некоторый перехват, прежде чем сообщения попадают в канал. □ Клиент может использовать активатор для создания удаленного объекта на сервере или для получения прокси активированного сервером объекта. □ RemotingConfigurationявляется служебным классом для конфигурирования удаленных серверов и клиентов. Этот класс используется для чтения конфигурационных файлов или для динамического конфигурирования удаленных объектов. □ ChannelServicesявляется служебным классом для регистрации каналов и затем — для отправки сообщений в канал. Чтобы получить представление о функциональности, давайте рассмотрим концептуально, как элементы сочетаются друг с другом. Когда клиент вызывает методы на удаленном объекте, он на самом деле вызывает вместо этого методы на прозрачном прокси. Прозрачный прокси выглядит как реальный объект, он реализует открытые методы реального объекта. Прозрачный прокси узнает об открытых методах, используя механизм отражения для считывания метаданных из сборки. Прозрачный прокси, в свою очередь, вызывает реальный прокси. Реальный прокси отвечает за отправку сообщения в канал. Реальный прокси является подключаемым, можно заменить его с помощью специальной реализации, которая применяется для записи журнала для другого способа поиска канала и т.д. Используемая по умолчанию реализация реального прокси находит совокупность (или цепочку) уполномоченных приемников и передает сообщение в первый уполномоченный приемник. Уполномоченный приемник может перехватить и изменить сообщение. Примерами таких приемников являются приемники отладки, системы безопасности, синхронизации. Последний уполномоченный приемник досылает сообщение в канал. Как сообщение передается по линиям связи, зависит от форматтера. Ранее уже упоминалось, что имеются SOAP и двоичный форматтеры, которые также являются подключаемыми. Канал отвечает либо за соединение с принимающим сокетом на сервере, либо за отправку форматированных данных. Со специальным каналом можно производить различные действия, необходимые для передачи данных на другую сторону. Продолжим рассмотрение на серверной стороне. Канал получает форматированные сообщения от клиента и использует форматтер для демаршализации данных SOAP или двоичных данных в сообщениях. Затем канал вызывает приемники серверного контекста, которые, в свою очередь, являются цепочкой приемников, где последний приемник в цепочке продолжает вызов в цепочке объектов приемников контекста. Последний объект приемника контекста вызывает метод в удаленном объекте. Объектные приемники соответствуют объекту, серверные приемники контекста соответствуют контексту. Для доступа к ряду объектных приемников может использоваться единственный приемник контекста .NET Remoting легко модифицируется: можно заменить реальный прокси, добавить объекты приемника, заменить форматтер и канал. Конечно, можно также использовать все, что уже предоставлено. При этом необходимо отметить, что связанные с прохождением через эти слои накладные расходы можно считать почти отсутствующими. Если вы добавите собственную функциональность, накладные расходы будут зависеть от нее. КонтекстыПрежде чем рассматривать возможности .NET Remoting для создания серверов и клиентов, которые общаются в сети, давайте рассмотрим те случаи, когда внутри домена приложения требуется канал, осуществляющий вызов объектов через контексты. При создании компонентов COM+ использовались контексты COM+. Контексты в .NET являются очень похожими. Как уже было сказано, один процесс может иметь несколько доменов приложений. Домен приложения является чем-то типа подпроцесса с границами безопасности и может иметь различные контексты. Контекст используется для группирования объектов с аналогичными требованиями выполнения. Контексты состоят из множества свойств и используются для перехватывания: когда к ограниченному контекстом объекту обращаются из другого контекста, перехватчик может сделать некоторую работу, прежде чем вызов достигнет объекта. Класс, который выводится из MarsnalByRefObject, ограничен доменом приложения. Вне домена приложения требуется прокси для доступа к объекту. Класс, который выводится из ContextBoundObjectограничен контекстом. Вне контекста для доступа к объекту требуется прокси. Ограниченные контекстом объекты могут иметь атрибуты контекста, объект без таких атрибутов создается в контексте создателя. Ограниченный контекстом объект с атрибутами контекста создается в новом контексте или в контексте создателя, если атрибуты являются совместимыми Чтобы понять контексты, необходимо знать некоторые термины: □ Создание домена приложения приводит к возникновению контекста по умолчанию в этом домене. Если вы возьмете экземпляр нового объекта, которому требуются другие свойства контекста, в соответствии с ними создастся новый контекст. □ Атрибуты контекста могут присваиваться классам, производным из ContextBoundObject. Можно создать класс специального атрибута, реализуя интерфейс IContextAttribute. .NET Framework имеет два класса атрибутов контекста: SynchronizationAttributeи ThreadAffinityAttribute. □ Атрибуты контекста определяют свойства контекста, необходимые объекту. Класс свойства контекста реализует интерфейс IContextProperty. Активные свойства предоставляют приемники сообщений в цепочку вызовов. Класс ContextAttribute, который может использоваться как базовый для специальных атрибутов, реализует как IContextProperty, так и IContextAttribute. □ Приемник сообщений является перехватчиком вызова метода. При этом свойства помогают работе приемников сообщений. АктивизацияНовый контекст создается, если экземпляру создаваемого класса требуется контекст, отличный от вызывающего контекста. Классы атрибутов, которые ассоциируются с целевым классом, запрашиваются в том случае, если все свойства текущего контекста в порядке. Если один из этих классов атрибутов присылает false, то среда выполнения запрашивает все классы свойств, связанные с классом атрибута, и создает новый контекст. Среда выполнения запрашивает затем у классов свойств о приемниках, которые они хотят установить. Класс свойства может реализовать интерфейсы IContributeXXXSinkдля содействия объектам приемника. Атрибуты и свойстваКласс атрибута контекста является прежде всего атрибутом. Более подробно можно прочитать об этом в главе 6. Классы атрибутов контекста должны реализовать интерфейс IContextAttribute. Специальный класс атрибута контекста можно вывести из класса ContextAttribute, так как этот класс уже имеет используемую по умолчанию реализацию данного интерфейса. В .NET Framework содержатся два класса атрибутов контекста: System.Runtime.Remoting.Contexts.SynchronizationAttributeи System.Runtime.Remoting.Contexts.ThreadAffinityAttribute. С помощью атрибута ThreadAffinityможно задать, что только один поток выполнения получает доступ к полям экземпляра и методам ограниченного контекстом класса. Это полезно для объектов интерфейса пользователя, так как дескрипторы окон определяются относительно потока выполнения. Атрибут Synchronization, с другой стороны, определяет требования синхронизации. Здесь можно задать, что несколько потоков выполнения не вправе получить доступ к объекту одновременно, но поток выполнения, получающий доступ к объекту, может меняться. С помощью этих атрибутов в конструкторе задаются четыре значения: □ NOT_SUPPORTEDопределяет, что экземпляр класса не должен создаваться в контексте, который имеет либо сходство с потоком выполнения, либо с множеством синхронизации. □ REQUIREDустанавливает, что требуется контекст со сходством с потоком выполнения/синхронизацией. □ REQUIRES_NEWвсегда обеспечивает получение нового контекста. □ SUPPORTEDозначает, что независимо от того, какой контекст мы получаем, объект сможет в нем существовать. Коммуникация между контекстамиКак же происходит коммуникация между контекстами? Клиент использует вместо реального объекта прокси. Оно создает сообщение, которое передается в канал, и приемники могут выполнить перехват. Тот же самый механизм используется для коммуникации между различными доменами приложения или различными системами. Канал TCP или HTTP не требуется для коммуникации между контекстами, но канал здесь, конечно же, есть. Класс CrossContextChannelможет использовать одну и ту же виртуальную память на клиентской и на серверной стороне канала, при этом для соединения контекстов не требуются форматтеры. Удаленные объекты, клиенты и серверыПрежде чем перейти к рассмотрению деталей архитектуры .NET Remoting, давайте рассмотрим кратко удаленный объект и очень маленькое простое клиентское серверное приложение, которое использует этот удаленный объект. Затем мы обсудим более подробно все необходимые шаги и параметры. Реализуемый удаленный объект называется Hello. HelloServerявляется основным классом приложения на сервере, a HelloClientпредназначен для клиента: Удаленные объектыУдаленные объекты требуются для распределенного вычисления. Объект, вызываемый удаленно с другой системы, выводится из объектов System.MarshalByRefObject.MarshalByRefObjectи соответствует домену приложения, в котором они создаются. Это означает, что такие объекты никогда не переходят между доменами приложений; вместо этого используется объект прокси для доступа к удаленному объекту изнутри другого домена приложения. Другой домен приложения может существовать внутри того же процесса, другого процесса, или на другой системе. Удаленный объект имеет распределенную идентичность. В связи с этим ссылка на объект может передаваться другим клиентам, и они также будут получать доступ к тому же объекту. Прокси знает об идентичности удаленного объекта. Класс MarshalByRefObjectимеет в дополнение к унаследованным методам из класса Objectметоды для инициализации и получения служб времени жизни. Службы времени жизни определяют, как долго живут удаленные объекты. Службы времени жизни и арендуемые свойства будут рассмотрены позже в этой главе. Чтобы увидеть .NET Remoting в действии, создается простая библиотека классов для удаленного объекта. Класс Helloвыводится из System.MarshalByRefObject. В конструкторе и деструкторе на консоли записывается сообщение, чтобы было известно о времени жизни объекта. Кроме того, имеется только один метод Greeting(), который будет вызываться от клиента. Для того чтобы легко различать в последующих разделах сборку и класс, дадим им различные имена аргументов, которые используют вызовы метода. Присвоим сборке имя RemoteHello.dll, а классу — Hello. Типом проекта Visual Studio.NET, используемым для этого класса, является Visual C# Class Library: namespace Wrox.ProfessionalCSharp { using System; /// <summary> /// Краткое описание Class1 /// </summary> public class Hello: System.MarshalByRefObject { public Hello() { Console.WriteLine("Constructor called"); } ~Hello() { Console.WriteLine("Destructor called"); } public string Greeting(string name) { Console.WriteLine("Greeting called"); return "Hello, " + name; } } } Простой серверДля сервера используется консольное приложение C#. Для применения класса TcpServerChannelнеобходимо сослаться на сборку System.Runtime.Remoting.dll. Также требуется, чтобы мы ссылались на созданную ранее сборку RemoteHello.dll. В методе Main()создается объект System.Runtime.Remoting.Channel.Тcр.TcpServerChannelс портом номер 8086. Этот канал регистрируется в классе System.Runtime.Remoting.Channels.ChannelServices, чтобы сделать его доступным для удаленных объектов. Тип удаленного объекта регистрируется с помощью System.Runtime.Remoting.RemotingConfiguration.RegisterWellKnownServiceType. Здесь определяется тип класса в удаленном объекте, используемый клиентом URI, и режим. Режим WellKnownObject.SingleCallозначает, что для каждого вызова метода создается новый экземпляр, мы не храним состояние в удаленном объекте. После регистрации удаленного объекта продолжим выполнение сервера, пока не будет нажата клавиша: using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace Wrox.ProfessionalCSharp { /// <summary> /// Краткое описание Class1 /// </summary> public class HelloServer { public static void Main(string[] args) { TcpServerChannel channel = new TcpServerChannel(8086); ChannelServices.RegisterChannel(channel); RemotingConfiguration.RegisterWellKnownServiceType( typeof(Hello), "Hi", WellKnownObjectMode.SingleCall); System.Console.WriteLine("hit to exit"); System.Console.ReadLine(); } } } Простой клиентКлиент также является консольным приложением C#. И здесь делается ссылка на сборку System.Runtime.Remoting.dll, чтобы можно было использовать класс TcpClientChannel. Кроме того, имеется ссылка на сборку RemoteHello.dll. Хотя объект будет создаваться на удаленном сервере, нам понадобится сборка на стороне клиента, чтобы прокси прочитал метаданные во время выполнения. В клиентской программе создается объект TcpClientChannel, который регистрируется в ChannelServices. Для TcpChannelиспользуется конструктор по умолчанию, поэтому выбирается свободный порт. Затем используется класс Activatorдля возврата прокси удаленному объекту. Прокси является типом System.Runtime.Remoting.Proxies._TransparentProxy. Этот объект выглядит как реальный. Это делается с помощью механизма отражения, в котором считываются метаданные реального объекта. Прозрачный прокси использует реальный для пересылки сообщений в канал: using System; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace Wrox.ProfessionalCSharp { /// <summary> /// Краткое описание Class1. /// </summary> public class HelloClient { public static void Main(string[] args) { ChannelServices.RegisterChannel(new TcpClientChannel()); Hello obj = (Hello)Activator.GetObject(typeof(Hello), "tcp://localhost:8086/Hi"); if (obj == null) { Console.WriteLine("could not locate server"); return; } for (int i = 0; i < 5; i++) { Console.WriteLine(obj.Greeting("Christian")); } } } } Когда запустятся сервер и клиентская программа Hello, Christian появится пять раз на клиентской консоли. В консольном окне серверного приложения можно будет увидеть вывод, аналогичный следующему: Первый конструктор вызывается во время регистрации удаленного объекта. Как утверждалось ранее, метод RemotingConfiguration.RegisterWellKnownServiceType()уже создает один экземпляр. Затем для каждого вызова метода создается новый экземпляр, так как был выбран режим активации WellKnownObjectMode.SingleCall. В зависимости от синхронизации и необходимых ресурсов, будут наблюдаться также вызовы деструктора. Если запустить клиент несколько раз, то вызовы деструктора будут присутствовать наверняка. Архитектура .NET RemotingПоказав в действии простой клиент, сервер и изучив архитектуру .NET, перейдем к деталям. На основе созданных ранее программ будут рассмотрены механизмы архитектуры и способы расширения. КаналыКанал используется для коммуникации между клиентом .NET и сервером. Среда .NET поставляется с классами каналов, которые общаются с помощью TCP или HTTP. Можно создать специальные каналы для других протоколов. Канал HTTP применяется большинством служб Web. Он использует для коммуникации протокол HTTP, так как брандмауэры обычно имеют открытым порт 80, чтобы клиенты могли получить доступ к серверам и службам Web. Прием на порту 80 также находится в распоряжении этих клиентов. В Интернете используется и канал TCP, но здесь брандмауэры должны специально конфигурироваться, чтобы клиенты получали доступ к указанному порту канала TCP. Канал TCP по сравнению с HTTP может применяется для коммуникации более эффективно в интранет. Когда выполняется вызов метода на удаленном объекте, объект клиентского канала посылает сообщение удаленному объекту канала. Как серверное, так и клиентское приложения, должны создавать канал. Следующий код показывает, как можно создать TcpServerChannelна серверной стороне: using System.Runtime.Remoting.Channels.Tcp; // ... TcpServerChannel channel = new TcpServerChannel(8086); Порт, который слушает сокет TCP, определяется аргументом конструктора. Серверный канал должен определить общеизвестный порт, а клиент использует его порт при доступе к серверу. Однако для создания TcpClientChannelна клиенте не требуется определять общеизвестный порт. Конструктор по умолчанию для TcpClientChannelвыберет доступный порт, который передается серверу во время соединения, чтобы сервер мог послать данные назад клиенту. Создание нового экземпляра канала немедленно включает сокет на прослушивание состояния, которое можно проверить, вводя в командной строке netstat -а. Каналы HTTP используются аналогично каналам TCP. Определяется порт, где сервер может создать слушающий сокет. У нас также есть конструктор, в котором можно задать, передавая Boolean, что должен использоваться защищенный протокол HTTP. Сервер способен слушать несколько каналов. Здесь создаются каналы как HTTP, так и TCP: using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; using System.Runtime.Remoting.Channels.Http; namespace Wrox.ProfessionalCSharp { /// <summary> /// Краткое описание Class1. /// </summary> public class HelloServer { public static void Main(string[] args) { TcpServerChannel tcpChannel = new TcpServerChannel(8086); HttpServerChannel httpChannel = new HttpServerChannel (8085); // ... Класс канала должен реализовать интерфейс IChannel. Интерфейс IChannelимеет два свойства: □ ChannelIName— только для чтения, которое возвращает имя канала. Имя канала зависит от типа, например, канал HTTP называется HTTP. □ ChannelPriority— только для чтения с условием, что более одного канала используется для коммуникации между клиентом и сервером. Приоритет определяет порядок каналов. На клиенте канал с более высоким приоритетом выбирается первым для соединения с сервером. В зависимости от того, является ли канал клиентским каналом или серверным каналом, реализуются дополнительные интерфейсы. Серверные версии каналов реализуют интерфейс IChannelReceiver, клиентские версии — интерфейс IChannelSender. Классы HttpChannelи TcpChannelиспользуются как для клиентов, так и для серверов. Они реализуют IChannelSenderи IChannelReceiver. Эти интерфейсы являются производными из IChannel. IChannelSenderклиентской стороны имеет в дополнение в IChannelединственный метод, называемый CreateMessageSink(), который возвращает объект, реализующий IMessageSink. Интерфейс IMessageSinkприменяется для размещения синхронных, а также асинхронных сообщений в канале. С помощью интерфейса серверной стороны IChannelReceiverканал можно перевести в режим прослушивания с помощью метода StartListening()и снова остановить с помощью метода StopListening(). Также имеется свойство для доступа к полученным данным. Информацию о конфигурации обоих каналов получают с помощью свойств классов каналов ChannelName, ChannelPriorityи ChannelData. С помощью свойства ChannelDataполучают информацию об URI, который хранится в классе ChannelDataStore. В классе HttpChannelимеется также свойство Scheme. Ниже представлен вспомогательный метод ShowChannelProperties(), демонстрирующий эти данные. protected static void ShowChannelProperties(IChannelReceiver channel) { Console.WriteLine("Name; " + channel.ChannelName"); Console.WriteLine("Priority: " + channel.ChannelPriority); if (channel is HttpChannel) { HttpChannel httpChannel = channel as HttpChannel; Console.WriteLine("Scheme: " + httpChannel.ChannelScheme); } ChannelDataStore data = (ChannelDataStore)channel.ChannelData; foreach (string uri in data.ChannelUris) { Console.WriteLine("URI: " + uri); } Console.WriteLine(); } После создания каналов вызывается метод ShowChannelProperties(): TcpServerChannel tcpChannel = new TcpServerChannel(8086); ShowChannelProperties(tcpChannel); HttpServerChannel httpChannel = new HttpServerChannel(8085); ShowChannelProperties(httpChannel); С помощью каналов TCP и HTTP будет получена следующая информация: Как можно видеть, именем по умолчанию для TcpServerChannelбудет tcp, а канал HTTP называется http. Оба канала имеют свойство по умолчанию, равное 1 (в конструкторах заданы порты 8085 и 8086). URI каналов показывает протокол, имя хоста (в данном случае CNagel) и номер порта. Задание свойств каналаМожно задать все свойства канала в списке с помощью конструктора TcpServerChannel(IDictionary, IServerChannelSinkProvider). Класс ListDictionaryреализует IDictionary, поэтому свойства Name, Priorityи Portзадаются с помощью этого класса. Для использования класса ListDictionaryнеобходимо объявить использование пространства имен System.Collections.Specialized. В дополнение к параметру IDictionaryпередается параметр IServerChannelSinkProvider, в данном случае SoapServerFormatterSinkProviderвместо BinaryServerFormatterSinkProvider, который используется по умолчанию для TCPServerChannel. Реализация по умолчанию класса SoapServerFormatterSinkProviderассоциирует класс SoapServerFormatterSinkс каналом, применяющим SoapFormatterдля преобразования данных передачи: ListDictionary properties = new ListDictionary(); properties.Add("Name", "TCP Channel with a SOAP Formatter"); properties.Add("Priority", "20"); properties.Add("Port", "8086"); SoapServerFormatterSinkProvider sinkProvider = new SoapServerFormatterSinkProvider(); TcpServerChannel tcpChannel = new TcpServerChannel(properties.sinkProvider); ShowChannelProperties(tcpChannel); Вывод, который будет получен из запускаемого на сервере кода, показывает новые свойства канала TCP: Подключаемость каналаМожно создать специальный канал для отправки сообщений с помощью транспортного протокола, отличного от HTTP или TCP, или расширить существующие каналы: □ Посылающая часть должна реализовать интерфейс IChannelSender. Наиболее важным является метод CreateMessageSink(), где клиент посылает URL, с помощью него создается экземпляр соединения с сервером. Здесь должен быть создан приемник сообщений, затем он используется прокси для отправки сообщений в канал. □ Получающая часть должна реализовать интерфейс IChannelReceiver. Необходимо запустить прослушивание с помощью свойства ChannelData, затем ожидать отдельного потока выполнения для получения данных от клиента. После демаршализации сообщения метод ChannelServices.SyncDispatchMessage()может использоваться для отправки сообщения объекту. Форматтеры.NET Framework предоставляет два класса форматтера: □ System.Runtime.Serialization.Formatters.Binary.BinaryFormatter. □ System.Runtime.Serialization.Formatters.Soap.SoapFormatter. Форматтер ассоциируется с каналом из-за наличия объектов приемников форматтера и провайдеров источников форматтера. Оба эти класса форматтера реализуют интерфейс System.Runtime.Remoting.Messaging.IRemotingFormatter, который определяет методы Serialize()и Deserialize()для передачи и приема данных из канала. Форматтер является подключаемым. При создании класса собственного форматтера экземпляр должен ассоциироваться с используемым каналом. Это делается с помощью приемника форматтера и провайдера приемника форматтера. Как было показано ранее, провайдер источника форматтера, например SoapServerFormatterSinkProvider, может передаваться как аргумент при создании канала. Провайдер источника форматтера реализует для сервера интерфейс IServerChannelSinkProvider, а для клиента IClientChannelSinkProvider. Оба эти интерфейса определяют метод CreateSink(), возвращающий источник форматтера, — SoapServerFormatterSinkProviderдаст экземпляр класса SoapServerFormatterSink. На клиентской стороне имеется класс SoapClientFormatterSink, который использует в методах SyncProcessMessage()и AsyncProcessMessage()класс SoapFormatterдля сериализации сообщения. SoapServerFormatterSinkдесериализует сообщение снова с помощью SoapFormatter. Все эти классы приемника и провайдера способны расширяться и заменяться собственными реализациями. ChannelServices и RemotingContigurationСлужебный класс ChannelServicesиспользуется для регистрации каналов в среде выполнения .NET Remoting. С помощью этого класса можно также получить доступ ко всем зарегистрированным каналам. Это крайне полезно, если для конфигурирования канала используются конфигурационные файлы, так как в этом случае канал создается неявно (см. ниже). Канал регистрируется с помощью статического метода ChannelServices.RegisterChannel(). Здесь представлен серверный код для регистрации каналов HTTP и TCP: TcpChannel tcpChannel = new TcpChannel(8086); HttpChannel httpChannel = new HttpChannel(8085); Channel Services.RegisterChannel(tcpChannel); СhannelServices.RegisterChannel(httpChannel); Служебный класс ChannelServicesможно теперь использовать для отправки синхронных и асинхронных сообщений и для отмены регистрации определенных каналов. Свойство RegisteredChannelsвозвращает массив IChannelвсех зарегистрированных каналов. Возможно также и< пользование метода GetChannel()для доступа к определенному каналу по его имени. С помощью ChannelServicesпишется специальная административная утилита для управления каналами. Вот небольшой пример показывающий как можно остановить режим прослушивания канала: HttpServerChannel channel = (HttpServerChannel)ChannelServices.GetChannel("http"); channel.StorListening(null); Класс RemotingConfiguration— другой служебный класс .NET Remoting. На серверной стороне он используется для регистрации типов удаленных объектов активированных на сервере объектов и для маршализации удаленных объектов в ссылочный класс маршализованного объекта ObjRef. ObjRefявляется сериализуемым представлением объекта, которое было послано по линии связи. На клиентской стороне работает RemotingServices, демаршализуя удаленный объект, чтобы создать прокси из объектной ссылки. Вот серверный код для регистрации хорошо известного типа удаленного объекта в RemotingServices: RemotingConfiguration.RegisterWellKnownServiceType( typeof(Hello), // Тип "Hi", // URI WellKnownObjectMode.SingleCall); // Режим Первый аргумент метода RegisterWellKnownServiceType()— Wrox.ProfessionalCSharp.Helloопределяет тип удаленного объекта. Второй аргумент — Hiявляется универсальным идентификатором ресурса удаленного объекта, который используется клиентом для доступа к удаленному объекту. Последний аргумент — режим удаленного объекта. Режим может быть значением перечисления WellKhownObjectMode: SingleCallили Singleton. □ SingleCallсоздается на сервере с помощью метода RemotingConfiguration.RegisterWellKnownServiceТуре()и аргумента WellKnownObjectMode.SingleCall. Это очень эффективно для сервера, так как получается, что не требуется поддерживать ресурсы может быть для тысяч клиентов. □ С помощью режима Singleton объект совместно используется всеми клиентами сервера. Такие объектные типы могут применяться, если желательно предоставить доступ к некоторым данным всем клиентам. Это не должно быть проблемой для читаемых данных, но для данных чтения-записи необходимо знать о вопросах блокировки и масштабируемости. Объект Singletonсоздается на сервере с помощью метода RemotingConfiguration.RegisterWellKnownServiceType()и аргумента WellKnownObjectMode.Singleton. Необходимо убедиться, что данные не могут быть повреждены, когда клиенты получают доступ одновременно, но нужно и проверять, что блокирование сделано достаточно эффективно для достижения требуемой масштабируемости. Сервер для активизированных клиентом объектовЕсли удаленный объект должен хранить состояние для определенного клиента, то можно использовать активизированные клиентом объекты. В следующем разделе будут рассмотрены возможности клиентской стороны. В частности, как вызывать объекты, активизированные сервером, и объекты, активизированные клиентом. На серверной стороне активизированные клиентом объекты должны регистрироваться по-другому, чем объекты, активизированные сервером. Вместо вызова RemotingConfiguration.RegisterWellKnownType()необходимо вызвать RemotingServices.RegisterActivatedServiceType(). С помощью этого метода определяются только типы, но не URI. Причина этого заключается в том, что для активированных клиентом объектов создаются экземпляры различных объектных типов с помощью одного URI. URI для всех активированных клиентом объектов должен быть определен с помощью RemotingConfiguration.ApplicationName: RemotingConfiguration.ApplicationName = "HelloServer"; RemotingConfiguration.RegisterActivatedServiceType(typeof (Hello)); Активизация объектовДля клиентов возможно использование и создание удаленных объектов с помощью класса Activator. Мы можем получить прокси для активированного сервером или хорошо известного удаленного объекта с помощью метода GetObject(). Метод CreateInstance()возвращает прокси для активированного клиентом удаленного объекта. Вместо класса Activatorдля активации удаленных объектов используется также оператор new. Чтобы сделать это, удаленный объект должен конфигурироваться внутри клиента с помощью класса RemotingConfiguration. URL-приложенияВо всех сценариях активации необходимо определять URL удаленного объекта. Этот URL является тем же самым, что и в браузере Web. Первая часть определяет протокол, за которым следует имя сервера или адрес IP, номер порта и URI, определенный при регистрации удаленного объекта на сервере в таком виде: protocol://server:port/URI Мы все время используем в нашем коде два примера URL: определяем протокол httpи tcp, имя сервера localhost, номер порта 8085 и 8086, и URIкак Hi, что дает нам запись: http://localhost:8085/Hi tcp://localhost:8086/Hi Активация хорошо известных объектовusing System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; /// ... TcpClientChannel channel = new TcpClientChannel(); ChannelServices.RegisterChannel(channel); Hello obj = (Hello)Activator.GetObject( typeof(Hello), "tcp://localhost:8086/Hi"); GetObject()является статическим методом класса System.Activator, который вызывает метод RemotingServices.Connect()для возврата объекта прокси удаленному объекту. Первый аргумент определяет тип удаленного объекта. Прокси реализует все открытые и защищенные методы и свойства, так что клиент может вызывать эти методы так же, как для реального объекта. Второй аргумент является URL удаленного объекта. Здесь используется строка tcp://localhost:8086/Hello, где tcp— протокол, localhost:8086— имя хоста и номер порта и, наконец, Hello— это URI объекта, который был определен с помощью RemotingConfiguration.RegisterWellKnownServiceType(). Вместо Activator.GetObject()можно также использовать RemotingServices.Connect(): Hello obj = (Hello)RemotingServices.Connect(typeof(Hello), "tcp://localhost:8086/Hi"); Если вы предпочитаете задать просто оператор new для активизации хорошо известных удаленных объектов, то удаленный объект можно зарегистрировать на клиенте с помощью все того же RemotingConfiguration.RegisterWellKnownClientType(). Здесь понадобятся похожие аргументы: тип удаленного объекта и URI. Теперь можно использовать оператор new, который на самом деле не создает новый удаленный объект, а возвращает прокси аналогично Activator.GetObject(). Если удаленный объект регистрируется с флажком WellKnownObjectMode.SingleCall, правило остается тем же самым: удаленный объект создается с каждым вызовом метода: RemotingConfiguration.RegisterWellKnownClientType( typeof(Hello), "tcp://localhost:8086/Hi"); Hello obj = new Hello(); Активизация объектов, активизированных клиентомУдаленные объекты могут хранить состояние для клиента. Activator.CreateInstance()создает активированный клиентом удаленный объект. С помощью метода Activator.GetObject()удаленный объект создается при вызове метода и разрушается, когда метод заканчивается. Объект не хранит состояние сервера. В этом отличие от Activator.CreateInstance(). С помощью статического метода CreateInstance()запускается последовательность активации для создания удаленного объекта. Этот объект живет, пока не закончится время аренды и не произойдет сборка мусора. Позже мы рассмотрим механизм аренды. Некоторые из перезагруженных методов Activator.CreateInstance()используются только для создания локальных объектов. Для получения удаленных объектов требуется метод, куда можно передавать activationAttributes. Один из таких перезагруженных методов используется в примере. Этот метод получает два строковых параметра, первый из которых является именем сборки, второй — типом, а третий — массивом объектов. В объектном массиве канал и имя объекта определяются с помощью UrlAttribute. Чтобы использовать класс UrlAttribute, должно быть определено пространство имен System.Runtime.Remoting.Activation. object [] attrs = { new UrlAttribute("tcp://localhost:8086/Hello") }; ObjectHandle handle = Activator.CreateInstance("RemoteHello", "Wrox.ProfessionalCSharp.Hello", attrs); if (handle == null) { Console.WriteLine("could not locate server"); return 0; } Hello obj = (Hello)handle.Unwrap(); Console.WriteLine(obj.Greeting("Christian")); Конечно, для активизированных клиентом объектов также возможно использование оператора newвместо класса Activator. Таким образом, мы должны зарегистрировать активизированный клиентом объект с помощью RemotingConfiguration.RegisterActivatedClientType(). В архитектуре активизированных клиентом объектов оператор new не только возвращает прокси, но также создает удаленный объект: RemotingConfiguration.RegisterActivatedClientType( typeof (Hello), "tcp://localhost:8086/HelloServer"); Hello obj = new Hello(); Объекты проксиМетоды Activator.GetObject()и Activator.CreateInstance()возвращают клиенту прокси. На самом деле создается два прокси (прозрачный и реальный). Прозрачный прокси выглядит как удаленный объект, он реализует все открытые методы удаленного объекта, вызывая метод Invoke()реального прокси. Реальный прокси посылает сообщения в канал посредством приемников сообщений. С помощью RemotingServices.IsTransparentProxy()проверяется, является ли объект на самом деле прозрачным прокси. Можно также добраться до реального прокси с помощью RemotingServices.GetRealProxy(). Используя отладчик, легко получить все свойства реального прокси: ChannelServices.RegisterChannel(new TCPChannel()); Hello obj = (Hello)Activator.GetObject(typeof(Hello), "tcp://localhost:8086/Hi"); if (obj == null) { Console.WriteLine("could not locate server"); return 0; } if (RemotingServices.IsTransparentProxy(Obj)) { Console.WriteLine("Using a transparent proxy"); RealProxy proxy = RemotingServices.GetRealProxy(obj); // proxy.Invoke(message); }Подключаемость прокси Реальный прокси может заменяться специально созданным прокси. Специально созданный прокси расширяет базовый класс System.Runtime.Remoting.RealProxy. Мы получаем тип удаленного объекта в конструкторе специального прокси. Вызов конструктора для RealProxyсоздает прозрачный прокси в дополнение к реальному. В конструкторе могут быть доступны зарегистрированные каналы с помощью класса ChannelServicesдля создания приемника сообщений в методе IChannelSender.CreateMessageSink(). Помимо реализации конструктора, специальный канал переопределяет метод Invoke(). В Invoke()получают сообщение, которое затем анализируется и посылается в приемник сообщений. СообщенияПрокси посылает сообщение в канал. На серверной стороне будет сделан вызов метода после анализа сообщения, поэтому давайте рассмотрим сообщения. Имеется несколько классов сообщений для вызова методов, ответов, возврата сообщений и т.д. Все классы сообщений должны реализовывать интерфейс IMessage. Этот интерфейс имеет единственное свойство: Properties. Это свойство представляет словарь, где URIуказывает объект, а вызываемые MethodName, MethodSignature, TypeName, Argsи CallContextявляются пакетами. Ниже представлена иерархия классов и интерфейсов сообщений: Посылаемое реальному прокси сообщение является MethodCall. С помощью интерфейсов IMethodCallMessageи IMethodMessageмы имеем более простой доступ к свойствам сообщения, чем через интерфейс IMessage. Вместо использования интерфейса IDictionaryмы имеем прямой доступ к имени метода, URI, аргументам и т.д. Реальный прокси возвращает ReturnMessageпрозрачному прокси. Приемники сообщенийМетод Activator.GetObject()вызывает RemotingServicesConnect()для соединения с хорошо известным объектом. В методе Connect()происходит Unmarshal(), где создается не только прокси, но и уполномоченные приемники. Прокси использует цепочку уполномоченных приемников для передачи сообщения в канал. Все приемники являются перехватчиками, которые могут изменять сообщение и выполнять некоторые дополнительные действия, такие как создание блокировки, запись события, выполнение проверки безопасности и т.д. Все приемники событий реализуют интерфейс IMessageSink. Такой интерфейс определяет одно свойство и два метода: □ Свойство NextSinkиспользуется приемником для получения следующего приемника и передачи сообщения дальше. □ Для синхронных сообщений вызывается метод SyncProcessMessage()предыдущим приемником или удаленной инфраструктурой. Он имеет параметр IMessageдля отправки и возврата сообщения. □ Для асинхронных сообщений вызывается метод AsyncProcessMessage()предыдущим приемником в цепочке или удаленной инфраструктурой. AsyncProcessMessage()имеет два параметра, куда могут передаваться сообщение и приемник сообщения, который получает ответ. Рассмотрим три доступных для использования приемника сообщения. Уполномоченный приемникМожно получить цепочку уполномоченных приемников с помощью интерфейса IEnvoyInfo. Маршализованная объектная ссылка ObjRefимеет свойство EnvoyInfo, которое возвращает интерфейс IEnvoyInfo. Список уполномоченных приемников создается из серверного контекста, поэтому сервер может добавлять функциональность клиенту. Уполномоченные приемники собирают информацию об идентичности клиента и предают ее серверу. Приемник серверного контекстаКогда сообщение получено на серверной стороне канала, оно передается приемникам серверного контекста. Последний из этих приемников направляет сообщение в цепочку объектных приемников. Объектный приемникОбъектный приемник ассоциируется с определенным объектом. Если объектный класс определяет атрибуты определенного контекста, то для объекта создаются приемники контекста. Передача объектов в удаленные методыТипы параметров для вызовов удаленных методов не ограничены только базовыми типами данных, но могут также быть классами, которые определяет программист. Для удаленных методов различают три типа классов: □ Классы, маршализуемые по значению, обычно сериализуются через канал. Классы, которые должны быть маршализованы, либо реализуют интерфейс ISerializable, либо помечаются с помощью атрибута [Serializable]. Объекты этих классов не имеют удаленной идентичности, так как весь объект маршализуется через канал, а объект, который сериализуется клиенту, является независимым от серверного объекта (или наоборот). Классы, маршализуемые по значению, называются также несвязанными классами, так как они не имеют данных, которые зависят от домена приложения. □ Классы, маршализуемые по ссылке, имеют удаленную идентичность. Объекты не передаются по линиям связи, а вместо этого возвращается прокси. Класс, который маршализуется по ссылке, должен выводиться из MarshalByRefObject. Объекты MarshalByRefObjectназывают объектами, связанными с доменом приложения. Специализированной версией MarshalByRefObjectявляется класс ContextBoundObject: абстрактный класс ContextBoundObjectвыводится из MarshalByRefObject. Если класс выводится из ContextBoundObject, требуется прокси даже в том же самом домене приложения, когда пересекаются границы контекстов. □ Классы, которые не являются сериализуемыми и не выводятся из MarshalByRefObject, не могут использоваться в параметрах открытых методов удаленных объектов. Эти классы связаны с доменом приложения, где они созданы. Такие классы должны использоваться, если класс имеет члены данных, допустимые только в домене приложения, такие как дескриптор файла Win32. Чтобы увидеть маршализацию в действии, изменим удаленный объект для пересылки двух объектов клиенту: пусть класс MySerializedпосылает маршализацию по значению, а класс MyRemoteмаршализует по ссылке. В методах сообщение записывается на консоль, чтобы можно было проверять, сделан ли вызов на клиенте или на сервере. Кроме того, класс Hello изменяется, чтобы возвращать экземпляры MySerilizedи MyRemote: using System; namespace Wrox.ProfessionalCSharp { [Serilizable] public сlass MySerilized { public MySerilized(int val) { a = val; } public void Foo() { Console.WriteLine("MySerialized.Foo called"); } public int A { get { Console.WriteLine("MySerialized A called"); return a; } set { a = value; } } protected int a; } public class MyRemote : System.MarshalByRefObject { public MyRemote(int val) { a = val; } public void Foo() { Console.WriteLine("MyRemote.Foo called"); } public int A { get Сonsole.WriteLine("MyRemote.A called"); return a; } set { a = value; } } protected int a; } /// <summary> /// Краткое описание Class1 /// </summary> public class Hello : System.MarshalByRefObject { public Hello() { Console.WriteLine("Constructor called"); } ~Hello() { Console.WriteLine("Destructor called"); } public string Greeting(string name) { Console.WriteLine("Greeting called"); return "Hello, " + name; } public MySerialized GetMySerilized() { return new MySerialized(4711); } public MyRemote GetMyRemote() { return new MyRemote(4712); } } } Клиентское приложение также необходимо изменить, чтобы увидеть результаты при использовании маршализации объектов по значению и по ссылке. Мы вызываем методы GetMySerialized()и GetMyRemote(), чтобы получить новые объекты и проверить, не используется ли прозрачный прокси. ChannelServices.RegisterChannel(new TcpChannel()); Hello obj = (Hello)Activator.GetObject(typeof(Hello), "tcp://localhost:8086/Hi"); if (obj == null) { Console.WriteLine("could not locate server"); return; } MySerialized ser = obj.GetMySerialized(); if (!RemotingServices.IsTransparentProxy(ser)) { Console.WriteLine("ser is not a transparent proxy"); } ser.Foo(); MyRemote rem = obj.GetMyRemote(); if (RemotingServices.IsTransparentProxy(rem)) { Console.WriteLine("rem is a transparent proxy"); } rem.Foo(); В консольном окне клиента видно, что объект serвызывается на клиенте. Этот объект не является прозрачным прокси, так как он сериализуется клиенту. В противоположность этому, объект remна клиенте является прозрачным прокси. Методы, вызванные для этого объекта, передаются на сервер: В серверном выводе можно видеть, что метод Foo()вызывается с удаленным объектом MyRemote: Направляющие атрибутыУдаленные объекты никогда не передаются по линиям связи в отличие от типов данных значений и сериализуемых классов. Иногда желательно послать данные только в одном направлении. Это особенно важно, когда данные передаются по сета. С помощью COM можно было объявить для аргументов направляющие атрибуты [in], [out]и [in, out], если данные должны посылаться на сервер, клиенту или в обоих направлениях. В C# существуют аналогичные атрибуты как часть языка: параметры методов refи out. Параметры методов refи outмогут использоваться для типов данных значений и для ссылочных типов, которые способны сериализоваться. С помощью параметра refаргумент маршализуется в обоих направлениях, outидет от сервера клиенту, а в отсутствие параметра метода посылает данные серверу. Управление временем жизниКак клиент и сервер определяют, какая возникла проблема и что при этом другая сторона более недоступна? Для клиента ответ может быть коротким. Как только клиент вызывает метод для удаленного объекта, мы получаем исключение типа System.Runtime.Remoting.RemotingException. Необходимо просто обработать это исключение и сделать, например, повторную попытку или записать в журнал, информировать пользователя и т.д. А что же сервер? Когда сервер обнаруживает, что клиент отсутствует (что означает возможность очистить ресурсы, которые он удерживает для клиента)? Если ждать следующего вызова метода с клиента, он может никогда не появиться. В области COM протокол DCOM использовал механизм ping. Клиент посылал на сервер ping с информацией об используемых объектах. Так как клиент мог иметь на сервере сотни используемых клиентов, то, чтобы сделать этот механизм более эффективным, посылалась информация не обо всех объектах, а только о различии с предыдущим ping. Этот механизм был эффективен в LAN, но не подходит для Интернета. Подумайте о тысячах или миллионах клиентов, посылающих ping-информацию на сервер. .NET Remoting использует существенно лучшее масштабируемое решение для управления временем жизни — LDGC (Leasing Distributed Garbage Collector — Сборщик мусора распределенной аренды. Это управление временем жизни активно только для активизированных клиентом объектов. Объекты SingleCallмогут разрушаться после каждого вызова метода, так как они не сохраняют состояние. Активизированные клиентам объекты имеют состояние и нам необходимо знать о ресурсах. Для активизированных клиентом объектов, на которые ссылаются из вне домена приложения, создается аренда. Аренда имеет время аренды. Когда время аренды достигает нуля, аренда заканчивается, удаленный объект отсоединяется и, наконец, мусор убирается. Для управления временем жизни можно сконфигурировать следующие значения: □ LeaseTimeопределяет время, пока не закончится аренда. □ RenewOnCallTimeявляется временем, которое аренда задает для вызова метода, если текущее время аренды имеет меньшее значение. □ Если спонсор недоступен в течение SponsorshipTimeout, то удаленная инфраструктура ищет следующего спонсора. Если больше нет спонсоров, аренда заканчивается. □ LeaseManagerPollTimeопределяет интервал времени, в течение которого менеджер аренды проверяет отслуживший объект.
Обновление арендыКак показано в таблице, время аренды по умолчанию для объекта составляет 300 секунд. Если клиент вызывает метод на объекте, когда аренда истекла, возникает исключение. Если имеется клиент, где удаленный объект может понадобиться на время более 300 с, то существует три способа обновления аренды: □ Неявное обновление делается автоматически, когда клиент вызывает метод на удаленном объекте. Если текущее время аренды меньше, чем значение RenewOnCallTime, то аренда задается как RenewOnCallTime. □ При явном обновлении клиент определяет новое время аренды. Это делается с помощью метода Renew()из интерфейса ILease. Доступ к интерфейсу ILeaseможно получить, вызывая метод GetLifetimeService()на прозрачном прокси. □ Третьей возможностью обновления аренды является спонсорство. Клиент может создать спонсора, который реализует интерфейс ISponsorи регистрирует спонсора в службах аренды с помощью метода Register()из интерфейса ILease. Когда аренда заканчивается, у спонсора запрашивают ее продления. Механизм спонсорства используется, если на сервере требуются долгоживущие удаленные объекты. Классы, используемые для управления временем жизниClientSponsorявляется спонсором, который реализует интерфейс ISponsor. Он применяется на клиентской стороне для продления аренды. С помощью интерфейса ILeaseможно получить всю информацию об аренде, все свойства аренды, а также время и состояние текущей аренды. Состояние определяется с помощью перечисления LeaseState. С помощью служебного класса LifetimeServicesможно получить и задать свойства аренды для всех удаленных объектов в домене приложения. Пример: получение информации об арендеВ этом небольшом примере кода доступ к информации аренды осуществляется с помощью вызова метода GetLifetimeService()на прозрачном прокси. Для интерфейса ILeaseнеобходимо открыть пространство имен System.Runtime.Remoting.Lifetime:
ILease lease = (ILease)obj.GetLifetimeService(); if (lease != null) { Console.WriteLine("Lease Configuration:"); Console.WriteLine( "InitialLeaseTime: " + lease.InitialLeaseTime); Console.WriteLine( "RenewOnCallTime: " + lease.RenewOnCallTime); Console.WriteLine( "SponsorshipTimeout: " + lease.SponsorshipTimeout); Console.WriteLine(lease.CurrentLeaseTime); } В результате получается следующий вывод в окне клиентской консоли: Изменение используемых по умолчанию конфигураций арендыСам сервер может изменить используемую по умолчанию конфигурацию аренды для всех удаленных объектов, используя служебный класс System.Runtime.Remoting.Lifetime.LifetimeServices: LifetimeServices.LeaseTime = TimeSpan.FromMinutes(10); LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(2); Если требуются другие используемые по умолчанию значения параметров времени жизни, в зависимости от типа удаленного объекта, можно изменить конфигурацию аренды удаленного объекта, переопределяя метод InitializeLifetimeService()базового класса MarshalByRefObject: public class Hello : System.MarshalByRefObject { public Hello() { Console.WriteLine("Constructor called"); } ~Hello() { Console.WriteLine("Destructor called"); } public override Object InitializeLifetimeService() { ILease lease = (ILease)base.InitializeLifetimeService(); lеase.InitialLeaseTime = TimeSpan.FromMinutes(10); lease.RenewOnCallTime = TimeSpan.FromSeconds(40); return lease; } } Конфигурация служб времени жизни также задается с помощью конфигурационного файла. Конфигурационные файлыВместо записи конфигурации канала и объекта в исходном коде, можно использовать конфигурационные файлы. Таким способом реконфигурируют канал, добавляют дополнительные каналы и т.д., не изменяя исходный код. Для этого, как и для всех других конфигурационных файлов на платформе .NET. используется XML на основе тех же самых приложений, о которых было написано в главе 10. В те же самые файлы в главе 25 будет добавлена конфигурация системы безопасности. В .NET Remoting имеются атрибуты и элементы XML для конфигурирования канала и удаленных объектов. Файл должен иметь то же самое имя, что и исполнимый файл, за которым следует .config. Для сервера HelloServer.exeконфигурационным файлом будет HelloServer.exe.config. В коде, загружаемом с web-сайта издательства Wrox, можно найти примеры конфигурационных файлов в корневом каталоге примеров с именами clientactivated.config, wellknown.configи wellknownhttp.config. Чтобы воспользоваться ими, переименуйте их, как показано выше, и поместите в каталог, содержащий исполнимый файл. Вот только один пример, как мог бы выглядеть такой файл. Мы рассмотрим все различные конфигурационные параметры: <configuration> <system.runtime.remoting> <application name="Hello"> <service> <wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri="Hi" /> </service> <channels> <channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" /> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" /> </channels> </application> </system.runtime.remoting> </configuration> <configuration>является корневым элементом XML для всех конфигурационных файлов .NET. Все удаленные конфигурации можно найти в подэлементе <system.runtime.remoting>. <application>является подэлементом <system.runtime.remoting>. Посмотрим на основные элементы и атрибуты в <system.runtime.remoting>: □ В элементе <application>определяется имя приложения с помощью атрибута name. На серверной стороне это имя сервера, а на клиентской стороне — имя клиентского приложения. Пример серверной конфигурации <application name="Hellо">определяет имя удаленного приложения Hello, которое используется клиентом как часть URL для доступа к удаленному объекту. □ На сервере элемент <service>используется для определения совокупности удаленных объектов. Он может иметь подэлементы <wellknown>и <activated>вместе с определенным типом удаленного объекта — well known или client-activated. □ Клиентской частью элемента <service>является <client>. Подобно элементу <service>он может иметь подэлементы <wellknown>и <activated>для определения типа удаленного объекта. В отличие от <service> элемент <client> имеет атрибут url для определения URL удаленного объекта. □ <wellknown>является элементом, который используется на сервере и на клиенте для определения хорошо известных удаленных объектов. Серверная часть выглядит так: <wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectURI="Hi" /> □ В то время как атрибут modeможет принимать значения SingleCallили Singleton, typeявляется типом удаленного класса, включая пространство имен Wrox.ProfessionalCSharp.Hello, за которым следует имя сборки RemoteHello. Именем удаленного объекта является objectURI, который зарегистрирован в канале. На клиенте атрибут typeявляется таким же, как и для серверной версии. modeи objectURIне нужны, вместо них используется атрибут urlдля определения пути доступа к удаленному объекту: протокол, имя хоста, номер порта, имя приложения и URI объекта: <wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="tcp://localhost:6791/Hello/Hi" /> □ Элемент <activated>используется для активированных клиентом объектов. С помощью атрибута typeдолжны быть определены тип данных и сборка как для клиентского, так и для серверного приложений: <activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" /> □ Для определения канала, используется элемент <channel>. Это подэлемент <channels>, так что совокупность каналов можно сконфигурировать для одного приложения. Его использование аналогично для клиентов и серверов. Атрибут typeиспользуется для определения типа канала и сборки. Атрибут portявляется номером порта, который нужен только для серверной конфигурации: <channels> <channel type = "System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" /> <channel type = "System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" /> </channels> Конфигурация сервера для хорошо известных объектовЭтот пример файла wellknown.configимеет значение Helloдля свойства Name. Мы используем канал TCP для прослушивания порта 6791, а канал HTTP для прослушивания порта 6792. Класс удаленного объекта — Wrox.ProfessionalCSharp.Helloв сборке RemoteHello.dll, объект в канале называется Hi, и используется режим SingleCall: <configuration> <system.runtime.remoting> <application name="Hello"> <service> <wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri ="Hi" /> </service> <channels> <channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" /> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" /> </channels> </application> </system.runtime.remoting> </configuration> Конфигурация клиента для хорошо известных объектовДля хорошо известных объектов в клиентском конфигурационном файле wellknown.configнеобходимо определить сборку и канал. Типы для удаленного объекта можно найти в сборке RemoteHello.dll, Hiявляется именем объекта в канале, a URI для удаленного типа Wrox.ProfessionalCSharp.Hello— это tcp://localhost:6791/Hi. На клиенте также работает канал TCP, но на клиенте не определяется порт, поэтому выбирается свободный порт. <configuration> <system.runtime.remoting> <application name="Client"> <client url="tcp:/localhost:6791/Hello"> <wellknown type = "Wrox.ProfessionalCSharp.Hello, RemoteHello" url="tcp://localhost:6791/Hello/Hi" /> </client> <channels> <channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" /> </channels> </application> </system.runtime.remoting> </configuration> Внесем небольшое изменение в конфигурационный файл и можем использовать канал HTTP (как видно в wellknownhttp.config): <client url="http://localhost:6792/Hello"> <wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="http://localhost:6792/Hello/Hi" /> </client> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" /> </channels> Серверная конфигурация для активизированных клиентом объектовПреобразуя только конфигурационный файл (который находится в clientactivated.config), можно изменить сервер с активизированных сервером объектов на активизированные клиентом объекты. Здесь определяется подэлемент <activated>элемента <service>. С его помощью для серверной конфигурации должен быть определен атрибут type. Атрибут nameэлемента applicationопределяет URI: <configuration> <system.runtime.remoting> <application name="HelloServer"> <service> <activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" /> </service> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" ports="6788" /> <channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" ports="6789" /» </channels> </application> </system.runtime.remoting> </configuration> Клиентская конфигурация для активизированных клиентом объектовФайл clientactivated.configопределяет активированный клиентом удаленный объект с помощью атрибута urlэлемента <client>и атрибута typeэлемента <activated>: <configuration> <system.runtime.remoting> <application> <client url="http://localhost:6788/HelloServer" > <activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" /> </client> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" /> <channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" /> </channels> </application> </system.runtime.remoting> </configuration> Серверный код, использующий конфигурационные файлыВ серверном коде необходимо сконфигурировать удаленное использование статического метода Configure()из класса RemotingConfiguration. Здесь создаются экземпляры всех определяемых каналов. Может быть мы захотим также узнать о конфигурациях каналов из серверного приложения. Поэтому созданы статические методы ShowActivatedServiceTypes()и ShowWellKnovmServiceTypes(), которые вызываются после загрузки и запуска удаленной конфигурации: public static void Main(string[] args) { RemotingConfiguration.Configure("HelloServer.exe.config"); Console.WriteLine( "Application: " + RemotingConfiguration.ApplicationName); ShowActivatedServiceTypes(); ShowWellKnownServiceTypes(); System.Console.WriteLine("hit to exit"); System.Console.ReadLine(); return; } Эти две функции показывают данные конфигурации хорошо известных и активированных клиентом типов: public static void ShowWellKnownServiceTypes() { WellKnownServiceTypeEntry[] entries = RemotingConfiguration.GetRegisteredWellKnownServiceTypes(); foreach (WellKnownServiceTypeEntry entry in entries) { Console.WriteLine("Assembly: " + entry.AssemblyName); Console.WriteLine("Mode: " + entry.Mode); Console.WriteLine("URI " + entry.ObjectUri); Console.WriteLine("Type: " + entry.TypeName); } } public static void ShowActivatedServiceTypes() { ActivatedServiceTypeEntry[] entries = RemotingConfiguration.GetRegisteredActivatedServiceTypes(); foreach(ActivatedServiceTypeEntry entry in entries) { Console.WriteLine("Assembly: " + entry.AssemblyName); Console.WriteLine("Type: " + entry.TypeName); } } Клиентский код, использующий конфигурационные файлыВ клиентском коде с помощью конфигурационного файла client.exe.configнужно сконфигурировать только удаленные службы. После этого можно использовать оператор new для создания новых экземпляров класса Remoteнезависимо от того, происходит ли работа с активированными сервером или с активированными клиентов удаленными объектами. Но помните, что существует небольшая разница. Для активированных клиентом объектов теперь можно использовать произвольные конструкторы с помощью оператора new. Это невозможно для активированных сервером объектов и не имеет смысла в этом случае: объекты SingleCallне могут иметь состояния, так как они разрушаются вместе с каждым вызовом, объекты Singletonсоздаются только однажды. Вызов произвольных конструкторов полезен только для активированных клиентом объектов, так как только для этого вида объектов оператор new реально вызывает конструктор удаленного объекта: RemotingConfiguration.Configure("HelloClient.exe.config"); Hello obj = new Hello(); if (obj == null) { Console.WriteLine("could not locate server"); return 0; } for (int i=0; i < 5; i++) { Console.WriteLine(obj.Greeting("Christian")); } Службы времени жизни в конфигурационных файлахАренда конфигурации для удаленных серверов также может делаться с помощью конфигурационных файлов приложений. Элемент <lifetime>имеет атрибуты leaseTime, sponsorshipTimeOut, renewOnCallTimeи pollTime: <configuration> <system.runtime.remoting> <application> <lifetime leaseTime="15M" sponsorshipTimeOut="4M" renewOnCallTime="3M" pollTime="30s" /> </application> </system.runtime.remoting> </configuration> Используя конфигурационные файлы, можно изменить удаленную конфигурацию, редактируя файлы вместо работы с исходным кодом. Легко изменить канал для использования HTTP вместо TCP, изменить порт, имя канала, и т. д. С помощью добавления одной строки сервер может слушать два канала вместо одного. Инструменты для файлов удаленной конфигурацииНе обязательно начинать создавать конфигурационный файл XML для .NET Remoting с чистого листа. Для этого существует несколько инструментов: □ При использовании версии .NET Remoting Beta 1 можно найти пример convertconfig.exeв списке примеров Framework SDK. С помощью этого инструмента можно преобразовать использовавшийся ранее компактный формат файлов в новый формат на основе XML. □ С помощью примера configfilegen.exeможно создать конфигурационный файл из сборки. Запустите эту программу без параметров, чтобы увидеть все возможные конфигурации. Следующая командная строка создает активированный клиентом (-а) конфигурационный файл для сервера (-s). configfilegen -ia:RemoteHello.dll -ос:HelloServer.exe.config -s -a Системный администратор использует утилиту .NET Admin, чтобы реконфигурировать существующие конфигурационные файлы. Утилиту .NET Admin можно запустить с помощью команды: mmc mecorcfg.msc При помощи этой утилиты можно изменить значения времени жизни, URI удаленных объектов и свойства канала. Приложения хостингаДо этого момента все примеры серверов выполнялись на автономных (self-hosted) серверах .NET. Автономный сервер должен запускаться вручную. Удаленный сервер .NET может запускаться во множестве других типов приложений. В службе Windows сервер автоматически запускается во время старта, и кроме того, процесс может выполняться с полномочиями системной учетной записи. Создание служб Windows описано в главе 24. Хостинг удаленных серверов в ASP.NETB ASP.NET существует специальная поддержка для серверов .NET Remoting. ASP.NET может использоваться для автоматического запуска удаленных серверов. В противоположность приложениям exe, ASP.NET Remoting использует для конфигурации другой файл. Для того чтобы использовать инфраструктуру Информационного сервера Интернета (IIS) и ASP.NET, необходимо создать класс, произвольный из System.MarshalByRefObject, который имеет конструктор по умолчанию. Использованный ранее код для нашего сервера с целью создания и регистрации канала больше не требуется; это делается средой выполнения ASP.NET. Необходимо только создать виртуальный каталог на сервере Web, который отображает каталог, куда помещается конфигурационный файл web.config. Сборка удаленного класса должна находиться в подкаталоге bin. Чтобы сконфигурировать виртуальный каталог на сервере Web, воспользуйтесь Информационными службами ММС. Выберите Default Web Site и, открыв меню Action, создайте новый виртуальный каталог. Конфигурационный файл web.configна сервере Web должен быть помещен в домашний каталог виртуального сайта Web. Согласно используемой по умолчанию конфигурации IIS, используемый канал слушает порт 80. <configuration> <system.runtime.remoting> <application> <service> <wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri = "HelloService.soap" /> </service> </application> </system.runtime.remoting> </configuration> Клиент может теперь соединиться с удаленным объектом, используя следующий конфигурационный файл. URL, который должен быть определен здесь для удаленного объекта, является локальным хостом сервера Web, за ним следует имя приложения Web RemoteHello, которое было определено при создании виртуального сайта Web и URI удаленного объекта HelloService.soap, определенного в файле web.config. Не обязательно определять порт номер 80, так как это порт по умолчанию для протокола http: <configuration> <system.runtime.remoting> <application> <client url="http:/localhost/RemoteHello"> <wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="http://localhost/RemoteHello/HelloService.soap" /> </client> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" /> </channels> </application> </system.runtime.remoting> </configuration>
Классы, интерфейсы и SOAPSudsВ клиент/серверных примерах, которые были выполнены до сих пор, мы всегда ссылались на удаленные объекты в клиентском приложении. В этом случае копируется код CIL удаленного объекта, хотя нужны только метаданные. Также невозможно, чтобы клиент и сервер программировались независимо. Значительно лучшим способом является использование вместо этого интерфейсов или утилиты SoapSuds.exe. ИнтерфейсыМы имеем четкое разделение клиентского и серверного кода с помощью интерфейсов. Интерфейс просто определяет методы без реализации. Мы отделяем контакт между клиентом и серверов от реализации. Вот необходимые шаги для использование интерфейса: 1. Определить интерфейс, который будет помещен в сборку. 2. Реализовать интерфейс в классе удаленного объекта. Чтобы сделать это, необходимо сослаться на сборку интерфейса. 3. На серверной стороне не требуется больше никаких изменений. Сервер можно программировать и конфигурировать обычным образом. 4. На клиентской стороне сошлитесь на сборку интерфейса вместо сборки удаленного объекта. 5. Клиент может теперь использовать интерфейс удаленного объекта, а не класс удаленного объекта. Объект можно создать с помощью класса Activator, как это делалось ранее. Нельзя при этом использовать new, так как невозможно создать экземпляр самого интерфейса. Интерфейс определяет контракт между клиентом и сервером. Два приложения могут теперь разрабатываться независимо друг от друга. Если при этом придерживаться старых правил COM об интерфейсах (что интерфейсы никогда не должны меняться), то не будет никаких проблем с версиями. SOAPSudsМожно также использовать утилиту soapsuds, чтобы получить метаданные из сборки, soapsudsпреобразовывает сборки в XML Schemas, XML Schemas для погружения классов и другие директивы. Следующая команда преобразует тип Helloиз сборки RemoteHello.dllв сборку HelloWrapper.dll, где генерируется прозрачный прокси, который вызывает удаленный объект. soapsuds -types:Wrox.ProfessionalCSharp.Hello, RemoteHello -oa:HelloWrapper.dll С помощью soapsudsможно также получить информацию о типах непосредственно с выполняющегося сервера: soapsuds -url:http://localhost:6792/hi -oa:HelloWrapper.dll На клиенте теперь можно ссылаться вместо исходной сборки на созданную soapsuds сборку. Некоторые из возможностей перечислены в таблице.
Отслеживание службОтладку и поиск неисправностей в приложениях, использующих .NET, можно проводить через службы удаленного отслеживания. Класс System.Runtime.Remoting.Services.TrackingServiceпредоставляет службу слежения для получения информации о том, когда происходит маршализация и демаршализация, когда вызываются удаленные объекты и разъединяются и т. д. □ С помощью служебного класса TrackingServicesрегистрируется и отменяется регистрация обработчика, который реализует ITrackingHandler. □ Интерфейс ITrackingHandlerвызывается, когда на удаленном объекте или на прокси происходит событие. Можно реализовать три метода в обработчике: MarshaledObject(), UnmarshaledObject()и DisconnectedObject(). Чтобы увидеть службы слежения в действии на клиенте и на сервере, создадим новую библиотеку классов TrackingHandler. Класс TrackingHandlerреализует интерфейс ITrackingHandler. В методах задаются два аргумента: сам объект и ObjRef. С помощью ObjRefвыдается информация об URI, канале и уполномоченных приемниках. Можно также присоединить новые приемники для добавления спонсоров всех вызываемых методов. В данном примере на консоль записывается URI и информация о канале. using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Services; namespace Wrox.ProfessionalCSharp ( public class TrackingHandler : ITrackingHandler { public TrackingHandler() { } public void MarshaledObject(object obj, ObjRef or) { Console.WriteLine("--- Marshaled Object " + obj.GetType() + " ---"); Console.WriteLine("Object URI: " + or.URI); object[] ChannelData = or.ChannelInfo.ChannelData; foreach(object data in ChannelData) { ChannelDataStore dataStore = data as ChannelDataStore; if (dataStore != null) { foreach (string uri in dataStore.ChannelUris) { Console.WriteLine("Channel URI: " + uri); } } } Console.WriteLine("---------"); Console.WriteLine(); } public void UnmarshaledObject(object obj, ObjRef or) { Console.WriteLine("Unmarshal"); public void DisconnectedObject(Object obj) { Console.WriteLine("Disconnect"); } } } Серверная программа изменяется, чтобы регистрировать TrackingHandler. Необходимо добавить только две строки, чтобы зарегистрировать обработчик. using System.Runtime.Remoting.Services; // ... public static void Main(string[] args) { TrackingServices.RegisterTrackingHandler(new TrackingHandler()); TCPChannel channel = new TCPChannel(8086); // ... При запуске сервера первый экземпляр создается во время регистрации хорошо известного типа, и мы получаем следующий вывод. Вызывается MarshaledObject()и выводит тип объекта для маршализации — Wrox.ProfessionalCSharp.Hello. С помощью Object URI мы видим GUID, который, используется внутренне в удаленной среде выполнения для различения определенных экземпляров и URI. С помощью канала URI можно проверить конфигурацию канала. В этом случае именем хоста будет Cnagel: Асинхронная удаленная работаЕсли серверные методы требуют времени для завершения работы и клиенту нужно произвести некоторую другую работу в это время- то необходимо запустить отдельный поток выполнения, чтобы сделать удаленный вызов. Асинхронные вызовы могут делаться на удаленном объекте так же, как они делаются на локальном объекте. Чтобы сделать асинхронный метод, создается делегат GreetingDelegateс тем же аргументом и возвращается значение как метод Greeting()удаленного объекта. Аргумент этого делегата является ссылкой на метод Greeting(). Мы запускаем вызов Greeting(), используя метод делегата BeginInvoke(). Второй аргумент BeginInvoke()является экземпляром: AsyncCallback, определяющим метод НеlloClient.Callback(), который вызывается когда удаленный метод заканчивается. В методе Callback()удаленный вызов заканчивается с помощью EndInvoke(): using System; using System.Runtime.Remoting; namespace Wrox.ProfessionalCSharp { public class HelloClient { private delegate String GreetingDelegate(String name); private statiс string greeting; public static old Main(string[] args) { RemotingConfiguration.Configure("HelloClient.exe.config"); Hello obj = new Hello(); if (obj == null) { Console.WriteLine("could not locate server"); return 0; } // синхронная версия // string greeting = obj.Greeting("Christian"); // асинхронная версия GreetingDelegate d = new GreetingDelegate(obj.Greeting); IAsyncResult ar = d.BeginInvoke("Christian", null, null); // выполнить некоторую работу и затем ждать ar.AsyncWaitHandle.WaitOne(); if (ar.IsCompleted) { greeting = d.EndInvoke(ar); } Console. WriteLine(greeting); } } }
Атрибут OneWayМетод, который возвращает voidи имеет только входящие параметры, может быть помечен атрибутом OneWay. Атрибут OneWayделает метод автоматически асинхронным независимо от того, как вызывает его клиент. Добавление метода TakeAWhile()в класс удаленного объекта RemoteHelloсоответствует созданию метода "породить и забыть". Если клиент вызывает его через прокси, то прокси немедленно возвращает управление клиенту. На сервере метод заканчивается немного позже: [OneWay] public Void TakeAWhile(int ms) { Console.WriteLine("TakeAWhile started"); System.Threading.Thread.Sleep(ms); Console.WriteLine("TakeAWhile finished"); } Удаленное выполнение и событияС помощью .NET Remoting не только клиент может вызывать методы на удаленном объекте через сеть, но и сервер может также вызывать методы на клиенте. Для этого используются детали механизма основных свойств языка: делегаты и события. В принципе это простая архитектура. Сервер имеет объект, который может вызывать клиент, а клиент имеет объект, который в свою очередь может вызывать сервер: □ Удаленный объект на сервере должен объявить внешнюю функцию (делегата) с сигнатурой метода, который клиент будет реализовывать в обработчике. □ Аргументы, которые передаются клиенту с функцией-обработчиком, должны быть маршализуемыми. Поэтому все посылаемые клиенту данные должны быть сериализуемыми. □ Удаленный объект должен также объявить экземпляр функции делегата, модифицированный ключевым словом event. Клиент будет использовать его для регистрации обработчика. □ Клиент должен создать объект приемника с методом обработчика, имеющий такую же сигнатуру, как и определенный делегат, и должен зарегистрировать объект приемника для события на удаленном объекте. Чтобы понять это, рассмотрим пример. Создадим пять классов для всех частей обработки событий в .NET Remoting. Класс Serverявляется удаленным сервером, таким как один из тех, которые уже до этого встречались. Класс Serverбудет создавать канал на основе информации из конфигурационного файла и регистрировать удаленный объект, который реализуется в классе RemoteObjectв удаленной среде выполнения. Удаленный объект объявляет аргументы делегата и порождает события в зарегистрированных функциях обработчика. Аргумент, который передается в функцию обработчика, имеет тип StatusEventArgs. Класс StatusEventArgsдолжен быть сериализуемым, поэтому его можно маршализовать клиенту. Класс Clientпредставляет клиентское приложение. Этот класс создает экземпляр класса EventSinkи регистрирует метод StatusHandler()этого класса как обработчика для делегата в удаленном объекте. EventSinkдолжен быть удаленным объектом, подобным классу RemoteClass, так как этот класс также будет вызываться через сеть. Удаленный объектКласс удаленного объекта реализуется в файле RemoteObject.cs. Класс удаленного объекта должен выводиться из MarshalByRefObjectтак, как было показано в предыдущих примерах. Чтобы сделать возможным для клиента регистрацию обработчика событий, который вызывается из удаленного объекта, необходимо объявить внешнюю функцию с помощью ключевого слова delegate. Мы объявляем делегата StatusEvent()с двумя аргументами: sender(поэтому клиент знает об объекте, который порождает событие) и переменную типа StatusEventArgs. В класс аргумента помещаем всю дополнительную информацию, которую необходимо послать клиенту. Метод, который реализуется на стороне клиента, требует строгих ограничений. Он может иметь только входные параметры, возвращаемые типы, при этом параметры refи outнедопустимы; а типы аргументов должны быть либо [Serializable], либо удаленными (выводимыми из MarshalByRefObject): public delegate void StatusEvent(object sender, StatusEventArgs e); public class RemoteObject : MarshalByRefObject { Внутри класса RemoteObjectобъявляется экземпляр функции делегата Status, модифицированный ключевым словом event. Клиент должен добавить обработчик событий в событие Status, чтобы получить статусную информацию из удаленного объекта: public class RemoteObject : MarshalByRefObject { public RemoteObject() { Console.WriteLine("RemoteObject constructor called"); } public event StatusEvent Status; В методе LongWorking()проверяется, что обработчик событий регистрируется прежде, чем событие порождается с помощью Status(this, е). Чтобы удостовериться, что событие порождается асинхронно, мы получаем событие в начале метода перед выполнением Thread.Sleep()и после Sleep: public void LongWorking(int ms) { Console.WriteLine("RemoteObject: LongWorking() Started"); StatusEventArgs e = new StatusEventArgs("Message for Client: LongWorking() Started"); // породить событие if (Status != null) { Console.WriteLine("RemoteObject: Firing Starting Event"); Status(this, e); } System.Threading.Thread.Sleep(ms); e.Message = "Message for Client: LongWorking() Ending"; // породить событие окончания if (Status != null) { Console.WriteLine("RemoteObject: Firing Ending Event"); Status(this, e); } Console.WriteLine("RemoteObject: LongWorking() Ending"); } } Аргументы событийМы видели в классе RemoteObject, что класс StatusEventArgsиспользуется как аргумент для делегата. С помощью атрибута [Serializable]экземпляр этого класса может передаваться от сервера клиенту. Мы используем простое свойство типа string для пересылки клиенту сообщения: [Serializable] public class StatusEventArgs { public StatusEventArgs(string m) { message = m; } public string Message { get { return message; } set { message = value; } } private string message; } СерверСервер реализуется внутри консольного приложения. Мы ожидаем только, чтобы пользователь завершил работу сервера после чтения конфигурационного файла и настройки канала и удаленного объекта: using System; using System.Runtime.Remoting; namespace Wrox.ProfessionalCSharp { class Server { static void Main(string[] args) { RemotingConfiguration.Configure("Server.exe.config"); Console.WriteLine("Hit to exit"); Console.ReadLine(); } } }Конфигурационный файл сервера Способ создания конфигурационного файла сервера Server.exe.configмы уже обсуждали. Существует только один важный момент. Так как клиент сначала регистрирует обработчик событий и после этого вызывает удаленный метод, то удаленный объект должен сохранять состояние клиента. Можно использовать с событиями объекты SingleCall, поэтому класс RemoteObjectконфигурируется в виде активированного клиентом типа: <configuration> <system.runtime.remoting> <application name="CallbackSample"> <service> <activated type="Wrox.ProfessionalCSharp.RemoteObject, RemoteObject" /> </service> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6791" /> </channels> </application> </system.runtime.remoting> </configuration> Приемник событийПриемник событий реализует обработчик StatusHandler(), который определен в делегате. Как ранее отмечалось, метод может иметь только входные параметры и возвращать только void. Это в точности соответствует требованиям методов [OneWay], как мы видели ранее при рассмотрении асинхронной удаленной работы. StatusHandler()будет вызываться асинхронно. Класс EventSinkдолжен также наследовать из класса MarshalByRefObject, чтобы сделать его удаленным, так как он будет вызывать с сервера удаленным образом: using System; using System.Runtime.Remoting.Messaging; namespace Wrox.ProfessionalCSharp; { public class EventSink MarshalByRefObject { public EventSink() { } [OneWay] public void StatusHandler(object sender, StatusEventArgs e) { Сonsole.WriteLine("EventSink: Event occurred: " + e.Message); } } } КлиентКлиент читает конфигурационный файл клиента с помощью класса RemotingConfiguration. Так было со всеми клиентами, которые создавались до сих пор. Клиент создает локально экземпляр удаленного класса приемника EventSink. Метод, который должен вызываться из удаленного объекта на сервере, передается в удаленный объект: using System; using System.Runtime.Remoting; namespace Wrox.ProfessionalCSharp { class Client { static void Main(string[] args) { RemotingConfiguration.Configure("Client.exe.config"); Различие начинается здесь. Мы должны создать локально экземпляр удаленного класса приемника EventSink. Так как этот класс не будет конфигурироваться элементом <client>, то его экземпляр создается локально. Затем мы получаем экземпляр класса удаленного объекта RemoteObject. Этот класс конфигурируется в элементе <client>, поэтому его экземпляр создается на удаленном сервере: EventSink sink = new EventSink(); RemoteObject obj = new RemoteObject(); Теперь можно зарегистрировать метод обработчика объекта EventSinkна удаленном объекте. StatusEventявляется именем делегата, который был определен на сервере. Метод StatusHandler()имеет те же самые аргументы, которые определены в StatusEvent. Вызывая метод LongWorking(), сервер будет делать обратный вызов в методе StatusHandler()в начале и в конце метода: // зарегистрировать клиентский приемник на сервере — подписаться // на событие obj.Status += new StatusEvent(sink.StatusHandler); obj.LongWorking(5000); Теперь мы более не заинтересованы в получении событий с сервера и отменяем подписку на событие. Следующий раз при вызове LongWorking()никакие события не будут получены. // отменить подписку на событие obj.Status -= new StatusEvent(sink.StatusHandler); obj.LongWorking(5000); Console.WriteLine("Hit to exit"); Console.ReadLine(); } } }Конфигурационный файл клиента Конфигурационный файл для клиента — client.exe.configявляется почти таким же конфигурационным файлом, как и для активированных клиентом объектов. Различие можно найти в определении номера порта для канала. Поскольку сервер должен соединяться с клиентом через известный порт, то необходимо определить номер порта для канала как атрибут элемента <channel>. Не требуется определять раздел <service>для класса EventSink, так как экземпляр этого класса будет создаваться клиентом локально с помощью оператора new. Сервер не получает доступ к этому объекту по его имени, вместо этого он получит маршализированную ссылку на экземпляр: <configuration> <system.runtime.remoting> <application name="Client"> <client url="http://localhost:6791/CallbackSample"> <activated type="Wrox.ProfessionalCSharp.RemoteObject, RemoteObject" /> </client> <channels> <channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="777" /> <channels> </application> </system.runtime.remoting> </configuration> Выполнение программыМы видим результирующий вывод на сервере: конструктор удаленного объекта вызывается один раз, так как имеется активированный клиентом объект. Затем происходит вызов метода LongWorking()и порождение события на клиенте. Следующий запуск метода LongWorking()не порождает событий, так как клиент уже отменил регистрацию своего интереса к событию: В выводе клиента видно, что события достигают его по сети: Контексты вызоваАктивированные клиентом объекты могут сохранять состояние для определенного клиента. Для активированных клиентом объектов на сервере требуются ресурсы. Для активированных сервером объектов SingleCallновый экземпляр создается для каждого вызова экземпляра и никакие ресурсы не удерживаются на сервере, эти объекты не могут хранить состояние для клиента. Для управления состоянием можно держать состояние на клиентском стороне, данные о состоянии этого объекта посылаются с каждым вызовом метода на сервер. Для передачи состояния не требуется изменять сигнатуры всех методов с целью включения дополнительного параметра, так как можно использовать контексты вызова. Контекст вызова перемещается вместе с логическим потоком выполнения и передается с каждым вызовом метода. Логический поток выполнения запускается из вызывающего потока выполнения и перемещается через все вызовы методов, которые запускаются из вызывающего потока выполнения и передаются через различные контексты, различные домены приложений и различные процессы. Можно присвоить данные контексту вызова с помощью метода CallContext.SetData(). Класс с объекта, который используется в качестве данных для метода SetData(), должен реализовать интерфейс ILogicalThreadAffinative. Эти данные можно получить снова в том же логическом потоке выполнения (но, возможно, в другом физического потоке выполнения) с помощью CallContext.GetData(). Для данных контекста вызова здесь создается новая библиотека классов C# с вновь созданным классом CallContextData. Этот класс будет использоваться для передачи некоторых данных от клиента серверу с каждым вызовом метода. Класс, который передается с контекстом вызова, должен реализовать интерфейс System.Runtime.Remoting.Messaging.ILogicalThreadAffinative. Этот интерфейс не имеет метода, это просто отметка для среды выполнения, определяющая, что экземпляры этого класса перемещаются вместе с логическим потока выполнения. Класс CallContextDataтакже помечается атрибутом Serializable, чтобы он мог передаваться по каналу: using System; using System.Runtime.Remoting.Messaging namespace Wrox.ProfessionalCSharp { [Serializable] public class CallContextData : ILogicalThreadAffinative { public CallContextData() { } public string Data { get { return data; } set { data = value; } } protected string data; } } В классе Helloметод Greeting()изменяется так, чтобы можно было получить доступ к контексту вызова. Для использования класса CallContextDataнеобходимо сослаться на созданную ранее сборку CallContextData.dll. Чтобы работать с классом CallContext, должно быть открыто пространство имен System.Runtime.Remoting.Messaging: public string Greeting(string name) { Console.WriteLine("Greeting started"); CallContextData cookie = (CallContextData)CallContext.GetData("mycookie"); if (cookie ! = null) { Console.WriteLine("Cookie: " + cookie.Data); } Console.WriteLine("Greeting finished"); return "Hello, " + name; } В клиентском коде передается информация контекста вызова: CallContextData cookie = new CallContextData(); cookie.Data = "information for the server"; CallContext.SetData("mycookie", cookie); for (int i=0; i< 5; i++) { Console.WriteLine(obj.Greeting("Christian")); } Такой контекст вызова может использоваться для отправки информации о пользователе, имени клиентской системы или просто как уникальный идентификатор, который применяется на серверной стороне для получения из базы данных некоторой информации о состоянии. ЗаключениеВ этой главе мы видели, что .NET Remoting использовать очень легко. Удаленный объект должен просто наследовать из объекта MarshalByRefObject. В серверном приложении требуется только один метод для загрузки конфигурационного файла, чтобы настроить и запустить каналы и удаленные объекты. На клиенте загружается конфигурационный файл и используется оператор new для создания экземпляра удаленного объекта. Также не слишком много работы требуется и в случаях, когда конфигурационные файлы не используются. На сервере создается канал и регистрируется удаленный объект. На клиенте делается канал и используется удаленный объект. Наравне с этими приемами применяют много механизмов из других частей .NET Framework, которые также работают с .NET Remoting, такие как вызов асинхронных методов, выполнение обратных вызовов с помощью ключевых слов delegateи eventи т. д. Таким образом, использование .NET Remoting является очень простым, архитектура достаточно гибкой и по желанию расширяемой. Можно использовать каналы HTTP и TCP, которые также расширяются, или написать новые каналы с самого начала. Существуют форматтер SOAP и двоичный форматтер, но легко можно использовать свой собственный. Также имеется много точек перехвата, где возможно добавление в классы специальной функциональности, которая доставляется с помощью .NET Framework. |
|
||||||||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх | ||||||||||||||||||||||||||||
|