Веб-сервисы, защищенные посредством промежуточного программного обеспечения, ориентированного на обработку сообщений - Подключение к упорядоченному потоку Soap

ОГЛАВЛЕНИЕ

Подключение к упорядоченному потоку Soap

При реализации класса, производного от WebRequest, нужно предоставлять поток для использования сериализацией Soap. Перед отправкой потока запроса Soap любому ресурсу сети посредством одного из новых протоколов передачи сначала нужно получить доступ к этому упорядоченному потоку.

Введенный выше класс, производный от WebRequest, из которого получаются версии MSMQ и MQ, содержит член данных под именем m_RequestStream, имеющий базовый тип Stream, переносящий исходящий поток Soap. Инфраструктура будет вызывать переопределенный метод GetRequestStream() в подходящее время для создания и предоставления данного потока во время фазы исходящей доставки вызова веб-сервиса. Первая попытка будет использовать переменную члена типа MemoryStream.

Из пошагового исполнения потока кода следует, что в порождениях WebRequest нет прямого доступа к потоку запроса, пока не вызвано переопределение GetResponse()(в какой точке поток запроса уже был уничтожен – смотрите пример свойства Length в m_RequestStream):

Рисунок 8. Обращение к потоку запроса Soap

Изучение диаграммы последовательности UML, представленной выше в разборе примеров, подтверждает, что инфраструктура уже закрыла поток в точке, когда он нужен. Нужно придумать другой способ получения доступа к потоку до его уничтожения. Оказывается, что инфраструктура Microsoft вынуждена закрывать методом Close() предоставляемый вами поток, чтобы вытолкнуть данные в сеть – это стандартная модель программирования для WebRequests.

Есть два следующих варианта:
•    Подключение к потоку сразу после сериализации путем создания и использования SoapExtension и сопутствующего атрибута. Можно перехватить этап обработки SoapMessageStage.AfterSerialize для сохранения упорядоченного потока Soap. Это добавит дополнительные классы в набор, вызовет дополнительную переделку (повторно) генерируемого посредника и может породить проблемы при дальнейшей разработке каркаса для включения WSE.
•    Предоставить пользовательский поток и переопределить Close() для изменения поведения потоков – это эффективно откладывает фактическое закрытие потока до тех пор, пока не появится возможность получить доступ к нему. Это предпочтительный подход, так как он лучше подходит для каркаса.

Рисунок 9. MySoapStream

Предоставляется класс под именем MySoapStream, запрещающий фактическое закрытие потока и предоставляющий альтернативу под именем InternalClose(), фактически закрывающий нижележащий поток.

public override void Close() 
{
    // Не закрываем, а перематываем!
    m_Stream.Position = 0;
}

internal void InternalClose()
{
    m_Stream.Close();
}

Меняем реализацию метода GetRequestStream(), чтобы использовать новый тип потока:

public override Stream GetRequestStream()
{   
   if (m_RequestStream == null)
      m_RequestStream = new MySoapStream(new MemoryStream(), true, true, true);
   else
      throw new InvalidOperationException("Request stream already retrieved.");

   return m_RequestStream;       
}

Переработка обработчика MSMQ WebRequest в качестве примера (,) и слегка измененное переопределение GetResponse() (показана версия MSMQ, но применимы обе версии) выглядит так:

public override WebResponse GetResponse() 
{
    …

    // Создаем сообщение с использованием потока
    objMsg.BodyStream = m_RequestStream;
    objMsg.Recoverable = true;

    …

    // Открываем очередь для записи
    objQueue = GetQ(strQueueName);

    // Отправляем сообщение в одиночной внутренней транзакции MSMQ
    objQueue.Send(objMsg, MessageQueueTransactionType.Single);

    …

    // Закрываем поток Soap
    m_RequestStream.InternalClose();

    …
   
    // Ожидаем ответа

    …
}

В данном случае поток может использоваться напрямую в качестве тела сообщения MSMQ с помощью свойства BodyStream последнего. Несмотря на то, что отсутствует эквивалентная прямая связь потоков в MQ, требуется выполнить лишь еще один шаг для считывания потока в массив байтов, записываемый прямо в сообщение MQ.
После отправки сообщения поток закрывается с помощью метода InternalClose().

Подключение новых протоколов к сгенерированным классам посредника

Имея два новых обработчика транспортных протоколов, будем их использовать из класса посредника. Полученный посредник немного отличается от исходной заново сгенерированной формы:

public class Service1 : MySoapClientProtocol
{
    ...       
       
    // Отдельные методы API не изменяются по сравнению с их сгенерированной формой
    [System.Web.Services.Protocols.SoapDocumentMethodAttribute(...)]
    public string[] HelloWorldArr(string name)
    {
           object[] results = this.Invoke("HelloWorldArr", new object[] {name});
        return ((string[])(results[0]));
    }
   
    // Отдельные методы API не изменяются по сравнению с их сгенерированной формой

    ...
       
    // Переопределенные методы для направления сообщений Soap
    protected override WebRequest GetWebRequest(Uri uri)
    {
           // Передает полномочия общему генератору для новых протоколов
        return ProxyCommon.GetWebRequest(uri, this);
    }

    protected override WebResponse GetWebResponse(WebRequest webReq)
    {
        // Передает полномочия общему генератору для новых протоколов
        return ProxyCommon.GetWebResponse(webReq);
    }
}

Посредник теперь получается из MySoapClientProtocol, включая переопределения для вызовов GetWebRequest() и GetWebResponse(). Требуются лишь указанные изменения. Эти изменения будут теряться, и их придется снова применять в случае последующей повторной генерации посредника по любой причине.

Использование нового посредника из клиента

Тяжелая работа окончена, клиенты посредника могут использовать его следующим образом:

localhost.Service1 objHelloWorldService = null;
string strRequestUrl = "msmq://.\\private$\\myreq";
string strResponseUrl = "msmq://.\\private$\\myresp";

try
{
    // Создаем посредник сервиса
    objHelloWorldService = new localhost.Service1();

    // Устанавливаем очередь запроса с помощью Uri
    objHelloWorldService.Url
                  = objHelloWorldService.FormatCustomUri(strRequestUrl);

    // Устанавливаем другую очередь ответа, отличную от "<QUEUENAME>_resp"
    objHelloWorldService.ResponseUrl
                  = objHelloWorldService.FormatCustomUri(strResponseUrl);

    // И время ожидания
    objHelloWorldService.Timeout = vintTimeoutInSecs * 1000;

    // Запускаем метод и распечатываем вывод
    Console.WriteLine(objHelloWorldService.HelloWorld("Simon"));
}
catch(Exception e)
{
    ...
}

Поддержка асинхронной доставки сообщений

Есть множество статей, описывающих, как использовать преимущества асинхронной поддержки в веб-сервисах (подробные данные смотрите в конце статьи), поэтому данная тема здесь подробно не разбирается. Достаточно сказать, что имеются три основных варианта реализации асинхронного вызова:
•    Опрос об окончании
•    Использование обработчиков ожидания
•    Использование обратных вызовов

Независимо от того, какой метод выбирается, все они основаны на доступе к маркеру, возвращаемому им после начала асинхронного вызова. Сгенерированные классы посредника имеют синхронные и асинхронные версии каждого вызова метода – асинхронные версии содержат два метода в форме:

public System.IAsyncResult BeginHelloWorldArr(
    string name,
    System.AsyncCallback callback,
    object asyncState)
{
    return this.BeginInvoke("HelloWorldArr", new object[] {name}, callback,
                           asyncState);
}

public string[] EndHelloWorldArr(System.IAsyncResult asyncResult)
{
    object[] results = this.EndInvoke(asyncResult);
    return ((string[])(results[0]));
}

Вызов метода BeginXXX сразу же возвращает маркер типа IAsyncResult, используемый для опроса или для ожидания окончания запроса.

Не будучи очень сложной, поддержка асинхронных запросов отлично инкапсулирована в сборке AsyncUIHelper, описанной в статье MSDN . Использование данной библиотеки означает, что транспортные протоколы очереди не должны реализовывать асинхронные функции для себя (пока).

Основная особенность поддержки – класс под именем Asynchronizer:

private Asynchronizer m_ssiReturnImage = null;

Вызывающий оператор метода использует асинхронизатор следующим образом, в данном примере сначала перехватывая основанный на делегировании обратный вызов, а затем вызывая стандартный метод ReturnImage():

// Создаем посредник сервиса
objHelloWorldService = new localhost.Service1();

// Устанавливаем URI
objHelloWorldService.Url = objHelloWorldService.FormatCustomUri(vstrURI);

// И время ожидания
objHelloWorldService.Timeout = vintTimeoutInSecs * 1000;

