Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции OOP c#.doc
Скачиваний:
44
Добавлен:
22.09.2019
Размер:
3.38 Mб
Скачать

3.8. Объекты-сообщения

При удаленном взаимодействии исполняющая среда манипулирует не ассемблерным кодом вызова методов, а специальными объектами-сообщениями. Для поддержки сообщений предоставлено несколько стандартных интерфейсов. Базовым является интерфейс IMessage (пространство имен System.Runtime.Remoting.Messaging). Любое сообщение – это объект, реализующий данный интерфейс. Интерфейс IMessage устроен просто. Он содержит единственное свойство Properties типа IDictionary для помещения в сообщение любых данных и идентифицирующих их ключей. Для удобства в .NET Framework описаны еще несколько интерфейсов – наследников IMessage. Схема наследования представлена на рисунке 8.

Рис. 8. Схема наследования интерфейсов

В таблице 20 указано назначение интерфейсов и перечислены некоторые их элементы.

Таблица 20

Интерфейсы для сообщений и их элементы

Интерфейс или класс

Имя элемента

Описание

IMessage

Реализуется любым сообщением

IMethodMessage

Сообщения, описывающие работу с методами

Args

Массив объектов, соответствующих аргументам метода

MethodName

Имя метода (строка)

Uri

Универсальный идентификатор (URI) объекта, которому направляется сообщение

TypeName

Строка с полным именем типа объекта, к которому направляется сообщение

IMethodReturnMessage

Сообщение, описывающее результат метода

Exception

Объект исключительной ситуации, если она сгенерирована удаленным объектом

OutArgs

Массив объектов, представляющих выходные параметры метода

ReturnValue

Объект, содержащий значение работы метода

IMethodCallMessage

Сообщение, соответствующее запуску метода

InArgs

Массив объектов, представляющих входные параметры метода

IConstructionCall-Message

Сообщение является первым направляемым к удаленному объекту, и определяет свойства для создания объекта

ActivationType

Тип удаленного объекта

Activator

Свойство для работы с активаторами объекта

IConstructionReturnMessage

Сообщение, реализующее интерфейс, посылается как ответ на IConstructionCallMessage

3.9. Пользовательские канальные приемники

Одна из возможностей расширения .NET Remoting – написание собственного канального приемника. Канальный приемник (channel message sink) выступает в роли перехватчика сообщений или потока данных на стороне клиента или сервера. Укажем некоторые сценарии, требующие использования канальных приемников:

  • Шифрование данных. Необходимы канальные приемники для обработки потока данных на стороне клиента и сервера. На стороне клиента производится шифровка отсылаемого потока, на стороне сервера – расшифровка.

  • Авторизация доступа. На стороне клиента канальный приемник добавляет в сообщение дополнительную информацию, идентифицирующую клиента (имя/пароль). На стороне сервера приемник извлекает данную информацию и в зависимости от ее содержания обрабатывает или отклоняет сообщение.

  • Протоколирование сообщений. На стороне сервера канальный приемник заносит информацию об обработанных сообщениях в специальный файл или базу данных.

В .NET Framework канальные приемники описываются как классы, реализующие интерфейс IClientChannelSink (для клиента) или IServerChannelSink (для сервера). Канальные приемники соединены в цепочку (message sink chain). Каждый приемник хранит информацию о следующем элементе цепочки. Канальный приемник производит предварительную обработку сообщения, передает сообщение следующему приемнику в цепочке, после этого производится пост-обработка исходящего сообщения.

Установкой и настройкой канальных приемников занимаются специальные классы – провайдеры приемников. Это классы, реализующие интерфейс IClientChannelSinkProvider (для клиента) или IServerChannelSinkProvider (для сервера).

Созданные провайдеры подключаются к каналу при помощи конфигурационного файла1. Например, пусть имеется следующий конфигурационный файл (фрагмент):

<channels>

<channel ref="http">

<clientProviders>

<provider type="MySinks.MessageSinkProvider, Client" />

<formatter ref="soap" />

<provider

type="MySinks.ClientChannelSinkProvider, Client" />

</clientProviders>

</channel>

