Статьи

Использование услуг с WCF

Windows Communication Foundation (WCF) от Microsoft — эффективная структура для реализации услуг, а также потребителей услуг. Всякий раз, когда вы имеете дело с объектами связи WCF, вы должны обращать внимание на распоряжение ресурсами, которые эти объекты хранят. Однако эти механизмы утилизации не так просты и во многом связаны с тем, как необходимо очищать ресурсы. Как и почему очистка сервисных ресурсов является темой этой статьи, которая состоит из предварительного релиза из предстоящей SOA с книгой .NET & Azure [REF-1], они одинаково актуальны для асинхронных коммуникаций.

Рисунок 1: WCF может использоваться для получения запросов и отправки ответных сообщений в сервисах. Его также можно использовать для отправки запросов и получения ответных сообщений от служб.

Очистка ресурсов

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

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

Правильное удаление и закрытие объекта ICommunicationObject.

Для использования службы с использованием WCF у вас есть два варианта: использовать класс, который наследуется от ClientBase, или подключиться с помощью класса ChannelFactory. Если вы сгенерируете прокси, используя Add Service Reference или SvcUtil, вы получите клиентский класс [ServiceName], который наследуется от универсального класса ClientBase. И ClientBase, и ChannelFactory реализуют интерфейс ICommunicationObject, который, в свою очередь, определяет метод Close ().

Метод ICommunicationObject.Close ()

Согласно документации MSDN метода Close (), объект, реализующий ICommunicationObject, не перейдет в закрытое состояние сразу после вызова метода Close (). Вместо этого вызов метода Close () запустит плавный переход, который работает следующим образом:

ICommunicationObject сразу же переходит в состояние закрытия (определяется значением перечисления CommunicationState.Closing ), как только вызывается метод Close (). Он останется в этом состоянии до тех пор, пока не будет завершена незавершенная работа, такая как отправка или получение буферизованных сообщений. По завершении этой работы состояние объекта ICommunicationObject будет изменено на закрытое состояние (определяется значением перечисления CommunicationState.Closed ), а затем возвращается метод Close ().

Другое очень интересное свойство метода Close () заключается в том, что он может генерировать исключения. Документированные исключения

TimeoutException; будет сброшено, если истекло время ожидания закрытия до возврата метода Close ().
CommunicationObjectFaultedException; будет сгенерирован, если метод Close () был вызван для объекта ICommunicationObject, который находится в состоянии Failed (определяется значением перечисления CommunicationState.Fapted ).

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

 Рисунок 2: постепенный переход метода Close () в закрытое состояние.

Метод ICommunicationObject.Abort ()

Интерфейс ICommunicationObject также определяет метод Abort (), использование этого метода, очевидно, несколько отличается от использования метода Close (). Abort () также переведет объект ICommunicationObject в закрытое состояние. Однако этот переход не будет корректным, как в случае метода Close (), вместо этого любая незавершенная работа игнорируется или завершается, и состояние будет изменено на Закрыто до возврата метода.

Abort () и Close ().

На практике есть только два различия между вызовом методов Abort () и Close ():

1. Метод Close () может генерировать исключения, а Abort () не может генерировать исключения.
2. Метод Close () позволяет завершить незавершенную работу в течение заданного времени ожидания, а метод Abort () немедленно прервет любую незавершенную работу.

Это означает, что если вы точно знаете, что вся начатая вами работа завершена, то вызов метода Abort () может быть хорошим выбором. Это также означает, что если есть вероятность, что некоторые из ваших работ не были завершены, вызов метода Close () является более разумным выбором.

Довольно просто создать сервис, который наглядно демонстрирует разницу между Close () и Abort (). Создайте службу, которая взаимодействует по каналу WS-SecureConversation, и клиент, который подключается к нему. Проследите взаимодействие с SvcTraceViewer и посмотрите, что происходит после вызова методов Close () и Abort () соответственно. Когда вы вызываете метод Close (), изящный переход позволит отправить еще несколько сообщений, чтобы удалить токен диалога. Эти дополнительные сообщения не будут отправлены, если вы вместо этого вызовете метод Abort ().

