Разработка распределенных приложений в Microsoft.NET Framework

         

Создание нестандартного канала


Одним из недостатков стандартных каналов среды Remoting является плохая поддержка асинхронного взаимодействия. Даже при вызове одностороннего метода удаленного объекта сервер и клиент Remoting должны быть запущены одновременно, поскольку оба канала требуют функционирующего соединения по TCP/IP. Поэтому в качестве примера полезной модификации среды Remoting можно рассмотреть создание собственного канала на основе службы MSMQ. В отличие от стандартных каналов TcpChannel и HttpChannel, данный канал обеспечивает асинхронный обмен между клиентом и сервером. Ограничимся реализацией канала для одностороннего удаленного вызова.

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


Рис. 8.6.  Простейший канал Remoting, использующий MSMQ

Данный канал будет позволять асинхронный вызов методов с атрибутом System.Runtime.Remoting.Messaging.OneWayAttribute, причем время работы клиента и сервера может не совпадать.

Вновь создаваемый канал состоит из двух основных классов: клиентcкой части MsmqChannelSender, реализующей интерфейс IChannelSender, и серверной части MsmqChannelReceiver, реализующей интерфейс IChannelReceiver. Указанные классы используют описанные в разделе MSMQ классы MsmqClient и MsmqServer, причем необходимо использовать бинарное форматирование сообщений MSMQ.

// Файл SevaRemotingMsmq.cs using System; using System.IO; using System.Collections; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Messaging; using System.Runtime.Serialization.Formatters.Binary; using System.Messaging; using System.Text.RegularExpressions; // использовать описанное в главе про MSMQ пространство имен // для работы c очередями сообщений using Seva.Msmq; // Набор классов для одностороннего удаленного вызова // на основе Remoting / MSMQ namespace Seva.Remoting.MsmqChannel { // константы с именами свойств сообщения public class MessageProperties { public const string Uri = "__Uri"; public const string ObjectUri = "__ObjectUri"; } Листинг 8.4.

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

msmq://.\Private$\remoting_queue\endpoint где .\Private$\remoting_queue – имя очереди (в данном случае – частной, локальной), а endpoint – идентификатор удаленного объекта.

// базовый класс для MsmqSender и MsmqReceiver public class MsmqBase: MarshalByRefObject, IChannel { // обязательные для интерфейса IChannel свойства public int ChannelPriority { get { return 1;} } public string ChannelName { get{ return "msmq";} } // разделяет URL на идентификаторы для объекта и канала public string Parse(string url, out string objectUri) { return Utils.ParseUrl(url, out objectUri); } } Конструктору класса MsmqChannelSender в параметре clientSinkProvider передается дополнительный поставщик, описанный в файле конфигурации, который следует вставить в цепочку поставщиков. Однако в данном случае клиентский канал может состоять из единственной цепочки сообщения, которая передает сообщение в MSMQ. Хотя в соответствии с идеологией Remoting следовало бы создать отдельную трубу форматирования, и отдельную – транспортную, для простоты примера можно ограничиться одной трубой без поддержки дополнительных поставщиков.

// канал для клиента public class MsmqChannelSender: MsmqBase, IChannelSender { public MsmqChannelSender(IDictionary properties, IClientChannelSinkProvider clientSinkProvider) { // поскольку в данном примере сообщение сразу направляется в MSMQ, // то дополнительные поставщики не поддерживаются if (clientSinkProvider!=null) { throw new NotSupportedException( "Дополнительные поставщики не поддерживаются."); } } Метод CreateMessageSink, единственный в интерфейсе IChannelSender, создает трубу сообщения.