// Создаем новый асинхронный маркер
m_ssiReturnImage = new Asynchronizer(
                             new AsyncCallback(this.ReturnImageCallback),
   objHelloWorldService);

// Начинаем вызов метода
IAsyncResult ar = m_ssiReturnImage.BeginInvoke(
                new ReturnImageEventHandler(objHelloWorldService.ReturnImage),
                null);

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

protected delegate byte[] ReturnImageEventHandler();            

При этом ответы на вызов (в данном случае данные изображения) доступны через член данных в асинхронном маркере:

protected void ReturnImageCallback(IAsyncResult ar)
{
    localhost.Service1 objHelloWorldService
                                        = (localhost.Service1)ar.AsyncState;

    ...

    AsynchronizerResult asr = (AsynchronizerResult) ar;
    byte[] bytImage = (byte[])asr.SynchronizeInvoke.EndInvoke(ar);

    ...
}

Имитация серверной службы

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

Но из-за желания ограничить набор навыков, требуемых для усвоения статьи, выполнимой величиной на стороне сервера используется приложение .NET для обработки сообщений и выработки (пустых) ответов для тестирования приема сообщений клиентов. Сервис служит лишь контрапунктом для разработанного каркаса клиента, и поэтому сервис не делается надежным или масштабируемым – он лишь демонстрирует возможности клиента.

В описываемой не связанной с WSE сфере сервис может быть простым. Его задача – выполнять следующие действия:
•    Считывать конфигурационный файл, содержащий очереди MSMQ / MQ для ожидания
•    Зацикливать очереди, ожидающие сообщения, в течение короткого периода времени
•    Для любых точно идентифицированных сообщений вручную генерировать поток ответа Soap и помещать в очередь ответов для захвата клиентом

Логика содержится внутри одного класса BEService.

Он сначала считывает передаваемый файл app.config для всех параметров уровня приложения (то есть для настроек, заданных внутри узла <appsettings>):

// Считываем параметры конфигурации
NameValueCollection colSettings = ConfigurationSettings.AppSettings;
Каждая очередь, найденная в конфигурационном файле, добавляется в соответствующую область памяти:
string[] arrVals = colSettings.GetValues(strKeyName);

if (arrVals[0].ToLower().IndexOf("msmq://") == 0)
    arrMSMQQueuesToMonitor[intNumMSMQQueues++]
                    = arrVals[0].Replace("msmq://", string.Empty);
else
if (arrVals[0].ToLower().IndexOf("mq://") == 0)
    arrMQQueuesToMonitor[intNumMQQueues++] = arrVals[0].Replace("mq://",
                                                          string.Empty);

Очереди MSMQ и MQ извлекаются из набора, так как имеется задержка опроса (серверная служба в настоящий момент не использует пул потоков). Затем программа входит в следующий простой цикл:

// Ожидаем в нескольких очередях - несколько MSMQ и несколько MQ...
for (;;)
{
    // Сначала обрабатываются очереди MSMQ
    foreach (string strName in arrMSMQQueuesToMonitor)
    {
        WaitOnMSMQQ(strName, intPollDelay);
    }

    // Теперь обрабатываются очереди MQ
    foreach (string strName in arrMQQueuesToMonitor)
    {
        WaitOnMQQ(strName, intPollDelay);
    }
}

Реализация WaitOnMSMQ

Данный метод простой. Сначала вся работа инкапсулируется в блоке попытка-перехват, чтобы перехватить все исключения и отсеять безопасные, такие как истечение срока ожидания сообщения запроса:

try
{
    … // Здесь выполняется работа
}
catch(Exception e)
{
    // Игнорируем истечения срока ожидания, так как они означают лишь отсутствие
// сообщения очереди
    if (e.Message.IndexOf("Timeout for the requested operation has expired") < 0)
        TSLog("Exception caught in WSAltRouteBEFake[MSMQ]: " + e.Message +
                                                    e.StackTrace);
}

Сначала немного ждем:

// Нужно немного подождать
TimeSpan tWaitResp = new TimeSpan(0, 0, 0, 0, vintPollTimerDelay);
 
// Захватываем очередь запросов
MessageQueue objQueue = GetQ(vstrQueueName);       
 
// Ожидаем сообщение
Message objRequestMsg = objQueue.Receive(
    tWaitResp,
    MessageQueueTransactionType.Single);
 
// Закрываем очередь
objQueue.Close();

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