IDisposable для очистки ресурсов

Интерфейс IDisposable указывает только один метод: Dispose (). Единственной целью этого метода является освобождение и очистка любых неуправляемых ресурсов, которые использовал объект.

Проверенная практика — всегда вызывать метод Dispose (), как только вы закончите работать с классом, реализующим IDisposable. Причиной этого является то, что у разработчика класса, вероятно, была хорошая причина для реализации интерфейса IDisposable. Как правило, классы, которые реализуют интерфейс IDisposable, используют неуправляемые ресурсы, такие как память или соединения, а обеспечение способа простого удаления этих ресурсов предназначено для того, чтобы вам было легче писать код, который использует эти классы, не сталкиваясь с проблемами. В некоторых случаях вы можете точно знать, что метод Dispose () ничего не реализует (например, проверяя исходный код с помощью .Net Reflector от Red-Gate), но даже в этих случаях вам все равно рекомендуется вызывать метод Dispose () из вашего кода.Это позволяет вам легко обновить вашу программу, чтобы использовать более новую версию класса с минимальными усилиями. В новой версии могут использоваться ресурсы, которые необходимо утилизировать, и, следовательно, фактически иметь логику, реализованную в методе Dispose (). Если вы не вызвали метод Dispose (), вы можете получить много непредвиденных проблем с более новой версией.

IDisposable и его связь с ClientBase и ChannelFactory

В своем первом воплощении интерфейс ICommunicationObject реализовал интерфейс IDisposable, но в его текущей версии это не так. Однако и ClientBase, и ChannelFactory реализуют интерфейс IDisposable. Таким образом, и ClientBase, и ChannelFactory должны вызывать либо метод Abort (), либо метод Close () изнутри своих реализаций метода Dispose (). На самом деле, команда WCF пыталась вызвать оба метода в разных версиях инфраструктуры WCF.

В очень ранней версии WCF (тогда WCF назывался Indigo) метод Abort () вызывался из метода Dispose (). По словам команды Indigo, это была первая и самая главная жалоба на бета-версию 1 WCF, поскольку она приводила к тому, что кэшированные сообщения не отправлялись. Чтобы смягчить это, команда Indigo попыталась сделать метод Dispose () умнее, вызвав метод Close (), если состояние объекта ICommunicationObject было в состоянии Opened (определено значением перечисления CommunicationState.Opened ), и в противном случае вызвав Abort. () метод. К сожалению, этот способ реализации удаления ресурсов приводит к ситуации, когда метод Dispose () может генерировать исключения, но не всегда дает вам знать, если что-то пошло не так!

Их окончательное решение заключалось в том, чтобы удалить интерфейс IDisposable из интерфейса ICommunicationObject, позволить ClientBase и ChannelFactory реализовать IDisposable и позволить обоим классам вызывать метод Close () из своих методов Dispose (). Конечным результатом является то, что метод Dispose () классов ClientBase и ChannelFactory может генерировать исключения. В документации MSDN для метода Dispose () четко указано, что объекты, реализующие интерфейс IDisposable, должны обеспечивать освобождение всех удерживаемых ресурсов, но, как показывает наше обсуждение ниже, это не всегда имеет место в ClientBase и ChannelFactory.

Очистка ресурсов с использованием блока

Очень удобный способ убедиться, что вы случайно не забыли вызвать метод Dispose (), например, из-за генерируемого исключения, состоит в использовании блока using. Блок using — это короткая рука для написания блока try-finally. Два следующих фрагмента кода показывают семантически идентичный код.

using (myObject)
{
//some code here
}

myObject;
try
{
//some code here
}
finally
{
if (myObject != null)
{
myObject.Dispose();
}
}

 

