Text
                    Построение СОА с использованием Windows Communication Foundation
Создание служб
О’REILLY®
^ППТЕР*
Джу вел Лёве

Programming WCF Services Juval Lowy O’REILLY” Beijing • Cambridge • Farnham • Koln • Paris • Sebastopol • Taipei • Tokyo
Создание служб Windows Communication Foundation Лжувел Лёве С^ППТЕР Москва • Санкт-Петербург • Нижний Новгород * Воронеж Ростов-на-Дону • Екатеринбург * Самара * Новосибирск Киев • Харьков • Минск 2008
Джувел Лёве Создание служб Windows Communication Foundation Серия «Бестселлеры O’Reilly» Перевели с английского Е. Матвеев и А. Пасечник Заведующий редакцией Руководитель проекта Научные редакторы Корректор Верстка ББК 32.973-018.2 УДК 004.451 А. Сандрыкин П. Маннинен Е. Матвеев, А. Пасечник В. Листова Л. Харитонов Дж. Лёве Л35 Создание служб Windows Communication Foundation. — СПб.: Питер, 2008. — 592 с.: ил. ISBN 978-5-91180-763-4 Книга посвящена новой и, по мнению многих специалистов, революционной объединенной платформе для разработки сервис-ориентированных приложений для Windows. В первой части объясняются все преимущества использования сервис-ориентированной архитектуры, далее подробно, на практических примерах, показано, как для этого использовать Windows Communication Foundation. Особое внимание в книге уделяется различным тонкостям и наиболее трудным аспектам создания СОА. Читатель не только сможет понять, как программировать, используя WCF, но и освоит на практике важнейшие принципы проектирования. Книга основана на опыте работы автора по разработке стратегии WCF и дальнейшего взаимодействия с командой разработки. Издание адресовано разработчикам и архитекторам, желающим не только освоить WCF, но и подняться на ступень выше в проектировании и разработке ПО. Права на издание получены по соглашению с O'Reilly. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-0596526993 (англ.) ISBN 978-5-91180-763-4 © O'Reilly, 2007 © Перевод на русский язык ООО «Питер Пресс», 2008 © Издание на русском языке, оформление ООО «Питер Пресс», 2008 ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, д. 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Подписано в печать 17.03.08. Формат 70x100/16. Усл. п. л. 56,76. Тираж 2300. Заказ 8564. Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., д. 15.
Содержание Предисловие........................ Введение .......................... Как устроена книга.............. Для кого написана книга.......... Что потребуется для работы с книгой . Благодарности.................... Глава 1. Основные сведения о WCF . . Что такое WCF?................... Службы........................... Адреса........................... Контракты........................ Хостинг.......................... Привязки ........................ Программирование на стороне клиента.............. Сравнение административной и программной настройки......... Архитектура WCF.................. Работа с каналами................ Надежность....................... Глава 2. Контракты служб............ Перегрузка операций.............. Наследование контрактов.......... Факторинг и проектирование контрактов служб................ Запросы на поддержку контрактов . . . Глава 3. Контракты данных........... Сериализация ..................... Атрибуты контракта данных......... Иерархия контрактов данных........ Эквивалентность контрактов данных. . Изменение версий.................. Перечисления ..................... Делегаты и контракты данных .... Наборы данных и таблицы........... Обобщенные типы................... Коллекции ........................ . 7 10 11 14 15 15 16 16 17 20 22 26 32 50 58 59 61 65 70 70 72 77 81 88 88 95 106 114 116 124 126 126 131 134 Глава 4. Управление экземплярами . 144 Аспекты поведения................144 Управление экземплярами для служб уровня вызова..........145 Сеансовые службы.................151 Синглетные службы................161 Демаркационные операции..........166 Деактивизация экземпляров........169 Регулирование нагрузки...........174 Глава 5. Операции...................181 Операции «запрос-ответ»..........181 Односторонние операции...........182 Операции обратного вызова........186 События .........................208 Потоковая передача...............212 Глава 6. Сбои и исключения..........216 Контракты сбоев..................219 Расширения обработки ошибок .... 231 Глава 7. Транзакции.................246 Восстановление после сбоев.......246 Транзакции.......................247 Распространение транзакций.......254 Класс Transaction................264 Транзакционное программирование служб............................266 Явное транзакционное программирование.................282 Управление состоянием служб .... 293 Управление экземплярами и транзакции.....................295 Обратный вызов...................315 Глава 8. Управление параллельной обработкой .........................322 Управление экземплярами и параллельная обработка.........323 Режим параллельной обработки службы...........................323 Экземпляры и параллельный доступ . . 331
6 Содержание Ресурсы и службы.....................332 Контекст синхронизации ресурсов. . . 335 Синхронизационный контекст службы . 343 Пользовательский синхронизационный контекст службы....................355 Обратный вызов и безопасность клиента..........................361 Обратные вызовы и синхронизационный контекст . . . 364 Асинхронные вызовы...............372 Глава 9. Очереди....................387 Отключенные службы и клиенты . . . 387 Очереди вызовов..................388 Управление экземплярами..........403 Управление параллельной обработкой . 410 Сбои при доставке................411 Сбои при воспроизведении.........419 Вызовы с очередями и подключенные вызовы............424 Ответная служба......................427 НТТР-мосты...........................446 Глава 10. Безопасность..................453 Аутентификация.......................453 Авторизация..........................454 Безопасность передачи................455 Управление личностью.................462 Общая политика.......................462 Сценарный подход.....................463 Интрасетевые приложения..............464 Интернет-приложения..................493 Приложение «бизнес-бизнес».......515 Отсутствие безопасности..............523 Декларативная инфраструктура безопасности ................... 525 Контроль средств безопасности . . . . 542 Приложение А. Введение в служебно- ориентированное программирование............547 Краткая история программирования . . 547 Объектно-ориентированное программирование.................549 Компонентно-ориентированное программирование.................549 Служебно-ориентированное программирование.................552 Преимущества служебно-ориентированного подхода........................553 Служебно-ориентированные приложения......................554 Каноны и принципы...............555 Практические принципы...........556 Необязательные принципы.........557 Приложение Б. Схема «публикация/подписка» .... 558 Схема «публикация/подписка» .... 559 Инфраструктура «публикация/подписка» ......... 560 Приложение В. Стандарты программирования WCF. . 577 Общие рекомендации из области проектирования ................ 577 Основные положения..............578 Контракты служб.................578 Контракты данных................579 Управление экземплярами.........580 Операции и вызовы...............580 Сбои............................582 Транзакции......................583 Управление параллельной обработкой.....................584 Службы с очередями..............585 Безопасность .................. 586 Алфавитный указатель ...............587
Предисловие Мы с Дж. Лёве, автором этой превосходной книги, связаны общим увлечени- ем - проектированием и построением распределенных систем. При этом мы прошли очень похожий путь в области технологий, хотя всегда работали в раз- ных компаниях над разными проектами, а большую часть карьеры — даже на разных континентах. Вначале 1990-х годов мы оба медленно вживались в идею «сделать что-ни- будь на одном компьютере, чтобы что-то произошло на другом компьютере», а мир технологий прикладных распределенных систем еще только начинал формироваться. Со временем оборудование рабочих станций и серверов стано- вилось более доступным, и построение крупных систем, не зависящих от цен- трального транзакционного узла, стало привлекательным с экономической точ- ки зрения. Прогресс наблюдался и в области пересылки данных. Сейчас в это трудно поверить, но тогда моя телефонная компания настаивала, что передача данных по телефонной линии со скоростью свыше 1200 бит/с никогда не будет возможна. В настоящий момент по тому же проводу идет передача со скоро- стью 6 Мбит/с и более. Интересные были времена. В процессе формирования технологий доступных распределенных вычисле- ний, в начале 1990-х годов появились два основных «лагеря»: технология DCE, во главе которой стояла фирма Digital Equipment Corporation (со временем по- глощенная фирмой Compaq, которая также была поглощена ИР), и технология CORBA, развиваемая консорциумом OMG с изрядной! поддержкой IBM. В 1996-1997 годах работа над этими замечательными проектами практически остановилась. Появился Интернет, и мир начал сходить с ума по (1) HTML. (2) HTTP, (3) венчурным предприятиям, и (4) первичному размещению акций. Чтобы прийти в себя от лопнувшего «Интернет-пузыря», отрасли понадоби- лось почти 10 лет. Проблемы лежа.-!и не только в экономической, но еще и в техно- логической плоскости. С другой стороны, были и положительные последст- вия - вместо двух основных технологий распределенных систем осталась толь- ко одна... пли несколько десятков, в зависимости от точки зрения. В 2007 году отрасль все еще не может прийти к единому мнению относи- тельно «правильного пути» написания кода распределенных систем. Вероятно, представители Sun Microsystems или ВЕА скажут вам, что это Java; мои колле- ги в Microsoft (и я сам) наверняка предпочтут C# или Visual Basic. Но ни в Sun, ни в ВЕА, ни в IBM, ни в Microsoft уже нет разногласий по поводу того, как должно быть организовано взаимодействие на канальному уровне. Войны «DCE против CORBA» остались в прошлом, и сам факт появления согласованной
8 Предисловие спецификации, заложенной в основу этого перемирия — SOAP 1.1, — был весь- ма впечатляющим достижением. Прошло почти шесть лет с тех пор, как спецификация SOAP 1.1 была подана в W3C в виде технической записки. С тех пор было разработано и согласовано множество спецификаций на базе SOAP, от фундаментальных (адресация и широкий спектр средств безопасности) до протоколов уровня предприятия (например, координации атомарных транзакций). Моя рабочая группа в Microsoft, которая до сих пор неформально называет- ся «Indigo» по кодовому названию проекта, занимала центральное место в этих усилиях по разработке и согласованию. Если бы не сильное желание IBM, Microsoft и других партнеров создать общий набор стандартов (при том, что все мы яростно конкурируем в области промышленных технологий), сама система открытых стандартов не могла бы существовать, не говоря уже о ее многочис- ленных реализациях от разных поставщиков для разных платформ. Откровенно говоря, все это заняло на пару лет больше, чем мы рассчитыва- ли. На согласование уходит время, и мы просто не выпускали свой программ- ный продукт — Windows Communication Foundation — без полной уверенности в том, что он будет нормально работать с продуктами наших партнеров и кон- курентов. Проектирование тоже требует времени, и при разработке нового про- дукта мы стремились к тому, чтобы разработка на новой платформе выглядела знакомой для наших клиентов, потративших время и усилия на освоение техно- логий распределенных систем предыдущей волны — служб ASP.NET, WSE (Web Services Enhancements), NET Remoting, Messaging/MSMQ и Enterprise Services/COM+. Только что приведенный список содержит названия пяти технологий, а с учетом их аналогов в неуправляемом коде и вспомогательных ветвей их бу- дет еще больше. Одну из важнейших целей проектирования Windows Commu- nication Foundation можно охарактеризовать одной простой фразой: единая концепция программирования. Независимо от того, создаете ли вы транзакци- онное N-уровневое приложение, сеть из клиентов P2P, сервер поставок RSS или собственное решение Enterprise Services Bus, вам не придется осваивать множество разных технологий, каждая из которых решает часть поставленной задачи. Изучайте и используйте Windows Communication Foundation — единую концепцию программирования. В этой книге подробно рассказано о том, что мы в Microsoft построили в ка- честве фундамента для построения ваших приложений и служб. Материал из- лагается с той точностью, преподавательским мастерством и архитектурной приверженностью, которые принесли автору заслуженную известность во мно- гих странах. Мы, участники группы Connected Framework из Microsoft, очень гордимся тем, что мы сделали. Созданная нами инфраструктура объединяет семейство распределенных технологий, обладает широкими возможностями взаимодейст- вия, способствует применению служебно-ориентированного программирова- ния, не особенно сложна в освоении и чрезвычайно эффективна в работе.
Предисловие Дж. Лёве — один из самых выдающихся экспертов современности в области распределенных систем, и нам лестно, что он посвятил свою книгу Windows Communication Foundation. Надеемся, он поможет вам понять, почему мы все и сообщество пользователей ранних версий продукта с таким энтузиазмом от- носимся к самому продукту и создаваемым им возможностям. Желаю приятно- го чтения и удачи при построении ваших первых служб WCF! Клеменс Вастерс, руководитель проекта, Connected Framework Team Microsoft Corporation
Введение В августе 2001 года я впервые познакомился с проектом Microsoft по перера- ботке СОМ+ с использованием управляемого кода. Довольно долго ничего ин- тересного не происходило. Затем в июле 2002 года, на встрече C# 2.0 Strategic Design Review были представлены общие планы но переработке технологий удаленного доступа в нечто, чем смогут пользоваться разработчики. Тогда же компания Microsoft вела работы по включению новых спецификаций безопас- ности веб-служб в стек ASMX и активно взаимодействовала с другими партне- рами по созданию предварительных версий дополнительных спецификаций веб-служб. В июле 2003 года мне представилась возможность ознакомиться с новой транзакционной инфраструктурой, в которой были исправлены многие недос- татки транзакционного программирования .NET. На тот момент еще не сущест- вовало логически связной программной модели, объединявшей эти разнород- ные технологии. К концу 2003 меня пригласили в немногочисленную группу внешних экспертов для участия в стратегической оценке проекта новой плат- формы разработки с кодовым обозначен нем «Indigo». Некоторые из самых ум- ных и хороших людей, которых я знал, также вошли в эту группу. За последую- щие' 2-3 года проект «Indigo» прошел три поколения программных моделей. Текущая декларативная модель, построенная на базе конечных точек, появи- лась в начале 2005 года, стабилизировалась к августу и в конечном итоге полу- чила название WCF (Windows Communication Foundation). Если спросить нескольких людей, что такое WCF, вы вряд ли получите оди- наковые ответы. Для разработчика веб-служб WCF — решение, которое на- конец-то решает проблему совместимости, реализация длинного списка про- мышленных стандартов. Для разработчика распределенных приложений — это простейший механизм организации удаленных вызовов и даже вызовов с очередями. Для разработчика систем — это следующее поколение средств, ориентированных на производительность (транзакции, хостинг и т.д.), предос- тавляющих готовый вспомогательный, «вторичный» код для приложений. Для прикладного программиста -- это декларативная модель программирования для структурирования приложении. Наконец, для архитектора WCF представ- ляет долгожданную возможность построения служебно-ориентированных при- ложений. В действительности WCF сочетает в себе все перечисленные аспекты, просто потому, что платформа проектировалась таким образом — как следую- щее поколение разных технологий Microsoft. Для меня WCF — всего лишь следующая платформа разработки, которая в значительной степени заменяет низкоуровневое программирование .NET. Предполагается, что WCF будет использоваться всеми разработчиками .NET независимо от типа приложения, его размера или отраслевого сектора. WCF —
Как устроена книга 11 фундаментальная технология, предоставляющая простой и логичный способ построения служб и приложений в соответствии с тем, что на мой взгляд явля- ется разумными архитектурными принципами. Платформа WCF изначально строилась для упрощения разработки и развертывания приложений, а также снижения общих затрат их владельцев. Службы WCF используются для по- строения служебно-ориентированных приложений, от автономных настольных приложений до веб-приложений и служб, а также высокопроизводительных приложений уровня крупного предприятия. Как устроена книга В книге рассматриваются различные темы и навыки, необходимые для проек- тирования и разработки служебно-ориентированных приложений на базе WCF. Читатель увидит, как правильно использовать встроенные средства WCF — хостинг служб, управление экземплярами, управление параллельной обработ- кой, транзакции, отключенные вызовы с очередями и безопасность. Хотя в кни- ге показано, как пользоваться перечисленными средствами, основное внимание уделяется ответам на вопрос «почему» и обоснованиям тех или иных конкрет- ных архитектурных решений. Материал не ограничивается программировани- ем WCF и связанным с ними системными вопросами; читатель также найдет в книге рекомендации по проектированию, советы и предупреждения о возмож- ных ловушках. Практически все темы представлены с точки зрения программи- ста, потому что цель автора — сделать читателя не только экспертом в WCF, но и повысить его профессиональный уровень как программиста. Книга избегает подробностей реализации WCF; изложение материала в ос- новном сконцентрировано на возможностях и практических аспектах примене- ния WCF — применению технологии, выбору архитектуры и программной мо- дели. Материал в полной мере использует возможности .NET 2.0, а в некоторых случаях также требует хорошего знания С#. В книге также представлены многие вспомогательные классы и программы, написанные мной. Эти классы и атрибуты предназначены для повышения эф- фективности труда и качества написанных вамп служб WCF. Я разработал не- большую инфраструктуру, работающую поверх WCF, которая компенсирует некоторые недочеты в архитектуре и упрощает выполнение некоторых задач. Кроме того, моя инфраструктура демонстрирует возможности расширения WCF. За прошедшие два года я опубликовал в MSDN Magazine несколько статей, посвященных WCF. В настоящее время я также веду раздел WCF в рубрике «Foundations». Некоторые из этих статей были взяты за основу для глав книги; я благодарен журналу, который разрешил мне это сделать. Даже если вы уже знакомы с этими статьями, соответствующие главы все равно стоит прочитать. Книга содержит гораздо более полную информацию, включая альтернативные точки зрения, практические приемы и примеры, а ее содержание часто связано с темами других глав.
12 Введение Каждая глава посвящена отдельной теме. Впрочем, материал каждой главы базируется на содержимом предыдущих глав, поэтому читать следует по по- рядку. Далее приводится краткий перечень глав и приложений. Глава 1. Основные сведения о WCF В начале главы приводится общее объяснение, что такое WCF, после чего опи- сываются важнейшие концепции и структурные блоки WCF — адреса, контрак- ты, конечные точки, хостинг и клиенты. Далее следует общее объяснение архи- тектуры WCF, хорошее понимание которой является залогом для понимания всех последующих глав. Предполагается, что читатель понимает основные пре- имущества служебно-ориентированного программирования. Если это не так, начните с приложения А. Даже если вы уже знакомы с основными концепция- ми WCF, я рекомендую хотя бы бегло просмотреть эту главу — не только что- бы убедиться в прочности имеющихся знаний, но и для знакомства с некото- рыми вспомогательными классами и терминами, которые будут использоваться в книге. Глава 2. Контракты служб Глава посвящена проектированию и использованию контрактов служб. Снача- ла разбираются некоторые полезные приемы перегрузки контрактов служб и наследования. Далее речь пойдет о проектировании и факторинге контрактов, хорошо подходящих для повторного использования, удобных в сопровождении и расширении. Напоследок я покажу, как организуется программное взаимо- действие с метаданными контрактов. Глава 3. Контракты данных Основная тема главы — обмен данными между клиентом и службой без переда- чи информации о типе данных или необходимости использования той же тех- нологии разработки. Рассматривается ряд интересных практических вопросов, в том числе контроль версии данных и передача коллекций. Глава 4. Управление экземплярами Глава отвечает на важный вопрос: какой экземпляр службы обрабатывает тот или иной запрос клиента? WCF поддерживает несколько механизмов управле- ния экземплярами, активизацией и управлением жизненным циклом, причем выбор имеет серьезные последствия для масштабируемости и производитель- ности приложения. В главе приводятся обоснования каждого режима управле- ния экземплярами, рекомендации относительно того, когда и как их лучше ис- пользовать, а также рассматриваются некоторые сопутствующие темы — например, регулирование нагрузки.
Как устроена книга 13 Глава 5. Операции В этой главе рассматриваются типы операций служб, вызываемых клиентом. Читатель найдет в ней полезные рекомендации из области проектирования — как улучшать и расширять базовые возможности для поддержки обратного вы- зова, управлять каналами и портами обратного вызова и обеспечить работу ти- пизованных дуплексных посредников. Глава 6. Сбои и исключения Вся глава посвящена одной теме: тому, как службы передают клиентам инфор- мацию о возникающих ошибках и исключениях. Такие конструкции, как ис- ключения и обработка исключений, привязаны к конкретным технологиям и не могут выходить за границы служб. В этой главе рассматриваются рекомендуе- мые методы обработки ошибок, обеспечивающие логическую изоляцию кли- ентской обработки ошибок от службы. Также читатель узнает, как происходит расширение возможностей базового механизма обработки ошибок. Глава 7. Транзакции Сначала объясняется необходимость применения транзакций, после чего обсу- ждаются многие аспекты работы транзакционных служб: архитектура управле- ния транзакциями, конфигурация распространения транзакций, декларативная поддержка транзакций в WCF, а также возможность создания транзакций кли- ентами. В завершение главы рассматриваются сопутствующие вопросы из об- ласти проектирования — такие, как режимы активизации экземпляров и управ- ление состоянием транзакционных служб. Глава 8. Управление параллельной обработкой Данная глава описывает простой, но мощный механизм декларативного управ- ления параллельной обработкой и синхронизацией (как на стороне клиента, так и на стороне службы). Затем рассматриваются более сложные вопросы: об- ратные вызовы, реентерабельность, контексты синхронизации, а также приво- дятся рекомендации по предотвращению взаимных блокировок. Глава 9. Очереди Глава посвящена организации очередей вызовов к службам; очереди делают возможными асинхронную, отключенную работу служб. В начале главы рас- сматривается настройка служб с очередями, после чего обсуждение переходит к таким аспектам, как транзакции, управление экземплярами и сбои, а также их влиянию на бизнес-модель службы и ее реализацию. Глава 10. Безопасность В последней главе многогранная тема безопасности разделяется на простые со- ставляющие (передача сообщений, аутентификация, авторизация и т. д.). Далее
14 Введение будет показано, как обеспечивается безопасность в типовых сценариях (напри- мер, интрасетевых иди интернет-приложениях). В завершение я представлю свою инфраструктуру декларативной безопасности WCF, автоматизирующую настройку системы безопасности и существенно упрощающую работу со сред- ствами безопасности. Приложение А. Введение в служебно-ориентированное программирование Приложение написано для читателей, которые хотят понять, что же собой пред- ставляет служебно-ориентированное программирование. Я предлагаю свое ви- дение служебно-ориентированного программирования и смысл его применения в конкретном контексте. В приложении определяются служебно-ориентирован- ные приложения (в отличие от простой архитектуры) и службы, а также рас- сматриваются преимущества методологии. Тема завершается анализом принци- пов служебно-ориентированного программирования, причем абстрактные каноны дополняются рядом практических аспектов, необходимых для боль- шинства приложений. Приложение Б. Схема «публикация/подписка» В этом приложении описана моя инфраструктура для реализации схемы управ- ления событиями «публикация/подписка», позволяющая создавать службы публикации и подписки буквально в одной-двух строках кода. Хотя данная схе- ма с таким же успехом могла стать частью главы 5, я вынес ее в отдельное при- ложен не. потому что в ней используются аспекты из других глав — такие, как транзакции и вызовы с очередями. Приложение В. Стандарты программирования WCF По сути это объединенный список рекомендаций и замечаний типа «делай так» или «так не делай», приводившихся в книге. Стандарт отвечает на вопросы «как» и «что», а не «почему» - за обоснованиями следует обращаться к другим главам книги. Кроме того, в стандарте используются вспомогательные классы, упоминаемые в книге. Для кого написана книга Предполагается, что читатель является опытным разработчиком, хорошо разби- рающимся в объектно-ориентированных концепциях (таких, как инкапсуляция и наследование). Я использую ваш существующий опыт использования объект- ных и компонентных технологий, а также знание терминологии и перенесу его в WCF. В идеале читатель должен неплохо разбираться в .NET, а также владеть C# хотя бы на базовом уровне (включая параметризованные классы и аноним-
Благодарности 15 ные методы). Хотя в книге в основном используется С#, материал также может быть использован разработчиками Visual Basic 2005. Что потребуется для работы с книгой Для работы с книгой вам потребуется .NET 2.0, Visual Studio 2005, компоненты .NET 3.0, пакет разработки .NET 3.0 SDK и расширения .NET 3.0 для Visual Studio 2005. Если в тексте прямо не упомянуто обратное, материал книги относит- ся к Windows ХР, Windows Server 2003 и Windows Vista. Возможно, также потре- буется установить дополнительные компоненты — такие, как MSMQ и I IS. Благодарности Я познакомился с платформой WCF на ранней стадии ее существования, и это произошло только благодаря постоянной поддержке и общению с руководите- лями проекта WCF (тогда Indigo). Я особенно благодарен своему другу Стиву Сварцу (Steve Swartz), одному из архитекторов WCF, не только за его знания и проницательность, но и за терпение и долгие часы, проведенные за совмест- ным планированием. Благодарю Ясера Шохуда (Yasser Shohoud), Дуга Парди (Doug Purdy) и Шая Коэна (Shy Cohen) за отличные стратегические оценки проекта, а Крита Шринивасана (Krish Srinivasan) — за его почти философский подход к программированию. Работа с ними была лучшим этапом изучения WCF и фактически сама по себе являлась привилегией. Следующие руководи- тели проекта WCF также уделяли мне свое время и помогали разобраться в WCF: Энди Миллиган (Andy Milligan), Брайан Макнамара (Brian McNamara), Юджин Осовецки (Eugene Osovetsky), Кенни Вольф (Kenny Wolf), Кирилл Гаврилюк (Kirill Gavrylyuk), Макс Фейн гольд (Max Feingold), Майкл Маручек (Michael Marucheck), Майк Бернал (Mike Vernal) и Стив Миллет(Steve Millet). Также я благодарен руководителю группы Анджеле Миллс (Angela Mills). Из тех, кто не работал в Microsoft, хочу поблагодарить Нормана Хедлама (Norman Headlam) и Педро Феликса (Pedro Felix) за полезную обратную связь. Спасибо Николасу Пальдино (Nicholas Paldino) за его помощь. Познания Ника в .NET не имеют равных, а его скрупулезное внимание к мелочам оказало ог- ромное влияние на качество и логическую связность материала книги. Наконец, моя жена Дана уговаривала меня записывать свои идеи, отлично зная, что работа над книгой лишает меня драгоценного времени общения с ней и девочками; я также благодарю своих родителей, наделивших меня любовью к программированию. Посвящаю книгу своим дочерям Эбигейл (7 лет) и Эли- нор (4 года). Вы все для меня -- самое главное в жизни.
1 Основные сведения о WCF В этой главе описаны важнейшие концепции и структурные элементы платфор- мы WCF и ее архитектуры. В частности, в ней рассматриваются основные по- нятия, относящиеся к адресам, привязкам, контрактам и конечным точкам; мы разберемся, как организовать хостинг службы и как написать клиента, а также обсудим ряд сопутствующих проблем (внутрипроцессный хостинг и надеж- ность). Даже если читатель уже знаком с основными концепциями WCF, я ре- комендую хотя бы бегло просмотреть эту главу — во-первых, чтобы убедиться в прочном, основательном понимании темы, а во-вторых, потому что некоторые вспомогательные классы и термины, представленные в этой главе, постоянно встречаются и используются в книге. Что такое WCF? Windows Communication Foundation (WCF) — инструментальный пакет (SDK) для разработки и развертывания служб в системе Windows. WCF обеспечивает среду времени выполнения для ваших служб, давая возможность предостав- лять доступ к типам CLR в качестве служб, а также использовать другие служ- бы как типы CLR. Хотя теоретически службы можно создавать и без WCF, на практике WCF значительно упрощает их построение. WCF представляет собой Microsoft-реализацию отраслевых стандартов, определяющих работу со служ- бами, преобразования типов, маршалинг и управление протоколами. Соответ- ственно WCF берет на себя обеспечение взаимодействия служб. В распоряже- ние разработчика предоставляется готовый служебный код, необходимый практически в каждом приложении, поэтому применение WCF кардинально повышает производительность. Первая версия WCF предоставляет много по- лезных инструментов для разработки служб — хостинг, управление экземпля- рами, асинхронные вызовы, надежность, управление транзакциями, очереди ав- тономных вызовов и безопасность. Кроме того, WCF обладает элегантной моделью расширяемости, которая позволяет выйти за рамки базовых возмож- ностей. Кстати говоря, эта модель использовалась при написании самого пакета
Службы 17 WCF. Остальные главы книги посвящены перечисленным аспектам и функци- ям WCF. Практически вся функциональность WCF сосредоточена в одной сборке System.ServiceModel.dll, находящейся в пространстве имен ServiceModel. Платформа WCF является частью .NET 3.0, а для ее работы необходимо присутствие .NET 2.0, поэтому WCF работает только в операционных системах с соответствующей поддержкой. В настоящее время этот список включает Win- dows Vista (клиент и сервер), Windows ХР SP2 и Windows Server 2003 SP1 и более поздних версий. Службы Службой (service) называется функциональный модуль, доступный извне. В этом отношении службы являются очередной ступенью эволюционного пути «функ- ции-объекты-компоненты-службы». Термином SO (Service-orientation) обозна- чается абстрактный набор принципов и оптимальных методов построения прило- жений, ориентированных на работу со службами. Если вы еще не знакомы с основ- ными принципами программирования, ориентированного на работу со службами, обратитесь к приложению А — в нем представлен краткий обзор и побудитель- ные причины для использования SO-программирования. В оставшейся части книги предполагается, что вы уже знакомы с этими принципами. Служеб- но-ориентированные* приложения SOA (Service-Oriented Applications) объ- единяют службы в единую логическую прикладную модель — по аналогии с тем, как компонентно-ориентированные приложения объединяют компонен- ты, или объектно-ориентированные приложения объединяют объекты (рис. 1.1). Рис. 1.1. Служебно-ориентированное приложение Задействованные службы могут быть локальными или удаленными, они мо- гут разрабатываться разными сторонами с применением любых технологий, их 1 Более правильно было бы сказать: «приложения, ориентированные на работу со службами», но то- гда текст станет совсем неудобочитаемым, поэтому далее в книге будет использоваться термин «служебно-ориентированные приложения». — Примеч. перев.
18 Глава 1. Основные сведения о WCF версии могут изменяться независимо друг от друга... даже одновременность их выполнения не является обязательным условием. Внутри служб встречаются всевозможные комбинации языков, технологий, платформ, версий и библиотек, но взаимодействие между службами всегда проходит по четко определенным правилам. Клиентом службы называется сторона, пользующаяся ее функционально- стью. В роли клиента может оказаться буквально все, что угодно — класс Windows Forms, страница ASP.NET, другая служба. Клиенты и службы взаимодействуют друг с другом, отправляя и принимая сообщения. Сообщения могут передаваться от клиента к службе напрямую или через посредника. В WCF все сообщения передаются в формате SOAP. Обрати- те внимание: сообщения независимы от транспортных протоколов — в отличие от веб-служб службы WCF могут взаимодействовать на базе разных транспорт- ных протоколов, не только HTTP. Клиенты WCF могут взаимодействовать со сторонними (не-WCF) службами, а службы WCF могут взаимодействовать со сторонними клиентами. Впрочем, если вы занимаетесь разработкой как клиен- та, так и службы, приложение обычно конструируется так, чтобы на обоих сто- ронах была задействована платформа WCF (для использовании ее преиму- ществ). Так как внутреннее строение службы скрыто от внешнего мира, службы WCF' обычно предоставляют метаданные, описывающие их функциональность и возможные способы взаимодействия со службой. Метаданные публикуются в заранее определенном формате, не зависящем от конкретной технологии — например, WSDL на базе HTTP-GET или отраслевого стандарта обмена мета- данными. Сторонний (не-WCF) клиент может импортировать метаданные в формате родных типов своей среды. Аналогично, клиент WCF может импор- тировать данные сторонних служб и потреблять их в виде интерфейсов и клас- сов CLR. Рис. 1.2. WCF-взаимодействия на одном компьютере
Службы 19 Границы выполнения и взаимодействие со службами В WCF клиент никогда не взаимодействует со службой напрямую, даже при общении с локальными службами в едином пространстве памяти. Вместо этого вызов клиента всегда передается службе через посредника (proxy). Посредник предоставляет те же операции, что и служба, а также ряд управляющих мето- дов. WCF позволяет клиенту взаимодействовать со службой через любые грани- цы выполнения. Если служба и клиент находятся на одном компьютере (рис. 1.2), клиент может обращаться к службе в том же прикладном домене, в другом прикладном домене того же процесса или в другом процессе. Если клиент и служба находятся на разных компьютерах (рис. 1.3), клиент может обращаться к службе в своей интрасети или в Интернете. Рис. 1.3. Межкомпьютерные взаимодействия с использованием WCF WCF и прозрачность размещения1 В прошлом технологии распределенных вычислений — такие, как DCOM и .NET Remoting — стремились предоставить клиенту единую модель программирова- ния для работы как с локальными, так и удаленными объектами. Для локаль- ных обращений клиент использовал прямую ссылку, а работа с удаленным объ- ектом осуществлялась через посредника. Проблема такого подхода, основанного на превращении локальной модели программирования в удаленную, заключа- лась в одном простом обстоятельстве: для удаленного обращения к объекту в действительности требуется нечто большее, чем объект и канал связи. Неиз- бежно возникает целый ряд нетривиальных проблем — управление жизненным циклом, надежность, управление состоянием, масштабируемость и безопас- ность. В результате удаленная модель значительно усложнялась — и все из-за того, что она пыталась выдать себя за то, чем она не была: локальным объектом. WCF также стремится предоставить клиенту единую модель программирова- ния независимо от размещения службы. Тем не менее WCF идет по прямо про- 1 Поскольку Microsoft старается переводить термин location словом «размещение», а не «расположе- ние», то и мы в данной книге последуем ее примеру. — Примеч. перев.
20 Глава 1. Основные сведения о WCF тивоположному пути: берется удаленная модель программирования, основан- ная на работе с экземплярами и посредниками, и применяется даже в самых локальных случаях. Поскольку все взаимодействия осуществляются через по- средника, с едиными требованиями к конфигурации и хостингу, WCF поддер- живает общую модель программирования для локального и удаленного случаев; это не только позволяет менять размещение службы без изменений в клиенте, но и значительно упрощает модель прикладного программирования. Адреса В WCF каждая служба связывается с уникальным адресом. Адрес содержит два важных элемента: размещение службы и транспортный протокол (транспорт - ная схема) для взаимодействия со службой. Первая часть адреса определяет имя целевого компьютера, сайта или сети; коммуникационный порт, канал или очередь; и необязательный конкретный путь, или URL URI (Universal Resource Identifier, универсальный идентификатор ресурса) может содержать любую уникальную строку — например, имя службы или GUID. WCF 1.0 поддерживает следующие транспортные схемы: О HTTP; О TCP; О одноранговая сеть; О IPC (межпроцессные коммуникации по именованным каналам); О MSMQ. Адреса всегда задаются в следующем формате: [базовый адрес]/[необязательный URI] Базовый адрес всегда имеет формат [транспорт]://[компьютер или домен][:необязательный порт] Несколько примеров адресов: http://1ocalhost:8001 http://localhost:8001/MyServlсе net.tcp://1ocalhost:8002/MyServlce net.pi pe://1 oca1 host/MyPIpe net.msmq://1ocalhost/private/MyServi ce net.msmq://localhost/MyService Адрес вида http://1ocalhost:8001 должен читаться так: «Используя HTTP, идем на компьютер localhost, где на порте 8001 кто-то ожидает моего вызова». Если же адрес содержит URI: http://1 оса1 host:8001/MyServlсе его интерпретация будет такой: «Используя HTTP, идем на компьютер loca- lhost, где на порте 8001 кто-то с именем MyService ожидает моего вызова».
Адреса 21 Адреса TCP Адреса TCP содержат транспортный префикс net.tcp. Обычно в них также включается номер порта: net. tcp: / /1 оса 1 host: 8002/MyServ i се Если номер порта не указан, по умолчанию используется порт 808: net. tcp://local host/MyServi ce Два адреса TCP (с одного хоста — см. далее в этой главе) могут совместно использовать один порт: net. tcp://local host :8002/MyServi ce net .tcp: //local host :8002/MyOtherServi ce Адреса TCP часто используются в книге. Адреса HTTP Адреса TCP содержат транспортный префикс http; также встречается префикс https (безопасный транспорт). Адреса HTTP обычно используются с наружны- ми службами на базе Интернета; в них также может указываться порт: http://local host:8001 Если номер порта не указан, по умолчанию используется порт 80. Как и в слу- чае с адресами TCP, два адреса HTTP с одного хоста могут использовать один порт, даже на одном компьютере. Адреса HTTP также часто встречаются в книге. Адреса IPC У адресов IPC транспортный префикс net.pipe указывает на использование меха- низма именованных каналов Windows. В WCF службы, использующие именован- ные каналы, могут принимать вызовы только с того же компьютера. Соответствен- но, адрес должен содержать либо явно заданное имя локального компьютера, либо обозначение localhost, за которым следует уникальная строка с именем канала: net.pipe://] оса Ihost/MyPi ре Именованный канал может быть открыт не более чем в одном экземпляре. Это означает, что два адреса IPC не могут использовать одно имя канала на од- ном компьютере. Адреса IPC тоже встречаются в книге. Адреса MSMQ Транспортный префикс net.msmq в адресах MSMQ указывает на использование механизма очередей Microsoft (Microsoft Message Queue). Имя очереди должно быть указано. При работе с приватными очередями необходимо указать тип очереди, но для публичных очередей тип можно опустить: net.msmq://1 осаIhost/private/MyServiсе net. msmq .//local host/MyServi ce Очереди вызовов рассматриваются в главе 9.
22 Глава 1. Основные сведения о WCF Адреса одноранговых сетей Адреса одноранговых сетей содержат префикс net.p2p, указывающий на ис- пользование транспорта одноранговых сетей Windows. В адресе должно быть задано имя одноранговой сети, а также уникальный путь и порт. Темы конфи- гурации и использования одноранговых сетей выходят за рамки книги, и в по- следующих главах одноранговые сети почти не упоминаются. Контракты В WCF каждая служба предоставляет контракт — стандартный, платформен- но-независимый способ описания того, что делает данная служба. WCF опреде- ляет четыре разновидности контрактов. О Контракты служб описывают операции, которые могут выполняться клиен- том со службой. Они подробно рассматриваются в следующей главе, но ши- роко используются во всех главах книги. О Контракты данных определяют, какие типы данных принимаются и переда- ются службой. WCF определяет косвенные контракты для встроенных типов (таких, как int и string), однако вы можете легко определить явные контрак- ты данных для пользовательских типов. Глава 3 посвящена определению и использованию контрактов данных, а в последующих главах контракты данных используются по мере надобности. О Контракты ошибок определяют, какие ошибки инициируются службой, как служба обрабатывает их и передает своим клиентам. Глава 6 посвящена оп- ределению и использованию контрактов ошибок. О Контракты сообщений позволяют службам напрямую взаимодействовать с сообщениями. Контракты сообщений могут быть типизованными и нети- пизованными; они бывают полезны при решении проблем взаимодействия, а также при наличии существующих форматов сообщений, которые необхо- димо соблюдать. Вам как разработчику WCF почти не придется иметь дела с контрактами сообщений, и в книге они не используются. Контракт службы Атрибут ServiceContractAttribute определяется следующим образом: fAttributellsage(Att ributeTargets. Interface|At.tributeTargets .Class. Inherited = false)1 public sealed class ServiceContractAttribute : Attribute { public string Name {get:set;} public string Namespace {get:set:} // }
Контракты 23 Атрибут используется для определения контракта службы. Он применяется к интерфейсу или к классу, как показано в листинге 1.1. Листинг 1.1. Определение и реализация контракта службы [Servicecontract] interface IMyContract { [Operationcontract] string MyMethod(string text): И He входит в контракт string MyOtherMethod(string text): I class MyService : IMyContract ( public string MyMethod(string text) return "Hello " + text: ) public string MyOtherMethodistring text) return "Cannot call this method over UCF"; Атрибут ServiceContract отображает интерфейс CLR (или обусловленный ин- терфейс, как мы вскоре увидим) на технологически-нейтральный контракт служ- бы. Атрибут ServiceContract представляет интерфейс (или класс) CLR как кон- тракт WCF, независимо от видимости его типа. Видимость типа на WCF вооб- ще не влияет, потому что она относится к концепциям CLR. Применение атрибута ServiceContract к внутреннему интерфейсу раскрывает этот интерфейс как публичный контракт службы, готовый к внешнему использованию. Без ат- рибута ServiceContract интерфейс остается невидимым для клиентов WCF. Все контракты должны быть сформулированы явно: только интерфейсы (и клас- сы), помеченные атрибутом ServiceContract, считаются контрактами WCF. Ос- тальные типы к таковым не относятся. Ни один из членов типа не будет автоматически включен в контракт при ис- пользовании атрибута ServiceContract. Вы должны явно указать WCF, какие ме- тоды включаются в контракт, при помощи атрибута OperationContractAttribute: [Attri buteUsage(Attri buteTargets. Method) ] public sealed class OperationContractAttribute : Attribute public string Name (get:set:} // Атрибут Operationcontract применим только к методам, но не к свойствам, индексаторам или событиям — все они относятся к концепциям CLR. WCF по- нимает только операции (логические функции), а атрибут OperationContract представляет метод контракта как логическую операцию, выполнение которой составляет часть контракта службы. Другие методы интерфейса (или класса),
24 Глава 1. Основные сведения о WCF не имеющие атрибута Operationcontract, в контракт не включаются. Тем самым обеспечиваются принципы четких границ служб и явного формулирования операций. Кроме того, в параметрах контрактных операций не могут использо- ваться ссылки на объекты — разрешены только примитивные типы или кон- тракты данных. Применение атрибута Servicecontract WCF позволяет применять атрибут ServiceContract к интерфейсам и классам. Если атрибут применяется к интерфейсу, некоторый класс должен реализовать этот интерфейс. Обычно для реализации интерфейса используется код C# или VB, и ничто в коде класса службы не указывает на то, что он принадлежит службе WCF: [ServiceContract] Interface IMyContract { [Operationcontract] string MyMethodO; } class MyService : IMyContract { public string MyMethodO { return "Hello WCF"; } } Реализация интерфейса может быть как косвенной, так и явной: class MyService : IMyContract { string IMyContract.MyMethodO { return "Hello WCF"; } } Один класс может поддерживать несколько контрактов; это делается по- средством наследования и реализации нескольких интерфейсов, помеченных атрибутом ServiceContract. [ServiceContract] Interface IMyContract { [Operationcontract] string MyMethodO; } [ServiceContract] Interface IMyOtherContract { [Operationcontract] void MyOtherMethodO; } class MyService : IMyContract.IMyOtherContract
Контракты 25 ( public string MyMethodO (...) public void MyOtherMethodO (...) } Впрочем, для класса реализации службы устанавливается ряд ограничений. Следует избегать параметризованных конструкторов, потому что WCF исполь- зует только конструктор по умолчанию. И хотя класс может использовать внут- ренние свойства, индексаторы и статические члены, клиент WCF ни при каких условиях не сможет работать с ними. WCF также позволяет применить атрибут ServiceContract непосредственно к классу службы, без предварительного определения отдельного контракта: // Неяелательно [ServiceContract] class MyService ( [Operationcontract] string MyMethodO return "Hello WCF": ) } «За кулисами» WCF все равно генерирует определение контракта. Атрибут OperationContract может применяться к любым методам класса — как приват- ным, так и открытым. ВНИМАНИЕ ----------------------------------------------------------------------------------- Постарайтесь обойтись без прямого использования атрибута ServiceContract с классами служб. Все- гда определяйте отдельный контракт, чтобы его можно было использовать в других контекстах. Имена и пространства имен Для своего контракта можно определить пространство имен — более того, это следует сделать. Пространство имен контракта служит в WCF той же цели, что и в программировании .NET: оно определяет область действия типа контракта и снижает вероятность конфликтов. Для определения пространства имен ис- пользуется свойство Namespace атрибута ServiceContract: [Servi ceContract (Namespace = "MyNamespace")] interface IMyContract (•} Если пространство имен контракта не указано, по умолчанию используется значение http://tempuri.org. Для наружных служб обычно используется URL ком- пании, а для интрасетевых служб — любое осмысленное уникальное имя (на- пример, MyApplication). По умолчанию внешнее имя контракта совпадает с именем используемого интерфейса. Однако для контракта можно определить псевдоним, чтобы клиен- ты видели его под другим именем — для этой цели используется свойство Name атрибута ServiceContract:
26 Глава 1. Основные сведения о WCF [ServiceContract(Name="IMyContract")] interface IMyOtherContract Аналогично, внешнее имя публичной операции по умолчанию совпадает с именем метода, но свойство Name атрибута Operationcontract позволяет опреде- лить для него псевдоним-заменитель: [ServiceContract] interface IMyContract { [OperationContractCName = "SomeOperation")] void MyMethod(string text): } Примеры использования этих свойств будут представлены в следующей главе. Хостинг Класс службы WCF не может существовать «сам по себе». Каждая служба WCF должна находиться под управлением некоторого процесса Windows, на- зываемого хостовым процессом. Один хостовой процесс может управлять не- сколькими службами, и один тип службы может иметь несколько хостовых процессов. WCF не требует, чтобы хостовой процесс был (или не был) клиент- ским процессом. Очевидно, наличие отдельного процесса способствует безопас- ности и изоляции ошибок. Также неважно, кто предоставляет процесс или ка- кой тип процесса задействован в хостинге. Хост может предоставляться IIS, службой WAS (Windows Activation Service) в Windows Vista или разработчи- ком как часть приложения. ПРИМЕЧАНИЕ ------------------------------------------------------ Особым случаем является внутрипроцессный хостинг, при котором служба размещается в одном процессе с клиентом. Хост для внутрипроцессной разновидности по определению предоставляется разработчиком. Хостинг IIS Главное преимущество хостинга служб в веб-сервере Microsoft IIS (Internet Information Server) заключается в том, что хостовой процесс автоматически за- пускается по первому запросу клиента, а его жизненный цикл находится под управлением IIS. С другой стороны, главный недостаток хостинга IIS — воз- можность использования только HTTP. В IIS5 устанавливается дополнитель- ное ограничение: все службы должны использовать один номер порта. Хостинг в IIS очень похож на хостинг классических веб-служб ASMX. Вы должны создать в IIS виртуальный каталог и предоставить файл .svc. Файл .svc — аналог файла .asmx — используется для идентификации файла программ- ной логики службы. Синтаксис файла .svc представлен в листинге 1.2.
Листинг 1.2. Название листинга <№ ServiceHost Language - "С#" Debug = "true" CodeBenind = "~/App_Code/MyService.cs" Service -- "MyService" ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Код службы даже можно встроить в файл .svc, но делать это не рекомендуется, как и в случае с веб- службами ASMX. При использовании хостинга IS базовый адрес службы всегда должен совпа- дать с адресом файла .svc. Visual Studio 2005 Visual Studio 2005 генерирует шаблонный код службы с хостингом IIS. Выпол- ните команду File ► New Website, а затем выберите в окне New Web Site пункт WCF Service. Visual Studio 2005 создает новый веб-сайт, код службы и соответствую- щий файл .svc. Также можно воспользоваться диалоговым окном Add New Item для добавления другой службы. Файл Web.Config В конфигурационном файле сайта (Web.config) должны быть перечислены типы, к которым предоставляется доступ как к службам. В перечислении необ- ходимо использовать полностью уточненные имена типов с включением имен сборок, если тип службы находится в сборке, на которую не существует ссылки: <system.serviceModel> <services> <service name = "MyNamespace.MyServ1ce"> .. J </service> </services> </system.serwiceModel> Автохостинг Термином «автохостинг» обозначается ситуация, при которой разработчик от- вечает за предоставление хостового процесса и управление его жизненным цик- лом. Автохостинг используется как в том случае, если клиент с сервером находят- ся в разных процессах (или на разных компьютерах), так и при внутрипроцессном использовании службы, то есть при ее нахождении в одном процессе с клиентом. Процесс, который должен предоставить разработчик, может быть любым про- цессом Windows — приложением Windows Forms, консольным приложением или службой Windows NT. Учтите, что процесс должен быть запущен до того, как клиент обратится к службе (обычно это означает предварительный запуск). Для служб NT и внутрипроцессного хостинга такой проблемы не существует. 1 Естественно, речь идет не об «автоматическом» хостинге, а о «самохосгинге». - Примеч. перев.
28 Глава 1. Основные сведения о WCF Предоставление хоста обычно реализуется несколькими строками программного кода, и этот способ обладает некоторыми преимуществами перед хостингом IIS. По аналогии с хостингом IIS, в конфигурационном файле хостового прило- жения (Арр.Config) перечисляются типы служб, к которым предоставляется внешний доступ под управлением хоста: <system.servlceModel> <services> <service name = "MyNamespace.MyService"> </service> </services> </system.serviceModel> Кроме того, хостовой процесс должен явно зарегистрировать типы служб во время выполнения и открыть хост для клиентских вызовов; именно по этой причине хостовой процесс должен выполняться до появления клиентских вы- зовов. Как правило, хост создается в методе Main() с использованием класса ServiceHost, как показано в листинге 1.3. Листинг 1.3. Класс ServiceHost public interface ICommunicationObject { void OpenO; void CloseO; // } public abstract class Communicationobject : ICommunicationObject (...) public abstract class ServiceHostBase : CommunicationObject.IDisposable.... {...} public class ServiceHost : ServiceHostBase.... { public ServiceHostCType serviceType.params Uri[] baseAddresses): // При вызове конструктора ServiceHost передается тип службы и возможно (но не обязательно) — базовые адреса по умолчанию. Набор базовых адресов может быть пустым. Даже если базовые адреса передаются, служба может быть на- строена на использование других базовых адресов. Наличие набора базовых ад- ресов позволяет службе принимать вызовы по разным адресам и протоколам, с использованием только относительного URI. Обратите внимание: каждый эк- земпляр ServiceHost связывается с определенным типом службы, и если хосто- вой процесс должен управлять несколькими типами служб, потребуется соот- ветствующее количество экземпляров ServiceHost. Вызов метода Ореп() для хоста разрешает прием вызовов, а вызов Close() обеспечивает корректное завер- шение экземпляра хоста — текущие вызовы будут обработаны, но дальнейшие вызовы от клиентов отклоняются, несмотря на то, что хостовой процесс еще ра- ботает. Закрытие обычно происходит при завершении работы хостового про- цесса. Например, код хостинга следующей службы в приложении Windows Forms:
[ServiceContract] Interface IMyContract (...) class MyService : IMyContract (...) будет выглядеть примерно так: public static void Main() Uri baseAddress = new Uri("http://localhost:8000/"): ServiceHost host = new ServiceHost(typeof(MyService).baseAddress): host.Open0: И Блокирующие вызовы: Application.Run(new MyFormO): host.CloseO: ) Открытие хоста приводит к загрузке runtime-среды ЦСА и запуску рабочих потоков для отслеживания входящих запросов. Благодаря участию рабочих по- токов после открытия хоста можно выполнять блокирующие операции. Явный контроль над открытием и закрытием хоста открывает удобную возможность, ко- торая трудно реализуема при хостинге IIS: вы можете построить специализиро- ванное управляющее мини-приложение, в котором администратор явно открыва- ет и закрывает хост по своему усмотрению, без завершения хостового процесса. Visual Studio 2005 Visual Studio 2005 позволяет включить службу WCF в любой проект приложе- ния; для этого в диалоговом окне Add New Item выбирается пункт WCF Service. Ко- нечно, служба, добавленная подобным образом, является внутрипроцессной по отношению к хостовому процессу, но внепроцессные клиенты тоже могут обра- щаться к ней. Автохостинг и базовые адреса При запуске хоста службы базовые адреса могут вообще не указываться: public static void MainO ServiceHost host = new ServiceHost(typeof(typeof(MyService)): host.OpenO: Application.Run(new MyFormO): host.CloseO; } ВНИМАНИЕ --------------------------------------------------------------------------------- He передавайте null вместо пустого списка, потому что это приведет к возникновению исключения: ServiceHost host: host = new ServiceHost(typeof(typeof(MyService).null):
30 Глава 1. Основные сведения о WCF Также можно зарегистрировать несколько базовых адресов, разделенных за- пятыми, при условии, что адреса не используют одинаковую транспортную схе- му, как в следующем фрагменте (обратите внимание на использование квали- фикатора params в листинге 1.3): Uri tcpBaseAddress = new Url("net.tcp://1 оса 1 host:8001/"); Uri httpBaseAddress = new Uri("http://localhost:8002/"); ServiceHost host = new ServiceHost(typeof(MyService). tcpBaseAddress,httpBaseAddress): WCF также позволяет перечислить базовые адреса в конфигурационном файле хоста: <system.serviceModel> <services> <service name = "MyNamespace.MyService"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "http://localhost:8002/"/> </baseAddresses> </host> </service> </services> </system.serviceModel > При создании хоста используется базовый адрес, обнаруженный в файле конфигурации, а также все базовые адреса, заданные на программном уровне. Будьте внимательны и следите за тем, чтобы адреса, заданные в двух разных местах, не пересекались. Допускается даже регистрация нескольких хостов для одного типа службы при условии, что они используют разные базовые адреса: Uri baseAddressl = new Uri("net.tcp://localhost:8001/"): ServiceHost hostl = new ServiceHost(typeof(MyService).baseAddressl): hostl,0pen(); Uri baseAddress2 = new Uri("net.tcp://localhost:8002/"); ServiceHost host2 = new ServiceHost(typeof(MyService),baseAddress2): host2.0pen(); Тем не менее, за исключением некоторых аспектов, связанных с программ- ными потоками (см. главу 8), открытие нескольких хостов подобным образом не дает никаких реальных преимуществ. Кроме того, открытие нескольких хос- тов для одного типа не работает с базовыми адресами, заданными в конфигура- ционном файле, и требует использования конструктора ServiceHost. Расширенные возможности хостинга Интерфейс ICommunicationObject, поддерживаемый классом ServiceHost, предо- ставляет ряд расширенных возможностей. Некоторые из них продемонстриро- ваны в листинге 1.4.
Листинг 1.4. Интерфейс ICommunicationObject public interface ICommunicationObject ( void OpenO. void Close! void Abort(): event EventHandler Closed: event EventHandler Closing: event EventHandler Fdulted; event EventHandler Opened: event EventHandler Opening; lAsyncResult BeginClosetAsyncCal 1 back calIback.object state). lAsyncResult BeginOpen(AsyncCalIback calIback.object state): void EndClose< lAsyncResult result): void EndOpendAsyncResuit result): Communication?taьз State (get;} // public enum CommumcationState ( Created. Opening. Opened. Closing. Closed. Faulted Если операции открытия или закрытия хоста занимают много времени, их можно выполнять асинхронно с использованием методов BeginOpen() и Begin- Close(). Возможна подписка на события хостинга (например, изменения состоя- ния или сбои), а также использование свойства State для проверки состояния хоста. Наконец, класс ServiceHost также реализует метод аварийного заверше- ния Abort(). При вызове Abort() все необработанные вызовы службы немедленно отменяются, а хост завершает свою работу. Активные клиенты получают ис- ключение. Класс ServiceHost<T> Класс ServiceHost, предоставленный WCF, можно усовершенствовать. Для этого мы определим класс ServiceHost<T>, представленный в листинге 1.5. Листинг 1.5. Класс ServiceHost<T> public class ServiceHost<T> : ServiceHost public ServiceHost!) basettypeof(T)) {} public ServiceHosttparams string[] baseAddress) : base(typeof(T).Convert(baseAddresses)) (} продолжение &
32 Глава 1. Основные сведения о WCF public ServiceHost(params Uri[] baseAddress) : base(typeof(T).baseAddresses) {} static Lirin Convert(string[] baseAddresses) Converter<string.llri> convert = delegate(string address) { return new Uri(address): }; return Array.ConvertAl1(baseAddresses.convert): } ServiceHost<T> предоставляет простые конструкторы, которые не требуют пе- редачи типа службы и могут работать с обычными строками вместо неудобных Uri. В книге шаблон ServiceHost<T> будет дополнен целым рядом расширений, функций и возможностей. Хостинг WAS WAS (Windows Activation Service) — системная служба, появившаяся в Win- dows Vista. WAS является частью IIS7, но может устанавливаться и настраи- ваться отдельно. Чтобы использовать WAS для хостинга службы WCF, необхо- димо предоставить файл .svc, как и при хостинге HS. Основное различие между IIS и WAS заключается в том, что WAS не ограничивается протоколом HTTP и может использоваться с любыми доступными в WCF транспортами, портами и очередями. WAS обладает многими преимуществами перед автохостингом: поддержка пулов приложений, повторное использование ресурсов, управление бездейст- вием, управление идентификацией и изоляция. Именно эту разновидность хос- товых процессов следует выбирать там, где это возможно — то есть когда ком- пьютер с Vista Server используется для обеспечения масштабируемости, или клиентский компьютер с Vista выполняет функции сервера для небольшой группы клиентов. Впрочем, у автохостинга тоже имеются свои преимущества — внутрипро- цессный хостинг, решение проблем с неизвестными клиентскими средами и простой программный доступ к расширенным возможностям хостинга, о ко- торых упоминалось ранее. Привязки Взаимодействие с любой конкретной службой имеет много аспектов. К тому же существует немало коммуникационных схем: синхронная передача по принци- пу «запрос/ответ» или асинхронная передача по принципу «отправить и за- быть»; двусторонние сообщения; немедленная доставка или постановка в оче- редь; наконец, сами очереди могут быть надежными или неустойчивыми. Существует много возможных транспортных протоколов для передачи сообще- ний: HTTP (или HTTPS), TCP, P2P (одноранговая сеть), IPC (именованные
Привязки 33 каналы) или MSMQ. Существуют разные варианты кодирования сообщений: пе- редача простого текста для обеспечения совместимости, двоичное шифрование для повышения производительности, МТОМ (Message Transport Optimization Mechanism) для больших объемов полезной информации. Также необходимо учитывать разные варианты безопасности сообщений: отсутствие защиты, за- щита только на транспортном уровне, защита конфиденциальности на уровне сообщений и конечно, разные способы аутентификации и авторизации клиен- тов. Доставка сообщений может быть надежной или ненадежной в отношении промежуточных инстанций и разрывов связи; сообщения могут обрабатываться в порядке их отправки или в порядке получения. Возможно, вашей службе при- дется взаимодействовать с другими службами или клиентами, поддерживаю- щими только простейший протокол веб-служб, или они будут поддерживать со- временные протоколы WS-* (такие, как WS-Security и WS-Atomic Transactions). Нельзя исключать и возможность общения с унаследованными (legacy) клиента- ми посредством низкоуровневых сообщений MSMQ, или же служба будет ограни- чиваться взаимодействием только с другой службой или клиентом WCF. Если начать перечислять все возможные варианты коммуникаций и взаимо- действий, количество всевозможных комбинаций будет исчисляться десятками тысяч. Одни варианты являются взаимоисключающими, другие подразумевают обязательный выбор других вариантов. Конечно, нормальное взаимодействие становится возможным только в том случае, если клиент и служба совмещены по всем допустимым вариантам. Решение сложных проблем такого уровня в большинстве приложений не добавляет практической пользы, но принятие неверных решений будет иметь серьезные последствия для производительно- сти и качества. Чтобы упростить принятие решений и управление наборами вариантов, WCF группирует подобные коммуникационные аспекты в привязках. Привязка (binding) представляет собой логически согласованный, фиксированный набор настроек, относящихся к транспортному протоколу, кодированию сообщений, коммуникационной схеме, надежности, безопасности, распространению тран- закций и совместимости. В идеальном случае все эти «технические» аспекты должны быть выведены из кода службы, чтобы служба могла сосредоточиться исключительно на реализации бизнес-логики. Привязки дают возможность ис- пользовать ту же логику службы со значительно меньшим объемом «техниче- ского» кода. Вы можете использовать привязки WCF в их исходном виде, настраивать их свойства, а также писать собственные привязки «с нуля». Служба публикует свою подборку привязок в метаданных, предоставляя возможность клиентам запрашивать тип и специфические свойства привязки, потому что клиент дол- жен использовать точно такие же параметры привязки, что и служба. Одна служба может поддерживать несколько привязок по разным адресам. Стандартные привязки WCF определяет девять стандартных привязок: О базовая привязка — реализуется классом BasicHttpBinding. Привязка предна- значена для предоставления доступа к службе WCF через унаследованный
34 Глава 1. Основные сведения о WCF механизм ASMX, чтобы старые клиенты могли работать с новыми служба- ми. При использовании на стороне клиента позволяет новым клиентам WCF работать со старыми службами ASMX; О привязка TCP — реализуется классом NetTcpBinding. Привязка использует ТОР для межкомпьютерных взаимодействий в интрасетях. Поддерживает широ- кий набор возможностей, включая надежность, транзакции и безопасность, оптимизирована для взаимодействий WCF-WCF. Как следствие требует, чтобы и клиент, и служба использовали WCF; О привязка одноранговой сети — реализуется классом NetPeerTcpBinding. При- вязка использует одноранговую сеть в качестве транспорта. И клиент, и служ- бы с одноранговой поддержкой подписываются на одну матрицу (grid) и рас- сылают в ней сообщения. Одноранговые сети выходят за рамки книги, потому что для их описания требуется понимание топологии матриц и организации вычислений в сетчатых структурах; О привязка IPC — реализуется классом NetNamedPipeBinding. Привязка исполь- зует именованные каналы в качестве транспорта в пределах компьютера. Данный вид привязок является наиболее защищенным, потому что он не принимает внешние (по отношению к компьютеру) вызовы и поддерживает разнообразные дополнительные функции, как и привязки TCP; О привязка WS (Web Service) — реализуется классом WSHttpBinding. Привязка использует HTTP или HTTPS в качестве транспорта и поддерживает такие возможности, как надежность, транзакции и безопасность в Интернете; О федеративная привязка WS — реализуется классом WSFedrationHttpBindingCLass. Привязка является частным случаем привязок WS с поддержкой федера- тивной безопасности. Тема федеративной безопасности выходит за рамки книги; О дуплексная привязка WS — реализуется классом WSDualHttpBinding. Привязка аналогична привязкам WS, но дополнительно поддерживает двусторонние взаимодействия между службой и клиентом, как обсуждалось в главе 5; О привязка MSMQ - реализуется классом NetMsmqBinding. Привязка исполь- зует MSMQ в качестве транспорта и была спроектирована для поддержки очередей автономных вызовов. Работе с этим видом привязок посвящена глава 9; О интеграционная привязка MSMQ — реализуется классом Msmqlntegration- Binding. Привязка преобразует сообщения WCF в сообщения MSMQ и об- ратно и была спроектирована для взаимодействия с унаследованными кли- ентами MSMQ. Использование данного вида привязок выходит за рамки книги. Формат и кодирование Стандартные привязки используют разные виды транспорта и кодирования, пе- речисленные в табл. 1.1.
Привязки 35 Таблица 1.1. Транспорт и кодирование для стандартных привязок (кодирование по умолчанию выделено жирным шрифтом) Название Транспорт Кодирование Взаимодействие BasicHttpBinding HTTP/HTTPS Текст, MTOM Да NetTcpBinding TCP Двоичное Нет NetPeerTcpBinding P2P Двоичное Нет NetNamedPipeBinding IPC Двоичное Нет WSHttpBinding HTTP/HTTPS Текст, МТОМ Да WSFederationHttpBinding HTTP/HTTPS Текст, МТОМ Да WSDualHttpBinding HTTP Текст, МТОМ Да NetMsmqBinding MSMQ Двоичное Нет MsmqlntegrationBinding MSMQ Двоичное Да Поддержка текстового кодирования позволяет службе (или клиенту) WCF общаться но HTTP с любой другой службой (пли клиентом) независимо от ее технологии. Двоичное кодирование по TCP или IPC обеспечивает наилучшее быстродействие, но за счет ограничения совместимости, так как оно применимо только во взаимодействиях WCF-WCF. Выбор привязки Логика выбора привязки для службы показана на рис. 1.4. Рис. 1-4- Выбор привязки Прежде всего следует спросить себя, должна ли ваша служба взаимодейст- вовать с другими клиентами, кроме клиентов WCF? Если ответ будет положи- тельным, а клиент являе гея унаследованным клиентом MSMQ, выбирайте при- вязку MsmqlntegrationBinding она позволит вашей службе взаимодействовать
36 Глава 1. Основные сведения о WCF с таким клиентом через MSMQ. Если необходимо взаимодействие с не-WCF клиентом, поддерживающим базовый протокол веб-служб (ASMX), выбирайте привязку BasicHttpBinding — служба WCF будет представлена «внешнему миру» так, словно она является службой ASMX. Правда, в этом случае вы лишаетесь возможности использовать большинство современных протоколов WS-*. Если же не-WCF клиент понимает эти стандарты, выбирается одна из привязок WS (WSHttpBinding, WSFederationBinding или WSDualHttpBinding). Если клиент является клиентом WCF, но требует автономного взаимодействия, выбирается привязка NetMsmqBinding, которая позволяет использовать MSMQ для транспорта сооб- щений. Если взаимодействие с клиентом должно проходить только в подклю- ченном состоянии, но клиент и служба находятся на разных компьютерах, вы- бирается привязка NetTcpBinding на базе TCP. Если клиент находится на одном компьютере со службой, выбирается привязка NetNamedPipeBinding, использую- щая именованные каналы для обеспечения максимальной производительности. Выбор уточняется по дополнительным критериям — таким, как необходи- мость использования обратных вызовов (WSDualHttpBinding) или федеративная безопасность (WSFederationBinding). ПРИМЕЧАНИЕ ------------------------------------------------------------------ Многие привязки работают за пределами своих основных сценариев. Например, привязка TCP мо- жет использоваться на одном компьютере и даже во внутрипроцессных комммуникациях, а базовая привязка — в интрасетевых коммуникациях WCF-WCF. И все же старайтесь выбирать привязку в со- ответствии с диаграммой на рис. 1.4. Использование привязок Каждая привязка обладает буквально десятками настраиваемых свойств. Суще- ствуют три режима работы с привязками. Во-первых, вы можете пользоваться встроенными привязками «как есть», если они соответствуют вашим требова- ниям. Во-вторых, можно настроить некоторые из их свойств — таких, как рас- пространение транзакций, надежность и безопасность. В-третьих, вы можете написать собственную пользовательскую привязку. Самый распространенный сценарий — использование существующих привязок почти в исходном виде, с настройкой двух-трех аспектов. Разработчикам приложений вряд ли потребу- ется писать пользовательские привязки, но у разработчиков библиотек такая необходимость может возникнуть. Конечные точки Каждая служба связывается с адресом, определяющим местоположение служ- бы; привязкой, определяющей способ взаимодействия со службой; и контрактом, который указывает, что делает служба. В WCF эта тройка атрибутов формализу- ется в концепции конечной точки. Конечная точка (endpoint) представляет со- бой совокупность адреса, контракта и привязки (рис. 1.5). Каждая конечная точка должна обладать всеми тремя элементами, а хост предоставляет ее внешнему пользователю. На логическом уровне конечная точ- ка определяет интерфейс службы и может рассматриваться как аналог интер- фейса CLR или СОМ. На рис. 1.5 для ее представления используется традици- онная диаграмма типа «леденец на палочке».
ПРИМЕЧАНИЕ------------------------------------------------------------------ На концептуальном уровне, даже в C# и VB, интерфейс является конечной точкой: адрес соответст- вует адресу памяти виртуальной таблицы типа, привязка — JIT-компиляции CLR, а контракт —само- му интерфейсу. В классическом программировании .NET программист не имеет дела с адресами или привязками, а просто принимает их как должное. В WCF адрес и привязка не устанавливаются извне и их приходится настраивать явно. Каждая служба должна предоставлять как минимум одну рабочую конечную точку, и каждая конечная точка имеет ровно один контракт. Все конечные точ- ки службы имеют уникальные адреса, и одна служба может предоставлять не- сколько конечных точек. Эти конечные точки могут использовать одинаковые или разные привязки, а также предоставлять одинаковые или разные контрак- ты. Разные конечные точки, предоставляемые службой, абсолютно никак не связаны друг с другом. Следует помнить, что конечные точки никак не представлены в коде служ- бы; они всегда являются внешними по отношению к коду службы. Конечные точки настраиваются на административном уровне (в конфигурационном фай- ле) или на программном уровне. Административная настройка конечной точки Административная настройка конечной точки осуществляется посредством ее размещения в конфигурационном файле хостового процесса. Для примера возьмем следующее определение службы: namespace MyNamespace { [ServiceContract] interface IMyContract class MyService IMyContract } В листинге 1.6 показаны необходимые записи в конфигурационном файле. Для каждого типа службы перечисляются его конечные точки. Листинг 1.6. Административная настройка конечной точки <system.serviceModel> <services> <service name = "MyNamespace.MyService"> продолжение &
38 Глава 1. Основные сведения о WCF <endpoint address = "http://1ocalhost:8000/MyServiсе/" binding = "wsHttpBinding" contract = "MyNamespace.IMyContract" /> </service> </services> </system.servi ceModel> При указании службы и контракта необходимо использовать полностью уточненные имена типов. Я не буду включать пространства имен в остальных примерах книги, но вы должны использовать их там, где это уместно. Если ко- нечная точка предоставляет базовый адрес, то адресная схема должна соответ- ствовать привязке — например, HTTP с WSHttpBinding. Несоответствие приве- дет к выдаче исключения во время загрузки службы. В листинге 1.7 приведен конфигурационный файл с определением одной службы, предоставляющей несколько конечных точек. Несколько конечных то- чек можно настроить с одним базовым адресом (при условии, что их URI раз- личаются). Листинг 1.7. Определение нескольких конечных точек для одной службы <service name = "MyService"> <endpoi nt address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoi nt address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service> В большинстве случаев применяется административная конфигурация, по- тому что она обладает большей гибкостью: адрес службы, привязку и даже кон- тракты можно менять без повторной сборки и развертывания службы. Использование базовых адресов В листинге 1.7 каждая конечная точка предоставляла свой базовый адрес. Явно заданный базовый адрес заменяет любые базовые адреса, которые могли быть предоставлены хостом. Несколько конечных точек могут иметь одинаковый базовый адрес, если ад- реса конечных точек различаются в части URI: <service name = "MyService"> <endpoint address = "net.tcp://localhost:8001/MyService/"
Привязки 39 binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp.//local host:8001/MyOtherService/" binding = "netTcpBinding" contract = "IMyContract" /> </service> Также возможен другой вариант: если хост предоставляет базовый адрес с под- ходящей транспортной схемой, адрес можно не указывать. В этом случае адрес конечной точки совпадает с базовым адресом транспорта: <endpoint binding = "wsHttpBinding" contract = "IMyContract" /> Если хост не предоставит подходящий базовый адрес, попытка загрузки хос- та завершится с исключением. Задавая адрес конечной точки, можно просто указать относительный URI под базовым адресом: <endpoint address = "SubAddress" binding = "wsHttpBinding" contract = "IMyContract" /> Адрес конечной точки в данном случае складывается из базового адреса и URI (как было сказано ранее, хост обязан предоставить подходящий базовый адрес). Конфигурация привязки Конфигурационный файл может использоваться для настройки привязок, ис- пользуемых конечной точкой. Для этого в секцию endpoint включается тег bindingconfiguration с именем, заданным в секции bindings конфигурационного файла. Назначение тега transaction Flow будет объяснено в главе 7. Листинг 1.8. Конфигурация привязки на стороне службы <system. servi ceMode'! > <services> <service name - "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService/" bindingconfiguration = "TransactionalTCP” binding = "netTcpBinding" contract = "IMyContract" <endpoint address = "net.tcp-//localhost:8001/MyService/" bindingconfiguration = "TransactionalTCP” binding = "netTcpBinding" contract = "IMyOtherContract" продолжение &
40 Глава 1. Основные сведения о WCF </serv1ce> </service> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.servi ceModel> Как видно из листинга 1.8, именованную конфигурацию привязки можно использовать в нескольких конечных точках, просто указывая ее имя. Программная настройка конечной точки Программная настройка конечной точки функционально эквивалентна ее адми- нистративной настройке. Вместо использования конфигурационного файла ко- нечные точки включаются в экземпляр ServiceHost программными вызовами. Как и при административной настройке, эти вызовы всегда производятся за пределами кода службы. Класс ServiceHost предоставляет перегруженные вер- сии метода AddServiceEndpoint(): public class ServiceHost : ServiceHostBase { public ServiceEndpoInt AddServiceEndpoint(Type ImplementedContract, Binding binding, string address); // Другие члены } Методам AddServiceEndpoint() могут передаваться как относительные, так и абсолютные адреса, как и при использовании конфигурационных файлов. В листинге 1.9 продемонстрирована программная настройка тех же конечных точек, что и в листинге 1.7. Листинг 1.9. Программная настройка конечных точек на стороне службы ServiceHost host = new ServiceHost(typeof(MyService)): Binding wsBlndlng = new WSHttpBIndlng(): Binding tcpBlndlng = new NetTcpB1nd1ng(); host.AddServiceEndpoi nt(typeofCIMyCont ract).wsBIndlng, "http://1 oca 1 host:8000/MyServlce"): host.AddServiceEndpoi nt(typeof(IMyContract),tcpBInding, "net.tcp://1 oca 1 host:8001/MyServlce"); host.AddServiceEndpoint(typeof(IMyOtherContract).tcpBlndlng, "net.tcp://local host:8002/MyService"): host.OpenO: При добавлении конечной точки на программном уровне адрес задается в формате строки, контракт — в формате Туре, а привязка — в формате одного из субклассов абстрактного класса Binding:
Привязки 41 public class NetTcpBinding • Binding. (...) Если вы намерены использовать базовый адрес хоста, передайте пустую строку (используется только базовый адрес) или только URI для использова- ния комбинации базового адреса с URI): Uri tcpBaseAddress = new Uri ("net .tcp://localhost:8000/"). ServiceHost host = new ServiceHost(tyoeof(MyService). tcpBaseAddress): Binding tcpBinding = new NetTcpBinding(): // Использовать только базовый адрес host.AddServiceEndpoint(typeof(IMyContract) .tcpBinding. ”"); // Использовать относительный адрес host.AddServiceEndpoint(typeof( IMyContract) .tcpBinding. "MyService"): // Игнорировать базовый адрес host.AddServiceEndpoint;typeof( IMyCont ract). tcpBi ndi ng. "net.tcp://1ocalhost:8001/MyServ ice"): host.OpenO: Как и при административной конфигурации, хост должен предоставить под- ходящий базовый адрес; в противном случае происходит исключение. По сути программная настройка почти не отличается от аппаратной. Когда вы исполь- зуете конфигурационный файл, WCF просто разбирает его и выполняет соот- ветствующие программные вызовы. Свойства привязки можно задать на программном уровне. Например, сле- дующий код необходим для включения распространения транзакций по анало- гии с листингом 1.8: ServiceHost host = new ServiceHost(typeof(MyService)): NetTcpBinding tctBinding -- new NetTcpBinding(): tcpBinding.TransactionFlow = true: host.AddServiceEndpoint(typeof(IMyContract). tcpBinding. "net.tcp://local host:8000/MyService"): host.OpenO: Обратите внимание: при настройке свойств привязок обычно используется конкретный субкласс привязки (такой, как NetTcpBinding), а не абстрактный класс Binding, как в примере 1.9. Обмен метаданными Публикация метаданных службы осуществляется двумя способами: по прото- колу HTTP-GET или при помощи специализированной конечной точки (см. далее). WCF может автоматически организовать передачу метаданных по HTTP- GET для вашей службы; все, что для этого потребуется — явно включить соот- ветствующее поведение для службы. Аспекты поведения рассматриваются в следующих главах. А пока достаточно сказать, что аспект поведения является локальной характеристикой службы — например, желает ли она обмениваться
42 Глава 1. Основные сведения о WCF метаданными через HTTP-GET. Поведение включается на административном или программном уровне. В листинге 1.10 приведен конфигурационный файл хостового приложения, в котором обе службы содержат ссылку на секцию, раз- решающую обмен метаданными через HTTP-GET. Адрес, который должен ис- пользоваться клиентами для HTTP-GET, является зарегистрированным базо- вым адресом HTTP для службы. В определении поведения также можно задать внешний URL для этой цели. Рис. 1.6. Страница подтверждения хостинга службы Листинг 1.10. Включение обмена метаданными в конфигурационном файле <system.servi ceModel> <services> <service name = "MyService" behaviorConfguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8000/"> </baseAddresses> </host>
Привязки 43 </service> service name = "MyOtherService" behaviorConfguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8001/"> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "MEXGET"> <serviceMetadata httpGetEnabled = "true”/> </beha vi on </serviceBehaviors> </behaviors> </system. servi ceModel > После включения обмена метаданными по HTTP-GET базовый адрес HTTP (если он имеется) можно открыть в браузере. Если все прошло нор- мально, вы получите страницу подтверждения, показанную на рис. 1.6; это оз- начает, что хостинг службы организован успешно. Страница подтверждения не относится к хостингу I IS; открыть адрес службы в браузере можно даже при ав- тохостинге. Программное включение обмена метаданными Чтобы включить обмен метаданными через HTTP-GET на программном уров- не, необходимо сначала добавить соответствующий аспект поведения в коллек- цию аспектов, поддерживаемую хостом для типа службы. Класс ServiceHostBase содержит свойство Description типа ServiceDescription: public abstract class ServiceHostBase public ServiceDescription Description (get:} // ) Тип ServiceDescription, как следует из его названия, описывает службу со все- ми ее аспектами и характеристиками. ServiceDescription содержит свойство Beha- viors типа KeyedByTypeCollection<I> с обобщенным параметром IServiceBehavior: public class KeyedByTypeCol lection<I> : KeyedCollection<]ype. 1> ( public T F1nd<।>(1: public T RemovnW ); // public clas> ServiceDescription public keyedBylypeCol lection<IServiceBehavior> Behaviors (get.}
44 Глава 1. Основные сведения о WCF Интерфейс IServiceBehavior реализуется всеми классами и атрибутами аспек- тов поведения. KeyedByTypeCoLLection<I> предоставляет шаблонный метод Find<T>(), который возвращает запрашиваемый аспект поведения, если он присутствует в коллекции, или null в противном случае. Конкретный тип аспекта поведения может быть найден в коллекции только один раз. Листинг 1.11 показывает, как включать аспекты поведения на программном уровне. Листинг 1.11. Включение обмена метаданными на программном уровне ServiceHost host = new ServiceHost(typeof(MyServiсе)): ServiceMetadataBehavlor metadataBehavior: metadataBehavior = host.Descri pti on.Behaviors.F i nd<ServiceMetadataBehavior>(); if(metadataBehavior == null) metadataBehavior = new ServiceMetadataBehavior(): metadataBehavior.HttpGetEnabled = true: host.Descript ion.Behaviors.Add(metadataBehavior); } host.OpenO: Сначала код хостинга проверяет, что поведение конечной точки обмена мета- данными не было включено в конфигурационном файле; для этого он вызывает метод Find<T>() класса KeyedByTypeCollection<I> с использованием ServiceMetadata- Behavior в качестве параметра типа. ServiceMetadataBehavlor определяется в про- странстве имен System.ServiceModel.Description: public class ServiceMetaDataBehavior : IServiceBehavior { public bool HttpGetEnabled {get:set.} // } Если возвращаемое значение отлично от null, код хостинга создает новый объект ServiceMetadataBehavlor, задает HttpGetEnabled равным true и включает его в коллекцию аспектов поведения в описании службы. Конечные точки обмена метаданными Служба также может публиковать свои метаданные через специальную конеч- ную точку, называемую конечной точкой обмена метаданными, или сокращенно конечной точкой МЕХ (Metadata EXchange). На рис. 1.7 показана служба с ра- бочими конечными точками и конечной точкой обмена метаданными. Впрочем, конечные точки обмена метаданными обычно не показываются на структурных диаграммах. Конечная точка МЕХ поддерживает отраслевой стандарт обмена метаданны- ми, представленный в WCF интерфейсом IMetadataExchange: [ServiceContract(...)] public interface IMetadataExchange
[Operationcontract(...)] Message Get(Message request): // ... Конечная точка МЕХ Рабочие конечные точки Служба Рис. 1.7. Конечная точка обмена метаданными Подробности интерфейса к делу не относятся — как и большинство отрасле- вых стандартов, он достаточно сложен в реализации. К счастью, WCF может сделать так, что хост службы автоматически предоставит реализацию IMeta- dataExchange и конечную точку обмена метаданными. Для этого необходимо лишь задать адрес и привязку, а также добавить аспект поведения обмена мета- данными. Для привязок WCF предоставляет специализированные транспорт- ные элементы для протоколов HTTP, HTTPS, TCP и IPC. Что касается адреса, вы можете предоставить полный адрес или использовать любой из зарегистриро- ванных базовых адресов. Включать поддержку HTTP-GET при этом не нужно, но вреда от нее тоже не будет. В листинге 1.12 представлена служба, предостав- ляющая три конечные точки МЕХ: для HTTP, TCP и IPC. Для демонстрации конечные точки TCP и IPC используют относительные адреса, а конечная точ- ка HTTP - абсолютный адрес. Листинг 1.12. Добавление конечных точек МЕХ <service name = "MyService" behaviorConfguration = "MEX"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "net.pipe://localhost/'7> </baseAddresses> </host> <endpoint> address = "MEX" binding = "mexTcpBinding" contract = "IMetadataExchange" /> <endpoint> address = "MEX" binding = "mexNamedPipeBinding" contract = "IMetadataExchange" /> <endpoint> address = "http://localhost:8000/MEX" binding = "mexHttpBinding" contract = "IMetadataExchange" /> продолжение &
46 Глава 1. Основные сведения о WCF </service> <behaviors> <serviceBehaviors> <behavior name = "MEX"> <serviceMetadata/> </behavior> </serviceBehaviors> </behaviors> Добавление конечных точек МЕХ на программном уровне Конечные точки обмена метаданными, как и любые другие конечные точки, мо- гут добавляться на программном уровне только перед открытием хоста. WCF не предлагает специализированного типа привязки для конечной точки обмена метаданными. Вместо этого необходимо сконструировать пользовательскую при- вязку, которая использует соответствующий транспортный элемент привязки, и передать этот элемент в параметре конструктора экземпляра пользователь- ской привязки. Далее вызывается метод AddServiceEndpoint() хоста, которому передается адрес, пользовательская привязка и тип контракта IMetadataExchange. В листинге 1.13 представлен код, необходимый для добавления конечной точки МЕХ через TCP. Обратите внимание: перед добавлением конечной точки необ- ходимо проверить наличие аспекта поведения метаданных. Листинг 1.13. Программное добавление конечной точки МЕХ для TCP Bindi ng Element bindingElement = new TcpTransportBindingElement(): CustomBinding binding = new CUstomBinding(bindingElement): Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/"): ServiceHost host = new ServiceHost(typeof(MyService).tcpBaseAddress): ServiceMetadataBehavior metadataBehavior: metadataBehavior = host.Description Behaviors Find<ServiceMetadataBenavior>(): if(metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); host.Descript ion.Behaviors.Add(metadataBehavior); } host.AddServiceEndpoint(typeof(IMetadataExchange).binding,"MEX"; host,0pen(); Усовершенствование ServiceHost<T> Класс ServiceHost<T> можно усовершенствовать так, чтобы автоматизировать выполнение кода в листингах 1.11 и 1.13. ServiceHost<T> обладает логическим свойством EnableMetadataExchange, которое может использоваться для добавле- ния как поведения метаданных HTTP-GET, так и конечных точек МЕХ: public class ServiceHost<T> : ServiceHost { public bool EnableMetadataExchange {get;set:} public bool HasMexEndpoint {get;}
Привязки 47 public void AddAl1MexEndPoints(); Л ... ) Задание EnableMetadataExchange значения true добавляет аспект поведения обмена метаданными. При отсутствии конечных точек МЕХ EnabLeMetadataEx- change добавляет конечную точку МЕХ для каждой зарегистрированной схемы базового адреса. При использовании ServiceHost<T> листинги 1.11 и 1.3 сокраща- ются до следующего фрагмента: ServiceHost<MyService> host = new ServiceHost<MyService>(): host.EnableMetadataExchange = true: host.OpenO: ServiceHost<T> также содержит логическое свойство HasMexEndpoint, которое возвращает true, если служба содержит хотя бы одну конечную точку МЕХ (не- зависимо от транспортного протокола), и метод AddALLMexEndpoints(), добавляю- щий конечную точку МЕХ для каждого зарегистрированного базового адреса со схемами HTTP, TCP и IPC. В листинге 1.14 показана реализация этих мето- дов. Листинг 1.14. Реализация свойства EnableMetadataExchange и его вспомогательных методов public class ServiceHost<T> : ServiceHost ( public bool EnableMetadataExchange ( set ( i f(State == Communi cationState.Opened) ( throw new InvalidOperationException("Host is already opened"): ServiceMetadataBehavlor metadataBehavior: metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(); if (metadataBehavior == null) ( metadataBehavior = new ServiceMetadataBehavior(); metadataBehavior.HttpGetEnabled = value: Description.Behaviors.Add(metadataBehavior); if(value == true) { if(HasMexEndpoint == false) { AddAl1MexEndPoints(): } } 1 get ( ServiceMetadataBehavi or metadataBehavior; metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(): продолжение &
48 Глава 1. Основные сведения о WCF 1 f(metadataBehavlor == null) { return false: } return metadataBehavlor.HttpGetEnabled: } } public bool HasMexEndpoint ( get { Predicate<Serv1ceEndpo1nt> mexEndPoint = delegate(ServiceEndpolnt endpolnt) { return endpoint.Contract.ContractType == typeof(IMetadataExchange); }; return Col 1ection.Exists(Description.Endpolnts.mexEndPoint): } } public void AddAllMexEndPointsO { Debug.Assert(HasMexEndpoint == false); foreach(Ur1 baseAddress In BaseAddresses) ( BlndlngElement blndlngElement = null; swltch(baseAddress.Scheme) { case "net.tcp": { blndlngElement = new TcpTransportB1nd1ngElement(); break; } case "net.pipe": {...} case "http": (...} case "https": {...} } iftbindingElement !=null) { Binding binding = new CustomBlndlng(blndlngElement); AddServiceEndpoint(typeof(IMetadataExchange).binding."MEX"); } } } } Сначала EnableMetadataExchange проверяет, что хост еще не был открыт; для этой цели используется свойство State базового класса Communicationobject. EnableMetadataExchange не переопределяет значение, заданное в конфигурацион- ном файле, и задает его только в том случае, если аспект поведения метаданных в конфигурационном файле отсутствует. При чтении свойство проверяет, был
Привязки 49 ли аспект поведения задан ранее (то есть присутствует ли в коллекции аспек- тов поведения). Если аспект поведения не задан, EnableMetadataExchange воз- вращает false, а если задан — возвращает свойство HttpGetEnabled. Свойство HasMexEndpoint использует анонимный метод1 для инициализации предиката, который проверяет, действительно ли контракт заданной конечной точки отно- сится к типу IMetadataExchange. Затем свойство использует статический класс Collection и вызывает метод Exists() с передачей коллекции конечных точек, полу- ченной у хоста службы. Exists() вызывает предикат для каждого элемента кол- лекции и возвращает true, если хотя бы один из элементов соответствует преди- кату (то есть вызов анонимного метода вернул true), и false в противном случае. Метод AddAUMexEndPoints() перебирает элементы коллекции BaseAddresses. Для каждого найденного базового адреса создается элемент привязки с подходящим транспортом, создается пользовательская привязка, и все эти данные использу- ются для добавления конечной точки (как в листинге 1.13). Metadata Explorer Конечная точка МЕХ предоставляет метаданные, которые описывают не только контракты и операции, но и предоставляют информацию о контрактах данных, безопасности, транзакциях, надежности и сбоях. Чтобы читателю было проще Рис. 1.8. Metadata Explorer 1 Если вы не знакомы с анонимными методами, почитайте мою статью в MSDN Magazine «Create Elegant Code with Anonymous Methods, Iterators, and Partial Classes» (май 2004 г.).
50 Глава 1. Основные сведения о WCF представить метаданные службы, я написал программу Metadata Explorer (она входит в число других примеров исходного кода книги). На рис. 1.8 показано окно Metadata Explorer с конечными точками из листинга 1.7. Чтобы использо- вать Metadata Explorer, достаточно указать адрес HTTP-GET конечной точки МЕХ работающей службы. Программирование на стороне клиента Для вызова операций службы клиент должен сначала импортировать контракт службы в родное представление своей среды. Если клиент использует WCF, вызов операций чаще всего производится через посредника — класс CLR с под- держкой единственного интерфейса CLR, представляющего контракт службы. Если служба поддерживает несколько контрактов (через но крайней мере такое же количество конечных точек), клиенту потребуется отдельный посредник для каждого тина контракта. Посредник представляет те же операции, что и кон- тракт службы, но при этом содержит дополнительные методы для управления своим жизненным циклом и подключением к службе. Он полностью инкапсу- лирует все аспекты службы: ее размещение, технологию реализации, платфор- му времени выполнения и коммуникационный транспорт. Построение посредника Visual Studio 2005 позволяет импортировать метаданные службы и сгенериро- вать посредника. Если служба использует автохостинг, запустите ее, а затем выберите команду Add Service Reference в контекстном меню проекта клиента. Если хостинг службы обеспечивает IIS или WAS, заранее запускать службу не обязательно. Интересная подробность: если служба использует автохостинг в другом проекте того же решения (solution), к которому принадлежит клиент- ский проект, вы можете запустить хост в Visual Studio 2005 и добавить ссыл- ку — в отличие от большинства команд проекта, эта возможность не блокирует- ся на время отладочного сеанса (рис. 1.9). i Solution Sei Hosting* (2 protects) j - Hoel j Properties :♦! --a References I App.conlig • Д MyServiee.es ; Program cs j J3 MyCbent -an Properties , AddServiceRe«елее. MiCSent.es Proxy.es I WlWHf1 11 I Enter ths service URI and reference name and cTck OK to add al the avertable Service URI ________________________________________________________ |http://tocabostROOO/ Browse | Service reference parne- Jlocalhost OK | Caicet | Рис. 1.9. Построение посредника в Visual Studio 2005
Программирование на стороне клиента 51 На экране появляется диалоговое окно Add Service Reference, в котором необ- ходимо ввести базовый адрес службы (или базовый адрес и МЕХ UR1) и про- странство имен, в котором должен размещаться посредник. Вместо Visual Studio 2005 также можно воспользоваться утилитой команд- ной строки SvcUtil.exe. Ей передается адрес HTTP-GET или адрес конечной точки МЕХ, а также (не обязательно) имя файла посредника. По умолчанию посредник генерируется в файле output.cs, но ключ /out позволяет задать дру- гое имя. Например, если хостинг службы MyService осуществляется в I1S или WAS и при этом включен обмен метаданными через HTTP-GET, выполняется сле- дующая командная строка: SvcUti 1 http://1 оса 1 host/MyService/MyServiсе. svc /out: Proxy.cs Если вы используете хостинг HS с портом, отличным от 80 (например, 81), номер порта должен быть включен в базовый адрес: SvcUti 1 http: / /1 ос а 1 hos t: 81 /MyServ i се/ MyServ i ce. svc / out: Proxy cs При автохостинге, если служба публикует метаданные через HTTP-GET, ре- гистрирует базовые адреса и предоставляет конечные точки обмена метаданны- ми с отосительпым адресом МЕХ: http://loca1 host: 8002/ net .tcp://local host: 8003 net.pipe://local host/MyPipe После запуска хоста посредник генерируется следующими командами: SvcUtil http://localhost:8002/МЬХ SvcUtil http://localhost:8002/ SvcUti 1 net. tcp: / /1 oca 1 host: 8003 / ME X SvcUtil net pipe://localhost/MyPipe/MEX /out:Proxy.cs /out:Proxy.cs /out:Proxy.cs /out:Proxy.cs ПРИМЕЧАНИЕ-------------------------------------------------------------------- Главным преимуществом SvcUtil перед Visual Studio 2005 являются разнообразные возможности на- стройки и управления генерируемыми посредниками. * I * * * * * * В Допустим, имеется следующее определение службы: [ServiceContract (Namespace = "MyNamespace")J interface IMyContract I [Operationcontract 1 void MyMethodt); class MyService : IMyContract public void MyMethodO } (•••) SvcUtil генерирует для него посредника, приведенного в листинге 1.15. В большинстве случаев значения Action и ReplyAction можно смело удалить, поскольку используемого по умолчанию режима имени метода вполне доста- точно.
52 Глава 1. Основные сведения о WCF Листинг 1.15. Посредник [ServiceContract(Namespace = "MyNamespace")] public interface IMyContract [OperationContract(Action = "MyNamespace/IMyContract/MyMethod", ReplyAction = "MyNamespace/IMyContract/MyMethodResponse")] void MyMethod(): } public partial class MyContractClient : ClientBase<IMyContract>,IMyContract public MyContractClientO {} public MyContractClient(string endpointName) : base(endpointName) {} public MyContractClient(Binding binding.EndpointAddress remoteAddress) : base(bi ndi ng.remoteAddress) {} /* Дополнительные конструкторы */ public void MyMethodO { Channel .MyMethodO; } } При взгляде на класс посредника бросается в глаза одно удивительное об- стоятельство: он не содержат ссылок на класс реализации службы, а только на контракт, предоставляемый службой! Посредник может использоваться как в сочетании с конфигурационным файлом клиентской стороны, предоставляю- щим адрес и привязку, так и без конфигурационного файла. Учтите, что каж- дый экземпляр посредника ссылается ровно на одну конечную точку. Конечная точка, с которой осуществляется взаимодействие, передается посреднику при конструировании. Как говорилось ранее, если контракт на стороне службы не предоставляет пространства имен, по умолчанию используется пространство http://tempuri.org. Административная настройка клиента Клиент должен знать, где находится служба, использовать ту же привязку, что и служба, — и конечно, импортировать определение контракта службы. Факти- чески речь идет об информации, которая хранится в конечной точке службы. Соответственно, клиентский конфигурационный файл содержит информацию о конечных точках и даже использует ту же схему конфигурации конечных то- чек, что и хост. В листинге 1.16 приведен клиентский конфигурационный файл, необходи- мый для взаимодействия со службой, хост которой настроен в соответствии с листингом 1.6.
Программирование на стороне клиента 53 Листинг 1.16. Клиентский конфигурационный файл <system. servi ceModel > <client> <endpoint name = "MyEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> </client> </system. servi ceModel > В клиентском конфигурационном файле может быть перечислено столько конечных точек, сколько служб он поддерживает, а клиент может использовать любую из них. В листинге 1.17 приведен клиентский конфигурационный файл для хостового конфигурационного файла из листинга 1.7. Обратите внимание: каждая конечная точка в клиентском конфигурационном файле обладает уни- кальным именем. Листинг 1.17. Клиентский конфигурационный файл с несколькими конечными точками <system. servi ceModel > <client> <endpoint name = "FirstEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint name = "SecondEndpoint" address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint name = "ThirdEndpoint" address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </client> </system. servi ceModel > Конфигурация привязок Стандартные привязки на стороне клиента можно настроить в соответствии с привязками служб (по аналогии с тем, как это делается при конфигурации служб). Пример приведен в листинге 1.18. Листинг 1.18. Конфигурация привязки на стороне клиента <system. servi ceModel > <client> <endpoint name = "MyEndpoint" address = "net.tcp://localhost:8000/MyService/" bindingconfiguration « "TransactionalTCP" продолжение &
54 Глава 1. Основные сведения о WCF binding = "netTcpBinding" contract = "IMyContract" /> </cl1ent> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel> Построение клиентского конфигурационного файла По умолчанию SvcUtil также автоматически генерирует клиентский конфигу- рационный файл с именем output.config. Вы также можете задать имя конфигу- рационного файла при помощи ключа /config: SvcUtil http://localhost:8002/MyService/ /out.-Proxy.cs /config:App.Config Ключ /noconfig запрещает построение конфигурационного файла: SvcUtil http://1 оса 1 host:8002/MyServiсе/ /out:Proxy.cs /noconfig Я не рекомендую использовать SvcUtil для построения конфигурационного файла. Дело в том, что утилита генерирует развернутые секции привязок, кото- рые обычно лишь содержат явно заданные значения по умолчанию, а это лишь приводит к загромождению конфигурационного файла. Внутрипроцессная конфигурация При внутрипроцессном хостинге клиентский конфигурационный файл также является конфигурационным файлом хоста службы. Этот файл содержит запи- си, относящиеся как к службе, так и к клиенту (листинг 1.19). Листинг 1.19. Конфигурационный файл при внутрипроцессном хостинге <system.servi ceModel> <services> <service name = "MyService"> <endpoint address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </service> </services> <client> <endpoint name = "MyEndpoint" address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </client> </system.serviceModel> Обратите внимание на использование привязки именованного канала при внутрипроцессном хостинге.
Программирование на стороне клиента 55 SvcConfigEditor В WCF входит редактор SvcConfigEditor.exe, подходящий для редактирования как хостовых, так и клиентских конфигурационных файлов (рис. 1.10). Редак- тор может запускаться из Visual Studio щелкните правой кнопкой мыши на конфигурационном файле (клиентском или хостовом) и выберите команду Edit WCF Configuration. * с Adocwnents and settmgs\idesign\desktop\deinaAessenhah\c0py of *е1^юН«пд\«ег*1Се\лрр. jEte - Mtdtotoft... - 1 Services i MyNamespace My Ser vice -J Host + । Endpoints + I Client - I Binding 5 Binding | Security | T ansactionalT CP IneU opBindtngj] t Diagnostics tI Advanced i Endpoint Behaviors - I Service Behaviors "П NewBehavior + I Extensions * I HostEnvironment CW* Binding Configuration , Create a New Service ^Create a New Client.. MaxBufferPoolSize MaxBufferSize MaxConnections M axR eceivedM essageS ize OpenTimeout PortS haringEnabled Receive? imeout SendTimeout T ransactionFlow TransactionProtocol T ransferMode S ReaderQuotaxPropertre* MaxArrayLength MaxBytesPerRead MaxDepth MaxNameT ableCharCount ♦ MaxStringContentLength El RefiairteSexsion Properties Enabled Inactivity!imeout Ordered Enabled Enables reliable sessions on this binding. 524288 6553Б 10 6553Б 00:81:00 False 00:10:00 00:01:00 True OleT ransactions Buffered 0 0 0 0 0 True 00:10:00 True Рис. 1.10. SvcConfigEditor используется для редактирования хостовых и клиентских конфигурационных файлов Лично я неоднозначно отношусь к SvcConfigEditor. С одной стороны, редак- тор хорошо работает с конфигурационными файлами, и при работе с ним разра- ботчику не обязательно учить конфигурационную схему. С другой стороны, он не избавляет от необходимости хороню понимать конфигурацию WCF, а не- сложные изменения проще внести вручную, чем прибегать к средствам Visual Studio 2005. Создание и использование посредника Класс посредника наследует от класса CLiendBase<T>, определение которого вы- глядит так: public abstract class ClientBase<T> ICommunicationObject.I Disposable protected ClientBase(string endpointName): protected ClientBase(Binding binding.EndpointAddress remoteAddress):
56 Глава 1. Основные сведения о WCF public void OpenO: public void CloseO; protected T Channel {get;} // Дополнительные члены } ClientBase<T> получает один обобщенный параметр типа, определяющий контракт службы, инкапсулируемый посредником. Свойство Channel класса ClientBase<T> относится к типу этого параметра. Сгенерированный субкласс ClientBase<T> просто делегирует Channel вызов метода (см. листинг 1.15). Чтобы использовать посредника, клиент сначала должен создать экземпляр посредника и передать конструктору информацию о конечной точке: либо имя секции из конфигурационного файла, либо адрес конечной точки и объекты привязок, если конфигурационный файл не используется. Затем клиент ис- пользует методы посредника для обращения к службе, а после завершения ра- боты со службой клиент должен закрыть экземпляр посредника. Например, для определений из листингов 1.15 и 1.16 клиент конструирует посредника с указа- нием конечной точки из конфигурационного файла, вызывает метод и закрыва- ет посредника: MyContractClient proxy = new MyContractClient("MyEndpoint"): proxy.MyMethodC); proxy.Close(); Если в клиентском конфигурационном файле определена только одна ко- нечная точка для типа контракта, используемого посредником, клиент может не указывать имя конечной точки в конструкторе посредника: MyContractClient proxy = new MyContractClient(); proxy.MyMethodC): proxy.Close(); Но если для одного типа контракта доступно несколько конечных точек, по- средник инициирует исключение. Закрытие посредника После завершения работы клиента с посредником рекомендуется всегда закры- вать посредника. В главе 4 будет показано, почему в некоторых случаях закры- тие необходимо — оно завершает сеанс работы со службой и закрывает подклю- чение. Также для закрытия посредника можно использовать метод Dispose(). Пре- имущество Dispose() заключается в том, что при помощи ключевого слова using можно обеспечить его вызов даже при возникновении исключений: using(MyContractClient proxy = new MyContractClient()) { proxy.MyMethod(); } Если контракт объявляется клиентом напрямую (вместо использования конкретного класса посредника), клиент может либо проверить поддержку I Disposable:
Программирование на стороне клиента 57 IMyContract proxy = new MyContractClient()); proxy. MyMethod(); IDIsposable disposable = proxy as {Disposable; {((disposable != null) ( disposable.Oisposet): ) либо свернуть запрос в конструкции using: IMyContract proxy = new MyContractClient(); using(proxy as IDisposable) ( proxy. MyMethodC); ) Тайм-аут вызова Каждый вызов, выданный клиентом WCF, должен быть завершен в течение времени, продолжительность которого настраивается заранее. Если по какой-ли- бо причине время обработки вызова превышает величину тайм-аута, вызов от- меняется, а клиент получает исключение TimeoutException. Точное значение тайм-аута является свойством привязки; значение по умолчанию составляет одну минуту. Чтобы установить другую величину тайм-аута, задайте свойство SendTimeout абстрактного класса Binding: public abstract class Binding : ( public TimeSpan SendTimeout (get;set;} // ... Например, при использовании WSHttpBinding: <client> <endpoint binding = "wsHttpBinding" bindingconfiguration = "LongTimeout" /> </client> <bindings> <wsHttpBi nding> <binding name = "LongTimeout" SendTimeout = "00:05:00"/> </wsHttpBi nding> </bindings> Программная настройка клиента Вместо того чтобы полагаться на содержимое конфигурационного файла, кли- ент также может на программном уровне сконструировать объекты адреса и привязки, соответствующие типу конечной точки службы, и передать их кон- структору посредника. Предоставлять контракт нет необходимости, потому что он передается посреднику в форме обобщенного параметра типа. Для представ- ления адреса клиент создает объект класса EndpointAddress:
58 Глава 1. Основные сведения о WCF public class EndpointAddress { public EndpointAddresststrmg uri); // } Этот способ продемонстрирован в листинге 1.20 для службы из листинга 1.9. Приведенный код является функциональным аналогом листинга 1.16. Листинг 1.20. Программная настройка клиента Binding wsBinding = new WSHttpBinding(); EndpointAddress endpointAddress = new EndpointAddress("http://local host:8000/MyService/"); MyContractClient proxy = new MyContractCllenttwsBinding,endpointAddress): proxy.MyMethodt): proxy.Closet): Клиент также может на программном уровне настроить свойства привязки (по аналогии с секциями привязок в конфигурационных файлах): WSHttpBinding wsBinding = new WSHttpBindlng(): wsBinding.SendTimeout = TimeSpan.FromMinutes(5): wsBinding.TransactionFlow = true: EndpointAddress endpointAddress = new EndpointAddress("http://local host:8000/MyService/"): MyContractClient proxy = new MyContractCllenttwsBinding.endpointAddress): proxy.MyMethodt); proxy.Closet): Как и прежде, обратите внимание на использование конкретного субкласса Binding для обращения к свойствам привязки. Сравнение административной и программной настройки Два представленных способа настройки клиента и службы дополняют друг друга. Административная настройка дает возможность менять важные аспекты рабо- ты службы и клиента без необходимости повторного построения или разверты- вания. Главный недостаток административной настройки заключается в том, что она небезопасна по отношению к типам, а ошибки конфигурации обнару- живаются только во время выполнения. Программная конфигурация полезна при динамическом формировании кон- фигурации (то есть во время выполнения на основании введенных данных или текущих условий), а также в статических, никогда не изменяемых конфигура- циях, которые можно жестко закодировать в программе. Например, если вас интересуют только внутрипроцессные вызовы, вы вполне можете жестко зако- дировать использование привязки NetNamedPipeBinding и ее конфигурации. Тем не менее в общем и целом большинство клиентов и служб прибегает к исполь- зованию конфигурационных файлов.
Архитектура WCF 59 Архитектура WCF К настоящему моменту я рассказал все, что необходимо знать для настройки и использования простых служб WCF. Тем не менее, как будет описано в остав- шейся части книги, WCF предлагает неимоверно полезную поддержку надеж- ности, транзакций, управления параллельным выполнением, безопасности и ак- тивизации экземпляров — все эти возможности базируются на архитектуре WCF, в основу которой заложен перехват взаимодействий. Взаимодействие клиента с посредником означает, что WCF всегда незримо присутствует между службой и клиентом, перехватывая вызовы и выполняя предварительную и завершающую обработку. Перехват начинается с того мо- мента, когда посредник сериализует кадр стека вызовов в сообщение и отправ- ляет сообщение по цепочке каналов. Канал представляет собой простой пере- хватчик, предназначенный для выполнения конкретной операции. Каналы на стороне клиента обеспечивают предварительную обработку сообщения. Точная структура и строение цепочки каналов зависит в основном от привязки. Напри- мер, один из каналов может отвечать за кодирование сообщения (двоичное, текст, МТОМ), другой! — за передачу контекста безопасности, третий — за рас- пространение клиентской транзакции, четвертый -- за управление надежным сеансом, пятый — за шифрование тела сообщения (если этот режим включен) и т. д. Последним каналом на стороне клиента является транспортный канал, который передает сообщение хосту по настроенному транспорту. На стороне хоста сообщение также проходит по цепочке каналов, обеспечи- вающих предварительную обработку сообщения перед вызовом. Первый канал на стороне хоста — транспортный канал — получает сообщение от транспорта. Последующие каналы выполняют различные операции: такие, как дешифро- вание тела сообщения, декодирование сообщения, присоединение распростра- няемой транзакции, назначение администратора безопасности, управление се- ансом и активизацию экземпляра службы. Последний канал на стороне хоста передает сообщение диспетчеру. Диспетчер преобразует сообщение в кадр сте- ка и вызывает экземпляр службы. Последовательность действий показана на рис. 1.11. Служба не знает, от какого клиента поступил вызов — локального или уда- ленного. На самом деле к ней обращается именно локальный клиент (диспет- чер). Перехват на стороне клиента и службы гарантирует, что клиент и служба получают среду времени выполнения, необходимую для их нормальной работы. Экземпляр службы выполняет вызов и возвращает управление диспетчеру, ко- торый преобразует возвращаемые данные и информацию об ошибке (если она есть) в возвращаемое сообщение. Далее весь процесс повторяется в обратном направлении: диспетчер передает сообщение по каналам на стороне хоста для выполнения завершающей обработки — управления транзакцией, деактивиза- цией экземпляра, кодирования ответа, шифрования и т. д. Возвращаемое сооб- щение переходит в транспортный канал, который отправляет его каналам сто- роны клиента для клиентской завершающей обработки. Последняя состоит из таких операций, как дешифрование, декодирование, закрепление или отмена
60 Глава 1. Основные сведения о WCF Рис. 1.11. Архитектура WCF транзакции и т. д. Последний канал передает сообщение посреднику. Посред- ник преобразует возвращаемое сообщение в кадр стека и возвращает управле- ние клиенту. Самое замечательное свойство описанной архитектуры заключается в том, что практически все ее точки предоставляют точки входа для расширения — вы можете предоставить пользовательские каналы для реализации закрытых фор- матов взаимодействия, пользовательские аспекты поведения для управления экземплярам, пользовательские аспекты безопасности и т. д. Все стандартные средства, предоставляемые WCF, реализуются на основе именно этой модели расширяемости. В книге мы неоднократно встретимся с примерами ее практи- ческого применения. Архитектура хоста Также интересно посмотреть, как происходит переход от технологически-ней- тральных, служебно-ориентированных взаимодействий к интерфейсам и клас- сам CLR. Для осуществления перехода используется хост. Каждый хостовой процесс .NET может иметь несколько прикладных доменов, а каждый приклад- ной домен содержит ноль или более экземпляров хоста службы. Однако каж- дый экземпляр хоста службы предназначается для определенного типа службы. При создании экземпляра вы фактически регистрируете экземпляр хоста служ- бы со всеми конечными точками для указанного типа на хостовом компьютере, соответствующими его базовым адресам. Каждый экземпляр хоста службы об- ладает нулем и более контекстов. Контекст (context) представляет собой внут- реннее состояние (scope) выполнения экземпляра службы. Контекст ассоци- ируется максимум с одним экземпляром службы; это означает, что контекст мо- жет быть пустым, то есть лишенным экземпляра службы. Архитектура показана на рис. 1.12.
Работа с каналами 61 Рис. 1.12. Архитектура хоста в WCF ПРИМЕЧАНИЕ------------------------------------------------------------------- Контексты WCF на концептуальном уровне сходны с контекстами Enterprise Services и контекстных объектов .NET. Именно совместная работа хоста службы и контекста обеспечивает пред- ставление родного типа CLR в виде службы. После того как сообщение будет передано по каналам, хост отображает это сообщение на новый или существую- щий контекст (с принадлежащим ему экземпляром) и поручает ему обработку вызова. Работа с каналами Каналы можно использовать для прямого вызова операций служб, без со- действия класса посредника. Класс ChannelFactory<T>, приведенный в лис- тинге 1.21 (со своими вспомогательными типами), позволяет создать посред- ника «на ходу». Листинг 1.21. Класс ChannelFactory<T> public class ContractDescription { public Type ContractType (get:set:} // public class ServiceEndpoint ( public Serv1ceEndpoint(ContractDescript1on contract.Binding binding. EndpointAddress address): public EndpointAddress Address продолжение &
62 Глава 1. Основные сведения о WCF {get:set;} public Binding Binding {get:set;} public ContractDescription Contract {get:} // ... public abstract class Channel Factory : ... { public ServiceEndpoint Endpoint {get:} // public class ChannelFactory<T> : Channel Factory.... { public ChannelFactory(ServiceEndpoint endpoint): public ChannelFactorytstring configurationName); public ChannelFactory(Binding binding.EndpointAddress endpointAddress): public static T CreateChannel(Binding binding, EndpointAddress endpointAddress); public T CreateChannel0; // Конструктору ChannelFactory<T> необходимо передать конечную точку - либо имя конечной точки из клиентского конфигурационного файла, либо объ- екты привязки и адреса, либо объект ServiceEndpoint Затем метод CreateChannel() используется для получения ссылки на посредника и использования его мето- дов. Наконец, посредник закрывается либо приведением к типу IDisposable и вызовом метода Dispose(), либо приведением к типу ICommunicationObject и вы- зовом метода Close(): ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(): IMyContract proxyl = factory.CreateChannel0: using(proxyl as IDisposable) { proxyl.MyMethod(): } IMyContract proxy2 = factory.CreateChannel(); proxy2.MyMethod(); ICommunicationObject channel = proxy2 as ICommunicationObject: Debug.Assert(channel !=null): channel.Close(): Также можно воспользоваться вспомогательным статическим методом CreateChannel() для создания посредника по привязки и адресу, без прямого кон- струирования экземпляра ChanneLFactory<T>: Binding binding = new NetTcpBinding(): EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000"): IMyContract proxy =
йботасканалами 63 Channel Factory < I MyCont га c t>. CreateChannel (bi nch ng. address): using(proxy as {Disposable) ( proxy 1. MyMethodt): I Класс InProcFactory Для демонстрации возможностей ChannelFactory<T> рассмотрим мой статический вспомогательный класс InProcFactory, определяемый следующим образом: public static class InProcFactory ( public static I LreateInstance<S.I>() where I : class where S 1: public static void CloseProxy<I>tI instance) where I class. // И т.д 1 Класс InProcFactory предназначен для оптимизации и автоматизации внутри- процессного хостинга. Метод Createlnstance() получает два обобщенных пара- метра: тип службы S и тип поддерживаемого контракта I. Createlnstance() огра- ничивает S тинами, производными от I. Процесс использования InProcFactory предел ьно и ря м о л и н ее 11: IMyContract proxy = InProcFactory .CreateInstance<MyService, IMyCont ract>(); proxy MyMethodt): InProcFactory. C ।osePrcxy (proxy); Фактически InProcFactory берет класс службы и трансформирует его в служ- бу WCF. Это самый близкий WCF-аналог старой функции Win32 LoadLibrary(). Реализация InProcFactory<T> Все внутрипроцессные вызовы должны использовать именованные каналы с распространением транзакций. Программная настройка позволит автоматизи- ровать задание конфигурации клиента и службы, a ChannelFactory<T> поможет обойтись без посредника. В листинге 1.22 представлена реализация InProcFac- tory (для экономии места листинг слегка сокращен). Листинг 1.22. Класс InProcFactory public static class InProcFactory struct HostReco'^d public HostRecordtServiceHost host.string address) { Host = host: Address = address; public readonly ServiceHost Host: public readonly string Address: } продолжение &
64 Глава 1. Основные сведения о WCF static readonly Uri BaseAddress = new Uri("net.pipe://localhost/"): static readonly Binding NamedPipeBinding: static Dictionary<Type,HostRecord> m_Hosts = new Dictionary<Type.HostRecord>( ); static InProcFactory( ) { NetNamedPipeBinding binding = new NetNamedPipeB1nding( ); binding.TransactionFlow = true; NamedPipeBinding = binding; AppDomain.CurrentDomain.ProcessExit += delegate { foreach(KeyValuePair<Type.HostRecord* pair in m_Hosts) { pair.Value.Host.Closet ): ) ): } public static I CreateInstance<S.I>( ) where I : class where S : I { HostRecord hostRecord - GetHostRecord<S.I>( ); return ChannelFactory<I>.CreateChannel(NamedPipeBinding. new Endpoi ntAddress(hostRecord.Address)); } static HostRecord GetHostRecord<S.I>( ) where I : class where S : I { HostRecord hostRecord; i f(m_Hosts.Conta i nsKey(typeof(S))) { hostRecord - m_Hosts[typeof(S)]: } else { ServiceHost host e new ServiceHost(typeof(S). BaseAddress); string address = BaseAddress.ToString() + Guid.NewGuidO .ToString( ); hostRecord e new HostRecord(host.address); m_Hosts.Add(typeof(S).hostRecord); host.AddServi ceEndpoi nt(typeof(I).NamedPi peBi ndi ng.address); host.Open( ); } return hostRecord: } public static void CloseProxy<I>(I Instance) where I : class { ICommunicationObject proxy = instance as ICommunicationObject: Debug.Assert(proxy != null); proxy.Close( ); } } Основная трудность при написании InProcFactory заключается в том, что Createlnstance() может вызываться для создания экземпляров служб любого типа. Для каждого типа службы должен существовать ровно один соответст-
Надежность 65 вующий хост (экземпляр ServiceHost). Создание экземпляра хоста для каждого вызова - мысль неудачная. Но что делать Createlnstance() при получении запро- са на создание второго объекта для того же типа? IMyContract proxyl = InProcFactory .CreateInstance<MyService. IMyContract>(): IMyContract proxy2 = InProcFactory.CreateInstance<MyService. IMyContract>(): Задача решается созданием ассоциативного массива (словаря), отображаю- щего тип службы на соответствующий экземпляр хоста. При вызове метода Createlnstance() для создания экземпляра определенного типа метод проводит поиск в словаре с использованием вспомогательного метода GetHostRecord(), и хост создается только в том случае, если данный тип службы еще не присут- ствует в словаре. Если хост необходимо создать, GetHostRecord() на программ- ном уровне добавляет конечную точку, используя GUID как уникальное имя канала. Затем Createlnstance() берет адрес конечной точки из данных хоста и использует ChanneLFactory() для создания посредника. В своем статическом конструкторе, который вызывается при первом использовании класса, InProc- Factory подписывается на событие завершения процесса, используя анонимный метод для закрытия всех хостов при завершении процесса. Наконец, чтобы по- мочь клиентам с закрытием посредника, InProcFactory предоставляет метод CloseProxy(), который запрашивает у посредника ICommunicationObject и закрыва- ет его. Надежность В WCF и других служебно-ориентированных технологиях различаются поня- тия надежности транспорта и надежности сообщений. Надежность транспорта (например, предоставляемая TCP) обеспечивает гарантированную доставку «точ- ка-точка» на уровне сетевых пакетов, а также гарантирует порядок пакетов. На- дежность транспорта пе выдерживает потери сетевых подключений и других коммуникационных проблем. Надежность сообщений, как подсказывает само название, работает на уровне сообщений независимо от того, сколько пакетов требуется для его доставки. Надежность сообщений обеспечивает гарантированную доставку и порядок со- общений, независимо от количества посредников и сетевых переходов (хопов), необходимых для доставки сообщения от клиента к службе. Надежность сооб- щений базируется на отраслевом стандарте надежных взаимодействий, с под- держкой сеанса на транспортном уровне. Она обеспечивает повторные попытки в случае отказа транспорта (например, потери беспроводного подключения); автоматически решает проблемы перегрузки канала, буферизации сообщений и контроля передачи, а также может соответствующим образом регулировать количество сообщений. К области надежности сообщений также относится управление самим подключением посредством верификации и освобождение неиспользуемых ресурсов.
66 Глава 1. Основные сведения о WCF Привязки и надежность В WCF управление надежностью и настройка ее параметров осуществляется на уровне привязки. Конкретная привязка может поддерживать или не поддержи- вать надежный обмен сообщениями, а при наличии поддержки он может быть включен или отключен. Уровень надежности, поддерживаемый привязкой, оп- ределяется основным сценарием ее применения. В табл. 1.2 показано, какие привязки поддерживают надежность и упорядоченную доставку, а также приве- дены их состояния по умолчанию. Таблица 1.2. Надежность и привязка Название Поддержка надежности Состояние надежности по умолчанию Поддержка Состояние упоря- упорядоченной доченной доставки доставки по умолчанию BasicHttpBinding Нет — Нет — NetTcpBinding Да Выкл Да Вкл NetPeerTcpBinding Нет — Нет — NetNamedPipeBinding Нет —(Вкл) Да — (Вкл) WSHttpBinding Да Выкл Да Вкл WSFederationHttpBinding Да Выкл Да Вкл WSDualHttpBinding Да Вкл Да Вкл NetMsmqBinding Нет — Нет — MsmqlntegrationBinding Нет — Нет — Надежность не поддерживают BasicHttpBinding, NetPeerTcpBinding и две при- вязки MSMQ: NetMsmqBinding и MsmqlntegrationBinding. Это объясняется тем, что BasicHttpBinding ориентируется на унаследованные веб-службы ASMX, не обладающие надежностью. Привязка NetPeerTcpBinding проектировалась для сце- нариев широковещательной рассылки. Привязки MSMQ предназначены для вы- зовов без подключения, при которых концепция транспортного сеанса все рав- но невозможна. Надежность всегда включена у WSDualHttpBinding, чтобы поддерживать дей- ствующий канал обратного вызова на сторону клиента даже через HTTP. У NetTcpBinding и различных WS-привязок надежность отключается по умолчанию, но может быть включена. Наконец, NetNamedPipeBinding считается надежным по определению, потому что клиент всегда отделен от службы ровно одним сетевым переходом. Упорядочение сообщений Надежность сообщений также обеспечивает упорядоченную доставку — иначе говоря, сообщения обрабатываются в порядке их отправки, а не в порядке дос- тавки. Кроме того, каждое сообщение заведомо доставляется только один раз. WCF позволяет включить надежность без упорядоченной доставки; в этом случае сообщения доставляются в порядке их получения. По умолчанию для
Надежность 67 всех привязок, поддерживающих надежность, при включении надежности так- же включается и упорядоченная доставка. Настройка надежности Надежность (и упорядоченная доставка) может настраиваться как на про- граммном, так и на административном уровне. Включение надежности должно осуществляться на стороне как клиента, так и хоста службы; в противном слу- чае клиент не сможет взаимодействовать со службой. Надежность настраивает- ся только для тех типов привязок, которые ее поддерживают. В листинге 1.23 приведен конфигурационный файл на стороне службы, у которого в секции па- раметров привязки включается надежность при использовании привязок TCP. Листинг 1.23. Включение надежности для привязок TCP <system. servi ceModel > <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService" binding = "netTcpBinding" bindingconfiguration = "ReliableTCP" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "ReliableTCP"> <reliableSession enabled = "true’7> </binding> </netTcpBinding> </bindings> </system, servi ceModel > В том, что касается программной настройки, у привязок TCP и WS надеж- ность включается несколько разными способами. Например, конструктору при- вязки NetTcpBinding передается логический параметр для включения надежно- сти: public class NetTcpBinding : Binding.... public NetTcpBi ndi ng(... .bool reliableSessionEnabled): // Надежность может включаться только при конструировании, поэтому при программной настройке привязка должна изначально конструироваться как на- дежная: Binding reliableTcpBinding = new NetTcpBinding(... .true): NetTcpBinding также предоставляет класс ReliableSession для проверки состоя- ния надежности (с доступом только для чтения):
68 Глава 1. Основные сведения о WCF public class RellableSession { public TimeSpan InactivityTImeout {get:set;} public bool Ordered {get;set:} // } public class OptionalRellableSession : RellableSession { public bool Enabled {get;set:} // } public class NetTcpBinding : Binding.... { public OptionalRellableSession RellableSession {get:} // } Включение упорядоченной доставки Теоретически код службы и определение контракта не должны зависеть от при- вязки и ее свойств. Служба должна быть полностью изолирована от привязки, ничто в ее коде не должно относиться к привязке, а служба должна сохранять работоспособность с любыми аспектами настроенной привязки. На практике реализация службы или контракта может зависеть от упорядоченности доставки сообщений. Чтобы разработчики контракта или службы могли ограничивать на- бор допустимых привязок, в WCF определяется атрибут DeliveryRequirementsAt- tribute: [AttrlbuteUsage(Attr1buteTargets.Cl ass|AttrlbuteTargets.Interface AllowMultlple = true)] public sealed class DelIveryRequlrementsAttrlbute : Attribute.... { public Type Targetcontract {get;set:} public bool RequlreOrderedDelivery {get:set:} // } Атрибут DeliveryRequirementsAttribute может применяться на уровне службы; в этом случае он распространяется на все конечные точки службы, а не только на конечные точки, предоставляющие определенный контракт. Использование атрибута на уровне службы означает, что требование упорядоченной доставки является решением реализации. Если же атрибут применяется на уровне кон- тракта, он распространяется на все службы, поддерживающие данный кон- тракт — это означает, что требование упорядоченной доставки является плани- ровочным решением. Ограничение проверяется во время загрузки службы. Если конечная точка обладает привязкой, не поддерживающей надежность, или
Надежность 69 надежность была включена при отключении упорядоченной доставки, загрузка службы завершается неудачей с исключением InvalidOperationException. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Привязка именованных каналов удовлетворяет ограничению упорядоченной доставки. Например, если вы хотите потребовать, чтобы у всех конечных точек служ- бы независимо от контракта была включена упорядоченная доставка, примени- те атрибут к классу службы: [Del iveryRequi cements (Requi reOrderedDel i very = true)] class MyService : IMyContract.IMyOtherContract (...) Задавая свойство Targetcontract, вы можете потребовать, чтобы ограничение надежной упорядоченной доставки распространялось только на конечные точ- ки службы, поддерживающие указанный контракт: [Del iveryRequi rements (Targetcontract = typeof(IMyContract). RequireOrderedDelivery = true)] class MyService : IMyContract. IMyOtherContract (...) Применяя атрибут DeliveryRequirements к интерфейсу контракта, вы устанав- ливаете ограничение для всех поддерживающих его служб: [DeliveryRequirements(RequireOrderedDel ivery = true)] [ServiceContract] interface IMyContract (...) class MyService : IMyContract (•••} class MyOtherService : IMyContract (...) По умолчанию значение RequireOrderedDelivery равно false, поэтому простое применение атрибута ни к чему не приведет. Например, следующие фрагменты эквивалентны: [ServiceContract] interface IMyContract (...) [DeliveryRequirements] [ServiceContract] interface IMyContract (...) [DeliveryRequirements(RequireOrderedDelivery = false)] [ServiceContract] interface IMyContract
Контракты служб Атрибут ServiceContract, описанный в предыдущей главе, представляет интер- фейс (или класс) как служебно-ориентированный контракт; это позволяет раз- работчику программировать на языках вроде С#, использовать такие конструк- ции, как интерфейсы, и представлять их в виде контрактов и служб WCF. В начале этой главы речь пойдет о том, как лучше преодолеть расхождение ме- жду двумя моделями программирования посредством перегрузки операций и наследования контрактов. Затем приводятся некоторые простые, но полезные рекомендации и приемы факторинга и проектирования контрактов служб. В за- вершение я покажу, как организовать программное взаимодействие с метадан- ными контрактов на стадии выполнения. Перегрузка операций Языки программирования — такие, как C++ и C# — поддерживают перегрузку методов: определение двух методов с одинаковым именем, но разными парамет- рами. Например, следующее определение интерфейса C# вполне допустимо: interface ICalculator int AddCint argl. int arg2); double Add(double argl.double arg2): Однако в мире WSDL-операций перегрузка недопустима. Соответственно следующее определение контракта компилируется, но выдает исключение InvalidOperation Exception при загрузке хоста службы: // Недопустимое определение контракта: [ServiceContract] interface ICalculator [Operationcontract] int Add(int argl.int arg2): [Operationcontract]
Перегрузка операций 71 double Add (double argl.double arg2); ) Впрочем, перегрузку операций можно реализовать вручную. Для этого в свойстве Name атрибута Operationcontract определяется псевдоним операции: [AttributeUsage(Attri buteTargets .Method)] public sealed class OperationContractAttribute : Attribute I public string Name (get:set:} // ... ) Псевдоним должен быть определен как на стороне службы, так и на стороне клиента. На стороне службы для перегружаемых операций предоставляются уникальные имена, как показано в листинге 2.1. Листинг 2.1. Перегрузка операций на стороне службы [ServiceContract] interface ICalculator ( [Operationcontract (Name = "Addlnt”)] int Add(int argl.int arg2): [OperationContract(Name = "AddDouble")] double Add (double argl,double arg2): ) Когда клиент импортирует контракт и генерирует посредника, в качестве имен импортируемых операций используются псевдонимы: [ServiceContract] public interface ICalculator ( [Operationcontract] int Addlnt(int argl.int arg2): [Operationcontract] double AddDouble(double argl.double arg2): ) public partial class Calculatorclient : ClientBase<ICalculator>,ICalculator public int Addlnt(int argl.int arg2) ( return Channel .Addlnt(argl,arg2): public double AddDouble(double argl.double arg2) return Channel .AddDouble(argl .arg2): ) // ) Клиент может использовать сгенерированного посредника и контракт в том виде, в котором они были получены, но их также можно переработать для под- держки перегрузки на стороне клиента. Переименуйте методы импортирован- ного контракта и посредника, но проследите за тем, чтобы класс посредника об- ращался к внутреннему посреднику с использованием перегруженных методов:
72 Глава 2. Контракты служб public int Add(int argl.int arg2) { return Channel.Add(argl.arg2); } Наконец, используйте свойство Name в импортированном на сторону клиен- та контракте для определения псевдонимов и перегрузки методов в соответст- вии с именами импортированных операций, как показано в листинге 2.2. Листинг 2.2. Перегрузка операций на стороне клиента [ServiceContract] public interface ICalculator { [OperationContract(Name = "Addlnt")] int Add(int argl.int arg2); [OperationContract(Name = "AddDouble")] double Add(double argl.double arg2); } public partial class CalculatorCllent : ClientBase<ICalculator>.ICalculator { public int Addfint argl.int arg2) { return Channel.Addlntfargl,arg2): } public double Addldouble argl.double arg2) { return Channel.Addlargl,arg2): } // } Теперь клиент может пользоваться преимуществами удобочитаемой и ло- гичной программной модели, основанной на применении перегруженных опе- раций: Calculatorclient proxy = new CalculatorClient(): int resultl = proxy.Add(1.2): double result2 = proxy.Add(l.0.2.0): proxy.Close(): Наследование контрактов Интерфейсы контрактов служб могут быть производными друг от друга, что позволяет разработчику определить иерархию контрактов. Однако атрибут ServiceContract не наследуется: [AttributeUsage(Inherited = false....)] public sealed class ServiceContractAttribute : Attribute {...}
Наследование контрактов 73 Соответственно на каждом уровне иерархии интерфейсов атрибут Service- Contract должен задаваться отдельно, как показано в листинге 2.3. Листинг 2.3. Иерархия контрактов на стороне службы [ServiceContract ] interface ISimpleCalculator ( [Operationcontract] int Add(int argl.int arg2); I [ServiceContract] interface IScientificCa 1 cu 1 ator : ISimpleCalculator ( [Operationcontract] int Muitiply(int argl.int arg2): ) При реализации иерархии контрактов один класс службы может реализо- вать всю иерархию, как и в классическом программировании С#: class MyCalculator IScientificCalculator ( public int AdcKint argl. int arg2) ( return argl + arg2: ) public int Multiply(int argl.int arg2) ( return arql * arg2; } ) Хост может предоставить одну конечную точку для интерфейса самого низ- кого уровня иерархии: <service name = "MyCalciilator"> <@060>endpoint address = "http://localhost :8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </service> Иерархия контрактов на стороне клиента Когда клиент импортирует метаданные конечной точки службы, контракт ко- торой входит в иерархию интерфейсов, итоговый контракт на стороне клиента не сохраняет исходную иерархию. В него включается «сжатая» иерархия в форме одного контракта, имя которого соответствует контракту конечной точки. Один контракт содержит объединение всех операций всех интерфей- сов, ведущих к нему в иерархии, включая его собственные операции. Однако импортированное определение интерфейса хранит в свойствах Action и Re- sponseAction атрибута Operationcontract имя исходного контракта, определив- шего каждую операцию:
74 Глава 2. Контракты служб [AttrlbuteUsage(AttrlbuteTargets.Method)] public sealed class OperationContractAttribute : Attribute { public string Action {get;set;} public string ReplyAction {get;set;} // } Наконец, один класс посредника может реализовать все методы импортиро- ванного контракта. В листинге 2.4 приведены импортированный контракт и сгенерированный класс посредника для определений из листинга 2.3. Листинг 2.4. Сжатие иерархии контрактов на стороне клиента [ServiceContract] public interface IScientificCalculator { [OperationContract(Action = ”.../ISimpleCalculator/Add". ReplyAction = ”.../ISimpleCalculator/AddResponse")] int Add(int argl.int arg2); [OperationContract(Action » ./IScientificCalculator/Multiply". ReplyAction = ”.../IScientificCalculator/MultiplyResponse")] int Multiplydnt argl.int arg2); } public partial class ScientificCalculatorClient : ClientBase<IScientificCalculator>,IScientificCalcul ator ( public int Addtint argl.int arg2) (...} public int Multiplydnt argl.int arg2) {•••} // ... } Восстановление иерархии на стороне клиента Клиент может вручную восстановить иерархию контрактов, переработав опре- деления посредника и импортированного контракта. Пример показан в листин- ге 2.5. Листинг 2.5. Иерархия контрактов на стороне клиента [ServiceContract] public interface ISimpleCaiculator { [Operationcontract] public int Adddnt argl.int arg2); } public partial class SlmpleCaiculatorCllent : Cli entBase<ISimpleCaiculator>.ISimpleCalculator { public int Adddnt argl.int arg2)
Наследование контрактов 75 ( return Channel. Add(argl ,arg2): ) // ... I [ServiceContract] public interface IScientificCa 1 culator : ISimpleCalculator ( [Operationcontract] public int Muitiply(int argl.int arg2); ) public partial class SclentificCa 1 culatorCllent : ClientBase<ISc 1 enti ficCa 1 culator>. IScientificCalculator ( public int AdcKint argl.int arg2) { return Channel .Add(argl,arg2): } public int Multipoint argl,int arg2) ( return Channel.Muitiply(argl,arg2); } // 1 Используя значение свойства Action разных операций, клиент может выделить определения контрактов, входящих в иерархию контрактов служб, и предоставить определения интерфейсов и посредников — например, ISi mрLeCalculatoг и Simple- CalculatorClient в листинге 2.5. Задавать свойства Action и ResponseAction не нуж- но, их можно спокойно удалить. Затем интерфейс вручную включается в цепоч- ку наследования: [ServiceContract ] public interface IScientificCa1culator : ISimpleCalculator (...) Хотя служба представляется одной конечной точкой для нижнего интерфей- са иерархии, клиент может рассматривать ее как разные конечные точки с оди- наковым адресом, соответствующие разным уровням иерархии контрактов: <client> <@060>endpoint name = "SimpleEndpoint" address = "http://local host :8001/MyCal cul ator/" binding = "basicHttpBinding" contract = "ISimpleCalculator" /> <@060>endpoint name = "ScientificEndpoint" address - "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </client> Теперь на стороне клиента можно написать следующий код, в полной мере использующий преимущества иерархии контрактов:
76 Глава 2. Контракты служб SimplеСа1culatorCl1 ent proxyl = new SimpleCalculatorCl1ent(); proxyl.Add(1,2); proxyl.Close(): SimpleCalculatorCllent proxy2 = new SimpleCalculatorCl1ent(); proxy2.Add(3,4); proxy2.Multiply(5.6): proxy2.Close(); Преимущество рефакторинга посредников в листинге 2.5 заключается в том, что каждый уровень контрактов отделяется от более низких уровней. Теперь на стороне клиента вместо ссылки на ISimpLeCaLcuLator может передаваться ссылка на IScientificCalculator: void UseCa1culator(ISImpleCalculator calculator) {•••} ISImpleCalculator proxyl = new SimpleCalculatorCl1ent(); ISImpleCalculator proxy2 = new Sc1ent1f1cCalculatorC11ent(); IScientificCalculator ргохуЗ = new SclentificCalculatorCl lentO; SimpleCalculatorCllent proxy4 = new SimpleCalculatorCl 1ent(): ScientificCalculatorClient proxy5 = new Sc1ent1f1cCalculatorC11ent(): UseCalculator(proxyl): UseCalculator(proxy2); UseCalculator(ргохуЗ): UseCalculator(proxy4); UseCalculator(proxy5); Однако между посредниками не существует отношений типа «является ча- стным случаем». Хотя интерфейс IScientificCalculator наследует от ISimpleCalcula- tor, ScientificCalculatorClient не является SimpleCalculatorClient. Кроме того, реали- зацию базового контракта приходится повторять в посреднике субконтракта. Проблему поможет решить прием, который я называю сцеплением посредников (листинг 2.6). Листинг 2.6. Сцепление посредников public partial class SimpleCalculatorClient : Cl1entBase<IScientificCalculator<@062>.ISImpleCalculator { public int Add(Int argl.Int arg2) { return Channel.Add(argl.arg2): } //... } public partial class ScientificCalculatorClient : SimpleCaiculatorCl1 ent.IScientlflcCalculator { public Int Multlplydnt argl.Int arg2) { return Channel.Muitiply(argl.arg2); } //... }
Факторинг и проектирование контрактов служб 77 Только посредник, реализующий базовый контракт самого верхнего уровня, наследует напрямую от CLientBase<T>, а субинтерфейсу нижнего уровня он пере- дается в параметре типа. Все остальные посредники наследуют от посредника, находящегося непосредственно над ними, и соответствующего контракта. Сцепление посредников реализует связь типа «является частным случаем» между посредниками, а также обеспечивает повторное использование кода. Везде на стороне клиента, где ожидается ссылка на SimpLeCalculatorCLient, может использоваться ссылка на ScientificCaLculatorCLient: void UseCalculator(SimpleCaiculatorClient calculator) SimpleCalculatorCl ient proxy1 = new Simpl eCa 1 cul atorC 1 ient(); SimpleCalculatorClient proxy2 = new ScientificCalculatorCl ient(): Scienti ficCal cul atorCl i ent ргохуЗ = new ScientificCalculatorCl ient(): useCalculator(proxyl): useCalculator(proxy2): useCalcul ator (ргохуЗ): Факторинг и проектирование контрактов служб Довольно о синтаксисе; как проектировать контракты служб? Как определить, какие операции должны быть выделены в тот или иной контракт службы? Сколько операций должен содержать каждый контракт? Ответы на эти вопро- сы не имеют особого отношения к WCF, а скорее относятся к области абстракт- ного служебно-ориентированного анализа и проектирования. Подробное обсу- ждение разложения системы на службы и идентификации методов контрактов выходит за рамки настоящей книги. Тем не менее в этом разделе приводится ряд рекомендаций, немного упрощающих работу по проектированию контрак- тов служб. Факторинг Контракт службы представляет собой совокупность логически связанных опе- раций. Трактовка термина «логическая связь» зависит от предметной области. Контракты служб могут рассматриваться как разные аспекты некоторой сущно- сти. Когда (после анализа требований) будут идентифицированы все операции, поддерживаемые этой сущностью, необходимо распределить их по контрактам. Это называется факторингом контрактов служб. При факторинге контрактов служб всегда старайтесь мыслить категориями многократного применения. В служебно-ориентированном приложении базовой единицей многократного применения является контракт службы. Удастся ли создать в результате факто- ринга контракты, которые могут использоваться другими сущностями систе- мы? Какие аспекты сущности могут быть логически выделены для использова- ния другими сущностями?
78 Глава 2. Контракты служб Возьмем конкретный, но простой пример: допустим, мы хотим смоделиро- вать службу собаки. Требования: собака должна уметь лаять и выполнять ко- манду «апорт», получать регистрационный номер ветеринарной клиники и проходить вакцинацию. Вариант первый: определяем контракт службы IDog с разными типами служб (PoodleService, GermanShepherdService и т. д.), реализую- щими контракт IDog: [ServiceContract] Interface IDog { [Operationcontract] void FetchO: [Operationcontract] void BarkO; [Operationcontract] long GetVetClinlcNumberO; [Operationcontract] void VaccinateO: } class PoodleService : IDog {...} class GermanShepherdService : IDog {...} Однако подобную структуру контракта службы IDog нельзя признать удач- ной с точки зрения архитектуры. Хотя в контракте присутствуют все необходи- мые операции, Fetch() и Bark() логически связаны друг с другом в большей сте- пени, чем GetVetClinicNumber() и Vaccinate(). Fetch() и Bark() отражают один аспект собаки как сущности живой и активной, a GetVetClinicNumber() и VaccinateO — дру- гой аспект собаки как записи в архивах ветеринарной клиники. В другом, более правильном решении операции GetVetCLinicNumber() и VaccinateO выделяются в отдельный контракт с именем IPet: [ServiceContract] Interface IPet { [Operationcontract] long GetVetClinlcNumberO: [Operationcontract] void VaccinateO; } [ServiceContract] interface IDog { [Operationcontract] void FetchO: [Operationcontract] void BarkO:
Факторинг и проектирование контрактов служб 79 Так как аспект IPet существует независимо от аспекта I Dog, он может повтор- но использоваться и поддерживаться другими сущностями (например, кошка- ми): [ServiceContract j interface ICat ( [Operationcontract] void PurrO; [Operationcontract] void CatchMouse(): ) class PoodleService : IDog.IPet (...) class SiameseService : ICat.IPet (...) В свою очередь, факторинг позволяет отделить аспект управления ветери- нарными данными в приложении от службы (то есть собаки или кошки). Выде- ление операций в отдельный интерфейс обычно производится при наличии слабых логических связей между операциями. Однако даже в разных контрак- тах иногда встречаются идентичные операции, логически связанные со своими контрактами. Например, и собаки, и кошки способны линять и выкармливать свое потомство. С логической точки зрения линька является такой же собачьей операцией, как лай, и такой же кошачьей операцией, как мурлыканье. В подобных случаях вместо простого разделения контракты служб выстраи- ваются в иерархию: [ServiceContract] interface IMammal I [Operationcontract] void ShedFur(): [Operationcontract] void LactateO; ) [ServiceContract] interface I Dog : IMammal (••} [ServiceContract] interface ICat : IMammal (...) Метрики факторинга Как видите, правильный факторинг приводит к созданию более специализиро- ванных, оптимизированных, пригодных к повторному использованию контрак- тов с неплотным соединением; соответственно, эти преимущества распростра- няются на систему в целом. В общем случае факторинг приводит к сокращению числа операций в контрактах.
80 Глава 2. Контракты служб Однако при проектировании систем на базе служб приходится учитывать взаимодействие двух противостоящих факторов (рис. 2.1). Первый фактор - затраты на реализацию контрактов служб, а второй — затраты на их объедине- ние или интеграцию в приложение. Рис. 2.1. Баланс между количеством и размером служб Если контракты служб окажутся слишком мелкими, реализация каждого контракта упрощается, но общие затраты на интеграцию всех контрактов служб окажутся непомерно большими. С другой стороны, если вы имеете дело с не- сколькими большими, сложными контрактами, затраты на их реализацию ока- жутся чрезмерно высокими (при низких затратах на интеграцию). Связь между затратами и размером контракта службы не линейна, потому что сложность не прямо пропорциональна размеру — при увеличении размера вдвое сложность возрастает от 4 до 6 раз. Аналогично, связь между затратами на интеграцию и количеством интегрируемых контрактов служб тоже не ли- нейна, потому что количество возможных сочетаний не прямо пропорциональ- но количеству задействованных служб. В любой конкретной системе общий объем работ по проектированию и со- провождению служб, реализующих контракты, складывается из двух факто- ров — затрат на реализацию и затрат на интеграцию. Как видно из рис. 2.1, су- ществует область минимальных затрат в отношении размера и количества контрактов служб. Хорошо спроектированная система содержит не слишком много и не слишком мало служб, и эти службы не слишком малы и не слиш- ком велики. Так как проблемы факторинга не зависят от используемой технологии служб, я могу предложить ряд метрик и эмпирических правил, основанных на моем собственном опыте факторинга и проектирования крупномасштабных приложений. Контракты служб с одной операцией возможны, но лучше их избегать. Кон- тракт службы представляет аспект сущности; если аспект выражается всего од- ной операцией, он должен быть весьма тривиальным. Рассмотрите подробнее эту операцию: не использует ли она слишком много параметров? Может, она
Запросы на поддержку контрактов 81 чрезмерно укрупнена и ее следует разбить на несколько операций? Нельзя ли внести операцию в уже существующий контракт службы? Оптимальное количество членов в контрактах служб (по моему опыту и лич- ному мнению) составляет от 3 до 5. Если спроектированный вами контракт со- держит больше операций (скажем, от 6 до 9), дела идут относительно неплохо. И все же попытайтесь проанализировать операции и определить, нельзя ли их объединить друг с другом, потому что чрезмерный факторинг тоже вреден. Если контракт службы содержит 12 и более операций, вам определенно стоит поискать способы выделения операций в отдельный контракт службы или иерар- хию контрактов. Установите в своих стандартах программирования верхний порог, который не должен превышаться ни при каких условиях (например, 20). Другой принцип относится к использованию операций, напоминающих свойства: [Operationcontract j long GetVetCl imcNumber(): Подобных операций тоже следует избегать. Контракты служб позволяют клиентам вызывать абстрактные операции, не заботясь о подробностях их реа- лизации. Операции, являющиеся аналогами свойств, известны под названием минимальной инкапсуляции. Конечно, бизнес-логику задания и чтения значения переменной можно инкапсулировать на стороне службы, но в идеале клиент во- обще не должен иметь дела со свойствами. Клиент вызывает операции, а служба сама заботится об управлении своим состоянием. Взаимодействие должно вес- тись в контексте вызовов вида Выполнить действие^) — например, Vaccinate(). Как служба будет решать свою задачу, какие при этом будут задействованы пе- ременные — клиента это не касается. Небольшое предупреждение: описанные общие правила и метрики — всего лишь вспомогательный инструмент для оценки конкретной архитектуры. Ни- какое правило не заменит практического опыта в предметной области. Будьте практичны, руководствуйтесь здравым смыслом и спросите себя, что будет ра- зумно сделать в свете этих рекомендаций. Запросы на поддержку контрактов Иногда клиенту требуется на программном уровне определить, поддерживает ли некоторая конечная точка (идентифицируемая адресом) тот или иной кон- тракт. Например, представьте себе ситуацию, когда пользователь задает или на- страивает приложение, взаимодействующее со службой, в процессе установки (или даже па стадии выполнения). Если служба не поддерживает необходимые контракты, приложение должно оповестить пользователя о недопустимом адре- се и запросить другой адрес. Например, такая возможность реализована в при- ложении Credentials Manager из главы 10: пользователь должен ввести адрес службы проверки безопасности, управляющей учетными записями и ролями. Credentials Manager проверяет адрес, введенный пользователем, и убеждается в том, что адрес поддерживает необходимые контракты служб.
82 Глава 2. Контракты служб Программная обработка метаданных Чтобы приложение поддерживало такую возможность, оно должно получить метаданные конечных точек службы и проверить, поддерживает ли по крайней мере одна из конечных точек необходимый контракт. Как объяснялось в главе 1, метаданные передаются конечными точками МЕХ, поддерживаемыми службой, или по протоколу HTTP-GET. При использовании HTTP-GET адрес обмена метаданными является адресом HTTP-GET (обычно это базовый адрес службы с суффиксом ?wsdl). Для упрощения задачи разбора возвращаемых метаданных WCF предлагает группу вспомогательных классов из пространства имен System. ServiceModeLDescription, представленных в листинге 2.7. Листинг 2.7. Типы, обеспечивающие обработку метаданных public enum MetadataExchangeClientMode { MetadataExchange. HttpGet } class MetadataSet : ... {...} public class ServiceEndpointCollection : Collection<ServiceEndpoint> {...} public class MetadataExchangeClient { public MetadataExchangeClientO; public MetadataExchangeClient(Binding mexBinding); public MetadataSet GetMetadataCUri address, MetadataExchangeClientMode mode); // ... } public abstract class Metadata Importer { public abstract ServiceEndpointCollection ImportAllEndpointsO; // ... } public class WsdlImporter : MetadataImporter { public WsdlImporter(MetadataSet metadata); // ... } public class ServiceEndpoint { public EndpointAddress Address {get;set;} public Binding Binding {get;set;} public ContractDescription Contract {get;} // ... } public class ContractDescription {
Запросы на поддержку контрактов 83 public string Name (get;set:} public string Namespace (get;set:} // ... ) Класс MetadataExchangeClient может использовать привязку, связанную с об- меном метаданными в конфигурационном файле приложения. Также можно передать конструктору MetadataExchangeClient уже инициализированный экзем- пляр привязки с некоторыми измененными параметрами — например, возмож- ностью передачи сообщений большего размера, если объем возвращаемых мета- данных превышает стандартный размер сообщения. Метод GetMetadata() класса MetadataExchangeClient получает экземпляр адреса конечной точки, в котором упакован адрес МЕХ и перечисление, задающее метод доступа, а возвращает метаданные в экземпляре MetadataSet. С этим типом не следует работать напря- мую. Вместо этого создайте экземпляр субкласса Metadataimporter (например, Wsdllmporter) с передачей метаданных в параметре конструктора, после чего вы- зовите метод ImportAUEndpointsO для получения коллекции всех конечных то- чек, обнаруженных в метаданных. Конечные точки представляются классом ServiceEndpoint. ServiceEndpoint предоставляет свойство Contract типа ContractDescription, в ко- тором содержится имя и пространство имен контракта. Чтобы определить через HTTP-GET, поддерживает ли заданный базовый адрес тот или иной контракт, выполните только что описанную последователь- ность действий для получения коллекции конечных точек. Для каждой конеч- ной точки в коллекции сравните свойства Name и Namespace в ContractDescription с нужным контрактом, как показано в листинге 2.8. Листинг 2.8. Проверка контракта по адресу bool contractSupported = false: string mexAddress = "...?WSDL"; MetadataExchangeClient MEXClient = new MetadataExchangeClient(new Uri(mexAddress). MetadataExchangeClientMode.HttpGet): MetadataSet metadata = MEXC1 ient .GetMetadataO : Metadataimporter importer = new Wsdl Importer(metadata): ServiceEndpointCol lection endpoints = importer. ImportAUEndpointsO: foreach(ServiceEndpoint endpoint in endpoints) I if(endpoint.Contract.Namespace == "MyNamespace" && endpoi nt.Contract.Name == "IMyContract") contractSupported = true: break:
84 Глава 2. Контракты служб ПРИМЕЧАНИЕ-------------------------------------------------------------------- Программа MetadataExplorer, представленная в главе 1, выполняет аналогичную последователь- ность действий для получения конечных точек службы. Получая адрес на базе HTTP, программа проверяет конечные точки обмена метаданными как для HTTP, так и для HTTP-GET. Metadata Explorer также может принимать метаданные по конечным точкам, базирующимся на TCP и IPC. Ос- новной объем реализации связан с обработкой метаданных и их отображением, потому что сложная задача получения и разбора метаданных решается классами, предоставляемыми WCF. Класс MetadataHelper Я инкапсулировал и обобщил основные этапы листинга 2.8 в методе Query- Contract моего статического вспомогательного класса MetadataHelper: public static class MetadataHelper public static bool QUeryContract(string mexAddress.Type contractType): public static bool QUeryContract(string mexAddress. string contractNamespace. string contractName): // } MetadataHelper передается либо тип проверяемого контракта, либо его имя и пространство имен: string address = bool contractSupported = Metadatahelper.QueryContract(address.typeof(IMyContract)); В качестве адреса обмена метаданными MetadataHelper предоставляется ад- рес HTTP-GET или адрес конечной точки обмена метаданными для HTTP, HTTPS, TCP или IPC. В листинге 2.9 представлена реализация MetadataHelper. QueryContract() (с исключением части кода обработки ошибок). Листинг 2.9. Реализация MetadataHelper.QueryContract() public static class MetadataHelper { const int MessageMultiplir = 5: static ServiceEndpontCollection QueryMexEndpoint(string mexAddress. BindingElement bindingElement) CustomBinding binding = new CustomBinding(bindingElement); MetadataExchangeClient MEXC1lent = new MetadataExchangeClient(binding); MetadataSet metadata = MEXC1ient.GetMetadata (new EndpointAddress(mexAddress)): Metadataimporter importer = new WsdlImporter(metadata): return importer.ImportAllEndpoints(); public static ServiceEndpoint[] GetEndpoints(string mexAddress) /* Обработка ошибок */ Uri address = new Uri(mexAddress);
Запросы на поддержку контрактов 85 ServiceEndpontCollection endpoints = null: if(address. Scheme == "net. tcp") ( TcpTransportBi ndi ngElement tcpBindingElement = new TcpTransportBindingElement(): tcpBindingElement.MaxReceivedMessageSize *= MessageMultiplier; endpoints = QueryMexEndpoint(mexAddress.tcpBindingElement); } iftaddress.Scheme == "net.pipe") (...) if(address.Scheme ~ "http") // Также проверяем HTTP-GET {•••) if (address. Scheme == "https") // Также проверяем HTTP-GET return Col lection. toArray(endpoi nts): public static bool QueryContract(string mexAddress.Type contractType) г i f (contractType. Islnterface == false) { Debug Assert(false.contractType + " is not an interface"); return false; objects attributes = contractType.GetCustomAttributes( typeof(ServiceContractAttribute).false); if (attributes. Length == 0) { Debug.Assert(false,"Interface " + contractType + " does not have the ServiceContractAttribute"); return false; ServiceContractAttribute attribute = attributesEO] as ServiceContractAttribute; if(attribute.Name == null) attribute.Name = ContractType.ToString(); if(attribute.Namespace == null) attribute.Namespace = "http;//tempuri .org/"; } return QueryContract(mexAddress.attribute.Namespace.attribute.Name); } public static bool QueryContract(string mexAddress. string contractNamespace. string contractName) { i f (Stri ng. IsNullOrEmpty(contractNamespace)) Debug.Assert(false."Empty namespace"); return false; ) i f(Str i ng.IsNu11 OrEmpty (cont ract Name)) ( Debug.Assert(false."Empty name"); продолжение &
86 Глава 2. Контракты служб return false; } try { ServiceEndpoint[] endpoints - GetEndpoints(mexAddress): foreach(ServiceEndpoint endpoint in endpoints) { if(endpoint.Contract.Namespace — contractNamespace && endpoint.Contract.Name — contractName) { return true: ) } } catch {} return false: } } В листинге 2.9 схема адреса обмена метаданными разбирается методом Get- EndPoints(). В соответствии с обнаруженной транспортной схемой (например, TCP), GetEndpoints() конструирует элемент привязки для задания его свойства MaxReceivedMessageSize: public abstract class TransportBindingElement : BindingElement { public virtual long MaxReceivedMessageSize {get:set;} } public abstract class Connect!onOrientedTransportBindingElement : T ransportBi ndi ngElement {•••} public class TcpTransportBindingElement : Connect i onOri entedTransportBi ndi ngElement {...} По умолчанию значение MaxReceivedMessageSize равно 64 Кбайт. Для про- стых служб этого достаточно, но службы с большим количеством конечных то- чек генерируют большие сообщения, для которых вызов MetadataExchangeClient. GetMetadata() завершится неудачей. Мои эксперименты показали, что для боль- шинства случаев поправочного множителя 5 оказывается достаточно. Затем GetEndpoints() использует приватный метод QueryMexEndpoint() для фактическо- го получения метаданных. QueryMexEndpoint() получает адрес конечной точки обмена метаданными и используемый элемент привязки. По элементу привяз- ки конструируется пользовательская привязка, которая передается экземпляру MetadataExchangeClient; последний получает метаданные и возвращает коллек- цию конечных точек. Вместо ServiceEndpointCollection метод GetEndpoints() воз- вращает массив конечных точек с использованием моего вспомогательного клас- са Collection. Метод QueryContract() с параметром Туре сначала проверяет, что тип пред- ставляет интерфейс и помечен атрибутом ServiceContract. Поскольку атрибут ServiceContract можА использоваться для назначения псевдонима как имени,
Запросы на поддержку контрактов 87 так и пространства имен запрашиваемого типа контракта, QueryContractQ снача- ла использует их для поиска контракта. Если псевдонимы не используются, QueryContract() использует имя типа с пространством имен по умолчанию http:// tempuri.org, и вызывает версию QueryContract(), получающую имя и пространство имен. Эта версия QueryContract() вызывает GetEndpoints() для получения массива конечных точек, после чего перебирает элементы массива и возвращает true при обнаружении хотя бы одной конечной точки, поддерживающей контракт. В случае каких-либо ошибок QueryContract() возвращает false. В листинге 2.10 перечислены дополнительные методы проверки метадан- ных, присутствующие в классе MetadataHelper. Листинг 2.10. Класс MetadataHelper public static class MetadataHelper { public static ServiceEndpointE] GetEndpoints(string mexAddress); public static stringE] GetAddresses(Type bindingType. string mexAddress.Type contractType); public static stringE] GetAddresses(string mexAddress.Type contractType); public static stringE] GetAddresses(Type bindingType. string mexAddress. string contractNamespace. string contractName) where В Binding; public static stringE] GetAddresses(string mexAddress. string contractNamespace. string contractName); public static stringE] GetContracts(Type bindingType. string mexAddress). public static stringE] GetContracts(string mexAddress); public static stringE] GetOperations(string mexAddress.Type contractType); public static stringE] GetOperations(string mexAddress. string contractNamespace. string contractName); public static bool QueryContract(string mexAddress.Type contractType); public static bool QueryContract(string mexAddress. string contractNamespace. string contractName): // ) Эти мощные, полезные функции часто оказывают неоценимую помощь при настройке конфигурации, в административных программах и утилитах, при этом их реализация сводится к обработке массива конечных точек, возвращае- мого методом GetEndpoints(). Методы GetAddresses() возвращают адреса всех конечных точек, поддержи- вающих заданный контракт, или только адреса конечных точек, использующих определенную привязку. Аналогично, методы GetContracts() возвращают все контракты, поддерживае- мые всеми конечными точками, или контракты, поддерживаемые всеми конеч- ными точками с определенной привязкой. Наконец, методы GetOperations() воз- вращают все операции заданного контракта. ПРИМЕЧАНИЕ------------------------------------------------------------------------------ В главе 10 класс MetadataHelper используется в приложении Credentials Manager, а в приложе- нии Б — для администрирования подписки.
Контракты данных С абстрактной точки зрения все функции WCF сводятся к хостингу и предо- ставлению родных типов CLR (интерфейсов и классов) в виде служб, а также к использованию служб как родных интерфейсов и классов CLR. Операции служб WCF принимают и возвращают типы CLR (целые числа, строки и т. д.), а клиенты WCD передают и обрабатывают возвращаемые типы CLR. Конечно, эти типы CLR относятся к специфике .NET. Один из основополагающих прин- ципов служебно-ориентированного программирования гласит, что информация о технологии реализации службы не передается за ее пределы. В результате со службой сможет взаимодействовать любой клиент независимо от своей техно- логии реализации. Разумеется, из этого следует, что WCF не позволит выво- дить типы данных CLR через границу службы; потребуется способ преобразо- вания типов CLR в стандартное нейтральное представление и обратно. Таким представлением является простая схема на базе XML, или инфонабор (infoset). Кроме того, службе понадобится формальный механизм описания того, как должно выполняться преобразование. Этот формальный механизм, называе- мый контрактом данных, является темой настоящей главы. В первой части гла- вы будет показано, как в контексте контрактов данных организуется марша- линг и преобразования типов и как в инфраструктуре решаются проблемы иерархий классов и версий контрактов данных. Во второй части рассматривает- ся использование различных типов .NET — перечислений, делегатов, таблиц данных и коллекций — в контрактах данных. Сериализация Контракт данных является частью контрактных операций, поддерживаемых службой (другой их частью является контракт службы). Контракт данных пуб- ликуется в метаданных службы, что позволяет клиентам преобразовать ней- тральное, технологически-независимое представление в родное представление клиента. Поскольку объекты и локальные ссылки относятся к концепциям CLR, объекты и ссылки CLR не могут передаваться службам WCF и прини- маться от них. Если бы такая возможность существовала, она не только нару-
Сериализация 89 шала бы упоминавшиеся ранее принципы служебно-ориентированного про- граммирования, но и нс могла бы применяться на практике, потому что объект состоит как из данных состояния, так и из кода, управляющего ими. Передать код или логику при вызове метода C# или Visual Basic невозможно, не говоря уже о его маршалинге на другую платформу или технологию. При передаче объекта или структурного типа (value type) в параметре операции вам в дейст- вительности нужно лишь передать его состояние, а получающая сторона долж- на преобразовать его обратно к своему родному представлению. Такой подход к передаче состояния носит название маршалинга по значению. Для выполнения маршалинга по значению проще всего воспользоваться встроенной поддержкой сериализации, присутствующей в большинстве платформ (включая .NET). Рис. 3.1. Сериализация и десериализация при вызове операции На стороне клиента WCF сериализует входные параметры из родного пред- ставления CLR в инфонабор XML и пересылает их в исходящем сообщении. Как только сообщение будет получено на стороне службы, WCF десериализует его и преобразует нейтральный инфонабор XML в соответствующее представ- ление CLR, прежде чем передавать вызов службе. Затем служба обрабатывает параметры в виде родных типов CLR. После того как выполнение операции бу- дет завершено, WCF сериализует выходные параметры и возвращаемые значения в нейтральный инфонабор XML, упаковывает их в возвращаемое сообщение и от- правляет сообщение клиенту. Наконец, на стороне клиента WCF десериализует возвращаемые данные в родные типы CLR и возвращает их клиенту (рис. 3.1).
90 Глава 3. Контракты данных Сериализация .NET WCF может воспользоваться готовой поддержкой сериализации в .NET. Плат- форма .NET автоматические сериализует и десериализует объекты, применяя механизм рефлексии. Значение каждого поля объекта фиксируется и сериали- зуется с направлением в память, файл или сетевое подключение. При десериа- лизации .NET создает новый объект соответствующего типа, читает сохранен- ные значения полей и задает поля нового объекта посредством рефлексии. Так как рефлексия может работать с приватными полями, включая поля базовых классов, .NET в процессе сериализации получает полный «снимок» состояния объекта и точно воспроизводит это состояние при десериализации. .NET сериа- лизует объект в поток (stream) — логическую последовательность байтов, не зависящую от конкретного носителя информации (файл, память, коммуника- ционный порт и т. д.). Атрибут Serializable По умолчанию пользовательские типы (классы и структуры) не сериализуют- ся. Дело в том, что .NET не может определить, имеет ли смысл дамп состояния объекта в виде потока. Возможно, члены объекта обладают переходящим, неус- тойчивым состоянием (например, открытое подключение к базе данных или коммуникационному порту). Если .NET просто сериализует состояние такого объекта, то после конструирования нового объекта при десериализации из по- тока мы получим неполноценный объект. Соответственно, сериализация долж- на выполняться только с одобрения разработчика класса. Чтобы сообщить .NET о сериализуемости экземпляров класса, в определе- ние класса или структуры включается атрибут SerializableAttribute: L At t г 1buteUsaqe(At tг1buteTargets.Del egate| AttributeTargets.Enum | At Гм but eTargets .Struct | A: tributf-'Targets.Class. Irherited-fa Ise)] public sealed Nass Qena 1 vableAttribute : Attribute П Например: l Seria1iZdble] puNic class MyClass Если класс объявлен сериализуемым, .NET проверяет сериализуемость всех его полей и при обнаружении несериализуемого поля выдает исключение. Но что делать, если некоторый класс или структура содержит поле, которое не се- риализуется? Такой гии не имеет атрибута Serializable, а следовательно, вме- щающий гип тоже окажется несериализуемым. Как правило, несериализуемое поле представляет собой ссылочный тип, требующий специальной инициализа- ции. Проблема решается пометкой поля как несериализуемого и его отдельной m।iiuita/iвi3aiLiieii при десериализации. Чтобы сериализуемый тип мог содержать поле несериализуемого типа, это поле должно быть помечено атрибутом NonSerialized; например:
public class MyOtherClass (••) [Serializable] public class MyClass ( [NonSerial ized] MyOtherClass m_OtherClass: /* Методы и свойства */ ) При сериализации поля .NET сначала проверяет, не помечено ли оно атри- бутом NonSerialized. Если атрибут присутствует, .NET попросту игнорирует это поле. Это позволяет исключить из сериализации даже обычно сериализуемые типы вроде string: [Serializable] public class MyClass ( [NonSerialized] string m_Name: Форматеры .NET .NET содержит два форматера для сериализации и десериализации типов. Bina- ryFormatter сериализует данные в компактный двоичный формат, обеспечивая возможность быстрой сериализации/десериализации, потому что преобразова- ние не требует разбора. SoapFormatter использует специфический для .NET фор- мат SOAP XML; с другой стороны, сериализация потребует дополнительных затрат на формирование, а десериализация — затрат на разбор. Оба форматера поддерживают интерфейс IFormatter: public interface IFormatter object Deserial 1ze(Stream serializationstream); void Serialize(Stream serializationstream.object graph); // ... public sealed class BinaryFormatter ; IFormatter.... (...) public sealed class SoapFormatter : IFormatter.... (...) Независимо от используемого формата, кроме состояния объекта оба фор- матера сохраняют в потоке информацию о сборке и версии типа, чтобы сделать возможным его десериализацию к правильному типу. Как следствие, оба фор- матера не подходят для служебно-ориентированных взаимодействий, потому что другая сторона должна располагать сборкой типа — и конечно, в первую очередь использовать .NET. Использование Stream тоже накладывает дополни- тельные ограничения, потому что клиент и служба должны каким-то образом совместно использовать поток.
92 Глава 3. Контракты данных Форматеры WCF Из-за недостатков классических форматеров .NET в WCF пришлось включить собственный служебно-ориентированный форматер. Форматер WCF DataCon- tractSerializer способен передавать только контракт данных, без информации о нижележащих типах. DataContractSerializer определяется в пространстве имен System.Runtime.Serialization и частично представлен в листинге 3.1. Листинг 3.1. DataContractSerializer public abstract class XmlObjectSerializer { public virtual object ReadObject(Stream stream): public virtual object ReadObject(XmlReader reader): public virtual void WriteObject(XmlWriter writer.object graph): public virtual void WriteObject(Stream stream.object graph): // } public sealed class DataContractSerializer : XmlObjectSerializer { public DataContractSerializer(Type type): // } DataContractSerializer сохраняет только состояние объекта в соответствии со схемой сериализации или контрактом данных. Также обратите внимание, что DataContractSerializer не поддерживает интерфейс IFormatter. WCF использует DataContractSerializer автоматически, и разработчику нико- гда не придется иметь дело с этим классом напрямую. Тем не менее вы можете использовать DataContractSerializer для сериализации типов в поток .NET и об- ратно, по аналогии с унаследованными форматерами. В отличие от двоичного или SOAP-форматера, конструктору DataContractSerializer необходимо передать тип, с которым он должен работать, потому что сам поток данных не содержит информации о типе: MyClass objl = new MyClassO; DataContractSerializer formatter = new DataContractSerializer(typeofCMyClass)): using(Stream stream = new MemoryStream()) { formatter.WriteObject(stream.objl): stream.Seek(O.SeekOrigin.Begin): MyClass obj2 = (MyClass)formatter.ReadObject(stream); } Хотя класс DataContractSerializer может использоваться с потоками .NET, его также можно использовать в сочетании с ридерами и райтерами XML, когда единственной формой входных данных является физический XML-код (вместо файла, памяти и т. д.). Обратите внимание на использование аморфного типа object в определении DataContractSerializer в листинге 3.1. Оно подразумевает отсутствие безопасл^- сти типов на стадии компиляции, потому что конструктор может получить
Сериализация 93 один тип, WriteObject() может получить второй тип, a ReadObject() — выполнить преобразование к третьему типу. Для решения этой проблемы можно определить для DataContractSeriaLizer собственную обобщенную «обертку» — наподобие той, что представлена в лис- тинге 3.2. Листинг 3.2. Параметризованный класс DataContractSerializer<T> public class DataContractSeriа 1 izer<r> : XmlObjectSerializer ( DataContractSerial izer m_DataContractSerializer: public DataContractSerializer() ( m_DataContractSerializer = new DataContractSerializer(typeof(T)); } public new T ReadObject(Stream stream) ( return (T)m_DataContractSenal izer.ReadObject(stream): public new T ReadObject(XmlReader reader) return (T)m_DataContractSerializer.ReadObject(reader); } public void WriteObject(Stream stream.? graph) ( m_DataContractSeri al i zer. WriteObject (stream .graph); public void WriteObject(XmlWriter.T graph) { m_DataContractSerial i zer. WriteObject(writer .graph); } Обобщенный класс DataContractSeriaLizer<T> гораздо безопаснее в использова- нии, чем DataContractSeriaLizer на базе object: MyClass objl = new MyClassO; DataContractSerial izer<MyClass> formatter = new DataContractSerializer<MyClass>(): using(Stream stream = new MemoryStream()) ( formatter.Wri teObject(stream. obj 1): stream. SeekO. SeekOrigin. Begin): MyClass obj2 = formatter.ReadObject(stream); В WCF также имеется форматер NetDataContractSeriaLizer, полиморфный c IFormatter: public sealed class NetDataContractSerializer : IFormatter,... (...) Как следует из названия, форматер NetDataContractSeriaLizer, по аналогии со старыми форматерами .NET, наряду с состоянием объекта сохраняет информа- цию о типе. Используется он почти так же, как и старые форматеры:
94 Глава 3. Контракты данных MyClass objl = new MyClassO; IFormatter formatter = new NetDataContractSerializer(); us Ing(Stream stream = new MemoryStreamO) { formatter.Seri a 1i ze(stream,obj1); stream.Seek(0.SeekOrlgin.Begin); MyClass obj2 = (MyClass)formatter.Deserlallze(stream); } Класс NetDataContractSerializer проектировался как дополнение DataContract- Serializer. Например, тип можно сериализовать с использованием NetDataContract- SeriaLizer и десериализовать с DataContractSerializer: MyClass objl = new MyClassO: Stream stream = new MemoryStreamO; IFormatter formatterl = new NetDataContractSerializerO: formatterl.Serialize(stream.objl); stream.Seek(0.SeekOrigin.Begin); DataContractSerializer formatted = new DataContractSerializer(typeof(MyClass)); MyClass obj2 = (MyClass)formatter2.Read0bject(stream); stream.Closet); Данная возможность открывает пути к написанию кода, устойчивого к вер- сиям, а также миграции унаследованного кода с передачей информации о типах в служебно-ориентированные решения, в которых сохраняется только схема данных. Контракт данных и сериализация Если операция службы получает или возвращает параметр любого типа, WCF использует DataContractSerializer для сериализации и десериализации этого па- раметра. Это означает, что в качестве типа параметра или возвращаемого значе- ния операции контракта может использоваться любой сериализуемый тип — при условии, что у другой стороны ймеется определение схемы данных или контракта данных. Все встроенные примитивные типы .NET сериализуемы. На- пример, определения int и string выглядят так: [Serializable] . public struct Int32 : {••} [Serializable] public sealed class String : ... {...} Только благодаря этому обстоятельству работали все контракты служб, представленные в предыдущей главе. WCF предоставляет для примитивных типов неявный контракт данных, потому что для их схемы существует отрасле- вой стандарт. Для передачи в параметре операции пользовательского типа должны выпол- няться два условия: во-первых, тип должен быть сериализуемым, а во-вторых,
Атрибуты контракта данных 95 клиент и служба должны иметь локальное определение этого типа, которое бы приводило к той же схеме данных. Для примера возьмем контракт службы IContactManager, предназначенной для управления списком контактов: [Serializable] struct Contact ( public string FirstName: public string LastName: ) [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO: ) Если клиент располагает эквивалентным определением структуры Contact, он сможет передать службе структуру. Эквивалентное определение может быть любым, лишь бы оно приводило к той же схеме данных при сериализации. На- пример, клиент может использовать следующее определение: [Serializable] struct Contact ( public string FirstName: public string LastName: [NonSeri al 1 zed] public string Address: ) Атрибуты контракта данных Хотя атрибут Serializable работает, для служебно-ориентированных взаимодей- ствий между клиентами и службами его возможностей недостаточно. Он поме- чает все поля типа как сериализуемые, а следовательно, являющиеся частью схемы данных типа. Гораздо лучше выглядит вариант «явного назначения»: в контракт данных включаются только те члены, которые были специально ука- заны разработчиком. Атрибут Serializable требует обязательной сериализуемо- сти данных для их передачи в параметре операции и не обеспечивает четкого разделения между возможностью использования типа в параметрах операций WCF и его сериализуемостью. Он не поддерживает ни назначения псевдони- мов для имен типов и их полей, ни отображения нового типа на заранее опреде- ленный контракт данных. Атрибут работает с полями напрямую и полностью обходит любые логические свойства, используемые для обращения к этим по- лям. Было бы лучше, если бы свойства могли добавлять свои значения при
96 Глава 3. Контракты данных обращении к полям. Наконец, Serializable не имеет прямого контроля версии - предполагается, что данные версии будут сохраняться форматерами. Соответ- ственно, по прошествии времени обновление версий усложняется. Как и прежде, на помощь приходят новые служебно-ориентированные, явно назначаемые атрибуты WCF. Первый из этих атрибутов, DataContractAttribute, определяется в пространстве имен System.Runtime.Serialization: [Attri buteUsage( Att ri buteTargets. Enum | AttributeTargets.Struct | AttributeTargets.Cl ass. Inherited=false)] public sealed class DataContractAttribute : Attribute { public string Name {get;set;} public string Namespace {get;set;} } Применение атрибута DataContract к классу или структуре не приведет к тому, что WCF начнет принудительно сериализовать его поля: [DataContract] struct Contact // Не войдет в контракт данных public string FirstName: public string LastName; } Атрибут DataContract всего лишь явно указывает, что маршалинг данного типа должен осуществляться по значению. Чтобы обеспечить сериализацию его полей, необходимо применить атрибут DataMemberAttrubute, определяемый сле- дующим образом: [Attri butellsage(Attri buteTargets.Field |Attri buteTargets.Property, Inheritedefalse)] public sealed class DataMemberAttribute : Attribute { public bool IsRequired {get;set:} public string Name {get;set;} public int Order {get;set;} } Атрибут DataMember может применяться непосредственно к полям: [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName;
Атрибуты контракта данных 97 Он также работает и со свойствами: [DataContract] struct Contact ( string m_FirstName: string m_LastName: [DataMember] public string FirstName { get (...} set {...} ) [DataMember] public string LastName ( get (...) set ) Как и в случае с контрактами служб, видимость полей данных или самого контракта для WCF несущественна. В контракт данных могут включаться внут- ренние типы с приватными полями данных: [DataContract] struct Contact ( [DataMember] string m_FirstName: [DataMember] string m_LastName: В этой главе атрибут DataMember в основном применяется напрямую к от- крытым полям данных; это сделано для экономии места. Конечно, в реальном коде вместо открытых переменных следует использовать свойства. ПРИМЕЧАНИЕ---------------------------------------------------------------------------------------- В контрактах данных учитывается регистр символов — как на уровне типов, так и на уровне полей. Импортирование контракта данных Контракт данных, используемый в контрактных операциях, публикуется в ме- таданных службы. Когда клиент импортирует определение контракта данных, он получает определение эквивалентное, но не идентичное. Импортированное определение сохраняет исходный тип класса или структуры. Кроме того, в от- личие от контракта службы, по умолчанию в импортированном определении сохраняется исходное пространство имен типа.
98 Глава 3. Контракты данных Для примера возьмем следующее определение на стороне службы: namespace MyNamespace { CDataContract] struct Contact [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contact[] GetContacts(): } } Импортированное определение будет выглядеть так: namespace MyNamespace { [DataContract] struct Contact {} } [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contacts GetContactsO: } Чтобы переопределить значение по умолчанию и предоставить для контрак- та данных альтернативное пространство имен, достаточно присвоить значение свойству Namespace атрибута DataContract. Возьмем следующее определение на стороне сервера: namespace MyNamespace [DataContract(Namespace * "MyOtherNamespace")] struct Contact {...} } Импортированное определение выглядит так: namespace MyOtherNamespace [DataContract] struct Contact
Атрибуты контракта данных 99 Импортированное определение всегда имеет свойства, помеченные атрибу- том DataMember, даже если исходный тип на стороне службы не определял ни- каких свойств. Если в исходном определении на стороне службы атрибут Data- Member применялся напрямую к полям, в импортированном определении типа для обращения к ним будут использоваться свойства, имена которых состоят из имени поля данных и суффикса Field. Например, для следующего определения на стороне службы: [DataContract] struct Contact [DataMember] public string FirstName: [DataMember] public string LastName: } импортированное определение на стороне клиента будет выглядеть так: [DataContract] public partial struct Contact { string FirstNameField; string LastNameField: [DataMember] public string FirstName ( get ( return FirstNameField; ) set { FirstNameField = value; ) } [DataMember] public string LastName { get ( return LastNameField; ) set { LastNameField = value: } } Конечно, клиент может вручную переработать любое важное определение и привести его в полное соответствие с определением на стороне службы.
100 Глава 3. Контракты данных ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Даже если атрибут DataMember на стороне службы применяется к приватному полю или свойству [DataContract] struct Contact { [DataMember] string FirstName {get;set;} [DataMember] string LastName; } импортированное определение все равно будет содержать открытое свойство. Если атрибут DataMember применяется к свойству, входящему в контракт данных на стороне службы, импортированное определение будет обладать идентичным набором свойств. Свойство на стороне клиента будет работать с полем, имя которого состоит из имени свойства и суффикса Field. Например, для следующего контракта данных на стороне службы: [DataContract] public partial struct Contact { string m_F1rstName; string m_LastName; [DataMember] public string FirstName { get { return m_FirstName: } set { m_FirstName = value: } } [DataMember] public string LastName { get { return m_LastName; } set { m_LastName = value: } } } импортированное определение будет выглядеть так:
Атрибуты контракта данных 101 [DataContract] public partial struct Contact { string FirstNameField; string LastNameField: [DataMember] public string FirstName ( get ( return FirstNameField: } set FirstNameField = value; ) } [DataMember] public string LastName ( get ( return LastNameField; set ( LastNameField = value: } ) ) Если атрибут DataMember применяется к свойству (на стороне службы или клиента), это свойство должно обладать методами доступа get и set. Без них при вызове произойдет исключение InvalidDataContractException. Это объясняет- ся тем, что когда свойство само является полем данных, WCF использует его при сериализации и десериализации, давая вам возможность применить любую пользовательскую логику. ВНИМАНИЕ ------------------------------------------------------------------------------------------- Не применяйте атрибут DataMember как к свойству, так и к полю, на котором оно базируется, — это приведен к дублированию полей на импортирующей стороне. Важно понять, что рассмотренные правила использования атрибута DataMem- ber действуют как на стороне службы, так и на стороне клиента. Когда клиент использует атрибут DataMember (и другие сопутствующие атрибуты, упоминае- мые в этой главе), он влияет на контракт данных, используемый для сериализа- ции и отправки параметров службе или десериализации и получения возвра- щаемых службой данных. Две стороны могут использовать эквивалентные, но не идентичные контракты данных — и как вы вскоре убедитесь, даже неэквива- лентные контракты Клиент управляет своими контрактами данных и настраи- вает их независимо от службы.
102 Глава 3. Контракты данных Контракт данных и атрибут Serializable Служба по-прежнему может использовать тип, помеченный только атрибутом Serializable: [Serializable] struct Contact { string m_FirstName; public string LastName: } При импортировании метаданных такого типа в импортированном опреде- лении будет использоваться атрибут DataContract. Кроме того, поскольку атри- бут Serializable распространяется только на поля, все будет происходить так, словно каждый сериализуемый член класса (открытый или приватный) являет- ся полем данных; в результате мы получаем набор свойств-«оберток», имена которых совпадают с именами исходных полей: [DataContract] public partial struct Contact { string LastNameField: string m_FirstNameField: [DataMember(...)] public string LastName { ...II Обращение к LastNameField } [DataMember(...)] public string m_FirstName { ... 11 Обращение к m_FirstNameField } } ПРИМЕЧАНИЕ------------------------------------------------------------------------------ Тип, помеченный только атрибутом DataContract, не может сериализоваться с использованием ста- рых форматеров. Если вы захотите сериализовать тип, следует применить к нему как атрибут Data- Contract, так и Serializable. Канальное представление такого типа выглядит так, как если бы был применен только атрибут DataContract, но к членам типа все равно должен применяться атрибут DataMember. КОНТРАКТ ДАННЫХ И СЕРИАЛИЗАЦИЯ XML-------------------------------------------------- .NET поддерживает еще один механизм сериализации — сериализацию в физический код XML с ис- пользованием специального набора атрибутов. Когда вы имеете дело с типом данных, требующим явного контроля над сериализацией XML, используйте атрибут XmlSerializerFormatAttribute с отдель- ными операциями в определении контракта — тем самым вы указываете WCF на необходимость ис- пользования сериализации XML во время выполнения. Если такая форма сериализации необходима для всех операций контракта, воспользуйтесь ключом /serializer:XmlSerializer утилиты SvcUtil, чтобы атрибут XmlSerializerFormat автоматически применялся ко всем операциям всех импортированных контрактов. Будьте внимательны с этим ключом: он влияет на все контракты данных, включая те, которые не требуют явного управления сериализацией XML.
Атрибуты контракта данных 103 Аналогичным образом клиент может использовать атрибут Serializable со своим контрактом данных, в результате чего канальное представление послед- него (то есть способ его маршалинга) будет идентично только что описанному. Составные контракты данных При определении контракта данных атрибут DataMember может применяться к полям, которые сами являются контрактами данных, как показано в листин- ге 3.3. Листинг 3.3. Составной контракт данных [DataContract] struct Address ( [DataMember] public string Street; [DataMember] public string City; [DataMember] public string State: [DataMember] public string Zip; [DataContract] struct Contract [DataMember] public string FirstName: [DataMember] public string LastName; [DataMember] public Address Address: } Возможность вложения контрактов данных демонстрирует тот факт, что контракты данных в действительности рекурсивны по своей природе. При се- риализации составного контракта данных DataContractSerializer отслеживает все ссылки в графе объекта и сохраняет их состояние. Публикация составного кон- тракта данных означает публикацию всех его составляющих контрактов. На- пример, для определения из листинга 3.3 метаданные следующего контракта службы: [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact):
104 Глава 3. Контракты данных [Operationcontract] Contacts GetContacts(): } будут включать и определение структуры Address. События контрактов данных В .NET 2.0 появилась поддержка событий сериализации для сериализуемых ти- пов, a WCF предоставляет аналогичную поддержку для контрактов данных. При выполнении сериализации и десериализации WCF вызывает заданные ме- тоды. Всего определено четыре события сериализации/десериализации. Собы- тие serializing инициируется непосредственно перед выполнением сериализа- ции, а событие serialized — сразу же после нее. Аналогично, событие deserializing инициируется перед десериализацией, а событие deserialized — после ее завер- шения. Методы, используемые в качестве обработчиков событий сериализации, задаются с использованием атрибутов методов, как показано в листинге 3.4. Листинг 3.4. Применение атрибутов событий сериализации [DataContract] class MyDataContract ( [OnSerializing] void OnSerializing(Streamingcontext context) {••} [OnSerializing] void OnSerialized(Streamingcontext context) [OnDeserializing] void OnDeserializing(StreamingContext context) {•••} [OnDeserializing] void OnDeserializing(StreamingContext context) {•••} // } Любой метод обработки события сериализации должен обладать следующей сигнатурой: void <имя_метода>(StreamingContext context): Данное требование является обязательным, потому что во внутренней реа- лизации WCF использует делегатов для подписки и вызова методов обработки событий. Если атрибуты применяются к методам с несовместимой сигнатурой, WCF выдает исключение. Структура Streamingcontext передает информацию типу о причине его сериа- лизации, но для контрактов данных WCF она может игнорироваться. Атрибуты событий определяются в пространстве имен System.Runtime.Serialization.
Атрибуты контракта данных 105 Как подсказывают имена атрибутов, атрибут OnSerializing определяет метод обработки события serializing, атрибут OnSerialized — метод обработки события serialized и т. д. На рис. 3.2 изображена диаграмма операций, на которой показан порядок инициирования событий при сериализации. Рис. 3.2. Последовательность событий при сериализации Сначала WCF инициирует событие serializing, что приводит к вызову соот- ветствующего обработчика события. Затем WCF сериализует объект, после чего инициируется событие serialized с вызовом соответствующего обработчика событий. На рис. 3.3 представлен порядок событий при десериализации. Рис. 3.3. Последовательность событий при десериализации Обратите внимание: для вызова метода обработки события десериализации WCF необходимо сначала сконструировать объект — тем не менее WCF при этом не вызывает конструктор по умолчанию класса контракта данных. ВНИМАНИЕ ------------------------------------------------------------------ WCF не разрешает применять один атрибут события сериализации к разным методам типа контрак- та данных. Это весьма печально — данное обстоятельство исключает поддержку частичных типов, когда каждая часть имеет дело со своими событиями сериализации.
106 Глава 3. Контракты данных Использование события deserializing В процессе десериализации конструктор не вызывается, поэтому на логическом уровне обработчик события deserializing может рассматриваться как конструк- тор десериализации. Метод предназначен для выполнения любых пользова- тельских действий, предшествующих десериализации, — чаще всего инициали- зации членов классов, не помеченных как поля данных. Любые попытки задать зна- чение членов, помеченных как поля данных, будут напрасными, потому что WCF снова задаст их при десериализации с использованием данных из сообщения. Среди других действий, выполняемых в методе обработки события deserializing, можно отметить задание переменных окружения, выполнение диагностики или вы- дачу сигналов для глобальных событий синхронизации. Использование события deserialized Событие deserialized позволяет контракту данных инициализировать члены, не являющиеся полями данных, с использованием уже десериализированных зна- чений. Листинг 3.5 демонстрирует сказанное: в нем событие используется для инициализации подключения к базе данных. Без обработки события правиль- ная работа контракта данных была бы невозможна. Листинг 3.5. Инициализация несериализуемых ресурсов с использованием события deserialized [DataContract] class MyDataContract { IDbConnectlon m_Connection; [OnDeserialized] void OnDeserlal 1 zed(Streamingcontext context) { m_Connection = new SqlConnect1on(...); } /* Поля данных */ } Иерархия контрактов данных Класс контракта данных может быть субклассом другого класса контракта дан- ных. WCF требует, чтобы для каждого уровня иерархии классов контракт дан- ных был обозначен явно, потому что атрибут DataContract не наследуется: [DataContract] class Contact { [DataMember] public string FirstName; [DataMember] public string LastName;
Иерархия контрактов данных 107 (DataContract] class Customer : Contact I [DataMember] public int OrderNumber I Если на каком-либо уровне иерархии классов будет отсутствовать пометка сериализуемости или контракта данных, при загрузке службы произойдет ис- ключение InvalidDataContract. WCF допускает смешанное использование атри- бутов Serializable и DataContract в иерархии классов: [Serializable] class Contact (...) [DataContract] class Customer : Contact (...) Но обычно атрибут Serializable, если он присутствует, располагается на кор- невом уровне иерархии, потому что новые классы должны использовать атри- бут DataContract. При экспортировании иерархии контрактов данных метадан- ные содержат иерархическую информацию, а при использовании субкласса в контракте службы экспортируются все уровни иерархии классов: [ServiceContract] interface IContactManager ( [Operationcontract] void AddCustomer (Customer customer); // Contact тоже экспортируется ) Известные типы Хотя такие языки, как С#, позволяют использовать субкласс вместо базового класса, для операций WCF это не так. По умолчанию субкласс класса контрак- та данных не может использоваться вместо базового класса. Возьмем следую- щий контракт службы: [ServiceContract] interface IContactManager И Здесь нельзя получать объект Customer: [Operationcontract] void AddContact(Contact contact); 11 Здесь нельзя возвращать объекты Customer: [Operationcontract] Contacts GetContactsO: Предположим, клиент также определяет класс Customer: [DataContract] class Customer : Contact
108 Глава 3. Контракты данных [DataMember] public int OrderNumber; } Следующий фрагмент компилируется, но во время выполнения происходит ошибка: Contract contact = new CustomerO; contact.FIrstName = "Juval": contact.LastName = "Lowy": ContactManagerCHent proxy = new ContactManagerCllento: // Попытка вызова службы завершается неудачей proxy.AddContact(contact); proxy.Closet) Дело в том, что при передаче объекта Customer вместо Contact, как в предыду- щем примере, служба не знает, как десериализовать полученный объект — ей ничего не известно об объекте Customer. Аналогично, при возвращении Customer вместо Contact клиент не будет знать, как десериализовать полученный объект, потому что он знает только о Contact, но не о Customer: ///////////////// Сторона службы ///////////////// [DataContract] class Customer : Contact { [DataMember] public Int OrderNumber: } class CustomerManager : IContactManager { L1st<Customer> m_Customers = new L1st<Customer>(): public Contacts GetContactst) { return m_Customers.ToArray(); } // Продолжение реализации } ///////////////// Сторона клиента ///////////////// ContactManagerCllent proxy = new ContactManagerCllent; // Попытка вызова завершается неудачей Contacts contacts = proxy.GetContacts(): proxy.Close(): Проблема решается явной передачей WCF информации о классе Customer. Для этой цели используется атрибут KnownTypeAttribute, определяемый следую- щим образом: [Attrlbutellsage(AttrlbuteTargets.Struct | AttrlbuteTargets.Cl ass, AllowMultiple = true)] public sealed class KnownTypeAttribute : Attribute { public KnownTypeAttr1bute(Type type): // }
ДЙрархия контрактов данных 109 Атрибут KnownType позволяет назначить допустимые субклассы для кон- тракта данных: ^taContract] ЦЬюилТуре (typeof (Customer)) ] class Contact {...} BfetaContract ] class Customer : Contact (...) На стороне хоста действие атрибута KnownType распространяется на все кон- тракты и операции с использованием базового класса, на все службы и конеч- ные точки, позволяя им принимать субклассы вместо базовых классов. Кроме того, субкласс включается в метаданные, чтобы клиент имел собственное опре- деление субкласса и мог передавать субкласс вместо базового класса. Если кли- ент применит атрибут KnownType к своей копии базового класса, он сможет по- лучать от службы объекты субкласса. Известные типы уровня служб Недостаток атрибута KnownType состоит в том, что он может иметь излишне ши- рокую область действия. WCF также предоставляет атрибут ServiceKnownType- Attribute, определяемый следующим образом: [AttrlbuteUsateCAttributeTargets. Interface | AttributeTargets.Method j AttributeTargets.Class. AllowMultiple = true)] public sealed class ServiceKnownTypeAttribate : Attribute { public ServiceKnownTypeAttribute(Type type); // ... ) В отличие от применения атрибута KnownType к базовому контракту данных, при применении атрибута ServiceKnownType к конкретной операции на стороне службы известный субкласс применяется только к этой операции (по всем под- держиваемым службам): [DataContract] class Contact [DataContract] class Customer : Contact (...) [ServoceContract] interface IContactManager [Operationcontract] [ServiceKnownType(typeof (Customer))]
110 Глава 3. Контракты данных void AddContact(Contact contact); [Operationcontract] Contacts GetContactsO; } Другим операциям субкласс передаваться не может. Если атрибут ServiceKnownType применяется на уровне контракта, все опера- ции этого контракта могут принимать известный субкласс по всем реализациям служб: [ServiceContract] [ServiceKnownType(typeof(Customer))] interface IContactManager { [Operationcontract] void AddContact(Contact contact) [Operationcontract] Contactp[] GetContactsO; } ВНИМАНИЕ -------------------------------------------------------------------------------- He применяйте атрибут ServiceKnownType к самому классу службы. Хотя такой код компилируется, он возможен только в том случае, если контракт службы не определяется в виде интерфейса — а по- ступать подобным образом я настоятельно не рекомендую. Если применить атрибут ServiceKnown- Type к классу службы при отдельном определении контракта, он ни на что не повлияет. Независимо от того, применяется ли атрибут ServiceKnownType на уровне операции или на уровне контракта, экспортированные метаданные и сгенериро- ванный посредник не содержат информации о нем и включают атрибут Known- Type только для базового класса. Например, для следующего определения на стороне службы: [ServiceContract] [Servi ceKnownType(typeof(Customer))] interface IContactManager импортированное определение будет выглядеть так: [DataContract] [KnownType(typeof(Customer))] class Contact {...} [DataContract] class Customer : Contact {...} [ServiceContract] interface IContactManager Вы можете вручную отредактировать класс посредника на стороне клиента, чтобы он правильно отражал семантику стороны службы — удалите атрибут KnownType из базового класса и примените атрибут ServiceKnownType на соответ- ствующем уровне контракта.
Иерархия контрактов данных 111 Определение нескольких известных типов Оба атрибута, KnownType и ServiceKnownType, могут применяться многократно для передачи WCF информации о необходимом количестве известных ти- пов: [DataContract] class Contact (••) [DataContract] class Customer : Contact (•••I [DataContract] class Person : Contact (...) [ServiceContract] [Servi ceKnownTy pe (ty peof (Customer)) ] [Servi ceKnownType (typeof (Person)) ] interface IContactManager (...) Настройка известных типов Главный недостаток атрибутов известных типов заключается в том, что служба или клиент должны заранее знать все возможные субклассы, которые могут быть использованы другой стороной. При добавлении нового субкласса прихо- дится вносить изменения в код, перекомпилировать и развертывать его заново. Для решения этой проблемы WCF позволяет настраивать известные типы в конфигурационном файле службы или клиента, как показано в листинге 3.6. В файле должно задаваться не только имя типа, но и полное имя вмещающей сборки. Листинг 3.6. Известные типы в конфигурационном файле <system. runtime, seri al ization> <dataContractSeri al i zer> <declaredTypes> <add type - "Contact.Host.Version*!.0.0.0,Culture=neutral, PublicKeyToken=null"> <knownType type = "Customer,MyClassLibrary,Version=l.0.0.0, Culture=neutral,PublicKeyToken=null”> </add> </declaredTypes> </dataCont ractSer i a 1 i zer> </system. runtime. seri a 1 i zat i on> Интересная подробность: объявление известного типа в конфигурационном файле — единственный способ добавления известного типа, внутреннего по от- ношению к другой сборке.
112 Глава 3. Контракты данных Объект и интерфейсы Базовым типом структуры или класса контракта данных может быть интер- фейс: interface IContact { string FirstName {get;set:} string LastName {get:set:} } [DataContract] Class Contact : IContact {•} Базовый интерфейс может использоваться в контракте службы или в полях контракта данных, при условии, что атрибут ServiceKnownType будет использо- ваться для обозначения фактического типа данных: [ServiceContract] [Servi ceKnownT уре(typeof(Contact))] interface IContactManager { [Operationcontract] void AddContact(IContact contact): [Operationcontract] IContact[] GetContactsO: } Атрибут KnownType не может применяться к базовому интерфейсу, потому что сам интерфейс не будет включен в экспортированные метаданные. Вместо этого экспортированный контракт службы базируется на object, а структура или субкласс контракта данных включается без наследования: // Импортированные определения: [DataContract] class Contact {...} [ServiceContract] public interface IContactManager { [Operationcontract] [Servi ceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] void AddContact(IContact contact): [Operationcontract] [Servi ceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] object[] GetContactsO: } В импортированном определении атрибут ServiceKnownType всегда применя- ется на уровне операции, даже если изначально он определялся на уровне кон-
Иерархия контрактов данных 113 тракта. Кроме того, каждая операция включает объединение всех атрибутов ServiceKnownType, необходимых для всех операций. Вы можете вручную перера- ботать импортированное определение, чтобы в нем остались только необходи- мые атрибуты ServiceKnownType: [DataContract] class Contact [ServiceContract] public interface IContactManager ( [Operationcontract] [ServiceKnownType(typeof (Contact)) ] void AddContact(object contact); [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] object[] GetContactsO; Если на клиентской стороне имеется определение базового интерфейса, оно может использоваться вместо object как дополнительная мера безопасности ти- пов - при условии, что в контракт данных будет добавлено наследование от ин- терфейса: [DataContract] class Contact : IContact [ServiceContract] public interface IContactManager [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] void AddContact(IContact contact); [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] IContact[] GetContactsO; Тем не менее в импортированном контракте заменить object конкретным типом контракта данных невозможно, потому что эти типы стали несовмести- мыми: // Недействительный контракт на стороне клиента [ServiceContract] public interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO;
114 Глава 3. Контракты данных Эквивалентность контрактов данных Два контракта данных считаются эквивалентными, если они обладают одинако- вым канальным представлением. Такая ситуация возможна при определении одного типа (но не обязательно в одинаковых версиях), или если два контракта данных относятся к двум разным типам с одинаковыми именами контракта и членов. Эквивалентные контракты данных взаимозаменяемы: WCF позволя- ет любой службе, определенной с одним контрактом данных, работать с эквива- лентным контрактом данных. Самым распространенным способом определения эквивалентного контракта данных является связывание двух контрактов данных посредством свойства Name атрибута DataContract или DataMember. В случае атрибута DataContract свой- ство Name по умолчанию совпадает с именем типа, поэтому следующие два оп- ределения тождественны: [DataContract] struct Contact {...} IDataContract(Name - "Contact")] struct Contact {...} В действительности полное имя контракта данных всегда включает про- странство имен, но как было показано ранее, пространство имен можно изме- нить. В случае атрибута DataMember свойство Name по умолчанию содержит имя поля, поэтому следующие два определения тождественны: [DataMember] public string FirstName: [DataMember(Name = "FirstName")] public string FirstName: Присваивая альтернативные имена контракту и полям, можно сгенериро- вать эквивалентный контракт данных на базе другого типа. Например, следующие два контракта данных эквивалентны: [DataContract] struct Contact { [DataMember] public string FirstName: [DataMember] public string LastName: } [DataContract(Name - "Contact")] struct Person { [DataMember(Name « "FirstName")] public string Name: [DataMember(Name e "LastName")]
Эквивалентность контрактов данных 115 public string Surname: ) Кроме тождественности имен, должны совпадать типы полей данных. ПРИМЕЧАНИЕ-------------------------------------------------------------- Класс и структура, поддерживающие одинаковый контракт данных, взаимозаменяемы. Порядок сериализации Эквивалентные контракты данных должны сериализовать и десериализовать свои поля данных в одинаковом порядке. По умолчанию порядок сериализации внутри типа определяется алфавитным порядком, а в иерархиях классов ис- пользуется порядок «сверху вниз». При несоответствии порядка сериализации поля инициализируются значениями по умолчанию. Например, при сериализа- ции экземпляра класса Customer, определяемого следующим образом: [DataContract] struct Contact [DataMember] public string FirstName: [DataMember] public string LastName: ) [DataContract] class Customer : Contact f [DataMember] public int CustomerNumber: ) поля будут сериализованы в следующем порядке: FirstName, LastName, Customer- Number. Но при этом возникает одна проблема: объединение иерархии контрактов данных с псевдонимами контрактов и полей может привести к нарушению по- рядка сериализации. Например, следующий контракт данных уже не эквива- лентен контракту Customer: [DataContract(Name - "Customer")] public class Person [DataMember (Name = "FirstName")] public string Name: [DataMember(Name - "LastName")] public string Surname: [DataMember] public int CustomerNumber: ) поскольку сериализация выполняется в порядке CustomerNumber, FirstName, LastName. Для разрешения конфликта необходимо передать WCF порядок
116 Глава 3. Контракты данных сериализации, задавая свойство Order атрибута DataMember. Свойство Order по умолчанию равно - 1 (признак стандартного порядка сериализации в WCF), но ему можно задать значения, определяющие нужный порядок: [DataContract(Name = "Customer")] public class Person { [DataMember(Name = "FirstName".Order - 1)] public string Name; [DataMember(Name = "LastName".Order 2)] public string Surname; [DataMember(Order = 3)] public int CustomerNumber; } Изменение версий1 Служба должна быть по возможности отделена от клиента, особенно в том, что касается версии и технологии. Любая версия клиента должна быть способна ис- пользовать любую версию службы, не прибегая к проверке номера версии (на- пример, по данным из сборки), потому что эта информация специфична для .NET. Когда клиент и служба используют общий контракт данных, очень важно предоставить возможность клиенту и службе развивать свои версии контракта данных по отдельности, то есть независимо друг от друга. Чтобы подобное раз- деление стало возможным, WCF необходимо обеспечить как прямую, так и об- ратную совместимость без обмена информацией о типах или версиях. Сущест- вуют три основных сценария изменения версий: О появление новых полей; О исключение существующих полей; О передачи с промежуточным изменением версии, когда новая версия переда- ется старой версии, а потом принимается от нее (требует как прямой, так и обратной совместимости). По умолчанию контракты данных терпимы к изменению версий, а несовмес- тимости игнорируются без выдачи ошибок. Появление новых полей Самым распространенным изменением, выполняемым с контрактами данных, является добавление новых полей на одной из сторон и последующая отправка нового контракта другой стороне. DataContractSerializer просто игнорирует новые 1 Versioning. Привычные переводы: «контроль версий» и «отслеживание версий» здесь не годятся - ведь речь как раз и идет об «отвязке» клиента от службы с независимым изменением версий. — Примеч. перев.
Изменение версий 117 поля при десериализации типа. В результате как служба, так и клиент могут принимать данные с новыми полями, не входящими в исходный контракт. На- пример, служба может быть построена на базе следующего контракта данных: [DataContract] struct Contact ( [DataMember] public string FirstName: [DataMember] public string LastName: ) При этом клиент может передавать ей информацию, соответствующую сле- дующему контракту данных: [DataContract] struct Contact f [DataMember] public string FirstName: [DataMember] public string LastName; [DataHenber] public string Address; ) Подобное добавление новых полей, игнорируемых при десериализации, на- рушает совместимость схемы контракта данных, так как служба (или клиент), совместимая с одной схемой, ни с того ни с сего оказывается совместима с но- вой схемой. Исключение существующих полей По умолчанию WCF позволяет любой из сторон исключать поля из контракта данных. Данные сериализуются без отсутствующих полей и отправляются дру- гой стороне, которая ожидает получить отсутствующие поля. Намеренное уда- ление полей встречается довольно редко, более распространен другой сцена- рий - когда клиент, написанный для старого определения контракта данных, взаимодействует со службой, написанной для нового определения того же кон- тракта, содержащего дополнительные поля. Когда DataContractSerializer на сто- роне получателя не находит в сообщении информацию, необходимую для десе- риализации новых полей, он задает им значения по умолчанию, то есть null для ссылочных типов или нулевой заполнитель для структурных типов. Все выгля- дит так, как если бы отправляющая сторона вообще не инициализировала эти поля. Такая политика дает службе возможность принимать данные с отсутст- вующими полями или возвращать клиенту данные с отсутствующими полями. Эта возможность продемонстрирована в листинге 3.7.
118 Глава 3. Контракты данных Листинг 3-7- Отсутствующие поля инициализируются значениями по умолчанию ///////////////// Сторона службы ///////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; [DataMember] public string Address: } [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact); ) class ContactManager : IContactManager { public void AddContact(Contact contact) { Trace.WrlteLlneC'First name - ” + contact.FlrstName); Trace.WrlteLlneCLast name = " + contact.LastName); Trace.WrlteLlneC'Address - " + (contact.Address ?? "Missing")): } } ///////////////// Сторона клиента ///////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; } Contact contact = new ContactO; contact.FirstName - "Juwal"; contact.LastName « "Lowy"; ContactManagerCllent proxy - new ContactManagerCl1ent(); proxy.AddContact(contact); proxy.Close();
Изменение версий 119 Результат выполнения этого кода выглядит так: First name = Juwal Last name - Lowy Address • Missing Это объясняется тем, что служба получила null в качестве значения поля Address и вывела в трассировке строку Missing. Использование события OnDeserializing Событие OnDeserializing может использоваться для инициализации отсутствую- щих полей данных на основании некой локальной эвристики. Если сообщение содержит данные, они заменят величины, заданные в событии OnDeserializing, а если нет — полю будет присвоено значение, отличное от значения по умолча- нию: [DataContract] struct Contact [DataMember] public string FirstName; [DataMember] public string LastName: [DataMember] public string Address; [OnDeserializing] void OnDeserializing(StreamingContext context) ( Address - "Some default address"; ) } В этом случае результат выполнения листинга 3.7 будет выглядеть так: First name * Juwal Last name • Lowy Address “ Some default address Обязательные поля В отличие от игнорирования новых полей, которое в большинстве случаев без- вредно, стандартная обработка отсутствующих полей с большой вероятностью вызовет сбой в цепочке вызовов на принимающей стороне, потому что отсутст- вующие поля могут быть необходимы для правильного функционирования. Это может привести к катастрофическим последствиям. Задавая свойство IsRe- quired атрибута DatMember равным true, вы приказываете WCF отказаться от вы- зова операции при отсутствии полей данных: [DataContract] struct Contact [DataMember] public string FirstName;
120 Глава 3. Контракты данных [DataMember] public string LastName; [DataMember(IsRequired - true)] public string Address; } По умолчанию свойство IsRequired равно false, то есть отсутствующие поля игнорируются. Если DataContractSerializer на принимающей стороне не находит информацию, необходимую для десериализации поля, помеченного в сообще- нии как обязательное, вызов отменяется с выдачей исключения NetDispatcher- FaultException на отправляющей стороне. Если бы в контракте данных на сторо- не службы в листинге 3.7 поле Address было помечено как обязательное, то вызов не достиг бы службы. «Обязательность» поля публикуется в метаданных службы, а при импортировании на сторону клиента в сгенерированном файле с определением посредника будет включено правильное значение этого свойства. Как клиент, так и служба могут пометить часть своих полей данных (или все поля) как обязательные, полностью независимо друг от друга. Чем больше по- лей помечено как обязательные, тем надежнее взаимодействие со службой или клиентом, но за счет гибкости и возможностей изменения версий. Когда контракт данных с обязательным новым полем отправляется прини- мающей стороне, не подозревающей о существовании такого поля, такой вызов вполне допустим. Иначе говоря, даже если свойство IsRequired задается равным true для нового поля контракта данных версии 2 (V2), V2 можно отправить сто- роне, которая ожидает получить контракт версии 1 (VI), не содержащий такого поля. Новое поле будет попросту проигнорировано. Свойство IsRequired дейст- вует только при отсутствии поля на стороне, осведомленной о существова- нии V2. Допустим, VI не знает о добавлении нового поля в V2; в табл. 3.1 приве- дены возможные комбинации разрешенных или запрещенных взаимодействий в зависимости от задействованных версий и значения свойства IsRequired. Таблица 3.1. Взаимодействие контрактов данных с обязательными полями IsRequired От VI к V2 От V2 к VI false Да Да true Нет Да Одним из интересных примеров применения обязательных полей являются сериализуемые типы. Поскольку сериализуемые типы по умолчанию не допус- кают отсутствие полей, при их экспортировании в итоговом контракте данных все поля данных помечаются как обязательные. Например, у следующего опре- деления Contact: [Serializable] struct Contact { public string FirstName; public string LastName; }
Изменение версий 121 представление метаданных будет выглядеть так: [DataContract] struct Contact [DataMember(IsRequired - true)] public string FirstName {get;set;} [DataMember(IsRequired « true)] public string LastName {get:set:} Чтобы пометить поле как необязательное, примените к нему атрибут OptionalField. Например, у следующего определения Contact: [Serializable] struct Contact public string FirstName; [OptionalField] public string LastName: ) представление метаданных принимает вид [DataContract] struct Contact { [DataMember (I sRequi red = true)] public string FirstName {get;set:} [DataMember] public string LastName {get:set:} I Передача с промежуточным изменением версии Методы обеспечения совместимости, рассматривавшиеся до настоящего мо- мента (игнорирование новых полей и задание значений по умолчанию для от- сутствующих полей), не оптимальны. Они делают возможными точечные взаи- модействия между клиентом и службой, но не поддерживают более широкого сценария сквозной передачи. Рассмотрим два примера взаимодействия, пока- занных на рис. 3.4. В первом взаимодействии клиент, построенный на базе нового контракта данных с новыми полями, передает этот контракт службе А, которой о новых полях ничего не известно. Затем служба А передает данные службе В, поддер- живающей новый контракт. Однако данные, передаваемые от А к В, не содер- жат новых полей — эти поля были потеряны при десериализации, потому что они не входят в контракт данных службы А. Вторая ситуация выглядит анало- гично: клиент, поддерживающий новый контракт данных с новыми полями,
122 Глава 3. Контракты данных передает данные службе С; службе известен только старый контракт, не содер- жащий новых полей. В данных, возвращаемых службой С клиенту, новые поля отсутствуют. Рис. 3.4. Передача с промежуточным изменением версии может привести к частичной потере данных Подобные взаимодействия вида «новый-старый-новый» называются переда- чами с промежуточным изменением версии. В WCF предусмотрена возмож- ность их обработки. Служба (или клиент) с поддержкой старого контракта про- сто передает данные состояния новых полей без их потери. Проблема лишь в том, как выполнить сериализацию и десериализацию неизвестных полей при отсутствии схемы и где хранить их между вызовами. Для решения проблемы тип контракта данных реализует интерфейс lExtensibleDataObject, определяемый следующим образом: public Interface lExtensibleDataObject { Extensi onDataObject Extens i onData {get;set:} } lExtensibleDataObject определяет единственное свойство типа Extension Data- Object. Точное определение ExtensionDataObject несущественно, потому что раз- работчику никогда не приходится работать с ним напрямую. ExtensionDataObject представляет собой внутренний связанный список, содержащий ссылки на object и информацию о типах; именно здесь сохраняются неизвестные поля дан- ных. Если тип контракта данных поддерживает lExtensibleDataObject, то неопо- знанные новые поля, обнаруженные в сообщении, десериализуются и сохраня- ются в этом списке. Когда служба (или клиент) передает при вызове тип старого контракта данных, который теперь содержит неизвестные поля данных в ExtensionDataObject, неизвестные поля сериализуются в сообщение. Если при- нимающая сторона знает о существовании нового контракта данных, она полу- чает полный набор данных без отсутствующих полей. Реализация и использо- вание lExtensibleDataObject продемонстрированы в листинге 3.8. Как видите, реализация весьма прямолинейна — в класс просто добавляется свойство Exten- sionDataObject для чтения/записи соответствующей переменной.
Изменение версий 123 Листинг 3.8. Реализация JExtensibleDataObject [DataContract] class Contact : lExtensibleDataObject ( ExtenslonDataObject m_ExtensionData: public ExtenslonDataObject ExtensionData { get { return mExtensionData: } set { m_ExtensionData - value: } } [DataMember] public string FirstName; [DataMember] public string LastName; ) Совместимость схем Хотя реализация lExtensibleDataObject делает возможной передачу с промежу- точным изменением версии, у нее имеется и обратная сторона: служба, совмес- тимая с одной схемой контракта данных, способна успешно взаимодействовать с другой службой, рассчитанной на другую схему контракта данных. В некото- рых специфических случаях служба может принять решение о запрете переда- чи с промежуточным изменением версии и потребовать строгого соблюдения своей версии контракта данных в нисходящих службах. Служба приказывает WCF переопределить механизм обработки неизвестных полей в lExtensibleData- Object и игнорировать их даже в том случае, если контракт данных поддержива- ет lExtensibleDataObject. Задача решается посредством использования атрибута ServiceBehavior. Аспекты поведения и этот атрибут будут подробно описаны в следующей главе, а пока достаточно сказать, что атрибут ServiceBehavior содер- жит логическое свойство IgnoreExtensionDataObject, определяемое следующим образом: [Attri butells age (Attri buteTargets .Class)] public sealed class ServIceBehaviorAttribute ; Attribute.... public bool IgnoreExtensionDataObject (get;set;} // ... I По умолчанию свойство IgnoreExtensionDataObject равно false. Если задать его равным true, все неизвестные поля данных во всех контрактах данных, исполь- зуемых службой, всегда будут игнорироваться:
124 Глава 3. Контракты данных [ServiceBehaviordgnoreExtensionDataObject - true)] class ContactManager : IContactManager {•••} Если контракт данных импортируется посредством SvcUtil или Visual Stu- dio, сгенерированный тип контракта данных всегда поддерживает lExtensible- DataObject даже если в исходном контракте данных этот интерфейс не поддер- живался. Полагаю, в своих контрактах данных вам всегда следует реализовывать lExtensibleDataObject, и по возможности избегать установки IgnoreExtensionData- Object. Интерфейс lExtensibleDataObject способствует изоляции службы от нис- ходящих служб, давая возможность ей развиваться отдельно от них. ПРИМЕЧАНИЕ----------------------------------------------------------------------------- При работе с известными типами реализация lExtensibleDataObject не обязательна, потому что суб- класс всегда десериализуется без потери данных. Перечисления Перечисления всегда сериализуемы по определению. При определении нового перечисления применять к нему атрибут DataContract не обязательно, и оно мо- жет свободно использоваться в контракте данных, как показано в листинге 3.9. Все значения, входящие в перечисление, неявно включаются в контракт дан- ных. Листинг 3.9. Перечисление в контакте данных enum ContactType { Customer. Vendor. Partner } [DataContract] struct Contact { [DataMember] public ContactType ContactType; [DataMember] public string FirstName; [DataMember] public string LastName; } Если некоторые элементы перечисления должны быть исключены из кон- тракта данных, перечисление необходимо снабдить атрибутом DataContract. Да- лее все элементы, которые должны войти в контракт, явно помечаются атрибу- том EnumMemberAttribute. Атрибут EnumMember определяется так: [AttributellsageCAttributeTargets.Field.Inherited - false)] public sealed class EnumMemberAttribute ; Attribute
Перечисления 125 public string Value (get;set:} ) Любой элемент перечисления, не помеченный атрибутом EnumMember, не вой- дет в контракт данных. Например, для перечисления [DataContract] enum ContactType ( [EnumMember] Customer, [EnumMember] Vendor. // He войдет в контракт данных Partner ) канальное представление будет выглядеть так: enum ContactType ( Customer. Vendor I Атрибут EnumMember также применяется для назначения псевдонимов неко- торых элементов перечисления в соответствии с уже существующим контрак- том данных; для этой цели используется свойство Value. Например, для пере- числения [DataContract] enum ContactType ( [EnumMember (Value = "MyCustomer")] Customer. [EnumMember] Vendor. // He войдет в контракт данных Partner ) будет создано следующее канальное представление: enum ContactType ( MyCustomer. Vendor. Partner I Действие атрибута EnumMember локально по отношению к той стороне, кото- рая его использует. При публикации метаданных (или при определении их на стороне клиента) в итоговом контракте данных этот атрибут не встречается, и используется только окончательный результат.
126 Глава 3. Контракты данных Делегаты и контракты данных Все определения делегатов компилируются в сериализуемые классы, поэтому теоретически делегаты могут входить в типы контрактов данных в виде пере- менных: [DataContract] class MyDataContract { [DataMember] public EventHandler MyEvent; } и даже событий (обратите внимание на квалификатор field): [DataContract] class MyDataContract { [f1eld:DataMember] public event EventHandler MyEvent; } На практике импортированный контракт будет содержать недействительное определение делегата. Хотя определение можно подправить вручную, более серьезная проблема заключается в том, что при сериализации объекта с пере- менной-делегатом также будет сериализован внутренний список вызова делега- тов. Как правило, для служб и клиентов такой эффект нежелателен, потому что точная структура списка является локальной для клиента или службы и не должна выходить за границу службы. Кроме того, нет гарантий, что объекты во внутреннем списке сериализуемы или имеют допустимые контракты данных. Соответственно, в одних случаях сериализация сработает, в других — нет. Самое простое решение проблемы — не применять атрибут DataMember к де- легатам. Если контракт данных является сериализуемым типом, делегата сле- дует явно исключить из него: [Serializable] public class MyClass { [NonSeri all zed] EventHandler m_MyEvent: } Наборы данных и таблицы Одну из самых распространенных категорий контрактов данных, передаваемых между клиентами и службами, составляют данные, полученные из базы данных (или предназначенные для сохранения в ней). В .NET взаимодействия с базами данных традиционно осуществляются через типы наборов и таблиц данных ADO.NET. Приложения могут работать с базовыми типами DataSet и DataTable или же сгенерировать типизированные субклассы средствами доступа к дан- ным VisuaX Studio.
Наборы данных и таблицы 127 Базовые типы DataSet и DataTable сериализуемы и помечены атрибутом Serializable: [Serializable] public class DataSet : ... (...) [Serializable] public class DataTable : ... (...) Следовательно, мы можем определять допустимые контракты служб, кото- рые принимают и возвращают таблицы и наборы данных: [DataContract] struct Contact (...) [DataContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact): [Operationcontract] void AddContacts(DataTable contacts): [Operationcontract] DataTable GetContactsO; При импортировании определения этого контракта службы сгенерирован- ный файл посредника будет содержать определение контракта данных Data- Table - только схему DataTable, без какого-либо кода. Вы можете удалить это оп- ределение из файла и воспользоваться ADO.NET. Также в контрактах можно использовать типизированные субклассы DataSet и DataTable. Допустим, имеется таблица базы данных с именем ContactsDataTable, в которой присутствуют столбцы FirstName, LastName и т. д. Visual Studio может построить класс набора данных MyDataSet, который содержит вложенный класс ContactsDataTable, класс записи и класс адаптера данных (листинг 3.10). Листинг 3.10. Типизированные классы набора и таблицы данных [Serializable] public partial class MyDataSet : DataSet ( public ContactsDataTable Contacts (get:) [Serializable] public partial class ContactsDataTable : DataTable. lEnumerable ( public void AddContactsRow(ContactsRow row); public ContactsRow AddContactsRow(string FirstName.string LastName): // ... I
128 Глава 3. Контракты данных public partial class ContactsRow : DataRow { public string FirstName {get;set;} public string LastName {get;set;} // } // } public partial class ContactsTableAdapter : Component { public virtual MyDataSet.ContactsDataTable GetDataO; // } Типизованная таблица данных может использоваться в контракте службы: [DataContract] struct Contact {...} [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] void AddContacts(MyDataSet.ContactsDataTable contacts); [Operationcontract] MyDataSet.ContactsDataTable GetContacts(); } ВНИМАНИЕ ---------------------------------------------------------------------------------- Класс записи данных не сериализуем, поэтому он (а также его типизованные субклассы) не может использоваться в операциях: // Недопустимое определение [Operationcontract] void AddContact(MyDataSet.ContactRow contact); Типизованная таблица данных является частью публикуемых метаданных службы. При импортировании на стороне клиента SvcUtil и Visual Studio вос- создают типизованную таблицу данных, а файл посредника включает не только контракт данных, но и сам код. Если у клиента уже имеется локальное опреде- ление типизованной таблицы, определение можно исключить из файла посред- ника. Массивы вместо таблиц Благодаря инструментарию ADO.NET и Visual Studio использование DataSet и Data Table (и их типизованных субклассов) в клиентах и службах WCF стано-
Наборы данных и таблицы 129 вится делом тривиальным. Правда, эти типы доступа к данным относятся к спе- цифике .NET. Хотя они сериализуемы, итоговая схема контракта данных полу- чается настолько сложной, что попытки организовать взаимодействие с ней на других платформах нельзя считать практичным решением. Использование таб- лиц и наборов данных имеет и другие недостатки: в частности, оно способству- ет раскрытию внутренней структуры данных, а будущие изменения в схеме базы данных могут отразиться на клиентах. Если передача таблицы данных в границах приложения может быть приемлема, выход таблицы данных за гра- ницы приложения или общедоступной службы вряд ли можно посчитать удач- ным решением. В общем случае доступ следует предоставлять к операциям с данными, а не к самим данным. Если же возникнет необходимость в передаче самих данных, воспользуйтесь нейтральной структурой данных — например, массивом. Для упрощения задачи преобразования таблицы данных в массив вы можете воспользоваться моим классом DataTableHelper, который определяется следующим образом: public static class DataTableHelper ( public static T[] ToArray<R.T>(DataTable table. Converters, T>. converter) where R : DataRow; Для работы DataTableHelper необходим лишь преобразователь записи данных таблицы в контракт данных. Кроме того, DataTableHelper обеспечивает частич- ную проверку типов на стадии компиляции и выполнения. Пример использова- ния DataTableHelper представлен в листинге 3.11. Листинг 3.11. Использование DataTableHelper [DataContract] struct Contact I [DataMember] public string FirstName: [DataMember] public string LastName: ) [ServiceContract] interface IContactManager I [Operationcontract] Contacts GetContactsO: class ContactManager : IContactManager ( public Contactf] GetContactsO ( ContactsTableAdapter adapter = new ContactsTableAdapter(): MyDataSet.ContactsDataTable contacsTable = adapter,GetData(): Converter<MyDataSet.ContactsRow.Contact> converter: Converter = delegate(MyDataSet.ContactsRow row) продолжение &
130 Глава 3. Контракты данных { Contact contact - new ContactO; contact.FirstName row.FirstName; contact.LastName - row.LastName; return contact: }; return DataTableHelper .ToAr ray (contactsTable,converter); } // } В листинге 3.11 метод GetContactsO использует типизованный адаптер табли- цы ContactsTableAdapter (см. листинг 3.10) для получения записей из базы дан- ных в форме типизованной таблицы MyDataSet.ContactsDataTable. Затем GetCon- tactsO определяет анонимный метод, преобразующий экземпляр типизованной записи данных MyDataSet.ContactsRow в экземпляр Contact. Далее GetContactsO вызывает метод DataTableHapler.ToArray(), передавая ему таблицу и преобразова- тель. Реализация DataTableHelper.ToArray() приведена в листинге 3.12. Листинг 3.12. Класс DataTableHelper public static class DataTableHelper { public static T[] ToArray<R.T>(DataTable table,Converters, T> converter) where R : DataRow { if(table.Rows.Count == 0) { return new T[]{}; } // Проверить T на [DataContract] или [Serializable] Debug.Assert(IsDataContract(typeof(T)) || typeof(T).IsSerializable); // Убедиться в том, что таблица содержит правильные записи Debug.Assert(MatchingTableRow<R>(table)): return Col 1ection.UnsafeToArray(table.Rows.converter); } static bool IsDataContracttType type) { object[] attributes = type.GetCustomAttributes(typeof(DataContractAttribute),false); return attributes.Length == 1; } static bool MatchingTableRow<R>(DataTable table) { if(table.Rows.Count — 0) { return true: } return table.Rows[0] is R; } }
Обобщенные типы 131 В сущности, метод DataTableHelper.ToArray() всего лишь вызывает преобразо- ватель для каждой записи в таблице при помощи вспомогательного класса myCollection. Каждая запись преобразуется в объект Contact, а полученный мас- сив возвращается методом. DataTableHelper также обеспечивает некоторый уро- вень безопасности типов. Во время компиляции для «параметра типа» R он убе- ждается в том, что этот тип представляет запись данных. На стадии выполнения метод ТоАггау() для пустой таблицы возвращает пустой массив. Метод также проверяет, что параметр типа Т помечен атрибутом DataContract или Serializable. Проверка атрибута DataContract осуществляется при помощи вспомогательного метода IsDataContract(), использующего рефлексию для поиска атрибута. Для атрибута Serializable метод проверяет, установлен ли для типа флаг IsSerializable. Напоследок ТоАггау() проверяет, содержит ли предоставленная таблица записи, определяемые параметром типа R. Для этого используется вспомогательный ме- тод MatchingTableRow(), который получает первую запись и проверяет ее тип. Обобщенные типы Не допускается определение контрактов WCF, зависящих от обобщенных пара- метров типа. Обобщенные типы (generics) относятся к специфике .NET, и их передача нарушила бы служебно-ориентированную природу .NET. Впрочем, использование ограниченных обобщенных типов (bounded generic types) в кон- трактах данных возможно — при условии, что в контракте службы будут указа- ны параметры типов, обладающие допустимыми контрактами данных, как по- казано в листинге 3.13. Листинг 3.13. Использование ограниченных обобщенных типов [DataContract] class MyClass<T> [DataMember] public T m_MyMember; [ServiceContract] interface IMyContract [Operationcontract] void MyMethod(MyClass<int> obj); ) При импортировании метаданных контракта из листинга 3.13 все параметры типов в импортированных типах заменяются конкретными типами, а контракт данных переименовывается по схеме исходное имя>О^<имена параметров типов><хеш> Для определений из листинга 3.13 импортированный контракт данных и контракт службы будут выглядеть так: [DataContract] class MyCl assOflnt
132 Глава 3. Контракты данных ( int MyMemberField; [DataMember] public int m_MyMember { get ( return MyMemberField; } set { MyMemberField = value; } } } [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodCMyClassOflnt obj): } Если бы вместо int в контракте службы использовался пользовательский тип SomeClass: [DataContract] class SomeClass {•••} [DataContract] class MyClass<T> {...} [Operationcontract] void MyMethod(MyClass<SomeClass> obj); то экспортированный контракт данных мог бы выглядеть примерно так: [DataContract] class SomeClass {...} [DataContract] class MyClass0fSomeClassMTRdqN6P {...} [Operationcontract] void MyMethod(MyClass0fSomeClassMTRdqN6P obj); Здесь MTRdqN6P — некий квазиуникальный хеш обобщенного параметра типа и вмещающего пространства имен. Для разных контрактов данных и про- странств имен генерируются разные хеши. Присутствие хеша снижает вероят- ность конфликта с другим контрактом данных, использующим другой параметр типа с тем же именем. При использовании примитивов в качестве обобщенных параметров типов хеш не генерируется.
Обобщенные типы 133 В большинстве случаев хеш оказывается лишней — и притом громоздкой — предосторожностью. Чтобы переименовать экспортированный контракт дан- ных, достаточно задать другое имя свойству Name контракта данных. Например, для следующего контракта данных на стороне службы: [DataContract] class SomeClass (•••} [DataContract(Name - "MyClass")] class MyClass<T> (...) [Operationcontract] void MyMethod(MyClass<SomeClass> obj): экспортированный контракт будет выглядеть так: [DataContract] class SomeClass (...) [DataContract] class MyClass (...) [Operationcontract] void MyMethodtMyClass obj); Впрочем, если вы хотите использовать схему с объединением имени обоб- щенного параметра типа и имени контракта данных, используйте директиву {<номер>}, где номер — порядковый номер параметра типа. Например, для сле- дующего определения на стороне службы: [DataContract] class SomeClass (...) [DataContract (Name = ”MyClassOf{O}{l}")] class MyClass<T.U> (...) [Operationcontract] void MyMethod(MyClass<SomeClass.int> obj); будет получено следующее экспортированное определение: [DataContract] class SomeClass (...) [DataContract] class MyClassOfSomeClassint (•••) [OperationContract(...)] void MyMethod(MyClassOfSomeClass1nt obj);
134 Глава 3. Контракты данных ВНИМАНИЕ ------------------------------------------------------------- Количество параметров типа не проверяется на стадии компиляции. Несовпадение приведет к ис- ключению времени выполнения. Наконец, присоединение # после номера генерирует уникальный хеш. На- пример, для следующего определения контракта данных: [DataContract] class SomeClass [DataContract(Name = "MyClassOf{0}{#}")] class MyClass<T> [Operationcontract] void MyMethod(MyClass<SomeClass> obj): будет получено следующее экспортированное определение: [DataContract] class SomeClass [DataContract] class MyClass0fSomeClassMTRdqN6P {••} [0perat1onContract(...)] void MyMethod(MyClass0fSomeClassMTRdqN6P obj); Коллекции В .NET коллекцией называется любой тип, поддерживающий интерфейсы IEnu- merable или IEnumerable<T>. Все встроенные коллекции .NET — массивы, списки и стеки — поддерживают эти интерфейсы. Контракт данных может включать коллекцию как поле данных, или контракт службы может определять опера- цию, которая взаимодействует с коллекцией напрямую. Поскольку коллекции .NET относятся к специфике .NET, WCF не может напрямую использовать их в метаданных службы. Тем не менее коллекции чрезвычайно полезны, поэтому в WCF были определены специальные правила их маршалинга. При определении операции службы, использующей любой из следующих интерфейсов коллекций: IEnumerable<T>, IList<T> и ICoLLection<T>, в канальном представлении всегда будет задействован массив. Например, следующее опре- деление контракта службы и реализация: [ServiceContract] Interface IContactManager { [Operationcontract] IEnumerable<Contact> GetContacts();
class ContactManager : IContactManager List<Contact> m_Contacts = new L1st<Contact>(); public lEnumerable<Contact> GetContactsO ( return m_Contacts: ) ) экспортируются в следующем виде: [ServiceContract] interface IContactManager ( [Operationcontract] Contactt] GetContactsO; ) Реальные коллекции Если коллекция, включенная в контракт, является реальной (concrete)(B отли- чие от интерфейсов) и сериализуемой (то есть помеченной атрибутом Seria- lizable, но не атрибутом DataContract), WCF может автоматически нормализо- вать коллекцию в массив с соответствующим типом элементов. Для этого коллекция должна содержать метод Add() с одной из следующих сигнатур: public void Add (object obj); // Коллекция использует lEnumerable public void Add(T item): // Коллекция использует lEnumerable<T> Например, возьмем следующее определение контракта: [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] L1st<Contact> GetContactsO: } Класс списка определяется следующим образом: public interface ICol 1 ection<T> : lEnumerable<T> (...) public interface IList<T> : ICollection<T> (••) [Serializable] public class List<T> : IList<T> ( public void Add(T item); //... ) Коллекция является допустимой и содержит метод Add(), поэтому итоговое канальное представление контракта будет выглядеть так:
136 Глава 3. Контракты данных [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contacts GetContactsO: } Итак, List<Contacts> маршализируется как Contact[]. При этом служба может возвращать List<Contacts>, но клиент будет взаимодействовать с массивом, как показано в листинге 3.14. Листинг 3.14. Маршалинг списка как массива ///////////////// Сторона службы ///////////////// [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] List<Contact> GetContactsO: } // Реализация службы class ContactManager : IContactManager { L1st<Contact> m_Contacts = new L1st<Contact>(): public void AddContact(Contact contact) { m_Contacts.Add(contact); } public List<Contact> GetContactsO { return m_Contacts; } } ///////////////// Сторона клиента ///////////////// [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO: } public partial class ContactManagerCllent : C11entBase<IContactManager>. IContactManager { public Contact[] GetContactsO
return Channel .GetContactsO; ) II Клиентский код ContactManagerCllent proxy = new ContactManagerClientO; Contact[] contacts = proxy.GetContactsO; proxy.Close(); Обратите внимание: коллекция должна содержать метод Add() для обеспече- ния маршалинга в виде массива; при этом наличие реализации Add() не обяза- тельно. Пользовательские коллекции Возможность автоматического маршалинга коллекции в виде массива не огра- ничивается встроенными коллекциями. Подойдет любая пользовательская кол- лекция, удовлетворяющая тем же исходным условиям (листинг 3.15). В приве- денном примере коллекция MyCollection<string> маршализируется в виде string[]. Листинг 3.15. Маршалинг пользовательской коллекции в виде массива III////////////// Сторона службы ///////////////// [Serializable] public class MyCol 1 ection<T> : IEnumerable<T> public void Add(T item) {} IEnumerator<T> I Enumerable<T>. Get Enumerator) (...) // ... ) [ServiceContract] interface IMyContract [Operationcontract] MyCollection<string> GetCollectionO: ) lllllllllllllllll Сторона клиента ///////////////// [ServiceContract] interface IMyContract [Operationcontract] string[] GetCollectionO; ) Контракт данных коллекций Описанный механизм маршалинга реальных коллекций не оптимален. Во-пер- вых, он требует, чтобы коллекция была сериализуемой, и не работает со служеб- но-ориентированным атрибутом DataContract. Одна сторона имеет дело с коллек- цией, а другая — с массивом. Эти две сущности семантически не эквивалентны —
138 Глава 3. Контракты данных скорее всего, коллекция обладает некоторыми преимуществами, иначе бы она изначально не использовалась. Во-вторых, поддержка метода Add() или интер- фейсов IEnumerable/IEnumerable<T> не проверяется на стадии компиляции или времени выполнения; их отсутствие приводит к неработоспособности контрак- та данных. Проблема решается при помощи очередного специализированного атрибута CollectionDataContractAttribute, определяемого следующим образом: [Attri butellsage(AttributeTargets.Struct|Attri buteTargets.Class,Inheri ted=fal se) ] public sealed class CollectionDataContractAttribute : Attribute { public string Name {get;set;} public string Namespace {get;set:} //... } Атрибут CollectionDataContractAttribute аналогичен DataContract, и он не делает коллекцию сериализуемой. Коллекция, к которой он применяется, предостав- ляется клиенту как обобщенный связанный список. Вполне возможно, что свя- занный список не имеет ничего общего с исходной коллекций, но по своему ин- терфейсу он все же больше напоминает коллекцию, чем массив. Например, для следующего определения коллекции: [CollectionDataContract(Name = "MyCol1ectionOf{0}")] public class MyCollection<T> : lEnumerable<T> { public void Add(T Item) 0 IEnumerator<T> IEnumerable<T>.GetEnumerator() // ... } и определения контракта на стороне службы: [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] MyCol1ection<Contact> GetContactsO; } после импортирования метаданных клиент получит следующие определения: [CollectionDataContract] public class MyCol1ectionOfContact : LIst<Contact> {} [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact);
[Operationcontract] MyCollectionOfContact GetContactsO: Кроме того, атрибут CollectionDataContract проверяет во время загрузки служ- бы присутствие метода Add(), а также интерфейса innumerable или IEnumerable<T>. При отсутствии их в коллекции происходит исключение InvalidDataContractEx- ception. Атрибуты DataContract и CollectionDataContract не могут применяться к кол- лекции одновременно. Это условие также проверяется во время загрузки службы. Сохранение типа коллекции WCF даже позволяет использовать на стороне клиента ту же коллекцию, что и на стороне службы. У программы SvcUtil имеется ключ /collectionType (сокра- щенно /ct), который позволяет сослаться на конкретную сборку коллекции на стороне клиента и использовать ее в определении контракта. Вы должны задать местонахождение сборки коллекции, и разумеется, сборка должна быть доступ- на для клиента. Например, служба может определить следующий контракт с использовани- ем коллекции Stack<T>: [ServiceContract] interface IContactManager [Operationcontract] void AddContact(Contact contact); [Operationcontract] Stack<Contact> GetContactsO: ) Клиент передает SvcUtil адрес обмена метаданными службы (например, http://localhost:8000), использует ключ /г для ссылки на сборку System.dll, содер- жащую класс Stack<T>, и при помощи ключа /ct указывает на необходимость со- хранения исходной коллекции: SvcUtil http://1 ocalhost:8000/ /г: С: \WINDOWS\Mi crosoft.NET\Framework\v2.0.50727\System.dll /ct: System. Col 1 ect 1 ons. Gener 1 c. Stack41 В итоговом определении контракта на стороне клиента будет использовать- ся Stack<T>: [ServiceContract] interface IContactManager [Operationcontract] void AddContact(Contact contact): [Operationcontract] Stack<Contact> GetContactsO:
140 Глава 3. Контракты данных Разумеется, решение с ключами /ret нельзя назвать служебно-ориентирован- ным. Тип используемой коллекции должен быть известен заранее, к тому же та- кое решение работает только во взаимодействиях WCF-WCF. Коллекции на стороне клиента До настоящего момента работа с коллекциями обсуждалась в определенном контексте: коллекция определяется и используется на стороне службы, а кли- ент взаимодействует с массивом или списком. Оказывается, возможен и обрат- ный вариант: служба определяется в виде массива, а клиент использует совмес- тимую коллекцию. Например, возьмем следующее определение контракта службы: [ServiceContract] interface IContactManager { [Operationcontract] void ProcessArray(string[] array); } По умолчанию импортированные определения службы и посредника будут идентичными. Однако клиент может вручную переработать контракт и посред- ника для использования интерфейса любой коллекции: // Переработанное определение: [ServiceContract] interface IContactManager { [Operationcontract] void ProcessArray(IIIst<string> list); } и передать при вызове коллекцию с методом Add() (сериализуемую или снаб- женную атрибутом CollectionDataContract): IList<string> list = new List<string>; MyContactClient.proxy = new MyContractClient(); proxy. ProcessArrayd 1st); proxy.CloseO; Итераторы C# При использовании механизма итераторов1 C# 2.0 можно поручить компилято- ру сгенерировать реализацию пользовательского итератора для коллекции. Тем не менее такая реализация оформляется в виде вложенного класса, не помечен- ного атрибутом Serializable. Соответственно, коллекция не может возвращаться напрямую методом службы: [ServiceContract] interface IContactManager { [Operationcontract] 1 Если вы незнакомы с итераторами C# 2.0, обратитесь к моей статье «Create Elegant Code with Ano- nymous Methods, Iterators, and Partial Classes» в «MSDN Magazine» за май 2004 г.
IEnumerable<Contact> GetContactsO; I class ContactManager : IContactManager I List<Contact> m_Contacts = new List<Contact>(); // Недопустимая реализация public IEnumerable<Contact> GetContactsO ( foreach(Contact contact in m_Contacts) ( yield return contact; } ) ) ПРИМЕЧАНИЕ---------------------------------------------------------------------------------- В следующей версии C# (а также в остальных компонентах .NET 3.5) компилятор будет добавлять атрибут Serializable во вложенный класс, сгенерированный командой yield return. Это сделает воз- можным прямой возврат итератора методами служб. Словари Словари (ассоциативные массивы) составляют особую разновидность коллек- ций, в которой один тип контракта данных отображается на другой тип. Как следствие, словари не слишком хорошо укладываются в модель массива или списка, и в WCF они получили собственное представление. Если словарь является сериализуемой коллекцией с поддержкой интерфей- са IDictionary, он представляется в виде Dictionary<objectobject>. Например, оп- ределение контракта службы [Serializable] public class MyDictionary : IDictionary {•••} [ServiceContract] interface IContactManager I [Operationcontract] MyDictionary GetContactsO; } будет представлено для внешнего доступа следующим определением: [ServiceContract] interface IContactManager [Operationcontract] Dictionary<object,object> GetContactsO; В частности, это относится к коллекции HashTable.
142 Глава 3. Контракты данных Для сериализуемых коллекций с поддержкой интерфейса IDictionary<KrT>: [Serializable] public class MyDictionary<K,T> ; IDictionary<K,T> {•••} [ServiceContract] interface IContactManager { [Operationcontract] MyDicti onary<i nt,Contact> GetContacts(); } используется канальное представление Dictionary<K,T>: [ServiceContract] interface IContactManager { [Operationcontract] Dictionary^ nt,Contact» GetContactsO; } Сюда относится и прямое использование Dictionary<K,T> в исходном опреде- лении. Если вместо обычной сериализуемой коллекции используется словарь, по- меченный атрибутом CollectionDataContract, он будет представлен субклассом со- ответствующего представления. Например, следующее определение контракта службы: [CollectionDataContract] public class MyDictionary : IDictionary [ServiceContract] interface IContactManager { [Operationcontract] MyDictionary GetContactsO; } будет обладать канальным представлением: [CollectionDataContract] public class MyDictionary : Dictionary<object,object» {} [ServiceContract] interface IContactManager { [Operationcontract] MyDictionary GetContactsO;
Коллекции 143 А обобщенная коллекция [CollectionDataContract] public class MyDictionary<K,T> : IDictionary<K,T> () [ServiceContract] interface IContactManager ( [Operationcontract] MyDictionary<int,Contact> GetContactsO: публикуется в метаданных в следующем виде: [Col 1 ect i onDa taCont ract ] public class MyDictionary : D1ct1onary<1nt,Contact> () [ServiceContract] interface IContactManager ( [Operationcontract] MyDictionary GetContactsO;
4 Управление экземплярами Термином «управление экземплярами» (instance management) обозначается со- вокупность приемов, используемых WCF для привязки клиентских запросов к экземплярам служб и определяющих, какой экземпляр службы будет обраба- тывать запрос клиента. Необходимость управления экземплярами объясняется тем, что приложения слишком сильно различаются по своим потребностям в области масштабируемости, производительности, пропускной способности, обработки транзакций и очередей вызовов. В том, что касается этих потребно- стей, единого решения «на все случаи жизни» просто не существует. Тем не менее существует ряд канонических стратегий управления экземплярами, при- менимых для широкого спектра приложений, разнообразных сценариев и про- граммных моделей. Эти стратегии будут рассмотрены в настоящей главе, а их понимание необходимо для разработки масштабируемых, логически целостных служебно-ориентированных приложений. WCF поддерживает три типа активи- зации экземпляров: у служб уровня вызова для каждого клиентского запроса создается (и впоследствии уничтожается) новый экземпляр службы. У служб уровня сеанса (или сеансовых служб) создается один экземпляр службы для ка- ждого подключения клиента. Наконец, у синглетных служб все подключения клиентов обслуживаются одним экземпляром. В этой главе описаны все режи- мы управления экземплярами; приводятся рекомендации относительно того, когда и как лучше их использовать; а также рассматриваются некоторые сопут- ствующие темы: аспекты поведения, контексты, демаркационные операции и деактивизация экземпляров1. Аспекты поведения В общем и целом выбор режима управления экземплярами является подробно- стью реализации на стороне службы, которая не должна как-либо проявляться 1 Глава содержит выдержки из моей статьи «WCF Essentials: Discover Mighty Instance Management Techniques for Developing WCF Apps» из «MSDN Magazine», июнь 2006 г.
Управление экземплярами для служб уровня вызова 145 пстороне клиента. Для поддержки этой и других локальных особенностей сто- роны службы в WCF определяется понятие аспектов поведения. Аспектом по- йдет (behavior) называется локальный атрибут службы, не влияющий на ее юммуникации. Клиент должен быть изолирован от аспектов поведения, и по- следние не должны проявляться в привязке или публикуемых метаданных службы. WCF определяет два типа аспектов стороны службы, которым соот- ветствуют два атрибута. ServiceBehaviorAttribute используется для настройки ас- пектов поведения службы, то есть аспектов поведения, распространяющихся на все конечные точки (все контракты и операции) службы. Атрибут ServiceBehavior применяется непосредственно к классу реализации службы В контексте этой главы атрибут ServiceBehavior используется для настройки режима управления экземплярами службы. Как показано в листинге 4.1, атрибут определяет свой- ство InstanceContextMode перечисляемого типа InstanceContextMode. Значение In- stanceContextMode определяет, какой режим управления экземплярами должен использоваться службой. Листинг 4.1. Атрибут ServiceBehaviorAttribute используется для настройки режима управления экземплярами public enum InstanceContextMode ( PerCall. PerSession. Single ) I [Attri buteUsage (Att r 1 buteTargets .Class)] public sealed class ServiceBehaviorAttribute : Attribute.... I public InstanceContextMode InstanceContextMode (get;set;) //... ) Атрибут OperationBehaviorAttribute используется для настройки аспектов по- ведения операций и распространяется только на реализацию конкретной опера- ции. Он может применяться только к методам, реализующим операцию кон- тракта, и никогда не применяется к определению операции в самом контракте. Применение OperationBehavior будет рассмотрено позднее в этой главе, а также в последующих главах. Управление экземплярами для служб уровня вызова Если тип службы настроен на активизацию уровня вызова, экземпляр службы (объект CLR) существует только в процессе обработки клиентского вызова. Ка- ждому клиентскому запросу (то есть вызов метода для контракта WCF) выде- ляется новый специализированный экземпляр службы. Следующая процедура
146 Глава 4. Управление экземплярами описывает механизм работы активизации уровня вызова; последовательность действий продемонстрирована на рис. 4.1. 1. Клиент обращается с вызовом к посреднику, который перенаправляет вызов службе. 2. WCF создаст экземпляр службы и вызывает метод. 3. При возврате управления методом, если объект реализует IDisposable, WCF вызывает для него IDisposable.Dispose(). 4. Клиент обращается с вызовом к посреднику, который перенаправляет вызов службе. 5. WCF создает объект и вызывает метод. Клиент Клиент Клиент Рис. 4.1. Режим управления экземплярами уровня вызова Уничтожение экземпляра службы — весьма интересный момент. Как упоми- налось ранее, если служба поддерживает интерфейс IDisposable, WCF автомати- чески вызывает метод Dispose(), предоставляя службе возможность выполнить всю необходимую зачистку. Обратите внимание: Dispose() вызывается в том же программном потоке, который перенаправил исходный вызов метода, и при этом вызов Dispose() выполняется в контексте операции (об этом чуть позже). После вызова Dispose() WCF отсоединяет экземпляр от остальной инфраструк- туры WCF, и он становится кандидатом для уборки мусора. Преимущества служб уровня вызова В классической модели программирования «клиент-сервер» с использованием таких языков, как C++ и С#, каждый клиент получает свой специализирован- ный объект сервера. Основная проблема такого подхода заключается в том, что он плохо масштабируется. Объект сервера может использовать высокозатрат- ные или дефицитные ресурсы — подключения к базе данных, коммуникацион- ные порты или файлы. Представьте себе приложение, которое должно обслу- живать многих клиентов. Как правило, такие клиенты создают необходимые объекты при запуске клиентского приложения и избавляются от них при завер- шении последнего. На масштабируемость модели «клиент-сервер» отрицатель- но влияет тот факт, что клиентское приложение может удерживать объекты в течение долгого времени, реально используя их в течение лишь малой доли этого времени. Выделение объекта для каждого клиента приведет к связыванию
Управление экземплярами для служб уровня вызова 147 критических или ограниченных ресурсов на долгое время и в конечном итоге к исчерпанию этих ресурсов. В более эффективной модели активизации объект выделяется клиенту толь- ко на время обработки вызова от клиента к службе. В этом случае количество объектов, созданных и поддерживаемых в памяти, совпадает с количеством од- новременно обрабатываемых вызовов, а не с количеством обслуживаемых кли- ентов. В типичной Enterprise-системе только один процент всех клиентов выда- ет параллельные вызовы (Enterprise-системы с высокой загрузкой имеют около трех процентов параллельных вызовов). Если ваша система может одновремен- но поддерживать только 100 высокозатратных экземпляров службы, это озна- чает, что в типичной ситуации она сможет обслуживать до 10 000 клиентов. Именно такую возможность предоставляет режим активизации экземпляров уровня вызова, поскольку между вызовами клиент получает ссылку на посред- ника, который не связан с реальным объектом на другом конце канала связи. Преимущество такой схемы очевидно: она позволяет избавиться от высокоза- тратных ресурсов, подолгу удерживаемых экземпляром службы до того момен- та, как клиент избавится от посредника. Аналогичным образом, захват ресурсов откладывается до того момента, как они фактически потребуются клиенту. Следует учесть, что многократное создание и уничтожение экземпляра службы на стороне службы без разрыва связи с клиентом (с посредником на стороне клиента) обходится гораздо дешевле, чем создание экземпляра и под- ключения. Второе преимущество заключается в том, что повторное выделение ресурсов (или подключение к ним) экземпляра службы при каждом вызове хо- рошо укладывается в модель транзакционного управления ресурсами и тран- закционного программирования (см. главу 7), поскольку оно упрощает задачу согласования с состоянием экземпляра. Третье преимущество служб уровня вызова — возможность их применения с очередями вызовов без подключения (см. главу 9), благодаря упрощенному отображению экземпляров служб на от- дельные сообщения в очереди. Настройка служб уровня вызова Чтобы пометить тип службы как службу уровня вызова, следует применить к нему атрибут ServiceBehavior со свойством InstanceContextMode, равным Instan- ceContextMode. PerCall: [ServiceContract] interface IMyContract (...) [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract (...) В листинге 4.2 приведена простая служба уровня вызова и ее клиент. Как видно из выходных данных программы, для каждого вызова метода клиента конструируется новый экземпляр службы.
148 Глава 4. Управление экземплярами Листинг 4.2. Служба уровня вызова и клиентский код ///////////////// Сторона службы ///////////////// [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodO: } [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract.IDisposable { int m_Counter = 0; MyServiceO { Trace.Wr1teL1ne("MyServ1ce.MyServ i ce()"): } public void MyMethodO { m_Counter++: Trace. Wri teLineCCounter = " + m_Counter); } public void Disposed { Trace. Wri teLine( "MyService. Disposed"): } } ///////////////// Сторона клиента ///////////////// MyContractClient proxy = new MyContractClient(); proxy.MyMethod(): proxy.MyMethod(); proxy.CloseO; // Результат MyService.MyServi ce() Counter = 1 MyService. Di sposeO MyService. MyServiceO Counter = 1 MyService.DisposeO Проектирование служб уровня вызова Хотя теоретически режим активизации уровня вызова может использоваться для любого типа службы, на практике служба и ее контракты должны изначаль- но проектироваться с расчетом на поддержку этого режима. Главная проблема заключается в том, что клиент не знает, что он каждый раз имеет дело с новым экземпляром. Службы уровня вызова должны быть службами с контролем со- стояния (state-aware); другими словами, они должны производить упреждаю- щее управление своим состоянием, создавая иллюзию непрерывного сеанса. Службы с контролем состояния следует отличать от служб без состояния (stateless). Если бы службы уровня вызова были полноценными службами без
Управление экземплярами для служб уровня вызова 149 состояния, исчезла бы исходная необходимость в применении активации уров- ня вызова. Режим уровня вызова нужен именно потому, что службы обладают состоянием, и притом высокозатратным. Экземпляр службы уровня вызова создается непосредственно перед каждым вызовом метода и уничтожается сразу же после каждого вызова. Следовательно, в начале вызова объект должен ини- циализировать свое состояние по данным из некоего хранилища, а в конце вы- зова - вернуть измененное состояние в это хранилище. Как правило, в качестве такого хранилища используется база данных или файловая система, но также им могут быть временные области памяти вроде статических переменных. Впрочем, не все состояние объекта может сохраняться «как есть». Напри- мер, если состояние содержит подключение к базе данных, объект должен зано- во установить подключение при конструировании или в начале каждого вызова и уничтожить его в конце вызова или своей реализации IDisposable. Dispose(). Ре- жим уровня вызова подразумевает одно важное следствие для архитектуры операций: каждая операция должна включать параметр для идентификации эк- земпляра службы, состояние которого требуется получить. При помощи этого параметра экземпляр загружает из хранилища именно свое состояние, а не со- стояние другого экземпляра того же типа. Примеры таких параметров — номер счета для службы управления банковскими счетами, номер заказа для службы обработки заказов и т. д. В листинге 4.3 представлен шаблон для реализации службы уровня вызова. Листинг 4.3. Реализация службы уровня вызова [DataContract] class Param (...) [ServiceContract] interface IMyContract ( [Operationcontract] void MyMethodt Pa ram stateidentifier); ) [Servi ceBehav ior(InstanceContextMode = InstanceContextMode.PerCai1)] class MyPerCallService : IMyContract,IDisposable ( public void MyMethodtParam stateidentifier) GetStatetstateldentifier); DoWorkt); SaveStatetstateldentifier); ) void GetStatetParam stateidentifier) (...) void DoWorkt) (...) void SaveState(Param stateidentifier) (...) void void DisposeO
150 Глава 4. Управление экземплярами Класс реализует операцию MyMethod(), которая получает параметр типа Param (псевдотип, придуманный для этого примера), идентифицирующий эк- земпляр: public void MyMethod(Param stateidentifier): Затем экземпляр использует идентификатор для загрузки состояния и его сохранения в конце вызова метода. Фрагменты состояния, общие для всех кли- ентов, инициализируются в конструкторе и уничтожаются в Dispose(). Режим активизации уровня вызова лучше всего работает тогда, когда объем работы при каждом вызове метода является конечной величиной, и не сущест- вует дополнительных действий, завершаемых в фоновом режиме после возвра- та управления методом. По этой причине вызовы не должны порождать фоно- вые программные потоки или возвращать экземпляру асинхронные вызовы, потому что после возврата из метода объект будет уничтожен. Так как службы уровня вызова загружают свое состояние из некоего хранилища при каждом вызове, они хорошо работают в сочетании с механизмами распределения за- грузки, при условии, что хранилище состояний представляет собой глобальный ресурс, доступный для всех компьютеров. Распределитель нагрузки может пе- ренаправлять вызовы разным компьютерам по своему усмотрению, зная, что каждая служба уровня вызова сможет обработать вызов после загрузки состоя- ния. Службы уровня вызова и производительность Службы уровня вызова представляют собой явный компромисс между произ- водительностью (затраты на повторное конструирование состояния экземпляра при каждом вызове метода) и масштабируемостью (сохранение состояния и связанных с ним ресурсов). Не существует однозначных правил относительно того, когда и до какой степени можно поступиться долей производительности ради значительного выигрыша в масштабируемости. Возможно, вам придется провести профильный анализ своей системы, и в конечном счете спроектиро- вать одни службы для использования активации уровня вызова, и отказаться от нее в других службах. Операции зачистки Поддерживает ли тип службы IDisposable или нет — подробность реализации, несущественная для клиента. В конце концов, у клиента все равно нет никакой возможности вызвать метод Dispose(). При проектировании контракта для служ- бы уровня вызова старайтесь избегать определения операций, полностью по- священных зачистке состояния и ресурсов: // Не рекомендуется [ServiceContract] interface IMyContract { void DoSomethingO; void Cleanup!); } [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCai 1)] class MyPerCallService : IMyContract.IDisposable
Сеансовые службы 151 I public void DoSomething() (...) public void Cleanup() public void DisposeO I CleanupO: ) ) Неразумность такой архитектуры очевидна. Вызов метода зачистки клиен- том приведет к нежелательному эффекту: объект создается только для того, чтобы клиент мог вызвать для него Cleanup(). Но сразу же после этого WCF вы- зовет метод IDisposable.Dispose() (если он присутствует) для повторного выпол- нения той же зачистки. Выбор служб уровня вызова Хотя модель программирования служб уровня вызова выглядит несколько чу- жеродной для разработчиков «клиент-сервер», в действительности именно ак- тивизация уровня вызова является предпочтительным режимом управления экземплярами для служб WCF. Первый аргумент в пользу служб уровня вызо- ва предельно прост: они лучше масштабируются. При проектировании службы я стремлюсь заложить в нее 10-кратный допуск по масштабируемости. Иначе говоря, каждая служба проектируется так, чтобы она справилась с нагрузкой по крайней мере на порядок большей, чем указано в требованиях. Дело в том, что в любой проектировочной области инженер никогда не проектирует систему для работы с точно заданной номинальной загрузкой. Вряд ли вам захочется входить в здание, несущие конструкции которого выдерживают только задан- ную в задании нагрузку, или садиться в лифт, выдерживающий ровно шесть пассажиров, как сказано в инструкции и т. д. С программными системами дело обстоит точно так же — зачем проектировать систему для конкретной текущей нагрузки, если все остальные работники трудятся над расширением бизнеса, что неминуемо приведет к увеличению нагрузки? Программные системы долж- ны работать годами, выдерживая не только текущие, но и будущие нагрузки. В результате при использовании «золотого правила 10Х» у вас очень быстро возникнет потребность в масштабировании, обеспечиваемом службами уровня вызова. Второй аргумент в пользу служб уровня вызова — транзакции. Как бу- дет показано в главе 7, транзакции являются абсолютно необходимой частью любой системы, и службы уровня вызова очень хорошо укладываются в тран- закционную модель программирования независимо от системной нагрузки. Сеансовые службы WCF может поддерживать сеанс между клиентом и определенным экземпля- ром службы. Когда клиент создает нового посредника для службы, настроенной как сеансовая служба, клиент получает новый специализированный экземпляр
152 Глава 4. Управление экземплярами службы, не зависящий от всех остальных экземпляров той же службы. Экземп- ляр продолжает существовать до тех пор, пока не станет ненужным клиенту. Режим активизации сильно напоминает классическую модель «клиент-сервер». Иногда этот режим также обозначается термином «приватный сеанс». Каждый приватный сеанс однозначно связывает посредника и его набор каналов на сто- роне клиента и службы с определенным экземпляром службы (точнее, с его контекстом, как будет показано далее). Так как экземпляр службы остается в памяти на протяжении всего сеанса, он может использоваться для хранения состояния, а модель программирования очень близка к классической модели «клиент-сервер». Соответственно, ей при- сущи те же проблемы масштабируемости и транзакционности, что и классиче- ской модели «клиент-сервер». Службы, настроенные на использование приват- ных сеансов, обычно не могут поддерживать более нескольких десятков (воз- можно, до сотни или двух) клиентов из-за затрат ресурсов, связанных с каждым специализированным экземпляром службы. ПРИМЕЧАНИЕ------------------------------------------------------------- Клиентский сеанс определяется на уровне конкретной конечной точки службы конкретного посред- ника. Если клиент создаст другого посредника для той же или другой конечной точки, второй по- средник будет связан с новым экземпляром и сеансом. Настройка приватных сеансов Поддержка сеанса складывается из трех составляющих: поведения, привязки и контракта. Первая часть необходима для того, чтобы платформа WCF под- держивала существование экземпляра службы на протяжении сеанса и переда- вала ему клиентские сообщения. Для этого свойству InstanceContextMode атри- бута ServiceBehavior задается значение InstanceContextMode.PerSession: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {•••} Поскольку InstanceContextMode.PerSession является значением по умолчанию для свойства InstanceContextMode, следующие определения эквивалентны: class MyService : IMyContract [ServiceBehavior] class MyService : IMyContract {...} [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {•••} Сеанс обычно завершается при закрытии посредника клиентом. При этом посредник оповещает службу о завершении сеанса. Если служба поддерживает IDisposable, то метод Dispose() будет вызван асинхронно с клиентом. Впрочем, №spose() вызывается в рабочем программном потоке без контекста операции.
Сеансовые службы 153 Чтобы все сообщения определенного клиента передавались конкретному эк- земпляру, WCF необходимы средства идентификации клиента. Один из спосо- бов основан на сеансах транспортного уровня, то есть непрерывного поддержа- ния связи на транспортном уровне (например, протоколов TCP и IPC). В резуль- тате при использовании привязок NetTcpBinding и NetNamedPipeBinding WCF свя- зывает это подключение с клиентом. Ситуация усложняется с протоколом HTTP, не обладающим состоянием. На концептуальном уровне каждое сообщение HTTP достигает службы по новому подключению, поэтому поддержка сеанса транс- портного уровня для BasicHttpBinding невозможна. С другой стороны, привязка WS позволяет эмулировать сеанс транспортного уровня — для этого в заголов- ки сообщений включается логический идентификатор сеанса, однозначно иден- тифицирующий клиента. Фактически WSHttpBinding эмулирует транспортный сеанс при включении безопасного или надежного обмена сообщениями. Контрактная составляющая поддержки сеанса необходима из-за того, что runtime-среда WCF на стороне клиента должна знать о необходимости исполь- зования сеанса. Атрибут ServiceContract предоставляет свойство SessionMode пе- речисляемого типа SessionMode: public enum SessionMode Allowed. Required, NotAllowed 1 [AttributeUsage(Attri buteTargets. Interface | Attri buteTargets. Cl ass, Inherited=false)] public sealed class ServiceContractAttribute : Attribute ( public SessionMode SessionMode (get:set:} //... 1 Свойство SessionMode по умолчанию равно SessionMode.Allowed. Заданное значение SessionMode включается в метаданные службы и правильно воспроиз- водится при импортировании метаданных контракта клиентом. SessionMode.Allowed Значение SessionMode.Allowed используется по умолчанию, поэтому следующие определения эквивалентны: [ServiceContract] interface IMyContract (...) [ServiceContract (SessionMode = SessionMode.Allowed)] interface IMyContract (...) Свойство SessionMode относится не к режиму создания экземпляров, а к под- держке сеансов транспортного уровня (или ее эмуляции в случае привя- зок WS). Как подсказывает имя, задание свойству SessionMode значения SessionMode.Allowed разрешает транспортные сеансы, но не делает их обязатель-
154 Глава 4. Управление экземплярами ними. Итоговое поведение определяется конфигурацией службы и используе- мой привязкой. Если служба настроена на активизацию уровня вызова, она продолжает вести себя как служба уровня вызова, как в листинге 4.2. Если служба настроена как сеансовая, она ведет себя как сеансовая служба только в том случае, если используемая привязка поддерживает сеансы транспортного уровня. Например, привязка BasicHttpBinding принципиально не поддерживает сеансы транспортного уровня, поскольку протокол HTTP по своей природе не имеет состояния. Привязка WSHttpBinding без безопасности и надежной достав- ки сообщений тоже не поддерживает сеансов транспортного уровня. В обоих указанных случаях, даже если служба настроена в режиме InstanceContextMode. PerSession, а контракт — в режиме SessionMode.Allowed, служба ведет себя как служба уровня вызова, а вызовы Dispose() осуществляются асинхронно; иначе говоря, работа клиента не блокируется после вызова, пока WCF занимается уничтожением экземпляра. Тем не менее при использовании привязки WSHttpBinding с поддержкой безо- пасности (конфигурация по умолчанию) или надежной доставки сообщений, а также привязок NetTcpBinding и NetNamedPipeBinding, служба ведет себя как се- ансовая. Например, при использовании NetTcpBinding следующая служба будет обладать сеансовой поддержкой: [ServiceContract] Interface IMyContract {...} class MyService : IMyContract (••} Обратите внимание: в приведенном фрагменте просто используются значе- ния по умолчанию для свойств SessionMode и InstanceContextMode. SessionMode.Required Значение SessionMode.Required разрешает использование сеансов транспортного уровня, но не гарантирует поддержки сеансов прикладного уровня. Контракт, в конфигурацию которого входит SessionMode.Required, не может использовать- ся с конечной точкой службы, привязка которой не поддерживает сеансов транспортного уровня; это ограничение проверяется во время загрузки службы. Впрочем, службу можно настроить как службу уровня вызова, экземпляры ко- торой создаются и уничтожаются при каждом вызове со стороны клиента. Только в том случае, если служба настроена как сеансовая, ее экземпляр будет существовать на протяжении всего клиентского сеанса: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract (...) class MyService : IMyContract {...} ПРИМЕЧАНИЕ ---------------------------------------------------------------------- При проектировании сеансовых контрактов я рекомендую явно задавать SessionMode.Required, не полагаясь на значение по умолчанию SessionMode.Allowed. Во всех примерах книги там, где архи- тектура подразумевает сеансовое взаимодействие, активно применяется SessionMode.Required.
Сеансовые службы 155 В листинге 4.4 приведены те же служба и клиент, что и в листинге 4.2, за ис- ключением того, что контракт и служба настроены на использование приватно- го сеанса. Как видно из выходных данных, клиенту выделяется специализиро- ванный экземпляр службы. Листинг 4.4. Сеансовая служба и клиентский код ///////////////// Сторона службы ///////////////// [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract I [Operationcontract] void MyMethod(); ) class MyService : IMyContract. IDisposable I int m_Counter - 0; MyServiceO ( Trace.WriteLineC'MyService.MyService()"): I public void MyMethodO m_Counter++; Trace. Wri teLineC Counter = ” + m_Counter); I public void Dispose!) ( Trace. Wri teLi ne( "MyService .Dispose!)"): I ) Illi 1111 / / / /11 /11 Сторона клиента 11111111111111111 MyContractClient proxy = new MyContractCllento: proxy. MyMethod (): proxy. MyMethod (); proxy.Closet); // Результат MyService.MyServiceO Counter = 1 Counter = 2 MyService.DisposeO SessionMode.NotAllowed Значение SessionMode.NotAllowed запрещает использование сеансов транспорт- ного уровня, что автоматически делает невозможными сеансы прикладного уровня. Независимо от конфигурации, служба всегда ведет себя как служба уровня вызова. Если служба реализует IDisposable, то метод Dispose() вызывает- ся асинхронно по отношению к клиенту; другими словами, после вызова управ- ление возвращается клиенту, a WCF вызывает Dispose() в фоновом режиме
156 Глава 4. Управление экземплярами (в программном потоке входящего вызова). Поскольку протоколы TCP и IPC поддерживают сеансы на транспортном уровне, вам не удастся настроить ко- нечную точку службы, предоставляющую контракт с пометкой SessionMode.Not- Allowed и при этом использующую привязку NetTcpBinding или NetNamedPipe- Binding; данное ограничение проверяется во время загрузки службы. Тем не менее использование WSHttpBinding с эмуляцией транспортных сеансов разре- шено. Чтобы программный код лучше читался, я рекомендую при выборе SessionMode.NotAllowed всегда настраивать службу как службу уровня вызова: [Serv1ceContract(SessionMode = SessionMode.NotAllowed)] Interface IMyContract [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract Привязка BasicHttpBinding не может иметь сеансов транспортного уровня, поэтому использующие ее конечные точки ведут себя так, как если бы контракт всегда настраивался со значением SessionMode.NotAllowed, а вызов Dispose() все- гда происходит асинхронно. Привязка, контракт и поведение службы В табл. 4.1 представлена сводка режимов управления экземплярами, используе- мых в зависимости от привязки, сеансового режима контракта и контекста, за- данного в поведении службы. В таблице отсутствуют недопустимые комбина- ции — например, SessionMode.Required с BasicHttpBinding. Таблица 4.1. Зависимость режима управления экземплярами от привязки, конфигурации контракта и поведения службы Привязка Сеансовый режим Контекстный режим Асинхронный вызов Dispose() Режим управления экземплярами Basic Allowed/NotAllowed PerCall/PerSession Да PerCall TCP, IPC Allowed/Required PerCall Нет PercCall TCP, IPC Allowed/Required PerSession Да PerSession WS (безопасность и надежность отсутствуют) NotAllowed/Allowed PerCall/PerSession Да PerCall WS (с безопасностью Allowed/Required или надежностью) PerSession Да PerSession WS (с безопасностью или надежностью) NotAllowed PerCall/PerSession Да PerCall Согласованные конфигурации Если один контракт, реализуемый службой, является контрактом с состоянием, все остальные контракты настоятельно рекомендуется сделать контрактами с состоянием, чтобы избегать смешения контрактов уровня вызова и уровня се- анса в одном типе службы, хотя WCF это и позволяет:
Сеансовые службы 157 [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract {...) [ServiceContract (SessionMode = SessionMode.NotAllowed)] interface IMyOtherContract (...) II He рекомендуется class MyService : IMyContract. IMyOtherContract (...) Причина очевидна: службы уровня вызова должны активно управлять сво- им состоянием, а сеансовые службы — нет. Хотя два контракта будут предо- ставляться по двум разным конечным точкам и могут независимо использовать- ся двумя разными клиентами, такой подход обернется громоздкой, усложнен- ной реализацией класса службы. Сеансы и надежность Сеанс между клиентом и экземпляром службы надежен лишь в той степени, в какой надежен нижележащий транспортный сеанс. Соответственно, у служ- бы, реализующей контракт с состоянием, все конечные точки, предоставляю- щие этот контракт, должны использовать привязки с поддержкой надежных транспортных сеансов. Проследите за тем, чтобы во всех случаях использова- лась привязка с поддержкой надежности и последняя была явно включена на стороне как клиента, так и службы — либо на программном, либо на админист- ративном уровне, как показано в листинге 4.5. Листинг 4.5. Включение надежности для сеансовых служб <!--Конфигурация хоста <system. servi ceModel > <services> <service name = "MyPerSessionService"> <endpoint address = "net.tcp://1 oca 1 host:8000/MyPerSess ionServi ce" binding = "netTcpBinding" bindingconfiguration = "TCPSession" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBi ndi ng> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system. servi ceModel > <!-Конфигурация клиента:--> продолжение^
158 Глава 4. Управление экземплярами <system.servi ceModel> <cl1ent> <endpoint address = "net.tcp://localhost:8000/MyPerSessionService" binding = "netTcpBinding" bindingconfiguration = "TCPSession" contract = "IMyContract" /> </client> <bindings> <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system.servi ceModel> Единственное исключение из этого правила составляют привязки именован- ных каналов. Они не нуждаются в протоколах надежной передачи сообщений (все вызовы все равно происходят в пределах одного компьютера), поэтому та- кой транспорт считается изначально надежным. Как надежный транспортный сеанс, так и упорядоченная доставка сообщений не являются обязательными, но WCF обеспечит сеанс даже при отключении упорядоченной доставки. Оче- видно, вследствие самой природы прикладных сеансов, взаимодействующий с сеансовой службой клиент ожидает, что все сообщения будут доставляться в порядке их отправки. К счастью, упорядоченная доставка включается по умолчанию при включении надежного транспортного сеанса, так что дополни- тельная настройка не потребуется. Идентификатор сеанса Каждый сеанс обладает уникальным идентификатором, доступным как для клиента, так и для службы. Идентификатор сеанса представляет собой GUID (глобально-уникальный идентификатор) и может использоваться для ведения журналов и диагностики. Служба обращается к идентификатору сеанса через контекст вызова операции. У каждой операции службы имеется контекст вызо- ва операции — набор свойств, используемых наряду с идентификатором сеанса для обратных вызовов, управления транзакциями, безопасности, обращения к хосту и обращений к объекту, представляющему сам контекст выполнения. Класс Operationcontext предоставляет доступ ко всем этим свойствам, а служба получает ссылку на контекст операции текущего метода при помощи статиче- ского метода Current класса Operationcontext: public sealed class Operationcontext : { public static Operationcontext Current {get:set:} public string Sessionld {get:}
Сеансовые службы 159 Чтобы получить идентификатор сеанса, служба читает значение свойства Sessionld, которое возвращает GUID в форме строки: string sessionld = Operationcontext .Current .Sessionld; Trace. Wri teLi ne( sessi onld); II Трассировка: //unv.uuid :c8141f66--51a6-4c66-9e03-927d5ca97153 Если служба уровня вызова без транспортного сеанса (например, с привяз- кой BasicHttpBinding или режимом SessionMode.NotAllowed) обращается к свойст- ву Sessionld, свойство вернет null — сеанса не существует, а следовательно, нет и идентификатора. ПРИМЕЧАНИЕ---------------------------------------------------------------------------- В методе IDisposable. Dispose() служба не имеет контекста операции, а следовательно, не может об- ращаться к идентификатору сеанса. Клиент получает идентификатор сеанса через посредника. Как упоминалось в главе 1, класс ClientBase<T> является базовым классом посредников, генери- руемых Visual Studio 2005 и SvcUtil. ClientBase<T> предоставляет свойство Inner- Channel класса IClientChannel, доступное только для чтения. Тип IClientChannel наследует от интерфейса IContextChannel, предоставляющего свойство Sessionld, которое возвращает идентификатор сеанса в форме строки: public interface IContextChannel I string Sessionld (get;} //... } public interface IClientChannel : IContextChannel.... (...) public abstract class ClientBase<T> : ... ( public IClientChannel InnerChannel (get;} //... Для определений из листинга 4.4 получение идентификатора сеанса клиен- том может происходить примерно так: MyContractClient proxy = new MyContractClient(); proxy. MyMet hod (); string sessionld = proxy. InnerChannel .Sessionld; Trace.WriteLine(sessionld); // Трассировка: //urn:uuid:c8141f66-51a6-4c66-9eO3-927d5ca97153 Тем не менее, до какой степени клиентский идентификатор сеанса совпадает с идентификатором сеанса службы и вообще разрешено ли клиенту обращаться к свойству Sessionld, зависит от используемой привязки и ее конфигурации. Идентификаторы сеансов на сторонах клиента и службы связаны надежным се- ансом на транспортном уровне. Если используется привязка TCP с включен-
160 Глава 4. Управление экземплярами ным надежным сеансом (как это должно быть), клиент может получить дейст- вительный идентификатор сеанса только после вызова первого метода службы, обозначающего начало сеанса, или явного открытия посредника. Если обраще- ние производится до первого вызова, свойство Sessionld задается равным null Идентификатор сеанса, получаемый клиентом, совпадает с идентификатором службы. Если используется привязка TCP, но надежный сеанс отключен, кли- ент может обратиться к идентификатору сеанса до первого вызова, однако по- лученное значение будет отличаться от получаемого службой. Со всеми при- вязками WS и надежным обменом сообщениями, идентификатор сеанса равен null до первого вызова (или открытия посредника), но после него клиент и служба всегда обладают одинаковыми идентификаторами сеанса. Если на- дежная передача сообщений отсутствует, перед обращением к идентификатору сеанса необходимо сначала использовать посредника (хотя бы просто открыть его), иначе вы рискуете получить исключение InvalidOperationException. После открытия посредника клиент и служба обладают совпадающими идентифика- торами сеансов. При использовании привязок именованных каналов клиент может обратиться к свойству Sessionld до первого вызова, но получаемое им значение идентификатора всегда будет отличаться от значения службы. Следо- вательно, при использовании привязок именованных каналов идентификатор сеанса лучше полностью игнорировать. Завершение сеанса Как правило, сеанс завершается с закрытием посредника клиентом. Тем не ме- нее в случае некорректного завершения клиента или возникновения проблем со связью для каждого сеанса устанавливается тайм-аут, по умолчанию рав- ный 10 минутам. Сеанс автоматически завершается через 10 минут бездействия со стороны клиента, даже если клиент собирается использовать его в будущем. Если клиент попытается использовать посредника после завершения сеанса по тайм-ауту, он получит исключение CommunicationObjectFaultedException. Клиент и служба могут задать разные величины тайм-аутов. Привязки, поддерживаю- щие надежные сеансы транспортного уровня, могут предоставить свойство Reli- ableSession типа ReliableSession или OptionalReliableSession. Класс ReliableSession обладает свойством TimeSpan InactivityTimeout, которое может использоваться для задания нового тайм-аута по бездействию: public class ReliableSession { public TimeSpan InactivityTimeout {get:set;} //... } public class OptionalReliableSession : ReliableSession { public bool Enabled {get:set;} //... } public class NetTcpBinding : Binding....
public Optional ReliableSession ReliableSession {get:} II... I public abstract class WSHttpBindingBase : public OptionalReliableSession ReliableSession {get:} //... public class WSHttpBinding : WSHttpBindingBase,... () public class WSDualHttpBinding : Binding,... public ReliableSession ReliableSession (get:} II... ) Например, следующий фрагмент на программном уровне устанавливает 25-минутный тайм-аут для привязки TCP: NetTcpBinding tcpSessionBinding = new NetTcpBinding(): tcpSessionBindi ng. Rel i abl eSessi on. Enabled = true: tcpSess i onBi ndi ng. Rel i abl eSes s i on. I nact i v i tyTi meout = T i meSpan.F romMi nutes(25): А вот как выглядит эквивалентный фрагмент в конфигурационном файле: <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true" inactivityTimeout = "00:25:00"/> </binding> </netTcpBinding> Если клиент и служба задают разные тайм-ауты, используется более корот- кий интервал. Синглетные службы Если служба настроена в синглетном режиме управления экземплярами, все клиенты независимо друг от друга подключаются к единственному экземпляру службы, обслуживающему все конечные точки. Срок существования синглет- ной службы не ограничен, и она уничтожается только при завершении хоста. Единственный экземпляр синглетной службы создается ровно один раз — при создании хоста. Использование синглетной службы не требует, чтобы клиент поддерживал сеансы или использовал привязку, поддерживающую сеанс транспортного уровня. Если контракт, потребляемый клиентом, обладает поддержкой сеансов, то во время вызова синглетная служба будет обладать тем же идентификатором сеанса, что и клиент (если это допускает привязка); но при закрытии клиента посредник только завершает сеанс, не уничтожая единственного экземпляра. Если синглетная служба поддерживает контракты без сеансов, эти контракты
162 Глава 4. Управление экземплярами не будут создавать экземпляры на уровне вызова; они будут связаны с тем же единственным экземпляром. По самой своей природе синглетная служба ори- ентирована на совместное использование, и каждый клиент просто создает для нее своих посредников. Чтобы настроить службу на синглетный режим активизации, задайте свой- ству InstanceContextMode значение InstanceContextMode.Single: [Serv1ceBehav1or(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : {•••} В листинге 4.6 представлена синглетная служба с двумя контрактами: один требует поддержки сеанса, а другой нет. Как видно из клиентского вызова, об- ращения к двум разным конечным точкам передаются одному экземпляру, а за- крытие посредников не приводит к уничтожению этого экземпляра. Листинг 4.6. Синглетная служба с двумя контрактами ///////////////// Код службы ///////////////// [ServiceContract(SessionMode = SessionMode.Required)] Interface IMyContract { [Operationcontract] void MyMethod(); } [Serv1ceContract(Sess1onMode = SessionMode.NotAllowed)] Interface IMyOtherContract { [Operationcontract] void MyOtherMethodO; } [ServiceBehav1or(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : IMyContract.IMyOtherContract.IDisposable { Int m_Counter = 0: public MySIngletonO { T race.Wri teLi ne("MvSi ngleton.MySingleton!)"): } public void MyMethodO { m_Counter++; Trace. Wri teLi neCCounter = " + m_Counter): } public void MyOtherMethodO { m_Counter++; Trace. Wri teLi neCCounter = " + m_Counter): } public void Disposed { Trace. WriteLineC "Si ngl eton. Di sposed"); } } ///////////////// Код клиента /////////////////
Синглетные службы 163 MyContractCllent proxy1 = new MyContractCl ientt); proxy 1. MyMethodt): proxy 1. Cl ose(); MyOtherContractCl 1 ent proxy2 = new MyOtherContractClientt): proxy2. MyOtherMethod (); proxy2. Closet): И Результат MySingl eton. MySingl eton () Counter = 1 Counter = 2 Инициализация синглетной службы Иногда для создания и инициализации синглетной службы оказывается недос- таточно конструктора по умолчанию. Например, для инициализации состояния могут потребоваться некие особые действия или информация, недоступная для клиентов (или не относящаяся напрямую к их работе). Для подобных ситуаций WCF позволяет напрямую создать экземпляр синглетной службы до нормаль- ного создания экземпляра CLR, инициализировать его, а затем открыть хост с этим экземпляром как синглетную службу. У класса ServiceHost имеется спе- циальный конструктор, которому при вызове передается объект: public class ServiceHost : ServiceHostBase.... public ServiceHost(object singletoninstance. params Uri[] baseAddresses): public virtual object Singletoninstance (get:} II... ) Учтите, что передаваемый объект должен быть настроен как синглетный эк- земпляр. Для примера рассмотрим код в листинге 4.7. Класс MySingleton снача- ла инициализируется, а затем передается в качестве синглетного экземпляра при создании хоста. Листинг 4.7. Инициализация и создание хоста для синглетного экземпляра II Код службы [ServiceContract] interface IMyContract I [Operationcontract] void MyMethodt): ) [ServiceBehavior (InstanceContextMode = InstanceContextMode. Single)] class MySingleton : IMyContract ( int m_Counter = 0: public int Counter I продолжение &
164 Глава 4. Управление экземплярами get { return m_Counter; } set { m_Counter = value: } } public void MyMethodO { m_Coiinter++; Trace. WrlteLlneCCounter = " + m_Counter): } } // Код хоста MySingleton singleton = new MyS1ngleton(): singleton.Counter = 42: ServiceHost host = new ServlceHost(slngleton); host.Open(); // Блокирующие вызовы host.CloseO; // Клиентский код MyContractCHent proxy = new MyContractC11ent(): proxy.MyMethod(); proxy.Close(): // Результат Counter = 43 Возможно, при таком способе инициализации и хостинга синглетного эк- земпляра вы также захотите иметь возможность обратиться к нему напрямую на стороне хоста. Для этого в WCF предусмотрено свойство Singletoninstance объекта ServiceHost. Любой участник цепочки вызовов, ведущей от вызова опе- рации к синглетному экземпляру, всегда может обратиться к хосту через свой- ство Host контекста операции (свойство доступно только для чтения): public sealed class Operationcontext : ... { public ServIceHostBase Host {get:} //... } Получив ссылку на синглетный экземпляр, можно работать с ним напря- мую: ServiceHost host = Operationcontext.Current.Host as ServiceHost; Debug.Assert(host != null); MySingleton singleton - host.Singletoninstance as MySingleton: Debug.Assert(singleton !=null): singleton.Counter = 388; Если при создании хоста синглетный экземпляр не был передан, Singletoninstance возвращает null.
Синглетные службы 165 Класс ServiceHost<T> Мы можем расширить класс ServiceHost<T> (см. главу 1) и включить в него ти- пизованную инициализацию и работу с синглетными экземплярами: public class ServiceHost<T> : ServiceHost ( public ServiceHost(T si ngl eton. pa rams Uri[] baseAddresses) : base(singleton.baseAddresses) (1 public virtual T Singleton ( get ( if(SingletonInstance == null) return default(T); } return (T)Singletonlnstance: ) //... ) Параметр типа обеспечивает типизацию объекта, используемого для конст- руирования: MySingleton singleton = new MySingleton(): singleton. Counter = 42: Servi ceHost<My Si ngl eton> host = new ServiceHost<MySingleton>(singleton): host.OpenO; а также объекта, возвращаемого свойством Singleton: Servi ceHost<My Si ngl eton> host = Operationcontext.Current.Host as ServiceHost<MySingleton>; Debug. Assert (host != null): host, si ngl eton. Counter = 388: ПРИМЕЧАНИЕ------------------------------------------------------------------------------- Шаблон InProcFactory<T>, представленный в главе 1, также расширяется аналогичным образом для инициализации синглетных экземпляров. Выбор синглетного режима Синглетная служба — заклятый враг масштабируемости. Причина кроется в синхронизации состояния синглетного экземпляра. Раз вы создаете синглет- ную службу, вероятно, она обладает каким-то ценным состоянием, которое должно совместно использоваться несколькими клиентами. Проблема заключа- ется в том, что подключения нескольких клиентов к синглетному экземпляру могут происходить одновременно, и входящие клиентские вызовы будут обра- батываться в нескольких рабочих программных потоках. Синглет должен син- хронизировать доступ к своему состоянию, чтобы избежать его порчи. Соответ- ственно это означает, что в любой момент времени с ним может работать только один клиент. Такое ограничение снижает производительность, скорость
166 Глава 4. Управление экземплярами отклика и доступность до такого уровня, что синглетные службы становятся неприемлемы для систем сколько-нибудь нормального размера. Например, если операция с синглетом занимает 1/10 секунды, клиент сможет обслуживать только 10 клиентов в секунду. При большем количестве клиентов (допустим, 20 или 100) производительность системы становится неприемлемой. В общем случае синглетный объект следует использовать в том случае, если он хорошо соответствует натуральному синглету в предметной области. Нату- ральным синглетом называется ресурс, по своей природе уникальный и непо- вторимый. Примером натурального синглета служит глобальный журнал, в ко- тором все службы должны регистрировать свои действия, единственный комму- никационный порт или одно физическое устройство. Избегайте использования синглетов там, где существует хотя бы ничтожная вероятность «размножения» службы в будущем — например, установки второго аналогичного устройства или второго коммуникационного порта. Причина очевидна: если все клиенты неявно зависят от подключения к единственному и неповторимому экземпля- ру, то при появлении другого экземпляра службы клиентам вдруг потребуется способ подключения к нужному экземпляру. Это может иметь серьезные по- следствия для программной модели приложения. Из-за этих ограничений я ре- комендую по возможности избегать синглетов и поискать средства совместно- го использования состояния синглета вместо самого синглетного экземпляра. Впрочем, даже с учетом сказанного существуют случаи, в которых применение синглетных экземпляров оправдано. Демаркационные операции Иногда контракт с состоянием подразумевает определенный порядок вызова операций. Одни операции не должны вызываться первыми, другие обязательно вызываются в последнюю очередь. Для примера возьмем следующий контракт, предназначенный для обработки клиентских заказов: [ServiceContract(SessionMode = SessionMode.Required)] interface lOrderManager { [Operationcontract] void SetCustomerId(int customerld): [OperationContract] void Addltem(int itemld): [OperationContract] decimal GetTotaK); [OperationContract] bool ProcessOrders(); } Контракт обладает следующими ограничениями: клиент должен передать свой идентификатор (SetCustomerld) в первой операции сеанса, в противном случае никакие другие операции выполняться не могут; добавление позиций
Демаркационные операции 167 (Additem) и вычисление итоговой суммы (GetTotal()) выполняются в произволь- ном порядке и так часто, как пожелает клиент; обработка заказа (ProcessOrders()) завершает сеанс и поэтому должна выполняться в последнюю очередь. WCF позволяет проектировщикам контрактов особым образом помечать операции контрактов, которые могут (или не могут) завершать сеанс. Для этого используются свойства Islnitiating и IsTerminating атрибута Operationcontract: [Attri buteUs age (Attri buteTargets. Method) ] public sealed class OperrationContractAttrlbute : Attribute ( public bool Islnitiating {get:set;} public bool IsTerminating {get:set:} //... ) Применение этих свойств как бы обозначает логические границы сеанса, по- этому я буду называть подобные операции демаркационными. Во время загруз- ки службы (или при использовании посредника на стороне клиента), если этим свойствам задаются значения, отличные от значений по умолчанию, WCF про- веряет, чтобы демаркационные операции были частью контракта с обязатель- ной сеансовой поддержкой (SessionMode задано значение SessionMode.Required); в противном случае будет выдано исключение InvalidOperationException. И сеан- совые, и синглетные службы могут реализовать контракты с демаркационными операциями для управления своими клиентскими сеансами. По умолчанию свойство Islnitiating задается равным true, а свойство IsTermi- nating - равным false. Соответственно, следующие два определения эквива- лентны: [ServiceContract(Sessi onMode = SessionMode.Required)] interface IMyContract [Operationcontract ] void MyMethod(); } [ServiceContract(SessionMode = SessionMode. Requi red)] interface IMyContract I [OperationContract(Islnitiating = true. IsTerminating = false)] void MyMethodO; Как видно из листинга, оба свойства могут быть заданы для одного метода. Кроме того, по умолчанию операции не задают границы сеансов — они могут вызываться первыми, последними или между другими операциями в сеансе. Только использование других значений позволяет указать, что метод не должен вызываться первым или что он должен вызываться последним (или и то и дру- гое одновременно): [ServiceContract (SessionMode = SessionMode. Requi red)] interface IMyContract { LOperat 1 onContract ]
void StartSess1on(); [OperatlonContractdsInltiatlng = false)] void CannotStart(); [OperatlonContractdsTermlnatlng = true)] void EndSess1on(); [OperatlonContractdsInltiatlng = false. IsTermlnatlng = true)] void CannotStartCanEndSession(); } Возвращаясь к примеру с обработкой заказов, мы можем использовать де- маркационные операции для обеспечения установленных ограничений: [Serv1ceContract(Sess1onMode = SessionMode.Required)] Interface lOrderManager { [OperationContract] void SetCustomerlddnt customerld): [OperatlonContractdsInltiatlng = false)] void Addltem(1nt Itemld); [OperatlonContractdsInltiatlng = false)] decimal GetTotalO: [OperatlonContractdsInltiatlng = false. IsTermlnatlng = true)] bool ProcessOrders(): } // Клиентский код OrderManagerCllent proxy = new OrderManagerCl1ent(): proxy.SetCustomer Id(123): proxy.AddItem(4); proxy.AddItem(5); proxy.AddItem(6): proxy.Processorders(); proxy.Close(); Если свойство Islnitiating равно true (по умолчанию), это означает, что опе- рация может начать новый сеанс, если это первый вызов метода со стороны клиента, но если она вызывается после другой операции, то становится ча- стью текущего сеанса. Если Islnitiating задано значение false, это означает, что операция никогда не может вызываться первой в новом сеансе и может лишь являться частью текущего сеанса. Если свойство IsTerminating равно false (по умолчанию), это означает, что се- анс продолжается и после возврата управления операцией. Если же IsTermi- nating задается значение true, это означает, что возврат управления из вызова метода завершает сеанс, a WCF асинхронно уничтожает экземпляр службы. Клиент не сможет обратиться к посреднику с дополнительными вызовами. Уч- тите, что клиент при этом все равно должен закрыть посредника.
Деактивизация экземпляров 169 ПРИМЕЧАНИЕ------------------------------------------------------------------- бели посредник генерируется для службы, использующей демаркационные операции, импортиро- ванное определение контракта содержит значения свойств. Кроме того, WCF обеспечивает раз- дельную демаркацию на стороне клиента и службы. Деактивизация экземпляров Методика управления экземплярами служб с поддержкой состояния, описан- ная ранее, связывает клиента (или клиентов) с экземпляром службы. Тем не менее реальная картина выглядит несколько сложнее. Вспомните, о чем говори- лось в главе 1: каждый экземпляр службы существует в определенном контек- сте (рис. 4.2). Рис. 4.2. Контексты и экземпляры В действительности сеанс ассоциирует клиентские сообщения не с экземп- ляром, а с контекстом, которому этот экземпляр принадлежит. В начале сеанса хост создает новый контекст. При завершении сеанса контекст уничтожается. По умолчанию срок жизни контекста совпадает со сроком жизни экземпляра, находящегося под его управлением. Тем не менее в целях оптимизации WCF предоставляет проектировщику службы возможность разделения двух жизнен- ных циклов и деактивизации экземпляра отдельно от его контекста. Реально WCF также позволяет создать контекст, вообще не имеющий экземпляра, как показано на рис. 4.2. Я называю этот способ управления экземплярами деакти- вацией контекста. Обычно для управления деактивизацией контекста ис- пользуется свойство ReleaselnstanceMode атрибута OperationBehavior: public enum ReleaselnstanceMode I None, BeforeCa11. AfterCall. BeforeAndAfterCa 11. ) [Attri buteUsage (At t ri buteTargets. Method) ] public sealed class OperationBehaviorAttribute : Attribute,... ( public ReleaselnstanceMode ReleaselnstanceMode (get:set;} //... )
170 Глава 4. Управление экземплярами Различные значения перечисляемого типа ReleaselnstanceMode определяют момент освобождения экземпляра по отношению к вызову метода: до, после, до и после или вообще без освобождения. При освобождении экземпляра, если служба поддерживает интерфейс IDisposable, будет вызван метод Dispose(), при- чем вызов производится в контексте операции. Как правило, деактивизация экземпляра применяется либо к отдельным (но не ко всем) методам службы, либо для разных методов применяются разные ре- жимы: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [Operationcontract] void MyMethod; [Operationcontract] void MyOtherMethod: } class MyService IMyContract.IDisposable { [OperationBehavior(ReleaseInstanceMode = ReleaselnstanceMode.AfterCall)] public void MyMethod() {• • •} public void MyOtherMethod() {...} public void Disposed } Причина выборочного применения понятна: при постоянном применении мы фактически получаем службу с активизацией уровня вызова и ее проще было бы сразу настроить как таковую. Если применение деактивизации пред- полагает определенный порядок вызовов, попробуйте обеспечить его при помо- щи демаркационных операций. Режим ReleaselnstanceMode.None По умолчанию свойство ReleaselnstanceMode равно ReleaselnstanceMode.None, по- этому следующие два определения эквивалентны: [OperationBehavior(ReleaseInstanceMode = ReleaselnstanceMode.None)] public void MyMethodO {•••} Сеанс Вызовы методов —“ попе “ None ж* None —w Экземпляр Рис. 4.3. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode.None
Деактивизация экземпляров 171 public void MyMethodO I.-} Значение ReleaselnstanceMode.None означает, что жизненный цикл экземпля- ра не зависит от вызова, как показано на рис. 4.3. Режим ReleaselnstanceMode.BeforeCall Если метод настроен в режиме ReleaselnstanceMode.BeforeCall, а в сеансе уже су- ществует экземпляр, перед перенаправлением вызова WCF деактивизирует его, создает новый экземпляр и предоставляет ему возможность обслужить вызов, как показано на рис. 4.4. Сеанс Вызовы методов —► BeforeCall ► None ► None —► Экземпляр Рис. 4.4. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode. BeforeCall WCF деактивизирует экземпляр и вызывает Dispose() перед вызовом метода в программном потоке входящего вызова, с блокировкой работы клиента. Тем самым гарантируется, что деактивизация будет выполнена до вызова, а не одно- временно с ним. Режим ReleaselnstanceMode.BeforeCall проектировался для опти- мизации методов типа Ореп() — методов, которые захватывают ценные ресурсы, но при этом должны освобождать выделенные ранее ресурсы. Вместо того что- бы захватывать ресурсы в начале сеанса, вы дожидаетесь вызова метода Ореп(), азатем одновременно освобождаете ранее выделенные и выделяете новые ре- сурсы. После вызова Ореп() можно переходить к вызову других методов экземп- ляра, обычно настраиваемых в режиме ReleaselnstanceMode.None. Режим ReleaselnstanceMode.AfterCall Если метод настроен в режиме ReleaselnstanceMode.AfterCall, WCF деактивизи- рует экземпляр после вызова, как показано на рис. 4.5. Сеанс Вызовы методов Экземпляр Рис. 4.5. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode.AfterCall
172 Глава 4. Управление экземплярами Схема деактивизации предназначена для оптимизации методов типа Close() — методов, освобождающих ценные ресурсы без ожидания завершения сеанса. Обычно методы ReleaselnstanceMode.AfterCall вызываются для методов, вызываемых после методов, настроенных в режиме ReleaselnstanceMode.None. Режим ReleaselnstanceMode.BeforeAndAfterCall Как подсказывает название, метод, настроенный в режиме ReleaselnstanceMode. BeforeAndAfterCall, сочетает в себе особенности ReleaselnstanceMode.BeforeCall и ReleaselnstanceMode.AfterCall. Если контекст содержал экземпляр перед вызо- вом, то непосредственно после вызова WCF деактивизирует экземпляр, создает новый экземпляр для обслуживания вызова и деактивизирует его после вызова, как показано на рис. 4.6. Сеанс Вызовы методов “ иетогеьан “ BeroreAndAnercall Апсгъан r • । । Экземпляр I • Н FH Н— Н-► Рис. 4.6. Жизненный цикл экземпляров при использовании методов в режиме ReleaselnstanceMode.BeforeAndAfterCall На первый взгляд режим ReleaselnstanceMode.BeforeAndAfterCall может пока- заться лишним, но в действительности он дополняет другие режимы. Он пред- назначен для методов, вызываемых после методов с пометкой Releaselnstance- Mode.BeforeCall или None или перед методами с пометкой ReleaselnstanceMode. AfterCall или None. Представьте ситуацию: сеансовая служба хочет пользоваться преимуществами поведения с контролем состояния (как у служб уровня вызо- ва), при этом удерживая ресурсы только тогда, когда это необходимо для опти- мизации распределения ресурсов и в целях безопасности. Если бы режим Rele- aselnstanceMode.BeforeCall был единственным вариантом, то в течение некоторого времени после вызова ресурсы оставались бы выделенными объекту, но при этом реально не использовались. Аналогичная ситуация возникнет и в том слу- чае, если доступен только вариант ReleaselnstanceMode.AfterCall: перед вызовом возникает период времени, в течение которого ресурс удерживается без исполь- зования. Явная деактивизация Вместо принимаемых на стадии проектирований решений относительного того, какие методы должны использоваться для деактивизации экземпляров, возмо- жен другой вариант: принятие решения деактивизации экземпляра во время выполнения, после возврата из метода. Задача решается вызовом метода ReleaseServiceInstance() для контекста экземпляра. Контекст экземпляра берется из свойства InstanceContext контекста операции:
Деактивизация экземпляров 173 public sealed class Instancecontext : Communicationobject.... I public void ReleaseServiceInstance(); II... ) public sealed class Operationcontext : ... I public Instancecontext Instancecontext (get;) //... ) Пример использования явной деактивизации приведен в листинге 4.8. Лиспжг 4.8. Использование метода ReleaseServiceInstance() [ServiceContract (SessionMode = SessionMode. Requi red)] interface IMyContract I [Operationcontract] void MyMethod(): ) class MyService : IMyContract.IDisposable ( public void MyMethodO ( 11 Выполнение полезной работы, а затем Operat i onContext.Current.Instancecontext.ReleaseServi celnstance(): ) public void Disposed Эффект от вызова ReleaseServiceInstance() сходен с использованием Release- InstanceMode.AfterCall. При использовании в методах с пометкой Releaselnstance- Mode. BeforeCall эффект получается таким же, как в режиме ReleaselnstanceMode. BeforeAndAfterCall. ПРИМЕЧАНИЕ------------------------------------------------------------------------------------- Деактивизация экземпляра действует и на синглетные службы, хотя такое сочетание и не имеет особого смысла — синглет по определению разрешено (и более того, желательно) никогда не деак- тивизировать. Использование деактивизации экземпляров Деактивизация экземпляров относится к средствам оптимизации; ее, как и большинство таких средств, не стоит применять в общем случае. Решение о применении деактивизации принимается только в том случае, если вам не удается добиться заданных целей по быстродействию и масштабируемости, а тщательный анализ и профилирование убедительно доказали, что деактивиза- ция экземпляров способна улучшить ситуацию. Если масштабируемость и про- изводительность являются первоочередными целями, используйте простую схему создания экземпляров на уровне вызовов и избегайте деактивизации экземпляров.
174 Глава 4. Управление экземплярами Регулирование нагрузки Хотя регулирование нагрузки и не относится напрямую к управлению экземп- лярами, она позволяет ограничивать клиентские подключения и ту нагрузку, которую они создают для вашей службы. Тем самым предотвращаются чрез- мерные нагрузки службы, выделяемых и используемых ей ресурсов. Если при включенном регулировании нагрузки превышаются пороговые значения, задан- ные вами, WCF автоматически помещает необработанные вызовы в очередь и последовательно обслуживает их по мере возможности. Если во время ожида- ния на стороне клиента происходит тайм-аут, клиент получает исключение TimeoutException. Регулирование осуществляется на уровне типа службы; други- ми словами, оно распространяется на все экземпляры службы со всеми конеч- ными точками. Для этого оно связывается с каждым диспетчером канала, ис- пользуемым службой. WCF позволяет управлять следующими параметрами использования служ- бы (некоторыми или всеми сразу): О максимальное количество параллельных сеансов — общее количество ожи- дающих обработки клиентов, имеющих сеанс транспортного уровня со служ- бой. Проще говоря, параметр определяет максимальное число ожидающих клиентов, использующих привязку TCP, IPC или любую из привязок WSc сеансами. При использовании базовой привязки или любых привязок WS без транспортного сеанса это число ни на что не влияет из-за самой природы базового соединения HTTP, не обладающего состоянием. Значение по умол- чанию равно 10; О максимальное количество параллельных вызовов — общее число вызовов, обрабатываемых в данный момент всеми экземплярами службы. Обычно чис- ло должно составлять от 1 до 3 процентов максимального количества парал- лельных сеансов. Значение по умолчанию равно 16; О максимальное количество параллельных экземпляров — фактически обозна- чает общее количество одновременно существующих контекстов. Значение по умолчанию не ограничено. Схема отображения экземпляров на контек- сты определяется режимом управления контекстами, а также правилами де- активизации контекстов и экземпляров. Для служб уровня сеанса макси- мальное количество экземпляров совпадает с общим числом параллельных активных экземпляров и общим числом параллельных сеансов. В случае применения деактивизации экземпляров количество последних может быть значительно ниже числа контекстов, но при этом клиенты все равно будут блокироваться, если количество контекстов достигнет максимального числа параллельных экземпляров. Для служб уровня вызова количество экземпля- ров фактически совпадает с количеством параллельных вызовов. Соответст- венно, максимальное количество экземпляров для службы уровня вызова является меньшим из двух чисел: максимального количества параллельных экземпляров и максимального количества параллельных вызовов. Для синг-
Регулирование нагрузки 175 летных служб максимальное количество параллельных экземпляров игнори- руется, поскольку экземпляр все равно может быть только один. ВНИМАНИЕ-------------------------------------------------------------------- Регулирование нагрузки относится к аспектам хостинга и развертывания. В процессе проектирования службы не следует делать никаких допущений относительно конфигурации ее регулирования нагруз- ки-всегда считайте, что служба берет на себя всю тяжесть клиентской нагрузки. Это объясняет, по- чему в WCF нет атрибута для такого аспекта поведения, хотя реализовать его было бы несложно. Настройка регулирования нагрузки Регулирование нагрузки обычно настраивается администратором в конфигура- ционном файле. Это позволяет по-разному регулировать нагрузку для одного кода службы в разные моменты времени или в разных местах развертывания. Хост также может настраивать регулирование нагрузки на программном уров- не, в зависимости от критериев, проверяемых на стадии выполнения. Административное регулирование нагрузки В листинге 4.9 показано, как регулирование нагрузки настраивается в конфигу- рационном файле хоста. Тег behaviorConfiguration наделяет службу пользова- тельским аспектом поведения, в котором задаются пороговые значения. Листинг 4.9. Административное регулирование нагрузки <system. servi ceModel > <services> <service name = "MyService" behaviorConfiguration = "ThrottledBehavior"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "ThrottledBehavior"> <serviceThrottl ing maxConcurrentCalIs = "12" maxConcurrentSessions = "34" maxConcurrentlnstances = "56" /> </behavior> </serviceBehaviors> </behaviors> </system. servi ceModel > Программное регулирование нагрузки Хостовой процесс может регулировать нагрузку службы на программном уров- не, в зависимости от критериев времени выполнения. Сделать это можно только до открытия хоста. Хотя хост может переопределить параметры регулирования нагрузки, заданные в конфигурационном файле, обычно программное регули- рование выполняется только при отсутствии определений в конфигурационном файле.
176 Глава 4. Управление экземплярами Класс ServiceHostBase предоставляет свойство Description типа ServiceDescrip- tion: public abstract class ServiceHostBase: ... { public ServiceDescription Description {get:} //... } Свойство, как следует из его названия, содержит описание службы со всеми ее аспектами поведения. Тип ServiceDescription содержит свойство Behaviors типа KeyedByTypeCollection<I> с использованием IServiceBehavior в качестве обобщен- ного параметра. В листинге 4.10 показано, как выполняется программная настройка регули- рования нагрузки. Листинг 4.10. Программное регулирование нагрузки ServiceHost host = new ServiceHost(typeof(MyServiсе)); ServiceThrottlingBehavior throttle: throttle - host.Description.Behaviors.Find<ServiceThrottlingBehavior>(): if(throttle == null) { throttle = new ServiceThrottlingBehavior(): throttle.MaxConcurrentCalIs = 12: throttle.maxConcurrentSessions = 34; throttle.MaxConcurrentlnstances = 56; host.Descri pti on.Behaviors.Add(throttle): } host.Open(); Сначала код хостинга убеждается в том, что аспект регулировки нагрузки не был задан в конфигурационном файле. Задача решается вызовом метода Find<T>() типа KeyedByTypeCollection<I>, с передачей ServiceThrottlingBehavior в качестве па- раметра типа. Класс ServiceThrottlingBehavior определяется в пространстве имен System.Ser- viceModel.Design: public class ServiceThrottlingBehavior : IServiceBehavior { public int MaxConcurrentCalls {get;set:} public int MaxConcurrentSessions {get;set:} public int MaxConcurrentlnstances //... } Если возвращаемое значение throttle равно null, код хостинга создает новый экземпляр ServiceThrottlingBehavior, задает его параметры и включает в коллек- цию аспектов поведения в описание службы.
Регулирование нагрузки 177 Класс ServiceHost<T> Очередное усовершенствование класса ServiceHost<T> автоматизирует выполне- ние кода, приведенного в листинге 4.10. Новая версия представлена в листин- ге4.11. Листинг 4.11. Расширение ServiceHost<T> для регулирования нагрузки public class ServiceHost<T> : ServiceHost I public void SetThrottle(int maxCalls.int maxSessions,int maxinstances) ( ServiceThrottlingBehavior throttle = new ServiceThrottlingBehaviorO: throttle.MaxConcurrentCal Is = maxCalls: throttle.MaxConcurrentSessions = maxSessions; throttle. MaxConcurrent Instances = maxinstances; SetThrottle( throttle); ) public void SetThrottle(ServiceThrottlingBehavior serviceThrottle) SetThrott 1 e (serv i ceTh rott 1 e. f a 1 se); I public void SetThrottle(ServiceThrottlingBehavior serviceThrottle. bool overrideConfig) ( iftState =- CommunicationState.Opened) ( throw new InvalidOperationExceptionCHost Is already opened"): } ServiceThrottlingBehavior throttle = Descri ption.Behaviors.Find<Servi ceThrottli ngBehavi or>(); if(throttle != null && overrideConfig == false) { return; } if(throttle != null) //overrideConfig == true, удалить конфигурацию { Description.Behaviors.Remove!throttle): } iffthrottle == null) { Descri pti on.Behaviors.Add(servi ceThrottle): } ) public ServiceThrottlingBehavior ThrottleBehavior ( get { return Description.Behaviors.Find<ServiceThrottlingBehavior>(); } } //... } Класс ServiceHost<T> предоставляет метод SetThrottle, которому передается объект регулирования нагрузки, а также логический флаг, который указывает,
178 Глава 4. Управление экземплярами нужно ли заменять настроенные значения (если они имеются). По умолчанию (в перегруженной версии SetThrottle()) используется значение false. Метод SetThrottle() проверяет, что хост еще не был открыт; для этого используется свойство State базового класса Communicationobject. Если настроенный объект конфигурации должен быть заменен, SetThrottle() удаляет его из описания. Ос- тальной код листинга 4.11 похож на листинг 4.10. Пример использования Ser- viceHost<T> для программного задания регулирования нагрузки: ServiceHost<MyService> host = new ServiceHost<MyService>(); host SetThrottle(12.32.56); host.Open(): ПРИМЕЧАНИЕ------------------------------------------------------------------------- Шаблон InProcFactory<T> (см. главу 1) аналогичным образом расширяется для поддержки регули- рования настройки. Чтение параметров регулирования нагрузки Разработчик службы может прочитать пороговые значения на стадии выполне- ния; обычно это делается для диагностических и аналитических целей. Во вре- мя выполнения экземпляр службы обращается к пороговым значениям регули- рования нагрузки из диспетчера. Прежде всего получите ссылку на хост от контекста операции. Базовый класс хоста ServiceHostBase содержит свойство ChannelDispatchers, доступное только для чтения: public abstract class ServiceHostBase : CommunicationObject.... { public Channel Dispatchercollection ChannelDispatchers {get:} //... } Тип ChannelDispatchers представляет собой типизованнную коллекцию объ- ектов ChannelDispatcherBase: public class Channel Dispatchercollection : Synchroni zedCol1ecti on<ChannelDi spatcherBase> {•••} Элементы коллекции относятся к типу ChannelDispatcher. Класс ChannelDis- patcher обладает свойством ServiceThrottle: public class ChannelDispatcher : ChannelDispatcherBase { public ServiceThrottle ServiceThrottle {get:set:} //... } public sealed class ServiceThrottle { public int MaxConcurrentCalls {get:set;} public int MaxConcurrentSessions {get:set:} public int MaxConcurrentlnstances
Регулирование нагрузки 179 (get:set:} ) Настроенные значения параметров регулирования хранятся в объекте Ser- viceThrottle: class MyService : ( public void MyMethodO // Контрактная операция ( Channel Dispatcher dispatcher - Operationcontext.Current. Host.ChannelDispatchers[O] as ChanneDispatcher: ServiceThrottle serviceThrottle = dispatcher.ServiceThrottle: Trace. WriteLine( "Max Calls = " + serviceThrottle.MaxConcurrentCalIs): Trace.WnteLineCMax Sessions = " + servi ceThrottle.Ma xConcurrentSessions): Trace.WriteLine("Max Instances = " + serviceThrottle.MaxConcurrentInstances): ) Обратите внимание: служба может только читать параметры, но не может их изменить. При попытке изменения параметров регулирования происходит ис- ключение InvalidOperation Exception. И снова класс ServiceHost<T> сделает работу с регулированием нагрузки бо- лее удобной. Сначала в класс добавляется свойство ServiceThrottle: public class ServiceHost<T> • ServiceHost ( public ServiceThrottle Throttle get ( 1f(State != CommunicatlonState.Opened) throw new InvalidOperationExceptionC'Host is not opened"): Channel Dispatcher dispatcher = Operationcontext.Current.Host.Channel Di spatchers[O] as ChannelDispatcher: return dispatcher.ServiceThrottle; ) //... Затем свойство ServiceThrottle используется для обращения к заданным пара- метрам регулирования: // Код хостинга ServiceHost<MyService> host = new ServiceHost<MyService>(): host.OpenO: class MyService : ...
180 Глава 4. Управление экземплярами public void MyMethodO // Контрактная операция ( ServiceHost<MyService> host = Operationcontext.Current. Host as ServiceHost<MyService>: ServiceThrottle serviceThrottle = host.Throttle; } } ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- Обращение к свойству Throttle класса ServiceHost<T> возможно только после открытия хоста. Эго объясняется тем, что диспетчерская коллекция инициализируется только после открытия хоста. Регулируемые подключения в привязке При использовании привязок TCP и именованных каналов также можно задать максимальное количество подключений для определенной конечной точки в самой привязке. Оба класса, NetTcpBinding и NetNamedPipeBinding, содержат свойство MaxConnections: public class NetTcpBinding : Binding.... public int MaxConnections {get;set;} } public class NetNamedPipeBinding : Binding.... { public int MaxConnections {get;set;} } На стороне хоста свойство задается либо на программном уровне, либо в конфигурационном файле: <bindings> <netTcpBinding> <binding name = "TCPThrottle" maxConnections » ,,25"/> </netTcpBinding> </bindings> Значение MaxConnections по умолчанию равно 10. Если максимальное коли- чество подключений задано и на уровне привязки и в аспекте поведения служ- бы, WCF выбирает меньшее из двух значений.
Операции I В классических моделях объектно- или компонентно-ориентированного про- граммирования предусмотрен только один способ вызова метода клиентом: клиент выдает вызов, блокируется на время вызова и продолжает выполнение после возврата управления методом. Все остальные модели вызова приходится реализовывать вручную, часто с потерями для производительности и качества. Хотя WCF поддерживает классическую модель вызова, наряду с ней имеется встроенная поддержка дополнительных типов операций: односторонние вызо- вы для операций, выполняемых по принципу «вызвал и забыл», дуплексный обратный вызов для обратной передачи управления службой клиенту, а также потоковые вызовы, позволяющие клиенту и службе справляться с высокими нагрузками. В общем случае тип используемой операции входит в контракт службы и является неотъемлемой частью ее архитектуры. Тип операции даже накладывает некоторые ограничения на допустимые привязки. Соответственно, клиенты и службы должны изначально проектироваться с расчетом на опреде- ленный тип операции, и вам не удастся легко переключиться на другой тип опе- раций. Эта глава посвящена различным способам вызова операций WCF и ре- комендациям по их проектированию. Два других режима вызова операций — асинхронный вызов и очереди — рассматриваются в следующих главах1. Операции «запрос-ответ» Операции всех контрактов, приводившихся в примерах предыдущих глав, отно- сились к типу «запрос-ответ». Как подсказывает название, клиент выдает за- прос в форме сообщения и блокируется вплоть до получения ответного сообще- ния. Если служба не отвечает в течение интервала тайм-аута (по умолчанию — одна минута), клиент получает исключение TimeoutException. Режим «запрос- ответ» используется по умолчанию. Программирование операций в этом режи- ме получается достаточно простым и напоминает классическую модель «кли- ент-сервер». Возвращаемое сообщение с результатами преобразуется в обыч- 1 Глава содержит выдержки из моей статьи «WCF Essentials: What You Need to Know About One-Way Calls, Callbacks, and Events» из «MSDN Magazine», октябрь 2006 г.
182 Глава 5. Операции ные возвращаемые значения метода. Если в коммуникациях или на стороне службы возникнут какие-либо исключения, посредник выдает исключение на стороне клиента. Операции «запрос-ответ» поддерживаются всеми привязками, кроме NetPeerTcpBinding и NetMsmqBinding. Односторонние операции В некоторых случаях операция не имеет возвращаемого значения, а клиента не интересует, как завершился вызов — успехом или неудачей. Для поддержки по- добных вызовов по принципу «вызвал и забыл» в WCF предусмотрены одно- сторонние (one-way) операции. Когда клиент выдает вызов, WCF генерирует сообщение с запросом, но ответное сообщение клиенту не возвращается. Как следствие, односторонние операции не могут возвращать данные, а любые ис- ключения, инициированные на стороне сервера, не попадают на сторону клиен- та. В идеальном случае при вызове одностороннего метода клиент блокируется на минимальное время, необходимое для передачи вызова. Тем не менее на практике односторонние вызовы не тождественны асинхронным. Когда одно- сторонний вызов достигает службы, он может не обрабатываться сразу, а по- пасть в очередь на стороне службы — все зависит от настроенного режима управления параллельной обработкой (этот режим и односторонние вызовы подробно рассматриваются в главе 8). Количество сообщений (как односторон- них операций, так и операций «запрос-ответ»), помещаемых службой в очередь, определяется настройкой канала и режимом надежности. Если количество со- общений в очереди превысит ее емкость, клиент блокируется даже в случае од- ностороннего вызова. Впрочем, после постановки вызова в очередь (как это обычно происходит) блокировка снимается, и клиент продолжает выполнение, пока служба занимается обработкой операции в фоновом режиме. Односторон- ние операции поддерживаются всеми привязками WCF. Настройка односторонних операций Атрибут Operationcontract содержит логическое свойство IsOneWay: [AttributeUsage(AttributeTargers.Method)] public sealed class OperatlonContractAAttrlbute : Attribute { public bool IsOneWay {get;set;} //... } По умолчанию свойство IsOneWay равно false, что означает операцию «за- прос-ответ» (стандартный для WCF режим). Если задать IsOneWay истинное значение, метод становится односторонней операцией: [ServiceContract] Interface IMyContract { [Operationcontract(IsOneWay » true)] void MyMethodO;
Односторонние операции 183 При вызове односторонней операции от клиента не требуется никаких осо- бых действий. Значение свойства IsOneWay отражено в метаданных службы. Уч- тите, что определение контакта службы и определение, импортированное кли- ентом, должны обладать одинаковыми значениями IsOneWay. Поскольку односторонние операции не требуют ответа, возвращать из них какие-либо значения или результаты бессмысленно. Например, следующее оп- ределение односторонней операции, возвращающей значение, недействительно: ' //Недействительный контракт [SeMceContf act] interface IMyContract. I • [OperationContract(IsOneWay -- true)] int MyMethodt): ) WCF обеспечивает принудительное выполнение этого требования, проверяя сигнатуру метода при загрузке хоста. В случае несоответствия инициируется исключение InvalidOperationException. Односторонние операции и надежность Из того факта, что клиента не интересует результат вызова, вовсе не следует, что клиента не интересует, произошел вызов пли нет. В общем случае надеж- ность для служб должна быть включена, даже для односторонних вызовов — это гарантирует доставку запроса службе. Однако для односторонних вызовов клиенту может быть (а может и не быть) безразличен порядок вызова односто- ронних операций. Это одна из основных причин, по которым WCF позволяет отделить включение падежной доставки от включения упорядоченной доставки и выполнения. Разумеется, все подробности должны быть заранее согласованы между клиентом и службой; в противном случае конфигурации привязки не совпадут. Односторонние операции и сеансовые службы WCF позволяет создать сеансовый контракт с односторонними операциями: [ServiceContract (SessionMode = SessionMode.Required) J interface IMvContract ( [OperationContract (IsOneWay = true)] int MyMethodt); Если клиент выдает односторонний вызов, а затем закрывает посредника во время выполнения метода, то клиент блокируется до завершения операции. Тем не менее я считаю, что в общем случае односторонние операции в сеан- совых контрактах свидетельствуют о неграмотном проектировании. Дело в том, что сеансовая поддержка обычно подразумевает, что служба поддерживает со- стояние по поручению клиента. Любые исключения, происходящие при одно- стороннем вызове, могут нарушить состояние, но клиент об этом не узнает. Кроме того, клиент (или служба) обычно выбирает сеансовое взаимодействие
184 Глава 5. Операции из-за того, что контракт подразумевает жесткую последовательность переходов по конечному автомату состояний. Односторонние вызовы плохо укладывают- ся в такую модель. Соответственно, я рекомендую применять односторонние операции только в службах уровня вызова и синглетных службах. Если односторонние операции все же применяются в сеансовых контрактах, постарайтесь сделать так, чтобы односторонней была только последняя опера- ция, завершающая сеанс (проследите за соблюдением правил одностороннего вызова — в частности, возврата void). Чтобы обеспечить соблюдение требова- ний, воспользуйтесь демаркационными операциями: [ServiceContract(SessionMode = SessionMode.Required)] interface lOrderManager 4 { [Operationcontract] void SetCustomerId(int customerld); [OperationContractdsInitiating = false)] void Addltem(int itemld): [OperationContractdsInitiating = false)] decimal GetTotalO: [OperationContractdsOneWay - true.Islnitiating = false. IsTerminating = true)] void ProcessOrders(); } Односторонние операции и исключения Было бы неверно воспринимать односторонние операции как улицу с односто- ронним движением или «черную дыру», в которой все бесследно исчезает. Во-первых, если при вызове односторонней операции произойдет ошибка, свя- занная с коммуникационными проблемами (неверный адрес, недоступность хоста), то на стороне клиента, пытающегося вызвать операцию, будет выдано исключение. Во-вторых, в зависимости от режима управления экземплярами службы и привязки исключения на стороне службы могут распространяться на клиента. В следующем описании предполагается, что служба не инициирует FaultException или производных исключений (см. главу 6). Службы уровня вызова В случае служб уровня вызова без поддержки транспортного сеанса (например, при использовании привязок BasicHttpBinding или WSHttpBinding без надежной доставки сообщений и безопасности), если при вызове односторонней опера- ции происходит исключение, оно не отражается на клиенте, и клиент может продолжить обращаться с вызовами к тому же экземпляру посредника: [ServiceContract] interface IMyContract { [OperationContractdsOneWay = true)] int MyMethodWithError(): [Operationcontract]
Односторонние операции 185 int MyMethodWi thoutError(): ) class MyService : IMyContract I public void MethodWithErrorO ( throw new ExceptlonO; ) public void MethodW1thoutError() () ) //На стороне клиента при использовании базовой привязки: MyContractClient proxy = new MyContractClient(): proxy.MethodWI thError (): proxy.MethodWithoutError(); proxy.Close(): Тем не менее при использовании привязки WSHttpBinding с безопасностью, или NetTcpBinding без надежной доставки сообщений, или NetNamedPipeBinding, исключения на стороне службы (в том числе инициированные в результате од- носторонних операций) приводят к отказу канала, и клиент не сможет обра- титься с новыми вызовами к тому же экземпляру посредника: [ServiceContract] interface IMyContract I [Operationcontract (IsOneWay = true)] int MyMethodWi thError(): [Operationcontract] int MyMethodWi thoutError(): I class MyService : IMyContract I public void MethodWithErrorO I throw new ExceptlonO; I public void MethodWithoutErrorO I) I // На стороне клиента при использовании привязки TCP или IPC: MyContractClient proxy = new MyContractClient(): proxy. MethodWi thError (); try ( proxy.MethodWIthoutError(): // Произойдет исключение из-за отказа канала proxy.CloseO: } catch () Клиенту даже не удастся корректно закрыть посредника. При использовании WSHttpBinding или NetTcpBinding с надежной доставкой исключение не приведет к отказу канала, и клиент сможет продолжить обра- щаться с вызовами.
186 Глава 5. Операции На мой взгляд, подобные расхождения выглядят по меньшей мере стран- но — и не только потому, что выбор привязки не должен влиять на клиентский код, но и потому, что они нарушают семантику односторонних операций - вы- зывающая сторона узнает о проблемах службы при одностороннем вызове. ПРИМЕЧАНИЕ --------------------------------------------------------- Синглетная служба без поддержки сеанса в этом отношении ведет себя аналогично службе уровня вызова. Сеансовые службы и односторонние исключения Когда доходит до инициирования исключений сеансовыми службами в одно- сторонних методах, ситуация становится еще сложнее. С NetTcpBinding и Net- NamedPipeBinding исключение завершает сеанс; WCF уничтожает экземпляр службы и инициирует отказ канала. Последующие вызовы операции с исполь- зованием того же посредника приводят к исключению CommunicationException (или CommunicationObjectFaultedException, при включении надежной доставки для привязок TCP и WS), потому что ни сеанса, ни экземпляра службы более не существует. Если Close() — единственный метод, вызываемый для посредни- ка после исключения, вызов Close() инициирует CommunicationException (или CommunicationObjectFaultedException). Если клиент закрывает посредника перед возникновением ошибки, вызов Close() блокируется вплоть до ошибки, после чего Close() инициирует исключение. Это малопонятное поведение — еще одна причина избегать односторонних вызовов для сеансовых служб В случае привязок WS с транспортным сеансом исключение приводит кот- казу канала, после чего клиент не может обращаться к посреднику с новыми вызовами. Немедленное закрытие посредника после вызова, инициировавшего исключение, приводит к тем же последствиям, что и с другими привязками. ПРИМЕЧАНИЕ ---------------------------------------------------------- Синглетная служба с поддержкой сеанса в этом отношении ведет себя аналогично службе уровня вызова. Операции обратного вызова WCF дает службе возможность обращаться к клиентам с обратными вызовами. Во время обратного вызова мы имеем дело с зеркальной ситуацией: служба иг- рает роль клиента, а клиент — службы (рис. 5.1). Рис. 5.1. Механизм обратного вызова позволяет службе обращаться с вызовом к клиенту Операции обратного вызова могут использоваться в разных сценариях и приложениях, но они особенно удобны для оповещения клиентов о некото- рых событиях, произошедших на стороне сервера. Не все привязки поддержи-
Операции обратного вызова 187 вают операции обратного вызова — для этой цели могут использоваться при- | вязки с возможностью двустороннего обмена. Например, протокол HTTP не ориентирован на соединение (connectionless), поэтому по своей природе он не может использоваться для обратных вызовов; следовательно, привязки Basic- HttpBinding и WSHttpBinding делают невозможным обратный вызов. WCF обес- печивает поддержку обратного вызова для NetTcpBinding и NetNamedPipeBinding, потому что протоколы TCP и IPC по своей природе поддерживают двусторон- ние коммуникации. Для поддержки обратного вызова через HTTP в WCF име- ется привязка WSDualHttpBinding, которая в действительности создает два кана- ла HTTP: по одному передаются вызовы от клиента к службе, а по другому передаются вызовы от службы к клиенту. Контракт обратного вызова Операции обратного вызова являются частью контракта службы, причем по- следний должен определить свой контракт обратного вызова. Контракт службы может иметь не более одного контракта обратного вызова. После его определе- ния клиенты обязаны поддержать обратный вызов и предоставить конечную точку для каждого обратного вызова. Для определения контракта обратного вызова атрибут ServiceContract предоставляет свойство Callbackcontract типа Туре: [Attributeusage(Attrl buteTargers. Interface|Attrl buteTargers. Cl ass) ] public sealed class OperationContractAAttribute : Attribute I public bool Callbackcontract {get: set;} //... ) При определении контракта службы с контрактом обратного вызова необхо- димо указать атрибуту ServiceContract тип контракта обратного вызова и его оп- ределение, как показано в листинге 5.1. Листинг 5.1. Определение и настройка контракта обратного вызова interface ISomeCa 11 backContract ( [OperationContract] void OnCal 1 back (); ) [ServiceiMract (Callbackcontract = typeofC ISomeCall backContract))] interface IMyContract [OperationContract] void DoSomething(); Обратите внимание: контракт обратного вызова не нужно помечать атрибу- том ServiceContract — присутствие последнего подразумевается при определении контракта обратного вызова, и он будет включен в метаданные службы. Конеч- но, все методы интерфейса обратного вызова могут быть помечены атрибутом OperationContract.
188 Глава 5. Операции После импортирования метаданных контракта обратного вызова клиентом имя импортированного интерфейса будет отличаться от имени исходного опре- деления на стороне службы. Оно образуется из имени интерфейса контракта службы и суффикса Callback. Например, в результате импортирования опреде- лений из листинга 5.1 клиент получит следующие определения: interface IMyContractCallback { [Operationcontract] void OnCal1 back(); } [ServiceContract(CallbackContract - typeof(IMyContractCallback))] interface IMyContract { [Operationcontract] void DoSomething(); } ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- Ради простоты я рекомендую даже на стороне службы присваивать контракту обратного вызова имя, построенное по той же схеме (имя интерфейса контракта службы и суффикс Callback). Настройка обратного вызова на стороне клиента В обязанности клиента входит создание объекта обратного вызова и предостав- ление конечной точки обратного вызова. Вспомните, о чем говорилось в главе 1: из всех логических уровней выполнения к экземпляру службы ближе всего нахо- дится контекст экземпляра. У класса InstanceContext имеется конструктор, кото- рый передает хосту экземпляр службы: public sealed class InstanceContext : CommunicationObject.... { public InstanceContext(object implementation); public object GetServiceInstance(); //... } Все, что требуется от клиента, — создать экземпляр объекта обратного вызо- ва и построить на его основе контекст: class MyCalIback : IMyContractCallback { public void OnCallbackO {•••} } IMyContractCallback callback = new MyCallback(); InstanceContext context = new InstanceContext(callback): Также стоит отметить, что хотя методы обратного вызова находятся на сто- роне клиента, они во всех отношениях являются операциями WCF, а следова- тельно, обладают контекстом вызова, доступным через свойство OperationCon- text.Current.
Операции обратного вызова 189 Дуплексный посредник При любых взаимодействиях с конечной точкой службы, контракт которой оп- ределяет контракт обратного вызова, клиент должен использовать посредника, поддерживающего двустороннюю передачу данных, и передать службе ссылку наконечную точку обратного вызова. Для этого посредник объявляется произ- водным от специализированного класса посредника DupLexCLientBase<T>, пред- ставленного в листинге 5.2. Листинг 5.2. Класс DuplexClientBase<T> public interface IDuplexContextChannel : IContextchannel Instancecontext Call back Instance (get:set:} //... ) public abstract class DuplexCl 1entBase<T> : Cl 1entBase<T> where T : class I protected DuplexCl 1entBase( Instancecontext callbackcontext): protected DuplexCl1entBase(Instancecontext callbackcontext, string endpointName); protected DuplexCl 1entBase(Instancecontext callbackcontext. Binding binding, EndpointAddress remoteAddress); protected DuplexCl 1entBase(object callbackinstance): protected DuplexCl 1entBase(object callbackinstance, string endpointConfigurationName): protected DuplexCl1entBase(object callbackinstance. Binding binding, EndpointAddress remoteAddress): public IDuplexContextChannel InnerDuplexChannel (get:} //... ) Клиент должен передать конструктору DupLexCLientBase<T> контекст элемен- та, под управлением которого находится объект обратного вызова (по аналогии с информацией о конечной точке службы для обычного посредника). Посред- ник конструирует конечную точку на основе контекста обратного вызова, полу- чая информацию о конечной точке обратного вызова из конфигурации конеч- ной точки службы. Конечная точка обратного вызова использует тот же тип привязки (и транспорта), что и исходящий вызов. Что касается адреса, WCF использует имя компьютера клиента и даже выбирает порт при использовании НТРР. Простая передача контекста экземпляра дуплексному посреднику и ис- пользование посредника для обращения к службе открывает доступ к конечной точке обратного вызова на стороне клиента. Для простоты DupLexCLientBase<T> также предоставляет конструкторы, которые получают объект обратного вызо- ва и «заворачивают» его в контекст во внутренней реализации. Если клиенту по какой-либо причине понадобится обратиться к контексту, DupLexCLientBase<T> также предоставляет свойство InnerDuplexChannel типа IDuplexContextChannel, от- крывающее доступ к контексту через свойство Callbackinstance.
190 Глава 5. Операции Если для построения класса посредника для службы с контрактом обрат- ного вызова используется SvcUtil или Visual Studio 2005, сгенерированный класс объявляется производным от DuplexClientBase<T>, как показано в листин- ге 5.3. Листинг 5.3. Автоматически сгенерированный дуплексный посредник partial class MyContractClient : DuplexCl1entBase<IMyContгасt>.IMyContract {} public MyContractClient(InstanceContext callbackcontext) : oase(caIIbackContext) {} public MyContractClient(InstanceCoriext calIcackContext. string endpointName) . base(calIbackContext.endpointName) {} public MyContractClient(InstanceContext callbackcontext, Binding binding, EndpointAddress remoteAddress) : ba s e u:a11 back Context.oi nding.remot eAddress) {} //... public void DoSomething() Channel.DoSomethingf). } } Используя производный класс посредника, клиент конструирует экземпляр объекта обратного вызова, размещает его в контексте, создает посредника и об- ращается с вызовом к службе, таким образом передавая ссылку на конечную точку обратного вызова: class MyCalIback : IMyContractCalIback { public void OnCallbackO {•••} } IMyContractCalIback = new MyCallback(); InstanceContext content = new InstanceContext(calIback): MyContractClient proxy = new MyContractClient(context); proxy.DoSomething(); Обратите внимание: пока клиент ожидает обратные вызовы, он не может закрыть посредника, потому что это приведет к закрытию конечной точки об- ратного вызова и выдаче о!нибки на стороне службы при попытке обратного вызова. Достаточно часто клиент сам реализует контракт обратного вызова. В этом случае клиент обычно сохраняет посредника в переменной класса и закрывает его при уничтожении клиента, как показано в листинге 5.4.
Операции обратного вызова 191 Листинг 5.4. Реализация контракта обратного вызова клиентом class MyClient : IMyContractCallback.IDisposable ( MyContractCl ient m_Proxy: public void CallService() I InstanceContext context = new InstanceContext(this); m_Proxy = new MyContractClient(context); m Proxy.DoSomething(): ) public void OnCallbackO (...) public void Disposed ( m_Proxy.Closet); ) Как ни странно, сгенерированный посредник не использует вспомогатель- ные конструкторы DuplexCLientBase<T>, которым объект обратного вызова пере- дается напрямую, но вы можете переработать посредника вручную и включить в него соответствующую возможность, как показано в листинге 5.5. Листинг 5.5. Использование переработанного посредника на базе object partial class MyContractClient : DuplexClientBase<IMyContract>,IMyContract ( public MyContractClient(object callbackinstance) : base(callbacklnstance) {} // Другие конструкторы public void DoSomething() Channel .DoSomething(); ) ) class MyClient : IMyContractCallback.IDisposable ( MyContractClient m_Proxy; public void CallServiceO m_Proxy = new MyContractClient(this); m_Proxy. DoSomethi ng (); public void OnCallbackO (...) public void Disposed I m_Proxy.Closet):
192 Глава 5. Операции Инициирование обратного вызова на стороне службы Ссылка на конечную точку обратного вызова на стороне клиента передается с каждым вызовом, обращенным от клиента к службе, и является частью вход- ных сообщений. Класс OperationContext позволяет службе легко получить эту ссылку посредством параметризованного метода GetCallbackChanneL<T>(): public sealed class Operationcontext { public T GetCa11backChannel<T>(); } Что именно служба сделает со ссылкой, как она будет использовать ее — ос- тается полностью на ее усмотрение. Служба может извлечь ссылку на конечную точку обратного вызова из контекста операции и сохранить его для использова- ния в будущем или же использовать ее во время операции для обращения к кли- енту с обратным вызовом. Первый вариант продемонстрирован в листинге 5.6. Листинг 5.6. Сохранение ссылки на конечную точку обратного вызова [Servi ceBehav i or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { static List<ISomeCallbackContract> m_Callbacks = new List<ISomeCallback<Contract>(); public void Dosomething() { ISomeCalIbackContract callback = OperationContext.Current. GetCallbackChannel<ISomeCallbackContract>(): if(m_Callbacks.Contains(callback) == false) { m_Ca11 backs.Add(ca 11 back): } } public static void CallClientsO { Action<ISomeCallbackContract> invoke = delegate(ISomeCal1backContract cal 1 back) { callback.OnCallbackO: }: m_Cal1 backs.ForEachtinvoke); } } Используя определения из листинга 5.1, служба использует статический шаблонный связанный список для хранения ссылок на интерфейсы типа ISome- CaLLbackContract. Поскольку служба не знает, какой клиент вызывает ее и посту- пил ли от клиента вызов или еще нет, при каждом вызове служба сначала про- веряет, присутствует ли ссылка на конечную точку обратного вызова в списке. Если ссылки в списке нет, служба включает ее. Класс службы также предостав- ляет статический метод CallCLients(). Сторона хоста может использовать его для обратного вызова клиентов: MyService.Cal 1 Cli ents():
Операции обратного вызова 193 В этом варианте вызывающая сторона использует для обратного вызова про- граммный поток на стороне хоста. Этот программный поток не связан с потока- ми, обрабатывающими входящий вызов. ПРИМЕЧАНИЕ------------------------------------------------------------------------ В листинге 5.6 (и других аналогичных примерах этой главы) обращения к списку обратных вызовов не синхронизируются. Разумеется, в коде реального приложения синхронизация должна присутст- вовать. Параллельная обработка (и особенно синхронизация доступа к общим ресурсам) рассматри- вается в главе 8. Реентерабельность обратного вызова Возможно, службе также потребуется обратиться по полученной ссылке обрат- ного вызова (или ее сохраненной копии) во время выполнения операции кон- тракта. Тем не менее по умолчанию такие вызовы запрещены. Это объясняется особенностями стандартной схемы управления параллельной обработкой. По умолчанию класс службы настроен на однопоточные обращения: экземпляр службы связывается с блокировкой, и в любой момент времени только один программный поток может захватить блокировку и обратиться к экземпляру службы. Обращение к клиенту во время вызова операции потребует блокиров- ки программного потока службы. Однако обработка ответного сообщения от клиента при возврате из обратного вызова потребует захвата той же блокиров- ки; возникает ситуация взаимной блокировки (deadlock). При этом служба мо- жет обращаться с обратными вызовами к другим клиентам или вызывать дру- гие службы. Взаимная блокировка возникает из-за обратного вызова клиента, от которого поступил вызов. Для предотвращения взаимной блокировки, если однопоточный экземпляр службы пытается обратиться к своему клиенту с обратным вызовом, WCF вы- дает исключение InvalidOperationException. Возможны три решения. Первое — настроить службу для многопоточного доступа, при котором она не будет свя- зываться с блокировкой; обратный вызов станет возможным, но на разработчи- ка службы будет возложено дополнительное бремя — необходимость синхрони- зации. Второе решение — настройка службы для повторного входа (реентера- бельности). В этом случае экземпляр службы по-прежнему связывается с бло- кировкой, и доступ к ней разрешен только в однопоточном режиме. Но когда служба обращается к клиенту с обратным вызовом, WCF сначала автоматиче- ски снимает блокировку. Глава 8 посвящена синхронизации и ее влиянию на модель программирования. А пока, если вашей службе потребуется обратиться к клиентам с обратным вызовом, установите для нее многопоточный или реен- терабельный режим при помощи свойства ConcurrencyMode атрибута ServiceBeha- vior: public enum ConcurrencyMode I Single. // По умолчанию Reentrant. Multiple ) [Attri buteUsage (Attri buteTargets .Class)] public sealed class ServiceBehaviorAttribute : ...
194 Глава 5. Операции { public ConcurrencyMode ConcurrencyMode {get:set;} //... } В листинге 5.7 представлена служба, настроенная в реентерабельном режи- ме. Во время выполнения операции служба обращается к контексту операции, получает ссылку на объект обратного вызова и обращается с вызовом. Управле- ние снова передается службе только после возврата из обратного вызова, при этом собственный программный поток службы должен заново захватить блоки- ровку. Листинг 5.7. Настройка реентерабельного режима для обратных вызовов [Servi ceContract(CallbackContract = typeof(IMyContractCa11 back))] interface IMyContract { [OperationContract] void DoSomethingO; } interface IMyContractCalIback { [OperationContract] void OnCallbackO: } [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService ; IMyContract { public void DoSomethingO { IMyContractCalIback = Operationcontext.Current. GetCal1backChannel<IMyContractCal1back>(): cal1 back.OnCal1back(): } } Третье решение, позволяющее службе безопасно обращаться к клиенту с об- ратными вызовами, — определение односторонних операций в контракте обрат- ного вызова. В этом случае служба сможет использовать обратные вызовы даже в однопоточном режиме, потому что у нее не будет ответных сообщений, пре- тендующих на получение блокировки. Пример такой конфигурации представ- лен в листинге 5.8. Обратите внимание: по умолчанию служба работает в одно- поточном режиме. Листинг 5.8. Односторонние обратные вызовы разрешены по умолчанию [ServiceContract(CalIbackContract = typeof(IMyContractCalIback))] interface IMyContract { [OperationContract] void DoSomethingO; } interface IMyContractCalIback
Операции обратного вызова 195 [Operationcontract(IsOneWay e true)] void OnCallbackO; 1 class MyService : IMyContract public void DoSomething() f IMyContractCallback = Operationcontext .Current. GetCal1 backchannel<IMyContractCallback>(); callback.OnCallbackO; 1 Управление подключениями ' при обратном вызове Механизм обратного вызова не предоставляет ничего похожего на высокоуров- невые протоколы управления соединением между службой и конечной точкой обратного вызова. Разработчик должен сам представить некий протокол уровня приложения или единую схему управления жизненным циклом подключения. Как упоминалось ранее, служба может обращаться к клиенту с обратным вызо- вом только в том случае, если канал на стороне клиента остается открытым — обычно для этого клиент не закрывает посредника. Кроме того, поддержание посредника в открытом состоянии предотвращает уничтожение объекта обрат- ного вызова в ходе уборки мусора. Если служба хранит ссылку на конечную точку обратного вызова, а посредник закрывается на стороне клиента или исче- зает само клиентское приложение, при обратном вызове служба получит от ка- нала службы исключение ObjectDisposedException. Следовательно, клиенту сле- дует уведомить службу о том, что он более не намерен принимать обратные вызовы или о завершении клиентского приложения. Для этого в контракт служ- бы включается специальный метод Disconnect(). Поскольку с каждым вызовом метода передается ссылка на объект обратного вызова, в методе Disconnect() служба может удалить ссылку из своего внутреннего хранилища. Кроме того, по соображениям симметрии желательно добавить отдельный метод Connect(). Наличие метода Connect() позволит клиенту подключаться к служ- бе и отключаться несколько раз, а также четко обозначить время, в которое он ожидает получения обратных вызовов (только после вызова Connect()). Этот прием продемонстрирован в листинге 5.9. В обоих методах Connect() и Discon- nect() служба должна получить ссылку на объект обратного вызова. В Connect(), прежде чем добавлять ссылку в список, служба проверяет, что ее там еще нет (благодаря чему служба нормально выдерживает повторные вызовы Connect()). В Disconnect() служба проверяет, что ссылка присутствует в списке, и иници- ирует исключение в противном случае. Листинг 5.9. Явное управление подключениями обратного вызова [ServiceContract(CallbackContract = typeof(IMyContractCa11 back))] interface IMyContract ( продолжение &
196 Глава 5. Операции [Operationcontract] void DoSomethingO: [Operationcontract] void ConnectO; [Operationcontract] void Disconnect); ) interface IMyContractCallback { [Operationcontract] void OnCal1 back(): } [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void ConnectO { IMyContractCallback = OperationContext.Current. GetCal1 backchannel<IMyContractCal1back>(); if(m_Callbacks.Contains(callback) -- false) { m_Ca11 backs.Add(ca11 back): } } public void DisconnectO { IMyContractCallback = OperationContext.Current. GetCal1 backchannel<IMyContractCal1back>(): if(m_Callbacks.Contains(callback) »» true) { m_Ca11 backs.Remove(ca11 back): } else { throw new InvalidOperationExceptionCCannot find callback"); } } public static void Cal1C1tents) { Action<IMyContractCallback> invoke » delegate(IMyContractCal1 back cal 1 back) { callback.OnCal IbackO: }: m_Ca 11 backs. ForEach (1 nvoke): } public void DoSomethingO
Операции обратного вызова 197 Управление подключениями ирежим управления экземплярами Служба уровня вызова может использовать ссылку на объект обратного вызова вовремя вызова операции или сохранить ее в некотором глобальном хранили- ще-например, в статической переменной, как в приведенных ранее примерах. Причина понятна: любая переменная состояния экземпляра, в которой служба сохранит ссылку, все равно исчезнет после возвращения из операции. Соответ - ственно, использование метода-аналога Disconnect() особенно актуально для служб уровня вызова. Аналогичная необходимость существует и для синглетных служб. Срок жизни синглетного экземпляра не ограничен, поэтому через какое-то время в нем накопится множество ссылок на объекты обратного вызова; с течением времени многие ссылки устаревают, поскольку объекты перестают существо- вать. Наличие метода Disconnect() гарантирует, что синглетная служба сохранит связь только с актуальными, «живыми» клиентами. Интересно, что сеансовые службы могут обойтись вообще без метода Disconnect(), при условии, что ссылка на объект обратного вызова хранится в некоторой переменной экземпляра. Дело в том, что экземпляр службы будет автоматически уничтожен при завершении сеанса (когда клиент закроет по- средника или по тайм-ауту), поэтому хранение ссылки в рамках сеанса не пред- ставляет опасности — ссылка всегда заведомо действительна. Но если сеансо- вая служба сохраняет свою ссылку обратного вызова в глобальном хранилище для использования другими участниками на стороне хоста или за пределами се- анса, метод Disconnect() становится обязательным для явного удаления ссылки обратного вызова, потому что при вызове Dispose() ссылка недоступна. Наконец, пару Connect()-Disconnect() можно включить в сеансовую службу просто для удобства, чтобы клиент мог решить, когда следует начинать или за- канчивать прием обратных вызовов во время сеанса. Дуплексный посредник и безопасность типов Класс DuplexClientBase<T>, предоставляемый WCF, не обеспечивает сильной ти- пизации для используемого интерфейса обратного вызова. Компилятор позво- лит передать любой объект, даже недействительный интерфейс обратного вы- зова. Он даже разрешит использовать в качестве Т тип контракта службы, в котором контракт обратного вызова вообще нс определен. Во время выполне- ния экземпляр посредника будет успешно создан. Несовместимость проявится только при попытке его использования, с выдачей исключения InvalidOperation- Exception. Аналогично, InstanceContext тоже базируется на object, а компилятор не проверяет на действительность его экземпляр контракта обратного вызова. При передаче InstanceContext в параметре конструктора дуплексного посредни- ка отсутствует проверка времени компиляции, которая бы проверяла его соот- ветствие с экземпляром обратного вызова, ожидаемым дуплексным посредни- ком. Ошибка обнаруживается только при попытке использования посредника. Шаблоны позволяют до определенной степени компенсировать эти недочеты и обнаружить ошибку на стадии времени выполнения, при создании экземпля- ра посредника.
198 Глава 5. Операции Сначала определяется типизованный шаблонный класс InstanceContext<T> (листинг 5.10). Листинг 5.10. Класс InstanceContext<T> public class InstanceContext<T> { InstanceContext m_InstanceContext; public lnstanceContext(T implementation) { m_lnstanceContext = new InstanceContext(implementation); } public InstanceContext Context { get { return m_InstanceContext: } } public T Serviceinstance { get { return (T)m_InstanceContext.GetServiceInstance(); } } } Шаблоны позволяют предоставить типизованный доступ к объекту обратно- го вызова и сохранить информацию о правильном типе. Затем определяется новый типизованный шаблонный класс, производный от DuplexClientBase<T> (листинг 5.11). Листинг 5.11. Класс DuplexClientBase<T,C> // Т - контракт службы. С - контракт обратного вызова public abstract class DuplexCiientBase<T.C> : DuplexClientBase<T> where T : class protected DuplexC11entBase(InstanceContext<C> context) : base(context.Context) {} protected DuplexCl1entBase(InstanceContext<C> context. string endpointName) : ba se(context.Context.endpoi ntName) 0 protected DuplexClientBase(InstanceContext<C> context.Binding binding. EndpointAddress remoteAddress) : base(context.Context.binding.remoteAddress) {} protected DuplexClientBase(C callback) : base(callback) {} protected DuplexClientBase(C call back.string endpointName) : base(cal1 back.endpoi ntName) (}
Операции обратного вызова 199 protected DuplexCl ientBase(C cal 1 back.В inding binding. EndpointAddress remoteAddress) : base(ca11 back.bi ndi ng.remoteAddress) () /* Другие конструкторы */ static DuplexClientBase() VerifyCallbackO; internal static void VerifyCallbackO Type contractType = typeof(T): Type calIbackType = typeof(C): objects attributes = contractType.GetCustomAttributes( typeof(ServiceContractAttribute),false): if(attributes.Length != 1) throw new InvalidOperationExceptionC’Type of ” + contractType + " is not a service contract"): ServiceContractAttribute ServiceContractAttribute; ServiceContractAttribute = attributesLO]] as ServiceContractAttribute: if(calIbackType != serviceContractAttribute.CalIbackContract) throw new InvalidOperationExceptionC'Type of " + calIbackType + " is not configured as callback contract for " + contractType); ( ) 1 Класс DuplexClientBase<T,C> использует два параметра типа: Т — тип контракта службы, С — типа контракта обратного вызова. Конструкторы DuplexClientBase<T,C> могут получать либо «необработанный» экземпляр С, либо экземпляр InstanceContext<C>, инкапсулирующий экземпляр С. Это позволяет компилятору убедиться в использовании совместимых контекстов. Тем не ме- нее в C# 2.0 отсутствуют средства ограничения декларативных связей между Т и С. Обходное решение заключается в том, чтобы выполнить проверку во время выполнения перед использованием DuplexClientBase<T,C> и немедленно прервать использование недопустимого типа, пока он не успел причинить вред. Для это- го код проверки помещается в статический конструктор С#. Статический кон- структор DuplexClientBase<T,C> вызывает статический вспомогательный метод VerifyCallback(). Последний при помощи рефлексии убеждается в том, что тип Т помечен атрибутом ServiceContract. Затем он проверяет, что его тип, заданный для контракта обратного вызова, соответствует параметру типа С. Выдача ис- ключения в статическом конструкторе позволяет как можно ранее обнаружить ошибку на стадии выполнения. ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Обратите внимание на проверку контракта обратного вызова в статическом конструкторе. Этот при- ем применим к любым ограничениям, соблюдение которых не удается обеспечить на стадии компи- ляции, но при этом существует способ их программной проверки во время выполнения.
200 Глава 5. Операции Далее необходимо переработать сгенерированный класс посредника на сто- роне клиента и сделать его производным от типизованного класса DuplexClient- Base<T,C>: partial class MyContractClient : DuplexClientBase<IMyContract.IMyContractCallback>. IMyContract { public MyContractClient(InstanceContext<IMyContractCallback> context) : base(context) {} public MyContractClient(IMyContractCalIback callback) : base(calIback) {} /* Другие конструкторы */ public void DoSomething) { Channel.DoSomethlngt): } } Переработанному посреднику передается либо типизованный контекст эк- земпляра, либо непосредственно экземпляр объекта обратного вызова: // Клиентский код class MyClient : IMyContractCalIback {••} IMyContractCalIback callback = new MyClientO: MyContractClient proxyl = new MyContractClient(calIback); InstanceContex<IMyContractCallback> context = new InstanceContext<IMyContractCallback>(cal Iback); MyContractClient proxy2 = new MyContractClient(context); Дуплексная фабрика Наряду с классом ChannelFactory<T> WCF также предоставляет класс Duplex- ChannelFactory<T>, который может использоваться для создания дуплексных по- средников на программном уровне: public class DuplexChannelFactory<T> : ChannelFactory<T> { public DuplexChannel Factory(object callback); public DuplexChannel Factory(object call back.strint endpointName); public DuplexChannelFactory(InstanceContext context.string endpointName): public T CreateChanneKInstanceContext context); public static T CreateChannel(object calIback.strint endpointName); public static T CreateChannel(InstanceContext context. string endpointName): public static T CreateChannel(object calIback.Binding binding. Endpoi ntAddress endpoi ntAddress); public static T CreateChannel(InstanceContext context.Binding binding. Endpoi ntAddress endpointAddress); //... }
Операции обратного вызова 201 DuplexChannelFactory<T> используется так же, как его базовый класс Channel- Factory^, если не считать того, что его конструкторам передается либо экземп- ляр объекта обратного вызова, либо контекст обратного вызова. Также обрати- те внимание на использование типа object для экземпляра объекта обратного вызова и отсутствие безопасности типов. В листинге 5.12 представлен перера- ботанный класс DuplexChanneLFactory<T,C>, обеспечивающий безопасность типов на стадиях компиляции и выполнения. Листинг 5.12. Класс DuplexChannelFactory<T,C> public class DuplexChannelFactory<T.C> : DuplexChannelFactory<T> where T : class ( static DuplexChannel Factory () ( DuplexCl i entBase<T. C>. Ver i fyCa 11 back (); 1 public static T CreateChannel(C call back.string endpointName) ( return DuplexChannelFactory<T>.CreateChannel(call back,endpointName): 1 public static T CreateChannel (InstanceContextO context, string endpointName) ( return DuplexChannelFactory<T>.CreateChannel(context.Context. endpointName): ) public static T CreateChannel(c call back.Blnding binding. EndpointAddress endpointAddress) return DuplexChannelFactory<T>.CreateChannel(callback.binding. endpointAddpress): 1 public static T CreateChannel(InstanceContextO context.Binding binding. EndpointAddress endpointAddress) ( return DuplexChannelFactory<T>.CreateChannel(context.binding. endpointAddpress): I public Dupl exChannel Factory (C callback) : base(callback) (} public Dupl exChannel Factory(C callback,string endpointName) : base(cal1 back.endpointName) () public Dupl exChannel Factory (InstanceContextO context. string endpointName) : base(context.Context,endpointName) () //... 1 Пример использования DuplexChanneLFactory приведен в листинге 5.13; под- держка обратного вызова добавляется в статический вспомогательный класс InProcFactory, представленный в главе 1.
202 Глава 5. Операции Листинг 5.13. Включение поддержки обратного вызова в InProcFactory public static class InProcFactory { public static I CreateInstance<S.I,C>(C callback) where I : class where S : class,I { InstanceContextO context = new InstanceContextO(cal Iback); return CreateInstance<S,I.O(context): } public static I CreateInstance<S. I ,C>( InstanceContextO context) where I : class where S : class.I ( HostRecord hostRecord = GetHostRecord<S,I>(): return DuplexChannelFactory<I,C>.CreateChannel( context.NamedPIpeBInding.new EndpolntAddress(hostRecord.Address)): } //... } // Пример клиентского кода IMyContractCalIback callback = new MyClient(); IMyContract proxy = InProcFactory.Createlnstance <MyServ1ce.IMyContract.IMyContractCallback>(calIback); proxy.DoSomething(); InProcFactory.CloseProxy(proxy); Иерархия контрактов обратного вызова При проектировании контрактов обратного вызова действует одно любопытное ограничение. Контракт службы может назначить контракт обратного вызова только в том случае, если последний является субинтерфейсом всех контрактов обратного вызова, определенных собственными базовыми контрактами данного контракта службы. Например, следующее определение контрактов обратного вызова недействительно: Interface ICalIbackContractl {...} Interface ICallbackContract2 [ServiceContract(Cal 1backContract = typeof(ICa11backContractl))] interface IMyBaseContract {...} // Недопустимо [ServiceContract(Cal 1backContract = typeof(ICal1backContract2))] interface IMySubContract : IMyBaseContract IMySubContract не может назначить ICallbackContract2 в качестве контракта об- ратного вызова, потому что ICallbackContract2 не является субинтерфейсом ICall- backContractl, тогда как IMyBaseContract (базовый для IMySubContract) определяет
Операции обратного вызова 203 ICallbackContractl своим контрактом обратного вызова. Причина такого ограни- чения очевидна: если клиент передает ссылку на конечную точку реализации IMySubContract, эта ссылка должна соответствовать типу конечной точки, ожи- даемому IMyBaseContract. WCF проверяет иерархию контрактов обратного вызо- ва во время загрузки службы и выдает исключение InvalidOperationException в случае нарушения. Простейший способ выполнения этих ограничений — воспроизведение иерар- хии контракта службы в иерархии контрактов обратного вызова: interface ICallbackContractl (...) interface ICal 1 backContract2 : ICallbackContractl (...) [ServiceContract (Callbackcontract = typeof (ICallbackContractl))] interface IMyBaseContract (...) II Недопустимо [ServiceContract (Cal 1 backContract = typeof(ICallbackContract2))] interface IMySubContract : IMyBaseContract (...) Впрочем, возможен и другой вариант — использовать наследование от не- скольких интерфейсов одного контракта обратного вызова. В этом случае вам не придется имитировать иерархию контракта службы: interface ICallbackContractl (...) interface ICa 11 backContract2 (...) interface ICal 1 backContract3 : ICallbackContractl,ICal 1 backContract2 (.•I [ServiceContract (Cal 1 backContract = typeof (ICal 1 backContractl)) ] interface IMyBaseContractl (...) [ServiceContract (Cal 1 backContract = typeof (ICal 1 backCont ract2)) ] interface IMyBaseContract2 (...) [ServiceContract(CallbackContract = typeof (ICal lbackContract3))] interface IMySubContract : IMyBaseContractl. IMyBaseContract2 (...) Также следует учесть, что служба может реализовать собственный контракт обратного вызова: [ServiceContract(Cal 1 backContract - typeof(ICal 1 backContract))] interface IMyContract (...) [ServiceContract] interface IMyContractCallback (...) class MyService : IMyContract.IMyContractCallback (...)
204 Глава 5. Операции Служба даже может сохранить ссылку на саму себя в некотором хранилище ссылок (если она желает получать обратные вызовы, как если бы она была кли- ентом). Обратный вызов, порты и каналы При использовании привязок NetTcpBinding и NetNamedPipeBinding обратные вы- зовы поступают клиенту по входному каналу, поддерживаемому привязкой. Открывать для них новый порт или канал не нужно. При использовании WS- DualHttpBinding WCF поддерживает отдельный канал HTTP, предназначенный специально для обратных вызовов, потому что протокол HTTP сам по себе яв- ляется односторонним. Для канала обратного вызова WCF по умолчанию вы- бирает порт 80 и передает службе адрес обратного вызова, образованный из протокола HTTP, имени клиентского компьютера и порта 80. Использование порта 80 оправдано для служб, базирующихся в Интернете, но для интрасетевых служб оно не имеет особого смысла. Кроме того, если на клиентском компьютере работает IIS, порт 80 уже зарезервирован и клиент не сможет предоставить конечную точку обратного вызова. Вероятность того, что интрасетевое приложение будет вынуждено использовать WSDualHttpBinding, не- велика, но разработчики, занимающиеся интернет-базированными приложения- ми, часто устанавливают I1S на свои компьютеры; на стадии тестирования и от- ладки порт обратного вызова начинает конфликтовать с IIS. Назначение адреса обратного вызова К счастью, привязка WSDualHttpBinding предоставляет свойство ClientBaseAddress, позволяющее задать на стороне клиента другой URI обратного вызова: public class WSDualHttpBinding : Binding,... { public Uri ClientBaseAddress {get:set:} //... } А вот как базовый адрес настраивается в конфигурационном файле клиента: <system.servi ceModel> <client> <endpoint address = "http://1ocalhost:8008/MyService/" binding = "WSDualHttpBinding" bindingconfiguration = "CllentCalIback" contract = "IMyContract" /> </client> <binaings> <wsDualHttpBinding> <binding name = "Clientcallback" ClientBaseAddress - "http://localhost:8009/" /> </wsDua1HttpBi ndi ng> </bi ndi ngs> </system.servi ceModel>
Операции обратного вызова 205 Но поскольку порт обратного вызова не обязан быть известен службе зара- нее, на деле подойдет любой доступный порт. Соответственно, лучше указать любой доступный порт в клиентском базовом адресе на программном уровне. Статический вспомогательный класс WsDualProxyHelper, представленный в лис- тинге 5.13, помогает автоматизировать эту задачу. Листинг 5.14. Класс WsDualHttpProxyHelper public static class WsDualProxyHelper I public static void SetCHentBaseAddress<T>(DuplexC11entBase<T> proxy. Int port) where T : class ( WSDualHttpBinding binding = proxy.Endpoint.Binding as WSDualHttpBinding; Debug.Assert(bind1ng != null); blnding.CHentBaseAddress = new Uri("http;//localhost:" + port +"/"); 1 public static void SetCl1entBaseAddress<T>(DuplexC11entBase<T> proxy) where T : class 1 ock (typeof (WsDua 1 ProxyHel per)) ( Int portNumber = FlndPortO; SetCl 1 entBaseAddress (proxy. portNumber): proxy.Open(); } I internal static Int FindPortO ( IPEndPoint endPoInt = new IPEndPolnt(IPAddress.Any.O); us1ng(Socket socket = new Socket(AddressFamlly.InterNetwork. SocketType.Stream, ProtocolType.Tcp)) ( socket. Bl nd(endPoi nt); IPEndPoint local = (IPEndPoint)socket.LocalEndPoInt; return local.Port; } ) WsDualProxyHelper содержит две перегруженные версии метода SetClientBase- Address(). Первая версия просто получает экземпляр посредника и номер порта. Она проверяет, что посредник использует привязку WSDualHttpBinding, после че- го задает клиентский базовый адрес с использованием указанного порта. Вто- рая версия SetClientBaseAddress() автоматически выбирает доступный порт и вызывает первую версию с доступным портом. Чтобы избежать «гонки» с другими параллельными вызовами SetClientBaseAddress() в том же приклад- ном домене, на время поиска доступного порта и задания базового адреса ус- танавливается блокировка по самому типу. Учтите, что ситуация «гонки» все еще может возникнуть с другими процессами или прикладными доменами на том же компьютере.
206 Глава 5. Операции Класс WsDualProxyHelper прост в использовании: // Пример клиентского кода class MyCllent : IMyContractCallback IMyContractCallback callback = new MyClientO; InstanceContext context = new InstanceContext(calIback); MyContractClient proxy = new MyContractClient(context): WsDualProxyHelper.SetClientBaseAddressCproxy); У программного назначения адреса обратного вызова (в отличие от жестко- го кодирования в конфигурационном файле) есть и другое преимущество: оно позволяет запустить несколько клиентов на одном компьютере во время тести- рования. Декларативное назначение адреса обратного вызова Процесс можно дополнительно автоматизировать и задать порт обратного вы- зова на декларативном уровне с использованием специального атрибута. CallbackBaseAddressBehaviorAttribute — атрибут поведения контракта, а его дейст- вие распространяется только на конечные точки, использующие привязку WS- DualHttpBinding. CallbackBaseAddressBehaviorAttribute содержит одно целочислен- ное свойство с именем CallbackPort: LAttributeusage(AttributeTargers.Class)] public class CalIbackBaseAddressBehaviorAttribute ; Attribute.lEndpointBehavior { public mt CallbackPort (get:set:} } По умолчанию значение CallbackPort равно 80. Если оно не задано, примене- ние CallbackBaseAddressBehavior приводит к стандартному поведению WSDualHttp- Binding, поэтому следующие два определения эквивалентны: class MyCllent : IMyContractCallback {...} [CallbackBaseAddressBehavior] class MyCllent : IMyContractCallback {...} Помер порта обратного вызова явно задается в свойстве атрибута: [Cal 1backBaseAddressBehavlor(Cal1backPort « 8009)] class MyCllent : IMyContractCallback {...} Но если задать CallbackPort равным 0, CallbackBaseAddressBehavior автоматиче- ски выбирает для обратного вызова любой доступный порт: [Cal 1 backBaseAddressBehavlor(Са11backPort = 0)] class MyClient : IMyContractCallback {...} Код CallbackBaseAddressBehaviorAttribute приведен в листинге 5.15.
Операции обратного вызова 207 Листинг 5.15. CallbackBaseAddressBehaviorAttribute [Attributeusage(Attrl buteTargers. Cl ass) ] public class CallbackBaseAddressBehaviorAttribute : Attri bute.IEndpoi ntBehavi or I int m_Call backPort = 80; public int CallbacPort // Обращение к m_CallbackPort (get;set:} void IEndpolntBehavior.AddBindingParameters(ServiceEndpoint endpoint. BindingParameterCollection bindingparameters) 1 if(Cal IbackPort =- 80) { return; } 1 ock (typeof(WsOua1 ProxyHelper)) I if(CalIbackPort — 0) ( CallbackPort = WsDualProxyHelper.FindPortO; } WSDualHttpBinding binding = endpoint.Binding as WSDualHttpBinding; if(binding != null) { binding.ClientBaseAddress = new Uri( "http://localhost:” + CallbackPort + ’7"): } } // Пустые методы lEndpointBehavior ) Атрибут CallbackBaseAddressBehaviorAttribute позволяет перехватить (на сто- роне клиента или службы) информацию о конфигурации конечной точки. Ат- рибут поддерживает интерфейс IContractBehavior: public interface lEndpointBehavior I void AddBindingParameters(ServiceEndpoint endpoint. BindingParameterCollection bindingparameters); //... ) WCF вызывает метод AddBindingParameters() на стороне клиента непосредст- венно перед первым использованием посредника на стороне службы, что позво- ляет атрибуту настроить привязку, используемую для обратного вызова. Add- BindingParameters() проверяет значение CallbackPort. Если оно равно 80, ничего не происходит. Если оно равно 0, AddBindingParameters() находит свободный порт и присваивает его CallbackPort. Затем AddBindingParameters() проверяет при- вязку, используемую для вызова службы. Если это WSDualHttpBinding, AddBin- dingParameters() задает клиентский базовый адрес с использованием порта об- ратного вызова.
208 Глава 5. Операции ВНИМАНИЕ --------------------------------------------------------------------------- При использовании CallbackBaseAddressBehaviorAttribute возможна ситуация «гонки» с другим объектом обратного вызова, захватывающим тот же порт — даже в границах одного прикладного домена. События Базовый механизм обратного вызова WCF не делает никаких предположений относительно характера взаимодействия между клиентом и службой. Они мо- гут быть равноправными участниками взаимодействия, каждый из которых от- правляет и получает вызовы от другого. Тем не менее в каноническом варианте дуплексные обратные вызовы приме- няются для реализации событий. События предназначены для оповещения клиента или клиентов о том, что происходит на стороне службы. Событие мо- жет быть прямым результатом клиентского вызова или же произойти при вы- полнении некоторого условия, отслеживаемого службой. Служба, инициирую- щая событие, называется издателем1 (publisher), а клиент, получающий собы- тие, — подписчиком (subscriber). Поддержка событий играет важную роль во многих приложениях (рис. 5.2). Рис. 5.2. Служба-издатель может выдавать события для нескольких клиентов-подписчиков Хотя в WCF события представляют собой не что иное, как операции обрат- ного вызова, по своей природе они обычно подразумевают более свободную 1 Также встречается перевод «сервер публикаций» — Примеч. перев.
События 209 связь между издателем и подписчиком (по сравнению со связью между клиен- том и службой). Как правило, одно событие публикуется службой для несколь- ких клиентов-подписчиков. Часто издателя не интересует, в каком порядке со- бытие будет получено подписчиками и какие ошибки произойдут при обработке события подписчиками. Издателю известно только одно: что событие должно быть доставлено подписчикам. Даже если у них возникнут проблемы с событи- ем, служба все равно с этим сделать ничего не сможет. Кроме того, служба не собирается принимать результаты, возвращаемые подписчиками. Соответствен- но, операции обработки событий возвращают void, не могут возвращать инфор- мации в параметрах и должны помечаться как односторонние. Я также рекомен- дую выделить события в отдельный контракт обратного вызова и не смешивать их с обычными обратными вызовами того же контракта: interface IMyEvents [Operationcontract(IsOneWay - true)] void OnEventlO; [Operationcontract(IsOneWay - true)] void 0nEvent2(int number); [Operationcontract(IsOneWay - true)] void 0nEvent3(int number.string text); ) На стороне подписчика, даже при использовании односторонних операций обратного вызова, реализация методов обработки событий должна иметь мини- мальную длину. Дело в том, что при большом количестве публикуемых собы- тий очередь событий подписчика может оказаться переполненной, и работа из- дателя будет заблокирована. Из-за блокировки издателя события не смогут своевременно достичь других подписчиков. Издатель может включить в свой контракт специализированные операции, при помощи которых клиенты смогут явно заявить о желании подписаться или прекратить подписку на события. Если издатель поддерживает несколько типов событий, он также может пре- доставить возможность подписчикам явно указать, на какие события они жела- ют подписаться (или прекратить подписку). Как служба организует внутреннее управление списком подписчиков, ка- кими критериями она руководствуется — все это относится к подробностям реализации на стороне службы, которые никак не должны отражаться на кли- ентах. Для ведения списка подписчиков и оформления подписки издатель даже может использовать делегатов .NET. В листинге 5.16 продемонстрирована эта методика, а также другие проектировочные методики, упоминавшиеся ранее. Листинг 5.16. Управление событиями с использованием делегатов enum EventType I Event1 = 1. Event? = 2. Event3 = 4. продолжение &
210 Глава 5. Операции All Events = Eventl|Event21Event3 } [ServiceContract(CallbackContract - typeof(IMyEvents))] interface IMyContract { [OperationContract] void DoSomethingO; [OperationContract] void Subscr1be(EventType mask): [OperationContract] void UnsubscribeCEventType mask): } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyPublisher : IMyContract { static GenerlcEventHandler m_Eventl - delegated; static Gener1cEventHandler<1nt> m_Event2 - delegate^}; static Gener1cEventHandler<1nt.str1ng> m_Event3 = delegate^}; public void Subscr1be(EventType mask) { IMyEvents subscriber = Operationcontext.Current. GetCa11 backchannel<IMyEvents>(); If((mask && EventType.Eventl) == EventType.Event1) { m_Eventl += subscrlber.OnEventl; } 1f((mask && EventType.Event?) — EventType.Event?) { m_Event2 += subscr1ber.0nEvent2; ) 1f((mask && EventType.Event3) — EventType.Event3) { m_Event3 +- subscriber.OnEventS: } ) public void UnsubscribeCEventType mask) { // Аналогично Subscribe!). но с операцией -= } public static void F1reEvent(EventType eventType) { switch(eventType) { case EventType.Eventl: { m_Eventl(); return; } case EventType.Event?: { m_Event2(42); return; }
case EventType.Event3: { m_Event3(42."Hello"); return; default; ( throw new Inval idOperatior,Except ion( "Unknown event tyoe"). } public void DoSomethingO ((...) Контракт службы IMyContract определяет методы Subscribe() и Unsubscribe(). Эти методы получают значение перечисляемого типа EventType, отдельные поля которого равны последовательным степеням 2. Это позволяет клиенту-подпис- чику объединить значения в маску, обозначающую типы событии, па которую он хочет подписаться (или прекратить подписку). Например, чтобы подписать- ся на Eventl и Event3, но не на Event2, подписчик использует вызов следующего вида: class MySubscrlber IMyEvents I void OnEventn) (...) void 0nEvent2( int number) (...) void 0nEvent3(int number.string text) (...) 1 IMyEvents subscriber = new MySubscrlber; Instancecontext context = new InstanceContext(subscrwer). MyContractCilent proxy = new MyContractCllent(context): proxy.Subscr 1 be(EventType. Eventl[EventType. Event3): Во внутренней реализации MyPublisher поддерживает трех статических деле- гатов, каждый из которых соответствует типу события. Все делегаты относятся к обобщенному типу делегата GenericDelegate: public delegate void GenericEventHandler(); public delegate void GenericEventHandler<T>(T t): public delegate void GenericEventHandler<T.U>(T t.U u); public delegate void GenericEventHanoler<l ,U.V>(T t.U u.V v); public delegate void GenericEventHandier<T.U.V,W>(T t.U u.V v.W w); public delegate void GenericEventHandler<T,U.V.W.X>(1 t.U u.V v.W w.X x); public delegate void GenericEventHandler<T.U.V.W.X.Y>(T t.U u.V v.W w.X x.Y y); GenericDelegate позволяет выразить буквально любую сигнатуру обработки события. Оба метода, Subscribe() и Unsubscribe(), проверяют переданное значение EventType и добавляют или удаляют ссылку обратного вызова подписчика в со- ответствующем делегате. Для инициирования событий в MyPublisher входит статический метод FireEvent(). При вызове он получает инициируемое событие и вызывает соответствующего делегата.
212 Глава 5. Операции Еще раз подчеркну: тот факт, что служба Му Publisher использует делегатов - не более чем подробность реализации, упрощающая поиск событий. Например, служба также могла бы использовать связанный список, но это привело бы к некоторому усложнению кода. ПРИМЕЧАНИЕ ---------------------------------------------------------- В приложении Б приведена заготовка для поддержки более эффективной схемы работы с собы- тиями. Потоковая передача При обмене сообщениями между клиентом и службой такие сообщения по умолчанию буферизуются на стороне получателя и доставляются только после приема всего сообщения. Сказанное относится как к отправке сообщения кли- ентом службе, так и возврате сообщений от службы клиенту. Когда клиент об- ращается с вызовом к службе, служба активизируется только после получения полного сообщения. Блокировка клиента снимается тогда, когда будет получе- но полное сообщение с результатами вызова. Для достаточно малых сообщений такая схема обмена упрощает модель программирования, потому что задержка, обусловленная приемом сообщений, обычно пренебрежимо мала по сравнению с продолжительностью обработки сообщения. Но когда мы имеем дело с боль- шими сообщениями (например, с мультимедийным содержимым, большими файлами или блоками данных), блокировка до приема полного сообщения мо- жет оказаться непрактичной. В подобных случаях WCF позволяет принимаю- щей стороне (будь то клиент или служба) приступить к обработке данных, пока сообщение все еще принимается каналом. Это называется режимом потоковой передачи (streaming transfer). При больших объемах передаваемых данных по- токовая передача улучшает пропускную способность и скорость отклика, пото- му что ни принимающая, ни отправляющая сторона не блокируется при от- правке или приеме сообщения. Потоки ввода/вывода Для потоковой передачи сообщений в WCF необходимо использовать класс .NET Stream. По сути контрактные операции, используемые для потоковой пе- редачи, выглядят как обычные методы ввода/вывода. Класс Stream является ба- зовым для всех потоков ввода/вывода в .NET (таких, как FileStream, Network- Stream и MemoryStream) и позволяет организовать потоковую передачу данных от всех перечисленных источников ввода/вывода. Все, что потребуется от вас - вернуть или получить Stream в параметре операции, как показано в листин- ге 5.17. Листинг 5.17. Потоковые операции [ServiceContract] interface IMyContract { [Operationcontract]
Streaa StreamReplylO; [OperationContract] void StreamReply2(out Stream stream): [OperationContract] void StreamRequest(Stream stream): [OperationContract(IsOneWay - true)] void OneWaySt ream (Stream stream): ) Обратите внимание: в параметре операции может использоваться только аб- страктный класс Stream или конкретный сериализуемый субкласс — такой, как MemoryStream. Такие субклассы, как FileStream, не сериализуются и вместо них придется использовать базовый класс Stream. WCF позволяет службам организовать потоковую обработку ответов, запро- сов или ответов и запросов, с применением односторонней потоковой пере- дачи. Чтобы передать в потоковом режиме ответ, либо верните Stream из опера- ции: [OperationContract] Streaa GetStreamK): либо передайте поток в выходном параметре: [OperationContract] void GetStream2(out Stream stream); Для потокового приема запроса передайте Stream в параметре метода: [OperationContract] void SetStreaml(Stream stream): Наконец, потоковая передача запроса возможна даже для односторонних операций: //Односторонняя потоковая передача [OperationContract(IsOneWay - true)] void SetStream2(Stream stream): Потоковая передача и привязка Только привязки BasicHttpBinding, NetTcpBinding и NetNamedPipeBinding поддер- живают потоковую передачу. У всех перечисленных привязок потоковый ре- жим отключен по умолчанию, а сообщение буферизуется даже при использова- нии Stream. Чтобы включить потоковый режим, задайте соответствующее зна- чение свойству TransferMode; например, для BasicHttpBinding: public enum TransferMode ( Buffered. // По умолчанию Streamed. StreamedRequest. StreamedRes ponse ) public class BasicHttpBinding : Binding....
214 Глава 5. Операции { public TransferMode TransferMode {get;set:} //... } TransferMode.Streamed поддерживает все варианты потоковой передачи. Это единственный режим, в котором поддерживаются все операции из листин- га 5.17. Но если контракт поддерживает определенную разновидность потоко- вой пересылки — например, потоковый ответ: [ServiceContract] interface IMyContract { // Потоковый ответ [Operationcontract] Stream GetStreamlC): [Operationcontract] int MyMethodO: } вы можете включить режим буферизованных запросов и потоковых ответов, выбрав значение TransferMode.StreamedResponse. Требуемый потоковый режим должен быть настроен на стороне клиента и/или службы: <configuration> <system.servi ceModel> <client> <endpoi nt binding = "basicHttpBinding" bindingconfiguration - "StreamedHTTP" /> </client> <bindings> <basicHttpBinding> <binding name = "StreamedHTTP" transferMode » "Streamed"> </binding> </basicHttpBinding> </bindings> </system.servi ceModel> </configuration> Потоковая передача и транспорт Важно понимать, что потоковая передача WCF — не более чем удобство про- граммной модели. Нижележащий транспорт (например, HTTP) не использует потоковый режим, а максимальный размер сообщения по умолчанию устанав- ливается равным 64 Кбайт. Это может создать проблемы с данными, для кото- рых обычно применяется потоковая передача, потому что потоковые сообщения обычно бывают очень большими (собственно, именно это и создает необхо- димость в потоковой передаче). Вероятно, вам придется увеличить максималь- ный размер сообщения на стороне получателя, чтобы сделать возможной пере-
Потоковая передача 215 дачу больших сообщений — для этого нужный максимальный размер задается в свойстве MaxReceivedMessageSize: public class BasicHttpBinding : Binding.. . public long MaxReceivedMessageSize (get: set:} //... Обычно подобные параметры следует задавать в конфигурационном файле, а не па программном уровне, поскольку размер сообщения часто зависит от конкретного места развертывания: <Dindings> <basicHttpBi ndi ng> <binding name = "StreamedHTTP" transferMode = ’Streamed" maxReceivedMessageSize = "120000"> Управление потоковой передачей Когда клиент передаст службе потоковый запрос, служба может прочитать дан- ные из потока уже после того, как клиент прекратит существование. Клиент не может узнать о том, что служба завершила работу с потоком. Соответственно, клиент не должен закрывать поток — WCF автоматически закроет поток на стороне клиента после того, как служба обработает его. Аналогичная проблема возникает при взаимодействии клиента с потоковым ответом. Поток был создан на стороне службы, но служба нс узнает, когда кли- ент завершит работу с потоком. WCF понятия не имеет, что клиент делает с по- током и поэтому помочь не сможет. Клиент всегда несет ответственность за за- крытие ответных потоков. ПРИМЕЧАНИЕ------------------------------------------------------------------------ Потоковая передача делает невозможным применение безопасности передачи на уровне сообще- ний. Тема безопасности более подробно рассматривается в главе 10. При потоковой передаче с привязками TCP также нельзя включить надежную доставку сообщений. Применение потоковой передачи имеет ряд дополнительных следствий. Прежде всего необходимо синхронизировать доступ к потоковому содержимо- му: например, открыв файловый поток в режиме «только для чтения», чтобы другие стороны могли к нему обращаться, или в монопольном режиме, чтобы запретить доступ к нему (если это необходимо). Кроме того, потоковая переда- ча не может использоваться с сеансовыми службами сеанс подразумевает же- стко фиксированную последовательность выполнения и четкую демаркацию, в отличие от потоковой передачи, которая может непрерывно продолжаться в течение долгого периода времени.
Сбои и исключения Любая операция службы может в произвольный момент времени столкнуться с непредвиденной ошибкой. Вопрос в том, как сообщить клиенту об этой ошиб- ке (и нужно ли это делать). Такие концепции, как исключения и механизмы обработки исключений, относятся к специфике конкретных технологий и не должны выходить за границы службы. Кроме того, обычно обработка ошибок является локальной подробностью реализации, которая не должна отражаться на клиенте — отчасти потому, что клиента могут не интересовать подробности ошибки (достаточно знать, что произошло что-то нежелательное), но чаще при- чина кроется в другом. В хорошо спроектированном приложении служба ин- капсулируется так, что клиент все равно не может осмысленно среагировать на ошибку. Хорошо спроектированная служба должна быть как можно более авто- номной и не зависящей от способности клиента обработать ошибку и вернуться к нормальной работе. Все, что выходит за пределы простого оповещения об ошибке, должно стать частью контрактного взаимодействия между клиентом и службой. В этой главе описано лишь то, как служба и клиент должны обраба- тывать такие объявленные сбои и как можно расширить и усовершенствовать базовый механизм. Ошибки и исключения В традиционной модели программирования .NET необработанное исключение немедленно завершает процесс, в котором оно произошло. В WCF эта модель поведения не используется. Если вызов службы от одного из клиентов приво- дит к исключению, это не должно нарушить работоспособность хостового про- цесса. Другие клиенты, обращающиеся к службе, и другие службы, находящие- ся под управлением того же процесса, страдать не должны. В результате, когда необработанное исключение выходит из области видимости службы, диспетчер перехватывает и обрабатывает его, возвращая в сериализованном виде в сооб- щении клиенту. Когда возвращаемое сообщение попадает к посреднику, по- следний инициирует исключение на стороне клиента. Вообще говоря, ошибки, с которыми может столкнуться клиент при попытке обращения к службе, делятся на три типа. Первый тип — коммуникационные
Сбои и исключения 217 ошибки (недоступность сети, неверный адрес, хостовой процесс не запущен нт. д. О коммуникационных ошибках на стороне клиента сигнализирует ис- ключение CommunicationException. Ошибки второго типа связаны с состоянием посредника и каналов - напри- мер, попытка обращения к уже закрытому посреднику, приводящая к исключе- нию ObjectDisposed Exception, или несоответствие между контрактом и уровнем безопасности привязки. Ошибки третьего типа возникают при вызове службы: либо сама служба инициирует исключение, либо оно возникает в результате обращения службы к другому объекту или ресурсу. Именно таким ошибкам посвящена настоящая глава. В интересах инкапсуляции и логической изоляции все исключения, иниции- рованные на стороне службы, по умолчанию всегда достигают клиента в виде FaultException: public class FaultException : CommunicationException (...) Делая все исключения служб неотличимыми друг от друга, WCF отделяет клиента от службы. Чем меньше клиент знает о том, что произошло на стороне службы, тем полнее логическое разделение такого взаимодействия. Исключения и управление экземплярами Хотя WCF и не уничтожает хостовой процесс, когда экземпляр службы сталки- вается с исключением, ошибка может повлиять на работу экземпляра службы и на способность клиента продолжить использование посредника (а вернее, ка- нала), связывающего его со службой. Воздействие, оказываемое исключением на клиента и на экземпляр службы, зависит от режима управления экземплярами. Службы уровня вызова и исключения Если при вызове происходит исключение, после исключения экземпляр служ- бы уничтожается, а посредник инициирует исключение FaultException на сторо- не клиента. По умолчанию все исключения, инициируемые службами (кроме классов, производных от FaultException), приводят к отказу канала. Даже если клиент перехватит исключение, он не сможет выдавать последующие вызовы, потому что это приведет к выдаче исключения CommunicationObjectFaultedExcep- tion. Клиент может только закрыть посредника. Сеансовые службы и исключения При использовании любой из сеансовых привязок WCF все исключения (кро- ме классов, производных от FaultException) по умолчанию завершают сеанс. WCF уничтожает экземпляр, а клиент получает исключение FaultException. Даже если клиент перехватит исключение, он не сможет выдавать последующие вы- зовы, потому что это приведет к выдаче исключения CommunicationObjectFaulted- Exception. Единственное, что может сделать клиент, — закрыть посредника; по- сле того как экземпляр службы, участвующей в сеансе, столкнется с ошибкой, сеанс не должен далее использоваться.
218 Глава 6. Сбои и исключения | Синглетные службы и исключения Если исключение происходит при вызове синглетной службы, синглетный эк- земпляр не уничтожается и продолжает работать. По умолчанию все исключе- ния (кроме классов, производных от FaultException) приводят к отказу канала, и клиент не может выдавать последующие вызовы, кроме закрытия посредника. Если у клиента существует сеанс с синглетной службой, этот сеанс завершается. Сбои Основная проблема с исключениями заключается в том, что они привязаны к конкретной технологии и не могут выходить за границы службы. Для обеспе- чения нормального взаимодействия необходимо найти способ отображения специфических исключений на некую нейтральную информацию об ошиб- ках. Такое представление называется сбоями (faults). Сбои основаны на про- мышленном стандарте, независимом от исключений, относящимся к конкретной технологии (например, исключениям CLR. Java или C++). Чтобы инициировать сбой, служба не может инициировать «простое» исключение CLR. Вместо этого она должна инициировать экземпляр класса FaultException<T> (листинг 6.1). Листинг 6.1. Класс FaultException<Т> LSerializablej // Другие атрибуты public class FaultException : Common i cat 1 or,Except ion J public Fan It Exceptюп(): public FaultException(string reason); public faultException(FaultReason reason); public vn+ual MessageFault CreateMessageFault(); I /. . j [Serializablel public class FaultExceptюп<Г> ; FaultException t public FaultException(T detail); public FaultException(T detail.string reason); public FauitException(Г detai 1.FauItReason reason). //... } FaultException<T> — специализация FaultException, поэтому любой клиент, за- программированный на работу с FaultException, сможет работать и с FaultExcep- tion<T>. Параметр типа Т у FaultException<T> содержит информацию об ошибке. Это может быть любой тип, не обязательно производный от Exception. Единственное ограничение заключается в том, что тип должен быть сериализуемым или яв- ляться контрактом данных. В листинге 6.2 представлена простая служба-калькулятор, реализация кото- рой выдает исключение FaultException<DivideByZeroException> при делении на ноль в операции Divide().
Листинг 6.2. Выдача FaultException<T> [ServiceContract] interface iCalculator I [OperationContract] double Divide(double numberl,double number?); //... I class Calculator : ICalculator I public double D1vide(double numberl.double number?) if (number? == 0) ( DivideByZeroException exception = new DivideByZeroException(); throw new FaultException<DivideByZeroException>(exception); return numberl / number?; ) //... I Вместо FaultException<DivideByZeroException> в параметре типа служба также могла бы передать класс, не являющийся производным от Exception: throw new Faul tExcepti on<doubl e>(); И все же, на мой взгляд, использование типа, производного от Exception, луч- ше отвечает традиционным принципам программирования .NET и делает код более удобным. Кроме того, это делает возможной доставку исключений (см. далее). Параметр reason, передаваемый конструктору FaultException<T>, содержит крат- кое описание исключения. В нем может передаваться как обычная строка: DivideByZeroException exception = new DivideByZeroException(): throw new Faul tExcepti on<DivideByZeroException>( except ion. "number? is 0"); так и объект FaultReason, который может пригодиться при локализации. Контракты сбоев По умолчанию любое исключение, инициированное службой, достигает клиен- та в виде FaultException. Дело в том, что вся информация, которой служба делит- ся с клиентом (кроме коммуникационных ошибок), должна быть частью кон- трактного поведения службы. С этой целью WCF предоставляет контракты сбоев - с их помощью служба перечисляет типы ошибок, которые она может инициировать. Идея заключается в том, что эти типы ошибок должны соответ- ствовать параметрам типов, используемым в FaultException<T>; по контракту сбоев клиент WCF может отличить контрактные ошибки от других. Служба определяет свои контракты сбоев при помощи атрибута FaultContract- Attribute:
220 Глава 6. Сбои и исключения LAttrbutellsage(AttributeTargets.Method.Al 1 owMultiplе « true. Inherited = false)] public sealed class FaultContractAttrlbute : Attribute { public FaultContractAttr1bute(Type detailType); //... Атрибут FaultContract применяется непосредственно к контрактным операци- ям, и в нем указывается тип детализации ошибки, как показано в листинге 6.3. Листинг 6.3. Определение контракта сбоя [ServiceContract] interface {Calculator { [Operationcontract] double Add(double number1.double number?); [Operationcontract] [FaultContract(typeof(DivideByZeroException))J double DIvide(double number1.double number?): //... Действие атрибута FaultContract ограничивается тем методом, который им помечен. Только этот метод может инициировать данный сбой, который в ко- нечном итоге будет передан клиенту. Кроме того, если операция инициирует исключение, отсутствующее в контракте, оно достигает клиента в виде простого FaultException. Для продвижения исходного исключения служба должна указать точно такой же тип, как указано в контракте сбоев. Например, для следующего определения контракта сбоев: [F a u1tContract(typeof(DIvldeByZeroExcept1 on))] служба должна инициировать FaultException<DivideByZeroException>. Чтобы ис- ключение было успешно доставлено, служба не может даже инициировать ис- ключение с подклассом детализирующего типа из контракта сбоев: [ServiceContract] interface IMyContract { [Operationcontract] [FaultCont ra c t(typeo f(Excepti on))] void MyMethodO; } class MyService : IMyContract { public void MyMethodO { //He соответствует контракту! throw new FaultExcept1on<D1v1deByZeroException>(new DIvldeByZeroExceptIonО);
Контракты сбоев 221 Атрибут FaultContract допускает многократное назначение, что позволяет за- дать для одной операции несколько контрактов сбоев: [ServiceContract] interface ICalculator [OperationContract] [Faul tCont ract (ty peof (I nval 1 dOperat 1 onExcept 1 on)) ] [Faul tContract (typeof (stri ng)) ] double Add (double numberl .double number2); [OperationContract] [FaultContract (typeof (Di v i deByZeroExcept i on)) ] double Divide(double number1.number?): //... 1 Это позволяет службе инициировать любое из исключений, упомянутых в контрактах, и это исключение будет успешно доставлено клиенту. ВНИМАНИЕ------------------------------------------------------------------------------------------ Контракты сбоев не могут определяться для односторонних операций, потому что односторонняя операция по определению не возвращает никаких результатов: //Недопустимое определение [ServiceContract] interface IMyContract I [OperationContract(IsOneWay = true)] [FaultContract(...)] void MyMethodt): Подобные попытки приведут лишь к выдаче исключения InvalidOperationException во время загруз- ки службы. Обработка сбоев Контракты сбоев публикуются наряду с другими метаданными службы. При импортировании метаданных клиентом WCF определение контракта содержит контракты сбоев, а также определения детализирующих типов, включая соот- ветствующий контракт данных. Последнее обстоятельство важно, если детали- зирующий тип представляет собой некоторый пользовательский тип исключе- ния со специализированными полями. Предполагается, что клиент будет перехватывать и обрабатывать импорти- рованные типы сбоев. Например, клиент, написанный для контракта из листин- га 6.3, может перехватить сбой FaultException<DivideByZeroException>: Calculatorclient proxy = new Calculator^ient(): try ( proxy. Di vide( 2.0): proxy.CloseO: 1 catch(FaultExcepti on<DivideByZeroException> exception)
222 Глава 6. Сбои и исключения (...) cdtcruCommuniCq!1onixception exception) i Обратите внимание на возможность возникновения коммуникационных ис- ключений. Клиент может решить, что все не-коммуникационные исключения со сторо- ны службы должны обрабатываться одинаково; для этого он обрабатывает толь- ко базовое исключение FaultException: Ca 'culator Client pruxy - new Ca IculatorQi lento: try Df’ox3 Div ide (2. O' pre*у (i)• catch;FaultException exception) catchcCommunicatlonException exception) ПРИМЕЧАНИЕ ----------------------------------------------------------------------- Возможна довольно экзотическая ситуация, когда разработчик клиента вручную изменяет опреде- ление импортированного контракта и удаляет из него контракт сбоев на стороне клиента. В этом случае, когда служба инициирует исключение, присутствующее в контракте сбоев на стороне служ- бы, исключение проявляется на стороне клиента как FaultException, а не как контрактный сбой. Когда служба инициирует исключение, указанное в контракте сбоев на сто- роне службы, исключение не приводит к отказу канала связи. Клиент может пе- рехватить исключение и продолжить пользоваться посредником или же за- крыть посредника. Неизвестные сбои Класс FaultException<T> является производным от FaultException. Служба (или любой используемый ей нисходящий объект) может инициировать исключение FaultException напрямую: throw new FaultException("Some Reason"): FaultException специальный тип для исключений, которые я называю неиз- вестными сбоями (unknown faults). Неизвестный сбой доставляется клиенту в виде FaultException, не приводит к отказу канала связи, и клиент может про- должить пользоваться посредником так, как если бы исключение входило в кон- тракт сбоев. ПРИМЕЧАНИЕ --------------------------------------------------------------- Обратите внимание: любое исключение FaultException<Т>, инициированное службой, всегда дос- тигает клиента в виде FaultException<Т> или FaultException. Если контракт сбоев отсутствует (или если T не входит в контракт), то любые исключения FaultException<Т> и FaultException, иницииро- ванные службой, достигают клиента в виде FaultException.
Контракты сбоев 223 Свойству Message объекта исключения на стороне клиента задается значение параметра конструирования reason объекта FaultException. В основном FaultEx- ception используется нисходящими объектами, не располагающими информа- цией о контрактах сбоев тех служб, от которых поступил вызов. Чтобы избе- жать логической привязки к службе верхнего уровня, такие объекты могут инициировать FaultException, если они хотят избежать отказа канала связи, или предоставить клиенту возможность обработать исключение отдельно от других коммуникационных ошибок. Отладка сбоев Уже развернутая служба должна быть по возможности отделена от клиентов, ее контракты сбоев должны быть сведены к абсолютному минимуму, а клиенты должны получать как можно меньше информации об исходной ошибке. Однако в процессе тестирования и отладки очень полезно передавать все исключения в информации, возвращаемой клиенту. Это позволит разработчику применять тестовых клиентов и отладчики для анализа источника ошибки (вместо того, чтобы иметь дело с универсальным, но не несущим полезной информации FaultException). Для этой цели используется класс Exception Detail, определяемый следующим образом: [DataContract] public class ExceptionDetail public Except!onDetail(Exception exception): [DataMember] public string HelpLink {getprivate set;} [DataMember] public ExceptionDetail InnerException {get private set:} [DataMember] public string Message {get;private set:} [DataMember] public string StackTrace {getprivate set;} [DataMember] public string Type {getprivate set;} Сначала вы создаете экземпляр Exception Detail и инициализируете его ис- ключением, которое должно быть доставлено клиенту. Затем инициируется объект FaultException<ExceptionDetail>, при конструировании которого указыва- ется экземпляр ExceptionDetail и сообщение исходного исключения. Последова- тельность действий показана в листинге 6.4.
224 Глава 6. Сбои и исключения Листинг 6.4. Включение исключения [ServiceContract] Interface IMyContract { [OperationContract] void MethodWithError(); } class MyServic : IMyContract ( public void MethodWithError() { InvalldOperatlonExceptlon exception - new InvalldOperatlonExceptlon("Some error"); ExceptionDeta11 detail e new ExceptionDeta11(exception); throw new FaultException<ExceptionDeta11>(detail.exception.Message); } } В этом случае клиент сможет получить информацию о типе и сообщении ис- ходного исключения. Объект сбоя на стороне клиента содержит свойство Detail. Туре, в котором хранится имя исходного исключения службы, и свойство Message с сообщением исходного исключения. В листинге 6.5 приведен клиент- ский код обработки исключения, инициированного в листинге 6.4. Листинг 6.5. Обработка вложенного исключения MyContractCllent proxy = new MyContractClient(endpointName): try { proxy.MethodWithError(); } catch(FaultException<ExceptionDetai1> exception) { Debug.Assert(exception.Detail.Type -- typeof(InvalidOperationException) .ToStringO); Debug.Assert(exception.Message == "Some error"); ) Декларативная передача исключений Атрибут ServiceBehavior содержит логическое свойство IncludeExceptionDetailsIn- Faults, которое определяется следующим образом: [Attr1buteUsage(AttrlbuteTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute. ... { public bool IncludeExceptionDetailInFaults {get;set:} //... } Значение IncludeExceptionDetailsInFaults по умолчанию равно false. Если за- дать ему true, как в следующем фрагменте: [ServiceBehaviorUncludeExceptionDetail InFaults = true)] class MyService : IMyContract {...}
Контракты сбоев 225 результат будет тем же, что и в листинге 6.4, но все произойдет автоматически: все неконтрактные сбои и исключения, инициированные службой или любы- ми нисходящими объектами, доставляются клиенту и включаются в возвращае- мое сообщение для обработки клиентской программой, как показано в листин- ге 6.5: [ServiceBehavior(IncludeExceptionDetailInFaults - true)] class MyService : IMyContract I public void MethodWithError() ( throw new InvalidOperationExceptionCSome error"); Сбои, инициированные службой (или ее нисходящими объектами) и при- сутствующие в контракте сбоев, доставляются клиенту в исходном виде. Передача всех исключений полезна для отладки, однако вы должны быть очень внимательны, чтобы не допустить распространения и развертывания служб с истинным свойством IncludeExceptionDetailsInFauLts. Для предотвращения по- тенциальных проблем можно воспользоваться условной компиляцией, как по- казано в листинге 6.6. Листинг 6.6. Свойство IncludeExceptionDetailsInFaults истинно только в отладочной версии public static class DebugHelper I public const bool IncludeExceptionDetailInFaults = lit DEBUG true; felse false: fendif [ServiceBehavior! IncludeExceptionDetail InFaults = DebugHelper.IncludeExcept i onDetai11nFaults)] class MyService ; IMyContract (...) ВНИМАНИЕ------------------------------------------------------------------------------------- При истинном свойстве IncludeExceptionDetailsInFaults исключение приведет к отказу канала, поэто- иу клиент не сможет выдавать последующие вызовы. Хост и диагностика исключений Очевидно, передача информации об исключениях в сообщениях о сбоях значи- тельно упрощает отладку, но она также находит применение при диагностике проблем в уже развернутых службах. К счастью, свойство IncludeExceptionDe- tailsInFaults может задаваться на уровне хоста — как программным, так и адми- нистративным способом в конфигурационном файле хоста. Если свойство зада-
226 Глава 6. Сбои и исключения ется на программном уровне, перед открытием хоста следует найти поведение в описании службы и установить свойство IncludeExceptionDetailsInFaults: ServiceHost host = new ServiceHost(typeof(MyService)): ServiceBehaviorAttribute debuggingBehavior = host.Descri pt1 on.Behaviors.F1nd<Servi ceBehavlorAttrlbute>(): debuggingBehavior.IncludeExceptionDeta11 InFaults « true; host.Open( ); Для удобства эту процедуру можно инкапсулировать в классе ServiceHost<T>, как показано в листинге 6.7. Листинг 6.7. ServiceHost<T> и возврат неизвестных исключений public class ServiceHost<T> : ServiceHost { public bool IncludeExceptionDeta11 InFaults { set { if(State == Communicationstate.Opened) { throw new InvalidOperationExceptionCHost is already opened"); } ServiceBehaviorAttribute debuggingBehavior = Description.Behaviors.F1nd<Serv1ceBehav1orAttr1bute>(); debuggingBehavior.IncludeExceptlonDetallInFaults = value: } get { ServiceBehaviorAttribute debuggingBehavior = DescriptIon.Behaviors.Flnd<ServiceBehavlorAttribute>(); return debuggingBehavior.IncludeExceptlonDetallInFaults: } } } Код использования ServiceHost<T> тривиален и компактен: ServiceHost<MyService> host = new ServiceHost<MyService>(): host.IncludeExceptlonDetal1 InFaults = true: host.OpenO: Чтобы применить это поведение на административном уровне, включите секцию нестандартного аспекта поведения в пользовательский файл конфигу- рации и включите ссылку на него в определение службы, как показано в лис- тинге 6.8. Листинг 6.8. Административная передача информации об исключениях в сообщениях о сбоях <system.serviceModel> <services> <service name = "MyService" behaviorConfiguration = "Debuggings </servlce> </services> <behaviors>
Контракты сбоев 227 <serviceBehaviors> <behavior name = "Debuggings <serviceDebug IncludeExceptionDetailInFaults = "true’7> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> К преимуществам административной настройки в данном случае стоит отне- сти возможность смены поведения во время эксплуатации без изменения кода службы. Сбои и обратный вызов Конечно, обратные вызовы, обращенные к клиенту, могут завершиться не- удачей из-за коммуникационных исключений или из-за того, что сам обратный вызов инициировал исключение. Операции контрактов обратного вызова, как и операции контрактов служб, тоже могут определять контракты сбоев (лис- тинг 6.9). Листинг 6.9. Контракт обратного вызова с контрактом сбоев [ServiceContract (Ca 11 backContract = typeof(IMyContractCallback))] interface IMyContract I [Operationcontract] void DoSomethingf ); interface IMyContractCallback I [Operationcontract] [Faul tContract (typeofC I nval 1 dOperati onExcepti on)) ] void OnCal 1 Back( ); ПРИМЕЧАНИЕ---------------------------------------------------------------------------- Обратные вызовы в WCF обычно настраиваются как односторонние, поэтому они не могут опреде- лять собственных контрактов сбоев. Тем не менее, в отличие от нормальных вызовов служб, проявление ошибки также зависит от следующих факторов: О способа активизации обратного вызова; иначе говоря, был ли он направлен непосредственно при вызове службы вызывающему клиенту или это внепо- лосное обращение от другого объекта на стороне хоста; О используемой привязки; О типа инициированного исключения. При внеполосной активизации (то есть не от самой службы, а от другой сто- роны во время операции службы) обратный вызов ведет себя как обычный вы- зов операции WCF. В листинге 6.10 продемонстрирован внеполосный вызов для контракта обратного вызова, определенного в листинге 6.
228 Глава 6. Сбои и исключения Листинг 6.10. Обработка сбоев при внеполосных вызовах [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>( ); public void DoSomething( ) { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(): if(m_Callbacks.Contains(calIback) == false) { m_Ca11 back s.Add(callback); ) ) public static void CallClientst ) { Action<IMyContractCallback> invoke = delegate(IMyContractCallback callback) ( try { cal Iback.OnCallbackO; } catch(FaultException<InvalidOperationException> exception) catchtFaultException exception) {...} catch(Conwuni cat i onExcept i on except i on) }: m_Ca11 backs.ForEach(i nvoke): } } Если клиентский обратный вызов инициирует исключение, указанное в кон- тракте сбоев обратного вызова, или если обратный вызов инициирует Fault- Exception, это не приведет к отказу канала обратного вызова; вы можете пере- хватить исключение и продолжать пользоваться каналом. Впрочем, как и в слу- чае с обычными вызовами служб, после возникновения исключений, не входя- щих в контракт сбоев, пользоваться каналом обратного вызова не стоит. Если обратный вызов инициируется службой во время операции службы и исключение указано в контракте сбоев или если клиентский обратный вызов инициирует FaultException, сбой обратного вызова ведет себя так же, как при внеполосной активизации: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomething( ) { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); try
Контракты сбоев 229 cal Iback.OnCal1Back(); ) catch(Fau 1 tException<1 nt> exception) {...} ( 1 Обратите внимание: служба должна быть настроена в реентерабельном ре- жиме для предотвращения взаимной блокировки (см. главу 5). Поскольку опе- рация обратного вызова определяет контракт сбоев, она гарантированно не яв- ляется односторонним методом, отсюда и необходимость в реентерабельности. И внеполосные вызовы, и обратные вызовы службы ведут себя интуитивно понятным образом. Ситуация заметно усложняется, если служба активизирует обратный вызов, а операция обратного вызова инициирует исключение, не входящее в контракт сбоев (и не являющееся FaultException). Если служба использует привязку TCP или IPC, то при выдаче исключения, не входящего в контракт, клиент, обратившийся к службе с исходным вызовом, немедленно получает CommunicationException, даже если исключение было пере- хвачено службой. Затем служба получает FaultException. Служба может перехва- тить и обработать исключение, но исключение приводит к отказу канала и его дальнейшее использование службой невозможно: [ServiceBehavior (ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract ( public void DoSomething( ) ( IMyContractCalIback callback = Operationcontext.Current.GetCal1 backchannel<IMyContractCal1back>(): try ( callback.OnCallBackO; I catch(FaultException exception) (...) ) ) Если служба использует двойственную привязку WS, то при выдаче исклю- чения, не входящего в контракт, клиент, обратившийся к службе с исходным вызовом, немедленно получает CommunicationException, даже если исключение было перехвачено службой. При этом служба блокируется, а блокировка сни- мается со временем по исключению тайм-аута: [Servi ceBehavi or (ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract f public void DoSomething( ) ( IMyContractCalIback callback = Operati onContext.Current.GetCa11 backchannel<IMyContractCa11back>(): try
230 Глава 6. Сбои и исключения { ca 11 back.ОпСа11 Back( ); } catchCTimeoutException exception) {...} } } Повторное использование канала обратного вызова службой невозможно. ПРИМЕЧАНИЕ------------------------------------------------------------------------- Описанные различия в поведении при обратном вызове являются архитектурным недостатком WCF; иначе говоря, они были спроектированы, а не являются результатом ошибки. Возможно, проблема будет частично решена в будущих версиях. Отладка обратных вызовов Хотя для обратных вызовов может использоваться способ, представленный в листинге 6.4 (ручная передача исключения в сообщении о сбое), у атрибута CaLLbackBehavior имеется логическое свойство IncLudeExceptionDetaiLInFauLts; оно используется для передачи в сообщении всех исключений, не входящих в кон- тракт сбоев (кроме FaultException): [Attri buteUsage(AttгibuteTargets.Cl ass)] public sealed class CallbackBehaviorAttribute : Attribute.... { public bool IncludeExceptionDetailInFaults {get:set:} //... } Как уже упоминалось ранее, передача исключений является эффективным средством отладки: [Cal 1backBehavior(IncludeExceptionDetailInFaults = true)] class MyClient : IMyContractCalIback { public void OnCallBack( ) { throw new InvalidOperationException( ); } } Описанное поведение также можно задать на административном уровне в клиентском конфигурационном файле: <client> <endpoint ... behaviorConfiguration = "Debug" /> </client> <behaviors> <endpointBehaviors> <behavior name = "Debug"> <callbackDebug IncludeExceptionDetailInFaults = "true"/> </behavior>
Расширения обработки ошибок 231 </endpointBehaviors> </behaviors> Обратите внимание на использование тега endpointBehaviors для воздействия наконечную точку обратного вызова. Расширения обработки ошибок WCF позволяет разработчику изменить стандартную схему передачи исключе- ний и даже устанавливать свои перехватчики (hooks). Расширение применяет- ся на уровне канального диспетчера, хотя вам, по всей вероятности, потребует- ся просто применить его для всех диспетчеров. Чтобы установить собственное расширение обработки ошибок, необходимо предоставить диспетчерам реализацию интерфейса lErrorHandter, определяемого следующим образом: public interface lErrorHandler ( bool HandleError(Exception error): void ProvideFault(Exception error.Messageversion version, ref Message fault); ) Реализация может быть предоставлена любой стороной, но обычно она пре- доставляется либо самой службой, либо хостом. Более того, несколько расши- рений обработки ошибок можно объединить в цепочку. Позднее в этом разделе будет показано, как устанавливаются расширения. Метод ProvideFaultQ Метод ProvideFault() объекта расширения вызывается немедленно после того, как служба или любой объект в нисходящей цепочке вызовов инициирует лю- бое исключение. WCF вызывает ProvideFauLtQ перед тем, как возвращать управ- ление клиенту, перед завершением сеанса (если он имеется) и перед уничтоже- нием экземпляра службы (если требуется). Поскольку ProvideFault() вызывается во входном потоке вызова, пока клиент блокируется в ожидании завершения операции, следует избегать выполнения длительных операций в ProvideFault(). Использование ProvideFaultQ Метод ProvideFaultQ вызывается независимо от типа инициированного исклю- чения, будь то обычное исключение CLR, контрактный или неконтрактный сбой. Параметр error содержит ссылку на только что инициированное исключе- ние. Если ProvideFaultQ не делает ничего, то клиент получает исключение в со- ответствии с контрактом сбоев (если он присутствует) и типом исключения, как было описано ранее в этой главе: class MyErrorHandler : lErrorHandler public bool HandleError(Exception error) public void ProvideFault(Exception error.Messageversion version.
232 Глава 6. Сбои и исключения ref Message fault) { // Ничего не делаем - исключение проходит, как обычно } } Однако метод ProvideFault() может проанализировать параметр error и либо вернуть его клиенту в исходном виде, либо предоставить альтернативный сбой. Это относится даже к исключениям, входящим в контракт сбоев. Чтобы пре- доставить альтернативный сбой, необходимо воспользоваться методом Create- MessageFault() объекта FaultException для создания сообщения альтернативного сбоя. Если вы предоставляете новое сообщение для контракта сбоев, необходи- мо создать новый объект детализации и при этом нельзя использовать исход- ную ссылку error. Затем созданный объект передается статическому методу CreateMessage() класса Message: public abstract class Message { public static Message CreateMessage(MessageVersion version. MessageFault fault.string action): //... } Обратите внимание: при вызове CreateMessage() также необходимо передать информацию об используемом действии (action). Пример представлен в лис- тинге 6.11. Листинг 6.11. Создание альтернативного сбоя class MyErrorHandler : lErrorHandler { public bool HandleError(Exception error) {...} public void ProvideFault(Exception error.Messageversion version, ref Message fault) { FaultException<int> faultException = new FaultException<int>(3): MessageFault messageFault = faultException.CreateMessageFault(): fault = Message.CreateMessage(version. messageFault.faultExcepti on.Action); } } В листинге 6.11 метод ProvideFault() передает FaultException<int> значение 3 в качестве сбоя, инициированного службой, — независимо от того, какое ис- ключение было инициировано в действительности. Реализация ProvideFault() также может задать параметр fault равным null: class MyErrorHandler : lErrorHandler { public bool HandleError(Exception error) {•••} public void ProvideFault(Exception error.Messageversion version. ref Message fault)
Расширения обработки ошибок 233 fault = null: //Подавление любых сбоев в контракте 1 Это приведет к тому, что все исключения — даже соответствующие контрак- ту сбоев — будут доставляться клиенту в виде FaultException. Задание fault рав- ным null — эффективный способ подавления существующих контрактов сбоев. Повышение исключений Одно из возможных применений Provide Fa u lt() — методика, которую я называю повышением исключения. Служба может использовать в своей работе нисходя- щие объекты. Вызовы к таким объектам поступают от нескольких разных служб. В интересах логической изоляции эти объекты могут и не знать о конкретных контрактах сбоев служб, от которых поступает вызов. При возникновении оши- бок они просто инициируют обычные исключения CLR. Служба в такой ситуа- ции может проанализировать исключение при помощи расширения обработки ошибок. Если исключение относится к типу Т, a FaultException<T> входит в кон- тракт сбоев операции, служба может повысить исключение до полноценного FaultException<T>. Для примера возьмем следующий контракт службы: interface IMyContract I [Operationcontract] [Faul tCont ract (typeof( I nval 1 dOperat 1 onExcept i on)) ] void MyMethod( ); 1 Если нисходящий объект инициирует InvalidOperationException, ProvideFault() повысит его до FaultException<InvalidOperationExceptions как показано в листин- ге 6.12. Листинг 6.12. Повышение исключений class MyErrorHandler : lErrorHandler ( public bool HandleErrorCException error) {...} public void ProvideFault(Except!on error.Messageversion version. ref Message fault) ( if(error is InvalidOperationException) FaultException<InvalidOperationException> faultException - new FaultException<InvalidOperationException>( new InvalidOperationException(error.Message)): MessageFault messageFault = faultException.CreateMessageFaultO. fault = Message.CreateMessage(version.messageFault. faultException.Action); У кода в листинге 6.12 имеется один недостаток: код привязывается с кон- кретному контракту сбоев, а для его реализации во всех службах потребуется
234 Глава 6. Сбои и исключения большая, утомительная работа, не говоря уже о том, что любые изменения в контракте сбоев потребуют изменения расширения. К счастью, задачу повышения исключений можно автоматизировать при по- мощи моего статического класса ErrorHandLerHeLper: public static class ErrorHandlerHelper { public static void PromoteExceptIon(Type servIceType. Exception error. Messageversion version, ref Message fault): //... } При вызове ErrorHandlerHeLper.PromoteException() в параметре передается тип службы. Метод анализирует все интерфейсы и операции указанного типа служ- бы с использованием рефлексии и ищет в них контракты сбоев для конкретной операции. Информация о сбойной операции получается посредством разбора объекта error. PromoteException() повышает исключение CLR до контрактного сбоя, если тип исключения совпадает с одним из детализирующих типов, опре- деленных в контракте сбоев операции. При использовании класса ErrorHandlerHelper листинг 6.12 сокращается до одной-двух строк программного кода: class MyErrorHandler : lErrorHandler { public bool HandleError(Except1on error) {...} public void ProvldeFault(Exceptlon error.Messageversion version, ref Message fault) { Type servIceType = ...: ErrorHandlerHelper.PromoteExcept1 on(serviceType.error. version.ref fault): } } Реализация PromoteException() не имеет особого отношения к WCF, поэтому в этой главе она не рассматривается. При желании вы можете изучить ее в ис- ходном коде, прилагаемом к книге. В реализации используются многие нетри- виальные концепции программирования C# — такие, как шаблоны и рефлек- сия, разбор строк, анонимные методы и позднее связывание. Обработка сбоев Метод HandleError() интерфейса lErrorHandler определяется следующим образом: bool HandleError(Except1on error): Метод HandleError() вызывается WCF после возврата управления клиенту. Он предназначен исключительно для использования на стороне сервера, и ни- чего из того, что он делает, не затрагивает клиента. HandleError() вызывается в отдельном программном потоке — не в том, который использовался для обра- ботки запроса службы (и вызова ProvideFault()). Наличие отдельного потока,
Расширения обработки ошибок 235 работающего в фоновом режиме, позволяет выполнять продолжительные опе- рации (например, подключение к базе данных) без отрицательного влияния на работу клиента. Так как вы можете установить цепочку из нескольких расширений обработ- ки ошибок, WCF также позволяет управлять тем, какие из расширений в цепоч- ке должны использоваться в конкретном случае. Если HandLeError() возвращает false, то WCD продолжает вызывать HandLeError() для остальных установленных расширений. Если HandLeError() вернет true, WCF перестает вызывать расшире- ния. Разумеется, большинство расширений должно возвращать false. Параметр error метода HandleError() определяет исходное инициированное ис- ключение. Традиционно HandleError() используется для ведения протоколов и трас- сировки, как показано в листинге 6.13. Листинг 6.13. Регистрация ошибок class MyErrorHandler : lErrorHandler public bool HandleError(Exception error) try { LogbookServiceCllent proxy = new LogbookServiceCl ient(); proxy.Log(.. proxy.Close(): catch () finally return false: } ) public void ProvideFault(Exception error.Messageversion version, ref Message fault) Служба Logbook В архиве примеров, прилагаемом к книге, содержится автономная служба Log- bookService, предназначенная для регистрации ошибок. LogbookService регистри- рует ошибки в базе данных SQL Server, в контракт службы входят операции для чтения записей и очистки журнала. В исходный код также включена про- стейшая программа просмотра журнала. Кроме регистрации ошибок, Logbook- Service позволяет явно заносить информацию в журнал, независимо от исклю- чений. Архитектура службы показана на рис. 6.1. Чтобы автоматизировать регистрацию ошибок с применением LogbookService, воспользуйтесь методом LogError() моего статического класса ErrorHandLerHelper: public static class ErrorHandlerHelper ( public static void LogError(Exception error): II... )
236 Глава 6. Сбои и исключения Рис. 6.1. LogbookService и программа просмотра В параметре error передается регистрируемое исключение. LogError() инкап- сулирует вызов LogbookService. Например, вместо листинга 6.13 оказывается достаточно одной строки: class MyErrorHandler : lErrorHandler { public bool Handl eError(Except!on error) { ErrorHandlerHelper.LogError(error); return false; } public void ProvideFault(Except!on error.MessageVersion version, ref Message fault) {•••} } Кроме непосредственной информации об исключениях, LogError() извлекает из исключения и переменных окружения данные, которые дают полное описа- ние ошибки и связанной с ней информации. Конкретнее, LogError() сохраняет следующую информацию: О где произошло исключение (имена компьютера и хостового процесса); О программный модуль, в котором произошло исключение (имя сборки, имя файла и номер строки); О тип и член типа, в котором произошло исключение; О дата и время исключения; О имя исключения и сообщение.
Расширения обработки ошибок 237 Реализация LogError() не имеет особого отношения к WCF, поэтому в этой главе она не рассматривается. С другой стороны, в ней широко применяются интересные программные методики .NET — разбор строк и исключений, полу- чение информации об окружении. Для передачи информация об ошибке Log- bookService используется специализированный контракт данных. Установка расширений обработки ошибок У каждого диспетчера канала в WCF имеется коллекция расширений обработ- ки ошибок: public class Channel Dispatcher : Channel Di spatcherBase public Collection<IErrorHandler> ErrorHandlers {get;} II... 1 Чтобы установить пользовательскую реализацию lErrorHandler, достаточно включить ее в коллекцию нужного диспетчера (обычно всех диспетчеров). Расширения обработки ошибок должны добавляться до получения службой первого вызова, но после конструирования диспетчеров хостом. Это узкое окно возникает после того, как хост был инициализирован, но до его открытия. Что- бы попасть в него, лучше всего рассматривать расширения ошибок как пользо- вательские аспекты поведения служб, потому что аспектам поведения предо- ставляется возможность взаимодействовать с диспетчерами как раз в нужный промежуток времени. Как упоминалось в главе 4, все аспекты поведения служб реализуют интерфейс IServiceBehavior, определяемый следующим образом: public interface IServiceBehavior { void AddBindingParameters(ServiceDescription description. ServiceHostBase host. Col 1ecti on<ServiceEndpoi nt> endpoi nts. BindingParameterCollection parameters): void ApplyDispatchBehavior(ServiceDescription description. ServiceHostBase host): void Validate(ServiceDescription description.ServiceHostBase host): ) Метод ApplyDispatchBehavior() — переключатель включения расширений в кол- лекции диспетчеров. На остальные методы IServiceBehavior можно не обращать внимания и предоставить для них пустые реализации. В ApplyDispatchBehaviог() обращение к коллекции диспетчеров производится через свойство ChannelDispatchers класса ServiceHostBase: public class Channel Dispatchercollection : Synchroni zedCol1ect i on<ChannelDi spatcherBase> {) public abstract class ServiceHostBase : public Channel Di spatcherCol lection ChannelDispatchers (get;) //... I
238 Глава 6. Сбои и исключения Элементы коллекции ChannelDispatchers относятся к типу ChannelDispatcher. Вы можете включить реализацию lErrorHandler в коллекции всех диспетчеров или же ограничиться конкретным диспетчером, связанным с определенной привязкой. В листинге 6.14 показано, как реализация lErrorHandler добавляется во все диспетчеры службы. Листинг 6.14. Добавление расширений обработки ошибок class MyErrorHandler : lErrorHandler {•} class MyService : IMyContract.IServiceBehavior { public void ApplyD1spatchBehav1or(Serv1ceDescr1pt1on description, ServiceHostBase host) { lErrorHandler handler = new MyErrorHandler(): foreach(ChannelDispatcher dispatcher in host.ChannelDispatchers) { di spatcher.ErrorHandlers.Add(handler): ) } public void Vaiidate(...) 0 public void AddBindingParameters(...) {} } В листинге 6.14 сама служба реализует IServiceBehavior. В методе ApplyDis- patcheBehavior() служба получает коллекцию диспетчеров и включает в каждый из них экземпляр класса MyErrorHandler. Вместо того чтобы полагаться на реали- зацию lErrorHandler внешним классом, класс службы сам может поддерживать lErrorHandler напрямую, как показано в листинге 6.15. Листинг 6.15. Поддержка lErrorHandler классом службы class MyService : IMyContract.IServiceBehavior,lErrorHandler { public void ApplyD1spatchBehav1or(ServiceDescr1pt1on description. ServiceHostBase host) { foreach(ChannelDIspatcher dispatcher In host.ChannelDispatchers) { di spatcher.ErrorHandlers.Add(this): } } public bool HandleError(Except1on error) {...} public void Prov1deFault(Exception error.Messageversion version, ref Message fault) {...} //... }
Расширения обработки ошибок 239 Атрибут обработки ошибок У решений из листингов 6.14 и 6.15 имеется одна общая проблема: они «загряз- няют» класс службы вспомогательным кодом WCF. Вместо того чтобы сосредо- точиться на бизнес-логике, разработчику приходится заниматься подключением расширений. К счастью, того же результата можно добиться на декларативном уровне; в этом поможет созданный мной атрибут ErrorHandlerBehaviorAttribute, определяемый следующим образом: public class ErrorHandlerBehaviorAttribute : Attribute. lErrorHandler. IServiceBehavior I protected Type ServiceType (get: set:} Применение атрибута ErrorHandlerBehavior вполне тривиально: [ErrorHandl erBehavi or ] class MyService : IMyContract (...) Атрибут устанавливает себя как расширение обработки ошибок. Его реали- зация использует ErrorHandlerHelper как для автоматического повышения исклю- чений в случае необходимости, так и для автоматической регистрации исклю- чений в LogbookService. Код атрибута ErrorHandlerBehavior приведен в листин- ге 6.16. Листинг 6.16. Атрибут ErrorHandlerBehavior [Attri butellsage (Attri buteTargets. Class) ] public class ErrorHandlerBehaviorAttribute : Attribute. IServiceBehavior. lErrorHandler ( protected Type ServiceType {get:set:} void IServiceBehavior.ApplyDispatchBehavior( ServiceDescription description. ServiceHostBase host) ServiceType = description. ServiceType: foreach(Channel Dispatcher dispatcher in host.ChannelDispatchers) dispatcher.ErrorHandlers.Add(this): bool IErrorHandler.HandleError(Exception error) ErrorHandlerHelper.LogError(error); return false: void IErrorHandler.ProvideFault(Exception error.Messageversion version. ref Message fault) ErrorHandl erHel per. PromoteExcept i on (Serv i ceType .error. version.ref fault): ) продолжение &
240 Глава 6. Сбои и исключения void IServiceBehavior.Vaiidatet...) {) void IServiceBehavior.AddBindingParameters!...) {) } Обратите внимание на сохранение типа службы в защищенном свойстве ме- тодом ApplyDispatchBehavior(). Тип службы необходим для вызова ErrorHandler- Helper.PromoteException() в ProvideFault(). Хост и расширения обработки ошибок Хотя атрибут ErrorHandlerBehavior значительно упрощает установку расшире- ний, он должен быть применен разработчиком службы. Было бы лучше, если бы хост мог добавлять расширения ошибок независимо от того, предоставляются они хостом или нет. Тем не менее из-за узких временных рамок установки рас- ширения добавление расширения хостом состоит из нескольких этапов. Снача- ла необходимо предоставить тип расширения обработки ошибок с поддержкой как IServiceBehavior, так и lErrorHandler. Реализация IServiceBehavior добавит рас- ширение в диспетчеры, как было показано ранее. Затем определяется класс хос- та, производный от ServiceHost, и переопределяется метод OnOpening() опреде- ляемый базовым классом Communicationobject: public abstract class Communicationobject : ICommunicationObject { protected virtual void OnOpen1ng( ): //... } public abstract class ServiceHostBase : Communicationobject .... {...} public class ServiceHost : ServiceHostBase.... {...} В OnOpening() пользовательский тип обработки ошибок включается в кол- лекцию аспектов поведения в описании службы. Коллекция была описана в главах 1 и 4: public class Col 1ectlon<T> : IL1st<T>.... { public void Add(T item); //... } public abstract class KeyedCollect1on<K.T> : Collect1on<T> {...} public class KeyedByTypeCollect1on<I> : KeyedCollect1on<Type.I> {...} public class ServIceDescrlptlon { public KeyedByTypeCollection<IServiceBehavior> Behaviors {get:} } public abstract class ServiceHostBase : ... { public ServiceDescription Description {get:}
Расширения обработки ошибок 241 //... I Эта последовательность действий уже инкапсулирована и автоматизирована B$erviceHost<T>: public class ServiceHost<T> : ServiceHost ( public void AddErrorHandler(lErrorHandler errorHandler); public void AddErrorHandler(); //... ) ServiceHost<T> содержит две перегруженные версии метода AddErrorHandler(). Версия, получающая объект lErrorHandler, во внутренней реализации связывает его с аспектом поведения, чтобы его можно было использовать с любым клас- сом, поддерживающим только lErrorHandler без IServiceBehavior: class MyService : IMyContract (...) class MyErrorHandler : lErrorHandler (...) ServiceHost<MyService> host = new ServiceHost<MyService>(): host.AddErrorHandler(new MyErrorHandler()): host. Open () ; Метод AddErrorHandler() без параметров устанавливает расширение с исполь- зованием ErrorHandlerHelper, как если бы класс службы был помечен атрибутом ErrorHandlerBehavior: class MyService : IMyContract (...) SerivceHost<MyServi ce> host = new SerivceHost<MyService>( ); host.AddErrorHandler( ); host.OpenO; В последнем примере ServiceHost<T> использует в своей внутренней реализа- ции экземпляр ErrorHandlerBehaviorAttribute. Реализация метода AddErrorHandler() приведена в листинге 6.17. Листинг 6.17. Реализация AddErrorHandler public class ServiceHost<T> : ServiceHost I class ErrorHandlerBehavior : IServiceBehavior.lErrorHandler lErrorHandler m_ErrorHandler; public ErrorHandlerBehavior(lErrorHandler errorHandler) m_ErrorHandler = errorHandler: ) void IServiceBehavior.ApplyDispatchBehavior( ServiceDescription description. ServiceHostBase host) { foreach(ChannelDispatcher dispatcher in host.Channel Dispatchers) { продолжение &
242 Глава 6. Сбои и исключения dispatcher.ErrorHandlers.Add(thi s): } } bool IErrorHandler.HandleError(Exception error) { return m_ErrorHandler.HandleError(error). } void lErrorHandler.ProvideFault(Exception error. Messageversion version, ref Message fault) ( m_ErrorHandler.ProvideFault(error.version.ref fault); } //... } L1st<IServ1ceBehavior> m_ErrorHandlers = new List<IServiceBehavior>(): public void AddErrorHandler(lErrorHandler errorHandler) ( 1f(State == CommunicationState.Opened) { throw new InvalidOperationExceptionC'Host is already opened"); } IServiceBehavior errorHandler = new ErrorHandlerBehavior(errorHandler): m_ErrorHandlers.Add(errorHandlerBehavi or); } public void AddErrorHandler() { if(State == CommunicationState.Opened) { throw new InvalidOperationExceptionC'Host is already opened"): } IServiceBehavior errorHandler = new ErrorHandlerBehaviorAttnbute(); m_ErrorHandlers.Add(errorHandlerBehavior); } protected override void OnOpeningO { foreachtIServiceBehavior behavior in m_ErrorHandlers) { Description.Behaviors.Add(behavior); } base.OnOpeningO: } //... } Чтобы избежать требования об обязательной поддержке IServiceBehavior ссылкой на lErrorHandler, ServiceHost<T> определяет приватный вложенный класс с именем ErrorHandlerBehavior. ErrorHandlerBehavior реализует как lErrorHandler, так и IServiceBehavior. Для конструирования ErrorHandlerBehavior необходимо пре- доставить реализацию lErrorHandler, которая сохраняется для последующего ис- пользования. Реализация IServiceBehavior включает сам экземпляр в коллекции обработчиков ошибок всех диспетчеров. Реализация lErrorHandler просто пере- дает обращения сохраненному параметру конструирования. ServiсеHost<T> опре-
Расширения обработки ошибок 243 деляет список ссылок на IServiceBehavior в переменной m_ErrorHandlers. Метод AddErrorHandler(), получающий ссылку на lErrorHandler, использует ее для конст- руирования экземпляра ErrorHandlerBehavior и включает экземпляр в m_Error- Handlers. Метод AddErrorHandler(), не получающий параметров, использует эк- земпляр ErrorHandlerBehaviorAttribute, потому что атрибут представляет собой всего лишь класс с поддержкой как lErrorHandler, так и IServiceBehavior. Экземп- ляр атрибута также включается в m_ErrorHandlers. Наконец, метод 0n0pening() перебирает содержимое m_ErrorHandlers, включая каждый элемент в коллекцию аспектов поведения. Обратные вызовы и расширения обработки ошибок Объект обратного вызова на стороне клиента также может предоставить реали- зацию lErrorHandler для обработки ошибок. По сравнению с расширениями на стороне службы главное отличие состоит в том, что для установки расширения должен использоваться интерфейс lEndpointBehavior, определяемый следующим образом: public interface lEndpointBehavior void AddBmdingParametersCServiceEndpoint ServiceEndpoint, BindingParameterCollection bindingParameters): void ApplyClientBehavior(ServiceEndpoint ServiceEndpoint. CllentRuntime behavior), void ApplyDispatchBehavior(ServiceEndpoint ServiceEndpoint, EndpointDispatcher endpointDispatcher); void ValidateCServiceEndpoint ServiceEndpoint); Интерфейс lEndpointBehavior поддерживается всеми аспектами поведения обратного вызова. Единственным методом, относящимся к установке расшире- ния обработки ошибок, является метод ApplyClientBehavior(), связывающий рас- ширение с одним диспетчером обратного вызова. Параметр behavior относится к типу ClientRuntime, обладающему свойством CallbackDispatchRuntime типа Dis- patchRuntime. Класс DispatchRuntime предоставляет свойство ChannelDispatcher с коллекцией обработчиков ошибок: public sealed class ClientRuntime public DispatchRuntime CallbackDispatchRuntime (get;} //... ) public sealed class DispatchRuntime public ChannelDispatcher ChannelDispatcher (get:} //... Как и в случае с расширениями на стороне службы, в эту коллекцию необхо- димо включить пользовательскую реализацию lErrorHandler.
244 Глава 6. Сбои и исключения Объект обратного вызова может сам реализовать lEndpointBehavior, как пока- зано в листинге 6.18. Листинг 6.18. Реализация lEndpointBehavior class MyErrorHandler : lErrorHandler {•••} class MyClient : IMyContractCallback.lEndpointBehavior { public void OnCallBackO {••} void lEndpointBehavior.ApplyClientBehavior( ServiceEndpoint ServiceEndpoint. ClientRuntime behavior) { lErrorHandler handler = new MyErrorHandler(); behavi or.Cal 1backDi spatchRuntime.Channel Di spatcher. ErrorHandlers.Add(handler); } void lEndpointBehavior.AddBindingParameterst...) {} void lEndpointBehavior.ApplyDispatchBehavior(...) {} void lEndpointBehavior Vaiidatet...) {} } Вместо того чтобы использовать внешний класс для реализации lErrorHandler, класс обратного вызова может реализовать lErrorHandler напрямую: class MyClient : IMyContractCalIback.lEndpointBehavior.lErrorHandler { public void OnCallBack( ) {...} void IEndpointBehavior.ApplyClientBehavior( ServiceEndpoint ServiceEndpoint. ClientRuntime behavior) { behavior.CallbackDi spatchRuntime. ChannelDispatcher.ErrorHandlers.Add(this); } public bool HandleError(Exception error) {...} public void ProvideFault(Exception error.MessageVersion version. ref Message fault) {...} } Атрибут CallbackErrorHandlerBehaviorAttribute Для автоматизации такого кода, как в листинге 6.18, определяется атрибут CallbackErrorHandlerBehaviorAttribute: public class CalIbackErrorHandlerBehaviorAttribute : ErrorHandlerBehaviorAttribute. lEndpointBehavior
Расширения обработки ошибок 245 public Cal 1 backErrorHandlerBehaviorAttribute(Type clientType); ) Атрибут CallbackErrorHandlerBehavior объявляется производным от атрибута ErrorHandlerBehavior и добавляет в него явную реализацию lEndpointBehavior. Ат- рибут использует ErrorHandlerHelper для повышения и регистрации исключений. Кроме того, в параметре конструирования атрибуту должен передаваться тип обратного вызова, к которому он применяется: [Cal 1 ЬаскЕггогНа nd 1 erBehavi or (typeof (MyCl i ent)) ] class MyCllent : IMyContractCallback ( public void OnCal 1 Back() Это необходимо, потому что не существует другого способа получения типа обратного вызова, требуемого для ErrorHandlerHelper.PromoteException(). Реализация CallbackErrorHandlerBehaviorAttribute приведена в листинге 6.19. Листинг 6.19. Реализация атрибута CallbackErrorHandlerBehavior public class CalIbackErrorHandlerBehaviorAttribute : ErrorHandlerBehaviorAttribute. lEndpointBehavior I public CallbackErrorHandlerBehaviorAttribute(Type clientType) ServiceType = clientType; ) void lEndpointBehavior .ApplyCl ientBehavior( ServiceEndpoint serviceEndpoint. ClientRuntime behavior) behavior .Call backDi spatchRuntime.Channel Di spatcher. ErrorHandlers.Add(this): ) void lEndpointBehavior.AddBindingParameters(...) (I void lEndpointBehavior.ApplyDispatchBehaviori...) (} void lEndpointBehavior.Vaiidate(...) (I I Обратите внимание, как переданный тип обратного вызова сохраняется всвойстве ServiceType, объявленном как защищенное (protected) в листинге 6.16.
Транзакции Транзакции играют ключевую роль при построении устойчивых к ошибкам, ка- чественных служебно-ориентированных приложений. WCF предоставляет в распоряжение разработчиков служб простую декларативную поддержку тран- закций, которая обеспечивает настройку различных параметров вне области действия самой службы. Кроме того, WCF позволяет клиентским приложени- ям создавать транзакции и распространять их за границу службы. Глава начи- нается с описания проблем, для решения которых создавались транзакции, и основных терминов. Далее рассматривается поддержка и управление транзак- циями в WCF. Оставшаяся часть главы посвящена транзакционным моделям программирования (на стороне как службы, так и клиентов), а также связи транзакций с другими аспектами WCF — в частности, управлением экземпля- рами и механизмом обратного вызова. Восстановление после сбоев Обработка ошибок и восстановление — ахиллесова пята многих приложений. Если системе не удается выполнить некоторую операцию, необходимо произве- сти восстановление и привести систему (то есть совокупность взаимодействую- щих служб и клиентов) в стабильное состояние — как правило, в то, в котором она находилась до выполнения операции, вызвавшей ошибку. Любая операция, в ходе которой может произойти сбой, обычно состоит из нескольких этапов (возможно, выполняемых параллельно). Одни этапы могут пройти успешно, в ходе других возникают сбои. Основная проблема восстановления — огромное количество комбинаций частичных успехов и неудач, которые придется запро- граммировать в приложении. Например, операция из 10 параллельных этапов имеет более трех миллионов сценариев восстановления, потому что в логике восстановления также должен учитываться порядок выполнения сбойных опе- раций, а факториал 10 равен примерно трем миллионам. Попытки ручного программирования восстановления в приложении сколь- ко-нибудь реального объема обычно обречены на неудачу — код получается слишком неустойчивым, слишком сильно зависимым от изменений в логике
Транзакции 247 приложения или бизнес-сценарии, а это приводит к снижению как эффектив- ности труда программиста, так и производительности самого приложения. Пер- вое обусловлено теми усилиями, которые программисту приходится тратить на ручную реализацию логики восстановления. Производительность ухудшается оттого, что после каждой операции приходится выполнять большой объем кода и убеждаться в том, что все прошло успешно. На практике разработчики имеют дело только с простыми сценариями восстановления, то есть с теми ситуация- ми, о которых они знают, и представляют возможные действия. Более сложные ситуации - например, промежуточные сетевые сбои или отказы дисков — оста- ются без внимания. Кроме того, поскольку восстановление связано с переводом системы в стабильное состояние (как правило, на момент до начала операции), настоящие проблемы возникает не со сбойными, а с успешными операциями — ведь сбойные операции обычно не изменяют состояния системы. Вопрос в том, как отменить успешные действия — удаление записи из таблицы, узла из свя- занного списка или обращения к удаленной службе. Сценарии могут оказаться очень сложными, и при ручной реализации восстановления вы почти наверня- ка упустите какие-нибудь успешные субоперации. Чем сложнее логика восстановления, тем в большей степени она подвержена ошибкам. Если в процессе восстановления произойдет ошибка, как восстанав- ливать восстановление? Как разработчик должен подходить к проектированию, тестированию и отладке сложной логики восстановления? Как имитировать бесконечное количество возможных ошибок и сбоев? А если до неудачной опе- рации приложение успешно выполнило ряд других операций, когда другая сто- рона обратилась к нему и стала действовать на основании текущего состояния системы - того, которое будет отменено в процессе восстановления? Теперь другая сторона действует на основании недействительной информации, что тоже по определению является ошибкой. Более того, ваша операция может быть этапом другой, более масштабной операции, в которой задействованы раз- ные службы от разных разработчиков на разных компьютерах. Как восстано- вить систему в целом в подобной ситуации? Даже если вы каким-то чудом вос- становите собственную службу, как подключить логику ее восстановления к процессу восстановления всей системы? Транзакции Лучшим (а возможно, и единственным) механизмом поддержания целостности системы и решения проблем восстановления после ошибок является использо- вание транзакций. Транзакцией называется набор операций (возможно, слож- ных), в которой сбой одной операции приводит к сбою всего набора как одной атомарной операции. Как показано на рис. 7.1, во время выполнения транзак- ции система может временно находиться в нестабильном состоянии, но после завершения транзакции она заведомо находится в стабильном состоянии - либо новом (В), либо исходном, в котором она находилась до начала транзак- ции (А).
248 Глава 7. Транзакции Рис. 7.1. Транзакция переводит систему из одного стабильного состояния в другое Если транзакция была выполнена успешна и система перешла из стабильно- го состояния А в стабильное состояние В, она называется закрепленной тран- закцией. Если в ходе выполнения транзакции возникли ошибки и все промежу- точные действия были отменены, транзакция называется отмененной. Если транзакция не перешла ни в закрепленное, ни в отмененное состояние, она на- зывается сомнительной транзакцией. Разрешение сомнительных транзакций обычно требует вмешательства администратора или пользователя, однако дан- ная тема выходит за рамки книги. Ресурсы транзакций Для транзакционного программирования необходима возможность работы с ре- сурсами (базы данных, очереди сообщений), которые способны участвовать в транзакциях, а также закреплять или отменять изменения, внесенные в ходе транзакции. Такие ресурсы в тех или иных формах существуют уже много лет. Традиционно разработчик должен оповестить ресурс о том, что с ним будут вы- полняться транзакционные операции. Это называется включением (enlistment) ресурса в транзакцию. Некоторые ресурсы поддерживают автоматическое включение, то есть обнаруживают факт обращения к ним из транзакций, и авто- матически включаются в нее. После привлечения с ресурсом выполняются не- обходимые операции, и при отсутствии ошибок ресурсу выдается запрос на за- крепление изменений, внесенных в его состояние. При возникновении ка- ких-либо ошибок ресурс получает запрос на отмену изменений. Очень важно, чтобы во время транзакции отсутствовали обращения к нетранзакционным ре- сурсам (таким, как файловая система в Windows ХР), потому что изменения в таких ресурсах не будут отменены в случае отмены транзакции. Свойства транзакций При использовании транзакций в служебно-ориентированных приложениях должны соблюдаться четыре свойства, обозначаемые сокращением ACID: ато- марность (Atomic), согласованность (Consistent), изолированность (Isolated) и устойчивость (Durable). При проектировании транзакционных служб необхо-
Транзакции 249 димо придерживаться требований ACID — они не являются необязательными. Как будет показано в этой главе, WCF жестко следит за их соблюдением. Атомарность Свойство атомарности' означает, что после завершения транзакции все изме- нения, вносимые в состояние ресурса, должны вноситься в виде одной атомар- ной, неделимой операции. Все происходит так, словно время останавливается, вносятся изменения, после чего время снова начинает идти. Никакая сторона за пределами транзакции не должна видеть ресурс с частью внесенных изменений. После транзакции не должны оставаться операции, выполняемые в фоновом режиме, потому что это нарушает свойство атомарности. Каждая операция, вы- полняемая вследствие транзакции, должна быть включена в саму транзакцию. Атомарность транзакций значительно упрощает разработку клиентских при- ложений. Клиенту не приходится иметь дело с частичными сбоями при запро- сах или использовать сложную логику восстановления. Клиент знает, что тран- закция завершается успехом или неудачей как единое целое. В случае сбоя клиент может выбрать между выдачей нового запроса (началом новой транзак- ции) или другими действиями — например, оповещением пользователя. Здесь важно то, что клиенту не нужно заниматься восстановлением системы. Согласованность Свойство согласованности означает, что транзакция должна оставлять систему в стабильном состоянии. Обратите внимание: согласованность — не то же са- мое, что атомарность. Даже если все изменения закрепляются как одна атомар- ная операция, транзакция должна гарантировать их «осмысленность». Обыч- но за согласованность семантики операций отвечает разработчик. Все, что тре- буется от транзакции — перевести систему из одного стабильного состояния в другое. Изолированность Свойство изолированности означает, что ни одна сторона (участвующая в тран- закции или нет) не видит промежуточного состояния ресурса во время транзак- ции, потому что это состояние может оказаться нестабильным. Более того, даже если состояние стабильно, транзакция может быть отменена вместе со всеми изменениями. Изоляция играет важную роль для общей стабильности системы. Допустим, транзакция А позволяет транзакции В обратиться к своему проме- жуточному состоянию. Транзакция А отменяется, а транзакция В закрепляется. Проблема в том, что выполнение транзакции В было основано на состоянии сис- темы, которое было отменено, что привело к непреднамеренному нарушению согласованности транзакции В. Управление изоляцией — тема нетривиальная. Ресурсы, участвующие в транзакции, должны заблокировать используемые 1 Слово «атом» происходит от греческого «атомос», означающего «неделимый». Древние греки пола- гали, что в процессе последовательного деления материи вы в конечном счете доберетесь до неде- лимых фрагментов, которые они назвали «атомос». Конечно, они ошибались, потому что атомы делятся на субатомные частицы (электроны, протоны и нейтроны). А вот транзакции должны оста- ваться неделимыми.
250 Глава 7. Транзакции данные от всех остальных сторон и открыть доступ к ним только после закреп- ления или отмены транзакции. Устойчивость Традиционно транзакционная поддержка подразумевает не только возмож- ность использования ресурса в контексте транзакций, но и его устойчивость. В любой момент времени в приложении может произойти сбой, и содержимое занимаемой им памяти окажется стертым. Если изменения в состоянии систе- мы существуют только в памяти, они будут утрачены, и система перейдет в не- стабильное состояние. Тем не менее устойчивость в действительности является диапазонной, а не точечной характеристикой. Устойчивость ресурса перед по- добными катастрофами зависит от природы и чувствительности данных, бюд- жета, доступности времени и персонала, занимающегося системным админист- рированием и т. д. Но если устойчивость определяется в виде диапазона, стоит рассмотреть и дальний конец спектра — неустойчивые (временные) ресурсы, хранящиеся в памяти. У временных ресурсов есть свои преимущества: они обла- дают более высокой скоростью доступа, и, что еще важнее, они позволяют лучше воспроизвести традиционные модели программирования, используя поддержку транзакций для восстановления ошибок. Позднее в этой главе будет показано, когда и как ваши службы могут пользоваться преимуществами VRM (Volatile Resource Manager). Управление транзакциями Службы WCF могут работать непосредственно с транзакционными ресурсами и управлять транзакциями с использованием различных программных моде- лей — например, модели ADO.NET. При использовании этой модели разработ- чик несет ответственность за запуск и управление транзакциями, как показано в листинге 7.1. Листинг 7.1. Явное управление транзакциями [ServiceContract] interface IMyContract { [OperationContract] void MyMethodO: class MyService : IMyContract public void MyMethodO { // Избегайте такой модели программирования: string connectionstring = " IDbConnection connection = new SqlConnect!on(connectionstring): connect!on.Open(): IDbCommand command = new SqlCommandO: command.Connection = connection: IDbTransaction transaction = connection.BeginTransactionO: // Включение command.Transaction = transaction:
ранзакции 251 try ( /* Взаимодействие с базой данных, затем закрепление транзакции */ transaction.Commit(): } catch transaction.RollbackO; // Отмена транзакции finally ( connect ion.Close(); command.Dispose(): transaction.DisposeO; ) ' Для получения объекта, представляющего нижележащую транзакцию базы данных, вызывается метод BeginTransaction() объекта подключения. BeginTrans- actionQ возвращает реализацию интерфейса IDbTransaction, используемого для управления транзакцией. При включении базы данных в транзакцию обращен- ные к ней запросы не выполняются немедленно, а регистрируются для данной транзакции. Если все обновления или другие изменения, внесенные в базу дан- ных, согласованы и при их выполнении не произошло никаких ошибок, остает- ся вызвать Commit() для объекта транзакции. Тем самым вы приказываете базе данных закрепить все изменения как одну атомарную операцию. Если в ходе выполнения возникают исключения, вызов Commit() пропускается, а блок catch отменяет транзакцию вызовом RoLLback(). База данных отменяет все изменения, зарегистрированные до настоящего момента. Проблема координирования транзакций Хотя модель явного управления транзакциями прямолинейна и не предъявляет никаких особых требований к службе, выполняющей транзакцию, она лучше всего подходит для клиента, вызывающего одну службу, взаимодействующую с одной базой данных (или одним транзакционным ресурсом), где служба за- пускает транзакцию и управляет ей (рис. 7.2). Рис- 7-2- Транзакция «одна служба/один ресурс» Такое ограничение обусловлено проблемой координирования транзакций. Например, возьмем служебно-ориентированное приложение, в котором клиент взаимодействует с несколькими службами, которые, в свою очередь, взаимо- действуют друг с другом и с несколькими ресурсами, как показано на рис. 7.3.
252 Глава 7, Транзакции Рис. 7.3. Служебно-ориентированное приложение с распределенными транзакциями А теперь вопрос: какая из участвующих служб отвечает за начало транзак- ции и включение каждого ресурса? Если этим будут заниматься все службы, мы получим несколько транзакций. Размещение логики включения в коде службы создает значительные логические привязки между службами и ресурсами. А ка- кая служба отвечает за закрепление и отмену транзакций? Как одна служба уз- нает о том, что «думают» другие службы по поводу транзакции? Как служба, управляющая транзакцией, оповестит других о ее исходе? Попытки передачи объекта транзакции или другого идентификатора в параметре операции нару- шает принципы служебно-ориентированного программирования, потому что клиенты и службы могут использовать разные платформы и технологии реали- зации. При этом службы могут быть развернуты в разных процессах и даже на разных компьютерах или площадках, и такие проблемы, как сетевые сбои или зависания компьютеров, дополнительно усложняют управление транзакциями; в одной службе может произойти сбой, тогда как остальные службы будут про- должать обработку транзакции. Одно из возможных решений — связать клиен- тов со службами, добавив логику координирования транзакций, однако такой подход чрезвычайно ненадежен, и даже небольшие изменения в бизнес-логике и количестве участвующих служб оказываются для него критичными. Кроме того, службы на рис. 7.3 могут быть разработаны разными фирмами, что сдела- ет невозможной подобную координацию. Даже если проблему координирова- ния каким-то образом удастся решить на уровне служб, при участии несколь- ких ресурсов появляется несколько независимых точек сбоев, потому что каж- дый ресурс может отказать независимо от служб. Распределенные транзакции Только что описанная ситуация называется распределенной транзакцией. Рас- пределенная транзакция состоит из двух и более независимых служб (часто на- ходящихся в разных контекстах выполнения) или хотя бы одной службы с двумя и более транзакционными ресурсами. Организовать явное управление ошибка- ми при распределенных транзакциях нереально — приходится использовать двухфазовый протокол закрепления и специализированного менеджера тран- закций. Менеджером транзакций называется третья сторона, обеспечивающая управление транзакциями, потому что логика управления транзакциями ни в коем случае не должна находиться в коде службы.
Транзакции 253 Двухфазовый протокол закрепления Для преодоления сложностей, связанных с распределенными транзакциями, менеджер транзакций использует двухфазовый протокол закрепления для опре- деления результата транзакции, а также принятия решения о закреплении или отмене изменений в состоянии системы. Именно двухфазовый протокол закре- пления обеспечивает атомарность и согласованность в распределенных систе- мах и позволяет WCF поддерживать транзакции с несколькими клиентами, службами и ресурсами. Позднее в этой главе будет показано, как запускаются транзакции и как они распространяются за границы служб. А пока достаточно сказать, что во время выполнения транзакций менеджер по возможности оста- ется в стороне. К транзакции могут присоединиться новые службы, и каждый ресурс, к которому происходит обращение, включается в транзакцию. Службы выполняют бизнес-логику, а ресурсы регистрируют изменения, вносимые в контексте транзакции. Во время транзакции все службы (и клиенты, участвую- щие в транзакции) обязаны проголосовать, хотят ли они закрепить выполнен- ные ими изменения или же отменить транзакцию по какой-либо причине. При завершении транзакции (о том, когда это происходит, будет рассказано далее) менеджер транзакции проверяет результаты голосования задействован- ных служб. Если какая-либо служба или клиент проголосует за отмену, тран- закция обречена на неудачу. Все задействованные ресурсы получают приказ от- менить изменения внесенные в ходе транзакции. Если все службы транзакции голосуют за закрепление, начинается двухфазовый протокол закрепления. В первой фазе менеджер транзакций опрашивает все ресурсы, участвующие в транзакции, нет ли у них возражений против закрепления изменений, внесен- ных в ходе транзакции. Иначе говоря, если бы им было предложено закрепить изменения, согласились бы они на это? Обратите внимание: менеджер транзак- ций не приказывает ресурсам закрепить изменения, а всего лишь просит их проголосовать по этому вопросу. В конце первой фазы менеджер транзакций располагает объединенными голосами всех ресурсов. Вторая фаза протокола выполняется в зависимости от полученных голосов. Если все ресурсы в первой фазе проголосовали за закрепление транзакции, то менеджер приказывает им закрепить транзакцию. Если хотя бы один ресурс в первой фазе заявил о невоз- можности закрепления изменений, во второй фазе менеджер приказывает всем ресурсам отменить изменения; транзакция отменяется, а система восстанавли- вается в состоянии до начала транзакции. Важно подчеркнуть, что обещание ресурса закрепить транзакцию по требо- ванию не может быть нарушено. Если ресурс голосует за закрепление транзак- ции, это означает, что во второй фазе, когда поступит приказ о закреплении, сбои невозможны. Прежде чем голосовать за закрепление, ресурс должен убе- диться в том, что все изменения согласованы и допустимы. Отказаться от сво- его голосования он уже не сможет — этот принцип лежит в основе распределен- ных транзакций. Разработчики ресурсов прилагают значительные усилия для правильной реализации такого поведения.
254 Глава 7. Транзакции Менеджеры ресурсов WCF Менеджером ресурса (RM, Resource Manager) в WCF называется любой ресурс, поддерживающий автоматическое включение ресурса в транзакцию и двухфа- зовый протокол закрепления, находящийся под управлением одного из менед- жеров транзакций WCF. Ресурс должен выявить обращение к нему из транзак- ции и автоматически включиться в нее ровно один раз. RM может быть как устойчивым, так и неустойчивым (временным) ресурсом — таким, как транзак- ционное целое, строка или коллекция. Хотя менеджер ресурса обязан поддер- живать двухфазовый протокол закрепления, он также может дополнительно реализовать оптимизированный протокол для тех случаев, когда он является единственным менеджером ресурса в транзакции. Оптимизированный прото- кол, называемый однофазовым протоколом, применяется в тех случаях, когда RM за один этап сообщает менеджеру транзакции об успехе или неудаче по- пытки закрепления. Распространение транзакций WCF может распространять транзакции за границы служб. Это позволяет службе участвовать в клиентской транзакции, а клиенту — включать в транзак- цию операции с разными службами. Сам клиент при этом может быть как службой WCF, так и чем-то иным. Решение о распространении клиентских транзакций принимается в зависимости как от привязки, так и конфигурации клиентского контракта. Я называю транзакционно-способной любую привязку, которая при соответствующей настройке может распространить клиентскую транзакцию до службы. Не все привязки являются транзакционно-способными; к этой категории относятся только TCP-, IPC- и WS-привязки (то есть Net- TcpBinding, NetNamedPipeBinding, WSHttpBinding, WSDualHttpBinding и WSFederation- HttpBinding соответственно). Прохождение транзакций и привязки По умолчанию транзакционно-способные привязки не распространяют тран- закции. Это объясняется тем, что распространение транзакций, как почти все в WCF, должно быть включено специально. Хост службы или администратор должен явно дать свое разрешение на прием входящих транзакций, возможно поступивших из-за границы организации или бизнес-области. Распространение транзакций должно быть явно включено в привязках как на стороне хоста службы, так и на стороне клиента. Все транзакционно-способные привязки об- ладают логическим свойством Transaction Flow: public class NetTcpBinding : Binding.... { public bool TransactionFlow {get:set:} //...
Распространение транзакций 255 По умолчанию свойство TransactionFlow равно false. Чтобы сделать возмож- ным распространение транзакций, задайте ему значение true — либо на про- фаммном уровне, либо в конфигурационном файле хоста. Пример для привяз- шТСР: netTcpBinding tcpBinding = new NetTcpBinding(): tcpBinding. Transact i onFl ow = true; С использованием конфигурационного файла: <bindings> <netTcpB1nd1 ng> binding name = "TransactionalTCP" transact lonFlow = "true" /> </netTcpBi ndi ng> </bindings> Обратите внимание: значение свойства TransactionFlow не публикуется в ме- таданных службы. Если клиентский конфигурационный файл генерируется в Visual Studio 2005 или SvcUtil, вам все равно придется вручную разрешить или запретить распространение транзакций по мере надобности. ТРАНЗАКЦИИ И НАДЕЖНОСТЬ--------------------------------------------------------------------- Строго говоря, надежная доставка сообщений не является строго обязательной для транзакций. Дело в том, что если при отключении надежной передачи сообщения WCF будут потеряны или связь клиента со службой будет разорвана, транзакция отменяется. Поскольку клиенту гарантируется полный успех или полная неудача транзакционной операции, транзакции по-своему надежны. Тем не менее включение надежности снижает вероятность отмены транзакции, потому что оно сни- кает вероятность отмены транзакции из-за коммуникационных проблем. По этой причине я реко- мендую взять за правило включать надежную передачу при включении транзакций с привязками NetTcpBinding и WSHttpBinding: <netTcpB1nding> binding name -- "TransactionalTCP" transactionFlow = "true"> <reliableSession enabled = "true"/> </binding> </netTcpBi ndi ng> Включать надежную передачу для привязок NetNamedPipedBinding и WSDualHttpBinding не обяза- тельно, потому что, как было показано в главе 1, эти две привязки заведомо надежны. Распространение транзакций и контракт операций Использование транзакционно-способных привязок и даже включение распро- странения транзакций не означает, что служба должна использовать клиент- ские транзакции в каждой операции или что у клиента вообще есть транзакция для распространения. Подобные решения уровня службы должны быть частью контрактного соглашения между клиентом и службой. Для этого WCF предла- гает атрибут метода TransactionFlowAttribute, управляющий тем, когда и как кли- ентские транзакции распространяются к службам: public enum Transact 1 onFlowOpt 1 on
256 Глава 7. Транзакции Allowed. NotAllowed. Mandatory } [Attri butellsage( Attri buteTargets. Method) ] public sealed class TransactionFlowAttribute : Attribute.lOperationBehavior ( public TransactionFlowAttribute(Transact!onFlowOption flowOption); } Обратите внимание: атрибут Transaction Flow является атрибутом уровня ме- тода, потому что WCF настаивает, чтобы решения о распространении транзак- ций производились на уровне операций, а не на уровне служб. [ServiceContract] interface IMyContract { [Operationcontract] [TransactionFlow(TransactionFlowOption.Al lowed)] void MyMethod(...); } Это сделано намеренно для улучшения детализации — чтобы одни методы службы использовали клиентские транзакции, а другие нет. Значение атрибута Transact!on Flow включается в опубликованные метадан- ные службы, поэтому импортированное определение контракта будет содер- жать заданное значение. WCF также позволяет применить атрибут Transaction- Flow непосредственно к классу службы, реализующему операцию: [ServiceContract] interface IMyContract { [Operationcontract] void MyMethod(...); } class MyService : IMyContract { [T ransact1onFlow(Transact1onFlowOpt1on.Al 1 owed)] public void MyMethod(...) {...} } Однако использовать атрибут подобным образом не рекомендуется, потому что это приводит к разбиению определения публикуемого контракта службы. TransactionFlowOption.NotAllowed Если операция настроена на запрет прохождения транзакций, клиент не может передавать свои транзакции службе. Даже если прохождение транзакций разрешено в привязке и у клиента имеется транзакция, она игнорируется и не достигает службы. В результате служба не может использовать клиентскую транзакцию, а служба и клиент могут выбрать любые привязки с любой кон- фигурацией. TransactionFlowOption.NotAllowed является значением по умолча- нию атрибута TransactionFlowOption, поэтому следующие два определения эк- вивалентны:
Распространение транзакций 257 [ServiceContract 1 interface IMyContract I [OperationContract] void MyMethodt...); 1 [ServiceContract] interface IMyContract I [OperationContract] [Transact i on F1 owt T ransact i onFl owOpt i on. Not Al 1 owed) ] void MyMethodt...): 1 TransactionFlowOption.Allowed Если операция настроена на разрешение прохождения транзакций (значение TransactionFlowOption.Allowed атрибута TransactionFlowOption) и у клиента имеет- ся транзакция, служба разрешит клиентской транзакции выход за границы службы. Тем не менее служба может использовать или не использовать кли- ентскую транзакцию даже в случае ее распространения. В режиме Transaction- FlowOption.Allowed служба может быть настроена на использование любой при- вязки, транзакционно-способной или нет, но конфигурации привязок клиента и службы должны быть совместимыми. Термин «совместимость» в контексте передачи транзакций означает, что если операция службы разрешает прохожде- ние транзакции, а привязка запрещает его, клиент тоже должен запретить его в привязке на своей стороне. Попытка передачи клиентской транзакции вызо- вет ошибку, потому что содержащаяся в сообщении информация о транзакции не будет понятна службе. Но когда конфигурация привязки на стороне службы настроена на разрешение прохождения транзакции, клиент может задать значе- ние TransactionFlow равным false в своей привязке, даже если у службы оно зада- но равным true. TransactionFlowOption.Mandatory Когда операция настроена в режиме TransactionFlowOption.Mandatory, служба и клиент должны использовать транзакционно-способную привязку с включен- ным прохождением транзакций. WCF проверяет это требование при загрузке службы и инициирует исключение InvalidOperationException, если служба содер- жит хотя бы одну несовместимую конечную точку. Значение TransactionFlow- Option.Mandatory означает, что клиент должен иметь транзакцию для передачи службе. Попытка вызова операции службы без транзакции инициирует исклю- чение FaultException на стороне клиента, означающее, что служба требует обяза- тельной транзакции. В режиме TransactionFlowOption.Mandatory транзакция кли- ента всегда передается службе. Как и прежде, служба может использовать или не использовать клиентскую транзакцию по своему усмотрению. Односторонние вызовы Распространение клиентской транзакции по определению требует, чтобы служ- ба могла отменить клиентскую транзакцию, если она этого пожелает. Это озна- чает, что клиентская транзакция не может передаваться службе по односторонней
258 Глава 7, Транзакции операции, потому что односторонние вызовы не имеют ответных сообщений. WCF проверяет соблюдение этого требования при загрузке службы; если одно- сторонняя операция настроена в любом режиме, кроме TransactionFlowOption. NotAllowed, происходит исключение. // Недопустимое определение: [ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] [TransactionFlowtTransactionFlowOption.Al lowed)] void MyMethodt...): } Протоколы и менеджеры транзакций WCF использует разные протоколы управления транзакциями в зависимости от контекста выполнения сторон, участвующих в транзакции. Возможно, термин «протокол» здесь не совсем уместен, потому что формально здесь используется двухфазовый протокол закрепления. Различия между протоколами управления транзакциями обусловлены типами удаленных вызовов, используемым комму- никационным протоколом и видами границ, которые могут пересекать транзак- ции. О Облегченный протокол — протокол управления транзакциями только в ло- кальном контексте, в пределах одного прикладного домена. Транзакции не могут выходить за границы прикладных доменов (не говоря уже о процессах или компьютерах), а также передаваться за границы служб (то есть от кли- ента к службе). Облегченный протокол используется только внутри служб или между службами. Он обеспечивает наилучшее быстродействие по срав- нению со всеми остальными протоколами. О Протокол OleTx — используется для передачи транзакций через границы прикладных доменов, процессов и компьютеров, а также управления прото- колом двухфазового закрепления. Протокол использует вызовы RPC, а кон- кретный двоичный формат вызовов определяется Windows. В результате ис- пользования как RPC, так и специфического формата Windows, протокол не может выходить за границу брандмауэров и взаимодействовать со сторона- ми, работающими не на базе Windows. Обычно это не создает особых про- блем, потому что протокол OleTx предназначен для управления транзак- циями в интрасетях, однородных средах Windows и с одним менеджером транзакций. О Протокол WSAT (WS-атомарный протокол транзакций) — протокол похож на OleTx в том отношении, что он тоже может распространять транзакции через границы прикладных доменов, процессов и компьютеров и управлять двухфазовым протоколом закрепления. Однако в отличие от протокола OleTx, протокол WSAT основан на отраслевом стандарте и при использова- нии на базе HTTP с кодированием текста может выходить за брандмауэры. Хотя протокол WSAT может использоваться в интрасетях, прежде всего, он
Распространение транзакций 259 предназначен для управления транзакциями в Интернете с участием не- скольких менеджеров транзакций. Протоколы и привязки Ни одна привязка не поддерживает облегченный протокол, потому что этот протокол все равно не может распространять транзакции через границы служб. Тем не менее поддержка двух других протоколов управления транзакциями от- личается для разных транзакционно-способных привязок. Привязки TCP и IPC могут быть настроены для работы как обоими протоколами OleTx и WSAT, так и с одним из них. Обе привязки по умолчанию используют прото- кол OleTx и переключаются на WSAT в случае необходимости. Кроме того, эти две интрасетевые привязки позволяют настроить протокол как в конфигураци- онном файле, так и на программном уровне, как любое другое свойство при- вязки. BWCF абстрактный класс Transactionprotocol определяется следующим об- разом: public abstract class Transactionprotocol public static Transactionprotocol Default {get:} public static Transactionprotocol OleTransactions (get:} public static Transactionprotocol WSAtomicTransactionOctober2004 {get:} ) Привязки NetTcpBinding и NetNamedPipeBinding обладают свойством Transac- tionProtocol соответствующего типа, например: public class NetTcpBinding : Binding.... I Transactionprotocol Transactionprotocol {get; set:} II... I Чтобы задать протокол на программном уровне, сначала сконструируйте оп- ределенный тип привязки, а затем задайте свойство одним из статических мето- дов: NetTcpBinding tcpBinding = new NetTcpBinding(): //Протокол важен только для распространения tcpBindi ng. Transact! onFl ow = true: tcpBinding.Transactionprotocol = <$[bold> TransactionProtocol. WSAtomicTransaction0ctober2004; Обратите внимание: настройка транзакционного протокола имеет смысл только при включенном распространении транзакций. Чтобы настроить протокол в конфигурационном файле, определите секцию привязок, как это делается обычно: <bindings> <netTcpBinding>
260 Глава 7, Транзакции <binding name = "TransactionalTCP" transactionFlow = "true" transactionprotocol = "WSAtomicTransaction0ctober2004" /> </netTcpBinding> </bindings> При настройке протокола для привязок TCP и IPC клиент и служба должны использовать один и тот же протокол. Поскольку привязки TCP и IPC могут использоваться только в интрасети, их настройка для протокола WSAT не имеет практической пользы и преду- смотрена в основном для полноты картины. Привязки WS (WSHttpBinding, WSDualHttpBinding и WSFederationHttpBinding) были созданы для использования в Интернете, когда в протоколе WSAT задей- ствовано несколько менеджеров транзакций. Однако в интернет-сценарии с участием только одного менеджера транзакций эти привязки по умолчанию используют протокол OleTx. В настройке конкретного протокола нет необходи- мости, и такая возможность не предусмотрена. Менеджеры транзакций Вспомните, о чем говорилось в начале главы: от самостоятельного управления транзакциями стоит держаться подальше. Лучше всего воспользоваться услуга- ми третьей стороны, называемой менеджером транзакций, которая управляет двухфазовым протоколом закрепления для ваших клиентов и служб. WCF мо- жет работать не с одним, а с тремя разными менеджерами транзакций, как пока- зано на рис. 7.4. Эти три менеджера транзакций — LTM (Lightweight Transaction Manager), КТМ (Kernel Transaction Manager) и DTC (Distributed Transaction Coordinator). WCF назначает подходящего менеджера транзакций в зависимости от исполь- зуемой платформы, функциональности приложения, вызываемых служб и по- требляемых ресурсов. Автоматическое назначение менеджера транзакций спо- собствует отделению управления транзакциями от кода служб и транзакционно- го протокола. Разработчикам никогда не приходится иметь дело с менеджерами транзакций, и следующее описание лишь проясняет некоторые часто встречаю- щиеся вопросы, относящиеся к производительности и эффективности.
Распространение транзакций 261 LTM Менеджер LTM управляет только локальными транзакциями, то есть транзак- циями в пределах одного прикладного домена. LTM использует облегченный протокол транзакций для управления двухфазовым протоколом закрепления. Он способен управлять только теми транзакциями, в которых задействовано не более одного устойчивого менеджера ресурса. LTM также может управлять лю- бым количеством VRM. Если присутствует только один менеджер ресурса, под- держивающий однофазовое закрепление, тогда LTM использует этот оптими- зированный протокол. Что еще важнее, LTM может управлять транзакциями только в границах одной службы и только в том случае, если служба не распро- страняет транзакции к другим службам. Из всех менеджеров транзакций LTM обладает наибольшей производительностью. ктм КТМ может использоваться для управления менеджерами транзакционных ре- сурсов ядра (KRM) в Windows Vista, а конкретно — транзакционной файловой системой (TXF) и транзакционным реестром (TXR). КТМ использует облег- ченный транзакционный протокол для прямых обращений к памяти и ядру. КТМ может управлять транзакцией при условии, что в ней задействовано не более одного устойчивого KRM, но количество VRM может быть произволь- ным. Как и в случае с LTM, в транзакции может быть задействовано не более одной службы, при условии, что эта служба не распространяет транзакции к другим службам. DTC Менеджер DTC способен управлять транзакциями через любые границы про- цессов, компьютеров или размещений. DTC может использовать протоколы OleTx и WSAT. Этот менеджер используется при выходе транзакций за грани- цы служб. DTC легко справляется с транзакциями, в которых задействовано любое количество служб и менеджеров ресурсов. DTC - системная служба, доступная по умолчанию на всех компьютерах, на которых работает WCF. Работа DTC тесно связана с WCF. Именно DTC создает новые транзакции, распространяет их между компьютерами, собирает результаты голосования менеджеров ресурсов и приказывает им отменить или закрепить транзакцию. Для примера возьмем служебно-ориентированное приложение на рис. 7.5, где нетранзакционный клиент вызывает службу на компьютере А. Служба на компьютере А настроена на использование транзакций. Она стано- вится корнем транзакции и получает возможность не только запустить транзак- цию, но и указать, когда транзакция будет завершена. ПРИМЕЧАНИЕ------------------------------------------------------------ Каждая транзакция в WCF имеет максимум одну корневую службу, причем корнем транзакции также может быть клиент. Корень не только запускает, но и завершает транзакцию. Когда служба, являющаяся частью транзакции на компьютере А, пытается об- ратиться к другой службе или ресурсу на компьютере В, у нее имеется посредник для обращения к удаленной службе или ресурсу. Посредник распространяет
262 Глава 7. Транзакции идентификатор транзакции на компьютер В. «Приемник» на компьютере В свя- зывается с локальным DTC на компьютере В, передает ему идентификатор тран- закции и приказывает начать управление транзакцией на компьютере В. Так как идентификатор транзакции был доставлен на компьютер В, менеджеры ре- сурсов на компьютере В могут автоматически включиться в транзакцию. Ана- логичным образом идентификатор транзакции доставляется на компьютер С. Если все службы проголосовали за закрепление транзакции, в действие вступает двухфазовый протокол закрепления. DTC на корневом компьютере собирает результаты голосования менеджеров ресурсов на этом компьютере, связывается с DTC на каждом компьютере, принимающем участие в транзак- ции, и приказывает им приступить к первой фазе на своих компьютерах. DTC на удаленных компьютерах собирают результаты голосования менеджеров ре- сурсов и пересылают результаты на DTC корневого компьютера. После того как DTC на корневом компьютере получит результаты от всех удаленных DTC, в его распоряжении оказываются объединенные результаты голосова- ния менеджеров ресурсов. Если все проголосовали за закрепление, то DTC на корневом компьютере снова связывается с DTC на удаленных компьютерах, приказывает приступить ко второй фазе и закреплению транзакций. Но если хотя бы один менеджер ресурса проголосует за отмену, DTC на корневом ком- пьютере информирует все DTC на удаленных компьютерах о переходе ко вто- рой фазе и отмене транзакции. Обратите внимание: только DTC на корневом компьютере располагает объединенными результатами голосования по первой фазе, и только он может отдать распоряжение об итоговой отмене или закреп- лении.
Распространение транзакций 263 Повышение менеджера транзакций VCF динамически назначает для транзакции подходящего менеджера. Если один менеджер транзакции оказывается неподходящим, WCF повышает тран- закцию, то есть предлагает ее обработать менеджеру транзакций следующего уровня. Одна транзакция может повышаться несколько раз. После повышения транзакция остается на новом уровне и не может понижаться. Предыдущий ме- неджер транзакций низводится до сквозного режима обработки. Вследствие ди- намической природы повышения разработчики не могут взаимодействовать с менеджерами транзакций напрямую, потому что это будет происходить в об- ход повышения. Кстати говоря, повышение — еще одна причина, по которой не следует писать код по схеме из листинга 7.1. Повышение LTM Любая транзакция в WCF всегда начинается как транзакция, находящаяся под управлением LTM. Пока транзакция взаимодействует с одним устойчивым ре- сурсом и пока в системе не делаются попытки передачи транзакции службе WCF, LTM обеспечивает управление транзакций с оптимальной эффективно- стью. Но если транзакция попытается включить второй устойчивый ресурс или произойдет ее распространение к службе, WCF повысит транзакцию от LTM kDTC. Другой тип повышения применяется в том случае, если первый устой- чивый ресурс, к которому происходит обращение, является ресурсом КТМ; в этом случае WCF повышает транзакцию от LTM до КТМ. Повышение КТМ Менеджер КТМ управляет транзакцией при условии, что она взаимодействует с одним KRM и остается локальной. КТМ может обслуживать произвольное количество VRM. Транзакция КТМ повышается до DTC в том случае, если она переходит к другой службе или в нее включается второй устойчивый ресурс (обычный или ресурс ядра). Ресурсы и повышение На момент написания книги в транзакциях LTM могли участвовать только VRM и различные разновидности SQL Server 2005. Старые менеджеры ресур- сов-такие, как SQL Server 2000, Oracle, DB2 и MSMQ — могут участвовать только в транзакциях DTC. Соответственно, когда к наследному ресурсу обра- щается транзакция LTM, даже если это единственный ресурс в транзакции, транзакция автоматически повышается до DTC. Отношения между ресурсами и менеджерами транзакций представлены в табл. 7.1. Таблица 7.1. Ресурсы и менеджеры транзакций Ресурс LTM КТМ DTC Временный Да Да Да SQLServer 2005 Да Нет Да Ресурс ядра Нет Да Да Любой другой RM Нет Нет Да
264 Глава 7. Транзакции Класс Transaction Класс Transaction из пространства имен System.Transactions, появившийся в .NET 2.0, представляет транзакцию, с которой могут работать все менеджеры транзакций WCF: [Serializable] public class Transaction : IDisposable.ISerialIzable { public static Transaction Current {get:set;} public void RollbackO; // Отмена транзакции public void DlsposeO; //... } Разработчику редко приходится работать с классом Transaction напрямую. Чаще всего класс Transaction используется для ручной отмены транзакций вы- зовом метода Rollback(). К числу дополнительных возможностей класса Trans- action относится включение менеджеров ресурсов, задание уровня изоляции, подписка на транзакционные события, клонирование транзакций для парал- лельных потоков, а также получение информации о состоянии транзакций. Окружающая транзакция В .NET 2.0 определяется концепция окружающей транзакции — транзакции, в которой выполняется ваш код. Чтобы получить ссылку на окружающую тран- закцию, обратитесь к статическому свойству Current класса Transaction: Transaction amblentTransactlon = Transaction.Current: При отсутствии окружающей транзакции Current возвращает null. Любой фрагмент кода, как на стороне клиента, так и на стороне службы, всегда может обратиться к своей окружающей транзакции. Объект окружающей транзакции хранится в локальном хранилище потока (TLS, Thread Local Storage). В контексте WCF окружающая транзакция играет важнейшую роль. Все ме- неджеры ресурсов WCF автоматически включаются в окружающие транзак- ции. Если клиент с окружающей транзакцией вызывает службу WCF, а на- стройка привязки и контракта разрешает распространение транзакции, окру- жающая транзакция переходит к службе. Локальные и распределенные транзакции Класс Transaction используется для представления как локальных, так и распре- деленных транзакций. Каждая транзакция имеет два идентификатора, исполь- зуемых для идентификации локальной и распределенной транзакции. Иденти- фикаторы транзакций могут быть получены из свойства Transactioninformation класса Transaction: [Serializable] public class Transaction : IDisposable.ISerlallzable
Класс Transaction 265 public Transactionlnformation Transactioninformation {get:} II... I Свойство Transactioninformation типа Transactioninformation определяется сле- дующим образом: public class Transactionlnformation { public Guid Distributedldentifier {get:} public string Local Identifier {get;} II... I Transactioninformation открывает доступ к обоим идентификаторам. В основ- ном эти идентификаторы предназначены для ведения протоколов, трассировки и анализа. В этой главе они будут использоваться для демонстрации распро- странения транзакций. Локальный идентификатор транзакции Локальный идентификатор транзакции содержит как идентификатор LTM в текущем прикладном домене, так и порядковый номер транзакции. Значение локального идентификатора берется из свойства Localidentifier класса Transaction- Information. При наличии окружающей транзакции локальный идентификатор всегда имеет действительное значение, отличное от null. Значение локального идентификатора состоит из двух частей: постоянного кода GUID, уникального для каждого прикладного домена (представляет назначенный LTM для доме- на), и последовательно увеличиваемого целочисленного счетчика транзакций, находившихся под управлением данного LTM. Например, если служба трассирует три последовательные транзакции, начи- ная с первого вызова, локальные идентификаторы будут выглядеть примерно так: 8947aec9-lfac-42bb-8de7-60df836e00d6:1 8947aec9-lfac-42bb-8de7-60df836e00d6.2 8947aec9-lfac-42bb-8de7-60df836e00d6:3 Значение GUID остается постоянным в пределах прикладного домена. Если служба находится в одном прикладном домене с клиентом, их GUID будут оди- наковыми. Если клиент осуществляет междоменный вызов, он имеет свой уни- кальный код GUID, идентифицирующий его локальный LTM. Распределенный идентификатор транзакции Распределенный идентификатор транзакции генерируется автоматически каж- дый раз, когда транзакция, управляемая LTM или КТМ, повышается до тран- закции, управляемой DTC (например, при распространении окружающей тран- закции к другой службе). Значение распределенного идентификатора берется из свойства Distributedldentifier класса Transactioninformation. Распределенный идентификатор уникален для каждой транзакции, и две транзакции ни при ка- ких условиях не могут обладать одинаковыми идентификаторами. Что еще
266 Глава 7. Транзакции важнее, распределенный идентификатор остается постоянным при переходе за границы служб и по всей цепочке вызовов от клиента к другим службам и объ- ектам. По этой причине он удобен для регистрации и трассировки. Учтите, что распределенный идентификатор может быть равен Guid.Empty, если транзакция еще не повышалась. На стороне клиента распределенный идентификатор обыч- но равен Guid.Empty, если клиент, который является корнем транзакции, еще не обращался с вызовом к службе. На стороне службы распределенный идентифи- катор пуст, если служба не использует транзакцию клиента и начинает собст- венную локальную транзакцию. Транзакционное программирование служб Для служб WCF предоставляет простую и элегантную декларативную модель. Впрочем, эта модель недоступна для не-служебного кода, вызываемого служба- ми, и для клиентов WCF. Назначение окружающей транзакции По умолчанию класс службы и все ее операции не имеют окружающей транзак- ции, даже при распространении клиентской транзакции к службе. Рассмотрим следующую службу: [ServiceContract] Interface IMyContract { [Operationcontract] [T ransactionFlow(TransactionFlowOption.Mandatory)] void MyMethod(...): } class MyService : IMyContract { public void MyMethod(...) { Transaction transaction = Transaction.Current; Debug.Assert(transaction null); } } Окружающая транзакция службы будет равна null даже несмотря на то, что обязательная передача транзакции гарантирует распространение клиентской транзакции. Чтобы окружающая транзакция присутствовала, для каждого ме- тода контракта служба должна указать, что тело метода должно рассматривать- ся WCF в контексте транзакции. Для этой цели WCF предоставляет свойство TransactionScopeRequired атрибута OperationBehaviorAttribute: [AttributeUs age(Att г1buteTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute.... { public bool TransactionScopeRequired {get;set;}
Транзакционное программирование служб 267 II... ) По умолчанию свойство TransactionScopeRequired равно false, поэтому по умол- чанию служба не имеет окружающей транзакции. Задание TransactionScopeRequired истинного значения предоставляет операции окружающую транзакцию: class MyService : IMyContract ( [OperationBehav lor (TransactionScopeRequired * true)] public void MyMethodO Transaction transaction = Transaction.Current: Debug.Assert(transaction !* null): ) 1 Если клиентская транзакция распространяется к службе, WCF назначает клиентскую транзакцию окружающей транзакцией данной операции. В против- ном случае WCF создает для операции новую транзакцию и назначает ее окру- жающей транзакцией. ВНИМАНИЕ------------------------------------------------------------------------------ Конструктор класса службы не имеет транзакции: он никогда не участвует в клиентских транзакци- ях, и вы не можете потребовать у WCF, чтобы он рассматривался в контексте транзакции. Не выпол- няйте транзакционные действия в конструкторе службы, если только вы не создадите вручную но- вую окружающую транзакцию (см. далее). Рис. 7.6. Распространение транзакций в зависимости от контракта, привязки и поведения операции
268 Глава 7. Транзакции На рис. 7.6 показано, какая транзакция используется службой WCF в зави- симости от конфигурации привязки, операции контракта и локального атрибу- та поведения операции. На рисунке нетранзакционный клиент вызывает службу 1. Контракт опера- ции настроен с атрибутом TransactionFlowOption.Allowed. Даже несмотря на то, что распространение транзакции разрешено в привязке, у клиента транзакции нет, поэтому распространение не происходит. Поведение операции службы 1 на- строено на обязательное наличие транзакционного контекста. В результате WCF создает для службы 1 новую транзакцию (транзакция А на рис. 7.6). Да- лее служба 1 вызывает три другие службы с разными конфигурациями. В при- вязке службы 2 включено распространение транзакций, а контракт операции требует обязательной передачи клиентской транзакции. Поскольку поведение операции требует транзакционного контекста, WCF задает транзакцию А окру- жающей транзакцией для службы 2. При вызове службы 3 привязка и контракт запрещают распространение транзакции, но поскольку поведение операции требует транзакционного контекста, WCF создает для службы 3 новую транзак- цию (В) и назначает ее окружающей транзакцией для службы 3. По аналогии со службой 3, при вызове службы 4 привязка и контракт операции запрещают распространение. Но так как служба 4 не требует транзакционного контекста, окружающая транзакция для нее не создается. Режимы распространения транзакций Транзакция, используемая службой, определяется в зависимости от свойства привязки (два значения), режима контракта операции (три значения) и свойст- ва транзакционного контекста в поведении операции (два значения). Следова- тельно, всего существует 12 возможных комбинаций. Из них 4 либо логически противоречивы и отвергаются WCF (например, распространение запрещено в привязке, но является обязательным в контракте операции), либо просто не- практичны. В табл. 7.2 перечислены 8 оставшихся комбинаций1. Таблица 7.2. Зависимость режима распространения транзакций от привязки, контракта и поведения Привязка (TxFIow) TransactionFlow- Option TransactionScope- Required Режим False Allowed False Без транзакций False Allowed True Служба False NotAllowed False Без транзакций False NotAllowed True Служба True Allowed False Без транзакций True Allowed True Клиент/служба True Mandatory False Без транзакций True Mandatory True Клиент 1 Я впервые изложил свою концепцию режимов распространения транзакций в «MSDN Magazine* за май 2007 г.
Транзакционное программирование служб 269 Фактически эти восемь комбинаций порождают только четыре режима рас- пространения транзакций. В табл. 7.2 рекомендуемые настройки для каждого режима выделены жирным шрифтом. Каждый режим занимает свое место при проектировании приложений, и умение выбрать правильный режим значитель- но упрощает настройку транзакционной поддержки. Т|йнзакции типа «клиент/служба» Как следует из названия, режим «клиент/служба» гарантирует, что служба бу- дет использовать клиентскую транзакцию, если это возможно, а если ее нет — транзакцию стороны службы. Настройка режима: 1. Выберите транзакционную привязку и включите распространение транзак- ций, задав Transaction Flow значение true. 2. Включите распространение транзакций в контракте операции, установив ат- рибут TransactionFlowOption. Allowed. 3. Задайте свойство TransactionScopeRequired аспекта поведения операции рав- ным true. Режим «клиент/служба» отличается наибольшей логической изоляцией, по- тому что служба ограничивается минимумом предположений относительно того, что делает клиент. Если у клиента имеется транзакция для распростране- ния, служба присоединяется к клиентской транзакции. Присоединение к кли- ентской транзакции всегда полезно для стабильности системы в целом. Пред- ставьте, что служба имеет собственную транзакцию, независимую от транзак- ции клиента. Появляется вероятность закрепления одной из этих двух тран- закций при отмене другой, с переходом системы в нестабильное состояние. Если служба присоединяется к клиентской транзакции, вся работа, выполняемая кли- ентом и службой (и возможно, другими службами, вызываемыми клиентом), бу- дет закрепляться или отменяться в виде одной атомарной операции. Если у кли- ента нет транзакции, служба все равно требует транзакционной защиты, по- этому режим предоставляет службе специально созданную транзакцию. В листинге 7.2 показана служба, настроенная для режима «клиент/служба». Листинг 7-2- Настройка режима «клиент/служба» [ServiceContract] interface IMyContract ( [Operationcontract] [TransactionFlow(TransactionFlowOption.Al lowed)] void MyMethod(...): ) class MyService : IMyContract [0perationBehav1or(TransactionScopeRequired = true)] public void MyMethod(...) I Transaction transaction = Transaction.Current: Debug.Assert(transaction !« null): I }
270 Глава 7. Транзакции Режим «клиент/служба» применяется в ситуациях, когда служба может ис- пользоваться автономно или в составе более крупной транзакции. При выборе этого режима следует иметь в виду возможные взаимные блокировки — если итоговая транзакция является транзакцией стороны службы, она может блоки- роваться с другими транзакциями, пытающимися обратиться к тем же ресур- сам. В режиме «клиент/служба» служба может быть или не быть корнем тран- закции, причем ее поведение должно быть одинаковым в обеих ситуациях - как в роли корня транзакции, так и при присоединении к клиентской транзакции. Включение распространения транзакций Режим «клиент/служба» требует использования транзакционно-способной привязки с включенным режимом распространения транзакций, однако соблю- дение этого требования не проверяется WCF во время загрузки службы. Чтобы избавиться от этого дефекта, можно воспользоваться моим атрибутом Binding- ReqpuirementAttribute: [Attri butel)sage( Attri buteTargets .Class)] public class BindingRequirementAttribute : Attribute.IServiceBehavior { public bool TransactionFlowEnabled // По умолчанию false {get;set:} //... } Атрибут применяется непосредственно к классу службы. По умолчанию зна- чение TransactionFlowEnabled равно false. Когда оно задается равным true, если контракт конечной точки содержит хотя бы одну операцию с атрибутом Transac- tionFlow, равным TransactionFlow.Allowed, атрибут BindingRequirement гарантирует, что конечная точка использует транзакционно-способную привязку со свойст- вом TransactionFlow, равным true: [ServiceContract] interface IMyContract { [Operationcontract] [TransactionFlow(TransactionFlowOption.Allowed)] void MyMethod(...): } [BindingRequirement(TransactionFlowEnabled = true)] class MyService : IMyContract {••} В случае нарушения требований при запуске хоста инициируется исключе- ние InvalidOperationException. В листинге 7.3 приведена упрощенная реализация BindingRequirementAttribute. Листинг 7-3- Атрибут BindingRequirement [Attri butel)sage(Attri buteTargets. Cl ass) ] public class BindingRequirementAttribute : Attribute.IServiceBehavior { public bool TransactionFlowEnabled {get:set:}
Транзакционное программирование служб 271 void IServiceBehavior.Vaiidate(ServiceDescription description. ServiceHostBase host) ( if(TransactionFlowEnabled == false) ( return; 1 foreach(ServiceEndpoint endpoint in description.Endpoints) ( Exception exception = new InvalidOperationException(...); foreach(Operati onDescri pti on operation in endpoint.Contract.Operations) ( foreach(IOperationBehavior behavior in operation.Behaviors) ( if(behavior is TransactionFlowAttribute) { TransactionFlowAttribute attribute = behavior as TransactionFlowAttribute; if(attribute.Transactions == TransactionFlowOption.Allowed) ( if(endpoint.Binding is NetTcpBinding) { NetTcpBinding tcpBlndlng = endpoint.Binding as NetTcpBinding; if(tcpBinding.TransactionFlow == false) { throw exception: } break; } ...II Аналогичные проверки для других // транзакционно-способных привязок throw new InvalidOperationException(...); ) } } } void IServiceBehavior.AddBindingParameters(...) 0 void IServiceBehavior.ApplyDispatchBehavior(...) () ) Класс BindingRequirementAttribute является аспектом поведения службы, по- этому он поддерживает интерфейс IServiceBehavior, упоминавшийся в главе 6. Метод Validate() интерфейса IServiceBehavior вызывается во время запуска хоста и позволяет отменить процедуру загрузки службы. Прежде всего Validate() проверя- ет, задано ли свойству TransactionFlowEnabled значение false. Если это так, Validate() не делает ничего и возвращает управление. Если свойство TransactionFlowEnabled
272 Г лава 7. Транзакции истинно, Validate() перебирает элементы коллекции конечных точек, доступной в описании службы. Для каждой конечной точки он получает коллекцию опера- ций, а для каждой операции обращается к коллекции аспектов поведения операции. Все аспекты поведения операций, включая TransactionFlowAttribute, реализуют ин- терфейс lOperationBehavior. Если аспект является TransactionFlowAttribute, Vali- date() проверяет, настроен ли атрибут в режиме TransactionFlowOption.Allowed. Если это так, Validate() проверяет привязку. Для каждой транзакционно-способ- ной привязки Validate() проверяет, что ее свойство TransactionFlow истинно, а если нет — инициирует исключение InvalidOperationException. Исключение также инициируется в том случае, если для конечной точки используется нетранзак- ционная привязка. ПРИМЕЧАНИЕ------------------------------------------------------------------------------- Реализация BindingRequirementAttribute, представленная в листинге 7.3, основана на приемах об- щего назначения, которые могут использоваться для проверки любых требований к привязкам. На- пример, класс BindingRequirementAttribute содержит еще одно свойство WCFOnly, которое разрешает использовать только привязки «WCF-WCF», и свойство ReliabilityRequired, требующее использования только надежных привязок с включенной надежностью: [Attri buteUs age(AttributeTargets.Class)] public class BindingRequirementAttribute : Attribute.IServiceBehavior { public bool ReliabilityRequired {get;set;} public bool TransactionFlowEnabled {get;set:} public bool WCFOnly {get;set;} } Транзакции типа «клиент» Режим «клиент» гарантирует, что служба будет использовать только клиент- ские транзакции. Чтобы настроить этот режим, выполните следующие дейст- вия: 1. Выберите транзакционную привязку и включите распространение транзак- ций, задав TransactionFlow значение true. 2. Включите распространение транзакций в контракте операции, установив ат- рибут TransactionFlowOption. Mandatory. 3. Задайте свойство TransactionScopeRequired аспекта поведения операции рав- ным true. Режим «клиент» выбирается в тех ситуациях, когда служба должна исполь- зовать транзакции своих клиентов и вследствие своей архитектуры никогда не может использоваться автономно. Основная причина — предотвращение взаим- ных блокировок и обеспечение максимальной стабильности системы. Исполь- зование службой клиентской транзакции снижает риск взаимной блокиров- ки — все обращения к ресурсам будут включаться в одну транзакцию, и это пре- дотвратит появление другой транзакции, претендующей на доступ к тем же
Транзакционное программирование служб 273 ресурсам и блокировкам. Наличие одной транзакции повышает стабильность, потому что транзакция закрепляется или отменяется как одна атомарная опера- ция. В листинге 7.4 показана служба, настроенная для режима «клиент». Листинг 7.4. Настройка режима «клиент» [ServiceContract] interface IMyContract I [Operationcontract] [Transact i onF 1 ow(Transacti onFl owOpti on. Mandatory) ] void MyMethod(...): ) class MyService : IMyContract I [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod(...) I Transaction transaction = Transaction.Current; Debug. Assert(transact!on.Transact ioninformation. Distributedldentifier !« Guid.Empty): Обратите внимание: метод проверяет, что окружающая транзакция является распределенной, то есть что она была получена от клиента. Транзакции типа «служба» Режим «служба» гарантирует, что служба всегда имеет собственную транзак- цию, отдельную от транзакции, имеющейся (или не имеющейся) у клиента. Служба всегда является корнем новой транзакции. Чтобы настроить этот ре- жим, выполните следующие действия: 1. Выберите любую привязку. Если будет выбрана транзакционно-способная привязка, оставьте ее свойству TransactionFlow значение по умолчанию или явно задайте его равным false. 2. Не применяйте атрибут TransactionFLowOption или задайте его равным Trans- action FLowOpti о n. N о tA Но wed. 3. Задайте свойство TransactionScopeRequired аспекта поведения операции рав- ным true. Режим «служба» выбирается в тех ситуациях, когда служба должна выпол- нять транзакционную работу вне контекста клиентской транзакции — скажем, вести журнал или заниматься аудитом, или публиковать события для подпис- чиков независимо от закрепления или отмены клиентской транзакции. Пред- ставьте журнальную службу, которая регистрирует ошибки в базе данных. Ко- гда на стороне клиента происходит ошибка, клиент использует службу для сохранения информации об ошибке. Если бы служба использовала клиентскую транзакцию, то в случае ее отмены зарегистрированная ошибка была бы удале- на из базы данных; это противоречит самой идее регистрации. Если служба имеет собственную транзакцию, регистрация ошибки все равно будет закрепле- на даже в случае отмены клиентской транзакции. Впрочем, у такого решения
274 Г лава 7. Транзакции есть и очевидный недостаток: оно ставит под угрозу стабильность системы, если транзакция службы будет отменена при закреплении транзакции клиента. Эвристический критерий, которым следует руководствоваться при выборе этого режима, — существенно большая вероятность успешного завершения тран- закции службы по сравнению с транзакцией клиента. В примере со службой ре- гистрации ошибок этот критерий обычно выполняется, потому что детерминиро- ванная процедура регистрации имеет больше шансов на успех, чем бизнес-логика, в которой возможны сбои по различным причинам. В общем случае при выборе режима «служба» необходимо проявить максимум осторожности и позаботить- ся о том, чтобы две транзакции (клиента и службы) не создавали угрозы для целостности системы, если одна из них будет закреплена, а вторая — отменена. Классические кандидаты для этого режима — всевозможные службы регистра- ции и аудита. В листинге 7.5 показана служба, настроенная для режима транзакций «служба». Листинг 7.5. Настройка режима «служба» [ServiceContract] Interface IMyContract { [OperationContract] void MyMethodt...): } class MyService : IMyContract { [Operat1onBehav1or(Transact1onScopeRequ1red = true)] public void MyMethodt...) { Transaction transaction = Transaction.Current: Debug. Assert (transact, ion. Transactionlnformation. Distributedldentifier e Guid.Empty); } } В листинге 7.5 обратите внимание на проверку наличия локальной транзак- ции. Режим «без транзакций» Режим «без транзакций» означает, что служба никогда не использует транзак- ции. Его настройка производится так: 1. Выберите любую привязку. Если будет выбрана транзакционно-способная привязка, оставьте ее свойству TransactionFlow значение по умолчанию или явно задайте его равным false. 2. Не применяйте атрибут TransactionFlowOption или задайте его равным Trans- actionFlowOption.NotAllowed. 3. Задавать свойство TransactionScopeRequired аспекта поведения операции не нужно. Если вы все же это сделаете, задайте его равным false. Режим «без транзакций» применяется в тех случаях, когда операции, выпол- няемые службой, полезны, но не критичны, и их сбои не должны приводить
Транзакционное программирование служб 275 к отмене клиентской транзакции. Например, служба, печатающая квитанцию для денежного перевода, не должна отменять клиентскую транзакцию, если в принтере кончилась бумага. Разумеется, режим «без транзакций» сопряжен с определенным риском, по- тому что он ставит под угрозу целостность системы. Если клиент использует транзакцию и вызывает службу, настроенную в режиме «без транзакций», а по- том транзакция клиента отменяется, изменения, внесенные службой в состоя- ние системы, не отменяются. В листинге 7.6 приведена служба, настроенная в режиме «без транзакций». Листинг 7.6. Настройка режима «без транзакций» [ServiceContract J interface IMyContract [Operat i onContract ] void MyMethod(); i class MyService : IMyContract { public void MyMethodO Transaction transaction = Transaction.Current; Debug.Assert(transaction == null); В листинге 7.6 обратите внимание на то, как служба проверяет отсутствие окружающей транзакции. Режим «без транзакций» позволяет организовать вызов нетранзакционной службы транзакционным клиентом. Как упоминалось ранее, настройка режима «без транзакций» в основном предназначена для операций «полезных, но не критичных». Проблема в том, что любое исключение, инициированное службой «без транзакций», приведет к отмене клиентской транзакции — для операций «полезных, но не критичных» это нежелательно. В таких случаях клиент пере- хватывает все исключения от службы «без транзакций», чтобы предотвратить порчу клиентской транзакции; например, вызов службы из листинга 7.6 может выглядеть так: MyContractClient proxy = new MyContractCl ient() ; try 1 proxy.MyMethod(); proxy.Close(); catch {} ПРИМЕЧАНИЕ------------------------------------------------------------------------------ Вызов службы в режиме «без транзакций» должен включаться в команду catch даже в том случае, если операции службы являются односторонними, потому что даже односторонние операции могут инициировать исключения доставки.
276 Глава 7. Транзакции Выбор режима транзакций для службы Два из четырех представленных режимов — «служба» и «без транзакций» - встречаются относительно редко. Эти режимы полезны в контексте описанных сценариев, но в других случаях они могут поставить под угрозу целостность системы. Используйте режим «клиент/служба» или «клиент» и выбирайте ме- жду ними в зависимости от возможности автономного использования службы, учитывая факторы взаимных блокировок и стабильности системы. Режимы «служба» и «без транзакции» лучше избегать. Голосование и завершение Хотя WCF обеспечивает распространение транзакции и общее управление двухфазовым протоколом закрепления среди менеджеров ресурсов, WCF не знает, что должно произойти с транзакцией — закрепление или отмена. WCF просто не может знать, имеют ли смысл изменения, вносимые в состояние сис- темы. Каждая участвующая служба должна проголосовать за исход транзакции и высказать свое мнение по поводу закрепления или отмены транзакции. Кроме того, WCF не знает, когда нужно запустить двухфазовый протокол закрепле- ния, то есть когда транзакция завершается и все службы выполнили свою рабо- ту. Эта информация тоже должна передаваться WCF службами (а вернее, кор- невой службой). WCF поддерживает две программные модели, используемые службами для голосования по исходу транзакции: декларативную и явную. Как вы вскоре убедитесь, голосование напрямую связано с завершением тран- закции. Декларативное голосование WCF может автоматически голосовать от имени служб за закрепление или от- мену транзакции. Для управления автоматическим голосованием используется логическое свойство TransactionAutoComplete атрибута OperationBehavior: [Attri buteUsage(Att г1buteTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute,... { public bool TransactionAutoComplete {get:set;} //... } Свойство TransactionAutoComplete по умолчанию равно true, поэтому следую- щие два определения эквивалентны: [OperationBehavior(Transact!onScopeRequired = true.TransactionAutoComplete - true)] public void MyMethod(...) {•••} [OperationBehavior (TransactionScopeRequired = true)] public void MyMethod(...) {...} Если свойство равно true, а в операции отсутствуют необработанные исклю- чения, WCF автоматически голосует за закрепление транзакции. При наличии
Транзакционное программирование служб 277 необработанного исключения WCF голосует за отмену транзакции. Хотя WCF приходится перехватывать исключение для отмены транзакции, оно иницииру- ется заново и проходит вверх по цепочке вызовов. Для использования автома- тического голосования свойство TransactionScopeRequired метода службы долж- но быть равно true. Когда свойство TransactionScopeRequired равно true, очень важно избежать пе- рехвата и обработки исключений с явным голосованием за отмену: // Нежелательно [OperationBehavi or (Transact 1 onScopeRequi red = true)] public void MyMethod(...) I try ( ) catch Transact 1 on. Current. Rol 1 back(); . ) 1 Дело в том, что ваша служба может быть частью более крупной транзакции, в которой участвуют другие службы и компьютеры. Все остальные стороны транзакции усердно работают, поглощая системные ресурсы; и все напрасно, потому что ваша служба проголосовала за отмену, но об этом никто не знает. Когда исключение проходит вверх по цепочке вызовов, оно в конечном счете достигает корневой службы или клиента и завершает транзакцию. Отказ от об- работки исключения повышает производительность и эффективность. Если вы хотите перехватить исключение для локальной обработки (например, регистра- ции ошибки в журнале), обязательно инициируйте его заново: [OperationBehavlor(Transact 1 onScopeRequi red = true)] public void MyMethod(...) I try ) catch /* Локальная обработка */ throw; )1 Явное голосование Явное голосование необходимо при ложном свойстве TransactionAutoComplete. Задать TransactionAutoComplete равным false можно только в том случае, если свойство TransactionScopeRequired равно true. Когда декларативное голосование отключено, WCF по умолчанию голосует за отмену всех транзакций, независимо от наличия или отсутствия исключений.
278 Глава 7. Транзакции Голосование должно производиться явно, с использованием метода SetTrans- actionComplete() контекста операции: public sealed class Operationcontext : ... { public void SetTransactionCompieteO: //... } Проследите за тем, чтобы после вызова SetTransactionCompieteO не выполня- лась никакая работа, особенно транзакционная. Вызов SetTransactionCompieteO должен быть последней строкой кода в операции перед возвратом управления: [Operat1onBehav1or(Transact1onScopeRequired = true. Transact!onAutoComplete я false)] public void MyMethodt...) { /* Транзакционные операции, затем: */ Operationcontext.Current.SetTransactionCompletet): } Если вы попытаетесь выполнить какие-либо транзакционные действия (включая обращение к Transaction.Current) после вызова SetTransactionCompie- teO, WCF инициирует исключение InvalidOperationException и отменяет тран- закцию. Раз после вызова SetTransactionCompieteO никакая работа не выполняется, все исключения, происходящие после вызова, переходят далее, и WCD по умолчанию отменяет транзакцию. В результате пропадает необходимость в пе- рехвате исключения, если только вы не собираетесь выполнить какую-либо ло- кальную обработку. Как и при декларативном голосовании, если исключение будет перехвачено, обязательно инициируйте его заново, чтобы обеспечить его продвижение и отмену транзакции: COperat1onBehavlor(TransactlonScopeRequIred = true. TransactlonAutoComplete = false)] public void MyMethodt...) { try { /* Транзакционная работа, затем: */ Operationcontext.Current.SetT ransact1onCompletet): } catch { /* Обработка ошибки, затем */ throw; } } Явное голосование проектировалось для ситуаций, в которых результат го- лосования зависит от другой информации, полученной в ходе транзакции, по- мимо исключений и ошибок. Тем не менее для подавляющего большинства приложений и служб предпочтение следует отдавать более простому механиз- му декларативного голосования.
Транзакционное программирование служб 279 Завершение транзакции Решение о том, когда должна завершаться транзакция, принимается той сторо- ной, которая ее начала. Представьте себе клиента, который либо не имеет тран- закции, либо не распространяет ее к службе; такой клиент вызывает операцию службы с истинным свойством TransactionScopeRequired. Операция службы ста- новится корнем транзакции. Корневая служба может вызывать другие службы и распространять транзакцию на них. Транзакция заканчивается в момент за- вершения транзакции корневой операцией. Корневая операция завершает тран- закцию либо декларативно (TransactionAutoComplete = true), либо явно (Transac- tionAutoComplete = false с вызовом SetTransactionComplete()). Кстати, это отчасти объясняет выбор имен TransactionAutoComplete и SetTransactionComplete() — они не ограничиваются простым голосованием, а завершают транзакцию корневой службы. Любые нисходящие службы, вызываемые корневой операцией, могут только проголосовать за транзакцию, но не завершать ее. Только корневая служба и голосует, и завершает транзакцию. Если транзакцию начинает клиент, она завершается в момент уничтожения объекта транзакции клиентом. Эта тема более подробно рассматривается в од- ном из ближайших разделов. Изолированность транзакций В общем случае максимальная изолированность транзакций обеспечивает мак- симум стабильности их результатов. Высочайшая степень изолированности на- зывается серийностью; этот термин означает, что результаты, полученные от группы параллельных транзакций, идентичны результатам, полученным при последовательном выполнении транзакций в виде серии. Для обеспечений се- рийности все ресурсы, задействованные в транзакции, блокируются от всех ос- тальных транзакций. Если другая транзакция попытается обратиться к такому ресурсу, она блокируется, и ее выполнение может быть продолжено только по- сле закрепления или отмены исходной транзакции. Уровень изоляции опреде- ляется при помощи перечисления IsolationLevel из пространства имен System. Transactions: public enum IsolationLevel I Unspecified. ReadUncommitted. ReadCommitted. Repeatabl eRead. Serializable. Chaos. // Изоляция отсутствует Snapshot // Особая форма ReadCommitted. поддерживаемая SQL 2005 ) Различия между четырьмя уровнями изоляции (ReadUncommitted, ReadCom- mitted, RepeatableRead и Serializable) проявляются в том, как на разных уровнях происходит блокировка для чтения и записи. Блокировка может удерживаться на время обращения транзакции к данным менеджера ресурса или до закрепле- ния/отмены транзакции. Первый вариант обеспечивает более высокую произ-
280 Глава 7. Транзакции водительность, второй — стабильность. Комбинации двух разновидностей бло- кировок и двух разновидностей операций (чтение/запись) дают четыре уровня изоляции. Кроме того, не все менеджеры ресурсов поддерживают все уровни изоляции и могут решить принять участие в транзакции на уровне, превышаю- щем настроенный. Все уровни изоляции, за исключением Serializable, в той или иной степени подвержены нестабильности, обусловленной обращением к той же информации со стороны других транзакций. Уровни изоляции, отличные от Serializable, обычно выбираются в системах с интенсивным чтением. Разработчик должен хорошо разбираться в теории об- работки транзакций, понимать семантику самой транзакции, возникающих про- блем параллелизма и последствий для целостности системы. Возможность на- стройки уровня изоляции предусмотрена из-за того, что высокая степень изоляции обеспечивается за счет общей производительности системы, потому что задействованные менеджеры ресурсов удерживают блокировки как чтения, так и записи на все время выполнения транзакции, а все остальные транзакции при этом блокируются. Тем не менее в некоторых ситуациях можно слегка по- низить уровень стабильности системы ради производительности за счет сниже- ния уровня изоляции. Представьте банковскую систему. Одно из возможных требований — получение текущих сумм на всех счетах клиентов. Хотя эта тран- закция может выполняться на серийном уровне изоляции, для банка с сотнями тысяч счетов на ее выполнение уйдет немало времени. Скорее всего, произой- дет тайм-аут, и транзакция будет отменена, потому что некоторые счета в это время будут заблокированы другими транзакциями. С другой стороны, боль- шое количество счетов может принести неожиданную пользу: если выполнить транзакцию на более низком уровне изоляции, она может получить неверные значения по некоторым счетам, но эти ошибки статистически компенсируют друг друга. Итоговая накопленная ошибка может оказаться приемлемой для потребностей банка. В WCF изоляция относится к аспектам поведения службы, поэтому все ме- тоды службы будут использовать один уровень изоляции. Уровень изоляции задается при помощи свойства Transactionisolation Level атрибута ServiceBehavior: [AttributeUsage(AttributeTargets.Cl ass)] public sealed class ServiceBehaviorAttribute : Attribute.... { public IsolationLevel TransactionlsolationLevel {get;set;} //... } Уровень изоляции не может настраиваться в хостовом конфигурационном файле. Свойство TransactionlsolationLevel задается только в том случае, если служба содержит как минимум одну операцию с истинным значением Trans- actionScopeRequired. Изоляция и распространение транзакций Свойство TransactionlsolationLevel по умолчанию равно IsolationLeveLUnspecified, поэтому следующие две команды эквивалентны:
Транзакционное программирование служб 281 class MyService : IMyContract (...) [ServiceBehavior( Transact! on I sol ationLevel = IsolationLevel .Unspecified)] class MyService : IMyContract (...) Когда служба, настроенная co значением IsolationLeveLUnspecified, присоеди- няется к клиентской транзакции, она использует уровень изоляции клиента. Но если для службы указан любой другой уровень изоляции, кроме Isolation- LeveLUnspecified, он должен соответствовать уровню изоляции клиента; в случае несовпадения на стороне клиента инициируется FaultException. Если служба с уровнем IsolationLeveLUnspecified является корнем транзак- ции, WCF устанавливает уровень изоляции равным IsolationLeveLSerializable. Если корневая служба поддерживает уровень, отличный от IsolationLevel.Unspe- cified, WCF использует указанный уровень. Тайм-аут Применение изоляционных блокировок повышает вероятность взаимной бло- кировки при обращении одной транзакции к менеджеру ресурса, принадлежа- щему другой транзакции. Если завершение транзакции занимает слишком много времени, это может свидетельствовать о возникновении взаимной блокировки (deadlock). Из-за этой проблемы транзакция, выполняемая более заранее уста- новленного интервала тайм-аута (60 секунд по умолчанию), автоматически от- меняется. Интервал тайм-аута настраивается как на программном, так и на ад- министративном уровне. Тайм-аут является свойством поведения службы, поэтому все операции по всем конечным точкам службы должны использовать одинаковый тайм-аут. Для его настройки используется строковое свойство TransactionTimeout атрибу- та ServiceBehaviorAttribute: [AttributeUsage(Attri buteTargets. Cl ass)] public sealed class ServiceBehaviorAttribute : Attribute.... I public string TransactionTimeout (get; set;} II... ) Например, 30-секундный интервал тайм-аута задается так: [ServiceBehavior(TransactionTimeout = "00:00:30")] class MyService : (...) Тайм-аут также можно задать в конфигурационном файле хоста — создайте пользовательскую секцию behavior и включите ссылку на нее в секцию service: <services> service name = "MyService" behaviorConfiguration = "ShortTransactionBehavior"> </service> </services> <behaviors>
282 Глава 7. Транзакции <serviceBehaviors> <behavior name = "ShortTransactionBehavior" transactlonTImeout = "00:00:30" /> </serviceBehaviors> </behaviors> Максимальный допустимый интервал тайм-аута составляет 10 минут. Это значение используется даже при указании более высокого значения. Если вы хотите повысить стандартный максимальный тайм-аут с 10 минут до 30, вклю- чите следующую запись в файл machine.config: <conf1gurat1on> <system.transact!ons> <mach1neSettings maxTImeout = "00:30:00"/> </system.transactions> </configuration> Настройка параметров в machine.config влияет на все приложения данного компьютера. Увеличенный тайм-аут полезен в основном для отладки, когда вы хотите по- пытаться выявить проблему в бизнес-логике посредством пошагового выполне- ния кода, чтобы отлаживаемая транзакция не была отменена по тайм-ауту во время поиска проблемы. Во всех остальных случаях с длинными тайм-аутами необходимо быть крайне осторожным, потом что они фактически снимают за- щиту от взаимных блокировок. Как правило, снижение продолжительности тайм-аута выполняется в двух ситуациях. Первая — во время разработки, когда вы хотите протестировать от- мену транзакций в своем приложении. Устанавливая малое значение тайм-аута (скажем, одну миллисекунду), вы искусственно инициируете сбой своей тран- закции и наблюдаете за кодом обработки ошибок. Вторая ситуация с понижением тайм-аута — когда у вас есть основания по- лагать, что служба забирает лишние ресурсы и создает взаимные блокировки. В этом случае транзакция должна отменяться как можно быстрее, до истечения тайм-аута по умолчанию. Распространение транзакций и тайм-аут Когда транзакция распространяется к службе, настроенной на более короткий тайм-аут, транзакция принимает величину тайм-аута службы (иначе говоря, служба принудительно устанавливает более короткий тайм-аут). Это сделано для решения проблем с взаимными блокировками у проблемных служб, о чем говорилось ранее. Если транзакция распространяется к службе, настроенной на более длинный тайм-аут, то настройка службы не учитывается. Явное транзакционное программирование Модель транзакционного программирования, описанная до настоящего момента, может применяться на декларативном уровне только транзакционными служ- бами. Клиенты, нетранзакционные службы и просто объекты .NET, вызывав-
Явное транзакционное программирование 283 ше службой, не могут использовать ее. Для таких случаев в WCF используется транзакционная инфраструктура, доступная в пространстве имен System.Trans- actions .NET 2.0. Кроме того, средства System.Transactions могут быть задейство- ваны даже в транзакционных службах при использовании некоторых расши- ренных возможностей (транзакционные события, клонирование, асинхронное закрепление и ручные транзакции). Я описал возможности System.Transactions встатье «Introducing System.Transactions in the .NET Framework 2.0» (MSDN, апрель 2005 г., обновление в декабре 2005 г.). Далее приводятся выдержки из этой статьи, в которых описано использование основных аспектов System.Transactions в контексте WCF. За более подробным описанием обращайтесь к статье. Класс Transactionscope Явное использование транзакций чаще всего организуется на базе класса Transactionscope: public class Transactionscope : IDisposable public Transactionscope(); II Дополнительные конструкторы public void Complete(); public void DisposeO: I Как подсказывает название, класс TransactionScope предназначен для опреде- ления транзакционного контекста фрагмента кода (листинг 7.7). Листинг 7.7. Использование TransactionScope using(TransactionScope scope = new TransactionScopeO) I /* Транзакционные действия */ II Ошибок нет - транзакция закрепляется scope.Complete(): I Конструктор может создать новую транзакцию LTM и сделать ее окружаю- щей транзакцией посредством задания Transaction.Current или же присоединить- ся к существующей окружающей транзакции. Если будет создана новая тран- закция, она завершается с вызовом метода Dispose() (в конце команды using из листинга 7.7) Метод Dispose() также восстанавливает исходную окружающую транзакцию (null в листинге 7.7). Наконец, если объект TransactionScope не используется в команде using, он становится мусорным объектом при истечении тайм-аута и отмене транзакции. TransactionScope и голосование Объект TransactionScope не знает, что должно произойти с транзакцией — закреп- ление или отмена. По этой причине в каждый объект TransactionScope включа- ется флаг закрепления, по умолчанию равный false. Установка флага произво- дится вызовом метода Complete(). Учтите, что Complete() может вызываться только один раз. При последующих вызовах Complete() инициируется исключе-
284 Глава 7. Транзакции ние InvalidOperationException. Это сделано намеренно, чтобы разработчики не пытались включать транзакционный код после вызова Complete(). Если транзак- ция завершается (вследствие вызова Dispose() или уборки мусора), а флаг за- крепления остается равным false, транзакция отменяется. Например, транзак- ция следующего объекта scope будет отменена, потому что флаг закрепления сохраняет значение по умолчанию: using(Transact1onScope scope = new Transact! onScopeO) {} Вызов Complete() на последнем этапе транзакционного контекста автомати- зирует голосование за отмену транзакции в случае ошибки. Любое исключение, инициированное из транзакционного контекста, пропускает вызов Complete(); секция finally команды using уничтожает объект TransactionScope, и транзакция отменяется. С другой стороны, если метод Complete() был вызван, и транзак- ция завершится с установленным флагом закрепления, как в листинге 7.7, будет сделана попытка ее закрепления. Учтите, что после вызова Complete() по- пытки обращения к окружающей транзакции вызовут исключение InvalidOpera- tionException. После того как объект транзакционного контекста будет уничто- жен, вы снова сможете обратиться к окружающей транзакции (через Transaction. Current). Факт вызова Complete() в транзакционном контексте еще не гарантирует за- крепления транзакции. Даже если вы вызовете Complete() и транзакционный контекст будет уничтожен, WCF всего лишь попытается закрепить транзак- цию. Конечный успех или неудача этой попытки зависит от двухфазового про- токола, в котором могут быт задействованы другие ресурсы и службы, неиз- вестные вашему коду. Если закрепить транзакцию не удастся, Dispose)) инициирует TransactionAbortedException. Вы можете перехватить и обработать это исключение, например, для оповещения пользователя — пример представ- лен в листинге 7.8. Листинг 7.8. TransactionScope и обработка ошибок try { usingCTransactionScope scope = new TransactionScope()) { /* Транзакционные действия */ // Ошибок нет - транзакция закрепляется scope.Complete(): } } catch(Transact!onAbortedExcepti on e) { Trace.Wri teline(e.Message): } catch // Произошло любое другое исключение { Trace.WritelIneCCannot complete transaction"): throw: }
Явное транзакционное программирование 285 Управление распространением транзакций Допускается вложение транзакционных контекстов — как прямое, так и косвен- ное. В листинге 7.9 scope2 является вложенным в scopel. Листинг 7.9. Прямое вложение транзакционных контекстов uslngdransactionScope scopel = new Transact!onScope()) I usingdransactionScope scope2 = new TransactionScope()) ( scope2.Complete() ; ) scopel. Completed; ) Также возможно неявное вложение при вызове методов, использующих TransactionScope, методов с собственным транзакционным контекстом, как с ме- тодом RootMethod() в листинге 7.10. Листинг 7.10. Косвенное вложение транзакционных контекстов void RootMethod () I uslngdransactionScope scope = new TransactionScopeO) [ /* Транзакционные действия */ SomeMethodO: scope.Completed; 1 I told SomeMethodO I usingtTransactionScope scope - new TransactionScopeO) ( /* Транзакционные действия */ scope. Complete О; ) I Транзакционный контекст также может быть вложен в методе службы, как в листинге 7.11. При этом метод службы может быть или не быть транзакцион- ным. Листинг 7.11. Вложение транзакционных контекстов в методе службы class MyService : IMyContract ( [OperationBehaviorCTransactionScopeRequired - true)] public void MyMethod(...) ( using(TransactionScope scope = new TransactionScopeO) scope.Complete(); ) 1 I
286 Глава 7. Транзакции Если транзакционный контекст создает для себя новую транзакцию, он на- зывается корневым контекстом. Станет ли транзакционный контекст корневым или нет, зависит от конфигурации контекста и наличия окружающей транзак- ции. С установлением корневого контекста возникает неявная связь между ним и всеми вложенными контекстами, вызываемыми службами. Класс TransactionScope содержит несколько перегруженных конструкторов, получающих перечисление тина TransactionScopeOption: public enum TransactionScopeOption { Required. RequiresNew. Suppress } public class TransactionScope : IDisposable { public TransactionScope(TransactionScopeOption scopeOption): public TransactionScope(TransactionScopeOption scopeOption. Transactionoptions transactionoptions); public TransactionScope(TransactionScopeOption scopeOption. TimeSpan scopeTimeout): //Другие конструкторы и методы } Значение TransactionScopeOption позволяет управлять тем, задействован ли контекст в транзакции, и если задействован — присоединится ли он к окружаю- щей транзакции или станет корневым контекстом новой транзакции. Например, вот как задается значение TransactionScopeOption в конструкторе контекста: using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Required)) {•••} По умолчанию используется значение TransactionScopeOption.Required; други- ми словами, именно оно будет использовано при вызове одного из конструкто- ров, не получающих параметр TransactionScopeOption, поэтому следующие два определения эквивалентны: using(TransactionScope scope = new TransactionScope()) {...} using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Required)) {•••} Объект TransactionScope определяет, к какой транзакции он принадлежит, во время конструирования. После определения транзакционный контекст всегда будет принадлежать этой транзакции. TransactionScope принимает решение с учетом двух факторов: наличия окружающей транзакции и значения парамет- ра TransactionScopeOption. У объекта TransactionScope есть три варианта: О присоединиться к окружающей транзакции; О стать корнем нового транзакционного контекста (то есть начать новую тран- закцию и сделать ее окружающей в своем контексте); О вообще не принимать участия в транзакции.
Явное транзакционное программирование 287 Если транзакционный контекст настроен в режиме TransactionScopeOption. Required, а окружающая транзакция присутствует, объект присоединяется к этой транзакции. Если же окружающей транзакции нет, объект создает новую транзакцию и становится корневым. Если транзакционный контекст настроен в режиме TransactionScopeOption. Suppress, он никогда не является частью транзакции независимо от наличия или отсутствия окружающей транзакции. Окружающая транзакция у него всегда равна null. Голосование во вложенном транзакционном контексте Важно понимать, что хотя вложенный транзакционный контекст может при- соединиться к окружающей транзакции своего родительского транзакционного контекста, эти два объекта будут иметь разные флаги закрепления. Вызов Complete() во вложенном контексте не оказывает влияния на родительский кон- текст: usingCTransactionScope scopel = new TransactionScope()) ( usingCTransactionScope scope2 = new TransactionScopeO) scope2.Complete(): // Флаг закрепления scopel остается равным false 1 Только если все транзакционные контексты, от корневого до последнего вложенного, проголосуют за закрепление, транзакция будет закреплена. Кроме того, только корневой контекст определяет жизненный цикл транзакции. Когда объект TransactionScope присоединяется к окружающей транзакции, его уничто- жение не прекращает транзакцию. Транзакция завершается только с уничтоже- нием корневого транзакционного контекста или при возврате управления мето- дом службы, начавшем транзакцию. TransactionScopeOption.Required TransactionScopeOption.Required — не только самое распространенный из исполь- зуемых режимов, но и обладающий минимальными логическими зависимостя- ми. Если транзакционный контекст имеет окружающую транзакцию, он при- соединяется к окружающей транзакции для улучшения стабильности. Если это невозможно, транзакционный контекст по меньшей мере предоставляет про- граммному коду новую окружающую транзакцию. При использовании Trans- actionScopeOption.Required поведение кода TransactionScope не должно различать- ся в зависимости от того, является ли он корнем или просто присоединяется к окружающей транзакции. В обеих ситуациях код должен работать одинако- во. На стороне службы TransactionScopeOption.Required чаще всего используется нисходящими классами, вызываемыми службой, как показано в листинге 7.12. Листинг 7.12. Использование TransactionScopeOption.Required в нисходящих классах class MyService : IMyContract [OperationBehavior(Transacti onScopeRequi red = true)] продолжение &
288 Глава 7. Транзакции public void MyMethodt...) { MyClass obj = new MyClassO: obj. SomeMethodO; } } class MyClass { public void SomeMethodO { usingtTransactionScope scope = new TransactionScopeO) { // Некоторые действия, затем scope. Complete О: } } } Хотя сама служба тоже может использовать TransactionScopeOption.Required напрямую, практической пользы это не принесет: class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod(...) { //One transaction only using(TransactionScope scope = new TransactionScopeO) { // Некоторые действия, затем scope. Complete О; } } } Причина очевидна: служба может просто потребовать у WCF транзакцион- ный контекст для операции, задавая TransactionScopeRequired равным true. Обра- тите внимание: хотя служба может использовать декларативное голосование, для закрепления транзакции все нисходящие (или непосредственно вложен- ные) транзакционные контексты должны явно вызвать Complete(). Сказанное относится и к явному голосованию в методе службы: [OperationBehavior(TransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethod(...) { using(TransactionScope scope = new TransactionScopeO) { //Do some work then scope.CompleteО; } /* Транзакционные действия, затем: */ OperationContext.Current.SetTransactionComplete(): } Короче говоря, голосование за отмену в транзакционном контексте с Trans- actionScopeRequired, вложенным в вызов службы, приведет к отмене транзакции
Явное транзакционное программирование 289 службы независимо от исключений, применения декларативного голосования (с TransactionAutoComplete) или явного голосования службы SetTransactionCom- pleteQ). TransactionScopeOption.RequiresNew Настройка в режиме TransactionScopeOption.RequiresNew полезна при выполне- нии транзакционной работы вне контекста окружающей транзакции — напри- мер, при выполнении регистрации или аудита, или публикации событий для подписчиков независимо от закрепления, или отмены окружающей транзакции: class MyService : IMyContract I [OperationBehavi or (Transact) onScopeRequi red = true)] public void MyMethod(...) // Две разные транзакции using(TransactionScope scope = new Transacti onScope(TransactionScopeOpti on.Requi resNew)) { И Некоторые действия, затем: scope. CompleteO; ( } Вызов Complete() необходим для закрепления новой транзакции. Также сто- ит рассмотреть возможность заключения транзакционного контекста с Trans- actionScopeOption.RequiresNew в конструкцию try/catch для его изоляции от окру- жающей транзакции службы. При использовании TransactionScopeOption.RequiresNew действуйте чрезвы- чайно осторожно и убедитесь в том, что отмена одной из двух транзакций (ок- ружающей и созданной для вашего контекста) при закреплении другой не по- ставит под угрозу целостность вашей системы. TransactionScopeOption.Suppress Режим TransactionScopeOption.Suppress используется на стороне как клиента, так и службы, когда операции, выполняемые секцией кода, не должны приводить к отмене окружающей транзакции в случае их сбоя. TransactionScopeOption.Sup- press позволяет включить нетранзакционный код в транзакционный контекст или операцию службы, как показано в листинге 7.13. Листинг 7.13. Использование TransactionScopeOption.Suppress [OperationBehavi or (Transact i onScopeRequi red = true)] public void MyMethod(...) try // Начало нетранзакционной секции using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress)) // Нетранзакционные действия продолжение &
290 Глава 7. Транзакции } // Восстановление окружающей транзакции } catch {} } В листинге 7.13 обратите внимание на то, что в транзакционном контексте с Suppress не нужно вызывать Complete(). Другая ситуация, в которой может по- надобиться TransactionScopeOption.Suppress, — когда при реализации некоторого нестандартного поведения вам потребовалось организовать собственную про- граммную поддержку транзакций или ручного включения ресурсов. И все же при смешанном использовании транзакционных контекстов или методов служб с нетранзакционными необходима осторожность, потому что оно создает угрозу для изоляции и стабильности системы — изменения, внесен- ные в состояние системы в контексте с Suppress, не будут отменены вместе с ок- ружающей транзакцией. Вдобавок ошибки, возникающие в нетранзакционном контексте, не должны влиять на исход окружающей транзакции. Вот почему в листинге 7.13 Suppress-контекст заключен в конструкцию try/catch, которая также подавляет все выходящие исключения. Transactionscope и тайм-аут Если код в транзакционном контексте выполняется слишком долго, это может свидетельствовать о возникновении взаимной блокировки. По этой причине транзакция, выполняемая более заранее установленного интервала тайм-аута (60 секунд по умолчанию), автоматически отменяется. Тайм-аут по умолчанию может настраиваться в конфигурационном файле приложения. Например, что- бы установить тайм-аут по умолчанию равным 30 секундам, включите в файл следующую строку: <system.transactions> <defaultSettlngs timeout = ,,00:00:30"/> </system.transactions> Установка нового значения по умолчанию в конфигурационном файле приложения влияет на все транзакционные контексты, используемые всеми клиентами и службами приложения. Тайм-аут также может задаваться для конкретного транзакционного контекста. Некоторые перегруженные конст- рукторы TransactionScope получают объект типа TimeSpan, управляющий тайм-аутом транзакции: public TransactionScopeCTransactlonScopeOption scopeOption, ТimeSpan scopeTimeout): Чтобы задать интервал тайм-аута, отличный от стандартных 60 секунд, про- сто передайте нужное значение: TimeSpan timeout = TimeSpan.FromSeconds(30): us1ng(Transact1onScope scope = new Transact1onScope(Transact1onScopeOptlon.Required.timeout)) {...} Когда к окружающей транзакции присоединяется объект TransactionScope с меньшим интервалом тайм-аута, фактически он устанавливает для окружаю- щей транзакции новый, укороченный тайм-аут; транзакция должна завершиться
Явное транзакционное программирование 291 за указанное время, иначе произойдет ее автоматическая отмена. Если интервал тайм-аута транзакционного контекста выше, чем у окружающей транзакции, он не оказывает влияния на окружающую транзакцию. TransactionScope и уровень изоляции Если транзакционный контекст является корневым, по умолчанию транзакция выполняется с уровнем изоляции Serializable. Некоторые перегруженные конст- рукторы TransactionScope получают структуру типа Transactionoptions, которая определяется следующим образом: public struct Transactionoptions public Isol at 1 onLevel IsolationLevel (get; set:} public TimeSpan Timeout (get: set;} II... Свойство Timeout может использоваться для установки тайм-аута, но струк- тура Transactionoptions главным образом используется для задания уровня изо- ляции. Свойству IsolationLevel структуры Transactionoptions присваивается зна- чение из перечисления IsolationLevel, описанного ранее: Transactionoptions options = new Transact 1 onOptlons(): options.IsolationLevel = IsolationLevel .ReadCommitted: options. Timeout = TransactionManager .DefaultTimeout: using(TransactionScope scope = new ГransactionScope(TransactionScopeOption.Required.options)) (••) Контекст, присоединяющийся к окружающей транзакции, должен быть на- строен на одинаковый уровень изоляции с последней; в противном случае ини- циируется исключение ArgumentException. Не-служебные клиенты Хотя службы могут использовать класс TransactionScope, его основную область применения составляют «обычные» клиенты. Использование транзакционного контекста практически является единственным способом, посредством которо- го не-служебный клиент может сгруппировать несколько вызовов служб в одну транзакцию, как показано на рис. 7.7. Транзакция Клиент Рис. 7.7. Не-служебный клиент использует одну транзакцию для вызова нескольких служб
292 Глава 7. Транзакции Наличие возможности создания корневого транзакционного контекста по- зволяет клиенту распространить свою транзакцию к службам, управлять и за- крепить транзакцию на основании объединенных результатов служб, как пока- зано в листинге 7.14. Листинг 7.14. Использование TransactionScope для вызова нескольких служб в одной транзакции ////////////////////////// Сторона службы //////////////////////////// [ServiceContract] Interface IMyContract { [Operationcontract] [Transact1onFlow(TransactionFlowOption.AH owed)] void MyMethod(...); } [ServiceContract] Interface IMyOtherContract { [Operationcontract] [T ransactionFlow(Transact1onFlowOpt1 on.Mandatory)] void MyOtherMethod(...): } class MyService : IMyContract { [0perat1onBehav1or(Transact1onScopeRequ1red = true)] public void MyMethod(...) } class MyOtherServIce : IMyOtherContract { [0perat1onBehav1or(Transact1onScopeRequ1red = true)] public void MyOtherMethod(...) {...} } ////////////////////////// Сторона клиента //////////////////////////// usIngtTransactlonScope scope - new TransactionScope!)) { MyContractClient proxyl = new MyContractCl lentO; proxy1.MyMethod(...); proxyl. CloseO: MyOtherContractCllent proxy2 = new My0therContractC11ent(); proxy2.My0therMethod(...); proxy2.Close(); scope.Complete(): } // Объединяются в одном блоке using: using(MyContractCl 1 ent ргохуЗ = new MyContractCllentO) us1ng(My0therContractC11ent proxy4 = new MyOtherContractCl1ent()) us1ng(Transact1onScope scope = new TransactlonScopeO) { ргохуЗ.MyMethod!...):
Управление состоянием служб 293 ргоху4. MyOtherMethod (...): scope. Complete О; 1 Управление состоянием служб Единственной целью транзакционного программирования является решение проблемы восстановления и сохранение стабильного состояния системы. Со- стояние системы складывается из всех ресурсов, задействованных в транзак- ции, а также находящихся в памяти клиентов и экземпляров служб. Помимо та- ких преимуществ менеджеров ресурсов WCF, как автоматическое включение в транзакцию и участие в двухфазовом протоколе, основное и очевидное пре- имущество менеджера состоит в том, что любые изменения, вносимые в его со- стояние во время транзакции, автоматически отменятся при отмене транзакции. Тем не менее это утверждение не относится к членам экземпляров, находящих- ся в памяти, и статическим членам задействованных служб. Соответственно, после отмены транзакции стабильность состояния системы будет нарушена. Проблема усложняется тем обстоятельством, что в транзакции, в которой уча- ствует служба, может быть задействовано несколько служб и компьютеров. Даже если экземпляр службы не обнаруживает ошибок и голосует за закрепле- ние транзакции, транзакция в конечном счете может быть отменена другими сторонами, находящимися за границами службы. Если бы служба просто хра- нила свое состояние в памяти, как она узнает о результате транзакции, чтобы каким-то образом вручную отменить изменения, внесенные в ее состояние? Проблема управления состоянием экземпляров состоит в разработке служ- бы с контролем состояния (state-aware) и активном управлении ее состоянием. Как объяснялось в главе 4, служба с контролем состояния — не то же самое, что служба, не имеющая состояния. Если бы служба не имела состояния, то не су- ществовало бы и проблем с откатом состояния экземпляра. В ходе выполнения транзакции экземпляру службы разрешается управление состоянием в памяти. Между транзакциями состояние службы должно храниться в менеджере ресур- са. Менеджер ресурса состояния может быть не связан с другими ресурсами, за- действованными в бизнес-логике и используемыми в транзакции, а может быть одним из них. В начале транзакции служба должна загрузить свое состояние из ресурса и тем самым включить ресурс в транзакцию. В конце транзакции со- стояние снова сохраняется в менеджере ресурса. Элегантность такого решения состоит в том, что оно обеспечивает автоматическое восстановление состояния. Любые изменения, вносимые в состояние экземпляра, закрепляются и отменя- ются как часть транзакции. Если транзакция закрепляется, то при следующем получении состояния служба будет обладать новым состоянием. Если транзак- ция отменяется, служба возвращается к состоянию до начала транзакции. В обо- их случаях служба обладает стабильным состоянием, готовым к обращению со стороны новой транзакции. По умолчанию при завершении транзакции WCF уничтожает экземпляр службы; это гарантирует, что все остаточные данные, ко- торые могут поставить под угрозу стабильность, будут удалены из памяти.
294 Глава 7. Транзакции Границы транзакций При создании служб с контролем транзакционного состояния остается решить еще две проблемы. Первая: как служба узнает о начале и конце транзакции, чтобы она могла получить и сохранить свое состояние? Служба может быть ча- стью более масштабной транзакции, в которой участвуют разные службы и компьютеры. В любой момент между вызовами службы транзакция может за- вершиться. Кто обратится к службе со специальным вызовом, сообщая ей о не- обходимости сохранить состояние? Вторая проблема связана с изоляцией - разные клиенты могут параллельно вызывать службу в разных транзакциях. Как службе изолировать изменения, вносимые в ее состояние разными транзак- циями? Служба не может разрешить межтранзакционные вызовы, потому что это поставит под угрозу изоляцию. Если другая транзакция обратится к ее со- стоянию и будет работать на основании полученных значений, то состояние транзакции будет испорчено в случае отмены исходной транзакции и отката из- менений. Обе проблемы решаются совмещением границ методов с границами тран- закций. В начале каждого метода служба должна прочитать свое состояние, а в конце метода — сохранить его в менеджере ресурса. Это гарантирует, что при завершении транзакции между вызовами методов измененное состояние службы будет сохранено или отменено. Так как границы методов совмещаются с границами служб, экземпляр службы также должен голосовать за исход тран- закции в конце каждого метода. С точки зрения службы транзакция завершает- ся с возвратом управления методом. Вот почему свойство TransactionAutoCom- pLete называется именно так, а не Transaction AutoVote или что-нибудь в этом роде. Служба утверждает, что с ее точки зрения транзакция закончена. Если служба является корнем транзакции, это приводит к фактическому заверше- нию транзакции. Чтение и сохранение состояния в менеджере ресурса при каждом вызове ме- тодов решает проблему изоляции, потому что служба просто позволяет менед- жеру ресурса изолировать состояние между параллельными транзакциями. Идентификатор состояния С одним менеджером ресурсов могут работать сразу несколько экземпляров службы определенного типа, поэтому каждая операция должна содержать пара- метры, при помощи которых экземпляр службы мог бы найти свое состояние в менеджере ресурсов и привязаться к нему. Оптимальный вариант — включе- ние в каждую операцию ключевого параметра, идентифицирующего состояние. Я буду называть этот параметр идентификатором состояния. Идентификатор состояния предоставляется клиентом; в этом качестве может использоваться номер счета, номер заказа и т. д. Например, клиент создает новый транзакцион- ный объект обработки заказа и при каждом вызове метода передает его в пара- метре (наряду с другими параметрами). В листинге 7.15 приведен образец реализации транзакционной службы уров- ня вызовов.
Управление экземплярами и транзакции 295 Листинг 7.15. Реализация транзакционной службы [DataContract] class Param ь) [ServiceContract] interface IMyContract [Operationcontract] [Transact!onFlow(...)] void MyMethod(): [Servi ceBeha vi or (I nstanceContextMode = I nstanceContextMode. PerCa ID] class MyService : IMyContract. IDisposable I [OperationBehavi or (Transact! onScopeRequi red = true)] public void MyMethod(Param stateidentifier) GetState( stateidentifier); DoWorkO; SaveState(stateidentifier); } void GetState(Param stateidentifier) void DoWorkO void SaveState(Param stateidentifier) (...) public void Dispose() Сигнатура MyMethodO содержит идентификатор состояния типа Param (псев- дотип, придуманный для данного примера), который используется для получе- ния состояния от менеджера ресурса методом GetState(). Экземпляр службы вы- полняет свою работу методом DoWork(), после чего сохраняет свое состояние в менеджере ресурсов методом SaveState() с указанием идентификатора. Учтите, что не все состояние экземпляра службы может сохраняться по зна- чению в менеджере ресурсов. Если состояние содержит ссылки на другие объ- екты, вызов GetState() создает эти объекты, а вызов SaveState() (или Dispose()) уничтожает их. Управление экземплярами и транзакции Если экземпляр службы идет на хлопоты, связанные с загрузкой и сохранением состояния при каждом вызове метода, зачем дожидаться конца транзакции для уничтожения объекта? Следовательно, для транзакционных служб WCF самой естественной программной моделью является служба уровня вызова. Кроме то- го, требования к поведению транзакционных объектов с контролем состоя- ния и к объектам уровня вызова одинаковы — в обоих случаях происходит загрузка и сохранение состояния на границах методов (сравните листинг 4.3
296 Глава 7. Транзакции с листингом 7.15). Несмотря на тот факт что режим создания экземпляров уровня вызова наиболее «идейно близок» к транзакциям, WCF поддерживает сеансовые и даже синглетные службы, хотя и с существенно более сложной мо- делью программирования. Транзакционные службы уровня вызова В том, что касается вызовов служб уровня вызова, транзакционное программи- рование почти не требует дополнительных усилий со стороны программиста. Для каждого вызова службы создается новый экземпляр, и этот вызов может принадлежать или не принадлежать той же транзакции, что и предыдущий вы- зов (рис. 7.8). Независимо от транзакций, при каждом вызове служба получает и сохраня- ет свое состояние при помощи менеджера ресурса, поэтому методы всегда заве- домо работают с целостным состоянием, полученным от предыдущей транзакции, или с временным, но хорошо изолированным состоянием текущей транзакции. Служба уровня вызова должна голосовать и завершать свою транзакцию при каждом вызове метода. Фактически служба уровня вызова всегда обязана ис- пользовать автозавершение (свойство TransactionAutoComplete должно быть рав- но true — значение по умолчанию). С точки зрения клиента, один посредник может участвовать в нескольких разных или одинаковых транзакциях. Например, в следующем фрагменте все вызовы будут производиться в разных транзакциях: MyContractClient proxy = new MyContractCl1 ent(); using(Transact1onScope scope = new TransactionScopeO) { proxy.MyMethocK ...): scope.Complete(); } using(TransactionScope scope = new TransactionScopeO) { proxy.MyMethocK ...): scope.Complete(); } proxy.Close(); Или же клиент может несколько раз использовать посредника в одной тран- закции и даже закрыть посредника независимо от каких-либо транзакций:
Управление экземплярами и транзакции 297 MyContractClient proxy = new MyContractClient(); usingCTransactionScope scope = new TransactionScope()) I proxy. MyMethod (...): proxy. MyMethodt...): scope.CompleteO; I proxy.Closet); ПРИМЕЧАНИЕ------------------------------------------------------------------------------------------- Вызов Di$pose() в службах уровнях вызова не имеет окружающей транзакции. Жизненный цикл транзакции Когда служба уровня вызова является корнем транзакции (то есть когда тран- закция настроена в режиме «клиент/служба» и клиентская транзакция отсутст- вует или когда транзакция настроена в режиме «служба»), транзакция завер- шается с деактивизацией экземпляра службы. Как только метод вернет управ- ление, WCF завершает транзакцию даже до вызова Dispose(). Если клиент является корнем транзакции (или когда транзакция клиента распространяется к службе и последняя присоединяется к ней), транзакция завершается с завер- шением клиентской транзакции. Транзакционная служба уровня сеанса В стандартной транзакционной конфигурации WCF превращает любую служ- бу, независимо от ее режима активизации экземпляров, в службу уровня вызо- ва. Такое поведение ориентировано на единство управления состоянием служб, и оно требует, чтобы службы поддерживали контроль состояния. Однако нали- чие службы с контролем состояния противоречит исходной необходимости службы сеансового уровня. WCF позволяет поддерживать сеансовую семантику в транзакционных службах. С экземпляром транзакционной сеансовой службы могут работать разные транзакции, или он может быть привязан к определен- ной транзакции; в последнем случае доступ к нему разрешен только для этой транзакции (пока она не будет завершена). Тем не менее, как вы вскоре увиди- те, эта поддержка оборачивается непропорциональным ростом сложности про- граммной модели. Освобождение экземпляра службы Жизненный цикл любой транзакционной службы, не являющейся службой уровня вызова, определяется логическим свойством ReleaseServicelnstanceOnTrans- actionCompLete атрибута ServiceBehavior: [AttributeUsage( Attrl buteTargets. Cl ass)] public sealed class ServiceBehaviorAttribute : Attribute,... ( public bool ReleaseServicelnstanceOnTransactionComplete (get: set:} //... )
298 Глава 7. Транзакции Если свойство ReleaseServicelnstanceOnTransactionComplete задано равным true (значение по умолчанию), экземпляр службы уничтожается после завер- шения транзакции экземпляром. Учтите, что освобождение экземпляра про- исходит после завершения транзакции экземпляром, не обязательно с реаль- ным завершением транзакции (которое может произойти намного позже). Если свойство ReleaseServicelnstanceOnTransactionComplete истинно, у экземп- ляра имеются две возможности завершения транзакции и освобождения: на границе метода, у которого свойство TransactionAutoComplete равно true, или при вызове SetTransactionComplete() любым методом с ложным значением TransactionAutoComplete. Во взаимодействии ReleaseServicelnstanceOnTransactionComplete с другими свойствами поведения операций и служб необходимо выделить два интересных обстоятельства. Во-первых, ему невозможно задать значение (как true, так и false), если хотя бы у одной операции службы свойство TransactionScopeRequired уста- новлено равным true. Это условие проверяется во время загрузки службы set-методом свойства ReleaseServicelnstanceOnTransactionComplete. Например, следующая конфигурация является допустимой: [ServiceBehavior(ReleaseServi celnstanceOnTransacti onComplete = true)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO {••} [OperationBehavior(...)] public void MyOtherMethodO {•••} } Из этого ограничения следует, что даже в случае истинного по умолчанию значения ReleaseServicelnstanceOnTransactionComplete следующие два определе- ния семантически эквивалентны, потому что второе инициирует исключение во время загрузки службы: class MyService : IMyContract { public void MyMethodO {•} } // Недопустимое определение [ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = true)] class MyService : IMyContract { public void MyMethodO {...} } Второе ограничение, связанное с использованием ReleaseServicelnstanceOn- TransactionComplete, связано с параллельным многопоточным доступом к экземп- ляру службы.
Управление экземплярами и транзакции 299 Управление параллельной обработкой подробно рассматривается в следую- щей главе. А пока достаточно сказать, что параллельным доступом к экземпля- ру службы управляет свойство ConcurrencyMode атрибута ServiceBehavior: public enum ConcurrencyMode I Single. Reentrant. Multiple [Attn buteUsage (Att r i buteTargets .Class)] public sealed class ServiceBehaviorAttribute : ... 1 public ConcurrencyMode ConcurrencyMode (get; set;} //... По умолчанию свойство ConcurrencyMode равно ConcurrencyMode.Single. WCF во время загрузки службы убеждается в том, что если хотя бы у одной операции службы установлено истинное значение TransactionScopeRequired, ко- гда свойство ReleaseServicelnstanceOnTransactionComplete истинно (по умолчанию или явно заданное), значение ConcurrencyMode должно быть равно Concurrency- Hode.Single. Например, для следующего контракта: [ServiceContract] interface IMyContract ( [Operationcontract] [Transact!onFlow(...)] void MyMethodO: [Operationcontract] [Transact! onFlow(...)] void MyOtherMethod(); 1 следующие два определения эквивалентны и действительны: class MyService : IMyContract [OperationBehavi or (Transact! onScopeRequi red = true)] public void MyMethodO (...) public void MyOtherMethod() [ServiceBehavior (ConcurrencyMode = ConcurrencyMode.Single. ReleaseServicelnstanceOnTransactionComplete = true)] class MyService : IMyContract I [OperationBehavi or(Transacti onScopeRequi red = true)] public void MyMethodO
300 Глава 7. Транзакции public void MyOtherMethodO Следующее определение тоже допустимо, потому что ни один метод не тре- бует транзакционного контекста, даже несмотря на то, что свойство Release- ServicelnstanceOnTransactionComplete истинно: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract { public void MyMethodO public void MyOtherMethodO {•••} Напротив, следующее определение недопустимо, потому что по крайней мере один метод требует транзакционного контекста, свойство ReleaseService- InstanceOnTransactionComplete истинно, но при этом режим параллельной обра- ботки отличен от ConcurrencyMode.Single. 11 Недопустимая конфигурация [ServiceBehaviorCConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO public void MyOtherMethodO } ПРИМЕЧАНИЕ ---------------------------------------- Ограничение действует во всех режимах активизации экземпляров. Свойство ReleaseServicelnstanceOnTransactionComplete может сделать возмож- ным транзакционное сеансовое взаимодействие между клиентом и службой. По умолчанию оно имеет истинное значение, которое означает, что сразу же после завершения транзакции экземпляром службы (декларативного или явного) возврат управления методом приведет к деактивизации экземпляра службы, как у служб уровня вызова. Например, служба в листинге 7.16 ведет себя в точности как служба уровня вызова. Листинг 7.16. Сеансовая транзакционная служба, которая одновременно ведет себя как служба уровня вызова [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethodO:
Управление экземплярами и транзакции 301 class MyService : IMyContract [Operati onBehavior(TransactionScopeRequired = true)] public void MyMethodO i Каждый раз, когда клиент вызывает MyMethod(), он получает новый экземп- ляр службы. Новый клиентский вызов также может поступить к новой транзак- ции; экземпляр службы не имеет жесткой привязки к определенной транзак- ции. Связь между экземплярами служб и транзакциями выглядит так же, как на рис. 7.8. Запрет освобождения экземпляра службы Очевидно, конфигурации вроде показанной в листинге 7.16 не приносят прак- тической пользы. Чтобы проявлять признаки сеансового поведения, служба мо- жет задать свойство ReleaseServicelnstanceOnTransactionComplete равным false, как в листинге 7.17. Листинг 7.17. Транзакционная служба сеансового уровня [ServiceContract (SessionMode - SessionMode.Required)] interface IMyContract ( [Operationcontract] [TransactionFlow(...)] void MyMethod(): [ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)] class MyService : IMyContract ( [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO 1 {•} При ложном значении ReleaseServicelnstanceOnTransactionComplete экземпляр не уничтожается при завершении транзакции, как показано на рис. 7.9. Транзакция — Транзакция -> Метод Метод ► Метод — Экземпляр Рис. 7.9. Сеансовые экземпляры служб и транзакции Например, ситуация на рис. 7.9 может быть результатом следующего кли- ентского кода, в котором все вызовы поступают одному экземпляру службы: MyContractClient proxy = new MyContractClient(); using(TransactionScope scope = new Transact!onScope())
302 Глава 7. Транзакции proxy.MyMethocK); scope.Complete(): } usingtTransactionScope scope = new TransactionScopeO) { proxy.MyMethocK): proxy.MyMethocK): scope.Complete(); } proxy.Closet): Сеансовые службы с контролем состояния Если свойство ReleaseServicelnstanceOnTransactionComplete ложно, WCF не вме- шивается в происходящее, а все хлопоты с транзакционным управлением эк- земпляром службы достаются разработчику. Очевидно, вы должны каким-то образом отслеживать транзакции и откатывать изменения в состоянии экземп- ляра в случае отмены транзакции. Служба сеансового уровня по-прежнему должна совмещать границы методов с границами транзакций, потому что каж- дый метод может принадлежать отдельной транзакции. Существуют программ- ные модели; первая — служба с контролем состояния, но с использованием идентификатора сеанса как идентификатора состояния. В начале каждого метода служба получает свое состояние от менеджера ресурса, используя идентифика- тор сеанса в качестве ключа, а в конце каждого метода экземпляр службы снова сохраняет свое состояние в менеджере ресурса, как показано в листинге 7.18. Листинг 7.18. Служба с контролем состояния [ServiceBehavi or(ReleaseServicelnstanceOnT ransacti onComplete = false) ] class MyService : IMyContract.IDisposable { readonly string m_StateIdentifier: public MyServiceO { InitializeStateO: m_StateIdentifier = OperationContext.Current.Sessionld; SaveState(): } [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO { GetStateC); DoWorkO; SaveState(): } public void DisposeO { RemoveStatet); } // Вспомогательные методы void InitializeStateO
Управление экземплярами и транзакции 303 void GetStateO ( // Использование m_StateIdentifier для получения состояния ) void DoWorkO void SaveStateO // Использование m_StateIdentitier для сохранения состояния void RemoveState() // Использование m_StateIdentifier для удаления состояния из RM ) В листинге 7.18 конструктор сначала инициализирует состояние объекта, азатем сохраняет его в менеджере ресурса, чтобы оно могло быть получено лю- бым методом. Обратите внимание: объект сеансового уровня поддерживает ил- люзию сеансового взаимодействия с клиентом с поддержкой состояния. Служ- ба должна быть «дисциплинированной», чтобы загрузка и сохранение состоя- ния производились при каждом вызове операции. В конце сеанса служба уда- ляет свое состояние из менеджера ресурса в методе Dispose(). Сеансовые службы с состоянием Вторая, более современная программная модель основана на использовании VRM в качестве членов службы (см. врезку); пример приведен в листинге 7.19. Листинг 7.19. Использование временных менеджеров ресурсов для реализации сеансовой транзакционной службы с состоянием [Servi ceBehav 1 or (Rel easeServi oelnstanceOnT ransacti onCompl ete = f al se) ] class MyService : IMyContract i Transactional<string> m_Text = new Transactional<string>("Some Initial value"); TransactionalArray<int> m_Numbers = new TransactionalArray<int>(3); [OperationBehaviorCTransactionScopeRequired = true)] public void MyMethodO m_Text.Value = "This value will roll back if the transaction aborts"; И Возврат к прежнему состоянию при отмене транзакции m_Numbers[O] =11; m_Numbers[l] = 22; m_Numbers[2] = 33;
304 Глава 7. Транзакции ВРЕМЕННЫЕ МЕНЕДЖЕРЫ РЕСУРСОВ-------------------------------------------------------------- В статье «Volatile Resource Managers in .NET Bring Transactions to the Common Type» (MSDN Magazine, май 2005 г.) я представил методику реализации обобщенного временного менеджера ресурсов Transactional <Т>: public class Transact1onal<T> : { public Transactional(T value); public Transactional0; public T Value {get;set;} /* Операторы преобразования к типу Т и обратно */ } Передавая Transactional<Т> параметр любого сериализуемого типа (например, int или string), вы превращаете этот тип в полноценный VRM, который автоматически включается в окружающую транзакцию, участвует в двухфазовом протоколе закрепления и изолирует текущие изменения от остальных транзакций. Например, в следующем фрагменте кода транзакционный контекст не завершается. В результате транзакция отменяется, а значения number и city возвращаются к состоянию до начала транзак- ции: Transactional<int> number = new Transactional<int>(3); Transactional<string> city = new Transactional<string>("New York"); using(TransactionScope scope = new TransactionScopeO) { city.Value = "London"; number.Value = 4; number.Vaiue++; Debug.Assert(number.Value == 5); Debug.Assert(number == 5): } Debug.Assert(number == 3); Debug.Assert(city == "New York"); Кроме Transactional<T>, я также представил массив TransactionalArray<T> и транзакционные вер- сии всех коллекций в System.Collections.Generic — например, TrarisactionalDictionary<K,T> и Tran- sactionalList<T>. Реализация моих VRM не имеет никакого отношения к WCF, поэтому я не стал включать ее в книгу. С другой стороны, в ней широко используются многие расширенные возможно- сти C# 2.0, пространства имен System.Transactions и системного программирования .NET, поэтому она может заинтересовать читателя сама по себе. В листинге 7.19 используются мои временные менеджеры ресурсов Transac- tional<T> и TransactionalArray<T> (имеются в архиве исходного кода книги). Бла- годаря обобщению Transaction<T> может получить любой сериализуемый тип и предоставить транзакционный доступ к нему. Использование временных ме- неджеров ресурсов делает возможной модель программирования с состоянием, а экземпляр службы просто обращается к состоянию так, как если бы транзак- ции вообще не участвовали. Временные менеджеры ресурсов автоматически включаются в транзакцию и изолируют ее от других транзакций. Любые изме- нения, вносимые в состояние, закрепляются или отменяются вместе с транзак- цией.
Управление экземплярами и транзакции 305 Жизненный цикл транзакций Если сеансовая служба является корнем транзакции, транзакция прекращается при ее завершении службой, то есть при возврате управления методом. Если корнем транзакции является клиент (или когда транзакция распространяется к службе), транзакция прекращается с прекращением клиентской транзакции. Если сеансовая служба предоставляет реализацию IDisposable, метод Dispose() не имеет транзакции независимо от корня. Параллельные транзакции Поскольку у сеансовых служб один экземпляр может быть задействован не- сколькими клиентскими вызовами, они также способны поддерживать несколь- ко параллельных транзакций. В листинге 7.20 приведен клиентский код (для определения службы из листинга 7.17), который запускает параллельные тран- закции для одного экземпляра. Объект scope2 использует новую транзакцию, отдельную от транзакции scopel, но при этом обращается к тому же экземпляру службы в одном сеансе. Листинг 7.20. Запуск параллельных транзакций using(TransactionScope scopel = new Transact!onScope()) I MyContractClient proxy = new MyContractClient(): proxy. My Met hod (): using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.RequiresNew)) ( proxy.MyMethod(); scope2.Complete(): proxy.MyMethod(): proxy. Cl ose(): scopel. Complete (): 1 Транзакции, созданные в листинге 7.20, показаны на рис. 7.10. Такой код, как в листинге 7.20, почти наверняка приведет к транзакционной взаимной бло- кировке за ресурсы, используемые службой. Первая транзакция устанавливает блокировку ресурса. Вторая транзакция дожидается установления своей блоки- ровки, пока первая ожидает завершения второй. Транзакция Транзакция ► Метод — Метод —► Метод — -> Экземпляр Рис. 7.10. Параллельные транзакции
306 Глава 7. Транзакции Завершение по окончании сеанса WCF предлагает еще одну программную модель для транзакционных служб сеансового уровня, абсолютно независимую от ReleaseServicelnstanceOnTransac- tionComplete. Эта модель предназначена для тех случаев, когда жизненный цикл сеанса составляет часть жизненного цикла транзакции; это означает, что весь сеанс умещается в одну транзакцию. Идея состоит в том, что служба не должна завершать транзакцию в сеансе, потому что это заставляет WCF осво- бодить экземпляр службы. Чтобы избежать завершения транзакции, сеансовая служба может задать свойство TransactionAutoComplete равным false, как показа- но в листинге 7.21. Листинг 7.21. Задание TransactionAutoComplete ложного значения [ServiceContract(SessionMode « SessionMode.Required)] interface IMyContract { [OperationContract] [TransactionFlow(...)] void MyMethodK): [OperationContract] [TransactionFlow(...)] void MyMethod2(): [OperationContract] [TransactionFlow(...)] void MyMethod3(): } class MyService IMyContract { [OperationBehavior(Transact!onScopeRequired = true. TransactionAutoComplete = false)] public void MyMethodlO {...} [OperationBehavior(TransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethod2() {••} [OperationBehavior(TransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethod3() {...} } Только сеансовая служба может задать TransactionAutoComplete значение false, и это условие проверяется во время загрузки службы. Проблема листин- га 7.21 состоит в том, что транзакция, в которой участвует служба, всегда будет отменяться, потому что служба не голосует за ее закрепление. Если жизненный цикл сеанса полностью заключен в одной транзакции, служба должна проголо- совать сразу же после окончания сеанса. Для этой цели атрибут ServiceBehavior
Управление экземплярами и транзакции 307 предоставляет свойство TransactionAutoCompleteOnSessionClose, определяемое следующим образом: [Attri butellsage (At t r i buteTargets. Class) ] public sealed class ServiceBehaviorAttribute : Attribute.... public bool TransactionAutoCompleteOnSessionClose [get; set:} II... По умолчанию свойство TransactionAutoCompleteOnSessionClose равно false. Ко- гда оно устанавливается равным true, это приводит к автоматическому заверше- нию всех незавершенных методов в сеансе. Если во время сеанса не произошло ни одного исключения, при истинном значении TransactionAutoCompleteOnSes- sionClose служба голосует за закрепление. Например, переработанная версия листинга 7.21 выглядит так: [ServiceBehavior(TransactionAutoCompleteOnSessionClose = true)] class MyService : IMyContract I- 1 На рис. 7.11 изображена диаграмма с экземпляром и сеансом. Транзакция Метод Метод Метод Конец сеанса Экземпляр Рис. 7.11. Свойство TransactionAutoCompleteOnSessionClose равно true Во время сеанса экземпляр может работать со своим состоянием, хранящим- ся в переменных класса; необходимость в контроле состояния или временных менеджерах ресурсов отсутствует. ВНИМАНИЕ В случае присоединения к клиентской транзакции и использования автозавершения при закрытии сеанса служба должна избегать продолжительных действий в Dispose() или, с более практичной точки зрения, вообще избегать реализации IDisposable. Это делается для предотвращения «ситуа- ции гонки». Вспомните, о чем говорилось в главе 4: метод Dispose() вызывается асинхронно в конце сеанса. Автоматическое завершение в конце сеанса происходит сразу же после уничтожения экземп- ляра. Если клиент получит управление до уничтожения экземпляра, транзакция будет отменена, потому что служба еще не завершила ее. Использование TransactionAutoCompleteOnSessionClose рискованно, потому что оно всегда сопряжено с тайм-аутом транзакции. Сеансы по самой природе яв- ляются «долгожителями», тогда как хорошо спроектированные транзакции имеют короткий срок жизни. Данная программная модель существует для тех случаев, когда для принятия решения по голосованию необходима информа- ция, получаемая в результате будущих вызовов во время сеанса.
308 Глава 7. Транзакции Так как при истинном значении TransactionAutoCompleteOnSessionClose грани- ца сеанса совмещается с границей транзакции, при использовании клиентской транзакции клиент должен закрыть сеанс внутри этой транзакции: us1ng(TransactionScope scope = new TransactionScopeO) { MyContractClient proxy = new MyContractCl1ent(): proxy.MyMethodO; proxy. MyMethodO; proxy. Cl oseO; scope. Compl eteO; } Если этого не сделать, транзакция будет отменена. Побочный эффект заклю- чается в том, что клиент не сможет легко вкладывать команды using транзакци- онного контекста и посредника, потому что это может привести к уничтожению посредника после транзакции: // Всегда приводит к отмене транзакции: using(MyContractCl 1 ent proxy = new MyContractCllentO) us1ng(Transact1onScope scope = new TransactionScopeO) { proxy.MyMethod(): proxy.MyMethod(); scope.Complete(): } Кроме того, поскольку посредник фактически рассчитан на одноразовое ис- пользование, хранить его в переменной объекта нет смысла. Транзакционная связь Задание TransactionAutoComplete равным false приводит к уникальному эффекту, который не реализуется другими средствами WCF: между экземпляром служ- бы и транзакцией формируется «родственная связь», так что к экземпляру службы может обращаться только одна транзакция. Связь устанавливается с первым обращением транзакции к экземпляру службы, а после установления продолжает существовать на протяжении всего жизненного цикла экземпляра (до завершения сеанса). Транзакционная связь доступна только для сеансовых служб, потому что только сеансовая служба может задать TransactionAutoCom- plete значение false. Наличие такой связи очень важно, потому что служба не является службой с контролем состояния — она использует нормальные чле- ны и должна изолировать доступ к ним со стороны других транзакций на тот случай, если родственная ей транзакция будет завершена. Таким образом формируется примитивная форма транзакционной блокировки. С ней код из листинга 7.20 гарантированно приводит к взаимной блокировке (и в конечном итоге отмене по тайм-ауту), потому что вторая транзакция блокируется (неза- висимо от того, к каким ресурсам обращается служба), ожидая завершения первой транзакции, тогда как первая транзакция блокируется в ожидании второй.
Управление экземплярами и транзакции 309 Гибридное управление состоянием BWCF также существует гибридный режим, объединяющий две представлен- ные программные модели. Гибридный режим проектировался для того, чтобы экземпляр службы мог поддерживать свое состояние в памяти до завершения транзакции, после чего состояние уничтожается с использованием ReleaseSer- vicelnstanceOnTransactionComplete. Для примера рассмотрим службу из листин- га 7.22, реализующую контракт из листинга 7.21. Листинг 7.22. Гибридная сеансовая служба [Serv1ceBehav1or(Transact1onAutoConipleteOnSess1onClose = true)] class MyService : IMyContract ( [OperationBehavior(TransactionScopeRequi red = true. TransactionAutoComplete = false)] public void MyMethodlO (...) [OperationBehavior(TransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethod2() [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod3() (...) 1 Служба использует значение по умолчанию TransactionAutoCompleteOnSes- sionClose (false), при этом она содержит два метода (MyMethodlO и MyMethod2()), не завершающие транзакцию и обладающие ложным свойством TransactionAuto- Complete, что создает связь с конкретной транзакцией. Проблема в том, что служба всегда будет отменять транзакцию, потому что транзакция не заверша- ется. Для решения этой проблемы в службу включается метод MyMethod3(), за- вершающий транзакцию. Так как служба использует для ReleaseServicelnstanceOnTransactionComplete значение по умолчанию (true), после вызова MyMethod3() транзакция завершается, а экземпляр службы уничтожается, как показано на рис. 7.12. Метод MyMethod3() также мог использовать явное голосование посредством SetTransactionCompieteO; важен сам факт завершения транзакции. Гибридный режим изначально ненадежен. Прежде всего, экземпляр службы должен завершить транзакцию до наступления тайм-аута. Предсказать, когда клиент вызовет завершающий метод, заранее невозможно, поэтому возникает опасность тайм-аута. Кроме того, служба продлевает блокировки менеджеров ресурсов, к которым она может обращаться во время сеанса. Чем дольше удер- живаются блокировки, тем выше вероятность того, что у других транзакций произойдет тайм-аут или возникнет взаимная блокировка с транзакцией служ- бы. Наконец, служба начинает зависеть от клиента, потому что клиент должен вызвать завершающий метод для прекращения сеанса. Чтобы заставить клиента
310 Глава 7. Транзакции TransactionAutoComplete = true Транзакция TransactionAutoComplete = false Рис. 7.12. Гибридный режим выполнить это требование, можно и нужно применить демаркационные опе- рации: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [Operationcontract] [TransactionFlow(...)] void MyMethodlO: [OperationContractdsInitiating = false)] [TransactionFlowf...)] void MyMethod2(): [OperationContractdsInitiating = false, IsTerminating = true)] [TransactionFlow(...)] void MyMethod3(): Выбор транзакционной службы сеансового уровня Транзакционные сеансы существуют для ситуаций, когда для выполнения транзакции и последующего решения по голосованию требуется информация, полученная в ходе сеанса. Для примера рассмотрим следующий контракт, ис- пользуемый при обработке заказов: [ServiceContractCSessionMode = SessionMode.Required)] interface lOrderManager ( [Operationcontract] [TransactionFlow(...)] void SetCustomerId(int customerld); [OperationContractdsInitiating = false)] [TransactionFlow(...)] void Addltem(int itemld): [OperationContractdsInitiating = false, IsTerminating = true)] [TransactionFlowC...)] bool ProcessOrders(); } Служба может обрабатывать заказы только после получения идентификато- ра клиента и информации обо всех заказанных предметах. Тем не менее исполь- зование транзакционных сеансов обычно свидетельствует о некачественном
Управление экземплярами и транзакции 311 проектировании, так как оно создает проблемы с производительностью и мас- штабируемостью. Службе приходится дольше удерживать блокировки, а это повышает риск взаимных блокировок. Лично я считаю транзакционные сеансы архитектурным дефектом в лучшем случае и безусловным злом в худшем. Не- пропорциональная сложность транзакционных сеансов, связанная с управле- нием состояния, транзакционными связями и завершением транзакции, переве- шивает приносимую ими пользу. Обычно бывает лучше сформировать контракт так, чтобы он не зависел от сеанса: 'ServiceContract ] interface lOrderManager ( [Operationcontract] [TransactionFlow(...)] bool ProcessOrders (int customerld.int[] itemlds): Я отдаю предпочтение простым службам уровня вызовов и избегаю транзак- ционных сеансов. Транзакционные синглетные службы По умолчанию транзакционные синглеты ведут себя как службы уровня вызова. Дело в том, что по умолчанию свойство ReLeaseServicelnstanceOnTransaction- Complete истинно, поэтому после автоматического завершения транзакции WCF уничтожает синглетный экземпляр в интересах управления состоянием и ста- бильности системы. В свою очередь, это подразумевает, что синглет должен обеспечивать контроль состояния и активно управлять своим состоянием при каждом вызове метода. Существенное отличие от служб уровня вызова заклю- чается в том, что WCF поддерживает синглетную семантику, поэтому в любой момент времени существует не более одного экземпляра службы. Для обеспече- ния этого правила WCF использует средства управления параллельной обра- боткой и деактивизации экземпляров. Напомню, что при истинном свойстве ReleaseServicelnstanceOnTransactionComplete должен быть установлен режим Соп- currencyMode.SingLe, запрещающий параллельные вызовы. WCF поддерживает транзакционный контекст и просто деактивизирует экземпляр, находящийся в управлении контекста, как обсуждалось в главе 4. Это означает, что хотя синглетная служба и должна поддерживать контроль состояния, ей не потребу- ется идентификатор состояния, передаваемый клиентом при каждом вызове. Для идентификации своего состояния в менеджере ресурса состояния синглет может использовать любую константу уровня типа, как показано в листинге 7.23. Листинг 7.23. Синглетная служба с контролем состояния [ServiceBehavl or (InstanceContextMode = InstanceContextMode.Single)] class MySingleton : IMyContract readonly static string m_StateIdentitier = typeof (MySingleton),GUID.ToString(); [OperationBehavior(TransactionScopeRequired = true)] продолжение &
312 Глава 7. Транзакции public void MyMethodO { GetState(); DoWorkO; SaveStateO: } // Вспомогательные методы void GetStateO { // Использование m_StateIdentlfier для получения состояния } void DoWorkO {} public void SaveStateO { // Использование m_StateIdentifier для сохранения состояния } public void RemoveStateO { // Использование m_StateIdentif1er для удаления состояния } } // Код хостинга MySingleton singleton - new MySIngletonO: singleton.SaveStateO://Создание начального состояния в менеджере ресурса ServiceHost host = new ServlceHost(slngleton): host. Open О; /* Блокирующие вызовы */ host.Close(); singleton.RemoveStateC): В этом примере синглет использует в качестве идентификатора состояния уникальный GUID. В начале каждого метода синглет читает свое состояние, а в конце — сохраняет его в менеджере ресурса. Тем не менее еще до поступления самого первого вызова состояние должно быть сохранено в менеджере ресурса. Для этого перед запуском хоста следует создать синглетный экземпляр, сохра- нить его состояние в менеджере ресурса, а затем передать синглетный экземп- ляр ServiceHost, как объяснялось в главе 4. После прекращения работы хоста не забудьте удалить состояние синглетного экземпляра из менеджера ресурса, как показано в листинге 7.23. При этом исходное состояние не может создаваться в конструкторе, потому что конструктор будет вызываться для каждой операции с синглетным экземпляром, и это приведет к потере предыдущего сохраненного состояния. Хотя реализация синглетного экземпляра с контролем состояния возможна (см. листинг 7.23), из-за обилия сложностей использовать эту мето- дику не рекомендуется. Лучше использовать транзакционный синглет с состоя- нием, как показано далее. Синглетный экземпляр с состоянием Задание свойству ReleaseServicelnstanceOnTransactionComplete значения false со- храняет синглетную семантику. Синглетный экземпляр создается только один
Управление экземплярами и транзакции 313 раз при запуске хоста и в дальнейшем совместно используется всеми клиента- ми и транзакциями. Конечно, проблема в том, как организовать управление его состоянием. Синглет должен обладать состоянием, иначе и создавать его неза- чем. Как и в предыдущем случае, проблема решается использованием времен- ных менеджеров ресурсов, как показано в листинге 7.24. Листинг 7.24. Синглетная транзакционная служба с состоянием ////////////////// Сторона службы/////////////////////////////////// [Servi ceBeha v 1 or (I nstanceContextMode = I nstanceContextMode. Si ngl e. ReleaseServicelnstanceOnTransactionComplete - false)] class MySingleton : IMyContract 1 Transact!onal<1nt> m_Counter = new Transactional<1nt>(); [OperationBehavior(Transact!onScopeRequired = true)] public void MyMethodO m_Counter.Value++; Trace.WriteLineC'Counter: " + m_Counter.Value); } ////////////////// Сторона клиента 11111111111111111111111111111111 using(TransactionScope scopel = new TransactionScopeO) I MyContractCl ient proxy = new MyContractClientO; proxy. MyMethodO: proxy. Close О; scopel. Complete О: ) u$ing(TransactionScope scope2 = new TransactionScopeO) I MyContractClient proxy = new MyContractClientO; proxy. MyMethodO; proxy. Close О; I usinglTransactionScope scope3 - new TransactionScopeO) I MyContractClient proxy = new MyContractClientO; proxy.MyMethodO: proxy. Close О; scope3. Complete О; I ////////////////// Вывод /////////////////////////////////// Counter: 1 Counter: 2 Counter: 2 В листинге 7.24 клиент создает три транзакционных контекста, каждый из которых обладает собственным посредником для работы с синглетной службой. При каждом вызове синглетный экземпляр увеличивает счетчик, хранящийся в менеджере ресурсов типа Transactional<int>. Объект scopel завершает транзак- цию и закрепляет новое значение счетчика. В scope? клиент обращается с вызо- вом к синглетному экземпляру и временно увеличивает счетчик до 2. Однако
314 Глава 7. Транзакции scope2 не завершает транзакцию, поэтому менеджер ресурса отвергает увеличе- ние и возвращается к предыдущему значению 1. Затем вызов в scope3 снова увеличивает счетчик с 1 до 2, как показывает трассировочный вывод. Учтите, что при установке ReleaseServicelnstanceOnTransactionComplete синг- летный экземпляр должен содержать по крайней мере один метод, у которого свойство TransactionScopeRequired задано равным true. Кроме того, у синглетного экземпляра у каждого метода должно быть установлено истинное свойство TransactionAutoComplete; конечно, это исключает какие-либо транзакционные связи и допускает параллельные транзакции. Все вызовы и все транзакции на- правляются одному экземпляру. Например, диаграмма транзакций для следую- щего клиентского кода показана на рис. 7.13. using(MyContractCl1 ent proxy = new MyContractClient()) us1ng(TransactionScope scope = new TransactionScopeO) { proxy.MyMethod(): scope.Complete(): } using(MyContractClient proxy = new MyContractCllentO) using(TransactionScope scope = new TransactionScopeO) { proxy.MyMethod(): proxy.MyMethod(); scope.Complete(); } Рис. 7.13. Синглетная транзакционная служба с состоянием Транзакции и режимы активизации экземпляров Как следует из приведенного обсуждения, настройка транзакционной службы на какой-либо режим активизации, кроме режима уровня вызова, приводит к непропорциональному росту сложности программной модели. Хотя мощность и расширяемость платформы WCF позволяют поддерживать самый широкий спектр конфигурационных комбинаций, я рекомендую придерживаться служб уровня вызова. Транзакционная синглетная служба, использующая временный менеджер ресурса, тоже приемлема — при условии, что приемлем сам синглет- ный режим. В завершение темы режимов управления экземплярами и транзак- ций в табл. 7.3 перечислены описанные ранее конфигурации. В таблицу вклю- чены только реально используемые конфигурации. Другие комбинации могут быть возможны технически, но не имеют смысла или попросту запрещаются WCF.
Обратный вызов 315 Таблица 7.3. Режимы активизации экземпляров, конфигурации и транзакции Режим ак- тивизации Авто- завер- шение Освобож- дение по заверше- нии Завершение при оконча- нии сеанса Итоговый ре- жим активи- зации экземпляров Управление состоянием Транзак- ционная связь Уровня вызова True True/false True/false Уровня вызова Контроль состояния Вызов Сеансовый False True/false True Сеансовый С состоянием Экземпляр Сеансовый True False True/false Сеансовый С состоянием (контроль состояния, VRM) Вызов Сеансовый True True True/false Уровня вызова Контроль состояния Вызов Сеансовый Гибрид True True/false Г ибридный Г ибридный Экземпляр Синглетный True True True/false Уровня вызова Контроль состояния Вызов Синглетный True False True/false Синглетный С состоянием Вызов (контроль состояния, VRM) Кратный вызов Контракты обратного вызова, как и контракты служб, могут распространять транзакции служб к клиентам обратного вызова. Как и в случае с контрактом службы, для этого применяется атрибут Transaction Flow: interface IMyContractCalIback [OperationContract] [Transact 1 onF 1 ow( T r ansact 1 onFl owOpt 1 on. Al 1 owed) ] void OnCallbackO: [ServiceContract (Cal 1 backContract = typeof (IMyContractCal Iback))] interface IMyContract Как и операции служб, реализация метода обратного вызова может исполь- зовать атрибут OperationBehavior, а также требовать обязательного использова- ния транзакционного контекста и автоматического завершения: class MyClient : IMyContractCal Iback ( [OperationBehavior(TransactionScopeRequired = true)] public void OnCallbackO Transaction transaction = Transaction.Current; Debug.Assert(transaction != null);
316 Глава 7. Транзакции Транзакционные режимы обратного вызова Клиент обратного вызова может быть настроен в одном из четырех режимов: «служба», «служба/клиент обратного вызова», «клиент обратного вызова» и «без транзакции». Эти режимы аналогичны транзакционным режимам служб, за исключением того, что теперь служба играет роль клиента, а клиент обратно- го вызова — роль службы. Например, чтобы настроить клиента обратного вызо- ва на использование транзакционного режима «служба» (то есть чтобы всегда использовалась транзакция службы), выполните следующие действия: 1. Используйте транзакционно-способную дуплексную привязку с включен- ным распространением транзакций. 2. Включите обязательное распространение транзакции для операции обратно- го вызова. 3. Настройте операцию обратного вызова так, чтобы она требовала обязатель- ного использования транзакционного контекста. В листинге 7.25 представлен клиент обратного вызова, настроенный для ис- пользования режима «служба». Листинг 7.25. Настройка клиента обратного вызова для транзакционного режима «служба» interface IMyContractCallback { [Operationcontract] [TransactionFlow(TransactionFlowOption.Mandatory)] void OnCallbackO; } class MyClient ; IMyContractCallback { [OperationBehavior(TransactionScopeRequired = true)] public void OnCallbackO { Transaction transaction = Transact!on.Current; Debug.Assert(transaction.Transactioninformation. Distributedldentifier !- Guid.Empty); } } Если операция обратного вызова настроена на обязательное распростране- ние транзакции, WCF заставляет использовать транзакционно-способную при- вязку с включенным распространением транзакции. При настройке режима «служба/клиент обратного вызова» WCF не требует обязательного использова- ния транзакционно-способной привязки или включенного распространения тран- закции. Для проверки можно воспользоваться моим атрибутом BindingRequirement interface IMyContractCallback { [Operationcontract] [Transact!onFlow(TransactionFlowOption.Allowed)] void OnCallbackO; }
Обратный вызов 317 [BindingRequirement(TransactionFlowEnabled - true)] class MyCllent : IMyContractCallback I [OperationBehavi or(Transacti onScopeRequi red = true)] public void OnCal IbackO 1 (•••} Атрибут BindingRequirement был расширен для проверки привязок обратного вызова посредством реализации интерфейса lEndpointBehavior: public interface lEndpointBehavior ( void AddBi ndi ngParameters( Servi ceEndpoi nt endpoint. BindingParameterCollection bindingparameters); void ApplyClientBehavior(ServiceEndpoint serviceEndpoint. ClientRuntime behavior): void ApplyDispatchBehavior(ServiceEndpoint endpoint. Endpoi ntDIspatcher endpoi ntDi spatcher); void Vai idateCServiceEndpoint serviceEndpoint); ) Как объяснялось в главе 6, интерфейс lEndpointBehavior позволяет настроить конечную точку на стороне клиента, используемую при обратном вызове со стороны службы. В случае атрибута BindingRequirement используется метод IEnd- pointBehavior.Validate(), а его реализация почти идентична приведенной в лис- тинге 7.3. Изоляция и тайм-ауты По аналогии со службами, атрибут CalLbackBehavior позволяет типу клиента об- ратного вызова управлять тайм-аутом и уровнем изоляции транзакции: [Attributeusage (Attri buteTargets .Class)] public sealed class CallbackBehaviorAttribute: Attribute.lEndpointBehavior ( public IsolationLevel TransactionlsolationLevel (get: set:} public string Transact!onTimeout (get: set:} II... i Свойства получают те же значения, как и для служб, а при выборе того или иного режима учитываются те же факторы. Голосование при обратном вызове По умолчанию WCF использует для операций обратного вызова автоматиче- ское голосование, как и для операций служб. Любое исключение при обратном вызове приводит к голосованию за отмену транзакции, а без ошибок WCF го- лосует за закрепление, как в листинге 7.25. Тем не менее, в отличие от экземпля- ров служб, жизненный цикл любого экземпляра клиента обратного вызова нахо- дится под управлением клиента. Любой экземпляр обратного вызова может быть настроен на явное голосование; для этого свойство TransactionAutoComplete
318 Глава 7. Транзакции задается равным false, после чего явное голосование производится вызовом SetTransactionCompieteO: class MyCllent : IMyContractCalIback { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)! public void OnCallbackO { /* Транзакционные действия, затем */ Operationcontext.Current.SetTransactionCompleteC); } } Как и в случае со службами сеансового уровня, явное голосование предна- значено для случаев, когда его результат зависит от других факторов, кроме ис- ключений. После вызова SetTransactionCompieteO не должна выполняться ника- кая работа, особенно транзакционная. Вызов SetTransactionCompieteO должен располагаться в последней строке операции обратного вызова, непосредственно перед возвратом управления. Если вы попытаетесь выполнить после вызова SetTransactionCompieteO какие-либо транзакционные действия (включая обра- щение к Transaction.Current), WCF инициирует исключение InvalidOperationEx- ception и отменяет транзакцию. Использование транзакционных обратных вызовов Хотя WCF предоставляет инфраструктуру распространения транзакций служб через обратный вызов, на практике обратные вызовы и транзакции служб пло- хо совмещаются. Во-первых, обратные вызовы обычно являются односторонни- ми операциями, а односторонние операции не могут использоваться для рас- пространения транзакций. Во-вторых, чтобы служба могла использовать об- ратный вызов, она не может быть настроена в режиме ConcurrencyMode.Single; в противном случае WCF отменит вызов для предотвращения взаимной блоки- ровки. Обычно службы настраиваются в режиме распространения транзакций «клиент/служба» или «клиент». В идеале служба должна быть способна рас- пространять исходную транзакцию клиента, от которого поступил вызов, на осуществляемые ей обратные вызовы. Но чтобы служба могла использовать транзакцию клиента, свойство TransactionScopeRequired должно быть задано рав- ным true. Поскольку свойство ReleaseServicelnstanceOnTransactionComplete истин- но по умолчанию, оно требует режима ConcurrencyMode.Single, что делает обрат- ный вызов невозможным. Внеполосные транзакционные обратные вызовы Существует два способа выполнения транзакционных обратных вызовов. Пер- вый — внеполосные обратные вызовы от не-служебных участников на стороне хоста, при которых используются ссылки обратного вызова, сохраненные служ- бой. Такие участники могут легко распространять свои транзакции (обычно
Обратный вызов 319 вTransactionScope) к клиенту обратного вызова, потому что при этом не возни- кает риска взаимной блокировки, как показано в листинге 7.26. Листинг 7.26. Внеполосный обратный вызов 'ServiceBehavior (InstanceContextMode = InstanceContextMode. PerCai 1) ] class MyService : IMyContract j static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void MyMethodO IMyContractCallback callback = Operationcontext.Current. GetCallbackChannel<IMyContractCallback>(); if(m_Callbacks.Contains(calIback) == false) m_Cal1 backs.Add(calIback); ( public static void Cal 1C1 ients() Action<IMyContractCallback> invoke = delegate(IMyContractCa11 back cal 1 back) { using(TransactionScope scope = new TransactionScopeO) { callback.OnCallbackO; scope.Complete(); } }: m Callbacks.ForEach(invoke); //Внеполосные обратные вызовы: MyService. Cal 1 Cl i ent s С); Транзакционные обратные вызовы служб Второй вариант — тщательная настройка конфигурации транзакционной служ- бы, чтобы она могла обратиться с обратным вызовом к вызывающему клиенту. Для этого службе назначается режим ConcurrencyMode.Reentrant, свойство ReLe- aseServicelnstanceOnTransactionComplete задается равным false, а по крайней мере у одной операции свойство TransactionScopeRequired задается равным true, как показано в листинге 7.27. Листинг 7.27. Настройка службы для транзакционных обратных вызовов [ServiceContract (Са 11 backContract = typeof (IMyContractCalIback))] interface IMyContract i [Operationcontract] [Transact!onFlow(TransactionFlowOption.Al lowed)] void MyMethod(...): 1 продолжение &
320 Глава 7. Транзакции Interface IMyContractCalIback { [OperationContract] [TransactlonFlow(TransactlonFlowOptlon.Allowed)] void OnCallbackO; } [Serv1ceBehav1or(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Reentrant. ReleaseServicelnstanceOnTransactionComplete - false)] class MyService : IMyContract { [Operat1onBehav1or(TransactionScopeRequired - true)] public void MyMethodO..) { Trace.WriteLineC“Service ID: " + Transaction.Current.TransactioninformatIon.Distributedldentifier): IMyContractCalIback callback = Operationcontext.Current.GetCal1backChannel<IMyContractCallback>(): callback.OnCallbackO: } } Обоснования этого ограничения будут объяснены в следующей главе. Итак, для определений из листинга 7.27 и включенного на уровне привязки распространения транзакций следующий клиентский код: class MyCllent : IMyContractCalIback { [OperationBehav1or(Transact1onScopeRequ1red = true)] public void OnCallbackO { Trace.Wr1teL1ne("0nCalIback ID: " + Transaction.Current.Transactioninformation.Distributedldentifier): } } MyCHent client = new MyCllentO: InstanceContext context = new InstanceContext(cllent); MyContractCllent proxy = new MyContractCllent(context): usingCTransactionScope scope = new TransactlonScopeO) { proxy. MyMethodO: Trace.WriteLlneC'Cllent ID: “ + Transaction.Current.Transactioninformation.Distributedldentifier): scope.CompleteC); } proxy.Close(): выдает следующий результат: Service ID: 23627e82-507a-45d5-933c-05e5e5alae78 OnCalIback ID: 23627e82-507a-45d5-933c-05e5e5alae78 Client ID: 23627e82-507a-45d5-933c-05e5e5alae78 Это означает, что клиентская транзакция была распространена к службе, а потом возвращена при обратном вызове.
Обратный вызов 321 Конечно, присваивание ReleaseServicelnstanceOnTransactionComplete значения false означает, что WCF не уничтожит экземпляр после завершения транзак- ции. Лучшее решение проблемы — отдавать предпочтение службам уровня вы- зова перед транзакционными обратными вызовами (как в листинге 7.27), пото- му что они все равно уничтожаются после возврата управления методом, а их программная модель с контролем состояния независима от ReleaseServicelnstan- ceOnTransactionCompLete. При использовании службы сеансового уровня необходимо следовать при- веденным ранее рекомендациям, относившимся к управлению состоянием сеан- совой службы при ложном значении ReleaseServicelnstanceOnTransactionComplete — а именно программированию с контролем состояния и использованию VRM.
8 Управление параллельной обработкой Входящие клиентские вызовы передаются службе в программных потоках из пула. Так как клиенты могут обращаться с несколькими параллельными вызо- вами, сама служба должна быть готова к работе в условиях многопоточности. Если вызовы передаются одному и тому же экземпляру службы, вы должны предоставить синхронизированный доступ к состоянию службы в памяти, что- бы предотвратить возможное повреждение состояния. Это относится и к со- стоянию клиента в памяти во время обратных вызовов, потому что обратные вызовы тоже передаются по программным потокам из пула потоков. Кроме синхронизации доступа к состоянию экземпляра (там, где это необходимо), все службы также должны синхронизировать доступ к ресурсам, совместно исполь- зуемым разными транзакциями (например, статическим переменным или эле- ментам пользовательского интерфейса). Принципиально иной круг задач при управлении параллельной обработкой связан с проверкой того, что служба (или ресурсы, к которой она обращается) работает с определенными программ- ными потоками. WCF предоставляет в распоряжение разработчика два режима синхрониза- ции. Режим автоматической синхронизации поручает синхронизацию доступа к экземпляру службы WCF. Автоматическая синхронизация проста и удобна, но она доступна только для классов служб и обратного вызова. Ручная синхро- низация возлагает всю ответственность на разработчика и требует интеграции на уровне приложений. Разработчик должен применять синхронизационные блокировки .NET, а эта область рассчитана только на экспертов. Впрочем, у ручной синхронизации есть и свои преимущества: она доступна как для клас- сов служб, так и для других классов и позволяет разработчику выполнять опти- мизацию производительности и масштабируемости. Глава открывается описа- нием основных режимов параллельной обработки, после чего переходит к более сложным темам — таким, как безопасность ресурсов, потоковая привязка и пользовательский контекст синхронизации, обратный вызов и асинхронные вызовы. В ней также представлены полезные приемы и рекомендации по проек- тированию схем параллельной обработки.
Режим параллельной обработки службы 323 (правление экземплярами шараллельная обработка Потоковая безопасность экземпляра службы тесно связана с режимом создания ее экземпляров. Службы уровня вызова обладают потоковой безопасностью по определению, потому что каждому вызову предоставляется свой специализиро- ванный экземпляр. Этот экземпляр доступен только для указанного рабочего потока, и необходимость в синхронизации отсутствует, потому что другие пото- ки с ним работать все равно не будут. С другой стороны, в службах уровня вызова обычно используется контроль состояния (state-aware). К хранили- щу состояния возможен многопоточный доступ, потому что служба может под- держивать обработку параллельных вызовов. Соответственно, вам придется синхронизировать доступ к хранилищу состояния. Службы сеансового уровня требуют управления параллельностью и синхро- низации. Дело в том, что клиент может через одного посредника передавать вы- зовы службе от разных программных потоков на стороне клиента. Синглетная служба еще в большей степени подвержена параллельному доступу и нуждает- ся в его синхронизации. Синглетная служба обладает состоянием, которое хра- нится в памяти и совместно используется всеми клиентами. Кроме возможно- сти диспетчеризации вызовов по нескольким программным потокам, как и в слу- чае с сеансовыми службами, синглет может просто обслуживать нескольких клиентов в разных контекстах выполнения, при этом каждый клиент вызывает службу в своем программном потоке. Все эти вызовы поступают к синглетной службе по разным программным потокам из пула, отсюда и необходимость в синхронизации. Щким параллельной обработки службы Параллельный доступ к экземпляру службы определяется свойством Concur- rencyMode атрибута ServiceBehavior: public enum ConcurrencyMode I Single. Reentrant. Multiple ) [Attributeusage(Attri buteTargets. Cl ass) ] public sealed class ServiceBehaviorAttribute : ... I public ConcurrencyMode ConcurrencyMode {get: set:) II... Элементы перечисления ConcurrencyMode определяют, разрешены ли парал- лельные вызовы экземпляра службы, и если разрешены — когда именно.
324 Глава 8. Управление параллельной обработкой ConcurrencyMode.Single Если служба настроена в режиме ConcurrencyMode.Single, WCF обеспечивает ав- томатическую синхронизацию экземпляра службы и запрещает параллельные вызовы, связывая экземпляр службы с синхронизационной блокировкой. Каж- дый вызов, поступающий к службе, должен сначала попытаться получить блоки- ровку. Если блокировка свободна, вызывающая сторона захватывает ее и получа- ет доступ. После возврата управления операцией WCF снимает блокировку, разрешая доступ другой вызывающей стороне. Здесь важно то, что в любой мо- мент времени служба доступна только для одного вызова. Если во время уста- новления блокировки поступает несколько параллельных вызовов, они поме- щаются в очередь и обслуживаются по порядку. Если во время блокировки происходит тайм-аут, WCF исключает вызов из очереди, а клиент получает ис- ключение Timeout.Exception. Режим ConcurrencyMode.Single используется в WCF по умолчанию, поэтому следующие определения эквивалентны: [ServiceBehaviorCInstanceContextMode - InstanceContextMode.PerCai 1)] class MyService : IMyContract {...} [ServiceBehaviorUnstanceContextMode - InstanceContextMode.PerCall. ConcurrencyMode = ConcurrencyMode.Single)] class MyService : IMyContract {...} Так как режим синхронизации доступа используется по умолчанию, сеансо- вый и синглетный режимы активизации элементов также синхронизируются по умолчанию: [ServiceContract(SessionMode = SessionMode.Required)] Interface IMyContract // Сеансовые службы обладают потоковой безопасностью class MyServicel : IMyContract {...} [ServiceBehavior(InstanceContextMode - InstanceContextMode.PerSession)] class MyService2 : IMyContract {...} [Servi ceBehavi or(InstanceContextMode - InstanceContextMode.Single)] class MySingleton : IMyContract {...} ПРИМЕЧАНИЕ---------------------------------------------------------------------- Для сеансовых и синглетных служб операция должна выполняться быстро, чтобы клиенты не блоки- ровались на продолжительное время. Так как доступ к экземпляру службы синхронизируется, если выполнение операции займет много времени, это повышает риск тайм-аута необработанных вызовов. Синхронизация доступа и транзакции Как объяснялось в главе 7, WCF во время загрузки службы убеждается в том, что если хотя бы у одной операции службы свойство TransactionScopeRequired
Режим параллельной обработки службы 325 задано равным true, а свойство ReleaseServicelnstanceOnTransactionComplete тоже равно true, то для службы выбран режим ConcurrencyMode.Single. Это сделано на- меренно, чтобы экземпляр службы был гарантированно уничтожен в конце транзакции и другой программный поток не смог обратиться к нему. ConcurrencyMode.Multiple Если служба настроена в режиме ConcurrencyMode.Mutiple, WCF не вмешивается в происходящее и не синхронизирует доступ к экземпляру службы. Значение ConcurrencyMode.Mutiple означает, что экземпляр службы не связан с синхрони- зационной блокировкой и поэтому может получать параллельные вызовы. Дру- гими словами, если экземпляр службы настроен в режиме ConcurrencyMode.Mutiple, WCF не ставит клиентские сообщения в очередь и передает их экземпляру службы сразу же после поступления. ПРИМЕЧАНИЕ------------------------------------------------------------ Большое количество параллельных обращений со стороны клиента не приведет к соответствующе- му количеству параллельно обрабатываемых службой вызовов. Максимальное количество парал- лельных вызовов зависит от настройки параметров регулирования нагрузки. Как упоминалось в гла- ве! по умолчанию максимальное количество параллельно обрабатываемых вызовов равно 16. Разумеется, это обстоятельство критично для сеансовых и синглетных служб, но иногда оно также относится и к службам уровня вызова (см. далее). Такие службы должны вручную синхронизировать доступ к своему состоянию. Чаще всего для этого применяются блокировки .NET — такие как классы, про- изводные от Monitor и WaitHandle. Должен предупредить, что ручная синхрони- зация - занятие не для слабонервных. С другой стороны, она позволяет разра- ботчику службы оптимизировать пропускную способность экземпляра службы по количеству клиентских вызовов, потому что экземпляр службы можно бло- кировать только там и тогда, когда возникнет действительная необходимость в синхронизации, предоставляя другим клиентским вызовам доступ к экземп- ляру между синхронизированными секциями. Служба с ручной синхронизаци- ей представлена в листинге 8.1. Листинг 8.1. Ручная синхронизация с использованием фрагментарной блокировки [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract I void MyMethodt); 1 [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract I int[] m_Numbers; List<string> m_Names; public void MyMethodO ( lock(mNumbers) { продолжение &
326 Глава 8. Управление параллельной обработкой } /* Здесь обращения к переменным класса отсутствуют */ lock(mNames) { } } } Служба в листинге 8.1 настроена для параллельного доступа. Поскольку критическими секциями операций, требующими синхронизации, является об- ращение к любым переменным класса, служба использует объект Monitor (ин- капсулированный в команде lock) для блокировки объекта перед обращением. Локальные переменные не требуют синхронизации, потому что они доступны только для программного потока, создавшего их в своем стеке. Недостаток ме- тодики, показанной в листинге 8.1, заключается в том, что она подвержена ошибкам и взаимным блокировкам. Потоково-безопасный доступ обеспечива- ется только в том случае, если все остальные операции службы будут достаточ- но «дисциплинированы» и всегда будут блокировать переменные класса перед обращением к ним. Но даже если все операции будут устанавливать все необхо- димые блокировки, все равно остается риск взаимных блокировок — если одна операция в потоке А заблокирует переменную Ml, пытаясь обратиться к пере- менной М2, а другая параллельно выполняемая операция в потоке В заблокиру- ет переменную М2, пытаясь обратиться к Ml, возникнет взаимная блокировка. ПРИМЕЧАНИЕ--------------------------------------------------------------------- Для решения проблемы взаимных блокировок в WCF используется тайм-аут: слишком долгие вызо- вы отменяются с выдачей исключения TimeoutException. Избегайте слишком долгих интервалов тайм-аута, потому что это ограничивает возможности WCF по своевременному разрешению взаим- ных блокировок. По этой причине я рекомендую избегать фрагментарных блокировок. Лучше блокировать весь экземпляр службы: public void MyMethodO { lock(this) { } /* Здесь обращения к переменным класса отсутствуют */ lock(this) { } } Но и такое решение чревато ошибками — если когда-нибудь в будущем кто-нибудь включит в несинхронизированную секцию вызов метода, в котором
Режим параллельной обработки службы 327 используются обращения к переменным класса, синхронизация доступа нару- шается. Лучше блокировать все тело метода: public void MyMethodO j lock(this) { } ) Вы даже можете приказать .NET автоматически внедрять вызов блокировки экземпляра — для этого используется атрибут Methodlmpl с флагом Methodlmpl- Options.Synchronized: [Servi ceBeha vi or (ConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract I int[] m_Numbers: List<string> m_Names: [Methodlmpl(Methodlmpl Opt ions.Synchronized)] public void MyMethodO ) Присваивание атрибута Methodlmpl должно быть продублировано во всех реализациях операций служб. Но теперь возникает новая проблема: хотя код стал потоково-безопасным, практическая польза от ConcurrencyMode.Multiple невелика — общий эффект в от- ношении синхронизации близок к использованию ConcurrencyMode.Single, но сложность кода и его зависимость от дисциплинированности разработчика воз- росли. Впрочем, в некоторых случаях такая конфигурация неизбежна, особенно при использовании обратных вызовов (см. далее). Несинхронизированный доступ и транзакции Если служба настроена в режиме ConcurrencyMode.Multiple и хотя бы у одной операции свойство TransactionScopeRequired равно true, то свойство ReleaseSer- vicelnstanceOnTransactionComplete должно быть ложным. Например, следующее определение является допустимым, потому что ни у одного метода свойство TransactionScopeRequired не равно true, хотя свойство ReleaseServicelnstanceOnTrans- actionComplete истинно по умолчанию: [ServiceBehavior (ConcurrencyMode = ConcurrencyMode.Multiple)] class MyService : IMyContract public void MyMethodO (...) public void MyOtherMethodO
328 Глава 8. Управление параллельной обработкой Напротив, следующее определение недопустимо, потому что у одного мето- да свойство TransactionScopeRequired истинно: // Недействительная конфигурация: [ServiceBehavlor(ConcurrencyMode e ConcurrencyMode.Multiple)] class MyService : IMyContract { [OperationBehavlordransactionScopeRequlred - true)] public void MyMethodO {...} public void MyOtherMethodO {...} } У транзакционных несинхронизированных служб свойству ReleaseServiceln- stanceOnTransactionComplete должно быть явно задано значение false: [Serv1ceBehav1or(ConcurrencyMode = ConcurrencyMode.Multiple. ReleaseServicelnstanceOnTransactionComplete - false)] class MyService : IMyContract { [OperatlonBehavlorCTransactlonScopeRequlred = true)] public void MyMethodO (...) public void MyOtherMethodO {...} } Данное ограничение объясняется тем, что несинхронизированный доступ приносит реальную пользу только сеансовым или синглетным службам, поэто- му в случае транзакционного доступа WCF стремится обеспечить семантику заданного режима активизации экземпляров. Кроме того, этим предотвращает- ся опасность того, что вызывающая сторона обратится к экземпляру, завершит транзакцию и освободит экземпляр в то время, пока он продолжает использо- ваться другой стороной. ConcurrencyMode. Reentrant Режим ConcurrencyMode.Reentrant представляет собой усовершенствованный ва- риант ConcurrencyMode.Single. Как и ConcurrencyMode.Single, режим Concurrency- Mode.Reentrant ассоциирует экземпляр службы с синхронизационной блокиров- кой, поэтому параллельные вызовы к одному экземпляру недопустимы. Но если реентерабельная служба вызывает другую службу или клиента обратного вызова и цепочка вызовов каким-то образом снова дойдет до экземпляра служ- бы, как показано на рис. 8.1, этому вызову разрешается повторный вход в эк- земпляр службы. Реализация ConcurrencyMode.Reentrant чрезвычайно проста — когда реентера- бельная служба осуществляет вызов через WCF, WCF освобождает синхрони- зационную блокировку, связанную с экземпляром. Режим ConcurrencyMode.Re- entrant создавался для предотвращения потенциальных взаимных блокировок при повторном входе. Если бы служба сохраняла блокировку при передачи управления, то при попытке повторного входа в тот же экземпляр возникала бы взаимная блокировка.
Режим параллельной обработки службы 329 Рис. 8.1. Повторный вход Поддержка повторного входа полезна в нескольких случаях: О внешние вызовы со стороны синглетной службы создают риск взаимной блокировки, если какая-либо из вызванных служб попытается обратиться с обратным вызовом к синглетной службе; О в границах одного прикладного домена, если клиент хранит ссылку на по- средника в глобально доступной статической переменной, то одна из вы- званных служб может использовать ссылку на посредника для обратного вызова исходной службы; О обратным вызовам по не-односторонним операциям должен быть разрешен повторный вход в вызывающую службу; О если внешний вызов имеет относительно большую продолжительность, вы можете попытаться оптимизировать производительность, разрешая другим клиентам использовать тот же экземпляр службы. Служба, настроенная в режиме ConcurrencyMode.Multiple, по определению ре- ентерабельна, потому что на время внешнего вызова блокировка не удержива- ется. Тем не менее, в отличие от режима ConcurrencyMode.Reentrant, изначально обладающего потоковой безопасностью, служба с режимом ConcurrencyMode. Multiple должна сама обеспечить свою синхронизацию — например, посредством блокировки экземпляра при каждом вызове, как объяснялось ранее. Разработ- чик службы сам должен решить, хочет ли он освободить блокировку экземпля- ра перед внешним вызовом, чтобы предотвратить взаимную блокировку при повторном входе. Проектирование с учетом реентерабельности Очень важно понять ответственность, связанную с повторным входом. Во-пер- вых, когда реентерабельная служба осуществляет внешний вызов, она должна остаться в работоспособном стабильном состоянии, потому что во время внеш- него вызова к экземпляру службы могут обратиться другие клиенты. Термин стабильное потоково-безопасное состояние» означает, что реентерабельная служба не имеет других взаимодействий со своими членами, другими локаль- ными объектами или статическими переменными, а при возврате из внешнего вызова реентерабельная служба может просто вернуть управление клиенту. Предположим, реентерабельная служба изменила состояние связанного списка и оставила его в нестабильном состоянии (например, без головного элемента) из-за того, что новое значение узла должно быть получено от другой службы.
330 Глава 8. Управление параллельной обработкой Затем реентерабельная служба обращается к другой службе, но теперь ее кли- енты оказываются в уязвимом положении — если они вызовут службу и обра- тятся к связанному списку, произойдет ошибка. По тем же соображениям при возврате из внешнего вызова реентерабельная служба должна обновить все локальное состояние метода. Например, если служ- ба содержит локальную переменную, в которой хранится копия состояния пе- ременной класса, эта локальная переменная может содержать неверное значе- ние, потому что во время внешнего вызова другая сторона могла обратиться к реентерабельной службе и изменить переменную класса. Реентерабельность и транзакции Для реентерабельных служб устанавливаются точно такие же ограничения, относящиеся к транзакциям, как и для служб, настроенных в режиме Concur- rencyMode.Multiple; а именно, если по крайней мере у одной операции свойство TransactionScopeRequired задано равным true, то свойство ReleaseServicelnstance- OnTransactionComplete должно быть равно false. Обратные вызовы и реентерабельность Обратный вызов — главная причина для поддержки реентерабельности. Как объяснялось в главе 5, если служба WCF захочет обратиться к вызывающему клиенту с дуплексным обратным вызовом, она должна быть реентерабельной (или синхронизация должна отсутствовать в режиме ConcurrencyMode.Multiple). Дело в том, что для обработки ответного сообщения от клиента после возврата из обратного вызова требуется блокировка экземпляра; следовательно, если разрешить службе с режимом ConcurrencyMode.Single обращаться к клиентам с обратными вызовами, произойдет взаимная блокировка. Чтобы обратные вы- зовы стали возможными, служба должна быть настроена в режиме Concurrency- Mode.Multiple или желательно — в режиме ConcurrencyMode.Reentrant. Это требо- вание должно выполняться даже службами уровня вызова, которые в остальных отношениях не требуют ничего, кроме ConcurrencyMode.Single. Обратите внима- ние: служба может обращаться с обратными вызовами к другим клиентам или вызывать другие службы. Запрещены только обратные вызовы к вызывающему клиенту. Если служба, настроенная в режиме ConcurrencyMode.Single, попытается акти- визировать дуплексный обратный вызов, WCF инициирует исключение Inva- lidOperationException. Пример настройки реентерабельной службы приведен в листинге 8.2. Во время выполнения операции служба обращается к клиенту с обратным вызовом. Управление вернется к службе только после возврата из обратного вызова, и программный поток самой службы должен заново устано- вить блокировку. Листинг 8.2. Настройка реентерабельности для выполнения обратных вызовов [ServiceContract(Cal 1backContract - typeof(IMyContractCal1 back))] interface IMyContract { [Operationcontract] void DoSomething();
Экземпляры и параллельный доступ 331 ) interface IMyContractCalIback I [OperationContract] void OnCallbackO: ) [ServiceBehavior (ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract public void DoSomethingO ( IMyContractCalIback callback = Operationcontext.Current. GetCal1backChannel<IMyContractCal 1 back>(); callback.OnCalIback(): ) 1 Как упоминалось в главе 5, служба, настроенная в режиме ConcurrencyMode. Single, может обращаться к клиентам с обратным вызовом только в одном слу- чае - если контрактная операция обратного вызова настроена как односторон- няя (чтобы не было ответных сообщений, претендующих на получение блоки- ровки). Экземпляры и параллельный доступ Через одного посредника клиент может передать службе несколько параллель- ных вызовов. Для этого он может использовать несколько программных пото- ков или же выдавать односторонние вызовы в быстрой последовательности. В обоих случаях параллельная обработка вызовов от одного клиента зависит от настроенного режима активизации экземпляров службы, режима параллельной обработки и режима доставки, то есть типа привязки и сеансового режима. Службы уровня вызова Службы уровня вызова не обладают сеансом транспортного уровня; иначе гово- ря, если вызов осуществляется через BasicHttpBinding или любую из привязок группы WS, а контракт настроен в режиме SessionMode.NotAllowed или в режиме SessionMode.Allowed без надежной доставки сообщений и безопасности, разреша- ется параллельная обработка вызовов. Вызовы доставляются по мере поступле- ния новым экземплярам и выполняются параллельно. Это правило работает не- зависимо от режима параллельной обработки службы. На мой взгляд, такое поведение было выбрано правильно. Если служба уровня вызова поддерживает сеанс транспортного уровня (то есть имеет привязку TCP или IPC) или привязку WS, когда у контракта свойству SessionMode задано значение SessionMode.Allowed с безопасностью или надежной доставкой или SessionMode.Required, — параллельная обработка вызовов опреде- ляется настройкой ConcurrencyMode службы. Если служба настроена в режиме ConcurrencyMode.Single, параллельная обработка входящих вызовов запрещена. Хотя данная схема является непосредственным результатом канальной архи-
332 Глава 8. Управление параллельной обработкой тектуры, я считаю ее несовершенной, потому что блокировка должна связы- ваться с экземпляром, а не с типом. Если служба настроена в режиме Concur- rencyMode.Multiple, параллельная обработка разрешена. Вызовы передаются по мере поступления (каждый — новому экземпляру) и обрабатываются парал- лельно. Если служба настроена в режиме ConcurrencyMode.Reentrant, то при от- сутствии внешних вызовов она ведет себя аналогично ConcurrencyMode.Single. Если служба выполняет внешние вызовы, то она принимает следующий вызов, а возвратный вызов должен претендовать на получение блокировки, как и все остальные необработанные вызовы. Сеансовые и синглетные службы У сеансовых и синглетных служб параллельное выполнение необработанных вызовов зависит только от настроенного режима параллельной обработки. Если служба настроена в режиме ConcurrencyMode.Single, вызовы последовательно пе- редаются экземпляру службы. Необработанные вызовы помещаются в очередь. Избегайте длительной обработки вызовов, потому что она создает опасность тайм-аута. Если экземпляр службы настроен в режиме ConcurrencyMode.Mutiple, то па- раллельная обработка вызовов разрешена. Вызовы будут обрабатываться эк- земпляром службы сразу же после их поступления из канала (в количестве, ус- тановленном в параметрах регулировки нагрузки). Конечно, как это всегда бывает для экземпляров служб с состоянием, доступ к экземпляру службы должен быть синхронизирован; в противном случае возникает риск порчи со- стояния. Если экземпляр службы настроен в режиме ConcurrencyMode.Reentrant, он ве- дет себя почти так же, как в режиме ConcurrencyMode.Single. Но если служба вы- полняет внешний вызов, разрешается выполнение следующего вызова (одно- стороннего или нет). Рекомендации по программированию в реентерабельных службах приводились ранее. ПРИМЕЧАНИЕ --------------------------------------------------------------- Чтобы сеансовые службы, настроенные в режиме ConcurrencyMode. Mutiple, получали параллельные вызовы, клиент должен использовать несколько рабочих программных потоков для обращения код- ному экземпляру посредника. Но если клиентские потоки используют функцию автоматического от- крытия посредника (то есть они просто вызывают метод, а вызов открывает посредника, если он еще не открыт), то вызовы ставятся в очередь до открытия посредника и становятся параллельны- ми после него. Если вы хотите, чтобы доставка параллельных вызовов не зависела от состояния по- средника, клиент должен явно открыть посредника (вызовом метода Ореп()), прежде чем выдавать какие-либо вызовы в рабочих потоках. Ресурсы и службы Синхронизация доступа к экземпляру службы с использованием Concurrency- Mode.Single или явной синхронизационной блокировки только управляет па- раллельным доступом к состоянию экземпляра службы. Она не обеспечивает безопасного доступа к ресурсам, которые могут быть задействованы в транзак-
Ресурсы и службы 333 ции. Такие ресурсы тоже должны быть потоково-безопасными. Для примера рассмотрим приложение, показанное на рис. 8.2. Рис. 8.2. Приложения должны синхронизировать доступ к ресурсам Даже несмотря на потоковую безопасность экземпляров, два экземпляра мо- гут параллельно обратиться к одному ресурсу (статической переменной, вспо- могательному статическому классу или файлу), поэтому доступ к самому ре- сурсу тоже должен быть синхронизирован. Это положение истинно независимо от режима активизации экземпляров службы. Даже служба уровня вызова мо- жет попасть в ситуацию, показанную на рис. 8.2. Взаимная блокировка при доступе к ресурсам Наивный способ обеспечения потоково-безопасного доступа к ресурсам — пре- доставить каждому ресурсу собственную блокировку (возможно, инкапсулиро- ванную в самом ресурсе), требовать у ресурса устанавливать блокировку при обращении и снимать ее при завершении работы с ресурсом. Недостаток такого решения заключается в том, что оно подвержено взаимным блокировкам. Рас- смотрим ситуацию на рис. 8.3. Рис. 8.3. Взаимная блокировка при доступе к ресурсам На рисунке экземпляр службы А пытается обратиться к потоково-безопас- ному ресурсу А. Ресурс А обладает собственной синхронизационной блокиров- кой, и экземпляр А получает эту блокировку. Аналогично, экземпляр В обраща- ется к ресурсу В и получает его блокировку. Взаимная блокировка происходит тогда, когда экземпляр А пытается обратиться к ресурсу В, а экземпляр В — к ресурсу А, поскольку каждый экземпляр будет ожидать, когда другой освобо- дит его блокировку.
334 Г лава 8. Управление параллельной обработкой Параллельность и режим активизации экземпляров службы практически не способны предотвратить взаимную блокировку. Блокировка не возникает толь- ко в одном случае: если служба настроена в режимах InstanceContextMode.Single и ConcurrencyMode.Single, потому что синхронизированный синглетный экземп- ляр по определению в произвольный момент времени может иметь только одно- го клиента, а другого экземпляра, создающего взаимную блокировку за доступ к ресурсу, попросту нет. Например, синхронизированная служба сеансового уров- ня может иметь два разных потоково-безопасных экземпляра, связанных с дву- мя разными клиентами, но при этом два экземпляра будут создавать взаимную блокировку при обращении к ресурсам. Предотвращение взаимной блокировки Взаимную блокировку можно предотвратить несколькими способами. Если все экземпляры службы будут педантично обращаться к ресурсам в одном порядке (например, сначала захватывать блокировку ресурса А и только потом - ре- сурса В), взаимная блокировка не возникнет. Недостаток такого решения за- ключается в том, что обеспечить единый порядок обращения к ресурсам на про- тяжении жизненного цикла службы будет очень сложно. Если когда-нибудь в процессе сопровождения кода кто-нибудь отклонится от строгих требований (даже непреднамеренным вызовом методов вспомогательных классов), это при- ведет к взаимной блокировке. Другое решение — заставить все ресурсы совместно использовать одну об- щую блокировку. Чтобы свести к минимуму риск взаимной блокировки, жела- тельно уменьшить количество блокировок в системе и заставить саму службу использовать ту же блокировку. Для этого можно настроить саму службу в ре- жиме ConcurrencyMode.Multiple (даже для служб уровня вызова), чтобы избежать использования блокировок WCF. Первый экземпляр службы, получивший об- щую блокировку, перекрывает доступ другим экземплярам и становится вла- дельцем всех используемых ресурсов. Простым способом реализации таких блокировок является блокировка по типу службы, показанная в листинге 8.3. Листинг 8.3. Использование типа службы для реализации общей блокировки [ServiceBehavlor(InstanceContextMode = InstanceContextMode.PerCall. ConcurrencyMode - ConcurrencyMode.Multiple)] class MyService : IMyContract { public void MyMethodO { 1ock(typeof(MyService)) { MyResou rce.DoWork(); } } static class MyResource { •
Контекст синхронизации ресурсов 335 public static void DoWorkO lock(typeof(MyService)) { Сами ресурсы также должны блокироваться по типу службы (или другому заранее согласованному общему типу). Такой способ использования общих блокировок создает две проблемы. Во-первых, он создает логическую привязку между ресурсами и службой, потому что разработчик ресурса должен распола- гать информацией о типе службы или типе, используемом для синхронизации. Хотя логическую привязку можно свести к минимуму, передавая тип в пара- метре при конструировании ресурса, скорее всего, для ресурсов, предоставлен- ных третьими сторонами, этот способ не подойдет. Во-вторых, во время выпол- нения экземпляра службы все остальные экземпляры (и их клиенты) будут заблокированы. Ради обеспечения нормальной производительности и време- ни отклика при использовании общих блокировок следует избегать долгих операций. ВНИМАНИЕ------------------------------------------------------------------------ Службы не должны совместно использовать ресурсы. Независимо от управления параллельной об- работкой, ресурсы представляют собой локальные подробности реализации, а следовательно, не должны совместно использоваться службами. Что еще важнее, совместный доступ к ресурсам, пере- ходящий границу службы, создает опасность взаимной блокировки. У общих ресурсов нет механиз- ма совместного использования блокировок, подходящего для разных технологий и организаций, а службы должны каким-то образом координировать порядок блокировки. Все это создает высокую степень логической привязки между службами и нарушает правила и каноны служебно-ориентиро- ванного программирования. Контекст синхронизации ресурсов Входящие вызовы служб выполняются в рабочих программных потоках. Про- граммные потоки находятся под управлением WCF и никак не связаны с пото- ками служб или ресурсов. Это означает, что по умолчанию служба не может за- висеть ни от каких потоковых привязок, то есть полагаться на обращения со стороны только одного потока. Аналогично, служба по умолчанию не может по- лагаться на выполнение в некотором программном потоке на стороне хоста, созданном хостом или разработчиком службы. Но при этом возникает пробле- ма: некоторые ресурсы могут зависеть от потоковой привязки. Скажем, обраще- ние к ресурсам пользовательского интерфейса, обновляемым службой, должно выполняться только программными потоками интерфейса. Другой пример — ресурс (или служба), использующий локальное хранилище потока (TLS, Thread Local Storage) для хранения внеполосной информации, глобально используе- мой всеми сторонами в одном потоке. Работа с TLS требует обязательного ис- пользования того же потока. Вдобавок, в целях оптимизации масштабируемости
336 Глава 8. Управление параллельной обработкой и производительности, некоторые ресурсы могут требовать обязательного обра- щения со стороны потоков, принадлежащих к их собственным пулам. Всюду, где предполагается жесткая связь с определенным потоком или по- токами, служба не может просто обработать вызов во входящем рабочем потоке WCF. Вместо этого служба должна перенаправить вызов программному потоку (или потокам) согласно требованиям ресурса, которому адресован вызов. Синхронизационные контексты .NET 2.0 В .NET 2.0 появилась концепция синхронизационного контекста. Суть в том, что любая сторона может предоставить контекст выполнения и заставить дру- гие заинтересованные стороны перенаправлять вызовы в этот контекст. Син- хронизационный контекст состоит из любого количества программных пото- ков, хотя чаще используется именно один конкретный поток. Синхронизацион- ный контекст всего лишь обеспечивает выполнение вызова в правильном потоке или потоках. Обратите внимание на перегрузку термина «контекст». Синхро- низационные контексты не имеют ничего общего с контекстами экземпляров служб или контекстом операций, описанных ранее в книге. Хотя на концептуальном уровне идиома синхронизационных контекстов достаточно проста, их реализация превращается в сложную задачу, не рассчи- танную на рядового разработчика. Класс Synchronizationcontext Синхронизационный контекст представляется классом Synchronizationcontext из пространства имен System.Threading: public delegate void SendOrPostCallback(object state): public class Synchronizationcontext { public virtual void Post(SendOrPostCal1 back call back.object state): public virtual void Send(SendOrPostCalIback callback.object state); public static void SetSynchron1zat1onContext(Synchron1zat1onContext context): public static Synchronizationcontext Current {get:} //... } С каждым программным потоком в .NET 2.0 может быть связан синхрониза- ционный контекст. Чтобы получить синхронизационный контекст потока, сле- дует обратиться к статическому свойству Current класса Synchronizationcontext. Если поток не имеет синхронизационного контекста, то Current вернет null Ссылка на синхронизационный контекст также может передаваться между по- токами, чтобы поток мог перенаправить вызов другому потоку. Для представления вызова, активизируемого в синхронизационном контек- сте, метод инкапсулируется в делегате типа SendOrPostCallback. Обратите внима- ние на использование аморфного типа object в сигнатуре делегата. Если потре- буется передать несколько параметров, упакуйте их в структуру и передайте ее в формате object.
Контекст синхронизации ресурсов 337 ВНИМАНИЕ-------------------------------------------------------------------- Синхронизационные контексты работают с аморфным типом object. Работая с синхронизационными контекстами, будьте чрезвычайно осторожны, так как безопасность типов на стадии компиляции от- сутствует. Работа с синхронизационным контекстом Существует два режима доставки вызовов синхронизационным контекстам: синхронный (Send()) и асинхронный (Post()). Метод Send() блокирует вызываю- щую сторону до завершения вызова в другом синхронизационном контексте, тогда как метод Post() просто передает вызов синхронизационному контексту и возвращает управление вызывающей стороне. Например, чтобы асинхронно доставить вызов некоторому синхронизацион- ному контексту, вы сначала получаете ссылку на контекст, а затем вызываете метод Send(): II Получение синхронизационного контекста Synchronizationcontext context = ... SendOrPostCal 1 back doWork = del egate (object arg) { // Находящийся здесь код гарантированно // выполняется в нужном потоке(-ах) }: context.Send(doWork."Some argument"): Менее абстрактный пример приведен в листинге 8.4. Листинг 8.4. Обращение к ресурсу в нужном синхронизационном контексте class MyResource ( public Int DoWorkO {...} public Synchronizationcontext MySynchronlzationContext {get;} ) class MyService : IMyContract ( MyResource GetResource() {...} public void MyMethodO ( MyResource resource = GetResource(); Synchronizationcontext context = resource.MySynchronlzationContext: Int result = 0; SendOrPostCalIback doWork = delegate { result = resource. DoWorkO: context.Send(doWork.null): ) I
338 Глава 8. Управление параллельной обработкой В этом примере служба MyService взаимодействует с ресурсом MyResource, ко- торый выполняет для нее метод DoWork() и возвращает результат. Тем не менее MyResource требует, чтобы все вызовы к нему выполнялись в определенном син- хронизационном контексте. MyResource предоставляет доступ к этому контексту в свойстве MySynchronizationContext. Операция службы MyMethod() выполняется в рабочем потоке WCF. MyMethodO сначала получает ссылку на ресурс и его синхронизационный контекст, определяет анонимный метод, инкапсулирую- щий вызов DoWork(), и присваивает его делегату doWork типа SendOrPostCallback. Наконец, MyMethodO вызывает Send() с передачей аргумента null, потому что ме- тод DoWork() ресурса вызывается без параметров. Обратите внимание на прием, использованный в листинге 8.4 для получения возвращаемого значения. По- скольку Send() возвращает void, анонимный метод присваивает возвращаемое значение DoWorkO внешней переменной. Без анонимных методов для этого по- требовалось бы сложное использование синхронизированной переменной класса. Недостатком листинга 8.4 является излишне жесткая логическая связь службы с ресурсом. Службе необходимо знать, что ресурс чувствителен к син- хронизационному контексту, получить контекст и управлять выполнением. Го- раздо лучше инкапсулировать эту необходимость в самом контексте, как пока- зано в листинге 8.5. Листинг 8.5. Инкапсуляция синхронизационного контекста class MyResource { public Int DoWorkO { Int result = 0: SendOrPostCallback doWork = delegate { result = DoWorkInternal 0: }: MySynchron1zat1onContext.Send(doWork.null); return result; } SynchronizatlonContext MySynchronizationContext (get:} Int DoWorklnternal() {...} } [ServiceBehavior!InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { MyResource GetResource() (...) public void MyMethodO { MyResource resource = GetResourceO; Int result = resource.DoWorkO:
Контекст синхронизации ресурсов 339 Сравните листинг 8.5 с листингом 8.4. В листинге 8.5 служба всего лишь об- ращается к ресурсу, и ничего более. Внутренняя реализация службы сама пере- дает вызов синхронизационному контексту. Синхронизация контекста пользовательского интерфейса (UI) Канонический пример использования синхронизационных контекстов встреча- ется в UI-библиотеках Windows — Windows Forms и WPF (Windows Presen- tation Foundation). Для простоты в оставшейся части этой главы будет упоми- наться только библиотека Windows Forms, хотя все сказанное в равной степени относится к WPF. Работа U 1-приложения Windows зависит от сообщений Win- dows и цикла их обработки. Цикл сообщений должен обладать потоковой при- вязкой, потому что сообщения окна доставляются только тому программному потоку, который его создал. В общем случае любые попытки обращения к эле- ментам управления и формам Windows всегда должны доставляться UI-потоку; в противном случае возникает риск ошибок и сбоев. Это создает проблемы, если вашим службам требуется обновить элемент пользовательского интерфей- са в результате клиентского вызова или какого-либо другого события. К сча- стью, Windows Forms поддерживает схему синхронизационных контекстов. Программный поток, доставляющий сообщения, обладает синхронизационным контекстом, представленным классом WindowsFormsSynchronizationContext: public sealed class WindowsFormsSynchromzationContext : Synchronizationcontext.... {...} Каждый раз, когда вы вызываете метод Application.Run() библиотеки Win- dows Forms для вызова главного окна приложения, метод не только начинает об- работку сообщений окон, но и устанавливает WindowsFormsSynchronizationContext в качестве синхронизационного контекста текущего потока. WindowsFormsSynchronizationContext преобразует вызов Send() или Post() в пользовательское сообщение Windows и отправляет его в очередь сообщений Ш-потока. Каждый UI-класс Windows Forms, производный от Control, содер- жит специальный метод, обрабатывающий пользовательское сообщение вызо- вом переданного делегата SendOrPostCallback. В какой-то момент пользователь- ское сообщение Windows обрабатывается UI-потоком, что приводит к вызову делегата. Поскольку окно или элемент управления могут быть изначально вызваны в правильном контексте синхронизации, для предотвращения взаимной блоки- ровки при вызове Send() реализация синхронизационного контекста Windows Forms проверяет, что перенаправление вызова действительно необходимо. Если оно не обязательно, используется прямая активизация в вызывающем потоке. Обновление элементов UI Когда службе требуется обновить некоторые элементы пользовательского ин- терфейса, она должна каким-то образом найти обновляемое окно. После того как окно будет найдено, служба должна как-то получить синхронизационный
340 Глава 8. Управление параллельной обработкой контекст окна и организовать перенаправление вызовов. Возможная схема взаимодействия представлена в листинге 8.6. Листинг 8.6. Использование синхронизационного контекста формы partial class MyForm : Form { Label m_CounterLabel: Synchronizat1onContext m_SynchronizationContext; public MyFormO { Initial izeComponentO; m_SynchronizationContext - SynchronizationContext.Current: Debug.Assert(m_SynchronizationContext != null); } public Synchronizationcontext MySynchronlzationContext { get ( return m_SynchronizationContext: } } public int Counter { get { return Convert.ToInt32(m_CounterLabel.Text): } set ( m_CounterLabel.Text - value.ToStringO; } } } [ServiceContract] interface IFormManager { [Operationcontract] void IncrementLabelO; } [ServiceBehaviorCInstanceContextMode - InstanceContextMode.PerCai 1)] class MyService : IFormManager { public void IncrementLabelO { MyForm form = Application.OpenForms[0] as MyForm; Debug.Assert(form !« null); SendOrPostCalIback callback - delegate { form.Counter++: }: form.MySynchronizationContext.Send(calIback,nul1);
Контекст синхронизации ресурсов 341 1 static class Program ( static void MainO ( ServiceHost host = new ServiceHost(typeof(MyServ1ce)): host. Open 0; Application.Run(new MyFormO): host.CloseO: 1 I В листинге 8.6 приведена форма MyForm со свойством MySynchronizationCon- text, позволяющим клиентам получать ее синхронизационный контекст. MyForm инициализирует MySynchronizationContext в конструкторе, получая синхронизационный контекст текущего программного потока. Поток обладает синхронизационным контекстом, потому что метод Main() вызвал Application. Run(), что привело к запуску цикла обработки сообщений. MyForm также содер- жит свойство Counter, обновляющее значение счетчика — надписи Windows Forms. Обращение к Counter должно производиться потоком, которому принад- лежит форма. Служба MyService реализует операцию IncrementLabel(). В этой операции служба получает ссылку на форму из статической коллекции Open- Forms класса Application: public class FormCollection : ReadOnlyCollectlonBase ( public virtual Form thls[1nt Index] (get:} public virtual Form thlsEstrlng name] (get;} I public sealed class Application ( public static FormCollection OpenForms (get;} //... 1 После того как IncrementLabel() получит форму для обновления, метод обра- щается к синхронизационному контексту через MySynchronizationContext и вызы- вает метод Send(). Последнему предоставляется анонимный метод, обновляю- щий Counter. В листинге 8.6 представлен конкретный пример программной модели, приведенной в листинге 8.4. Соответственно, решение в листинге 8.6 страдает от того же недостатка, а именно тесной логической связи между служ- бой и формой. Если службе потребуется обновить несколько элементов, про- граммная модель становится слишком громоздкой. Любые изменения в пользо- вательском интерфейсе, в элементах на форме и в требуемом поведении с большой вероятностью потребуют серьезных изменений в коде службы. Безопасные элементы Взаимодействие с синхронизационным контекстом Windows Forms лучше ин- капсулировать в безопасных элементах или безопасных методах формы, чтобы
342 Глава 8. Управление параллельной обработкой отделить его от службы и по возможности упростить программную модель. В листинге 8.7 приведен код SafeLabel — класса, производного от Label и предо- ставляющего потоково-безопасный доступ к своему свойству Text. Так как класс SafeLabel является производным от Label, все визуальные возможности стадии конструирования и интеграция с Visual Studio сохраняются; мы лишь «хирур- гическим способом» изменяем свойство, требующее безопасного доступа. Листинг 8.7. Инкапсуляция синхронизационного контекста public class SafeLabel : Label { Synchronizationcontext m_Synchron1zat1onContext = Synchronlzat1onContext.Current: override public string Text { set { SendOrPostCallback setText = delegate(object text) { base.Text = text as string; }; m_Synchron1zatlonContext.Send(setText.value): } get { string text = String.Empty: SendOrPostCallback getText = delegate { text = base.Text: }: m_SynchronizatlonContext.Send(getText,null): return text: } } } При конструировании SafeLabel сохраняет свой синхронизационный кон- текст. SafeLabel переопределяет свойство Text базового класса и использует ано- нимный метод в get/set для отправки вызова правильному UI-потоку. В блоке get обратите внимание на использование внешней переменной для возврата значения из Send(), о чем упоминалось ранее. При использовании SafeLabel код в листинге 8.6 сокращается до фрагмента, приведенного в листинге 8.8. Листинг 8.8. Использование потоково-безопасного элемента class MyForm : Form { Label m_CounterLabel: public MyFormO { Initial1zeComponent(): } void InitializeComponentO
Синхронизационный контекст службы 343 m_CounterLabel = new SafeLabelO: 1 public int Counter get return Convert.ToInt32(m_CounterLabel.Text); } set { m_CounterLabel .Text = value.ToStringO; ) } ) [ServiceBehaviordnstanceContextMode - InstanceContextMode.PerCai 1)] class MyService : IFormManager I public void IncrementLabelO ( MyForm form = Application.0penForms[0] as MyForm; Debug.Assert(form != null); form.Counter++; 1 1 В листинге 8.8 служба просто напрямую обращается к форме: form.Counter++; При этом форма выглядит как самая обычная форма. Листинг 8.8 является конкретным примером программной модели, приведенной в листинге 8.5. ПРИМЕЧАНИЕ---------------------------------------------------------------------------------- В сборке ServiceModelEx.dll из прилагаемого к книге кода содержится не только код SafeLabel, но и безопасные версии других часто используемых элементов — в частности, SafeButton, SafeListBox, SafeProgressBar, SafeStatusBar и SafeTextBox. Синхронизационный контекст службы В примерах, приводившихся ранее, бремя ответственности за обращение к ре- сурсу в правильном программном потоке возлагалось исключительно на разра- ботчика службы или ресурса. Было бы лучше, если бы служба могла связать себя с определенным синхронизационным контекстом, а среда WCF обнаружи- вала этот контекст и автоматически перенаправляла вызовы от рабочего потока синхронизационному контексту службы. К счастью, WCF предоставляет такую возможность. Вы можете приказать WCF поддерживать привязку между всеми экземплярами службы от определенного хоста и определенным синхронизаци- онным контекстом. Атрибут ServiceBehavior содержит логическое свойство Use- SynchronizationContext, которое определяется следующим образом:
344 Глава 8. Управление параллельной обработкой [AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : ... { public bool UseSynchronizationContext {get:set:} //... } Связь между типом службы, ее хостом и синхронизационным контекстом фиксируется при открытии хоста. Если поток, открывающий хост, обладает синхронизационным контекстом и свойство UseSynchronizationContext истинно, WCF устанавливает связь между синхронизационным контекстом и всеми эк- земплярами службы, находящимися под управлением хоста. WCF автоматиче- ски перенаправляет все входящие вызовы синхронизационному контексту службы. Вся потоково-специфическая информация, хранящаяся в TLS (напри- мер, транзакция клиента или данные безопасности — см. главу 10), будет пра- вильно передаваться синхронизационному контексту. Если свойство UseSynchronizationContext ложно, то независимо от синхрони- зационного контекста открывающего потока служба не будет связана ни с ка- ким синхронизационным контекстом. Аналогично, даже если свойство UseSyn- chronizationContext истинно, но открывающий поток не имеет синхронизационного контекста, его не будет и у службы. По умолчанию свойство UseSynchronizationContext истинно, поэтому следую- щие определения эквивалентны: [ServiceContract] interface IMyContract {...} class MyService : IMyContract {...} [ServiceBehavior(UseSynchronizationContext true)] class MyService : IMyContract {...} Хостинг в UI-потоке Классический пример использования UseSynchronizationContext — прямое обнов- ление службой элементов пользовательского интерфейса и окон, без приемов, продемонстрированных в листингах 8.6 и 8.7. Средства привязки всех экземп- ляров службы от определенного хоста к конкретному UI-потоку значительно упрощают обновление UI в WCF. Чтобы добиться желаемого, организуйте хос- тинг службы в UI-потоке, который также создает окна или элементы, с которыми служба должна взаимодействовать. Поскольку синхронизационный контекст Windows Forms устанавливается во время инициализации цикла обработки со- общений в Application.Run(), а эта операция является блокирующей, открытие хоста до или после Application.Run() бессмысленно — до Application.Run() син- хронизационного контекста еще нет, а после него приложение обычно заверша- ет работу. Простейшее решение — сделать так, чтобы окно (или форма), с кото- рым должна взаимодействовать служба, открывало хост перед загрузкой формы, как показано в листинге 8.9.
Синхронизационный контекст службы 345 Листинг 8.9. Организация хостинга службы формой [Servi ceBehavi ord nstanceContextMode = I nstanceContextMode. PerCa 11) ] class MyService : IMyContract (...) partial class HostForm : Form ( ServiceHost m_Host: public HostFormO ( Initial izeComponent(): m_Host = new ServiceHost(typeof(MyService)); mHost.OpenO; } void OnFormClosedtobject sender.EventArgs e) ( m_Host.Close(); ) I static class Program I static void MainO ( Application.Run(new HostFormO); } } Служба в листинге 8.9 по умолчанию использует синхронизационный кон- текст, обнаруженный ее хостом. Форма HostForm сохраняет ссылку на хост в пе- ременной, чтобы форма при закрытии могла закрыть службу. Конструктор HostForm уже обладает синхронизационным контекстом, связь с которым уста- навливается при открытии хоста. Обращение к форме Хотя хостинг службы в листинге 8.9 обеспечивается формой, экземпляры служ- бы должны обладать специфическим механизмом обращения к форме. Если эк- земпляр службы должен обновлять несколько форм, вы можете воспользовать- ся коллекциями Application.OpenForms (как в листинге 8.6) для поиска нужной формы. Получив форму, служба может свободно обращаться к ней напрямую (в отличие от листинга 8.6, где требовалось перенаправление): [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IFormManager ( public void IncrementLabel0 ( HostForm form = Application.0penForms[0] as HostForm; Debug.Assert(form != null); form.Counter++; ) )
346 Глава 8. Управление параллельной обработкой Ссылки на формы можно сохранять в статических переменных. Проблема такого подхода заключается в том, что если нескольким UI-потокам потребует- ся передавать сообщения разным экземплярам одного типа формы, вы не смо- жете использовать одну статическую переменную для каждого типа — потребу- ется отдельная переменная для каждого потока, а это значительно усложняет дело. Вместо этого форма (или формы) может сохранять ссылки на себя в TLS, а экземпляр службы — обращаться к хранилищу и получать ссылки. С другой стороны, работа с TLS весьма громоздка и небезопасна по отношению к типам. Усовершенствованная версия такого решения могла бы использовать статиче- ские переменные уровня потока. По умолчанию статические переменные «вид- ны» всем программным потокам в прикладном домене. При использовании ста- тических переменных уровня потока каждый прикладной поток в прикладном домене получает собственную копию статической переменной. Для пометки статических переменных уровня потока используется атрибут ThreadStaticAttri- bute. Статические переменные уровня потока всегда потоково-безопасны, пото- му что обращения к ним осуществляются только одним потоком, и каждый поток получает собственную копию статической переменной. Статические пе- ременные уровня потока хранятся в TLS, но при этом для работы с ними ис- пользуется упрощенная программная модель, безопасная по отношению к ти- пам. Пример представлен в листинге 8.10. Листинг 8.10. Хранение ссылки на форму в статической переменной уровня потока partial class HostForm : Form { Label m_CounterLabel: ServiceHost m_Host; [ThreadStatic] static HostForm m_CurrentForm: public static HostForm CurrentForm { get { return m_CurrentForm; } set { m_CurrentForm = value; } } public int Counter { get ( return Convert.ToInt32(m_CounterLabel.Text); } set {
Синхронизационный контекст службы 347 m_CounterLabel .Text = value.ToStringO: } ) public HostFormO ( Initial IzeComponentO: CurrentForm - this; m_Host = new ServiceHost(typeof(MyService)): m_Host.0pen(); void OnFormClosed(object sender.EventArgs e) ( m_Host.Close(): ) [ServiceContract] interface I FormManager I [OperationContract] void IncrementLabel0; 1 [Servi ceBehavl or (I nst anceContextMode = InstanceContextMode. PerCa ID] class MyService : I FormManager I public void IncrementLabel0 HostForm form = HostForm.CurrentForm: form.Counter++; I static class Program I static void MainO Appl i cat ion. Run (new HostFormO): ) ) Форма HostForm сохраняет ссылку на себя в статической переменной уровня потока с именем m_CurrentForm. Служба обращается к статическому свойству CurrentForm и получает ссылку на экземпляр HostForm в UI-потоке. Множественные UI-потоки Процесс хоста службы может иметь несколько UI-потоков, каждый из которых доставляет сообщения своей группе окон. Подобные конфигурации обычно ис- пользуются в приложениях с интенсивным использованием пользовательского интерфейса, когда совместное использование одного UI-потока для обслужива- ния нескольких окон и хостинга службы нежелательно, так как во время обра- ботки UI-потоком вызова службы (или сложного обновления пользовательско- го интерфейса) некоторые окна перестают реагировать на действия пользо- вателя. Поскольку синхронизационный контекст устанавливается на уровне хоста, при наличии нескольких UI-потоков экземпляр хоста для данного типа
348 Глава 8. Управление параллельной обработкой службы должен открываться в каждом UI-потоке. Таким образом, разные хос- ты имеют разные синхронизационные контексты для своих экземпляров служб. Как упоминалось в главе 1, чтобы один тип службы мог иметь несколько хос- тов, разным хостам должны быть предоставлены разные базовые адреса. Самое простое решение — передать базовый адрес в параметре конструктора формы. Я также рекомендую в подобных случаях использовать для конечных точек службы адреса, заданные относительно базовых адресов. Клиент по-прежнему может обращаться с вызовами к различным конечным точкам службы, но при этом конечные точки соответствуют разным хостам в соответствии с используе- мой схемой базовых адресов и привязок. Пример такой конфигурации пред- ставлен в листинге 8.11. Листинг 8.11. Хостинг с множественными UI-потоками partial class HostForm : Form { public HostForm(string baseAddress) { InitializeComponent(): CurrentForm = this; m_Host = new ServiceHost(typeof(MyService).new Uri(baseAddress)): m_Host.Open(); } // Далее как в листинге 8-10 } static class Program { static void MainO { ParameterizedThreadStart threadMethod = delegate(object baseAddress) { string address = baseAddress as string; Application.Run(new HostForm(address)): }; Thread threadl = new Thread(threadMethod); threadl. Start ("http .7/1 ocal host: 8001/"); Thread thread2 = new Thread(threadMethod); thread2.Sta rt("http;//1 oca 1 host;8002/"); } } /* MyService как в листинге 8-10 */ /////////////////// Конфигурационный файл хоста //////////////////////// <services> <service name = "MyNamespace.MyServiсе"> <endpoint address = "MyService" binding = "basicHttpBinding" contract = "I FormManager" /> </service> </services> //////////////////// Конфигурационный файл клиента 111111111111111111111 <client>
Синхронизационный контекст службы 349 <endpoint name = "Form А" address = "http://local host:8001/MyServlce/" binding = "basicHttpBinding" contract = "I FormManager" /> <endpoint name = "Form B" Address = "http://localhost:8002/MyService/" binding = "basicHttpBinding" contract = "I FormManager" /> </client> В листинге 8.11 метод Main() запускает два UI-потока, каждый из которых обладает собственным экземпляром HostForm. При конструировании каждому экземпляру формы передается базовый адрес, который он, в свою очередь, пе- редает своему экземпляру хоста. После открытия хоста устанавливается при- вязка к синхронизационному контексту UI-потока. Вызовы от клиента по базо- вому адресу теперь перенаправляются соответствующему UI-потоку. Обратите внимание: служба предоставляет конечную точку HTTP с использованием при- вязки BasicHttpBinding. Если бы служба предоставляла конечную точку с при- вязкой TCP, то хосту также передавался бы базовый адрес TCP. Форма как служба Главной причиной для хостинга службы WCF в UI-потоке является необходи- мость обновления пользовательского интерфейса или формы службой. При этом всегда возникает одна проблема: как службе получить ссылку на форму? Приемы и идеи, приводившиеся в рассмотренных ранее примерах, работают, однако было бы гораздо проще, если бы форма не только была службой, но и одновременно обеспечивала собственный хостинг. Чтобы подобная схема ра- ботала, форма (или любое окно) должна быть синглетной службой. Дело в том, что только синглетный режим управления экземплярами позволяет передать WCF экземпляр для хостинга. Кроме того, никому не нужна форма, которая су- ществует только на время клиентского вызова (обычно крайне непродолжи- тельное), а также форма, которая может участвовать в сеансе и обновляться только одним клиентом. Для формы, которая одновременно является службой, синглетный режим оптимален во всех отношениях. Пример такой службы пред- ставлен в листинге 8.12. Листинг 8.12. Форма как синглетная служба [ServiceContract] interface IFormManager ( [Operationcontract] void IncrementLabelO: ) [Servi ceBehavior(InstanceContextMode = InstanceContextMode.Single)] partial class MyForm : Form. IFormManager ( Label m_CounterLabel: продолжение &
350 Глава 8. Управление параллельной обработкой ServiceHost m_Host; public MyFormO { InitialIzeComponentC): m_Host = new ServiceHost(this): m_Host.Open(); } void OnFormClosedtobject sender. Event Args args) { m_Host.Close(): } public void IncrementLabel() { Counter++; } public int Counter { get { return Convert.ToInt32(m_CounterLabel .Text): } set { m_CounterLabel .Text - value.ToStringO: } } } MyForm реализует контракт IFormManager и настраивается как синглетная служба WCF. Как и прежде, MyForm содержит переменную типа ServiceHost. При конструировании хоста MyForm использует конструктор, получающий ссылку на объект (см. главу 4). В качестве объекта MyForm передает себя. MyForm откры- вает хост при создании формы и закрывает его при закрытии формы. Обновле- ние элементов управления формы в результате клиентских вызовов осуществ- ляется прямым обращением, потому что форма, естественно, работает в собст- венном синхронизационном контексте. Класс FormHost<F> Для упрощения и автоматизации кода, приведенного в листинге 8.12, можно воспользоваться моим классом FormHost<F>: [ServiceBehavlor(InstanceContextMode = InstanceContextMode.SIngle)] public abstract class FormHost<F> : Form where F : Form { public FormHost(params string[] baseAddresses): protected ServiceHost<F> Host {get:set:} } При использовании FormHost<F> листинг 8.12 сокращается до следующего фрагмента: partial class MyForm : FormHost<MyForm>,IFormManager
Синхронизационный контекст службы 351 Label m_CounterLabel; public MyFormO ( Initial izeComponent О; ) public void IncrementLabelO Counter++; ) public int Counter ( get { return Convert.ToInt32(m_CounterLabel.Text): ) set m_CounterLabel.Text = value.ToString(): ) ) 1 ПРИМЕЧАНИЕ------------------------------------------------------------------------------------ Дизайнер Windows Forms не способен отображать формы с абстрактными базовыми классами (не говоря уже об использовании шаблонов). Вам придется переключиться на базовый класс Form для визуального редактирования, а затем снова вернуться к FormHost<F> для отладки. Будем надеять- ся, что этот раздражающий недостаток будет устранен в будущем. В листинге 8.13 приведена реализация FormHost<F>. Листинг 8.13. Реализация FormHost<F> [Servi ceBehavi or (InstanceContextMode = InstanceContextMode. Single)] public abstract class FormHost<F> : Form where F : Form I ServiceHost<F> m_Host: protected Servi ceHost<F> Host I get ( return m_Host: set m_Host = value: ) ) public FormHost (pa rams string[] baseAddresses) ( m_Host = new ServiceHost<F>(this as F.baseAddresses): Load += delegate { if(Host.State == Communicationstate.Created) продолжение &
352 Глава 8. Управление параллельной обработкой { Host.OpenO: } }: FormClosed += delegate { if(Host.State == Communi cationState.Opened) { Host.CloseO; } }: } } FormHost<F> — абстрактный базовый класс, настроенный как синглетная служба. FormHost<F> также представляет собой шаблонный класс с одним пара- метром типа F. Круг возможных значений F ограничивается классом Form из Windows Forms. FormHost<F> содержит переменную с типом моего класса Service- Host<T>, при этом F передается как параметр типа хоста. FormHost<F> предостав- ляет доступ к хосту производным формам, в основном для расширенной на- стройки конфигурации, поэтому свойство Host помечено как защищенное. Кон- структор FormHost<F> создает хост, но не открывает его. Это объясняется тем, что в производной форме может выполняться часть инициализации хоста - на- пример, настройка регулирования нагрузки. Инициализация выполняется только перед открытием хоста. Субкласс может разместить код инициализации в конструкторе: public MyFormO { Initial izeComponentO; Host.SetThrottle(10.20.1): } Чтобы это стало возможным, конструктор использует анонимный метод для подписки на событие Load формы. В нем он сначала проверяет, что хост еще не был открыт производной формой, а затем открывает его. Аналогичным образом конструктор подписывается на событие FormClosed, в обработчике которого хост закрывается. UI-поток и управление параллельной обработкой При использовании хостинга в UI-потоке (как и в любом другом случае одно- поточной привязки синхронизационных контекстов) возникает риск взаимной блокировки. Например, следующая конфигурация заведомо приведет к взаим- ной блокировке: приложение Windows Forms используется для хостинга служ- бы с истинным значением UseSynchronizationContext и привязкой к Ш-потоку. Приложение Windows Forms осуществляет внутрипроцессный вызов службы через одну из своих конечных точек. Вызов службы блокирует UI-поток, a WCF отправляет UI-потоку сообщение о вызове службы. Это сообщение ни- когда не будет обработано из-за блокировки UI-потока; возникает взаимная блокировка.
Синхронизационный контекст службы 353 Или другой возможный сценарий взаимной блокировки: приложение Windows Forms используется для хостинга службы с истинным значением UseSynchronizationContext и привязкой к UI-потоку. Служба получает вызов от удаленного клиента. Вызов перенаправляется UI-потоку и в конечном счете выполняется в нем. Если службе разрешаются внешние вызовы к другим служ- бам, это может привести к взаимной блокировке, если субъект внешнего вызова попытается каким-то образом обновить UI или выполнить обратный вызов к конечной точке службы, поскольку все экземпляры службы, связанные с ка- кой-либо конечной точкой (независимо от режима активизации экземпляров службы), совместно используют один UI-поток. Скорость реакции UI Каждый клиентский вызов службы, хостинг которой выполняется в UI-потоке, преобразуется в сообщение Windows и в конечном итоге выполняется в UI-по- токе - том самом, который отвечает за обновление UI, а также за дальнейшую реакцию на ввод пользователя, обновление UI и оповещение пользователя о со- стоянии приложения. Пока UI-поток обрабатывает вызов службы, он не может заниматься обработкой UI-сообщений. Следовательно, в операциях служб сле- дует избегать выполнения долгих вычислений, потому что это приведет к замет- ному ухудшению скорости отклика UI. Проблема может быть частично решена явным вызовом статического метода Application.DoEvents(), обеспечивающим об- работку всех сообщений Windows в очереди, или MessageBox.Show(), направлен- ным на обработку части (но не всех) сообщений. К недостаткам подобных по- пыток обновления UI следует отнести то, что при этом экземпляру службы могут быть преждевременно доставлены клиентские вызовы из очереди, что приведет к нежелательным повторным входам или взаимной блокировке. Си- туацию усугубляет еще одно обстоятельство, обусловленное режимом парал- лельной обработки службы (см. далее): даже если вызовы служб имеют малую продолжительность, что произойдет, если множество таких вызовов будет од- новременно доставлено службе? Эти вызовы будут поставлены в очередь сооб- щений Windows друг за другом, а их последовательная обработка может занять немало времени. Во время этого периода пользовательский интерфейс обнов- ляться не будет. При хостинге в UI-потоках следует тщательно проанализиро- вать продолжительность вызовов и их частоту, чтобы понять, будет ли прием- лемо неизбежное снижение скорости реакции UI. Что именно считать при- емлемым, зависит от специфики приложения; как правило, большинство поль- зователей не обращает внимания на задержки в 1/2 секунды и менее, замечает задержки в 3/4 секунды, тогда как задержка, продолжительность которой пре- вышает одну секунду, их раздражает. В таких случаях рассмотрите возможность хостинга частей UI (и связанных с ними служб) в дополнительных UI-потоках, о чем говорилось ранее. Наличие нескольких UI-потоков обеспечивает макси- мальную производительность: пока один поток занят обслуживанием клиент- ского вызова, остальные потоки могут обновлять свои окна и элементы управ- ления. Если применение нескольких UI-потоков невозможно в приложении, а обработка вызовов служб приводит к неприемлемому снижению скорости ре- акции Ш, разберитесь, что делают операции служб и что создает задержку. Как
354 Глава 8. Управление параллельной обработкой правило, причиной задержки являются не обновления UI, а выполнение про- должительных операций — вызова других служб или интенсивных вычислений (например, обработки графических изображений). Поскольку хостинг службы обеспечивается UI-потоком, WCF выполняет в UI-потоке всю указанную рабо- ту, а не только ее критическую часть с прямым взаимодействием с UI. Если это относится к вашей ситуации, запретите потоковую привязку к UI-потоку, задав свойство UseSynchronizationContext равным false: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1. UseSynchronizationContext = false)] class MyService : IMyContract {' public void MyMethodO { Debug.Assert(Application.MessageLoop == false): //... } } Выполняйте продолжительные операции во входном рабочем потоке и ис- пользуйте безопасные элементы (такие, как SafeLabel) для перенаправления вы- зова UI-потоку только тогда, когда это будет необходимо. Недостаток такого решения заключается в том, что оно связано со сложной программной моделью: сама служба не может быть окном или формой (с использованием простого шаблона FormHost<F>), поэтому вам потребуются средства привязки к форме, а разработчик службы должен работать совместно с разработчиками UI и сле- дить за тем, чтобы они использовали потоково-безопасные элементы и предо- ставляли доступ к синхронизационному контексту формы. UI-поток и режимы параллельной обработки Служба с привязкой к UI-потоку изначально является потоково-безопасной по определению, потому что к ее экземплярам может обращаться с вызовами толь- ко UI-поток. Следовательно, настройка службы в режиме ConcurrencyMode.Single не повышает уровень безопасности, потому что служба в любом случае являет- ся однопоточной. В режиме ConcurrencyMode.Single параллельные клиентские вызовы сначала ставятся в очередь по блокировке экземпляра, а затем последо- вательно доставляются циклу обработки сообщений службы. Таким образом, клиентские вызовы могут перемежаться другими UI-сообщениями Windows, а режим ConcurrencyMode.Single обеспечивает наилучшую скорость реакции, по- тому что UI-поток будет чередовать обработку клиентских вызовов с обработ- кой UI. В режиме ConcurrencyMode.Multiple клиентские вызовы доставляются циклу обработки сообщений службы сразу же после их поступления из канала и активизируются последовательно. Однако такая схема создает вероятность формирования «пакетов» клиентских вызовов, следующих вплотную или в не- посредственной близости друг от друга в очереди сообщений Windows; во вре- мя обработки пакета UI-поток не реагирует на действия пользователя. Соответ- ственно, режим ConcurrencyMode.Multiple обепечивает наихудшую скорость реак- ции UI. В режиме ConcurrencyMode.Reentrant служба вовсе не становится реенте- рабельной, а взаимные блокировки все равно возможны, как объяснялось в на-
Пользовательский синхронизационный контекст службы 355 чале раздела. Очевидно, при привязке к UI-потоку службу лучше всего настраивать в режиме ConcurrencyMode.Single. Избегайте режима ConcurrencyMode.Multiple из-за его отрицательного воздействия на скорость реакции, а режима ConcurrencyMode. Reentrant — из-за обещанной, но не реализованной безопасности. 1ользовател ьски й си нхрон изацион н ый онтекст службы Хотя синхронизационный контекст представляет собой идиому общего назна- чения, в исходном варианте .NET 2.0 и WCF реализуют его только в одном случае: в синхронизационном контексте Windows Forms. Разработка пользовательско- го синхронизационного контекста службы состоит из двух аспектов: собствен- но реализации пользовательского синхронизационного контекста и установки (или даже декларативного применения) его для службы. Первый аспект не име- ет никакого отношения к WCF, поэтому в книге он не рассматривается. В этом разделе я буду использовать готовый класс AffinitySynchronizer, определяемый следующим образом: public class Affim ty Synchronize г : Synchronizationcontext. IDisposable public AffimtySynchrornzer(). public AffimtySynchromzer(string threadName); public void Dispose(): i Класс AffinitySynchronizer выполняет все вызовы, направленные ему, в одном приватном рабочем потоке. При присоединении к потоку, открывшему хост, все экземпляры службы независимо от режима управления экземплярами, режима параллельной обработки, конечных точек и контрактов, будут выполняться в одном рабочем потоке. Реализация AffinitySynchronizer включена в архив приме- ров исходного кода, прилагаемый к книге. В двух словах, AffinitySynchronizer создает рабочий поток и ведет синхронизированную очередь рабочих элемен- тов. AffinitySynchronizer даже можно передать имя потока в параметре конструк- тора (для целей отладки и ведения журнала). Если имя рабочего потока не за- дано, по умолчанию используется строка «AffinitySynchronizer Worker Thread». Каждый рабочий элемент в очереди содержит делегата типа SendOrPostCallback. При вызове методов Post() и Send() AffinitySynchronizer «заворачивает» делегата в рабочий элемент и направляет его в очередь. Рабочий поток отслеживает со- стояние очереди. Если в ней имеются элементы, рабочий поток извлекает из очереди первый из них и активизирует делегата. Чтобы завершить рабочий по- ток, уничтожьте экземпляр AffinitySynchronizer. Второй аспект реализации пользовательского синхронизационного контек- ста - а именно его установка — служит отличным примером расширяемости WCF, а также списка задач, которые необходимо учесть при установке таких расширений.
356 Глава 8. Управление параллельной обработкой Службы с потоковой привязкой При разработке служб WCF класс AffinitySynchronizer полезен в тех случаях, когда служба, создающая и взаимодействующая с ресурсами, имеет потоковую привязку (например, при использовании TLS). Службы, использующие Affinity- Synchronizer, всегда потоково-безопасны, потому что они могут вызываться толь- ко внутренним рабочим потоком конкретного экземпляра AffinitySynchronizer. Для служб, настроенных в режиме ConcurrencyMode.Single, это не повышает уро- вень потоковой безопасности, потому что экземпляр службы и без того являет- ся однопоточным. Фактически организуется двойная очередь параллельных вызовов: все параллельные вызовы службы сначала ставятся в очередь блоки- ровки, а затем последовательно передаются рабочему потоку. В режиме Concur- rencyMode.Multiple вызовы передаются в очередь рабочего потока сразу же после поступления, а затем ставятся в очередь; в дальнейшем они обрабатываются по- следовательно и никогда — параллельно. Учтите, что при использовании поль- зовательского синхронизационного контекста, перенаправляющего вызовы пулу потоков вместо одного потока, режим ConcurrencyMode.Multiple обеспечивает наилучшую производительность. Наконец, в режиме ConcurrencyMode.Reentrant служба, конечно, реентерабельной не является, потому что входной вызов с по- вторным входом будет поставлен в очередь, и возникнет взаимная блокировка. При работе с AffinitySynchronizer рекомендуется использовать режим Concurren- cyMode.Single. Установка синхронизационного контекста службы В листинге 8.14 продемонстрирована процедура установки AffinitySynchronizer перед открытием хоста, чтобы все экземпляры службы выполнялись в одном программном потоке. Листинг 8.14. Установка AffinitySynchronizer Synchronizationcontext synchronizationcontext = new Aff1n1tySynchron1zer(): Synchronizationcontext.SetSynchronizationContext(synchronizationcontext); using(synchronizationcontext as IDisposable) { ServiceHost host = new Serv1ceHost(typeof(MyService)); host.OpenO; /* Блокирующие операции */ host.CloseO; } Чтобы присоединить синхронизационный контекст к текущему потоку, вы- зовите метод SetSynchronizationContext() класса Synchronizationcontext После от- крытия хост будет использовать указанный контекст. Обратите внимание: по- сле закрытия хоста в листинге 8.14 экземпляр AffinitySynchronizer уничтожается, и используемый рабочий поток закрывается.
Пользовательский синхронизационный контекст службы 357 Для упрощения кода в листинге 8.14 можно инкапсулировать установку AffinitySynchronizer в пользовательском хосте — например, в шаблоне Service- Host<T>: public class ServiceHost<T> : ServiceHost I public void SetThreadAffinity(string threadName): public void SetThreadAffinity(): П... 1 Использование метода SetThreadAffinity() для присоединения AffinitySynchro- nizer весьма тривиально: ServiceHost<MyServiсе> host = new ServiceHost<MyService>(); host. SetThreadAf f i ni ty(): host. Open (): /* Блокирующие операции */ host.CloseO; В листинге 8.15 представлена реализация методов SetThreadAffinity(). Листинг 8.15. Включение потоковой привязки в ServiceHost<T> public class ServiceHost<T> : ServiceHost AffinitySynchronizer m_AffinitySynchronizer: public void SetThreadAffinity(string threadName) if (State == Communi cationState.Opened) ( throw new InvalidOperationException("Host is already opened"): } m_AffinitySynchronizer = new AffinitySynchronizer(threadName): Synchronizationcontext.SetSynchronizationContext( m_AffinitySynchronizer): } public void SetThreadAffinityO ( SetThreadAffinityC"Executing all endpoints of " + typeof(T)); ) protected override void OnClosingO ( us i ng(m_Affi n i tySynch roni zer) (I base.OnClosingO; 1 ServiceHost<T> содержит переменную типа AffinitySynchronizer и предоставля- ет две версии метода SetThreadAffinity(). Параметризованная версия получает имя потока, а версия без параметров вызывает первую с передачей имени пото- ка, построенного с использованием имени типа службы (например, «Executing
358 Глава 8. Управление параллельной обработкой all endpoints of MyService»). SetThreadAffinity() сначала проверяет, что хост еще не был открыт, потому что присоединение синхронизационного контекста мо- жет происходить только до открытия хоста. Если хост не открыт, SetThread- Affinity() конструирует новый экземпляр AffinitySynchronizer, передает ему имя потока и присоединяет к текущему потоку. Наконец, ServiceHost<T> переопреде- ляет метод базового класса OnCLosing(), чтобы переменная типа AffinitySynchro- nizer уничтожалась для закрытия рабочего потока. Поскольку переменная Affi- nitySynchronizer может быть равна null, если метод SetThreadAffinity() не вызывался, OnClosing() использует команду using, которая осуществляет внутреннюю про- верку null перед вызовом Dispose(). Атрибут ThreadAffinityBehavior В предыдущем разделе было показан способ установки AffinitySynchronizer, не зависящий от конфигурации службы. Тем не менее, если служба по своей архи- тектуре должна всегда выполняться в одном потоке, лучше избежать зависимо- сти от хоста и того программного потока, который его откроет. Используйте мой атрибут ThreadAffinityBehaviorAttribute, который определяется следующим образом: [Attrl but ells age (At t г 1 buteTargets .Class)] public class ThreadAfflnltyBehavlorAttrlbute : Attribute. IContractBehavior.IServiceBehavior { public ThreadAff1n1tyBehav1orAttr1bute(Type serviceType): public ThreadAfflnltyBehaviorAttrlbuteCType serviceType, string threadName); public string ThreadName {get;set;} } Как подсказывает название, атрибут ThreadAffinityBehavior предоставляет ло- кальный аспект поведения, проверяющий тот факт, что все экземпляры службы всегда выполняются в одном программном потоке. Во внутренней реализации ThreadAffinityBehavior используется класс AffinitySynchronizer. При применении атрибута необходимо передать тип службы и (не обязательно) имя программ- ного потока: [ServiceBehavlor(InstanceContextMode = InstanceContextMode.PerCai 1)] [ThreadAffinityBehavior(typeof(MyService))] class MyService ; IMyContract {•••} По умолчанию в качестве имени используется строка «Executing all endpoints of <тип_службы>». Атрибут ThreadAffinityBehavior является пользовательским аспектом поведе- ния контракта, потому что он реализует интерфейс IContractBehavior, представ- ленный в главе 5. IContractBehavior содержит метод Apply Dispatch Behavior(), при помощи которого задается синхронизационный контекст диспетчера конечной точки: public Interface IContractBehavior { void ApplyD1spatchBehav1or(ContractDescript1on description.
Пользовательский синхронизационный контекст службы 359 ServiceEndpoint endpoint, DispatchRuntime dispatch); //... ) Каждая конечная точка обладает собственным диспетчером, и каждый дис- петчер обладает собственным синхронизационным контекстом. В листинге 8.16 приведена реализация атрибута ThreadAffinityBehavior. Листинг 8.16. Реализация ThreadAffinityBehaviorAttribute [AttributeUsage(Attri buteTargets. Cl ass)] public class ThreadAffimtyBehavi orAttri bute : Attribute, IContractBehavior. IServiceBehavior string m_ThreadName; Type m_ServiceType; public string ThreadName // Работает c m_ThreadName {get;set:} public ThreadAffinityBehaviorAttribute(Type ServiceType) ; this(serviceType.null) {} public ThreadAffinityBehaviorAttribute(Type ServiceType, string threadName) m_ThreadName = threadName; m_ServiceType = ServiceType; } void IContractBehavior.ApplyDispatchBehavior( ContractDescription description, ServiceEndpoint endpoint. DispatchRuntime dispatch) { m_ThreadName = m_ThreadName ?? "Executing endpoints of " + m_ServiceType; ThreadAffi m tyHelper.ApplуDispatchBehavlor(m_Servi ceType. m_ThreadName. dispatch); I void IContractBehavior.Validate!...) 0 void IContractBehavior.AddBindingParameterst...) 0 void IContractBehavior.ApplyClientBehavior!...) () void IServiceBehavior.Vai1date(ServiceDescription description. ServiceHostBase ServiceHostBase) ServiceHostBase.Closed += delegate { ThreadAffi ni tyHelper.CloseThread(m_ServiceType); }: void IServiceBehavior.AddB1ndingParameters(...) продолжение &
360 Глава 8. Управление параллельной обработкой {} void IServiceBehavior.ApplyDispatchBehavior(...) {} } Конструкторы атрибута ThreadAffinityBehavior сохраняют переданный тип службы и имя программного потока. Метод Apply Dispatch Behavior() использует оператор ??, появившийся в C# 2.0, для назначения имени потока в случае необходимости. Выражение m_ThreadName = m_ThreadName ?? "Executing endpoints of " + m_ServiceType; является сокращенной записью для if(m_ThreadName == null) { m_ThreadName = "Executing endpoints of " + m_ServiceType: } Атрибут ThreadAffinityBehavior обеспечивает координацию верхнего уровня, делегируя фактическую реализацию ApplyDispatchBehavior() вспомогательному статическому классу ThreadAffinityHelper: public static class ThreadAffinityHelper { internal static void ApplyDispatchBehavior(Type type.string threadName. DispatchRuntime dispatch) public static void CloseThread(Type type); } Класс ThreadAffinityHelper также содержит статический метод CloseThreadQ, который закрывает рабочий поток, связанный с синхронизационным контек- стом типа службы. ThreadAffinityBehavior также является аспектом поведения службы. Он реализует интерфейс IServiceBehavior, чтобы в своей реализации Validate() он мог получить ссылку на хост службы и подписаться на событие Closed с использованием анонимного метода. Анонимный метод закрывает рабо- чий поток, вызывая ThreadAffinityHelper.CloseThread() и передавая тип службы. В листинге 8.17 приведена реализация класса ThreadAffinityHelper. Листинг 8.17. Реализация ThreadAffinityHelper public static class ThreadAffinityHelper { static Dictionary<Type.AffinitySynchronizer> m_Contexts = new Dictionary<Type.AffinitySynchronizer>(): [Methodlmpl(MethodlmplOptions.Synchronized)] internal static void ApplyDispatchBehavior(Type type.string threadName, DispatchRuntime dispatch) { Debug.Assert(dispatch.SynchronizationContext == null); if(m_Contexts.ContainsKey(type) == false) { m_Contexts[type] = new AffinitySynchronizer(threadName): } dispatch.SynchronizationContext = m_Contexts[type]: }
Обратный вызов и безопасность клиента 361 [Method I mp 1 (Met hod I mp 1 Opt 1 on s. Sy nch ron 1 zed) ] public static void CloseThread(Type type) i f (m_Context s. Conta i nsKey (type)) ( m_Contexts[type]. Di spose(): m_Context s.Remove(type): ) ( 1 Класс DispatchRuntime предоставляет свойство Synchronizationcontext, исполь- зуемое для назначения синхронизационного контекста диспетчера: public sealed class DispatchRuntime ( public Synchronizationcontext Synchronizationcontext (get: set;} //... 1 Перед присваиванием ThreadAffinityHelper проверяет, что диспетчер не имеет другого синхронизационного контекста, потому что это указывало бы на нали- чие неразрешенного конфликта. Задача ThreadAffinityHelper заключается в том, чтобы связать всех диспетчеров всех конечных точек указанного типа службы с одним экземпляром синхронизационного контекста. Для ее решения Thread- AffinityHelper ведет статический словарь (ассоциативный список), связывающий тип с его синхронизационным контекстом. Класс ThreadAffinityHelper в Apply- DispatchBehavior() проверяет, присутствует ли в словаре синхронизационный контекст для данного типа. Если подходящий элемент не найден, ThreadAffini- tyHelper создает новый синхронизационный контекст для данного типа и назна- чает его диспетчеру. Метод CloseThread() использует тип как ключ для поиска в словаре синхронизационного контекста и его последующего уничтожения с закрытием потока. Весь доступ к статическому словарю синхронизируется на декларативном уровне, с использованием атрибута Methodim pl с флагом Method- ImplOptions.Synchronized. кратный вызов и безопасность клиента Существует довольно много ситуаций, в которых клиент может получать па- раллельные обратные вызовы. Если клиент предоставил ссылку обратного вы- зова нескольким службам, эти службы могут одновременно обратиться к нему. Но даже если ссылка всего одна, служба может запустить несколько программ- ных потоков и использовать их для обращения по этой единственной ссылке. Дуплексные обратные вызовы поступают к клиенту в рабочих потоках и могут повредить его состояние, если это будет происходить параллельно без синхро- низации. Клиент должен синхронизировать доступ не только к своему состоя- нию в памяти, но и к любым ресурсам, к которым могут обращаться потоки об- ратного вызова. Клиент обратного вызова, как и службы, может использовать
362 Глава 8. Управление параллельной обработкой ручную или декларативную синхронизацию. Атрибут CaLLbackBehavior, упоми- навшийся в главе 6, предоставляет свойства ConcurrencyMode и UseSynchroniza- tionContext: [AttributeUsage(AttributeTargets.Cl ass)] public sealed class CalIbackBehaviorAttribute : Attribute.... { public ConcurrencyMode ConcurrencyMode {get:set:} public bool UseSynchronizationContext {get;set;} } Оба свойства используют по умолчанию те же значения, что и атрибут ServiceBehavior, и ведут себя аналогичным образом. Например, свойство Concur- rencyMode по умолчанию равно ConcurrencyMode.Single, поэтому следующие два определения эквивалентны: class MyCllent ; IMyContractCallback {...} [CalIbackBehavior(ConcurrencyMode = ConcurrencyMode.Single)] class MyClient ; IMyContractCallback {...} Обратные вызовы в режиме ConcurrencyMode.Single Если обратный вызов настроен в режиме ConcurrencyMode.Single (по умолча- нию), в любой момент времени в объект обратного вызова разрешен вход толь- ко одному вызову. Принципиальное отличие по сравнению со службой заклю- чается в том, что объекты обратного вызова часто существуют независимо от WCF. Если экземпляр службы принадлежит WCD и обращения к нему произ- водятся только рабочими потоками, направляемыми WCF, объект обратного вызова также может взаимодействовать с локальными потоками на стороне клиента. Этим клиентским потокам неизвестно о синхронизационной блоки- ровке, связываемой с объектом обратного вызова при использовании режима ConcurrencyMode.Single. Все, что делает режим ConcurrencyMode.Single для объекта обратного вызова, — это упорядочение доступа со стороны потоков WCF. Сле- довательно, вы должны вручную синхронизировать доступ к состоянию обрат- ного вызова и всем остальным ресурсам, к которым обращается метод обратно- го вызова, как показано в листинге 8.18. Листинг 8.18. Ручная синхронизация обратных вызовов в режиме ConcurrencyMode.Single interface IMyContractCalIback { [Operationcontract] void OnCa11 back(); } class MyClient : IMyContractCalIback.IDisposable { MyContractCllent m_Proxy;
Обратный вызов и безопасность клиента 363 public void Cal 1Serviсе() ( InstanceContext callbackcontext = new InstanceContext(this); m_Proxy = new MyContractClient(calIbackContext): m_Proxy.DoSomethi ng(); // Активизируется обратными вызовами последовательно. // а также клиентскими потоками public void OnCallbackO И Обращения к состоянию и ресурсам синхронизируются вручную lock(this) (•••} public void DisposeO m_Proxy. Closet); )1 Обратные вызовы в режиме ConcurrencyMode.Multiple При настройке обратного вызова в режиме ConcurrencyMode.Multiple WCF разре- шает параллельные вызовы к экземпляру. Это означает, что в операциях обрат- ного вызова должна производиться синхронизация доступа, потому что они мо- гут вызываться параллельно как рабочими потоками WCF, так и потоками клиентской стороны, как показано в листинге 8.19. Листинг 8.19. Ручная синхронизация обратных вызовов в режиме ConcurrencyMode.Multiple [CallbackBehavi or (ConcurrencyMode = ConcurrencyMode.Multiple)] class MyClient : IMyContractCallback.IDisposable MyContractCl ient m_Proxy: public void Ca11Service() InstanceContext callbackcontext = new InstanceContext(this): m_Proxy = new MyContractClienttcalIbackContext): m_Proxy.DoSomething(); // Может активизироваться параллельно несколькими обратными вызовами. // а также клиентскими потоками public void OnCallbackO ( il Обращения к состоянию и ресурсам синхронизируются вручную lock(this) } (•••} public void DisposeO m_Proxy.Closet);
364 Глава 8. Управление параллельной обработкой Обратные вызовы в режиме ConcurrencyMode.Reentrant Поскольку объект обратного вызова также может выполнять исходящие вызо- вы средствами WCF, такие вызовы со временем могут попытаться повторно войти в объект обратного вызова. Чтобы предотвратить взаимную блокировку при использовании ConcurrencyMode.Single, настройте обратный вызов в режиме ConcurrencyMode.Reentrant: [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyClient : IMyContractCalIback {...} Обратные вызовы и синхронизационный контекст Как и при вызове служб, при обратном вызове может возникнуть необходи- мость в обращении к ресурсам, привязанным к конкретному потоку(-ам). Кро- ме того, экземпляру обратного вызова может потребоваться потоковая привяз- ка для работы с TLS или взаимодействия с UI-потоком. Хотя при обратном вызове могут использоваться приемы перенаправления взаимодействия в син- хронизационный контекст ресурса, представленные в листингах 8.4 и 8.5, WCF также можно заставить связать обратный вызов с определенным синхронизаци- онным контекстом — для этого свойство UseSynchronizationContext задается рав- ным true. Однако в отличие от служб, клиент не использует хост для предостав- ления конечной точки. Если свойство UseSynchronizationContext истинно, ис- пользуемый синхронизационный контекст фиксируется при открытии посред- ника, или чаще — при первом вызове службы клиентом с использованием посредника, если метод Ореп() не был вызван явно. Если вызывающий кли- ентский поток имеет синхронизационный контекст, то этот контекст будет использоваться WCF для всех обратных вызовов к конечной точке клиента, связанной с посредником. Учтите, что только первому вызову (или первому вызову Ореп()), обращенному к посреднику, предоставляется возможность оп- ределения синхронизационного контекста. Последующие вызовы уже ни на что не влияют. Если вызывающий клиентский поток не имеет синхронизационного контекста, то даже при истинном свойстве UseSynchronizationContext синхрони- зационный контекст не будет использоваться для обратных вызовов. Обратные вызовы и синхронизационный контекст UI Если объект обратного вызова выполняется в синхронизационном контексте Windows Forms или ему потребуется обновить пользовательский интерфейс, такие вызовы должны перенаправляться UI-потоку. Для этого могут использо- ваться приемы, продемонстрированные в листингах 8.6 и 8.8. Тем не менее на
Обратные вызовы и синхронизационный контекст 365 практике форма чаще сама реализует контракт обратного вызова и обновляет пользовательский интерфейс, как показано в листинге 8.20. Листинг 8.20. Использование синхронизационного контекста UI для обратных вызовов partial class MyForm : Form.IMyContractCallback I MyContractCl 1 ent m_Proxy; public MyFormO f Initial 1zeComponent(): Instancecontext callbackcontext = new Instancecontext(this): m_Proxy = new MyContractClient(ca1IbackContext): 1 // Вызывается в результате события UI public void OnCallService(object sender.EventArgs args) m_Proxy.DoSomething();//Affinity established here ) //Этот метод всегда выполняется в UI-потоке public void OnCalIbackО // Необходимость в синхронизации и перенаправлении отсутствует Text = "Some Callback": } public void OnClose(object sender.EventArgs args) I m_Proxy.Close(): )1 В листинге 8.20 прокси сначала используется в методе CallService(), вызывае- мом Ш-потоком в результате некоторого UI-события. Вызов посредника в син- хронизационном контексте UI создает потоковую привязку, поэтому клиент обратного вызова может напрямую обновлять UI без перенаправления вызовов. Кроме того, поскольку в синхронизационном контексте будет выполняться только один поток, обратный вызов гарантированно синхронизируется. Потоковую привязку к синхронизационному контексту UI также можно ус- тановить явно, открывая посредника в конструкторе формы без вызова опера- ции. Это особенно полезно, если вы хотите доставлять вызовы службе в рабо- чих потоках (или даже асинхронно, как обсуждается в конце главы), но при этом обратные вызовы должны поступать в синхронизационном контексте UI, как показано в листинге 8.21. Листинг 8.21. Явное открытие посредника для установления синхронизационного контекста partial class MyForm : Form.IMyContractCallback I MyContractCl i ent m_Proxy: public MyFormO продолжение &
366 Глава 8. Управление параллельной обработкой { InitializeComponent(); InstanceContext callbackcontext = new InstanceContext(this); m_Proxy = new MyContractCl1 ent(calIbackContext); //Здесь устанавливается привязка к синхронизационному контексту UI: ш_Ргоху,0реп(): } // Вызывается в результате III-события public void Cal 1Service(object sender.EventArgs args ) { Threadstart invoke = delegate { m_Proxy.DoSomething(); }; Thread thread = new Thread(invoke); thread.Start(); ) //Этот метод всегда выполняется в III-потоке public void OnCallbackO { // Необходимость в синхронизации и перенаправлении отсутствует Text = "Some Callback"; } public void OnClosetobject sender.EventArgs args) { m_Proxy.Closet): } } Обратные вызовы в UI-потоке и скорость реакции Во время обработки обратных вызовов в UI-потоке интерфейс программы не реагирует на действия пользователя. Даже если обратные вызовы выполняются относительно быстро, в режиме ConcurrencyMode.Multiple в очереди сообщений UI может оказаться несколько обратных вызовов, следующих друг за другом, и их последовательная обработка приведет к снижению скорости отклика. Избегай- те продолжительной обработки обратных вызовов в UI-потоке и используйте режим ConcurrencyMode.Single, чтобы обратные вызовы ставились в очередь, а их последовательная доставка объекту обратного вызова могла чередоваться с со- общениями UI. Обратные вызовы в UI-потоках и управление параллельной обработкой Настройка обратного вызова на потоковую привязку к UI-потоку может при- вести к возникновению взаимной блокировки. Рассмотрим следующую конфи- гурацию: клиент Windows Forms устанавливает потоковую привязку между объектом обратного вызова (которым может быть даже он сам) и синхрониза- ционным контекстом UI. Затем клиент вызывает службу, передавая ссылку об- ратного вызова. Служба, настроенная в реентерабельном режиме, вызывает клиента. Возникает взаимная блокировка, потому что обратный вызов клиента должен выполняться в UI-потоке, а этот поток блокируется в ожидании возврата
Обратные вызовы и синхронизационный контекст 367 из вызова службы. Настройка обратного вызова как односторонней операции не решит проблемы, потому что односторонний вызов все равно должен быть сначала перенаправлен UI-потоку. Один из способов устранения взаимной бло- кировки заключается в запрете использования синхронизационного контекста Ш обратным вызовом и ручным асинхронным перенаправлением обновления формы с использованием ее синхронизационного контекста. Пример приведен в листинге 8.22. Листинг 8.22. Предотвращение взаимной блокировки обратного вызова в UI-потоке ///////////Н Н/И//И И// Client Side ///////////////////// [CallbackBehavior(UseSynchronizat1onContext = false)] partial class MyForm : Form.IMyContractCalIback { Synchronizationcontext m_Context; MyContractClient m_Proxy: public MyFormO Initial izeComponent(): m_Context = SynchronizationContext.Current; InstanceContext callbackcontext = new InstanceContext(this); m Proxy = new MyContractClient(calIbackContext): } public void CallServicetobject sender.EventArgs args) { m_Proxy.DoSomethi ng (); } // Обратный вызов выполняется в рабочих потоках public void OnCallbackO ( SendOrPostCalIback setText = delegate { Text = "Manually marshaling to UI thread"; }: m_Context.Post(setText.null); } public void OnClosetobject sender.EventArgs args) { m_Proxy.Close!); } ////////////////////////// Сторона службы ///////////////////// [ServiceContract(Cal 1backContract = typeof(IMyContractCal1 back))] interface IMyContract [Operationcontract] void DoSomethingO; interface IMyContractCallback f [Operationcontract] void OnCalIback(); 1 продолжение &
368 Глава 8. Управление параллельной обработкой [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomethingO { IMyContractCallback callback = OperationContext.Current. GetCallbackChannel<IMyContractCallback>(); cal Iback. OnCal IbackO; } } Как видно из листинга 8.22, необходимо использовать метод Post() синхро- низационного контекста. Ни при каких обстоятельствах не следует использо- вать метод Send() — даже несмотря на то, что обратный вызов выполняется в ра- бочем потоке, UI-поток остается заблокированным по исходящему вызову. Вызов Send() приведет к взаимной блокировке, которую мы пытаемся предот- вратить, потому что Send() блокирует работу до того момента, когда UI-поток сможет обработать запрос. По тем же причинам обратный вызов в листин- ге 8.22 не может использовать потоково-безопасные элементы (такие, как Safe- Label), потому что они тоже используют метод Send(). Пользовательский синхронизационный контекст обратного вызова Как и в случае с пользовательским синхронизационными контекстами служб, вы можете установить пользовательский синхронизационный контекст для ис- пользования при обратном вызове. Все, что для этого потребуется, — чтобы к потоку, открывшему посредника (или вызвавшему его в первый раз), был присоединен пользовательский синхронизационный контекст. В листинге 8.23 показано, как присоединить класс AffinitySynchronizer к объекту обратного вызо- ва перед использованием посредника. Листинг 8.23. Задание пользовательского синхронизационного контекста для объекта обратного вызова interface IMyContractCallback { [Operationcontract] void OnCal1 back(); } class MyClient : IMyContractCallback { // Метод всегда вызывается одним программным потоком public void OnCalIbackO {•••} } MyClient client = new MyClientO: Instancecontext callbackcontext = new InstanceContext(client); MyContractClient proxy = new MyContractClient(callbackContext); Synchronizationcontext synchronizationcontext = new AffinitySynchronizer(); Synchroni zati onContext.SetSynchroni zati onContext(synchroni zati onContext);
Обратные вызовы и синхронизационный контекст 369 using(synchronizationContext as IDisposable) ( proxy.DoSomething(): /* Блокирующие операции до возврата из обратного вызова */ proxy. Close О; ) Атрибут CallbackThreadAffinityBehaviorAttribute По аналогии со службами, для назначения пользовательского синхронизацион- ного контекста конечным точкам обратного вызова может использоваться атри- бут. Например, вот как выглядит мой атрибут CallbackThreadAffinityBehavior- Attribute: [Attrl buteUsage (Attrl buteTargets .Class)] public class CalIbackThreadAffinityBehaviorAttribute : Attribute. lEndpointBehavior ( public CallbackThreadAffinityBehaviorAttribute(Type calIbackType); public CalIbackThreadAffinityBehaviorAttributeCType calIbackType. string threadName); public string ThreadName [get; set;} ) Атрибут CallbackThreadAffinityBehavior заставляет все обратные вызовы по всем контрактам обратных вызовов, поддерживаемым клиентом, выполняться водном программном потоке. Так как атрибут должен влиять на конечные точ- ки обратного вызова, он реализует интерфейс lEndpointBehavior, представлен- ный в главе 6. Атрибут применяется непосредственно к типу обратного вызова, как показано в листинге 8.24. Листинг 8.24. Применение атрибута CallbackThreadAffinityBehavior [Cal 1 back Th readAf f i n i tyBeha v i or (ty peof (MyC 11 ent)) ] class MyClient ; IMyContractCalIback.IDisposable MyContractClient m_Proxy; public void CallService() 1 InstanceContext callbackcontext = new InstanceContext(this); m_Proxy = new MyContractClient(calIbackContext); m_Proxy.DoSomething(); } // Метод активизируется одним и тем же потоком обратного вызова // и клиентскими потоками public void OnCallbackO I И Обращение к состоянию и ресурсам, ручная синхронизация 1 public void DisposeO I m_Proxy.Close(): ) 1
370 Глава 8. Управление параллельной обработкой При конструировании атрибута передается тип обратного вызова, к которо- му он применяется. Учтите, что хотя обратный вызов всегда активизируется WCF в одном потоке, возможно, вам все равно придется синхронизировать дос- туп к нему, если другие потоки на стороне клиента тоже обращаются к этому методу. При использовании атрибута CallbackThreadAffinityBehavior из листинга 8.24 листинг 8.23 сводится к следующему виду: MyCllent client = new MyCHentO; InstanceContext callbackcontext = new InstanceContext(cllent): MyContractClient proxy = new MyContractCl1 ent(ca11backContext): proxy.DoSomething(); /* Блокирующие операции до возврата из обратного вызова */ proxy.CloseО: Реализация атрибута CallbackThreadAffinityBehavior приведена в листинге 8.25. Листинг 8.25. Реализация CallbackThreadAfRnityBehaviorAttribute [AttributeUsage(AttributeTargets.Cl ass)] public class CallbackThreadAffinityBehaviorAttribute : Attribute. lEndpointBehavior { string m_ThreadName; Type m_CallbackType: public string ThreadName // Обращается к m_ThreadName {get:set;} public CallbackThreadAffinityBehaviorAttributeCType callbackType) : this(callbackType.null) {} public CallbackThreadAff1nityBehaviorAttributeCType cal 1backType. string threadName) { m_ThreadName - threadName; m_Ca11backType - callbackType; AppDomain.CurrentDomain.ProcessExit +- delegate { ThreadAffi nityHelper.CloseThread(m_Cal1backType): }: } void IEndpointBehavior.ApplyClientBehavior( ServiceEndpoint ServiceEndpoint, ClientRuntime clientRuntime) { m_ThreadName - m_ThreadName ?? "Executing callbacks of " + m_Ca11backType; ThreadAffi ni tyHelper.ApplyDi spatchBehavi or(m_Ca11backType. m_ThreadName. clientRuntime.CallbackDispatchRuntime); } void lEndpointBehavior.AddB1ndingParameters(...)
Обратные вызовы и синхронизационный контекст 371 (} void lEndpointBehavior.ApplyDispatchBehavlor(...) (} void lEndpointBehavior.Vaiidate(...) (} 1 Конструктор атрибута CallbackThreadAffinityBehavior сохраняет в переменных класса переданный тип обратного вызова и имя потока (если оно задано). Атри- бут использует класс ThreadAffinityHeLper, приведенный ранее, для присоедине- ния AffinitySynchronizer к диспетчеру. В отличие от службы хост в происходя- щем не участвует, поэтому нет события закрытия, на котором можно было бы подписаться как на сигнал о закрытии рабочего потока AffinitySynchronizer. Вме- сто этого конструктор использует анонимный метод для подписки на событие завершения процесса в текущем прикладном домене. При закрытии клиентско- го приложения анонимный метод использует ThreadAffinityHeLper для закрытия рабочего потока. Хотя атрибут поддерживает интерфейс lEndpointBehavior, для нас представ- ляет интерес только метод ApplyClientBehavior(), в котором можно повлиять на диспетчера конечной точки обратного вызова: public interface lEndpointBehavior ( void ApplyClientBehavior(ServiceEndpoint serviceEndpoint. ClientRuntime clientRuntime): //... 1 В AppLyCLientBehavior() атрибут извлекает из параметра ClientRuntime с ис- пользованием свойства CallbackDispatchRuntime: public sealed class ClientRuntime ( public DispatchRuntime CallbackDispatchRuntime (get:} //... и передает его ThreadAffinityHeLper для присоединения AffinitySynchronizer. Хотя рабочий поток стороны клиента, используемый AffinitySynchronizer, бу- дет автоматически закрыт с завершением клиентского приложения, возможно, клиент захочет ускорить ход событий и закрыть его ранее. Для этого он явно вызывает метод CLoseThread() класса ThreadAffinityHeLper: MyClient client = new MyClientO: Instancecontext callbackcontext = new Instancecontext (cl ient): MyContractClient proxy = new MyContractClient(calIbackContext); proxy.DoSomethingO: /* Блокирующие операции до возврата из обратного вызова */ proxy. Close О: ThreadAf f i ni tyHel per. Cl oseThread (typeof (MyClient));
372 Глава 8. Управление параллельной обработкой Асинхронные вызовы При вызове службы клиент обычно блокируется на время обработки вызова, и управление возвращается ему только с завершением операции. Однако во многих случаях операции предпочтительнее выполнять в асинхронном режиме; другими словами, управление немедленно возвращается клиенту, а выполнение операции службы происходит в фоновом режиме. После этого служба каким-то образом оповещает клиента о том, что выполнение метода завершено, и переда- ет клиенту результат. Такой режим называется асинхронным выполнением опе- раций, а само действие называется асинхронным вызовом. Асинхронные вызовы улучшают скорость отклика и доступность клиента. ПРИМЕЧАНИЕ------------------------------------------------- Односторонние операции не подходят для асинхронных вызовов. Во-первых, асинхронность одно- сторонних вызовов отнюдь не гарантирована. Если очередь входящих вызовов службы заполнена до предела, WCF блокирует вызывающую сторону до тех пор, пока не сможет поместить вызов в очередь. Во-вторых, не существует простого механизма оповещения клиента о результатах или ошибках при завершении односторонней операции. Можно вручную построить механизм, который передает при каждом одностороннем вызове идентификатор метода, а затем использует обратный вызов для оповещения клиента о завершении, результатах и ошибках, но такое решение будет гро- моздким и специализированным. Служба должна всегда перехватывать все исключения, а коммуни- кационные ошибки могут вообще не дойти до клиента. Кроме того, оно требует применения дуп- лексной привязки и делает невозможным вызов операции службы как в синхронном, так и асин- хронном режиме. Требования к механизму асинхронных вызовов Чтобы извлечь максимум пользы из многочисленных возможностей, доступных для асинхронных вызовов WCF, следует сначала привести список общих тре- бований для любой служебно-ориентированной поддержки асинхронных вызо- вов. Далее приводятся некоторые из них. О Один код службы должен использоваться как для синхронных, так и для асин- хронных вызовов. Это позволяет разработчику сосредоточиться на биз- нес-логике и обслуживать как синхронных, так и асинхронных клиентов. О Из первого требования следует, что клиент сам выбирает, как должна вы- зываться операция — синхронно или асинхронно. В свою очередь, это под- разумевает, что на стороне клиента для этих двух случаев (синхронного или асинхронного) используется разный код. О Клиент должен иметь возможность выдать несколько асинхронных вызовов и иметь несколько незавершенных асинхронных вызовов в отдельный мо- мент времени. Клиент должен отличать оповещения о завершении методов друг от друга. О Если операция службы имеет выходные параметры или возвращаемые зна- чения, эти параметры недоступны при возврате управления клиенту. Клиент должен иметь возможность получить результаты при завершении операции. О Сведения о коммуникационных ошибках и ошибках службы тоже должны передаваться клиенту. Исключения, инициированные во время работы службы, должны позднее воспроизводиться на стороне клиента.
Асинхронные вызовы 373 О Реализация этого механизма не должна зависеть от привязки и технологии передачи данных. Любая привязка должна поддерживать асинхронные вы- зовы. О Механизм не должен использовать конструкции, относящиеся к специфике конкретных технологий, — такие, как исключения .NET или делегаты. О Последний пункт — не столько требование, сколько архитектурная рекомен- дация: механизм асинхронных вызовов должен быть прямолинейным и про- стым в использовании. В частности, механизм должен по возможности скры- вать подробности своей реализации (скажем, рабочие потоки, используемые для передачи вызова). У клиента имеется несколько стратегий ожидания завершения операции. Клиент выдает асинхронный вызов, а затем: О выполняет некоторую работу, пока вызов продолжает обрабатываться, а за- тем блокируется до его завершения; О выполняет некоторую работу, пока вызов продолжает обрабатываться, а за- тем переходит в режим периодического опроса, ожидая завершения; О получает оповещение о завершении метода. Оповещение реализуется в фор- ме обратного вызова метода, предоставленного клиентом. Обратный вызов должен содержать информацию о том, какая именно операция завершилась, и возвращаемые результаты; О выполняет некоторую работу, пока вызов продолжает обрабатываться, затем ждет в течение заранее определенного промежутка времени и прекращает ожидание, даже если выполнение вызова еще не завершено; О ожидает завершения сразу нескольких операций. WCF предоставляет в распоряжение клиентов все перечисленные возмож- ности. Поддержка WCF существует полностью на стороне клиента, и служба даже не знает, что она вызвана в асинхронном режиме. Это означает, что любая служба способна поддерживать асинхронные вызовы и что одна и та же служба может вызываться как в синхронном, так и асинхронном режиме. Кроме того, так как вся асинхронная поддержка вызовов сосредоточена на стороне клиента и не зависит от служб, для асинхронных вызовов могут использоваться любые привязки. ПРИМЕЧАНИЕ------------------------------------------------------------- Поддержка асинхронных вызовов WCF, представленная в этом разделе, аналогична, но не иден- тична поддержке асинхронных вызовов на базе делегатов, предоставляемой .NET для обычных типов CLR. Асинхронные вызовы на базе посредников Поскольку клиент сам решает, должен ли вызов быть синхронным или асин- хронным, для каждого случая асинхронности должен создаваться отдельный посредник. Используя SvcUtil с ключом /async, вы можете сгенерировать посредника, содержащего асинхронные методы наряду с синхронными. Для
374 Глава 8. Управление параллельной обработкой каждой операции исходного контракта асинхронный посредник и контракт до- бавляют два дополнительных метода следующего формата: [0perat1onContract(AsyncPattern true. Action - '^исходное имя действия>", ReplyAction - "<исходное имя ответа">)] lAsyncResult Вед1п<0рега11оп>(<входные аргументы>. AsyncCalIback calIback,object asyncState); <returned type> End<Operation>(<BbixoflHbie аргументы>,lAsyncResult result); Атрибут Operationcontract содержит логическое свойство AsyncPattern, опре- деляемое следующим образом: [Attri buteUsage(Att гi buteTa rgets.Method)] public sealed class OperationContractAttribute : Attribute { public bool AsyncPattern {get;set;} //... } Свойство AsyncPattern по умолчанию равно false. Оно имеет смысл только в копии контракта клиентской стороны. Оно может задаваться равным true только для методов с Вед\п<операция> — совместимой сигнатурой, а определяю- щий контракт также должен иметь соответствующий метод с Епй<операция> - совместимой сигнатурой. Эти требования проверяются во время загрузки по- средника. Свойство AsyncPattern связывает синхронный метод с парой Begin/End и устанавливает связь между синхронным и асинхронным выполне- нием. Вкратце, когда клиент вызывает метод в форме Вед\п<операция>() с ис- тинным значением AsyncPattern, WCF не вызывает метод с указанным именем напрямую. Вместо этого WCF использует поток из пула для синхронного вызо- ва метода, обозначенного свойством Action. Синхронный вызов блокирует по- ток из пула, а не вызывающего клиента. Клиент блокируется лишь на короткое время, необходимое для перенаправления запроса на вызов пулу потоков. От- ветный метод синхронного вызова ассоциирован с методом Епй<операция>. В листинге 8.26 показан контракт калькулятора с реализующей службой и класс посредника, сгенерированный с ключом /async. Листинг 8.26. Асинхронный контракт и посредник ////////////////////////// Сторона службы ////////////////////// [ServiceContract] interface ICalculator { [Operationcontract] int Add(int number1,int number2); //... } class Calculator : ICalculator { public int Add(int number1.int number2) { return numberl + number2; } //... )
Асинхронные вызовы 375 111/1///////////Н/////Н/ Сторона клиента ////////////////////// [ServiceContract] public interface ICalculator I [OperationContract] int Add(int numberl.int number?); [OperationContract(AsyncPattern - true. Action = ".../ICalculator/Add". ReplyAction = "..,/ICalculator/AddResponse")] lAsyncResult BeginAdd(int numberl.int number?.AsyncCalIback callback, object asyncState): int EndAdd(lAsyncResult result); //... ) public partial class Calculatorclient : ClientBase<ICalculator>.ICalculator I public int Addlint numberl.int number2) ( return Channel.Add(numberl.number2); } public lAsyncResult BeginAdd(int numberl.int number?. AsyncCalIback calIback.object asyncState) { return Channel.BeginAdd(numberl.number2.callback.asyncState): ) public int EndAddIlAsyncResult result) { return Channel.EndAdd(result); } //... 1 Обратите внимание: операция BeginAdd() в контракте содержит исходные имена действия и ответа, хотя на самом деле их можно опустить: [OperationContract(AsyncPattern я true)] lAsyncResult BeginAdd(int numberl.int number?.AsyncCalIback callback. object asyncState); Асинхронный вызов Щ\п<Операция>() получает входные параметры исходной синхронной опера- ции. К ним относятся контракты данных, передаваемые по значению или по ссылке (с использованием модификатора ref). Возвращаемые значения и явно заданные выходные параметры исходного метода (с модификаторами out и ref) являются частью метода End< Операциям Например, для следующего определе- ния операции: [Serviceoperation] string MyMethodOnt numberl.out int number?.ref int number?); соответствующие методы Вед\п<Операция>() и Еп6<Операция>() выглядят так: [ServiceOperation(...)] lAsyncResult BeginMyMethod(int numberl.ref int number?.
376 Глава 8. Управление параллельной обработкой AsyncCalIback calIback,object asyncState); string EndMyMethod(out int number2.ref int number3. lAsyncResult asyncResult); Метод Вед\п<Операция>() получает два дополнительных входных параметра, не входящих в исходную сигнатуру операции: callback и asyncState. Параметр callback — делегат для метода стороны клиента, используемого для оповещения о завершении. Параметр asyncState — объект с информацией состояния, необхо- димой для стороны, обрабатывающей событие завершения метода. Эта два па- раметра не являются обязательными: вызывающая сторона может передать null в любом из них. Например, если вы хотите асинхронно вызвать метод Add() службы Calculator из листинга 8.26 с использованием асинхронного посредника, а результаты или ошибки вас не интересуют, это делается так: Calculatorclient proxy = new CalculatorClient(); proxy.BeginAdd(2,3,nul1.null)://Асинхронное перенаправление proxy.Closet): Если у клиента имеется определение асинхронного контракта, асинхронный вызов операции также может осуществляться с использованием ChannelFactory: ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>(); [Calculator proxy = factory.CreateChannel0; proxy.BeginAddt2.3,nul1,nul1); ICommunicationObject channel = proxy as ICommunicationObject: channel.Closet); Недостаток такого вызова заключается в том, что клиент не сможет полу- чить его результаты. Интерфейс lAsyncResult Каждый метод Ведю<Операция>() возвращает объект, реализующий интерфейс lAsyncResult из пространства имен System.Runtime.Remoting.Messaging: public interface lAsyncResult { object AsyncState {get:} WaitHandle AsyncWaitHandle {get;} bool CompletedSynchronously {get;} bool IsCompleted {get;} } Возвращаемый объект lAsyncResult однозначно идентифицирует метод, вы- званный с использованием Вед1п<Операция>(). Его можно передать End<Onepa- ция>{) для идентификации асинхронного вызова, результаты которого вы хоти- те получить. End<Operation>() блокирует вызывающую сторону до завершения ожидаемой операции (идентифицируемой объектом lAsyncResult) и возможного получения результатов или ошибок. Если к моменту вызова Еп6<Операция>() метод уже будет завершен, Ех\А<Операция>{) не блокирует вызывающую сто- рону, а просто возвращает результаты. Последовательность показана в листин- ге 8.27.
Асинхронные вызовы 377 Листинг 8.27. Простая последовательность асинхронного вызова Calculatorclient proxy = new CalculatorCl1ent(): lAsyncResult asyncResultl = proxy.BeginAdd(2.3.nul1.nul1): lAsyncResult asyncResult2 = proxy.BeginAdd(4.5.null.null); proxy. Cl ose(): /* Некоторые действия */ int sum: sum = proxy.EndAdd(asyncResultl)://Возможна блокировка Debug. Assert (sum == 5): sum = proxy.EndAdd(asyncResult2)://Возможна блокировка Debug.Assert(sum == 9); При всей своей простоте листинг 8.27 демонстрирует ряд ключевых момен- тов. Во-первых, один экземпляр посредника может выдать несколько асинхрон- ных вызовов. Вызывающая сторона различает незавершенные вызовы по уни- кальным объектам lAsyncResult, возвращаемым Вед\п<Операция>(). Во-вторых, когда вызывающая сторона применяет асинхронные вызовы, как в листинге 8.27, она должна сохранять объекты lAsyncResult. Кроме того, она не может выдви- гать какие-либо предположения относительно порядка завершения незавер- шенных вызовов. Вполне может оказаться, что второй вызов завершится ранее первого. Наконец, если посредник далее использоваться не будет, его можно за- крыть сразу же после отправки асинхронных вызовов; это не помешает вызову Ы<Операция>(). Хотя это и не очевидно из листинга 8.27, у программирования асинхронных вызовов имеются две важные особенности: О Метод ЕпА<Операция>() вызывается только один раз для каждой асин- хронной операции. Попытка его повторного вызова приводит к исключе- нию InvalidOperation Exception. О Объект lAsyncResult передается Епй<Операция>() только с тем же объектом посредника, который использовался для перенаправления вызова. Передача объекта lAsyncResult другому экземпляру посредника приводит к исключе- нию AsyncCallbackException. Опрос или ожидание завершения При вызове ЕпА<Операция>() клиент блокируется до возврата управления асин- хронным методом. Это может быть приемлемо, если во время обработки вызова клиент выполняет ограниченный объем работы, а после ее завершения не мо- жет продолжить нормальное выполнение без возвращаемого значения или вы- ходных параметров операции (или хотя бы информации о том, что метод завер- шился). А если клиент всего лишь хочет проверить, не завершилась ли опера- ция? Или если клиент хочет ожидать завершения в течение фиксированного интервала, выполнить фиксированный объем дополнительной работы, а затем
378 Глава 8. Управление параллельной обработкой снова перейти к ожиданию? WCD поддерживает и эти альтернативные про- граммные модели. Объект lAsyncResult, возвращаемый Ведш<Оиерацмя>(), содержит свойство AsyncWaitHandle типа WaitHandle: public abstract class WaitHandle : { public static bool WaitAl1(WaitHandle[] waitHandles): public static int WaitAny(WaitHandle[] waitHandles): public virtual void Closed: public virtual bool WaitOneO: //... } Метод WaitOne() типа WaitHandle возвращает управление только при поступ- лении сигнала о завершении. Использование WaitOneO продемонстрировано в листинге 8.28. Листинг 8.28. Использование свойства IasyncResult.AsyncWaitHandle для блокировки до завершения Calculatorclient proxy = new CalculatorClient(); lAsyncResult asyncResult = proxy.BeginAdd(2,3,null.null): proxy.Close(): /* Некоторые действия */ asyncResult.AsyncWaitHandle.WaitOne(): //Возможна блокировка int sum = proxy.EndAdd(asyncResult): //Возможна блокировка Debug.Assert(sum == 5): На логическом уровне листинг 8.28 идентичен листингу 8.27 с вызовом только Еп6<Операция>(). Если операция все еще выполняется, WaitOne() блоки- руется. Если к моменту вызова WaitOne() выполнение метода уже завершено, то блокировка не выполняется, а клиент переходит к вызову ЕпА<Операция>() для возвращенного значения. Принципиальное различие между листингами 8.28 и 8.27 заключается в том, что вызов Еп6<Операция>() в листинге 8.28 гаранти- рованно не приведет к блокировке вызывающей стороны. В листинге 8.29 представлен более практичный способ использования WaitOne() с указанием тайм-аута (10 миллисекунд в данном примере). При указании тайм-аута WaitOne() возвращает управление при завершении метода или при ис- течении интервала тайм-аута (в зависимости от того, какое условие будет вы- полнено первым). Листинг 8.29. Использование WaitOneO для указания тайм-аута ожидания Са1culatorCl1 ent proxy = new CalculatorClient(): lAsyncResult asyncResult = proxy.BeginAdd(2.3.null.null): while(asyncResult.IsCompleted =--- false) { asyncResult.AsyncWaitHandle.WaitOnedO.false): // Возможна блокировка /* Некоторые действия */
Асинхронные вызовы 379 1 int sum = proxy.EndAdcKasyncResult); // Без блокировки В листинге 8.29 используется другое полезное свойство lAsyncResult с име- нем IsCompleted. Свойство IsCompleted позволяет проверить состояние вызова без ожидания или блокировки. IsCompleted даже может вызываться в режиме жесткого опроса (polling): Calculatorclient proxy = new CalculatorClientО: lAsyncResult asyncResult = proxy.BeginAdd(2.3.null .null): proxy. Cl ose(): // В дальнейшем: i f (asy ncRes u 11.1 sCompl eted) ( int sum = proxy.EndAdd(asyncResult): // Без блокировки Debug.Assert(sum == 5); else ( // Делается что-то полезное Но в полной мере возможности свойства AsyncWaitHandle раскрываются при управлении несколькими параллельными асинхронными методами. Статиче- ский метод WaitAll() класса WaitHandle используется для ожидания завершения нескольких асинхронных вызовов, как показано в листинге 8.30. Листинг 8.30. Ожидание завершения нескольких методов Calculatorclient proxy = new Ca 1 culatorClient(): lAsyncResult asyncResult1 = proxy.BeginAddC2.3.null.null): lAsyncResult asyncResult2 = proxy.BeginAdd(4,5.null.null); proxy. Cl ose(): WaitHandle[] handleArray « {asyncResultl.AsyncWaitHandle. asyncResult2.AsyncWaitHandle}; Wai tHandle. Wai tAll (handleArray): int sum: //Вызовы EndAddO не блокируются sum = proxy.EndAdd(asyncResultl): Debug. Assert (sum == 5); sum = proxy.EndAdd(asyncResult2): Debug. Assert (sum == 9): Чтобы использовать WaitAll(), необходимо сконструировать массив WaitHandle. Обратите внимание: для обращения к возвращаемым данным все равно необхо- димо вызвать Еп6<Операция>(). Вместо того чтобы ожидать возврата из всех пе- речисленных методов, можно ожидать возврата любого из них статическим ме- тодом WaitAny() класса WaitHandle. Как и WaitOne(), WaitAll() и WaitAny() существу- ют в нескольких перегруженных версиях, которые позволяют задать интервал тайм-аута вместо неопределенно долгого ожидания.
380 Глава 8. Управление параллельной обработкой Обратные вызовы завершения Помимо блокировки, ожидания и опроса с проверкой завершения асинхронных вызовов WCF предлагает еще одну программную модель — обратные вызовы завершения. Клиент передает WCF метод, которому должен поступать обрат- ный вызов при завершении асинхронного метода. Клиент может передать для обратного вызова как метод экземпляра, так и статический метод, причем один метод обратного вызова может обслуживать завершение нескольких асинхрон- ных вызовов. При завершении выполнения асинхронного метода вместо того, чтобы спокойно вернуться в пул, рабочий поток передает управление методу обратного вызова. Для назначения метода обратного вызова, используемого для оповещения о завершении, клиент должен передать Вед\п<Операция>() делегата типа AsyncCallback, определяемого следующим образом: public delegate void AsyncCallback(lAsyncResult asyncResult); Делегат передается в предпоследнем параметре Вед\п<Операция>(). В лис- тинге 8.31 продемонстрировано управление асинхронными вызовами с исполь- зованием обратного вызова завершения. Листинг 8.31. Оповещение о завершении с использованием методов обратного вызова class MyCllent : IDisposable CalculatorCllent m_Proxy = new CalculatorClient(); public void CallAsyncO m_Proxy.BeginAdd(2,3.0nCompletion.null); } void OnCompletion(lAsyncResult result) { int sum = m_Proxy.EndAdd(result); Debug.Assert(sum == 5); } void DisposeO { m_Proxy.Closet); } } В отличие от программных моделей, описанных ранее, при использовании обратного вызова для оповещения о завершении возвращаемый Begin<Onepa- ция>() объект lAsyncResult сохранять не обязательно — когда WCF активизиру- ет метод обратного вызова, объект lAsyncResult передается в виде параметра. Так как WCF предоставляет уникальный объект lAsyncResult для каждого асин- хронного метода, завершения разных асинхронных вызовов могут обслужи- ваться одним методом обратного вызова: m_Proxy.Beg1nAdd(2.3.0nCompletion,null): m_Proxy.BeginAdd(4.5.0nCompletion,nul1):
Асинхронные вызовы 381 Вместо метода класса также можно воспользоваться локальным анонимным методом: Calculatorclient proxy = new CalculatorClient(); int sum; AsyncCalIback completion = delegate(lAsyncResult result) { sum = proxy.EndAddCresult): Debug.Assert(sum == 5): }: proxy. Begi nAdd (2.3,completi on, nul 1): proxy. Cl ose(); Обратите внимание: анонимный метод задает внешнюю переменную sum для передачи результата операции Add(). Модель оповещения о завершении через методы обратного вызова является предпочтительной во всех приложениях, управляемых событиями. В таких приложениях существуют методы, инициирующие события (или запросы), а также методы, обрабатывающие эти события (и в результате инициирующие собственные события). Модель приложений, управляемых событиями, упроща- ет управление программными потоками, событиями и обратными вызовами, а также обеспечивает хорошую масштабируемость, быстрый отклик и произво- дительность. Управление асинхронными вызовами WCF с применением моде- ли оповещения о завершении через обратные вызовы идеально укладывается в эту архитектуру. Другие варианты (ожидание, блокировка и опрос) остаются для приложений, выполнение которых четко определено, предсказуемо и детер- минировано. Я рекомендую всюду, где это возможно, использовать оповещения о завершении через методы обратного вызова. Обратные вызовы завершения и потоковая безопасность Поскольку метод обратного вызова выполняется в программном потоке из пула, вы должны обеспечить потоковую безопасность как метода обратного вы- зова, так и объекта, который его предоставил. Это означает, что при обращении к переменным клиента должны использоваться объекты синхронизации и бло- кировки. Также необходимо обеспечить синхронизацию между программными потоками на стороне клиента и рабочими потоками из пула, и, возможно, — синхронизацию между несколькими рабочими потоками, параллельно обра- щающимися к методу обратного вызова для обработки завершения своих асин- хронных вызовов. Проследите за тем, чтобы метод обратного вызова заверше- ния был реентерабельным и потоково-безопасным. Передача информации состояния Последний параметр Вед\п<Операция>() называется asyncState. Объект asyncState, называемый объектом состояния, предоставляется в качестве необязательного контейнера для передачи информации той стороне, которой он потребуется. Сторона, обрабатывающая оповещение о завершении метода, может обратиться к контейнерному объекту через свойство AsyncState интерфейса lAsyncResult.
382 Глава 8. Управление параллельной обработкой Конечно, объекты состояния могут использоваться и с другими моделями про- граммирования асинхронных вызовов (блокировка, опрос или ожидание), од- нако наибольшую пользу они приносят в сочетании с обратным вызовом завер- шения. Причина проста: во всех остальных моделях программирования вы должны сами работать с объектом lAsyncResult, и работа с дополнительным кон- тейнером ничего принципиально не изменит. С другой стороны, при использо- вании обратного вызова для оповещений о завершении контейнерный объект обеспечивает единственный способ передачи дополнительных данных методу обратного вызова, имеющему заранее определенную сигнатуру. В листинге 8.32 приведен пример использования объекта состояния для пе- редачи целочисленного параметра методу обратного вызова, оповещающего о завершении. Обратите внимание: метод обратного вызова должен привести свойство AsyncState к фактическому типу. Листинг 8.32. Передача дополнительного параметра с использованием объекта состояния class MyClient : IDisposable { CalculatorCllent m_Proxy = new CalculatorCl1 ent(); public void CallAsync() { Int asyncState = 4: // Для примера используется Int m_Proxy.BeglnAdd(2.3.OnComplet1 on.asyncState); } void OnCompletion(lAsyncResult result) { Int asyncState = (Int)asyncResult.AsyncState; Debug.Assert(asyncState == 4); int sum = m_Proxy.EndAdd(result); } void Disposes { m_Proxy.Closet): } } Объект состояния часто используется для передачи посредника, используе- мого Вед1‘п<Оиер(2цня>(), вместо его сохранения в переменной класса: class MyClient public void CallAsyncO { Calculators lent proxy = new CalculatorC11ent(); p roxy.BeginAdd(2.3,OnComp1e11 on.proxy); proxy.Closet); } void OnCompletlontlAsyncResult result) { Calculatorclient proxy = asyncResult.AsyncState as Calculatorclient; Debug.Assert(proxy != null); int sum = proxy.EndAddtresult);
Синхронные вызовы 383 Debug.Assert(sum == 5): } 1 Синхронизационный контекст обратного вызова завершения Метод обратного вызова завершения может требовать потоковой привязки для выполнения в конкретном синхронизационном контексте. Наиболее характер- ный пример — когда в результате асинхронного вызова должен обновляться пользовательский интерфейс. К сожалению, вам придется вручную перенапра- вить вызов из метода обратного вызова завершения в нужный синхронизацион- ный контекст любыми из описанных ранее способов. В листинге 8.33 представлен метод обратного вызова завершения, который взаимодействует напрямую со своей формой и следит за тем, чтобы обновление пользовательского интерфейса производилось в синхронизационном контексте пользовательского интерфейса. Листинг 8.33. Использование синхронизационного контекста в обратном вызове завершения partial class CalculatorForm : Form I Calculatorclient m_Proxy; Synchronizationcontext m_SynchronizationContext; public MyClientO Initial IzeComponentO: m_Proxy = new CalculatorCl 1 ent(); m_SynchronizationContext = SynchronizationContext.Current; } public void Cal 1 Async(object sender.EventArgs args) m_Proxy.BeginAdd(2.3.0nCompletion.null); void OnCompletion(lAsyncResult result) SendOrPostCallback callback = delegate { Text = "Sum = " + m_Proxy.EndAdd(result); }: m_Synchronizat IonContext.Send(call back.nul1): ) public void OnClose(object sender.EventArgs args) i m_Proxy.Close(): Односторонние асинхронные операции Попытки вызова односторонних операций в асинхронном режиме не имеют осо- бого смысла, потому что одной из главных отличительных особенностей одно- стороннего вызова является получение ответного сообщения, которых у одно-
384 Глава 8. Управление параллельной обработкой сторонних вызовов быть не может. При асинхронном вызове односторонней операции Еп6<Операция>() никогда не блокируется, а исключения не иници- ируются. Если для асинхронного вызова односторонней операции предоставля- ется метод обратного вызова завершения, он будет вызван немедленно после возврата из &ед\п<Операция>(). Единственным объяснением для асинхронного вызова односторонней операции является предотвращение потенциальной бло- кировки одностороннего вызова; в этом случае вместо объекта состояния и ме- тода обратного вызова завершения передаются null. Асинхронная обработка ошибок Во время асинхронного вызова недоступны не только выходные параметры и возвращаемые значения, но и исключения. После вызова &ед\п<Операция>() управление возвращается клиенту, однако это может произойти за какое-то время до того, как асинхронный метод обнаружит ошибку и инициирует ис- ключение, и только по прошествии какого-то времени после этого клиент вызо- вет Еп6<Операция>(). Следовательно, платформа WCF должна каким-то обра- зом сообщить клиенту о выданном исключении и дать клиенту возможность обработать его. Когда асинхронный метод инициирует исключение, оно пере- хватывается посредником, а при вызове клиентом ЕпА<Операция>() посредник «перезапускает» объект исключения, давая клиенту возможность обработать его. Если метод обратного вызова завершения был задан, WCF вызывает его сразу же после получения исключения. Инициируемое исключение соответст- вует контракту сбоев и типу исключения (см. главу 6). ПРИМЕЧАНИЕ--------------------------------------------------------- Если в контракте операций службы определены контракты сбоев, атрибут FaultContract должен при- меняться только к синхронным операциям. После вызова Епс1<Операция> При каждом вызова Вед\п<Операция>() возвращаемый объект lAsyncResult со- держит ссылку на объект WaitHandle, доступный через свойство AsyncWaitHandle. Вызов Еп6<Операция>() для этого объекта не приводит к закрытию дескриптора WaitHandle. Вместо этого дескриптор будет закрыт при уничтожении реализую- щего объекта в ходе уборки мусора. Может случиться так (по крайней мере, теоретически), что приложение отработает асинхронные вызовы быстрее, чем .NET освободит дескрипторы, что приведет к утечке ресурсов. Чтобы этого не произошло, можно явно закрыть дескрипторы после вызова Еп6<Операция>(). Например, для определений из листинга 8.31 это может происходить так: void OnCompletion(lAsyncResult result) { int sum = m_Proxy.EndAdd(result): Debug.Assert(sum == 5): result.AsyncWaitHandle.Closet):
Асинхронные вызовы 385 Асинхронные вызовы и транзакции Транзакции плохо сочетаются с асинхронными вызовами. Во-первых, хорошо спроектированная транзакция имеет малую продолжительность, тогда как глав- ной причиной для использования асинхронных вызовов является нежелатель- ная задержка, обусловленная выполнением операции. Во-вторых, окружающая транзакция клиента не будет распространяться к службе по умолчанию, потому что асинхронная операция вызывается в рабочем, а не клиентском потоке. Хотя можно разработать специализированный механизм, использующий клонирова- ние транзакций, этот путь в лучшем случае непрост и его следует избегать. На- конец, при завершении транзакции не должно оставаться никаких остаточных действий на заднем плане, которые могут закрепляться или отменяться незави- симо от транзакции, тогда как вызовы асинхронных операций из транзакций приведут именно к такому результату. Не смешивайте транзакции с асинхрон- ными вызовами. Синхронные и асинхронные вызовы Хотя теоретически одна и та же служба может вызываться как синхронно, так и асинхронно, вероятность такого развития событий невысока. Дело в том, что асинхронное использование службы приводит к радикаль- ным структурным изменениям в клиенте; соответственно, клиент попросту не сможет использовать ту же логику выполнения, как при синхронном доступе. Для примера возьмем электронный магазин. Предположим, клиент (серверный объект, обрабатывающий запрос заказчика) обращается к службе Store с под- робной информацией о заказе. Служба Store использует три вспомогательные службы для обработки заказа: Order (заказ), Shipment (доставка) и Billing (вы- писка счета). В синхронном сценарии служба Store вызывает службу Order для размещения заказа. Только если службе Order удается успешно обработать заказ (то есть если товар имеется на складе), служба Store вызывает службу Shipment, и только при успешном выполнении Shipment служба Store обращается к Billing для выписки счета заказчику. Последовательность действий показана на рис. 8.4. Рис- 8-4- Синхронная последовательность действий Недостаток схемы, показанной на рис. 8.4, состоит в том, что магазин дол- жен обрабатывать заказы синхронно и последовательно. На первый взгляд мо- жет показаться, что асинхронные обращения к вспомогательным объектам
386 Глава 8. Управление параллельной обработкой ускорят работу Store, так как входящие заказы будут обрабатываться сразу же после поступления. Однако обращения к службам Order, Shipment и Billing могут завершиться сбоями независимо друг от друга, и тогда в системе разразится на- стоящий хаос. Например, служба Order обнаружит, что на складе нет товаров, соответствующих клиентскому запросу, а служба Shipment попытается доста- вить несуществующий товар, тогда как служба Billing уже выставила клиенту счет за него. Использование асинхронных обращений к взаимодействующим службам потребует изменений как в программном коде, так и в рабочем процессе. Для асинхронного вызова вспомогательных объектов служба Store должна вызывать только службу Order, которая, в свою очередь, вызовет службу Shipment только в том случае, если обработка заказа прошла успешно (рис. 8.5). И только после успешного оформления заказа служба Shipment асинхронно вызовет службу Billing. Рис. 8.5. Видоизмененная последовательность действий при асинхронной обработке заказа Итак, использование асинхронных вызовов вместо синхронных приводит к радикальным изменениям в интерфейсе служб и схеме работы клиента. Асин- хронный вызов службы, рассчитанной на синхронное выполнение, работает только в условиях логической изоляции. Если вы имеете дело с несколькими взаимодействующими службами, лучше просто породить новый рабочий поток для их вызова, а затем использовать его для реализации асинхронного выпол- нения. В этом случае будет сохранен как интерфейс служб, так и исходная по- следовательность выполнения.
Очереди WCF позволяет клиенту взаимодействовать со службой в отключенном (dis- connected) режиме. Клиент посылает сообщения службе, а служба их обрабаты- вает. Такой режим взаимодействия открывает некоторые новые возможности, но его программная модель отличается от тех, которые рассматривались нами до настоящего момента. В начале этой главы читатель узнает, как создать и на- строить простую службу с очередью запросов. Далее будут рассмотрены раз- личные аспекты новой модели (в частности, транзакции, управление экземпля- рами и сбои) и их влияние как на бизнес-модель службы, так и на ее реализа- цию. В конце главы я представлю свою библиотеку для создания ответных служб и HTTP-мостов с очередями вызовов в Интернете. гключенные службы и клиенты Все предыдущие главы строились на предположении о наличии постоянного подключения между клиентом и службой; чтобы взаимодействовать друг с дру- гом, обе стороны должны находиться в постоянной готовности. Однако сущест- вует немало факторов, оправдывающих применение модели отключенного взаимодействия в служебно-ориентированном приложении. О Доступность — у клиента может возникнуть необходимость в использова- нии службы даже в отключенном состоянии (например, при работе на мо- бильном устройстве). Запросы ставятся в локальную очередь и передаются службе при подключении клиента. Служба тоже может быть временно не- доступна — например, из-за проблем с сетью, однако вы хотите, чтобы кли- енты могли продолжать работу даже в этом случае. При подключении служ- ба извлекает необработанные запросы из очереди. Может оказаться и так, что клиент со службой работают, но недоступен сетевой канал — но при этом и клиент, и служба хотят продолжить свою работу. Использование оче- редей на обеих сторонах дает такую возможность. О Изоляция фаз — если бизнес-логику удается разделить на несколько опера- ций (возможно, даже транзакционных), разделенных по времени — то есть
388 Глава 9. Очереди когда все операции должны быть выполнены, но не обязательно в конкретном порядке, — использование очередей улучшает доступность и производитель- ность. Операции ставятся в очередь и выполняются независимо друг от друга. О Компенсация — если на завершение бизнес-транзакции требуются часы и даже целые дни, она обычно делится как минимум на две транзакции. Первая ста- вит в очередь операции, которые должны быть выполнены немедленно, а вто- рая проверяет успех первой и при необходимости компенсирует ее неудачу. О Балансировка нагрузки — нагрузка (load) обычно определяется количеством клиентов, ожидающих обработки, а напряжение (stress) — количеством па- раллельных вызовов. Если не принять специальных мер, высокая нагрузка обернется прямым напряжением для системы. В большинстве систем уров- ни нагрузки и напряжения не являются постоянными, как показано на рис. 9.1. Если система проектируется в расчете на пиковую нагрузку, это приведен к не- эффективному использованию системных ресурсов во время большей части цикла загрузки. Если система спроектирована в расчете на среднюю нагруз- ку, она не справится с пиковой нагрузкой. По тем же причинам необходимо анализировать не только абсолютный уровень нагрузки, но и скорость его изменения (первую производную). Даже если неожиданный бросок нагруз- ки и не превышает возможности вашей системы в абсолютных показателях, может оказаться, что система не успевает своевременно обработать его, и скорость отклика оказывается неприемлемо низкой. При использовании очередей запросов служба может просто сбросить лишнюю нагрузку в оче- редь и обработать тогда, когда это будет удобно. Такой подход позволяет проектировать систему в расчете на номинальное среднее значение желае- мой производительности, а не на максимальную нагрузку. Рис. 9.1. Колебания нагрузки Очереди вызовов В WCF поддержка очередей вызовов представлена классом NetMsmqBinding. Вместо передачи сообщения по протоколу TCP, HTTP или IPC используется протокол MSMQ. WCF упаковывает сообщение SOAP в сообщение MSMQ
Очереди вызовов 389 и отправляет его в заданную очередь. Обратите внимание: между сообщениями WCF и сообщениями MSMQ не существует однозначного соответствия, как не существует прямого соответствия между сообщениями WCF и пакетами TCP Одно сообщение MSMQ может содержать несколько сообщений WCF или только одно, в зависимости от контрактного сеансового режима (см. далее). Вместо отправки сообщения WCF «живой» службе, клиент отправляет сообще- ние в очередь MSMQ. Клиент взаимодействует только с очередью, а не с конеч- ной точкой службы. В результате вызовы изначально являются асинхронными и отключенными. Они будут выполнены позже, когда служба обработает сооб- щения (что делает их асинхронными), а служба или клиент взаимодействуют слокальными очередями (что делает возможным отключенные вызовы). Архитектура очередей вызовов Как всегда бывает при работе со службами WCF, клиент взаимодействует с по- средником (рис. 9.2). Рис. 9-2. Архитектура очередей вызовов Но поскольку посредник настроен на использование привязки MSMQ, он не отправляет сообщение WCF какой-то определенной службе. Вместо этого вы- зов (или вызовы) преобразуется в сообщение (или сообщения) MSMQ и от- правляется в очередь, указанную в адресе конечной точки. На стороне службы, при запуске хоста с конечной точкой, использующей очередь, хост устанавлива- ет слушателя (listener) очереди; на концептуальном уровне он напоминает слу- шателей, связываемых с портами при использовании TCP и HTTP. Слушатель очереди обнаруживает новые сообщения, извлекает их из очереди, а затем создает на стороне хоста цепочку перехватчиков, завершаемую диспетчером. Диспетчер вызывает экземпляр службы, как обычно. Если в очередь поставлено несколько сообщений, слушатель может создавать новые экземпляры сразу же
390 Глава 9. Очереди при выходе сообщений из очереди; таким образом, мы получаем асинхронные параллельные вызовы в отключенном режиме. Если хост отключен, сообщения просто находятся в очереди. При следую- щем подключении хоста они будут переданы службе. Разумеется, если и кли- ент, и служба работают и находятся в подключенном состоянии, хост обрабаты- вает вызовы немедленно. Контракты очередей Отключенный вызов, поставленный в очередь, не может возвращать никаких значений, потому что отправка сообщения в очередь не сопровождается вызо- вом какой-либо логики службы. Более того, вызов может быть доставлен служ- бе и обработан уже после того, как клиентское приложение завершило работу, когда возвращаемое значение попросту некому обрабатывать. По тем же со- ображениям вызов не может возвращать клиенту исключения со стороны службы — может оказаться, что клиент не сможет перехватить и обработать исключение. WCF запрещает использовать контракты сбоев для операций с очередями. Поскольку клиент не может блокироваться при вызове операции (а вернее, он блокируется на очень короткий промежуток времени, необходи- мый для постановки сообщения в очередь), вызовы с очередями изначально асинхронны с точки зрения клиента. Все перечисленные характеристики явля- ются классическими для односторонних вызовов. Соответственно, любой кон- тракт, предоставляемый конечной точкой с использованием NetMsmqBinding, мо- жет содержать только односторонние операции, и WCF проверяет выполнение этого условия во время загрузки службы (и посредника): // Контракты с очередями содержат только односторонние вызовы [ServiceContract] interface IMyContract [OperationContractdsOneWay = true)] void MyMethodO: } Поскольку взаимодействие c MSMQ инкапсулировано в привязке, в коде вызова службы или клиента ничто не указывает на то обстоятельство, что вы- зов помещается в очередь. Код службы и клиента ничем не отличается от любо- го другого кода службы или клиента WCF, как показано в листинге 9.1. Листинг 9.1. Реализация и использование службы с очередями //////////////////////// Сторона службы /////////////////////////// [ServiceContract] interface IMyContract { [OperationContractdsOneWay = true)] void MyMethodO: } class MyService : IMyContract { public void MyMethodO
Очереди вызовов 391 //////////////////////// Сторона клиента /////////////////////////// MyContractClient proxy = new MyContractClient(); proxy. MyMethod(): proxy.Close(): Конфигурация и настройка При определении конечной точки для службы с очередями адрес конечной точ- ки должен включать имя очереди и ее тип. В MSMQ определяются два типа очередей: общедоступные и приватные. Общедоступные очереди требуют уста- новки контроллера домена MSMQ и могут вызываться с выходом за границу компьютера. Реально эксплуатируемые приложения часто требуют использова- ния общедоступных очередей вследствие их безопасной и отключенной приро- ды. Приватные очереди являются локальными по отношению к компьютеру, на котором они находятся, и не требуют контроллера домена. Такой вариант раз- вертывания MSMQ называется установкой для рабочих групп. На стадии разра- ботки, а также для приватных очередей, созданием и администрированием ко- торых они занимаются, разработчики обычно выбирают установку для рабочих групп. Тип очереди (приватная или общедоступная) указывается в адресе ко- нечной точки: <endpoi nt address = "net.msmq://localhost/private/MyServiceQueue" binding = "netMsmqBinding" /> Для общедоступных очередей обозначение public можно опустить, для при- ватных очередей обозначение private обязательно. Также обратите внимание на отсутствие знака $ в типе очереди. Установка для рабочих групп и безопасность При использовании приватных очередей в установке для рабочих групп необ- ходимо отключить безопасность MSMQ на стороне клиента и службы. В гла- ве 10 рассматривается настройка безопасности вызовов WCF, в том числе и вы- зовов, направленных в очередь. В двух словах, конфигурация безопасности MSMQ по умолчанию предполагает, что пользователи предоставляют сертифи- каты для проверки подлинности, а сертификатная безопасность MSMQ требует контроллера домена MSMQ. Выбор безопасности Windows для обеспечения транспортной безопасности MSMQ требует интеграции с Active Directory, не- возможной в установках MSMQ для рабочих групп. В листинге 9.2 показано, как отключается безопасность MSMQ. Листинг 9.2. Отключение безопасности MSMQ <system. servi ceModel > <endpoint name = ... address = "net.msmq://localhost/private/MyServiceQueue" binding = "netMsmqBinding" bindingconfiguration = "NoMSMQSecurity" продолжение &
392 Глава 9. Очереди contract = "IMyContract" /> <bindings> <netMsmqBinding> <binding name = "NoMSMQSecurity"> <security mode = ”None"> </security> </binding> </netMsmqBinding> </bindings> </system.serviceModel> Создание очереди Как на стороне службы, так и на стороне клиента очередь должна создаваться заранее, до постановки в нее клиентских вызовов. Существует несколько спосо- бов создания очереди. Администратор (или разработчик во время разработки) может создать очередь при помощи панели управления MSMQ, однако это руч- ная работа, которую стоило бы автоматизировать. Хостовой процесс может ис- пользовать API System.Messaging для проверки существования очереди перед открытием хоста. Класс MessageQueue содержит метод Exists() для проверки и методы Create() для создания очереди: public class MessageQueue : ... { public static MessageQueue Create(string path): //Нетранзакционный public static MessageQueue Create(string path,bool transactional): public static bool Exists(string path): public void PurgeO; //... } Если очередь не существует, хостовой процесс может сначала создать ее, а затем переходить к открытию хоста. Процедура продемонстрирована в лис- тинге 9.3. Листинг 9.3. Проверка существования очереди в хосте ServiceHost host = new ServiceHost(typeof(MyService)); if(MessageQueue.Exists(@",\private$\MyServiceQueue") == false) { MessageQueue.Create(@".\pri vate$\MyServi ceQueue",true): } host.OpenO; В этом примере хост проверяет наличие очереди перед открытием хоста для установки MSMQ на своем компьютере. Если потребуется, код хостинга созда- ет очередь. Обратите внимание на использование параметра true для транзакци- онной очереди (см. далее) и на присутствие знака $ в типе очереди. У листин- га 9.3 имеется один очевидный недостаток: жесткое кодирование имени очереди. Лучше прочитать имя очереди из конфигурационного файла приложе- ния, сохраненное в виде параметра приложения. Впрочем, у этого подхода есть
Очереди вызовов 393 и другие недостатки. Во-первых, вы должны постоянно синхронизировать имя очереди в параметрах приложения с адресом конечной точки. Во-вторых, этот код должен повторяться во всех ситуациях с использованием службы с очере- дями. К счастью, код из листинга 9.3 можно инкапсулировать и автоматизиро- вать в классе ServiceHost<T>, как показано в листинге 9.4. Листинг 9.4. Создание очередей в ServiceHost<T> public class ServiceHost<T> : ServiceHost ( protected override void OnOpeningO ( foreach(ServiceEndpoint endpoint in Description.Endpoints) { QueuedServi ceHelper.Veri fyQueue(endpoi nt); } base.OnOpening(); ) //... public static class QueuedServiceHelper ( public static void VerifyQueue(ServiceEndpoint endpoint) if(endpoint.Binding is NetMsmqBinding) string queue = GetQueueFromllri (endpoint.Address.Uri); if(MessageQueue.Exists(queue) == false) MessageQueue.Create(queue.true); } } } // Выделение имени очереди из адреса static string GetQueueFromUri(Uri uri) (...) 1 В листинге 9.4 ServiceHost<T> переопределяет метод OnOpeningO своего базо- вого класса. Этот метод вызывается перед открытием хоста, но после вызова метода Open(). ServiceHost<T> перебирает элементы коллекции настроенных ко- нечных точек. Если текущая конечная точка использует привязку NetMsmqBin- ding (то есть ожидается постановка вызовов в очередь), ServiceHost<T> вызывает статический вспомогательный класс QueuedServiceHelper, передает конечную точку и просит проверить очередь. Метод VerifyQueue() класса QueuedService- Helper выделяет имя очереди из адреса конечной точки, и если потребуется, создает очередь при помощи кода, сходного с листингом 9.3. При использовании ServiceHost<T> листинг 9.3 сокращается до следующего вида: ServiceHost<MyService> host = new ServiceHost<MyService>(): host.OpenO;
394 Глава 9. Очереди Клиент тоже должен проверить существование очереди перед тем, как на- правлять в нее вызовы. Необходимые действия на стороне клиента показаны в листинге 9.5. Листинг 9.5. Проверка очереди на стороне клиента л f(MessageQueue.Ext sts(.\pr1vate$\MyServiceQueue") == false) { MessageQueue. Created" .\private$\MyServiceQueue" .true); } MyContractClient proxy = new MyContractClientO; proxy.MyMethocK); proxy.Close(): И снова вместо жесткого кодирования имени очереди следует прочитать его из конфигурационного файла приложения. И снова вы сталкиваетесь с пробле- мой постоянной синхронизации имени очереди в параметрах приложения с ад- ресом конечной точки. Класс QueuedServiceHelper можно использовать напря- мую с конечной точкой, но это заставит вас создать посредника (или экземпляр ServiceEndpoint) только для проверки существования очереди. Расширение клас- са QueuedServiceHelper с поддержкой проверки очереди на стороне клиента пока- зано в листинге 9.6. Листинг 9.6. Расширение QueuedServiceHelper для проверки очереди на стороне клиента public static class QueuedServiceHelper { public static void VerifyQueue<T>(string endpointName) where T : class { ChannelFactory<T> factory = new ChannelFactory<T>(endpointName); Ver i fyQueue(factory.Endpoint); } public static void VerifyQueue<T>() where T ; class { VerifyQueue<T>(""); } // To же. что в листинге 9.4 public static void VerifyQueue(ServiceEndpoint endpoint) {...} //... } В листинге 9.6 класс QueuedServiceHelper дополняется двумя методами. Вер- сия метода VerifyQueue<T>(), получающая имя конечной точки, использует Chan- nelFactory для чтения конечной точки из конфигурационного файла, а затем вызывает метод VerifyQueue() из листинга 9.4. Версия VerifyQueue<T>() без аргу- ментов использует конечную точку по умолчанию для заданного типа контрак- та из конфигурационного файла. При использовании QueuedServiceHelper лис- тинг 9.5 сокращается до следующего вида: QueuedServiceHelper.VerifyQueue<IMyContract>(): MyContractClient proxy = new MyContractClientO;
Очереди вызовов 395 proxy. MyMet hod (): proxy. Cl ose(): Обратите внимание: клиент должен проверить существование очереди для каждого контракта с очередями. Очистка очереди При запуске хоста очередь уже может содержать сообщения, полученные MSMQ во время отключения хоста; тогда хост приступает к обработке этих со- общений. Обработка этого сценария является одной из важнейших возможно- стей служб с очередями, обеспечивающих возможность работы служб в отклю- ченном режиме. Хотя такое поведение желательно при развертывании службы с очередями, на стадии отладки оно создает немало хлопот. Представьте себе отладочный сеанс службы с очередями. Клиент выдает несколько вызовов, служба начинает обработку первого вызова, и во время пошагового выполнения кода обнаруживается дефект. Вы прекращаете отладку, изменяете код службы и перезапускаете хост — а он приступает к обработку сообщений, оставшихся в очереди с предыдущего отладочного сеанса, даже если эти сообщения наруша- ют новый код службы. Обычно сообщения из одного отладочного сеанса не должны переходить в следующий сеанс. Проблема решается программной очи- сткой очереди при отключении хоста только в отладочном режиме. Для этого в шаблон ServiсеHost<T> включается фрагмент, показанный в листинге 9.7. Листинг 9.7. Очистка очереди при завершении хоста в отладочном режиме public static class QueuedServiceHelper ( public static void PurgeQueue(Serv1ceEndpoint endpoint) { if(endpoint.Binding is NetMsmqBindlng) { string queueName = GetQueueFromUri(endpoint.Address.Uri); if(MessageQueue.Exists(queueName) == true) { MessageQueue queue = new MessageQueue(queueName): queue.Purge(); } ) public class ServiceHost<T> : ServiceHost protected override void OnClosingO { PurgeQueues(); //Дополнительная зачистка (если потребуется) base.OnClosing(); [Conditional("DEBUG")] void PurgeQueues() ( foreach(ServiceEndpoint endpoint in Description.Endpoints) продолжение &
396 Глава 9. Очереди ( QueuedServiceHelper.PurgeQueue(endpoi nt); } } //... } В этом примере класс QueuedServiceHelper содержит статический метод Purge- Queue(). При вызове ему передается конечная точка службы. Если конечная точка использует привязку NetMsmqBinding, PurgeQueue() извлекает имя очереди из адреса конечной точки, создает новый объект MessageQueue и очищает его со- держимое. ServiceHost<T> переопределяет метод OnCLosing(), вызываемый при корректном завершении хоста. Затем вызывается приватный метод Purge- Queues(). Этот метод помечен атрибутом Conditional с условием DEBUG. Это озна- чает, что хотя тело PurgeQueues() всегда компилируется, но вызывается условно по определению символического имени DEBUG. Таким образом, OnClosing() будет вызывать PurgeQueues() только в отладочном режиме. PurgeQueues() перебирает все конечные точки хоста, вызывая для каждой из них QueuedServiceHelper. PurgeQueue(). Атрибут Conditional рекомендуется использовать в .NET для условной ком- пиляции. Он избегает всех проблем, связанных с условной компиляцией #if. Очереди, службы и конечные точки В WCF для каждой конечной точки службы должна создаваться отдельная оче- редь. Другими словами, служба с двумя контрактами должна иметь две очереди для разных конечных точек: <service name = "MyService"> <endpoint address = "net.msmq://localhost/private/MyServiceQueuel” binding = "netMsmqBinding" contract = "IMyContract" /> <endpoint address = "net.msmq://localhost/private/MyServiceQueue2" binding = "netMsmqBinding" contract = "IMyOtherContract" /> </service> Дело в том. что клиент фактически взаимодействует с очередью, а не с ко- нечной точкой службы. Более того, служба может вообще быть недоступна - только очередь. Две конечные точки не могут использовать общую очередь, по- тому что они будут получать чужие сообщения. Так как сообщения WCF в со- общениях MSMQ окажутся несоответствующими, WCF игнорирует сообще- ния, которые сочтет недействительными, и это приведет к потере вызовов. По тому же принципу две полиморфные конечные точки двух служб не могут ис- пользовать общую очередь, потому что они будут получать сообщения друг друга.
Очереди вызовов 397 Предоставление метаданных WCF не может передавать метаданные через MSMQ. По этой причине даже те службы, которые всегда используют только очереди вызовов, обычно пре- доставляют конечную точку МЕХ или разрешают передачу метаданных через HTTP- GET, потому что клиентам службы все равно требуется механизм полу- чения описания службы. Транзакции MSMQ является транзакционным менеджером ресурсов WCF. При создании очереди (на программном или административном уровне) можно создать ее как транзакционную. Содержимое транзакционных очередей устойчиво, а сообще- ния хранятся на диске. Что еще важнее, отправка и удаление сообщений из очереди всегда осуществляются в транзакциях. Если код, который пытается взаимодействовать с очередью, имеет окружающую транзакцию, то очередь автоматически присоединяется к этой транзакции. Если окружающей транзак- ции нет, MSMQ создает для этого взаимодействия новую транзакцию. Все про- исходит так, как если бы очередь была заключена в TransactionScope в режиме TransactionScopeOption.Required. В транзакции состояние очереди отменяется или закрепляется вместе с обращающимися транзакциями. Например, если транзакция отправляет сообщение в очередь, а затем отменяется, очередь отвер- гает сообщение. Доставка и воспроизведение Если нетранзакционпый клиент обращается к службе с очередью, клиентские сбои после вызовов не отменят отправки сообщения в очередь, а находящийся в очереди вызов будет доставлен службе. Тем не менее клиент, вызывающий службу с очередью, может поступить подобным образом в транзакции, как по- казано на рис. 9.3. Рис. 9.3. Отправка сообщения в клиентскую очередь Клиентские вызовы преобразуются в сообщения WCF, а затем упаковыва- ются в сообщениях MSMQ. Если клиентская транзакция закрепляется, сообще- ния MSMQ отправляются в очередь. При отмене транзакции очередь отвергает эти сообщения. В сущности, WCF предоставляет клиентам службы с очередя- ми механизм автоматической отмены асинхронных вызовов, потенциально
398 Глава 9. Очереди выполняемых в отключенном режиме. Обычные асинхронные вызовы с под- ключением плохо сочетаются (или вовсе не сочетаются) с транзакциями, пото- му что отправленный вызов уже не удастся отозвать в случае отмены исходной транзакции. В отличие от асинхронных вызовов с подключением, вызовы служб с очередями спроектированы именно для этого транзакционного сцена- рия. Кроме того, клиент может взаимодействовать с несколькими службами с очередями в одной транзакции. Отмена клиентской транзакции по какой-ли- бо причине автоматически отменяет все вызовы, стоящие в очередях. Транзакция доставки Так как клиент со службой могут находиться на разных компьютерах, и по- скольку клиент и/или служба могут быть отключены, MSMQ также поддержи- вает очередь на стороне клиента. Клиентская очередь выполняет функции «посредника» для очереди на стороне сервера. В случае удаленного вызова с очередью клиент сначала отправляет сообщение в очередь на стороне клиента. При подключении клиента MSMQ доставляет сообщения из очереди на сторо- не клиента в очередь на стороне службы, как показано на рис. 9.4. Клиентская Очередь очередь службы — Транзакция доставки ► — Рис. 9.4. Транзакция доставки Так как MSMQ является менеджером ресурса, удаление сообщения из оче- реди на стороне клиента создает транзакцию (если очередь действительно яв- ляется транзакционной). В случае если MSMQ не удается доставить сообщение в очередь на стороне службы независимо от причины (сбой сети, зависание компьютера и т. д.), то удаление сообщения из очереди клиентской стороны от- меняется, как и отправка сообщения в очередь на стороне службы; сообщение снова оказывается в очереди на стороне клиента, a MSMQ снова пытается дос- тавить его. Хотя вы можете настроить и организовать обработку сбоев (см. да- лее), за исключением фатальных и в принципе неустранимых ошибок, службы с очередями обеспечивают гарантированную доставку — если существует тех- ническая возможность доставки сообщения (в пределах режимов обработки сбо- ев), сообщение рано или поздно будет доставлено от клиента к службе. Так в WCF обеспечивается надежная доставка сообщений с очередями. Конечно, в данном случае не существует прямой поддержки протокола надежной доставки, как при вызовах с подключением; речь идет всего лишь об аналогичном механизме. Транзакция воспроизведения Удаление сообщения из очереди для воспроизведения (playback) на стороне службы инициирует создание новой транзакции (предполагается, что очередь является транзакционной), как показано на рис. 9.5.
Очередь службы Рис. 9.5. Транзакция воспроизведения Службы обычно настраиваются на участие в транзакции воспроизведения. Если транзакция воспроизведения отменяется (обычно из-за исключений на стороне службы), сообщение откатывается в очередь, где WCF снова обнару- живает его и доставляет службе. Фактически так при этом реализуется меха- низм автоматического повтора. Следовательно, обработка службой вызовов из очереди должна выполняться относительно быстро, в противном случае возни- кает риск отмены транзакции воспроизведения. Важно заметить, что вызовы с использованием очередей нельзя приравни- вать к продолжительным асинхронным вызовам. При асинхронной доставке вызовов служба может не торопиться с обработкой вызова, а транзакции со сто- роны клиента в ней обычно не участвуют. В случае вызовов с очередями обра- ботка службы не должна приводить к тайм-ауту транзакции воспроизведе- ния — в этом случае WCF повторно отправит вызов, поскольку у службы име- ется входная транзакция. Настройка транзакций служб Как только что было показано, в передаче каждого вызова через очередь (при условии использования транзакционных очередей) участвуют три транзакции: клиентская, транзакция доставки и транзакция воспроизведения (рис. 9.6). Клиентская транзакция Клиент Очередь клиента Очередь службы Транзакция доставки Транзакция воспроизведения Служба Рис. 9.6. Вызовы с очередями и транзакции
400 Глава 9. Очереди С точки зрения архитектуры вам вряд ли придется изображать транзакцию доставки на своих структурных схемах; просто принимайте ее как должное. Кроме того, служба никогда не будет участвовать в клиентских транзакциях, поэтому четыре логических режима транзакций из главы 7 (клиент, клиент/ служба, служба, без транзакций) в данном случае неприменимы. Если операция контракта службы настроена в режиме TrasactionFlowOption.Allowed или TrasactionFlowOption.NotAllowed, результат в обоих случаях будет одинаковым - клиентская транзакция никогда не предоставляется службе. Более того, режим TrasactionFlowOption.Mandatory запрещен для конфигураций, включающих контрак- ты с очередями; это требование проверяется во время загрузки службы. Настоя- щим вопросом является связь между транзакцией воспроизведения и транзакци- онной конфигурацией службы. Участие в транзакции воспроизведения С точки зрения WCF транзакция воспроизведения рассматривается как вход- ная транзакция службы. Чтобы служба участвовала в транзакции воспроизве- дения, поведение ее операций должно быть настроено с истинным значением TransactionScopeRequired, как показано в листинге 9.8 и на рис. 9.5. Листинг 9.8. Участие в транзакции воспроизведения [ServiceContract] interface IMyContract { [OperationContractdsOneWay = true)] void MyMethodO: } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO { Transaction transaction = Transaction.Current: Debug.Assert(transact!on.Transact!onlnformation. Distributedldentifier != Guid.Empty): } } Очередь службы Рис. 9.7. Игнорирование транзакции воспроизведения
Очереди вызовов 401 В листинге 9.8 стоит обратить внимание на интересное обстоятельство: как в MSMQ 3.0, так и в MSMQ 4.0 для управления транзакциями всегда использу- ется менеджер DTC, даже в случае одной службы с одним воспроизведением. Игнорирование транзакции воспроизведения Если служба настроена на отсутствие транзакций (как в листинге 9.9 и на рис. 9.7), WCF все равно будет использовать транзакцию для чтения сообще- ния из очереди, хотя транзакция всегда будет завершаться успешно (не считая непредвиденных сбоев в подсистеме MSMQ). Исключения и сбои самой служ- бы не отменяют транзакцию воспроизведения. Листинг 9.9. Игнорирование транзакции воспроизведения [ServiceContract] interface IMyContract ( [OperationContractdsOneWay = true)] void MyMethodO; ) [Servi ceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { public void MyMethodO Transaction transaction = Transaction.Current: Debug.Assert(transaction == null); } ) У служб, не участвующих в транзакции воспроизведения, WCF не использу- ет автоматический повтор в случае сбоя. Главной причиной для отказа от авто- матического повтора является продолжительная обработка; если вызов не уча- ствует в транзакции воспроизведения, до его завершения может пройти сколь- ко угодно времени. Использование отдельной транзакции Службу также можно запрограммировать с ручным созданием новой транзак- ции, как показано в листинге 9.10 и на рис. 9.8. Листинг 9.10. Использование новой транзакции [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract ( public void MyMethodO ( usingdransactionScope scope = new Transacti onScopedransacti onScopeOpti on.RequiresNew)) { scope.CompleteO; } } )
402 Глава 9. Очереди Очередь службы Рис. 9.8. Использование новой транзакции Если служба использует собственную новую транзакцию, она также не должна использовать транзакцию воспроизведения (используя принятое по умолчанию значение false свойства TransactionScopeRequired). Конечно, при этом она лишается преимуществ механизма автоповтора. Отделение новой транзак- ции от транзакции воспроизведения дает службе возможность выполнить свою собственную транзакционную работу. Как правило, служба настраивается на использование собственной транзакции, когда вызываемая из очереди опера- ция относится к категории «полезных, но не критичных»; она должна выпол- няться под защитой транзакции, но без повторных попыток в случае сбоя. Неустойчивые очереди Очереди MSMQ, описанные до настоящего момента, были как устойчивыми, так и транзакционными. Сообщения сохранялись на диске, а операции отправ- ки и чтения сообщения из очереди были транзакционными. MSMQ также под- держивает нетранзакционные неустойчивые очереди. Их содержимое хранится в памяти, а транзакции не используются. В случае сбоя или отключения компь- ютера все сообщения очереди теряются. При создании очереди (как в адми- нистративном приложении MSM, так и на программном уровне) можно ука- зать, должна ли она быть транзакционной или нет; выбор закрепляется на весь жизненный цикл очереди. Неустойчивые очереди не обладают никакими преимуществами транзакционного обмена сообщениями: автоматической отме- ной, гарантированной доставкой или автоповтором. WCF может работать с не- устойчивыми очередями, хотя делать это и не рекомендуется. MsmqBindingBase (базовый класс NetMsmqBinding) содержит логическое свой- ство Durable, по умолчанию равное true. public abstract class MsmqBindingBase : Binding,... { public bool Durable {get;set;} public bool ExactlyOnce {get;set;} //... } public class NetMsmqBinding ; MsmqBindingBase {...}
Управление экземплярами 403 Если свойство Durable содержит false, WCF не использует транзакции при обращении к очереди. И клиент, и служба должны использовать одинаковые значения Durable. При использовании неустойчивой очереди, в случае отмены клиентской транзакции, сообщения остаются в очереди и доставляются службе. В случае отмены транзакции воспроизведения сообщение теряется. Из-за отсутствия гарантированной доставки при использовании неустойчи- вых очередей WCF также требует, чтобы свойство ExactlyOnce привязки было равно false (по умолчанию оно равно true); в противном случае WCF иницииру- ет исключение' InvalidOperationException при загрузке службы. Таким образом, конфигурация неустойчивой очереди выглядит так: <netMsmqBi ndi ng> <binaing name ="VolatileQueue" durable = "false" exactlyOnce = "false"> </bi ndi ng> </netMsmqBi nd i ng> Управление экземплярами Сеансовый режим контракта и режим управления экземплярами службы ока- зывают решающее влияние на поведение служб с очередями, на воспроизведе- ние вызовов, на общую логику работы программы и действующие допущения. Службы уровня вызова Для служб уровня вызова клиент не может узнать о том, что вызовы в конеч- ном счете попадают к службе уровня вызова с очередями. Все, что видит кли- ент, — это сеансовый режим контракта. Если свойство SessionMode контракта равно SessionMode.Allowed или SessionMode.NotAllowed, вызов с постановкой в очередь (через привязку NetMsmqBinding) считается не-сеансовым. Все вызовы к не-сеансовым конечным точкам помещаются в отдельные сообщения MSMQ, при этом для каждого вызова создается одно отдельное сообщение MSMQ. Нетранзакционные клиенты Если клиент без окружающей транзакции вызывает не-сеансовую конечную точку с очередью (как в листинге 9.11), сообщения MSMQ, сгенерированные для каждого вызова, ставятся в очередь сразу же после вызова. Если у клиента возникает исключение, сообщения, отправленные до этого момента, не отверга- ются и доставляются службе. Листинг 9.11. Нетранзакционный клиент не-сеансовой конечной точки с очередью using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress)) { MyContractClient proxy = new MyContractClient(): proxy.MyMethod()://Сообщение ставится в очередь proxy.MyMethod();//Сообщение ставится в очередь proxy.Close():
404 Глава 9. Очереди Транзакционные клиенты С транзакционным клиентом (то есть при нахождении клиентского кода в ок- ружающей транзакции) не-сеансовой конечной точки с очередью, как в листин- ге 9.12, все сообщения, соответствующие вызовам, отправляются в очередь при закреплении клиентской транзакции. Если клиентская транзакция отменяется, все сообщения удаляются из очереди, а все вызовы отменяются. Листинг 9.12. Транзакционный клиент не-сеансовой конечной точки с очередью usingdransactionScope scope = new TransactionScopeO) { MyContractClient proxy = new MyContractClientO: proxy.MyMethodO://Сообщение записывается в очередь proxy.MyMethodO-.//Сообщение записывается в очередь proxy.Closet): scope. Compl eteO: }//Сообщения закрепляются в очереди Не существует связи между посредником и окружающей транзакцией. Если клиент использует транзакционный контекст, как в листинге 9.12, он может за- крыть посредника как внутри, так и за пределами транзакционного контекста и продолжить использование посредника даже после транзакции или в новой транзакции. Клиент также может закрыть посредника до или после вызова Complete(). Обработка на уровне вызова На стороне хоста вызовы из очереди доставляются службе по отдельности, при этом каждый вызов воспроизводится в отдельном экземпляре службы. Это про- исходит даже в том случае, если служба работает в сеансовом режиме управле- ния экземплярами. Чтобы программный код был более понятным, при исполь- зовании не-сеансовых контрактов с очередями всегда следует явно настраивать службу на режим уровня вызова и запрещать сеансы в контракте: [ServiceContracttSessionMode = SessionMode.NotAllowed)] interface IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract {•••} После каждого вызова экземпляр службы уничтожается, как и в случае под- ключенных служб уровня вызова. Служба уровня вызова может быть или не быть транзакционной; если служба является транзакционной, то при отмене транзакции воспроизведения только этот конкретный вызов откатывается в очередь для повторной попытки. Как вы вскоре увидите, вследствие парал- лельности воспроизведения и особенностей обработки ошибок в WCF, вызовы служб уровня вызовов с очередями могут выполняться и завершаться в произ- вольном порядке, и клиент не может делать какие-либо предположения относи- тельно порядка вызовов. Даже вызовы, отправленные транзакционным клиен-
Управление экземплярами 405 том, могут завершаться успехом или неудачей независимо друг от друга. Никогда не полагайтесь на определенный порядок вызовов в службах уровня вызова с очередями. Сеансовые службы с очередями Если вы хотите разработать и настроить сеансовую службу с очередью, кон- тракт службы должен быть настроен в режиме SessionMode.Required: [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract class MyService : IMyContract Если клиент ставит вызовы в очередь сеансовой конечной точки с очередью, все вызовы, осуществляемые в сеансе, группируются в одно сообщение MSMQ. После отправки этого сообщения и его воспроизведения на стороне службы WCF создает новый экземпляр службы для обработки всех вызовов в сообще- нии. Все вызовы в сообщении воспроизводятся в экземпляре с сохранением ис- ходного порядка. После последнего вызова экземпляр автоматически уничто- жается. Как на стороне клиента, так и на стороне службы WCF предоставляет клиенту и службе уникальный идентификатор сеанса. Тем не менее клиентский идентификатор сеанса никак не связан с идентификатором сеанса службы. Для имитации сеансовой семантики все вызовы к одному экземпляру на стороне хоста используют одинаковое значение идентификатора сеанса. Клиенты и транзакции В случае сеансовой конечной точки с очередью клиент должен иметь окружаю- щую транзакцию для вызова посредника, а нетранзакционные клиенты отверга- ются с исключением InvalidOperationException: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract [OperationContract(IsOneWay = true)] void MyMethodO: usingCTransactionScope scope = new Transact!onScope(Transact!onScopeOption.Suppress)) MyContractClient proxy = new MyContractClient(): proxy.MyMethodt): // Инициирует InvalidOperationException proxy.MyMethodt): proxy.Closet); В случае транзакционного клиента сеансовой конечной точки с очередью WCF отправляет в очередь одно сообщение при закреплении транзакции, а в слу- чае ее отмены это сообщение удаляется из очереди: usingCTransactionScope scope - new Transact!onScopet))
406 Глава 9. Очереди MyContractClient proxy = new MyContractClientO; proxy.MyMethodO: proxy.MyMethod(); proxy,Close():// Завершить составление сообщения. // записать в очередь scope.Complete(); } // Здесь в очереди закрепляется одно сообщение Важно заметить, что одно сообщение, подготовленное посредником, должно быть отправлено в очередь в той же клиентской транзакции — клиент должен завершить сеанс в транзакции. Если клиент не закроет посредника до заверше- ния транзакции, транзакция всегда отменяется: MyContractClient proxy = new MyContractClientO; using(TransactionScope scope = new TransactionScopeO) { proxy.MyMethod(); proxy.MyMethod(): scope. Compl eteO; } //Транзакция отменяется proxy.Close(); У такого поведения имеется интересный побочный эффект: нет смысла хра- нить посредника сеансовой конечной точки с очередью в переменной экземпля- ра, потому что посредник может использоваться только в одной транзакции, а его повторное использование в других клиентских транзакциях невозможно. Клиент может только использовать посредника в одной транзакции, а затем за- крыть его. Клиент не только обязан закрывать посредника при завершении транзакции, но и при использовании транзакционного контекста он должен закрыть посред- ника до завершения транзакции. Дело в том, что закрытие посредника в сеансо- вой конечной точке очереди требует обращения к текущей окружающей транзак- ции, невозможного после вызова Complete(). Попытка приводит к исключению InvalidOperationException: MyContractClient proxy = new MyContractClientO; using(TransactionScope scope = new TransactionScopeO) { proxy,MyMethod(); proxy.MyMethod(); scope.Complete(); proxy.CloseO; // Транзакция отменяется } Из данного требования следует, что простое вложение конструкций using не- возможно, потому что это может привести к вызову Dispose() в неверном поряд- ке — сначала для транзакционного контекста, потом для посредника: using(MyContractClient proxy = new MyContractClientO) using(TransactionScope scope = new TransactionScopeO) { proxy.MyMethod(); proxy.MyMethod();
Управление экземплярами 407 scope.Comp1 ete(); } //Транзакция отменяется Службы и транзакции Сеансовая служба с очередью должна быть настроена на использование тран- закций во всех операциях, для чего TransactionScopeRequired задается равным true. Если этого не сделать, все транзакции воспроизведения отменяются. Кро- ме того, служба должна опеспечить транзакционную привязку к экземпляру, задавая TransactionAutoComplete равным false во всех операциях сеанса, кроме последней. Из-за дефекта архитектуры WCF служба не может положиться на задание true свойству TransactionAutoCompleteOnSessionClose, и транзакция долж- на завершаться вызовом последнего метода сеанса — автоматически или вруч- ную. В листинге 9.13 приведен образец реализации сеансовой службы с очередью (предполагается, что MyMethod3() является последним вызовом операции в сеансе). Листинг 9.13. Реализация сеансовой службы с очередью [Servi ceContract (SessionMode = SessionMode.Required)] interface IMyContract [OperationContract(IsOneWay = true)] void MyMethodlO; [OperationContractUsOneWay = true)] void MyMethod2(); [OperationContractIsOneWay = true)] void MyMethodO(). class MyService : IMyContract ( [OperationBehavior(TransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethodlO {•••} [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] public void MyMethod2() (...) [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodOО {•••) Разумеется, программирование службы в предположении, что некий метод будет вызываться последним в сеансе, часто оказывается неприемлемым. От- части проблема решается использованием демаркационных операций с указа-
408 Глава 9. Очереди нием метода, завершающего сеанс. Для определений из листинга 9.13 можно написать: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContractdsOneWay = true, IsTerminating = false)] void MyMethodlO; [OperationContractdsOneWay = true. Islnitiating = false, IsTerminating = false)] void MyMethod20; [OperationContractdsOneWay = true, Islnitiating = false, IsTerminating = true)] void MyMethod3(); } Но даже этот способ не решает всех проблем, если клиент не вызовет завер- шающую операцию; в этом случае сеанс отменяет свою транзакцию. Также можно включить в контракт специальную операцию CompleteTransaction(), един- ственным назначением которой будет завершение транзакции и окончание се- анса. Обязательно документируйте необходимость явного вызова этого метода в конце сеанса: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContractdsOneWay = true)] void MyMethodlO; [OperationContractdsOneWay = true)] void MyMethod2(); [OperationContractdsOneWay = true)] void CompleteTransactionO; } class MyService : IMyContract { [OperationBehaviordransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethodlO {...} [OperationBehaviordransactionScopeRequired = true. TransactionAutoComplete = false)] public void MyMethod2() {••} [OperationBehaviordransactionScopeRequired = true)] public void CompleteTransactionO } Учитывая все сказанное, постарайтесь обойтись без сеансовых служб с оче- редями — они ненадежны и нарушают логическую изоляцию.
Управление экземплярами 409 Синглетная служба Синглетная транзакционная служба с очередью не поддерживает сеансы и мо- жет реализовывать только несеансовые контракты. Задание SessionMode значе- ний SessionMode.Allowed или SessionMode.NotAllowed приводит к одному резуль- тату: бессеансовому взаимодействию. По этой причине я рекомендую всегда настраивать контракты синглетных служб с очередями в бессеансовом режиме: [ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract (•} [Servi ceBehavi or(InstanceContextMode=InstanceContextMode.Si ngle)] class MyService : IMyContract (...) Нетранзакционная синглетная служба с очередью в отношении управления экземплярами ведет себя как обычная синглетная служба WCF. Независимо от клиентов и посредников, разные вызовы к посредникам упаковываются в от- дельные сообщения MSMQ и отдельно доставляются синглетному экземпляру, как и в случае со службами уровня вызова. Однако в отличие от служб уровня вызова, все эти вызовы будут воспроизводиться в одном экземпляре службы. Что касается транзакционных синглетных служб с очередями, по умолча- нию они ведут себя как службы уровня вызова: после каждого вызова, завер- шающего транзакцию, WCF освобождает синглетный экземпляр. Единственное отличие «настоящей» службы уровня вызова от синглетной заключается в том, что WCF допускает существование не более одного экземпляра синглетной службы независимо от количества сообщений в очереди. Хотя в данном случае можно применить методы, описанные в главе 7 для транзакционных синглет- ных служб с контролем состояния, вы также можете восстановить синглет- ную семантику, задав ReleaseServicelnstanceOnTransactionComplete равным false. Вспомните, о чем говорилось в главе 7: это означает, что синглетная служба должна содержать как минимум одну операцию с истинным значени- ем TransactionScopeRequired. В листинге 9.14 приведен образец реализации тран- закционной синглетной службы с очередью. Листинг 9.14. Транзакционная синглетная служба с очередью [ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract [OperationContractdsOneWay = true)] void MyMethodO: ) [Servi ceBehavi or(InstanceContextMode=InstanceContextMode.Si ngle. ReleaseServicelnstanceOnTransactionComplete - false)] class MySingleton : IMyContract.IDisposable { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO )
410 Глава 9. Очереди ПРИМЕЧАНИЕ ----------------------------------------------------------- Транзакционная синглетная служба не может реализовать сеансовый контракт, если только все клиенты не ограничатся одним вызовом в каждом сеансе (отчего сама идея сеанса теряет всякий смысл). Избегайте сеансовых транзакционных синглетных служб с очередями. Вызовы и порядок Так как вызовы упаковываются в отдельные сообщения MSMQ, в синглетном экземпляре они могут воспроизводиться в произвольном порядке из-за повто- ров и транзакций. Кроме того, вызовы также могут завершаться в любом поряд- ке, и даже вызовы, переданные транзакционным клиентом, могут завершаться успехом или неудачей независимо друг от друга. Никогда не полагайтесь на оп- ределенный порядок вызовов в синглетных службах. Управление параллельной обработкой Свойство ConcurrencyMode, как и в подключенных службах, управляет парал- лельным воспроизведением сообщений в очередях. В службах уровня вызова все сообщения в очередях воспроизводятся в разных экземплярах сразу же по- сле их выхода из очереди, вплоть до порогового количества, заданного в пара- метрах регулирования нагрузки. Настройка реентерабельности для поддержки обратного вызова не нужна, потому что контексты операций заведомо не могут иметь ссылки обратного вызова. Также нет необходимости в настройке конфи- гурации для одновременного параллельного доступа, потому что разные сооб- щения пи при каких условиях не используют экземпляр совместно. Короче говоря, для служб уровня вызовов с очередями режим параллельности игнори- руется. Что касается сеансовых служб с очередями, они должны настраиваться в ре- жиме ConcurrencyMode.Single. Дело в том, что только этот режим параллельной обработки позволяет отключить автозавершение, что необходимо для поддер- жания сеансовой семантики. Вызовы в сообщениях всегда воспроизводятся по одному в экземпляре службы. Синглетная служба с очередью — единственный режим управления экземп- лярами, обладающий некоторой свободой действий в режиме параллельной об- работки. Если синглетная служба настроена в режиме ConcurrencyMode.Single, WCF немедленно извлекает все сообщения из очереди (вплоть до пределов, ус- тановленных пулом потоков и параметрами регулирования нагрузки), после чего параллельно воспроизводит их в синглетном экземпляре. Очевидно, синг- летный экземпляр должен обеспечить синхронизацированный доступ к своему состоянию. Если синглетный экземпляр также является транзакционным, он подвергается опасности транзакционной взаимной блокировки. Болес того, транзакционный синглетный экземпляр в режиме ConcurrencyMode.Multiple дол- жен обладать ложным значением ReleaseServicelnstanceOnTransactionComplete, и по крайней мере у одной операции свойство TransactionScopeRequired должно быть истинным:
Сбои при доставке 411 [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.Si ngle, ConcurrencyMode = ConcurrencyMode.Multiple, ReleaseServicelnstanceOnTransactionComplete = false)] class MySingleton IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO (...) I Регулирование нагрузки Регулирование нагрузки у служб с очередями позволяет управлять нагрузкой, чтобы она не превратилась в напряжение. Важным параметром регулирования является количество параллельных воспроизведений. С его помощью можно эффективно регулировать количество воспроизводимых сообщений, потому что в случае превышения количества параллельных вызовов (общего напряже- ния) лишние сообщения будут оставаться в очереди. Например, если в очереди службы стоят 50 сообщений, возможно, вы не захотите создавать 50 параллель- ных экземпляров и рабочих потоков. Для служб уровня вызова параметры ре- гулирования нагрузки определяют допустимое общее количество параллельно существующих экземпляров (и соответствующий уровень потребления ресур- сов). Для сеансовых служб они управляют количеством допустимых сеансов. В случае синглетной службы с очередью регулирование нагрузки можно объ- единить с ConcurrencyMode.Multiple для управления количеством параллельных воспроизведений (напряжение) и количеством сообщений, хранимых в очереди (буферизованная нагрузка). Сбои при доставке Как обсуждалось в главе 6, вызовы при подключенных взаимодействиях могут завершаться неудачей вследствие коммуникационных сбоев и ошибок стороны службы. Точно так же и вызов, передаваемый через очередь, тоже может завер- шиться неудачей из-за сбоев при доставке или ошибок воспроизведения на сто- роне службы. WCF предоставляет специализированные механизмы обработки ошибок обоих типов и хорошее понимание этих механизмов, а также их инте- грации с пользовательской логикой обработки ошибок является важнейшим фактором использования служб с очередями. Хотя MSMQ может гарантировать доставку сообщения при наличии техни- ческой возможности, существует целый ряд ситуаций, в которых доставка сооб- щения невозможна. Приведенный далее список не является полным. О Тайм-аут и истечение срока жизни. Как вы вскоре увидите, каждое сообще- ние снабжается временным штампом и должно быть доставлено и обработа- но в течение интервала тайм-аута. Если этого не произойдет, доставка завер- шается неудачей.
412 Глава 9. Очереди О Несоответствие параметров безопасности. Если мандат безопасности сооб- щения (или сам выбранный механизм проверки подлинности) не соответст- вует ожиданиям службы, то служба отвергает сообщение. О Транзакционное несоответствие. Клиент не может использовать локальную нетранзакционную очередь при отправке сообщений в транзакционную оче- редь на стороне службы. О Сетевые проблемы. Если в сети происходит фатальный сбой или сеть просто ненадежна, сообщение может никогда не добраться до службы. О Сбои компьютеров. Компьютер со службой может не принимать сообщения из очереди из-за аппаратных или программных сбоев. О Очистка очереди. Даже если доставка сообщения прошла успешно, админи- стратор (или приложение на программном уровне) может очистить содер- жимое очереди. В этом случае службе не удастся обработать сообщения. О Нарушение квоты. У каждой очереди имеется квота, управляющая макси- мальным размером хранящихся в ней данных. В случае превышения квоты будущие сообщения отвергаются. При любых сбоях с доставкой сообщение возвращается в очередь клиента, a MSMQ постоянно повторяет попытки доставить его. В некоторых случаях (например, при сбоях сетевых каналов или проблемах с квотами) попытка мо- жет оказаться успешной, но во многих случаях MSMQ так и не удается доста- вить сообщение. Более того, в практическом контексте слишком большое коли- чество повторных попыток может оказаться неприемлемым из-за опасного уровня нестабильности. Механизм обработки ошибок доставки сообщает MSMQ, что по- вторные попытки не должны быть вечными, определяет, после какого количе- ства попыток от них стоит отказаться и что делать с недоставленными сообще- ниями. Класс MsmqBindingBase содержит ряд свойств, управляющих обработкой сбоев при доставке: public abstract class MsmqBindingBase : Binding.... { public TimeSpan TimeToLive {get:set;} // Настройки DLQ public Uri CustomDeadLetterQueue {get:set;} public DeadLetterQueue DeadLetterQueue {get:set;} //... } Очередь зависших сообщений В системах доставки сообщений после очевидного сбоя при доставке сообще- ния попадают в специальную очередь, называемую очередью зависших сообще- ний (DLQ, dead-letter queue). В контексте нашего обсуждения к сбоям доставки относится не только недоступность очереди на стороне службы, но и невозмож- ность закрепления транзакции воспроизведения. При этом может оказаться
Сбои при доставке 413 так, что службе не удается обработать воспроизведение, в то время как транзак- ция воспроизведения будет закреплена. MSMQ на стороне клиента и на сторо- не службы постоянно посылают подтверждения друг другу в процессе приема и отправки сообщений. Если подсистема MSMQ на стороне службы успешно получает и читает сообщения из очереди на стороне службы (то есть транзак- ция воспроизведения закреплена), она отправляет положительное подтверждение (АСК) подсистеме MSMQ на стороне клиента. Подсистема MSMQ на стороне службы также может отправить клиенту сигнал отрицательного подтвержде- ния (NACK). Получив NACK, MSMQ на стороне клиента отправляет сообще- ние в DLQ. Если MSMQ на стороне клиента не получает ни АСК, ни NACK, статус сообщения считается неопределенным. В MSMQ 3.0 (то есть в Windows ХР и Windows Server 2003) очередь завис- ших сообщений является общесистемной. Все сбойные сообщения всех прило- жений поступают в единое хранилище. В MSMQ 4.0 (то есть в Windows Vista) можно настроить очередь DLQ уровня приложения, в которую поступают толь- ко приложения, предназначенные для этой конкретной службы. Очереди завис- ших сообщений уровня приложения кардинально упрощают работу как адми- нистратора, так и разработчика. ПРИМЕЧАНИЕ-------------------------------------------------------------- При использовании неустойчивой очереди сбойные нетранзакционные сообщения поступают в спе- циальную общесистемную устойчивую очередь DLQ. Срок жизни сообщений В MSMQ каждое сообщение содержит временной штамп, инициализируемый при первой отправке сообщения в очередь на стороне клиента. Кроме того, каж- дое сообщение в очереди WCF обладает интервалом тайм-аута, который опре- деляется свойством TimeToLive класса MsmqBindingBase. После отправки сообще- ния в очередь на стороне клиента WCF следит за тем, чтобы сообщение было доставлено и обработано в течение установленного интервала. Обратите внима- ние: успешной доставки сообщения в очередь на стороне службы недостаточ- но — сообщение должно быть обработано. Таким образом, свойство TimeToLive отчасти аналогично свойству SendTimeout привязок с подключением. Свойство TimeToLive относится только к клиенту-отправителю; оно не влияет на сторону службы и не может изменяться службой. По умолчанию значение TimeToLive со- ставляет один день. После многократных попыток и неудач при доставке (и об- работке) сообщения в течение срока, определяемого TimeToLive, MSMQ прекра- щает дальнейшие попытки и перемещает сообщение в DLQ. Срок жизни сообщений может задаваться как программным, так и админи- стративным способом. Например, вот как выглядит конфигурационный файл для задания 5-минутного срока жизни: <bindings> <netMsmqB1 ndi ng> <binding name = "ShortTImeout" tlmeToLlve = "00:05:00"> </binding> </netMsmqB1nding> </bindings>
414 Глава 9. Очереди Короткий интервал тайм-аута обычно используется при обработке вызовов, критичных по времени. Однако передача через очередь вызовов, критичных по времени, противоречит самой идее отключенных вызовов с очередями, и чем сильнее критичность, тем более сомнительно выглядит сам выбор служб с оче- редями. К сроку жизни сообщений правильнее относиться как к последнему средству, которое в конечном итоге привлекает внимание администратора к тому, что сообщение не было доставлено, а не как к средству интерпретации временного характера сообщений на бизнес-уровне. Настройка очереди зависших сообщений MsmqBindingBase содержит свойство DeadLetterQueue перечисляемого типа Dead- LetterQueue: public enum DeadLetterQueue ( None. System. Custom } Если свойству задано значение DeadLetterQueue.None, WCF не использует очередь DLQ. В случае сбоя при доставке WCF просто уничтожает сообщение, словно никакого вызова и не было. По умолчанию используется значение Dead- LetterQueue.System. Как подсказывает название, в нем используется DLQ сис- темного уровня, а в случае неудачной доставки WCF перемещает сообщение из клиентской очереди в общесистемную очередь DLQ. Если свойство равно DeadLetterQueue.Custom, приложение может использо- вать специализированные очереди DLQ. Режим DeadLetterQueue.Custom работа- ет только в MSMQ 4.0, и WCF проверяет это требование во время вызова. Кро- ме того, WCF требует, чтобы приложение указывало адрес DLQ в свойстве CustomDeadLetterQueue привязки. По умолчанию CustomDeadLetterQueue содержит null, но при использовании режима DeadLetterQueue.Custom оно должно быть от- лично от null: <netMsmqBinding> <binding name = "CustomDLQ" deadLetterQueue = "Custom” CustomDeadLetterQueue = "net.msmq://1ocalhost/pri vate/MyCustomDLQ"> </binding> </netMsmqBinding> И наоборот, когда DeadLetterQueue задается любое другое значение, кроме DeadLetterQueue.Custom, свойство CustomDeadLetterQueue должно быть равно null. Важно понимать, что пользовательская очередь DLQ — не более чем еще одна очередь MSMQ. Разработчик клиентской стороны также должен обеспе- чить работу службы DLQ, которая будет обрабатывать сообщения. В MSMQ 4.0 WCF всего лишь автоматизирует акт перемещения сообщения в DLQ при обна- ружении сбоев.
Сбои при доставке 415 Проверка состояния пользовательской очереди DLQ Как и при работе с любой другой очередью, перед выдачей вызовов на стадии | выполнения клиент должен убедиться в том, что пользовательская очередь | DLQ существует, и создать ее в случае необходимости. В листинге 9.15 пред- ставлен способ автоматизации и инкапсуляции этого процесса в методе Queued- ServiceHelper.VerifyQueue() по представленной ранее схеме. Листинг 9.15. Проверка пользовательской очереди DLQ public static class QueuedServiceHelper I public static void VerifyQueue(ServiceEndpoint endpoint) if(endpoint.Binding is NetMsmqBinding) ( string queue = GetQueueFromUri(endpoint.Address.Uri); if(MessageQueue.Exists(queue) == false) { MessageQueue.Create(queue.true): } NetMsmqBinding binding = endpoint.Binding as NetMsmqBinding; if(binding.DeadLetterQueue == DeadLetterQueue.Custom) { Debug.Assert(binding.CustomDeadLetterQueue != null); string DLQ = GetQueueFromUri(binding.CustomDeadLetterQueue); if(MessageQueue.Exists(DLQ) == false) { MessageQueue.Create(DLQ.true): ) } } //... Обработка очереди зависших сообщений Клиент должен каким-то образом обрабатывать сообщения, накапливающиеся в DLQ. Для общесистемной очереди клиент может предоставить «суперслуж- бу», поддерживающую все контракты всех конечных точек с очередями в систе- ме для обработки всех сбойных сообщений. Разумеется, такое решение нереаль- но, потому что служба не может знать обо всех контрактах с очередями, не говоря уже об организации осмысленной обработки для всех приложений. Оно может работать только в одном случае: если в системе на стороне клиента будет использоваться не более одной службой с очередью. Также можно написать приложение для прямого администрирования и управления системной очере- дью DLQ с использованием System.Messaging. Приложение будет извлекать из очереди нужные сообщения и обрабатывать их. Недостаток такого решения (помимо чрезмерного объема работы) заключается в том, что если сообщения зашифрованы и защищены, как это должно быть, приложению будет нелегко организовать их обработку. На практике единственно возможным решением
416 Глава 9. Очереди для общей клиентской среды является то, которое предлагает MSMQ 4.0, - пользовательская очередь DLQ. При работе с пользовательской очередью DLQ вы также предоставляете службу стороны клиента, очередь которой является пользовательской очередью DLQ приложения. Служба обрабатывает сбойные сообщения в соответствии со специфическими требованиями конкретного при- ложения. Определение службы DLQ Службы DLQ реализуются по тем же принципам, что и другие службы с очере- дями. Единственным требованием является полиморфизм службы DLQ с кон- трактом исходной службы. Если в схеме задействовано несколько конечных то- чек с очередями, потребуется отдельная очередь DLQ для каждого контракта и каждой конечной точки. Пример конфигурации представлен в листинге 9.16. Листинг 9.16. Конфигурационный файл службы DLQ ////////////////// Client side 111111111111111111111 <system.servi ceModel> <client> <endpoint address = "net.msmq://localhost/private/MyServiceQueue" binding - "netMsmqBinding" bindingconfiguration = "MyCustomDLQ" contract = "IMyContract" /> </client> <bindings> <netMsmqBinding> <binding name = "MyCustomDLQ" deadLetterQueue = "Custom" customDeadLetterQueue = "net.msmq://localhost/private/MyCustomDLQ"> </binding> </netMsmqBinding> </bindings> </system.servi ceModel> ////////////////// Сторона службы DLQ ///////////////////// <system.servi ceModel> <services> <service name - "MyDLQService"> <endpoint address = "net.msmq://localhost/private/MyCustoinDLQ" binding = "netMsmqBinding" contract = "IMyContract" /> </service> </services> </system.serviceModel > Конфигурационный файл клиента определяет конечную точку с очередью и контрактом IMyContract. Клиент использует пользовательскую секцию binding для определения адреса пользовательской очереди DLQ. Отдельная служба с очередью (возможно, находящаяся на отдельном компьютере) также поддер- живает контракт IMyContract. Служба DLQ использует в качестве адреса оче- редь DLQ, определяемую клиентом.
Сбои при доставке 417 Свойства сбоев Службе DLQ обычно необходимо знать, почему попытка доставки вызова за- вершилась неудачей. Для этой цели в WCF включен класс MsmqMessageProperty, используемый для определения причины сбоя и текущего состояния сообще- ния. Класс MsmqMessageProperty определяется в пространстве имен System.Servi- ceModel.Channels: public sealed class MsmqMessageProperty public const string Name = "MsmqMessageProperty": public int AbortCount {getinternal set;} public DeliveryFailure? DeliveryFailure {get:} public Deliverystatus? Deliverystatus {get:} public int MoveCount {get;internal set:} //... Служба DLQ получает MsmqMessageProperty из свойств входящего сообще- ния в контексте операции: public sealed class Operationcontext : ... ( public Messageproperties IncomingMessageProperties {get:} //... public sealed class Messageproperties : IDictionary<string,object>.... { public object this[string name] {get:set:} //... При передаче сообщения в DLQ WCF включает в его свойства экземпляр MsmqMessageProperty с описанием сбоя. MessageProperties — всего лишь коллек- ция свойств сообщений, к которым можно обращаться по строковому ключу. Для получения MsmqMessageProperty используется константа MsmqMessageProper- ty.Name, как показано в листинге 9.17. Листинг 9.17. Получение MsmqMessageProperty [Servi ceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethodO: ) [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyDLQService : IMyContract ( [OperationBehavior(TransactionScopeRequired = true)] продолжение &
418 Глава 9. Очереди public void MyMethodO { MsmqMessageProperty msmqProperty = OperationContext.Current. IncomingMessageProperties[MsmqMessageProperty.Name] as MsmqMessageProperty: Debug.Assert(msmqProperty != null): //Обработка msmqProperty } В листинге 9.17 обратите внимание на использование средств, упоминав- шихся ранее применительно к сеансовым режимам, управлению экземплярами и транзакциями — ведь в конце концов, служба DLQ представляет собой обыч- ную службу с очередью. Свойства MsmqMessageProperty содержат дополнительные сведения о сбое, а также кое-какую контекстную информацию. MoveCount — количество попыток воспроизведения сообщения на стороне службы. AbortCount — количество попы- ток чтения сообщения из очереди. Свойство AbortCount относится к области от- ветственности MSMQ и для разработчиков обычно интереса не представляет. Deliverystatus — свойство перечисляемого типа DeliveryStatus, который определя- ется следующим образом: public enum Deliverystatus { InDoubt. NotDelivered } Свойство Deliverystatus задается равным Deliverystatus.InDoubt, если только нет точной информации о неудачной доставке (получен сигнал NACK). Напри- мер, считается, что сообщения с истекшим сроком жизни имеют неопределен- ное состояние, потому что их срок жизни истек до того, как служба смогла под- твердить или опровергнуть их получение. Свойство DeliveryFailure содержит значение перечисляемого типа DeliveryFailure, определяемого следующим образом: public enum DeliveryFailure { AccessDenied. NotTransactionalMessage. Purged. QueueExceedMaximumSize. ReachQueueTimeout. ReceiveTimeout. Unknown //... } Реализация службы DLQ Служба DLQ не может влиять на свойства сообщений — например, продлевать их срок жизни. При обработке сбоев доставки обычно выполняются такие дей- ствия, как компенсационные транзакции; оповещения администратора; попытки повторной отправки нового сообщения или отправки нового запроса с расши- ренным тайм-аутом; регистрация ошибок; наконец, простая обработка сбойного
Сбои при воспроизведении 419 вызова и возврат (то есть уничтожение сообщения). Один из примеров реали- зации представлен в листинге 9.18. Листинг 9.18. Реализация службы DLQ [Servi ceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyDLQService : IMyContract [0perat1onBehav1or(TransactionScopeRequired = true)] public void MyMethod(string someValue) MsmqMessageProperty msmqProperty = Operationcontext.Current. IncomingMessagePropertiesCMsmqMessageProperty.Name] as MsmqMessageProperty: // Если сделано более 25 попыток, отвергнуть сообщение if(msmqProperty.MoveCount >= 25) ( return: } // В случае тайм-аута: попробовать снова if(msmqProperty.DeliveryStatus == DeliveryStatus.InDoubt) { 1f(msmqProperty.Deli veryFa ilure == Del 1veryFa11ure.Recei veT1meout) { MyContractClient proxy = new MyContractClient(): proxy.MyMethod(someVa1ue): proxy.Close(): } return: } if(msmqProperty.DeliveryStatus == DeliveryStatus.InDoubt || msmqProperty.DeliveryFailure == DeliveryFailure.Unknown) { NotifyAdmin(): } } void NotlfyAdmin() } {•••} Служба DLQ в листинге 9.18 анализирует причину сбоя. Если WCF пытает- ся доставить сообщение более 25 раз, служба DLQ просто уничтожает сообще- ние и отказывается от дальнейших попыток. Если сбой произошел из-за тайм- аута, служба DLQ делает новую попытку, создавая посредника к службе с оче- редью и передавая вызов с аргументами от исходного вызова. Если сообщение имеет неопределенное состояние или произошел неизвестный сбой, служба оповещает администратора приложения. Сбои при воспроизведении Даже после успешной доставки возможны сбои при воспроизведении сообще- ния на стороне службы. Такие сбои обычно приводят к отмене транзакции вос- произведения, в результате чего сообщение возвращается в очередь службы.
420 Глава 9. Очереди WCF обнаруживает сообщение в очереди и делает повторную попытку. Если и следующий вызов завершается сбоем, сообщение снова отправляется в оче- редь и т. д. Многократные попытки такого рода часто неприемлемы. Если служ- ба с очередью создавалась для балансирования нагрузки, автоповторы порож- дают значительное напряжение для службы. Вам потребуется схема «умной» обработки сбоев, учитывающая возможность того, что вызов никогда не завер- шается удачно (конечно, понятие «никогда» определяется в практическом кон- тексте). Механизм обработки сбоев определяет, после скольких попыток следует сделать паузу, сколько должна продолжаться эта пауза и даже с какой частотой должны совершаться попытки. Для разных систем нужны разные стратегии, с разной чувствительностью к дополнительной нагрузке и вероятности успеха. Например, 10 повторных попыток через каждый час — совсем не то же самое, что 10 повторных попыток через каждую минуту, или 5 повторений из двух по- пыток подряд через каждый день. Кроме того, когда вы отказываетесь от даль- нейших попыток, что делать со сбойным сообщением и какую информацию следует передать его отправителю? Вредные сообщения Транзакционные системы передачи сообщения по своей природе уязвимы для повторяющихся сбоев, потому что они могут парализовать работу системы. Со- общения, воспроизведение которых постоянно завершается неудачей, называ- ются вредными сообщениями (poison messages), потому что они «отравляют» систему бесплодными попытками. Транзакционные системы должны активно выявлять и ликвидировать вредные сообщения. Так как никогда не известно, не окажется ли следующая попытка успешной, можно воспользоваться следую- щей простой эвристикой: чем больше сбоев было получено при доставке сооб- щения, тем выше вероятность того, что сбой произойдет и в следующий раз. Например, если сбой произошел всего один раз, есть смысл попробовать снова. Если же доставка 1000 раз завершилась неудачей, маловероятно, чтобы 1001 попытка оказалась успешной, так что и продолжать бессмысленно. Разумеется, что именно считать бессмысленным (или просто напрасным), зависит от при- ложения, но по крайней мере вы сможете настроить параметры своего решения. MsmqBindingBase содержит ряд свойств, управляющих обработкой сбоев при воспроизведении: public abstract class MsmqBindingBase : Binding.... { // Обработка вредных сообщений public int ReceiveRetryCount {get;set;} public int MaxRetryCycles {get;set;} public TimeSpan RetryCycleDelay {get;set;} public ReceiveErrorHandling ReceiveErrorHandling
Сбои при воспроизведении 421 {get;set;} ) Обработка вредных сообщений в MSMQ 4.0 В MSMQ 4.0 (только в Windows Vista) WCF повторяет попытки воспроизведе- ния сбойного сообщения сериями пакетов. WCF предоставляет каждой конеч- ной точке с очередью очередь повторных попыток и очередь вредных сообще- ний. После того как все попытки вызовов в пакете завершатся неудачей, сооб- щение не возвращается в очередь конечной точки. Вместо этого оно поступает в очередь повторных попыток. Когда сообщение будет расценено как вредное, WCF может переместить его в очередь вредных сообщений. Пакеты повторных попыток В каждом пакете WCF немедленно пытается сделать ReceiveRetryCount повтор- ных попыток после первого сбоя. По умолчанию значение ReceiveRetryCount рав- но пяти, то есть всего делается шесть попыток, включая первую. После того как попытка отправки пакета завершится неудачей, сообщение перемещается в оче- редь повторных попыток. После задержки в RetryCycleDeLay минут сообщение переходит из очереди повторных попыток в очередь конечной точки для сле- дующей попытки пакетной отправки. Задержка по умолчанию составляет 30 минут. Если и этот пакет отправить не удастся, сообщение возвращается в очередь повторных попыток, откуда WCF попытается снова отправить его по истечению задержки. Разумеется, эта процедура не может продолжаться вечно. Максимальное количество циклов отправки определяется свойством MaxRetry- Cycles. По умолчанию делается всего два цикла. После MaxRetryCycles попыток сообщение считается вредным. При пользовательской настройке MaxRetryCycles я рекомендую задавать значение прямо пропорциональным RetryCycleDeLay. Чем длиннее задержка, тем лучше ваша система перенесет дополнительные пакет- ные циклы, потому что напряжение распределяется по большему периоду вре- мени. При коротком значении RetryCycleDeLay количество разрешенных пакетов следует уменьшить до минимума. Наконец, свойство ReceiveErrorHandling управляет действиями после того, как последняя попытка завершится неудачей, и сообщение будет сочтено вредным. Его значения относятся к перечисляемому типу ReceiveErrorHandling, определяе- мому следующим образом: public enum ReceiveErrorHandling ( Fault, Drop. Reject. Move ) ReceiveErrorHandling.Fault В режиме Fault вредное сообщение рассматривается как катастрофический сбой, приводящий к отказу канала MSMQ и хоста службы. Служба не сможет
422 Глава 9. Очереди обрабатывать другие сообщения как из очередей, так и от обычных подключен- ных клиентов. Вредное сообщение остается в очереди конечной точки, и должно быть извлечено из нее явно администратором или компенсирующей логикой. Чтобы продолжить обработку клиентских вызовов, необходимо либо уничто- жить хостовой процесс, либо открыть новый хост (после удаления вредного сообщения из очереди). Хотя вы можете установить расширение обработки ошибок (см. главу 6) и выполнить часть работы, на практике неизбежно по- требуется участие администратора приложения. ReceiveErrorHandling.Fault явля- ется значением по умолчанию свойства ReceiveErrorHandling. Никакие подтверж- дения при это отправителю не посылаются. ReceiveErrorHandling.Drop В режиме Drop вредное сообщение просто устраняется, а служба продолжает об- рабатывать сообщения. Режим ReceiveErrorHandling.Drop выбирается в том слу- чае, если система обладает высокой устойчивостью как к ошибкам, так и к по- вторным попыткам. Если сообщение не критично, то его потеря с продолжением работы приемлема. Потеря сообщения дает возможность для повторной попыт- ки, но с концептуальной точки зрения повторений не должно быть слишком много — если сообщение представляет для вас ценность, его не следовало про- сто терять после последнего сбоя. Режим ReceiveErrorHandling.Drop также переда- ет отправителю сигнал АСК, поэтому с точки зрения отправителя сообщение было успешно доставлено и обработано. ReceiveErrorHandling.Reject В режиме Reject система активно отвергает вредное сообщение, не желая иметь с ним ничего общего. Как и в режиме ReceiveErrorHandling.Drop, сообщение теря- ется, но при этом отправителю посылается сигнал NACK, который сообщает о том, что доставить и обработать сообщение не удалось. Отправитель реагиру- ет на него, помещая сообщение в очередь зависших сообщений отправителя. ReceiveErrorHandling.Move Вероятно, режим Move является самым лучшим и самым полезным. Сообщение перемещается в специализированную очередь вредных сообщений, а отправите- лю никакое сигналы не передаются — ни АСК, ни NACK. Подтверждение обра- ботки сообщения выдается только после того, как сообщение будет обработано из очереди вредных сообщений. Пример конфигурации В листинге 9.19 приведен фрагмент конфигурационного файла хоста с настрой- кой обработки вредных сообщений для MSMQ 4. На рис. 9.9 представлена на- глядная схема действий, выполняемых при обработке вредного сообщения. Листинг 9.19. Обработка вредных сообщений в MSMQ 4 <bindlngs> <netMsmqB1nding> <binding name = "PolsonMessageHandllng" recelveRetryCount = "2"
Сбои при воспроизведении 423 retryCycleDelay = "00:05:00" maxRetryCycles = "3" receiveErrorHandling = "Move" /> </netMsmqBinding> </bindings> Рис. 9.9. Обработка вредных сообщений в листинге 9.19 В очередь вредных сообщений Служба вредных сообщений Разработчик может предоставить специализированную службу вредных сооб- щений, которая будет обрабатывать сообщения, отправленные в очередь вред- ных сообщений при настройке привязки в режиме ReceiveErrorHandling.Move. Служба вредных сообщений должна быть полиморфна контракту конечной точки с очередью службы. WCF извлекает вредное сообщение из очереди и вос- производит его для службы вредных сообщений. Очень важно, чтобы служба вредных сообщений не инициировала необработанные исключения и не отме- няла транзакцию воспроизведения (например, ее стоит настроить на игнориро- вание транзакции воспроизведения, как в листинге 9.9, или использовать но- вую транзакцию, как в листинге 9.10). Такая служба вредных сообщений обычно участвует в компенсационных действиях, связанных со сбойным сообщением, — например, возврате средств за товар, отсутствующий на складе. Впрочем, служ- ба вредных сообщений может решать и другие задачи: оповещать администра- тора, регистрировать ошибки или просто проигнорировать сообщение, вернув управление. Служба вредных сообщений разрабатывается и настраивается так же, как любая другая служба с очередью. Единственное различие заключается в том, что адрес конечной точки состоит из адреса исходной конечной точки с суффиксом ;poison. В листинге 9.20 представлена необходимая конфигурация основной службы и ее службы вредных сообщений. В данном примере основ- ная служба использует общий процесс совместно со службой обработки вред- ных сообщений, но, конечно, это совершенно не обязательно. Листинг 9.20. Настройка службы вредных сообщений <system. servi ceModel > <services> <service name = "MyService"> <endpoint address = "net.msmq://local host/private/MyServiceQueue" binding = "netMsmqBinding" продолжение &
424 Глава 9. Очереди bindingconfiguration = "PolsonMesssageSettings" contract = "IMyContract" /> </service> <service name = "MyPoisonServiceMessageHandler"> <endpoint address = "net.msmq://localhost/private/MyServiceQueue;poison" binding = "netMsmqBinding" contract = "IMyContract" /> </service> </services> <bindings> <netMsmqBinding> <binding name = "PolsonMesssageSettings" receiveRetryCount = "2" retryCycleDelay = "00:05:00" maxRetryCycles - "3" receiveErrorHandling = "Move" /> </netMsmqB1nding> </bi ndi ngs> </system.serv i ceModel > Обработка вредных сообщений в MSMQ 3.0 В MSMQ 3.0 (Windows XP и Windows Server 2003) не существует очередей по- вторных попыток или специализированных автоматических очередей вредных сообщений. В результате WCF поддерживает только один пакет повторных по- пыток в исходной очереди конечной точки. После последнего сбоя первого па- кета сообщение считается вредным. Таким образом, WCF ведет себя так, как если бы свойство MaxRetryCycles всегда оставалось равным 1, а свойство Retry- CycleDelay игнорировалось. Для свойства ReceiveErrorHandling допустимы только значения ReceiveErrorHandling.Fault и ReceiveErrorHandling.Drop. При любых дру- гих значениях во время загрузки службы выдается исключение InvalidOperation- Exception. Оба допустимых варианта, ReceiveErrorHandling.Fault и ReceiveErrorHandling.Drop, особого восторга не вызывают. В MSMQ 3.0 лучшим способом обработки сбоев воспроизведения на стороне службы (то есть сбоев, напрямую обусловленных бизнес-логикой службы, а не коммуникационными проблемами) является ис- пользование ответных служб, о которых будет рассказано далее. Вызовы с очередями и подключенные вызовы С технической точки зрения один код службы может использоваться как для подключенных вызовов, так и вызовов с очередями (с небольшими изменения- ми — такими, как настройка операций как односторонних или добавлением другого контракта с односторонними операциями), на практике использование
Вызовы с очередями и подключенные вызовы 425 одной службы в обоих режимах маловероятно. Это объясняется теми же причи- нами, которые уже упоминались в контексте асинхронных вызовов в главе 8. Синхронные и асинхронные вызовы, задействованные в одном сценарии биз- нес-логики, часто работают по разным схемам, поэтому в коде службы прихо- дится вносить изменения, адаптирующие его для разных случаев. Использова- ние вызовов с очередями воздвигает еще один барьер на пути к универсальному использованию кода службы: изменения в транзакционной семантике службы. Для примера рассмотрим приложение на рис. 9.10: электронный магазин, в котором используются только вызовы с подключением. Рис. 9.10. Приложение, работающее в подключенном режиме, с одной транзакцией Основная служба управления магазином Store использует при обработке за- казов три вспомогательные службы: Order, Shipment и Billing. В подключенном сценарии служба управления магазином вызывает службу Order для размеще- ния заказа. Только если Order удастся обработать заказ (то есть если товар име- ется на складе), служба Store вызывает службу Shipment, и только при успешном вызове Shipment служба Store вызывает службу Billing для выписки счета. В под- ключенном сценарии используется ровно одна транзакция, созданная клиен- том. Все операции закрепляются или отменяются как одна атомарная операция. Теперь предположим, что служба Billing предоставляет конечную точку с очере- дью для службы Store, как показано на рис. 9.11. Рис. 9.11. В работе отключенного приложения задействовано несколько транзакций
426 Глава 9. Очереди Вызов с очередью, обращенный к службе Billing, будет воспроизведен в спе- циальной транзакции, отдельной от остальных служб магазина, а также может закрепляться или отменяться отдельно от транзакции, группирующей Order и Shipment. В свою очередь, это ставит под угрозу стабильность системы, поэто- му в службу Billing придется включить дополнительную логику для обнаруже- ния сбоев других служб и выполнении некой компенсирующей логики. В ре- зультате служба Billing уже не будет той службой, которая использовалась в подключенном случае. Необходимость использования очередей Поскольку не каждая служба может работать как в подключенном режиме, так и с очередями, а некоторые службы могут быть рассчитаны только на один оп- ределенный режим. В таких случаях WCF позволяет установить ограничения для коммуникационных схем. Атрибут DeliveryRequirements, упоминавшийся в главе 1, требует для службы подключенной доставки сообщений или доставки через очередь: public enum QueuedDeliveryRequirementsMode { Al lowed. Required. NotAl lowed } [Attri butellsage( Attri buteTargets.Interface|AttributeTargets.Class. AllowMultiple = true)] public sealed class DeliveryRequirementsAttribute : Attribute,... { publi c QueuedDeliveryRequirementsMode QueuedDeli veryRequi rements {get:set;} public bool RequireOrderedDel!very {get;set;} public Type Targetcontract {get;set;} } Атрибут может использоваться для ограничения контрактов (и всех его ко- нечных точек) или определенного типа службы. По умолчанию свойство QueuedDeliveryRequirements равно QueuedDeliveryRequirementsMode.Allowed, поэто- му следующие определения эквивалентны: [ServiceContract] interface IMyContract {...} [ServiceContract] [DeliveryRequi rements] interface IMyContract {•••} [ServiceContract] [DeliveryRequi rements(QueuedDeli veryRequi rements = QueuedDeli veryRequi rementsMode.Al 1 owed)]
Ответная служба 427 interface IMyContract I...} QueuedDeliveryRequirementsMode.Allowed разрешает использование контракта или службы как для подключенных вызовов, так и для вызовов с очередями. QueuedDeliveryRequirementsMode.NotAllowed запрещает использование привязки MSMQ, авсе вызовы к конечной точке должны быть подключенными. Режим использу- ется в том случае, когда контракт или служба спроектированы в расчете только на подключенное использование. Режим QueuedDeliveryRequirementsMode.Required выполняет обратную функцию — он требует обязательного использования при- вязки MSMQ для конечной точки и используется для контрактов и служб, из- начально проектируемых в расчете на работу с очередями. Обратите внимание: хотя атрибут DeliveryRequirements предоставляет свойст- во RequireOrderedDelivery (см. главу 1), при использовании QueuedDeliveryRequire- mentsMode.Required свойство RequireOrderedDelivery должно быть ложным, потому что вызовы с очередями по своей природе не упорядочены, а сообщения могут воспроизводиться в произвольном порядке. Если атрибут DeliveryRequirements применяется к интерфейсу, его действие распространяется на все службы, предоставляющие конечные точки с указан- ным контрактом: [ServiceContract] [Del i veryRequi rements (QueuedDel 1 veryRequirements = QueuedDeli veryRequi rementsMode.Requi red)] interface IMyQueuedContract (...) Клиент также может применить атрибут DeliveryRequirements к своей копии контракта службы. Когда атрибут DeliveryRequirements применяется к классу службы, он влияет на все конечные точки этой службы: [Del i veryRequi rements (QueuedDel i veryRequi rements = QueuedDeli veryRequirementsMode.Requi red)] class MyQueuedService : IMyQueuedContract.IMyOtherContract (...) При применении к классу службы при использовании свойства Targetcontract, атрибут влияет на все конечные точки службы, предоставляющие указанный контракт: [Deli veryRequirements(Targetcontract = typeof(IMyQueuedContract). QueuedDeliveryRequirements = QueuedDeli veryRequi rementsMode.Requi red)] class MyService : IMyQueuedContract,IMyOtherContract тветная служба Программная модель вызовов с очередями, описанная ранее, была асимметрич- ной: клиент отправлял в очередь сообщение, а служба обрабатывала его. Такая модель хорошо подходит для ситуаций, в которых операции с очередями явля-
428 Глава 9. Очереди ются односторонними вызовами вследствие своей архитектуры. Однако может оказаться, что служба с очередью должна вернуть клиенту результат вызова, возвращаемое значение или даже ошибку. В конфигурации по умолчанию это невозможно. WCF приравнивает вызовы с очередями к односторонним вызо- вам, что делает любые ответы в принципе невозможными. Кроме того, службы с очередями (и их клиенты) могут работать в отключенном режиме. Если кли- ент отправляет вызов с очередью отключенной службе, к тому моменту, когда служба получит сообщения и обработает их, клиент может быть недоступен. Проблема решается отправкой ответа службой в специальную службу с очере- дью, предоставленную клиентом. Я называю такие службы ответными служба- ми1 (response service). Архитектура подобного решения представлена на рис. 9.12. Очередь службы Рис. 9.12. Ответная служба Ответная служба — всего лишь еще одна служба с очередью в системе. От- ветная служба может быть отключена от клиента, может размещаться в отдель- ном процессе или на отдельном компьютере или же использовать клиентский процесс. Если ответная служба использует клиентский процесс, то при запуске клиента она начинает обрабатывать ответы в очереди. Выделение ответной службы в отдельный процесс (и даже на отдельный компьютер) способствует повышению уровня логической изоляции ответной службы от клиента(-ов), ко- торый ее использует. ПРИМЕЧАНИЕ --------------------------------------------------------------- Не всем службам с очередями нужна ответная служба, и как вы вскоре увидите, ответы повышают сложность системы. Будьте практичны и используйте ответные службы только там, где это уместно (то есть там, где они работают наиболее эффективно). 1 Мое описание ответных служб впервые было опубликовано в «MSDN Magazine» (февраль 2007 г.).
Ответная служба 429 Проектирование контракта ответной службы Как и при использовании любой другой службы WCF, клиент и служба долж- ны заранее согласовать контракт ответа и его использование (возвращаемые значения с информацией об ошибках или просто возвращаемые значения). От- ветную службу можно разделить на две службы: одна отвечает за результаты, а другая — за сбои и ошибки. Для примера возьмем контракт ICaLuLator, реализо- ванный службой с очередью MyCalcuLator: [ServiceContract] interface ICalculator [OperationContractdsOneWay = true)] void Add(int number1,int number2): //... [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculator : ICalculator [...} Служба MyCalculator должна вернуть клиенту результат вычисления и сооб- щения о любых ошибках. Результат вычисления является целым числом, а ошибка представлена контрактом данных ExceptionDetail, описанным в главе 6. Для ответной службы контракт ICalculatorResponse может определяться следую- щим образом: [ServiceContract] interface ICalculatorResponse [OperationContractdsOneWay = true)] void OnAddCompleted(int result,ExceptionDetail error); Ответная служба, поддерживающая ICalculatorResponse, должна проанализи- ровать возвращаемую информацию об ошибке, оповестить клиентское прило- жение, пользователя или администратора приложения о завершении метода и предоставить результаты заинтересованным сторонам. Пример простой от- ветной службы с поддержкой ICalculatorResponse представлен в листинге 9.21. Листинг 9.21. Простая ответная служба [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyCalculatorResponse : ICalculatorResponse ( public void OnAddCompleted(int result,ExceptionDetail error) { MessageBox.Show("result = " + result,"MyCalculatorResponse"); if(error != null) ( // Обработка ошибки ) } )
430 Глава 9. Очереди Адрес ответа и идентификатор метода В реализации MyCaLcuLator и MyCalculatorResponse существуют две очевидные проблемы. Проблема первая: одна ответная служба может использоваться для обработки ответов по нескольким вызовам нескольких служб с очередями, од- нако в листинге 9.21 MyCalculatorResponse (и что еще важнее, обслуживаемый ей клиент) не различает эти вызовы. Для решения этой проблемы клиент, выдав- ший исходный вызов с очередью, помечает его неким уникальным идентифика- тором (уникальным хотя бы в пределах клиентского приложения). Служба с очередью MyCalculator должна передавать идентификатор ответной службе MyCalculatorResponse, чтобы та могла применить соответствующую логику. Про- блема вторая — определение службой с очередью адреса ответной службы. В отличие от дуплексных обратных вызовов, в WCF не существует встроенной поддержки передачи службе ссылки на ответную службу, поэтому службе с очередью придется конструировать посредника к ответной службе и вызывать операции ответного контракта. Хотя ответный контракт определяется во время проектирования и при этом всегда используется привязка NetMsmqBinding (или ее разновидность), служба с очередью не располагает адресом ответной службы, чтобы иметь возможность ответить. Адрес можно разместить в конфигурацион- ном файле хоста службы (в клиентской секции), но, конечно, такого способа сле- дует избегать. Дело в том, что одна служба с очередью может вызываться не- сколькими клиентами, каждый из которых имеет собственную ответную службу и адрес. Одно из возможных решений — явная передача как идентифи- катора, управляемого клиентом, так и желательного адреса ответной службы в параметрах каждой операции контракта службы с очередями: [ServiceContract] interface ICalculator { [OperationContract(IsOneWay = true)] void Adddnt numberl.int number2.string responseAddress,string methodld): } По аналогии, служба с очередью может явно передавать идентификатор ме- тода ответной службе в параметре каждой операции ответного контракта: [ServiceContract] interface ICalculatorResponse { [OperationContractCIsOneWay = true)] void OnAddCompleteddnt result.ExceptionDetail error.string methodld); } Заголовки сообщений Решение с передачей адреса и идентификатора в специальных параметрах рабо- тает, но оно искажает исходный контракт, а параметры бизнес-уровня смешива- ются со «служебными» параметрами в одной операции. Более правильное ре- шение — сохранение клиентом адреса ответа и идентификатора операции в заголовках исходящего сообщения. Такое использование заголовков сообще- ний является универсальным способом передачи службе внеполосной инфор- мации (то есть информации, не входящей в контракт службы).
Ответная служба 431 Контекст операции содержит коллекции входящих и исходящих заголовков, хранящихся в свойствах IncomingMessageHeaders и OutgoingMessageHeaders: public sealed class OperationContext : ... { public MessageHeaders IncomingMessageHeaders [get:} public MessageHeaders OutgoingMessageHeaders {get:} //... Все коллекции относятся к типу MessageHeaders — коллекции объектов Mes- sageHeader: public sealed class MessageHeaders : IEnumerable<...>.... public void Add(MessageHeader header); public T GetHeader<T>(int index); public T GetHeader<T>(string name.string ns): //... Класс MessageHeader не предназначен для прямого использования разработ- чиками. Вместо него следует использовать класс MessageHeader<T>, который обеспечивает типизованные и простые преобразования типов CLR в заголовки сообщений: public abstract class MessageHeader : {...} public class MessageHeader<T> public MessageHeader(); public MessageHeader(T content): public T Content (get;set:} public MessageHeader GetUntypedHeader(string name.string ns): //... В качестве параметра типа в шаблоне MessageHeader<T> может использовать- ся любой тип, сериализуемый или входящий в контракт данных. MessageHeade- г<Т> можно сконструировать на базе типа CLR, а затем воспользоваться мето- дом GetUntypedHeader() для его преобразования в MessageHeader и сохранения в исходящих заголовках. Методу GetUntypedHeader() передаются имена парамет- ра типа и пространства имен. Эти имена используются позднее для поиска заго- ловка в коллекции. Поиск осуществляется методом GetHeader<T>() класса Mes- sageHeaders. Класс Responsecontext Так как клиент должен передать в заголовках сообщений как адрес, так и иден- тификатор метода, простого примитивного параметра типа недостаточно. Вме- сто него стоит воспользоваться моим классом ResponseContext, определенным в листинге 9.22.
432 Глава 9. Очереди Листинг 9.22. Класс Responsecontext namespace ServiceModelEx { [DataContract] public class Responsecontext { [DataMember] public readonly string ResponseAddress: [DataMember] public readonly string FaultAddress: [DataMember] public readonly string Methodld: public ResponseContext(string responseAddress.string methodld) : this(responseAddress,methodId,null) {} public Responsecontext(Responsecontext responsecontext) : thi s(responsecontext.ResponseAddress.responsecontext.Methodld. responsecontext.FaultAddress) {} public Responsecontext(string responseAddress) : this(responseAddress. Guid.NewGuid().ToString()) {} public Responsecontext(string responseAddress.string methodld. string faultAddress) { ResponseAddress = responseAddress: Methodld = methodld; FaultAddress = faultAddress: } //... } } ResponseContext предоставляет место для хранения ответного адреса и иден- тификатора; если клиент вдобавок пожелает использовать отдельную ответную службу для сбоев, ResponseContext также предоставляет поле для ее адреса (в на- стоящей главе эта возможность не используется). Клиент несет ответствен- ность за конструирование экземпляра ResponseContext с уникальным идентифи- катором. Хотя клиент может передать идентификатор в параметре конструктора, он также может использовать конструктор ResponseContext, получающий только ответный адрес. В этом случае конструктор сам сгенерирует GUID для иденти- фикатора. Программирование на стороне клиента Клиент может предоставлять идентификатор при каждом вызове метода, даже при использовании сеансовой службы с очередью, посредством использования разных экземпляров ResponseContext при каждом вызове. Клиент должен хра- нить экземпляр ResponseContext в заголовках исходящих сообщений. Кроме того, клиент должен это делать в новом контексте операции; он не может
юльзовать существующий контекст операции. Данное требование относится с к службам, так и к традиционным («не-служебным») клиентам. WCF по- шет клиенту принять для текущего потока новый контекст операции при мощи класса OperationContextScope, определяемого следующим образом: lie sealed class OperationContextScope : IDisposable public OperationContextScopedContextChannel channel); public OperationContextScope(OperationContext context): public void Dispose(); Использование OperationContextScope — универсальный способ порождения вого контекста в тех случаях, когда имеющийся контекст вас не устраивает, юструктор OperationContextScope заменяет контекст операции текущего пото- новым контекстом. Вызов Dispose() для экземпляра OperationContextScope вос- анавливает старый контекст (даже если он был равен null). Если метод Dis- se() не будет вызван, это может повредить другим объектам того же программного тока, рассчитывающим на наличие предыдущего контекста. По этой причине асе OperationContextScope спроектирован в расчете на использование в конст- кциях using и предоставляет новый контекст операции только в ограничен- iM блоке: ing(OperationContextScope scope = new OperationContextScope(...)) 11 Работа с новым контекстом //Здесь восстанавливается предыдущий контекст При конструировании нового экземпляра OperationContextScope конструкто- г передается внутренний канал посредника, используемого для вызова. Кли- т должен создать экземпляр нового объекта ResponseContext с ответным адре- м и идентификатором, добавить ResponseContext в заголовок сообщения, здать новый экземпляр OperationContextScope, добавить заголовок в заголовки ходящих сообщений нового контекста и вызвать посредника. Все эти дейст- я продемонстрированы в листинге 9.23. 1стинг 9.23. Программирование использования ответной службы на стороне клиента 'ing methodld = GenerateMethodldO; 'ing responseQueue = "net.msmq://1ocalhost/pri vate/MyCalculatorResponseQueue"; ResponseContext responsecontext - new ResponseContext(responseQueue.methodld); MessageHeader<ResponseContext> responseHeader = new MessageHeader<ResponseContext>(responsecontext); culatorClient proxy = new CalculatorsientO; ng(OperationContextScope contextscope = new OperationContextScope(proxy.InnerChannel)) Operati onContext.Current.Outgoi ngMessageHeaders.Add( responseHeader GetUntypedHeader("ResponseContext"."ServiceModelEx")); продолжение &
434 Глава 9. Очереди proxy.Add(2.3): } proxy. Closed; // Вспомогательный метод string GenerateMethodldO В листинге 9.23 клиент генерирует уникальный идентификатор вспомога- тельным методом GenerateMethodId(). Затем он создает новый экземпляр Respon- seContext, передавая в параметрах конструктора ответный адрес и идентифика- тор. Клиент использует конструкцию MessageHeader<ResponseContext> для упаков- ки экземпляра ResponseContext в заголовке сообщения. Клиент конструирует но- вого посредника к службе с очередью и использует его внутренний канал в ка- честве параметра конструирования нового экземпляра OperationContextScope. Конструкция using обеспечивает автоматическое восстановление предыдущего контекста операции. Внутри блока клиент работает с новым контекстом опера- ции, получая его из коллекции OutgoingMessageHeaders. Клиент вызывает метод GetUntypedHeader() типизованного заголовка для преобразования его в Message- Header, предоставляет имя и пространство имен ответного контекста и включает заголовок в коллекцию исходящих заголовков. Клиент вызывает посредника, уничтожает временный контекст и закрывает посредника. Теперь сообщение содержит внеполосный ответный адрес и идентификатор. Программирование на стороне службы Служба с очередью обращается к своей коллекции заголовков входящих сооб- щений и читает из нее ответный адрес с идентификатором метода. Адрес нужен службе для конструирования посредника к ответной службе, а идентифика- тор — для передачи его ответной службе. Обычно служба не может использо- вать идентификатор напрямую, и ей приходится прибегать к приему, исполь- зуемому клиентом для передачи идентификатора метода ответной службе; а именно использовать исходящие заголовки для внеполосной передачи иден- тификатора ответной службе (вместо явной передачи в параметрах). Как и в случае с клиентом, служба тоже должна использовать новый контекст опера- ции при помощи OperationContextScope для изменения коллекции исходящих за- головков. Служба может на программном уровне сконструировать посредника к ответной службе, предоставляя посреднику ответный адрес и экземпляр Net- MsmqBinding, и даже прочитать параметры привязки из конфигурационного файла. Процедура продемонстрирована в листинге 9.24. Листинг 9.24. Программирование использования ответной службы на стороне службы [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculator : ICalculator { [0perat1onBehavior(Transact1onScopeRequired = true)] public void Adddnt numberl.int number2) { int result = 0;
Ответная служба 435 ExceptionDetail error = null; try { result = numberl + number2: } // Исключение не перезапускается catch(Except!on exception) { error = new ExceptionDetail(exception); } finally ( ResponseContext responsecontext = Operationcontext.Current.Incomi ngMessageHeaders. GetHeader<ResponseContext>("ResponseContext"."ServiceModelEx"); EndpointAddress responseAddress = new EndpointAddress(responsecontext.ResponseAddress); MessageHeader<ResponseContext> responseHeader = new MessageHeader<ResponseContext>(responsecontext); NetMsmqBinding binding = new NetMsmqBindingO; CalculatorResponseClient proxy = new Ca1culatorResponseClient(binding.responseAddress): using(OperationContextScope scope = new OperationContextScope(proxy.InnerChannel)) ( Operationcontext.Current.Outgo!ngMessageHeaders.Add( responseHeader.GetUntypedHeader("ResponseContext", "ServiceModelEx")); proxy.OnAddCompleted(result,error); } proxy.CloseO: } } В листинге 9.24 служба перехватывает все исключения, инициированные операцией бизнеслогики, и упаковывает их в объекты ExceptionDetail. Служба не перезапускает исключение — как будет показано позднее при описании от- ветных служб и транзакций, перезапуск приведет к отмене ответа. Более того, при использовании ответных служб возможность реагирования в случае ошиб- ки является гораздо лучшей стратегией, чем средства обработки ошибок WCF при воспроизведении. В секции finally, независимо от исключений, служба выдает ответ. Она обра- щается к коллекции входящих заголовков своего контекста операции и при по- мощи метода GetHeader<T>() извлекает контекст ответа, предоставленный кли- ентом. Обратите внимание на использование имени и пространства имен при поиске ответного контекста в коллекции входящих заголовков. Затем служба конструирует посредника к ответной службе, используя адрес из ответного кон- текста и новый экземпляр NetMsmqBinding. В новом блоке OperationContextScope служба включает в исходящие заголовки ответный контекст, полученный на
436 Глава 9. Очереди входе, и вызывает посредника ответной службы, что фактически означает по- становку ответа в очередь. Далее служба уничтожает временный контекст и за- крывает посредника. Программирование на стороне ответной службы Ответная служба обращается к своей коллекции входящих заголовков, читает из нее идентификатор метода и соответствующим образом реагирует на него. В листинге 9.25 приведена возможная реализация такой службы. Листинг 9.25. Реализация ответной службы [ Servi ceBeha v 1 or (I nstanceContextMode = I nstanceContextMode. PerCa ID] class MyCalculatorResponse : ICalculatorResponse { public static event GenericEventHandler<string.int> AddCompleted = delegate}}; public static event GenericEventHandler<string> AddError = delegate}}; [0perat1onBehavior(TransactionScopeRequired = true)] public void OnAddCompleted(int result.ExceptionDetail error) { ResponseContext responsecontext = Operationcontext.Current.Incomi ngMessageHeaders. GetHeader<ResponseContext>("ResponseContext"."ServiceModelEx"); string methodld = responsecontext.Methodld; if(error == nul1) { AddCompleted(method Id.result): ) else { AddError(methodld); } } } Ответная служба в листинге 9.25 предоставляет два открытых статических события: для оповещения о завершении и для оповещения об ошибках. Ответ- ная служба обращается к своим входным заголовкам контекста операции, и как в случае со службой с очередью, извлекает из них ответный контекст, а из него — идентификатор метода. Затем ответная служба активизирует делегата завершения, с предоставлением результата операции и идентификатора. В слу- чае исключения ответная служба активизирует делегата оповещения о сбоях, передавая только идентификатор метода. Упрощение ответной службы Представленная схема реализации и использования ответной службы работает, но у нее есть свои недостатки. Главная проблема реализаций из листингов 9.23, 9.24 и 925 заключается в том, что бизнес-логика службы и клиентский код «за-
Ответная служба 437 грязняются» низкоуровневым программированием WCF. К счастью, все техни- ческие подробности можно упростить и инкапсулировать во вспомогательных классах до такого уровня, при котором каждая участвующая сторона взаимо- действует с другими сторонами без открытого использования «служебного» кода. На первом этапе инкапсулируется взаимодействие с низкоуровневыми заго- ловками сообщений, для чего в ResponseContext включается статическое свойст- во Current: [DataContract] public class ResponseContext { public static ResponseContext Current {get;set;} //... Реализация свойства Current представлена в листинге 9.26. Листинг 9.26. Свойство ResponseContext.Current [DataContract] public class ResponseContext f public static ResponseContext Current { get { Operationcontext context = Operationcontext.Current; If(context == null) { return nul1; } try { return context.IncomlngMessageHeaders. GetHeader<ResponseContext>("ResponseContext"."ServiceModel Ex"); } catch { return null; } } set { Operationcontext context = Operationcontext.Current; Debug.Assert(context != null); // Наличие нескольких заголовков ResponseContext - ошибка bool headerExIsts - false; try { context.OutgolngMessageHeaders.GetHeader<ResponseContext> ("ResponseContext"."ServiceModelEx"); headerExIsts - true; } продолжение &
438 Глава 9. Очереди catch(MessageHeaderException exception) { Debug.Assert(excepti on.Message == "There is not a header with name ResponseContext and namespace ServiceModelEx in the message."): } if(headerExists) { throw new InvalidOperationException( "A header with name ResponseContext and namespace ServiceModelEx already exists in the message."); } MessageHeader<ResponseContext> responseHeader = new MessageHeader<ResponseContext>(value): context.Outgoi ngMessageHeaders.Add (responseHeader.GetUntypedHeader( "ResponseContext","ServiceModelEx")); } } // Далее как в листинге 9.22 } Секция get свойства Current возвращает null, если входящие заголовки не со- держат ответного контекста. Это позволяет службе с очередью проверить, су- ществует ли сторона, которой нужно ответить: if(ResponseContext.Current != null) ( // Здесь выдается ответ } Обратите внимание: секция set свойства Current проверяет, чтобы в исходя- щих заголовках текущего контекста операции не было другого ответного кон- текста. Упрощение клиентского кода Для упрощения и автоматизации работы клиента потребуется базовый класс посредника, инкапсулирующий основные этапы подготовки ответной службы из листинга 9.23. В WCF такого класса нет (в отличие от дуплексного обратно- го вызова), поэтому его придется построить вручную. Для упрощения этой за- дачи я написал класс ResponseClientBase<T>, определяемый следующим образом: public abstract class ResponseClientBase<T> : ClientBase<T> where T : class { public readonly string ResponseAddress: public ResponseClientBase(string responseAddress): public ResponseClientBase(string responseAddress,string endpointName): public ResponseClientBase(string responseAddress, NetMsmqBinding binding,EndpointAddress remoteAddress): /* Дополнительные конструкторы */ protected string Enqueue(string operation,params object[] args): protected virtual string GenerateMethodldO: } Чтобы использовать ResponseClientBase<T>, создайте на его основе конкрет- ный класс, указав в качестве параметра типа тип контракта с очередью. В отли-
Ответная служба 439 чие от обычных посредников, субкласс не наследует от контракта. Вместо этого следует предоставить похожий набор методов, возвращающих строку с иденти- фикатором метода вместо void (вот почему наследование от контракта невоз- можно — операции контракта ничего не возвращают, поскольку все они явля- ются односторонними). Для примера возьмем следующий контракт службы с очередью: [ServiceContract] interface ICalculator f [OperationContractdsOneWay = true)] void Add(int number1,int number2); //... В листинге 9.27 показан соответствующий посредник для ответной службы. Листинг 9.27. Создание класса, производного от ResponseClientBase<T> class Calculatorclient : ResponseClientBase<ICa1culator> public CalculatorClient(string responseAddress) : base(responseAddress) {} public CalculatorClient(string responseAddress.string endpointName) : base(responseAddress.endpoi ntName) 0 public CalculatorClient(string responseAddress. NetMsmqBinding binding.EndpointAddress remoteAddress) : base(responseAddress.binding.remoteAddress) {} /* Другие конструкторы */ public string Add(int numberl.int number2) { return Enqueue("Add".numberl.number2); } } При использовании ResponseClientBase<T> листинг 9.23 сокращается до сле- дующего фрагмента: string responseAddress = "net.msmq://1 оса 1 host/pr ivate/MyCa1culatorResponseQueue": Calculatorclient proxy = new CalculatorClient(responseAddress): string methodld = proxy.Add(2.3); proxy. Cl ose(); Виртуальный метод GenerateMethodId() класса ResponseClientBase<T> исполь- зует GUID в качестве идентификатора метода. Ваш субкласс ResponseClientBase<T> может переопределить его и предоставить любую другую уникальную строку — например, статический целочисленный счетчик: class Calculatorclient : ResponseClientBase<ICalculator> static int m_MethodId = 123; protected override string GenerateMethodId() { 1ock(typeof(CalculatorClient))
440 Глава 9. Очереди { int id = ++m_MethodId; return id.ToStringO; } } //... } В листинге 9.28 приведена реализация ResponseClientBase<T>. Листинг 9.28. Реализация ResponseClientBase<T> public class ResponseCIientBase<T> ; ClientBase<T> where T : class { public readonly string ResponseAddress; public ResponseCIientBase(string responseAddress) { ResponseAddress = responseAddress; QueuedServi ceHelper.Veri fyQueue(Endpoi nt); Debug.Assert(Endpoint.Binding is NetMsmqBinding); } public ResponseCIientBase(string responseAddress.string endpointName) : base(endpointName) { ResponseAddress = responseAddress; QueuedServi ceHelper.Veri fyQueue(Endpoi nt); Debug.Assert(Endpoint.Binding is NetMsmqBinding); } public ResponseCIientBase(string responseAddress. NetMsmqBinding binding.EndpointAddress remoteAddress) : base(binding.remoteAddress) { ResponseAddress = responseAddress; QueuedServi ceHelper.Veri fyQueue(Endpoi nt); } protected string Enqueue(string operation.params object[] args) { using(OperationContextScope contextscope = new OperationContextScopednnerChannel)) { string methodld = GenerateMethodId(); ResponseContext responsecontext = new ResponseContext(ResponseAddress.methodld); ResponseContext.Current = responsecontext; Type contract = typeof(T); //He поддерживает иерархию контрактов и перегрузку Methodinfo methodinfo = contract.GetMethod(operation); methodinfo.Invoke(Channel.args); return responsecontext.MethodId; } } protected virtual string GenerateMethodldO { return Guid.NewGuidO.ToStringO; } }
Ответная служба 441 Конструкторы ResponseClientBase<T> получают ответный адрес и типичные параметры посредника — имя конечной точки, адрес и привязку. Конструкторы сохраняют ответный адрес в открытом поле, доступном только для чтения. ResponseCLientBase<T> наследует от обычного ClientBase<T>, поэтому все конст- рукторы перепоручают работу соответствующим базовым конструкторам. Кро- ме того, конструкторы используют мой метод QueuedServiceHelper.VerifyQueue() для проверки существования очереди (и DLQ) и ее создания в случае необхо- димости. Конструкторы также проверяют, что для конечной точки службы ис- пользуется привязка NetMsmqBinding, потому что посредник может использовать- ся только для вызовов с очередями. «Сердцем» ResponseClientBase<T> является метод Enqueue(), получающий имя вызываемой операции (вернее, для которой сообщение ставится в очередь) и параметры операции. Метод Enqueue() создает блок с новым контекстом операции, генерирует новый идентификатор метода и сохраняет идентификатор и адрес ответа в ResponseContext. Затем Response- Context заносится в исходящие заголовки установкой метода посредством зада- ния ResponseContext.Current. Затем Enqueue() использует механизм рефлексии для вызова операции с указанным именем. Вследствие применения рефлексии и позднего связывания ResponseClientBase<T> не поддерживает иерархии кон- трактов или перегрузки операций. Эти возможности придется программиро- вать вручную, как в листинге 9.23. ПРИМЕЧАНИЕ----------------------------------------------------------------------- Если ответная служба с очередью должна использоваться с подключенной службой (не использую- щей NetMsmqBinding), переработайте класс ResponseClientBase<T> для использования Binding, пе- реименуйте Enqueue() в InvokeQ и избегайте предположений об использовании вызовов с поста- новкой в очередь. Упрощение кода службы с очередью Для упрощения и автоматизации работы, выполняемой службой при извлечении параметров из заголовков и передаче ответа, я создал класс ResponseScope<T>: public class ResponseScope<T> : IDisposable where T : class f public readonly T Response; public ResponseScopeO; public ResponseScope(string bindingconfiguration): public ResponseScope(NetMsmqBinding binding); public void DisposeO; } Объект ResponseScope<T> является временным — он устанавливает новый контекст операции, а при его уничтожении восстанавливается старый контекст операции. Чтобы восстановление выполнялось даже при возникновении ис- ключений, ResponseScope<T> следует использовать в конструкции using. Respon- seScope<T> получает параметр типа, представляющий ответный контракт, и пре- доставляет открытое, доступное только для чтения поле Response того же типа (см. листинг 9.29). Response содержит посредника к ответной службе, а клиенты ResponseScope<T> используют Response для вызова операций ответной служ- бы. Уничтожать Response не обязательно, потому что ResponseScope<T> делает
442 Глава 9. Очереди это в Dispose(). При использовании ResponseScope<T> секция finally из листинга 9.24 сокращается до следующего фрагмента: finally { using(ResponseScope<ICalculatorResponse> scope « new ResponseScope<ICalculatorResponse>0) { scope.Response.OnAddCompleted(result.error); } } Листинг 9.29. Реализация ResponseScope<T> public class ResponseScope<T> : IDisposable where T : class { OperationContextScope m_Scope; public readonly T Response; public ResponseScope() : this(new NetMsmqBinding()) {} public ResponseScope(string bindingconfiguration) : thi s(new NetMsmqBi nding(bi ndi ngConfigurati on)) {} public ResponseScope(NetMsmqBinding binding) { ResponseContext responsecontext = ResponseContext.Current: EndpointAddress address = new EndpointAddress(responsecontext.ResponseAddress); ChannelFactory<T> factory = new ChannelFactory<T>(binding.address): QueuedServiceHelper.Veri fyQueue(factory.Endpoi nt); Response = factory.CreateChannel0: 11 Переключение контекста m_Scope = new OperationContextScope(Response as IContextChannel): ResponseContext.Current = responsecontext: } public void DisposeO { IDisposable disposable = Response as IDisposable: disposable.Dispose(): m_Scope.Dispose(); } } Конструктор ResponseScope<T> использует ResponseContext.Current для извле- чения входного ответного контекста. Затем при помощи ChannelFactory он про- веряет, что ответная очередь существует, и инициализирует Response посредни- ком к ответной службе. Хитрость в реализации ResponseScope<T> заключается в использовании OperationContextScope без конструкции using. Конструируя но- вый объект OperationContextScope, ResponseScope<T> устанавливает новый кон- текст операции. Старый контекст будет восстановлен при уничтожении
Ответная служба 443 ResponseScope<T> конструкцией using1. Затем ResponseScope<T> использует ResponseContext.Current для включения контекста ответа в исходящие заголовки нового контекста и возвращает управление. Упрощения кода ответной службы Ответная служба должна извлечь идентификатор метода из входящих заго- ловков. Для этого достаточно воспользоваться соответствующим свойством ResponseContext.Current. После этого метод OnAddCompleted() в листинге 9.25 со- кращается до следующего фрагмента: public void OnAddCompleted(Int result.ExceptionDetall error) string methodld = ResponseContext.Current.Methodld: if(error == null) AddCompleted(method Id,result): } else ( AddError(methodld); Транзакции Служба с очередью обычно ставит ответ в очередь как часть входной тран- закции воспроизведения. Для определения службы из листинга 9.30 на рис. 9.13 изображена схема итоговой транзакции и тех действий, из которых она состоит. Листинг 9.30. Постановка ответа в очередь как часть транзакции воспроизведения [Servi ceBehavi or (I nstanceContextMode = I nstanceContextMode. PerCa ID] class MyCalculator : ICalculator ( [OperationBehav1or(TransactionScopeRequired я true)] public void Adddnt numberl.Int number2) try { } catch //He перезапускать исключение { } final ly ( us1ng(ResponseScope<ICalculatorResponse> scope _ л продолжение тУ 1 Речь идет о внешнем using. — Примеч. перев.
444 Глава 9. Очереди = new ResponseScope<ICalciilatorResponse>()) { scope.Response.OnAddCompleted(...); Очередь службы Рис. 9.13. Постановка сообщения в очередь в транзакции воспроизведения С точки зрения архитектуры у объединения в одной транзакции воспроизве- дения вызова из очереди и ответа с очередью имеется важное преимущество: если транзакция воспроизведения по какой-то причине будет отменена (в том числе и из-за других служб, участвующих в транзакции), то и ответ автоматиче- ски отменяется. Этот вариант является самым распространенным для большин- ства приложений. В листинге 9.30 обратите внимание на то, что служба пере- хватывает все исключения и не перезапускает их. Это важно, потому что любое необработанное (или перезапущенное) исключение приведет к отмене ответа, а значит, службе не стоит даже пытаться отвечать. Использование ответной службы по сути означает, что служба не полагается на механизм автоповтора WCF и самостоятельно обрабатывает свои сбои бизнес-логики, потому что кли- ент ожидает от нее заранее определенной реакции. Использование новой транзакции Вместо постоянного присутствия ответа в транзакции воспроизведения, служба также может ответить в новой транзакции; для этого ответ заключается в но- вый транзакционный контекст, как показано в листинге 9.31, и графически - на рис. 9.14.
Ответная служба 445 Листинг 9.31. Ответ в новой транзакции [ServiceBehavlor(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculator : ICalculator I [OperationBehavior(Transact!onScopeRequired = true)] public void Add(int number1.int number?) { finally { using(TransactionScope transactionscope = new TransactionScope(TransactionScopeOption.RequiresNew)) using(ResponseScope<ICalculatorResponse> responseScope = new ResponseScope<ICalculatorResponse>0) { scope.Response.OnAddCompleted(...); } } } ) Очередь службы Рис. 9.14. Ответ в новой транзакции Ответ в новой транзакции необходим в двух случаях. Первый — если служба желает ответить независимо от исхода транзакции воспроизведения, которая может быть отменена другими вызываемыми службами. Второй случай — если ответ полезен, но не критичен, и служба не возражает против закрепления транзакции воспроизведения с отменой ответа. Ответная служба и транзакции Поскольку ответная служба представляет собой обычную службу с очередью, механика управления такой транзакцией и участия в ней в целом не отличается
446 Глава 9. Очереди от других служб с очередями. Однако существует ряд особенностей, заслужи- вающих упоминания в этом конкретном контекста. Ответная служба может об- работать ответ как часть входной транзакции воспроизведения ответа: [ServiceBehavlor(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculatorResponse : ICalculatorResponse { [OperatlonBehavlorCTransactionScopeRequlred = true)] public void OnAddCompleted(...) {} } Этот способ является самым распространенным, потому что он допускает повторные попытки. С другой стороны, ответная служба должна избегать про- должительной обработки ответов с очередями, потому что это может создать риск отмены транзакции воспроизведения. Ответная служба может обработать ответ в отдельной транзакции, если ответ полезен, но не критичен (с точки зре- ния стороны, предоставляющей ответную службу): [ServiceBehavlor(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculatorResponse : ICalculatorResponse { public void OnAddCompleted(Int result.ExceptionDetai1 error) { usingCTransactionScope scope = new Transact1onScopeCTransactlonScopeOptlon.RequiresNew)) } } При обработке ответа в новой транзакции, в случае ее отмены WCF не пыта- ется снова передать ответ из очереди ответной службы. Наконец, при длитель- ной обработке ответа ответную службу можно настроить так, чтобы она вообще не использовала транзакции (включая транзакцию воспроизведения): [ServiceBehaviorCInstanceContextMode = InstanceContextMode.PerCai 1)] class MyCalculatorResponse : ICalculatorResponse { public void OnAddCompleted(...) {...} } HTTP-мосты Привязка MSMQ спроектирована для применения в интрасетях. По умолча- нию она не проходит через брандмауэры и, что еще важнее, использует коди- ровку и формат сообщений, специфические для Microsoft. Даже если вам удаст- ся организовать туннельную передачу через брандмауэр, другая сторона тоже должна обязательно использовать WCF. Если предположение о применении WCF обеими сторонами вполне разумно в интрасети, было бы нереалистично требовать этого от клиентов и служб в Интернете; к тому же такое требование нарушает основной принцип служебно-ориентированного программирования, согласно которому технология реализации службы должна быть несущественна
HTTP-мосты 447 для клиентов. Безусловно, интернет-службы могли бы извлечь пользу из вызо- вов с очередями, как и интрасетевые клиенты и службы, однако отсутствие от- раслевого стандарта для подобных взаимодействий (и отсутствие поддержки в WCF) делает их невозможными. Проблема решается с использованием меха- низма, который я называю HTTP-мостом. В отличие от большинства других приемов, продемонстрированных в книге, HTTP-мост представляет собой ско- рее архитектурную идиому, нежели группу вспомогательных классов в виде не- большой библиотеки. HTTP-мост, как следует из его названия, обеспечивает поддержку вызовов с очередями при взаимодействии со службой или клиентом по Интернету. Для построения моста используется WSHttpBinding, потому что эта привязка является транзакционной. Мост состоит из двух частей. Во-пер- вых, он позволяет клиентам WCF ставить вызовы в очередь к интернет-службе с привязкой WS, а во-вторых, он позволяет службе WCF, предоставляющей ко- нечную точку HTTP через привязку WS, ставить в очередь вызовы от интер- нет-клиентов. Две части моста могут использоваться как по отдельности, так и совместно. Мост может использоваться только в том случае, если контракт удаленной службы допускает очереди (то есть контракт состоит только из од- носторонних операций), но обычно это условие выполняется (иначе у клиента не было бы необходимости в использовании моста). Проектирование моста Так как привязка WS не позволяет реально ставить вызовы в очередь, прихо- дится использовать промежуточного мостового клиента и службу. Когда кли- ент желает поставить в очередь вызов к интернет-службе, он ставит его в оче- редь локальной (то есть интрасетевой) службы MyClientHttpBridge. Мостовая служба на стороне клиента при обработке вызова использует привязку WS для обращения к удаленной интернет-службе. Если интернет-служба желает получать Очередь ClientHttpBridge Очередь ServiceHttpBridge Рис. 9.15. НТТР-мост
448 Глава 9. Очереди вызовы с очередями, она использует очередь. Поскольку эта очередь может по- лучать обращения по Интернету только от WCF-клиентов, служба использует фасад — специальную подключенную службу MyServiceHttpBridge, предостав- ляющую конечную точку с привязкой WS. При обработке интернет-вызовов MyServiceHttpBridge просто ставит вызов в очередь локальной службы. Архитек- тура HTTP-моста показана на рис. 9.15. Настройка транзакций Очень важно, чтобы между клиентской стороной моста MyClientHttpBridge и уда- ленной службой использовались транзакции, а мост на стороне службы (MyServiceHttpBridge) был настроен в транзакционном режиме «клиент» (см. главу 7). Дело в том, что использование одной транзакции от воспроизведения клиентского вызова в MyClientHttpBridge до MyServiceHttpBridge позволяет имити- ровать транзакционную семантику доставки нормального вызова с очередью, как показано на рис. 9.16. Рис. 9.16. HTTP-мост и транзакции Сравните рис. 9.16 с рис. 9.6. Если транзакция доставки в мосте будет отме- нена по какой-либо причине, то сообщение вернется в очередь MyClientHttp- Bridge для повторной попытки. Чтобы шансы на успешную доставку были максимальными, также следует включить надежную доставку между MyClient- HttpBridge и удаленной службой. Конфигурация на стороне службы MyServiceHttpBridge преобразует обычный подключенный вызов через привязку WS в вызов с очередью и отправляет его в очередь службы. MyServiceHttpBridge реализует похожий, но не идентичный контракт к службе с очередью. Это свя- зано с тем, что мост на стороне службы должен иметь возможность участвовать
HTTP-мосты 449 во входной транзакции, но транзакции не могут распространяться по односто- ронним операциям. Проблема решается модификацией поддерживаемого кон- тракта. Например, если исходный контракт службы выглядит так: [ServiceContract] public interface IMyContract f [OperationContract(IsOneWay = true)] void MyMethodO: to MyServiceHttpBridge будет предоставлять следующий контракт: [ServiceContract] public interface IMyContractHttpBridge [OperationContract] [Transacti onFlow(T ransact1onFlowOpti on.Mandatory)] void MyMethodO: ) В сущности, необходимо задать IsOneWay равным false и использовать режим TransactionFlowOption.Mandatory. Чтобы программный код лучше читался, я реко- мендую также переименовать интерфейс, снабдив его суффиксом HttpBridge. MyServiceHttpBridge может размещаться где угодно в интрасети службы, в том числе и в собственном процессе службы. В листинге 9.32 показана необходимая конфигурация службы и ее НТТР-моста. Листинг 9.32. Конфигурация HTTP-моста на стороне службы ///////////// Конфигурационный файл MyService //////////////////// <services> <service name = "MyService"> <endpoint address - "net.msmq://localhost/private/MyServiceQueue" binding = "netMsmqBinding" contract = "IMyContract" /> </service> </services> ///////////// Конфигурационный файл MyServiceHttpBridge ////////////// <services> <service name = "MyServiceHttpBridge"> <endpoint address = "http://localhost:8001/MyServiceHttpBridge" binding = "wsHttpBinding" bindingconfiguration = "ReliableTransactedHTTP" contract - "IMyContractHttpBridge" /> </service> </services> <cl ient> <endpoint address « "net.msmq://localhost/private/MyServiceQueue" binding = "netMsmqBinding" contract = "IMyContract" /> продолжение &
450 Глава 9. Очереди </cl1ent> <bindings> <wsHttpBinding> <binding name = "RellableTransactedHTTP" transactionFlow = "true"> ReliableSession enabled = "true"/> </binding> </wsHttpBinding> </bindings> Служба MyService предоставляет простую конечную точку с очередью и под- держкой IMyContract. Служба MyServiceHttpBridge предоставляет конечную точку с привязкой WSHttpBinding и контрактом IMyContractHttpBridge. MyServiceHttpBridge также является клиентом конечной точки с очередью, определяемой службой. В листинге 9.33 представлена соответствующая реализация. Обратите внима- ние на настройку MyServiceHttpBridge в транзакционном режиме «клиент». Листинг 9.33. Реализация HTTP-моста на стороне службы [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { 11 Вызов передается no MSMQ [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO } [ServiceBehaviordnstanceContextMode = InstanceContextMode. PerCai 1)] class MyServiceHttpBridge : IMyContractHttpBridge { // Вызов передается no HTTP [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO { MyContractClient proxy = new MyContractClientO: // Вызов передается no MSMQ proxy.MyMethod(): proxy.Close(): } } Конфигурация на стороне клиента Клиент использует вызовы с очередью к локальной службе MyClientHttpBridge. MyClientHttpBridge может находиться даже в одном процессе с клиентом или на другом компьютере в интрасети клиента. MyClientHttpBridge использует WSHttp- Binding для вызова удаленной службы. Клиент должен загрузить метаданные удаленной интернет-службы (такие, как определение IMyContractHttpBridge) и преобразовать их в контракт с очередью (такой, как IMyContract). В листин- ге 9.34 представлена необходимая конфигурация клиента и его НТТР-моста. Листинг 9.34. Конфигурация HTTP-моста на стороне клиента /////////////// Конфигурационный файл клиента ///////////////// <client>
HTTP-мосты 451 <endpoint address = "net.msmq://1 oca1 host/private/MyCl1entHttpBri dgeQueue" binding = "netMsmqBinding” contract = "IMyContract" /> </client> ///////////// Конфигурационный файл MyClientHttpBridge 11111111111111 <services> <service name = "MyClientHttpBridge"> <endpoint address = "net.msmq://1ocalhost/pri vate/MyCli entHttpBri dgeQueue" binding = "netMsmqBinding" contract = "IMyContract" /> </service> </services> <cl ient> <endpoint address = "http://localhost:8001/MyServiceHttpBridge" binding = "wsHttpBinding" bindingconfiguration = "ReliableTransactedHTTP" contract = "IMyContractHttpBridge" /> </cl ient> <bindi ngs> <wsHttpBinding> <binding name = "ReliableTransactedHTTP" transact!onFlow = "true"> <reliableSession enabled = "true"/> </bi ndi ng> </wsHttpBinding> </bi ndi ngs> MyClientHttpBridge предоставляет простую конечную точку с поддержкой IMyContract. MyClientHttpBridge также является клиентом подключенной конеч- ной точки с WS-привязкой, определяемой службой. Соответствующая реализа- ция приведена в листинге 9.35. Листинг 9-35- Реализация HTTP-моста на стороне клиента MyContractClient proxy = new MyContractClient(); // Вызов передается no MSMQ proxy. MyMethod (): proxy. Cl ose(); //////////////// Реализация моста на стороне клиента //////////// [ServiceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyClientHttpBridge : IMyContract 11 Вызов передается no MSMQ [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO продолжение &
452 Глава 9. Очереди MyContractHttpBrldgeCl1 ent proxy = new MyContractHttpBrldgeClientO: // Вызов передается no HTTP proxy.MyMethodO: proxy.CloseO: } }
Безопасность Обеспечение безопасного взаимодействия между клиентом и службой подразу- мевает ряд аспектов. Как и в традиционных приложениях «клиент/сервер» и компонентно-ориентированных приложениях, служба должна проверить под- линность вызывающих сторон; нередко проверка проводится перед выполнением критических операций. Кроме того, при защите службы (и ее клиентов) незави- симо от применяемой технологии требуется организовать защиту сообщений на пути от клиента к службе. После того как сообщения будут безопасно доставле- ны, пройдут аутентификацию и авторизацию, у службы имеются различные ва- рианты выбора идентификационных данных, то есть личности (identity), от имени которой выполняется операция. К этим классическим аспектам безопас- ности — аутентификации, авторизации, безопасности передачи и управлению личностью — я добавляю более абстрактную концепцию, называемую общей по- литикой безопасности, то есть отношение и подход вашей организации к реали- зации безопасности. Глава начинается с определения аспектов безопасности в контексте WCF и возможностей, имеющихся в распоряжении разработчика при использовании средств безопасности WCF и .NET. Затем читатель увидит, как организовать безопасность в канонических, широко распространенных ти- пах приложений. Напоследок я представлю свою декларативную инфраструк- туру безопасности, которая значительно упрощает настройку многих деталей безопасности WCF. утентификация Аутентификацией (проверкой подлинности) называется процедура, которая выясняет, является ли вызывающая сторона именно тем, за кого она себя выда- ет. Хотя аутентификация обычно рассматривается в контексте проверки вызы- вающей стороны, с точки зрения клиента тоже существует необходимость в аутен- тификации службы; другими словами, клиент должен быть уверен в том, что вызов поступил именно той службе, к которой он собирался обратиться. Это особенно важно для вызовов, передаваемых по Интернету; если злоумышлен-
454 Глава 10. Безопасность ник переведет на себя обращения клиента к службе DNS, он сможет перехваты- вать вызовы клиента. Далее перечислены основные механизмы аутентифика- ции в WCF. О Без аутентификации — служба не проверяет, от кого поступили вызовы. Обращение разрешено практически любому желающему. О Аутентификация Windows — служба обычно использует Kerberos при дос- тупном Windows Domain Server, или NTLM при установке в конфигурации для рабочих групп. Вызывающая сторона предоставляет службе свои удо- стоверения (credentials) - например, мандат или электронный ключ, а служ- ба проводит их проверку средствами Windows. О Имя пользователя и пароль — вызывающая сторона предоставляет службе имя пользователя и пароль. Служба проверяет полученные данные по неко- торому источнику — например, по данным учетных записей Windows, или пользовательскому хранилищу вроде специальной таблицы в базе данных. О Сертификат Х509 — клиент идентифицирует себя при помощи сертификата. Обычно этот сертификат известен службе заранее. Служба проводит поиск сертификата на стороне хоста и убеждается в его подлинности, тем самым удостоверяясь в подлинности клиента. Также служба может автоматически доверять стороне, выдавшей сертификат, а следовательно, и представивше- му его клиенту. О Пользовательский механизм — WCF позволяет заменит механизм аутенти- фикации любым протоколом и типом удостоверений (например, биометри- ческими данными). Нестандартные решения такого рода выходят за рамки книги. О Выдача электронного ключа — как вызывающая сторона, так и служба могут использовать службу электронных ключей для выдачи клиенту электронно- го ключа. Служба распознает выданный ключ и доверяет ему. Пример служ- бы такого рода — Windows CardSpace. Темы федеральной безопасности и CardSpace выходят за рамки книги. Авторизация Авторизация определяет, какие действия разрешены вызывающей стороне; обычно — какие операции службы разрешено вызывать клиенту. Авторизация вызывающей стороны производится в предположении, что вызывающая сторо- на действительно является тем, за кого она себя выдает — иначе говоря, автори- зация бессмысленна без аутентификации. Служба обычно использует некое хранилище, устанавливающее соответствие между вызывающими сторонами и логическими ролями. При использовании авторизации каждая операция объ- являет или категорически требует, чтобы доступ к пей ограничивался опреде- ленными ролями. Служба должна найти роль вызывающей стороны в хранили- ще и убедиться в том, что вызывающая сторона принадлежит к запрашиваемой роли. В исходном виде WCF поддерживает два типа хранения авторизацион- ных удостоверений: служба может использовать группы (и учетные записи
Безопасность передачи 455 Windows) или же воспользоваться одним из провайдеров ASP.NET (например, провайдером SQL Server) для хранения пользовательских учетных записей и ролей. WCF также поддерживает использование репозиториев для хранения ролей, но я выяснил, что простейшим способом реализации пользовательского хранилища является реализация провайдера ASP.NET. Именно этот вариант будет подробно рассмотрен в настоящей главе. ПРИМЕЧАНИЕ---------------------------------------------------------------- WCF содержит хорошо разработанную, расширяемую инфраструктуру аутентификации и авториза- ции пользователей на основании заявок (claims), содержащихся в сообщении. К сожалению, обсуж- дение этого механизма выходит за рамки книги. Безопасность передачи Аутентификация и авторизация имеют дело с двумя локальными аспектами безопасности — как и до какого уровня должен предоставляться доступ вызы- вающей стороне при получении сообщения службой. В этом отношении служ- бы WCF не слишком отличаются от традиционных классов «клиент/сервер». Непременным условием как для аутентификации, так и для авторизации явля- ется безопасная доставка самого сообщения. Передача сообщения от клиента к службе должна быть безопасной; без нее как аутентификация, так и авториза- ция оказываются под сомнением. Безопасность передачи имеет три важных ас- пекта, и для обеспечения безопасности службы должны соблюдаться все три. Целостность сообщения определяет механизм проверки того, что само сообще- ние не подвергалось изменениям на пути от клиента к службе. На практике зло- умышленник может перехватить сообщение и изменить его содержимое (ска- жем, подменить номера счетов при операции перевода средств банковской службой). Конфиденциальность сообщения обеспечивает защиту содержимого от посторонних, так что никакая третья сторона не может даже прочитать его. Конфиденциальность дополняет целостность — даже если злоумышленник не изменяет сообщения, он по-прежнему может извлечь из сообщения конфиден- циальную информацию (те же номера счетов). Наконец, безопасность передачи должна обеспечивать взаимную аутентификацию, то есть клиент должен быть уверен, что содержимое сообщения будет прочитано только соответствующей службой (иначе говоря, что клиент подключается к правильной службе). Полу- чив удостоверения в сообщении, служба должна провести их локальную аутен- тификацию. Взаимная аутентификация также должна выявлять и устранять атаки замещения и отказа в обслуживании (DoS, Denial of Service). В первом случае злоумышленник повторяет допустимое сообщение, обращенное к служ- бе, а во втором выдает фиктивные недействительные сообщения, но с такой частотой, чтобы снизить доступность службы для клиентов. Режимы безопасности передачи WCF поддерживает пять разных способов обеспечения трех аспектов безопас- ности передачи. Вероятно, выбор правильного режима безопасности передачи является самым главным решением, принимаемым в контексте защиты служ- бы. Всего существует пять режимов безопасности передачи.
456 Глава 10. Безопасность Режим безопасности None Как следует из самого названия, в режиме None безопасность передачи полно- стью отключена. Служба не получает удостоверения клиента, а само сообщение открыто для вмешательства со стороны любого злоумышленника. Разумеется, устанавливать режим безопасности None категорически не рекомендуется. Режим безопасности Transport При настройке в режиме Transport WCF использует безопасный транспорт. Воз- можные варианты безопасных транспортных протоколов — HTTPS, TCP, IPC и MSMQ. В режиме транспортной безопасности шифруются все передачи дан- ных в канале; таким образом обеспечивается целостность, конфиденциальность и взаимная аутентификация. Без знания ключа шифрования любые попытки модификации сообщения приведут к его порче, и сообщение станет бесполез- ным; так обеспечивается его целостность. Ни одна сторона, кроме получателя, не сможет прочитать содержимое сообщения; так обеспечивается конфиденци- альность. Наконец, взаимная аутентификация частично обеспечивается шиф- рованием удостоверений клиента вместе с остальным содержимым сообщения, так что только предполагаемый получатель сможет прочитать его; следователь- но, клиенту не нужно беспокоиться о возможном перенаправлении сообщения на конечные точки злоумышленника, потому что последний все равно не смо- жет сообщением воспользоваться. После того как сообщение будет расшифро- вано, служба читает удостоверения и проводит аутентификацию клиента. Транспортная безопасность требует согласования деталей шифрования меж- ду клиентом и службой, но это делается автоматически в составе коммуникаци- онного протокола соответствующей привязки. Положительный вклад в безопас- ность передачи вносит аппаратное ускорение, выполняемое сетевой картой, - благодаря ему процессор хоста избавляется от необходимости шифрования/де- шифрования сообщений. Разумеется, аппаратное ускорение способствует по- вышению производительности и может даже замаскировать дополнительные затраты ресурсов, связанные с обеспечением безопасности. Транспортная без- опасность является простейшим способом достижения безопасности сообще- ний и к тому же самым производительным. Ее главный недостаток заключается в том, что она гарантирует безопасность только в режиме «точка-точка», то есть при прямом подключении клиента к службе. При наличии промежуточных ин- станций между клиентом и службой выбор транспортной безопасности стано- вится сомнительным, так как посредники могут оказаться небезопасными. Соот- ветственно, транспортная безопасность часто применяется только в интрасете- вых приложениях, где клиент отделен от службы всего одним переходом в кон- тролируемой среде. При настройке любых привязок HTTP в режиме транспортной безопасно- сти WCF на стадии загрузки службы проверяет, что соответствующий адрес ко- нечной точки использует HTTPS вместо простого протокола HTTP. Режим безопасности Message Режим безопасности Message ограничивается простым шифрованием самого со- общения. Шифрование обеспечивает целостность, конфиденциальность и делает
Безопасность передачи 457 возможной взаимную аутентификацию по тем же причинам, по которым они обеспечиваются при шифровании коммуникационного канала в режиме транс- портной безопасности. Однако шифрование сообщений вместо транспорта дает службе возможность безопасной передачи по небезопасным транспортным про- токолам, таким как HTTP. Тем самым обеспечивается межконцевая (end-to- end) безопасность независимо от количества промежуточных инстанций, участ- вующих в передаче сообщения, а также защищенности самого транспорта. Вдо- бавок безопасность сообщений базируется на отраслевых стандартах, спроекти- рованных для обеспечения совместимости и борьбы с основными видами атак (замещение, DOS), а ее обширная поддержка в WCF достаточно обширна сама по себе и к тому же предусматривает возможность расширения. Недостаток безопасности сообщений — возможная задержка вызовов, обусловленная до- полнительными затратами ресурсов. Безопасность сообщений обычно исполь- зуется в интернет-приложениях, где схемы вызовов более компактны, а безо- пасность транспорта не гарантирована. Режим безопасности Mixed В смешанном режиме безопасности Mixed используется транспортная безопас- ность для обеспечения целостности и конфиденциальности сообщений, а также аутентификации служб, а для защиты удостоверений клиента используется безопасность сообщений. Режим Mixed пытается сочетать преимущества обоих видов безопасности: безопасный транспорт и даже аппаратное ускорение, обес- печиваемые транспортной безопасностью, способствуют повышению произво- дительности, а от безопасности сообщений берется возможность расширения и более широкий выбор типов клиентских удостоверений. К недостаткам режи- ма Mixed относится то, что он безопасен только на уровне «точка-точка» как следствие использования транспортной безопасности. Разработчикам приложе- ний редко приходится прибегать к режиму Mixed, но он доступен для нетриви- альных ситуаций. Режим безопасности Both Как подсказывает название, в режиме безопасности Both задействованы оба вида безопасности, Transport и Mixed. Сообщения защищаются с использованием безопасности Message, после чего передаются службе по безопасному транс- портному протоколу. Режим Both обеспечивает максимальную безопасность, но во многих случаях он выходит за пределы необходимого — возможно, исключе- ние составляют отключенные приложения, в которых лишняя задержка прой- дет незамеченной. Настройка режима безопасности передачи Режим безопасности передачи настраивается в привязке. Как клиент, так и служба должны использовать одинаковый режим безопасности передачи, и конечно, удовлетворять его требованиям. Как и при любой другой настройке привязки, режим безопасности может настраиваться как на программном, так и административном уровне в конфигурационном файле. Все привязки (за ис- ключением привязок одноранговых сетей) поддерживают параметр конструи-
458 Глава 10. Безопасность рования, обозначающий режим безопасности передачи, и у всех привязок име- ется свойство Security со свойством Mode, значение которого определяет режим безопасности. Как показано в табл. 10.1, не все режимы безопасности поддер- живаются всеми привязками, а поддерживаемые режимы определяются сцена- рием использования привязки. Таблица 10.1. Привязки и режимы безопасности передачи Название None Transport Message Mixed Both BasicHttpBinding Да (no умолчанию) Да Да Да Нет NetTcpBinding Да Да (по умолчанию) Да Да Нет NetPeerTcpBinding Да Да (по умолчанию) Да Да Нет NetNamedPipeBinding Да Да (по умолчанию) Нет Нет Нет WSHttpBinding Да Да Да (по умолчанию) Да Нет WSFederationHttpBinding Да Нет Да (по умолчанию) Да Нет WSDualHttpBinding Да Нет Да (по умолчанию) Нет Нет NetMsmqBinding Да Да (по умолчанию) Да Нет Да Все интрасетевые привязки (NetTcpBinding, NetNamedPipeBinding и NetMsmq- Binding) по умолчанию используют транспортную безопасность. Это объясняет- ся тем, что интрасеть является относительно безопасной средой по сравнению с Интернетом, а транспортная безопасность обеспечивает наилучшую произво- дительность. Три транспортных протокола (TCP, IPC и MSMQ) безопасны по своей природе и не требуют специального программирования от разработчика службы или клиента. Тем не менее интрасетевые привязки также могут на- страиваться в режиме None; они используются с теми же транспортами, но без безопасности. Привязка NetNamedPipeBinding поддерживает только режимы None и Transport — использовать режим безопасности Message по IPC бессмысленно, потому что клиента отделяет от службы ровно один переход. Также обратите внимание, что режим Both поддерживается только привязкой NetMsmqBinding. Все интернет-привязки по умолчанию используют режим Message, чтобы они могли использоваться с небезопасными транспортами (а именно HTTP). Хотя WSHttpBinding может использоваться с транспортной безопасностью, с WSDualHttpBinding это невозможно. Дело в том, что эта привязка использует отдельный канал HTTP для подключения службы к клиенту обратного вызова, и этот канал не удается легко настроить на использование HTTPS — в отличие от службы, работающей под управлением «настоящего» веб-сервера. За одним заметным исключением, все привязки WCF настраиваются с той или иной разновидностью безопасности передачи, а следовательно, являются безопасными по умолчанию. Только привязка BasicHttpBinding по умолчанию не имеет безопасности. Это объясняется тем, что она спроектирована для пред- ставления служб WCF в виде старых служб ASMX, а службы ASMX по умол- чанию небезопасны. С учетом этого обстоятельства следует настроить Basic- HttpBinding на использование безопасного режима (например, Message).
Безопасность передачи 459 Примеры конфигурации привязок Привязка BasicHttpBinding использует перечисление BasicHttpSecurityMode для настройки режима передачи. Режим задается при помощи свойства Mode свой- ства Security привязки: public enum BasicHttpSecurityMode None. Transport. Message. TransportWithMessageCredential. Transportcredential Only ) public sealed class BasicHttpSecurity public BasicHttpSecurityMode Mode {get:set:} //... public class BasicHttpBinding : Binding.... public BasicHttpBinding(): public BasicHttpBinding(BasicHttpSecurityMode securityMode); public BasicHttpSecurity Security {get:} //... 1 Свойство Security относится к типу BasicHttpSecurity. Один из конструкторов BasicHttpBinding получает перечисление BasicHttpSecurityMode в параметре. Что- бы настроить для базовой привязки безопасность в режиме Message, следует либо сконструировать ее как безопасную, либо задать режим безопасности после кон- струирования. Соответственно, в листингах 10.1 и 10.2 bindingl и binding? экви- валентны. Листинг 10.1. Программная настройка безопасности для базовой привязки BasicHttpBinding bindingl = new BasicHttpBinding(BasicHttpSecurityMode.Message): BasicHttpBinding binding? = new BasicHttpBinding(): binding?.Security.Mode = BasicHttpSecurityMode.Message: Вместо настройки на программном уровне можно воспользоваться конфигу- рационным файлом, как показано в листинге 10.2. Листинг 10.2. Административная настройка безопасности для базовой привязки <bindings> <basicHttpBinding> <binding name = "SecuredBasic"> <security mode = "Message"> </security> </bi ndi ng> </basicHttpBinding> </bi ndi ngs>
460 Глава 10. Безопасность Другие привязки используют свои перечисления и специализированные классы безопасности, но настраиваются точно так же, как в листингах 10.1 и 10.2. Например, NetTcpBinding, NetPeerTcpBinding и WSHttpBinding используют перечисление SecurityMode, определяемое следующим образом: public enum SecurityMode { None. Transport. Message. T ransportWi thMessageCredenti al//Mi xed } He все привязки предоставляют параметр конструктора, но у всех привязок имеется свойство Security. Привязка NetNamedPipeBinding использует перечисле- ние NetNamedPipeSecurityMode, которое поддерживает только режимы None и Transport: public enum NetNamedPipeSecurityMode { None. Transport } Привязка WSDualHttpBinding использует перечисление WSDualHttpSecurityMode, которое поддерживает только режимы None и Message: public enum WSDualHttpSecurityMode { None. Message } Привязка NetMsmqBinding использует перечисление NetMsmqSecurityMode: public enum NetMsmqSecurityMode { None. Transport, Message. Both } NetMsmqBinding — единственная привязка, поддерживающая режим передачи Both. Причина, по которой почти каждая привязка обладает специальным пере- числением для определения режима безопасности, заключается в том, что про- ектировщики WCF отдали предпочтение повышению безопасности за счет по- вышения общей сложности. Они могли определить единое перечисление с элементами, соответствующими всем возможным режимам безопасности, но тогда на стадии компиляции появляется возможность присваивания недопус- тимых значений — например, режим Message будет назначен привязке Net- NamedPipeBinding, или режим Transport — привязке WSDualHttpBinding. Использо- вание специализированных перечислений повышает устойчивость программы к ошибкам, но в ней появляется больше компонентов, с которыми приходится иметь дело разработчику.
Безопасность передачи 461 Транспортная безопасность и удостоверения WCF предоставляет на выбор несколько разных типов клиентских веритель- ных данных. Клиент может идентифицировать себя при помощи классического имени пользователя и пароля или же воспользоваться электронным ключом безопасности Windows. Проверка подлинности удостоверений Windows может быть выполнена с использованием NTLM или Kerberos, когда эти механизмы доступны. Клиент может использовать сертификат Х509 или же полностью от- казаться от передачи удостоверений и сохранить анонимность. Когда безопас- ность передачи настраивается в режиме Transport, не все привязки поддержива- ют все типы клиентских удостоверений, как показано в табл. 10.2. Таблица 10.2. Привязки и клиентские удостоверения безопасности передачи Название Режим None Windows Имя пользователя Сертификат BasicHttpBinding Да (no умолчанию) Да Да Да NetTcpBinding Да Да (по умолчанию) Нет Да NetPeerTcpBinding Нет Нет Да (по умолчанию) Да NetNamedPipeBinding Нет Да (по умолчанию) Нет Нет WSHttpBinding Да Да (по умолчанию) Да Да WSFederationHttpBinding — — — — WSDualHttpBinding — — — — NetMsmqBinding — Да (по умолчанию) Нет Да Какая привязка поддерживает тот или иной тип удостоверений, в основном зависит от сценария, для которого привязка проектировалась. Например, все интрасетевые привязки по умолчанию используют удостоверения Windows, по- тому что они используются в среде Windows, а привязка BasicHttpBinding по умолчанию не использует удостоверения, как и классическая веб-служба ASMX. Привязки WSFederationHttpBinding и WSDualHttpBinding вообще не могут исполь- зовать транспортную безопасность. Безопасность сообщений и удостоверения Что касается безопасности сообщений, WCF разрешает приложениям исполь- зовать те же типы удостоверений, что и для транспортной безопасности, с до- бавленипем нового типа — электронного ключа (issued token). В режиме Message не все привязки поддерживают все типы клиентских удостоверений, как пока- зано в табл. 10.3. Таблица 10.3. Привязки и клиентские удостоверения безопасности сообщений Название Режим None Windows Имя пользователя Серти- фикат Электронный ключ BasicHttpBinding Нет Нет Нет Да Нет- NetTcpBinding Да Да (по умолчанию) Да Да Да NetPeerTcpBinding — — — — — продолжение &
462 Глава 10. Безопасность Таблица 10.3 (продолжение) Название Режим None Windows Имя пользователя Серти- фикат Электронный ключ NetNamedPipeBinding — — — — — WSHttpBinding Да Да (по умолчанию) Да Да Да WSFederationHttpBinding — — — — — WSDualHttpBinding Да Да (по умолчанию) Да Да Да NetMsmqBinding Да Да (по умолчанию) Да Да Да Вполне логично, что все интрасетевые привязки с поддержкой безопасности сообщений по умолчанию используют удостоверения Windows, однако интерес- но заметить, что интернет-привязки (WSHttpBinding и WSDualHttpBinding) тоже по умолчанию используют удостоверения Windows, хотя (как будет показано да- лее) интернет-приложения редко используют удостоверения Windows по HTTP. Это сделано для того, чтобы разработчики могли безопасно использовать эти привязки в исходном виде, не прибегая к пользовательским хранилищам удо- стоверений. ВНИМАНИЕ --------------------------------------------------------------------- Привязка BasicHttpBinding поддерживает удостоверения с именем пользователя для безопасности сообщений только в режиме Mixed. Это может привести к ошибкам проверки на стадии выполнения, потому что в перечисление BasicHttpMessageCredentialType входит значение BasicHttpMessage- CredentialType.UserName. Управление личностью Управлением личностью называется аспект безопасности, который определяет, какие идентификационные данные личности клиент посылает службе и что служба может с ними сделать. Более того, при проектировании службы необхо- димо заранее решить, под какой личностью она будет работать. Служба может работать со своей личностью; она может воспользоваться личностью клиента (если это возможно); или же использовать их комбинацию, то есть в одной опе- рации будут попеременно использоваться личность самой службы, клиента и даже какой-нибудь третьей стороны. Правильный выбор личности оказывает огромное влияние на масштабируемость приложения и затраты на администри- рование. В WCF безопасность личности (когда она включена) проходит вниз по цепочке вызовов, а служба может узнать, от кого поступил вызов, независи- мо от личности, используемой самой службой. Общая политика К традиционным, общепринятым аспектам безопасности (аутентификация, ав- торизация, безопасность передачи и управление личностью) я хотел бы доба- вить еще один аспект — не столь формальный и традиционный, но, на мой взгляд, не менее важный. Речь идет об отношении вашей организации и даже
Сценарный подход 463 вас лично к безопасности — иначе говоря, о политике безопасности. Я считаю, что в абсолютном большинстве случаев отказ от защиты приложений является непозволительной роскошью. И хотя безопасность связана с определенными затратами ресурсов и некоторыми потерями производительности, эти пробле- мы уходят на второй план. Попросту говоря, эти затраты жизненно необходи- мы. Они являются неотъемлемой частью проектирования и администрирова- ния современных подключенных приложений. Давно прошли те времена, когда разработчик мог себе позволить не беспокоиться о безопасности и создавать приложения, полагаясь на средства безопасности целевой среды — например, на физическую безопасность с картами доступа или брандмауэры. Поскольку многие разработчики не могут себе позволить изучать область безопасности на экспертном уровне, при выборе общей политики безопасности я рекомендую использовать простой подход: поднимайте уровень безопасности до тех пор, пока кто-нибудь не пожалуется. Если производительность приложе- ния на максимальном уровне безопасности остается приемлемой, так тому и быть. Если же производительность вас не устраивает, только тогда следует переходить к подробному анализу рисков и выяснять, чем можно пожертвовать в области безопасности ради производительности. По собственному опыту могу сказать, что вам редко придется идти по этому пути. Стратегии безопасности, описанные в этой главе, следуют моей общей политике безопасности. Общий подход к безопасности в WCF во многом соответствует моему собственному; я особо укажу на те моменты, в которых они расходятся, и предложу возмож- ные решения. Не считая BasicHttpBinding, среда WCF безопасна по умолчанию и даже привязку BasicHttpBinding можно легко защитить. Все остальные привяз- ки WCF по умолчанию аутентифицируют все вызывающие стороны служб и используют безопасность передачи. Сценарный подход Безопасность является самой нетривиальной и сложной областью WCF. Напри- мер, в следующем списке перечислены элементы, от которых зависит безопас- ность каждого вызова операции WCF: О контракт службы; О контракт операции; О контракт сбоев; О поведение службы; О поведение операции; О конфигурация хоста; О конфигурация и код метода; О поведение на стороне клиента; О конфигурация посредника; О конфигурация привязки.
464 Глава 10. Безопасность Каждый из элементов этого списка может содержать десяток, а то и более свойств, относящихся к безопасности. Количество возможных комбинаций и перестановок огромно. Кроме того, не все комбинации допустимы или под- держиваются, и не все разрешенные комбинации имеют смысл или последова- тельны. Например, бессмысленно (хотя и технически возможно) использовать сертификаты в качестве клиентских удостоверений в однородных интрасетях Windows — настолько же, насколько бессмысленно использовать учетные запи- си в интернет-приложениях. В этой книге я решил сосредоточиться на несколь- ких ключевых сценариях (и их разновидностях), покрывающих потребности в безопасности большинства современных приложений: О интрасетевые приложения; О интернет-приложения; О приложения «бизнес-бизнес»; О анонимные приложения; О отсутствие безопасности. Я покажу, как обеспечить безопасность в каждом из перечисленных сцена- риев и как организовать поддержку основных ее аспектов: безопасности переда- чи, аутентификации, авторизации и управления личностью. Если вам потребу- ется другой сценарий, воспользуйтесь моей аналитической схемой для определения необходимых аспектов и настроек. Интрасетевые приложения Для интрасетевых приложений характерно использование WCF как клиентом, так и службой, а клиент со службой развертываются в одной интрасети. Клиент располагается за брандмауэром, а для обеспечения безопасности передачи, аутен- тификации и авторизации могут использоваться средства безопасности Win- dows. Для хранения клиентских удостоверений можно воспользоваться учет- ными записями и группами Windows. Под интрасетевой сценарий подходит широкий спектр бизнес-приложений, от финансовых и производственных до IT-приложений, предназначенных для внутреннего пользования. Безопасность интрасетевых привязок В интрасетевом сценарии следует использовать интрасетевые привязки, а имен- но NetTcpBinding, NetNamedPipeBinding и NetMsmqBinding. Безопасность передачи обеспечивается режимом Transport, потому что все вызовы заведомо относятся к разновидности «точка-точка». Именно режим Transport используется по умол- чанию для интрасетевых привязок (см. табл. 10.1). Для клиентских удостовере- ний выбирается тип Windows, который тоже используется по умолчанию (см. табл. 10.2). Параметр должен задаваться как на стороне клиента, так и на сторо- не службы.
Интрасетевые приложения 465 Защита на уровне транспортной безопасности Три интрасетевые привязки должны быть настроены на безопасность транс- портного уровня. Каждая из трех интрасетевых привязок обладает настраивае- мым уровнем защиты, от которого зависит транспортная безопасность. Три уровня защиты: О None (нет) — WCF не защищает сообщение при передаче от клиента к служ- бе. Любой злоумышленник может прочитать содержимое сообщения и даже изменить его. О Sign (цифровая подпись) — при настройке защиты этого уровня WCF прове- ряет, чтобы сообщение поступало только от аутентифицированного отпра- вителя, для чего к сообщению присоединяется зашифрованная контрольная сумма. Получив сообщение, служба вычисляет контрольную сумму и срав- нивает ее с оригиналом. Если суммы не совпадают, сообщение отклоняется. В результате сообщения защищаются от изменения, но их содержимое по- прежнему остается видимым в процессе пересылки. О EncryptAndSign (шифрование и цифровая подпись) — на этом уровне защиты WCF снабжает сообщение цифровой подписью и шифрует его содержимое. Данный уровень обеспечивает целостность, конфиденциальность и аутен- тичность. Уровень Sign представляет собой компромисс между безопасностью и произ- водительностью. Тем не менее я считаю этот компромисс нежелательным, по- этому всегда выбираю режим EncryptAndSign. В WCF уровень защиты представ- лен перечислением Protection Level, определяемым следующим образом: public enum Protect!onLevel { None. Sign. EncryptAndSign He все интрасетевые привязки по умолчанию используют одинаковый уро- вень защиты. Для NetTcpBinding и NetNamedPipeBinding по умолчанию использу- ется шифрование с цифровой подписью, а для NetMsmqBinding — только цифро- вая подпись. Конфигурация NetTcpBinding Конструктор NetTcpBinding получает параметр, определяющий желательный ре- жим безопасности передачи: public class NetTcpBinding : ... public NetTcpBinding(SecurityMode SecurityMode): public NetTcpSecurity Security {get:} //... Свойство Security типа NetTcpSecurity содержит режим передачи (Transport или Message) и два свойства для разных режимов:
466 Глава 10. Безопасность public sealed class NetTcpSecurity { public SecurityMode Mode {get:set;} public MessageSecurityOverTcp Message {get:} public TcpTransportSecurity Transport {get:} } В интрасетевом сценарии безопасности следует выбрать для безопасности передачи режим Transport и задать значение свойства Transport типа TcpTrans- portSecurity: public sealed class TcpTransportSecurity { public TcpClientCredentialType ClientCredentialType {get:set;} public ProtectionLevel Protect!onLevel {get;set;} } Свойство Transport инициализируется типом клиентских удостоверений Win- dows с использованием перечисления TcpClientCredentialType, определение кото- рого выглядит так: public enum TcpClientCredentialType { None, Windows, Certificate } Уровень защиты свойства Transport задается равным Protection Level. Encrypt- AndSign. Поскольку оба параметра используются по умолчанию для привязки, следующие два объявления эквивалентны: NetTcpBinding bindingl = new NetTcpBinding(); NetTcpBinding binding2 == new NetTcpBinding(SecurityMode.Transport); bindi ng2.Security.T ransport.Cli entCredenti al Type = TcpCli entCredenti a1 Type.Wi ndows; binding2.Security.Transport.ProtectionLevel » ProtectionLevel.EncryptAndSign; To же самое можно сделать и в конфигурационном файле: <bindings> <netTcpBi ndi ng> <binding name = "TCPWindowsSecurity"> security mode = "Transports transport clientCredentialType » "Windows" protectionLevel » "EncryptAndSign" /> </security> </bi ndi ng> </netTcpBinding> </bindings>
Для полноты картины я покажу, как настроить NetTcpBinding для безопасно- сти сообщений с использованием в качестве клиентских удостоверений имени пользователя (хотя это и не требуется в интрасетевом сценарии): public enum MessageCredentialType None. Windows. UserName. Certificate. IssuedToken public sealed class MessageSecurityOverTcp ( public Messagecredential Type ClientCredentialType (get; set;} / /... NetTcpBinding binding = new NetTcpBinding(SecurityMode.Message): binding. Security.Message.Cl 1entCredenti al Type = MessageCredent1 al Type.UserName; NetTcpSecurity предоставляет свойство Message типа MessageSecurityOverTcp. При задании типа удостоверений используется перечисление MessageCredential- Туре. Большинство привязок использует перечисление MessageCredentialType для представления типа клиентских удостоверений безопасности сообщений. На рис. 10.1 изображена структура элементов NetTcpBinding, относящихся к области безопасности. Рис. 10.1. NetTcpBinding и безопасность NetTcpBinding содержит ссылку на объект NetTcpSecurity, в котором перечис- ление SecurityMode используется для задания режима безопасности передачи. При использовании транспортной безопасности NetTcpSecurity использует эк- земпляр TcpTransportSecurity с типом клиентских удостоверений, заданным из
468 Глава 10. Безопасность перечисления TcpClientCredentialType, а также уровнем защиты, заданным из пе- речисления ProtectionLevel. При использовании безопасности сообщений NetTcp- Security использует экземпляр MessageSecurityOverTcp с типом клиентских удо- стоверений, заданным из перечисления MessageCredentialType. Конфигурация NetNamedPipeBinding Конструктор NetNamedPipeBinding получает параметр, определяющий желатель- ный режим безопасности передачи: public class NetNamedPipeBinding : Binding,... { publi c NetNamedPi peBi ndi ng(NetNamedPi peSecurityMode securityMode): public NetNamedPipeSecurity Security {get;} //... } Свойство Security типа NetNamedPipeSecurity содержит режим передачи (Transport или None) и одно свойство с настройками транспортной безопасно- сти: public sealed class NetNamedPipeSecurity { public NetNamedPipeSecurityMode Mode {get;set:} public NamedPipeTransportSecurity Transport {get:} } В интрасетевом сценарии безопасности следует выбрать для безопасности передачи режим Transport и задать значение свойства Transport типа NamedPipe- TransportSecurity: public sealed class NamedPipeTransportSecurity { public ProtectionLevel ProtectionLevel {get;set;} } Уровень защиты свойства Transport задается равным Protection Level. Encrypt- AndSign. Поскольку это значение используется для привязки по умолчанию, следующие два объявления эквивалентны: NetNamedPipeBinding bindingl = new NetNamedPipeBinding(); NetNamedPipeBinding binding2 = new NetNamedPipeBinding( NetNamedPi peSecuri tyMode.T ransport); bi ndi ng2.Securi ty.T ransport.ProtectionLevel = ProtectionLevel.EncryptAndSi gn: To же самое можно сделать и в конфигурационном файле: <bindings> <netNamedPi peBi ndi ng> <binding name = "IPCWindowsSecurity"> <security mode = "Transports transport protectionLevel = "EncryptAndSign’7> </security> </binding>
Интрасетевые приложения 469 </netNamedP 1 реВ 1 ndi ng> </bindi ngs> Задавать тип клиентских удостоверений не обязательно (да и невозможно), потому что поддерживаются только удостоверения Windows (см. табл. 10.2). На рис. 10.2 изображена структура элементов NetNamedPipeBinding, относящихся к области безопасности. Рис. 10.2. NetNamedPipeBinding и безопасность NetNamedPipeBinding содержит ссылку на объект NetNamedPipeSecurity, в кото- ром перечисление NetNamedPipeSecurityMode используется для задания режима безопасности передачи. При использовании транспортной безопасности NetNa- medPipeSecurity использует экземпляр NamedPipeTransportSecurity с уровнем за- щиты, заданным из перечисления ProtectionLevel Конфигурация NetMsmqBinding Привязка NetMsmqBinding поддерживает параметр конструктора, определяющий желательный режим безопасности передачи, и свойство Security: public class NetMsmqBinding : MsmqBindingBase.... public NetMsmqBinding(NetMsmqSecurityMode securityMode); public NetMsmqSecurity Security {get:} //... Свойство Security типа NetMsmqSecurity содержит режим передачи (Transport или Message) и два свойства для разных режимов: public sealed class NetMsmqSecurity ! public NetMsmqSecurityMode Mode {get:set:} public MsmqTransportSecurity Transport
470 Глава 10. Безопасность {get;} public MessageSecurityOverMsmq Message {get;} } В интрасетевом сценарии безопасности следует выбрать для безопасности передачи режим Transport и задать значение свойства Transport типа MsmqTrans- portSecurity: public sealed class MsmqTransportSecurlty { public MsmqAuthentlcatlonMode MsmqAuthentlcatlonMode {get;set;} public ProtectlonLevel MsmqProtectlonLevel {get;set;} //... } Свойство Transport инициализируется типом клиентских удостоверений Win- dowsDomain из перечисления MsmqAuthentificatonMode: public enum MsmqAuthentlcatlonMode { None. WindowsDomain. Certificate Тип удостоверений WindowsDomain используется по умолчанию. Также необ- ходимо задать уровень защиты свойства Transport равным ProtectionLevel.Encrypt- AndSign. Следующие два объявления эквивалентны: NetMsmqBinding Ыndingl - new NetMsmqBinding(); blndingl.Security.Transport.MsmqProtectlonLevel = ProtectlonLevel.EncryptAndSign; NetMsmqBinding binding? = new NetMsmqBinding(); binding?.Security.Mode = NetMsmqSecurityMode.Transport; bi ndi ng?.Securlty.T ransport.MsmqAuthent1cati onMode = MsmqAuthentlcatlonMode.WindowsDomain; binding?.Security.Transport.MsmqProtectlonLevel = ProtectlonLevel.EncryptAndSign; To же самое можно сделать и в конфигурационном файле: <bindings> <netMsmqBinding> <binding name = "MSMQW1ndowsSecurity"> <security mode = "Transports transport msmqAuthenticationMode = "WindowsDomain" msmqProtectlonLevel = "EncryptAndSign” /> </security> </binding> </netMsmqBinding> </bindings> На рис. 10.3 изображена структура элементов NeMsmqBinding, относящихся к области безопасности.
Интрасетевые приложения 471 Рис. 10.3. NetMsmqBinding и безопасность NetMsmqBinding содержит ссылку на объект NetMsmqSecurity, в котором пере- числение NetMsmqSecurityMode используется для задания режима безопасности передачи. При использовании транспортной безопасности NetMsmqSecurity ис- пользует экземпляр MsmqTransportSecurity с уровнем защиты, заданным из пере- числения ProtectionLevel, и типом клиентских удостоверений из перечисления MsmqAuthenticationMode. Безопасность сообщений Хотя служба должна использовать максимально высокий уровень безопасно- сти, в действительности она зависит от хоста, потому что конфигурация при- вязки определяется именно хостом. Это создает особенно много проблем, если служба будет развертываться в произвольной среде с неизвестным заранее хос- том. WCF позволяет разработчикам служб затребовать обязательный уровень защиты, или вернее, ограничить минимальный уровень защиты, на котором со- гласится работать их служба. Как служба, так и клиент устанавливают ограни- чения уровня защиты независимо друг от друга. Ограничения могут устанавли- ваться в трех местах. В случае применения на уровне контракта службы все операции контракта считаются конфиденциальными и защищенными. В случае применения на уровне контракта операции защищается только эта операция, но не другие операции того же контракта. Наконец, также можно установить огра- ничения на уровне контракта сбоев, потому что иногда возвращаемая клиенту информация об ошибке содержит конфиденциальные сведения: значения пара- метров, сообщения об исключениях, стек вызовов. Соответствующие атрибуты контракта обладают свойством ProtectionLevel перечисляемого типа Protection- Level: [AttrlbuteUsage(AttrlbuteTargets.Interface|AttrlbuteTa rgets.Cl ass. Inherited = false)] public sealed class ServiceContractAttribute : Attribute public ProtectionLevel ProtectionLevel {get:set;} //...
472 Глава 10. Безопасность [AttributeUsage(AttributeTargets.Method)] public sealed class OperatlonContractAttrlbute : Attribute { public ProtectionLevel ProtectlonLevel {get:set;} //... } [Attri buteUsage(AttributeTargets.Method,AllowMultlple = true, Inherited = false)] public sealed class FaultContractAttrlbute : Attribute { public ProtectlonLevel ProtectlonLevel {get;set;} //... } А вот как уровень защиты задается в контракте службы: [ServiceContract(ProtectionLevel = ProtectlonLevel.EncryptAndSIgn)] Interface IMyContract {...} Задание свойства ProtectlonLevel для атрибутов контракта всего лишь уста- навливает «нижнюю планку», то есть минимально допустимый уровень защиты для контракта. Если привязка настроена на более низкий уровень защиты, то во время загрузки службы или при открытии посредника происходит исключение InvalidOperationException. Если привязка настроена на более высокий уровень за- щиты, это допустимо по контракту. По умолчанию свойство ProtectlonLevel ат- рибутов контракта равно Protection Level. None, то есть фактически ограничение не действует. Желательное ограничение относится к числу локальных деталей реализации службы, поэтому оно не экспортируется с метаданными службы. Соответствен- но, клиент может затребовать другой уровень защиты и следить за его соблюде- нием отдельно от службы. ПРИМЕЧАНИЕ ------------------------------------------------------------------------------ Хотя не-интрасетевые привязки не обладают свойством уровня защиты, ограничения уровня защи- ты для контрактов служб, операций и сбоев удовлетворяются при использовании безопасности Transport или Message. При отключении безопасности (в сценарии с отсутствием безопасности) ог- раничение не выполняется. Аутентификация Когда клиент вызывает посредника для конечной точки с привязкой, настроен- ной на использование удостоверений Windows с транспортной безопасностью, по умолчанию клиенту не нужно выполнять никакие явные действия для пере- дачи удостоверений. WCF автоматически передает службе идентификацион- ные данные личности Windows клиентского процесса: class MyContractCllent : Cl1entBase<IMyContract>,IMyContract {...} MyContractCllent proxy = new MyContractClientO; proxy.MyMethod(); //Здесь передается личность клиента proxy.Close();
Интрасетевые приложения 473 Если при получении вызова службой клиентские удостоверения представ- ляют действительную учетную запись Windows, WCF на стороне службы аутентифицирует вызывающую сторону и ей разрешается обратиться к опера- ции службы. Передача альтернативных удостоверений Windows Вместо личности процесса, в котором работает клиент, последний может пере- дать альтернативные удостоверения Windows. Базовый класс ClientBase<T> пре- доставляет свойство Clientcredentials типа Clientcredentials: public abstract class ClientBase<T> : ... I public Clientcredentials Clientcredentials (get;} ) public class Clientcredentials : ....lEndpointBehavior ( public WindowsClientCredential Windows (get;} //... ) Класс Clientcredentials содержит свойство Windows типа WindowsClientCredential, определяемое следующим образом: public sealed class WindowsClientCredential ( public Networkcredential Clientcredential (get; set;} //... WindowsClientCredential содержит свойство ClientCredential типа NetworkCredential; именно здесь клиент задает альтернативные удостоверения: public class NetworkCredential : ... ( public NetworkCredentialО; public NetworkCredential(string userName,string password); public NetworkCredential(string userName.string password.string domain); public string Domain (get:set;} public string UserName (get: set;} public string Password (get:set;} ) В листинге 10.3 показано, как использовать эти классы и свойства для пере- дачи альтернативных удостоверений Windows. Листинг 10.3. Передача альтернативных удостоверений Windows NetworkCredential credentials - new NetworkCredentialО; credentials. Dorna in = "My Dorna in"; credent! a Is. UserName = "MyUsername"; продолжение#
474 Глава 10. Безопасность credentials.Password = "MyPassword": MyContractClient proxy = new MyContractClient(); proxy.Clientcredentials.Windows.Clientcredential я credentials; proxy.MyMethod(): proxy.Close(); Обратите внимание: клиент должен создать новый объект типа NetworkCre- dential, он не может просто задать удостоверения существующему свойству ClientCredential типа WindowsClientCredential. После использования назначенной личности посредник не может позднее переключиться на другую личность. Клиенты используют схему из листинга 10.3 при динамическом получении удо- стоверений на стадии выполнения (например, с использованием диалогового окна входа в систему). С другой стороны, если альтернативные удостоверения являются статическими, разработчик может инкапсулировать их в конструкто- рах посредника: public partial class MyContractClient: ClientBase<IMyContract>.IMyContract { public MyContractClient() { SetCredentials(): } /* Другие конструкторы */ void SetCredentials() ( Cl ientCredentials.Windows.ClientCredential = new NetworkCredential("MyClient"."MyPassword"."MyDomain"); } public void MyMethodO { Channel.MyMethod!); } } Базовый класс ChannelFactory предоставляет свойство Credentials типа Client- Credentials: public abstract class ChannelFactory : { public Clientcredentials Credentials {get:} //... } public class ChannelFactory<T> : ChannelFactory.... { public T CreateChannel0; //... } Просто задайте альтернативные удостоверения в свойстве Credentials, как по- казано в листинге 10.3: ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(""):
Интрасетевые приложения 475 factory.Credentials.Windows.Clientcredential = new Networkcredential (...); IMyContract proxy = factory.CreateChannel0; using(proxy as IDisposable) proxy.MyMethod(): ) Учтите, что вы не можете использовать статические методы CreateChannel класса ChanneLFactory<T>, потому что для обращения к свойству Credentials необ- ходимо предварительно создать экземпляр. 1ичность О Каждому процессу Windows ставятся в соответствие аутентифицированные идентификационные данные, то есть личность; процесс, обеспечивающий хостинг службы WCF, не является исключением. Личность Windows факти- чески представляет собой учетную запись Windows, электронный ключ ко- торой присоединяется к процессу и по умолчанию — ко всем программным потокам этого процесса. Тем не менее администратор приложения сам выбира- ет личность для использования. Хост может работать с интерактивной лич- ностью пользователя, то есть с личностью пользователя, запустившего хос- товой процесс. При простом запуске процесса пользователем учетная запись этого пользователя будет использоваться в качестве личности процесса. Ин- терактивная личность обычно используется при автохостинге и идеально подходит для отладки, потому что отладчик автоматически присоединяется к хостовому процессу при запуске из Visual Studio. Тем не менее было бы непрактично полагаться на интерактивную личность при развертывании службы на сервере, где вход пользователя в систему не обязателен (и даже после входа пользователь может не обладать необходимыми удостоверения- ми для работы службы). При развертывании, рассчитанном на реальную эксплуатацию, обычно используется выделенная учетная запись — заранее настроенная учетная запись Windows, используемая в основном службой (или службами). Для запуска службы под выделенной учетной записью ис- пользуются функции запуска от имени указанного пользователя. Впрочем, эта возможность полезна лишь при простом тестировании. Также можно ис- пользовать службу NT в качестве хоста и воспользоваться приложением па- нели управления Службы для назначения хосту выделенной учетной записи, от имени которой он должен запускаться. Если хостинг осуществляется в IIS6 или WAS, для назначения выделенной учетной записи можно вос- пользоваться конфигурационными возможностями этих сред. 1нтерфейс Ildentity } .NET личность представляется интерфейсом Ildentity из пространства имен ystem.Security.Principal: ublic interface Ildentity string AuthenticationType
476 Глава 10. Безопасность {get:} bool IsAuthentlcated {get;} string Name {get;} } Интерфейс позволяет узнать, прошла ли аутентификацию личность (и ка- кой механизм аутентификации при этом использовался), а также получить ее имя. В исходном варианте WCF использует три реализации Ildentity, пре- доставляемые .NET. Класс Windowsldentity представляет учетную запись Windows. Класс Genericidentity является классом общего назначения, предназначенным в основном для инкапсуляции имени личности. Если в классах Genericidentity и Windowsldentity название личности представляет собой пустую строку, личность считается неаутентифицированной, тогда как любая ненулевая длина считается признаком аутентификации. Наконец X509Identity — внутренний класс, пред- ставляющий личность, аутентифицированную с сертификатом Х509. Личности, представляемые X509Identity, всегда аутентифицированы. Работа с Windowsldentity Помимо простой реализации Ildentity, класс Windowsldentity предоставляет не- сколько полезных методов: public class Windowsldentity : Ildentity.... { public W1ndowsldent1ty(str1ng sUserPrlndpalName): public static Windowsldentity GetAnonymousO; public static Windowsldentity GetCurrent(); public virtual bool IsAnonymous {get:} public virtual bool IsAuthentlcated {get:} public virtual string Name {get;} //... } Логическое свойство IsAnonymous указывает, является ли представленная личность анонимной, а метод GetAnonymous() возвращает анонимную личность Windows (обычно используемую для маскировки реальных данных): Windowsldentity Identity = Windows Identity.GetAnonymousO; Debug.Assert(Identity.Name == ""); Debug.Assert(Identity.IsAuthentlcated == false); Debug.Assert(Identity.IsAnonymous == true); Статический метод GetCurrent() возвращает личность процесса, в котором он был вызван. Эти данные по умолчанию являются не-анонимными и аутентифи- цированными: Windowsldentity currentidentity = Windowsldentity.GetCurrent(); Debug. Assert (current Identity. Name != "О; Debug.Assert(currentIdentity.IsAuthentlcated — true); Debug.Assert(currentldent1ty.IsAnonymous == false):
Интрасетевые приложения 477 Контекст безопасности вызова Каждая операция безопасной службы WCF обладает контекстом безопасности вызова. Для представления контекста используется класс ServiceSecurityContext, определение которого выглядит так: public class ServiceSecurityContext { public static ServiceSecurityContext Current {get:} public bool IsAnonymous {get:} public I Identity Primaryidentity {get;} public Windows Identity Windows Identity {get;} //... ) Контексты безопасности вызова в основном применяются в пользователь- ских механизмах безопасности, а также при анализе и аудите. Здесь они пред- ставлены в разделе интрасетевого сценария, но во всех остальных сценариях контексты безопасности вызова тоже находят свое применение. Контекст безопасности вызова определяется на уровне отдельных вызовов, а не службы в целом. Контекст безопасности вызова хранится в TLS, поэтому к контексту безопасности вызова может обратиться каждый метод каждого объ- екта в цепочке вызовов службы, включая конструктор службы. Для получения текущего контекста безопасности вызова достаточно обратиться к статическо- му свойству Current. Другой способ получения контекста безопасности вызо- ва — свойство ServiceSecurityContext класса Operationcontext: public sealed class Operationcontext : ... { public ServiceSecurityContext ServiceSecurityContext {get;} //... ) Независимо от выбранного способа будет получен один и тот же объект: ServiceSecurityContext context1 = ServiceSecurityContext.Current: ServiceSecurityContext context2 = Operationcontext.Current.ServiceSecurityContext: Debug.Assert(contextl — context2); ВНИМАНИЕ ------------------------------------------------------------------------------- Служба обладает контекстом безопасности вызова только при включенной безопасности. Если безопасность отключена, ServiceSecurityContext.Current возвращает null. Свойство Primaryidentity класса ServiceSecurityContext содержит личность бли- жайшего клиента по цепочке вызовов. Если клиент не аутентифицирован, Primaryidentity содержит ссылку на реализацию Ildentity с пустой личностью. При использовании аутентификации Windows свойству Primaryidentity задается экземпляр Windowsldentity.
478 Глава 10. Безопасность Свойство Windowsldentity имеет смысл только при использовании аутенти- фикации Windows и всегда относится к типу Windowsldentity. При предоставле- нии действительных удостоверений Windows свойство Windowsldentity содер- жит соответствующую клиентскую личность и совпадает со значением Primary- Identity. ПРИМЕЧАНИЕ------------------------------------------------------------------ Конструктор синглетной службы не имеет контекста безопасности вызова, поскольку он вызывается при запуске хоста, а не в результате клиентского вызова. Перевоплощение Некоторые ресурсы (файловая система, SQL Server, сокеты и даже объекты DCOM) предоставляют доступ к себе на основании электронного ключа вызы- вающей стороны. Как правило, хостовому процессу назначается личность с удостоверениями более высокого уровня, необходимыми для обращения к та- ким ресурсам. Однако клиенты по сравнению со службой обладают ограничен- ными удостоверениями. Традиционно разработчики использовали механизм перевоплощения (impersonation) для преодоления этого разрыва. Перевоплоще- ние позволяет службе принять личность клиента — главным образом для про- верки, разрешено ли клиенту выполнять ту работу, которую он требует у служ- бы. Перевоплощение создает ряд негативных последствий для приложений, которые будут описаны в конце раздела. Вместо перевоплощения для авториза- ции вызывающих сторон следует применять средства ролевой безопасности. Впрочем, многие разработчики продолжают проектировать такие системы с ис- пользованием перевоплощения, поэтому эта необходимость была учтена и в .NET, и в WCF. Ручное перевоплощение Чтобы принять личность вызывающего клиента, служба может вызвать метод Impersonate() класса Windowsldentity: public class Windowsldentity : Ildentity,... { public virtual Windows Impersonationcontext Impersonate(); //... } public class Windows Impersonationcontext : IDisposable { public void Dispose(): public void UndoO: } Метод Impersonate() возвращает экземпляр WindowsImpersonationContext с пре- дыдущей личностью службы. Чтобы вернуться к ней, служба вызывает метод Undo(). Для перевоплощения служба должна вызвать Impersonate() для лично- сти вызывающей стороны, хранимой в свойстве Windowsldentity контекста без- опасности вызова, как показано в листинге 10.4.
Интрасетевые приложения 479 Листинг 10.4. Ручное перевоплощение и возврат class MyService : IMyContract ( public void MyMethodO ( Windows Impersonationcontext impersonationcontext - ServiceSecurityContext.Current.Wi ndowsIdenti ty.ImpersonateC): try { /★ Работа с использованием личности клиента */ ) finally { impersonationcontext. UndoO; } В листинге 10.4 служба возвращается к своей исходной личности даже при возникновении исключений — для этого вызов Undo() размещается в секции finally. Реализация Dispose() класса WindowsImpersonationContext тоже осуществ- ляет возврат, что позволяет использовать класс в командах using: public void MyMethodO ( usi ng(Servi ceSecuri tyContext.Current.Wi ndowsIdenti ty.ImpersonateC)) ( /* Работа с использованием личности клиента */ } } Декларативное перевоплощение Ручная реализация перевоплощения не обязательна — вы можете приказать WCF автоматически принимать личность стороны, вызывающей метод. Атри- бут OperationBehavior содержит свойство Impersonation перечисляемого типа ImpersonationOption: public enum ImpersonationOption NotAl lowed. Al 1 owed. Requi red ) [AttributeUsage(AttributeTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute.lOperationBehavior public ImpersonationOption Impersonation {get:set:} //... По умолчанию используется значение ImpersonationOption.NotAllowed. Авто- матическое перевоплощение в этом режиме не используется, но вы можете реа- лизовать его вручную при помощи кода, наподобие приведенного в листин- ге 10.4.
480 Глава 10. Безопасность В режиме ImpersonationOption.Allowed WCF автоматически подставляет лич- ность вызывающей стороны при каждом использовании аутентификации Win- dows. На другие механизмы аутентификации действие ImpersonationOption.Allowed не распространяется. При автоматическом перевоплощении WCF возвращает- ся к предыдущей личности службы при возврате из метода. Режим ImpersonationOption.Required требует обязательного использования аутентификации Windows; при нарушении этого требования инициируется ис- ключение. Как подсказывает название, WCF всегда автоматически меняет лич- ность при каждом вызове операции (с последующим возвратом к прежней лич- ности): class MyService : IMyContract { [Operat1onBehavi or(Impersonation = ImpersonationOption.Requi red)] public void MyMethodO { /* Работа с использованием личности клиента */ } } Обратите внимание: декларативное перевоплощение не может использо- ваться с конструктором службы, потому что атрибут Operation Behavior не может применяться к конструктору. Конструкторы всегда используют только ручное перевоплощение. Если вы меняете личность в конструкторе, возврат к старой личности должен происходить там же, для предотвращения побочных эффек- тов в операциях службы и даже других служб того же хоста. Перевоплощение во всех операциях Если перевоплощение должно использоваться во всех операциях службы, класс ServiceHostBase содержит свойство Authorization типа ServiceAuthorizationBehavior: public abstract class ServiceHostBase : ... { public ServiceAuthorizationBehavior Authorization {get:} //... } public sealed class ServiceAuthorizationBehavior : IServiceBehavior { public bool ImpersonateCallerForAllOperations {get:set:} //... } Класс ServiceAuthorizationBehavior предоставляет логическое свойство Imper- sonateCallerForAUOperations, по умолчанию равное false. Несмотря на название, задание истинного значения этого свойства всего лишь проверяет, что служба не содержит операций с режимом ImpersonationOption.NotAllowed. Ограничение проверяется во время загрузки службы, а при его нарушении происходит ис- ключение InvalidOperationException. При использовании аутентификации Win- dows это приводит к автоматическому использованию перевоплощения во всех операциях, однако все операции должны быть помечены ImpersonationOption.
Интрасетевые приложения 481 Allowed или ImpersonationOption.Required. Действие ImpersonateCallerForAUOperations не распространяется на конструкторы. Свойство ImpersonateCallerForAUOperations может задаваться как на программ- ном уровне, так и в конфигурационных файлах. Программное задание свойства может происходить только перед открытием хоста: ServiceHost host = new ServiceHost(typeof(MyServiсе)): host.Author! zation.ImpersonateCallerForAllOperations = true; host.OpenO; При задании свойства в конфигурационном файле необходимо включить в объявление службы ссылку на соответствующую секцию поведения: <services> <service name = "MyService" behaviorConfiguration= "ImpersonateAl1"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "ImpersonateAl1"> <serviceAuthorization ImpersonateCallerForAllOperations = ”true"/> </behavior> </serviceBehaviors> </behaviors> Чтобы автоматизировать перевоплощение во всех операциях без необходи- мости применять атрибут OperationBehavior ко всем методам, я написал статиче- ский класс SecurityHelper с методами ImpersonateAll(): public static class SecurityHelper ( public static void ImpersonateAl1(ServiceHostBase host); public static void ImpersonateAl1(ServiceDescription description); //... } Вызовы ImpersonateAll() должны располагаться перед открытием хоста: // Перевоплощение во всех операциях class MyService ; IMyContract ( public void MyMethodO {...} } ServiceHost host = new ServiceHost(typeof(MyService)); Securi tyHelper.ImpersonateAl1(host); host.OpenO; Реализация ImpersonateAll() представлена в листинге 10.5. Листинг 10.5. Реализация SecurityHelper.ImpersonateAII() public static class SecurityHelper ( public static void ImpersonateAl1(ServiceHostBase host) { host.Authorization.ImpersonateCallerForAllOperations = true; ServiceDescription description = host.Description; продолжение &
482 Глава 10. Безопасность ImpersonateAl1(description); ) public static void ImpersonateAl!(ServiceDescription description) { foreach(ServiceEndpoint endpoint in description.Endpoints) { foreach(0perat1onDescr1pt1on operation in endpoint.Contract.Operations) { foreachdOperationBehavior behavior in operation.Behaviors) { if(behavior is OperationBehaviorAttribute) { OperationBehaviorAttribute attribute = behavior as OperationBehaviorAttribute; attribute.Impersonation = ImpersonatlonOptlon.Required; break; } } } } } //... } В листинге 10.5 метод ImpersonateAU() устанавливает свойство Impersonate- CallerForAUOperations предоставленного хоста равным true, после чего получает от хоста описание службы со всеми конечными точками. Описание передается перегруженной версии ImpersonateAll(), которая задает всем операциям режим ImpersonationOption.Required. Задача решается перебором коллекции конечных точек в описании. Для каждой конечной точки ImpersonateAll() обращается к коллекции операций контракта. У каждой операции может быть один или не- сколько аспектов поведения в форме lOperationBehavior (всегда существует по крайней мере один, предоставленный WCF в форме OperationBehaviorAttribute, даже если он не был предоставлен явно). Все аспекты поведения операции по- очередно проверяются в поисках OperationBehaviorAttribute, а когда поиск ока- жется успешным, свойству Impersonation задается значение ImpersonationOption. Required. Для дальнейшей инкапсуляции и упрощения кода SecurityHelper.ImpersonateAU() можно воспользоваться классом ServiceHost<T>: public class ServiceHost<T> : ServiceHost { public void ImpersonateAl10 { If(State == Communicationstate.Opened) { throw new InvalldOperatlonExceptlonC’Host Is already opened”); } SecurltyHelper.ImpersonateAl1(th1s); } //...
Интрасетевые приложения 483 Автоматическое перевоплощение с использованием ServiceHost<T> реализу- ется очень просто: Servi ceHost<MyService> host = new ServiceHost<MyService>(); host.ImpersonateAllO; host.OpenO; Ограничение перевоплощения Авторизация и аутентификация защищают службу от обращений со стороны посторонних клиентов, возможно — злоумышленников. Но как защитить кли- ента от злонамеренных служб? Один из способов злоупотребления со стороны служб является принятие службой личности и удостоверений клиента для при- чинения вреда. В некоторых случаях клиент предпочел бы ограничить получение его лично- сти службой. WCF позволяет клиенту задать степень, до которой служба может получать его личность и пользоваться ею. В действительности существует це- лый спектр вариантов перевоплощения, соответствующих разным уровням до- верия между клиентом и службой. Класс WindowsClientCredential предоставляет свойство AllowedlmpersonationLevel типа TokenlmpersonationLevel из пространства имен System.Security.Principal: public enum TokenlmpersonationLevel None. Anonymous. Identification. Impersonation. Delegation public sealed class WindowsClientCredential ( public TokenlmpersonationLevel AllowedlmpersonationLevel (get;set;} //... Клиент может ограничить разрешенный уровень перевоплощения как на программном, так и на административном уровне. Например, следующий код, выполняемый перед открытием посредника, устанавливает программное огра- ничение перевоплощения до уровня TokenlmpersonationLevel.Identification: MyContractCl ient proxy = new MyContractClient(); proxy.Cli entCredenti als.Wi ndows.Al 1 owedImpersonati onLevel = TokenlmpersonationLevel.Identification; proxy. My Met hod (); proxy.Close(); При использовании конфигурационного файла администратор определяет разрешенный уровень перевоплощения как пользовательское поведение конеч- ной точки и включает ссылку на него в соответствующую секцию endpoint: <client> <endpoint behaviorConfiguration = "ImpersonationBehavior" />
484 Глава 10. Безопасность </client> <behav1ors> <endpointBehaviors> <behavior name = ”ImpersonationBehavior”> <clientCredentials> <windows allowedlmpersonationLevel = "Identification"^ </clientCredentials> </behavior> </endpointBehaviors> </behaviors> Уровень TokenlmpersonationLeveLNone означает, что уровень перевоплощения не назначен. Если задан уровень воплощения TokenlmpersonationLeveLAnonymous, клиент не предоставляет удостоверения. В практическом смысле Tokenlmperso- nationLeveLNone и TokenlmpersonationLeveLAnonymous означают одно и то же - клиент не передает информацию о своей личности. Конечно, они являются наи- более безопасными с точки зрения клиента, но приносят наименьшую пользу с точки зрения приложения, потому что любая аутентификация и авторизация со стороны службы становится невозможной. Отказ от передачи удостоверений возможен только в том случае, если служба рассчитана на анонимный доступ или отсутствие безопасности, что не относится к интрасетевому сценарию. Если служба настроена на использование безопасности Windows, эти два значе- ния приведут к выдаче исключения ArgumentOutOfRangeException на стороне клиента. В режиме TokenlmpersonationLeveLIdentification службе разрешается иденти- фикация клиента, то есть получение сведений о личности вызывающего клиен- та. Однако при этом службе запрещается перевоплощение в клиента — все, что делает служба, должно делаться под ее собственной личностью. Попытка пере- воплощения приводит к выдаче исключения ArgumentOutOfRangeException на стороне службы. Значение TokenlmpersonationLeveLIdentification используется по умолчанию системой безопасности Windows и является рекомендуемым для интрасетевого сценария. Учтите, что если клиент со службой располагаются на одном компьютере, служба сможет перевоплощаться в клиента даже при ис- пользовании режима TokenlmpersonationLeveLIdentification. Режим TokenlmpersonationLeveLImpersonation разрешает службе как получать сведения о личности клиента, так и использовать их для перевоплощения. Пе- ревоплощение означает высокую степень доверия между клиентом и службой, потому что служба может выполнять любые действия, разрешенные клиенту, даже если хост службы настроен на использование менее привилегированной личности. Единственное отличие реального клиента от перевоплощенной служ- бы заключается в том, что в случае нахождения службы на разных компьютерах с клиентом, она не может обращаться к ресурсам и объектам на других компью- терах под видом клиента, потому что компьютер службы не располагает паро- лем клиента. Если же служба и клиент находятся на одном компьютере, перево- площенная служба может обратиться к другому компьютеру на расстоянии одного сетевого перехода (hop), потому что ее компьютер способен аутентифи- цировать перевоплощенную личность клиента.
Интрасетевые приложения 485 Наконец, уровень TokenlmpersonationLeveLDelegation предоставляет службе клиентский билет (ticket) Kerberos. Служба может свободно обращаться к лю- бым ресурсам на других компьютерах, доступным для клиента. Если служба также настроена на делегирование, то при вызове других нисходящих служб личность клиента будет передаваться далее по цепочке вызовов. Аутентифика- ция Kerberos, необходимая для делегирования, невозможна в установках для рабочих групп Windows. Учетные записи на сторонах клиента и сервера долж- ны быть правильно настроены в Active Directory для поддержки делегирова- ния, что объясняется высоким уровнем доверия (а следовательно, и риском для безопасности). Делегирование чрезвычайно опасно с точки зрения клиента, поскольку кли- ент не может управлять тем, кто и где будет использовать его личность. При за- дании уровня TokenlmpersonationLeveLImpersonation клиент идет на рассчитан- ный риск, потому что он знает, к каким службам он будет обращаться, и если эти службы размещаются на другом компьютере, личность клиента не может передаваться по сети. На мой взгляд, делегирование позволяет службе выда- вать себя за клиента, не ограничиваясь простым перевоплощением. Использование перевоплощения Проектируйте свои службы так, чтобы они не зависели от перевоплощения, 1 используйте на стороне клиента режим TokenlmpersonationLeveLIdentification. Согласно общим рекомендациям проектирования, по мере удаления от кли- ента роль его личности уменьшается. Если при проектировании системы ис- юльзуется многоуровневая архитектура, каждый уровень должен работать под воей личностью, аутентифицировать вызывающие стороны и неявно полагать- я на то, что вызывающий уровень аутентифицирует исходные вызывающие тороны для поддержания цепочки доверенных, аутентифицированных вызо- ов. Напротив, перевоплощение требует передачи личности далее и далее по епочке вызовов, вплоть до низкоуровневых ресурсов. Такая схема ухудшает асштабируемость, потому что многие ресурсы (например, подключения SQL erver) выделяются на уровне личностей. При использовании перевоплощения еобходимое количество ресурсов совпадает с количеством клиентов, и вы не можете организовать пул ресурсов (например, пул подключений). Перевопло- ение также усложняет администрирование ресурсов, потому что вам придется эедоставлять доступ к ресурсам исходным личностям клиентов, число кото- ях может быть достаточно большим. Служба, которая всегда работает под сво- i личностью, не создает таких проблем независимо от только, сколько разных мностей обращается к ней. Для управления доступом к ресурсам следует ис- >льзовать авторизацию (см. далее). Наконец, если в основу архитектуры будет ложено перевоплощение, это исключает применение других механизмов, кро- » аутентификации Windows. Если вы все же выберете перевоплощение, при- ‘няйте его продуманно, и только в качестве последней меры при отсутствии угих, более качественных архитектурных схем. ‘ИМЕЧАНИЕ--------------------------------------------------------------- зевоплощение не работает для служб с очередями.
486 Глава 10. Безопасность Авторизация Итак, аутентификация проверяет, что клиент действительно является тем, за кого он себя выдает. В большинстве приложений также возникает другая необ- ходимость: убедиться в том, что клиент (а вернее, представленная им личность) обладает правом выполнения операции. Программировать разрешения доступа для каждой отдельной личности было бы неэффективно — лучше предостав- лять разрешения по ролям, назначенным клиентами в прикладном домене. Роль представляет собой символическую категорию личностей, обладающих одина- ковыми привилегиями безопасности. Назначая роль ресурсу приложения, вы предоставляете доступ к ресурсу всем членам этой роли. Определение ролей клиентов в бизнес-области является частью анализа требований к приложениям и архитектуры наряду с факторингом служб и интерфейсов. Взаимодействие с ролями (вместо конкретных личностей) изолирует приложение от внешних изменений — добавления новых и перемещения существующих пользователей, повышения привилегий или исчезновения пользователей. .NET позволяет при- менять ролевую безопасность как на декларативном, так и на программном уровне, если потребуется проверить принадлежность к ролям с принятием ди- намических решений. Субъекты безопасности В целях безопасности удобно объединить личность с информацией о ролевой принадлежности. Такое представление называется субъектом безопасности (se- curity principal). Субъектом безопасности в .NET называется любой объект, реализующий интерфейс IPrincipal из пространства имен System.Security.Principal: public interface IPrincipal { Ildentity Identity {get:} bool IsInRole(string role); } Метод IsInRole() просто возвращает true, если личность, связанная с данным субъектом безопасности, входит в указанную роль, или false в противном слу- чае. Доступное только для чтения свойство Identity обеспечивает доступ к све- дениям о личности в форме объекта, реализующего интерфейс Ildentity. В ис- ходном варианте .NET предоставляет несколько реализаций IPrincipal. Gene- ricPrincipal — субъект безопасности общего назначения, который должен быть заранее заполнен информацией о ролях. Обычно он используется в тех случаях, когда авторизация не обязательна, в качестве «обертки» для пустой личности. Класс WindowsPrincipal берет информацию о ролевой принадлежности из групп Windows NT. С каждым программным потоком .NET связывается объект субъекта без- опасности, полученный с использованием статического свойства Currentprincipal класса Thread: public sealed class Thread
Интрасетевые приложения 487 public static IPrincipal Currentprincipal (get;set:} П... Например, следующий фрагмент определяет имя пользователя, а также про- веряет, прошла ли аутентификацию вызывающая сторона: IPrincipal principal = Thread.CurrentPrincipal: string userName = principal.Identity.Name; bool isAuthenticated = principal.Identity.IsAuthenticated; Выбор режима авторизации Как упоминалось ранее, класс ServiceHostBase предоставляет свойство Authori- zation типа ServiceAuthorizationBehavior. ServiceAuthorizationBehavior содержит свойство PrincipalPermissionMode перечисляемого типа PrincipalPermissionMode, оп- ределяемого следующим образом: public enum PrincipalPermissionMode None. UseWindowsGroups. UseAspNetRoles. Custom public sealed class ServiceAuthorizationBehavior : IServiceBehavior public PrincipalPermissionMode PrincipalPermissionMode {get;set:} П... Перед открытием хоста можно воспользоваться свойством PrincipalPermission- Mode для выбора режима субъекта (то есть типа субъекта, устанавливаемого для авторизации вызывающей стороны). Если свойство PrincipalPermissionMode задано равным PrincipalPermissionMode. None, после аутентификации вызывающей стороны (если аутентификация во- обще необходима) WCF устанавливает GenericPrincipal с пустой личностью и присоединяет его к программному потоку, вызывающему операцию службы. Субъект будет доступен через свойство Thread.Currentprincipal. При выборе режима PrincipalPermissionMode.UseWindowsGroups WCF устанав- ливает объект WindowsPrincipal с личностью, соответствующей предоставленным удостоверениям. Если аутентификация Windows не выполнялась (потому что не требовалась для работы службы), WCF устанавливает WindowsPrincipal с пус- той личностью. PrincipalPermissionMode.UseWindowsGroups является значением по умолчанию свойства PrincipalPermissionMode, поэтому следующие два определения эквива- лентны: ServiceHost hostl = new ServiceHost(typeof(MyServiсе)); ServiceHost host2 = new ServiceHost(typeof(MyService)): host2.Aut ho r i zat i on.P r i nc i pa1Pe rmi s s i onMode = Pr i nci pa 1Permi s s i onMode.UseWi ndowsGroups:
488 Глава 10. Безопасность При использовании конфигурационного файла необходимо включить ссыл- ку на пользовательскую секцию behavior, в которой устанавливается режим субъекта: <services> <service name = "MyService" behaviorConfigyration = "WindowsGroups"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "WindowsGroups"> <serviceAuthorization principalPermissionMode « "UseWindowsGroups"/> </behavior> </serviceBehaviors> </behaviors> Декларативная ролевая безопасность Для применения декларативной ролевой безопасности на стороне службы ис- пользуется атрибут PrincipalPermissionAttribute из пространства имен System.Security. Permissions: public enum SecurityAction { Demand. //... } [Attri butellsage( Attri buteTargets.Cl ass | Attri buteTargets.Method)] public sealed class Principal PermissionAttribute : CodeAccessSecurityAttribute { public PrincipalPermissionAttribute(SecurityAction action): public bool Authenticated {get:set; } public string Name {get:set:} public string Role {get:set:} //... } При помощи атрибута Principalpermission объявляется принадлежность к ро- лям. В интрасетевом сценарии, когда в качестве роли задается группа Windows NT, необходимо использовать префикс с именем домена или локального компьюте- ра (если роль определена только локально). В листинге 10.6 объявление атри- бута Principalpermission предоставляет доступ к MyMethod() только тем вызываю- щим сторонам, личность которых входит в группу пользователей Managers. Листинг 10.6. Декларативная ролевая безопасность в интрасети [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodO:
Интрасетевые приложения 489 } class MyService : IMyContract ( [Pri nci pa1Permi ss1 on(SecurityAct1 on.Demand.Role = @"<doma1n>\Managers")] public void MyMethodO } {•••} Если пользователь не принадлежит к этой роли, .NET инициирует исключе- ние типа SecurityException. ПРИМЕЧАНИЕ----------------------------------------------------------------------------- Во время экспериментов с ролевой безопасностью Windows приходится часто добавлять или уда- лять пользователей из групп. Поскольку информация о группах кэшируется Windows при входе в систему, вносимые изменения отражаются лишь при следующем входе в систему. Если доступ к методу разрешен нескольким ролям, атрибут можно приме- нять многократно: [Рг 1 ncipa1Permi s s1 on(Securi tyAct i on.Demand.Role=@"<doma i n>\Managers")] [Pri nc i pa 1 Permi s s i on(Secu r i tyAct i on.Dema nd.Ro1e=@"<doma i n>\Customers")] public void MyMethodO {...} При использовании нескольких атрибутов PrincipalPermission .NET проверя- ет, что вызывающая сторона входит хотя бы в одну из необходимых ролей. Если вы хотите убедиться в том, что пользователь принадлежит к обеим ролям, придется использовать программную проверку принадлежности к роли (см. да- лее). Хотя атрибут по своему определению может применяться к методам и клас- сам, в классах служб WCF он может применяться только к методам. Дело в том, что в WCF (в отличие от нормальных классов) конструктор класса служ- бы всегда выполняется под субъектом GenericPrinicipal с пустой личностью, не- зависимо от применяемых механизмов аутентификации. В результате личность, под которой выполняется конструктор, не аутентифицирована, а любые попыт- ки авторизации всегда завершаются неудачей (даже если группы Windows NT не используются): // Попытка всегда неудачна [PrincipalPermission(SecurityAction.Demand.Role = "...")] class MyService : IMyContract В конструкторе службы следует избегать выполнения конфиденциальных операций, требующих авторизации. Для служб сеансового уровня такую работу следует выполнять в самих операциях, а при полноценной сеансовой поддерж- ке определяется специализированная операция Initialize(), в которой проводит- ся инициализация экземпляра и авторизация вызывающих сторон. При помощи свойства Name атрибута PrincipalPermission даже можно потре- бовать, чтобы доступ предоставлялся конкретному пользователю. Впрочем, по- ступать так не рекомендуется, потому что жесткое кодирование имен пользова- телей — в принципе неверный подход: [PrincipalPermission(SecurityAction.Demand.Name = "John")]
490 Глава 10. Безопасность Также можно потребовать, чтобы доступ предоставлялся конкретному поль- зователю, принадлежащему к определенной роли: [Princi palPermiss1on(Secur1tyAction.Demand.Name = "John". Role = @"<domain>\Managers")] ПРИМЕЧАНИЕ ------------------------------------------------------------------------ Декларативная ролевая безопасность требует жесткого кодирования имен ролей. Если в приложе- нии имена ролей выбираются динамически, вам придется использовать программную проверку ро- левой принадлежности, описанную в следующем разделе. Программная ролевая безопасность Иногда принадлежность к ролям приходится проверять на программном уров- не. Обычно это делается в ситуациях, когда решение о предоставлении доступа базируется на принадлежности к роли и других факторах, известных только при вызове — например, значениях параметров, времени дня или физического местоположения. Другой пример ситуации, требующей программной проверки принадлежности к роли — локализованные пользовательские группы. В качест- ве примера первой категории, представьте банковскую службу, которая позво- ляет клиентам переводить денежные суммы со счета на счет. Только клиентам и кассирам разрешается вызывать операцию TransferMoney(), со следующим биз- нес-правилом: перевод сумм, превышающих 5000, разрешен только кассирам. В таком случае необходимо использовать метод IslnRole() интерфейса IPrincipal, как показано в листинге 10.7. Листинг 10.7. Программная ролевая безопасность [ServiceContract] Interface IBankAccounts { [Operationcontract] void TransferMoney(double sum.long sourceAccount. long destinationAccount); } static class AppRoles { public const string Customers = @"<domain>\Customers"; public const string Tellers = @"<domain>\Tellers"; } class BankService : IBankAccounts ( [Pri nci palPermi ssi on(Securi tyActi on.Demand.Role = AppRoles.Customers)] [Pri nci palPermi ssi on(Securi tyAction.Demand.Role = AppRoles.Tellers)] public void TransferMoney(double sum.long sourceAccount. long destinationAccount) { IPrincipal principal = Thread.CurrentPrincipal; Debug.Assert(principal.Identity.IsAuthenticated): bool isCustomer = principal.IsInRole(AppRoles.Customers); bool isTeller = principal.IsInRole(AppRoles.Tellers): if(isCustomer && ! IsTeller)
Интрасетевые приложения 491 { if(sum > 5000) { string message = "Caller does not have sufficient " + "authority to transfer this sum"; throw(new UnauthorizedAccessException(message)); } } DoTransfer(sum.sourceAccount.destlnatlonAccount); ) // Вспомогательный метод void DoTransfer(double sum.long sourceAccount.long destinationAccount) {•••} } Листинг 10.7 демонстрирует ряд других интересных моментов. Во-первых, несмотря на программную проверку принадлежности к роли со значением аргу- мента sum, в нем все равно используется декларативная ролевая в качестве «первой линии обороны», которая перекрывает доступ клиентам, не принадле- жащим к ролям Customers и Tellers. Во-вторых, свойство IsAuthenticated интер- фейса Ildentity позволяет на программном уровне проверить, что вызывающая сторона прошла аутентификацию. Наконец, обратите внимание на применение статического класса AppRoles для инкапсуляции строки роли; тем самым пре- дотвращается жесткое кодирование ролей в нескольких местах. Аналогичным образом реализуется программная ролевая безопасность, обеспечивающая вы- полнение дополнительных правил — например, разрешающих перевод средств только в нормальное рабочее время. ЛОКАЛИЗАЦИЯ РОЛЕЙ WINDOWS ------------------------------------------------------------------ Декларативная ролевая безопасность требует жесткого кодирования названия роли. Если ваше приложение поставляется на международный рынок, и в качестве ролей используются группы Windows, скорее всего, в других странах имена ролей не совпадут. В интрасетевом сценарии объект субъекта безопасности, присоединяемый к программному потоку, обращающемуся к службе, отно- сится к типу WindowsPrincipal: public class WindowsPrincipal : IPrincipal ( public WindowsPrincipal(Windowsldentity ntldentity); // Реализация IPrincipal public virtual Ildentity Identity {get;} public virtual bool IsInRole(string role); // Дополнительные методы: public virtual bool IsInRole(int rid); public virtual bool IsInRole(WindowsBuiltInRole role); } WindowsPrincipal предоставляет две дополнительные версии IsInRoleQ, упрощающие задачу лока- лизации групп Windows NT. Вы можете передать IsInRoleQ элемент перечисления Windows- BuiltlnRole, представляющий встроенную роль NT — например, WindowsBuiltlnRole.Administrator или WindowsBuiltlnRole.User. Другая версия IsInRoleQ получает целочисленный индекс роли. Например, индекс 512 соответствует группе Administrators. В MSDN Library представлен список стандартных ин- дексов, а также описаны средства определения псевдонимов и индексов для групп пользователей.
492 Глава 10. Безопасность Ролевая безопасность никак не связана с фактическим типом субъекта без- опасности. Когда атрибут Principalpermission используется для проверки при- надлежности к роли, он просто получает текущего субъекта программного по- тока в форме IPrincipal и вызывает его метод IsInRole(). Это относится и к про- граммной проверке принадлежности, использующей только IPrincipal, как в лис- тинге 10.7. Отделение интерфейса IPrincipal от его реализации является ключом к обеспечению других механизмов ролевой безопасности, помимо групп Win- dows NT, как будет показано в других сценариях. Управление личностью В интрасетевом сценарии после проведения успешной аутентификации WCF присоединяет к программному потоку операции личность субъекта безопасно- сти типа Windowsldentity, у которой свойству Name задано имя пользователя (или учетной записи Windows), предоставленное клиентом. Вследствие пре- доставления действительных удостоверений две личности из контекста безо- пасности вызова — первичная личность и личность Windows — будут совпадать с личностью субъекта безопасности. Все три личности считаются аутентифици- рованными. Личности и их значения представлены в табл. 10.4. Таблица 10.4. Управление личностями в интрасетевом сценарии Личность Тип Значение Аутентификация Субъект безопасности программного Windowsldentity потока Имя пользователя Да Первичная личность из контекста безопасности Windowsldentity Имя пользователя Да Личность Windows из контекста безопасности Windowsldentity Имя пользователя Да Обратные вызовы В том, что касается безопасности в интрасети, между нормальными операциями служб и обратными вызовами существует ряд ключевых различий. Прежде все- го, с контрактом обратного вызова уровень защиты может устанавливаться только на уровне операций, но не на уровне контракта обратного вызова. На- пример, следующее ограничение уровня защиты будет проигнорировано: [ServiceContract(Cal 1backContract = typeof(IMyContractCalIback))] interface IMyContract {•••} // Требование к уровню защиты игнорируется [ServiceContract(ProtectionLevel s ProtectlonLevel.EncryptAndSign)] interface IMyContractCallback {...} Уровень защиты задается только на уровне операций:
Интернет-приложения 493 [ServiceContract(Cal 1backContract = typeof(IMyContractCalIback))] interface IMyContract (•) interface IMyContractCallback [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)] void OnCallback(): Все обращения к объекту обратного вызова поступают с неаутентифициро- ванным субъектом безопасности, даже если безопасность Windows неукосни- тельно использовалась при вызове службы. В результате личности субъекта безопасности будет присвоена личность Windows с пустыми данными, что ис- ключает авторизацию и ролевую безопасность. Хотя обратный вызов имеет контекст безопасности вызова, личности Win- dows будет задан экземпляр Windowsldentity с пустыми данными, вследствие чего перевоплощение становится невозможным. Осмысленная информация бу- дет содержаться только в первичной личности, которой задается личность хос- тового процесса службы и имя компьютера: class MyClient : IMyContractCallback public void OnCalIbackO IPrincipal principal = Thread.Currentprincipal: Debug.Assert(principal.Identity.IsAuthenticated = false); ServiceSecurityContext context = ServiceSecurityContext.Current: Debug.Assert(context.PrimaryIdentity.Name e "MyHost/localhost"); Debug.Assect(context.IsAnonymous e false); } ) Из-за затруднений с ролевой безопасностью я рекомендую избегать выпол- нения каких-либо критичных действий при обратном вызове. нтернет-приложения В интернет-сценарии клиенты и службы могут не использовать не только WCF, но и даже Windows. При написании службы или клиента уже нельзя предполагать, что на другой стороне будет использоваться WCF. Кроме того, для интернет-приложений обычно характерно относительно большое количе- ство клиентов, обращающихся к службе. Вызовы клиентов поступают из-за брандмауэра. Вам приходится полагаться на транспорт HTTP, а клиент может быть отделен от службы несколькими промежуточными инстанциями. В интер- нет-приложениях обычно нежелательно использовать удостоверения на базе учетных записей и групп Windows; вместо этого приложение должно работать с пользовательским хранилищем удостоверений. Впрочем, даже с учетом всех перечисленных факторов вы все равно можете воспользоваться средствами безопасности Windows, как показано далее.
494 Глава 10. Безопасность Защита интернет-привязок В интернет-приложениях для обеспечения безопасности передачи и межконце- вой безопасности при нескольких промежуточных инстанциях необходимо ис- пользовать средства безопасности сообщений. Клиент предоставляет удостове- рения в форме имени пользователя и пароля. Для интернет-сценария следует использовать привязки WSHttpBinding и WSDualHttpBinding. Вдобавок если у вас имеется интрасетевое приложение с NetTcpBinding, но при этом вы почему-либо не хотите применять безопасность Windows для учетных записей пользовате- лей и групп, используйте такую же конфигурацию, как для WS-привязок. Для всех привязок эта задача решается одинаково: для клиентских удостоверений, используемых при безопасности сообщений, выбирается тип MessageCredential- Туре.Username. Такая настройка привязок должна использоваться как клиентом, так и службой. Конфигурация WsHttpBinding Привязка WsHttpBinding предоставляет свойство Security типа WsHttpSecurity: public class WSHttpBinding : WSHttpBindingBase { public WSHttpBinding(); public WSHttpBinding(SecurityMode securityMode); public WSHttpSecurity Security {get;} //... } Свойству Mode типа SecurityMode должно быть задано значение SecurityMode. Message. В этом случае вступает в силу свойство Message класса WSHttpSecurity: public sealed class WSHttpSecurity { public SecurityMode Mode {get:set:} public NonDualMessageSecurityOverHttp Message {get:} public HttpTransportSecurity Transport {get:} } Свойство Message относится к типу NonDualMessageSecurityOverHttp, производ- ному от MessageSecurityOverHttp: public class MessageSecurityOverHttp ( public MessageCredentialType ClientCredentialType {get;set;} //... } public sealed class NonDualMessageSecurityOverHttp ; MessageSecuri tyOverHttp {...} Свойство ClientCredentialType класса MessageSecurityOverHttp задается равным MessageCredentialType.Username. Вспомните, что по умолчанию для привязки WS- HttpBinding используется тип удостоверений Windows (см. табл. 10.3).
Интернет-приложения 495 Поскольку безопасность Message является режимом безопасности по умол- чанию для WSHttpBinding (см. табл. 10.1), следующие три определения эквива- лентны: WSHttpBinding blndlngl = new WSHttpBinding(): bindingl.Security.Message.CH entCredentlal Type = MessageCredentlalType.UserName; WSHttpBlndlng bindings = new WSHttpBindlng(SecurityMode.Message); bindings.Security.Message.Cl 1entCredentlal Type = MessageCredent1 a 1 Type.UserName; WSHttpBlndlng bindings = new WSHttpBindlng(); bindings.Security.Mode = SecurityMode.Message; bindings.Security.Message.ClientCredentlal Type = MessageCredent1 al Type.UserName; Настройка с использованием конфигурационного файла: <Ь1 ndl ngs> <wsHttpB1nding> <b1nd1ng name = "UserNameWS"> <secur1ty mode = "Message"> <message cl 1entCredentlal Type = "UserName”/> </secur1ty> </b1ndlng> </wsHttpBind1ng> </b1ndlngs> Поскольку режим безопасности Message используется по умолчанию, в кон- фигурационном файле его указывать не обязательно: <b1nd1ngs> <wsHttpB1ndlng> <b1nd1ng name = "UserNameWS"> <secur1ty> <message cl 1entCredentlal Type = "UserName"/> </secur1ty> </b1ndlng> </wsHttpB1nding> </bi ndlngs> Рис. 10.4. WSHttpBinding и безопасность
496 Глава 10. Безопасность На рис. 10.4 изображена структура элементов WSHttpBinding, относящихся к области безопасности. WSHttpBinding содержит ссылку на объект WSHttpSecurity, в котором перечис- ление SecurityMode используется для задания режима безопасности передачи. При использовании транспортной безопасности WSHttpSecurity использует эк- земпляр HttpTransportSecurity. При использовании безопасности сообщений WS- HttpSecurity использует экземпляр NonDualMessageSecurityOverHttp с типом кли- ентских удостоверений, заданным из перечисления MessageCredentialType. Конфигурация WSDualHttpBinding Привязка WSDualHttpBinding предоставляет свойство Security типа WsDualHttpSe- curity: public class WSDualHttpBinding : Binding,... { public WSDua1HttpBInding(); public WSDualHttpBInding(WSDualHttpSecurltyMode securityMode): public WSDualHttpSecurlty Security {get;} //... } Для привязки WSDualHttpSecurity свойству Mode типа WSDualHttpSecurityMode должно быть задано значение WSDualHttpSecurityMode.Message. В этом случае вступает в силу свойство Message класса WSDualHttpSecurity: public sealed class WSDualHttpSecurity { public MessageSecurityOverHttp Message {get:} public WSDualHttpSecurityMode Mode {set;set:} } Свойство Message относится к типу MessageSecurityOverHttp, описанному ранее. Свойство ClientCredentialType класса MessageSecurityOverHttp задается равным MessageCredentialType.Username. Вспомните, что по умолчанию для привязки WS- DualHttpBinding используется тип удостоверений Windows (см. табл. 10.3). Поскольку безопасность Message является режимом безопасности по умолча- нию для WSDualHttpBinding (см. табл. 10.1), следующие три определения эквива- лентны: WSDualHttpBinding bindingl = new WSDualHttpBInding(); blndingl.Security.Message.Cl 1entCredentlal Type = MessageCredentla 1 Type.UserName; WSDualHttpBinding bindings = new WSDualHttpBInding(WSDualHttpSecurltyMode.Message); bl nding2.Security.Message.Cl 1entCredent1 a 1 Type = MessageCredentla 1 Type.UserName: WSDualHttpBinding bindings = new WSDualHttpBInding(); bindings.Security.Mode = WSDualHttpSecurltyMode.Message:
Интернет-приложения 497 binding3. Secur i ty. Message. Cl i entCredenti al Type - MessageCredent i al Type. UserName; Настройка с использованием конфигурационного файла: <bindings> <wsDual HttpBinding> <binding name » "WSDualW1ndowsSecurity"> <security mode - ”Message’’> <message cl 1entCredential Type - ”UserName”/> </security> </binding> </wsDualHttpBinding> </bindi ngs> Поскольку режим безопасности Message используется по умолчанию, в кон- фигурационном файле его указывать не обязательно: <bindings> <wsDua 1 HttpBinding> <binding name - "WSDualWindowsSecurity"> <security> <message clientCredential Type - ”UserName’7> </security> </binding> </wsDual HttpBinding> </bindi ngs> На рис. 10.5 изображена структура элементов WSDualHttpBinding, относящих- ся к области безопасности. Рис. 10.5. WSDualHttpBinding и безопасность WSDualHttpBinding содержит ссылку на объект WSDualHttpSecurity, в котором перечисление WSDualHttpSecurityMode используется для задания режима без- опасности передачи: Message или None. При использовании безопасности Message WSDualHttpSecurity использует экземпляр MessageSecurityOverHttp. При использо- вании безопасности сообщений WSDualHttpSecurity использует экземпляр Mes- sageSecurityOverHttp с типом клиентских удостоверений, заданным из перечис- ления MessageCredentialType.
498 Глава 10. Безопасность Защита сообщений В интернет-сценарии сообщение клиента передается службе по обычному про- токолу HTTP, поэтому очень важно защитить его содержимое (как удостовере- ние клиента, так и тело сообщения) посредством шифрования. Шифрование обеспечит целостность и конфиденциальность сообщения. В одном из техниче- ских решений при шифровании используется пароль клиента. Однако WCF никогда не использует пароль клиента при шифровании по нескольким причи- нам. Во-первых, нет никаких гарантий, что пароль обладает достаточной стой- костью и перехват передаваемых данных не позволит взломать шифр. Во-вто- рых, такое решение требует, чтобы служба (а вернее, ее хост) обладала досту- пом к паролю, поэтому хост привязывается к хранилищу удостоверений. Наконец, хотя пароль защищает сообщение, он не поможет аутентифицировать службу по отношению к клиенту. Для защиты сообщений в WCF используется сертификат Х509. Сертификат обеспечивает сильную защиту и однозначно аутентифицирует службу для кли- ента. В работе сертификатов задействованы два ключа, открытый и приватный, а также общее имя (CN) -- например, MyServiceCert. Самая главная особенность ключей заключается в том, что данные, зашифрованные с открытым ключом, могут быть расшифрованы только при наличии приватного ключа. Сертификат содержит открытый ключ и общее имя, а приватный ключ находится в без- опасном хранилище на компьютере, доступном для хоста. Сертификат (и его открытый ключ) являются общедоступными; иначе говоря, любой клиент мо- жет обратиться к конечным точкам хоста и получить открытый ключ. В двух словах, при вызове происходит следующее: WCF на стороне клиента использует открытый ключ для шифрования всех сообщений службы. При по- лучении зашифрованного сообщения WCF на стороне хоста расшифровывает сообщение с приватным ключом. После того как сообщение будет расшифрова- но, WCF читает из него удостоверения клиента, аутентифицирует клиента и предоставляет ему доступ к службе. Впрочем, реальная картина выглядит по- сложнее, потому что WCF также необходимо защитить ответные сообщения и обратные вызовы от службы к клиенту. Один из стандартов, поддерживаемых в WCF, относится к настройке такого безопасного обмена данными. На практи- ке первому запросу от клиента к службе предшествует несколько служебных вызовов; WCF на стороне клиента генерирует секретный ключ, который пере- дается службе в зашифрованном виде (с использованием сертификата служ- бы). Служба использует его для шифрования ответных сообщений и обратных вызовов (если они последуют). Настройка сертификата хоста Класс ServiceHostBase содержит свойство Credentials типа ServiceCredentials. Ser- viceCredentials относится к числу аспектов поведения службы: public abstract class ServiceHostBase : ... { public ServiceCredentials Credentials
(get;} //... public class Servicecredentials : ...,IServiceBehavior pub11c X509Cert1f1cateRecIpientServiceCredent1 a 1 ServiceCerti fl cate (get:} //More members... ServiceCredentials предоставляет свойство ServiceCertificate типа X509Certificate- RecipientServiceCredential: public sealed class X509CertificateRecIpientServiceCredential public void SetCertificate(Storel_ocation storeLocation. StoreName storeName, X509F1ndType flndType. object flndValue): //... Метод SetCertificate() сообщает WCF, где и как следует загружать сертифи- кат службы. Обычно информация передается в конфигурационном файле хоста в виде пользовательской секции behavior в секции ServiceCredentials, как показа- но в листинге 10.8. Листинг 10.8. Настройка сертификата службы <services> <service name = "MyService" behaviorConfiguration = "Internet"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "Internet"> <servi ceCredent1 a 1s> <serviceCertificate flndValue = "MyServiceCert" storeLocation = "LocalMachlne" storeName = "My" x509FindType = "FlndBySubjectName" /> </servi ceCredent1 als> </behavior> </serviceBehaviors> </behaviors> Использование сертификата хоста Разработчик клиента получает сертификат службы с использованием любого внеполосного механизма (например, по электронной почте или через общедос- тупную веб-страницу). Затем в секцию поведения конечных точек конфигура- ционного файла клиента включается подробная информация о сертификате службы — в частности, где он хранится на стороне клиента и как его найти.
500 Глава 10. Безопасность С точки зрения клиента этот способ является самым безопасным, потому что любая попытка вмешаться в схему разрешения адресов клиентом и перенапра- вить вызов вредоносной службе завершится неудачей из-за отсутствия пра- вильного сертификата у другой службы. Впрочем, он же является наименее гибким — каждый раз, когда клиенту потребуется взаимодействовать с другой службой, администратору клиента придется вносить изменения в клиентский конфигурационный файл. Разумная альтернатива включению явных ссылок на сертификаты всех служб, с которыми может взаимодействовать клиент, — хранение этих сертифи- катов в папке Trusted People клиента. Далее по распоряжению администратора WCF разрешает вызовы только к тем службам, сертификаты которых хранятся в этой папке. В этом случае следует получить сертификат службы во время вы- полнения (во время начального согласования, предшествующего вызову), про- верить его наличие в хранилище Trusted People, и если сертификат будет най- ден — использовать его для защиты сообщения. Более того, хотя подобное со- гласование используется по умолчанию в WCF его можно отключить (и ис- пользовать жестко настроенные сертификаты) для WSHttpBinding и WSDualHttp- Binding. В интернет-сценарии я рекомендую использовать согласование сертификатов в сочетании с хранением сертификатов в Trusted People. Проверка сертификата службы Чтобы сообщить WCF, до какой степени следует проверять сертификат служ- бы и доверять ему, включите в описание конечной точки в конфигурационном файле клиента пользовательскую секцию behavior. В ней должна присутство- вать секция Clientcredentials. Этот аспект поведения конечной точки предостав- ляет свойство ServiceCertificate типа X509CertificateRecipientClientCredential: public class Clientcredentials ; ....lEndpointBehavior { public X 509Ce rt i ficateReci pientClientCredenti a1 Servi ceCerti ficate {get;} //... } Класс X509CertificateRecipientClientCredential содержит свойство Authentication типа X509CertificateRecipientClientCredential: public sealed class X509CertificateRecipientClientCredential { public X509ServiceCertificateAuthentication Authentication {get;} //... } X509CertificateRecipientClientCredential предоставляет свойство CertificateValida- tionMode co значениями из перечисления X509CertificateValidationMode: public enum X509CertificateValidationMode { None. PeerTrust. ChainTrust. PeerOrChainTrust.
Custom ) public class X509ServiceCertificateAuthentication I public X509CertificateValidationMode CertificateValidationMode (get: set;} //... В листинге 10.9 показано, как настраивается режим проверки сертификата службы в клиентском конфигурационном файле. Листинг 10.9. Проверка сертификата службы <client> <endpoint behaviorconfiguration - "Servicecertificate" </endpoint> </client> <behaviors> <endpointBehaviors> <behavior name - "ServiceCertificate"> <clientCredentials> <serviceCertificate> authentication certlficateValidationMode - "PeerTrust"/> </serviceCertificate> </clientCredentials> </behavior> </endpo intBehavi ors> </behaviors> В режиме X509CertificateValidationMode.PeerTrust WCF доверяет сертификату службы, проходящему согласование, если он также присутствует в клиентском хранилище Trusted People. В режиме X509CertificateValidationMode.ChainTrust WCF доверяет сертификату службы, если он был выдан доверенным авторитетным источником (например, Verisign или Thwart), сертификат которого находится в папке Trusted Root Authority. Значение X509CertificateValidationMode.ChainTrust используется WCF по умолчанию. Режим X509CertificateValidationMode.Peer0r- ChainTrust разрешает любой из этих двух вариантов. Работа с тестовым сертификатом Разработчики часто не имеют доступа к сертификату своей организации, поэто- му в своей работе им приходится прибегать к тестовым сертификатам — напри- мер, сгенерированным утилитой командной строки MakeCertexe. Использование тестовых сертификатов сопряжено с двумя проблемами. Во-первых, они не проходят стандартную проверку сертификата на стороне клиента, потому что клиент по умолчанию использует режим X509CertificateVatidationMode.ChainTrust. Проблема легко решается установкой тестового сертификата в клиентском хра- нилище Trusted Store и выбором режима X509CertificateVatidationMode.PeerTrust или X509CertificateValidationMode.Peer0rChainTrust. Во-вторых, WCF по умолча- нию ожидает, что имя сертификата службы совпадает с именем домена хоста
502 Глава 10. Безопасность службы (или именем компьютера). Это означает, что клиент должен явно ука- зать имя тестового сертификата в параметре dns секции identity конечной точки: <с!1ent> <endpoint address = "http://localhost:8001/MyServl ce" binding = "WSHttpBinding" contract = "IMyContract"> <identity> <dns value я "MyServiceCert’7> </identity* </endpoint> </client> Аутентификация Клиент должен предоставить свои удостоверения посреднику. Свойство Client- Credentials (см. ранее) базового класса ClientBase<T> содержит свойство UserName типа UserNamePasswordClientCredential: public class Clientcredentials : ....lEndpointBehavior { public UserNamePasswordClientCredential UserName {get:} //... } public sealed class UserNamePasswordCl1entCredent!al { public string UserName {get:set:} public string Password . {get;set;} } Клиент использует UserNamePasswordClientCredential для передачи службе имени пользователя и пароля, как показано в листинге 10.10. ПРИМЕЧАНИЕ -------------------------------------------------------------------------- Клиент не обязан предоставлять имя домена (при использовании безопасности Windows) или имени приложения (при использовании провайдеров ASP.NET). Хост использует домен службы или задан- ное имя приложения, в зависимости от обстоятельств. Листинг 10.10. Передача имени пользователя и пароля MyContractClient proxy = new MyContractClientO: proxy.Clientcredentials.UserName.UserName = "MyUsername": proxy.Clientcredentials.UserName.Password = "MyPassword": proxy.MyMethod(): proxy.CloseO; В отличие от интрасетевого сценария (см. листинг 10.3), где от клиента тре- буется создание нового объекта NetworkCredential, в интернет-сценарий не нуж- но (да и невозможно) присвоить новый объект UserNamePasswordClientCredential. При работе с ChannelFactory вместо класса посредника удостоверения задают- ся свойству Credentials:
Интернет-приложения 503 ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(""); factory.Credentials.UserName.UserName = "MyUsername"; factory.Credentials.UserName.Password = "MyPassword": IMyContract proxy = factory.CreateChannel0; uslngtproxy as IDisposable) ( proxy.MyMethodt); Обратите внимание на невозможность использования статических методов CreateChanneL() класса ChannelFactory<T>, потому что обращение к свойству Cre- dentials требует создания экземпляра. После того как удостоверения (имя пользователя и пароль) будут получены WCF на стороне службы, хост может выбрать между их аутентификацией в виде удостоверений Windows, удостоверений провайдера ASP.NET и даже пользовательских удостоверений. Какой бы вариант вы ни выбрали, он должен соответствовать вашей конфигурации вашей ролевой политики. Класс ServiceCredentials (свойство Credentials класса ServiceHostBase) предо- ставляет свойство UserNameAuthentication типа UserNamePasswordClientCredential: public class ServiceCredentials : ....IServiceBehavior public UserNamePasswordServIceCredentlal UserNameAuthentication (get;} //... ) UserNamePasswordClientCredential содержит свойство UserNamePasswordValida- tionMode co значениями из одноименного перечисления: public enum UserNamePasswordValldatlonMode WI ndows. Membershipprovider. Custom public sealed class UserNamePasswordServIceCredentlal public Membershipprovider Membershipprovider (get;set;} public UserNamePasswordVa11datlonMode UserNamePasswordVal1datlonMode (get; set;} //... Задавая свойство UserNamePasswordValidationMode, хост выбирает способ аутентификации входных удостоверений (имени пользователя и пароля). Удостоверения Windows WCF позволяет службам, доступным через Интернет, аутентифицировать входные удостоверения как удостоверения Windows (хотя это и не означает, что данный способ получил широкое распространение). Чтобы выполнить
504 Глава 10. Безопасность аутентификацию имени пользователя и пароля Windows как удостоверений Windows, необходимо задать UserNamePasswordValidationMode значение UserNamePassword- ValidationMode. Windows. Поскольку значение UserNamePasswordValidationMode.Win- dows используется по умолчанию для свойства UserNamePasswordValidationMode, следующие два определения эквивалентны: ServiceHost hostl = new ServiceHost(typeof(MyService)); ServiceHost host2 = new Serv1ceHost(typeof(MyService)): host2.Credentials.UserNameAuthentlcation.UserNamePasswordVal1 da11onMode = Use rNamePas swordVa11 da11onMode.Wi ndows; При использовании конфигурационного файла включите секцию behavior, в которой задается режим аутентификации имени пользователя и пароля наря- ду с информацией о сертификате службы, как показано в листинге 10.11. Листинг 10.11. Безопасность в Интернете с использованием удостоверений Windows <serv1ces> <serv1ce name = "MyService" behaviorconfiguration = "UsernameWIndows"> </serv1ce> </serv1ces> <behav1ors> <serv1ceBehav1ors> <behav1or name - "UsernameWIndows"> <serv1ceCredent1als> <userNameAuthent1cat1on UserNamePasswordValidationMode в "Windows"/> <serv1ceCertif1cate /> </serv1ceCredent1als> </behav1or> </serv1ceBehav1ors> </behav1ors> Как и при программной настройке, включение в конфигурационный файл строки <userNameAuthent1cation UserNamePasswordValidationMode = "WIndows"/> не является обязательным, потому что значение используется по умолчанию. Авторизация Если свойству PrincipalPermissionMode класса ServiceAuthorizationBehavior задано значение по умолчанию PrincipalPermissionMode.UseWindowsGroups, после аутен- тификации имени пользователя и пароля по данным Windows WCF устанавли- вает новый объект субъекта безопасности Windows и присоединяет его к про- граммному потоку. Это дает возможность службе свободно пользоваться груп- пами Windows NT для авторизации, как и в интрасетевом случае, на деклара- тивном и программном уровне. Управление личностью При условии, что субъекту безопасности задан режим разрешений PrincipalPer- missionMode.UseWindowsGroups, аспект управления личностью ничем не отлича-
Интернет-приложения 505 ется от интрасетевого сценария, включая личности контекста безопасности вы- зовов (см. табл. 10.4). Главное отличие интрасетевых приложений от интер- нет-приложений, использующих удостоверения Windows, заключается в том, что клиент не может диктовать разрешенный уровень перевоплощения, и хост может перевоплощаться по своему усмотрению. Это объясняется тем, что WCF задает личности Windows из контекста безопасности вызовов значение Tokenlm- personationLeveLImpersonation. Провайдеры ASP.NET По умолчанию ролевая безопасность WCF использует группы пользователей Windows для ролей и учетные записи Windows для личностей. Подобная стан- дартная политика обладает рядом недостатков. Во-первых, создание учетной записи Windows для каждого клиента интернет-приложения может быть неже- лательным. Во-вторых, политика безопасности детализирована лишь до уровня групп пользователей в хостовом домене. IT-отделы конечных пользователей часто находятся за пределами вашей компетенции. Если приложение будет ус- тановлено в среде с широким составом групп, или если группы пользователей недостаточно хорошо соответствуют ролям пользователей вашего приложения, или при других именах групп, ролевая безопасность Windows окажется беспо- лезной. Локализация ролей создает дополнительные проблемы, потому что имена ролей могут различаться между пользовательскими площадками, нахо- дящимися в разных локальных контекстах. По этой причине учетные записи и группы пользователей Windows в интернет-приложениях почти не использу- ются. .NET 2.0 предоставляет готовую модель пользовательского управления удо- стоверениями, называемую моделью провайдера ASP.NET, Впрочем, эта модель может легко использоваться и другими приложениями, не имеющими отноше- ния к ASP.NET (в том числе и приложениями WCF), с целью аутентификации и авторизации пользователей без использования учетных записей Windows. В одной из конкретных реализаций этой архитектуры задействовано храни- лище SQL Server. SQL Server используется многими интернет-приложениями, поэтому я тоже выберу его в этом сценарии. Чтобы использовать провайдера SQL Server, запустите установочный файл aspnet_regsql.exe из папки %Windir°/o\ Microsoft.NET\Framework\<Bepcu«>\. Программа установки создает новую базу данных aspnetdb с таблицами и хранимыми процедурами для управления удо- стоверениями. Механизм хранения удостоверений с использованием SQL Server хорошо спроектирован; в нем задействованы новейшие средства управления удостове- рениями: добавление случайной поправки к паролям (password salting), храни- мые процедуры и т. д. Эта инфраструктура повышает производительность, экономит время и силы разработчиков, не говоря уже о создании более качест- венных и безопасных решений. При этом архитектура управления удостовере- ниями определяется моделью провайдера, и вы можете легко добавить другой способ хранения — скажем, базу данных Access.
506 Глава 10. Безопасность Провайдеры удостоверений На рис. 10.6 изображена архитектура провайдеров удостоверений ASP.NET 2.0. Рис. 10.6. Модель провайдера ASP.NET Провайдеры принадлежности отвечают за управление пользователями (име- на и пароли), а провайдеры ролей — за управление ролями. В исходном состоя- нии ASP.NET поддерживает хранение данных принадлежности в SQL Server и Active Directory, а роли могут храниться в SQL Server, в файлах (хранилище данных авторизации) или группах NT (электронный ключ Windows). Аутентификация имен пользователей и паролей осуществляется классом MembershipProvider из пространства имен System.Web.Security, определяемого сле- дующим образом: public abstract class Membershipprovider : ProviderBase { public abstract string ApplicationName {get;set:} public abstract bool Vaiidatellser(string name.string password): //... } Основная цель MembershipProvider — инкапсуляция провайдера и подробно- стей доступа к данным, а также возможность изменения провайдера принад- лежности без влияния на работу самого приложения. В зависимости от провай- дера безопасности, настроенного в конфигурационном файле хоста, WCF ис- пользует конкретный класс доступа к данным — например, класс SqLMembership- Provider для SQL Server или SQL Server Express: public class SqlMembershipProvider : MembershipProvider {•••} Впрочем, WCF имеет дело только с базовой функциональностью Member- shipProvider. Для получения провайдера принадлежности WCF обращается к статическому свойству Provider класса Membership: public sealed class Membership { public static string ApplicationName {get:set:} public static MembershipProvider Provider {get:} public static bool VaiidateUser(string username.string password): //... }
Интернет-приложения 507 Membership содержит много методов, представляющих различные аспекты управления пользователями. Свойство Membership.Provider получает тип про- вайдера, заданный в секции System.Web конфигурационного меню хоста. Если провайдер не задан явно, по умолчанию используется значение SqlMembership- Provider. ПРИМЕЧАНИЕ------------------------------------------------------------------- Так как все провайдеры принадлежности являются производными от абстрактного класса Member- shipProvider, написанные вами провайдеры тоже должны быть производными от Membership- Provider. Одно хранилище удостоверений может обслуживать сразу несколько прило- жений, поэтому в этих приложениях могут определяться одинаковые имена пользователей. Для этого каждая запись в хранилище уточняется именем при- ложения (по аналогии с тем, как имена пользователей Windows уточняются до- меном или именем компьютера). Свойство ApplicationName класса Membership используется для задания и чте- ния имени приложения, а метод ValidateUser() аутентифицирует заданные удо- стоверения по хранилищу данных, возвращая true при совпадении, или false в обратном случае. Membership.ValidateUser — сокращенная запись для получе- ния и использования настроенного провайдера. Если вы настроите свое приложение на авторизацию с использованием хра- нилища удостоверений ASP.NET и включите поддержку ролей, после аутенти- фикации WCF устанавливает экземпляр внутреннего класса RoleProviderPrin- cipal и присоединяет его к программному потоку, вызывающему операцию: sealed class RoleProviderPrincipal : IPrincipal (•} RoleProviderPrincipal использует абстрактный класс RoleProvider для выполне- ния авторизации: public abstract class RoleProvider : ProviderBase { public abstract string ApplicationName (get;set;} public abstract bool IsUserInRole(string username,string roleName); //... Свойство ApplicationName класса RoleProvider привязывает провайдера роли к конкретному приложению. Метод IsUserInRole() проверяет принадлежность пользователя к роли. Как и провайдеры принадлежности, все провайдеры ро- лей (в том числе и пользовательские) должны быть производными от RolePro- vider. Класс RoleProvider инкапсулирует фактически используемого провайдера, а провайдер роли задается в конфигурационном файле хоста. В зависимости от настроенного провайдера роли RoleProviderPrincipal использует соответствую- щий класс доступа к данным (такой, как SqlRoleProvider) для авторизации вызы- вающей стороны:
508 Глава 10. Безопасность public class SqlRoleProvider : RoleProvider {...} Для получения провайдера роли следует обратиться к свойству Provider класса Roles, которое определяется следующим образом: public sealed class Roles { public static string ApplicationName {get;set;} public static bool IsUserlnRoleCstring username.string roleName); public static RoleProvider Provider {get:} //... } Roles.IsUserlnRoleQ — сокращенная запись для обращения к Roles.Provider с последующим вызовом IsUserInRole(). Roles.Provider получает тип настроенного провайдера из конфигурационного файла хоста. Если тип не задан, по умолча- нию используется SqlRoleProvider. Администрирование удостоверений Если для хранения информации о пользователях и ролях в вашем приложении будут выбраны средства Windows или Active Directory, для администрирова- ния удостоверений придется использовать специализированные инструменты - такие, как приложение Управление компьютером или инструментарий Active Directory. При использовании SQL Server .NET 2.0 устанавливает страницы админист- рирования в папку \Inetpub\wwwroot\aspneCwebadmin\<Bepcu«>. Разработчик также может задать конфигурацию приложения прямо в Visual Studio 2005. При выборе команды ASP.NET Configuration в меню Web Site Visual Studio 2005 открывает административные страницы ASP.NET с возможностью выбора раз- личных параметров, в том числе и конфигурации безопасности. Здесь можно выполнять следующие административные операции: О создание новых и удаление существующих пользователей; О создание новых и удаление существующих ролей; О назначение ролей пользователям; О получение подробной информации о пользователе; О задание статуса пользователя; О дополнительные операции, не имеющие отношения к данной главе. Недостатки административного инструментария Visual Studio 2005 У административных страниц, предоставляемых Visual Studio 2005, существует ряд важных недостатков. Прежде всего, для них необходимо наличие Visual Studio 2005. Вряд ли у системных администраторов или администраторов при- ложения окажется собственная копия Visual Studio 2005, не говоря уже об уме- нии ей пользоваться. По умолчанию административные страницы используют имя приложения </> и не предоставляют никаких визуальных средств для его
Интернет-приложения 509 изменения. Для активизации административных страниц необходимо создать веб-приложение, а удаленный доступ не поддерживается: чтобы среда Visual Studio 2005 могла работать с конфигурационным файлом, приложение и Visual Studio 2005 должны находиться в одном месте. Браузерный интерфейс весьма неудобен (приходится часто щелкать на кнопке Назад) и убог. Многие функ- ции, часто используемые администраторами, недоступны на административных страницах — и это при том, что они поддерживаются нижележащими классами провайдеров. Вот лишь некоторые из административных операций, недоступ- ных в административных страницах Visual Studio 2005: О получение пароля пользователя; О изменение пароля пользователя; О сброс пароля пользователя; О получение информации о текущем количестве подключенных пользователей; О удаление всех пользователей из роли за одну операцию; О получение информации о политике управления паролями (длина, политика сброса, тип паролей и т. д.) О проверка удостоверений пользователя; О проверка принадлежности пользователя к роли. Более того, существует целый ряд других полезных административных опе- раций, которые не поддерживаются даже классами провайдеров. К их числу от- носится получение списка всех приложений в базе данных, возможность удале- ния всех пользователей приложения, возможность удаления всех ролей из приложения, возможность удаления приложения (и всех связанных с ним ро- лей и пользователей), а также возможность удаления всех приложений. Credentials Manager Очевидные недостатки инструментария заставили меня разработать приложе- ние Credentials Manager — более совершенное клиентское приложение, лишен- ное всех перечисленных недостатков. На рис. 10.7 показано, как выглядит окно Credentials Manager1. Credentials Manager присутствует в архиве программного кода, прилагаемом к книге. Я упаковал провайдеров ASP.NET в службу WCF (с авто-хостингом или хостингом IIS), а также добавил ряд отсутствующих функций — в частно- сти, удаления приложений. Credentials Manager использует конечные точки службы для администриро- вания хранилищ удостоверений. Кроме того, приложение позволяет админист- ратору на стадии выполнения выбрать адрес службы удостоверений, а при по- мощи класса MetadataHelper, представленного в главе 2, оно убеждается в том, что указанный адрес действительно поддерживает необходимые контракты. 1 Более ранняя версия Credential Manager была впервые описана в моей статье «Manage Custom SecurityCredentials the Smart (Client) Way», CoDe Magazine, ноябрь 2005 г. С тех пор Credentials Manager фактически превратился в стандартный инструмент администрирования хранилищ удо- стоверений ASP.NET SQL Server.
Applied ?n Applications) | OeateUy Users Roles Passwords Service Test Help ]sdentials Service | Users Brian N John Dj fe 3 Juval Li J Mary Di " 4«bi Roles for User: ^•Manager s Operator iie, T eller Update User Delete User Delete All Users Change Password Reset Password /e Refresh Users Status bAH t fulfil John Doe J< Juval Lowy { Mary Doe Рис. 10.7. Программа Credentials Manager Аутентификация Чтобы аутентифицировать имя клиента и пароль с использованием прова ASP.NET, задайте свойству UserNamePasswordValidationMode значение Usei PasswordValidationMode.Membershipprovider: ServiceHost host = new Serv1ceHost(typeof(MyService)); host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValldatlonMode.MembershlpProvIder; Выбор используемого провайдера зависит от конфигурационного ( хоста. Кроме того, конфигурационный файл хоста должен содержать спе] ческие настройки конкретного провайдера — например, строку подклю SQL Server, как показано в листинге 10.12. Листинг 10.12. Безопасность в Интернете с использованием провайдера ASP.NE на базе SQL Server <connect1onStrlngs> <add name= "AspNetDb" connectionstring = "data source=(local); Integrated Security=SSPI:Initial Catalog=aspnetdb"/> </connect1onStrings> <system.serviceModel> <services> <serv1ce name = "MyService" behaviorConfiguration = "ASPNETProv1ders"> <endpo1nt /> </service>
</services> <behaviors> <serviceBehaviors> <behavior name = "ASPNETProv1ders"> <serviceCredent1als> <userNameAuthenti cati on UserNamePasswordValidationMode - "MembershipProvider’7> <serv1ceCertificate /> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> </system. servi ceModel > По умолчанию в качестве имени приложения используется бесполезный символ /, поэтому вы должны задать имя своего конкретного приложения. По- сле настройки провайдеров ASP.NET WCF инициализирует свойство Member- shipProvider класса UserNamePasswordServiceCredential экземпляром заданного провайдера принадлежности. Вы можете на программном уровне обратиться к этому провайдеру и задать имя приложения: ServiceHost host = new ServiceHost(typeof(MyService)); Debug. Assert (host. Credent) al s.UserNameAuthenti cati on. Membershipprovider != null): Membership.ApplicationName • "MyApplication"; Имя приложения также можно задать в конфигурационном файле, но для этого необходимо определить специального провайдера принадлежности ASP.NET, как показано в листинге 10.13. Листинг 10.13. Настройка имени приложения для провайдера принадлежности <system. web> «membership defaultProvider = "MySqlMembershipprovider"> <providers> <add name = "MySqlMembershipprovider" type = "System.Web.Security.SqlMembershipprovider" connectwnStringName = "AspNetDb" applicationName e "MyApplication" /> </providers> </membership> </system. web> <connectionStrings> <add name = "AspNetDb" /> </connectionStrings> В листинге 10.13 добавляется секция system.Web с подсекцией providers, где определяется пользовательский провайдер принадлежности, назначаемый для использования по умолчанию. Затем следует список всех полных имев типов нового провайдера. Ничто не мешает вам ссылаться на любые существующие реализации провайдеров принадлежности — например, SqlMembershipProvider, как в листинге 10.13. При использовании провайдера SQL также необходимо
512 Глава 10. Безопасность указать используемую строку подключения, не полагаясь на строку по умолча- нию из файла machine.config. Что еще важнее, в теге ApplicationName должно быть указано имя приложения. Авторизация Для поддержки авторизации пользователей хост должен включить ролевую безопасность, для чего в конфигурационный файл добавляется следующий фрагмент: <system.web> <roleManager enabled - "true"/> </system.web> ПРИМЕЧАНИЕ --------------------------------------------------------------- Для включения менеджера ролей на программном уровне необходимо использовать рефлексию. Включение ролей таким способом инициализирует класс Roles, а его свойст- ву Provider задается настроенный провайдер. Чтобы использовать провайдера ролей ASP.NET, задайте свойству PrincipalPermissionMode значение PrincipalPer- missionMode.UseAspNetRoles: ServiceHost host - new ServiceHost(typeof(MyService)); host.Author! zati on.Pri ncipalPermi ssionMode-PrincipalPermi ssionMode.UseAspNetRoles: При использовании конфигурационного файла того же эффекта можно до- биться вставкой следующего фрагмента: <services> <service name - "MyService" behaviorconfiguration - "ASPNETProviders"> </service> </services> <behaviors> <serviceBehaviors> <behavior name - "ASPNETProviders"> <serviceAuthorization PrincipalPermissionMode - "UseAspNetRoles" </behavior> </serviceBehaviors> </behaviors> После аутентификации клиента свойству RoleProvider класса ServiceAuthori- zationBehavior задается настроенный провайдер роли: public sealed class ServiceAuthorizationBehavior : IServiceBehavior { public RoleProvider RoleProvider {get;set:} //... } По умолчанию в качестве имени приложения используется бесполезный символ /, поэтому вы должны задать имя своего конкретного приложения. В отличие от провайдера принадлежности, имя приложения не может задавать- ся свойством RoleProvider класса ServiceAuthorizationBehavior, потому что вплоть
Интернет-приложения 513 до завершения аутентификации оно остается равным null. Вместо этого следует использовать статический вспомогательный класс Roles: ServiceHost host = new ServiceHost(typeof(MyService)): Debug. Assert(host.Credentials.UserNameAuthentication.MembershipProvider != null): Roles.ApplicationName я "MyApplication": Имя приложения также можно настроить в конфигурационном файле, но для этого необходимо определить пользовательского провайдера роли ASP.NET, как показано в листинге 10.14. Листинг 10.14. Настройка имени приложения для провайдера роли <system.web> <roleManager enabled = "true" defaultprovider = "MySqlRoleManager"> <providers> <add name = "MySqlRoleManager" type = "System.Web.Security.SqlRoleProvider" connectlonStringName = "AspNetDb" applIcationName = "MyApplication" /> </prov1ders> </roleManager> </system.web> <connectionStr1ngs> <add name = "AspNetDb" /> </connect1onStrings> Как и в случае провайдера принадлежности, в файл включается секция system.Web с секцией providers, где создается пользовательский провайдер роли, назначаемый новым провайдером роли по умолчанию. Затем указывается пол- ное имя типа нового провайдера. Как и в случае с провайдером принадлежно- сти, вы можете сослаться на любую существующую реализацию провайдера принадлежности — например, SqlMembershipProvider (в этом случае указывается строка подключения). Наконец, в теге ApplicationName задается нужное имя приложения. Декларативная ролевая безопасность Атрибут Principalpermission может использоваться для проверки принадлежно- сти к роли, как и в интрасетевом сценарии, потому что этот атрибут всего лишь обращается к объекту субъекта безопасности, которому уже был задан объект RoleProviderPrincipal. В листинге 10.15 продемонстрирована декларативная роле- вая безопасность с использованием провайдеров ASP.NET. Листинг 10.15. Декларативная ролевая безопасность с использованием провайдеров ASP.NET class MyService : IMyContract ( [PrincipalPermission(SecurityAct1 on.Demand.Role = "Manager")] public void MyMethodO
514 Глава 10. Безопасность Единственное существенное различие между интрасетевым и интернет-сце- нарием заключается в том, что в интернет-сценарии при задании имени роли не указывается префикс с именем домена или приложения. Управление личностями В интернет-сценарии при использовании провайдеров ASP.NET личность, свя- зываемая с объектом субъекта безопасности, представляет собой объект Gene- ricidentity, в котором инкапсулируется имя пользователя, предоставленное кли- ентом. Эта личность считается аутентифицированной. Первичная личность контекста безопасности вызовов совпадает с личностью субъекта безопасности. Напротив, в качестве личности Windows задается объект Windowsldentity с пус- тым именем пользователя, то есть неаутентифицированный. Личности и их значения представлены в табл. 10.5. Таблица 10.5. Управление личностями в интернет-сценарии Личность Тип Значение Аутентификация Субъект безопасности программного потока Genericidentity Имя пользователя Да Первичная личность из контекста безопасности Genericidentity Имя пользователя Да Личность Windows из контекста безопасности Windowsldentity — Нет Перевоплощение Из-за отсутствия предоставляемых удостоверений Windows служба не может перевоплощаться под личностью какого-либо из ее клиентов. Обратные вызовы При использовании провайдеров ASP.NET сообщения обратного вызова защи- щены, но все вызовы, обращенные к объекту обратного вызова, связываются с неаутентифицированным субъектом безопасности. В результате личности субъекта безопасности Windows присваивается объект Windowsldentity с пустым именем пользователя, в результате чего авторизация и ролевая безопасность становятся невозможными, так как личность считается анонимной. Хотя обрат- ный вызов обладает контекстом безопасности вызова, личности Windows зада- ется объект Windowsldentity с пустой личностью, поэтому перевоплощение не- возможно. Содержательная информация хранится только в первичной лич- ности; ей задается экземпляр класса X509Identity, в котором в качестве имени задается общее имя сертификата хоста службы с суффиксом из хеш-кода серти- фиката: class MyClient : IMyContractCallback { public void OnCallbackO { IPrincipal principal = Thread.Currentprincipal;
Приложение «бизнес-бизнес» 515 Debug.Assert(principa 1.Identity.IsAuthenticated e false); ServiceSecurityContext context = ServiceSecurityContext.Current; Debug.Assert(context.PrimaryIdenti ty.Name == "CN=MyServiceCert; D6E33B50BCF6D9609E68762F2C6A14F65679268B”); Debug.Assert(context.IsAnonymous = false); } ) Из-за затруднений с ролевой безопасностью я рекомендую избегать выпол- нения каких-либо критичных действий при обратном вызове. 1риложение «бизнес-бизнес» В сценарии «бизнес-бизнес» служба и ее клиенты представлены как разнород- ные сущности. Они не имеют общих удостоверений или учетных записей, а взаимодействие между ними обычно закрыто от окружающего мира. Количе- ство клиентов, взаимодействующих со службой, относительно невелико, а кли- ент может общаться со службой только при удовлетворении тщательно прора- ботанного соглашения и других условий. Вместо имен пользователей и учетных записей Windows, клиенты идентифицируют себя перед службой при помощи сертификатов Х509. Эти сертификаты обычно известны службе заранее. Кли- ент или служба может не использовать WCF, и даже систему Windows. Если вы пишете клиента или службу, нельзя предполагать, что на другом конце ис- пользуется WCF. Клиентские вызовы поступают из-за брандмауэра, для пере- дачи данных приходится полагаться на протокол HTTP, и между службой и клиентом может быть несколько промежуточных точек. Защита привязок «бизнес-бизнес» В сценарии «бизнес-бизнес» следует использовать привязки Интернета; а имен- но BasicHttpBinding, WSHttpBinding и WSDualHttpBinding. Для обеспечения меж- концевой безопасности через все промежуточные точки необходимо использо- вать защиту сообщений. Сообщения защищаются с использованием сертификата стороны службы, как и в интернет-сценарии. Однако в отличие от интер- нет-сценария, клиенты предоставляют удостоверения в форме сертификата. Это делается одинаково для всех привязок, выбором значения MessageCreden- tialType.Certificate для типа клиентских удостоверений, используемого с без- опасностью сообщений. Настройка должна быть выполнена как на стороне клиента, так и на стороне службы. Например, программная настройка WSHttp- Binding должна выполняться так: WSHttpBinding binding = new WSHttpBinding(); bindi ng.Securi ty.Message.Cli entCredenti al Type = MessageCredentlal Type.Certi fi cate; To же с использованием конфигурационного файла: <bi ndi ngs> <wsHttpBinding> <binding name = "WSCertificateSecurity"> <security mode = "Message"> <message clIentCredentialType = "Certificate"^
516 Глава 10. Безопасность </security> </binding> </wsHttpBinding> </bindings> Аутентификация Администратор службы выбирает один из нескольких способов аутентифика- ции сертификатов, переданных клиентом. Если сертификат проходит проверку, клиент считается аутентифицированным. Если проверка не выполняется, дос- таточно одного факта отправки сертификата. Если проверка настроена на ис- пользование «цепочки доверия» (то есть сертификат выдан доверенным авто- ритетным источником), клиент считается аутентифицированным. Однако чаще для проверки клиентского сертификата используется механизм доверия равно- правных узлов (peer trust). Администратор службы должен установить все сер- тификаты клиентов, которым разрешено взаимодействие со службой, в храни- лище Trusted People на локальном компьютере службы. Получая сертификат клиента, служба ищет его в хранилище. Если поиск оказывается успешным, клиент проходит аутентификацию. Я буду использовать в сценарии «бизнес- бизнес» механизм доверия равноправных узлов. Класс ServiceCredentials содержит свойство Clientcertificate типа X509Certifica- telnitiatorServiceCredential: public class ServiceCredentials : ....IServiceBehavior { public X509CertificateInitiatorServiceCredential Clientcertificate {get:} //... } X509CertificateInitiatorServiceCredential содержит свойство Authentication типа X509ClientCertificateAuthentication, используемое для настройки режима провер- ки сертификата: public sealed class X509CertificatelnitiatorServiceCredential { public X509ClientCertificateAuthentication Authentication {get:} //... } public class X509ClientCertificateAuthentication { public X509CertificateValidationMode CertificateValidationMode {get:set;} //... } В листинге 10.16 представлены настройки конфигурационного файла хоста для сценария «бизнес-бизнес». Обратите внимание: хост должен предоставить собственный сертификат для обеспечения безопасности сообщений. Листинг 10.16. Настройка хоста для безопасности «бизнес-бизнес» <services> <service name = "MyService" behaviorConfiguration = "BusinessToBusiness">
Приложение «бизнес-бизнес» 517 </service> </services> <behaviors> <serviceBehavlors> <behavior name = "BusinessToBusiness"> <serv1ceCredentials> <serviceCertificate /> <cl1entCert1ficate> authentication certiflcateValidatlonMode "PeerTrust"/> </cl1entCert1ficate> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> Для определения используемого сертификата клиент указывает его располо- жение, имя и метод поиска. Для этого он обращается к свойству Clientcredentials посредника, предоставляющему свойство Clientcertificate типа X509CertificateIni- tiatorClientCredential: public class Clientcredentials : ....lEndpointBehavior ( public X509CertificatelnitiatorClientCredential ClientCertif1 cate {get:} //... ) public sealed class X509CertificateInitiatorClientCredential public void SetCertificate(StoreLocation storeLocation. StoreName storeName, X509FindType findType. object findValue): //... Однако чаще настройка производится в конфигурационном файле, как по- казано в листинге 10.17. Устинг 10.17. Настройка клиентского сертификата client> <endpoint behaviorConfiguration = "BusinessToBusiness" /> /cl i ent> )ehaviors> <endpointBehaviors> <behavior name = "BusinessToBusiness"> <clientCredentials> <clientCertificate findValue = "MyClientCert" storeLocation = "LocalMachine" storeName = "My" x509FindType = "FindBySubjectName" продолжение &
518 Глава 10. Безопасность /> </clientCredentials> </behavior> </endpointBehaviors> </behaviors> В конфигурационном файле также должен быть указан режим проверки сер- тификатов службы. При использовании привязки BasicHttpBinding, не позволяю- щей согласовать сертификат службы, в соответствующей секции конфигураци- онного файла клиента должно быть указано местонахождение используемого сертификата службы. Учтите, что при использовании тестового сертификата службы, как в интернет-сценарии, конфигурационный файл клиента все равно должен включать информацию, касающуюся личности конечной точки. После того как сертификат клиента будет настроен, дальнейшая работа с классом посредника выглядит вполне обычно: MyContractClient proxy = new MyContractClient(): proxy.MyMethodO: proxy.Close(); Авторизация По умолчанию служба не может применять безопасность, основаннук) на моде- ли субъектов и ролей. Дело в том, что предоставляемые удостоверения, а имен- но сертификат клиента, не имеет соответствия среди учетных записей Windows или ASP.NET. Поскольку конечные точки и службы в приложениях «биз- нес-бизнес» часто специализируются на обслуживании небольшой группы кли- ентов или даже одного конкретного клиента, может оказаться, что отсутствие поддержки авторизации не создает проблем. Если это так, задайте свойству PrincipalPermissionMode значение PrincipalPermissionMode.None, чтобы использо- вался обобщенный субъект безопасности с пустыми данными личности (в от- личие от личности Windows с пустыми данными). С другой стороны, если вы все равно хотите организовать авторизацию кли- ентов, это тоже возможно. Все, что для этого потребуется, — создать хранилище удостоверений, включить в него имя сертификата клиента (то есть общее имя и хеш-код), а затем осуществлять проверки по данным в хранилище по мере не- обходимости. В сущности, ничто не мешает вам использовать провайдеров ролей ASP.NET для авторизации, даже несмотря на то, что провайдер принадлежно- сти не использовался в аутентификации. Возможность раздельного использова- ния провайдеров была одной из основных целей при проектировании модели провайдеров ASP.NET. Сначала включите провайдера роли в конфигурационный файл хоста и на- стройте имя приложения так, как показано в листинге 10.14 (или задайте его на программном уровне). Затем добавьте сертификат клиента с хеш-кодом в хранилище в виде пользова- теля и назначьте ему роли. Например, при использовании сертификата с общим именем MyClientCert следует включить в хранилище пользователя с именем вида «CN=MyClientCert;12A06153D25E94902F50971F68D86DCDE2A00756»
Приложение «бизнес-бизнес» 519 и указать пароль. Конечно, пароль ни на что не влияет и при авторизации не ис- пользуется. После того как пользователь будет создан, включите его в соответ- ствующие роли. Остается сделать самый важный шаг — задать свойству PrincipalPermission- Mode значение PrincipalPermissionMode.UseAspNetRoles. В листинге 10.18 перечис- лены необходимые настройки в конфигурационном файле хоста. Листинг 10.18. Ролевая безопасность ASP.NET для сценария «бизнес-бизнес» <system.web> <roleManager enabled - "true" defaultprovider « "MySqlRoleManager"> <providers> <add name = "MySqlRoleManager" applicationName я "MyApplication" /> </providers> </roleManager> </system.web> <system.servi ceModel> <services> <service name = "MyService" behaviorconfiguration = "BusinessToBusiness"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "BusinessToBusiness"> <serviceCredentials> <serviceCertificate /> <clientCertificate> authentication certificateValidationMode = "PeerTrust,,/> </clientCertificate> </servi ceCredenti als> <servi ceAuthori zati on PrincipalPermissionMode = "UseAspNetRoles"/> </behavior> </serviceBehaviors> </behaviors> <bindings> </bindings> </system.serviceModel> Теперь вы можете использовать ролевую безопасность так, как показано в листинге 10.15. Управление личностями Если свойству PrincipalPermissionMode задается значение PrincipalPermissionMode. None, то в качестве первичной личности будет использоваться объект Generic- Identity с пустым именем пользователя. Контекст безопасности вызова относится
520 Глава 10. Безопасность к типу X509Identity; в нем содержится общее имя клиентского сертификата и его хеш-код. Личность Windows контекста безопасности содержит пустое имя поль- зователя, так как удостоверения Windows не были предоставлены. Если свойст- ву PrincipalPermissionMode задано значение PrincipalPermissionMode.UseAspNetRoles, то и личности субъекта безопасности, и первичной личности контекста безопас- ности задается экземпляр X509Identity с сертификатом и хеш-кодом клиента. Личность Windows из контекста безопасности имеет пустое имя пользователя, как и прежде. Эта конфигурация представлена в табл. 10.6. Таблица 10.6. Управление личностями в сценарии «бизнес-бизнес» с провайдерами ролей ASP.NET Личность Тип Значение Аутентификация Субъект безопасности программного потока X509Identity Имя клиентского сертификата Да Первичная личность из контекста безопасности X509Identity Имя клиентского сертификата Да Личность Windows из контекста безопасности Windowsldentity — Нет Перевоплощение Из-за отсутствия предоставляемых удостоверений Windows служба не может перевоплощаться под личностью какого-либо из ее клиентов. Обратные вызовы В сценарии «бизнес-бизнес» обратные вызовы работают точно так же, как и в интернет-сценарии, так как в обоих случаях используется один механизм безопасности передачи данных, а служба идентифицирует себя с использова- нием сертификата. Как и в интернет-сценарии, при обратном вызове следует избегать выполнения каких-либо критичных действий, поскольку применение ролевой безопасности невозможно, а вызывающая сторона не аутентифициро- вана с точки зрения субъекта безопасности. Конфигурация безопасности хоста Хотя рис. 10.8 и не относится к специфике сценариев «бизнес-бизнес», только после рассмотрения этого сценария я могу представить все компоненты хоста службы, относящиеся к безопасности. Анонимное приложение В анонимном сценарии клиенты обращаются к службе, не представляя ка- ких-либо удостоверений. С другой стороны, клиентам и службе необходима безопасная передача сообщений, защищенная от фальсификаций и перехвата. Необходимость в предоставлении анонимного, но при этом безопасного меж- концевого доступа может возникнуть как в интернет-приложениях, так и в ин-
Приложение «бизнес-бизнес» 521 трасетевых приложениях. Анонимный сценарий подразумевает произвольное количество клиентов, малое или большое. Клиенты могут подключаться по протоколу HTTP или TCP. Рис. 10.8. Компоненты безопасности ServiceHostBase Безопасность анонимных привязок Необходимость в защите сообщений и тот факт, что клиенты могут передавать вызовы по Интернету с несколькими промежуточными инстанциями, означает, что в анонимном сценарии должна использоваться безопасность сообщений, поскольку при ней можно легко выполнить оба требования, отказавшись от пе- редачи удостоверений. Служба должна быть настроена с сертификатом защиты самих сообщений. В анонимном сценарии могут использоваться только NetTcp- Binding, WSHttpBinding, WSDualHttpBinding и NetMsmqBinding — как привязки Ин- тернета, так и интрасетевые привязки, в соответствии с требованиями данного сценария. Обратите внимание: использование привязок BasicHttpBinding, NetNa- medPipeBinding, NetPeerTcpBinding и WSFederationHttpBinding невозможно, так как эти привязки либо не поддерживают безопасность сообщений, либо не поддер- живают возможность отсутствия удостоверений (см. табл. 10.1 и 10.3). На- стройка привязок выполняется аналогично предыдущим сценариям. Единст- венное заметное различие — отказ от клиентских удостоверений при помощи режима MessageCredentialsType.None в случае WSHttpBinding: WSHttpBindlng binding = new WSHttpBinding(); binding.Security.Message.Cli entCredenti al Type = MessageCredenti al Type.None: Или с использованием конфигурационного файла: <bi ndi ngs> <wsHttpBinding> <binding name = "WSAnonymous"> <security mode = "Message">
522 Глава 10. Безопасность <message clientCredentia 1 Type = "None"/> </security> </binding> </wsHttpBinding> </bindings> Аутентификация Разумеется, никакая аутентификация в анонимном сценарии не выполняется. Для аутентификации службы по отношению к клиенту и для обеспечения без- опасности сообщений служба должна предоставить свой сертификат (см. лис- тинг 10.8). Авторизация Поскольку анонимные клиенты не проходят аутентификацию, авторизация и ролевая безопасность исключаются. Хост службы должен задать свойству PrincipalPermissionMode значение PrincipalPermissionMode.None, чтобы среда WCF установила объект GenericPrincipal с пустой личностью (вместо объекта Windows- Principal с пустой личностью). Управление личностями Если предположить использование PrincipalPermissionMode.None, личность, свя- занная с объектом субъекта безопасности, будет представлена объектом Generic- Identity с пустым именем пользователя. Эта личность считается неаутентифи- цированной. Первичная личность контекста безопасности вызовов совпадает с личностью субъекта безопасности. Напротив, в качестве личности Windows задается объект Windowsldentity с пустым именем пользователя, то есть неаутен- тифицированный. Личности и их значения представлены в табл. 10.7. Таблица 10.7. Управление личностями в анонимном сценарии Личность Тип Значение Аутентификация Субъект безопасности программного потока Genericidentity — Нет Первичная личность из контекста Genericidentity — Нет безопасности Личность Windows из контекста безопасности Windowsldentity — Нет Перевоплощение Из-за отсутствия предоставляемых удостоверений Windows служба не может перевоплощаться под личностью какого-либо из ее клиентов. Обратные вызовы Хотя вызовы, обращенные от клиента к службе, анонимны, служба раскрывает свою личность для клиента. Первичной личности в контексте безопасности вы-
Отсутствие безопасности 523 зова задается экземпляр X5O9Identity, в котором имя состоит из общего имени службы и хеш-кода. В качестве личности субъекта безопасности используется объект Windowsldentity с пустым именем пользователя, что исключает примене- ние авторизации и ролевой безопасности. Избегайте выполнения критичных операций при обратном вызове, потому что применение ролевой безопасности невозможно, а вызывающая сторона не аутентифицирована с точки зрения субъекта безопасности. Отсутствие безопасности В последнем сценарии приложение полностью отключает все защитные средст- ва. Служба не полагается на безопасность передачи, не проводит аутентифи- кацию и авторизацию клиентов. Разумеется, такая служба открыта для любых атак; отказ от безопасности обычно должен объясняться очень вескими при- чинами. Количество клиентов может быть любым. В режиме отсутствия безо- пасности могут настраиваться как интернет-службы, так и интрасетевые службы. Отключение безопасности привязок Для отключения безопасности выбирается режим безопасности передачи None. Тем самым вы также избегаете хранения каких-либо клиентских удостоверений в сообщении. Режим отсутствия безопасности поддерживается всеми привязка- ми (см. табл. 10.1), но с привязкой WSFederationHttpBinding нет ни единственного довода в пользу использования этого режима, так как единственной причиной для выбора этой привязки является необходимость в федеративной безопасности. Настройка привязок производится так же, как в предыдущих сценариях, если не считать выбора соответствующего режима безопасности; например, для привязки NetTcpBinding используется режим MessageCredentialType.None: NetTcpBinding binding = new NetTcpBIndlng(Secur1tyMode.None): Или с использованием конфигурационного файла: <b1nd1ngs> <netTcpB1nd1ng> <b1nd1ng name = "NoSecur1ty"> <secur1ty mode = "None,7> </bindlng> </netTcpB1nd1ng> </b1ndlngs> Аутентификация Разумеется, никакая аутентификация в этом сценарии не выполняется, и кли- ент не должен предоставлять никакие удостоверения посреднику. Служба так- же не аутентифицируется перед клиентом.
524 Глава 10. Безопасность Авторизация Поскольку клиенты являются анонимными и не проходят аутентификацию, ав- торизация и ролевая безопасность исключаются. Свойству PrincipalPermission- Mode автоматически задается значение PrincipalPermissionMode.None, чтобы среда WCF установила объект GenericPrincipal с пустой личностью. Управление личностями Личность, связанная с объектом субъекта безопасности, представлена объектом Genericidentity с пустым именем пользователя. Эта личность считается неаутен- тифицированной. В отличие от всех предыдущих сценариев, в этом сценарии контекст безопасности вызова отсутствует, а свойство ServiveSecurityContext.Cur- rent возвращает null. Личности и их значения представлены в табл. 10.8. Таблица 10.8. Управление личностями в сценарии «бизнес-бизнес» с провайдерами ролей ASP.NET Личность Тип Значение Аутентификация Субъект безопасности программного Genericidentity — Нет потока Первичная личность из контекста — — — безопасности Личность Windows из контекста ______ безопасности Перевоплощение Так как клиенты являются анонимными, перевоплощение службы невозможно. Обратные вызовы В отличие от всех предыдущих сценариев, при отсутствии безопасности переда- чи данных обратный вызов поступает с собственной личностью клиента. Лич- ности субъекта безопасности задается экземпляр Windowsldentity с пользова- тельским именем клиента. Обратный вызов аутентифицируется, но в перевопло- щении или ролевой безопасности нет смысла, потому что клиент будет авторизо- вать сам себя. Кроме того, контекст безопасности для обратных вызовов задает- ся равным null. Сводка сценариев Итак, мы рассмотрели все пять ключевых сценариев. В табл. 10.9 и 10.10 пред- ставлена сводка их ключевых элементов. В табл. 10.9 перечислены привязки, используемые в каждом сценарии. Напомню, что хотя с технической точки зре- ния практически в каждом сценарии могут использоваться и другие привязки, мой выбор привязок основан на контексте использования сценария.
Декларативная инфраструктура безопасности 525 Таблица 10.9. Привязки и сценарии безопасности Привязка Интрасеть Интернет Бизнес-бизнес Анонимный Режим None BasicHttpBinding Нет Нет Да Нет Да NetTcpBinding Да Да Нет Да Да NetPeerTcpBinding Нет Нет Нет Нет Да NetNamedPipeBinding Да Нет Нет Нет Да WSHttpBinding Нет Да Да Да Да WSFederationHttpBinding Нет Нет Нет Не Нет WSDualHttpBinding Нет Да Да Да Да NetMsmqBinding Да Нет Нет Да Да В табл. 10.10 показано, какое место в каждом из сценариев занимают аспек- ты безопасности, описанные в начале главы (безопасность передачи данных, аутен- тификация службы и клиента, авторизация, перевоплощение). Таблица 10.10. Аспекты сценариев безопасности Аспект Интрасет ь Интернет Бизнес-бизнес Анонимный । Режим None Безопасность транспорта Да Нет Нет Нет Нет Безопасность сообщений Нет Да Да Да Нет Аутентификация службы Windows Сертификат Сертификат Сертификат Нет Аутентификация клиента Windows ASP.NET Сертификат Нет Нет Авторизация Windows ASP.NET Нет/ASP.NET Нет Нет Перевоплощение Да Нет Нет Нет Нет Декларативная инфраструктура безопасности Безопасность в WCF — воистину огромная тема. Она содержит устрашающее количество подробностей, о которых следует помнить, а между ее различными компонентами существуют неочевидные связи. Программная модель чрезвы- чайно сложна, и на первый взгляд возникает ощущение, что вы вслепую блуж- даете по лабиринту. Ситуация усложняется еще и тем, что ошибки грозят серь- езными последствиями как на уровне приложения, так и на бизнес-уровне. По этой причине я разработал декларативную инфраструктуру безопасности для WCF. Для службы предоставляется атрибут безопасности (а также соответст- вующая поддержка на уровне хоста), а для клиента я разработал несколько вспомогательных классов и безопасный класс посредника. Моя декларативная инфраструктура сильно упрощает применение безопасности WCF, а также по- зволяет настраивать конфигурацию безопасности наравне с другими аспектами конфигурации WCF (такими, как транзакции или синхронизация). Я стремил- ся к тому, чтобы декларативная модель была простой в использовании и избав- ляла от необходимости изучать многие технические детали безопасности. Все, что потребуется от разработчика, — выбрать правильный сценарий (один из
526 Глава 10. Безопасность пяти стандартных сценариев, описанных в этой главе), а инфраструктура авто- матизирует процесс настройки конфигурации. Более того, инфраструктура ав- томатически применяет нужные режимы и следит за соблюдением рекоменда- ций. В то же время я хотел, чтобы модель сохраняла достаточный уровень детализации и обеспечивала возможность дополнительной настройки базовой конфигурации, если такая необходимость вдруг возникнет. SecurityBehaviorAttribute В листинге 10.19 приведены определения атрибута SecurityBehaviorAttribute и перечисления ServiceSecurity. Листинг 10.19. SecurityBehaviorAttribute public enum ServiceSecurity { None. Anonymous. BusinessToBusiness. Internet. Intranet } [Attri buteUsage(Attri buteTargets.Cl ass)] public class SecurityBehaviorAttribute : Attribute.IServiceBehavior ( public SecurityBehaviorAttribute(ServiceSecurity mode): public SecurityBehaviorAttribute(ServiceSecurity mode. string serviceCertificateName); public SecurityBehaviorAttribute(ServiceSecurity mode. StoreLocation storeLocation. StoreName storeName. X509FindType findType, string serviceCertificateName): public bool ImpersonateAl1 {get:set:} public string ApplicationName {get:set:} public bool UseAspNetProviders {get:set:} } При применении атрибута SecurityBehavior необходимо задать целевой сцена- рий в форме значения SecurityValue. Вы можете ограничиться конструкторами SecurityBehavior или задать значения свойств. Если свойства не заданы, им зада- ются значения по умолчанию, осмысленные в контексте выбранного сценария. Настройка конфигурации при выборе сценария буквально следует предшест- вующему описанию конкретных сценариев. Атрибут SecurityBehavior реализует композиционную модель безопасности с довольно большим количеством ком- бинаций и субсценариев. При использовании атрибута конфигурационный файл хоста может не содержать настроек безопасности, или же настройки из конфи- гурационного файла могут объединяться с настройками, определяемыми атри-
Декларативная инфраструктура безопасности 527 бутом. Аналогично, код хостинга может быть свободен от настроек безопасно- сти, или же программные настройки безопасности хоста могу объединяться с настройками атрибута. Настройка безопасности для интрасетевой службы Чтобы настроить службу для интрасетевого сценария, примените атрибут Secu- rityBehavior в режиме ServiceSecurity.Intranet: [ServiceContract] interface IMyContract f [OperationContract] void MyMethodO; } [SecurityBehavior(ServiceSecurity.Intranet)] class MyService : IMyContract public void MyMethodO j {...} Хотя контракт службы не ограничивает уровень защиты, атрибут на про- граммном уровне добавляет соответствующее требование, чтобы обеспечить за- щиту сообщений. Для реализации ролевой безопасности можно использовать группы Windows NT: [Securi tyBehavi or(Servi ceSecuri ty.Intranet)] class MyService ; IMyContract [Pri nci pal Permi ss ion (Securi tyAct ion. Demand .Role = P’^Domain^Customer") ] public void MyMethodO Настройка автоматического перевоплощения службы для всех вызывающих сторон всех методов осуществляется при помощи свойства ImpersonateAlL По умолчанию свойство ImpersonateAlL равно false, но если задать его равным true, атрибут использует автоматическое перевоплощение во всех операциях без применения атрибутов поведения операций или конфигурации хоста: [Securi tyBehav i or(Servi ceSecuri ty.Intranet.ImpersonateAl1 = true)] class MyService : IMyContract {...} Настройка безопасности для интернет-службы Для интернет-служб необходимо выбрать как соответствующий сценарий, так и используемый сертификат службы. В листинге 10.19 обратите внимание на то, что конструктор атрибута ServiceBehavior может получать имя сертификата службы. Если имя не задано, сертификат службы загружается из конфигураци- онного файла хоста, как в листинге 10.8: [Securi tyBehavior(Servi ceSecurity.Internet)] class MyService : IMyContract
528 Глава 10. Безопасность Если указать имя сертификата службы, сертификат загружается из хранили- ща LocalMachine в папке Му: [SecurityBehavior(ServiceSecurity.Internet,’’MyServiceCert")] class MyService : IMyContract Если в качестве имени сертификата указана пустая строка, атрибут строит имя сертификата по имени хостового компьютера (или домена) и загружает его из хранилища LocalMachine в папке Му: [Securi tyBehavi or(ServiceSecurity.Internet."")] class MyService : IMyContract {•••} Наконец, атрибут позволяет явно задать местонахожение хранилища, его имя и метод поиска: [Securi tyBehavior(ServiceSecurity.Internet. StoreLocati on.LocalMachi ne,StoreName.My, X509F1 ndType. Fi ndBySubjectName, ’’MyServiceCert") ] class MyService : IMyContract {•••} Явное задание местоположения может сочетаться с автоматическим вычис- лением имени сертификата: [SecurityBehavior(Servicesecurity.Internet, StoreLocation.LocalMachine,StoreName.My. X509F i ndType. F i ndBySub jectName. ’’ ”) ] class MyService : IMyContract {• •} Хранилище удостоверений, по которому аутентифицируется клиент, задает- ся свойством UseAspNetProviders. По умолчанию свойство равно false; это означа- ет, что имя пользователя и пароль клиента будут проверяться как удостовере- ния Windows, как в листинге 10.11. По этой причине при ложном свойстве UseAspNetProviders можно по умолчанию выполнять авторизацию с использова- нием групп Windows NT: [Securi tyBehavi or(Servi ceSecuri ty.Internet,"MyServiceCert")] class MyService : IMyContract { [Pri nci palPermi ssi on(Securi tyActi on.Demand.Role = @"<Domain>\Customer")] public void MyMethodO {...} } и даже выполнять перевоплощение для всех вызывающих сторон: [SecurityBehavior(ServiceSecurity.Internet."MyServiceCert".ImpersonateAl! - true)] class MyService : IMyContract Если свойство UseAspNetProviders равно true, вместо удостоверений Windows атрибут будет использовать провайдеров роли и принадлежности ASP.NET, как описано в интернет-сценарии: [SecurityBehavior(ServiceSecurity.Internet."MyServiceCert", UseAspNetProviders - true)] class MyService : IMyContract
Декларативная инфраструктура безопасности 529 [Prind palPermission(SecurityAction.Demand,Role = "Manager")] public void MyMethodO } (•} Атрибут позволяет использовать привязку NetTcpBinding в режиме Service- Security.Internet с провайдерами ASP.NET, чтобы избежать использования в ин- трасетевых приложениях учетных записей и групп Windows (см. ранее). Следующий вопрос — передача имени приложения для провайдеров ASP. NET. Задача решается свойством ApplicationName. Если оно не задано, атрибут ищет имя приложения в конфигурационном файле, как показано в листин- гах 10.13 и 10.14. Если значение не найдено в конфигурационном файле хоста, атрибут не инициализируется бессмысленным значением по умолчанию / из machine.config. Вместо этого в качестве имени приложения используется имя сборки хоста. Если свойству ApplicationName задано значение, оно заменяет имя приложения в конфигурационном файле хоста: [Securi tyBehavi or(Servi ceSecuri ty.Internet."MyServiceCert", UseAspNetProviders = true, ApplicationName = ’’MyApplication’’^ class MyService : IMyContract {•••} Настройка безопасности для служб «бизнес-бизнес» Для настройки сценария «бизнес-бизнес» свойство ServiceSecurity задается рав- ным ServiceSecurity.BusinessToBusiness. Атрибут использует механизм доверия равноправных узлов для проверки клиентского сертификата. Сертификат службы настраивается так же, как и в сценарии ServiceSecurity.Internet. Пример: [Securi tyBehavior(Servi ceSecuri ty.Busi nessToBusi ness)] class MyService : IMyContract (•••} [Securi tyBehavi or(Servi ceSecuri ty.Busi nessToBusi ness."")] class MyService : IMyContract {••} [SecurityBehavior(ServiceSecurity.BusinessToBusiness."MyServiceCert")] class MyService : IMyContract {••} По умолчанию в режиме ServiceSecurity.BusinessToBusiness атрибут задает свой- ству хоста PrincipalPermissionMode значение PrincipalPermissionMode.None, и служ- ба не сможет проводить авторизацию своих вызывающих сторон. Но истинное значение UseAspNetProviders включает использование ролевых провайдеров ASP.NET, как в листинге 10.18: [Securi tyBehavior(ServiceSecuri ty.Busi nessToBusi ness. UseAspNetProviders = true)] class MyService : IMyContract {...} При использовании провайдеров ролей ASP.NET поиск и выбор имени при- ложения осуществляется так же, как в сценарии ServiceSecurity.Internet:
530 Глава 10. Безопасность [SecurityBehavior(ServiceSecurity.BusinessToBusiness."MyServiceCert", UseAspNetProviders s true. ApplicationName = "MyApplication")] class MyService : IMyContract {...} Настройка безопасности для анонимных служб Чтобы разрешить обслуживание вызовов по анонимному сценарию, необходи- мо настроить атрибут в режиме ServiceSecurity. Настройка сертификата службы осуществляется так же, как в сценарии ServiceSecurity.Internet. Пример: [SecurityBehavior(ServiceSecurity.Anonymous)] class MyService : IMyContract {•} [Securi tyBehavior(Servi ceSecuri ty.Anonymous class MyService : IMyContract [SecurityBehavi or(Servi ceSecuri ty.Anonymous."MyServi ceCert")] class MyService : IMyContract {...} Настройка конфигурации службы с отключенной безопасностью Для полного отключения безопасности передайте атрибуту значение Service- Security.None: [Securi tyBehavi or(Servi ceSecuri ty.None)] class MyService : IMyContract Реализация SecurityBehaviorAttribute В листинге 10.20 приведена часть реализации SecurityBehaviorAttribute. Листинг 10.20. Реализация SecurityBehaviorAttribute [AttrlbuteUsage(AttrlbuteTargets.Cl ass)] class SecurityBehaviorAttribute : Attribute.IServiceBehavior { SecurityBehavior m_Securi tyBehavi or; string m_ApplicationName = String.Empty; bool mJJseAspNetProviders; bool m_ImpersonateAll; public SecurityBehaviorAttribute(ServiceSecurity mode) { m_SecurityBehavior = new SecurityBehavior(mode); } public SecurityBehaviorAttribute(ServiceSecurity mode. string serviceCertificateName) ( m_SecurityBehavior = new SecurityBehavior(mode, serviceCertificateName):
Декларативная инфраструктура безопасности 531 public bool ImpersonateAl1 //Обращение к m_ImpersonateAl1 (get;set;} public string ApplicationName //Обращение к m_ApplIcationName {get;set;} public bool UseAspNetProviders //Обращение к mJJseAspNetProviders {get;set;} void IServ1ceBehav1or.ApplyD1spatchBehavior(...) {} vold IServiceBehav1 or.AddBIndingParameters(ServiceDescrlpt1 on descrlpt1 on, ServiceHostBase ServiceHostBase, Col 1ect1on<Serv1ceEndpoint> endpoints, BlndingParameterCol1ect1 on parameters) mSecur i tyBehavi or. AddBI ndi ngParameters (description, servi ceHostBase, endpoints,parameters); } void IServiceBehavior.Vai1date(ServiceDescription description, ServiceHostBase ServiceHostBase) m_Secur1tyBehavior.UseAspNetProviders = UseAspNetProviders; m_Secur1tyBehavior.ApplIcationName = ApplicationName; m_Secur1tyBehavior.ImpersonateAl1 = ImpersonateAl1; m_SecurityBehavior.Validate(description,ServiceHostBase); } //... Три открытых свойства атрибута соответствуют трем приватным полям дан- ных, в которых атрибут сохраняет заданные параметры конфигурации. SecurityBehaviorAttribute — атрибут поведения службы, поэтому он может применяться напрямую к классу службы. При вызове метода AddBindingPara- meters() интерфейса IServiceBehavior SecurityBehaviorAttribute создает конфигура- цию сценария, соответствующую запрашиваемому сценарию. SecurityBehavior- Attribute настраивает хост в методе Validate() интерфейса IServiceBehavior. В остальном функции атрибута практически ограничиваются определением об- щего порядка конфигурации. Фактическая настройка конфигурации произво- дится с использованием вспомогательного класса SecurityBehavior. Атрибут кон- струирует экземпляр SecurityBehavior, передавая ему сценарий (параметр mode) и имя сертификата в подходящем конструкторе. SecurityBehavior обеспечивает систематическую настройку всех сценариев безопасности с использованием программных вызовов. SecurityBehavior инкапсулирует все указанные дейст- вия, описанные ранее для каждого сценария. Класс SecurityBehavior сам по себе представляет аспект поведения службы и спроектирован в расчете на автоном- ное использование независимо от атрибута. В листинге 10.21 представлена часть реализации класса SecurityBehavior, де- монстрирующая основные моменты его поведения. Листинг 10.21. Реализация SecurityBehavior (неполная) :lass SecurityBehavior : IServiceBehavior г I Servicesecurity m_Mode; продолжение &
532 Глава 10. Безопасность StoreLocation m_StoreLocation; StoreName m_StoreName; X509F1ndType m_F1ndType: string m_SubjectName; bool m_UseAspNetProv1ders; string m_ApplIcatlonName = String.Empty; bool m_ImpersonateAl 1; public Secur1tyBehav1or(Serv1ceSecur1ty mode) : th1s(mode.StoreLocation.Local Maehlne.StoreName.My, X509F1ndType.F1ndBySubjectName.null) {} public SecurityBehavior(ServiceSecurity mode.StoreLocation storeLocation. StoreName storeName.X509F1ndType flndType.string subjectName) {...} // Задает соответствующие поля данных public bool ImpersonateAl1 //Обращение к m_ImpersonateAl1 {get;set;} public bool UseAspNetProviders //Обращение к UseAspNetProviders {get;set;} public string ApplicationName //Обращение к m_ApplIcatlonName {get;set;} public void Val1date(Serv1ceDescr1pt1on description. ServiceHostBase ServiceHostBase) { if(m_SubjectName != null) { switch(m_Mode) { case ServiceSecurity.Anonymous: case ServiceSecurity.BusinessToBusiness: case ServiceSecurity.Internet: { string subjectName; 1f(m_SubjectName .'= String.Empty) { subjectName = m_SubjectName; ) else { subjectName = description.Endpoints[0].Address.Uri.Host: } ServiceHostBase.Credentials.ServiceCert1 fl cate. SetCert1 flcate(m_StoreLocat1on.m_StoreName. m_F1ndType.subjectName): break; } } } } public void AddBindingParameterstServiceDescription description. ServiceHostBase ServiceHostBase.
Декларативная инфраструктура безопасности 533 Col 1ection<ServiceEndpoint> endpolnts. BindingParameterCol1ection parameters) switch(m_Mode) { case ServiceSecurity.Intranet: { Configurelntranet(endpoints); break: } case ServiceSecurity.Internet: { Confi gureInternet(endpoi nts,UseAspNetProvi ders): break: } } ) internal static void ConfigureInternet(Collection<ServiceEndpoint> endpoints) { foreach(ServiceEndpoint endpoint in endpoints) { Binding binding = endpoint.Binding: if(binding is WSHttpBinding) { WSHttpBinding wsBinding = (WSHttpBinding)binding: wsBinding.Security.Mode = SecurityMode.Message; wsBi ndi ng.Securi ty.Message.ClientCredenti al Type = MessageCredent!al Type.UserName; continue: } throw new InvalidOperationException(binding.GetType()+ "is unsupported with ServiceSecurity.Internet"): } } //... Конструкторы SecurityBehavior сохраняют в переменных класса параметры конструирования (такие, как режим безопасности и информация о сертифика- те). Метод Validate() содержит дерево принятия решений, настраивающее хост в соответствии со сценарием и предоставленной информацией, для поддержки описанного ранее поведения SecurityBehaviorAttribute. Метод AddBindingParame- ters() вызывает специализированный вспомогательный метод каждого сценария для настройки конечных точек, предоставляемых хостом. Каждый вспомога- тельный метод (такой, как Configurelnternet()) перебирает коллекцию конечных
534 Глава 10. Безопасность точек службы. Для каждой конечной точки метод проверяет, соответствует ли сценарию используемая привязка, а затем настраивает привязку в соответствии со сценарием. Хост и декларативная безопасность Декларативная безопасность с использованием атрибута SecurityBehavior проста и удобна, однако на практике настройка безопасности часто поручается хосту, а служба просто концентрируется на бизнес-логике. Кроме того, может возник- нуть необходимость в хостинге служб, написанных другим разработчиком, в ко- торых не используется мой механизм декларативной безопасности. Следующий естественный шаг — включение декларативной безопасности в ServiceHost<T> с использованием методов SetSecurityBehavior(): public class ServiceHost<T> : ServiceHost { public void SetSecurltyBehavlor(ServiceSecurity mode, bool UseAspNetProviders, string applicationName, bool ImpersonateAl1); public void SetSecurltyBehavlor(ServiceSecurity mode. string serviceCertificateName, bool UseAspNetProviders, string applicationName, bool ImpersonateAl1): //... } Использование декларативной безопасности на уровне хоста следует тем же рекомендациям, которые были приведены ранее для атрибута SecurityBehavior. Например, вот как происходит настройка хоста (и службы) для интернет-без- опасности с провайдерами ASP.NET: ServiceHost<MyServ1ce> host = new ServiceHost<MyServ1ce>(): host.SetSecurltyBehavlor(ServiceSecurity.Internet, "MyServiceCert".true,"MyApplication".false); host.OpenO; В листинге 10.22 приведена часть реализации поддержки декларативной безопасности в классе ServiceHost<T>. Листинг 10.22. Поддержка декларативной безопасности в ServiceHost<T> public class ServiceHost<T> : ServiceHost { public void SetSecurltyBehavlor(ServiceSecurity mode, string serviceCertificateName, bool UseAspNetProviders, string applicationName, bool ImpersonateAl1) { if(State == CommunicationState.Opened) { throw new InvalidOperationExceptionCHost is already opened");
Декларативная инфраструктура безопасности 535 SecurityBehavior SecurityBehavior = new Securi tyBehavi or(mode.serviceCerti fi cateName); SecurityBehavior.UseAspNetProviders = UseAspNetProviders; securityBehavior.ApplicationName = applicationName; SecurityBehavior.ImpersonateAl1 = ImpersonateAl1; Description.Behaviors.Add(securityBehavior); } //... } Реализация SetSecurityBehavior() основана на том факте, что красе Security- Behavior поддерживает IServiceBehavior. SetSecurityBehavior() инициализирует эк- земпляр SecurityBehavior переданными параметрами, а затем добавляет их в кол- лекцию аспектов поведения в описании службы, как если бы служба была помечена атрибутом SecurityBehaviorAttribute. Декларативная безопасность на стороне клиента WCF не позволяет применять атрибут к классу посредника, и хотя атрибуты уровня контракта возможны, вероятно, клиенту потребуется передать свои удо- стоверения и другие настройки во время выполнения. Поддержка декларатив- ной безопасности на стороне клиента начинается со вспомогательного класса SecurityHelper, определение которого приведено в листинге 10.23. Листинг 10.23. Вспомогательный класс SecurityHelper public static class SecurityHelper { public static void UnsecuredProxy<T>(ClientBase<T> proxy) where T : class; public static void AnonymousProxy<T>(ClientBase<T> proxy) where T : class; public static void SecureProxy<T>(ClientBase<T> proxy. string userName.string password) where T : class; public static void SecureProxy<T>(ClientBase<T> proxy. string domain.string userName.string password) where T : class; public static void SecureProxy<T>(ClientBase<T> proxy.string domain. string userName.string password. TokenlmpersonationLevel impersonationLevel) where T ; class; public static void SecureProxy<T>(ClientBase<T> proxy. string clientCertificateName) where T : class; public static void SecureProxy<T>(ClientBase<T> proxy. StoreLocation storeLocation.StoreName storeName. X509FindType findType. string clientCertificateName) where T ; class; //... } Класс SecurityHelper используется для настройки посредника в соответствии с желаемым сценарием безопасности и поведением при помощи специализиро- ванных статических методов класса SecurityHelper. Настройка посредника мо- жет выполняться только до его открытия. В настройки параметров безопасно- сти в конфигурационном файле клиента или в клиентском коде нет необходи- мости.
536 Глава 10. Безопасность Класс SecurityHelper достаточно «умен»; он выбирает правильное поведение безопасности на основании переданных параметров и вызываемого метода. Явно использовать перечисление ServiceSecurity не понадобится. Например, вот как осуществляется защита посредника в интрасетевом сце- нарии и передача ему удостоверений Windows клиента: MyContractCllent proxy = new MyContractCllentt); SecurItyHelper.SecureProxy(proxy."MyDomaln"."MyUsername"."MyPassword"): proxy.MyMethod(); proxy.Closet): В интернет-сценарии клиент должен предоставить только имя пользователя и пароль (помните, что решение о том, являются ли эти данные удостоверения- ми Windows или ASP.NET, принимается на стороне службы): MyContractCllent proxy = new MyContractCllentt): SecurityHelper.SecureProxytproxy."MyUsername"."MyPassword"); proxy.MyMethodt); proxy.Closet): Для сценария «бизнес-бизнес» клиент может передать вместо имени клиент- ского сертификата null или пустую строку, если он собирается использовать сертификат из своего конфигурационного файла, или же указать имя сертифи- ката явно: MyContractCllent proxy = new MyContractCllentt): SecurItyHelper.SecureProxytproxy."MyCl1entCert"); proxy.MyMethodt): proxy.Closet); SecurityHelper загружает сертификат по имени из клиентского хранилища LocalMachine папки Му. Клиент также может задать информацию, необходимую для поиска и загрузки сертификата. При использовании привязки BasicHttp- Binding в сценарии «бизнес-бизнес» клиент должен явно задать местонахожде- ние сертификата службы, в конфигурационном файле или на программном уровне (это было сделано ради простоты проектирования SecurityHelper). Дня анонимных клиентов используется метод AnonymousProxy(): MyContractCllent proxy = new MyContractCllentt); SecurityHelper.AnonymousProxy(proxy); proxy.MyMethodt): proxy.Closet): При полном отсутствии безопасности используется метод UnsecuredProxy(): MyContractCllent proxy = new MyContractCllentt); SecurityHelper.UnsecuredProxy(proxy); proxy.MyMethodt); proxy.Closet): Реализация SecurityHelper Внутренняя реализация SecurityHelper использует класс SecurityBehavior для на- стройки конечной точки посредника, а также задания удостоверений, как пока- зано в листинге 10.24.
Декларативная инфраструктура безопасности 537 Листинг 10.24. Реализация SecurityHelper (неполная) public static class SecurityHelper { public static void SecureProxy<T>(ClientBase<T> proxy, string userName.string password) where T : class { if(proxy.State == CommunicationState.Opened) { throw new InvalidOperationException( "Proxy channel is already opened"); } Col 1ection<ServiceEndpoint> endpoints = new Col 1ection<ServiceEndpoint>(): endpoints.Add(proxy.Endpoint); SecurityBehavior.Confi gurelnternet(endpoi nts); proxy.Clientcredentials.UserName.UserName = userName: proxy.ClientCredentials.UserName.Password = password: proxy.Cli entCredenti a 1s.Servi ceCerti fi cate.Authentication. Certi fi cateVa1i dati onMode = X509Certi fi cateVa1i dati onMode.PeerT rust; } II... ) Класс SecureClientBase<T> К преимуществам класса SecurityHelper относится то, что он может работать с любым посредником — даже с тем, за создание которого разработчик клиента не отвечает. Недостаток заключается в том, что этот шаг должен быть выполнен клиентом. Если вы отвечаете за построение посредника, используйте мой класс SecureClientBase<T>, определенный в листинге 10.25. Листинг 10.25. Класс SecureClientBase<T> public abstract class SecuredientBase<T> : ClientBase<T> where T : class { //Конструкторы для конечной точки по умолчанию protected SecuredientBase(): protected SecuredientBase(ServiceSecurity mode): protected SecuredientBase(string userName,string password): protected SecuredientBase(string domain,string userName,string password. TokenlmpersonationLevel impersonationLevel): protected SecuredientBase(string domain,string userName, string password): protected SecuredientBase(string clientCertificateName): protected SecuredientBase(Storel_ocation storeLocation, StoreName storeName.X509FindType findType, string clientCertificateName): //...Конструкторы для других типов конечных точек... } Класс SecureClientBase<T>, производный от обычного CLientBase<T>, добавляет поддержку декларативной безопасности. Вы должны объявить посредника про- изводным от SecureClientBase<T> вместо ClientBase<T>, предоставить конструкторы
538 Глава 10. Безопасность для своего сценария безопасности и вызвать базовые конструкторы Secure- ClientBase<T> с полученной информацией об удостоверениях и конечных точках: class MyContractClient : SecuredientBase<IMyContract>,IMyContract { public MyContractClient(ServiceSecurity mode) : base(mode) {} public MyContractClient(string userName,string password) : base(userName.password) {} ’ /* Другие конструкторы */ public void MyMethodO { Channel .MyMethodO; } } В работе с производным посредником нет ничего сложного. Пример для интернет-сценария: MyContractClient proxy = new MyContractClientCMyUsername","MyPassword"): proxy.MyMethod(): proxy.Close(): или для анонимного сценария: MyContractClient proxy = new MyContractClient(Servicesecurity.Anonymous): proxy. MyMethodO: proxy.CloseO; Реализация SecureClientBase<T> просто использует SecurityHelper, как показа- но в листинге 10.26, так что SecureClientBase<T> следует тем же правилам поведе- ния — в частности в том, что касается клиентского сертификата. Листинг 10.26. Реализация SecureClientBase<T> (неполный) public class SecuredientBase<T> : ClientBase<T> where T : class { protected SecuredientBase(ServiceSecurity mode) { switch(mode) { case ServiceSecurity.None: { Securi tyHelper.UnsecuredProxy(thi s); break; } case Servicesecurity.Anonymous: { Securi tyHelper.AnonymousProxy(thi s); break; ) } } protected Secured ientBaset string userName.string password) { Securi tyHelper.SecureProxy(thi s.userName.password):
Декларативная инфраструктура безопасности 539 } //... } Безопасная версия ChannelFactory Если посредник вообще не используется, то от классов SecurityHelper и Secure- ClientBase<T> особой пользы не будет. Для подобных ситуаций я написал класс SecureChannelFactory<T>, определение которого представлено в листинге 10.27. Листинг 10.27. SecureChannelFactory<T> public class SecureChannelFactory<T> : ChannelFactory<T> { public SecureChannelFactory() {} public SecureChannelFactory(string endpointName) : base(endpointName) {} //... public void SetSecurityMode(ServiceSecurity mode); public void SetCredentials(string userName.string password); public void SetCredentials(string domain.string userName.string password. TokenlmpersonationLevel impersonationLevel); public void SetCredentials(string domain.string userName. string password); public void SetCredentials(string clientCertificateName); public void SetCredentials(StoreLocation storeLocation. StoreName storeName. X509F1ndType findType,string clientCertificateName); } SecureChannelFactory<T> используется точно так же, как класс WCF Channel- Factory, за исключением применения декларативной безопасности. Перед от- крытием канала необходимо вызвать метод SetSecurityMode() или один из мето- дов SetCredentials(), подходящих для целевого сценария. Например, с посредни- ком для службы, основанной на безопасности в Интернете: SecureChannelFactory<IMyContract> factory = new SecureChannelFactory<IMyContract>(""): factory.SetCredenti als("MyUsername","MyPassword"); IMyContract proxy = factory.CreateChannel0; using(proxy as IDisposable) { proxy.MyMethodt); } Реализация SecureChannelFactory<T> очень похожа на реализацию SecurityHelper, поэтому я опускаю подробные описания. Дуплексный клиент и декларативная безопасность Я также разработал класс SecureDuplexClientBase<T,C> (аналог SecureClientBase<T>), определение которого приведено в листинге 10.28.
540 Глава 10. Безопасность Листинг 10.28. Класс SecureDuplexClientBase<T> public abstract class SecureDuplexC11entBase<T.C> : DuplexCl1entBase<T.C> where T : class { protected SecureDuplexC11entBase(C callback); protected SecureDuplexC11entBase(ServiceSecurity mode.C callback); protected SecureDuplexC11entBase(string userName.string password. C callback); protected SecureDuplexC11entBase(string domain.string userName. string password. TokenlmpersonatlonLevel ImpersonatlonLevel,C callback); protected SecureDuplexC11entBase(string domain.string userName. string password. C callback); protected SecureDuplexC11entBase(string clIentCertificateName. C callback); protected SecureDuplexC11entBase(StoreLocat1on storeLocation. StoreName storeName, X509F1ndType flndType. string clIentCertlficateName.C callback); /* Дополнительные конструкторы */ } SecureDuplexClientBase<T,C> является производным от моего класса DupLexCli- entBase<T,C>, представленного в главе 5, и добавляет к нему поддержку деклара- тивной сценарной безопасности. Как и в случае с классом DuplexClientBase<T,C>, следует объявить класс посредника производным от него и использовать либо параметр обратного вызова, либо типизованный контекст InstanceContext<C>. Например, для следующего контракта службы и определения контракта обрат- ного вызова: [ServiceContract(Cal 1backContract = typeof(IMyContractCallback))] Interface IMyContract { [Operationcontract] void MyMethodO; } Interface IMyContractCallback { [Operationcontract] void OnCallbackO; } производный класс посредника будет выглядеть так: class MyContractClient : SecureDuplexCi 1entBase<IMyContract.IMyContractCal1back>.IMyContract { public MyContractCl1ent(IMyContractCalIback callback) ; base(calIback) {} public MyContractCllent(ServiceSecurity mode. IMyContractCallback callback) : base(mode.calIback) {} /* Дополнительные конструкторы */ public void MyMethodO { Channel.MyMethod();
Декларативная инфраструктура безопасности 541 При использовании ему передается сценарий безопасности или удостовере- ния, объект обратного вызова и информация конечной точки. Пример исполь- зования для анонимного сценария: class MyClient : IMyContractCallback {•••} IMyContractCallback callback = new MyClientO; MyContractClient proxy = new MyContractClient(Servicesecurity.Anonymous.calIback); proxy.MyMethocK); proxy.Close(): Реализация SecureDuplexClientBase<T,C> почти идентична SecureClientBase<T>; главным различием является использование другого базового класса. Класс SecureDuplexChannelFactory<T,C> Если для настройки двусторонних коммуникаций не используется посредник, производный от SecureDuplexClientBase<T,C>, вы можете воспользоваться моим классом SecureDuplexChannelFactory<T,C>, определение которого приведено в лис- тинге 10.29. Листинг 10.29. SecureDuplexChannelFactory<T,C> public class SecureDuplexChannelFactory<T.C> : DuplexChannelFactory<T.C> where T : class { public SecureDuplexChannelFactory(C callback) public SecureDuplexChannel Factory(Instancecontext<C> context. Binding binding) : base(context.binding) //... public void SetSecurityMode(Servicesecurity mode): public void Setcredentials(string userName.string password): public void Setcredentials(string domain.string userName,string password, TokenlmpersonationLevel impersonationLevel): public void SetCredentials(string domain.string userName, string password): public void SetCredentials(string clientCertificateName): public void SetCredentials(StoreLocation storeLocation. StoreName storeName, X509FindType findType. string clientCertificateName): } SecureDuplexChannelFactory<T,C> является производным от параметризованно- го класса DuplexChannelFactory<T,C>, описанного в главе 5. SecureDuplexChannel- Factory<T,C> конструируется с экземпляром объекта обратного вызова (или эк- земпляром InstanceContext<C>). Перед открытием канала необходимо вызвать метод SetSecurityMethod() или один из методов SetCredentials() для нужного сце- нария. Пример для интернет-сценария: class MyClient : IMyContractCallback {...} IMyContractCallback callback = new MyClientO;
542 Глава 10. Безопасность SecureDuplexChannelFactory<IMyContract,IMyContractCallback> factory = new SecureDuplexChannelFactory<IMyContract.IMyContractCal1back>(cal1 back,; factory.SetCredentials("MyUsername","MyPassword"); IMyContract proxy = factory.CreateChannel0; usina(proxy as IDisposable) { proxy.MyMethod(); } Реализация SecureDuplexChannelFactory<T,C> почти идентична SecureChanel- Factory<T>; главным различием является использование другого базового класса. Контроль средств безопасности В завершение этой главы я представлю полезную функцию, поддерживаемую в WCF — так называемый журнал контроля безопасности (security audit). Как подсказывает название, WCF регистрирует в журнале попытки аутентифика- ции и авторизации, их название и местоположение, а также личность клиента. Контролем безопасности управляет класс ServiceSecurityAuditBehavior, представ- ленный в листинге 10.30 наряду со вспомогательными перечислениями. Листинг 10.30. Класс ServiceSecurityAuditBehavior public enum AuditLogLocation { Default. // Решение принимается операционной системой Application. Security } public enum AuditLevel { None, Success. Failure. SuccessOrFailure } public sealed class ServiceSecurityAuditBehavior : IServiceBehavior { public AuditLogLocation AuditLogLocation {get:set:} public AuditLevel MessageAuthenticationAuditLevel {get:set:} public AuditLevel ServiceAuthorizationAuditLevel (get:set:} //... } ServiceSecurityAuditBehavior является аспектом поведения службы. Свойство AuditLocation указывает, где должны сохраняться записи журнала — в журнале приложения или в журнеле безопасности (оба журнала находятся в журнале
Контроль средств безопасности 543 событий хостового компьютера). Свойство MessageAuthenticationAuditLevel управ- ляет детализацией контроля аутентификации. Регистрироваться могут как ус- пешные и неудачные попытки, так и только неудачные (в целях повышения производительности). Успешные попытки аутентификации могут регистриро- ваться в диагностических целях. По умолчанию свойство MessageAuthentication- AuditLevel равно AuditLeveLNone. Аналогичное свойство ServiceAuthorizationAudit- Level управляет детализацией контроля попыток авторизации; по умолчанию оно тоже отключено. Настройка контроля средств безопасности Контроль средств безопасности обычно включается в конфигурационном фай- ле хоста; для этого в него включается пользовательская секция behavior, ссылка на которую размещается в объявлении службы, как показано в листинге 10.31. Листинг 10.31. Настройка контроля средств безопасности <system.servi ceModel> <services> <service name = "MyService” behaviorconfiguration = "MySecurityAudit" > </service> </services> <behaviors> <serviceBehaviors> <behavior name = "MySecurityAudit"> <servi ceSecuri tyAudi t auditLogLocation = "Default" serviceAuthorizationAuditLevel = "SuccessOrFailure" messageAuthenticationAuditLevel = "SuccessOrFailure" /> </behavior> </serviceBehaviors> </behaviors> </system.servi ceModel > Контроль средств безопасности также может осуществляться на программ- ном уровне; для этого соответствующий аспект поведения добавляется к хосту на стадии выполнения перед его открытием. По аналогии с программным до- бавлением других аспектов поведения, рекомендуется проверить, не содержит ли хост соответствующее поведение, чтобы избежать переопределения настроек из конфигурационного файла, как показано в листинге 10.32. Листинг 10.32. Включение контроля средств безопасности на программном уровне ServiceHost host = new ServiceHost(typeof(MyService)): ServiceSecurityAuditBehavior securityAudit = host.Descri pti on.Behaviors.F i nd<Servi ceSecurityAudi tBehavi or>(): if(securityAudit -- null) securityAudit = new ServiceSecurityAuditBehaviorO; securi tyAudi t .MessageAuthenti cat i onAuditLevel = продолжение &
544 Глава 10. Безопасность AuditLevel.SuccessOrFailure: securi tyAudi t.ServiceAuthorizati onAuditLevel = AuditLevel.SuccessOrFailure; host.Descript1 on.Behaviors.Add(securityAudi t): } host.Open0: Для упрощения кода в листинге 10.32 можно добавить свойство Security- AuditEnabled в класс ServiceHost<T>: public class ServiceHost<T> : ServiceHost { public bool SecurityAuditEnabled {get:set:} //... } При использовании ServiceHost<T> листинг 10.32 сокращается до следующего фрагмента: ServiceHost<MyServ1ce> host = new ServiceHost<MyService>(): host.SecurityAuditEnabled = true: host.Open(): В листинге 10.33 приведена реализация свойства SecurityAuditEnabled. Листинг 10.33. Реализация свойства SecurityAuditEnabled public class ServiceHost<T> : ServiceHost { public bool SecurityAuditEnabled { get { ServiceSecurityAuditBehavior securityAudit = Description.Behaviors.Find<ServiceSecurityAuditBehavior>( ): 1f(securityAudit != null) return securityAudit.MessageAuthenticationAuditLevel == AuditLevel.SuccessOrFailure && securi tyAudi t.Servi ceAuthori zati onAudi tLevel == AuditLevel.SuccessOrFailure: } else { return false: } } set { If(State == CommunicationState.Opened) { throw new InvalidOperationExceptionCHost is already opened’’): } ServiceSecurityAuditBehavior securityAudit = Description.Behaviors.Find<ServiceSecurityAuditBehavior>( ): if(securityAudit == null && value == true)
Контроль средств безопасности 545 { securityAudit = new ServiceSecurityAuditBehavior( ): securi tyAudi t.MessageAuthenti cati onAudi tLevel = AuditLevel.SuccessOrFailure: securityAudi t.ServiceAuthori zati onAudi tLevel = AuditLevel.SuccessOrFailure: Description.Behaviors.Add(securi tyAudit): } } } //... В методе get свойство SecurityAuditEnabled обращается к описанию службы и ищет экземпляр ServiceSecurityAuditBehavior. Если поиск окажется успешным и если уровни контроля аутентификации и авторизации заданы равными Audit- Level.SuccessOrFailure, то SecurityAuditEnabled возвращает true; в противном случае возвращаеся false. В методе set свойство включает контроль средств безопасно- сти только тогда, когда описание не содержит предыдущего значения, заданно- го в конфигурационном файле. Если предыдущий аспект поведения не найден, SecurityAuditEnabled задает уровни контроля аутентификации и авторизации AuditLevel.SuccessOrFailure. Декларативный контроль безопасности Также можно написать атрибут, предоставляющий доступ к параметрам кон- троля средств безопасности на уровне службы. Я решил включить эту поддерж- ку в форме логического свойства атрибута SecurityBehavior с именем Security- AuditEnabled: [Attri buteUsage(Att г ibuteTargets.Class)] public class SecurityBehaviorAttribute : Attribute.IServiceBehavior ( public bool SecurityAuditEnabled {get;set:} //... } По умолчанию свойство SecurityAuditEnabled равно false, то есть контроль средств безопасности отсутствует. Свойство дополняет остальные компоненты модели декларативной безопасности, например: [SecurityBehavior(ServiceSecurity.Internet.UseAspNetProviders = true. SecurityAuditEnabled s true)] class MyService : IMyContract {...} В листинге 10.34 показано, как эта поддержка была включена в атрибут SecurityBehavior. Листинг 10.34. Реализация декларативного контроля средств безопасности [Attri buteUsage(AttributeTargets.Class)] public class SecurityBehaviorAttribute : Attribute.IServiceBehavior ( продолжение &
546 Глава 10. Безопасность bool m_SecurityAuditEnabled; public bool SecurityAuditEnabled //Обращение к m_SecurityAuditEnabled {get;set;} void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase ServiceHostBase) { i f(Securi tyAudi tEnabled) { ServiceSecurityAuditBehavior securityAudit = ServiceHostBase.Descri pti on. Behaviors.Find<ServiceSecurityAuditBehavior>( ); if(securityAudit == null) { securityAudit = new ServiceSecurityAuditBehavior( ); securi tyAudi t.MessageAuthenti cati onAudi tLevel = AuditLevel.SuccessOrFailure; securityAudit.ServiceAuthorizationAuditLevel = AuditLevel.SuccessOrFailure; ServiceHostBase.Descri pti on.Behavi ors.Add(securityAudi t); } // Далее как в листинге 10.20 } } //... } Метод Validate() интерфейса IServiceBehavior включает контроль средств без- опасности с тем же уровнем детализации, что и в ServiсеHost<T>, без переопреде- ление настроек из конфигурационного файла.
А Введение в служебно- ориентированное программирование Эта книга посвящена проектированию и разработке служебно-ориентирован- ных приложений с использованием WCF. В настоящем приложении я попы- тался представить свое видение служебно-ориентированного программирова- ния и его применения в конкретном контексте. Но чтобы понять, в каком направлении движется отрасль разработки программного обеспечения со слу- жебно-ориентированным программированием, необходимо сначала разобрать- ся, какой путь она проделала в прошлом, потому что новая методология воз- никла не в результате революционной теории, а базируется на постепенной эволюции, продолжавшейся несколько десятилетий. После краткого обсужде- ния истории разработки программного обеспечения и господствующих тенден- ций я определю само понятие служебно-ориентированных приложений (в от- личие от архитектуры), объясню, что собой представляют службы а также представлю основные преимущества данной методологии. Затем читатель позна- комится с основными принципами служебно-ориентированного программиро- вания, причем абстрактная теория будет дополнена практическими рекоменда- циями, необходимыми в большинстве приложений. Краткая история программирования Первый современный компьютер представлял собой электромеханическое уст- ройство размером с пишущую машинку. Он был создан в Польше в конце 1920-х годов для шифрования сообщений. Позднее устройство было продано Министерству торговли Германии, а в 1930-х годах было принято немецкой ар- мией для шифрования всего обмена данными. В наши дни оно известно под ко- довым названием «Энигма» (Enigma). В нем использовались механические рото- ры, изменявшие течение электрического тока от нажатой клавиши к световому табло с другой буквой (зашифрованной). Устройство «Энигма» не являлось ком- пьютером общего назначения: оно могло выполнять только операции шифрова- ния и дешифрования. Если оператор хотел изменить алгоритм шифрования, ему приходилось изменять механическую структуру машины — переставлять
548 Приложение А. Введение в служебно-ориентированное программирование роторы, менять их порядок и исходные позиции, а также соединения на клем- мах, связывающие клавиатуру со световым табло. Таким образом, «программа» предельно жестко привязывалась к проблеме, для решения которой она созда- валась (шифрование), и к механическому устройству компьютера. В конце 1940-х и 1950-х годах появились первые электронные компьютеры общего назначения, которые использовались в военных целях. На них уже мож- но было выполнять программный код для решения произвольных задач, не только одной заранее определенной задачи. С другой стороны, программы, вы- полнявшиеся на таких компьютерах, писались на машинно-зависимом «языке», а программа жестко привязывалась к оборудованию. Программы, написанные для одного компьютера, могли не работать на других компьютерах. На первых порах это не создавало особых проблем, потому что количество компьютеров в мире было ничтожно мало, однако со временем компьютеры стали более рас- пространенным явлением. В начале 1960-х годов появление языка ассемблера ознаменовало отделение программного кода от конкретной аппаратной плат- формы; появилась возможность выполнения написанного кода на разных ком- пьютерах. Тем не менее код оставался привязанным к архитектуре компьютера. Программа, написанная для 8-разрядного компьютера, не могла работать на 16-разрядном компьютере, не говоря уже о различиях в регистрах, доступной памяти и структуре памяти. Это привело к эскалации затрат на создание и со- провождение программ. Примерно в то же время компьютеры стали широко распространяться в гражданском и правительственном секторе, в которых огра- ниченность ресурсов и бюджета требовали иных решений. В 1960-х годов с появлением языков высокого уровня (таких, как COBOL и FORTRAN) возникла концепция компилятора. Разработчик писал програм- мы па абстрактном уровне (язык программирования), а компилятор преобразо- вывал их в машинный код. Компиляторы впервые отделили программный код от оборудования и его архитектуры. Главный недостаток языков первого поко- ления заключался в том, что программирование было неструктурным, а про- граммный код жестко привязывался к определенной структуре из-за использо- вания команд перехода. Незначительные изменения в структуре кода приводили к катастрофическим последствиям в разных местах программы. В 1970-х годах появились первые языки структурного программирования — такие, как С и Pascal. В них программный код отделялся от своего внутреннего строения за счет использования функций и структур данных. Кроме того, имен- но в 1970-х годах разработчики и аналитики впервые начали рассматривать программы как объект инженерного анализа. Чтобы свести к минимуму затра- ты обладателей программного кода, компании начали искать возможности повторного использования кода в других контекстах. В таких языках, как С, основной единицей повторного использования является функция. Недостаток повторного использования на основе функций заключается в том, что функция привязывается к данным, с которыми она работает. Если данные имеют гло- бальную природу, изменения, полезные в одной функции в одном контексте повторного использования, могут повредить работе другой функции в другом контексте.
Компонентно-ориентированное программирование 549 объектно-ориентированное рограммирование Для решения подобных проблем в 1980-х годах была разработана концепция объектно-ориентированного программирования. В ней были задействованы та- кие языки, как Smalltalk и позднее C++. В объектно-ориентированном програм- мировании функции упаковываются в одном объекте с данными, с которыми они работают. Функции (в ООП называемые методами) инкапсулируют логи- ку, а объект инкапсулирует данные. Объектно-ориентированное программиро- вание позволяет моделировать предметные области в виде иерархий классов. В основу механизма повторного использования заложены классы, благодаря чему становится возможным как прямое повторное использование, так и специали- зация посредством наследования. Впрочем, у объектно-ориентированного про- граммирования есть свои серьезные проблемы. Такие языки, как C++, никак не связаны с двоичным представлением сгенерированного кода. Для внесения от- носительно небольших изменений разработчику приходится развертывать ог- ромную кодовую базу. Данное обстоятельство отрицательно сказывается на про- цессе разработки, качестве, сроках выхода на рынок и затратах. Хотя основной единицей повторного использования кода был класс, это был класс в формате ис- ходного кода. Таким образом, приложение жестко привязывалось к используе- мому языку. Клиент, написанный на Smalltalk, не мог пользоваться классом C++ или определять на его основе производные классы. Кроме того, оказалось, что наследование как механизм повторного использования кода оставляет же- лать лучшего, а нередко приносит больше вреда, чем пользы, потому что разра- ботчик производного класса должен хорошо разбираться в тонкостях реализации базового класса; так формировались нежелательные вертикальные логические привязки в иерархиях классов. Объектно-ориентированное программирование игнорировало многие реальные проблемы — такие, как развертывание прило- жений и контроль версии. Сериализация и сохранение объектов создавали но- вую разновидность проблем; приложения обычно не создавали объекты «на ровном месте», а загружали некоторое сохраненное состояние, которое должно было быть преобразовано в объекты, но при этом не существовало способа обеспечения совместимости между сохраненным состоянием и возможной мо- дификацией кода объекта. Если объекты распределялись по нескольким про- цессам или компьютерам, использование «обычного» кода C++ для вызова ста- новилось невозможным, так как C++ требовал прямых обращений к памяти и не поддерживал распределенные вызовы. Разработчику приходилось писать хостовые процессы и использовать технологии удаленного вызова (такие, как со- кеты TCP), но такие вызовы не имели ничего общего с «родными» вызовами C++. омпонентно-ориентированное рограммирование Со временем стали появляться средства, решающие проблемы объектно-ориен- тированного программирования — например, статические (.lib) и динамические (.dll) библиотеки. В конце концов, в 1994 году появилась первая компонент-
550 Приложение А. Введение в служебно-ориентированное программирование но-ориентированная технология COM (Component Object Model). Компонентно- ориентированное программирование позволяет создавать взаимозаменяемые, совместимые двоичные компоненты. Вместо совместного использования фай- лов с исходным кодом клиент согласует с сервером двоичную систему типов (такую, как IDL) и способ представления метаданных в закрытых двоичных компонентах. Поиск и загрузка компонентов производятся на стадии выполне- ния, что открывает различные нетривиальные возможности — например, пере- таскивание элемента на форму с его автоматической загрузкой на компьютере клиента. Клиент программируется только в расчете на абстрактное представле- ние службы — на ее контракт, называемый интерфейсом. Пока интерфейс оста- ется неизменным, служба может изменяться по усмотрению ее разработчиков. Посредник может реализовать тот же интерфейс, инкапсулируя всю низкоуров- невую механику удаленных вызовов. Наличие общей двоичной системы типов обеспечивает межъязыковую совместимость; клиент Visual Basic может исполь- зовать COM-компоненты, написанные на C++. Основной единицей повторного использования кода становится интерфейс, а не компонент, а полиморфные реализации являются взаимозаменяемыми. Контроль версии осуществляется назначением уникального идентификатора каждому интерфейсу, объекту СОМ и библиотеке типов. Хотя технология СОМ ознаменовала фундаментальный прорыв в современном программировании, многие разработчики отнеслись к ней с неприязнью. Архитектура СОМ была чрезмерно уродливой, потому что она создавалась на базе операционной системы, которой о СОМ ничего не было известно, а языки, использовавшиеся для написания компонентов СОМ (в ча- стности, C++ и Visual Basic), были в лучшем случае объектно-ориентированны- ми, но не компонентно-ориентированными. Все это значительно усложняло мо- дель программирования, и для объединения двух миров приходилось создавать специальные инфраструктуры — например, ATL. Приняв во внимание все эти обстоятельства, компания Microsoft в 2002 году выпустила платформу .NET 1.0. На абстрактном уровне .NET представляет собой не что иное, как улучшенный вариант СОМ, C++ и Windows, работающие в единой, новой компонентно-ори- ентированной среде времени выполнения. Платформа .NET обладает всеми дос- тоинствами СОМ, и на ней стандартизированы многие аспекты СОМ — обмен метаданными типов, сериализация и контроль версий. С .NET на порядок удобнее работать, чем с СОМ, и все же у СОМ и .NET имеется ряд общих не- достатков. О Технология и платформа. Приложение и код привязываются к технологии и платформе. И СОМ, и .NET доступны только в системе Windows. Обе тех- нологии предполагают что клиент и служба одновременно работают либо на базе СОМ, либо на базе .NET и нормальное взаимодействие с другими тех- нологиями (на базе Windows или для других систем) невозможно. Хотя мос- товые технологии (например, веб-службы) позволяют организовать такое взаимодействие, они создают собственные трудности и лишают разработчи- ка почти всех преимуществ от использования этих технологий. О Управление параллельной обработкой. Когда фирма-разработчик выпускает компонент, она обязана предусмотреть возможность его использования не- сколькими параллельными программными потоками. Компоненты должны
Компонентно-ориентированное программирование 551 быть потоково-безопасными и снабжаться синхронизационными блокировками. Если разработчик создает приложение посредством объединения нескольких компонентов от разных производителей, наличие нескольких блокировок создает опасность взаимной блокировки. С другой стороны, меры по предот- вращению взаимной блокировки создают логическую привязку между при- ложением и компонентами. О Транзакции. Если приложение хочет, чтобы его компоненты участвовали в одной транзакции, ему придется координировать транзакции и распростра- нять их между компонентами, а это довольно непростая задача из области программирования. Кроме того, координация транзакций создает логиче- скую привязку между приложением и компонентами. О Коммуникационные протоколы. Если компоненты размещаются в разных процессах или даже на разных компьютерах, они привязываются к подроб- ностям удаленных вызовов, используемому транспортному протоколу и по- следствиям его применения в модели программирования (таким, как надеж- ность и безопасность). О Коммуникационные схемы. Вызовы компонентов могут быть синхронными или асинхронными, производиться в подключенном или отключенном ре- жиме. Для некоторых компонентов вызов в одном из этих режимов может оказаться неразрешенным; приложение должно знать об этом. О Контроль версии. Приложение, написанное в расчете на одну версию компо- нента, в ходе эксплуатации может столкнуться с другой версией. Надежное решение проблем изменения версии привязывает приложение к используе- мыми им компонентами. О Безопасность. У компонентов может возникнуть необходимость в аутенти- фикации и авторизации вызывающих сторон, но как ему определить, какую безопасность следует использовать или к какой роли принадлежит тот или иной пользователь? Кроме того, компонент может потребовать защиты своего обмена данными с клиентами — а это, разумеется, наложит некоторые ограни- чения на клиентов и привяжет их к потребностями компонента в безопас- ности. СОМ и .NET попытались решить некоторые (но не все) из перечисленных проблем при помощи технологий СОМ+ и Enterprise Services соответственно (по аналогии с Java и J2EE), но на практике такие приложения оказываются пе- регруженными вторичным кодом. В приложениях нормального размера разра- ботчику приходится тратить большую часть усилий и времени разработки/от- ладки на решение этих вторичных проблем, вместо того чтобы сосредоточиться на бизнес-логике. Ситуация усугубляется тем, что вторичный код мало интере- сует конечного пользователя (и руководителя проекта), поэтому разработчикам не выделяется достаточно времени, необходимого для создания надежного, за- щищенного от ошибок вторичного кода. «Самодельные» решения в основном делаются специализированными и закрытыми (в ущерб повторному использо- ванию и миграции кода) — ведь многие разработчики не являются экспертами в области безопасности и синхронизации и им не выделяется достаточно време- ни и ресурсов для разработки качественного вторичного кода.
552 Приложение А. Введение в служебно-ориентированное программирование Служебно-ориентированное программирование Присмотревшись к только что изложенной истории программирования, можно заметить одну закономерность: каждое новое методологическое и технологиче- ское поколение включает достоинства своих предшественников и исправляет их недостатки. Тем не менее каждое новое поколение также создает новые про- блемы. Я бы сказал, что современное программирование представляет собой не- прерывное совершенствование постоянной тенденции к разрыву логических при- вязок. Иначе говоря, логические привязки — это плохо, но неизбежно. Приложе- ние, полностью лишенное каких-либо логических привязок, не принесет никакой пользы. Его практическая ценность формируется только с появлением логиче- ских привязок. Сам акт написания программного кода означает логическую привязку одних сущностей к другим. Настоящий вопрос в том, как разумно вы- брать такие привязки. На мой взгляд, существует две разновидности логиче- ских привязок. К первой — «хорошей» — разновидности относятся логические привязки бизнес-уровня. Практическая ценность приложения создается реали- зацией сценариев использования или функций, посредством создания логиче- ских привязок между аспектами функциональности приложения. К «плохим» логическим привязкам относится все, что связано с вторичным, служебным ко- дом. Главным недостатком .NET и СОМ была не концепция, а тот факт, что разработчику приходилось писать слишком много вторичного кода. Проблемы прошлого были учтены, и в конце XX века появилась новая слу- жебно-ориентированная методология, которая должна была решить проблемы объектно- и компонентно-ориентированных методологий. В служебно-ориен- тированном приложении разработчик направляет свои усилия на реализацию бизнес-логики и предоставления доступа к ней через взаимозаменяемые со- вместимые конечные точки служб. Клиент работает с конечными точками, а не с кодом службы или с его упаковкой. Взаимодействие между клиентами и ко- нечной точкой службы базируется на стандартном механизме обмена сообще- ниями, а служба публикует некую разновидность стандартных метаданных, ко- торые точно описывают, что может делать служба и как клиенты должны вызывать ее операции. Метаданные службы можно рассматривать как аналоги заголовочных файлов C++, библиотек типов СОМ или метаданных сборок .NET. Конечная точка службы может использоваться любым клиентом, совмес- тимым с ее ограничениями взаимодействия (например — синхронные защи- щенные коммуникации с поддержкой транзакций), независимо от технологии реализации последнего. Во многих отношениях службы являются естественным результатом эволю- ции компонентов, подобно тому, как компоненты были естественным результа- том эволюции объектов. С точки зрения отрасли именно служебно-ориентирован- ное программирование является наиболее правильным подходом к построению надежных, безопасных и простых в сопровождении программных продуктов. При разработке служебно-ориентированного приложения код службы изо- лируется от технологии и платформы, используемой клиентом; от многих аспек-
Преимущества служебно-ориентированного подхода 553 тов управления параллельной обработкой; от распространения и управления транзакциями, от надежности коммуникаций, от протоколов и схем. В общем и целом защита передачи данных самого сообщения от клиента к службе тоже находится за пределами компетенции самой службы, как и аутентификация вы- зывающей стороны. Впрочем, служба может провести собственную локальную авторизацию, если потребуется. По тем же причинам клиент не привязывается к версии службы: если конечная точка поддерживает контракт, ожидаемый клиентом, версия службы клиента попросту не интересует. 1реимущества пужебно-ориентированного подхода Поскольку взаимодействие между клиентом и службой базируется на отрасле- вых стандартах, которые описывают, как организуется защита вызовов, распро- странение транзакций, управление надежностью и т. д., для такого вторичного кода может существовать готовая реализация. В вою очередь, это упрощает со- провождение приложения, так как дальнейшее совершенствование вторичного кода не влияет на работу приложения. Служебно-ориентированные программы более устойчивы к ошибкам — разработчик может использовать готовый прове- ренный код. Данное обстоятельство также повышает эффективность труда раз- работчика, так как разработчик может уделять больше времени функционально- сти приложения, а не вторичному коду. Именно в этом заключается истинная ценность служебно-ориентированных методологий: они избавляют разработчи- ка от возни со вторичным кодом и позволяют сосредоточиться на бизнес-логи- ке и необходимым функциональным возможностям. Многие другие достоинства — такие, как межтехнологическая совмести- мость — всего лишь являются проявлением этого основного преимущества. Ко- нечно, взаимодействие может осуществляться и без участия служб, как это было принято до появления служебно-ориентированных методологий. Разли- чие в том, что в новом сценарии взаимодействие обеспечивается готовым вто- ричным кодом. При написании службы вас обычно не интересует, на какой платформе работает клиент, — это несущественно, в чем и заключается вся суть хорошо интегрированных взаимодействий. Но возможности служебно-ориен- тированных приложений этим отнюдь не ограничиваются — они позволяют разработчикам выходить за границы технологий и платформ. Более того, между клиентом и службой могут существовать и другие границы: границы безопасно- сти и доверия, географические, организационные, временные, транзакционные границы и даже границы бизнес-моделей. Беспроблемный переход через все перечисленные границы становится возможным благодаря стандартному взаи- модействию на базе обмена сообщениями. Например, существуют стандарты защиты сообщений и создания надежного обмена данными между клиентом и службой, даже в том случае, если они находятся в местах, не связанных пря- мыми отношениями доверия. Существует стандарт, который позволяет менед- жеру транзакций на стороне клиента распространять транзакцию к менеджеру транзакций на стороне службы и организовать участие службы в этой транзак-
554 Приложение А. Введение в служебно-ориентированное программирование ции, даже при том, что два менеджера транзакций никогда не включаются явно в транзакции друг друга. Служебно-ориентированные приложения Служебно-ориентированное приложение представляет собой результат агреги- рования служб в одно логически завершенное, связное приложение (рис. А.1) — по аналогии с тем, как объектно-ориентированное приложение образуется в ре- зультате агрегирования объектов. Рис. А.1. Служебно-ориентированное приложение Само приложение может представить агрегат как новую службу, по анало- гии с тем, как объекты строятся из меньших объектов. Внутри служб разработчики по-прежнему используют конкретные языки программирования, версии, технологии и инфраструктуры, операционные сис- темы, API и т. д. Однако взаимодействие между службами организуется с при- менением стандартных протоколов и сообщений, контрактов и обмена метадан- ными. Разные службы приложения могут находиться в одном месте, быть распре- деленными в интрасети или Интернете; они могут быть созданы разными раз- работчиками, работать на разных платформах и технологиях, менять версии не- зависимо друг от друга и даже выполняться в разных временных интервалах. Все эти вторичные аспекты остаются скрытыми от клиентов приложения, взаимодействующих со службами. Клиент отправляет стандартные сообщения службам, а вторичный код на обоих концах маскирует различия между клиен- тами и службами, преобразуя сообщения в нейтральное канальное представле- ние, и наоборот. Поскольку служебно-ориентированные инфраструктуры предоставляют го- товый вторичный код для объединения служб, чем выше степень детализации службы, чем интенсивнее приложение использует эту инфраструктуру и тем меньше вторичного кода приходится писать разработчикам. Если довести эту
Каноны и принципы 555 концепцию до крайности, каждый класс и примитив должен быть оформлен в виде службы — тем самым обеспечивается максимальная степень использова- ния готовых средств взаимодействия и предотвращается необходимость «руч- ного» создания вторичного кода. Однако на практике существует предел дета- лизации, определяемый в основном производительностью используемой инфраструктуры (такой, как WCF). Думаю, с течением времени и по мере раз- вития служебно-ориентированных технологий границы служб будут непрерыв- но сужаться, и службы будут становиться все более детализированными, до тех пор пока самые примитивные структурные блоки не будут оформлены в виде служб. Каноны и принципы Служебно-ориентированная методология управляет тем, что происходит в про- странстве между службами (см. рис. А.1). Существует небольшой набор прин- ципов и рекомендаций для построения служебно-ориентированных приложе- ний, называемых канонами служебно-ориентированной архитектуры. О Четкие границы служб. Любая служба всегда существует в некоторых грани- цах, определяемых технологией ее реализации, размещением и т. д. Служба не должна предоставлять информацию о природе таких границ клиентам че- рез контракты и типы данных, выдающие информацию о ее технологиях или местоположении. При соблюдении этого канона такие аспекты, как местопо- ложение и технология, становятся несущественными. На этот канон можно взглянуть и с другой точки зрения: чем больше клиент знает о реализации службы, тем сильнее логическая привязка клиента к службе. Чтобы свести к минимуму потенциальную логическую привязку, служба должна выдать явное описание предоставляемой функциональности, и только явно описан- ные операции (или контракты данных) будут использоваться совместно с клиентом. Все остальное инкапсулируется. О Автономность служб. Работа службы (и изменение ее версии) никак не за- висит от клиентов или других служб. Тем самым обеспечивается возмож- ность модификации службы отдельно от клиента. Служба также защищает себя и передаваемые ей сообщения независимо от степени использования безопасности на стороне клиента. Кроме того, соблюдение этого принципа (наряду с простым здравым смыслом) также ликвидирует логическую при- вязку между клиентом и службой в отношении безопасности. О Службы предоставляют контракты операций и схемы данных, а не мета- данные, специфические для конкретных типов и технологий. Все, что служба предоставляет «внешнему миру», должно быть технологически-нейтраль- ным. Служба должна уметь преобразовывать «родные» типы данных в ней- тральное представление и обратно; при этом другим сторонам не должны пе- редаваться такие технологически-специфические сведения, как номер вер- сии сборки или ее тип. Служба также не должна сообщать клиенту локальные подробности реализации — такие, как режим управления экземплярами или режим управления параллельной обработкой. Служба предоставляет внеш-
556 Приложение А. Введение в служебно-ориентированное программирование нему пользователю только логические операции, а информация о том, как эти операции реализованы, остается закрытой. О Совместимость служб определяется политикой. Служба должна публико- вать политику, в которой указано, что она может делать и как клиенты должны взаимодействовать с ней. Любые ограничения доступа, выраженные в политике (например, требование надежной передачи данных), должны быть отделены от подробностей реализации службы. Не все клиенты могут взаимодействовать со всеми службами. Вполне могут существовать ситуа- ции несовместимости, не позволяющие конкретному клиенту работать со службой. Только на основании опубликованной политики клиент решает, сможет ли он работать со службой. Его решение не должно зависеть ни от каких внеполосных механизмов. Иначе говоря, служба должна иметь воз- можность выразить в стандартном представлении политики то, что она дела- ет, и как клиенты должны взаимодействовать с ней. Невозможность выраже- ния такой политики является признаком некачественного проектирования службы. Впрочем, это не значит, что служба не может отказаться от публи- кации политики по соображениям приватности (если служба не является общедоступной). Данный канон подразумевает лишь то, что служба должна быть способна опубликовать политику в случае необходимости. Практические принципы Перечисленные каноны чрезвычайно абстрактны, и их поддержка в значитель- ной мере зависит от технологии, применяемой при разработке и использовании служб и их архитектуры. Соответственно, приложения могут в разной степени соответствовать канонам (по аналогии с тем, как разработчик C++ может пи- сать не-объектно-ориентированный код). И все же хорошо спроектированное приложение старается по возможности соответствовать этим канонам, поэтому после теоретических канонов я приведу набор более «приземленных», практи- ческих принципов. О Службы должны быть безопасными. Служба и ее клиенты должны использо- вать безопасные взаимодействия. По меныпей мере передача сообщения от клиента к службе должна быть защищена, и у клиентов должен быть способ аутентификации службы. Клиент также может передавать свои удостовере- ния в сообщении, чтобы служба могла провести аутентификацию и автори- зацию О Службы должны оставлять систему в стабильном состоянии. Такие усло- вия, как частичное выполнение запроса клиента, запрещены. Все ресурсы, с которыми работает служба, должны находиться в корректном состоянии после вызова клиента. Ошибки не должны приводить к таким последствиям, как частичное изменение состояния системы. Восстановление системы в ста- бильном состоянии после ошибки не должно требовать помощи со стороны клиентов.
Необязательные принципы 557 О Службы должны быть потоково-безопасными. Служба должна быть спроек- тирована в расчете на возможность параллельных обращений со стороны не- скольких клиентов. О Службы должны быть надежными. Если клиент вызывает службу, он всегда должен иметь возможность точно определить, было ли сообщение получено службой. Сообщения должны обрабатываться в порядке их отправки, а не в порядке получения. О Службы должны быть устойчивы к ошибкам. Служба изолирует свои сбои, не позволяя им нарушить работу самой себя или других служб. Служба не должна требовать, чтобы клиент менял свое поведение в соответствии с ти- пом ошибки, обнаруженной службой. Тем самым ликвидируется логическая привязка клиента к службе в области обработки ошибок. еобязательные принципы Я рассматриваю практические принципы как обязательные, однако наряду с ними существует целый набор дополнительных принципов. Они не являются обязательными для всех приложений и все же их обычно стоит соблюдать. О Службы должны быть совместимы. Службы следует проектировать так, что- бы они могли вызываться любым клиентом независимо от технологии. О Службы должны быть масштабно-инвариантны. Другими словами, один код службы должен работать при любом количестве клиентов и нагрузке на службу. Такой подход капитально снижает затраты владельца службы при расширении системы и позволяет применять другие сценарии разверты- вания. О Службы должны быть доступны. Служба всегда должна быть готова к при- нятию клиентских запросов и у нее не должно быть простоев. В противном случае клиенту придется принимать меры на случай недоступности службы, а это создает нежелательные логические привязки. О Службы должны обладать разумным временем отклика. Не заставляйте кли- ента подолгу ожидать, пока служба займется обработкой его запроса. В про- тивном случае клиенту придется принимать меры на случай отсутствия ре- акции службы, а это создает нежелательные логические привязки. О Службы должны работать в нормальных временных рамках. Выполнение любой операции службой должно происходить относительно быстро, как и обработка клиентских запросов. Если клиенту придется принимать меры на случай слишком долгой обработки, это также создаст нежелательные ло- гические привязки.
Б Схема «публикация/ подписка» Использование низкоуровневых дуплексных обратных вызовов для обработки событий часто приводит к появлению излишних логических привязок между издателем и подписчиком. Подписчик должен знать расположение всех служб-из- дателей в приложении и уметь к ним подключаться. Если какой-либо издатель неизвестен подписчику, он не сможет оповещать подписчика о событии. В свою очередь, это усложняет добавление новых подписчиков (или удаление сущест- вующих) в уже развернутых приложениях. Подписчик не может потребовать, чтобы он оповещался каждый раз, когда в приложении инициируется событие определенного типа. Кроме того, подписчик должен обращаться к издателю с несколькими (потенциально затратными) вызовами для оформления и отказа от подписки. Разные издатели могут инициировать одно событие, но при этом использовать слегка различающиеся схемы оформления и отказа от подписки; конечно, это формирует нежелательную логическую привязку между подпис- чиком и этими механизмами. Аналогично, издатель может оповещать только тех подписчиков, которые ему известны. Не существует механизма, который бы позволил издателю доста- вить событие всем, кто желает его получить, или выполнить широковещатель- ную рассылку события. Кроме того, все издатели должны содержать код управ- ления списком подписчиков и самого акта публикации. Этот код не имеет ничего общего с бизнес-проблемой, для решения которой проектируется служ- ба, но может стать достаточно сложным в случае применения нетривиальных возможностей — например, параллельной публикации. Но это еще не все. Дуплексные обратные вызовы создают логическую при- вязку между жизненным циклом издателя и подписчиков. Подписка и получе- ние событий требуют активности подписчика. Подписчик не может потребо- вать, чтобы в случае возникновения события приложение создало экземпляр подписчика и позволило ему обработать событие. Безопасность открывает еще одну область потенциальных логических при- вязок: подписчики должны иметь возможность провести аутентификацию себя для всех издателей, всех режимов безопасности и используемых удостоверений.
Схема «публикация/подписка» 559 Издатель также должен обладать удостоверениями безопасности, достаточны- ми для выдачи события, и разные подписчики могут использовать разные роле- вые механизмы. Наконец, настройка подписки должна осуществляться на программном уровне. Не существует административных средств настройки подписки в при- ложениях или административного изменения конфигурации подписчика во время работы системы. Эти проблемы характерны не только для дуплексных вызовов WCF. Они также присущи таким технологиям прошлого, как точки подключения СОМ или делегаты .NET — все они относятся к механизмам управления события с жесткой логической привязкой. Схема «публикация/подписка» Перечисленные проблемы решаются на уровне проектирования, с использова- нием архитектурной схемы «подписка/публикация». Идея, заложенная в основу этой схемы, проста: издатели отделяются от подписчиков, для чего между ними создаются специализированные службы подписки и публикации (рис. Б.1). Рис. Б.1. Система на базе схемы «публикация/подписка» Подписчики, желающие подписаться на события, регистрируются в службе подписки, которая ведет списки подписчиков, а также предоставляет функции отказа от подписки. Аналогичным образом, все издатели используют службу публикации для выдачи событий, отказываясь от прямой доставки событий
560 Приложение Б. Схема «публикация/подписка» клиентам. Службы подписки и публикации создают дополнительный промежу- точный уровень, способствующий изоляции компонентов системы. Отныне подписчикам не нужно ничего знать о личности издателей. Подписавшись на определенный тип события, они будут получать это событие от любого издате- ля, а механизм подписки является общим для всех издателей. Фактически ни одному издателю не требуется вести список подписки, а издатели понятия не имеют, кто именно подписался на их события. Они передают события службе публикации, которая доставляет их всем заинтересованным подписчикам. Типы подписчиков Мы даже можем определить два типа подписчиков: временные подписчики нахо- дятся в памяти, а постоянные подписчики хранятся на диске и представляют службы, которые должны вызываться при возникновении события. Для вре- менных подписчиков можно использовать механизм дуплексного обратного вызова как удобное средство передачи ссылки обратного вызова работающей службе. Для постоянных подписчиков достаточно хранить адрес подписчика. При возникновении события публикующая служба обращается по адресу по- стоянного подписчика и доставляет ему событие. Другое важное различие меж- ду двумя типами подписчиков заключается в том, что данные постоянной под- писки могут храниться на диске или в базе данных. В результате подписка сохраняется при сбоях, выключении и перезагрузке компьютера, что делает возможным административное управление конфигурацией подписки. Очевид- но, временная подписка не переживает выключения компьютера и должна на- страиваться заново при каждом запуске приложения. И нфраструктура « публи кация / подп иска » Архив исходного кода, прилагаемый к книге, содержит полный пример реали- зации схемы «публикация/подписка». Я стремился не только предоставить пример служб и клиентов, действующих по схеме «публикация/подписка», но также предоставить инфраструктуру общего назначения, которая бы автомати- зировала реализацию таких служб и включения их поддержки в любое прило- жение. Построение инфраструктуры должно начинаться с формирования ин- терфейсов управления публикацией/подпиской, а также определения от- дельных контрактов для временной и постоянной подписки и публикации1. Управление временной подпиской Для управления временной подпиской я определил интерфейс ISubscriptionSer- vice, приведенный в листинге Б.1. 1 Описание моей инфраструктуры публикации/подписки было впервые опубликовано в статье «WCF Essentials: What You Need to Know About One-Way Calls, Callbacks, and Events» (MSDN Magazine, октябрь 2006 г.).
Инфраструктура «публикация/подписка» 561 Листинг Б.1. Интерфейс ISubscriptionService предназначен для управления временной подпиской [ServiceContract] public interface ISubscriptionService { [Operationcontract] void Subscribe(string eventoperation); [Operationcontract] void Unsubscribe(string eventoperation); } Обратите внимание: интерфейс ISubscriptionService не идентифицирует кон- тракт обратного вызова, ожидаемый реализующей конечной точкой. Будучи интерфейсом общего назначения, он ничего не знает о конкретных контрактах обратного вызова. Приложение должно само определять эти контракты. Интер- фейс обратного вызова предоставляется в приложении наследованием от ISub- scriptionService с указанием нужного контракта обратного вызова: interface IMyEvents { [OperationContractdsOneWay = true)] void OnEventlO; [OperationContractdsOneWay = true)] void 0nEvent2(int number); [OperationContractdsOneWay = true)] void 0nEvent3(int number,string text); } [ServiceContract(CallbackContract я typeof(IMyEvents))] interface IMySubscriptionService : ISubscriptionService {} Как правило, каждая операция контракта обратного вызова соответствует определенному событию. Субинтерфейсу ISubscriptionService (в данном случае IMySubscriptionService) не нужно добавлять новые операции. Функциональность управления временной подпиской обеспечивается ISubscriptionService. При каж- дом вызове Subscribe() или Unsubscribe() подписчик должен предоставить имя операции (а следовательно, и события), на которую он хочет оформить или прекратить подписку. Если вызывающая сторона желает подписаться на все со- бытия, она передает пустую строку или null. Реализация методов ISubscriptionService в моей инфраструктуре представле- на в форме параметризованного абстрактного класса SubscriptionManager<T>: public abstract class SubscriptionManager<T> where T ; class { public void Subscribe(string eventoperation): public void Unsubscribe(string eventoperation): //... } Параметр типа SubscriptionManager<T> определяет контракт событий. Обра- тите внимание: SubscriptionManager<T> не реализует ISubscriptionService.
562 Приложение Б. Схема «публикация/подписка» Использующее приложение должно предоставить доступ к своей службе временной подписки в виде конечной точки, поддерживающей специализиро- ванный субинтерфейс ISubscriptionService. Для этого приложение должно пре- доставить класс службы, производный от SubscriptionManager<T>, передать кон- тракт обратного вызова в параметре типа и использовать наследование от кон- кретного субинтерфейса ISubscriptionService. Пример реализации службы вре- менной подписки для интерфейса обратного вызова IMyEvents: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MySubscriptionService : SubscriptionManager<IMyEvents>. IMySubscri pt1onServi ce {} Класс MySubscriptionService не содержит новый код, потому что IMySubscrip- tionService не добавляет новых операций, a SubscriptionManager<T> уже реализует методы ISubscriptionService. Учтите, что простого наследования от SubscriptionManager<IMyEvents> недос- таточно — для поддержки временной подписки необходимо добавить наследо- вание от IMySubscriptionService. Наконец, использующее приложение должно определить конечную точку для IMySubscriptionService: <services> <@060>service name = "MySubscriptionService”> <endpoint address = "..." binding = "..." contract = "IMySubscriptionService" /> </service> </services> В листинге Б.2 показано, как в SubscriptionManager<T> организовано управле- ние временной подпиской. Листинг Б.2. Управление временной подпиской в SubscriptionManager<T> public abstract class SubscriptionManager<T> where T : class { static Dictionary<string.List<T>> m_TransientStore; static SubscriptionManagerO { m_TransientStore = new Dictionary<string.List<T»(); stringE] methods = GetOperationsO: Action<string<@062> insert = delegate(string methodName) { m_TransientStore.Add(methodName.new List<T>()); }; Array.ForEach(methods.insert): } static stringE] GetOperationsO { MethodlnfoE] methods = typeof(T).GetMethods(BindingFlags.Public| Bi ndi ngFlags.FlattenHierarchy|
Инфраструктура «публикация/подписка» 563 BindingFlags.Instance): List<string> operations = new List<string>(methods.Length): Action<MethodInfo<@062> add = delegate(MethodInfo method) { Debug.Assert(!operations.Contai ns(method.Name)): operations.Add(method.Name); }: Array.ForEach(methods.add); return operations.ToArray(); } static void AddTransient(T subscriber.string eventoperation) ( 1ock(typeof(Subscri pti onManager<T>)) { List<@060>T> list = m_TransientStore[eventOperation]; i f(1i st.Contai ns(subscriber)) { return: } list.Add(subscriber); } } static void RemoveTransient(T subscriber.string eventoperation) { 1ock(typeof(Subscri pti onManager<T>)) { List<@060>T> list = m_TransientStore[eventOperation]: list.Remove(subscriber); } } public void Subscribe(string eventoperation) { 1ock(typeof(Subscripti onManager<T>)) { T subscriber = Operationcontext.Current.GetCallbackChannel<T>(): if(String.IsNu!10rEmpty(eventOperation) == false) { AddT rans i ent(subscri ber,eventoperation); } else { string[] methods = GetOperations(): Action<string> addTransient = delegate(string methodName) ( AddT ransi ent(subscriber.methodName); } Array.ForEach(methods.addTrans i ent): ) } } public void Unsubscribe(string eventoperation) { продолжение &
564 Приложение Б. Схема «публикация/подписка» 1ock(typeof(Subscr1pti onManager<T>)) { T subscriber = OperationContext.Current.GetCallbackChannel<T>(); if(String.IsNullOrEmpty(eventOperation) == false) { RemoveT ransi ent(subscri ber.eventOperati on); } else { string[] methods = GetOperations(); Action<str1ng> removeTransient = delegate(string methodName) { RemoveT ransi ent(subscriber.methodName): }: Array.ForEach(methods.removeT ransi ent); } } } //... } SubscriptionManager<T> хранит временных подписчиков в статическом слова- ре с именем m_TransientStore: static Dictionary<string.List«T>> m_TransientStore: Каждая запись в словаре содержит имя операции события и всех его подпис- чиков в форме связанного списка. Статический конструктор SubscriptionMana- ger<T> использует механизм рефлексии для получения всех операций интер- фейсов обратного вызова (параметр типа для SubscriptionManager<T>) и инициа- лизирует словарь всех операций пустыми списками. Метод Subscribe() извлека- ет ссылку обратного вызова из контекста вызова операции. Если вызывающая сто- рона задает имя операции, Subscribe() вызывает вспомогательный метод AddTran- sient(). Метод получает список подписчиков события из хранилища. Если под- писчик отсутствует в списке, он добавляется в него. Если вызывающая сторона передает вместо имени операции пустую строку или null, Subscribe() вызывает AddTransient() для каждой операции в контракте обратного вызова. Метод Unsubscribe() работает аналогично. Учтите, что вызывающая сторона может подписаться на все события, а затем прекратить подписку на определен- ное событие. Управление постоянной подпиской Для управления постоянной подпиской я определил интерфейс IPersistentSub- scriptionService, приведенный в листинге Б.З. Листинг Б.З. Интерфейс IPersistentSubscriptionService предназначен для управления постоянной подпиской [ServiceContract] public interface IPersistentSubscriptionService { [Operationcontract] [TransactionFlow(TransactionFlowOption.Allowed)]
Инфраструктура «публикация/подписка» 565 void Subscribe(string address.string eventsContract. string eventoperation); [Operationcontract] [Transacti onFlow(T ransacti onFlowOpti on.Al 1 owed)] void Unsubscribe(string address.string eventsContract. string eventoperation); //... } Чтобы добавить постоянного подписчика, вызывающая сторона должна вы- звать Subscribe() с передачей адреса подписчика, имени контракта события и конкретной операции события. Чтобы отказаться от подписки, вызывающая сторона использует Unsubscribe() с той же информацией. Обратите внимание: IPersistentSubscriptionService ничего не знает о способе хранения данных подпис- чиков на стороне службы — это подробность реализации. Класс SubscriptionManager<T>, представленный ранее, также реализует мето- ды IPersistentSubscriptionService: [BindingRequirement(TransactionFlowEnabled = true)] public abstract class SubscriptionManager<T> where T ; class { public void Unsubscribe(string address.string eventsContract. string eventoperation); public void Subscribe(string address.string eventsContract. string eventoperation); //... } Класс SubscriptionManager<T> хранит информацию о постоянных подписчи- ках в SQL Server. Он настроен на транзакционный режим «клиент/сервер» (см. главу 7), для чего используется мой атрибут BindingRequirement. В обобщенном параметре типа SubscriptionManager<T> передается контракт событий. Обратите внимание: SubscriptionManager<T> не является производным от IPersistentSubscriptionService. Использующее приложение должно предоста- вить собственную службу управления постоянной подпиской, но объявлять но- вый контракт производным от IPersistentSubscriptionService нет необходимости, потому что ссылки обратного вызова в данном случае не нужны. Приложение просто наследует от SubscriptionManager<T>, передавая контракт событий в пара- метре типа и добавляя наследование от IPersistentSubscriptionService. Пример [ServiceBehaviordnstanceContextMode - InstanceContextMode.PerCai 1)] class MySubscriptionService ; SubscriptionManager<IMyEvents>. IPersistentSubscriptionService {} Наличие программного кода в MySubscriptionService не обязательно, потому что SubscriptionManager<T> уже реализует методы IPersistentSubscriptionService. Учтите, что простого наследования от SubscriptionManager<IMyEvents> недос- таточно — для поддержки постоянной подписки необходимо добавить наследо- вание от IPersistentSubscriptionService.
566 Приложение Б. Схема «публикация/подписка» Наконец, использующее приложение должно определить конечную точку для IMySubscriptionService: <services> <service name = "MySubscriptionService’^ <endpoint address = "..." binding = "..." contract = "IPersistentSubscriptionService" /> </service> </services> Реализация методов IPersistentSubscriptionService классом SubscriptionMana- ger<T> приведена в листинге Б.4. Листинг Б.4 очень похож на листинг Б.2, если не считать того, что данные подписчиков хранятся в SQL Server, а не в памяти. Листинг Б.4. Управление постоянной подпиской в Subscri ption Manager <Т> public abstract class SubscriptionManager<T> where T : class { static void AddPersistent(string address.string eventsContract. string eventoperation) { // Использует ADO.NET для сохранения подписки в SQL Server } static void RemovePersistent(string address.string eventsContract. string eventoperation) { // Использует ADO.NET для удаления подписки из SQL Server } [OperationBehavior(Transact)onScopeRequired = true)] public void Unsubscribe(string address.string eventsContract. string eventoperation) { if(String.IsNullOrEmpty(eventOperation) == false) { RemovePersi stent(address.eventsContract.eventoperation); } else { string[] methods = GetOperationsO: Action<string> removePersistent = delegate(string methodName) ( RemovePersi stent(address.eventsContract.methodName): }: Array.ForEach(methods.removePersi stent): } } [OperationBehavior(TransactionScopeRequired = true)] public void Subscribe(string address.string eventsContract. string eventoperation) { if(String.IsNullOrEmpty(eventOperation) == false)
Инфраструктура «публикация/подписка» 567 { AddPersi stent(address.eventsContract,eventOperati on): } else { string[] methods = GetOperations(); Action<string> addPersistent = delegate(string methodName) { AddPersistent(address.eventsContract.methodName); }: Array.ForEach(methods.addPersistent); } } //... } Если приложение хочет поддерживать как временных, так и постоянных подписчиков для одного контракта событий, просто объявите класс службы подписки производным как от специализированного субинтерфейса ISubscrip- tionService, так и от IPersistentSubscriptionService: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MySubscrlptionService : SubscriptionManager<IMyEvents>. IMySubscri pti onServi ce,IPersi stentSubscri pti onServi ce {} и объявите две соответствующие конечные точки: <services> <service name = "MySubscrlptionServiсе"> <endpoint address = "..." binding = "..." contract = "IMySubscriptionService" /> <endpoint address = "..." binding = "..." contract = "IPersistentSubscriptionService" /> </service> </services> Публикация событий Части инфраструктуры публикации/подписки, приводившиеся до настоящего момента, относились только к аспектам управления подпиской. Инфраструкту- ра также позволяет легко реализовать службу публикации. Служба публикации должна реализовать тот же контракт событий, что и подписчики, и должна быть единственной контактной точкой, известной издателям в приложении. Так как служба публикации предоставляет контракт событий в конечной точке, контракт событий должен быть помечен как контракт службы, даже если он исполь- зуется только для дуплексных обратных вызовов с временными подписчиками: [ServiceContract] interface IMyEvents
{ [OperationContractdsOneWay = true)] void OnEventlO; [OperationContractdsOneWay = true)] void 0nEvent2(int number): [OperationContractdsOneWay = true)] void 0nEvent3(int number.string text): } Инфраструктура публикации/подписки содержит вспомогательный класс PublishService<T>, определяемый следующим образом: public abstract class PublishService<T> where T : class { protected static void FireEvent(params object[] args); } В параметре типа PublishService<T> передается тип контракта службы. Чтобы предоставить собственную службу публикации, определите класс, производ- ный от PublishService<T>, и воспользуйтесь методом FireEvent() для доставки со- бытия всем подписчикам — как временным, так и постоянным, как показано в листинге Б.5. Листинг Б.5. Реализация службы публикации событий [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyPublishService : PublishService<IMyEvents>.IMyEvents { public void OnEventlO { FireEvent(): } public void 0nEvent2(int number) { Fi reEvent (number-); } public void 0nEvent3(int number.string text) { F i reEvent(number,text): } } Обратите внимание: благодаря использованию массива params object метод FireEvent() может использоваться для инициирования событий любого типа не- зависимо от параметров. Наконец, приложение должно предоставить конечную точку службы публи- кации с контрактом событий: <services> <service name = "MyPublishServiсе"> <endpoint address = "..." binding - ”..." contract = "IMyEvents" />
Инфраструктура «публикация/подписка» 569 </service> </services> В листинге Б.6 представлена реализация PublishService<T>. Листинг Б.6. Реализация PublishService<T> public abstract class PublishService<T> where T : class { protected static void FireEvent(params objectt] args) { StackFrame stackFrame = new StackFrame(l); string methodName = stackFrame.GetMethod().Name; // Выделение реализации интерфейса i f(methodName.Contains(".")) { stringt] parts - methodName.Split('.'); methodName = parts[parts.Length-1]; ) Fi reEvent(methodName.args); } static void FireEvent(string methodName.params objectt] args) { Publi shPersi stent(methodName.args); Publi shTransi ent(methodName.args): } static void PublishPersistent(string methodName.params objectt] args) { T[] subscribers = SubscriptionManager<T>.GetPersistentList(methodName): Publi sh(subscri bers.true.methodName.args); } static void PublishTransient(string methodName.params objectt] args) { T[] subscribers = SubscriptionManager<T>.GetTransientList(methodName): Publi sh(subscri bers.false.methodName.args): } static void Publish(T[] subscribers.bool closeSubscribers. string methodName. params objectt] args) { WaitCalIback fire = delegate(object subscriber) { Invoke(subscriber as T.methodName.args); if(closeSubscribers) { using(subscriber as IDisposable) {) ) ): Action<T> queueUp = delegated- subscriber) { ThreadPool.QueueUserWorkItem(fire.subscriber); ): Array.ForEach(subscri bers.queueUp); } static void Invoked subscriber.string methodName.objectt] args) продолжение &
570 Приложение Б. Схема «публикация/подписка» Debug.Assert(subscriber != null): Type type = typeof(T); Method Info methodInfo = type.GetMethod(methodName): try methodinfo.Invoke(subscriber.args): } catch(Exception e) ( Trace.WriteLine(e.Message): } } } Чтобы упростить выдачу событий службой публикации, метод FireEvent() по- лучает параметры для передачи подписчикам, однако вызывающая сторона не передает ему имя операции, вызываемой на стороне подписчиков. Для решения этой задачи FireEvent() обращается к кадру стека и извлекает имя вызывающего метода, после чего используется перегруженная версия FireEvent(), получающая имя метода. В свою очередь, она использует вспомогательный метод Publish- Persistent() для публикации события среди всех постоянных подписчиков и вспомогательный метод PublishTransient() для публикации среди всех времен- ных подписчиков. Оба метода публикации работают почти одинаково: сначала они обращают- ся к SubscriptionManager<T> для получения соответствующего списка подписчи- ков, а затем используют метод Publish() для инициирования события. Данные подписчиков возвращаются в форме массива посредников для работы с ними. Массив передается методу Publish(). В этот момент метод Publish() мог бы про- сто активизировать подписчиков. Однако я решил обеспечить параллельную публикацию событий; если какой-либо подписчик окажется недисциплиниро- ванным и на обработку события уйдет слишком много времени, это не помеша- ет другим подписчикам своевременно получить событие. Учтите, что пометка операций событий как односторонних не гарантирует асинхронности вызова, к тому же я хотел поддерживать параллельную публикацию даже в тех случаях, когда операция события не помечена как односторонняя. Publish() определяет два анонимных метода. Первый вызывает вспомогательный метод Invoke(); это приводит к выдаче события указанному подписчику и последующему закры- тию посредника (при необходимости). Так как метод Invoke() не компилирует- ся для конкретного типа подписчика, для вызова используется рефлексия и позднее связывание. Invoke() также подавляет все исключения, инициирован- ные при вызове, потому что они не интересуют сторону издателя. Второй ано- нимный метод ставит первый анонимный метод на выполнение программным потоком из пула потоков. Наконец, Publish() вызывает второй анонимный метод для каждого подписчика в переданном массиве. Обратите внимание на универсальность работы с подписчиками в Publish- Service<T> — почти неважно, являются ли они временными или постоянными. Единственное различие состоит в том, что после публикации события для по- стоянного подписчика необходимо закрыть посредника. Универсальность достига- ется при помощи вспомогательных методов GetTransientList() и GetPersistentList()
Инфраструктура «публикацумУподписка» 571 класса SubscriptionManager<T>. Из этих двух методов GetTransientList() является более простым: public abstract class SubscriptionManager<T> where T : class { internal static T[] GetTransientList(string eventoperation) { 1ock(typeof(Subscri pt i onManager<T>)) { i f(m_Transi entStore.Contai nsKey(eventOperati on)) { List<T> list = m_TransientStore[eventOperation]; return 1ist.ToArray(): } return new T[] {}; } } //... } Метод GetTransientList() ищет в хранилище временных данных всех подпис- чиков, относящихся к указанной операции, и возвращает их в виде массива. GetPersistentList() сталкивается с более сложной задачей: готового списка по- средников для постоянных подписчиков не существует, для них известны толь- ко адреса. Следовательно, метод GetPersistentList() должен создать экземпляры посредников для постоянных подписчиков, как показано в листинге Б.7. Листинг Б.7. Создание списка посредников для постоянных подписчиков public abstract class SubscriptionManager<T> where T : class { internal static ТЕ] GetPersistentList(string eventoperation) { stringE] addresses = GetSubscribersToContractEventOperation( typeof(T).ToString().eventoperation); List<T> subscribers e new List<T>(addresses.Length); foreach(string address in addresses) { Binding binding = GetBindingFromAddress(address); T proxy = ChannelFactory<T>.CreateChannel(binding, new EndpointAddress(address)); subscribers.Add(proxy); ) return subscribers.ToArray(); } static stringE] GetSubscribersToContractEventOperation( string eventsContract. string eventoperation) { // Использует ADO.NET для создания запроса к SQL Server // обо всех подписчиках события } static Binding GetBindingFromAddresststring address) { if(address.StartsW1th("http:") || address.StartsWithC"https:”)) продолжение
572 Приложение Б. Схема «публикация/подписка» { WSHttpBinding binding = new WSHttpBinding( SecurityMode.Message,true); binding.ReliableSession.Enabled = true; binding.TransactionFlow = true; return binding; } i f (address.StartsWi th("net.tcp: ”)) { NetTcpBinding binding = new NetTcpBi ndi ng(Securi tyMode.Message.true); binding.ReliableSession.Enabled = true; binding.TransactionFlow = true; return binding; } /* Аналогичный код для привязок MSMQ и именованных каналов */ Debug.Assert(false,“Unsupported protocol specified”); return null; } //... } Чтобы создать посредников для всех подписчиков, методу GetPersistentList() необходимы адрес подписчика, привязка и контракт. Конечно, контракт опре- деляется параметром типа SubscriptionManager<T>. Для получения адресов Get- PersistentList() обращается с запросом к базе данных посредством GetSubscri- bersToContractEventOperation() и возвращает массив всех адресов постоянных подписчиков, подписанных на указанное событие. Далее GetPersistentList() оста- ется лишь получить привязки, используемые каждым подписчиком. Для этого GetPersistentList() вызывает вспомогательный метод GetBindingFromAddress(), ко- торый вычисляет привязку по адресной схеме. GetBindingFromAddress() предпо- лагает, что для всех адресов HTTP используется WSHttpBinding. Кроме того, GetBindingFromAddress() включает для каждой привязки надежную доставку и распространение транзакций, чтобы событие включалось в транзакцию изда- теля в тех случаях, когда односторонние операции не используются, как со сле- дующим контрактом событий: [ServiceContract] interface IMyEvents { [Operationcontract] [Transact!onFlow(Transact!onFlowOption.Al lowed)] void OnEventlO; [Operationcontract] [Transact!onFlow(T ransacti onFlowOpti on.Al 1 owed)] void 0nEvent2(int number); [Operationcontract] [Transacti onFlow(T ransact i onFlowOpti on.Al 1 owed)] void 0nEvent3(int number,string text); }
Инфраструктура «публикация/подписка» 573 Администрирование постоянных подписчиков Хотя постоянную подписку можно оформлять и прекращать на стадии выпол- нения с использованием методов интерфейса IPersistentSubscriptionService, пред- ставленного в листинге Б.З, именно из-за ее постоянной природы такую под- писку логичнее оформлять на административном уровне. Для этого в интерфейс IPersistentSubscriptionService были включены дополнительные операции для об- работки запросов к хранилищу данных подписки. Они представлены в листин- ге Б.8. Листинг Б.8. Интерфейс IPersistentSubscriptionService [DataContract] public struct PerslstentSubscrlptlon { [DataMember] public string Address: [DataMember] public string EventsContract; [DataMember] public string Eventoperation: } [ServiceContract] public Interface IPersistentSubscriptionService { //Административные операции [OperationContract] [T ransact1onFlow( T ransact1onFlowOpt1 on.Al 1 owed)] Pers1stentSubscr1pt1on[] GetAllSubscribers( ): [OperationContract] [T ransact1onFlow(T ransact1onFlowOpt1 on.Al 1 owed)] Pers1stentSubscr1pt1on[] GetSubscr1bersToContract(string eventsContract): [OperationContract] [T ransact1onFlow(Transact1onFlowOpt1 on.Al 1 owed)] str1ng[] GetSubscribersToContractEventType(string eventsContract. string eventoperation): [OperationContract] [Transact1onFlow(T ransact1onFlowOpt1 on.Al 1 owed)] PerslstentSubscrlpt1on[] GetAl1SubscribersFromAddress(string address): //... } Во всех административных операциях используется простая структура данных PersistentSubscription, содержащая адрес подписчика, подписанный контракт и со- бытие. Метод GetAlLSubscribers() просто возвращает список всех подписчиков. Метод GetSubscribersToContract() возвращает всех подписчиков определенного контракта, a GetSubscribersToContractEventType() возвращает всех подписчиков определенной операции события в заданном контракте. Наконец, для полноты
картины метод GetAUSubscribersFromAddress() возвращает всех подписчиков, п доставивших заданный адрес. Моя инфраструктура публикации/подписки вк. чает программу администрирования постоянной подписки Persistent Subscri on Manager, показанную на рис. Б.2. Рис. Б.2. Приложение Persistent Subscription Manager Программа использует интерфейс IPersistentSubscriptionService для добав ния и удаления подписки. Чтобы добавить новую подписку, необходимо и доставить адрес обмена метаданными из определения контракта событий. , пускается использование как адреса обмена метаданными самих постоянг подписчиков, так и адреса обмена метаданными службы публикации (вр приведенной в листинге Б.5), потому что эти адреса полиморфны. Введите зовый адрес обмена метаданными в поле МЕХ Address и щелкните на кно: Lookup. Программа автоматически загружает метаданные службы событий и полняет поля Contract и Event. Чтение метаданных и их разбор осуществляю классом MetadataHelper, представленным в главе 2. Введите адрес постоянного подписчика и щелкните на кнопке Subscr Программа добавляет подписку посредством обращения к службе подпив (служба MySubscriptionService в рассмотренном примере). Адрес службы подп ки хранится в конфигурационном файле Persistent Subscription Manager. ПРИМЕЧАНИЕ -------------------------------------------------------------------- Схема «публикация/подписка» также обеспечивает изоляцию компонентов системы в отноше безопасности. Все подписчики должны пройти аутентификацию в единой службе публикации (е личие от использования разных механизмов безопасности для разных подписчиков). В свою < редь, подписчики должны разрешить доставку события всего одной службе публикации, а не е издателям в системе; служба публикации отвечает за аутентификацию и авторизацию издате Применяя ролевую безопасность к службе публикации, вы можете легко обеспечить центре зованное выполнение разных правил, определяющих, кому разрешается публикация собы в системе.
Инфраструктура «публикация/подписка» 575 Подписка и публикация с очередями Вместо того чтобы использовать синхронные привязки при подписке или пуб- ликации событий, также можно воспользоваться привязкой NetMsmqBinding. Служба публикации/подписки с очередями объединяет преимущества систем с логической изоляцией компонентов с гибкостью отключенных взаимодейст- вий. Конечно, при использовании событий с очередями все события контракта должны быть помечены как односторонние операции. Как показано на рис. Б.З, очереди могут использоваться на обеих сторонах независимо друг от друга. Рис. Б.З. Публикация/подписка с очередью Издатель с очередью может использоваться в сочетании с подключенными синхронными подписчиками. И наоборот, подключенный издатель может ис- пользоваться в сочетании с подписчиками с очередью, или же очереди могут использоваться как на стороне подписчика, так и на стороне издателя. Однако учтите, что временная подписка с очередью невозможна — привязка MSMQ не поддерживает дуплексные обратные вызовы, поскольку это сделало бы беспо- лезной саму природу коммуникаций с очередями. Как и прежде, для управле- ния подписчиками можно использовать административную программу, а адми- нистративные операции остаются синхронными. Публикация с очередью Чтобы использовать издателя с очередью, служба публикации должна предо- ставить конечную точку с очередью, использующую привязку MSMQ. При вы- даче событий издателем с очередью служба публикации может быть временно недоступна или сам публикующий клиент может быть отключен. При публика- ции двух событий в службу публикации с очередью порядок доставки и обра-
576 Приложение Б. Схема «публикация/подписка» ботки этих событий конечными подписчиками не гарантирован. Предположе- ния о порядке публикации возможны только в том случае, если контракт событий настроен на использование сеанса и только при использовании одной публикующей службы. Подписка с очередью Для обслуживания подписчика с очередью служба постоянной подписки долж- на предоставить конечную точку с очередью. Это позволит подписчику нахо- диться в отключенном состоянии даже тогда, когда сам издатель подключен. При повторном подключении подписчик получает все события, поставленные в очередь. При выдаче нескольких событий, направленных одному подписчику с очередью, порядок их доставки не гарантирован. Подписчик может делать предположения о порядке публикации только в том случае, если в контракте событий предусмотрена поддержка сеанса. Конечно, если и издатель, и подпис- чик используют очереди, они оба могут одновременно находиться в отключен- ном состоянии.
В Стандарты программирования WCF Подробные стандарты программирования чрезвычайно важны для успешного создания конечного продукта. Стандарт помогает соблюдать рекомендации и избегать ловушек, а также упрощает распространение полезных знаний среди участников группы. Традиционно стандарты программирования представляют собой толстые, тяжеловесные документы из сотен страниц, на которых приво- дятся обоснования по каждому пункту. Конечно, даже такой документ — луч- ше, чем ничего, и все же такие монументальные труды обычно не по зубам сред- нему разработчику. Напротив, представленный здесь стандарт программирова- ния WCF подробно отвечает на вопрос «что», но практически не объясняет, «почему». На мой взгляд, полное понимание всех нюансов, влияющих на то или иное решение, может потребовать чтения книг и даже нескольких лет прак- тической работы, тогда как для применения стандарта это неприемлемо. При- нимая в свою группу нового разработчика, вы просто указываете ему на стан- дарт и говорите: «Сначала прочитайте это». Умение соблюдать хороший стандарт должно предшествовать его полному пониманию — последнее прихо- дит со временем и с опытом. В стандарте программирования, представленном далее, отражены основные правила «делай так» и «так не делай», отражены ре- комендации и потенциальные проблемы. В нем задействованы многие практи- ческие приемы и вспомогательные классы, обсуждавшиеся в книге. Общие рекомендации из области проектирования 1. При проектировании служб необходимо соблюдать следующие принципы: • службы должны быть безопасными; • службы должны оставлять систему в стабильном состоянии; • службы должны быть потоково-безопасными; • службы должны быть надежными; • службы должны быть устойчивы к ошибкам.
578 Приложение В. Стандарты программирования WCF 2. По возможности желательно соблюдать дополнительные принципы: • службы должны быть совместимы; • службы должны быть масштабно-инвариантны; • службы должны быть доступны; • службы должны обладать разумным временем отклика; • службы должны работать в нормальных временных рамках. 3. Избегайте контрактов сообщений. Основные положения 1. Размещайте код службы в библиотеке классов, а не в хостовом ЕХЕ-файле. 2. Не определяйте параметризованные конструкторы для класса службы, если только он не является синглетным. 3. Включайте надежность в привязках, которые поддерживают такую возмож- ность. 4. Определяйте содержательные имена контрактов. Для служб с внешними пользователями включайте URL-адрес своей компании или эквивалентный URN с годом и месяцем для поддержки контроля версии; например: [Serv1ceContract(Namespace = "http://www.ides1gn.net/2007/08")] Interface IMyContract {•••} 5. Для интрасетевых служб используйте осмысленное уникальное имя, например: [ServiceContract(Namespace = "MyApplication")] Interface IMyContract {...} 6. Для интрасетевых приложений, работающих под управлением Windows ХР и Windows Server 2003, отдавайте предпочтение автохостингу перед хостин- гом в IIS. 7. В Windows Vista хостинг WAS (IIS7) предпочтительнее автохостинга. 8. Используйте класс ServiceHost<T>. 9. Включайте обмен метаданными. 10. Всегда указывайте имена всех конечных точек в конфигурационном файле клиента. И. Не используйте SvcUtil или Visual Studio 2005 для построения конфигура- ционного файла. 12. Не дублируйте код посредника. Если два и более клиента используют один контракт, выделите посредника в отдельную библиотеку классов. 13. Всегда закрывайте или уничтожайте посредника. Контракты служб 1. Всегда применяйте ServiceContractAttribute к интерфейсу, а не к классу: //Нежелательно: [ServiceContract] class MyService
Контракты данных 579 ( [Operationcontract] public void MyMethodO {...} } //Правильно: [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodO: } class MyService : IMyContract { public void MyMethodO {•••} } 2. Имя контракта службы должно начинаться с буквы I: [ServiceContract] interface IMyContract {•••} 3. Избегайте операций, являющихся аналогами свойств: //Нежелательно: [ServiceContract] interface IMyContract { [Operationcontract] string GetNameO: [Operationcontract] void SetName(string name): } 4. Избегайте контрактов, состоящих из одного компонента. 5. Контракт службы в среднем должен содержать от 3 до 5 компонентов. 6. Контракт службы не должен содержать более 20 компонентов. Вероятно, на практике верхний предел составляет 12. Контракты данных 1. Используйте атрибут DataMemberAttribute только для свойств и открытых по- лей, доступных только для чтения. 2. Избегайте явной сериализации XML в своих типах. 3. При использовании свойства Order присваивайте одинаковое значение всем компонентам, находящимся на одном уровне иерархии классов. 4. Поддерживайте lExtensibleDataObject в своих контрактах данных. 5. Не задавайте свойство IgnoreExtensionDataObject атрибутов ServiceBehavior и CallbackBehavior равным true. Сохраняйте используемое по умолчанию зна- чение false. 6. Не помечайте делегатов и события как поля данных.
580 Приложение В. Стандарты программирования WCF 7. Не передавайте специфические типы .NET (например, Туре) в параметрах операций. 8. Операции не должны принимать типы ADO.NET DataSet и DataTable (или их типизованные субклассы). Используйте естественное представление — на- пример, массив. 9. Откажитесь от использования хеш-кода для параметра типа; вместо этого следует предоставлять содержательное имя. 10. Не используйте ключ /ct программы SvcUtil или любой другой способ со- вместного использования типа сборки через границу службы. Управление экземплярами 1. Отдавайте предпочтение режиму создания экземпляров уровня вызова. 2. Избегайте сеансовых служб. 3. Выбирая для контракта режим SessionMode.Required, всегда явно задавайте для службы режим InstanceContextMode.PerSession. 4. Выбирая для контракта режим SessionMode.NotAllowed, всегда настраивайте службу в режиме InstanceContextMode.PerCall. 5. Не смешивайте сеансовые контракты с контрактами уровня вызова для од- ной службы. 6. Старайтесь не использовать синглетные службы, если только служба не яв- ляется синглетной по своей природе. 7. Используйте упорядоченную доставку для сеансовых служб. 8. Избегайте деактивизации экземпляров в сеансовых службах. 9. Старайтесь не использовать демаркационные операции. Операции и вызовы 1. Не рассматривайте односторонние вызовы как асинхронные. 2. Не рассматривайте односторонние вызовы как параллельные. 3. Предусмотрите возможность выдачи исключений односторонней операцией. 4. Включайте надежность даже для односторонних операций. Упорядоченная доставка для односторонних вызовов не обязательна. 5. Избегайте односторонних операций в сеансовых службах. Если такая опера- ция все же используется, сделайте ее завершающей: [ServiceContract(SessionMode = SessionMode.Required)] interface JOrderManager { [Operationcontract] void SetCustomerlddnt customerld); [OperationContractdsInitiating - false)] void Addltem(int itemld): [OperationContractdsInitiating - false)] decimal GetTotaK):
Операции и вызовы 581 [OperationContractdsOneWay » true.Islnitiating » false, IsTerminating » true)] void ProcessOrdersO; } 6. Присвойте контракту обратного вызова имя, состоящее из контракта служ- бы и суффикса Callback: interface IMyContractCallback {...} [ServiceContract(CallbackContract = typeof(IMyContractCalIback))] interface IMyContract {•••} 7. Старайтесь помечать операции обратного вызова как односторонние. 8. Используйте контракты обратного вызова только для обратных вызовов. 9. Старайтесь не смешивать обычные обратные вызовы и события в одном кон- тракте обратного вызова. 10. Операции событий должны быть хорошо спроектированы: • тип возвращаемого значения void; • отсутствие выходных параметров; • пометка как односторонних операций. 11. Избегайте использования низкоуровневых контрактов обратного вызова для управления событиями; вместо них следует использовать инфраструктуру публикации/подписки. 12. Всегда предоставляйте специальные методы для подключения/отключения обратного вызова: [ServiceContract(Cal 1backContract » typeof(IMyContractCal1 back))] interface IMyContract { [Operationcontract] void DoSomethingO; [Operationcontract] void ConnectO; [Operationcontract] void DisconnectO; } interface IMyContractCallback {...} 13. Используйте типизованный класс DuplexClientBase<T,C> вместо DuplexClient- Base<T>. 14. Используйте типизованный класс DuplexChannelFactory<T,> вместо DuplexChan- nelFactory<T>. 15. В процессе отладки, а также при интрасетевом развертывании обратных вы- зовов через привязку WSDualHttpBinding используйте атрибут CallbackBaseAd- dressBehaviorAttribute с нулевым значением CallbackPort: [CallbackBaseAddressBehavior(CallbackPort - 0)] class MyClient ; IMyContractCallback {•••}
582 Приложение В. Стандарты программирования WCF Сбои 1. Никогда не используйте экземпляр посредника после исключения, даже если исключение было перехвачено. 2. Не используйте канал обратного вызова после исключения, даже если ис- ключение было перехвачно. 3. Используйте FaultContractAttribute с классами исключений, в отличие от обыч- ных сериализуемых типов: //Нежелательно: [OperationContract] [FaultCont гa ct(typeof(double))] double Divide(double numberl.double number2): //Правильно: [OperationContract] [FaultCont ract(typeof(DIvldeByZeroException))] double DivideCdouble numberl.double number?); 4. Избегайте продолжительной обработки в IErrorHandler.ProvideFault() (напри- мер, ведения журнала). 5. И в классах служб, и в классах обратного вызова задайте IncludeException- DetaiLInFauLts равным true в отладочных сеансах — либо в конфигурационном файле, либо на программном уровне: public class DebugHelper { public const bool IncludeExceptionDetailInFaults = #if DEBUG true; #else false; #endif } [ServiceBehavior(IncludeExceptionDetail InFaults = DebugHelper.IncludeExceptionDetailInFaults)] class MyService : IMyContract {...} 6. В окончательной версии неизвестные исключения не должны возвращаться как сбои (кроме диагностики). 7. Рассмотрите возможность использования ErrorHandlerBehaviorAttribute с клас- сом службы как для повышения исключений, так и для автоматической ре- гистрации ошибок: [ErrorHandlerBehavior] class MyService : IMyContract {...} 8. Рассмотрите возможность использования CalLbackErrorHandlerBehaviorAttribute с клиентом обратного вызова как для повышения исключений, так и для ав- томатической регистрации ошибок: [Cal 1backErrorHandlerBehavi or(typeof(MyClient))] public partial class MyClient : IMyContractCalIback { public void OnCallabckO
Транзакции 583 Транзакции 1. Никогда не используйте транзакции ADO.NET ADO.NET напрямую. 2. Применяйте атрибут TransactionFlowAttribute к контракту, а не к классу службы. 3. Не выполняйте транзакционные операции в конструкторе службы. 4. Настраивайте службы в транзакционном режиме «клиент» или «клиент/служ- ба» (в терминологии книги). Избегайте режимов «служба» и «без транзак- ций». 5. Настраивайте обратные вызовы в транзакционном режиме «служба» или «служба/обратный вызов» (в терминологии книги). Избегайте режимов «обратный вызов » и «без транзакций». 6. При использовании режима «клиент/служба» или «служба/обратный вы- зов» ограничивайте распространение транзакций привязкой при помощи ат- рибута BindingRequirement. 7. На стороне клиента всегда перехватывайте все исключения службы, настро- енной в транзакционном режиме «служба» или «без транзакций». 8. Постарайтесь всегда настраивать операции так, чтобы транзакции были как минимум разрешены: [ServiceContract] interface IMyContract { [Operationcontract] [T ransacti onFlow(Transacti onFlowOpti on.Al 1 owed)] void MyMethodK ...): } 9. Включайте надежность и упорядоченную доставку даже при использовании транзакций. 10. В операциях служб никогда не используйте ручную отмену транзакций при перехвате исключений: //Нежелательно: [OperationBehavior(TransactionScopeRequired = true)] public void MyMethodO { try ( } catch ( Transaction.Current.RollbackO ; } } И. Перехватив исключение в транзакционной операции, всегда перезапустите его (или другое исключение). 12. Всегда используйте стандартный уровень изоляции IsolationLeveLSerializable. 13. Не вызывайте односторонние операции из транзакций. 14. Не вызывайте нетранзакционные службы из транзакций.
584 Приложение В. Стандарты программирования WCF 15. Не обращайтесь к нетранзакционным ресурсам (например, файловой систе- ме) из транзакций. 16. Избегайте транзакционных сеансовых служб. 17. Отдавайте предпочтение транзакционным службам уровня вызова. 18. При работе с сеансовыми или транзакционными синглетными службами ис- пользуйте VRM для управления состоянием; избегайте явного программи- рования с контролем состояния или зависимости от деактивизации экземп- ляров по завершению в WCF. Управление параллельной обработкой 1. Всегда предоставляйте потоково-безопасный доступ: • к находящемуся в памяти состоянию сеансовых и синглетных служб; • к находящемуся в памяти состоянию клиента во время обратных вызо- вов; • к общим ресурсам; • к статическим переменным. 2. По возможности используйте режим ConcurrencyMode.Single (по умолчанию). Он разрешает транзакционный доступ и обеспечивает потоковую безопас- ность без каких-либо дополнительных усилий. 3. Делайте операции сеансовых и синглетных служб одиночного режима по возможности более короткими, чтобы предотвратить продолжительную бло- кировку других клиентов. 4. В режиме ConcurrencyMode.Multiple необходимо использовать автозавершение транзакций. 5. Рассмотрите возможность использования ConcurrencyMode.Multiple для служб уровня вызова, чтобы сделать возможными параллельные вызовы. 6. У транзакционных синглетных служб в режиме ConcurrencyMode.Multiple свой- ство ReleaseServicelnstanceOnTransactionComplete должно быть равно false: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, ReleaseServicelnstanceOnTransactionComplete - false)] class MySingleton : IMyContract {•••} 7. Никогда не используйте автохостинг в UI-потоках с вызовами служб из UI-при- ложений. 8. Не разрешайте обратные вызовы к UI-приложениям, вызвавшим службу, ес- ли только вызов не будет отправлен с использованием Synchronizationcontext. Post(). 9. Используйте асинхронные вызовы только на стороне клиента. 10. Если посредник поддерживает как синхронные, так и асинхронные методы, применяйте атрибут FaultContractAttribute только к синхронным методам. 11. Не смешивайте транзакции с асинхронными вызовами.
Службы с очередями 585 Службы с очередями 1. Перед вызовом службы с очередями всегда проверяйте доступность очереди (и очереди зависших сообщений, когда потребуется). Для проверки исполь- зуйте QueuedServiceHelper.VerifyQueue<T>(). 2. Всегда проверяйте доступность очереди при хостинге службы с очередью (класс ServiceHost<T> делает это автоматически). 3. Избегайте проектирования служб, рассчитанных на вызовы как с очередями, так и без них (кроме изолированных сценариев). 4. Включайте обмен метаданными для служб с очередями. 5. Служба должна участвовать в транзакции воспроизведения. 6. При участии в транзакции воспроизведения избегайте продолжительной об- работки в службе с очередью. 7. Избегайте использования сеансовых служб с очередями. 8. Для управления состоянием синглетной службы с очередью используйте VRM. 9. При использовании служб уровня вызовов с очередями следует явно на- строить контракт и службу на управление экземплярами на уровне вызова и отсутствие сеанса: [Serv1ceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract {...} [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai1)] class MyService : IMyContract {•••} 10. Всегда явно запрещайте использование сеанса в контрактах синглетных служб с очередями: [ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] class MyService : IMyContract {...} И. Вызовы службы с очередями должны осуществляться клиентом в транзак- ции. 12. На стороне клиента посредник службы с очередью не должен храниться в по- ле данных. 13. Избегайте относительно коротких значений TimeToLive, потому что они про- тиворечат самому смыслу использования служб с очередями. 14. Никогда не используйте неустойчивые (volatile) очереди. 15. При использовании ответных очередей служба должна участвовать в тран- закции воспроизведения и ставить ответ в очередь в этой транзакции. 16. Ответная служба должна участвовать в транзакции воспроизведения ответа. 17. Избегайте продолжительной обработки в ответных операциях с очередями.
586 Приложение В. Стандарты программирования WCF 18. При обработке повторяющихся сбоев службы отдавайте предпочтение ответ- ной службе перед службой с очередью вредных сообщений. 19. Никогда не полагайтесь на определенный порядок следования вызовов с оче- редями (кроме использования сеансовых контрактов и служб). Безопасность 1. Всегда защищайте сообщения, обеспечивайте их конфиденциальность и це- лостность. 2. В интрасетевом сценарии допускается использование транспортной безопас- ности без безопасности сообщений, но при этом должен быть выбран уро- вень защиты EncryptAndSign. 3. В интрасетевом сценарии избегайте применения перевоплощения. Задайте уровню перевоплощения значение TokenlmpersonationLeveLIdentification. 4. При использовании перевоплощения клиент должен использовать режим TokenlmpersonationLeveLImpersonation. 5. Используйте инфраструктуру декларативной безопасности, избегая ручной настройки. 6. Никогда не применяйте атрибут PrincipaLPermissionAttribute прямо к классу службы: // Работать не будет [PrincipalPermission(SecurityAction.Demand.Role = public class MyService : IMyContract {•••} 7. Избегайте выполнения критических операций, требующих авторизации, в кон- структоре службы. 8. Избегайте требования определенного пользователя (с требованием роли или без него): // Нежелательно: [PrincipalPermission(SecurityAction.Demand.Name = "John")] public void MyMethod( ) 9. He полагайтесь на ролевую безопасность в клиентских операциях обратного вызова. 10. С интернет-клиентами всегда используйте безопасность сообщений. И. Разрешайте клиентам согласование сертификата службы (используется по умолчанию). 12. Используйте провайдеров ASP.NET для нестандартных удостоверений. 13. Если вы разрабатывайте пользовательское хранилище удостоверений, офор- мите его в виде провайдера ASP.NET. 14. Проверяйте сертификаты с использованием механизма доверия равноправ- ных узлов.
Алфавитный указатель А Abort(), метод, 31 ACID, транзакции, 248 Action, свойство, 73 Add(), метод, 135 AddAllMexEndPoints(), метод (ServiceHost<T>), 47, 49 AddBindingParameters(), метод, 207 AddErrorHandler(), метод, 241 AddServiceEndPoint(), метод, 46 ADO.NET, 126 AffinitySynchronizer, класс, 355, 371 AllowedlmpersonationLevel, свойство, 483 Арр.Config, файл, 28 Application, класс DoEventsQ, 353 Open Forms, 341 Run(), 339 ApplicationName, свойство, 529 Apply Client Behavior(), метод, 243 ApplyDispatchBehavior(), метод, 240 ASMX, веб-службы, 34 ASP.NET, провайдеры, 505 AsyncState, свойство, 381 AsyncWaitHandle, свойство, 384 Authentication, свойство, 516 Authorization, свойство, 480 в BasicHttpBinding, класс, 33 BasicHttpSecurityMode, перечисление, 459 BeginClose() и BeginOpen(), методы, 31 Binary Formatter, класс, 91 BindingRequirement, атрибут, 270, 316 c С#, итераторы, 140 CallbackBaseAddressBehavior, атрибут, 206 CallbackBehavior, атрибут, 317 CallbackDispatchRuntime, свойство, 243 CallbackErrorHandlerBehavior, атрибут, 244 Callbackinstance, свойство, 189 CallbackPort, свойство, 206 CallbackThreadAffinityBehavior, атрибут, 369 Channel, свойство, 56 ChannelFactory<T>, класс, 61, 200, 442 ClientBase<T>, класс, 56 ClientBaseAddress, свойство, 204 ClientCertificate, свойство, 516 ClientCredentials, свойство, 473, 517 Close(), метод, 186 CollectionDataContract, атрибут, 138 COM, модель, 550 CommunicationException, класс, 217 Communicationobject, класс, 240 CommunicationObjectFaultedException, класс, 217 Complete(), метод, 290 ConcurrencyMode, свойство, 362 Connect() и Disconnect(), методы, 195 CreateChannel, метод, 62 CreateMessage(), метод, 232 CreateMessageFault(), метод, 232 Credentials, свойство, 474 Current, свойство, 443 DataContract, атрибут, 96 DataContractSerializer, класс, 92 DataContractSerializer<T>, класс, 93 DataMember, атрибут, 96 D DataSet, класс, 126 DataTable, класс, 126 DataTableHelper, класс, 129 DebugHelper, класс, 225 Deli very Failure, свойство, 418 DeliveryRequirements, атрибут, 68 DeliveryStatus, свойство, 418 DispatchRuntime, класс, 243, 361 Dispose(), метод, 56, 146 DLQ, 412
588 Алфавитный указатель DoS, атаки, 455 DTC, 260 Е EnableMetadataExchange, свойство, 46, 48 EndpointAddress, класс, 57 EnumMember, атрибут, 124 ErrorHandlerBehavior, класс, 241 ErrorHandlerBehaviorAttribute, класс, 239 ErrorHandlerHelper, класс, 234 ExceptionDetail, класс, 223 ExistsQ, метод, 49 F FaultContractAttribute, класс, 219 FaultException, класс, 217 FormHost<F>, класс, 350 G Generic Delegate, класс, 211 GenericEventHandler, 211 Generic Identity, класс, 476 GenericPrincipal, класс, 522 GetAnonymous(), метод, 476 GetCallbackChannel<T>(), метод, 192 GetEndpoints(), метод, 86 GetHeader<T>(), метод, 431 GetMetadata(), метод, 83 GetOperationsO, метод, 87 GetPersistentList(), метод, 570 GetTransientList(), метод, 570 GetUntypedHeader, метод, 434 H HasMexEndpoint, свойство, 47, 49 Host, свойство, 164 HTTP, адреса, 21 HTTPS, 456 I lAsyncResult, интерфейс, 381 IClientChannel, интерфейс, 159 ICommunicationObject, интерфейс, 62 I Contract Behavior, интерфейс, 358 I Dictionary, интерфейс, 141 IDisposable, интерфейс, 56 I DuplexContextChannel, интерфейс, 189 lEndpointBehavior, интерфейс, 244, 317 lErrorHandler, интерфейс, 231 lExtensibleDataObject, интерфейс, 122 lidentity, интерфейс, 476 IIS, 26, 50 Impersonate(), метод, 478 Impersonation, свойство, 479 ImportAllEndpoints(), метод, 83 IncludeExceptionDetaillnFaults, свойство, 224 IncomingMessageHeaders, свойство, 431 InnerChannel, свойство, 159 InProc Factory, класс, 63 IPC, 20 адреса, 21 IPersistentSubscriptionService, интерфейс, 564 IsAnonymous, свойство, 476 IsCompleted, свойство, 379 IServiceBehavior, интерфейс, 237, 531 IServiceBehavior, интерфейс, 44, 176 Islnitiating, свойство, 167 IsInRole(), метод, 486 IsolationLevel, свойство, 291 IsTerminating, свойство, 167 IsUserInRole(), метод, 507 к KeyedByTypeCollection<I>, класс, 176 KnownType, атрибут, 108 KTM, 260 L List<T>, класс, 135 LogBookService, класс, 235 LogError(), метод, 235 LTM, 260 M MaxConnections, свойство, 180 MaxReceivedMessageSize, свойство, 215 MaxRetryCycles, свойство, 421 MembershipProvider, класс, 506 Message, свойство, 465 MessageAuthenticationAuditLevel, свойство, 543 MessageQueue, класс, 392 MessageSecurityOverHttp, класс, 494
Алфавитный указатель 589 Metadata Explorer, программа, 49 MetadataExchangeClient, класс, 83 MetadataHelper, класс, 84 MetadataSet, класс, 83 МЕХ, 44 MSMQ адреса, 21 привязки, 34 MsmqBindingBase, класс, 402 MsmqMessageProperty, класс, 417 N МТОМ, 33 NACK, 413 Namespace, свойство, 25, 83 NetDataContractSerializer, класс, 93 NetMsmqBinding, класс, 34 NetMsmqSecurity, класс, 469 NetNamedPipeBinding, класс, 34 NetNamedPipeSecurity, класс, 468 NetPeerTcpBinding, класс, 34 NetTcpBinding, класс, 34 NetTcpSecurity, класс, 465 NonDual MessageSecurityOverHttp, класс, 494 NonSerialized, атрибут, 90 о OnDeserialized, атрибут, 106 OnOpeningQ, метод, 240 OperationBehavior, атрибут, 145, 481 OperationContext, класс, 158 OperationContextScope, класс, 433 OptionalReliableSession, класс, 160 OutcomingMessageHeaders, свойство, 431 р PrincipalPermissionMode, свойство, 487 PromoteException(), метод, 234 ProtectionLevel, свойство, 466 ProvideFault(), метод, 231 Purge(), метод, 392 PurgeQueue(), метод, 396 Q QueryContract(), метод, 84 QueuedDeliveryRequirements, свойство, 426 QueuedServiceHelper, класс, 393 R ReceiveErrorHandling, свойство, 421 ReceiveRetryCount, свойство, 421 ReleaselnstanceMode, свойство, 169 ReleaseServiceInstance(), метод, 172 ReliableSession, класс, 67, 160 RequireOrderedDelivery, свойство, 427 Response Act ion, свойство, 73 ResponseClientBase<T>, класс, 438 ResponseContext, класс, 431 RoleProvider, класс, 507 RoleProviderPrincipal, класс, 513 Rollback(), метод, 251 s SecureChannelFactory<T>, класс, 539 SecureClientBase<T>, класс, 537 SecureDuplexClientBase<T,C>, класс, 539 Security, свойство, 458 SecurityAuditEnabled, свойство, 544 SecurityBehavior, атрибут, 526 Serializable, атрибут, 90 ServiceAuthorizationBehavior, класс, 480 ServiceBehavior, атрибут, 123 ServiceCertificate, свойство, 499 ServiceContract, атрибут, 25, 187 ServiceCredentials, класс, 498 ServiceHost, класс, 28 ServiceHost<T>, класс, 31 ServiceSecurityContext, класс, 477 SessionMode, свойство, 153 SetThrottle(), метод, 178 SO A, 17 SoapFormatter, класс, 91 SQLServer, провайдер, 506 svc, расширение, 26 System.Transactions, пространство имен, 264 т TargetContract, свойство, 427 TCP адреса, 21 безопасный транспорт, 456 привязки, 34 TimeToLive, свойство, 413 TLS, 264, 335 TokenlmpersonationLevel, класс, 483
590 Алфавитный указатель Transactionlnformation, класс, 264 TransactionScopeRequired, свойство, 266 TransactionTimeout, свойство, 281 TransferMode, свойство, 213 и URI, 20 UserName, свойство, 502 UserNamePasswordValidationMode, свойство, 503 UseSynchronizationContext, свойство, 343 V Validate(), метод, 271, 531 VerifyCallback(), метод, 199 VerifyQueue<T>(), метод, 393 Vista, 413 Visual Studio 2005, 29, 50 w WaitHandle, класс, 384 WAS, 26, 32 WCF, 16 Web.Config, файл, 27 Windowsldentity, класс, 477 WSDualHttpBinding, класс, 34 WSDualHttpSecurity, класс, 496 WSFederationHttpBinding, класс, 34 WSHttpBinding, класс, 34 X X509, сертификат, 454 X509CertificateInitiatorClientCredential, класс, 517 X509CertificateInitiatorServiceCredential, класс, 516 X509CertificateRecipientClientCredential, класс, 500 X509Identity, класс, 514 XmlSerializerFormat, атрибут, 103 A авторизация, 454 адреса, 20 HTTP, 21 IPC, 21 MSMQ, 21 адреса (продолжение) TCP, 21 одноранговые сети, 22 ответная служба, 430 анонимные приложения, 520 архитектура, WCF, 59 асинхронный вызов операций, 372 аспекты поведения, 145 атомарность транзакций, 249 аутентификация, 453 Б безопасность, 453 авторизация, 454 анонимные приложения, 520 аутентификация, 453 бизнес-бизнес, 515 интернет-приложения, 493 интрасетевые приложения, 464 общая политика, 462 бизнес-бизнес, приложения, 515 брандмауэры, 515 в взаимная аутентификация, 455, 456 взаимная блокировка, 364 восстановление, 246 вредные сообщения, 420 голосование, 276 д двухфазовый протокол, 253 деактивизация экземпляров, 169 декларативная ролевая безопасность, 488 декларативное перевоплощение, 480 делегаты, 126, 336 демаркационные операции, 166 дуплексные посредники, 189 3 завершение транзакции, 276 заголовкисообщений, 431 запрос-ответ, операции, 181 защита сообщений, 498
Алфавитный указатель 591 И изолированность транзакций, 249 именованные каналы, 21, 34 интернет-приложения, 493 интерфейсы, 550 интрасетевые привязки, 461 интрасетевые приложения, 464 инфо-набор, 88 исключения, 216 асинхронная обработка, 384 односторонние операции, 182 к каналы, 59 клиенты, 18 коллекции, 134 конечные точки, 36, 561 контекст синхронизации, 335 контексты, 60 контракты данных, 22, 88 ошибок, 22 служб, 22, 70 событий, 565 сообщений, 22 контроль средств безопасности, 542 конфиденциальность сообщений, 455 корневая служба, 261 л личности, 453. 476 локальное хранилище потока, 264, 335 м массивы, 140 масштабируемость, 173 менеджеры ресурсов, 254 менеджеры транзакций, 252 метаданные, 18, 574 н надежность сообщений, 65 надежность, 65 односторонние операции, 181 сеанс, 153 транзакции, 255 наследование, 549 о общая политика, 462 общедоступные очереди, 391 односторонние операции, 181 окружающие транзакции, 264 операции обратного вызова, 186 ответная служба, 427, 430 отладка, 223 отмена транзакций, 247 очереди зависших сообщений, 412 очереди, 575 п параллельная обработка, 322 перевоплощение, 478 передача, безопасность, 455 повторный вход. 328 подписчики, 31 подтверждение (АСК), 413 потоки, 90 приватные очереди, 391 привязки, 32 выбор, 35 защита, 494 конечные точки, 36 стандартные, 33 транзакционные, 254 примитивные типы данных, 94 провайдеры роли, 507 р регулирование нагрузки, 174 реентерабельность, 328 ресурсы взаимная блокировка, 333 синхронность доступа, 332 транзакционные, 248 с сбои, 218 сеансовые службы, 151 сериализация, 88, 104 сертификаты, 454 синглетные службы, 161 синхронизация доступа, 332 синхронизация, 322 словари, 141 службы уровня вызова, 145
592 Алфавитный указатель служебно-ориентированные приложения, 17 события, 208 сериализация, 104, 550 согласованнность транзакций, 249 субъект безопасности, 486 тайм-аут, 413 тестовые сертификаты, 518 типы данных, 94, 109 транзакции, 247, 397 транзакция воспроизведения, 397 транспортные протоколы, 18 транспортные схемы, 20 У удостоверения Windows, 461 управление личностью, 492 управление экземплярами, 144 уровни изоляции, 279 устойчивость транзакций, 250 ф фрагментарная блокировка, 326 X хостовой процесс, 26 хосты архитектура, 60 каналы, 59 регулирование нагрузки, 174 ш шифрование, 498 э электронный ключ, 461 я явная деактивизация, 172
O’REILLY* Создание служб WCF Построение СОА с использованием Windows Communication Foundation В этой книге подробно рассказано о Windows Communication Foundation — платформе, которую разработчики из компании Microsoft возвели в качестве фундамента для создания пользова- тельских приложений и служб. Материал излагается с той точностью, преподавательским мастерством и архитектурной строгостью, которые принесли автору заслуженную известность во многих странах. Джувел Лёве — один из самых выдающихся экспертов современности в области распределенных систем, и это замечательно, что он посвятил свою книгу Windows Communication Foundation. Тема: Платформа .NET Уровень пользователя: опытный С^ППТЕР Заказ книг: 197198, Санкт-Петербург, а/я 619 тел.: (812) 703-73-74, postbook@piter.com 61093, Харьков-93, а/я 9130 тел.: (057) 758-41-45, 751-10-02, piter@kharkov.piter.com www.piter.com — вся информация о книгах и веб-магазин