// Получаем содержимое сообщения прямо из BodyStream
string strResp = string.Empty;
byte[] bufIn = new Byte[objRequestMsg.BodyStream.Length];
objRequestMsg.BodyStream.Position = 0;
objRequestMsg.BodyStream.Read(bufIn, 0, (int)msg.BodyStream.Length);
string strCorrelationId = objRequestMsg.Id;

// Закрываем поток
objRequestMsg.BodyStream.Close();

// Получаем очередь ответов – нужно брать стандартную, но
// надеяться, что клиент идентифицировал очередь для обратного вызова?
string strResponseQueue = vstrQueueName + "_resp";
if (objResponseMsg.ResponseQueue != null)
    strResponseQueue = objRequestMsg.ResponseQueue.QueueName;

Затем проверяем, с целью выяснить, является ли данное сообщение известным, и если да, строим сообщение ответа для него. Метод сопоставления или генерации сообщения ответа не особо разумны – они существуют лишь для обслуживания кода ответа клиента.

// Имитируем ответ на сообщение strResp = BuildFakeResponseFromBytes("MSMQ" , bufIn, strResponseQueue);

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

// Если имеется правильный ответ, используем его
if (strResp.Length > 0)
{
    // Получаем очередь ответа
    objQueue = GetQ(strResponseQueue);

    // Отправляем сообщение обратно
    MemoryStream stResp = new MemoryStream();
    stResp.Write(new UTF8Encoding().GetBytes(strResp), 0, strResp.Length);
    Message objMsg = new Message();
    objMsg.BodyStream = stResp;
    objMsg.Recoverable = true;
    objMsg.CorrelationId = strCorrelationId;
   
    // Отправляем сообщение в одиночной внутренней транзакции MSMQ
    objQueue.Send(objMsg, MessageQueueTransactionType.Single);

    // Освобождаем ресурсы в очереди ответов
    objQueue.Close();
}

Здесь снова используется свойство BodyStream сообщения MSMQ, поэтому содержимое строки ответа записывается в поток. Он снова отправляется в виде одиночной транзакции MSMQ, и затем убирается очередь ответов.

Это все, что требуется для успешной базовой сквозной обработки веб-сервиса с помощью очередей. Можно остановиться на этом этапе или использовать очень интересную новую технологию, защищающую сообщения независимо от средства передачи, по которому они передаются.

Как модернизация веб-сервисов (WSE) изменяет шаблон клиента и сервера

WSE – первоначальная реализация Microsoft некоторых из первых спецификаций веб-сервисов GXA 2-го поколения – защита WS, вложения WS (посредством DIME) и маршрутизация WS / передача WS. Он был выпущен в декабре 2002 года, после 4 месяцев бета-тестирования. Волна стандартизации продолжает инициативу по улучшению возможностей взаимодействия / совместимости в стеке веб-сервисов, и реализации данных конкретных стандартов формируют основу для приложений базового уровня для торговых партнеров, базирующихся в интернете.

Хотя ожидается, что Soap посредством транспортного протокола с очередями найдет применение в надежных сетях отдельной организации, это не отрицает вероятность того, что данные потребуется защищать при передаче между корпоративными приложениями (например, конфиденциальные данные, релевантные для расчета зарплаты).

Основной важный аспект WSE коренится в реализации спецификации защиты WS; то есть имеется в виду возможность делать следующее:
•    контролировать "действительное время существования" сообщения Soap,
•    генерировать "маркер имени пользователя" с сопутствующим паролем, идентифицирующим пользователя для проверки,
•    подписывать полезную нагрузку Soap маркером для обеспечения проверки на искажение,
•    шифровать полезную нагрузку тела Soap с помощью симметричного ключа (именуемого "разделеный секрет", так как серверная служба имеет доступ к тому же самому ключу),
•    защищать запросы и ответы Soap для создания сквозного решения.

Данная функциональность является подгруппой того, что WSE способен предоставлять. Не используемые здесь интересные возможности включают в себя использование двоичных маркеров доступа (которые могут содержать билеты Kerberos) и асимметричное шифрование с использованием сертификатов X509.

Так как используется WSE, и WSE реализует спецификацию вложений WS, можно попробовать расширить демонстрационное приложение для изучения возможности добавления двоичных вложений посредством поддержки DIME. Становится ясно, что поддержка DIME не придерживается стандартной модели обработки Soap – ожидается изменение ее реализации в течение следующих 12 месяцев.