</channels>

В этом случае в структуре http-канала будут находиться такие провайдеры (и соответствующие им приемники):

Рис. 9. Провайдеры в структуре http-канала

Обратите внимание: среди провайдеров имеется стандартный, отвечающий за SOAP-форматирование, а последний провайдер – это элемент http-канала. Подчеркнем, что порядок объявления провайдеров имеет значение.

Работу с канальными приемниками рассмотрим на нескольких примерах. В первом примере создадим серверный приемник для протоколирования сообщений. Начнем с рассмотрения интерфейса System.Runtime.Remoting.Channels.IServerChannelSink.

public interface IServerChannelSink {

IServerChannelSink NextChannelSink { get; }

ServerProcessing ProcessMessage(

IServerChannelSinkStack sinkStack,

IMessage requestMsg,

ITransportHeaders requestHeaders,

Stream requestStream,

out IMessage responseMsg,

out ITransportHeaders responseHeaders,

out Stream responseStream);

void AsyncProcessResponse(

IServerResponseChannelSinkStack sinkStack,

object state,

IMessage msg,

ITransportHeaders headers,

Stream stream);

Stream GetResponseStream(

IServerResponseChannelSinkStack sinkStack,

object state,

IMessage msg,

ITransportHeaders headers);

}

Свойство NextChannelSink содержит ссылку на следующий канальный приемник в цепочке. Оно устанавливается конструктором класса-приемника.

Метод ProcessMessage() – основной метод при синхронной обработке сообщений и потоков. Его параметры:

  • sinkStack – стек серверных канальных приемников, предшествующих вызываемому;

  • requestMsg – сообщение-запрос;

  • requestHeaders – транспортные заголовки сообщения-запроса. Это таблица пар «ключ-значение». При помощи заголовка к сообщению можно присоединять дополнительную описательную информацию;

  • requestStream – поток, который следует обработать и направить в десериализатор;

  • responseMsg – после выполнения метода удаленного объекта этот параметр содержит возвращаемый IMessage;

  • responseHeaders – после выполнения метода удаленного объекта этот параметр содержит транспортные заголовки возвращаемого сообщения (если они есть);

  • responseStream – параметр содержит поток, направляемый клиенту после выполнения метода удаленного объекта.

При реализации тела метода ProcessMessage() нужно действовать по такой схеме. Вначале производится предварительная обработка сообщений или потоков1. После этого обработчик помещается в стек (sinkStack). Стек используется для асинхронной обработки. Затем следует вызвать метод обработки у следующего приемника в цепочке. Далее производится пост-обработка сообщений или потока.

Метод ProcessMessage() возвращает значение из перечисления ServerProcessing:

  • Async – вызов обработан асинхронно, и приемник должен сохранить возвращаемые данные в стеке для дальнейшей обработки;

  • Complete – сервер обработал сообщение в нормальном, синхронном режиме;

  • OneWay – сообщение было обработано, ответ высылаться не будет.

Метод AsyncProcessResponse() возвращает ответ сервера при обработке асинхронного сообщения. Параметры метода:

  • sinkStack – стек серверных канальных приемников, предшествующих серверному транспортному приемнику;

  • state – дополнительная информация, помещенная в стек вместе с объектом-обработчиком;

  • msg – сообщение-ответ;

  • headers – транспортные заголовки возвращаемого сообщения;

  • stream – поток, направляемый в транспортный приемник.

Метод GetResponseStream() создает объект Stream, содержащий объект IMessage, а также нужные пары «ключ-значение» из объекта ITransportHeaders.

Вернемся к нашему примеру. Представим код серверного канального приемника. Для краткости в листинге опущены параметры методов:

using System;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Messaging;

using System.Collections;

using System.IO;