Блок using внутренне испускает IL-код, который по сути оборачивает ваш код в блок try-finally. В блоке finally будет вызван метод Dispose (). На самом деле это именно то, что вы хотите в обычном случае, так как это означает, что метод Dispose () будет вызван, даже если из вашего кода внутри блока using выдается исключение.

Прелесть использования блочного синтаксиса в том, что он компактен, и если вы его используете, вам не нужно беспокоиться об утилизации ваших ресурсов. К сожалению, это также приводит к тому, что многие разработчики следуют проверенной практике и переносят объекты ClientBase и ChannelFactory в операторы использования. Они привыкли писать такой код для своих соединений с базой данных и предполагают, что он будет одинаково хорошо работать для ClientBase и ChannelFactory. Проблема заключается в том, что вызов метода Close () (помните, что методы Dispose () в ClientBase и ChannelFactory вызывают метод Close ()) объекта ICommunicationObject может привести к возникновению исключения. Это приводит к двум отдельным проблемам: худшая из них заключается в том, что в некоторых ситуациях соединение может не закрываться.Другая причина в том, что если выдается исключение CommunicationObjectFaptedException, оно будет скрывать исключение, которое изначально было выброшено изнутри вашего блока using. Чтобы избежать этих проблем, объекты ClientBase и ChannelFactory не следует заключать в блок using.

Очистка ресурсов с помощью шаблона Try-Catch-finally-Abort

Лучший способ обработать закрытие объекта ICommunicationObject — это обернуть логику в блок try и вызвать метод Close изнутри блока try. Если что-то идет не так в блоке try, метод Abort следует вызывать из блока finally. Этот подход иногда называют try-close-finally-abort и очень похож на работу блока using. Фрагмент кода ниже показывает, как выглядит этот шаблон.

var finallyClient = new OrganizationClient();
try
{
//method calls
finallyClient.Close();
}
finally
{
if (finallyClient.State != CommunicationState.Closed)
{
finallyClient.Abort();
}
}

Обратите внимание, что надлежащий критерий для вызова метода Abort () заключается в том, что состояние канала отличается от состояния «Закрыто». Если у канала есть другое состояние, это означает, что что-то пошло не так (было сгенерировано исключение) либо до вызова метода Close (), либо во время выполнения метода Close (). В обоих случаях вы хотите вызвать метод Abort (), в любом другом случае вам не следует вызывать метод Abort ().

Чтобы сделать этот подход максимально эффективным, вы должны попытаться включить только код WCF и код, необходимый для выполнения вызовов WCF, как запланировано, в блоке try. Попытайтесь разместить другой код до и / или после блока try, где это возможно. В идеале код внутри блока try должен состоять только из вызова метода Open (), вызовов службы и, наконец, вызова метода Close (). Когда среда выполнения .NET выйдет из блока try, вы либо завершили всю работу, которая вам нужна, либо что-то пошло не так с вызовами, которые проходили через WCF, и ваша работа не может быть завершена.

Если все прошло хорошо, ты дома свободен. Если что-то пошло не так, вы можете вызвать метод Abort (), зная, что вы сделали столько, сколько могли, чтобы завершить вызовы к составленным сервисам.

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

Обработка исключений и очистка ресурсов с помощью шаблона Try-Close-Catch-Abort

Вызов метода Abort () может быть заключен в блок catch, а не в блок finally. Если вам нужна обработка исключений перед закрытием канала, это правильный выбор. Если вам не нужна обработка исключений перед закрытием, тогда шаблон try-close-finally-abort больше подходит, так как ресурсы будут утилизированы до того, как произойдет какая-либо обработка исключений. Всякий раз, когда обнаруживается исключение, необходимо создать стек исключений, это трудоемкая задача. Иными словами, если вы поместите вызов метода Abort () в блоке catch, вам придется подождать, пока будет создан стек исключений, прежде чем вы сможете освободить свои ресурсы.