public IMessageSink CreateMessageSink(string url, object channelData, out string objectUri) { // выделить из URL идентификатор для удаленного объекта string remoteobjectUrl = Utils.ParseUrl(url, ChannelName, out objectUri); if (remoteobjectUrl == null) return null; // создание трубы return new MsmqClientChannelSink(remoteobjectUrl); } } // Seva.Remoting.MsmqChannel.Msmq.MsmqSender Класс MsmqClientChannelSink – транспортная труба клиента, передающая сериализованное сообщение в MSMQ.



// клиентская часть трубы канала public class MsmqClientChannelSink: IMessageSink { // клиент MSMQ, передающий объекты с интерфейсом IMessage // ответы данный объект получать не будет, но указать // отличный от void тип ответов необходимо private MsmqClient<IMessage, IMessage> msmqClient; // путь к очереди MSMQ и идентификатор удаленного объекта (endpoint) private string queuePath = null; private string objectUri = null; Конструктор клиентской трубы канала должен создать клиента MSMQ для передачи сообщений.

public MsmqClientChannelSink(string remoteObjectUri) { // выделение из URL пути к очереди и адреса объекта queuePath = Utils.ParseUrl(remoteObjectUri, out objectUri); // создание клиента MSMQ, не ожидающего ответов от сервера // и использующего бинарный класс форматирования msmqClient = new MsmqClient<IMessage, IMessage>(queuePath, null, QueueFormatter.Binary); } public IMessage SyncProcessMessage(IMessage msg) { throw new NotSupportedException( "Поддерживаются только методы с OneWayAttribute."); } Метод AsyncProcessMessage используется для реализации асинхронного вызова. В данном примере он должен передать сообщение Remoting клиенту MSMQ.

public IMessageCtrl AsyncProcessMessage(IMessage message, IMessageSink replySink) { // свойство сообщения c адресом удаленного объекта message.Properties[MessageProperties.ObjectUri] = objectUri; // передача сообщения клиенту MSMQ для его отсылки серверу msmqClient.Send(message); return null; } // данная труба - первая и последняя в цепочке public IMessageSink NextSink { get { return null; } } } // Seva.Remoting.MsmqChannel.MsmqClientChannelSink Принимающая сторона состоит из канала MsmqChannelReceiver.

public class MsmqChannelReceiver: MsmqBase, IChannelReceiver { // Класс-сервер MSMQ для приема сообщений Remoting private MsmqServer<IMessage, IMessage> msmqServer; // Серверная труба канала private IServerChannelSink sink; // Стек труб на стороне сервера private ServerChannelSinkStack stack; // необходимое для реализации интерфейса свойство public object ChannelData { get { return null; } } Десериализация сообщения осуществляется в классе MsmqServer, поэтому цепочка труб сервера состоит только из стандартной трубы диспетчеризации.


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

public MsmqChannelReceiver(IDictionary properties, IServerChannelSinkProvider serverSinkProvider) { // создать сервер MSMQ msmqServer = new MsmqServer<IMessage, IMessage>( properties["queue"].ToString(), QueueFormatter.Binary); msmqServer.ProcessMessage += OnReceive; if (serverSinkProvider!=null) { throw new NotSupportedException( "Поставщики не поддерживаются."); } Поскольку начальной обработкой пришедших сообщений занимается класс MsmqReceiver, а десериализацией – класс MsmqServer, то в конструкторе достаточно создать стандартную трубу диспетчеризации сообщений на сервере.

sink = ChannelServices.CreateServerChannelSinkChain(null, this); // создание стека труб stack = new ServerChannelSinkStack(); stack.Push(sink, null); // Начать ожидание собщений в очереди StartListening(null); } // метод интерфейса IChannelReceiver, возваращает все URL для данного URI public virtual string[] GetUrlsForUri(string objectURI) { return new string[] {objectURI}; } // обработка пришедшего сообщения MSMQ private IMessage OnReceive(Object sender, IMessage request, MessageQueue queueResponse) { return ProcessMessage(request); } // методы, управляющие прослушивание канала public void StartListening(Object data) { msmqServer.BeginReceive(); } public void StopListening(Object data) { msmqServer.EndReceive(); } Листинг 8.5. Метод ProcessMessage обрабатывает пришедшее сообщение .NET Remoting. В данном классе достаточно передать его дальше в трубу канала для передачи, в итоге, диспетчеру сообщений Remoting.

private IMessage ProcessMessage(IMessage request) { IMessage response = null; // если в сообщении не указан URI объекта, то ничего не делать object uri = request.Properties[MessageProperties.ObjectUri]; if (uri == null) return null; string url = uri.ToString(); // Необходимо заполнить свойство сообщения __URI request.Properties[MessageProperties.Uri] = uri; // Передача сообщения в трубу канала Stream responseStream = null; ITransportHeaders responseHeaders = null; sink.ProcessMessage(stack, request, null, null, out response, out responseHeaders, out responseStream); return response; } } // Seva.Remoting.MsmqChannel.MsmqReceiver Класс Utils содержит вспомогательные статические методы для разбора URL.



public static class Utils { // метод выделяет из URL идентификатор объекта и трубы public static string ParseUrl(string url, string channelName, out string objectUri) { objectUri = null; Regex re = new Regex(@"^" + channelName + @"\:\/\/(.+)$"); Match m = re.Match(url); if (!m.Success) return null; string sinkUri = m.Groups[1].Value; Utils.ParseUrl(url, out objectUri); return sinkUri; } // метод выделяет из URL идентификатор объекта и путь к очереди MSMQ public static string ParseUrl(string url, out string objectUri) { string queuePath = null; objectUri = null; Regex r = new Regex(@"^(.*)(\\|\/)(.+)$"); Match m = r.Match(url); if (m.Success) { objectUri = m.Groups[3].Value; queuePath = m.Groups[1].Value; } else { throw new Exception("Не найден идентификатор объекта в " + url); } return queuePath; } } // Seva.Remoting.MsmqChannel.Utils } // Seva.Remoting.MsmqChannel // Файл SevaRemotingMsmq.cs Листинг 8.6. Для компиляции сборки с классом канала можно использовать make файл следующего содержания.

makefile : SevaRemotingMsmq.dll common = SevaRemotingMsmq.cs SevaMsmq.cs SevaRemotingMsmq.dll: $(common) csc /out:SevaRemotingMsmq.dll /t:library $(common) Для использования созданного канала необходимо описать его в файле конфигурации, например, указав его полное имя в разделе <channel>.

<configuration> <system.runtime.remoting> <application> <client> <wellknown type="RemoteService.RemoteServiceOneWay, RemoteService" url="msmq://.\Private$\remoting_queue\service" /> </client> <channels> <channel type="Seva.Remoting.MsmqChannel.MsmqChannelSender, SevaRemotingMsmq"/> </channels> </application> </system.runtime.remoting> </configuration> Пример файла конфигурации сервера.

<configuration> <system.runtime.remoting> <application name="JobServer"> <service> <wellknown mode="SingleCall" type="RemoteService.RemoteServiceOneWay, RemoteService" objectUri="service" /> </service> <channels> <channel type="Seva.Remoting.MsmqChannel.MsmqChannelReceiver, SevaRemotingMsmq" queue = ".\Private$\remoting_queue"/> </channels> </application> </system.runtime.remoting> </configuration> Как видно из приведенного примера, среда .NET Remoting имеет уникальные возможности по модификации своей структуры разработчиком.Но даже в рассмотренном простейшем случае такие изменения требуют как минимум глубокого понимания структуры .NET Remoting.


Содержание раздела