namespace MySink {

public class LogSink : BaseChannelSinkWithProperties,

IServerChannelSink {

private IServerChannelSink nextSink;

public LogSink(IServerChannelSink next) {

nextSink = next;

}

public IServerChannelSink NextChannelSink {

get { return nextSink; }

}

public void AsyncProcessResponse(. . .) {

IMethodReturnMessage msg_ret =

(IMethodReturnMessage)msg;

Console.WriteLine("[{0}]: {1} - return {2}",

DateTime.Now.ToString(),

msg_ret.MethodName,

msg_ret.ReturnValue);

sinkStack.AsyncProcessResponse(msg,headers,stream);

}

public Stream GetResponseStream(...) { return null; }

public ServerProcessing ProcessMessage(. . .) {

IMethodCallMessage msg =

(IMethodCallMessage) requestMsg;

Console.WriteLine("[{0}]: {1}",

DateTime.Now.ToString(),

msg.MethodName);

foreach (DictionaryEntry de in requestHeaders) {

Console.WriteLine("KEY: {0} VALUE: {1}",

de.Key, de.Value);

}

sinkStack.Push(this,null);

ServerProcessing srvProc =

nextSink.ProcessMessage(sinkStack,

requestMsg,

requestHeaders,

requestStream,

out responseMsg,

out responseHeaders,

out responseStream);

IMethodReturnMessage msg_ret =

(IMethodReturnMessage) responseMsg;

Console.WriteLine("[{0}]: {1} - return {2}",

DateTime.Now.ToString(),

msg_ret.MethodName,

msg_ret.ReturnValue);

return srvProc;

}

}

}

Дадим комментарии по коду класса. Наш класс LogSink является наследникам класса BaseChannelSinkWithProperties. Дело в том, что интерфейс IServerChannelSink (как и IClientChannelSink) наследуется от интерфейса IChannelSinkBase, имеющего единственное свойство – словарь Properties. Абстрактный класс BaseChannelSinkWithProperties предоставляет реализацию данного свойства.

Конструктор класса LogSink не выполняет никаких особых действий. Он просто устанавливает указатель на следующий канальный приемник. Вызывать данный конструктор будет провайдер нашего приемника, который и передаст требуемое значение параметра.

Основная логика работы класса сосредоточена в методе ProcessMessage(). Получив сообщений, метод производит вывод на консоль информации о нем. Кроме этого, выводятся данные транспортных заголовков сообщения. Затем объект-приемник помещается в стек. После обработки сообщение передается в следующий канальный приемник. Если этот канальный приемник завершил свою работу, то это значит, что можно производит обработку возвращаемого клиенту сообщения. Об этом сообщении также выводится некоторая информация на консоль.

Метод AsyncProcessResponse() по сути содержит логику пост-обработки сообщения, а затем передает сообщение дальше по цепочке. Так как наш приемник не создает особого потока по сообщению, метод GetResponseStream() просто возвращает null.

Для установки серверного канального приемника необходимо написать класс, реализующий IServerChannelSinkProvider:

public interface IServerChannelSinkProvider {

IServerChannelSinkProvider Next { get; set; }

IServerChannelSink CreateSink(IChannelReceiver channel);

void GetChannelData(IChannelDataStore channelData);

}

Свойство Next хранит ссылку на следующий провайдер в цепочке. В методе CreateSink() создается объект, соответствующий пользовательскому канальному приемнику. При этом в начале вызывается метод для создания стека приемников, а затем пользовательский приемник помещается на вершину стека. При помощи метода GetChannelData() можно получить различные характеристики того канала, с которым ассоциирован приемник.

Создадим провайдер приемника для нашего примера. Поместим его в ту же сборку, что и класс-приемник. Код провайдера приведен ниже:

namespace MySink {

public class LogSinkProvider: IServerChannelSinkProvider {

private IServerChannelSinkProvider nextProvider;

public LogSinkProvider(IDictionary properties,

ICollection providerData) { }

public IServerChannelSinkProvider Next {

get { return nextProvider; }

set { nextProvider = value; }

}

public IServerChannelSink CreateSink(

IChannelReceiver channel) {

IServerChannelSink next =

nextProvider.CreateSink(channel);

return new LogSink(next);

}

public void GetChannelData(

IChannelDataStore channelData) { }

}

}

В классе LogSinkProvider задан пустой конструктор. Наличие у провайдера конструктора с подобной сигнатурой является обязательным требованием. При помощи таких конструкторов можно извлечь данные для настройки провайдера, хранящиеся, например, в конфигурационном файле. Реализация остальных методов типична для провайдера.