Следующий фрагмент кода показывает, как это можно сделать.

var catchClient = new OrganizationClient();
try
{
//method calls
catchClient.Close();
}
catch (Exception)
{
//code that handles the exception
catchClient.Abort();
}

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

var catchClient = new OrganizationClient();
try
{
//method calls
catchClient.Close();
}
catch (ApplicationException)
{
//code that handles ApplicationException
}
catch (Exception)
{
//code that handles all other exceptions
}
finally
{
if (catchClient.State != CommunicationState.Closed)
{
catchClient.Abort();
}
}

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

Удобная очистка ресурсов

Добавление кода для закрытия каналов на все ваши вызовы WCF утомительно и подвержено ошибкам. Это также не поддается принципу СУХОЙ (не повторяй себя). Поскольку поведение по существу одинаково при каждом вызове службы с использованием WCF, это хорошая возможность написать служебный метод, который инкапсулирует требуемое поведение. Предложенные служебные методы позволят вам написать код, который будет выглядеть и чувствовать себя очень похожим на тот код, который вы бы написали, если бы вы могли обернуть вызовы WCF в блок using. В приведенном ниже листинге кода показан служебный класс WcfClient.

public static class WcfClient
{
/// <summary>
/// Performs the action on the client (a <see cref="ICommunicationObject"/>) using the try-close-finally-abort pattern
/// </summary>
/// <typeparam name="TClient">The type of client</typeparam>
/// <param name="client">The client</param>
/// <param name="action">The action to perform on the client</param>
public static void Using<TClient>(TClient client, Action<TClient> action) where TClient : ICommunicationObject
{
if (client == null)
{
throw new ArgumentNullException("client");
}
if (action == null)
{
throw new ArgumentNullException("action");
}

try
{
client.Open();
action(client);
client.Close();
}
finally
{
if (client.State != CommunicationState.Closed)
{
client.Abort();
}
}
}


/// <summary>
/// Creates a WCF client channel using a <see cref="ChannelFactory"/> and performs the action on the channel using the try-close-finally-abort pattern
/// </summary>
/// <typeparam name="TChannel">The type of channel</typeparam>
/// <param name="channelFactory">The channel factory</param>
/// <param name="action">The action to perform on the created channel</param>
public static void Using<TChannel>(ChannelFactory<TChannel> channelFactory, Action<TChannel> action) where TChannel : class
{
if (channelFactory == null)
{
throw new ArgumentNullException("channelFactory");
}
if (action == null)
{
throw new ArgumentNullException("action");
}

TChannel clientChannel = channelFactory.CreateChannel() ;
try
{
channelFactory.Open();
action(clientChannel);
channelFactory.Close();
}
finally
{
if (channelFactory.State != CommunicationState.Closed)
{
channelFactory.Abort();
}
clientChannel = null;
}
}
}

Все, что вам нужно сделать для правильной утилизации объектов ICommunicationObjects, — это написать такой код (если вы используете сгенерированный клиент WCF):

GetUsersResponse response = null;
WcfClient.Using(new OrganizationClient(), client =>
{
response = client.GetUsers(new GetUsersRequest());
});

Обратите внимание, что вы можете использовать любые специфические классы WCF, которые необходимо использовать после открытия канала. Например, OperationContextScope, как описано в разделе идемпотентности главы 10.

GetUsersResponse response = null;
WcfClient.Using(new OrganizationClient(), client =>
{
using (new OperationContextScope(client.InnerChannel))
{
OperationContext.Current.OutgoingMessageHeaders.MessageId =
messageId;
response = client.GetUsers(new GetUsersRequest());
}
});

Если вы предпочитаете использовать общий класс ChannelFactory, вместо этого ваш код будет выглядеть примерно так:

GetUsersResponse response = null;
WcfClient.Using(new ChannelFactory("WSHttpBinding_IOrganization"), client =>
{
response = client.GetUsers(new GetUsersRequest());
});

Как обрабатывать подключения при использовании служб с использованием WCF

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

Различные привязки используют разные протоколы. Для некоторых протоколов очень важно закрыть соединение, это относится к протоколам с полным соединением. В таких протоколах последующие вызовы к вашей использованной услуге будут происходить по тому же соединению. Чтобы сделать это возможным, между вызовами поддерживается транспортный сеанс. Но это не так для http, так как это протокол без состояния или без соединения, означающий, что соединение разрывается автоматически. В ранних версиях http соединение разрушалось после каждого вызова, но с тех пор оно было оптимизировано, чтобы фактически поддерживать соединение таким образом, чтобы многочисленные ответы / запросы могли передаваться без повторного согласования. Из этого следует, что закрытие соединения не является критичным с точки зрения обработки ресурсов, когда вы используете службу через http.

С другой стороны, WCF позволяет вам настроить привязку, которую вы хотите использовать, когда вы используете службу, и когда вы это делаете, вы также можете изменить протокол. Если бы вы использовали сервис WCF через http и не закрывали клиентское соединение, у вас не возникло бы никаких проблем. Однако предположим, что вы получаете возможность использовать этот сервис по протоколу tcp, который имеет значительно меньшую нагрузку, чем http. Вы или кто-то еще несколько лет спустя измените конфигурацию и убедитесь, что служба все еще работает. И это делает! Отлично. Но через некоторое время у вас появляются странные проблемы. Вскоре вы можете достичь порога, когда количество открытых каналов блокирует ваше приложение.

Чтобы избежать подобных проблем, рекомендуется всегда закрывать соединение, так как это позволит вам изменять конфигурацию и использовать службу по разным протоколам, как требуется, не беспокоясь об изменении вашего кода.

Однако есть одно исключение: службы REST. Службы REST построены на принципах, которые определяют, как использовать веб-стандарты, и по определению не требуют подключения. Единственные соответствующие стандарты, доступные сегодня для реализации REST, — это http и URI. Служба REST, созданная с использованием http, скорее всего, всегда будет использовать http. Более конкретно: вы не можете просто изменить свою конфигурацию, чтобы использовать службу REST по другому протоколу, чем http. Http является неотъемлемой частью службы REST, а не просто транспортным протоколом.

Рисунок 3: Протоколы, поддерживаемые стандартными привязками WCF. Службы REST всегда используют протокол http, который без установления соединения.

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

Заключение

Эффективное удаление ресурсов важно, и важность правильного использования должна быть отражена в вашем коде. Как правило, использование блока является отличным способом обеспечения утилизации ресурсов, но это не так в случае использования услуг с ICommunucationObject. Вместо этого вам нужно написать собственный код для вызова методов Close () или Abort () в зависимости от ситуации. В идеале вы должны утилизировать ресурсы перед выполнением любого кода обработки исключений.

Ресурсы, которые вы освобождаете от объекта ICommunucationObject, являются ресурсами базового канала. Некоторые протоколы (без сохранения состояния или без установления соединения) не требуют, чтобы вы закрывали клиентское соединение, так что это может быть безопасно пропущено при использовании служб REST через http.

Благодарности

Я хотел бы поблагодарить Джошуа Энтони ( http://www.nsilverbullet.net/ ) и Скотта Сили ( http://www.scottseely.com/ ) за их ценные комментарии к этой статье.

Ссылки

[REF-1] «SOA с .NET», Прентис Холл / Пирсон, ISBN: 0131582313 (запланировано на 2010 год).

Эта статья была первоначально опубликована в журнале The SOA Magazine ( www.soamag.com ), официально связанном с серией сервис-ориентированных вычислений Prentice Hall от Томаса Эрла ( www.soabooks.com ). Copyright © SOA Systems Inc. ( www.soasystems.com )