Использование указанного канального приемника происходит при помощи следующего конфигурационного файла сервера. Для провайдера указывается его тип (включая имя пространства имен) и имя сборки.

<configuration>

<system.runtime.remoting>

<application>

<channels>

<channel ref="http" port="1234">

<serverProviders>

<formatter ref="soap" />

<provider type="MySink.LogSinkProvider, MySink" />

</serverProviders>

</channel>

</channels>

<service>

<wellknown mode="SingleCall"

type="BSUIR.Calculator, Calc"

objectUri="Calculator.soap" />

</service>

</application>

</system.runtime.remoting>

</configuration>

Сервер и клиент для данного примера можно написать по аналогии с рассматриваемыми в параграфе 4. Вот что выводит на консоль сервер:

Press [enter] to stop server...

[02.10.2005 16:33:04]: Add

KEY: __ConnectionId VALUE: 1

KEY: __IPAddress VALUE: 127.0.0.1

KEY: __RequestUri VALUE: /Calculator.soap

KEY: Content-Type VALUE: text/xml; charset="utf-8"

KEY: __RequestVerb VALUE: POST

KEY: __HttpVersion VALUE: HTTP/1.1

KEY: User-Agent VALUE: Mozilla/4.0+(compatible; MSIE 6.0;

Windows 5.1.2600.0; MS .NET Remoting;

MS .NET CLR 1.1.4322.573 )

KEY: SOAPAction VALUE:

"http://schemas.microsoft.com/clr/nsassem/BSUIR.Calculator/calc#Add"

KEY: Host VALUE: localhost:1234

KEY: __CustomErrorsEnabled VALUE: False

[02.10.2005 16:33:04]: Add - return 7

Рассмотрим другой пример. Пусть требуется производить шифрование потока, передаваемого от сервера к клиенту и наоборот. Для этого требуется реализовать канальные приемники, работающие с потоком, на стороне клиента и на стороне сервера. В нашем учебном примере «шифрование» будет выполняться как применение к каждому байту потока побитовой операции XOR с некой константой.

Начнем с реализации клиентского канального приемника. Рассмотрим интерфейс IClientChannelSink.

public interface IClientChannelSink {

IClientChannelSink NextChannelSink { get; }

void AsyncProcessRequest(IClientChannelSinkStack sinkStack,

IMessage msg,

ITransportHeaders headers,

Stream stream);

void AsyncProcessResponse(

IClientResponseChannelSinkStack sinkStack,

object state, ITransportHeaders headers,

Stream stream);

Stream GetRequestStream(IMessage msg,

ITransportHeaders headers);

void ProcessMessage(IMessage msg,

ITransportHeaders requestHeaders,

Stream requestStream,

out ITransportHeaders responseHeaders,

out Stream responseStream);

}

Основное отличие этого интерфейса от интерфейса IServerChannelSink заключается в наличие отдельных методов для осуществления асинхронного запроса и получения асинхронного ответа. Кроме этого, метод ProcessMessage() не работает со стеком объектов-приемников. В остальном (включая схему использования) интерфейс практически аналогичен IServerChannelSink.

Код клиентского канального приемника EncClientSink представлен ниже (для краткости опущены формальные параметры методов):

using System;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Messaging;

using System.IO;

using System.Collections;

namespace Encryption {

public class EncClientSink: BaseChannelSinkWithProperties,

IClientChannelSink {

private IClientChannelSink nextSink;

public EncClientSink(IClientChannelSink next) {

nextSink = next;

}

public IClientChannelSink NextChannelSink {

get { return nextSink; }

}

public void AsyncProcessRequest(. . .) {

stream =

EncriptionHelper.GetEncryptedStreamCopy(stream);

sinkStack.Push(this,null);

nextSink.AsyncProcessRequest(sinkStack,msg,headers,stream);

}

public void AsyncProcessResponse(. . .) {

stream =

EncriptionHelper.GetDecryptedStreamCopy(stream);

sinkStack.AsyncProcessResponse(headers,stream);

}

public Stream GetRequestStream(. . .) {

return nextSink.GetRequestStream(msg, headers);

}

public void ProcessMessage(. . .) {

requestStream =

EncriptionHelper.GetEncryptedStreamCopy(requestStream);

nextSink.ProcessMessage(msg, requestHeaders,

requestStream,

out responseHeaders,

out responseStream);

responseStream =

EncriptionHelper.GetDecryptedStreamCopy(responseStream);

}

}

}

Для осуществления шифрования и дешифровки клиентский канальный приемник (равно как и серверный) используют методы вспомогательного класса EncriptionHelper.

namespace Encryption {

public class EncriptionHelper {

public static Stream GetEncryptedStreamCopy(Stream inStream) {

Stream outStream = new MemoryStream();

byte[] buf = new byte[1000];

int cnt = inStream.Read(buf,0,1000);

while (cnt > 0) {

for (int i = 0; i < cnt; i++) { buf[i] ^= 123; }

outStream.Write(buf,0,cnt);

cnt = inStream.Read(buf,0,1000);

}

outStream.Position = 0;

return outStream;

}

public static Stream GetDecryptedStreamCopy(Stream inStream) {

// В нашем случае дешифровка и шифровка симметричны

return GetEncryptedStreamCopy(inStream);

}

}

}

Нам необходим клиентский канальный провайдер. Это должен быть класс, реализующий интерфейс IClientChannelSinkProvider:

public interface IClientChannelSinkProvider {

IClientChannelSinkProvider Next { get; set; }

IClientChannelSink CreateSink(IChannelSender channel,

string url,

object remoteChannelData);

}

Код класса для клиентского провайдера тривиален:

public class EncClientSinkProvider:

IClientChannelSinkProvider {

private IClientChannelSinkProvider nextProvider;

public EncClientSinkProvider(IDictionary properties,

ICollection providerData) { }

public IClientChannelSinkProvider Next {

get { return nextProvider; }

set { nextProvider = value; }

}

public IClientChannelSink CreateSink(IChannelSender channel,

string url,

object remoteChannelData)

{

IClientChannelSink next =

nextProvider.CreateSink(channel,url,remoteChannelData);

return new EncClientSink(next);

}

}

Далее представлен листинг серверного приемника канала и провайдера (размещенных в пространстве имен Encryption):

public class EncServerSink : BaseChannelSinkWithProperties,

IServerChannelSink {

private IServerChannelSink nextSink;

public EncServerSink(IServerChannelSink next) {

nextSink = next;

}

public IServerChannelSink NextChannelSink {

get { return nextSink; }

}

public void AsyncProcessResponse(. . .) {

stream =

EncriptionHelper.GetEncryptedStreamCopy(stream);

sinkStack.AsyncProcessResponse(msg,headers,stream);

}

public Stream GetResponseStream(. . .) { return null; }

public ServerProcessing ProcessMessage(. . .) {

requestStream =

EncriptionHelper.GetDecryptedStreamCopy(requestStream);

sinkStack.Push(this,null);

ServerProcessing srvProc =

nextSink.ProcessMessage(sinkStack,

requestMsg,

requestHeaders,

requestStream,

out responseMsg,

out responseHeaders,

out responseStream);

responseStream =

EncriptionHelper.GetEncryptedStreamCopy(responseStream);

return srvProc;

}

}

public class EncServerSinkProvider : IServerChannelSinkProvider {

private IServerChannelSinkProvider nextProvider;

public EncServerSinkProvider(IDictionary properties,

ICollection providerData) { }

public IServerChannelSinkProvider Next {

get { return nextProvider; }

set { nextProvider = value; }

}

public IServerChannelSink CreateSink(IChannelReceiver channel) {

IServerChannelSink next =

nextProvider.CreateSink(channel);

return new EncServerSink(next);

}

public void GetChannelData(IChannelDataStore channelData) { }

}

Выполним подключение разработанных канальных приемников при помощи конфигурационных файлов. Для сервера файл имеет следующий вид:

<configuration>

<system.runtime.remoting>

<application>

<channels>

<channel ref="http" port="1234">

<serverProviders>

<provider type =

"Encryption.EncServerSinkProvider, EncSink" />

<formatter ref="soap" />

<provider type="MySink.LogSinkProvider, MySink" />

</serverProviders>

</channel>

</channels>

<service>

<wellknown mode="SingleCall"

type="BSUIR.Calculator, Calc"

objectUri="Calculator.soap" />

</service>

</application>

</system.runtime.remoting>

</configuration>

Клиентский конфигурационный файл:

<configuration>

<system.runtime.remoting>

<application>

<channels>

<channel ref="http" port="0">

<clientProviders>

<formatter ref="soap" />

<provider type =

"Encryption.EncClientSinkProvider, EncSink"/>

</clientProviders>

</channel>

</channels>

<client>

<wellknown type="BSUIR.Calculator, Calc"

url="http://localhost:1234/Calculator.soap" />

</client>

</application>

</system.runtime.remoting>

</configuration>

Обратите внимание на порядок, в котором провайдеры размещены на клиенте и сервере.

Рассмотрим некоторые возможности расширения предыдущего примера. Представим ситуацию, согласно которой не все клиенты реализуют шифрование. Как на стороне сервера различать клиентов? Решение может быть следующим. Клиент с шифрованием внедряет в заголовок транспортного протокола некоторую служебную информацию, а сервер анализирует ее и либо выполняет дешифровку, либо нет.

Работа с заголовком транспортного протокола выполняется как с таблицей «ключ-значение». По соглашению имена пользовательских ключей в заголовке принято начинаться с "X". Вот как могут выглядеть измененные методы на клиенте:

public void AsyncProcessRequest(. . .) {

headers["X-Encrypted"] = "yes";

stream = EncriptionHelper.GetEncryptedStreamCopy(stream);

sinkStack.Push(this,null);

nextSink.AsyncProcessRequest(sinkStack,msg,headers,stream);

}

public void AsyncProcessResponse(. . .) {

string xencrypted = (string)headers["X-Encrypted"];

if(xencrypted != null && xencrypted == "yes") {

stream =

EncriptionHelper.GetDecryptedStreamCopy(stream);

}

sinkStack.AsyncProcessResponse(headers,stream);

}

public void ProcessMessage(. . .) {

requestStream =

EncriptionHelper.GetEncryptedStreamCopy(requestStream);

requestHeaders["X-Encrypted"] = "yes";

nextSink.ProcessMessage(. . .);

string xencrypted = (string)responseHeaders["X-Encrypted"];

if(xencrypted != null && xencrypted == "yes") {

responseStream =

EncriptionHelper.GetDecryptedStreamCopy(responseStream);

}

}

В коде серверного приемника метод ProcessMessage() должен поместить информацию о том, является ли поток зашифрованным в стек обработчиков, чтобы она могла быть извлечена при асинхронной обработке:

public ServerProcessing ProcessMessage(. . .) {

bool isEncrypted = false;

string xencrypted = (string)requestHeaders["X-Encrypted"];

if(xencrypted != null && xencrypted == "yes") {

requestStream =

EncriptionHelper.GetDecryptedStreamCopy(requestStream);

isEncrypted = true;

}

sinkStack.Push(this, isEncrypted);

ServerProcessing srvProc = nextSink.ProcessMessage(. . .);

if(srvProc == ServerProcessing.Complete ) {

if(isEncrypted) {

responseStream =

EncriptionHelper.GetEncryptedStreamCopy(responseStream);

responseHeaders["X-Encrypted"] = "yes";

}

}

return srvProc;

}

В методе AsyncProcessResponse() на сервере анализируется значение параметра state. Если этот параметр указывает на то, что принятое сообщение было зашифровано, ответ шифруется и в транспортный заголовок помещается необходимая информация:

public void AsyncProcessResponse(. . .) {

bool hasBeenEncrypted = (bool) state;

if(hasBeenEncrypted) {

stream = EncriptionHelper.GetEncryptedStreamCopy(stream);

headers["X-Encrypted"] = "yes";

}

sinkStack.AsyncProcessResponse(msg,headers,stream);

}

4. ADO.NET