Текст
                    Программирование
сетевых приложений
на C++
Том 2
Систематическое повторное использование:
АСЕ и каркасы
Дуглас Шмидт
Стивен Хьюстон
Предисловие Фрэнка Бушманна
Серия C++ In-Depth ♦ Бьерн Страуструп

Программирование сетевых приложений на C++ Том 2
C++ Network Programming Volume 2 Systematic Reuse with ACE and Frameworks Douglas C. Schmidt Stephen D. Huston A Addison-Wesley Boston • San Francisco • New York • Toronto • Montreal London • Munich • Paris ’ Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Дуглас С. Шмидт Стивен Д. Хьюстон Программирование сетевых приложений на C++ Том 2 Перевод с английского под редакцией А. П. Караваева Москва Издательство БИНОМ 2007
УДК 004.43 ББК 32.973.26-018.1 Ш73 Д. Шмидт, С. Хьюстон Программирование сетевых приложений на C++. Том 2. — М.: ООО «Бином-Пресс», 2007. — 400 с.: ил. В книге излагается один из самых перспективный подходов к профессиональному програм- мированию сетевых приложений на C++. Этот подход связан с применением АСЕ ПО промежуточного слоя с открытыми исходными кодами, которое является одним из наиболее переносимых и широко используемых инструментальных средств сетевого программирования на C++ в мире. Основные темы второго тома объектно-ориентированные каркасы и системати- ческое повторное использование ПО. Концептуальное изложение основных идей и принци- пов, раскрывающих роль каркасов в проектировании сетевых приложений, сочетается с прак- тическим руководством по их применению. Книга адресована разработчикам-практикам, которым необходимо в сжатые сроки и без голово- ломных трудностей создавать гибкие и эффективные сетевые приложения. Книга будет также полезна студентам старших курсов, аспирантам и всем заинтересованным в изучении и систематизации мате- риала, связанного с применением языка C++, объектно-ориентированного подхода, паттернов, ин- терфейсных фасадов и каркасов АСЕ при разработке сетевого программного обеспечения. Authorized translation from the English language edition, entitled C++ Network Programming, Volume 2: Systematic Reuse with ACE and Frameworks, First Edition by Schmidt, Douglas C., published by Pearson Education, Inc, publishing as Addison Wesley Professional, Copyright © 2003 by Addison-Wesley. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Russian language edition published by Binom Publishers. Copyright © 2004 by Binom Publishers. Все права защищены. Никакая часть этой книги не может быть воспроизведена в любой форме или любыми средства- ми, электронными или механическими, включая фотографирование, магнитную запись или иные средства копирова- ния или сохранения информации без письменного разрешения издательства. ISBN 978-5-9518*0208-8 (русск.) ISBN 0-201-79525-6 (англ.) Authorized translation from the English language edition © Original Copyright. Addison-Wesley, 2002 © Издание на русском языке. Издательство БИНОМ, 2003 Научно-техническое издание Дуглас С. Шмидт, Стивен Д. Хьюстон Программирование сетевых приложений на C++*. Том 1 Компьютерная верстка К.А. Свиридова Подписано в печать 18.03.2003. Формат 70X100 Усл. печ. л. 32,5. Бумага газетная. Печать офсетная. Тираж 3000 экз. Заказ/ОЗУ. Издательство «Бином-Пресс», 2003 г. 170026, Тверь, Комсомольский просп., 12 Отпечатано в ГУП И ПК «Ульяновский дом печати» 432601, г. Ульяновск, ул. Гончарова, 14
Содержание Предисловие..................................................7 Об этой книге...............................................11 Г лава 1. Объектно-ориентированные каркасы и сетевые приложения..........................................21 1.1 Общее представление об объектно-ориентированных каркасах............................................21 1.2 Сравнение методов разработки и повторного использования ПО.................................. 25 1.3 Применение каркасов в сетевых приложениях...........35 1.4 Экскурс в АСЕ-каркасы...............................37 1.5 Пример: сетевая служба регистрации..................42 1.6 Резюме..............................................45 Глава 2. Аспекты проектирования: службы и конфигурации. . . 47 2.1 Асцекты проектирования: службы и серверы............48 2.2 Аспекты проектирования: конфигурации................59 2.3 Резюме..............................................64 Глава 3. ACE-каркас Reactor..................................65 3.1 Обзор...............................................65 3.2 Класс ACE_Time_Value................................68 3.3 Класс ACE_Event_Handler.............................74 3.4 ACE-классы очередей таймеров........................88 3.5 Класс ACE_Reactor...................................99 3.6 Резюме............................................ 116 Глава 4. Реализации АСЕ Reactor.............................119 4.1 Обзор..............................................119 4.2 Класс ACE_Select_Reactor......................... 121
6 Программирование сетевых приложений на C++. Там 2 4.3 Класс ACE_TP_Reactor................................132 4.4 Класс ACE_WFMO_Reactor .............................136 4.5 Резюме..............................................148 Глава 5. АСЕ-каркдс Service Configurator.....................151 5.1 Обзор...............................................151 5.2 Класс ACE_Service_Object............................154 5.3 Классы ACE_Service_Repository.......................163 5.4 Класс ACE_Service_Config............................175 5.5 Резюме. ............................................192 Глава 6. АСЕ-каркасТавк......................................193 6.1 Обзор...............................................193 6.2 Класс ACE_Message_Queue. ...........................195 6.3 Класс ACE_Task......................................224 6.4 Резюме..............................................244 Глава 7. ACE-каркас Acceptor-Connector.......................245 7.1 Обзор.............................................. 245 7.2 Класс ACE_Svc_Handler...............................248 7.3 Класс ACE_Acceptor..................................260 7.4 Класс ACE_Connector........................ ........273 7.5 Резюме. ........................................... 302 Глава 8. ACE-каркас Proactor...................................303 8.1 Обзор .......'.................................... 303 8.2 Классы-фабрики асинхронного ввода/вывода............308 813 Класс ACE_Handler...................................318 8.4 Классы Proactive Acceptor-Connector. . .-...-.......326 8.5 Класс ACE_Proactor..................................335 8.6 Резюме..............................................346 Глава 9. ACE-каркас Streams.................................. 347 9.1 Обзор...............................................347 9.2 Класс ACE_Module....................................350 9.3 Класс ACE_Stream....................................365 9.4 Резюме............................................. 370 Словарь терминов............................................373 Англо-русский указатель терминов............................385 Литература..................................................389
Предисловие Инструментальное средство ADAPTIVE Communication Environment (АСЕ) достигло небывалого успеха в области ПО промежуточного слоя, используемо- го в сетевом вычислениях. Благодаря его гибкости, производительности, охва- ту платформ и другим ключевым свойствам, АСЕ пользуется большой попу- лярностью у сообщества разработчиков сетевых приложений. Об этом свиде- тельствует хотя бы то, что его используют в тысячах приложений, во многих странах и в десятках прикладных областей. АСЕ привлекает к себе большое внимание и за пределами сообщества разработчиков промежуточного слоя как образец применения ПО с открытыми исходными кодами в высококачествен- ных, использующих современные методы проектирования, ориентированных на паттерны программных архитектурах. Но почему АСЕ пользуется таким успехом? Достойный внимания ответ на этот вопрос потребует некоторых размышлений. Для начала давайте вспомним предисловие к первому тому «Программирование сетевых приложений на C++. Профессиональный подход к проблеме сложности: АСЕ и паттерны» [C++NPvl ] и разовьем аналогию с системами общественного транспорта, кото- рую использует там мой коллега Стив Виноски (Steve Vinoski). Стив прав, что высококачественные системы общественного транспорта состоят не только из самолетов, аэропортов, поездов, вокзалов и рельсов. Нужна еще инфраструкту- ра (она меньше бросается в глаза): составление расписаний, выбор маршрутов, продажа билетов, техническое обслуживание и диспетчерское управление. Но даже полного комплекта составляющих недостаточно для создания эффектив- ных систем общественного транспорта. Организовать эти составляющие таким образом, чтобы они полностью удовлетворяли своей первоначальной цели — быстрой и надежной транспортировке людей — вот не менее важная задача. Понравилось бы вам пользоваться системой общественного транспорта, би- летные кассы которой находились бы в паровозных депо и мастерских или в ан- гарах аэродромов, а расписания и маршруты не были бы известны широкой публике? Сомневаюсь! Успех общественных транспортных систем зависит не только от знания имеющихся элементов инфраструктуры — он зависит еще и от того, как эти разнородные элементы объединяются в единое целое и интегрируются с той средой, в которой они функционируют. Это знание дает возможность создате- лям общественных транспортных систем объединять отдельные элементы в более высокоуровневые строительные блоки и эффективно их соединять. На- пример, билетные кассы, справочные табло, багажные отделения и коробки зданий объединяются в вокзалы, которые размещаются в центрах городов или крупных пригородных центрах. Аэропорты тоже чаще размещаются вблизи больших городов и соединяются с центром регулярно курсирующими экспрес- сами. Да и сами учреждения общественного транспорта организуются так, чтобы их работа была эффективной. Например, когда входишь на вокзал или в аэро- порт, видишь там транспортные агентства, информационные центры, табло
8 Программирование сетевых приложений на C++. Том 2 расписаний. Видишь магазины, торгующие тем, что необходимо во время по- ездки. При входе на главный вокзальный перрон или главный вестибюль аэро- порта — опять информационные центры, постоянно обновляемая информа- ция о расписании, платформы для посадки на поезда и самолеты. Учреждения общественного транспорта, таким образом, не только предлагают услуги, необ- ходимые для того, чтобы начать и закончить поездку, но и эффективно органи- зуют свои внутренние «потоки управления». Если основные структуры и пото- ки управления на большинстве вокзалов и аэропортов похожи, их конкретные реализации могут сильно отличаться. Тем не менее, все мы узнаем образы (пат- терны!) учреждений общественного транспорта мгновенно, так как все они со- храняют неизменным то основное, что мы, благодаря долгому опыту, хорошо знаем. Итак, что объединяет успешно работающие системы общественного транс- порта и успех АСЕ? Ответ прост: кроме базовых элементов сетевых вычислений (интерфейсных фасадов, которые Дат и Стив представили в [C++NPvl]), АСЕ включает также объектно-ориентированные каркасы, которые создаются по- верх этих интерфейсных фасадов и обеспечивают реализацию полезных ком- муникационных служб более высокого уровня, таких как демультиплексирова- ние и диспетчеризация событий, управление соединениями, конфигурирова- ние служб, параллелизм и иерархическая многоуровневая конвейерная обработка. Службы, предоставляемые каркасами АСЕ, удовлетворяют многим потребностям сетевого ПО за счет эффективной организации структур и внут- ренних потоков управления приложений с помощью основных паттернов, сформировавшихся как результат многолетней работы. -Использование каркасов АСЕ обеспечивает несколько важных преиму- ществ: • Не нужно разрабатывать то, что уже сделано в АСЕ. Это экономит много времени и усилий. Благодаря этому можно сосредоточиться на своих ос- новных обязанностях: реализации функциональности приложения, кото- рая востребована клиентами и конечными пользователями. • Каркасы АСЕ воплощают в себе большой опыт сетевого программирова- ния, который Даг, Стив и их коллеги приобрели за несколько десятиле- тий. В частности, каркасы АСЕ обеспечивают эффективную реализацию стандартных классов, взаимосвязей между ними и общих для всех сете- вых приложений потоков управления. Каркасы АСЕ регулярно тестиру- ются тысячами пользователей по всему миру, благодаря чему в них по- стоянно вносится много полезных исправлений и улучшений. Все это могут использовать в своих приложениях те, кто работает с АСЕ. • Каркас — не каркас, если он не может быть приспособлен к конкретным потребностям, пользователя. Это значит, что каркасы АСЕ можно на- страивать. Например, ACE-каркас Reactor может быть настроен на ис- пользование различных функций демультиплексирования событий, та- ких как select() или WaitForMultipleObjects(). Аналогичным образом, ACE-каркас Acceptor-Connector может быть сконфигурирован с различ- ными механизмами IPC. Хотя такая адаптируемость выгодна сама по
Предисловие 9 себе, АСЕ идет на шаг дальше: для многих вариантов адаптации можно конфигурировать нужные стратегии функционирования из уже имею- щихся взаимозаменяемых реализаций. Например, в дополнение к раз- личным реализациям Reactor, упомянутым выше, АСЕ предоставляет интерфейсные фасады для механизмов IPC, таких как Sockets, SSL, TLI и совместно используемая память, которые помогают настраивать АСЕ- каркас Acceptor-Connector на конкретные платформы и приложения. • Последнее, по порядку, но не по значению, каркасы АСЕ не являются замкнутыми. Их можно пытаться объединять какими-то новыми спосо- бами для того, чтобы создавать сетевые приложения и совершенно новые типы промежуточного слоя. Например, можно объединить каркас Reactor с каркасом Acceptor-Connector, чтобы отделить установление со- единений от функций служебной обработки в событийно-управляемых приложениях. Аналогичным образом, можно вводить в свои приложе- ния различные формы параллелизма с помощью ACE-каркаса Task. За годы консультирования и руководства многими программными проек- тами я понял, что АСЕ значительно упрощает применение легко настраиваемо- го на потребности сетевых приложений повторно используемого промежуточ- ного слоя. Не всем сетевым приложениям нужен «тяжеловесный» промежуточ- ный слой, типа серверов приложений, web-служб и сложных компонентных моделей. Но большинство сетевых приложений может выйграть от примене- ния переносимого и эффективного промежуточного слоя инфраструктуры хоста, такого как АСЕ. Эта гибкость — основа успеха АСЕ. Не нужно обреме- нять свои приложения полным комплектом промежуточного слоя, если вы его не используете весь. Вместо этого, можно использовать только основные клас- сы промежуточного слоя АСЕ, которые действительно нужны для создания приложений, пусть небольших, но делающих то, что нужно. По этой причине я предсказываю, что АСЕ будет, по-прежнему, широко использоваться и после того, как влияние сегодняшнего крупномасштабного промежуточного слоя уже станет убывать. Невероятная гибкость АСЕ не приводит к росту числа несовместимых реа- лизаций промежуточного слоя. Например, если вы создаете встроенную систе- му, которая «общается» с внешним миром с помощью протокола CORBA ПОР (Internet Inter-ORB Protocol), то можете использовать The АСЕ ORB (TAO), ко- торый является совместимым с CORBA брокером объектных запросов (ORB), работающим в реальном времени, созданным с помощью интерфейсных фаса- дов и каркасов АСЕ. Если же применение CORBA является для вашего приложе- ния избыточным, вы можете сами создать специализированный, интеропера- бельный промежуточный слой с помощью соответствующих классов АСЕ. Оба решения базируются, по сути, на одних и тех же структурах и протоколах, таких как классы АСЕ Common Data Representation (CDR) и интерфейсные фасады TCP/IP Socket. Они могут также органично взаимодействовать друг с другом, как вы — съездить из Парижа в Стамбул, знаменитый «Восточный экспресс», и пересечь многие европейские страны, не меняя поезда из-за несовместимости железнодорожных путей.
10 Программирование сетевых приложений на C++. Том 2 И Стив Виноски (Steve Vinoski) и я, мы оба, отмечаем много общего между высококачественными общественными транспортными системами и высоко- качественным сетевым промежуточным слоем. Для меня и тысяч других разра- ботчиков на C++ во всем мире, АСЕ является ИНСТРУМЕНТОМ для создания последнего! И теперь, после того как сказано так много хорошего об АСЕ, да- вайте вернемся к основной цели нашего предисловия — к представлению вто- рого тома «Программирование сетевых приложений на C++» [C++NPv2]. Для всех программных технологий и промежуточного слоя справедлива одна и та же истина: чем лучше владеешь инструментом, тем лучше его используешь. Применение АСЕ в приложениях, — это только один из аспектов совершенст- вования сетевого ПО. Чтобы извлечь существенную выгоду из многих неоспо- римых достоинств АСЕ, требуется также твердое знание основных концепций, паттернов и принципов проектирования, лежащих в основе обладающих боль- шим потенциалом АСЕ-каркасов. На протяжении многих лет обычным способом освоения АСЕ было штуди- рование исходного кода, комментариев и примеров приложений. Ясно, что та- кой процесс требовал много времени и не исключал ошибок. Более того, даже умудрившись просмотреть несколько сотен тысяч строк кода на C++ в АСЕ, было легко «за деревьями не увидеть Леса». Как заметил древнегреческий фило- соф Фукидид более двух тысяч лет тому назад: «Для человека, который знает, но не может ясно выразить свои мысли, было бы лучше не иметь никаких мыслей совсем». Поэтому нам повезло, что Даг и Стив, при всей их занятости, нашли время создать такую замечательную книгу о каркасах АСЕ. Во втором томе, в доступ- ной форме, излагаются идеи и концепции, лежащие в основе АСЕ-каркасов, ис- пользующих популярные паттерны параллелизма и сетевой обработки, опи- санные в книгах по паттернам POSA [POSA1, POSA2] и в книге «банды четырех» («Gang of Four») [GoF]. Эти паттерны, в свою очередь, являются воплощением хорошо продуманных и проверенных временем решений сетевых проблем об- щего характера. Они объясняют в чем состоит проблема, почему ее трудно ре- шать, какие есть способы решения этой проблемы и-каким образом использо- вание этих решений в АСЕ обеспечивает высокое качество приложений. Если вы заинтересованы в получении исчерпывающей информации о паттернах и каркасах АСЕ, напрямую участвующих в формировании следующего поколе- ния сетевого прикладного ПО—прочтите эту книгу. Я многому у нее научился, не сомневаюсь, что научитесь и вы. Фрэнк Бушманн Главный разработчик Siemens Corporate Technology Мюнхен, Германия
Предисловие 11 Об этой книге В современной, динамично развивающейся, конкурентной компьютерной индустрии программному обеспечению сетевых приложений, для успеха, необ- ходимо обладать следующими качествами: • Доступность (affordability) должна гарантировать, что общая стоимость владения и развития ПО не будет чрезмерно высокой. • Расширяемость (extensibility) должна поддерживать преемственность оперативных обновлений и дополнений с целью удовлетворения новых требований и извлечения выгод, связанных с развивающимися рынками. • Гибкость (flexibility) должна поддерживать растущее число типов муль- тимедийных данных, моделей трафика и требований, связанных со сквозным качеством обслуживания (QoS). • Переносимость (portability) должна сокращать объем работ, связанных с поддержкой приложений в гетерогенной среде платформ ОС и компи- ляторов. • Предсказуемость (predictability) и эффективность (efficiency) должны обеспечивать приемлемое время ожидания для чувствительных к за- держке приложений реального времени, высокую производительность для приложений, требующих большой полосы пропускания, и практич- ность использования сетей с низкой производительностью, таких как беспроводные каналы связи. • Надежность (reliability) должна обеспечивать безошибочность, отказо- устойчивость и постоянную готовность приложений. • Масштабируемость (scalability) должна позволять приложениям рабо- тать одновременно с большим количеством клиентов. Создавать высококачественные сетевые приложения, демонстрирующие перечисленные выше качества, трудно — дорого, сложно и чревато ошибками. Паттерны, возможности языка C++ и принципы объектно-ориентированного проектирования, представленные в первом томе «Программирование сетевых приложений на C++» [C++NPvl], помогают минимизировать сложности и ошибки в сетевых приложениях за счет рефакторинга обычных структур и функций в повторно используемые библиотеки классов интерфейсных фаса- дов. Основные преимущества повторного использования будут, тем не менее, утрачены, если большую часть прикладного ПО, использующего эти библио- теки классов, — или, еще хуже, сами библиотеки классов — придется перепи- сывать для каждого нового проекта. Исторически, многие программные проекты сетевых приложений начина- лись со следующих действий: 1. Разработка и реализация инфраструктурных механизмов диспетчеризации и демультиплексирования событий, связанных с синхронизацией и вво- дом/выводом на множестве дескрипторов сокетов.
12 Программирование сетевых приложений на C++. Том 2 2. Добавление, поверх уровня диспетчеризации и демультиплексирования, механизмов создания служб и их функционирования, совместно с меха- низмами буферизации и организации очередей сообщений. 3. Написание большого объема кода, отражающего специфику приложения, с использованием этого специально созданного (ad hoc) на двух предыду- щих этапах промежуточного слоя инфраструктуры хоста. Такого рода процесс разработки применялся много раз во множестве ком- паний, множеством проектов параллельно. Хуже того, он применялся и одни- ми и теми же командами разработчиков в нескольких проектах. Достойно со- жаления, но этот бесконечный процесс повторения открытий и изобретений основных концепций и кода поддерживал затраты на жизненный цикл разра- ботки программ на таком высоком уровне, который вовсе не является необхо- димым. Эта проблема обострялась принципиальным различием аппаратных средств, операционных систем, компиляторов и коммуникационных платформ. Объектно-ориентированные каркасы (object-oriented frameworks) [FJS99b, FJS99a] являются одним из наиболее гибких и эффективных методов решения перечисленных выше проблем. Каркас — это повторно используемый «полу- фабрикат» приложения, который можно настраивать для создания заказных приложений [ JF88]. Каркасы помогают снизить стоимость и улучшить качест- во сетевых приложений за счет «конкретизации» (reifying) проверенных про- граммных решений и паттернов в конкретном исходном коде. За счет акцента на интеграции и совместной работе классов, отражающих специфику приложе- ния и независимых от приложений, каркасы дают возможность осуществлять более широкомасштабное повторное использование ПО, чем то, которое мо- жет быть достигнуто при повторном использовании отдельных классов или ав- тономных функций. В начале 1990-х гг. Дат Шмидт начал проект с открытыми исходными кода- ми АСЕ с целью использовать большие возможности и эффективность паттер- нов и каркасов при разработке Сетевых приложений. Как и другие работы Дага, АСЕ решил многие проблемы практического характера, с которыми сталкива- лись пррфессиональные разработчики программного обеспечения. В течение следующего десятилетия его сотрудники в University of California, Irvine; Washington University, St. Louis; и Vanderbilt University совместно с сообщест- вом пользователей АСЕ и Стивом Хьюстоном из Riverace создали инструмен- тальное средство C++, включающее несколько обладающих наибольшим по- тенциалом и широко используемых в мире параллельных объектно-ориенти- рованных каркасов сетевого программирования. За счет применения повторно используемых паттернов ПО и упрощенного уровня адаптации к ОС, каркасы в инструментальной библиотеке АСЕ обеспечили синхронную и асинхронную обработку событий, параллельную обработку и синхронизацию, управление соединениями, а также конфигурирование, инициализацию и многоуровневую интеграцию служб. Успех АСЕ основательно изменил методы проектирования и реализации сетевых приложений и промежуточного ПО во многих операционных систе- мах, перечисленных в блоке 2. АСЕ используют тысячи групп разработчиков,
Предисловие 13 от больших компаний из списка Fortune 500 до небольших начинающих и про- двинутых исследовательских проектов в университетах и лабораториях компа- ний. Его модель разработки с открытыми исходными кодами и независимый характер похожи и по духу, и по энтузиазму на руководимую Линусом Тор- вальдсом разработку популярной операционной системы Linux. Данный том посвящен тому, как устроены ACE-каркасы и как они помога- ют разработчикам обходить следующие ограничения: 1. Низкоуровневые API операционных систем, которые являются негибки- ми и непереносимыми. 2. Высокоуровневый промежуточный слой, типа промежуточного слоя рас- пределительного уровня (distribution middleware) и общих служб промежуточ- ного слоя (common middleware services), которому часто не хватает эффектив- ности и гибкости для поддержки сетевых приложений с жесткими требова- ниями QoS и переносимости. Знания, нужные для создания и использования каркасов сетевых приложе- ний были всегда скрыты в головах разработчиков-экспертов или глубоко за- прятаны в исходном коде бесчисленных проектов, разбросанных по множеству компаний и индустрии ПО в целом. Ни одно из этих мест расположения^ ко- нечно, не является идеальным, поскольку для того, чтобы раскапывать эти све- дения при разработке каждого нового приложения или проекта требуется мно- го времени, не исключены и ошибки. Для решения этой проблемы книга знако- мит, в общих чертах, с основными паттернами [POSA2, POSA1, GoF], лежащими в основе структуры и функциональности каркасов АСЕ. Сведения о паттернах облегчают понимание процессов проектирования, реализации и эффективного использования самой инструментальной библиотеки АСЕ. Кому адресована эта книга Эта книга предназначена для разработчиков-практиков, использующих C++, студентов старших курсов и аспирантов, заинтересованных в изучении того, как проектировать объектно-ориентированные каркасы и как их приме- нять при разработке сетевых приложений. Во втором томе используется мате- риал первого тома [C++NPvl], посвященный применению паттернов для пре- одоления сложностей, возникающих из-за использования собственных API ОС при программировании сетевых приложений. Поэтому, прежде чем перехо- дить к чтению второго тома, важно ознакомиться со следующими темами, из- ложенными в первом томе [C++NPvl]: • Аспекты проектирования сетевых приложений, включая альтернатив- ные коммуникационные протоколы и механизмы передачи данных, из- ложенные в главе 1 [C++NPvl]. • Механизмы программирования в Internet, например, управление соеди- нениями TCP/IP и API передачи данных, рассмотренные в главе 2 [C++NPV1]. • Аспекты проектирования, связанные с параллелизмом, включая ис- пользование процессов и потоков, последовательных, параллельных
14 Программирование сетевых приложений на C++. Том 2 и взаимно-согласованных (reactive) серверов, а также модели поточной обработки [Ste99], рассмотренные в главах 5-9 [C++NPvl]. • Методы синхронизации, необходимые для координации взаимодейст- вия процессов и потоков на различных платформах ОС [KSS96, Lew95, Ric97], рассмотренные в главе 10 [C++NPvl]. • Методы объектно-ориентированного проектирования и программиро- вания [Воо94, Меу 97], которые помогают упростить API ОС и избежать ошйбок программирования за счет использования паттернов, таких как Wrapper Facade [POSA2] и Proxy [POSA1, GoF], рассмотренных в главе 3 и приложении A [C++NPvl]. Каркасы А'СЕ являются такими гибкими иобладают таким большим потен- циалом, в значительной степени, благодаря тому, что используют возможно- сти языка C++ [BjaOO], Поэтому нужно быть в курсе наследования классов в C++ и виртуальных функций (динамического связывания), а также шаблонов (параметризованных типов) и тех механизмов, которые позволяют создавать их реализации на вашем компьютере(ах). АСЕ предоставляет большую по- мощь в преодолении различий, свойственных компиляторам C++. И все же, как это обычно бывает, нужно .знать возможности своих средств разработки и то, как ими пользоваться. Знание инструментальных средств облегчает рабо- ту с примерами исходного кода данной книги, при этом следует помнить об особенностях, касающихся диаграмм UML и кода C++, которые отмечены в блоке 7. Структура и содержание Материал первого тома, [C++NPvl], касался того, как преодолевать извест- ные сложности разработки сетевых приложений. В основном, использования интерфейсных фасадов АСЕ, позволяющих избежать проблем с API операци- онных систем, написанных на С. Второй том (на него мы ссылаемся как на [C++NPv2]) смещает центр нашего внимания выше, на обоснование и деми- стификацию паттернов, методов проектирования и возможностей C++, свя- занных с разработкой и использованием каркасов АСЕ. Каркасы помогают сни- зить стоимость и улучшить качество сетевых приложений путем конкретиза- ции (reifying) проверенных временем программных проектов и паттернов в виде исходного кода, который может повторно использоваться на системати- ческой основе во многих проектах и предметных областях. Каркасы АСЕ рас- ширяют технологию повторного использования далеко за пределы того, чего можно достичь повторным использованием отдельных классов или даже биб- лиотек классов. В книге представлены многочисленные примеры приложений C++ с целью подкрепить рассуждения о проектировании конкретными примерами того, как нужно использовать каркасы АСЕ. Эти примеры являются, по сути, пошаго- вым руководством, которое поможет вам применять основные объектно-ори- ентированные методы и паттерны в собственных сетевых приложениях. Книга также показывает путь совершенствования навыков проектирования, концен-
Предисловие 15 трируя внимание на ключевых концепциях и принципах, которые формируют дизайн удачных объектно-ориентированных каркасов, предназначенных для сетевых приложений и промежуточного слоя: Главы в книге организованы следующим образом: • Глава 1 знакомит с концепцией объектно-ориентированного каркаса и показывает чем каркасы отличаются от других методов повторного ис- пользования, таких как библиотеки классов, компрненты, паттерны и вычисления на основе интегрированных моделей (model-integrated computing). Затем мы кратко перечисляем каркасы инструментальной библиотеки АСЕ, рассматриваемые в следующих главах. • Глава 2 завершает анализ предметной области, начатый в [C++NPvl], где рассматривались коммуникационные механизмы и протоколы, а также архитектуры параллелизма, используемые сетевыми приложениями. Во втором томе основное внимание уделяется аспектам проектирования, связанным со службами и конфигурациями, которые определяют такие ключевые свойства сетевых приложений, как время существования, структура, возможность идентификации сетевых служб и момент време- ни, когда они объединяются в единое целое для формирования закончен- ных приложений. • В главе 3 описываются структура и применение ACE-каркаса Reactor, ко- торый реализует паттерн Reactor [POSA2], чтобы дать возможность со- бытийно-управляемым приложениям осуществлять диспетчеризацию и демультиплексирование запросов на обслуживание, поступающих от одного или нескольких клиентов приложения. • Затем в главе 4 описываются структура и применение наиболее распро- страненных реализаций интерфейса ACE_Reactor, который поддержива- ет большой диапазон механизмов ОС демультиплексирования событий, включая WaitForMultipleObjects(), XtAppMainLoopO, select() и /dev/poll. • В главе 5 описываются структура и применение ACE-каркаса Service Configurator. Этот каркас реализует паттерн Component Configurator [POSA2], чтобы дать возможность приложениям подключать/отключать' (link/unlink) реализации своих компонентных служб во время выполне- ния, без модификации, повторной статической компиляции или компо- новки приложения. • В главе 6 описываются структура и применение ACE-каркаса Task. Этот каркас можно использовать для реализации основных паттернов парал- лелизма, таких как Active Object и Half-Sync/Half-Async [POSA2]. • В главе 7 описываются структура и использование АСЕ-каркаса Acceptor-Connector. Этот каркас реализует паттерн Acceptor-Connector [POSA2] с целью разделения процессов (1) соединения и инициализации взаимодействующих одноранговых служб сетевой системы и (2) обра- ботки, которую они затем осуществляют.
16 Программирование сетевых приложений на C++. Том 2 • В главе 8 описываются структура и применение ACE-каркаса Proactor. Этот каркас реализует паттерн Acceptor-Connector [POSA2], чтобы дать возможность событийно-упрайляемым приложениям эффективно осу- ществлять демультиплексирование и диспетчеризацию запросов на об- служивание., связанных с завершением асинхронно инициированных операций. • В главе 9 описываются структура и применение ACE-каркас Streams. Этот каркас реализует паттерн Pipes & Filters [POSA1 ] с целью создания струк- туры для систем, которые обрабатывают потоки данных. • Книгу завершают словарь терминов и список литературы для дальней- шего изучения? Главы организованы таким образом, что материал каждой следующей гла- вы использует материал предыдущих глав, чтобы свести к минимуму перекре- стные ссылки. Поэтому мы рекомендуем читать главы по порядку. Хотя в книге рассматриваются основные возможности наиболее важных каркасов АСЕ, мы, тем не менее, не даем исчерпывающей информации обо всех рассматриваемых каркасах. Для получения дополнительной информации по АСЕ мы отсылаем к АСЕ Programmer’s Guide [HJS] и к онлайновой справочной документации по АСЕ, генерируемой Doxygen [DimOl]. Справочная докумен- тация по АСЕ находится по адресам: http://ace.ece.uci.edu/Doxygen/ и http://www.riverace.com/docs/. Дополнительный материал Материал книги базируется на АСЕ версии 5.3, выпущенной осенью 2002 г. АСЕ 5.3 и все примеры приложений, рассматриваемые в нашей книге, являются программами с открытыми исходными кодами. Блок 3 поясняет, как можно по- лучить копию АСЕ, чтобы иметь возможность знакомиться с реальными клас- сами и каркасами АСЕ, во всех подробностях, и выполнять примеры кода по мере чтения книги. Для получения дополнительной информации по АСЕ или при желании со- общить о любых ошибках, которые вы найдете в этой книге, мы рекомендуем вам подписаться на список рассылки АСЕ ace-users@cs. wustl. edu. Вы можете подписаться, послав запрос по адресу ace-users-request@cs.wustl.edu. В тело письма (поле темы игнорируется) включите следующую команду: subscribe ace-users (emailaddress@domain] Вы должны включить адрес электронной почты в формате email- address@domain, но только в том случае, если адрес вашего сообщения в поле From не является тем адресом, на который вы хотите получать сообщения. Если вы используете этот альтернативный способ указания адреса, сервер реестра потребует дополнительного этапа авторизации, прежде чем разрешить вам присоединиться к списку. При переводе добавлен англо-русский указатель терминов. — Прим. ред.
Предисловие 17 Почтовые отправления в список ace-users, передаются также в группы новостей USENET comp.soft-sys.асе, одновременно е рййсылкой несколь- ким другим почтовым спискам, связанным с АСЕ. Чтение сообщений в группах новостей — это хороший способ быть в курсе новостей и событий, связанных с АСЕ, если вам не требуется оперативная доставка от 30 до 50 сообщений, кото- рые отсылаются каждый день в списки рассылки. Архивы почтовых отправлений в группу новостей comp. soft-sys. асе дос- тупны по адресу http://groups.google.com/. Введите comp.soft-sys.асе в поле поиска, чтобы перейти к списку архива сообщений. На Google есть пол- ный, с возможностью поиска, архив более 40000 сообщений. Вы можете также с сайта Google отправить сообщение в группу новостей. Благодарности Чемпионские медали за рецензирование получают Ален Декамп (Alain Decamps), Дон Хинтон (Don Hinton), Александр Маак (Alexander Maack), Крис Уздавинис (Chris Uzdavinis) и Джонни Уиллемзен (Johnny Willemsen), которые просматривали книгу много раз и дали обширные, подробные комментарии, значительно улучшившие ее форму и содержание. Также большое спасибо официальным рецензентам Тимоти Гальпу (Timothy Gulp), Денису Манклу (Dennis Mancl), Филу Месниеру (Phil Mesnier) и Джейсон Пейзион (Jason Ра- sion), которые прочитали всю книгу и предоставили нам много полезных ком- ментариев. Многие другие пользователи АСЕ прислали свои соображения об этой книги, к ним относятся Марк Адкинз (Marc М. Adkins), Томер Эймиаз (Tomer Amiaz), Ви Тхуан Банг (Vi Thuan Banh), Кевин Бейли (Kevin Bailey), Сте- фан Бастиен (Stephane Bastien), Джон Диллей (John Dilley), Эрик Эйде (Eric Eide), Эндрю Финнелл (Andrew Finnell), Дэйв Финдлей (Dave Findlay), Джоди Хейгинз (Jody Hagins), Джон Харниш (Jon Harnish), Джим Гавличек (Jim Havli- cek), Мартин Джонсон (Martin Johnson), Кристофер Колхофф (Christopher Kohlhoff), Алекс Либман (Alex Libman), Гаральд Миттерхофер (Harald Mitter- hofer), Ллори Паттерсон (Llori Patterson), Ник Пратт (Nick Pratt), Дитер Кель (Dieter Quehl), Тим Розмайзл (Tim Rozmajzl), Ирма Растегаева (Irma Rastegaye- va), Имонн Сондерз (Eamonn Saunders), Харвиндер Сонэй (Harvinder Sawhney), Кристиан Шухеггер (Christian Schuhegger), Майкл Серлз (Michael Searles), Кал- виндер Сингх (Kalvinder Singh), Хенни Сипма (Henny Sipma), Стивен Стурте- вант (Stephen Sturtevant), Лео Штуцманн (Leo Stutzmann), Томми Свенссон (Tommy Svensson), Брюс Траск (Bruce Trask), Доминик Уилльямс (Dominic Williams) и Вадим Залива (Vadim Zaliva). Мы в глубоком долгу перед всеми членами, прошлыми и настоящими, DOC groups из Washington University в St. Louis и University of California, Irvine, а также сотрудникам Riverace Corporation и Object Computing Inc., которые раз- рабатывали, уточняли и оптимизировали многие средства АСЕ, представлен- ные в этой книге. Эта группа включает Эверетт Андерсон (EVerett Anderson), Алекс Аруланту (Alex Arulanthu), Шон Аткинс (Shawn Atkins), Джон Огей (John Aughey), Лютер Бейкер (Luther Baker), Джейганеш Баласубраманиан (Jaiganesh Balasubramanian), Даррелл Бранш (Darrell Brunsch), Дон Буш (Don Busch), Крис
18 Программирование сетевых приложений на C++. Том 2 Клиланд (Chris Cleeland), Анджело Корсаро (Angelo Corsaro), Чад Эллиот (Chad Elliot), Серджио Флорес-Гайтан (Sergio Flores-Gaitan), Крис Джилл (Chris Gill), Прейдип Гор (Pradeep Gore), Энди Гокхейл (Andy Gokhale), Приянка Гонтла (Priyanka Gontla), Мирна Харбибсон (Мута Harbibson), Тим Гаррисон (Tim Harrison), Джон Хеннан (Shawn Hannan), Джон Хейтманн (John Heitmann), Джо Гофферт (Joe Hoffert), Джеймс Хью (James Hu), Франк Ханлет (Frank Hunleth), Прашант Джейн (Prashant Jain), Вишел Кейчру (Vishal Kachroo), Рэй Кифстад (Ray Kiefstad), Китти Кришнакумар (Kitty Krishnakumar), Ямуна Кришнамурти (Yamuna Krishnamurthy), Майкл Керчер (Michael Kircher), Фред Кунс (Fred Kuhns), Дейвид Левин (David Levine), Чанака Лиянааракчи (Chanaka Liyanaa- rachchi), Майкл Моран (Michael Moran), Эбрахим Мошири (Ebrahim Moshiri), Сумедх Манги (Sumedh Mungee), Бала Натараян (Bala Natarajan), Оссама От- хман (Ossama Othman), Джефф Парсонс (Jeff Parsons), Киртика Парамесваран (Kirthika Parameswaran), Криш Патаяпура (Krish Pathayapura), Ирфан Пейрали (Irfan Pyarali), Сумита Рао (Sumita Rao), Карлос О’Райен (Carlos O’Ryan), Рич Сибел (Rich Siebel), Малколм Спенз (Malcolm Spence), Марина Спивак (Marina Spivak), Нага Сурендран (Naga Surendran), Стив Тоттен (Steve Totten), Брюс Траск (Bruce Trask), Нанбор Ванг (Nanbor Wang) и Сет Уидофф (Seth Widoff). Мы хотим также поблагодарить тысячи разработчиков, использующих язык C++, из почти пятидесяти стран, которые внесли свой вклад в АСЕ за по- следние десять лет. Достижения и успех АСЕ—это памятник мастерству и щед- рости многих талантливых разработчиков и дальновидных компаний, которые были так прозорливы, что приняли участие в создании основы открытого ис- ходного кода АСЕ. Без их поддержки, постоянной обратной связи и содействия, мы никогда бы не написали эту книгу. В знак признания за работу, проделан- ную сообществом открытого исходного кода АСЕ, мы ведем список всех, кто участвовал в этой работе, который находится по адресу: http://асе.есе.uci.edu/ACE-members.html. Мы признательны также за поддержку коллегам и спонсорам нашей иссле- довательской работы, связанной с паттернами, и разработкой инструменталь- ной библиотеки АСЕ, особый вклад внесли Рон Акерс (Ron Akers, Motorola), Стив Бакинский (Steve Bachinsky, SAIC), Джон Бэй (John Bay, DARPA), Детлиф Беккер (Detlef Becker, Siemens), Фрэнк Бушманн (Frank Buschmann, Siemens), Дейв Бусиго (Dave Busigo, DARPA), Джон Буттитто (John Buttitto, Sun), Бекки Каллисон (Becky Callison, Boeing), Вэй Чианг (Wei Chiang, Nokia), Джо Кросс (Joe Cross, Lockheed Martin), Лу ДиПалма (Lou DiPalma, Raytheon), Брайан До- epp (Bryan Doerr, Boeing), Карлхайнц Дорн (Karlheinz Dorn, Siemens), Скотт Эл- лард (Scott Ellard, Madison), Матт Эмерсон (Matt Emerson, Escient Convergence Group, Inc.), Силвестер Фернандес (Sylvester Fernandez, Lockheed Martin), Никки Форд (Nikki Ford, DARPA), Андреас Гейслер (Andreas Geisler, Siemens), .Хелен Джилл (Helen Gill, NSF), Джоди Хагинс (Jody Hagins, ATD), Энди Харвей (Andy Harvey, Cisco), Сю Келли (Sue Kelly, Sandia National Labs), Гари Кооб (Gary Koob, DARPA), Петри Коскелайнен (Petri Koskelainen, Nokia Inc), Шон Ландис (Sean Landis, Motorola), Патрик Лардьери (Patrick Lardieri, Lockheed Martin), Даг Ли (Doug Lea, SUNY Oswego), Джо Лойелл (Joe Loyall, BBN), Кент Мейдсен (Kent Madsen, EO Thorpe), Эд Марганд (Ed Margand, DARPA), Майк Мастерс (Mike
Предисловие 19 Masters, NSWC), майор Эд Мейз (Ed Mays, U.S. Marine .Corps), Джон Меллби (John Mellby, Raytheon), Джейнетт Милос (Jeanette Milo^DAJU’A), Стен Мойер (Stan Moyer, Telcordia), Иван Мерфи (Ivan Murphy, Siemens), Расс Ноусворти (Russ Noseworthy, Object Sciences), Адам Портер (Adam Porter, U. of Maryland), Дитер Кель (Dieter Quehl, Siemens), Виджей Рагха-ван (Vijay Ragha-van, Van- derbilt U.), Люси Робиллард (Lucie Robillard, U.S. Air Force), Крэг Родригес (Craig Rodrigues, BBN), Рик Шантц (Rick Schantz, BBN), Андреас Шульке (Andreas Schulke, Siemens), Стив Шаффер (Steve Shaffer, Kodak), Том Шцлдс (Tom Shields, Raytheon), Дэйв Шарп (Dave Sharp, Boeing), Навал Сода (Naval Sodha, Ericsson), Пол Стефенсон (Paul Stephenson, Ericsson), Татсуйя Суда (Tatsuya Suda, UCI), Умар Сейид (Umar Syyid, Hughes), Янос Штипанович (Janos Sztipanovits, Van- derbilt U.), Гаутам Тейкер (Gautam Thaker, Lockheed Martin), Лотар Верзингер (Lothar Werzinger, Krones) и Дон Винтер (Don Winter, Boeing). Особая благодарность Сюзан Купер (Susan Cooper), редактору, за улучше- ние написанного нами материала. Кроме того, мы благодарны за сотрудничест- во и терпение редактору Дебби Лафферти (Debbie Lafferty), координатору из- дания Элизабет Райан (Elizabeth Ryan), редактору серии и автору языка С++ Бьерну Страуструпу (Bjarne Stroustrup) и всем сотрудникам издательства Addi- son-Wesley, которые сделали возможной публикацию этой книги. В заключение, мы хотели бы также выразить нашу признательность и от- дать дань уважения покойному Ричарду Стивенсу (W. Richard Stevens) родона- чальнику литературы о сетевом программировании. Вот стихотворение Сэ- мюеля Батлера, которое выражает суть нашего ощущения того неподвластного смерти влиянии, которое оказал на нас Ричард: Not on sad Stygian shore, nor in clear sheen Of far Elysian plain, shall we meet those Among the dead whose pupils we have been... Yet meet we shall, and part, and meet again, Where dead men meet, on lips of living men.1 Благодарности Стива Да... Первый том потребовал для завершения почти трех лёт — этот том приблизительно девять месяцев. Спасибо моей жене Джейн, которая безропот- но вынесла этот процесс. Твой постоянный призыв поддерживать жизненный баланс и «никуда не торопиться» действительно помог мне довести дело до кон- ца, без твоего безграничного терпения, длившегося многие дни и ночи, я не смог бы завершить эту работу — спасибо! Спасибо Дагу Шмидту за то, что он упорядочил и написал большую часть этой книги в рекордное время в проме- жутках между работой, занимающей весь, день, и обычной бесконечной рабо- Не на мрачном Стигийском брегу, и не в чистом сиянии недоступных райских полей, встретим мы тех, из усопших, чьими учениками мы были... Мы с ними встретимся, расстанемся и снова встретимся, там, где и встречаются с усопшими, в устах живых людей.
20 Программирование сетевых приложений на C++. Том 2 той по АСЕ. Наконец, спасибо клиентам Riverace, которые с таким энтузиазмом поддержали эту работу. То, что я могу быть вам полезен, — большая честь для меня. Благодарности Дата Я хотел бы поблагодарить жену Соню и родителей за любовь и поддержку, пока я писал эту книгу. Теперь, когда дело сделано, у нас будет гораздо больше времени для развлечений! Спасибо Стиву Хьюстону, выкроившему время в своем перегруженном расписании, чтобы закончить эту книгу. Хотелось бы также сказать спасибо моим друзьям и коллегам из College of William and Mary; Washington University, St. Louis; University of California, Irvine; Vanderbilt University; DARPA; и Siemens — а также тысячам разработчиков и пользовате- лей АСЕ и ТАО по всему миру — которые очень обогатили мою интеллектуаль- ную и межличностную жизнь за последние два десятилетия. Я с нетерпением жду продолжения нашей совместной работы в будущем.
Глава 1 Объектно-ориентированные каркасы и сетевые приложения Краткое содержание Объектно-ориентированные каркасы способствуют снижению стоимости и улучшению качества сетевых приложений за счет конкретизации (reifying) программных дизайнов и языков паттернов, эффективность которых в конкрет- ных прикладных областях была подтверждена на практике. Данная глава поясня- ет, что такое каркасы, и приводит их сравнение с другими распространенными методами разработки программного обеспечения (ПО) такими как библиотеки классов, компоненты, паттерны и вычисления на основе интегрированных мо- делей. Затем демонстрируется процесс применения каркасов к разработке сете- вых приложений, и описываются ACE-каркасы, которые являются основной темой данной книги. ACE-каркасы базируются на языке паттернов [POSA1, POSA2], который использовался в тысячах коммерческих сетевых приложений и промежуточном слое по всему миру. 1.1 Общее представление об объектно-ориентированных каркасах В то время как вычислительная мощность и сетевая пропускная способ- ность растут поразительными темпами, разработка сетевого прикладного ПО остается процессом дорогостоящим, трудоемким и подверженным ошибкам. Высокая стоимость и трудоемкость являются следствием как роста требований, предъявляемых к сетевому ПО, так и следствием непрерывного процесса по- вторных открытий и изобретений основных артефактов, связанных с разработ- кой и реализацией программ, что характерно для всей индустрии ПО. Более того, гетерогенные архитектуры аппаратных средств, разнообразие операцион- ных систем (ОС) и сетевых платформ и жесткая глобальная конкурентная борь-
22 Глава 1 ба все более и более осложняют создание высококачественного сетевого при- кладного ПО с нуля. Ключом к созданию высококачественного сетевого ПО в среде, управляе- мой временем продажи (time-to-market-driven), является возможность повтор- но использовать уже созданные и успешно зарекомендовавшие себя программ- ные проекты и реализации.Повторное использование (reuse) является попу- лярной темой обсуждений и дискуссий в программистском сообществе на протяжении 30 лет [Мс168]. Существует два основных типа повторного исполь- зования: • Приспособительное (opportunistic) повторное использование, при ко- тором разработчики вырезают и вставляют код из существующего ПО для создания нового. Действие приспособительного повторного исполь- зования ограничивается отдельными программистами или небольшими группами. Оно не может масштабироваться до уровня подразделений или предприятий, и поэтому не может существенно снизить время раз- работки и затраты, или существенно улучшить качество. Хуже того, при- способительное повторное использование может, на самом деле, поме- шать успешной разработке, так как такой «склеенный» код часто изменя- ется по мере распространения, вынуждая разработчиков обнаруживать много раз в разных местах одни и те же ошибки. * Систематическое повторное использование, которое представляет со- бой целенаправленную работу с намерением создания многократно ис- пользуемых программных структур, паттернов, каркасов и компонентов и их использование во всех создаваемых конечных продуктах [CN02]. В хорошо налаженном процессе систематического повторного использо- вания, каждый новый проект использует апробированные временем проекты и реализации, добавляя только специфичный для конкретного приложения новый код. Такой способ повторного использования явля- ется основным для повышения качества и эффективности ПО. Промежуточный слой {middleware) [SS02] является разновидностью про- граммного обеспечения, которое может существенно повысить уровень систе- матического повторного использования путем заполнения существующего разрыва между функциональными требованиями, которые предъявляются к сетевым приложениям, и функциональными возможностями, которые реа- лизованы в базовых операционных системах и стеках сетевых протоколов. Промежуточный слой обеспечивает жизненно важные для сетевых приложе- ний возможности, поскольку автоматизирует решение стандартных задач сете- вого программирования. Разработчики, использующие промежуточный слой, могут программировать свои сетевые приложения почти так же, как и автоном- ные, а не возиться с множеством запутанных и подверженных ошибкам дета- лей, связанных с низкоуровневыми механизмами ОС, такими как демультип- лексирование событий, буферизация и организация очередей сообщений, мар- шалинг и демаршалинг и управление соединениями. Распространенными примерами ПО промежуточного слоя являются виртуальные машины Java
Объектно-ориентированные каркасы и сетевые приложения 23 (JVM), Enterprise JavaBeans (EJB), .NET, Common ObjectRequest Broker Architec- ture (CORBA) и ADAPTIVE Communication Environment (ACE). Систематическая разработка высококачественного, повторно используе- мого сетевыми приложениями промежуточного слоя связана с множеством сложных технических проблем, включая следующие: • Обнаружение случайных сбоев и частичных отказов сетей и хостов и вос- становление работоспособности независимым от приложений образом. • Минимизация влияния задержек и флуктуаций на результирующую производительность приложений. • Определение способа декомпозиции распределенного приложения на отдельные компоненты служб. • Принятие решения о том, в каких узлах сети и когда распределять и вы- равнивать нагрузку, связанную с реализацией служб. Так как повторно используемый промежуточный слой, по своей сути, яв- ляется абстрактным, трудно оценивать его качество и управлять его созданием. Более того, навыки, нужные для разработки, развертывания и поддержки про- межуточного слоя повторно используемых сетевых приложений традиционно являются своего рода «черной магией», скрытой в головах экспертов, разработ- чиков и архитекторов. Эти технические трудности систематического повторно- го использования часто осложняются множеством препятствий нетехническо- го характера [Но197], таких как организационные, экономические, администра- тивные, политические, социальные и психологические факторы. Поэтому неудивительно, что существенные уровни повторного использования ПО во многих проектах и организациях достигаются медленно [SchOO]. Несмотря на то, что создать универсальный механизм повторного исполь- зования всегда непросто, мы разработали обладающий большими возможно- стями промежуточный слой инфраструктуры хоста (host infrastructure middleware), который называется АСЕ и который целенаправленно проектиро- вался с мыслью о повторном использовании. За последние десять лет мы напи- сали сотни тысяч строк кода на C++ в процессе разработки АСЕ и его примене- ния к сетевым приложениям; это было частью нашей повседневной работы с множеством телекоммуникационных, аэрокосмических, медицинских и фи- нансовых компаний. В результате проделанной работы мы задокументировали множество паттернов и языков паттернов [POSA2, POSOO], которые направля- ли разработку повторно используемого промежуточного слоя и приложений. Кроме того, мы подготовили сотни учебных пособий и курсов по повторному использованию, промежуточному слою и паттернам, и обучили тысячи разра- ботчиков и студентов. Несмотря на многие технические и нетехнические про- блемы, мы проделали основательную часть работы, которая объединила пер- спективные исследования, проверенные временем принципы проектирования, практический опыт и программные продукты, которые могут существенно улучшить систематическое повторное использование прикладного сетевого ПО. В основе этой работы — объектно-ориентированные каркасы [FJS99b, FJS99a], которые представляют собой обладающую большим потенциалом тех-
24 Глава 1 нологию систематического повторного использования прикладного сетевого ПО.1 Далее мы приводим три характеристики каркасов [JF88], которые помога- ют им достичь тех важных качеств сетевых приложений, которые были пере- числены в начале раздела «Об этой книге». На рис. 1.1 показано, каким образом эти характеристики взаимодействуют. Каркас обеспечивает интегрированный набор структур и функциональ- ных возможностей, связанных с конкретной предметной областью. Система- тическое повторное использование ПО в значительной степени зависит от того, насколько точно каркасы отражают общее и частное [CHW98] прикладных об- ластей, таких как обработка коммерческих данных, обработка телекоммуника- ционных вызовов, графические пользовательские интерфейсы или промежу- точное ПО распределенных объектных вычислений. Так как каркасы представ- ляют собой реализацию (reify) основных ролей и зависимостей классов в прикладной области, то, в связи с этим, количество повторно используемого кода увеличивается, а количество кода, который приходится переписывать в ка- ждом приложении, уменьшается. Каркас реализует во время выполнения «инверсию управления» («inver- sion of control») посредством callback-объектов. Callback-объект (callback) представляет собой объект, который осуществляет обратный вызов метода объекта при наступлении некоторого конкретного события, например, при за- просе на соединение или при поступлении данных на один из дескрипторов со- кетов. Инверсия управления отделяет стандартные процедуры обнаружения, демультиплексирования и диспетчеризации, реализуемые каркасом, от опреде- ляемых приложением обработчиков событий, которые управляются каркасом. При наступлении событий каркас осуществляет обратный вызов виртуальных hook-методов зарегистрированных обработчиков событий, которые затем, в от- вет на события, выполняют определяемую приложением обработку. Так как каркасы реализуют инверсию управления, они могут упростить проектирование приложений, поскольку именно каркас, — а не приложение — выполняет цикл обработки событий с целью их обнаружения, демультиплек- сирования событий обработчикам и диспетчеризации hook-методов обработ- чиков, которые выполняют обработку события. Использование виртуальных hook-методов в классах обработчиков отде- ляет классы приложений от каркаса, позволяя изменять их независимо друг от друга до тех пор, пока сигнатура их интерфейса и протоколы взаимодействия остаются неизменными. Каркас — это «полуфабрикат» приложения, который программисты мо- гут настраивать с целью создания законченных приложений путем наследова- ния и создания экземпляров классов в каркасе. Наследование позволяет изби- рательно использовать общие возможности базовых классов каркаса. Если ба- зовый класс обеспечивает реализацию своих методов по умолчанию, разработчикам приложения нужно перегрузить только те виртуальные мето- ды, встроенное поведение которых не соответствует .их потребностям. В остальной части этой книги мы используем термин каркас (framework) для обозначения объектно-ориентированного каркаса (object-oriented framework).
Объектно-ориентированные каркасы и сетевые приложения 25 ---- Возможности каркаса, ---- зависящие от конкретной ---- предметной области Рис. 1.1 Совместные возможности каркасов Так как каркас представляет собой «полуфабрикат» приложения, он спо- собствует более широкомасштабному повторному использованию ПО, чем то, которое может быть достигнуто путем повторного использования отдельных классов и автономных функций. Степень повторного использования возраста- ет благодаря способности каркаса интегрировать классы, зависящие от прило- жения, и классы, независимые от него. В частности, каркас абстрагирует стан- дартный поток управления приложений некоторой прикладной области в се- мейства взаимосвязанных классов, которые могут взаимодействовать с целью интеграции кода, независимого от приложения, (его можно настраивать) и кода, сделанного под конкретное приложение. 1.2 Сравнение методов разработки и повторного использования ПО Объектно-ориентированные каркасы существуют не в изоляции. Библио- теки классов, компоненты, паттерны и вычисления на основе интегрированных моделей (model-integrated computing) также являются методами, применяемы- ми при повторном использовании ПО и с целью повышения производительно- сти. В данном разделе каркасы сравниваются с указанными выше методами, чтобы показать их общие стороны и отличия, а также показать каким образом их можно объединять с целью улучшения систематического повторного ис- пользования сетевых приложений.
26 Глава 1 (1) Архитектура на основе библиотеки классов (2) Архитектура на основе каркасов Рис. 1.2 Архитектура на основе библиотеки классов по сравнению с архитектурой на осно- ве каркасов 1.2.1 Каркасы и библиотеки классов Класс является универсальным повторно используемым строительным блоком, который определяет интерфейс и инкапсулирует представление внут- ренних данных и функциональности создаваемых на его основе объектов. Биб- лиотека классов была самым распространенным средством объектно-ориенти- рованной разработки первого поколения [Меу97]. Библиотеки классов, как правило, поддерживают повторное использование «в локальном масштабе» (reuse-in-the-small) более эффективно, чем библиотеки функций, так как классы делают акцент на единстве данных и методов, оперирующих с этими данными. Хотя библиотеки классов часто не зависят от предметной области и могут применяться в разных приложениях, область их эффективного повторного ис- пользования ограничена, так как они не распространяются на стандартный поток управления, на общие функции и специфику семейств взаимосвязанных про- граммных продуктов. Поэтому общий объем повторного использования биб- лиотек классов относительно невелик, по сравнению с тем кодом, который дол- жен переписываться для каждого приложения. Необходимость повторного соз- дания и повторной реализации общей архитектуры ПО и большей части управ- ляющей логики для каждого нового приложения является основным источни- ком материальных и временных затрат для многих программных проектов. Стандартная библиотека C++ [BjaOO] — хорошая иллюстрация обсуждае- мой темы. Она предлагает классы для строк, векторов и других контейнеров. Хотя эти классы можно повторно использовать во многих прикладных областях,
Объектно-ориентированные каркасы и сетевые приложения 21 они являются относительно низкоуровневыми. Поэтому на разработчиков при- ложений ложиться обязанность писать (переписывать) большой объем связую- щего кода («glue code»), который реализует большую часть потока управления приложения и логики интеграции классов, как показано на рис. 1.2 (1). Каркасы являются средством разработки второго поколения [ Joh97], кото- рые развивают преимущества библиотек классов в нескольких направлениях. Самое главное, совокупность классов в каркасе совместно обеспечивает по- вторно используемую архитектуру для семейства родственных приложений. Взаимосвязь классов в каркасе образует «полуфабрикаты» приложений, кото- рые воплощают в себе объектные структуры и функциональность, зависящие от конкретной предметной области. Каркасы можно классифицировать по раз- ным признакам, например, по признаку непрозрачности (blackbox) и прозрач- ности (whitebox) (см. блок 1). Блок 1: Общее представление о прозрачных и непрозрачных каркасах Каркасы можно классифицировать по методам, используемым для их расши- рения, которые изменяются в диапазоне от «прозрачных» (.whitebox') каркасов до «непрозрачных» (blackbox) каркасов (HJE95), в соответствии с изложенным ниже: • Прозрачные каркасы. Расширяемость прозрачного каркаса достигается за счет таких свойств объектно-ориентированного языка как наследование и динамическое связывание. Существующую функциональность можно по- вторно использовать и настраивать путем наследования от базовых классов каркаса и перегрузки предопределенных hook-методов (Рге95) с помощью папернов типа Template Method (GoF), который определяет алгоритм с по- следовательностью шагов, задаваемых производным классом. Для расши- рения прозрачного каркаса разработчики приложения должны распола- гать некоторой информацией о его внутренней структуре. • Непрозрачные каркасы. Расширяемость непрозрачного каркаса достига- ется путем определения интерфейса, который позволяет подключать (plug) объекты к каркасу путем композиции (composition) и делегирования (dele- gation). Существующую функциональность можно повторно использовать путем определения классов, которые соответствуют конкретному интер- фейсу, и последующей их интеграции в каркас с помощью таких папернов как Function Object (Kuh97), Bridge/Strategy (GoF) и Pluggable Factory (Vli98b, VII99, Cul99), которые обеспечивают абстракцию «черного ящика» для выбо- ра одной из многих реализаций. Непрозрачные каркасы, возможно, проще использовать, чем прозрачные, так как, в этом случае, разработчикам при- ложений требуется меньше информации о внутренней структуре каркаса. Вместе с тем, непрозрачные каркасы сложнее проектировать, так как раз- работчики каркаса должны определить набор интерфейсов, который пре- дугадывал бы некоторый набор вариантов применения. Библиотеки классов отличаются также от каркасов тем, что классы в биб- лиотеке обычно пассивны, так как выполняют обработку, заимствуя поток управления у так называемых самоуправляемых (self-directed) приложений, которые вызывают их методы. Как следствие, разработчикам приходится все
28 Глава 1 время переписывать большую часть управляющей логики, необходимой для объединения повторно используемых классов в законченные сетевые приложе- ния. Напротив, каркасы являются активными, так как они направляют поток управления внутри приложения посредством управляемых обратными вызо- вами паттернов, связанных с обработкой событий, например, Reactor [POSA2] и Observer [GoF]. Эти паттерны инвертируют поток управления приложения, используя голливудский принцип (Hollywood Principle): «не звоните, мы сами по- звоним» [Vli98a], Так как каркасы активны и воздействуют на поток управле- ния приложения, они могут выполнять действия от имени приложений в более широком диапазоне, чем это могут обеспечить пассивные библиотеки классов. Каркасы и библиотеки классов фактически являются дополняющими друг друга технологиями. Каркасы обеспечивают приложениям базовую структуру. Тем не менее, поскольку каркасы сосредоточены на конкретной предметной об- ласти, не приходится полагать, что они удовлетворят все потребности, возни- кающие при разработке приложений. Поэтому библиотеки классов часто тоже используются внутри каркасов и приложений при реализации повсеместно не- обходимых программных конструкций, таких как строки, файлы и классы вре- мени/даты. Например, ACE-каркасы используют классы интерфейсных фасадов АСЕ, чтобы обеспечить переносимость. Аналогичным образом, приложения могут использовать контейнерные классы АСЕ, рассмотренные в [HJS], чтобы упро- стить реализацию обработчиков событий. В то время как контейнерные классы АСЕ и интерфейсные фасады являются пассивными, АСЕ-каркасы — активны и обеспечивают инверсию управления во время выполнения. Инструменталь- ная библиотека АСЕ предоставляет и каркасы, и библиотеку классов, чтобы по- мочь программистам решать весь комплекс проблем, возникающих при разра- ботке сетевых приложений. 1.2.2 Каркасы и компоненты Компонент — это инкапсулированная часть программной системы, кото- рая является реализацией конкретного сервиса или набора сервисов. Компо- нент имеет один или несколько интерфейсов, которые обеспечивают доступ к его сервисам. Компоненты служат в качестве строительных блоков конструк- ции приложения и могут быть повторно использованы, основываясь только на знании протоколрв их интерфейсов. Компоненты представляют собой средства разработки третьего поколения [Szy98], которые широко используются разработчиками многозвенных корпо- ративных приложений. Распространенными примерами компонентов являют- ся управляющие элементы ActiveX [Egr98] и COM-объекты [Вох98], web-служ- 6bi.NET [TL01], Enterprise JavaBeans [МН01] и CORBA Component Model (ССМ) [ObjOla]. Компоненты могут подключаться друг к другу или объединяться с по- мощью сценариев с целью создания законченных приложений, как показано на рис. 1.3. На рис. 1.3 показано также, каким образом компонент реализует бизнес-ло- гику приложения в контексте контейнера. Контейнер позволяет своим компо-
Объектно-ориентированные каркасы и сетевые приложения 29 Рис. 1.3 Компонентная архитектура нентам обращаться к ресурсам и службам, предоставляемым нижележащей платформой промежуточного слоя. Кроме того, на рисунке показано как обоб- щенные серверы приложений могут использоваться для создания экземпляров контейнеров и управления ими, а также для выполнения встроенных в них ком- понентов. Ассоциированные с компонентами метаданные содержат инструк- ции, которые используют серверы приложений для настройки и подключения компонентов. Многие взаимосвязанные компоненты в корпоративных приложениях мо- гут располагаться в разных — возможно распределенных — серверах приложе- ний. Каждый сервер приложений состоит из нескольких компонентов, которые реализуют службы для клиентов. Эти компоненты, в свою очередь, могут включать другие совмещенные (collocated) или удаленные службы. Говоря в об- щем, компоненты помогают разработчикам уменьшить первоначальный объ- ем работы, связанной с разработкой ПО, путем объединения изготавливаемых под заказ прикладных компонентов с повторно используемыми готовыми Компонентами в настраиваемые каркасы серверов приложений. Причем при изменении требований к приложениям, компоненты могут упростить перенос и перераспределение отдельных служб, с целью адаптации к новому окруже- нию, сохраняя ключевые свойства приложения, такие как доступность и безо- пасность. Компоненты, обычно, менее зависимы, и пространственно и лексически, чем каркасы. Например, приложения могут использовать компоненты, не соз- давая производные классы на основе существующих базовых классов. Кроме того, за счет применения общих паттернов, таких как Proxy [GoF] и Broker [POSA1], компоненты могут распределяться по разным серверам сети, при этом клиенты осуществляют удаленный доступ к ним. Новые серверы прило- жений, такие как JBoss и Web-Logic Server (BEA Systems), используют паттерны такого типа, чтобы упростить использование компонентов для приложений. Каркасы и компоненты в значительной степени взаимодополняют, а не за- меняют, друг друга [Joh97]. Например, ACE-каркасы можно использовать для создания более высокоуровневых прикладных компонентов, интерфейсы ко-
зо Глава 1 торых затем обеспечат фасад для внутренней структуры классов этих каркасов. Аналогичным образом, компоненты могут быть использованы в качестве сменных стратегий в непрозрачных каркасах [HJE95]. Каркасы часто использу- ются для упрощения разработки компонентных моделей промежуточного слоя [TL01, MHO 1, ObjQla], тогда как компоненты часто используют для упрощения разработки и настройки прикладного сетевого ПО. 1.2.3 Каркасы и паттерны Разработчикам сетевых приложений приходится решать проблемы проек- тирования, связанные с такими сложными вопросами, как управление соедине- ниями, инициализация служб, распределение, управление параллелизмом, управление потоками, обработка ощибок, объединение циклов обработки со- бытий и надежность функционирования. Так как, чаще всего, эти проблемы яв- ляются независимыми от требований конкретных приложений, разработчики могут их решать путем применения следующих типов паттернов [POSA1 ]: * Паттерны проектирования (design patterns) предлагают образец улуч- шения элементов программной системы и взаимосвязей между ними, и описывают некоторую часто встречающуюся структуру взаимосвязан- ных элементов, которая является решением одной из общих проблем проектирования в некотором конкретном контексте. • Архитектурные паттерны (architectural patterns) являются выражением фундаментальных, общих закономерностей структурной организации программных систем и предлагают набор предопределенных подсистем, специфицируют их функции и включают принципы организации связей между ними. • Языки паттернов (pattern languages) определяют словарь для обсужде- ния проблем разработки программного обеспечения и предлагают про- цесс упорядоченного решения этих проблем. Обычно паттерны и языки паттернов скрыты в головах разработчиков-экс- пертов или в исходном коде программных приложений и систем. Оставлять эту ценную информацию в указанных местах — и дорого, и рискованно. Фиксация и документирование паттернов сетевых приложений в явном виде помогает до- биться следующего: * Сохранить важную проектную информацию для программистов, кото- рые совершенствуют и сопровождают существующее программное обес- печение. Эта информация, если ее не задокументировать, будет утрачена, что может привести к росту энтропии ПО и снижению удобства сопрово- ждения и качества ПО. • Управлять выбором проектных решений разработчиками, создающи- ми новые приложения. Так как паттерны документируют подводные камни и ловушки общего характера в своей предметной области, они по- могают разработчикам выбирать подходящие архитектуры, протоколы, алгоритмы и возможности платформ, не тратя времени и усилий на дуб-
Объектно-ориентированные каркасы и сетевые приложения 31 лирование решений, о которых уже известно, что они неэффективны или подвержены ошибкам. Знание паттернов и языков паттернов помогает уменьшить объем проект- ных работ и затраты на сопровождение. Однако само по себе повторное исполь- зование паттернов не приводит к созданию гибких и эффективных программ. Хотя паттерны способствуют повторному использованию абстрактных кон- цепций и знаний об архитектуре, программные абстракции, задокументиро- ванные в виде паттернов, не создают собственно повторно используемый код. Поэтому важно дополнить изучение паттернов созданием и применением кар- касов. Каркасы помогают разработчикам избежать дорогостоящего дублирова- ния стандартных программных решений путем конкретизации общих паттер- нов и языков паттернов и путем рефакторинга обычных ролей в реализациях. Пользователи АСЕ могут быстро создавать сетевые приложения, так как ACE-каркасы реализуют основные паттерны, связанные с доступом к службам, обработкой событий, параллелизмом и синхронизацией [POSA2]. Такой спо- соб передачи знаний делает АСЕ более доступным и непосредственно примени- мым по сравнению со многими другими обычными способами передачи зна- ний, такими как семинары, конференции или непосредственный анализ проек- тов и программ. Хотя все эти способы полезны, они являются ограниченными, так как участвующие учатся по прошлой работе других, а потом пытаются при- менить ее к своим .текущим и будущим проектам. По сравнению с этим, АСЕ обеспечивает непосредственную передачу знаний путем включения паттернов, использующих каркасы, в мощное инструментальное средство, которое содер- жит и накопленный в области сетевых приложений опыт и, что важно, рабо- тающий код. Например, JAWS [HS99] является высокопроизводительным адаптивным web-сервером, с открытыми исходными кодами, созданным с использованием ACE-каркасов. На рис. 1.4 показано, что web-сервер JAWS состоит из набора взаимодействующих каркасов, при создании которых использовались паттер- ны, перечисленные вдоль границ рисунка. Эти паттерны помогают решать об- щие проблемы проектирования, которые возникают при разработке парал- лельных серверов, включая инкапсуляцию низкоуровневых API операционных систем; отделение демультиплексирования событий и управление соединения- ми от обработки, связанной с реализацией протоколов; масштабирование про- изводительности серверов за счет многопоточности; минимизации издержек, связанных с поточной реализацией серверов; эффективного использования асинхронного ввода/вывода и улучшения возможностей конфигурирования серверов. Дополнительную информацию о паттернах и структуре JAWS можно найти в первой главе POSA2. 1.24 Каркасы и интегрированные модели Вычисления на основе интегрированных моделей (model-integrated computing, MIC) JSK97] являются одной из новых парадигм разработки, кото- рая использует языки моделирования, зависящие от конкретной предметной области, для систематической разработки программ: от небольших программ
Глава 1 32 Reactor/Proactor Каркас стратегий ввода/вывода Strategy Singleton Кэшируемая виртуальная^ файловая система Экспандер тильды Strategy Active object Каркас конвейеров протоколов Каркасстратегий параллелизма Asynchronous completion token Управление протоколами фильтрация • j протоколов Pipes and filters Component configurator ,/home/ Диспетчер событий о 3 о a о Рис. 1.4 Паттерны, образующие архитектуру JAWS встроенных систем реального времени до больших корпоративных приложе- ний. MIC-среды разработки включают средства анализа на основе использова- ния моделей, зависящих от конкретных предметных областей и средства синте- за программ на основе моделей. MIC-модели могут, отражать суть некоторого класса приложений или концентрироваться на одном, заказном, приложении. MIC позволяет также моделировать сами языки моделирования и среды с по- мощью так называемых метамоделей (meta-models) [SKLN01], что помогает синтезировать языки моделирования, зависящие от конкретной предметной области, которые могут отражать мельчайшие детали предметных областей, для моделирования которых они предназначены, делая это знание доступным для повторного использования. Распространенные примеры MIC, используемые сегодня, включают Gene- ric Modeling Environment (GME) [LBM+01] и Ptolemy [BHLM94] (последний при- мер используется, в основном, во встроенных приложениях реального време- ни) и средства UML/XML, основанные на OMG Model Driven Architecture (MDA) [ObjOlb] (до сих пор использовались, в основном, в коммерческих при- ложениях). При правильной реализации эти MIC-технологии помогают до- биться следующего:
Объектно-ориентированные каркасы и сетевые приложения 33 • Снять с разработчиков приложений зависимость от конкретных про- граммных API, что обеспечивает возможность повторного использова- ния моделей в течение долгого времени, даже в том случае, если сущест- вующие программные API будут заменены новыми. • Обеспечить доказательство правильности различных алгоритмов путем автоматического анализа моделей и предложить улучшения для удовле- творения различного рода ограничений. • Создать код, который является высоконадежным и отказоустойчивым, так как сами средства моделирования могут быть синтезированы из ме- тамоделей с помощью доказуемо правильных технологий. • Быстро прототипировать новые концепции и приложения, которые с по- мощью данной парадигмы могут быть смоделированы быстро, по срав- нению с тем объемом работы, который требуется для создания прототи- па «вручную». • Повторно использовать удачные модели конкретных предметных облас- тей, экономя значительное количество времени и усилий, а также снижая время создания приложений и улучшая их совместимость и качество. Как показано на рис. 1.5, процесс разработки MIC использует набор средств для анализа взаимозависимых свойств приложения, зафиксированных в моде- ли, и определения возможности обеспечения различного рода требований QoS в контексте заданных ограничений. Другой набор средств затем преобразует модели в исполняемые спецификации, которые отражают функциональность платформы, ограничения и взаимосвязи с окружающей средой. Эти исполняе- мые спецификации, в свою очередь, могут быть использованы для синтеза при- кладных программ. Рис. 1.5 Этапы процесса разработки с использованием вычислений на основе интегрированных моделей 2 Программирование сетевых приложений на C++. Том 2
34 Глава 1 При прежних попытках разработки на основе моделей и синтеза кода, кото- рые предпринимались в CASE-средствах, обычно реализовать потенциал этого подхода не удавалось по следующим причинам [ АП02]: • Эти средства пытались генерировать приложения целиком, включая ин- фраструктуру & прикладную логику, что приводило к неэффективному, раздутому коду, который было трудно оптимизировать, проверять, раз- вивать и объединять с существующим кодом. • Из-за отсутствия достаточно развитых языков, зависящих от конкретной предметной области, и связанных с ними средств моделирования, было трудно достичь взаимообратимого (round-trip) проектирования, то есть прозрачного прямого И обратного преобразования между модельными представлениями и синтезированным кодом. • Так как CASE-средства и первые языки моделирования могли работать, в основном, с ограниченным набором платформ (таких как мэйнфреймы) и традиционными языками программирования (такими как COBOL), они не очень соответствовали парадигме распределенных вычислений, кото- рая возникла из достижений в технологии персональных компьютеров и Internet, а также новых объектно-ориентированных языков програм- мирования, таких как Java, C++ и С#. Многие ограничения вычислений с интегрированными моделями, пере- численные выше, могут быть преодолены путем объединения средств и про- цессов MIC с объектно-ориентированными каркасами [GSNW02]. Это объеди- нение помогает преодолеть проблемы CASE-средств предыдущих поколений, так как оно не требует от средств моделирования генерировать весь код. Вместо этого, значительные части приложений могут формироваться (compose) из по- вторно используемых, предварительно апробированных классов каркасов. Аналогичным образом, интеграция MIC с каркасами помогает работать в сре- дах, где требования к приложениям и их функциональность изменяются быст- рыми темпами за счет синтеза и сборки новых расширенных классов каркасов и автоматизации настройки многих QoS-критичных аспектов, таких как парал- лелизм, распределение, транзакции, безопасность и надежность. Объединение обработки, основанной на интегрированных моделях, с кар- касами, компонентами и паттернами является областью интенсивных исследо- ваний [Вау02]. В группе DOC, например, в настоящее время ведется научно-ис- следовательская работа по созданию комплекта MIC-средств, называемых Com- ponent Synthesis with Model-Integrated Computing (CoSMIC)1 [GSNW02]. CoSMIC расширяет возможности популярных средств моделирования и синтеза GME [LBM+01] и The АСЕ ORB (TAO) [SLM98] с целью поддержки разработки, сбор- ки и развертывания QoS-совместимых сетевых приложений. Чтобы гарантиро- вать, что требования QoS могут быть реализованы в промежуточном слое, в ин- струментальных средствах CoSMIC есть возможность задавать и анализировать требования QoS к прикладным компонентам в связанных с ними метаданных. 1 Синтез компонентов на основе интегрированных моделей. — Прим. ред.
Объектно-ориентированные каркасы и сетевые приложения 35 1.3 Применение каркасов в сетевых приложениях Одна из причин того, что писать отказоустойчивые, расширяемые и эф- фективные сетевые приложения трудно, заключается в том, что разработчики должны владеть многими сложными концепциями и механизмами сетевого программирования, включая следующие: • Сетевая адресация, обнаружение и идентификация служб. • Преобразования на уровне представления, такие как маршалинг, демар- шалинг и кодирование, для работы в гетерогенной среде с хостами, у ко- торых процессоры имеют разный порядок байтов. • Механизмы локального и удаленного межпроцессного взаимодействия (IPC). • Демультиплексирование событий и диспетчеризация обработчиков со- бытий. • Синхронизация и управление временем жизни процессов/потоков. Интерфейсы прикладного программирования (API) и инструментальные средства развивались на протяжении нескольких лет в направлении упроще- ния разработки сетевых приложений и промежуточного слоя. На рис. 1.6. пока- заны IPC API, имеющиеся на платформах ОС: от UNIX до многих операцион- ных систем реального времени. На этом рисунке показаны способы, с помо- щью которых приложения обращаются к сетевым API, для локального и удаленного IPC на нескольких уровнях абстрагирования. Ниже мы кратко рассмотрим каждый из уровней абстрагирования, начиная с низкоуровневых API ядра до имеющихся в ОС сетевых API пользовательского уровня и до про- межуточного слоя инфраструктуры хоста. Сетевые API уровня ядра. Низкоуровневые сетевые API находятся в под- системе ввода/вывода ядра ОС. Например, системные функции UNIX put- msg () ngetmsgO могут быть использованы для обращения к Transport Provi- der Interface (TPI) [OSI92b] и Data Link Provider Interface (DLPI) [OSI92a], которые имеются в System V STREAMS [Rit84]. Можно также разрабатывать сетевые Высокий j k ПРОМЕЖУТОЧНЫЙ СЛОЙ ИНФРАСТРУКТУРЫ ХОСТА Уровень абстракции Низкий SOCKETS &TLI open()/dose()/putmsg()/getmsg() STREAM^™ NPI V FRAMEWORK ж DLPI Пользовательское пространство Пространство ядра Рис. 1.6 Уровни абстракции сетевого программирования г
36 Глава 1 службы, такие как службы маршрутизации [КМС+ОО], сетевые файловые систе- мы [WLS*85] или даже web-серверы [ JKN+01 ], которые полностью располагают- ся внутри ядра ОС. Однако непосредственное программирование сетевых API уровня ядра редко бывает переносимым на платформы других ОС. Часто такое программирование не является переносимым даже на другие версии одной и той же ОС! Так как программирование на уровне ядра в большинстве сетевых приложений не используется, в этой книге мы его рассматривать не будем. Из- ложение этой темы, в контексте System V UNIX, BSD UNIX и Windows 2000, можно найти в [Rag93], [SW95, MBKQ96] и [SR00] соответственно. Сетевые API пользовательского уровня. Стеки сетевых протоколов в со- временных коммерческих операционных системах располагаются в защищен- ном адресном пространстве ядра ОС. Приложения, работающие в пользова- тельском пространстве, обращаются к стекам протоколов ядра ОС с помощью IPC API, таких как Socket или TLI API. Эти API работают совместно с ядром ОС, чтобы реализовать возможности, представленные в следующей таблице: Возможность Описание Управление локальной конечной 1 точкой Создание и удаление локальных конечных точек соединений, позволяющих обращаться к имеющимся сетевым средствам. Установление и завершение соединений Возможность приложений устанавливать соединения, активно или пассивно, с удаленными партнерами и закрывать все соединения, или часть соединений, после завершения процессов обмена данными. Управление опциями Настройка и разрёшение/запрет (enable/disable)' опций протоколов и конечных точек соединений. Механизмы передачи данных Обмен данными между одноранговыми приложениями. Преобразование имя/адрес Преобразование удобных для восприятия человеком имен в низкоуровневые сетевые адреса и обратно. | Эти возможности, применительно к Socket API, рассмотрены в главе 2 [C++NJM]. Многие IPC API сделаны кое-как поверх UNIX API файлового ввода/выво- да, в котором определены функции open (), read (), write (), close (), ioctl (), Iseek () и select () [Rit84]. Однако из-за синтаксических и семан- тических различий между файловым и сетевым вводом/выводом сетевые API обеспечивают дополнительные функции, которые не поддерживаются напря- мую стандартными API UNIX файлового ввода/вывода. Например, имена пу- тей (полные имена), используемые в UNIX-системе для идентификации фай- лов не являются глобально уникальными на уровне хостов в гетерогенной рас- пределенной среде. Поэтому для однозначной идентификации конечных точек соединений, используемых сетевыми приложениями, были разработаны раз- личные схемы именования, такие как IP-адреса хостов и номера TCP/UDP пор- тов.
Объектно-ориентированные каркасы и сетевые приложения 37 Каркасы промежуточного слоя инфраструктуры хоста. Многие сетевые приложения обмениваются сообщениями с помощью синхронных и/или асин- хронных протоколов типа запрос/ответ и каркасов промежуточного слоя ин- фраструктуры хоста. Промежуточный слой инфраструктуры хоста инкапсули- рует ОС-механизмы параллелизма п IPC с целью автоматизации многих низко- уровневых аспектов разработки сетевых приложений, включая следующие: • Управление соединениями и инициализация обработчиков событий. • Обнаружение и демультиплексирование событий, диспетчеризация об- работчиков событий. • Разбиение сообщений на кадры поверх протоколов управления потока- ми байтов, таких как TCP. • Вопросы, связанные с преобразованием формы представления, включая сетевой порядок байтов и маршалинг/демаршалинг параметров. • Модели параллелизма и синхронизация параллельных операций. • Компоновка сетевых приложений из динамически конфигурируемых служб. • Иерархическое упорядочение многоуровневых сетевых приложений и служб. • Управление характеристиками, связанными с качеством обслуживания (QoS), например, планирование доступа к процессорам; сетям и памяти. Растущая доступность и популярность высококачественного и приемлемо- го по цене промежуточного слоя инфраструктуры хоста способствует повыше- нию уровня абстрагирования, на котором разработчики сетевых приложений могут работать эффективно. Например, в [C++NPvl, SS02] представлен обзор более высоких уровней промежуточного слоя в модели вычислений с распреде- ленными объектами, таких как CORBA [Obj02] и The АСЕ ORB (TAO) [SLM98], который представляет собой реализацию CORBA, созданную с использованием каркасов и классов из АСЕ. Тем не менее, по-прежнему-, полезно знать, как рабо- тают низкоуровневые механизмы IPC, чтобы в полной мере осознавать пробле- мы, которые возникают при проектировании, переносе на другие платформы и оптимизации сетевых приложений. 1.4 Экскурс в АСЕ-каркасы 14.1 Общая характеристика АСЕ АСЕ представляет собой широко используемое, высоко переносимое инст- рументальное средство промежуточного слоя инфраструктуры хоста с открыты- ми исходными кодами. Исходный код можно свободно загрузить по следующим адресам: http: / /асе.есе.uci . edu/ или http: //www.riverace. com/. Основная часть библиотеки АСЕ содержит приблизительно четверть мил- лиона строк кода на C++, включающего около 500 классов. Многие из этих
Глава 1 38 классов совместно формируют большую часть каркасов АСЕ. Библиотека АСЕ включает также компоненты более высокого уровня, а также большой набор примеров и обширный комплект автоматизированных регрессивных тестов. Чтобы разделить уровни ответственности, уменьшить сложность и сделать возможной функциональную декомпозицию, АСЕ спроектирован с использо- ванием многоуровневой архитектуры [POSA1], показанной на рис. 1.7. Воз- можности, реализуемые АСЕ, охватывают прикладной уровень, уровень пред- ставления и уровень сеансов в эталонной модели OSI [В1а91 ]. Фундаментом ин- струментальной среды АСЕ является объединение уровня адаптации к ОС и интерфейсных фасадов, написанных на языке C++, которые совместно ин- капсулируют основные механизмы ОС, связанные с сетевым программирова- нием, чтобы обеспечить переносимое функционирование на всех платформах ОС, представленных в блоке 2. Уровни АСЕ, расположенные выше, строятся на этом фундаменте, и обеспечивают повторно используемые каркасы, компонен- ты сетевых служб и стандартный промежуточный слой. Уровень компонентов сетевых служб Уровень каркасов приложений JAWS ADAPTIVE WEBSERVER CAPI Стандартный промежуточный слой THE АСЕ ORB (TAO) Уровень интерфейс- ных фаса- SHARED LOG MSG MEM MAP Й FILE SAP CORBA SERVICE CONFI- GURATOR REACTOR/ PRO- ACTOR PROCESS/ TREADS SYNCH WRAPPERS SPIPE SAP FIFO SAP Уровень адаптации к ОС SOCKETS/ TLI PROCESSED/ HnamEDRPES! TREADS SOCK iSAP/TU SAP SHARED MEMORY SELECT/ IO COMP DYNAMIC LINKING Подсистема коммуникаций Подсистема виртуальной памяти и файлов FILE SYS APIS Подсистема процессов/потоков Общие сервисы операционной системы Рис. 1.7 Многоуровневая архитектура АСЕ 1.4.2 Краткий обзор АСЕ-каркасов Каркасы представляют собой интегрированные совокупности классов, ко- торые можно настраивать и создавать на их основе экземпляры объектов с це-
Объектно-ориентированные каркасы и сетевые, приложения 39 лью создания законченных сетевых приложений и компонентов служб. Карка- сы способствуют передаче накопленных десятилетиями знаний непосредственно от разработчиков АСЕ пользователям в виде экспертных знаний, зафиксирован- ных в хорошо проверенных и. повторно используемых программных конст- рукциях C++. Каркасы АСЕ реализуют язык паттернов для программирования параллельных объектно-ориентированных сетевых приложений. На рис. 1.8 изображены ACE-каркасы. Чтобы показать, как ACE-каркасы связаны между собой и как используют друг друга, стрелки между блоками показывают на- правление зависимости. Ниже кратко описан каждый каркас. Рис. 1.8 Основные каркасы АСЕ Блок 2: Платформы ОС, поддерживаемые АСЕ АСЕ работает на многих операционных системах, включая: • ОС персональных компьютеров (PC), например, Windows (32- и 64-разряд- ные версии), WinCE и Macintosh OSX; • Большинство версий UNIX, например, SunOS/Solaris, IRIX, Tru64 UNIX (Digital UNIX), AIX, DG/UX, Linux (Redhat, Debian и SuSE), SCO OpenServer, UnixWare, NetBSD и FreeBSD; • Операционные системы реального времени, например, VxWorks, ChorusOS. LynxOS, Pharlap TNT, QNX Neutrino и RTP, RTEMS и pSoS; • ОС больших корпоративных систем, например, OpenVMS, MVSOpenEdition, Tandem NonStop-UX и Cray UNICOS. АСЕ можно использовать co всеми основными компиляторами C++ для указан- ных платформ. Web-сайт, посвященный АСЕ, который находится по адресу http: / /асе. есе. uci. edu, содержит полный, обновляемый список платформ, а также инструкции по загрузке и компоновке АСЕ. ACE-каркасы Reactor и Proactor. Данные каркасы реализуют паттерны Reactor и Proactor [POSA2] соответственно. Оба они являются архитектурными паттернами, которые предоставляют приложениям возможность управления по событиям, поступающим к приложению от одного или нескольких источ- ников, самыми важными из которых являются конечные точки ввода/вывода. Каркас Reactor упрощает модель взаимно-согласованного (reactive) ввода/выво- да, используя события, сообщающие о возможности начать операцию син- хронного ввода/вывода. Каркас Proactor спроектирован для модели актив- но-превентивного (proactive) ввода/вывода, в которой инициируются одна или
40 Глава 1 несколько операций асинхронного ввода/вывода, а завершение каждой опера- ции является источником события. Модели активно-превентивного ввода/вы- вода могут достигать высокой производительности, характерной для парал- лельной обработки, избегая многих ее недостатков. Каркасы Reactor и Proactor автоматизируют обнаружение, демультиплексирование и диспетчеризацию определяемых приложением обработчиков, реагирующих на события разных типов. В главах 3 и 4 рассматривается ACE-каркас Reactor, а в главе 8 — АСЕ- каркас Proactor. ACE-каркас Service Configurator. Данный каркас реализует паттерн Com- ponent Configurator [POSA2], который является паттерном проектирования, позволяющим подключать и отключать реализации своих компонентов, не требуя модификации, повторной статической компиляции или компоновки. ACE-каркас Service Configurator поддерживает конфигурацию приложений, службы которых могут быть смонтированы на более позднем этапе цикла про- ектирования, например, во время инсталляции и/или во время выполнения. Приложения, к работоспособности которых предъявляются повышенные тре- бования, например, ответственные (mission-critical) системы, выполняющие онлайн-обработку транзакций или автоматизирующие технологические про- цессы реального времени, часто требуют наличия такого, рода возможностей гибкого конфигурирования. В главе 2 рассматриваются аспекты проектирова- ния, связанные с конфигурацией сетевых служб, а глава 5 — ACE-каркас Service Configurator. ACE-каркас Task. Данный каркас реализует различные паттерны, связан- ные с параллельной обработкой, такие как Active Object и Half-Sync/Half-Async [POSA2]. Active Object — паттерн проектирования, отделяющий поток, кото- рый выполняет метод, от потока, который его вызывает. Назначение этого пат- терна — повысить степень параллелизма и упростить синхронизацию доступа к объектам, которые.выполняются в своих собственных потоках управления. Half-Sync/Half-Async — архитектурный паттерн, разделяющий синхронную и асинхронную обработку в параллельных системах для упрощения програм- мирования, без заметного снижения производительности. Этот паттерн вклю- чает два взаимодействующих уровня, один для асинхронной и один для син- хронной служебной обработки. Уровень организации очередей является свя- зующим звеном во взаимодействии между службами асинхронного и синхронного уровней. В главе 5 [C++NPvl] рассмотрены аспекты проектиро- вания, связанные с параллельными сетевыми приложениями, а в главе 6 данной книги рассматривается ACE-каркас Task. ACE-каркас Acceptor-Connector. Данный каркас использует каркас Reactor и реализует паттерн Acceptor-Connector [POSA2]. Этот паттерн проектирова- ния отделяет подключение и инициализацию взаимодействующих одноранго- вых служб сетевой системы от той обработки, которую они выполняют после подключения и инициализации. Каркас Acceptor-Connector отделяет роли ак- тивной и пассивной инициализации от определяемой приложением служеб- ной обработки, выполняемой взаимодействующими одноранговыми служба- ми после завершения инициализации. Этот каркас рассматривается в главе 7.
Объекзпно-ориентированные каркасы и сетевые приложения 41 ACE-каркас Streams. Данный каркас реализует паттерн Pipes & Filters, архи- тектурный паттерн, который обеспечивает структуру для систем обработки по- токов данных [POSA1]. ACE-каркас Streams упрощает разработку и формиро- вание иерархически упорядоченных многоуровневых служб, таких как стеки протоколов прикладного уровня и агенты управления сетью [SS94]. Этот кар- кас рассматривается в главе 9. При совместном использовании, перечисленные выше ACE-каркасы по- зволяют разрабатывать сетевые приложения, которые можно обновлять и рас- ширять, при этом не требуется модифицировать, повторно компилировать и компоновать или перезапускать выполняющиеся приложения. Такой беспрецедентной гибкости и расширяемости АСЕ достигает путем объединения: • Механизмов ОС, таких как демультиплексирование событий, IPC, дина- мическое связывание (linking), многопоточность, многозадачность (mul- tiprocessing) и синхронизация [Ste99]. • Возможности языка C++, такие как шаблоны, наследование и динамиче- ское связывание (binding) [BjaOO], • Паттерны, такие как Component Configurator [POSA2], Strategy [GoF] й Handler/Callback [Вег95]. ACE-каркасы обеспечивают инверсию управления с помощью обратных вызовов, в соответствии со следующей таблицей, приведенной ниже: АСЕ-каркасы Инверсия управления Reactor и Proactor Осуществляют обратные вызовы обработчиков событий, реализуемых приложениями с целью обработки синхронных или асинхронных событий. Service Configurator Осуществляют обратные вызовы объектов служб, реализуемых приложениями, чтобы инициализировать их, приостанавливать их работу, возобновлять ее и завершать. Task Осуществляют обратные вызовы hook-методов, реализуемых приложениями с целью выполнения обработки в одном или нескольких потоках управления. Acceptor- Connector Осуществляют обратные вызовы обработчиков служб, чтобы инициализировать их после подключения. Streams Осуществляют обратные вызовы, чтобы инициализировать и завершать задачи при их включении (push) в конвейер (stream) и исключении (pop) из него. Callback-методы определяются в классах ACE-каркасов как виртуальные методы C++. Такое использование динамического связывания позволяет сете- вым приложениям свободно создавать и расширять интерфейсные методы, не модифицируя и не компонуя заново существующие классы каркасов. Интер- фейсные фасады АСЕ, наоборот, редко используют обратные вызовы или вир- туальные методы, поэтому они не такие гибкие, как ACE-каркасы. Тем не менее,
42 Глава 1 Рис. 1.9 Применение библиотек классов при разработке и использовании АСЕ-каркасов интерфейсные фасады АСЕ все же поддерживают большой диапазон вариантов применения и могут объединяться с помощью методов обобщенного програм- мирования [ AleOl ] на основе характеристик (traits) C++ и идиом классов харак- теристик (traits classes idioms); которые описаны в блоке 40. Из рис. 1.9 видно, что библиотеки классов и каркасы в АСЕ являются допол- няющими друг друга технологиями. Инструментальная среда АСЕ упрощает реализацию своих каркасов с помощью собственной библиотеки контейнер- ных классов, которая включает списки, очереди, хэш-таблицы, строки и другие, повторно используемые структуры данных. Аналогично, определяемый при- ложением код, который вызывается обработчиками событий в АСЕ-каркасе Reactor может использовать интерфейсные фасады АСЕ и стандартную биб- лиотеку классов C++ [ Jos99] для выполнения операций, связанных с IPC, син- хронизацией, управлением файлами и обработкой строк. В блоке 3 приведена информация о том, как скомпоновать библиотеку АСЕ, чтобы можно было экс- периментировать с примерами, которые мы приводим в этой книге. 1.5 Пример: сетевая служба регистрации Наш опыт показывает, что принципы, методы и навыки, нужные для раз- работки и применения повторно используемого прикладного сетевого ПО, не могут быть усвоены только посредством утверждений общего характера или упрощенных примеров. Нет, программисты должны освоить конкретные тех- нические приемы и приобрести практический опыт, разрабатывая и применяя реальные каркасы и приложения. Поэтому мы иллюстрируем важные моменты и возможности АСЕ на протяжении всей книги путем развития и совершенст- вования примера сетевой службы регистрации, он был представлен
Объектно-ориентированные каркасы и сетевые приложения 43 Блок 3: Компоновка АСЕ и программ, использующих АСЕ АСЕ — это программное обеспечение с открытыми исходными кодами, его МОЖНО загрузить С сайтов http: //ace. ece.uci .edu HAHhttp://www. rivera- ce . com и скомпоновать самостоятельно. Эти сайты содержат большое коли- чество материалов по АСЕ, таких как учебники, отчеты и обзор интерфейсных фасадов и каркасов АСЕ, которые не рассматриваются в данной книге. Вы мо- жете также приобрести в компании Riverace по номинальной цене предвари- тельно скомпонованную версию АСЕ. Список компиляторов и платформ ОС, для которых имеются предварительно скомпонованные версии, смотрите на http://www.riverace.com. Если вы хотите скомпоновать АСЕ самостоятельно, вы должны загрузить и раз- архивировать дистрибутив АСЕ в свободный каталог. Корневой каталог дист- рибутива называется ACE_wrappers. Мы ссылаемся на этот корневой каталог как на ace_root. Создайте переменную окружения с этим именем и присвой- те ей значение полного пути к корневому каталогу АСЕ. Исходные и заголовоч- ные файлы находятся в каталоге $АСЕ_яоот/асе. Файл $ace_root/ACE-iNSTALL.html содержит полный набор инструкций по компоновке АСЕ, включая настройку АСЕ на нужные ОС и компилятор. Исход- ные тексты и заголовочные файлы для примеров сетевой службы регистрации этой книги находятся в $ACE_ROOT/examples/c++npv2 и подготовлены к компо- новке на любой из платформ, поддерживаемых АСЕ. При компиляции ваших программ каталог ace_root должен быть включен в список путей к заголовоч- ным файлам в вашем компиляторе. Для компиляторов командной строки это можно сделать с помощью опций компилятора -I или /х. IDE с графическим интерфейсом обеспечивают аналогичные опции, например, в среде MSVC++ раздел «Preprocessor, Additional include directories» (Препроцессор, Дополни- тельные каталоги включаемых файлов) на вкладке C/C++ в диалоговом окне Project Settings (Настройки проекта). в [C++NPvl], которая собирает и фиксирует диагностическую информацию по- сылаемую одним или несколькими клиентскими приложениями. Служба регистрации в [C++NPvl], использовала двухзвенную архитектуру клиент/сервер и многие интерфейсные фасады АСЕ. Примеры службы регист- рации в данной книге используют более мощную архитектуру, которая иллю- стрирует расширенное множество возможностей и паттернов и Показывает ка- ким образом ACE-каркасы могут помочь успешно создавать эффективные, предсказуемые и масштабируемые сетевые приложения. Эта служба демонст- рирует также основные проектные и реализационные соображения и решения, с которыми вы встретитесь при разработке собственных параллельных объект- но-ориентированных сетевых приложений. На рис. 1.10 показаны прикладные процессы и процессы-демоны нашей се- тевой службы регистрации, которые мы перечисляем ниже. Прикладные клиентские процессы (такие как Рь Р2 и Рэ) выполняются на хостах-клиентах и создают регистрационные записи в диапазоне от отладочной информации до сообщений о критических ошибках. Регистрационная инфор- мация, посылаемая клиентским приложением, содержит время создания реги- страционной записи, идентификатор процесса приложения, уровень приори- тета регистрационной записи и строку переменной длины, содержащую текст
Глава 1 сообщения регистрационной записи. Клиентские приложения посылают эти регистрационные записи клиентскому процессу-демону регистрации (client log- ging daemon), работающему на том же локальном хосте. Клиентские процессы-демоны регистрации работают на каждом хост- компьютере, участвующем в работе данной сетевой службы регистрации. Каж- дый клиентский демон регистрации получает регистрационные записи от кли- ентских приложений своего хоста с помощью одного из механизмов локально- го IPC, типа общей памяти, каналов (pipes) или сокетов. Клиентский демон ре- гистрации использует механизм удаленного IPC, типа TCP/IP, для передачи регистрационных записей серверному процессу-демону регистрации (server log- ging daemon), работающему на заданном хосте. Серверные процессы-демоны регистрации собирают и выводят посту- пающие регистрационные записи, Которые они получают от клиентских при- ложений при посредничестве клиентских процессов-демонов регистрации. Серверный процесс-демон регистрации имеет возможность определять какой клиентский хост послал каждое из сообщений, используя адресную информа- Oct 31 14:48:13 2001@tango.ece.uci.edu@38491@7(?client::unable to fork in function spawn Oct 31 14:50:28 2001@mambo.cs.wustl. edu@18352@2@drwho::sending request to server tango Консоль Запоминающее устройство Локальный IPC Клиентский демон регистрации TCP- соеди- нение Клиент Tango int spawn (void){ if (ACE_OS: -.fork () ACE-ERROR (LM_ERROR, "unable to fork in function spawn"); jl Принтер if (Options::instance ()->debug()) ACE_DEBUG ((LM_DEBUG, "sending request to server %s server_host)); [ Tango ) [Mambo] Pi Локальный IPC Серверный демон регистрации Р2 Сервер Клиентский демон регистрации Сеть Рис. 1.10 Процессы и демоны в сетевой службе регистрации
Объектно-ориентированные каркасы и сетевые приложения 45 цию, которую он получает от базового Socket API. Существует, как правило, один серверный процесс-демон регистрации на одну системную конфигура- цию, хотя они могут дублироваться с целью защиты от сбоя. На рис. 1.11 показан ряд сетевых серверов приложений, которые мы разра- ботаем и будем использовать в этой книге. Эти клиентские и серверные процес- сы-демоны регистрации послужат примерами того, как можно использовать ACE-каркасы и интерфейсные фасады со следующими моделями параллелизма. Модель параллелизма Раздел Взаимно-согласованная (reactive) 3.5,4.2,5.4 Пул потоков 4.3,4.4,6.3 Поток-на-соединение 7.27.3 Производитель/потребитель 6.2,7.4,9.2 Активно-превентивная (proactive) 8.2 —8.5 1.6 Резюме Прикладное сетевое ПО на протяжении десятилетий разрабатывалось «вручную» и фактически с нуля. Непрерывный процесс повторных открытий и изобретений основных концепций и возможностей, очень долго поддержи- вал стоимость разработки и развития сетевых приложений на слишком высо- ком уровне. Самым важным, для решения этой проблемы, является повыше- ние объема и качества систематического повторного использования ПО. Промежуточный слой — это разновидность .ПО, которое является особен- ноэффективным для решения задачи систематического повторного использо- вания программных конструкций при создании сетевых приложений. Разра- ботка и применение промежуточного ПО является, следовательно, важным способом повышения уровня повторного использования. Тем не менее, суще- ствует множество технических и нетехнических проблем, которые делают раз- работку и повторное использование промежуточного слоя сложным. В данной главе было показано, как использовать объектно-ориентированные каркасы для преодоления этих сложностей. Чтобы упорядочить информацию, необходимую для выбора наиболее подходящих технологий разработки ПО, мы привели также описание различий между каркасами и библиотеками классов, компонентами, паттернами и разра- боткой на основе интегрированных моделей. Каждая технология играет свою роль в снижении затрат на разработку и поддержание жизненного цикла ПО, а также в повышении качества, функциональных возможностей и производи- тельности ПО. Результатом применения принципов разработки каркасов и паттернов к области сетевых приложений стали ACE-каркасы. Эти каркасы решают об- щие задачи сетевого программирования и могут настраиваться средствами
46 Глава 1 Рис. 1.11 Примеры серверов регистрации языка C++ для создания законченных сетевых приложений. При совместном применении ACE-каркасы упрощают создание, формирование, конфигурацию и перенесение на другие платформы сетевых приложений, без существенного снижения производительности. Далее в книге объясняется, как и для чего созда- вались ACE-каркасы, и приводится много примеров того, как АСЕ использует возможности C++ для достижения этих целей. Неосязаемым, но ценным, достоинством АСЕ является то, что она передает накопленные десятилетиями знания от разработчиков — пользователям АСЕ- каркасов в виде экспертных знаний, зафиксированных в тщательно протести- рованных классах C++, которые реализуют проверенные временем стратегии разработки прикладного сетевого ПО. Эти каркасы вобрали в себя множество человеко-лет разработки, оптимизации и отладки. К счастью, вы можете вос- пользоваться преимуществом экспертного знания, воплощенного в этих кар- касах. Вам не нужно заново открывать или заново изобретать паттерны и клас- сы, которые составляют их основу.
Глава 2 Аспекты проектирования: службы и конфигурации Краткое содержание Служба — это набор функциональных возможностей, предлагаемых кли- енту сервером. Распространенными службами, доступными сегодня в Internet являются: • Службы получения и поиска web-контента, например, Apache и Google. • Службы дистрибуции программного обеспечения, такие как Castanet, Citrix или Softricity. • Службы передачи электронной почты и сетевых новостей. • Доступ к файлам на удаленных компьютерах. • Синхронизация времени в сети. • Службы обработки платежей. • Службы потокового аудио/видео, такие как RealPlayer и QuickTime. Сетевые приложения можно создавать путем объединения в единое целое составляющих их служб в различные моменты времени: во время компиляции, в процессе статической компоновки, во время инсталляции или во время вы- полнения. В главах 1 и 5 [C++NPvl] был приведен предметный анализ коммуникаци- онных протоколов и механизмов, а также архитектур параллелизма, используе- мых сетевыми приложениями. Данная глава распространяет этот анализ на другие аспекты проектирования, связанные с основными характеристиками се- тевых приложений. Эти характеристики включают срок действия и структуру служб, способ идентификации сетевых служб и момент времени, в который они объединяются в единое целое с целью формирования законченных прило- жений. Эти аспекты проектирования важны для любого сетевого приложения, но особенно важны для ACE-каркаса Service Configurator (глава 5). Если, тем не менее, вы уже знакомы с этими аспектами проектирования, то можете перейти сразу к главе 3, с которой начинается описание собственно АСЕ-каркасов.
48 Глава 2 2.1 Аспекты проектирования: службы и серверы При проектировании сетевых приложений важно понимать разницу меж- ду службой (service), которая представляет собой некую возможность, предос- тавляемую клиентам, и сервером (server), который является механизмом, по- средством которого указанная возможность предоставляется. В проектных ре- шениях, касающихся служб и серверов, легко запутаться, важно рассматривать их отдельно. Данный раздел охватывает следующие аспекты проектирования служб и серверов: • Краткосрочные (short-duration) и долговременные (long-duration) службы. • Внутренние (internal) и внешние (external) службы. • Службы с сохранением (stateful) и без сохранения (stateless) состояния. • Многоуровневые/модульные (layered/modular) и монолитные (monoli- thic) службы. • Одно- (single-) и многофункциональные (multiservice) серверы. • Одноразовые (one-shot) и постоянные (standing) серверы. 2.1.1 Краткосрочные и долговременные службы Службы, предлагаемые сетевыми серверами, можно классифицировать как краткосрочные и долговременные. Продолжительность указывает на то, как долго служба удерживает системные ресурсы. Основная альтернатива этого ас- пекта проектирования касается потребления системных ресурсов, в то время как их лучше было бы использовать в других целях, и издержек на перезапуск службы, когда в ней возникает потребность. В сетевых приложениях этот ас- пект тесно связан с выбором протокола, поскольку требования, связанные с ус- тановкой различных протоколов, могут существенно варьироваться. Краткосрочные (short-duration) службы выполняются короткие, часто фиксированные, промежутки времени и обычно обрабатывают один запрос за одно обращение. Примеры краткосрочных служб включают вычисление теку- щего времени суток, определение Ethernet-адреса по IP-адресу и выборка дис- кового блока из кэша сетевого файлового сервера. Чтобы сэкономить время, за- трачиваемое на установление соединения, краткосрочные службы часто реали- зуются с помощью протоколов без установления соединения, таких как UDP/IP [Ste94]. Долговременные (long-duration) службы работают продолжительное, часто разное, время и могут в течение этого времени обрабатывать множество запросов. Примеры долговременных служб включают передачу больших про- граммных пакетов по FTP, загрузку МРЗ-файлов с Web-сервера по HTTP, пото- ковое аудио/видео с сервера посредством RTSP, удаленный доступ к ресурсам хоста по TELNET и выполнение удаленного архивирования файловой системы через сеть. Долговременные службы допускают более гибкий выбор протоко-
Аспекты проектирования: службы и конфигурации 49 лов. Например, с целью повышения эффективности и надежности, эти службы часто реализуются с использованием протоколов, ориентированных на уста- новление соединения, таких как TCP/IP [Ste94] или протоколов, ориентирован- ных на сеансы, таких как RTSP [SRL98] и SCTP [SX01 ]. Служба регистрации => С позиции отдельной регистрационной записи, наш серверный демон регистрации выглядит как краткосрочная служба. Каж- дая регистрационная запись ограничена максимальным размером в 4 Кбайта, хотя, на самом деле, многие гораздо меньше. Реальное время, затрачиваемое на обработку одной регистрационной записи, относительно мало. Тем не менее, поскольку один клиент может передавать много регистрационных записей, мы оптимизируем производительность за счет такого дизайна, при котором кли- ентские демоны регистрации устанавливают соединения с их серверными де- монами регистрации. Затем мы многократно используем эти соединения для обработки следующих регистрационных записей. Было бы неэкономно и рас- точительно по времени устанавливать и разрывать соединение сокетов для ка- ждой регистрационной записи, особенно в том случае, когда часто посылаются записи небольшого объема. Поэтому мы реализуем наших клиентских и сер- верных демонов регистрации в виде долговременных служб. 2.1.2 Внутренние и внешние службы Службы можно разделить на внутренние и внешние. Основными альтерна- тивами этого аспекта являются время инициализации службы, изолирован- ность одной службы от другой и простота. Внутренние службы выполняются в том же адресном пространстве, что и сервер, принимающий запросы, как показано на рис. 2.1(1). Как было отмече- но в главе 5 [C++NPvl], внутренняя служба может выполняться последователь- но, параллельно или взаимно-согласованно по отношению к другим внутрен- ним службам. Внутренние службы обычно имеют небольшое время инициали- зации и время переключения контекста у них обычно меньше, чем у служб, работающих в разных процессах. (1) Внутренние службы Рис. 2.1 Внутренние и внешние службы (2) Внешние службы Процесс-диспетчер
50 Глава 2 Тем не менее, использование внутренних служб может привести к сниже- нию отказоустойчивости, так как разные службы внутри одного процесса не за- щищены друг от друга. Одна некорректно работающая служба может, следова- тельно, повредить данные, используемые совместно с другими внутренними службами этого процесса, что может привести к неправильным результатам, аварийному отказу или «зависанию» процесса. Таким образом, внутренние службы должны представлять собой код, о котором известно, что он работает корректно в сочетании с другими службами в адресном пространстве приложе- ния. Внешние службы выполняются в адресных пространствах разных процес- сов. На рис. 2.1 (2), например, показан основной (master) процесс службы, кото- рый контролирует набор сетевых портов. Когда от клиента приходит запрос на соединение, этот основной процесс принимает соединение и затем создает но- вый процесс, чтобы выполнить внешнее обслуживание клиента. Внешние службы могут быть более надежными, чем внутренние службы, так как ошибка одной из служб не приведет к ошибке в другой. Поэтому, чтобы повысить на- дежность, ответственные (mission-critical) прикладные службы часто изолиру- ют в отдельных процессах. Ценой такой надежности, тем не менее, может стать снижение производительности, из-за издержек, связанных с IPC и управлением процессами. Некоторые серверные каркасы поддерживают и внутренние, и внешние службы. Суперсервер (superserver) INETD [Ste98], например, является демоном, который прослушивает запросы на соединение или сообщения на отдельных портах и запускает программы, чтобы выполнить обслуживание, связанное с этими портами. Системные администраторы могут выбирать между внутрен- ними и внешними службами в INETD, изменяя конфигурационный файл inetd. conf следующим образом: • INETD можно настроить так, чтобы краткосрочные службы, такие как ECHO и DAYTIME, выполнялись как внутренние службы посредством вы- зовов статически компонуемых функций в программе INETD. • INETD можно также настроить так, чтобы долговременные службы, та- кие как ftp и telnet, выполнялись как внешние службы, путем созда- ния автономных процессов. В блоке 4 описывается этот и другие механизмы подготовки служб к работе, которые используют внутренние и внешние службы. Служба регистрации => Все реализации сервера регистрации в этой книге спроектированы как внутренние службы. До тех пор пока наш сервер регистра- ции настроен только на одну службу, нам не нужно ее изолировать от вредных побочных эффектов других служб. Тем не менее, существуют веские причины защищать работу сеансов разных клиентов друг от друга, особенно в том слу- чае, если службы компонуются динамически с помощью паттерна Component Configurator [POSA2]. Поэтому в главе 8 в [C++NPvl] показано как реализовать сервер регистрации в виде внешней службы с помощью классов ACE_Process and ACE_Process_Manager.
Аспекты проектирования: службы и конфигурации 2.1.3 Службы с сохранением и без сохранения состояния Службы могут быть разделены на службы с сохранением (stateful) и без со- хранения (stateless) информации о состоянии. Объем данных о состоянии, или контекст, который служба поддерживает между запросами, влияет на слож- ность клиентов и серверов и на потребление ими ресурсов. Альтернативами служб с сохранением и без сохранения состояния являются эффективность и надежность, а правильный выбор зависит от множества факторов, таких как переносимость и влияние сбоев хостов и сети. Службы с сохранением состояния хранят в кэше на сервере некоторую ин- формацию, такую как состояние сеанса, ключи аутентификации, коды иденти- фикации и дескрипторы ввода/вывода,.с целью уменьшения коммуникацион- ных и вычислительных непроизводительных издержек. Например, web-cookies позволяют web-серверу сохранять информацию о состоянии при выполнении нескольких запросов на страницы. Службы без сохранения состояния не поддерживают на сервере времен- ную информацию о состоянии. Например, Network File System (NFS) [Ste94] предоставляет службы распределенного хранения и извлечения данных, кото- рые не поддерживают временную информацию о состоянии в адресном про- странстве сервера. Каждый запрос, посылаемый клиентом, является полностью самодостаточным со всей необходимой для его выполнения информацией, та- кой как дескриптор файла, счетчик байтов, начальное смещение в файле и пол- номочия пользователя. Некоторые обычные сетевые приложения, такие как FTP и TELNET, не тре- буют поддержания постоянной информации о состоянии приложения между последовательностью вызовов службы. Такие службы без сохранения состоя- ния обычно достаточно просто настраивать и перенастраивать, сохраняя на- дежную работу. Напротив, CORBA Naming Service [Obj98] является распростра- ненной службой промежуточного слоя, которая управляет различными при- вязками (bindings), значения которых, возможно, нужно сохранять, даже если происходит полный отказ сервера, поддерживающего работу данной службы. Если сохранение информации о состоянии при сбоях является важным для пра- вильного функционирования системы, возможно, потребуется использовать монитор транзакций [GR93] или что-то типа активной репликации [BvR94]. Служба регистрации => Наша сетевая служба регистрации проявляет свой- ства и службы с сохранением и без сохранения состояния. Информация о со- стоянии, поддерживаемая серверным процессом, постоянно хранится, по боль- шей части, в ядре ОС (блоки управления соединениями) и в файловой системе (регистрационные записи). Тем не менее, и клиентские и серверные процес- сы-демоны службы регистрации в данной книге являются службами без сохра- нения состояния, так как они обрабатывают каждую запись автономно, не тре- буя, не используя и не ожидая никакой информации ни от предыдущего, ни от возможного последующего запроса. Потребность обрабатывать любую воз- можную последовательность запросов доводом не является, так как мы исполь- зуем протоколы TCP/IP, которые обеспечивают передачу упорядоченного, на- дежного потока байтов.
Глава 2 52 2.1.4 Многоуровневые/модульные и монолитные службы Реализации служб могут быть разделены на многоуровневые/модульные (layered/modular) и монолитные (monolithic). Основными альтернативами это- го аспектами являются повторное использование, расширяемость и эффектив- ность. Многоуровневые/модульные службы можно разложить на ряд автоном- ных, иерархически упорядоченных задач. Например, семейства приложений могут задаваться и реализовываться как многоуровневые/модульные службы, как показано на рис. 2.2 (1). Каждый уровень может обрабатывать автономный сегмент службы, такой как ввод и вывод, анализ событий, фильтрация событий и служебная обработка. Взаимосвязанные службы могут работать совместно, обмениваясь управляющими и информационными сообщениями, входящей и исходящей информацией. За последние несколько лет появились коммуникационные каркасы с боль- шими возможностями для упрощения и автоматизации разработки и конфигу- рации многоуровневых/модульных служб [SS93]. Примерами являются System V STREAMS [Rit84], х-kernel [НР91], каркас Conduits+ [HJE95] и АСЕ-каркас Streams (глава 9). Эти каркасы отделяют функциональные возможности служб от следующих аспектов их проектирования: • Композиционные стратегии, например, время и порядок объединения служб и протоколов в единое целое (главы 5 и 9 данной книги). • Стратегии параллелизма и синхронизации, такие как архитектуры с управлением по задачам и по сообщениям (глава 5 [C++NPvl]), кото- рые службы реализуют во время выполнения. (1) Многоуровневые/модульные службы Рис. 2.2 Многоуровневые/модульные и монолитные службы Глобальные данные svc4 SVC» SVCj (2) Монолитные службы
Аспекты проектирования: службы и конфигурации 53 • Коммуникационные стратегии, такие как механизмы протоколов и об- мена сообщениями (главы 1-3 [C++NPvl]), которые обеспечивают взаи- мосвязь служб друг с другом [SS95b ]. Монолитные службы являются тесно связанными наборами функцио- нальных возможностей, которые не упорядочены иерархически. Они могут включать отдельные функциональные модули, которые отдаленно напомина- ют уровни, но чаще всего они непосредственно связаны через общие, глобаль- ные переменные, как показано на рис. 2.2 (2). Они также часто сильно связаны функционально; их диаграммы потоков управления выглядят как «спагетти».. Монолитные службы, поэтому, трудно осваивать, поддерживать и развивать. И хотя иногда они могут пригодиться для временных, «одноразовых» прототи- пов [FYOO], они редко подходят для программ, которые придется поддерживать и совершенствовать нескольким разработчикам в течение долгого времени.1 У разработчиков часто есть возможность выбирать и многоуровневые и монолитные архитектуры служб для структурирования своих сетевых прило- жений. ACE-каркасы Task и Streams, рассматриваемые в главах 6 и 9, обеспечи- вают эффективные и открытые для изменения способы построения модульных служб. Проектирование многоуровнёвых/модульных служб имеет следующие преимущества: • Разбиение на уровни повышает возможность повторного использова- ния, поскольку множество прикладных служб более высокого уровня могут совместно использовать службы более низкого уровня. • Реализация приложений с помощью ряда взаимосвязанных многоуров- невых служб позволяет осуществлять прозрачное, поэтапное наращива- ние их функциональных возможностей. • Многоуровневая/модульная архитектура упрощает улучшение характе- ристик макроуровня, допуская избирательное исключение необязатель- ных функциональных возможностей службы или избирательную на- стройку службы на функциональные возможности, оптимально соответ- ствующие некоторому контексту. • Модульные дизайны обычно улучшают реализацию, тестирование и со- провождение сетевых приложений и служб. Однако использование многоуровневой/модульной архитектуры при раз- работке сетевых приложений имеет и свои недостатки: • Модульность многоуровневых реализаций может приводить к повыше- нию непроизводительных издержек. Например, разбиение на уровни может оказаться неэффективным, если размеры буферов соседних уров- цей не соответствуют друг другу, приводя, в связи с этим, к дополнитель- ной сегментации, сборке и задержках при передаче. • Связь между уровнями должна быть правильно спроектирована и реали- зована, что может служить еще одним источником ошибок. Когда вы получше освоитесь с АСЕ, вы поймете, что обычно гораздо быстрее сделать пра- вильно разделенный на уровни прототип, чем наспех «сколачивать» монолитные.
54 Глава 2 Приложение сетевой службы регистрации Управление соединениями и параллелизм Управление конфигурациями Инфраструктура событий Рис. 2.3 Уровни архитектуры сетевой службы регистрации • Информация, скрытая внутри уровней, может затруднять предсказуемое управление ресурсами в приложениях с жесткими требованиями, связан- ными с обработкой в реальном времени. Служба регистрации =* За счет тщательного разграничения задач проекти- рования наши клиентские и серверные демоны регистрации спроектированы в виде многоуровневой/модульной архитектуры, изображенной на рис. 2.3 и описанной ниже. 1. Уровень инфраструктуры событий, который отвечает за обнаружение со- бытий и за их диспетчеризацию связанным с ними обработчикам событий. В главах 3 и 4 описывается, каким образом паттерн Reactor может быть ис- пользован при реализации обобщенного уровня инфраструктуры собы- тий. Аналогично, в главе 8 описывается, каким образом для похожей цели могут быть применены паттерн Proactor и ACE-каркас Proactor. 2. Уровень управления конфигурациями, который отвечает за установку, инициализацию, управление и завершение работы компонентов служб. В главе 5 описывается, каким образом паттерн Component Configurator и ACE-каркас Service Configurator могут быть применены при реализации обобщенного уровня управления конфигурациями. 3. Уровень управления соединениями и параллелизма, который отвечает за выполнение соединений и инициализацию служб, представляющих собой независимые прикладные функциональные возможности. В главах 6 и 7 описывается как с помощью паттернов Acceptor-Connector и Half-Sync/ Half-Async, а также АСЕ-каркасов Acceptor-Connector и Task можно реали- зовать обобщенный уровень управления соединениями. 4. Прикладной уровень, который отвечает за настройку независимых от при - ложений классов, предоставляемых другими уровнями, с целью создания конкретных объектов, которые формируют приложения, обрабатывают события, устанавливают соединения, обмениваются данными и выполня- ют обработку, связанную со службой регистрации. На всем протяжении этой книги мы показываем, как реализовать эти возможности уровня при- ложений с помощью классов АСЕ-каркасов и интерфейсных фасадов АСЕ.
Аспекты проектирования: службы и конфигурации 55 2.1.5 Одно- и многофункциональные серверы Протоколы и службы редко используются автономно, чаще они использу- ются приложениями в контексте сервера. Серверы могут быть спроектированы как однофункциональные и как многофункциональные (мультисервисные). Альтернативами этого аспекта являются потребление ресурсов и отказоустой- чивость. Однофункциональные серверы предоставляют только одну службу. Как показано на рис. 2.4 (1), служба может быть внутренней или внешней, но суще- ствует только одна служба на процесс. Примерами однофункциональных служб являются: • Демон RWHO (RWHOD), который сообщает, сколько в системе актив- ных пользователей и выдает их идентификационную информацию. • Первые версии стандартных сетевых служб UNIX, таких как FTP и TEL- NET, которые работали как отдельные однофункциональные демоны, запускавшиеся во время загрузки ОС [Ste98]. Каждый экземпляр этих однофункциональных серверов выполнялся внешним образом в отдельном процессе. Однако по мере того как количество системных серверов увеличивалось, такой статически формируемый подход типа служба-на-процесс приводил к следующим ограничениям: • Требовал слишком много ресурсов ОС, таких как виртуальная память и записи таблицы процессов. • Требовал писать отдельно код, связанный с инициализацией и сетевым взаимодействием, для каждой служебной программы. • Требовал завершения выполняющихся процессов и их повторного за- пуска «вручную» при установке новых реализаций служб. • Требовал использования специализированных (ad hoc) и несовместимых ме- ханизмов администрирования для управления различными типами служб. Многофункциональные серверы снимают ограничения, связанные с од- нофункциональными серверами за счет интеграции совокупности однофунк- I жж Вйтренняя Дслужба/- Основной процесс | Внешняя служба Внешняя * служба Внешняя’ . служба, Внешняя служба служба'^ ВСПОМ( Вспомогательные процессы пр< Вспом< —;---- --- пре ВСПОМ< гт-7-------j —--------S Вспомогательны^ юмогательные процесс^',н'- I ж I (1) Однофункциональные серверы (2) Многофункциональные серверы Рис. 2.4 Одно- и многофункциональные серверы
56 Глава 2 циональных серверов в один административный блок, как показано на рис. 2.4 (2). Примерами многофункциональных серверов являются INETD (ко- торый ведет свое происхождение от BSD UNIX [MBKQ96, Ste98]), LISTEN (яв- ляется сетевой службой прослушивания System V UNIX [Rag93]) и Service Cont- rol Manager (SCM) (который ведет свое происхождение из Windows NT [SROO]). В блоке 4 проводится сравнение и противопоставление этих многофункцио- нальных серверов. Многофункциональный сервер может иметь следующие преимущества: • Может уменьшать потребление ресурсов ОС за счет запуска серверов по требованию. • Упрощает разработку серверов и способствует повторному использова- нию общего кода путем автоматической «демонизации» (daemonizing) серверного процесса (см. блок 5), инициализации транспортных конеч- Блок 4: Сравнение каркасов многофункциональных серверов В данном блоке проводится сравнение каркасов многофункциональных сер- веров, поддерживаемых различными версиями UNIX и Windows. • Внутренние службы inetd, такие как echo и daytime, фиксируются во время статической компоновки. Главный демон inetd позволяет динамически пе- ренастраивать свои внешние службы, такие как ftp или telnet. Например, когда демон inetd посылает сигнал sighup, он читает файл inetd.conf и выполняет последовательность вызовов socket О /bind. <) /listen () для всех служб, перечисленных в этом файле. Так как inetd, обычно, не поддер- живает динамическую реконфигурацию внутренних служб, любые новые службы в списке должны обрабатываться путем создания вспомогательных демонов с помощью семейства системных функций fork () и ехес* * (). • Средство мониторинга портов listen в System V UNIX похоже на inetd, хотя оно поддерживает протоколы, ориентированные на установление соеди- нения. обращение к которым осуществляется через ты и System V-streams, и не поддерживает внутренние службы. Однако в отличие от inetd listen поддерживает постоянно действующие серверы, дескрипторы инициали- зированных файлов через каналы streams от процесса listen к предвари- тельно зарегистрированному постоянно действующему серверу. ♦ В отличие от inetd и li sten, Windows scm не является монитором портов, так как не обеспечивает встроенную поддержку прослушивания множества портов ввода/вывода и диспетчеризацию серверных процессов по требо- ванию, при поступлении клиентских запросов. Вместо этого, scm обеспечи- вает интерфейс на основе RPC, который позволяет основному процессу scm автоматически инициировать и управлять (то есть приостанавливать, возоб- новлять или завершать) службами, установленными администратором (та- кими как ftp и telnet), которые обычно работают как отдельные потоки внутри или одиночной службы, или многофункционального демона-процес- са. Каждая установленная служба сама отвечает за свою настройку и управление всеми конечными точками соединения. Эти конечные точки могут быть не только сокетами тср или udp, но и, например, именованными каналами Windows.
Аспекты проектирования: службы и конфигурации 57 ных точек, управления портами и демультиплексирования/диспетчери- зации клиентских запросов к обработчикам служб. • Позволяет добавлять внешние службы без модификации существующе- го исходного кода и без завершения работающих серверных процессов. • Консолидирует администрирование сетевых служб с помощью унифи- цированного набора утилит управления конфигурациями. Например, суперсервер INETD обеспечивает унифицированный интерфейс для ко- ординации и запуска внешних служб, таких как FTP и TELNET, и внут- ренних служб, таких как DAYTIME и ECHO. . Служба регистрации => Все реализации сетевой службы регистрации в [C++NPvl] использовали однофункциональные серверы. Начиная с главы 5 данной книги, различные элементы сетевой службы регистрации будут конфи- гурироваться с помощью ACE-каркаса Service Configurator, который может быть использован для конфигурирования многофункциональных суперсерве- ров типа INETD. Блок 5: Демоны и демонизация Любой демон — это долговременный серверный процесс, работающий в фо- новом режиме и выполняющий различную служебную обработку по запросам от клиентов (Ste98). Демон не связан с интерактивным пользователем или управляющим терминалом. Поэтому важно обеспечить, чтобы демон был спроектирован отказоустойчивым, восстанавливался после сбоев и аккурат- но управлял своими ресурсами. «Демонизация» (daemonizing) процесса в UNIX включает создание нового серверного процесса, закрытие всех необязательных дескрипторов вво- да/вывода, изменение текущего каталога файловой системы на отличный от каталога вызывающего пользователя, восстановление маски доступа к файлу при его создании, разрыв связи с группой управляющих процессов и управ- ляющим терминалом и игнорирование событий сигналов от терминала, свя- занных с вводом/вывбдом. ACE-сервер в UNIX может сам себя преобразовы- вать в демона, путем вызова статического метода ace : : daemon!ze () или пу- тем передачи опции '-Ь' методу ACE_Service_config: -.openо. Windows Service (Ric97) является разновидностью демона и может программироваться В АСЕ С ПОМОЩЬЮ Класса ACE NT Service. 2.1.6 Одноразовые и постоянные серверы В дополнение к тому, что сетевые серверы бывают одно- и многофункцио- нальными, они могут быть спроектированы как «одноразовые» (one shot) или постоянные (standing). Основные альтернативы этого аспекта включают про- должительность работы сервера и использованце системных ресурсов. При оценке вариантов этого аспекта, следует рассматривать ожидаемую частоту ис- пользования служб(ы), реализуемых сервером, а также требования к. скорости запуска и гибкости настройки. Одноразовые серверы создаются по требованию, например, суперсерве- ром INETD. Они выполняют служебные запросы в отдельном потоке или про-
58 Глава 2 цессе, как показано на рис. 2.5 (1). Одноразовый сервер завершает свою работу после выполнения запроса (или завершения сеанса), для обслуживания которо- го он и был создан. Примером одноразового сервера является FTP-сервер в UNIX. Когда FTP-клиент соединяется с сервером, создается новый процесс для управления FTP-сеансом, включая аутентификацию пользователя и процесс передачи файлов. Серверный процесс FTP завершает свою работу, после завер- шения сеанса с данным клиентом. Одноразовый сервер не сохраняется в памяти системы в отсутствие запросов на обслуживание. Следовательно, при данной стратегии проектирования потребляется меньше ресурсов, таких как виртуаль- ная память и записи в таблице процессов. Это преимущество гораздо ярче про- является у тех служб, которые редко используются. Постоянные серверы продолжают выполняться, и после завершения каж- дого отдельного запроса на обслуживание или сеанса, который они обслужива- ют. Постоянные серверы часто запускаются в момент загрузки системы или су- персервером после поступления первого запроса от клиента. Они могут прини- мать соединения и/или запросы на обслуживание через локальные каналы IPC, такие как именованные каналы или сокеты, которые закреплены за суперсерве- ром, как показано на рис. 2.5 (2). В качестве альтернативы, постоянный сервер может позаимствовать или унаследовать IPC-канал у стороны, которая обрати- лась за обслуживанием. Одним из примеров постоянного сервера является web-сервер Apache [HMS98]. Исходный родительский процесс сервера Apache может быть настро- ен на упреждающее создание пула порожденных (child) процессов, которые об- служивают HTTP-запросы клиентов. Каждый порожденный процесс обслужи- вает заданное количество (его можно изменять) клиентских запросов, прежде чем завершить свою работу. Родительский процесс может, если нужно, порож- дать новые процессы, по мере роста нагрузки на web-сервер. По сравнению с одноразовыми, постоянные серверы могут улучшить вре- мя отклика службы за счет снижения непроизводительных издержек создания серверных процессов или потоков для множества клиентских запросов. Они могут, также как в примере с сервером Apache, адаптивно настраиваться на раз- ные уровни нагрузки. Способность постоянного сервера периодически завер- (1) Одноразовый сервер Рис. 2.5 Одноразовые и постоянные серверы (2) Постоянный сервер
Аспекты проектирования: службы и конфигурации 59 шать и создавать служебные процессы может также предохранять от некото- рых проблем ОС и приложений, таких как утечка памяти, которые с течением времени приводят к снижению производительности или к появлению «дыр» в системе безопасности. Служба регистрации => Мы реализуем клиентский и серверный демоны в нашей сетевой службе регистрации в виде постоянных серверов с целью по- вышения производительности всей системы в целом. Основанием выбора ва- рианта, связанного с дополнительным потреблением записей в таблице про- цессов и системных ресурсов, является то, что служба регистрации использует- ся часто. Поэтому постоянные запуски сервера регистрации для каждого клиентского запроса привели бы к увеличению времени ожидания для клиента, сделавшего запрос и к снижению общей производительности системы. Выбор между одноразовыми или постоянными серверами прямо противо- положен выбору между краткосрочными и долговременными службами, кото- рый описан в разделе 2.1.1. Первая из двух приведенных выше альтернатив проектирования обычно отражает ограничения, связанные с управлением ре- сурсами ОС, тогда как вторая альтернатива проектирования представляет со- бой свойство самой службы. Например, мы могли бы легко перейти в режим краткосрочной службы, не изменяя постоянной природы самого сервера. Ана- логично, если служба регистрации в каких-то средах используется редко, она может быть легко перестроена в одноразовый сервер. 2.2 Аспекты проектирования: конфигурации Этот раздел охватывает следующие аспекты проектирования, связанные с конфигурациями: • Статическое и динамическое именование. • Статическое и динамическое связывание (linking). • Статическое и динамическое конфигурирование. 2.2.1 Статическое и динамическое именование Приложения можно классифицировать в соответствии с тем как присваи- ваются имена их службам — статически или динамически. Основной альтерна- тивой этого аспекта является эффективность в процессе выполнения или гиб- кость. Службы со статическим именованием ассоциируют имя службы с объект- ным кодом, который существует во время компиляции и/или во время статиче- ского связывания (link). Например, внутренние службы INETD, такие как ECHO и DAYTIME, связаны с функциями со статическим именованием, встроенными в программу INETD. Службы со статическим именованием могут быть реали- зованы и в статических, и в динамических библиотеках. Службы с динамическим именованием откладывают установление связи имени службы и объектного кода, который ее реализует, на последующие эта-
60 Глава 2 пы. Таким образом, от кода реализации не требуется, чтобы он был идентифи- цирован, — не требуется даже, чтобы он был написан, скомпилирован и скомпо- нован —до тех пор, пока приложению не потребуется начать исполнять соответ- ствующую службу в процессе выполнения приложения. Распространенным примером динамического именования является то, как INETD работает с TEL- NET, как с внешней службой. Внешние службы можно обновлять, изменяя кон- фигурационный файл ine.td. conf и посылая сигнал sighup процессу INETD. Когда INETD получает этот сигнал, он считывает inetd. conf и осуществляет динамическое изменение связей служб, которые он предлагает для создания но- вых исполняемых файлов. 2.2.2 Статическое и динамическое связывание Приложения можно также классифицировать в зависимости от того, как их службы связываются с адресным пространством процесса — статически или динамически. Основные альтернативы этого аспекта включают расширяе- мость, безопасность, надежность и эффективность. Статическое связывание (linking) — создание завершенной исполняемой программы путем связывания в единое целое всех ее объектных файлов во вре- мя компиляции и/или во время статической компоновки (link), как показано на рис. 2.6 (1). Динамическое связывание (linking) — загружает объектные файлы в ад- ресное пространство процесса и выгружает из него тогда, когда программа вы- зывается в первый раз или обновляется во время выполнения, как показано на рис. 2.6 (2). Редактор связей ОС времени выполнения (run-time linker) обновля- ет адреса внешних символов для каждого процесса, в который загружается дан- ный объектный файл в соответствии с областью памяти, в которую загружается данный файл. В общем случае операционные системы поддерживают два типа динамического связывания: * Неявное динамическое связывание приводит к загрузке указанных объ- ектных файлов в процессе выполнения программы, без каких-либо яв- ных действий самой программы. Многие платформы предоставляют оп- цию, позволяющую отложить операции определения адресов и загрузки в память до того момента, когда некоторый метод будет вызван в первый раз. Такая стратегия отложенных операций минимизирует издержки, связанные с редактированием связей в процессе инициализации прило- жения. Неявное динамическое связывание используется при реализации динамически компонуемых библиотек (dynamically linked libraries, DLL) [SROO], известных также как совместно используемые библиотеки (shared libraries) [GLDW87]. * Явное динамическое связывание позволяет приложению получать, ис- пользовать и удалять привязки адресов реального времени для некоторых, относящихся к функциям или данным символов, определенных в дина- мических библиотеках DLL. Механизмы явного динамическог*р связыва- ния, поддерживаемые популярными операционными системами, включа-
Аспекты проектирования: службы и конфигурации 61 Рис. 2.6 Статическое и динамическое связывание ют функции UNIX (diopen (),dlsym() и diclose ()) и функции Win- dows (LoadLibrary (),GetProcAddress() и FreeLibrary()). Динамическое связывание может способствовать уменьшению потребления памяти: процессом в оперативной памяти и его программным образом, сохра- няемым на диске. В идеальном случае, существует только одна копия кода DLL, независимо от количества процессов, одновременно выполняющих ее код. При выборе между динамическим и статическим связыванием, разработ- чики должны тщательно взвесить альтернативы между гибкостью, безопасно- стью и отказоустойчивостью, с одной стороны, и потенциальными преимуще- ствами эффективного использования пространства и времени. Далее перечис- лены некоторые проблемы, связанные с использованием динамического связывания: • Проблемы с безопасностью и отказоустойчивостью. Приложение с ди- намическим связыванием может оказаться менее безопасным и надеж- ным, чем приложение со статическим связыванием. Оно может оказаться менее безопасным, так как в библиотеки DLL могут быть включены тро- янские кони (trojan horses). Оно может оказаться менее надежным, так как некорректно работающая DLL может повредить другой код или данные в том же прикладном процессе. • Непроизводительные издержки реального времени. Динамическое свя- зывание может приводить к большим непроизводительным издержкам во время выполнения по сравнению со статическим связыванием. В до- полнение к необходимости открывать и отображать в память множество файлов, внешние символические адреса в библиотеках DLL должны на- страиваться в соответствии с расположением памяти, в которую загружа- ются файлы. Хотя отложенное редактирование связей может смягчить этот эффект, он все же может оказаться значительным, особенно при первой загрузке DLL в память. Более того, компиляторы, генерирующие
62 Глава 2 непозиционный код, часто используют дополнительные уровни преоб- разования логических адресов в физические при определении адресов вызовов методов и доступе к глобальным переменным в DLL [GLDW87]. • Большие задержки (jitter). Для приложений критичных ко времени вы- полнения, задержка, связанная с подключением DLL к процессу и динами- ческим определением адресов методов, может оказаться неприемлемой. Конечно, лучше экспериментально оценить воздействие динамического связывания, чтобы определить представляет ли оно реальную проблему для ва- ших приложений. 2.2.3 Статическое и динамическое конфигурирование Как отмечалось в разделе 2.1, сетевые приложения часто предлагают или ис- пользуют ряд служб. Объединяя аспекты именования и связывания, изложенные в разделах 2.2.1 и 2.2.2, мы можем классифицировать сетевые прикладные служ- бы как статически и динамически конфигурируемые. Также как в случае со ста- тическим и динамическим связыванием, основными альтернативами этого ас- пекта являются расширяемость, безопасность, надежность и эффективность. Статическое конфигурирование относится к действиям, связанным с ини- циализацией приложения, которое содержит службы со статическим именова- нием (то есть каждая служба создается как отдельная функция или класс), и за- тем с компиляцией, компоновкой и выполнением их в отдельном процессе ОС. В таком случае, службы данного приложения не могут быть расширены во вре- мя выполнения. Такой дизайн может быть необходим для защищенных прило- жений, которые содержат только доверенные службы. Статически конфигури- руемые приложения могут также выиграть от более глубокой оптимизации компилятором, применимой к статически компонуемым программам. Тем не менее, у статически конфигурируемых сетевых приложений и служб существуют следующие проблемы: • Они приводят к созданию нерасширяемых приложений и программных архитектур, которые жестко привязаны к реализации и конфигурации конкретной службы по отношению к другим службам в приложении. • Статическая конфигурация ограничивает способность системных адми- нистраторов изменять параметры или конфигурации приложений с це- лью удовлетворения локальных условий работы или их изменений в се- тевых и аппаратных конфигурациях. • Большие статически конфигурируемые приложения могут оказаться не- целесообразными из-за большого размера их загрузочных модулей, что может затруднять их распространение, требовать слишком большого времени при загрузке и приводить к пробуксовке систем, если имеющей- ся памяти оказывается недостаточно. Динамическое конфигурирование относится к. действиям, связанным с инициализацией приложения, которое предлагает службы с динамическим именованием. При объединении с явным динамическим связыванием и меха-
Аспекты проектирования: службы и конфигурации 63 низмами создания процессов/потоков, службы предлагаемые динамически конфигурируемыми приложениями могут расширяться во время инсталляции или загрузки или даже во время выполнения. Такое расширение может способ- ствовать следующей, связанной с конфигурированием, оптимизации: • Динамическая реконфигурация служб. Сетевые приложения постоян- ной готовности, такие как онлайновая обработка транзакций или систе- мы обработки телефонных звонков, могут требовать гибкой динамиче- ской реконфигурации возможностей управления. Например, может ока- заться необходимым включить новую версию службы в сервер, не нарушая работы других уже выполняющихся служб. Протоколы рекон- фигурации, основанные на механизмах явного динамического связыва- ния могут улучшить функциональность и гибкость сетевых приложе- ний, так как дают возможность вставлять службы, удалять или модифи- цировать их во время выполнения, без предварительной остановки и повторного запуска базового процесса или потока(ов) [SS94]. • Разложение на функции. Динамическое конфигурирование упрощает шаги, необходимые для получения подмножеств функциональных воз- можностей для семейств приложений, разработанных для ряда плат- форм ОС. Явное динамическое связывание предоставляет возможность добавления, удаления и модификации служб на уровне небольших функциональных модулей. Это, в свою очередь, позволяет использовать один и тот же каркас и для экономичных встроенных приложений и для больших корпоративных распределенных приложений. Например, при- ложение просмотра содержимого Web, возможно, сможет работать и на «карманных компьютерах» (PDA), и на персональных компьютерах (PC) и/или на рабочих станциях с помощью динамически конфигурируемых подмножеств, таких как визуализация изображений, возможность выпол- нения Java-кода, печать или телефонная связь с прямым набором номера. • Балансировка рабочей нагрузки приложения. Часто трудно определить рабочие характеристики прикладных служб заранее, так как нагрузка мо- жет значительно изменяться во время выполнения. Поэтому может ока- заться необходимым использовать динамическую конфигурацию для поддержки методов балансировки нагрузки [OOS01] и распределения системы, при котором прикладные службы размещаются на разных ком- пьютерах-хостах сети. Например, разработчики могут иметь возмож- ность совмещать или распределять некоторые службы, такие как обра- ботка изображений, по обе стороны границы, разделяющей клиента и сервер. Если в серверном приложении сконфигурированы несколько служб и большое количество активных клиентов одновременно обраща- ется к этим службам, то результатом может быть появление узких мест. Наоборот, объединение нескольких служб в клиентах может вылиться в узкое место, если клиенты работают на более дешевых, менее произво- дительных компьютерах. Служба регистрации ♦ Реализации нашей сетевой службы регистрации в [С+4-NPvl] и в главах 3 и 4 данной книги конфигурируются статически. В гла-
64 Глава 2 ве 5 данной книги, мы описываем ACE_DLL, который представляет собой пере- носимый интерфейсный фасад, инкапсулирующий способность динамически загружать и выгружать совместно используемые библиотеки (DLL) и искать в них идентификаторы. Мы описываем также ACE-каркас Service Configurator, который может конфигурировать прикладные службы динамически. Начиная с главы 5, большинство наших примеров являются динамически конфигури- руемыми. 2.3 Резюме В данной главе были описаны две группы аспектов проектирования, от ко- торых зависит успех разработки и развертывания сетевых приложений. Аспек- ты проектирования служб, влияют на способы структурирования, разработки и создания экземпляров прикладных служб. Аспекты конфигурации служб влияют на способность пользователей и администраторов изменять размеще- ние и конфигурации сетевых служб во время выполнения, после установки и развертывания. Аспекты проектирования служб оказывают существенное влияние на эф- фективность использования приложениями системных и сетевых ресурсов. Эффективное использование ресурсов непосредственно связано со временем отклика приложения, также как с общей масштабируемостью и производи- тельностью приложения. Производительность — это важный фактор, непо- средственно воспринимаемый конечными пользователями. Хотя согласован- ный, модульный дизайн менее видим конечным пользователям, он является критически важным для долговременного успеха продукта. Хороший проект упрощает сопровождение и позволяет развивать функ- циональные возможности приложения в ответ на изменения на рынке и конку- рентное давление, не теряя качества и производительности. К счастью, произ- водительность и модульность не являются альтернативами типа или-или. Пу- тем тщательного анализа аспектов проектирования служб и разумного применения АСЕ, вы сможете создать высокоэффективные и полностью соот- ветствующие требованиям сетевые приложения. Даже хорошо спроектированные службы и приложения могут нуждаться в адаптации к множеству сред развертывания и требований пользователей. Ас- пекты конфигурирования служб включают компромиссы между проектными решениями, связанными с определением конкретного набора служб и включе- ния этих служб в-адресное пространство одного или нескольких приложений. Для получения успешных решений, соображения гибкости сетевых приложе- ний должны перевешивать проблемы их безопасности, пакетирования и слож- ности. При разработке сетевых приложений, рассмотренные в данной главе аспек- ты проектирования должны рассматриваться совместно с аспектами, описан- ными в главах 1 и 5 [C++NPvl]. ACE-каркасы, рассматриваемые в данной кни- ге,, предлагают мощные средства реализации гибких и расширяемых решений с различными комбинациями компромиссов и возможностей.
Глава 3 ACE-каркас Reactor Краткое содержание В данной главе описывается структура и применение ACE-каркаса Reactor. Этот каркас реализует паттерн Reactor [POSA2], который позволяет событий- но-управляемым приложениям реагировать на события, исходящие от целого ряда разнотипных источников, таких как дескрипторы ввода/вывода, таймеры и сигналы. Приложения подменяют определенные в каркасе hook-методы, а каркас осуществляет диспетчеризацию событий, распределяя их по методам обработки. Показано как, с помощью каркаса Reactor, создать сервер регистра- ции, который (1) обнаруживает и демультиплексирует события, связанные с установлением соединений и передачей данных от источников разного типа и (2) осуществляет диспетчеризацию событий по определяемым приложением методам, которые и выполняют обработку этих событий. 3.1 Обзор АСЕ-каркас Reactor упрощает разработку событийно-управляемых про- грамм, к которым относятся многие сетевые приложения. Обычными источни- ками событий в этих приложениях являются: активизация операций ввода/вы- вода в канале IPC; POSIX-сигналы; обработка сигналов в Windows и сигналы таймеров. В этом контексте, АСЕ-каркас Reactor отвечает за следующие дейст- вия: • Обнаружение наступления событий в различных источниках событий. • Демультиплексирование событий предварительно зарегистрированным обработчикам. • Диспетчеризация hook-методов, реализуемых обработчиками, с целью определяемой приложением обработки событий. В данной главе описываются следующие классы ACE-каркаса Reactor, кото- рые сетевые приложения могут использовать для обнаружения событий, де- 3 Программирование сетевых приложений на C++. Том 2
66 Глава 3 мультиплексирования и диспетчеризации соответствующих обработчиков со- бытий. Клосс АСЕ Описание АСЕ_Т ime_Vа1ие Обеспечивает переносимое, нормированное представление времени и временных интервалов путем перегрузки операторов C++ с целью упрощения арифметических операций и операций отношений, связанных со временем. ACE_Event_Handler Абстрактный класс, в интерфейсе которого определены hook-методы, принимающие обратные вызовы от ACE_Reactor. Большинство прикладных обработчиков событий в АСЕ, являются производными ОТ ACE_Event_Handler. ACE_Timer__Queue Абстрактный класс, определяющий функции и интерфейс очереди таймеров (timer queue). В АСЕ содержится много классов, производных от ACE_Timer_Queue, который обеспечивает гибкую поддержку различных требований, связанных с согласованием во времени. ACE_Reactor Предоставляет интерфейс для регистрации обработчиков и выполнения цикла обработки событий, который управляет, в каркасе Reactor, обнаружением, демультиплексированием и диспетчеризацией. Наиболее важные взаимосвязи между классами в ACE-каркасе Reactor при- ведены на рис. 3.1. Эти классы, в соответствии с паттерном Reactor [POSA2], иг- рают следующие роли: • Классы уровня инфраструктуры событий обеспечивают независимые от приложений стратегии синхронного обнаружения и демультиплекси- рования событий обработчикам событий и диспетчеризации, связанных с обработчиками событий hook-методов. Компоненты инфраструктур- ного уровня в ACE-каркасе Reactor включают ACE_Time_Value, ACE_Event_Handler, ACE-классы очередей таймеров и различные реа- лизации ACE_Reactor. • Классы прикладного уровня определяют обработчики событий для вы- полнения их hook-методами прикладной обработки. В ACE-каркасе Re- actor классы уровня приложений являются производными от ACE_Event_Handler. Большие возможности ACE-каркаса Reactor связаны с инкапсуляцией раз- личий между механизмами демультиплексирования событий, имеющихся в разных операционных системах и поддержкой разделения обязанностей меж- ду классами каркаса и классами приложений. За счет разделения на независи- мые от приложений механизмы демультиплексирования и диспетчеризации со- бытий и на определяемые приложениями стратегии обработки событий, ACE-каркас Reactor обеспечивает следующие преимущества:
АСЕ-каркас Reactor 67 Рис. 3.1 Классы ACE-каркаса Reactor • Свободная переносимость. Каркас можно настроить на использование механизмов демультиплексирования событий многих ОС, таких как selectO (имеется в UNIX, Windows и во многих ОС реального време- ни), /dev/poll (имеется на некоторых платформах UNIX) и WaitFor- MultipleObj ects () (только в Windows). • Автоматизация обнаружения, демультиплексирования и диспетчериза- ции. АСЕ-каркас Reactor предоставляет приложениям унифицированный объектно-ориентированный механизм обнаружения, демультиплексиро- вания и диспетчеризации событий, исключая зависимость от внутренних API ОС, которые не являются переносимыми. Объекты обработчиков со- бытий могут быть зарегистрированы с помощью ACE_Reactor для обра- ботки событий различных типов. • Прозрачная расширяемость. Каркас, путем наследования и динамиче- ского связывания (binding), использует hook-методы, чтобы отделить низкоуровневые механизмы событий, такие как обнаружение событий на множестве дескрипторов ввода/вывода, сигналы таймеров и методов де- мультиплексирования и диспетчеризации соответствующих обработчи- ков, предназначенных для обработки этих событий, от высокоуровневых стратегий обработки сообщений приложениями, таких как стратегии ус- тановления соединений, маршалинг и демаршалинг данных и обработка клиентских запросов. Такой дизайн позволяет прозрачно расширять АСЕ-каркас Reactor, не требуя изменения существующего прикладного кода. • Улучшение повторного использования и минимизация ошибок. Разра- ботчикам, использующим операции демультиплексирования ОС, при- ходится вновь и вновь создавать, отлаживать и оптимизировать один и тот же низкоуровневый код для каждого приложения. В отличие от это- го, механизмы обнаружения, демультиплексирования и диспетчеризации событий ACE-каркаса Reactor являются общими для всех ОС и могут, по- этому, повторно использоваться многими сетевыми приложениями. Та- кое разделение обязанностей позволяет разработчикам сосредоточиться на высокоуровневых стратегиях обработки событий, определяемых при- г
68 Глава 3 ложениями, вместо того, чтобы снова и снова бороться с низкоуровневы- ми механизмами. * Эффективное демультиплексирование событий. АСЕ-каркас Reactor обеспечивает эффективную реализацию логики демультиплексирования и диспетчеризации событий. Например, ACE_Select_Reactor, пред- ставленный в разделе 4.2, использует интерфейсный фасад ACE_Hand- le_Set_Iterator, рассмотренный в главе 7 [C++NPvl]. Этот интер- фейсный фасад использует оптимизированную реализацию паттерна Iterator [GoF], позволяющую избежать проверки битовых масок f d_set по одному биту. Эта оптимизация базируется на усовершенствованном алгоритме, который использует оператор «исключающее-ИЛИ» для уменьшения среднего времени вычисления с О(общее число битов) до О(число установленных битов), что может существенно повысить произ- водительность крупномасштабных приложений во время выполнения. В остальной части данной главы приводится обоснование каждого класса ACE-каркаса Reactor и описание его функциональных возможностей. Мы пока- жем, как использовать этот каркас, чтобы улучшить дизайн нашего сервера се- тевой регистрации. Если вы не знакомы с паттерном Reactor [POSA2], то мы со- ветуем вам познакомиться с ним, прежде чем погружаться в детали примеров этой главы. Затем, в главе 4, рассматривается структура и применение представ- ленных здесь наиболее общих реализаций интерфейса ACE_Reactor. В той же главе рассматриваются различные модели параллелизма, поддерживаемые эти- ми реализациями ACE_Reactor. 3.2 Класс ACE Time Value Обоснование Различные операционные системы предлагают разные функции и данные для доступа к информации о времени и операций со временем и датами. Напри- мер, платформы UNIX определяют структуру timeval, следующим образом: struct timeval { long secs; long usees; Иные представления дат и времени используются на других платформах ОС, таких как POSIX, Windows или патентованные операционные системы ре- ального времени. Временные значения используются в целом ряде ситуаций, включая задание тайм-аутов. В соответствии с изложенным в блоке 6, тайм-ау- ты в АСЕ, в некоторых ситуациях, задаются в абсолютном времени, например, в интерфейсных фасадах параллелизма и синхронизации [C++NPvl], а в других ситуациях в относительном времени, например в ACE_Reactor тайм-ауты ввода/вывода и установки таймеров. Широкий диапазон применений и различ- ных представлений на разных платформах делает решение этих проблем пере-
АСЕ-каркас Reactor 69 носимости в каждом отдельном приложении излишне утомительным и доро- гостоящим, вот почему АСЕ-каркас Reactor обеспечивает класс ACE_Ti- me_Value. Функциональные возможности класса ACE_Time_Value применяет паттерн Wrapper Facade [POSA2] и перегруз- ку операторов C++ для упрощения переносимого использования операций, связанных со временем и продолжительностью. Класс предоставляет следую- щие возможности: • Обеспечивает стандартное представление времени, переносимое на раз- ные платформы ОС. • Может осуществлять преобразования представлений времени для раз- ных платформ, таких как timespec_t и timeval в UNIX; FILETIME и timeval в Windows. • Использует перегрузку операторов для упрощения операций сравнения, относящихся ко времени, за счет использования стандартного синтакси- са C++ в арифметических выражениях и операциях отношений, связан- ных со значениями времени. • Его конструкторы и методы нормируют значения времени, преобразуя поля в структуре timeval в стандартный формат, который обеспечива- ет точность сравнений между различными экземплярами ACE_Ti- me_Value. • Может представлять продолжительность в любом виде, например, 5 се- кунд и 310000 микросекунд, или абсолютные дату и время, например, 2001-09-11-08.46.00. Хотя некоторые методы, например, opera- tor^ (), применительно к абсолютному времени не имеют смысла. Интерфейс ACE_Time_Value показан на рис. 3.2. При чтении книги учти- те замечания, сделанные в блоке 7, относительно диаграмм UML и кода C++. Основные методы ACE_Time_Value приведены в следующей таблице: Метод Описание ACE_Time_Value() set () Перегружаемые конструкторы и методы, которые осуществляют преобразование из разных форматов времени, таких как timeval, FILETIME, timespec_t или long В нормированный формат ACE_Time_Value. sec () Метод, возвращающий ту часть ACE_Time_vaiue, которая хранит секунды. usec() Метод, ВОЗВращаЮЩИЙ Ту ЧаСТЬ ACE_Time_Value, которая хранит микросекунды. msec () Преобразует формат ACE_Time_Value sec () /usee () в миллисекунды. operator+= () operator-= () operator*=() Арифметические методы, которые суммируют, вычитают И умножают значения ACE_Time_Value.
70 Глава 3 ACB_Tim_Valu* + zero : ACE_Time_Value - tv_ : timeval + АСЕ Time Value (sec : long, usee : long = 0) (t : const struct timeval &) + АСЕ Time Value + + + * * + + * * * ACE_Time_Value (t : const timespec_t &) ACE_Time_Value (t : const FILETIME &) set (sec : long, usee : long) set (t : const.struct timeval &) set (t : const timespec_t &) set (t : const FILETIME &) set () : long usee () : long msec () : long operator*» (tv : const ACE Time Value &) : ACE_Time_Value & * operator-» (tv : const ACE_Time_Value &) : ACE_Time_Value & * operator*» (d : double) : ACE Time Value & Рис. 3.2 Класс ACE Time Value Дополнительно к методам, перечисленным выше, следующие бинарные операторы, определяющие арифметические операции и операции отношений, являются дружественными классу ACE_Time_Value: Метод Описание operator* () operator- () Арифметические методы, суммирующие и вычитающие два значения ACE_Time_Value. operator»» () operator !=() Методы, определяющие равенство или неравенство двух значений ACE_Time_Value. operator< () operator> () operator<= () operator>= () Методы, определяющие отношение двух значений ACE_Time_Value. 1 Все конструкторы и методы, ACE_Time_Value нормируют значения вре- мени, с которыми они оперируют. Процедура нормирования преобразует коли- чество микросекунд, эквивалентное одной секунде, перенося соответствующее количество секунд в secs, оставляя оставшееся значение микросекунд в usees. Например, нормирование величины ACE_Time_Value (1,1000000) будет при сравнении адекватно нормированной величине ACE_Time_Value (2). Поразрядное сравнение ненормированных объектов, наоборот, не выявит их эквивалентности. В блоке 6 описаны различия в интерпретации ACE_Time_Value, при исполь- зовании для представления значений тайм-аутов для различных классов АСЕ.
АСЕ-каркас Reactor 71 Блок 6: Абсолютные и относительные тайм-ауты ACE Time Value Некоторые классы АСЕ используют относительные тайм-ауты, другие — абсо- лютные: • Семантика относительного времени. • Методы ввода/вывода интерфейсных фасадов АСЕ IPC (глава 3 (C++NPv1)), а также классы каркасов более высокого уровня, которые используют эти методы, например, классы ACE-каркаса Acceptor-Con- nector (глава 7), • Методы циклов обработки событий ACE_Reactor и ACE_Proactor, а так- же методы планирования с применением таймера (главы 3 и 8). • MeTOAblwait () КЛОССОВ ACE_Process,ACE_Process_Manager HACE__Thre- ad_Manager (главы 8 и 9 (C++NPv1)). • Шаг квантования времени в ACE_sched_Params (глава 9 (C++NPv1)). . • Семантика абсолютного времени. • Интерфейсные фасады АСЕ, связанные с синхронизацией, такие как АСЕ Condition Thread Mutex И АСЕ Thread Semaphore (глава 10 (C++NPV1)). • Методы планирования очередей таймеров (глава 3). • Методы ACE_Tasк (глава 6). • Методы ACE_Message_Queue (глава 6), а также классы, которые базиру- ются на ACE_Message_Queue или используют его, например классы ACE-каркаса Streams (глава 9), ACE_Activation_Queue, ACE_Future И ACE_Future_Set (HJS). Относительные тайм-ауты часто используются там, где операции (например, операции ACE_Thread_ Manager:: wa it ()), возможно, придется приостанав- ливать свою работу, прежде чем она сможет продолжиться, но вызывается та- кая операция только один раз. Наоборот, абсолютные тайм-ауты часто ис- пользуются там, где операция (например, операция ACE_Cpndition_Thre~ ad_Mutex::wait ()) может вызываться несколько раз за один цикл. Использование абсолютного времени избавляет от необходимости повторно вычислять значения тайм-аута на каждой итерации цикла (KSS96). Пример В следующем примере создается два объекта ACE_Time_Value, значения которых могут быть установлены с помощью аргументов командной строки. Затем выполняется проверка попадания в диапазон, чтобы убедиться в кор- ректности заданных значений. 1#include "ace/OS.h" 2 3 const ACE_Time_Value max_interval (60 * 60); // 1 час. 4 5int main (int argc, char *argv[]) { 6 ACE_Time_Value expiration = ACE_OS::gettimeofday (); 7 ACE_Time_Value interval; 8 9 ACE_Get_Opt opt (argc, argv, e : i :) ) ; 10 for (int c; ('c = opt ()) != -1;) 11 switch (c) {
72 Глава 3 12 rer: expiration += ACE_Time_Value (atoi (opt.opt_arg ())); 13 break; 14 ’i’: interval = ACE_Time_Value (atoi (opt.opt_arg ())); 15 break; 16 } 17 if (interval > max_interval} 18 cout « "interval must be less than ” 19 « max_interval.sec() « endl ; 20 else if (expiration > (ACE_Time_Value::max_time - interval)) 21 cout << "expiration + interval must be less than ” 22 « ACE_Time_Value::max_time.sec () « endl; 23 return 0; 24 ) Строки 3-7 Инициализация объектов ACE_Time_Value. По умолчанию объект ACE_Time_Value инициализируется нулем. Строки 9-16 Обработка аргументов командной строки с помощью класса ACE_Get_Opt, рассмотренного в блоке 8. Блок 7: Изображение классов АСЕ и программ на C++ Как правило, мы приводим UML диаграмму и таблицу, которые описывают основ- ные методы для каждого используемого в этой книге C++ класса АСЕ. Полные ин? терфейсы C++ классов АСЕ доступны в онлайн на http://ace.ece.uci.edu и http: //www. riverace. coir./docs/. Мы рекомендуем иметь под рукой копию исходного кода АСЕ, чтобы можно было быстро навести справку. Полные реа- лизации C++ классов и примеры сетевой службы регистрации находятся в ка- талогах дистрибутива АСЕ $ACE_ROOT/ace И $ACE_R.OOT/examples/C++NPv2 соответственно. С целью экономии места, в приводимых нами UML-диаграммах классов выде- лены атрибуты и операции, которые используются в примерах наших про- грамм. Диаграммы классов не содержат раздела атрибутов, если ни один из атрибутов этого класса не имеет отношения к обсуждаемой теме. Мы исполь- зуем курсив для обозначения абстрактных классов или методов следующим образом: • Имя метода, выделенное курсивом, указывает на виртуальный метод C++, реализация которого может быть изменена в производном классе. • Имя класса, выделенное курсивом, указывает на то, что, возможно, придет- ся изменить реализацию одного или нескольких виртуальных методов дан- ного класса, чтобы эффективно его использовать. Если вам нужна вводная информация по UML, мы рекомендуем книгу UML Distil- led (wKSOO). Также с целью экономии места мы используем некоторые сокращения про- граммного кода, которых нет в оригинале АСЕ. Например, мы опустили в при- мерах на C++ большую часть кода обработки ошибок. В АСЕ, конечно, всегда осуществляется проверка на наличие ошибок, и выполняются все действия, необходимые для корректной работы приложений. В некоторых из наших при- меров на C++ реализации методов представлены в определениях классов. В АСЕ, тем не менее, сделано не так, поскольку это загромождает интерфейсы классов и замедляет компиляцию. Руководящие указания по программирова- нию АСЕ находятся В $ACE_ROOT/docs/ ACE-guidelines.html.
АСЕ-каркас Reactor 73 Строки 17-22 Проверка попадания в диапазон, чтобы убедиться в коррект- ности заданных значений. Блок 8: Класс ACE Get Opt Класс ACE_Get_Opt — это итератор для обработки опций аргументов команд- ной строки. Опции, передаваемые в opts tring, предваряются символом ДЛЯ коротких ОПЦИЙ ИЛИ '- ДЛЯ ДЛИННЫХ ОПЦИЙ. Класс ACE_Get_Opt можно ис- пользовать для обработки аргументов argc/argv, например, таких, которые передаются функции main() программы из командной строки, или hook-мето- ду init (). Данный класс обеспечивает следующие функциональные возмож- ности: • Тонкий интерфейсный фасад на C++ для стандартной функции POSIX де- topt (). Однако в отличие от get opt (), каждый экземпляр AC?:_Get_Opt хра- нит информацию о своем собственном состоянии, так что может использо- ваться реентерабельно. Кроме того, ACE_Get_Opt легче использовать, чем ge.toptO, так как аргументы optstring и argc/argv передаются только один раз его конструктору, а не при каждом обращении к итератору. • Ему можно указать, что нужно начать обработку командной строки с некото- рой произвольной точки, задаваемой параметром skip^args, который по- . зволяет пропускать имя программы при обработке командной строки, пере- данной в main () или возобновлять позднее прерванную обработку, • Он может осуществлять перегруппировку аргументов-опций в начало ко- мандной строки, сохраняя их относительный порядок, что упрощает обра- ботку опций и аргументов, не являющихся опциями. После того как все опции просмотрены, он возвращает еое, а opt_ind () указывает на первый аргу- мент, не являющийся опцией, так, что программа может продолжить обра- ботку оставшихся аргументов. • Несколько режимов упорядочения аргументов: permute_args, requi- RE_ORDERИ RETURN_JN_ORDER. • Двоеточие, следующее за символом короткой опции в optstring, означает, . что у данной опции есть аргумент. Данный аргумент берется из оставшихся -' символов текущего элемента argv или из следующего, если требуется. Если встречается argv-элемент это означает завершение раздела опций и возвращается еое. • Короткие опции без аргументов могут быть сгруппированы в командной строке вместе за символом но в этом случае только последняя короткая . опция данной группы может иметь аргумент, Если короткая опция не распо- знается, возвращается • Форматы длинных опций похожи на функцию GNU getopt_long (). Длинные опции могут быть определены с соответствующими короткими опциями. Ко- гда ACE_Get_Opt обнаруживает длинную опцию с соответствующей корот- ' кой опцией, он возвращает эту короткую опцию, значительно упрощая для вызывающей стороны ее обработку в операторе switch. Многие примеры •/; в этой книге иллюстрируют применение длинных и коротких опций. Так как короткие опции определяются как целочисленные значения, длин- ные опции, которые обычно не имеют осмысленного эквивалента короткой опции, могут обозначать значения, не являющиеся алфавитно-цифровыми, ; для соответствующих коротких опций. Эти значения, не являющиеся алфа- V. витно-цифровыми, не могут появляться в списке аргументов или в параметре j-; optstring, но могут возвращаться в операторе return и эффективно обраба- G тываться в операторе switch.
74 Глава 3 33 Класс ACEEventHandler Обоснование Сетевые приложения часто строятся по принципу реагирования (reactive model), при котором они реагируют на события разных типов, например, свя- занных с вводом/выводом, таймерами или сигналами. Независимые от прило- жений механизмы обнаружения событий и диспетчеризации-обработчиков со- бытий должны использоваться повторно в разных приложениях, тогда как оп- ределяемый приложениями код, реагирующий на эти события, должен находится в обработчиках событий. С целью уменьшения зависимости и уве- личения степени повторного использования, каркас выделяет повторно ис- пользуемые механизмы и обеспечивает средства для подключения определяе- мых приложениями обработчиков событий. Такое разделение обязанностей является основой для инверсии управления в каркасе АСЕ Reactor. Его механиз- мы обнаружения и диспетчеризации контролируют Поток управления и вызы- вают hook-методы обратного вызова обработчиков событий, когда требуется выполнить обработку, определяемую приложением. Чтобы реализовать такое разделение обязанностей, должен существовать способ осуществлять обратные вызовы. Один из способов реализации обрат- ных вызовов — определить для каждого типа событий отдельную функцию. Однако такой подход может оказаться громоздким для разработчиков прило- жений, так как они должны будут: 1. Помнить каким событиям, какие функции соответствуют. 2. Придумать, как связывать данные с функциями обратного вызова. 3. Использовать процедурную модель программирования, так как объектный интерфейс отсутствует. Для решения этих проблем и для поддержки объектно-ориентированной модели обратных вызовов, в ACE-каркасе Reactor определен базовый класс ACE_Event_Handler. Функциональные возможности класса Класс ACE_Event_Handler является базовым классом для всех обработ- чиков АСЕ, реагирующих на события. Этот класс предоставляет следующие функциональные возможности: • Определяет hook-методы для событий ввода/вывода, а также для собы- тий, связанных с исключительными ситуациями, событиями таймеров и сигналами.' • Его hook-методы позволяют приложениям расширять, разными спосо- бами, производные классы обработчиков событий, не изменяя самого каркаса. В Windows классACE_Event_Handler может также обрабатывать события, связанные с син- хронизацией, такие как переход из состояния «занято» в состояние «свободно» объектов собы- тий Windows, мьютексов или семафоров [SR00], которые мы рассматриваем в разделе 4.4.
АСЕ-каркас Reactor 75 ACHJEvan Handler - priority : int - reactor_ : ACE Reactor * # ACE_Event_Handler (г : ACE_Reactor * « О, prio : int - LO_PRIORITY) + ^ACE—EventJHandler () + handle—input (h : ACE_HANDLE » ACE_INVALID_HANDLE) : int + handle__output (h : ACE_HANDLE - ACE_INVALID_HANDLE) : int + handle^exception (h : ACE_HANDLE - ACE_INVALID_HANDLE) : int + handle-timeout (now : ACE_Time_Value &, act : void * - 0) : int + handlersignal (signum : int, info : siginfo_t * ~ 0, ctx : ucontext_t * - 0) : int + handle—close (h : ACE_HANDLE, mask : ACE_Reactor_Mask) • int * get_handle () : ACE—HANDLE + reactor (} : ACE_Reactor * + reactor (r : ACE_Reactor *> + priority () : int + priority (prio : int) Рис 33 Класс ACE_Event_Handler • To, что он использует объектно-ориентированные обратные вызовы, уп- рощает связывание данных с hook-методами, которые обрабатывают эти данные. • То, что он использует объекты, автоматизирует также связывание источ- ника событий (или множества источников) с его данными, например, с сеансом сетевого соединения. • Берет на себя удаление обработчиков событий, когда они больше не нужны. • Хранит указатель на ACE_Reac t or, который им управляет, упрощая для обработчика событий корректное управление его событиями регистра- ции и удаления. Интерфейс класса ACE_Event_Handler показан на рис. 3.3, а его основ- ные методы приведены в следующей таблице: Метод Описание ACE_Event_Handler () Назначает указатель на ACE_Reactor, который может быть ассоциирован с обработчиком событий. ~ACE_Event_Handler() Вызывает purge_pendipg_notifications(), чтобы открепиться от механизма уведомлений о событиях.' handle__input () Hook-метод, вызываемый в ответ на события ввода, например, события, связанные с соединениями или данными. handle_output () Ноок-метод, вызываемый в ответ на возможность событий вывода, например, когда завершается поток управления или неблокируемое соединение.
76 Глава 3 Метод Описание handle_exception () Hook-метод вызываемый в ответ на события, связанные с исключительными ситуациями, например, с поступлением срочных (urgent) данных по ТСР-соединению. handle_timeout() Hook-метод вызываемый для событий таймера. handle_signal() Ноок-метод вызываемый при поступлении сигналов от ОС, или посредством механизма сигналов POSIX, или когда объект синхронизации в Windows переходит в сигнальное состояние. handle_close() Ноок-метод который выполняет определяемые пользователем действия, связанные с завершением, когда один из остальных hook-методов handie_* () возвращает-1 или когда метод АСЕ Reactor: : remove handler () вызывается ЯВНО, чтобы открепить (unregister) обработчик событий. get_handle() Возвращает низкоуровневый дескриптор ввода/вывода. Данный метод может не выполнять никаких действий (no-op), если обработчик событий обрабатывает только события, управляемые временем. reactor () Методы доступа для получения/установки (get/set) указателя на ACE_Reactor, который может быть ассоциирован с ACE_Event_Handler. priority() Методы доступа для получения/установки (get/set) приоритета обработчика событий, используемого ACE_Priority_Reactor (СМ. главу 4). Приложения могут наследовать отACE_Event_Handler для создания об- работчиков событий, которые имеют следующие свойства: • Подменяют один или несколько hook-методов handle_* () класса ACE_Event_Handler, чтобы выполнять обработку, определяемую приложением в ответ на события соответствующих типов. • Регистрируются или планируются классом ACE_Reactor, который за- тем осуществляет диспетчеризацию hook-методов обработчиков с целью обработки событий. • Так как обработчики событий являются объектами, а не функциями, то достаточно просто установить связь между данными и hook-методами обработчика, чтобы сохранять состояние на протяжении множества об- ратных вызовов, осуществляемых реагирующим объектом (reactor). Ниже мы обсудим три аспекта программирования hook-методов АСЕ_ Event_Handler. 1. Типы событий и hook-методы обработчиков событий. Когда приложение регистрирует обработчик события у реагирующего объекта, оно должно
АСЕ-каркас Reactor 77 указать какой тип(ы) события(й) должен обрабатывать данный обработ- чик. АСЕ обозначает эти типы событий с помощью следующих констант перечисления, определенных в ACE_Event_Handler: Тип события Описание READ-MASK Означает события ввода, такие как поступление данных для некоторого сокета или дескриптора файла. Реагирующий объект осуществляет диспетчеризацию с помощью hook-метода handie_input () с целью обработки событий ввода. WRITE-MASK Означает события вывода, такие как завершение управления потоком. Реагирующий объект осуществляет диспетчеризацию с помощью hook-метода handle_output () с целью обработки событий вывода. EXCEPT—MASK Означает события, связанные с исключительными ситуациями, такие как поступление на один из сокетов срочных (urgent) данных. Реагирующий объект осуществляет диспетчеризацию С ПОМОЩЬЮ hook-метода handle_exception () с целью обработки события, связанного с исключительной ситуацией. ACCEPT—MASK Означает события, связанные с пассивным приемом соединений. Реагирующий объект осуществляет диспетчеризацию с помощью hook-метода handle_input () с целью обработки событий установления соединений. C0NNECTJ4ASK Означает завершение неблокируемого соединения. Реагирующий объект осуществляет диспетчеризацию с помощью hook-метода handie_output () с целью обработки событий завершения неблокируемых соединений. Эти значения могут объединяться (по «ИЛИ») с целью рационального обо- значения нескольких событий. Это множество событий может входить в пара- метр ACE_Reactor_Mask, который передается методам ACE_Reactor: : ге- gister_handler(). Обработчики событий, используемые для обработки событий ввода/выво- да, могут предоставлять дескриптор, например дескриптор сокета, с помощью своего hook-метода get_handle (). Когда приложение регистрирует обработ- чик событий у реагирующего объекта, тот осуществляет обратный вызов мето- да get_handle () обработчика, чтобы получить его дескриптор. Затем этот дескриптор включается в набор дескрипторов, который используется реаги- рующим объектом при обнаружении событий ввода/вывода. 2. Возвращаемые значения hook-методов обработчиков событий. Когда на- ступает зарегистрированное событие, реагирующий объект, с целью обработ- ки этого события, осуществляет диспетчеризацию hook-методов hand- 1е_* () соответствующего обработчика событий. В блоке 9 описываются не- которые приемы, которые применяются при реализации этих Ьоок-методов. Когда метод handle_* () завершает обработку, он должен вернуть значение, которое интерпретируется реагирующим объектом следующим образом:
78 Глава 3 • Возвращаемое значение 0 указывает на то, что реагирующий объект должен продолжать обнаружение и диспетчеризацию зарегистрирован- ных событий для данного обработчика событий (и дескриптора, если это событие ввода/вывода). Такая реакция является обычной для обработчи- ков событий, которые обрабатывают несколько копий некоторого собы- тия, например, читают данные из сокета по мере их поступления. • Возвращаемое значение больше 0 также указывает на то, что реагирую- щему объекту следует продолжать обнаружение и диспетчеризацию за- регистрированного события для данного обработчика событий. Кроме того, если значение > 0 возвращается после обработки события ввода/вы- вода, реагирующий объект будет осуществлять диспетчеризацию этого обработчика событий для данного дескриптора снова до того момента, когда данный реагирующий объект будет блокирован своим демультип- лексором событий. Такая возможность улучшает общую готовность сис- темы при работе с несколькими обработчиками событий ввода/вывода, позволяя одному обработчику событий выполнять ограниченное коли- чество действий, а затем прекращать его работу, чтобы позволить осуще- ствить диспетчеризацию других обработчиков событий до того, как дан- ный обработчик возобновит работу. • Возвращаемое значение -1 указывает реагирующему объекту, что нуж- но прекратить слежение за зарегистрированным событием для данного Блок 9: Приемы создания обработчиков событий в АСЕ Далее следует несколько приемов создания обработчиков событий для АСЕ- каркаса Reactor: • Чтобы предотвратить голодание (starvation), следует обеспечить неболь- шое время выполнения hook-методов handle_* *() обработчика событий, в идеале, оно должно быть меньше среднего интервала между наступлени- ем событий. Если hook-метод, обрабатывая некоторый запрос, может рабо- тать продолжительное время, подумайте о том, чтобы поместить данный за- прос в очередь в ACE_Message Queue и обработать его позже. Пример из раздела 6,3 иллюстрирует такой подход, путем объединения АСЕ-каркасов Task и Reactor, при реализации параллельного сервера регистрации на базе паперна Half-Sync/Half-Async. • Объединяйте действия, связанные с завершением обработчика событий в его hook-методе handle_close о, не рассредоточивайте их по разным ме- тодам. Соблюдение такого стиля особенно важно при работе с динамиче- ски создаваемыми обработчиками событий, которые удаляются через de- lete this, так как, удаляя не динамически выделенную Намять, легче обна- ружить потенциальные проблемы. • Вызывайте delete this только из метода обработчика событий hand- le_close(), и только после того, как последнее, из зарегистрированных у реагирующего объекта событий, будет удалено. Этот прием помогает из- бежать появления «висячих» указателей, которые могут появиться, если об- работчик событий, зарегистрированный у реагирующего объекта для не- скольких событий преждевременно удаляется. В блоке 10 показан один из способов контроля за этой ситуацией.
АСЕ-каркас Reactor 79 обработчика событий (и дескриптора, если это событие ввода/вывода). Прежде чем реагирующий объект удалит этот обработчик/дескриптор события из своей внутренней таблицы, он вызывает hook-метод hand- le_close () данного обработчика, передавая ему значение ACE_Reac- tor_Mask события, регистрация которого отменяется. Этот обработчик события может оставаться зарегистрированным для других событий на том же или другом дескрипторе. Следить за тем, для каких событий он продолжает оставаться зарегистрированным — это обязанность самого обработчика, как отмечено в блоке 10. 3. Завершение работы обработчика событий. Метод handle close () об- работчика событий вызывается тогда, когда один из его hook-методов при- нимает решение о том, что нужно завершить работу. Метод handle_clo- se () может затем выполнить определяемые пользователем действия, свя- занные с завершением работы, например, освобождение памяти, выделенной данному объекту, закрытие объектов IPC, или регистрацион- ных файлов и т.д. АСЕ-каркас Reactor игнорирует значение, возвращаемое самим методом handle_close (). ACE_Reactor вызывает handle_close () только тогда, кргда hook-ме- тод возвращает отрицательное значение, как описано выше, или когда обработ- чик удаляется из реагирующего объекта явным образом. Он не будет вызывать handle_close () автоматически, когда механизм IPC достигает конца файла или закрывается дескриптор ввода/вывода локальным приложением или его удаленным партнером. Следовательно, приложения должны знать, когда деск- риптор ввода/вывода закрывается и предпринимать шаги, чтобы убедиться, что ACE_Reactor вызывает’handle_close (). Например,-когда вызовы recv () или read () возвращают 0, данный обработчик событий должен вер- нуть из метода handle_* () -1 или вызвать метод ACE_Reactor: :remo- ve_handler(). В дополнение к тем типам событий, которые были приведены в таблице выше, реагирующий объект может передавать следующие значения, опреде- ленные в ACE_Event_Handler для handle_close(): Тип события Описание TIMER_MASK Указывает на события, управляемые временем, и передается реагирующим объектом тогда, когда hook-метод handle_timeout о возвращает-1. SIGNAL_MASK Указывает на события, связанные с сигналами, (или на события, связанные с дескрипторами, в Windows) и передается реагирующим объектом тогда, когда hook-метод handie_signai о возвращает-1. Возможности АСЕ по работе с сигналами описаны в (HJS).
80 Глава 3 Блок 10: Проблемы с регистрацией динамически создаваемых обработчиков событий Приложения должны следить за тем, когда динамически создаваемый обра- ботчик события можно удалить. Например, следующий класс показывает при- ем, при котором обработчик события следит за тем, когда все события, на ко- торые он зарегистрирован, будут удалены из связанного с ним реагирующего объекта. class My_Event_Handler : public ACE_Event_Handler { private: //Следит за событиями, зарегистрированными на обработчик. ACE_Reactor_Mask mask_; public: // ... методы класса, приведенные ниже ... Конструктор класса инициализирует член данных mask_ для событий read и write, й затем регистрирует объект this с параметром реагирующего объ- екта с целью обработки событий и того, и другого типа, следующим образом: My_Event_Handler (ACE_Reactor *r) : ACE_Event_Handler (г){ ACE_SET_BITS (mask_, ACE_Event_Handler::READ_MASK I ACE_Event_Handler::WRITE_MASK); reactor ()->register_handler (this, mask_); } Методы handle_input () nhandle_output () должны возвращать-1, когда они завершают обработку событий read и write, соответственно. Каждый раз, ко- гда метод handle_* () возвращает -1, реагирующий объект осуществляет пе- редачу управления hook-методу handle_ciose о, передавая ему значение ACE_Reactor_Mask того события, регистрация которого была прекращена. Этот hook-метод сбрасывает соответствующий бит в mask_ следующим обра- зом: virtual int handle_close(ACE_HANDLE,ACE_Reactor_Mask mask) { if (mask =- ACEJEventJHandler::READ_MASK) { ACE_CLR_BITS (mask_, ACE_Event_Handler::READ_MASK); // Реализуем логику очистки READJMASK... } if (mask == ACE_Event_Handler::WRITE_MASK) ( ACE_CLR_BITS (mask_, ACE_Event_Handler::WRITEJ4ASK) ; // Реализуем логику очистки WRITE_MASK. } if (mask_ == 0) delete this; return 0; J Только тогда, когда mask_ принимает нулевое значение, handle_close() вызы- воет delete this.
АСЕ-каркас Reactor 81 Пример Мы реализуем наш сервер регистрации путем наследования от АСЕ_ Event_Handler и приводя его в действие посредством цикла событий, реали- зуемого ACE_Reactor. Мы обрабатываем два типа событий: 1. События данных, которые указывают на поступление регистрационных записей от установивших соединение клиентских демонов регистрации. 2. События соединения, которые указывают на запрос новых соединений от клиентских демонов регистрации. В соответствии с этим, мы определяем два типа обработчиков событий в нашем сервере регистрации: 1. Logging_Event_Handler —Этот класс обрабатывает регистрационные записи, получаемые от установивших соединение клиентских демонов ре- гистрации. Он использует класс ACE_SOCK_Stream из главы 3 [C++NPvl], для чтения регистрационных записей, поступающих по соеди- нению. 2. Logging_Acceptor — Этот класс является фабрикой, которая динамиче- ски размещает Logging_Event_Handler и инициализирует его при под- ключении клиентского демона регистрации. Он использует класс ACE_SOCK_Acceptor из главы 3 [C++NPvl] для инициализации ACE_SOCK_Stream в Logging_Event_Handler. Оба обработчика событий наследуют от ACE_Event_Handler, который позволяет реагирующему объекту осуществлять диспетчеризацию своих мето- дов handle_input (). Связь между ACE_Reactor, ACE_Event_Handler, Logging_Acceptor и Logging_Event_Handler показана на рис. 3.4. Рис. 34 Классы сервера регистрации на базе ACE_Reactor Начнем с создания файла Logging_Acceptor. h, который включает нуж- ные заголовочные файлы: ♦include "ace/Event_Handler.h" ♦include "ace/INET_Addr.h" ♦include "ace/Log_Record.h" ♦include "ace/Reac.tor. h"
82 Глава 3 ♦include "ace/SOCK_Acceptor.h" ♦include "ace/SOCK—Stream.h" ♦include "Logging_Handler.h” Все заголовочные файлы, кроме одного, определены в АСЕ. Исключением является Logging^Handler. h, который содержит класс Logging_Handler, определенный в главе 4 [C++NPvl]. Класс Logging_Acceptor является производным от ACE_Event_Handler и определяет закрытый (private) экземпляр фабрики.АСЕ-SOCK-Acceptor: class Logging—Acceptor : public АСЕ—Event—Handler { private: // Фабрика пассивных соединений <ACE_SOCK_Stream>. ACE_SOCK_Acceptor acceptor^; protected: virtual ~Logging_Acceptor () {} // Пустой деструктор. Мы объявляем пустой (no-op) деструктор в разделе с защищенным (protec- ted) управлением доступом, чтобы обеспечить динамическое размещение Log- ging-Acceptor. В блоке И объясняется, почему обработчики событий долж- ны, как правило, создаваться динамически. Далее мы приводим интерфейс и фрагменты реализации метода Log- ging-Acceptor. Хотя в нашем примере почти нет кода, связанного с обработ- кой ошибок, готовый продукт (сервер регистрации из библиотеки компонен- тов сетевых служб АСЕ) выполняет все положенные действия в случае возник- новения ошибок. public: // Простой конструктор. Logging—Acceptor (АСЕ—Reactor *r = АСЕ—Reactor::instance ()) : ACE—Event—Handler (r) {) // Метод инициализации. virtual int open (const ACE_INET_Addr &local_addr); // Вызывается реактором1 при появлении нового запроса // на соединение. virtual int handle-input (ACE-HANDLE = ACE—INVALID—HANDLE); // Вызывается при удалении данного объекта, то есть когда // он' удаляется из реактора. Терминам reactor, reactive и т.д. нелегко найти однозначный эквивалент, поэтому в зависи- мости от контекста они переводятся по-разному: реагирующий объект или элемент (reactor), взаимно-согласованный сервер (reactive server) и т.д. В некоторых случаях, например, в лис- тингах программ, с целью экономии места, или в тексте, с целью улучшения восприятия тек- ста, приходится использовать кальку «реактор» (reactor). Читателю следует помнить, что термины реагирующий объект, реагирующий элемент и реактор являются синонимами в контексте механизма реагирования на события. — Прим. ред.
АСЕ-каркас Reactor 83 virtual int handle_close (ACE-HANDLE « ACE_INVALID—HANDLE, ACE_Reactor_Mask = 0) ; // Возвращает I/O дескриптор сокета пассивного режима. virtual ACE_HANDLE get_handle () const { return acceptor^.get__handle (); } // Возвращает ссылку на базовый <acceptor_>. АСЕ—SOCK—Acceptor &acceptor () { return acceptor—; } ); Блок 11: Стратегии управления памятью обработчиков событий Обработчики событий обычно должны размещаться в памяти динамически по следующим соображениям: ♦ Упростить управление памятью. Например, освобождение памяти может быть локализовано в методе handle_close () обработчика события, исполь- зуя прием слежения за событиями регистрации обработчика событий, при- веденный в блоке 10. • Избежать проблем с “висящими» обработчиками. Например, жизненный цикл обработчика события, экземпляр которого создается в стеке или в ка- честве члена другого класса, управляется извне, тогда как его регистрация у реагирующего объекта осуществляется изнутри. Если обработчик удаля- ется, продолжая оставаться зарегистрированным у реагирующего объек- та, то позже могут возникнуть не поддающиеся прогнозированию проблемы, если реагирующий объект попытается осуществить диспетчеризацию для несуществующего обработчика. • Избежать проблем с переносимостью. Например, динамическое разме- щение сглаживает неочевидные проблемы, возникающие в связи с логикой отложенного удаления обработчиков событий в ACE_WFMO_Reactor. Некоторые типы приложений, например системы реального времени, избега- ют использования динамической памяти или минимизируют его, чтобы повы- сить предсказуемость реакции. Если, для такого рода приложений, требуется выделять память под обработчики событий статически, то нужно следовать следующим соглашениям: 1. Не вызывайте delete this из handle_close (). 2. Отменяйте регистрацию всех событий у реагирующего объекта в деструк- торе данного класса в самую последнюю очередь. 3. Убедитесь, что время жизни у зарегистрированного обработчика события больше, чем у того реагирующего объекта, у которого он зарегистрирован, если, по каким-то причинам, его регистрация не может быть отменена. 4. Избегайте использования ACE_WFMO_Reactcr, поскольку он откладывает удаление обработчиков событий, тем самым, делая проблематичным вы- полнение соглашения 3. 5. При использовании ACE_WFMO_Reactor, передавайте методу АСЕ Reac- tor: : remove handler о признак dont_call, тщательно организуйте за- вершение действий и не используйте возможность обратного вызова с по- мощью метода реагирующего объекта handie_ciose ().
84 Глава 3 Метод Logging_Acceptor::ореп() инициализирует сокет-акцептор (acceptor socket) пассивного режима с целью прослушивания запросов на со- единение по local_addr. Затем Logging_Acceptor сам регистрируется у данного реагирующего объекта (реактора) для обработки событий ACCEPT. int Logging-Acceptor: .-open (const ACE_INET_Addr &local__addr) { if (acceptor_.open (local—addr) == -1) return -1; return reactor ()->register_handler (this, ACE—Event—Handler::ACCEPT—MASK); } Так как сокет пассивного режима в ACE_SOCK_Acceptor становится ак- тивным, когда нужно принять новое соединение, реагирующий объект автома- тически осуществляет передачу управления методу Logging-Accep- tor : : handle—input (). Мы покажем реализацию этого метода позже. Сна- чала приведем следующее определение класса Logging-Event-Handler: class Logging_Event—Handler : public ACE—Event_Handler { protected: // Файл, где хранятся регистрационные записи. АСЕ—FILE—10 log_file_; // Соединение с удаленным партнером. Logging_Handler logging—handler_; Этот класс наследует от ACE_Event_Handler и настраивает Log- ging-Handler, определенный в главе 4 [C++NPvl], для использования с ACE-каркасом Reactor. В дополнение к Logging-Handler, каждый Log- ging-Event-Handler содержит объект ACE_FILE_IO с целью создания от- дельного регистрационного файла для каждого установившего соединение клиента. Открытые (public) методы класса Logging-Event-Handler приведены ниже. public: // Инициализирует базовый класс и обработчик регистрации. Logging_Event_Handler (ACE_Reactor *reactor) : ACE_Event_Handler (reactor), logging_handler_ (log-file-) {} virtual ^Logging-Event—Handler () {) // Пустой деструктор, virtual int open (); // Активирует данный объект. // Вызывается реактором при наступлении событий регистрации, virtual int handle-input (ACE-HANDLE = ACE_INVALID-HANDLE); II Вызывается при удалении данного объекта, то есть при его // удалении из реактора.
АСЕ-каркас Reactor 85 ^virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE, ACE_Reactor_Mask = 0); // Возвращает дескриптор сокета <Logging_Handler>. virtual ACE_HANDLE get_handle() const; // Получает ссылку на <ACE_SOCK_Stream>. ACE_SOCK_Stream &peer() { return logging-handler-.peer (); } // Возвращает ссылку на <log_file_>. ACE_FILE_IO &log_file () const { return log_file__; } }; Метод Logging_Event—Handler::get_handle() определяется сле- дующим образом: ACE_HANDLE Logging_Event_Handler::get_handle (void) const { Logging—Handler &h = ACE_const_cast (Logging—Handler &, logging—handler—); return h.peer ().get—handle (); ) Так как get—handle () является const-методом, мы используем макрос ACE_const—cast для вызова не-const-метода Logging—Handler: :peer (). Это не опасно, так как мы с его помощью вызываем const-метод get_hand- 1е (). В блоке 17 [C++NPvl] рассматриваются макросы, предлагаемые АСЕ для поддержки переносимости операций приведения для всех компиляторов C++. Использовать эти макросы нет необходимости, если приложения используют только те компиляторы, которые поддерживают стандартные операторы при- ведения C++. Теперь, после того как мы познакомились с Logging-Event-Handler, займемся реализацией Logging_Acceptor: : handle_input (), к которому обращается реагирующий объект всякий раз, когда' требуется принять новое соединение. Этот метод-фабрика создает, соединяет и активизирует Log- ging—Event-Handler: lint Logging—Acceptor::handle—input (ACE—HANDLE) { 2 Logging—Event—Handler *peer_handler = 0; 3 ACE-NEW-RETURN (peer_handler, 4 Logging—Event—Handler (reactor ()), -1) ; 5 if (acceptor—.accept (peer_handler->peer ()) == -1) { 6 delete peer_handler; 7 return -1; 8 } else if (peer_handler->open () == -1) { 9 peer—handler->handle—close (); 10 return -1; 11 ) 12 return 0; 13 }
86 Глава 3 Строки 2-4 Создаем новый Logging_Event_Handler, который будет работать с новым сеансом регистрации клиента. В блоке 12 описан макрос ACE_NEW_RETURN и другие макросы АСЕ, связанные с управлением памятью. Вновь созданный Logging_Event_Handler- получает указатель на ACE_Re- actor данного объекта, что обеспечивает регистрацию нового обработчика у реагирующего объекта, который осуществляет диспетчеризацию данного hook-метода, замыкая, таким образом, цикл обработки событий сервера реги- страции. Строки 5-7 Принимаем новое соединение в дескриптор сокета Log- ging_Event_Handler, удаляя peer_handler и возвращая -1, если возни- кает ошибка. Как упоминалось выше, если возвращаемое методом hand- le_input () обработчика события значение равно-1, то реагирующий объект Блок 12: ACE-макросы управления памятью И в первом и во втором томах данной книги решаются многие проблемы, свя- занные с отличиями API разных ОС. Еще одна проблема связана с различиями между компиляторами C++. Хорошим примером является средство выделения динамической памяти в C++ operator new о. Первые исполнительные систе- мы C++ возвращали null-указатель, если не удавалось выделить память, в то время как в новых системах это приводит к возникновению исключительной си- туации. В АСЕ определен макрос, который унифицирует данную реакцию и возвращает указатель null независимо от действий конкретного компилято- ра. АСЕ использует эти макросы, чтобы гарантировать согласованное, пере- носимое поведение. Ваши приложения также могут их использовать. Если при выделении памяти происходит ошибка, все ACE-макросы управле- ния памятью устанавливают заданный указатель в null, а errno в enomem. ACE-макрос ace_new_return возвращает из текущего метода заданное зна- чение в случае ошибки, в то время как макрос ace_new просто завершает ра- боту, а макрос ace_new_return продолжает выполнение текущего метода. Данные макросы обеспечивают работу приложений в переносимом стиле, независимо от стратегий обработки ошибок выделения памяти, принятых в конкретных компиляторах C++. Например, макрос ace_new_return опреде- ляется следующим образом для компиляторов, которые вызывают исключение C++ std:: bad_a 11ос, в случае ошибки при выполнении new: «define ACE_NEW_RETURN (POINTER,CTOR, RET_VAL) \ do { try { POINTER = new CTOR; } catch (std::bad_alloc) \ { errno = ENOMEM; POINTER = 0; return RETJVAL; } \ } while (0) В отличие от этого, ace_new_return, для конфигураций компиляторов, которые предлагают вариант operator new, несвязанный с вызовом исключительной ситуации, определяется следующим образом: «define ACE_NEW_RETURN (POINTER, CTOR, RET_VAL) \ do ( POINTER = new (ACE_nothrow) CTOR; \ if (POINTER == 0) { errno = ENOMEM; return RET_VAL; }\ } while (0)
АСЕ-каркас Reactor 87 автоматически вызовет hook-метод handle_close () данного обработчика, который для Loggii)g_Acceptor определяется следующим образом: int Logging_Ac,ceptor: :handle_close (ACE_HANDLE, ACE_Reactor_Mask) { acceptor_.close (); delete this; return 0; ) Так как мы всегда используем класс Logging_Acceptor в тех обстоятель- ствах, которые требуют, чтобы он сам себя удалял, то мы, в различных приме- рах этой книги, выделяем под него память динамически. Строки 8-10 Активизируем установивший соединение peer_handler, вы- зывая его метод open (). Если данный метод возвращает -1, то мы закрываем peer_handler, который удаляет сам себя в методе Logging_Event_Hand- ler: : handle_close (). Метод open () приведен ниже. lint Logging_Event_Handler::open () { 2 static const char LOGFILE_SUFFIX[] = ".log"; 3 char filename[MAXHOSTNAMELEN + sizeof (LOGFILE_SUFFIX)]; 4 ACE_INET_Addr logging_peer_addr; 5 6 loggingjhandler_.peer ().get_remote_addr (logging_peer_addr); 7 logging_peer_addr.get_host_name (filename, MAXHOSTNAMELEN); 8 ACE_OS_String::strcat (filename, LOGFILE_SUPFIX); 9 10 ACE_FILE_Connector connector; 11 connector.connect (log_file_, 12 ACE_FILE_Addr (filename), 13 0, // Без тайм-аута. 14 ACE_Addr::sap_any, // Игнорируется. 15 0, // He используем повторно этот адрес. 16 O_RDWR|O_CREAT|0_APPEND, 17 ACE DEFAULT FILE PERMS); 18 19 return reactor ()->register_handler 20 (this, ACE_Event_Handler::READ_MASK); 21} Строки 3-8 Определяем имя хоста установившего соединение клиента и используем его в качестве имени регистрационного файла. Строки 10-17 Создаем или открываем файл, в котором хранятся регистра- ционные записи от клиента, с которым установлено соединение. Строки 19-20 Используем метод ACE_Reactor:: register_hand- ler (), чтобы зарегистрировать обработчик события this для событий READ у реагирующего объекта Logging_Acceptor.
88 ЕлаваЗ Когда от клиента поступают регистрационные записи, реагирующий объ- ект автоматически передает управление следующему методу Log- gin g_E vent _Handle г ::handle_input(): int Logging_Event_Handler::handle_input (ACE_HANDLE) { return logging_handler_. log_record (); } Этот метод обрабатывает регистрационную запись, вызывая Log- ging_Handler: : log_record (), который осуществляет чтение данной за- писи из сокета и ее запись в регистрационный файл, ассоциированный с дан- ным клиентским соединением. Так как logging_handler_ поддерживает свой собственный дескриптор сокета, метод handle_input () просто игнори- рует его параметр ACE_HANDLE. Когда же возникает ошибка или клиент закрывает соединение с сервером регистрации, метод log_record() возвращает значение -1, Которое метод handle_input () затем передает обратно тому реагирующему объекту, кото- рый осуществляет его диспетчеризацию (в блоке 13 обсуждаются стратегии действий с одноранговыми процессами, которые прекращают взаимодейство- вать). Данное значение приводит к тому, что реагирующий объект передает управление hook-методу Logging_Event_Handler: :handle_close (), который закрывает и сокет, созданный для данного клиента и его регистраци- онный файл, а затем удаляет самого себя, следующим образом: int Logging_Event_Handler::handle_close (ACE_HANDLE, ACE_Reactor_Mask) { logging_handler_.close (); log_file_.close (); delete this; return 0; } Это метод реализует delete this в безопасном стиле, так как память под объект Logging_Event_Handler выделяется динамически и не будет ис- пользоваться ни реагирующим объектом, ни какой другой частью данной про- граммы. Пример раздела 3.5 демонстрирует применение этого метода в контек- сте каркаса Reactor в целом. 3.4 ACE-классы очередей таймеров Обоснование Многие сетевые приложения выполняют действия периодически или по уведомлению об истечении заданного интервала времени. Например, web-cep- веру нужен контрольный таймер, который освобождает ресурсы, если кдиенты не посылают HTTP-запрос GET сразу после подключения. Собственные возможности ОС; связанные с таймерами, варьируются, но на многих платформах существуют следующие проблемы:
АСЕ-каркас Reactor 89 Блок 13: Что делать, если партнеры «молчат» Если клиент отключается, неважно штатно или внезапно, его сокет остается в состоянии открытом для чтения. Реагирующий элемент в состоянии отсле- дить эту ситуацию и вызвать hook-MeTOAhandle_input () того обработчика со- бытия, который связан с данным дескриптором сокета. Сам обработчик затем определяет, что данное соединение закрыто, обычно с помощью вызовов recv () или read (), возвращающих 0 или -1. Если, тем не менее, данный клиент прекратил связь совсем, этому могут быть несколько причин, включая: ♦ Ethernet-кабель отошел от разъема, Если его быстро вернуть в прежнее по- ложение, то соединение будет восстановлено. • Произошел аварийный отказ хоста. Не было возможности закрыть соедине- ние, поэтому локальная конечная точка соединения находится в подвешен- . ном состоянии, ее работа не может быть возобновлена. В таких ситуациях нет событий, которые реагирующий элемент мог бы обнару- жить, . В зависимости от потребностей прикладных служб и используемых протоко- лов прикладного уровня, есть несколько способов поведения в ситуации от- сутствия отклика от удаленного партнера. Они включают: • Подождать пока механизм потверждения соединения (keepalive) тср пре- , кратит поддерживать связь с данным партнером и закроет данное соедине- ние, что приведет к наступления соответствующего события для данного со- кета, которое может быть обработано также, как если бы сам клиент закрыл соединение. К сожалению, это может потребовать много времени — воз- можно часов — как указывается в (SW95), • Реализовать на прикладном уровне стратегию или протокол, типа дежурных («heartbeat») фраз или периодической отправки сообщения типа «вы на свя- зи?» («аге you there?»). Если партнер не отвечает на дежурную фразу или за- прос типа «вы на связи?» в течение периода времени, задаваемого прило- жением, завершить соединение в одностороннем порядке. Приложение может затем попытаться позднее снова установить данное соединение. • Реализовать стратегию, при которой в случае, если партнер не отвечает в течение некоторого заданного промежутка времени, соединение закры- вается. Такого типа стратегия используется классом Logging_Event_Hand- 1ег_Ех. > • Ограниченное количество таймеров. Многие платформы позволяют приложениям устанавливать ограниченное количество таймеров. На- пример, каждая системная функция POSIX alarm () и ua la rm () могут устанавливать один сигнал («alarm clock») таймера на каждый вызов. По- этому управление временными интервалами множества таймеров часто включает разработку механизма очередей таймеров (timer queue), кото- рые отслеживают порядок истечения сроков, на которые установлены таймеры. Задание нового ближайшего («earliest») таймера может уста- навливать (переустанавливать), если нужно, время выдачи сигнала. • Срабатывание таймера сопровождается сигналом. Например, систем- ная функция а 1 a rm () вырабатывает сигнал SIGALRM при истечении ус- тановленного срока. Программировать сигналы таймеров сложно, так как действия приложений, связанные с сигналами, ограничены. Прило- жения могут минимизировать обработку, связанную с сигналами, на
90 Глава 3 платформах UNIX с помощью системной фу'нкции sleep () или, ис- пользуя системную функцию sigsuspend (). Тем не менее, такие реше- ния не являются переносимыми и приводят к блокировке вызывающего потока, что может затруднить параллельную работу и усложнить про- граммирование. Одним из способов обойти эти проблемы является встраивание таймеров в обычный механизм обработки событий, следующим образом: 1. Создать механизм очередей таймеров, который упорядочивает работу тай- меров и связывает каждый таймер с некоторым действием, которое следует выполнить при истечении установленного срока. 2. Интегрировать очередь таймеров с используемым приложением демуль- типлексором синхронных событий, таким как select () или WaitFor- MultipleObjects (), чтобы объединить обработку событий таймеров с обработкой других событий. Тем не менее, трудно реализовать такого рода возможности, связанные с таймерами, так, чтобы они были переносимы на другие платформы ОС, из-за больших различий в их возможностях и ограничениях. Кроме того, такую воз- можность часто приходится создавать, во многих проектах, заново из-за тесной связи между механизмом очередей таймеров и механизмом демультиплекси- рования синхронных событий. Чтобы разработчики в меньшей степени нужда- лись в создании заново эффективных, масштабируемых и переносимых дис- петчеров, управляемых временем, для каждого нового проекта, АСЕ-каркас Reactor определяет семейство повторно используемых классов, связанных' с очередями таймеров. Функциональные возможности классов ACE-классы очередей таймеров позволяют приложениям регистрировать об- работчики событий, управляемые временем, производные от ACE_Event_Hand- ler. Данные классы предоставляют следующие функциональные возмржности: • Позволяют приложениям управлять такими обработчиками событий, для hook-методов которых handle_timeout () осуществляется эф- фективная и масштабируемая диспетчеризация в моменты времени в бу- дущем, заданные вызывающей стороной. Причем диспетчеризация мо- жет осуществляться как однократно, так и периодически. • Позволяют приложениям сбрасывать таймер, связанный с конкретным обработчиком событий, или все связанные с ним таймеры. • Позволяют приложениям настраивать источники времени для очереди тай- меров, такие как ACE_OS: :gettimeofday () или ACE_High_Res_Ti- mer:: gettimeofday_hr (), описанные в блоке 14. Интерфейсы и взаимосвязи всех классов АСЕ, относящихся к очередям таймеров,, показаны на рис. 3.5. Основные методы этих классов приведены в следующей таблице:
АСЕ-каркас Reactor 91 Метод Описание | schedule() Планирует обработчик события, для метода handie_timeout () которого будет осуществляться диспетчеризация в момент вре- мени в будущем, заданный вызывающей стороной и, факульта- тивно, через периодические промежутки времени. cancel () Удаляет таймер, связанный с конкретным обработчиком собы- тий или все связанные с ним таймеры. expire () Осуществляет диспетчеризацию метода handie_timeout () всех обработчиков событий, время истечения срока которых меньше или равно текущему времени суток, которое представ- ляется как абсолютное время, например, 2001-09-11-09.37.00. gettime- of day () Два перегружаемых метода, которые позволяют приложениям (1) задавать метод, используемый для управления источником текущего значения времени для очереди таймеров и (2) вызы- вать этот метод, чтобы вернуть текущее абсолютное значение времени. | Методу schedule () ACE-очереди таймеров должны передаваться два па- раметра: • Указатель на обработчик события, который будет исполнителем при сле- дующей диспетчеризации handle_timeout (). • Ссылку на ACE_Time_Value, указывающее момент в будущем, в абсо- лютном времени, когда hook-метод handle_timeout () должен быть вызван для данного обработчика события. Этому методу, факультативно, могут быть переданы следующие параметры: • Указатель типа void, который хранится в очереди таймеров и возвраща- ется обратно в неизмененном виде, когда осуществляется диспетчериза- ция Метода handle_timeout (). Этот указатель может использоваться в качестве асинхронного маркера завершения (asynchronous completion token, ACT) в соответствии с паттерном Asynchronous Completion Token [POSA2]. Этот паттерн позволяет приложению эффективно демультип- лексировать и обрабатывать ответы на асинхронные операции, которые оно выполняет с помощью служб. С помощью ACT, один и тот же обра- ботчик события может быть зарегистрирован в очереди таймеров на дис- петчеризацию нескольких моментов в будущем. • Ссылка на второе значение ACE_Time_Value, обозначающее интервал периодической диспетчеризации данного обработчика события. Если этот параметр опущен, диспетчеризация метода handle_timeout () данного обработчика события будет осуществлена только один раз. При задании очень коротких временных интервалов, помните, что реальная разрешающая способность таймера будет ограничена частотой обновле- ния того таймера, который используется в качестве источника для очере- ди таймеров, а частота обновления на разных платформах разная.
92 ГлаваЗ Рис. 3-5 ACE-классы очередей таймеров Когда очередь таймеров осуществляет диспетчеризацию метода hand- le_timeout () обработчика событий, она передаёт ему в качестве параметра текущее время и void-указатель (ACT), передаваемый методу schedule (), если обработчик событий его использует изначально. Возвращаемое методом schedule () значение уникально идентифициру- ет каждое событие, связанное с таймерами, управляемое очередью таймеров. Приложения могут передавать методу cancel () уникальный идентификатор таймера с целью удаления конкретного обработчика события до того как он сработает. Приложения могут также передавать адрес данного обработчика ме- тоду cancel (), чтобы удалить все таймеры, связанные с конкретным обработ- чиком событий. Если методу cancel () передается указатель на void, значе- ние которого не-NULL, то это значение установлено по ACT, переданному при- ложением, при первоначальной установке этого таймера, что дает возможность выделять память для ACT динамически без утечек памяти. ACE-классы очередей таймеров объединяют паттерны проектирования, hook-методы и шаблонные аргументы, чтобы обеспечить следующие реализа- ции очередей таймеров: • ACE_Timer_Heap, которая является частично упорядоченным, почти полным бинарным деревом, реализованным в массиве [Rob99]. Ее про- изводительность в среднем и худшем случае при планировании, завер- шении и истечении срока обработчика событий определяется как O(lg п), поэтому она является в ACE-каркасе Reactor механизмом по умолчанию для очередей таймеров. Очереди таймеров, основанные на использова- нии кучи (heap), полезны для операционных систем [BL88] и промежу-
АСЕ-каркас Reactor 93 Блок 14: Источники времени в АСЕ Рис. 3.5 показывает, ЧТО ACE_Timer_Queue: : qettimeof day () МОЖНО заменить, , чтобы использовать любой статический метод, который возвращает значение ACE_Time_Vaiue. Возвращенное временное значение считается абсолют- ным, но оно не обязано быть текущей датой и временем (известно как «настен- ное» время, «wall clock»). Основное требование к статическому методу, воз- вращающему временное значение, заключается в том, чтобы он обеспечивал правильную основу для планирования таймеров и принятия решений об исте- чении установленных сроков. АСЕ предоставляет два механизма, удовлетво- ряющих этому требованию: • ACE_os: :gettimeofday О является статическим методом, возвращающим ACE_Time_Value, которое включает абсолютные дату и время, в представ- лении, принятом в данной операционной системе. • ACE_High_Res_Timer: :gettimeofday_hr () является статическим методом, возвраща'ющим значение таймера с высокой разрешающей способно- стью, зависящей от ОС, преобразованное в единицы ACE_Time_vaiue. Эти таймеры часто используют количество тиков генератора тактовых импульсов центрального процессора с момента загрузки системы, а не фактическое «настенное» время. Квант времени у этих таймеров может отличаться по величине на три-четыре порядка. Однако при использовании их для измерения временных интервалов в механизмах демультиплексирования событий, разрешающая способность является обычно приблизительно одинаковой, из-за сложности тактовых гене- раторов, обслуживания таймера по прерываниям и планирования ОС. В кон- тексте очередей таймеров в Reactor, они отличаются, в основном, своим поведе- нием при изменении системных даты и/или времени, когда значения, передавае- мые AfcE_High_Res_Timer: :gettimeofday_hr (), будут меняться, а значения, передаваемые ACEJ-Iigh__Res_Timer: : gettimeofday_hr () не будут менять- ся — будут продолжать увеличиваться с постоянной скоростью. Если поведе- ние таймера в приложении должно оставаться постоянным — даже если фак- тическое «настенное» время изменяется — источник времени, используемый ПО умолчанию должен быть заменен на ACE_High_Res_Timer: :gettimeof- day_hr (), как показано в примере раздела 3.4. точного слоя [SLM98], приложения которых требуют предсказуемой, с небольшим временем ожидания, временной диспетчеризации. Блок 15 описывает некоторые моменты, которые разработчики приложений ре- ального времени должны учитывать при использовании в своих про- граммах ACE-механизмов очередей таймеров. • ACE_Timer_Wheel, которая использует колесо времени (tinting wheel) [VL97], с кольцевым буфером, который используется при планирова- нии, завершении и диспетчеризации таймеров, со среднем временем 0(1); для худшего случая оценка времени О(м). • ACE_Timer_Hash, которая использует хэш-таблицу для организации очереди. Также как при реализации «колеса времени», среднее время пла- нирования, завершения и истечения срока таймеров составляет 0(1), а в худшем случае — О(п).
Глава 3 Блок 15: Использование таймеров в сетевых приложениях реального времени Сетевые приложения реального времени применяются все шире и приобре- тают все большее значение (GSC02). Распространенными примерами явля- ются телекоммуникационные сети (например, службы мобильной телефонной связи), телемедицина (например, дистанционная хирургия), автоматизация технологических процессов (например, станов горячего проката) и оборон- ные приложения (например, вычислительные системы авиационной радио- электроники). Для всех приложений такого типа обычно справедлив афо- ризм: правильный ответ, полученный слишком поздно, все равно, что ошибоч- ный ответ. Если один и тот же реагирующий объект используется для диспетчеризации и обработчиков ввода/вывода и обработчиков событий очередей таймеров, то разброс в количестве времени, затрачиваемого на обработку ввода/выво- да, может привести к дрейфу («drift») при обработке событий очередей тайме- ров, что может нарушить расчеты, связанные с планированием, и предсказуе- мость сетевых приложений реального времени. Кроме того, механизмы демуль- типлексирования событий и синхронизации, используемые для интеграции диспетчеризации событий ввода/вывода и таймеров в реализациях АСЕ-кар- каса Reactor, могут привести к издержкам, которые приложения реального времени не могут себе позволить. Следовательно, у разработчиков сетевых приложений реального времени мо- жет появиться желание использовать автономные ACE-очереди таймеров, ра- ботающие в разных потоках, чтобы осуществлять диспетчеризацию событий таймеров. В зависимости от сравнительной значимости событий таймеров и ввода/вывода, для разных потоков могут использоваться разные приоритеты. Класс ACE_Thread_Timer_Queue_Adapter полезен, так как упрощает дизайн сетевых приложений реального времени. • ACE_Timer_List, которая реализуется как связный список абсолют- ных таймеров, упорядоченных по величине срока истечения. Хотя ее производительность при планировании и завершении таймеров в сред- нем и худшем случае составляет О(и), она использует меньший объем па- мяти в реализациях очередей таймеров в АСЕ. ACE-каркас Reactor позволяет разработчикам использовать любые из реа- лизаций очередей таймеров, для достижения функциональных возможностей, нужных их приложениям, не ограничивая их одной реализацией на все случаи жизни («one size fits all»). Поскольку методы базового класса ACE_Timer_Qu- eue являются виртуальными, приложения могут также встраивать свои собст- венные реализации механизмов очередей таймеров. Пример Хотя обработчики событий Logging_Acceptor и Logging_Event_Hand- ler из раздела 3.3 корректно реализуют функциональность сервера регистра- ции, они могут потреблять слишком много системных ресурсов. Например, клиенты могут подключиться к серверу и затем, в течение долгого времени, не посылать регистрационных записей. В примере, который мы приводим ниже, мы иллюстрируем применение ACE-механизмов очередей таймеров с целью
АСЕ-каркас Reactor 95 восстановления ресурсов за счет тех обработчиков событий, клиенты которых посылают регистрационные записи нечасто. Haiti дизайн основан на паттерне Evictor [HV99], который определяет когда и как освобождать ресурсы, такие как память и дескрипторы ввода/вывода, с целью оптимизации управления ресур- сами системы. Мы используем паттерн Evictor в сочетании с механизмом очередей тайме- ров ACE-каркаса Reactor, чтобы периодически проверять, когда каждый из за- регистрированных обработчиков событий получил последнюю регистрацион- ную запись от своего клиента. Если время с момента получения последней реги- страционной записи превышает заданный порог (один час), осуществляются следующие действия: 1. Данный обработчик событий отключается от своего клиента. 2. Его ресурсы возвращаются операционной системе. 3. Он удаляется из списка реагирующего объекта. Клиенты должны обнаруживать закрытые соединения и устанавливать их повторно, если им нужно послать дополнительные регистрационные записи, как отмечено в примерах разделов 6.2 и 7.4. В данном примере используется метод ACE_Timer_Queue: : gettimeof- day () для получения всех истинных значений времени. Этот метод позволяет нам изменять таймер программы во время выполнения, в соответствии с изло- женным в блоке 14. Например, если переход на системный датчик времени не должен ни замедлять, ни ускорять процесс вытеснения клиентов, к программе main () можно добавить следующий код (до начала работы с таймерами): ACE_Reactor *r = ACE_Reactor::instance (); ACE_High_Res_Timer::global_scale_factor (); r->timer_queue ()-> gettimeofday (&ACE_High_Res_Timer::gettimeofday_hr); Вызов ACE_High_Res_Timer: : global_scale_f actor () выполняет всю инициализацию, нужную для получения точных значений таймера, до того как программа начнет выполнение конкретной работы. Для реализации паттерна Evictor, мы расширяем Logging_Acceptor и Logging_Event_Handler из радела 3.3 с целью создания Logging_Ac- ceptor_Ex и Logging_Event_Handler_Ex. Затем мы регистрируем таймер для каждого экземпляра Logging_Event_Handler_Ex. Поскольку создавае- мая АСЕ по умолчанию очередь таймеров (ACE_Timer_Heap) является высоко- эффективной и масштабируемой, то ее издержки, связанные с планированием, завершением и осуществлением диспетчеризации, незначительны, даже если создаются тысячи таймеров и экземпляров Logging_Event_Handler_Ex. Начнем с создания нового заголовочного файла с именем Logging_Ac- ceptor_Ex. h, который содержит новый класс Logging_Acceptor_Ex. Из- менения в этом классе минимальны. Мы всего лишь подменяем и модифици- руем его метод handle_input (), чтобы создать.Logging_Event_Hand- 1ег_Ех, а не Logging_Event_Handler, как показано ниже:
96 ГлаваЗ ♦include "ace/INET_Addr.h" #include "ace/Reactor.h" #include "Logging_Acceptor.h" ♦include "Logging_Event_Handler_Ex.h" class Logging_Acceptor_Ex : public Logging_Acceptor { public: typedef ACE_INET_Addr PEER_ADDR; // Простой конструктор для передачи <ACE_Rea|ctor> // базовому классу. Logging_Acceptor_Ex (ACE_Reactor *r = ACE_Reactor::instance ()): Logging_Acceptor (r) {} int handle_input (ACE_HANDLE) { Logging_Event_Handler_Ex *peer_handler = 0; ACE_NEW_RETURN (peer_handler, Logging_Event_Handler_Ex (reactor ()), -1); // . .. то же, что и Logging_Acceptor::handle_input () } }; В главе 7 мы покажем, как ACE-каркас Acceptor-Connector использует воз- можности C++ (такие как шаблоны, наследование и динамическое связывание) и паттерны проектирования (такие как Template Method и Strategy [GoF]), что- бы дополнить обработчик Событий новыми функциональными возможностя- ми, не копируя и не модифицируя существующий код. Класс Logging_Event_Handler_Ex включает более существенные до- полнения, поэтому мы создаем два файла: Logging_Event_Handler_Ex. h и Logging_Event_Handler_Ex . срр. В Logging_Event_Handler_Ex. h мы расширяем Logging_Event_Handler с целью создания следующего класса Logging_Event_Handler_Ex: class Logging_Event_Handler__Ex : public Logging_Event_Handler { private: // Время отправки 'клиентом последней регистрационной записи. ACE_Time_Value time_of_last_log_record_; // Максимальное время ожидания регистрационной записи // от клиента. const ACE_Time_Value max_client_timeout_; Мы реализуем паттерн Evictor путем добавления ACE_Time_Value, кото- рое хранит время отправки клиентом последней регистрационной записи. Ме- тоды public-интерфейса Logging_Event_Handler_Ex показаны ниже: public: typedef Logging_Event_Handler PARENT;
АСЕ-каркас Reactor 97 //3600 секунд == один час. enum { MAX___CLIENT_TIMEOUT = 3600 }; Logging_Event_Handler_Ex (ACE_Reactor *reactor, const ACE_Time_Value &max_client_timeout = ACE_Time_Value (MAX_CLIENT__TIMEOUT) ) Logging_Event_Handler (reactor), time_of__last_log_record (0), maX—Client—timeout— (max_client_timeout) {) virtual ~Logging_Event_Handler_Ex () { /* no-op */ ) virtual int open (); // Активизирует обработчик событий. // Вызывается реактором при наступлении событий регистрации, virtual int handle_input (ACE__HANDLE) ; // Вызывается при истечении тайм-аута, чтобы проверить, что // время простоя клиента превышает установленное время, virtual int handle_timeout (const ACE_Time_Value &tv^ const void *act); // Вызывается при удалении объекта this, например, когда //он удаляется из реактора (reactor). virtual int handle__close (ACE_HANDLE == ACE_INVALID__HANDLE, ACE_Reactor_Mask = 0); }; Метод Logging__Event_Handler_Ex: : handle__input () засекает вре- tyw получения регистрационной записи от установившего соединение клиента. Мы получаем время от источника времени, используемого очередью таймеров, чтобы гарантировать согласованность сравнений. После фиксации времени, этот метод передает управление методу handle_input() своего родитель- ского класса, чтобы обработать регистрационную запись: intLogging_Event_Handler_Ex::handle_input (ACE_HANDLE h) { time_of_last_log_record_ = reactor ()->timer_queue ()->gettimeofday () ; return PARENT::handle_input (h); } Далее приведен метод open (): lint Logging_Event_Handler_Ex::open () { 2 int result = PARENT::open (); 3 if (result ’= -1). { 4 ACE_Time_Value reschedule (max_client_timeout_.sec () /4); 5 result = reactor ()->schedule_timer 6 (this, 0, 7 max_client_timeout_, // Начальный тайм-аут. 8 reschedule); // Следующие тайм-ауты. 4 Программирование сетевых приложений на C++. Том 2
98 Глава 3 9 } 10 return result; И } Строка 2 Передаем управление методу open () родительского класса. Строки 4-8 Вызываем ACE_Reactor:: schedule_timer () с целью ус- тановки обработчика событий this на периодическую диспетчеризацию, что- бы проверять, как давно клиент отправлял ему регистрационные записи. Мы планируем начальное значение срока истечения равном max_client_time- out_ секунд (по умолчанию один час) и проверяем, также периодически, каж- дые max_client_.timeout_/4 секунд (например, каждые 15 минут) не истек- ло ли время тайм-аута. Когда время, на которое установлен таймер, истекает, реагирующий эле- мент использует свой механизм очереди таймеров для осуществления автома- тической диспетчеризации следующему hook-методу handle_timeout (): int Logging_Event_Handler_Ex::handle_timeout (const ACE_Time_Value &now, const void *} { if (now - time_of_last_log_record^ >= max_client_timeout_) reactor ()->remove_handler (this, ACE_Event_Handler::READ_MASK); return 0; ) Когда ACE-очередь таймеров, при истечении срока таймера, передает управление методу handle_timeout (), тот устанавливает параметр now в абсолютное значение времени (получаемое от ACE_Timer_Queue: :getti- meofdayO). Logging_Event_Handler_Ex: :handle_timeout () прове- ряет, является ли время прошедшее между now и тем временем, когда данный обработчик событий получил свою последнюю регистрационную запись, боль- ше, чем порог, установленный max_client_timeout_. Если ответ положи- тельный, он вызывает ACE_Reactor: : remove_handler (), который пере- ключает реагирующий элемент на вызов следующего hook-метода, чтобы уда- лить данный обработчик событий из данного реагирующего элемента: int Logging_Event_Handler_Ex::handle_close (ACE_HANDLE, ACE_Reactor_Mask) { reactor ()->cancel_timer (this); return PARENT::handle_close (); ) Этот метод сбрасывает таймер обработчика и вызывает его родительский метод handle_close (), который закрывает регистрационный файл и сокет данного клиента, а затем удаляет сам себя.
АСЕ-каркас Reactor 99 3.5 Класс ACE_Reactor Обоснование Событийно-управляемые сетевые приложения до сих пор программирова- лись, так сложилось исторически, на основе внутренних механизмов ОС, таких как Socket API и демультиплексор синхронных событий selectQ.TeMHe ме- нее, приложения, разработанные таким образом, являются не только неперено- симыми, но и негибкими, так как в них все перемешано: и низкоуровневый код, связанный с обнаружением, демультиплексированием и диспетчеризацией со- бытий, и код обработки событий, прикладного уровня. Поэтому разработчи- кам приходится снова и снова писать этот код для каждого нового сетевого при- ложения, что утомительно, дорого и чревато ошибками. Кроме того, это не яв- ляется необходимым, так как многое из того, что связано с обнаружением, демультиплексированием и диспетчеризацией может быть обобщено и может использоваться повторно во многих сетевых приложениях. Одним из способов решения этих проблем является сочетание профессио- нального объектно-ориентированного проектирования с практическим опы- том, накопленным в области разработки сетевых приложений, для создания на- бора каркасных классов, отделяющих код обработки прикладных событий от повторно используемого кода каркаса, связанного с обнаружением, демультип- лексированием и диспетчеризацией событий. Разделы с 3.2 по 3.4 заложили осно- ву такого каркаса, определив повторно используемые значения времени и классы очередей таймеров, и создав интерфейс, на базе класса ACE_Event_Handler, между каркасом и кодом обработки событий приложения. В данном разделе описывается, каким образом класс ACE_Reactor, основа ACE-каркаса Reactor, предоставляет приложениям возможность зарегистрироваться на получение уведомлений о событиях от множества источников. Функциональные возможности класса Класс ACE_Reactdr реализует паттерн Facade [GoF] для создания интер- фейса, который приложения могут использовать для доступа к различным воз- можностям ACE-каркаса Reactor. Данный класс предоставляет следующие воз- можности: • Централизует цикл обработки событий в событийно-управляемом при- ложении. • Отвечает за обнаружение событий, используя демультиплексоры собы- тий, такие как select () или WaitForMultipleObjects (), предос-. тавляемые ОС и используемые реализацией реагирующего объекта, ре- актора (reactor). • При регистрации демультиплексором наступления заданных событий, осуществляет их демультиплексирование обработчикам событий. • Осуществляет диспетчеризацию hook-методов зарегистрированных об- работчиков событий с целью выполнения определяемой приложением обработки как ответной реакции на соответствующие события.
100 Глава 3 • Гарантирует, что любой поток может изменить набор событий каркаса Reactor или поставить в очередь обратный вызов обработчика события и рассчитывать на то, что Reactor своевременно среагирует на запрос. Интерфейс ACE_Reactor показан на рис. 3.6. Данный класс имеет разви- тый интерфейс, который экспортирует все возможности ACE-каркаса Reactor. Поэтому мы разделяем описание его методов на шесть групп, перечисленных ниже. 1. Методы инициализации и удаления реагирующего объекта. Следующие методы осуществляют инициализацию и удаление ACE_Reactor: Метод Описание ACE_Reactor() open() Эти методы создают и инициализируют экземпляры реактора. ~ACE_Reactor() close () Эти методы освобождают ресурсы, выделенные при инициализации реактора. В классе ACE_Reactor, за неизменным интерфейсом, скрыто множество механизмов демультиплексирования, обсуждаемых в данной главе. С целью та- кого разделения механизмов, при котором их легко было бы использовать и со- провождать, класс ACE_Reactor, чтобы разделить реализацию и интерфейс классов, использует паттерн Bridge [GoF]. Такой дизайн позволяет пользовате- лям заменять реализацию по умолчанию, если она не подходит, на специализи- рованную реализацию реактора. Конструктору ACE_Reactor можцо факультативно передать указатель на реализацию, используемую для обнаружения и демультиплексирования собы- тий и диспетчеризации методов соответствующих обработчиков событий. ACE_Select_Reactor, описанный в разделе 4.2 является реализацией ACE_Re- actor по умолчанию на большинстве платформ. Исключением является Win- dows, в которой реализация по умолчанию — ACE_WFMO_Reactor, по причи- нам, изложенным в блоке 25. Методу ACE_Reactor: : open () могут быть переданы: • Количество дескрипторов ввода/вывода и обработчиков событий, управ- ляемых данным реактором. Значение по умолчанию варьируется в зави- симости от реализации реактора, в соответствии с изложенным в бло- ке 20. • Тип реализации очереди таймеров, которую будет использовать данный реактор. Реализацией по умолчанию является ACE_Timer_Heap, опи- санная в разделе 3.4. Хотя АСЕ предлагает полнофункциональный набор конструкторов класса, лучше их использовать при моделировании или в случае отсутствия ошибок, т.е. в тех ситуациях, когда контроль ошибок не так важен (см. раздел 10 в [Меу96]). Предпочтительным использованием в АСЕ является автономный вызов методов open () (и close () в случае удаления объекта). Это предпочте-
АСЕ-каркас Reactor 101 ACE_Reactor # reactor, ; ACEjteactpr * # implementation- : ACE_Reactor_Impl * + ACE_Reactor (implementation : ACE_Reactor_Impl * - 0, delete_implementation : int « 0) + open (max—handles : int, restart : int - 0, sig_handler : ACE_Sig_Handler * - 0, timer_queue : ACE_Timer_Queue * - 0) : int + close () : int + register_handler (handler : ACE_Event—Handler *, mask : ACE—Reactor—Mask) : int * register—handler (io : ACE—HANDLE, handler : ACE_Event—Handler ★, mask : ACE—Reactor—Mask) : int + remove_handler (handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask) : int > + remove_handler (io : ACE—HANDLE, mask : ACE_Reactor_Mask) : int + remove-handler (hs : const ACE_Handle_Set&, m : ACE_Reactor_Mask) : int + suspend—handler (handler : ACE—Event—Handler *) : int + resume_handler (handler : ACE_Event_Handler *) : int + mask—ops (handler : ACE_Event—handler *, mask : ACE—Reactor_Mask, ops : int) : int + schedulв-Wakeup (handler : ACE—Event_Handler *, masks—tO—be—added : $CE_Reactor_Mask ) : int + cancel—wakeup (handler : ACE—Event—Handler. ★, masks—to_be_cleared : ACE—Reactor_Mask) : int + handle—events (max_wait_td.me : ACE_Time_Value * • 0) : int + run_reactor_event_loop (event_hook : int (*) (void *) - 0) : int + end—reactor_event—loop () : int + reactor—event—loop_done (}: int + schedule_timer (handler : ACE—Event_Handler *, arg : void *, delay : ACE_Time_Value &, repeat : ACE_Time-Value 6 • ACE_Time_Value: :zero) : int + cancel—timer (handler : ACE_Event_Handler *, dont_call—handle_close : int - 1) : int + cancel—timer (timer_id : long, arg : void ** - 0, dont_call—handlenclose : int - 1) : int + notify (handler : ACE_Event_Handler * • 0, mask : ACE—Reactor_Mask • ACE_Event_Handler:: EXCEPT—MASK, timeout : ACE_Time__Value * - 0) : int + max_notify_iterations (iterations : int) : int + purge-pending_notifications (handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask « ALL—EVENTS—MASK) : int + instance 0 s.ACEJtaac.tQr * + owner (new owner : ACE thread—t, old_owner : ACE—thread—t * 0) : int Рис. 3.6 Класс ACE_Reactor ние связано с тем, что open () Hclose () возвращают указатели ошибок, тогда как конструкторы и деструкторы АСЕ не используют исключений C++. Обос- нование отказа от исключений C++ в АСЕ приведено в разделе А.6 в [C++NPvl]-. Класс ACE_Svc_Handler, рассматриваемый в разделе 7.2 данной книги, за- крывает дескриптор базового сокета автоматически. Деструктор ACE_Reactor и методы close () освобождают все ресурсы, .используемые реактором. Процесс освобождения включает вызов hook-мето- дов handle_close () всех обработчиков событий, связанных с дескрипторами, которые остаются зарегистрированными у реактора. Любые установленные тай-
102 Глава 3 меры удаляются без предупреждения и все уведомления, которые помещены в буфер механизма уведомлений реактора теряются, когда реактор закрывается. 2. Методы управления обработчиками событий. Следующие методы осуще- ствляют регистрацию и удаление обработчиков событий в ACE_Reactor: Метод Описание register_handler (.) Регистрирует обработчик события для событий, основанных на вводе/выводе и на сигналах. remove_handler() Удаляет обработчик события из списка диспетчеризации событий ввода/вывода и сигналов. suspend-handler() Временно приостанавливает диспетчеризацию событий для некоторого обработчика событий. resume-handler() Возобновляет диспетчеризацию событий для ранее приостановленного обработчика. mask-Ops () Получает, устанавливает, добавляет или сбрасывает тип(ы) событий, связанных с обработчиком событий и его маской диспетчеризации. schedule_wakeup() Добавляет заданные маски элементу обработчика событий, который должен быть предварительно зарегистрирован С ПОМОЩЬЮ register_handler (). cancel—wakeup() Очищает заданные маски элемента обработчика событий, но не удаляет обработчик из реактора. Методы регистрации и удаления ACE_Reactor предлагают множество пе- регружаемых сигнатур для облегчения их использования в различных ситуаци- ях. Например, методы register_handler () могут использоваться с любы- ми из следующих сигнатур: • (ACE_Event_Handler *, ACE_Reactor_Mask) — в данной версии первый параметр определяет обработчик событий приложения, а второй указывает на тип события(й), которое данный обработчик предназначен обрабатывать. Реализация этого метода использует двойную диспетчери- зацию (double-dispatching) [GoF], чтобы получить дескриптор с помощью метода get_handle () обработчика. Преимущество такого решения за- ключается в том, что прикладному коду не нужно ни получать, ни предъ- являть дескриптор ввода/вывода в явном виде, что предохраняет от слу- чайного связывания ошибочного дескриптора с обработчиком событий. Поэтому большинство примеров в этой книге используют именно этот вариант register_handler (). •(ACE_HANDLE, ACE_Event_Handler *, ACE_Reactor_Mask) — в данной версии добавляется новый первый параметр, чтобы явно ука- зать дескриптор ввода/вывода, связанный с обработчиком событий при- ложения. Такое решение потенциально больше подвержено ошибкам, чем версия с двумя параметрами, приведенная выше, так как вызываю- щие программы могут случайно передать дескриптор ввода/вывода, ко-
АСЕ-каркас Reactor 103 торый не подходит данному обработчику событий. Тем не менее, этот параметр позволяет приложению регистрировать множество дескрипто- ров ввода/вывода для одного и того же обработчика событий, что необ- ходимо тем обработчикам, которые должны быть ассоциированы с не- сколькими объектами IPC. Кроме того, этот метод можно использовать с целью экономии памяти в тех ситуациях, когда один обработчик собы- тий может обрабатывать события от многих несвязанных друг с другом потоков ввода/вывода, не требующих сохранения состояния для каждого дескриптора. Пример клиентского демона регистрации в примере из раз- дела 6.2 иллюстрирует использование варианта register_handler () с тремя параметрами. • (const ACE_Sig_Set &sigset, ACE_Event_Handler *new_sh, ACE_Sig_Action *new_disp) — в данной версии задан новый обра- ботчик событий для набора сигналов POSIX. При появлении любого сиг- нала из sigset, реактор будет вызывать hook-метод handle_sig- na 1.() соответствующего обработчика событий. В отличие от других об- ратных вызовов реактора, handle_signal () вызывается в контексте сигнала. Поэтому его действия ограничены подмножеством доступных системных вызовов. Разработчикам советуем обратиться за деталями к документации по используемой ими платформе ОС. Методы ACE_Reactor: : remove_handler () можно использовать для удаления обработчиков событий из реактора, так чтобы их регистрация на об- работку одного или нескольких типов событий ввода/вывода или сигналов была прекращена. Существуют варианты с явной спецификацией дескриптора и без нее (точно также как и первые два варианта метода register_hand- ler (), описанные выше). Один вариант принимает ACE_Handle_Set, чтобы удалить сразу несколько дескрипторов; другой принимает ACE_Sig_Set, что- бы отменить обработку сигналов реактором. Для удаления обработчиков собы- тий, связанных с таймерами, нужно использовать метод ACE_Reac- tor::cancel_timer(). Когда приложение вызывает один из методов ACE_Reactor:: remo- ve_handler () для удаления событий ввода/вывода, оно может передать би- товую маску, составленную из констант перечисления, приведенных в таблице типов событий. Эта битовая маска указывает, какие типы событий ввода/выво- да больше не представляют интереса. Затем, чтобы уведомить об удалении, вы- зывается метод обработчика событий handle_close (). После того как hand- le_close () завершает работу, и данный обработчик событий больше не явля- ется зарегистрированным на обработку событий ввода/вывода, ACE_Reactor удаляет данный обработчик событий из своих внутренних структур данных, связанных с демультиплексированием событий ввода/вывода. Приложение может запретить обратный вызов handle_close (), доба- вив флаг ACE_Event_Handler: : DONT_CALL к параметру маски remo- ve_handler (). Этот флаг информирует реактор о том, что не следует осуще- ствлять диспетчеризацию метода handle_close () при удалении обработчи- ка событий, что иллюстрирует метод Service_Reporter: : f ini (). Чтобы
104 Глава'З гарантировать, что реактор не будет бесконечно рекурсивно вызывать hand- le_close (), флаг DONT_CALL всегда должен передаваться remove_hand- ler (), если он вызывается из самого hook-метода handle_close (). По умолчанию, hook-метод handle_close () не вызывается при удале- нии таймеров с помощью метода cancel_timer (). Тем не менее, последний, факультативный, аргумент может добавляться к запросу, которым вызывается handle_close (). Метод handle_close () не вызывается при удалении об- работчика событий, связанных с обработкой сигналов. Метод suspend_handler () можно использовать для временного удале- ния обработчика или множества обработчиков из сферы действий реактора, связанных с демультиплексированием событий дескрипторов. Метод resu- me_handler() возобновляет действия suspend_handler () таким образом, что данный дескриптор(ы) включается во множество дескрипторов, для кото- рых данный реактор осуществляет демультиплексирование событий. Так как suspend_handler () и resume_handler () действуют только на диспетче- ризацию на основе дескрипторов ввода/вывода, они не оказывают влияния на таймеры, обработку сигналов или на уведомления. Метод mask_ops () выполняет операции, которые получают, устанавли- вают, добавляют или очищают тип(ы) событий, связанный с маской диспетче- ризации обработчика событий. Метод mask_ops () предполагает, что обра- ботчик событий уже присутствует и не пытается регистрировать или удалять его. Поэтому его применение более эффективно, чем register_handler () и remove_handler (). Методы schedule_wakeup () и cancel_wakeup () представляют собой «синтаксическое украшение» («syntactic sugar») для обыч- ных операций, включающих mask_ops (). Тем не менее, они способствуют предотвращению неочевидных ошибок, таких как замена маски вместо плани- ровавшегося добавления битов. Например, следующие вызовы mask_ops () разрешают и запрещают ACE_Event_Handler: : WRITE_MASK: ACE_Reactor::instance ()->mask_ops (handler, ACE_Event_Handler::WRITE_MASK, ACE_Reactor::ADD_MASK); H ... ACE_Reactor::instance ()->mask_ops (handler, ACE_Event_Handler::WRITE_MASK, ACE_Reactor::CLR_MASK); Данные вызовы могут быть заменены На следующие, более лаконичные и информативные, вызовы методов: ACE_Reactor::instance ()->schedule_wakeup (handler, ACE_Event_Handler::WRITE_MASK); // ... ACE_Reactor::instance ()->cancel_wakeup (handler, ACE_Event_Handler::WRITE_MASK);
АСЕ-каркас Reactor 105 3. Методы управления циклами событий. Инверсия управления является ос- новной возможностью предлагаемой ACE-каркасом Reactor. Также как другие каркасы, такие как X Windows Toolkit или Microsoft Foundation Clas- ses (MFC), ACE_Reactor реализует цикл событий, который управляет дис- петчеризацией обработчиков событий приложения. Приложение, после регистрации обработчиков событий, может управлять связанным с ними циклом обработки событий с помощью методов, представленных в сле- дующей таблице: Метод Описание handle__events () Ждет наступления события и затем осуществ- ляет диспетчеризацию связанного с этим со- бытием обработчика(ов) событий. Параметр тайм-аута может ограничивать время ожида- ния события. run_reactor_event_loop() Многократно вызывает метод handle events () до наступления одного из событий: (1) возника- ет ошибка, (2) reactor_event_loop_done () возвращает 1, (3) истекает факультативный тайм-аут. end_reactor_event_loop() Указывает реактору, что ему следует завер- шить цикл обработки событий. reactor_event_loop_done() Возвращает 1, если посредством вызова end reactor_event_loop () ЦИКЛ обработки событий реактора завершен. Метод handle_events () собирает дескрипторы всех зарегистрирован- ных обработчиков событий, передает их демультиплексору событий соответст- вующего реактора и приостанавливает, на заданный приложением интервал времени, свою работу до наступления некоторого события, такого как дейст- вия, связанные с вводом/выводом или истечение установленного таймером срока. Когда наступает событие, данный метод осуществляет диспетчеризацию соответствующих предварительно зарегистрированных обработчиков собы- тий, вызывая их метод(ы) handle_* (), определенный данным приложением с целью обработки данного события(й). Если наступает сразу несколько собы- тий, то для всех этих событий осуществляется диспетчеризация до того, как данный метод завершит свою работу. Возвращаемое значение указывает на ко- личество обработанных событий: 0, если событий до истечения заданного вы- зывающей стороной тайм-аута не наступило, или -1, если произошла ошибка. Метод run_reactor_event_loop () — это простой интерфейс методов handle_events (), непрерывно выполняющий цикл обработки событий, вызывая handle_events () до наступления одного из следующих событий: • Возникает ошибка. • Истекает время, заданное в факультативном параметре ACE_Time_Va- lue.
106 Глава 3 • Вызывается метод end_reactor_event_loop () — возможно из од- ного из обратных вызовов, связанных с обработкой событий — для за- вершения цикла обработки событий. Приложения, которым не нужна специализированная прикладная обра- ботка событий, часто используют run_reactor_event_loop () и end_re- actor_event_loop () для организации циклов обработки событий, так как эти методы автоматически обнаруживают и обрабатывают ошибки. Многие сетевые приложения выполняют цикл обработки событий реакто- ра в одном потоке управления. В разделах 4.3 и 4.4 описывается, каким образом классы ACE_TP_Reactor и ACE_WFMO_Reactor позволяют нескольким по- токам параллельно вызывать свои методы, связанные с циклом обработки со- бытий. 4. Методы управления таймерами. По умолчанию, ACE_Reactor использу- ет механизм очередей таймеров, ACE_Timer_Heap, описанный в разде- ле 3.4, для планирования и диспетчеризации обработчиков событий в соот- ветствии со сроками истечения их тайм-аутов. Методы управления тайме- рами, предлагаемые классом ACE_Reactor, включают: Метод Описание schedule_timer() Регистрирует обработчик события, который будет выполняться после истечения заданного пользователем интервала времени. cancel_timer() Отменяет установку одного или нескольких таймеров, которые предварительно были зарегистрированы. ACE_Reactor систематизирует правильное использование функций АСЕ, связанных с очередями таймеров, в контексте обработки разнотипных событий, включая ввод/вывод и таймеры. Фактически, большинство пользователей АСЕ взаимодействуют с очередями таймеров, только посредством ACE_Reactor, ко- торый интегрирует функциональность очередей таймеров, в каркас Reactor следующим образом: •Метод schedule_timer () позволяет пользователям устанавливать таймеры, используя относительное время, с которым, как правило, про- ще работать, чем с абсолютным временем, которое используют АСЕ-оче- реди таймеров. Данный метод использует механизм get time of day () очередей таймеров для автоматической настройки значений времени, за- даваемых пользователем, на тот метод представления времени, который используется данной очередью таймеров. • Метод handle_events () запрашивает очередь таймеров, чтобы опре- делить у какого из таймеров раньше истекает срок. Затем он использует это значение, чтобы ограничить количество времени, в течение которого демультиплексор событий ждет наступления событий ввода/вывода.
АСЕ-каркас Reactor 107 •Метод handle_events () вызывает методы очереди таймеров для сброса (expire) таймеров при истечении тайм-аута демультиплексора со- бытий, определяемого ближайшим из сроков установки таймеров. Все вместе, эти действия обеспечивают эффективную интеграцию тайме- ров в АСЕ-каркас Reactor, таким способом, который облегчает работу с ними и позволяет приложениям повторно использовать возможности АСЕ-очередей таймеров, не взаимодействуя непосредственно с методами самих очередей. В блоке 16 приведены соображения о минимизации выделения динамической памяти в ACE-очередях таймеров. 5. Методы уведомления. Реактор имеет механизм уведомлений, который приложения могут использовать для включения событий и обработчиков событий в его механизм Диспетчеризации. Следующие методы регулируют различные аспекты механизма уведомлений реактора: Метод Описание notify () Включает событие (и, факультативно, обработчик события) в детектор событий реактора, что при- водит к обработке этого события на следующем цикле обработки событий данного реактора. max__notify__iterations () Устанавливает максимальное количество обработчиков, диспетчеризацию которых будет осуществлять реактор, используя свой механизм уведомлений. purge_pending_notifi- cations() Удаляет заданный обработчик событий или все обработчики событий, связанные с механизмом уведомлений данного реактора. Метод ACE_Reactor: :notify() можно использовать для нескольких целей: • Механизм уведомлений реактора позволяет другим потокам запускать собственный поток реактора, функция-демультиплексор которого бло- кирована в состоянии ожидания событий ввода/вывода. Например, так как mask_ops (), schedule_wakeup () и cancel_wakeup () не тре- буют от реактора возобновить просмотр его набора дескрипторов и обра- ботчиков, любые новые маски будут учтены только при следующем вы- зове метода реактора handle_events (). Если в ближайшее время дру- гих действий не предвидится или если маски ожидания должны быть пересмотрены немедленно, чтобы заставить реактор просмотреть его на- бор дескрипторов и обработчиков, можно вызвать ACE_Reactor: : no- tify О . • Методу notify () может быть передан указатель на обработчик собы- тий и одно из значений ACE_Reactor_Mask, например, READ_MASK, WRITE_MASK или EXCEPT_MASK. Эти параметры приводят к тому, что реактор осуществляет диспетчеризацию соответствующего hook-метода
108 Глава 3 Блок 16: Минимизация выделения памяти в АСЕ-очередях таймеров 1 ,. - ~ • - Базовый класс ACE_Timer_Queue, изображенный «на рис, 3,5, не предлагает методов для установки размера очереди таймеров. Это не случайно, посколь- ку отсутствует единое понимание того, что такое размер («size») на этом уровне иерархии классов. Каждый производный класс очередей таймеров имеет свое представление о размере («size»), которое определяется его базовыми структурами данных. Поэтому производные классы очередей таймеров пред- лагают в своих конструкторах параметры, связанные с размером. Эти пара- метры являются рекомендациями, указывающими реализациям очередей таймеров, какого объема им следует создавать их внутренние структуры дан- ных первоначально. Хотя очереди таймеров могут автоматически изменять свой размер, чтобы настраиваться на произвольно большое количество тай- меров, изменение размера связано С динамическим выделением памяти, что может привести к издержкам, недопустимым для некоторых приложений. Дополнительно к изменению размера очереди, классы ACE_Timer_Heap и ACE_Timer_wheei предлагают возможность выделять память под элементы очереди таймеров, заранее, так чтобы данной очереди не требовалось в дальнейшем выделять память динамически. Чтобы ACE_Reactor мог исполь- зовать специализированную очередь для своих операций с таймерами, нуж- но сделать следующее: 1. Создать реализацию класса ACE-очереди таймеров, задав нужный раз- мер очереди и аргумент, связанный с предварительным выделением памя- ти, если он применим. 2. Создать экземпляр объекта реализации ACE-реактора с очередью тай- меров, соответствующей шагу 1. 3. Создать экземпляр нового объекта ACE_Reactor с экземпляром объекта реализации, соответствующим шагу 2. обработчика событий, не требуя при этом ассоциировать обработчик с дескрипторами ввода/вывода или событиями таймера. Эта возмож- ность позволяет реактору настраиваться на расширяемое количество об- работчиков событий, так как не существует требования, в соответствии с которым обработчик, указатель на который передан ACE_Reac- tor: : notify (), должен быть всегда зарегистрирован или даже будет когда-нибудь зарегистрирован у данного реактора. По умолчанию, любой реактор осуществляет диспетчеризацию всех обра- ботчиков событий, зарегистрированных у его механизма уведомлений, при об- наружении события, о котором следует уведомлять. Метод max_noti- fy_iterations () может изменять количество обработчиков событий, для которых выполняется диспетчеризация. Установка небольшого значения по- вышает доступность и предохраняет от голодания (starvation), хотя в какой-то степени увеличивает издержки диспетчеризации. Уведомления помещаются во внутреннюю очередь реактора, и ждут когда реактор сможет осуществить их диспетчеризацию (в блоке 17 описывается, как избежать тупиковых ситуаций в очереди). Если обработчик событий, связан- ный с уведомлением, отключается до того, как будет осуществлена диспетчери- зация данного уведомления, может произойти аварийный отказ в том случае,
АСЕ-каркас Reactor 109 Блок 17: Как избежать тупиков в механизме уведомлений реактора По умолчанию, механизм уведомлений реактора реализуется с буфером ко- нечного объема, а метод noti f у () использует при отправке уведомления бло- кируемый вызов для постановки уведомлений в очередь. Поэтому тупиковая ситуация может возникнуть, если буфер полон, а метод handie_* () обработ- чика событий вызывает метод notify (). Существует несколько способов из- бежать тупиков такого типа: • Следует задавать для метода notify () тайм-аут. Такое решение перекла- дывает ответственность за обработку ситуации, связанной с переполнени- ем буфера, на тот поток, который вызывает notify (). • Спроектировать приложение таким образом, чтобы оно не генерировало вызовы notify () чаще, чем реактор может обработать. Это, конечно, са- мое лучшее решение, хотя оно и требует тщательного анализа поведения программы. В блоке 22 описывается способ, с помощью которого можно избежать тупико- вых ситуаций с ACE_Select_Reactor. если реактор попытается осуществить диспетчеризацию для указателя на от- ключенный обработчик событий. Поэтому может возникнуть необходимость использовать метод purge_pending_notif ications () для удаления из очереди всех уведомлений, связанных с некоторым обработчиком событий. АСЕ-каркас Reactor оказывает пользователям помощь, вызывая метод pur- ge_pending_notif ications () из деструктора ACE_Event_Handler. Та- кое поведение наследуется всеми прикладными обработчиками событий, по- скольку данный деструктор объявлен как virtual. Уведомления остаются в очереди до тех пор, пока не будет осуществлена их диспетчеризация, или пока они не будут удалены обработчиком событий, что гарантирует, что уведомление будет обработано, даже если данный реактор за- нят обработкой других событий в момент вызова notify (). Если, тем не ме- нее, данный реактор прекращает обнаружение и диспетчеризацию событий (например, при завершении работы run_react.or_event_loop ()), все на- ходящиеся в очереди уведомления остаются там, но для них не будет осуществ- ляться диспетчеризация до тех пор, пока данный реактор снова не возобновит обнаружение и диспетчеризацию событий. Поэтому уведомления будут поте- ряны, если данный реактор закрывается или удаляется до того, как осуществит диспетчеризацию находящихся в очереди уведомлений. Приложения отвечают за принятие решения о том, когда следует прекратить обработку событий, и по- сле Принятия такого решения, ни для каких событий, ни от каких источников обнаружение, демультиплексирование и диспетчеризация не будут осуществ- ляться. 6. Вспомогательные методы. Класс ACE_Reactor определяет также следую- щие, вспомогательные методы:
по Глава 3 Метод Описание I instance () Статический метод, возвращающий указатель на синглтон ACE_Reactor, который создается и управляется с помощью паттерна Singleton (GoF) совместно р паттерном Double-Checked Locking Optimization (POSA2). owner() Назначает поток для выполнения цикла обработки событий некоторого реактора. ACE_Reactor можно использовать двумя способами: • Как синглтон [GoF] с помощью метода instance (), приведенного в таблице выше. • Путем создания одного или нескольких экземпляров. Эта возможность может использоваться для поддержки нескольких реакторов в одном процессе. Каждый реактор часто ассоциируется с потоком, имеющим оп- ределенный приоритет [Sch98]. Некоторые реализации реактора, такие как ACE_Select_Reactor, рас- смотренная в разделе 4.2, позволяют только одному потоку выполнять свой ме- тод handle_events (). Метод owner () изменяет поток-собственник реакто- ра, чтобы дать возможность этому потоку выполнять цикл обработки событий данного реактора. В блоке 18 описывается, как избежать тупиковых ситуаций при использовании реактора в многопоточных приложениях. На рис. 3.8 представлена диаграмма последовательности взаимодействия между классами в ACE-каркасе Reactor. Дополнительную информацию о струк- туре ACE-каркаса Reactor можно найти в разделе Implementation паттерна Reac- tor в главе 3 [POSA2]. Пример Прежде чем мы продемонстрируем остальные части взаимно-согласован- ного (reactive) сетевого сервера регистрации, давайте быстро восстановим в па- мяти внешнее поведение серверов и клиентов регистрации, разработанных в [C++NPvl]. Сервер регистрации прослушивает TCP-порт, номер которого за- дается в командной строке, по умолчанию порт с номером, заданным как ace_logger в файле сетевых служб ОС. Например, следующая строка может присутствовать в UNIX в файле /etc/services: ace_logger 9700/tcp # Connection-oriented Logging Service Клиентские приложения могут факультативно задавать TCP-порт и имя хоста или IP-адрес, по которым клиентское приложение и сервер регистрации должны обмениваться регистрационными записями. Тем не менее, если эта ин- формация не задана, то номер порта берется из служебной базы данных, и счи- тается, что имя хоста ACE_DEFAULT_SERVER_HOST, которое на большинстве платформ ОС определяется как «localhost». Версия сервера регистрации, представленная ниже, предлагает те же воз- можности, что и версия Reactive_Logging_Server_Ex из главы 7
АСЕ-каркас Reactor 111 Блок 18: Как избежать тупиков реактора в многопоточных приложениях Хотя реакторы чаще используются в однопоточных приложениях, они могут ис- пользоваться и в многопоточных приложениях. В этом контексте, важно избе- жать тупиковых ситуаций, в которые могут попадать потоки, совместно исполь- зующие ACE_Reactor. Например, ACE_Reactor занимает рекурсивный мью- текс при диспетчеризации обратного вызова обработчика событий. Если метод обратного вызова, для которого осуществляется диспетчеризация, пря- мо или косвенно снова вызывает реактор в одном и том же потоке управления, метод acquire () рекурсивного мьютекса автоматически распознает эту си- туацию и просто инкрементирует свой счетчик вложенных блокировок, вместо того, чтобы блокировать такой поток. Но даже в случае рекурсивных мьютексов остается возможность попасть в ту- пиковую ситуацию в следующих обстоятельствах: • Первый метод обратного вызова вызывает второй метод, который блокирует- ся при попытке занять мьютекс, который занят вторым потоком, выполняю- щим тот же метод. • Второй поток прямо или косвенно вызывает тот же реактор. В этом случае можно попасть в тупиковую ситуацию, так как рекурсивный мью- текс этого реактора не знает, что второй поток осуществляет вызов от лица первого потока, для которого с самого начала осуществлялась диспетчери- зация метода обратного вызова. Один из способов избежать тупиков в ACE_Reactor в многопоточных приложе- ниях —не прибегать к блокируемым вызовам других методов из обратных вызо- вов, если указанные методы выполняются параллельно конкурирующими по- токами, которые прямо или косвенно опять вызывают тот же реактор. Возмож- но, придется использовать ACE_Message_Queue, описанный в разделе 6.2 для асинхронного обмена информацией, если один из методов обратного вызова handle_* () должен обмениваться информацией с другим потоком, который вызывает тот же реактор. [C++NPvl]. Оба сервера работают в одном потоке управления процесса, обраба- тывая регистрационные записи от множества клиентов, отвечая на их запросы. Основное отличие заключается в том, что версия, описываемая в данном разде- ле, повторно использует возможности обнаружения, демультиплексирования, и диспетчеризации, существующие в ACE-каркасе Reactor. Такой рефакторинг устраняет следующий независимый от приложения код из исходной реализа- ции Reactive_Logging_Server_Ex: Преобразование дескриптор-объект. Две структуры данных в Reacti- ve_Logging_Server_Ex, выполняют следующие преобразования: 1. ACE_Handle_Set содержал все дескрипторы сокетов клиентов, с которы- ми установлено соединение, и дескриптор ACE_SOCK_Acceptor, чтобы принимать новые клиентские соединения. 2. ACE_Hash_Map_Manager преобразовывал дескрипторы сокетов в слабо связанные (loosely associated) объекты ACE_FILE_IO, которые сохраняют регистрационные записи в соответствующем выходном файле. Так как теперь код, который осуществляет преобразование дескриптор- объект, предоставляет и поддерживает ACE-каркас Reactor, приложение, полу-
112 Глава 3 чающееся в результате этого, становится меньше, быстрее и гораздо лучше по- вторно использует программные элементы, имеющиеся в АСЕ. Обнаружение, демультиплексирование и диспетчеризация событий. Для обнаружения событий, связанных с установлением соединений, и событий, от- носящихся к передаче данных, сервер Reactive_Logging_Server_Ex ис- пользовал метод демультиплексирования синхронных событий АСЕ:: se- lect (). Такое решение, однако, имело следующие недостатки: 1. Оно работало толькозасчеттого,чтоОСпредоставляламетод sdlect (). 2. Оно работало неплохо только тогда, когда метод select () в ОС был реа- лизован эффективно. 3. Код, который вызывал АСЕ: : select () и обрабатывал результирующий набор дескрипторов было трудно использовать повторно другим приложе- ниям. Новый сервер регистрации повторно использует способность АСЕ-каркаса Reactor переносимо и эффективно обнаруживать, демультиплексировать и диспетчеризировать события, связанные с вводом/выводом и таймером. Дан- ный каркас позволяет также приложению интегрировать обработку сигналов, если возникает такая необходимость. После того как независимый от приложения код, описанный выше, удален, возникает важная проблема сопровождения исходного кода. Хотя данный код работал корректно, Reactive_Logging_Server_Ex, Logging_Handler, преобразование дескриптор-АСЕ_Е1ЬЕ_10 и объекты ACE_FILE_IO были тесно связаны, косвенно. Поэтому изменение механизма обработки србытий требует также изменения всего кода обработки событий, зависящего от прило- жения, что является иллюстрацией негативных эффектов смешения кода, зави- сящего от приложения, и кода, независящего от приложения. Это приводит к дизайну, который трудно расширять и сопровождать, что значительно повы- шает затраты на последующее развитие такого сервера регистрации; В отличие от этого, класс Logging_Event_Handler показывает, что но- вый взаимно-согласованный (reactive) сервер разделяет обязанности более эф- фективно, объединяя ACE_FI LE_IO с Logging_Handler и регистрируя деск- риптор сокета у реактора. Данный пример показывает, что разработчики могут использовать следующие шаги с целью интеграции приложений с АСЕ-карка- сом Reactor: 1. Создать обработчики событий путем наследования от базового класса ACE_Event_Handler и подменить его виртуальные методы обработки событий различных типов. 2. Зарегистрировать обработчики событий у экземпляра ACE_Reactor. 3. Выполнять цикл обработки событий, связанный с демультиплексировани- ем и диспетчеризацией событий обработчикам событий. На рис. 3.7 изображена архитектура взаимно-согласованного сервера реги- страции, который создан на базе предшествующих реализаций из [C++NPvl]. Данная архитектура улучшает повторное использование и расширяемость за счет разделения следующих аспектов сервера регистрации:
АСЕ-каркас Reactor 113 Рис. 3.7 Архитектура сервера регистрации ACE_Reactor • Классы ACE-каркаса Reactor. Эти классы инкапсулируют низкоуровне- вые механизмы ОС, которые выполняют обнаружение, демультиплекси- рование и диспетчеризацию событий hook-методам обработчиков собы- тий. • Классы интерфейсных фасадов АСЕ Socket. Классы ACE_SOCK_Accep- tor и ACE_SOCK_Stream, представленные в главе 3 [C++NPvl] исполь- зованы в данной версии сервера регистрации. Также как в предыдущей версии, ACE_SOCK_Acceptor принимает сетевые соединения от уда- ленных клиентов и инициализирует объекты ACE_SOCK_Stream. Ини- циализированный объект ACE_SOCK_Stream затем обрабатывает дан- ные, которыми он обмениваются с клиентом, установившим соединение. • Классы обработчиков событий регистрации. Эти классы реализуют воз- можности, специфичные для сетевой службы регистрации. Как показано в примере раздела 3.4, фабрика Logging_Acceptor_Ex использует ACE_SOCK_Acceptor, чтобы принимать соединения от клиентов. Ана- логично, Logging_Event_Handler_Ex использует ACE_SOCK_Stre- am, чтрбы получать регистрационные записи от клиентов, с которыми установлены соединения. Оба класса Logging_* являются производны- ми от ACE_Event_Handler, поэтому их методы handle_input () мо- гут принимать обратные вызовы от ACE_Reactor. Начнем реализацию с заголовочного файла Reactor_Logging_Ser- ver. h, который включает несколько заголовочных файлов, предоставляющих различные возможности, которые мы будем использовать в нашем сервере ре- гистрации на базе ACE_Reactor.
114 Глава 3 ♦include "ace/ACE.h" ♦include "ace/Reactor.h" Затем мы определяем класс Reactor_Logging_Server, который форми- рует основу для многих следующих примеров сервера регистрации в этой книге: template <class AGCEPTOR> class Reactor_Logging_Server : public ACCEPTOR { public: Reactor_Logging_Server (int argc, char *argv[], ACE_Reactor *); ); Этот класс наследует от шаблонного параметра ACCEPTOR. Чтобы изме- нить некоторые аспекты установления соединения Reactor_Logging_Ser- ver и действий, связанных с регистрацией, в следующих примерах его экземп- ляры будут создаваться с различными типами акцепторов (acceptor), таких как Logging_Acceptor_Ex, Logging_Acceptor_WFMO, TP_Logging_Ac- ceptor и TPC_Logging_Acceptor. Reactor_Logging_Server включает также указатель на ACE_Reactor, который он использует для обнаружения, демультиплексирования и диспетчеризации событий, связанных с вводом/вы- водом и временем, их обработчикам. Reactor_Logging_Server отличается от класса Logging_Server, опреде- ленного в главе 4 [C++NPvi], так как Reactor_Logging_Server использует метод ACE_Reactor: :handle_events() для обработки событий посредством обрат- ных вызовов экземпляров Logging_Acceptor и Logging_Event_Handler. Таким образом, hook-методы handle_connections (), handle_data () и wait_f or_multiple_events (), которые использовались во взаимно-со- гласованных серверах в [C++NPvl], больше не нужны. Реализация шаблона Reactor_Logging_Server находится в Reac- tor_Logging_Server_T. срр. Его конструкторы выполняют шаги, необхо- димые для инициализации взаимно-согласованного сервера регистрации: 1 template <class ACCEPTOR> 2 Reactor_Logging_Server<ACCEPTOR>::Reactor_Logging_Server 3 (int argc, char *argv[}, ACE_Reactor *reactor) 4 : ACCEPTOR .(reactor) { 5 u_short logger_port = argc > 1 ? atoi (argv[l]) : 0; 6 ACE_TYPENAME ACCEPTOR::PEER_ADDR server_addr; 7 int result; 8 9 if (logger_port != 0) 10 result = server_addr. set (logger_port, INADDR_ANY) ; 11 else 12 result = server_addr.set ("ace_logger", INADDR_ANY); 13 if (result != -1) 14 result - ACCEPTOR::open (server_addr); 15 if (result == -1) reactor->end_reactor_event_loop (); 16 }
АСЕ-каркас Reactor 115 Строка 5 Устанавливаем номер порта, который будем использовать для прослушивания запросов клиентов на соединение. Строка 6 Используем trait-класс PEER_ADDR, который является частью шаблонного параметра ACCEPTOR, для определения типа server_addr. Ис- пользование характеристик (traits) упрощает массовую замену классов IPC и связанных с ними адресных классов. В блоке 19 поясняется значение макроса ACE_TYRENAME. Строки 9-12 Задаем адрес локального сервера server_addr. Строка 14 Передаем server_addr методу ACCEPTOR: : open () с целью инициализации конечной точки соединения пассивного режима и регистрации объекта this у реактора для событий ACCEPT. Строка 15 Если возникла ошибка, даем указание реактору завершить цикл обработки событий, чтобы не «подвесить» функцию main(). Блок 19: Ключевое слово C++ typename и макрос ACEJYPENAME Ключевое слово C++ typename указывает компилятору, что идентификатор (та- кой как peer_addr) является типом. Данное ключевое слово необходимо, ко- гда спецификатор является аргументом типа шаблона (например, acceptor), поскольку у компилятора нет конкретного класса для проверки, пока не соз- даны реализации шаблонов, что происходит значительно позже, в процессе сборки. Так как typename является относительно новым изобретением в C++, АСЕ предлагает переносимый способ его задания. Макрос ace_typename обеспечивает использование ключевого слова typehame на тех компиляторах, которые его поддерживают, и не делает этого на остальных компиляторах. В завершение мы приводим функцию main () сервера регистрации, кото' рая находится в Reactor_Logging_Server. срр: 1 typedef Reactor_Logging_Server<Logging_Acceptor_Ex> 2 Server_Logging_Daemon; 3 4int main (int argc, char *argv[]) { 5 ACE_Reactor reactor; 6 Server_Logging_Daemon ‘server = 0; 7 ACE_NEW_RETURM (server, 8 Server_Logging_Daemon (argc, argv, &reactor), 9 1) ; 10 11 if (reactor.run_reactor_event_loop () == -1) 12 ACE_ERROR_RETURN ( (LM_ERROR, "%p\n", 13 "run_reactor_event_loop()"), 1); 14 return 0; 15 }
116 Строки 1-2 Создаем реализацию шаблона Reactor_Logging_Server с классом Logging_Acceptor_Ex, чтобы создать Server_Logging_Da- emon typedef. Строки 6-9 Динамически размещаем объект Server_Logging_Daemon. Строки 11-13 Используем локальный экземпляр ACE_Reactor, чтобы управлять всей последующей обработкой событий, связанных с установлением соединений и передачей данных, до появления ошибки. Макрос ACE_ER- ROR_RETURN и другие ACE-макросы отладки, описаны.в блоке 10 [C++NPvl]. Строка 15 Когда в завершение main () выполняется деструктор локально- го reactor, он вызывает метод Logging_Acceptor: :handle_close () для удаления динамически размещенного объекта Server_Logging_Dae- mon. Деструктор вызывает также Logging_Event_Handler_Ex: :hand- le_close () для каждого зарегистрированного обработчика событий, чтобы корректно удалить обработчики и завершить работу сервера. На рис. 3.8 показана последовательность действий в вышеприведенном примере. Так как обнаружением, демультиплексированием и диспетчеризаци- ей всех событий занимается АСЕ-каркас Reactor, данная реализация взаим- но-согласованного сервера гораздо короче, чем ее эквиваленты из [C++NPvl]. На самом деле работа, связанная с переходом от взаимно-согласованных серве- ров из [C++NPvl] к данному серверу заключается, в основном, в удалении кода, который больше не нужен, например, набора дескрипторов и управления ими, преобразования дескриптор-АСЕ_Е1ЬЕ_10, демультиплексирования син- хронных событий и диспетчеризации. Остальные определяемые приложением функции выделены в классы, унаследованные от ACE-каркаса Reactor. 3. 6 Резюме В данной главе было показано, каким образом АСЕ-каркас Reactor может упростить разработку компактных, корректных, переносимых и эффективных событийно-управляемых сетевых приложений за счет инкапсуляции механиз- мов ОС, связанных с демультиплексированием событий, в объектно-ориенти- рованные интерфейсы, написанные на C++. Было также показано, каким обра- зом АСЕ-каркас Reactor улучшает повторное использование, переносимость и дает возможность расширять обработчики событий за счет разделения (1) ме- ханизмов обнаружения, демультиплексирования и диспетчеризации событий и (2) определяемых приложением стратегий (policies) обработки событий. Так как низкоуровневое обнаружение, демультиплексирование и диспетче- ризацию событий выполняют повторно используемые классы АСЕ-каркаса Reactor, то остается написать относительно небольшой объем определяемого приложением кода. Например, служба регистрации в разделах 3.3 и 3.4 сосредо- точена на действиях, связанных с обработкой, определяемой приложением, а именно, с получением клиентских регистрационных записей. Любые приложе- ния, повторно использующие класс ACE_Reactor, рассмотренный в разделе 3.5 могут, соответственно, использовать экспертные знания и опыт разработчиков промежуточного слоя, а также будущие усовершенствования и оптимизацию.
АСЕ-каркас Reactor 117 ACE Reactor boggingJteceptQrJx । register handler() LQgging^EventJiand.ler.,EK run_reactor_event loop() T -----►A handle input() create open() register handler() handle input() recv() > t destroyш handle_close() । q'k--------““.tu x handle close() Рис. 3.8 UML диаграмма последовательности для взаимно-согласованного сервера регист- рации АСЕ-каркас Reactor широко использует динамическое связывание. Впечат- ляющие достижения в ясности, расширяемости и модульности, которые он обеспечивает, сопровождаются некоторым снижением эффективности, связан- ным с косвенной диспетчеризацией с использованием виртуальных таблиц [HLS97]. АСЕ-каркас Reactor используется, в основном, при разработке сетевых приложений, в которых основными источниками издержек являются кэширо- вание, время ожидания, аппаратные интерфейсы сеть/хост, форматирование уровня представления, динамическое выделение памяти и копирование, син- хронизация и управление параллелизмом. Дополнительные издержки, вноси- мые динамическим связыванием, чаще относительно невелики [Кое92]. Кроме того, хорошие компиляторы C++ могут полностью исключать издержки, свя- занные с виртуальными методами, благодаря использованию оптимизации на основе «корректирующих переходников» («adjustor thunk»)[Lip96].. Одним из потенциально очень ценных свойств ACE-каркаса Reactor явля- ется его способность улучшать расширяемость и снаружи, и изнутри его public-интерфейса. Реализации Reactor являются хорошим примером того, как применение паттернов позволяет создавать множество классов, которые и ис- пользуют преимущество уникальных возможностей платформ, и сохраняют способность сетевых приложений работать на различных вычислительных платформах без внесения изменений. В следующей главе рассматриваются ме- тоды, используемые АСЕ для достижения этой гибкости.

Глава 4 Реализации АСЕ Reactor Краткое содержание В данной главе описывается структура и применение нескольких реализа- ций интерфейса ACE_Reactor, рассмотренндго в главе 3. Эти реализации под- держивают различные механизмы операционных систем, связанные с демуль- типлексированием синхронных событий, включая select (), WaitForMul- tipleOb j ects (), XtAppMainLoop () и /dev/poll. Объясняются мотивы создания наиболее распространенных реализаций реакторов, имеющихся в ACE-каркасе Reactor, а также те возможности, которыми они располагают. Де- монстрируется также применение трех разных реализаций ACE_React or с це- лью совершенствования сервера регистрации, используемого в этой книге в ка- честве примера. Кроме того, демонстрируется несколько моделей параллелиз- ма, поддерживаемых этими реализациями ACE_Reactor. 4.1 Обзор АСЕ-каркас Reactor, рассмотренный в главе 3, представляет собой реаль- ный пример каркаса, созданного с целью поддержки расширяемости. Первона- чальная реализация ACE_Reactor базировалась исключительно на механизме демультиплексирования синхронных событий select (). По мере роста тре- бований, предъявляемых приложениями, и расширения поддерживаемых АСЕ платформ, внутренняя структура ACE-каркаса Reactor менялась таким обра- зом, чтобы поддерживать новые потребности приложений и новые возможно- сти платформ ОС. К счастью, интерфейс ACE_Reactor оставался относитель- но постоянным. Эта устойчивость важна, так как она позволяет одновременно: • обеспечить совместимость с приложениями, написанными для предыду- щих версий АСЕ; • каждому приложению использовать преимущество новых возможно- стей реактора, если возникает такая потребность.
120 Глава 4 Материал данной главы сосредоточен на наиболее распространенных реа- лизациях ACE-каркаса Reactor, которые перечислены в следующей таблице: Класс АСЕ Описание ACE_Select_Reactor Использует функцию демультиплексирования синхронных событий select () для обнаружения событий ввода/вывода и таймера; органично включает обработку сигналов POSIX. ACE_TP_Reactor Использует паперн Leader/Followers (POSA2) для расширения обработки событий ACE_Select_Reactor на пул ПОТОКОВ. ACE_WFMO_Reactor Использует функцию Windows демультиплексирования событий WaitForMultipleObjects () ДЛЯ обнаружения событий, связанных с сокетами ввода/вывода, тайм-аутами и синхронизацией в Windows. АСЕ предлагает также и другие, более специализированные, реализации ре- актора, которые перечислены в разделе 4.5. Основанием для создания всех этих реакторов, выросших из популярной взаимно-согласованной (reactive) модели проектирования сетевых приложений, является разнообразие требований, предъявляемых к реакторам, в сочетании со следующими обстоятельствами: • Растущая популярность и доступность многопоточных систем. • Добавление Windows к набору платформ, которые поддерживает АСЕ. • Высокопроизводительная аппаратура: ввод/вывод, центральные процес- соры, средства синхронной многопроцессорной обработки. • Желание объединить обработку событий с графическими средами, таки- ми как Windows, XII Windows, Trolltech AS's Qt, Fast Light Toolkit и TCL/Tk. Новейшие реализации реакторов начинались с порождения классов от ACE_Reactor и подмены методов для реализации новых возможностей. В 1997 году, в процессе разработки АСЕ версии 4.4, однако, этот подход был из- менен после того, как пользователь АСЕ Томас Джордан (Thomas Jordan) пред- ложил использовать паттерн Bridge [GoF] по следующим соображениям: • Интерфейс ACE_Reactor должен оставаться постоянным для поддер- жания обратной совместимости, а реализация должна быть гибкой и должна существовать возможность ее выбора во время выполнения. • Реализации реакторов должны использовать уже существующие воз- можности, создавая производные классы там, где это имеет смысл, и пол- ностью отказываясь от их создания там, где смысла нет. Например, ACE_TP_Reactor является производным от ACE_Select_Reactor, a ACE_WFMO_Reactor полностью от него отличается. • Расширяющийся набор реализаций реакторов указывает на необходи- мость большей гибкости, которая нужна, чтобы предоставить возмож-
Реализации АСЕ Reactor 121 ность дальнейших усовершенствований без изменения интерфейса ACE_Reactor. Применение паттерна Bridge в реализациях реакторов привело к структуре, изображенной на рис. 4.1, где представлены основные реализации реакторов, рассматриваемые в данной главе. Класс ACE_Reactor, который был первой реализацией, теперь играет роль Abstraction (абстракция) в паттерне Bridge. Класс ACE_Reactor_Impl играет роль Implementor (средство реализации), а что касается различных видимых пользователю реализаций реактора, то каж-. дая из них играет роль Concreteimplementor (конкретная реализация). Паттерн Bridge предоставляет значительную свободу при реализации и позволяет при- ложениям выбирать различные реакторы во время выполнения, в минималь- ной степени затрагивая существующий код. Рис. 4.1 Реализации интерфейса ACE_Reactor В остальной части данной главы приводится обоснование и описание воз- можностей наиболее распространенных реализаций интерфейса ACE_Reac- tor: ACE_Select_Reactor, ACE_TP_Reactor и ACE_WFMO_Reactor. Де- тально исследуются проектные решения, использованные в этих реализациях, чтобы проиллюстрировать тонкости разработки расширяемых и эффективных объектно-ориентированных каркасов. Кроме того, продемонстрировано как эти реализаций реакторов можно использовать для совершенствования дизай- на нашего сетевого сервера регистрации. 4.2 Класс ACE Select Reactor Обоснование Как отмечалось в главе 5 [C++NPvl], взаимно-согласованный (reactive) сер- вер реагирует на события от одного или нескольких источников. В идеальном варианте, несмотря на то, что события обрабатываются обычно одним пото- ком, быстрый отклик на события создает впечатление, что все запросы обраба- тываются одновременно. Сердцевиной каждого взаимно-согласованного сер- вера является демультиплексор синхронных событий. Этот демультиплексор-
122 Глава-4 ный механизм обнаруживает события, поступающие от нескольких источников, и реагирует на них, делая события доступными серверу синхрон- но, как часть его обычно выполняемой ветви. Функция select () является са- мым распространенным демультиплексором синхронных событий. Эта сис- темная функция ждет наступления заданных событий на множестве дескрип- торов ввода/вывода.1 Когда один или несколько дескрипторов ввода/вывода активизируется, или по истечении заданного промежутка времени, select () возвращает некоторое значение. Это значение указывает на количество актив- ных дескрипторов, или на то, что заданное вызывающей стороной время истек- ло до наступления события, или на то, что произошла ошибка. Вызывающая сторона может затем предпринять соответствующие действия. Дополнитель- ная информация О select () имеется в главе 6 [C++NPvl] и в [Ste98]. Хотя select () присутствует на большинстве платформ ОС, программиро- вание самой select () из API ОС, требует от разработчиков урегулирования множества низкоуровневых деталей, например: • Установки и очистки fd_sets. • Обнаружения событий и реагирования на сигналы прерываний. • Управление внутренними блокировками. • Демультиплексирование событий соответствующим обработчикам со- бытий. • Диспетчеризация функций, которые обрабатывают события, связанные с вводом/выводом, сигналами и таймером. В главе 7 [C++NPvl] рассматривались несколько классов интерфейсных фасадов, которые можно использовать для преодоления многих сложностей, связанных с этими низкоуровневыми деталями. Тем не менее, использование select () может быть полезным, в тех средах, где необходимо следующее: • Разрешать множеству потоков изменять набор дескрипторов ввода/вы- вода, используемый потоком select (). • Прерывать выполнение функции select () до наступления событий. • Полностью устранять издержки, связанные с многопоточностью, в тех ситуациях, когда многопоточность или не нужна, или когда платформа или конфигурация приложения ее не поддерживают. Для систематического решения указанных проблем, ACE-каркас Reactor определяет класс ACE_Select_Reactor, который обеспечивает все вышепе- речисленные возможности. Функциональные возможности класса ACE_Select_Reactor представляет собой реализацию интерфейса ACE_Reactor, которая использует функцию-демультиплексор синхронных событий select () для обнаружения событий ввода/вывода и событий тайме- ра. Класс ACE_Select_Reactor, кроме того, что он сохраняет все возможно- Версия select () из Windows работает только с дескрипторами сокетов.
Реализации ACE Reactor 123 сти интерфейса ACE_Reactor, дополнительно обеспечивает следующие воз- можности: * Поддерживает реентерабельные вызовы реактора, то есть приложения могут вызывать метод handle_events () из обработчиков событий, для которых в данный момент осуществляется диспетчеризация тем же самым реактором. • Допускает настройку на синхронный или несинхронный режимы рабо- ты, с альтернативами, связанными с безопасностью потоков и со сниже- нием издержек. • Поддерживает равноправие (fairness), осуществляя диспетчеризацию всех активных дескрипторов своего набора до нового вызова s е 1 е с t (). ACE_Select_Reactor является реализацией ACE_Reactor по умолча- нию на всех платформах за исключением Windows, в которой используется ACE_WFMO_Reactor по причинам, изложенным в блоке 25. Обзор реализации. ACE_Select_Reactor является производной от ACE_Reactor_Impl, как следует из рис. 4.1. Следовательно, она служит кон- кретной реализацией ACE_Reactor. Как видно из рис. 4.2, ACE_Select_Re- actor, на самом деле, представляет собой typedef шаблона ACE_Se- J,ect_Reactor_T (в разделе Параллелизм рассматриваются дополнительные аспекты данной реализации). Рис. 4.2 Внутреннее устройство каркаса ACE_Select_Reactor Класс ACE_Select_Reactor_Impl содержит данные и методы, незави- симые от аргумента шаблона ACE_Select_Reactor_T, который изолирует эти данные и методы от факторов, зависящих от аргумента шаблона, и препят- ствует их дублированию при каждом создании реализации шаблона. Блок 20 поясняет, как изменить количество обработчиков событий, управляемых эк- земпляром ACE_Select_Reactor.
124 Глава 4 Механизм уведомлений ACE-каркаса Reactor, позволяет реактору управ- лять обработчиками событий, количество которых может изменяться. Кроме того, механизм уведомлений может использоваться для открепления реактора от его цикла событий. По умолчанию, ACE_Select_Reac tor реализует меха- низм уведомлений с помощью ACE_Pipe, двунаправленного механизма IPC, семантика которого описана в блоке 21. Два конца канала (pipe) исполняют сле- дующие роли: • Роль записи (writer role). Метод ACE_Select_Reactor: : notify () предоставляет конец-записи (writer end) данного канала (pipe), потокам приложения, которые используют метод notify () для передачи указа- телей на обработчики событий реактору ACE_Select_Reactor по- средством его канала уведомлений (notification pipe). • Роль чтения (reader role). ACE_Select_Reactor внутренне регистри- рует конец-чтения данного канала с помощью READ_MASK. Когда реак- Блок 20: Управление размером ACE Select Reactor Количество обработчиков событий, которыми может управлять ACE_select_Re- actor по умолчанию равно значению из макроса fd_setsize, fd_setsize ис- пользуется обычно ОС для установления размера структур fd_set, рассмот- ренных в главе 7 (C++NPv1), Поскольку ACE_Select_Reactor зависит от fd_set, а fd_setsize управляет ее размером, то fd_setsize может играть важную роль в увеличении возможного числа обработчиков в ACE_Se- lect _Reactor. Это значение может контролироваться следующим образом: • Чтобы создать ACE_Select_Reactor, размер которого меньше, чем значение ' по умолчанию из f9_sets т 2,е, нужно просто передать соответствующее зна- чение методу ACE_Select Reactor::орепО. Это не требует повторной компиляции библиотеки АСЕ. ' •• Чтобы создать ACE_Select_Reactor, размер которого больше, чем значе- ние по умолчанию из fd-setsize, нужно изменить значение из fd_setsize в файле $ace_root/ace/config.h и повторно скомпилировать библиотеку АСЕ (а, на некоторых платформах, возможно, и ядро ОС и С-библиотеку). После повторной компиляции и повторной установки необходимых библио- тек, можно будет передать нужное количество обработчиков событий мето- ду ACE_Select_Reactor: : open О . Это будет работать до тех пор, пока пе- редаваемое значение будет меньше или равно новому значению в fd_sets I ze и максимальному количеству обработчиков, поддерживаемо- му данной ОС. Хотя шаги, изложенные выше, позволяют каждому ACE_Select_Reactor управ- лять большим количеством дескрипторов ввода/вывода, это не всегда хоро- шо, так как производительность может ухудшаться из-за недостатков, связанных с select () (ВМ98), Поэтому, при необходимости управлять большим количест- вом дескрипторов, можно подумать об использовании ACE_DevjPoli_Reactor, имеющимся на некоторых UNIX-платформах. Альтернативным выбором может быть использование асинхронного ввода/вывода, основанного на АСЕ-карка- се Proactor, обсуждаемого в главе 8 (имеется в Windows и на некоторых UNIX-платформах). Избегайте искушения разделить большое количество деск- рипторов между множеством экземпляров ACE_Seiect_Reactor, так как один из недостатков связан с тем, что select О приходится сканировать большие структуры fd set, а не в том, что АСЕ использует select (). .
Реализации АСЕ Reactor 125 тор обнаруживает событие на конце-чтения своего канала уведомлений он «запускает» обработчик уведомлений, чтобы обслужить обработчики событий, количество которых задается пользователем, и данного канала. Количество обработчиков, для которых выполняется диспетчеризация, контролируется методом max_notify_iterations (). В блоке 17 объясняется, как избежать тупиковых ситуаций, которые могут быть следствием того, что размер буфера ACE_Pipe ограничен. В блоке 22 от- мечается еще одна потенциальная проблема, связанная с уведомлениями; ре- шение этой проблемы обеспечивает способ расширения механизма уведомле- ний и помогает избежать тупиковых ситуаций. В отличие от обработчиков событий, зарегистрированных у реактора, те де- скрипторы, которые передаются через механизм уведомлений реактора, не обя- заны быть привязаны к событиям, связанным с вводом/выводом или тайме- ром, что способствует повышению гибкости и масштабируемости ACE_Se- lect_Reactor. Более того, этот механизм позволяет сериализовать обслуживание всех обработчиков событий в главном (main) потоке реактора, что упрощает реализации обработчиков событий, так как им самим не нужно поддерживать многопоточную обработку. На рис. 4.3 показано как ACE_Pipe используется в ACE_Select_Reactor. При наличии множества событий от разных источников, и определяемых приложениями (таймеры и дескрипторы ввода/вывода) и внутренних (канал уведомлений), для ACE_Select_Reactor важно осуществлять диспетчериза- цию этих событий по обработчикам событий эффективным образом. Годы экспериментов и усовершенствований вылились в следующую последователь- ность диспетчеризации обработчиков событий в методе ACE_Seleсt_Reac- tor ::handle_events(): Рис. 4.3 Механизм уведомлений ACE_Select_Reactor 1. События, управляемые временем. 2. Уведомления.
126 Глава 4 Блок 21: Класс ACE Pipe Механизм уведомлений ACE_Seiect_Reactor реализуется с помощью клас- са ACE_Pipe, что обеспечивает переносимый двунаправленный механизм IPC, который передает данные через ядро ОС. Этот класс интерфейсного фасада реализуется с помощью канала STREAMS на современных UNIX-платформах, socketpair о на традиционных UNIX-платформах или соединений сокетов TCP/IP на Windows-платформах. После инициализации ACE_?ipe, приложения могут получить его «read» и «write» дескрипторы с помощью методов доступа и вызывать операции ввода/вывода для получения и передачи данных. Эти де- скрипторы могут также быть включены в объекты ACE_Handie_set, передавае- мые методу асе :: select () или любому реактору, основанному на select (), например,' ACE_Select_Reactor ИЛИ АСЕ Priority_Reactor. 3. События вывода, относящиеся к вводу/выводу. 4. Исключительные ситуации, связанные с событиями ввода/вывода. 5. События ввода, относящиеся к вводу/выводу. Приложения, в общем случае, не должны зависеть от последовательности, в которой осуществляется диспетчеризация событий разных типов, так как не все реализации реакторов гарантируют один и тот же порядок. Например, ACE_Dev_Poll_Reactor может осуществлять диспетчеризацию уведомле- ний не раньше, чем событий ввода/вывода. Существуют, однако, ситуации, в которых знание последовательности диспетчеризации оказывается полез- Блок 22: Расширение механизма уведомлений ACE Select Reactor Возможны ситуации, в которых уведомление, помещенное в очередь ACE_Se- lect__Reactor, не будет доставлено, а целевой обработчик этого события бу- дет удален. Эта задержка связана с временным окном между моментом вызо- ва метода notify () и моментом, в который реактор реагирует на канал уве- домлений, читает уведомительное сообщение из канала и осуществляет диспетчеризацию соответствующего обратного вызова. Хотя у разработчиков приложения часто есть возможность урегулировать такой сценарий и избе- жать удаления обработчика событий, пока уведомления находятся в состоя- нии ожидания, но эта возможность существует не всегда. АСЕ предлагает способ изменить механизм постановки уведомлений в оче- редь в ACE_Seiect_Reactor с ACE_Pipe на очередь в пользовательском про- странстве, которая может быть произвольно большой, Этот альтернативный механизм обеспечивает следующие преимущества: • Существенно расширяет объем очередей механизма уведомлений, помо- гая, помимо прочего, избежать тупиковой ситуации (см. блок 17). • Метод ACE_Reactor:: purge_pending_notif ications () может просмат- ривать очередь и удалять искомые обработчики событий. Чтобы задействовать эту возможность, добавьте #define ace_has reac- tor_notification_queue в файл $ACE_ROOT/ace/conf ig .h и повторно ском- понуйте АСЕ. Эта опция по умолчанию отключена, так как выделение дополни- тельной динамической памяти, требующееся в этом случае, может оказаться недопустимым для высокопроизводительных или встроенных систем.
Реализации ACE Reactor 127 ным. Например, метод обратного вызова handle_output () обработчика со- бытий может столкнуться с ошибкой при записи в сокет, поскольку удаленное приложение аварийно завершило данное соединение. Случай выглядит так, как будто данный сокет все еще готов принять ввод и вскоре будет вызван метод об- ратного вызова handle_input () обработчика, а, на самом деле, речь идет об обычном удалении сокета и обработчика. Параллелизм. ACE_Select_Reactor является реализацией шаблона класса ACE_Select_Reactor_T, приведенного на рис. 4.2. Этот шаблон ис- пользует паттерн Strategized Locking [POSA2], чтобы дать возможность разра- ботчикам приложений настраивать их на нужный уровень синхронизации. Шаблонный аргумент TOKEN всегда является экземпляром ACE_Select_Re- actor_Token_T с одним из следующих типов: • ACE_Token — приводит к созданию синхронного реактора, позволяю- щего множеству потоков вызывать методы регистрации и удаления об- работчиков событий и управления ими с одним ACE_Reactor, который совместно используется всеми потоками процесса. Механизм рекурсив- ных блокировок ACE_Token описывается в блоке 23. • ACE_Noop_Token — приводит к созданию несинхронного реактора, ко- торый минимизирует издержки обработки событий однопоточными приложениями. ACE_Noop_Token предоставляет тот же интерфейс, что и ACE_Token, но без синхронизации. Этот тип используется по умолча- нию, когда АСЕ компонуется без поддержки многопоточной обработки В каждый момент времени только один поток (называемый владельцем— owner) может вызывать ACE_Select_Reactor: :handle_events (). По умолчанию, владельцем ACE_Reactor является поток, который его создает. Метод ACE_Select_Reactor:: owner () используется для изменения вла- дельца ACE_Select_Reactor на поток с конкретным идентификатором (id). Этот метод полезен в том случае, когда тот поток, который выполняет цикл об- работки событий данного реактора, отличается от того потока, который его соз- дает (инициализирует). Функция event_loop () иллюстрирует случай такого применения. Пример Поскольку взаимно-согласованный (reactive) сервер регистрации, из пре- дыдущей главы, работает в непрерывном режиме, не существует способа штат- но завершить его работу, ее можно завершить только аварийно. Например, ад- министратор в UNIX может послать его процессу сигнал «kill -9» с зарегистри- рованной консоли или завершить данный процесс с помощью диспетчера задач Windows. Аварийное завершение процесса с помощью указанных меха- низмов избавляет его от выполнения действий, связанных с очисткой, таких как сброс регистрационных записей на диск, освобождение синхронизирую- щих блокировок и закрытие TCP/IP-соединений. В данном примере мы пока- жем, как можно использовать механизм ACE_Select_Reactor:: notify () для корректного завершения работы сервера регистрации.
128 Глава4 Блок 23: Класс ACEJoken асе_Token представляет собой блокировку, интерфейс которой совместим с другими интерфейсными ACE-фасадами синхронизации, такими как ACEjrhread_Mutex или ACE_RW_Mutex из главы 10 (C++NPv1), но реализация которой имеет следующие возможности: • Реализует семантику рекурсивных мьютексов; то есть поток, владеющий ; маркером (token) может получить его повторно, не попадая в тупиковую си- туацию, Прежде чем маркер может быть получен другим потоком, тем не ме- нее, должен быть вызван его метод release () столько раз, сколько раз вызы- ВОЛСЯ acquire О . • Каждый ACEjroken поддерживает два упорядоченных списка, которые ис- пользуются для постановки в очередь высоко- и низкоприоритетных потоков, , ожидающих получения маркера. Потоки, запрашивающие маркер с помо- щью ACEjroken:: a cgui г e_write О, хранятся в списке с высоким приорите- том и имеют преимущество по сравнению с потоками, которые вызывают ACE_Token::acqurre_read() и хранятся в списке с низким приоритетом. В пределах приоритетного списка потоки, которые блокированы в ожида- нии получения маркера обслуживаются в режиме FIFO или LIFO в соответст- вии с текущей стратегией, по мере того как потоки освобождают маркер. ♦ Стратегия постановки в очередь в ACE_Token может быть определена или установлена с помощью вызовов ACE_Token: :queueing_strategyО и по умолчанию установлена в FIFO, что обеспечивает равноправие ожидающих потоков. Наоборот, мьютексы UNIX International и Pthreads не накладывают строгих правил, касающихся особого порядка получения маркера пото- ком. Для приложений, которым не требуется строгий порядок FIFO, стратегия ACE_Token LIFO может улучшить производительность за счет максимальной аффинности кэша (cache affinity) центрального процессора (SOP+00). • • Hook-метод АСЕ_токеп:: s ieep_hook () вызывается, если ПОТОК не может по- лучить маркер немедленно. Этот метод позволяет потоку освободить все ре- сурсы, которыми он владеет, прежде, чем он перейдет в режим ожидания получения маркера, таким образом, избегая тупиковой ситуации (dead- lock), голодания (starvation) и полной инверсии приоритетов; ACE_Select_Reactor ИСПОЛЬЗует КЛОСС АСЕ_Select-Reactor_Token, ПРОИЗ- ВОДНЫЙ от ACE_Token, для синхронизации доступа к реактору. Запросы на изме- нение внутренних СОСТОЯНИЙ реактора используют ACE-Token: :acquire_wri- . te (), чтобы гарантировать, что другие ждущие потоки воспримут эти изменения как можно раньше. ACE_Seiect_Reactor_Token перегружает его метод sleep_hook (), чтобы уведомлять реактор об ожидающих обслуживания пото- ках посредством своего механизма уведомлений, описанного в блоке 21. На рис. 4.4 приведена архитектура нашего решения, которая использует механизм уведомлений ACE_Select_Reactor Для завершения работы Re- actor_Logging_Server с помощью следующих шагов: 1. Создаем управляющий поток, который ждет команд от администратора на своем стандартном входе. 2. После получения команды «quit», управляющий поток передает специ- альный обработчик событий реактору с помощью метода notify () и за- тем завершает свою работу.
Реализации ACE Reactor 129 Рис. 44 Сервер регистрации на основе ACE_Select_Reactor с управляющим потоком 3. Реактор вызывает метод handle_except ion () этого обработчика собы- тий, который вызывает end_reactor_event_loop () и затем удаляет себя. 4. Когда ACE_Reactor: : run_reactor_event_loop () в следующий раз проверит результат, полученный от метода reactor_event_loop_do- пе (),он получит значение «истина» (true), что приведет к завершению ра- боты цикла событий реактора и к штатному завершению основного (main) серверного потока. Код C++, представленный ниже, иллюстрирует эти четыре шага. Это код находится в Select_Reactor_Logging_Server .срр. Сначала приведена пересмотренная функция main (): 1 finclude "ace/streams.h" 2#include "ace/Reactor.h" 3 #include "ace/Select_Reactor.h" 4 tinclude "ace/Thread_Manager.h" 5 #include "Reactor_Logging_Server.h" 6#include <string> 7// Предварительные объявления. 8ACE_THR_FUNC_RETURN controller (void *); 9ACE_THR_FUNC_RETURN event_loop (void *) ; 10 11 typedef Reactor_Logging_Server<Logging_Acceptor_Ex> 5 Программирование сетевых приложений Ш C++. Том 2
130 Глава 4 12 Server_Logging_Daemon; 13 14int main (int argc, char *argv[]) { 15 ACE_Select_Reactor selecL_reactor; 16 ACE_Reactor reactor (&select_reactor); 17 18 Server_Logging_Daemon ‘server = 0 ; 19 ACE_NEW_RETURN (server, 20 Server_Logging_Daemon (argc,argv, &reactor), 21 1) ; 22 ACE_Thread_Manager::instance()->spawn (event_loop, &reactor); 23 ACE_Thread_M3nager::instance()->spawn (controller, &reactor); 24 return ACE Thread Manager::instance ()->wait (); 25 } Строки 1-12 Включаем заголовочные файлы, задаем некоторые предвари- тельные объявления и создаем экземпляр шаблона Reactor_Logging_Ser- ver с классом Logging_Acceptor_Ex для создания определения типа Ser- ver_Logging_Daemon. ACE_THR_FUNC_RETURN задает в переносимом сти- ле тип возвращаемого значения функции данного потока. Строки 15-16 Задаем реализацию ACE_Select_Reactor локального эк- земпляра ACE_Reactor. Строки 20-21 Динамически создаем экземпляр Server_Logging_Dae- mon. Строка 22 Используем синглтон ACE_Thread_Manager из главы 9 [C++NPvl] для создания потока, который выполняет следующую функцию event_loop(): static ACE_THR_FUNC_RETURN event_loop (void *arg) ( ACE_Reactor *reactor = ACE_static_cast (ACE_Reactor *, arg); reactor->owner (ACE_OS::thr_self ()); reactor->run_reactor_event_loop (); return 0; ) Обратите внимание, каким образом мы устанавливаем в качестве владель- ца для данного реактора поток, который выполняет цикл обработки событий. В разделе Параллелизм поясняется использование потока-владельца ACE_Se- lect_Reactor. Строка23 Создаем поток для выполнения функции cont г oiler () .кото- рая ждет от администратора команды на завершение работы сервера на своем стандартном вводе. Строка 24 Ждем завершения двух других потоков, прежде чем завершить функцию main (). ACE_Thread_Manager: :wait () также получает статус завершения двух упомянутых потоков, чтобы избежать утечки памяти. В блоке 42 описываются соглашения, которым нужно следовать, чтобы избежать утеч- ки памяти при завершении работы потоков.
Реализации ACE Reactor 131 Строка 25 С этого момента цикл обработки событий завершен, но Ser- ver_Logging_Daemon и существующие соединения с клиентами остаются открытыми. Объекты reactor и select_reactor, тем не менее, вот-вот выйдут из области действия. Так как ACE_Reactor играет роль абстракции (Abstraction) в паттерне Bridge, то единственным важным полем реактора явля- ется указатель на его объект-реализацию, select_reactor. По умолчанию деструктор ACE_Reactor удаляет только объект-реализацию, если его создал ACE_Reactor. Поскольку select_reactor был создан в стеке и передан реак- тору, select_reactor не уничтожается деструктором ACE_Reactor. Вместо этого, он уничтожается при выходе из области действия. Его уничтожение при- водит к обратному вызову hook-методов Logging_Acceptor: :hand- le_close() и Logging_Event_Handler_Ex::handle_close() для ка- ждого обработчика регистрации и обработчика событий регистрации соответ- ственно, которые остаются Зарегистрированными у данного реактора. Функцию controller () можно реализовать следующим образом: 1 static ACE_THR_FUNC_RETURN controller (void *arg) { 2 ACE_Reactor *reactor = ACE_static_cast (ACE_Reactor *, arg); 3 Quit_Handler *quit_handler = 0; 4 ACE_NEW_RETURN (quit_handler, Quit_Handler (reactor), 0); 5 6 for (;;) ( 7 std::string user_input; 8 std: :getline (cin, user_input, '\n');. 9 if (user_input == "quit") { 10 reactor->notify (quit_handler); 11 break; 12 } 13 ) 14 return 0; 15 }. Строки 2-4 После приведения аргумента-указателя на vo i d обратно к ука- зателю на ACE_Reactor, мы создаем специальный обработчик событий, кото- рый называется Quit_Handler. Его методы handle_exception () и hand- le_close () завершают работу цикла событий ACE_Select_Reactor и, со- ответственно, удаляют обработчик событий: class Quit_Handler : public ACE_Event_Handler { public: Quit_Handler (ACE_Reactor *r): ACE_Event_Handler (r) {} virtual int handle_exception (ACE_HANDLE) { reactor ()->end_reactor_event_loop (); return -1; // Возбуждает вызов метода handle_close(). } virtual int handle_close (ACE_HANDLE, ACE_Reactor_Mask) { delete this; return 0; } 5»
132 Глава 4 private: // Закрытый деструктор обеспечивает динамическое размещение. virtual ~Quit_Handler () {} 1; Строки 6-13 Входим в цикл ожидания команды «quit» от администратора на стандартном потоке ввода. Когда это происходит, мы передаем quit_han- dler реактору через его метод notify () и завершаем работу управляющего потока. Реализация, приведенная выше, является переносимой на все АСЕ-плат- формы, поддерживающие поточную обработку. В разделе 4.4 показано как ис- пользовать преимущество специфических возможностей Windows, чтобы реа- лизовать такое же поведение. 4.3 Класс ACE TP Reactor Обоснование Хотя ACE_Select_Reactor является гибкой реализацией, она отчасти ограничена в многопбточных приложениях, так как только поток-владелец мо- жет вызывать ее метод handle_events (). Поэтому ACE_Select_Reactor сериализует обработку на уровне демультиплексирования событий, что может оказаться слишком строгим и немасштабируемым ограничением для некото- рых сетевых приложений. Один из способов решения этой проблемы — соз- дать несколько потоков и выполнять цикл обработки событий в отдельном экзем- пляре ACE_Select_Reactor в каждом из них. Такое решение может оказаться, однако, трудным для программирования, так как оно требует от разработчиков реализовать прокси (proxy), который равномерно распределит обработчики событий по их реакторам, чтобы равномерно распределить нагрузку по пото- кам. Часто более эффективным способом устранения указанных ограничений ACE_Select_Reactor является использование 'класса ACE_TP_Reactor, входящего в АСЕ-каркас Reactor, где «ТР» означает «thread pool» (пул потоков). Функциональные возможности класса ACE_TP_Reactor является еще одной реализацией интерфейса ACE_Re- actor. Данный класс реализует архитектурный паттерн Leader/Followers [POSA2], который предлагает эффективную модель параллелизма, где несколь- ко потоков по очереди вызывают select () на множестве дескрипторов вво- да/вывода с целью обнаружения, демультиплексирования, диспетчеризации и обработки поступающих запросов на обслуживание. ACE_TP_Reactor со- храняет все возможности интерфейса ACE_Reactor, и дополнительно обеспе- чивает следующие функциональные возможности: • Позволяет пулу потоков вызывать его метод handle_events (), что может улучшить масштабируемость за счет параллельной обработки со- бытий несколькими обработчиками. Как следствие, метод ACE_TP_Re- actor: : owner () не выполняет никаких действий (no-op).
Реализации ACE Reactor 133 • Предохраняет от одновременной диспетчеризации разными потоками нескольких событий ввода/вывода одному и тому же обработчику собы- тий. Это ограничение защищает поведение ACE_Select_Reactor, свя- занное с вводом/выводом, уменьшая необходимость включения в про- цесс обработки ввода/вывода блокировок синхронизации. После получения потоком набора активных дескрипторов от select О, другие потоки реактора осуществляют диспетчеризацию этого набора дескрип- торов вместо того, чтобы снова вызывать select (). Обзор реализации. Как показано на рис. 4.1, ACE_TP_Reactor является производным от ACE_Reactor_Impl. Он также служит конкретной реализа- цией интерфейса ACE_Reactor, как и ACE_Select_Reactor. Фактически, ACE_TP_Reactor является производным от ACE_Select_Reactor и по» вторно использует многое из его внутренней организации. Параллелизм. Множество потоков, выполняющих цикл событий ACE_TP_Reactor, могут обрабатывать параллельно события, связанные с разными дескрипторами. Кроме того, они могут параллельно осуществлять диспетчеризацию методов обратного вызова тайм-аутов и ввода/вывода для одного обработчика событий. Необходимость сериализации в ACE_TP_Reac- tor возникает только тогда, когда на одном дескрипторе одновременно насту- пает несколько событий ввода/вывода. В отличие от ACE_Select_Reactor, в котором осуществляется сериализация всех операций диспетчеризации обра- ботчиков, дескрипторы которых являются активными в данном наборе деск- рипторов. В отличие от других моделей пула потоков, таких как модель half-sync/half- async из главы 5 [C++NPvl] и раздела 6.3 данной книги, реализация leader/fol- lowers (ведущий/ведомые) в ACE_TP_Reactor осуществляет всю обработку событий локально относительно того потока, который осуществляет диспетче- ризацию данного обработчика. Такой дизайн обеспечивает следующее улучше- ние характеристик: • Улучшает аффиность кэша процессора и исключает необходимость ди- намического выделения памяти и совместного использования буферов несколькими потоками. • Минимизирует издержки, связанные с блокировками, исключая обмен данными между потоками. • Минимизирует инверсию приоритетов, так как не использует дополни- тельных очередей. • Не требует переключения контекста при обработке каждого события, что снижает задержку. Эти усовершенствования обсуждаются более подробно в описании паттер- на Leader/Followers в POSA2. Учитывая изложенные дополнительные возможности ACE_TP_Reactor, можно задать вопрос: зачем, вообще, использовать ACE_Select_Reactor. Существует две основных причины:
134 Глава 4 1. Меньше издержек —: хотя у ACE_Select_Reactor меньше возможно- стей, чем у ACE_TP_Reactor, но у него также меньше издержки времени и объема памяти. Более того, однопоточные приложения могут создавать реализации шаблона ACE_Select_Reactor_T с маркером, производ- ным от ACE_Noop_Token, чтобы полностью устранить внутренние из- держки, связанные с получением и освобождением маркеров. 2. Неявная сериализация — ACE_Select_Reactpr особенно полезен, ко- гда писать код сериализации в явном виде на прикладном уровне нежела- тельно. Например, прикладные программисты, незнакомые с методами синхронизации, возможно, предпочтут перепоручить сериализацию обра- ботки своих событий ACE_Select_Reactor, вместо того, чтобы исполь- зовать потоки в своем прикладном коде и включать в него блокировки. Рис. 4.5 Сервер регистрации на основе ACE_TP_Reactor с управляющим потоком Пример Чтобы проиллюстрировать большие возможности ACE_TP_Reactor, мы изменим функцию main () предыдущего примера так, чтобы создавался пул потоков, совместно использующих дескрипторы ввода/вывода Reactor_Log- ging_Server. На рис. 4.5-показана архитектура этого сервера. Этаархитекту- ра почти идентична архитектуре, изображенной на рис. 4.4, с единственным отличием: пулом потоков, которые вызывают ACE_Reactor: : hand- le_events(). Данный пример находится в файле TP_Reactor_Log- ging_Server. срр. Код C++ функции main () приведен ниже:
Реализации ACE Reactor 135 1tfinclude "ace/streams.h” 2 #include "ace/Reactor.h” 3 #include "ace/TP_Reactor.h" 4 #include "ace/Thread_Manager.h” 5 #include ”Reactor_Logging_Server.h" 6 tfinclude <string> 7 // Предварительные объявления. 8 ACE_THR_FUNC_RETURN controller (void *); 9 ACE_THR_FUNC_RETURN event_loop (void *); 10 11 typedef Reactor_Logging_Server<Logging_Acceptor_Ex> 12 Server_Logging_Daemon; 13 14int main (int argc, char *argv[]) { 15 const size_t N_THREADS = 4; 16 ACE__TP_Reactor tp_reactor; 17 ACE_Reactor reactor (&tp__reactor) ; 18 auto_ptr<ACE_Reactor> delete__instance 19 (ACE_Reactor::instance (&reactor)); 20 21 Server_Logging_Daemon *server = 0; 22 ACE_NEW_RETURN (server, 23 Server_Logging_Daemon (argc, argv, 24 ACE_Reactor::instance ()), 1); 25 ACE__Thread_Manager:: instance (}->spawn_n 26 (N_THREADS, event_loop, ACE_Reactorinstance ()); 27 ACE__Thread_Manager: : instance () ->spawn 28 (controller, ACE_Reactor::instance ()); 29 return ACE Thread Manager::instance ()->wait (); 30 } Строки 1-12 Включаем заголовочные файлы, задаем несколько предвари- тельных объявлений и создаем реализацию шаблона Reactor_Log- ging_Server с Logging_Acceptor_Ex для создания определения типа Server_Logging_Daemon. Строки 16-19 Создаем локальный экземпляр ACE_TP_Reactor и исполь- зуем его в качестве реализации локального объекта ACE_Reactor. Затем, для разнообразия, устанавливаем синглтон ACE_Reactor на адрес локального ре- актора. При следующем вызове ACE_Reactor: : instance () будет исполь- зовать наш локальный реактор. При переназначении реактора для синглтона, на вызывающей стороне лежит ответственность за управление временем жиз- ни предыдущего синглтона. В этом случае, мы назначаем ему auto_jptr так, чтобы он удалялся автоматически по завершению программы. Строки 21-24 Динамически размещаем экземпляр Server_Logging_Da- emon. Строки 25-26 Создаем N_THREADS, каждый из которых выполняет функ- цию event_loop (). Указатель на реактор нового синглтона передается мето- ду event_loop () (ACE_TP_Reactor игнорирует метод owner (), вызывае- мый в этой функции).
156 Глава 4 Строки 27-28 Создаем один поток для выполнения функции control- ler (). Строка 29 Ждем завершения работы остальных потоков и сохраняем ста- тус в виде возвращаемого функцией main () значения. Строка 30 Когда ^функция main () завершает свою работу, деструктор tp_reactor вызывает hook-методы Logging_Acceptor: :handle_clo- se() иLogging_Event_Handler_Ex::handle_close() для каждого ре- гистрирующегося обработчика и каждого обработчика событий регистрации соответственно, которые, по-прежнему, у него зарегистрированы. По умолча- нию, ACE_Object_Manager (блок23в [C-H-NPvl]) удаляет синглтон ACE_Re- actor в процессе завершения работы. Так как мы заменили исходный сингл- тон локальным объектом реактора, однако, АСЕ не станет удалять ни исходный экземпляр (поскольку мы назначили для него владельца в строке 18), ни наш локальный экземпляр (поскольку АСЕ не удаляет реакторы, которых он не соз- давал, если только не поступит явное указание сделать это.). Основное отличие этого примера от предыдущего — это количество пото- ков, выполняющих цикл обработки событий. Хотя множество потоков может осуществлять диспетчеризацию событий по объектам Logging_Event_Hand- ler и Logging_Acceptor_Ex, ACE_TP_Reactor гарантирует, что один и тот же обработчик не будет вызываться несколькими потоками одновремен- но. Так как классы обработки событий в сервере регистрации являются авто- номными, отсутствует вероятность возникновения гонок, характерная при дос- тупе, осуществляемом несколькими потоками. Поэтому нам не нужно вносить в них никаких изменений, чтобы обеспечить поддержку многопоточной обра- ботки. 4.4 Класс ACE_WFMO__Reactor Обоснование Хотя функция select () присутствует в большинстве операционных сис- тем, не на каждой платформе ОС и не всегда именно она является наиболее эф- фективным средством демультиплексирования событий. В частности, se- lect () имеет следующие ограничения: • На UNIX-платформах она поддерживает только демультиплексирование дескрипторов ввода/вывода, таких как файлы, терминалы, различные FIFO и каналы. Она не поддерживает, переносимым образом, демультип- лексирование синхронизаторов, потоков или очередей сообщений System V Message Queues. • В Windows select () поддерживает демультиплексирование только де- скрипторов сокетов. • Она может вызываться в каждый данный момент времени только одним потоком для заданного набора дескрипторов ввода/вывода, что может привести к снижению потенциальных возможностей параллельной об- работки.
Реализации ACE Reactor 137 Windows определяет системную функцию WaitForMultipleObj- ects (), описана в блоке 24, чтобы сгладить эти проблемы. Эта функция рабо- тает со многими типами дескрипторов Windows, с которыми связано наступле- ние событий. Хотя она напрямую с дескрипторами ввода/вывода не работает, ее можно использовать для демультиплексирования событий ввода/вывода дву- мя способами: 1. Дескрипторы событий, используемые в перегружаемых операциях вво- да/вывода. 2. Дескрипторы событий, связанные с дескрипторами сокетов с помощью WSAEventSelect(). Кроме того, множество потоков для одного и того же набора дескрипторов, могут одновременно вызывать WaitForMultipleObjects (), повышая, та- ким образом, потенциальные возможности параллелизма. Однако WaitForMultipleObjects () достаточно сложно использовать по следующим причинам [SS95a]: • WaitForMultipleObjects () возвращает индекс первого слота (slot) массива дескрипторов, в котором хотя бы один из дескрипторов нахо- дится в сигнальном состоянии. Однако он не указывает число дескрипто- ров, находящихся в сигнальном состоянии, и не существует простого способа поиска и проверки таких дескрипторов. Поэтому WaitForMul- tipleObjects () приходится вызывать несколько раз, чтобы найти все дескрипторы, находящиеся в сигнальном состоянии. Напротив, se- lect () возвращает набор активных дескрипторов ввода/вывода и счет- чик, указывающий их количество. • WaitForMultipleObjects () не гарантирует равенства дескрипторов при распространении уведомлений, а именно, всегда возвращается са- мый младший активный дескриптор массива, независимо от того как долго другие дескрипторы, которые расположены дальше в массиве, воз- можно, ждут обработки своих событий. Блок 24: Функция Windows WaitForMultipleObjectsQ Функция WaitForMultipleObjectsО, демультиплексор событий в Windows, похожа на select (). Она блокируется на массиве, размером до 64 дескрип- торов, в ожидании того момента, когда один или несколько из них станут актив- ными (что, в терминологии Windows, означает быть в «сигнальном» состоянии) или пока истечет время, заданное ее параметром тайм-аута. Ее можно запро- граммировать так, чтобы она возвращала управление вызывающей стороне, когда один, несколько или все дескрипторы массива становятся активными. В любом случае, она возвращает индекс самого младшего активного деск- риптора в задаваемом вызывающей стороной массиве дескрипторов. В отли- чие от функции select о, которая демультиплексирует только дескрипторы ввода/вывода, WaitForMultipleObjects () может работать со многими типа- ми объектов Windows, включая поток, процесс, синхронизатор (например, со- бытие, семафор или мьютекс), уведомление об изменении, консольный ввод и таймер.
138 Глава 4 Чтобы оградить программистов от этих низкоуровневых деталей, сохра- нить справедливый порядок демультиплексирования и усилить возможности WaitForMultipleOb j ects () на Windows-платформах, АСЕ-каркас Reactor определяет класс ACE_WFMO_Reactor. Функциональные возможности класса ACE_WFMO_Reactor — это еще одна реализация интерфейса ACE_Reac- tor, которая использует функцию WaitForMultipleObjects (), чтобы ждать наступления событий на множестве источников. ACE_WFMO_Reactor сохраняет все возможности интерфейса ACE_Reactor и дополнительно обес- печивает следующие возможности: • Позволяет пулу потоков одновременно обращаться к его методу hand- le_events () (как следствие метод ACE_WFMO_Reactor: : owner () не содержит операций). Эта возможность является более эффективной, чем аналогичная возможность у ACE_TP_Reactor. В ACE_TP_Reac- tor все потоки, обрабатывающие события, могут осуществлять диспет- черизацию событий параллельно вместо того, чтобы делать это по очере- ди в стиле ведущий/ведомые (leader/followers). Мы обсудим этот аспект ACE_WFMO_Reactor более подробно в разделе Параллелизм. • Позволяет приложениям ждать наступления событий для сокетов вво- да/вывода и установленных таймеров, также как и реакторы на базе se- lect (). ACE_WFMO_Reac tor объединяет также диспетчеризацию и де- мультиплексирование событий всех типов, поддерживаемых Wai'tFor- MultipleOb j ects (), как отмечено в блоке 24. • Каждый вызов handle_events () сопровождается ожиданием того, ко- гда дескриптор станет активным. Начиная с этого дескриптора, переби- раются все остальные активные дескрипторы, прежде чем возвращается управление. Такой дизайн защищает дескрипторы, расположенные в массиве набора дескрипторов дальше, чем активный дескриптор, от «голодания». • Используя производный класс ACE_Msg_WFMO_Reactor, приложения могут обрабатывать все события, перечисленные выше, и плюс сообще- ния Windows. В Windows ACE_WFMO_Reactor является реализацией по умолчанию по причинам, изложенным в блоке 25. Заметьте, что ACE_WFMO_Reactor осуще- ствляет диспетчеризацию событий в той же последовательности, что и ACE_Select_Reactor. Обзор реализации. Как показано на рис. 4.1, ACE_WFMO_Reactor насле- дует от ACE_Reactor_Impl. Поэтому она служит в качестве конкретной реа- лизации интерфейса ACE_Reactor. Также как ACE_Select_Reactor усили- вает возможности функции select (), ACE_WFMO_Reactor, как следует из рис. 4.6, усиливает возможности WaitForMultipleObjects (). Наиболее существенными отличиями ACE_WFMO_Reactor от ACE_Se- lect_Reactor и ACE_TP_Reactor являются следующие отличия:
Реализации АСЕ Reactor 139 Блок 25: Почему ACE_WFMO_Reactor является реализацией по умолчанию в Windows ACE_wFMO_Reactor является реализацией ACE_Reactor по умолчанию на Win- dows-платформах по следующим причинам: • Она более естественно согласуется с многопоточной обработкой, обычной для Windows (ACE_WFMO_Reactor был разработан до ACE_TP_Reactor и был первым реактором, обеспечивающим поддержку многопоточной обработ- ки событий). ♦ Приложения часто используют «сигнальные» дескрипторы в ситуациях, в ко- торых, возможно, используются сигналы POSIX (например, завершение про- цесса-потомка) и диспетчеризация этих событий может осуществляться С ПОМОЩЬЮ ACE_WFMO_Reactor. • Она может обрабатывать больше типов событий, чем ACE_Select_Reactor, которая в Windows может обрабатывать только события, связанные с соке- тами и таймером. • • Она легко встраивается в обработку событий с ACE_Proactor, рассматри- ваемым в блоке, 58. • Ограниченное количество дескрипторов. В отличие от ACE_Se- lect_Reactor и ACE_TP_Reactor, которые можно настроить на де- мультиплексирование сотен или тысяч дескрипторов, ACE_WFMO_Re- actor может обрабатывать не более 62 дескрипторов. Это ограничение вытекает из того факта, что Windows позволяет WaitForMultipleOb- jects () работать с 64 дескрипторами на поток. ACE_WFMO_Reactor использует два из этих дескрипторов для своих внутренних целей: один для механизма уведомлений и второй для синхронизации параллельного обновления обработчиков. Если нужно осуществлять демультиплекси- рование более 64 дескрипторов, то можно использовать несколько объ- ектов ACE_WFMO_Reactor в нескольких потоках, использовать ACE_Proactor (глава 8) или ACE_Select_Reactor и увеличивать его размер с цомощью механизмов, описанных в блоке 20. • Семантика WRITE_MASK отличается от select(). Когда у сокета остают- ся данные для передачи, select () распознает условие WRITE и будет продолжать распознавать это условие до тех пор, пока у сокета остаются данные, то есть пока он продолжает управляться потоком. Наоборот, функция Windows WSAEventSelect () выставляет флаг события WRI- TE только тогда, когда сокет в первый раз подключается, неважно пассив- но или активно, и когда данный сокет переходит из состояния управляе- мого потоком (flow-controlled) в состояние передачи данных (writeable). Если при использовании ACE_WFMO_Reactor полагаться на события WRITE, тогда приходится продолжать запись до закрытия соединения или пока сокет снова станет управляться потоком (flow controlled) и send () выдаст ошибку с EWOULDBLOCK. Если такое поведение нежела- тельно, можно рассмотреть вариант ACE_Select_Reactor в качестве реализации ACE_Reactor в Windows, так как у нее такая же семантика WRITE_MASK, как и на UNIX-платформах.
140 Глава 4 Рис. 4.6 Внутреннее устройство каркаса ACE_WFMO_Reactor • Разные механизмы уведомлений. Механизм уведомлений ACE_Se- lect_Reactor реализуется с помощью механизма ACE_Pipe, описан- ного в блоке 21. В отличие от этого, механизм уведомлений ACE_WFMO_Reactor реализуется с помощью синхронной версии ACE_Message_Queue, описанной в разделе 6.2. Как следствие, такую очередь можно настроить на размер, заданный пользователем, что может помочь избежать проблем с тупиками, о которых говорилось в блоке 17. Максимальное число уведомлений ACE_WFMO_Reactor, которые мож- но поставить в очередь, по умолчанию равно 1024. Чтобы изменить это значение, нужно сделать следующее: Создать новый объект ACE_WFMO_ReaCtor_Notify, задав в его конструкторе нужное максимальное число уведомлений в очереди. Создать новый объект ACE_WFMO_Reactor, передав указатель на новый объект ACE_WFMO_Reactor_Notify конструктору ACE_WFMO_Reac- tor. Параллелизм. ACE_WFMO_Reactor позволяет множеству потоков вызы- вать одновременно его метод handle_events (). Однако эта возможность ус- ложняет его дизайн и вносит некоторые тонкие различия в поведении по срав- нению с реакторами на основе select (): • Координация изменений, связанных с регистрацией. Любое изменение набора зарегистрированных дескрипторов затрагивает все потоки, вы- полняющие цикл обработки событий. Разрешение осуществлять эти из- менения, без синхронизации, привело бы к ошибкам: от потери событий до передачи управления на некорректные или недействительные деск- рипторы. Чтобы обрабатывать изменения регистрации должным обра- зом при наличии множества потоков, ACE_WFMO_Reactor поддержи- вает три набора объектов с информацией об обработчиках:
Реализации АСЕ Reactor 141 1. Текущие обработчики, те которые используются для обнаружения и де- мультиплексирования событий. 2. Новые обработчики, те которые ждут добавления к набору текущих обра- ботчиков. 3. Приостановленные обработчики, те, из набора текущих обработчиков, ко- торые приостановлены. Когда требуется внести изменения в регистрацию (например, зарегистри- ровать, удалить, приостановить или возобновить обработчик событий), извле- кается информация о соответствующем дескрипторе, обработчике и типе собы- тия и определяется необходимость изменения соответствующей информации. Следующий поток, который завершит свой проход по handle_events () за- метит необходимость изменения, займет блокировку реактора, и подождет пока все другие потоки, выполняющие handle_events (), завершат работу. Чтобы гарантировать, что они завершатся своевременно, ожидающий поток сигнализирует о внутреннем событии, которое входит в набор диспетчеризи- руемых дескрипторов, возобновляющее работу всех потоков, заблокирован- ных WaitForMultipleObjects О . С этого момента все потоки, обрабаты- вающие события, будут заблокированы, ожидая внесения изменений. Когда первоначальный поток, завершит необходимые изменения информации и де- скрипторов, блокировка реактора будет снята, и все потоки, обрабатывающие события, возобновят цикл ожидания, демультиплексирования и диспетчериза- ции с обновленным набором дескрипторов. • Отложенное удаление обработчиков событий. Задержка изменений ре- гистрации в ACE_WFMO_Reactor вносит, по сравнению с реакторами на основе select (), тонкие отличия в поведении. Когда метод hand- 1е_*() возвращает -1 или вызывается ACE_Reactor: : remo- ve_handler () из обработчика события, ACE_WFMO_Reactor отклады- вает удаление обработчика и осуществляет обратный вызов hook-метода handle_close () данного обработчика пока не будут сделаны измене- ния, описанные выше. Поэтому приложение не может удалить обработ- чик события немедленно после запроса ACE_WFMO_Reactor на его уда- ление, так как более поздний вызов реактором метода handle_close () будет осуществлять диспетчеризацию по недействительному указателю. Отличия между ACE_WFMO_Reactor и реакторами на базе select () можно на практике замаскировать, если придерживаться приема выполнения всей очистки в hook-методе handle_close () обработчика событий, как описа- но в блоке 9. Этот прием предохраняет от преждевременного удаления объекта обработчика событий. В тех случаях, когда этот прием неприменим (например, если обработчик событий является автоматическим объектом, уничтожение кото- рого не может быть отложено), флаг ACE_Event_Handler: : DONT_CALL дол- жен быть передан ACE_Reactor::remove^handler(), чтобы не дать ACE_WFMO_Reactor вызвать hook-метод handle_close () позже. Поэтому разумно размещать обработчики событий динамически, как рекомендуется В блоке 11, что исключает необходимость управлять временем жизни обработчи- ков событий, за пределами схемы управления обработчиками ACE_Reactor.
142 Глава 4 • Осуществление диспетчеризации обработчика несколькими потока- ми. В отличие от тех приложений, которые используют реакторы на базе select (), многопоточные приложения могут осуществлять демульти- плексирование и диспетчеризацию событий параллельно с помощью ме- тода ACE_WFMO_Reactor: : handle_events ()Поэтому в многопо- точном варианте, возможно, что разные потоки будут осуществлять дис- петчеризацию событий одновременно одному и тому же обработчику событий. Это может произойти, например, когда истечет срок таймера, управляющего обработчиком, пока ввод/вывод продолжает обрабаты- ваться. Это может также произойти в следующей ситуации: 1. Поток1 осуществляет диспетчеризацию события ввода сокета обра- ботчику. 2. ПотоК] вызывает ACE_SOCK_Stream: :recv(), чтобы прочитать данные. 3. В сокете остаются данные, из-за ограниченного приема на шаге 2 или из-за поступления дополнительных данных. 4. Поток2 осуществляет диспетчеризацию данного события ввода на тот же дескриптор, что приводит к состоянию гонок между потокоМ] и потоком2. Поэтому обработчики событий следует явно защищать от состояний го- нок, когда цикл обработки событий handle_events () выполняется несколь- кими потоками на одном объекте ACE_WFMO_Reactor. ACE_TP_Reactor из- бегает подобных состояний гонок за счет реализацйи внутреннего протокола, который автоматически приостанавливает дескриптор перед тем как осуществ- лять диспетчеризацию его обработчика событий. Любой следующий поток, ко- торый затем становится лидером, не осуществляет диспетчеризацию событий для этого дескриптора, пока не завершится обратный вызов и пока данный де- скриптор не будет освобожден. В блоке 26 объясняется почему протокол приос- тановки дескриптора нельзя использовать с ACE_WFMO_Reactor. Пример Этот пример показывает, как использовать дескриптор с сигнальным со- стоянием (signalable) с ACE_WFMO_Reactor. В нем показан также метод пра- вильной сериализации обработки ввода/вывода пулом потоков, выполняю- щим цикл событий ACE_WFMO_Reactor. На рис. 4.7 приведена архитектура этого сервера. Она похожа на представленную на рис. 4.4, с тем отличием, что имеется пул потоков, которые вызывают ACE_Reactor: : handle_eyents (). Этот пример работает только в Windows, так как в нем явно задается экземпляр ACE_WFMO_Re actor. Этот пример находится в файле WFMO_Reactor_Logging_Server. срр. Начнем с определения класса Quit_Handler: Хотя несколько потоков могут вызывать ACE_TP_Reactor: :handle_events О парал- лельно, в каждый момент времени только один поток (лидер) реально выполняет select().
Реализации ACE Reactor 143 class Quit_Handler : public ACE_Event_Handler { private: ACE—Manual—Event quit—seen—;// Отслеживаем момент завершения, public: Хотя этот класс наследует от АСЕ_Event-Handler, он используется со- вершенно по-другому, не так как Q'uit_Handler, определенный в разделе 4.2. Конструктор Quit_Handler иллюстрирует некоторые из различий: 1 Quit-Handler (ACE-Reactor * *r): ACE_Event_Handler (г) { 2 SetConsoleMode (ACE—STDIN, ENABLE-LINE-INPUT 3 I ENABLE-ECHO-INPUT 4 I ENABLE—PROCESSED—INPUT) ; 5 if (reactor ()->register_handler 6 (this, quit-Seen_.handle ()) == -1 Блок 26: Почему ACE_WFMO_Reactor не приостанавливает дескрипторы BACE_WFMO_Reactor отсутствует реализация внутреннего протокола приоста- новки обработчиков (handler suspension), чтобы свести к минимуму варианты поведения, реализуемые прикладными классами. В частности, многопоточ- ные приложения могут обрабатывать события более эффективно, если не тре- буется осуществлять межсобытийную сериализацию, как в случае получения UDP-дейтаграмм. Такое поведение невозможно в ACE_TP_Reactor, из-за се- мантических отличий в следующих механизмах ОС демультиплексирования событий; • waitForMuitipleObjects о. При демультиплексировании события вво- да/вывода дескриптора сокета, маску событий ввода/вывода от wsAEnum- NetworkEvents О получит один поток ACE_WFMO_Reactor, а ОС атомарно сбросит внутреннюю маску событий этого сокета. Даже если несколько по- токов одновременно осуществляют демультиплексирование данного деск- риптора сокета, только один получает маску событий ввода/вывода и будет осуществлять диспетчеризацию данного обработчика. Сам обработчик должен выполнить действия, восстанавливающие возможность демультип- лексирования для данного дескриптора, прежде чем другому потоку потре- буется осуществить для него диспетчеризацию. • select (). В ОС не существует автоматической сериализации для select (). Если бы нескольким потокам было разрешено следить за состоянием готов- ности дескриптора сокета, они бы все осуществляли его диспетчеризацию, что привело бы к непредсказуемому поведению на уровне ACE_Event_Hand- ler и снижению производительности из-за совместной работы множества потоков с одним и тем же дескриптором. Важно отметить, что протокол приостановки дескриптора не может быть реа- лизован в классе прикладного обработчика событий при использовании в со- четании с ACE_WFMO_Reactor. Не может из-за того, что запросы на приостанов- ку помещаются в очередь, а не обрабатываются немедленно. Обработчик мог бы, поэтому получать upcall-вызовы (upcalls) от множества потоков, пока данный обработчик был бы, фактически, приостановлен реактором ACE_WFMO_Reactor. КЛОСС Logging_Event_Handler_WFMO иллюстрирует, КОК использовать мьютекс, чтобы избежать состояния гонок при upcall-вызовах.
144 Глава 4 7 || ACE_Event_Handler: : register_stdin_handle.r 8 (this, r, ACE_Thread_Manager::instance (}) == -1) 9 r->er\d_reactor_event_loop (); 10 ) Строки 2-4 У прощаем обработку ввода, настраивая консоль на чтение всей строки текста (не по фрагментам), исключая управляющие символы, такие как Ctl-C. Строки 5-6 Показываем, как зарегистрировать обработчик событий у реак- тора. Создаем сигнал об этом событии, когда вводится команда quit. В блоке 27 перечислены возможности класса ACE_Manual_Event. Строки7-8 ИспользуемACE_Event_Handler: :register_stdin_hand- ler () для установления механизма ввода, что приводит к периодическому вы- зову hook-метода Quit_Handler: : handle_input (), пока он не вернет -1. В Windows все эти вызовы будут делаться из потока, отличного от потока(ов), выполняющего цикл обработки событий, что исключает необходимость сериа- лизации для метода обработки ввода. Строка 9 Если же зарегистрировать не получилось, мы сразу отмечаем цикл событий данного реактора как завершенный, что приводит к немедленно- му завершению данного цикла, когда основная (main) программа запустит его. Метод Quit_Handler: : handle_input () приведен ниже: virtual int handle_input (ACE_HANDLE h) { CHAR user_input[BUFSIZ]; Рис. 4.7 Сервер регистрации на основе ACE_WFMO_Reactor
Реализации ACE Reactor 145 DWORD count; if (IReadFile (h, user_input, BUFSIZ, &count, 0)) return -1; user_input[count] = *\0*; if (ACE_OS_String::strncmp (user_input, "quit", 4) == 0) return -1; return 0; ) Когда поступает команда «quit», handle_input С) возвращает -1, что приводит к запуску ACE_WFMO_Reactor с целью осуществления диспетчери- зации Quit_Handler::handle_close(): virtual int handle_close (ACE_HANDLE, ACE_Reactor_Mask) { quit_seen_.signal (); return 0; ) Дескриптор события, связанного с подаваемой командой, из quit_seen_ был зарегистрирован у реактора в конструкторе Quit_Handler. Когда событие подает сигнал в методе handle_close (), ACE_WFMO_Reactor осуществляет демультиплексирование данного события и вызывает метод Quit_Hand- ler: : handle_signal (), приведенный ниже. virtual int handle_signal (int, siginfo_t *, ucontext_t *) { reactor ()->end_reactor_event_loop (); return 0; } Данный hook-метод вызывает метод end_reactor_event_loop () реак- тора, который останавливает все потоки, обрабатывающие события. Можно вызвать end_reactor_event_loop () прямо из handle_close (). Мы пе- ренесли этот вызов в handle_signal (), чтобы показать, как использовать дескриптор с сигнальным состоянием с ACE_WFMO_Reactor. Когда объект Quit_Handler уничтожается, его деструктор выполняет следующие действия: 1 ~Quit_Handler () { 2 ACE_Event_Handler::remove_stdin_handler 3 (reactor (), ACE_Thread_Manager::instance ()); 4 reactor ()->remove_handler (quit_seen_.handle (), 5 ACE Event Handler::DONT CALL); 6 } Строки 2-3 Завершаем действия, связанные с предыдущим вызовом ге- gister_stdin_handler(). Строки 4-5 Отменяем регистрацию данного дескриптора события у реак- тора. Флаг DONT_CALL предупреждает реактор о том, что не следует делать об- ратный вызов handle_close (), так что очистка в этот момент завершается и нам не нужно беспокоиться о возможности более позднего обратного вызова по указателю на недействительный объект. Из-за отличий в параллелизме между ACE_WFMO_Reactor и реакторами на базе select () (приведенными в разделах 4.2 и 4.3), мы создаем новый про-
146 Глава 4 Блок 27: Классы ACE Manual Event и ACE Auto Event АСЕ предоставляет два класса интерфейсных фасадов синхронизации, кото- рые должны быть известны Windows-программистам: ACE_Manual_Event и ACE_Auto_Event. Эти классы позволяют потокам некоторого процесса ждать наступления события или информировать другие потоки о наступлении некоторого события в потокобезопасном стиле. В Windows эти классы являют- ся интерфейсными фасадами внутренних объектов Windows, связанных с со- бытиями, тогда как на других платформах АСЕ эмулирует возможность работы с объектами событий в стиле Windows. События похожи на условные переменные в том смысле, что поток может ис- пользовать их для того, чтобы или сообщать о наступлении определенного при- ложением события, или ждать наступления этого события. Однако в отличие от не сохраняющих состояния условных переменных, событие с сигнальным со- стоянием остается установленным пока не произойдет действие, определяе- мое классом. Например, событие ACE_Manuai_Event остается установлен- ным пока не будет явно сброшено, а асе Auto_Event остается установлен- ным пока у него есть хоть один ожидающий поток. Эти два класса позволяют пользователям контролировать количество потоков, возобновляющих работу по сигналам от операций и предоставляют событию информировать об изме- нении состояния, даже если в тот момент, когда событие переходит в сигналь- ное состояние, отсутствуют ожидающие потоки. События являются более затратными, чем мьютексы, но обеспечивают лучший контроль за планированием потоков. События предлагают более простой ме- ханизм синхронизации, чем условные переменные. Тем не менее, условные переменные более полезны для реализации сложных условий синхрониза- ции, так как они дают возможность потокамЖдать выполнения условий, зада- ваемых выражениями любой сложности. изводный класс Logging_Event_Handler_WFMO с целью защиты от воз- можных состояний гонок. Нам нужно всего лишь перегрузить hook-метод handle_input () Logging_Event_Handler_Ex и добавить мьютекс с це- лью явной сериализации доступа потоков пула к соединению с клиентским де- моном регистрации следующим образом: class Logging_Event_Handler_WFMO : public Logging_Event_Handler_Ex { public: Logging_Event_Handler_WFMO (ACE_Reactor *r) : Logging_Event_Handler_Ex (r) {} protected: int handle_input (ACE_HANDLE h) { ACE_GUARD_RETURN (ACE_SYNCH_MUTEX, monitor, lock_, -1); return logging_handler_.log_record (); ) ACE_Thread_Mutex lock_; // Сериализует потоки пула. };
Реализации ACE Reactor 147 Так как Logging_Acceptor_Ex создает новый экземпляр объекта Log- ging_Event_Handler_Ex для каждого нового клиентского соединения, то использование нами другого класса обработчика событий требует нового клас- са-акцептора. Следующий производный от Logging_Acceptor_Ex класс создает экземпляр обработчика событий нужного типа при поступлении ново- го клиентского соединения: class Logging_Acceptor_WFMO : public Logging_Acceptor_Ex { public: Logging_Acceptor_WFMO (ACE_Reactor *r - ACE_Reactor::instance ()) : Logging_Acceptor_Ex (r) {} protected: virtual int handle_input (ACE—HANDLE) { Logging_Event_Handler_WFMO *peer_handler = 0; ACE_NEW—RETURN (peer_handlerf Logging_Event_Handler_WFMO (reactor ()), -1) ; if (acceptor_.accept (peer_handler->peer ()) == -1) { delete peer_handler; return -1; } else if (peer—handler->open () == -1) { peer_handler->handle_close (); return -1; } return 0; } 1; Метод handle_input () не требует защиты от состояний гонок, так как он оперирует только с объектами локальными относительно данного метода. Фактически, за исключением типа обработчика событий, экземпляр которого создается для каждого нового соединения, Logging-Acceptor_WFMO и Log- ging-Acceptor_Ex являются идентичными. ACE-каркас Acceptor-Connector в главе 7 показывает как факторизовать тип обработчика событий в повторно используемый класс-акцептор. Функция main () нашего сервера регистрации ACE-WFMO-Reactor пока- зана ниже: ♦include "асе/Reactor.h" ♦include "ace/Synch.h" ♦include "ace/WFMO-Reactor.h" ♦include "ace/Thread—Manager.h" ♦include "Reactor_Logging—Server.h" // Предварительное объявление. ACE—THR—FUNC—RETURN event-loop (void *); typedef Reactor_Logging_Server<Logging_Acceptor_WFMO> Server—Logging_Daemon; int main (int argc, char *argv[]) {
148 Глава 4 const size_t N_THREADS = 4; ACE_WFMO_Reactor wfmo_reactor; ACE_Reactor reactor (&wfmo_reactor); Server_Logging_Daemon ‘server = 0; ACE_NEW_RETURN (server, Server_Logging_Daemon (argc, argv, &reactor), 1); Quit_Handler quit_handler (&reactor); ACE_Thread_Manager::instance ()->spawn_n (N_THREADS, event_loop, &reactor); return ACE Thread Manager::instance ()->wait (); ) Основными отличиями данной функции main () от функции main () из предыдущего примера являются: • Реализация Reactor_Logging_Server создается с Logging_Accep- tor_WFMO, а не с Logging_Acceptor_Ex. • Вместо ACE_TP_Reactor используется ACE_WFMO_Reactor. • Управляющий поток, назначение которого организовать завершение ра- боты, заменяется экземпляром Quit_Handler, описанным выше. •Вызовы демультиплексора событий WaitForMultipleObjects () могут действительно выполняться параллельно разными потоками, вме- сто сериализации вызовов select () с помощью паттерна Leader/Fol- lowers, как в случае ACE_TP_Reactor. • Эта программа будет работать не на всех платформах, поддерживаемых АСЕ, а только на Windows-платформах. • Мы не используем API реактора-синглтона, а вместо этого снова исполь- зуем экземпляр локального реактора. 4.5 Резюме В данной главе приведено описание устройства наиболее распространен- ных реализаций ACE_Reac tor и проиллюстрированы некоторые отличия в их возможностях. Дополнительно к отдельным указаниям, когда и как использо- вать ту или иную реализацию, наши рассуждения касались, в основном, двух пунктов: 1. Различные реализации механизмов ОС демультиплексирования событий могут создавать существенные проблемы, впрочем, как и предоставлять важные возможности. 2. Правильно спроектированный каркас может быть расширен с целью эф- фективного использования возможностей ОС за счет инкапсуляции слож- ностей дизайна в каркасе, а не в прикладном коде.
Реализации АСЕ Reactor 149 Каркас, спроектированный с умом, может существенно улучшить перено- симость и расширяемость и приложений, и самого каркаса. Реализации ACE-каркаса Reactor являются хорошими примерами того, как использование паттернов (patterns), таких как Wrapper Facade, Facade и Bridge, и возможностей (features) C++, таких как наследование и динамическое связывание, могут при- вести к созданию высококачественного и, в значительной степени, повторно используемого каркаса. Реализации ACE-каркаса Reactor, описанные в данной главе, инкапсулируют многие сложно реализуемые возможности, позволяя разработчикам сетевых приложений сосредоточиться на проблемах, связанных с самими приложениями. Одной из самых мощных возможностей проектирования с использовани- ем ACE-каркаса Reactor является его способность улучщать расширяемость в следующих направлениях: • Настройка обработчиков событий. Можно достаточно просто расши- рять функциональность приложений путем наследования от класса ACE_Event_Handler или от одного из его предопределенных произ- водных ACE-классов (таких как, ACE_Service_Object, ACE_Task или ACE_S vc_Handle г) и избирательно реализуя нужные виртуальные методы. Например, в главе 7 будет показано, как обработчики событий в наших клиентском и серверном демонах регистрации могут быть про- зрачно настроены на поддержку аутентификации. • Настройка реализаций ACE_Reactor. Можно достаточно просто моди- фицировать лежащие в основе ACE_Reactor механизмы демультип- лексирования событий, не затрагивая существующего прикладного кода. Например, перенос взаимно-согласованного (reactive) сервера регистра- ции с UNIX-платформы на Windows-платформу не требует сколько-ни- будь заметных изменений кода приложения. Наоборот, перевод С-реали- зации этого сервера с select () на WaitForMultipleObjects () пред- ставляет собой довольно большую и подверженную ошибкам работу. За прошедшее десятилетие применение АСЕ в новых средах породило но- вые требования, связанные с поддержкой событийно-управляемых приложе- ний. Например, интеграция с графическим интерфейсом стала важной обла- стью применения благодаря новым инструментальным средствам GUI и необ- ходимости использования циклов обработки событий. Благодаря модульной структуре ACE-каркаса Reactor, было проще сделать следующие новые реализа- ции Reactor: Класс АСЕ Описание ACE_Dev_Poll_Reactor Использует демультиплексор /dev/poii или /dev/epoii. Спроектирован так, чтобы быть более масштабируемым, чем реакторы на базе select о. ACE_Priority_Reactor Осуществляет диспетчеризацию событий с порядком приоритетов, назначенным разработчиками.
150 Глава 4 Класс АСЕ Описание ACE_XtReactor Интегрирует АСЕ и Х11 Toolkit. ACE_FlReactor Интегрирует АСЕ и GUI-каркас Fast Ught (FL). ACE_QtReactor Интегрирует АСЕ и GUI-библиотеку Qt. ACE_TkReactor Интегрирует АСЕ и GUI-библиотеку TCL/Tk. ACE Msg WFMO Reactor 1 “ Добавляет к асе WFMO_Reactor обработку сообщений Windows. Об этих реакторах можно прочитать в документации по АСЕ по адресам: http://асе.есе.uci.edu/Doxygen/иhttp://www.riverace.com/docs/.
Глава 5 АСЕ-каркас Service Configurator Краткое содержание В данной главе описывается структура и применение ACE-каркаса Service Configurator, который реализует паттерн Component Configurator [POSA2]. Этот паттерн повышает расширяемость и гибкость приложений за счет разде- ления (1) процесса функционирования служб и (2) того момента времени, ко- гда из реализаций этих служб формируются приложения. В завершение главы показано как АСЕ-каркас Service Configurator помогает повысить расширяе- мость нашего сервера сетевой регистрации. 5.1 Обзор В разделе 2.2 были рассмотрены аспекты проектирования, связанные с име- нованием и связыванием (linking), которые разработчикам необходимо учиты- вать при выборе конфигурации сетевых приложений. Стратегией учета этих аспектов, открытой для расширения, является применение паттерна проекти- рования Component Configurator [POSA2]. Этот паттерн позволяет приложе- нию перестраивать структуру своих служб во время выполнения, не требуя мо- дификации, повторной компиляции или компоновки самой программы, или прекращения и последующего возобновления работы данного приложения. АСЕ-каркас Service Configurator является переносимой реализацией пат- терна Component Configurator, позволяющего приложениям откладывать ре- шения, связанные с выбором конфигурации и реализации их служб на более поздний этап цикла проектирования — на момент инсталляции или даже на время выполнения. АСЕ-каркас Service Configurator поддерживает возмож- ность избирательной активизации служб во время выполнения, независимо от того, к какому типу относятся эти службы: • Статические службы, которые компонуются в прикладную программу статически.
152 Глава 5 • Динамические службы, которые компонуются из одной или нескольких динамических библиотек (DLL). Каждой службе могут передаваться аргументы в стиле argc/argv с целью установки некоторой информации во время выполнения. Благодаря дизайну АСЕ, состоящему из интегрируемых каркасов, для служб, использующих АСЕ- каркас Service Configurator, диспетчеризация может осуществляться АСЕ-кар- касом Reactor. В данной главе рассматриваются следующие классы ACE-каркаса Service Configurator: Класс АСЕ Описание ACE_Service_Object Определяет унифицированный интерфейс, который использует АСЕ-каркас Service Configurator для выбора конфигураций и управления реализациями служб. Операции управления включают инициализацию, приостановку, возобновление и завершение работы службы. АСЕ_S e rv i ce^Repo s i to гу Центральный репозиторий для всех служб, управляемых с помощью ACE-каркаса Service Configurator. Обеспечивает методы для. локализации, получения информации о состоянии и управлёния всеми службами, входящими в некоторое приложение. ACE_Service_ Repository_Iterator Переносимый механизм перебора всех служб некоторого репозитория. ACE—Service_Config Интерпретатор для анализа и выполнения сценариев, определяющих какие службы объединить (или переконфигурировать) в приложение (например, подключая и отключая библиотеки DLL), а также какие службы приостановить, а какиевозобновить. Наиболее важные связи между классами ACE-каркаса Service Configurator приведены на рис. 5.1. Эти классы, в соответствии с паттерном Component Con- figurator [POSA2], играют следующие роли: • Классы уровня управления конфигурациями реализуют независимые от приложений стратегии установки, инициализации, управления и за- вершения работы объектов служб. Классы уровня управления конфигу- рациями ACE-каркаса Service Configurator включают ACE_Servi- ce_Config, ACE_Service_Repository и ACE_Service_Reposi- tory_Iterator. • Классы прикладного уровня реализуют конкретные службы, выпол- няющие прикладную обработку. В ACE-каркасе Service Configurator классы прикладного уровня являются производными от ACE_Servi- се Object, который, в свою очередь, наследует от АСЕ Event Hand-
АСЕ-каркас Service Configurator 153 ler (глава 3), позволяя, таким образом, объектам служб динамически подключаться, отключаться и участвовать в работе ACE-каркаса Reactor. АСЕ-каркас Service Configurator обеспечивает следующие преимущества: • Гибкость (flexibility). Каркас позволяет разработчикам выбирать из мно- жества служб и их реализаций, которые могут объединяться во время вы- полнения. Выбор того, какую службу выполнять и в каком узле, или уз- лах, сети может быть сделан в любой момент: от момента компоновки приложения до того момента, когда службы начинают реально выпол- няться. У разработчиков есть также возможность, где нужно, ограничи- вать выбор (например, не предлагать динамическое связывание служб). Рис. 5.1 Классы ACE-каркаса Service Configurator • Способность изменять конфигурацию (configurability). Разработчики могут настраивать и конфигурировать характеристики приложения, из- меняя почти каждый аспект службы и ее развертывания. Службы могут быть настроены на чтение обычных конфигурационных данных, таких как номера портов, сетевые адреса и расположение файловых систем. Кроме того, службы предоставляют возможность откладывать принятие решений, связанных с настройкой и качеством функционирования, до того момента, когда станет доступна необходимая информация, позво- ляющая их скорректировать. Например, в зависимости от возможности многопоточной обработки на платформе времени выполнения и количе- ства центральных процессоров, выполнение множества служб в отдель- ных потоках или отдельных процессах может оказаться более или менее эффективным. АСЕ-каркас Service Configurator дает возможность адми- нистраторам сайта,' владеющим всей необходимой информацией, на- страивать приложения или выбирать приложения и гибко настраивать их работу в процессе.выполнения, то есть тогда, когда имеется достаточ- но информации для того, чтобы обеспечить удовлетворение запросов клиентов имеющимися ресурсами системы. • Управляемость (managability). Вся информация о конфигурации может быть сохранена в файле сценария конфигурации svc. conf. Каркас ис-
154 Глава 5 пользует эти сценарии для загрузки и конфигурирования служб. У про- цедуры установки приложения есть возможность записывать установоч- ную информацию в файл svc. conf. Администраторы тоже могут, если нужно, редактировать и настраивать эту информацию, без повторной сборки самого приложения. Приложения могут формировать свои соб- ственные директивы конфигурации и передавать их непосредственно ACE-каркасу Service Configurator. Каркас группирует службы приложе- ния в одну администрируемую единицу и предоставляет приложению возможность сообщать информацию о своих службах и об их состоянии. * Согласованность (consistency). Каркас устанавливает единообразный интерфейс для инициализации, приостановки, возобновления и завер-* шения работы службы. Это единообразие обеспечивает согласованность действий пользователей каркаса и позволяет относиться к службам как к строительным блокам, которые способны гибко объединяться с целью формирования законченных приложений. • Удобство сопровождения (maintainability). Разделение реализации служб и их конфигурирования в сетевые приложения, обеспечиваемое каркасом, позволяет реализациям служб развиваться с течением времени независимо от тех сетевых приложений, в которые они включены. Каж- дая служба может разрабатываться и тестироваться независимо, что уп- рощает последующее объединение служб и расширяет их повторное ис- пользование. * Повышенные динамизм (dynamism) и управляемость (control). Каркас предоставляет возможность динамически конфигурировать службу без модификации, повторной статической компиляции или компоновки су- ществующего кода. Каждую службу можно конфигурировать независи- мо от других служб, не прерывая и не возобновляя работу серверного процесса. Такого рода возможности реконфигурации часто требуются приложениям с высокой степенью готовности (high-availability), ответст- венным (mission-critical) системам, выполняющим обработку онлайно- вых транзакций или телефонных вызовов. Далее в данной главе приводится обоснование включения каждого из клас- сов в АСЕ-каркас Service Configurator, и описываются возможности классов каркаса. Показано также, как использовать данный каркас, чтобы улучшить расширяемость нашего сервера сетевой регистрации. Если вы незнакомы с пат- терном Component Configurator [POSA2], мы рекомендуем, прежде чем углуб- ляться в детали примеров этой главы, ознакомиться с этим паттерном. 5.2 Класс ACE Service Object Обоснование Конфигурирование служб и управление их жизненным циклом включает следующие аспекты, которые мы кратко перечисляли выше:
АСЕ-каркас Service Configurator 155 • Инициализация (initialization). Служба должна быть проинициализи- рована. Инициализация может заключаться в создании одного или не- скольких объектов или вызове метода фабрики. Параметры конфигура- ции передаются службе во время инициализации. • Управление выполнением (execution control). Некоторым приложени- ям нужна возможность приостанавливать и возобновлять работу служб. Предоставление такой возможности требует механизма, посредством ко- торого управляемое приложение может находить нужные ему службы и затем устанавливать с ними связь, чтобы иметь возможность запросить или выполнить операцию приостановки/возобновления (suspend/resu- me). • Уведомление (reporting). Ответственным (mission-critical) службам час- то требуется возможность унифицированным образом отвечать на за- просы об информации, касающейся их статуса и готовности. • Завершение (termination). Упорядоченное завершение работы процес- сов требуется для того, чтобы обеспечить своевременное освобождение ресурсов служб, необходимое обновление информации о состоянии и правильную последовательность завершения работы служб. Разработка и реализация этих возможностей под конкретный случай часто приводит к сильно взаимосвязанным структурам данных и классам, которые трудно развивать и повторно использовать в будущих проектах. Кроме того, если несколько проектов или групп разработчиков выполняют похожую работу, основные преимущества конфигурирования служб будут ут- рачены, так как ситуация, когда несколько проектов взаимодействуют на уров- не управления или на уровне служб, представляется маловероятной. Так как процессы конфигурирования служб и управления ими, в значи- тельной степени, являются независимыми от приложений, они являются хоро- шими кандидатами на включение в каркас. Создание унифицированного ин- терфейса всех сетевых служб упрощает согласованное конфигурирование и управление ими. В свою очередь, эта согласованность упроЩает разработку приложений и их развертывание за счет частичного решения основных про- блем, присущих созданию повторно используемых средств административного регулирования. Чтобы обеспечить унифицированный интерфейс между АСЕ- каркасом Service Configurator и определяемыми приложениями службами, каж- дая служба должна быть производной от общего базового класса ACE_Ser- vice_Object. Функциональные возможности класса ACE_Service_Object обеспечивает унифицированный интерфейс, ко- торый позволяет конфигурировать реализации служб и управлять ими с помо- щью ACE-каркаса Service Configurator. Данный класс обеспечивает следующие функциональные возможности:
156 Глава 5 • Предоставляет hook-методы, которые инициализируют службу (напри- мер, выделяют ей ресурсы) и завершают ее работу (например, освобож- дают ресурсы). • Предоставляет hook-методы, которые временно приостанавливают ра- боту службы и возобновляют работу приостановленной службы. • Предоставляет hook-методы, которые позволяют получить основную информацию о службе, например, ее назначение, текущее состояние и номер порта, на котором она ждет поступления клиентских запросов на соединение. Вызовы этих методов обычно реализуются как обратные вызовы от АСЕ- каркаса Service Configurator, в процессе интерпретации этим каркасом дирек- тив конфигурации, приведенных в разделе 5.4. Интерфейс ACE_Service_Obj ect показан на рис. 5.2. Для классов произ- водных от ACE_Service_Ob j ect, которые наследуют от ACE_Event_Hand- ler и ACE_Shared_Ob j ect, диспетчеризация может осуществляться с помо- щью ACE-каркаса Service Configurator, и они могут динамически подключаться из DLL и отключаться. Основные hook-методы ACE_Service_Object, свя- занные с конфигурированием, перечислены в следующей таблице: Метод Описание init () Используется каркасом, чтобы проинструктировать службу, что она должна осуществить инициализацию самостоятельно. Аргументы, в стиле «argc/argv», могут быть переданы init () для управления инициализацией службы. fini () Используется каркасом, чтобы проинструктировать службу, что она должна самостоятельно завершить свою работу. Этот метрд обычно выполняет операции, связанные с завершением работы, которые освобождают ресурсы службы, такие как память, блокировки синхронизации или дескрипторы ввода/вывода. suspend() resume() Используется каркасом, чтобы проинструктировать службу, что она должна приостановить или возобновить свою работу. info () Используется для запроса информации о самой службе, такой как имя службы, ее назначение и сетевой адрес. Клиенты могут запрашивать сервер с целью получения указанной информации и использовать эту информацию для связи с конкретной службой, работающей на данном сервере. Все вместе эти hook-методы реализуют унифицированный интерфейс ме- жду АСЕ^каркасом Service Configurator и прикладными службами, которыми он управляет. Прикладные службы, которые наследуют от ACE_Service_Object, мо- гут избирательно подменять его hook-методы, обратный вызов которых осу- ществляется АСЕ-каркасом Service Configurator в ответ на конкретные события. Например, hook-метод in it () служебного объекта вызывается тогда, когда каркас Service Configurator выполняет директиву активизации данной службы
АСЕ-каркас Service Configurator 157 Рис. 5.2 Класс ACE_Service_Obj ect (и динамические, и статические директивы активизации службы, как показано в разделе 5.4). Hook-метод init () должен возвращать 0, если инициализация завершилась успешно и -1, если она завершилась неудачно. Если (и только если) init () завершился успешно, соответствующий метод fini() будет вызван для данного объекта службы, когда АСЕ-каркас Service Configurator бу- дет выполнять директивы удаления данной службы, или завершения работы всех служб. Service Configurator — это первый из рассмотренных нами АСЕ-каркасов, который интенсивно взаимодействует с администраторами или приложения- ми. Такие взаимодействия связаны с необходимостью оперировать наборами локальных символов. На рис. 5.2 показан тип ACE_TCHAR, который помогает АСЕ оперировать с наборами не-ASCII символов в переносимом стиле. Воз- можности АСЕ работать с символами расширенного 16-битного алфавита и с символами Unicode, описаны в блоке 28. Мы будем пользоваться этой воз- можностью для обработки строк символов. Пример Чтобы проиллюстрировать возможности класса ACE_Service_Object, мы повторно реализуем наш взаимно-согласованный сервер регистрации из примера раздела 3.5. Эту версию можно будет конфигурировать динамически с помощью ACE-каркаса Service Configurator, вместо статического конфигури- рования программы main (), приведенной в разделе 3.5. Чтобы достичь этой цели, мы используем паттерн Adapter [GoF) для создания следующего шаблон- ного класса в заголовочном файлеReactor_Logging_Server_Adapter.h: template <class ACCEPTOR> class Reactor_Logging_Server_Adapter : public ACE_Service_Object { public: // Hook-методы, унаследованныё от <ACE_Service_Object>. virtual int init (int argc, ACE TCHAR *argv[]);
158 Глава 5 Блок 28: Как АСЕ работают с «узкими» и «широкими» символами Разработчикам за пределами США хорошо известно, что многие используе- мые в настоящее время наборы символов требуют больше одного байта, или октета, для представления каждого символа. Символы, которые требуют боль- ше одного октета, называются «широкими» символами (wide characters). Наи- более популярным многооктетным стандартом является ISO/IEC 10646, универ- сальный набор символов с многооктетным кодом (Universal Multiple-Octet Coded Character Set, UCS). Unicode представляет собой отдельный стандарт, но, в основном, является ограниченным подмножеством UCS, который исполь- зует два октета для каждого символа (UCS-2). Многие Windows-программисты знакомы с Unicode. В C++ «широкие» символы представляются типом wchar_t, что позволяет мето- дам предоставлять множество сигнатур, которые отличаются типом символов. «Широкие» символы имеют, тем не менее, отдельный набор С-функций, мани- пулирующих строками, и существующий код C++, например, строковые кон- станты, требуют изменения для использования «широких» символов. Как след- ствие, программирование приложений, использующих строки «широких» сим- волов, может оказаться достаточно дорогостоящим, особенно в том случае, когда приложения, первоначально написанные для рынка США, нужно лока- лизовать для других стран. Чтобы улучшить переносимость и упростить исполь- зование, АСЕ использует перегрузку методов C++ и макросы, описанные ниже, чтобы использовать разные типы символов, не изменяя API: Макрос Применение ] ACE_HAS_WCHAR Настройка конфигурации на компоновку АСЕ J с методами «широких» символов. .1 ACE„USES_WCHAR Настройка конфигурации, которая указывает АСЕ, использовать внутри «широкие» символы. ACEJCHAR Определяется или как char, или как wchar t, чтобы совпадать с внутренним размером символов в АСЕ ’ ACEJEXT(str) Корректно определяет строковую константу str на базе acejjses wchar ACE _TEXT_CHAR_TO„TCHAR (str) Преобразует, если нужно, строку char *, к формату ACE^TCHAR ACE JEXLALWAYS_CHAR (str) Преобразует, если нужно, строку ACEJCHAR к формату char * АСЕ должен быть скомпонован с установкой конфигурации ace_has_wchar для тех приложений, которые используют «широкие» символы. Кроме того, АСЕ должен быть скомпонован с установкой конфигурации ace_uses_wchar, если внутри.АСЕ должны использоваться «широкие» символы. Использование мак- росов acejtchar и асе_техт проиллюстрировано в примерах в этой книге. АСЕ предоставляет также два строковых класса, ACE_cstring и ace_wstring, которые хранят, соответственно, «узкие» и «широкие» символы. Эти классы аналогичны стандартным строковым классам C++, но могут настраиваться на использование специализированных распределителей памяти и являются бо- лее переносимыми. ACEjrstring представляет собой typedef для одного или двух типов строк в зависимости от установки конфигурации ACE_USES_WCHAR.
АСЕ-каркас Service Configurator 159 virtual int fini (); virtual int info (ACE_TCHAR **, size_t) const; virtual int suspend (); virtual int resume (); private: Reactor_Logging_Server<ACCEPTOR> *server_; ); Этот шаблон наследует от класса ACE_Service_Ob j ect и содержит ука- затель на объект Reactor_Logging_Server. Мы создали реализацию этого шаблона с параметром-классом ACCEPTOR, чтобы отложить выбор фабри- ки-акцептора (acceptor factory) на более поздний этап цикла проектирования. Паттерн Adapter является здесь подходящим вариантом, поскольку он позво- ляет использовать повторно уже существующий класс Reactor_Log- ging_Server. Тем не менее, если бы мы создавали этот пример, с возможностью конфигурирования службы, с нуля, более прямым путем было бы унаследовать Reactor_Logging_Server не от ACE_Event_Handler, а от ACE_Ser- vice_Object. В таком случае, переходной класс (adapter class).был бы не ну- жен, и мы, по-прежнему, могли бы отложить выбор фабрики-акцептора на бо- лее позднее время. Рис. 5.3 иллюстрирует жизненный цикл объектов этого примера при дина- мической конфигурации реализации Reactor_Logging_Server_Adapter. При конфигурации данной службы в адресном пространстве приложения, АСЕ-каркас Service Configurator создает экземпляр Reactor_Logging_Ser- ver_Adapter и автоматически вызывает следующий hook-метод init (): 1 template <class ACCEPTOR> int 2 Reactor_Logging_Server_Adapter<ACCEPTOR>::init 3 (int argc, ACE_TCHAR *argv[]) 4 { 5 int i; 6 char **array = 0 ; 7 ACE_NEW_RETURN (array, char*[argc], -1); 8 ACE_Auto_Array_Ptr<char *> char_argv (array) ; 9 10 for (i =0; i < argc; ++i) 11 char_argv[i] =ACE::strnew (ACE_TEXT_ALWAYS_CHAR(argv[i])); 12 ACE_NEW_NORETURN (server_, Reactor_Logging_Server<ACCEPTOR> 13 (i, char_argv.get (), 14 ACE_Reactor::instance ())); 15 for (i = 0; i < argc; ++i) ACE::strdelete (char_argv[i]); 16 return server == 0 ? -1 : 0; 17 } Строки 5-11 АСЕ-каркас Service Configurator передает ar gv как массив ука- зателей ACE_TCHAR, а конструктор Reactor_Logging_Server принимает массив char*. Метод init () использует поэтому макрос ACE_TEXT_AL-
160 Глава 5 Service Configurator Framework Reafctor Logging Server Adapter "create init () Reactor Logging Server "create fini () handle close() ± < < "destroy К Рис. 53 Жизненный цикл сервера регистрации Dynamic Reactor WAYS_CHAR для преобразования, если нужно, к формату char. Этот макрос создает временный объект с преобразованной строкой, который затем копиру- ется методом АСЕ: : strnew (), чтобы сохранить его с помощью конструктора Reactor_Logging_Server. В блоке 29 описаны методы АСЕ:: strnew () и АСЕ::strdelete (). Строки 12-14 Динамически создаем экземпляр Reactor_Logging_Ser- ver, который включает реактор, акцептор и обработчики. Строка 15 Освобождаем память, использовавшуюся под преобразованные строки argv. Когда поступает команда удалить динамически сконфигурированную службу регистрации, АСЕ-каркас Service Configurator вызывает hook-метод Reactor_Logging_Server_Adapter: : fini (), приведенный ниже: template <class ACCEPTOR> int Reactor_Logging_Server_Adapter<ACCEPTOR>: :fin'i () { server_->handle_close (); server^ = 0; return 0; ) Этот метод вызывает Reactor_Logging_Server: :handle_close (), который удаляет, созданный методом init(), объект Reactor_Log- ging_Server. АСЕ-каркас Service Configurator использует gobbler-функцию (см. блок 32) для удаления служебного объекта после вызова его hook-метода fini (). Поэтому мы не должны использовать в fini() delete this. Hook-метод inf о () сообщает информацию, касающуюся службы, когда кар- кас ее запрашивает. Наш метод info() форматирует строку, содержащую TCP-порт, который он прослушивает:
АСЕ-каркас Service Configurator 161 1 template <class ACCEPTOR> int 2 Reactor_Logging_Server_Adapter<ACCEPTOR>::info 3 (ACE_TCHAR **bufferp, size_t length) const ( 4 ACE_TYPENAME ACCEPTOR::PEER_ADDR local_addr; 5 server_->acceptor ().get_local_addr (local_addr); 6 7 ACE_TCHAR bu f(BUFSIZ] ; 8 ACE_OS::sprintf (buf, 9 ACE_TEXT ("%hu"), 10 local_addr.get_port_number ()); 11 ACE_OS_String::strcat 12 (buf, ACE_TEXT ("/tcp # Reactive logging server\n")); 13 if (*bufferp == 0) *bufferp = ACE.::strnew (buf); 14 else ACE_OS_String: :-strncpy (*bufferp, buf, length); 15 return ACE_OS_String::strlen (*bufferp); 16 ) Строки 4-5 Получаем сетевой адрес от экземпляра ACE_SOCK_Acceptor, который использует Reactor_Logging_Server. Строки 7-12 Форматируем сообщение, объясняющееся, что данная служба делает и как с ней связаться. Строка 13 Если вызывающая сторона не предоставляет буфер для хране- ния форматированного сообщения, выделяем буфер и копируем данное сооб- щение в метод АСЕ: : strnew (), который его использует. В этом случае, вызы- вающая сторона должна использовать АСЕ: : strdelete (), чтобы освободить буфер. АСЕ не указывает каким образом реализация inf о () должна выделять память. Разработчики, пишущие реализацию inf о (), должны, поэтому опре- делять й недвусмысленно документировать политику своих реализаций. Очень рекомендуется, чтобы разработчики использовали АСЕ: : strnew () для выде- ления памяти для строки, и требовали от своих пользователей вызывать АСЕ: :strdelete() для освобождения выделенной памяти. В блоке 29 приве- дено обоснование такого подхода. Строка 14 Если вызывающая сторона предоставила буфер для сообщения, копируем сформатированное сообщение в этот буфер, ограничивая его дли- ной, переданной вызывающей стороной. Строка 15 Возвращаем длину сообщения. В отличие от других hook-методов ACE_Service_Object, приведенных на рис. 5.2, метод inf о () не всегда вызывается ACE-каркасом Service Configu- rator, даже если может быть вызван. Вместо этого, он часто вызывается самой серверной программой, как показано в методе Service_Reporter: :hand- le_input (). Кроме того, разработчики приложений могут выбрать наиболее полезное содержание данного сообщения, так как i n f о () не предписывает ни- какого конкретного формата. Hook-методы suspend () и resume () похожи друг на друга. template <class ACCEPTOR> int Reactor_Logging_Server_Adapter<ACCEPTOR>::suspend () { return server_->reactor ()->suspend_handler (server_); } 6 Программирование сетевых приложений на C++. Том 2
162 Глава 5 Блок 29: Переносимые операции с кучей в АСЕ Библиотечные функции И классы, такие как Reactor Logging_Server_Adap- ter:: inf о (), -часто выделяют память динамически. Память, динамически вы- деленную в программах на C++, в конечном итоге нужно освобождать. Чтобы программы на C++ были переносимы, важно согласовывать операции выде- ления и освобождения памяти, чтобы избежать разрушения кучи (freestore). На удивление распространенным является неправильное представление о том, что если просто обеспечить согласованность вызовов operator new () И operator delete () (или вызовов ma 11 ос () и free О), то этого достаточно для корректного управления кучей. Такое мнение основано на неявном допуще- нии, что существует одна универсальная куча на каждый процесс. На практи- ке, однако, куча—это просто область памяти, управляемая некоторым компо- нентом времени выполнения (run-time component), таким как С или C++ биб- лиотека времени выполнения. Если исполняемая программа взаимодействует с несколькими экземплярами библиотек времени выполнения, то это все рав- но, как если бы было несколько куч. Например, Windows предоставляет множество вариантов C/C++ библиотек времени выполнения, таких как Debug и Release, и многопоточных и однопоточ- ных. Каждый из этих вариантов поддерживает свою собственную кучу. Память, выделенная из одной кучи, должна быть возвращена в ту же самую кучу. Таким образом, корректное управление кучей требует не только правильного согла- сования вызовов методов/ф’ункций, но и их реализации через одни и те же биб- лиотеки времени выполнения. Эти требования легко нарушить, когда код одной подсистемы или библиотеки освобождает память, которую выделила другая. С целью содействия переносимому управлению динамической памятью, АСЕ предлагает согласованные методы выделения и освобождения памяти, пере- численные в следующей таблице: Метод Использование ace::strnew() Выделяет память для копии строки символов и копирует в нее строку. ACE::strdelete () Освобождает память, выделенную str new (). iACE_OS_Memory::malloc () Выделяет блок памяти заданного объема. iACE_OS_Memory::calloc() Выделяет блок памяти для хранения заданного количества объектов заданного объема. Со- держимое выделяемой памяти явно инициали- зируется 0. ACE_OS^Memory::realloc() Изменяет объем блока памяти, выделенного 1 ACE_OS_Memory::malice(). ’ ACE_OS_Memory::free() : Освобождает память, выделенную любым из i трех методов ACE_os_Memory, приведенных i выше. До тех пор пока разработчики правильно согласовывают методы АСЕ, связан- ные с выделением и освобождением памяти, АСЕ гарантирует, что будут вызы- ваться нужные функции библиотек времени выполнения для соответствующих куч на всех платформах. Полную информацию об этих методах можно найти в онлайновой справочной документации по АСЕ: http: //ace.есе.uci. edu/Do- худеп/И http://www.riverace.com/docs/.
АСЕ-каркас Service Configurator 163 template <class ACCEPTOR> int Reactor_Logging_Server_Adapter<ACCEPTOR>::resume () { return server_->reactor ()->resume_handler (server_); ) Так как класс Reactor_Logging_Server является производным от ACE_Event_Handler, объект server_ может быть передан методам реакто- ра-синглтона suspend_handler () и resume_handler (). Оба метода осу- ществляют двойную диспетчеризацию метода Reactor_Logging_Ser- ver: : get_handle () для извлечения базового дескриптора сокета пассивно- го режима. Этот дескриптор сокета затем временно удаляется или заменяется в списке дескрипторов сокетов» обрабатываемых реактором-синглтоном. В примере раздела 5.4 показано как Reactor_Logging_Server_Adapter может быть динамически встроен в обобщенное серверное приложение и ис- ключен из него. 5.3 Классы ACE_Service_Repository Обоснование АСЕ-каркас Service Configurator поддерживает конфигурации серверов, реализующих как одну службу, так и несколько служб. В разделе 5.2 мы уже го- ворили о том, что задачи инициализации, управления выполнением, выдачи информации и завершения работы требуют, чтобы у прикладных служб был общий базовый класс каркаса. Чтобы каркас мог эффективно использовать возможности ACE_Service_Object, он должен сохранять информацию о службе в общеизвестном репозитории и иметь возможность обращаться к объектам службы и управлять ими индивидуально или коллективно. Прикладным службам многофункционального сервера тоже может потре- боваться доступ друг к другу. Чтобы избежать излишней зависимости служб и сохранить преимущества отложенного принятия решений о конфигурации, службы должны иметь возможность находить друг друга во время выполнения (run time). Таким образом, чтобы удовлетворить потребности каркаса и прило- жений, не требуя от разработчиков обеспечивать эти возможности для каждого случая отдельно, АСЕ-каркас Service Configurator включает классы ACE_Ser- vice_Repository и ACE_Service_Repository_Iterator. Функциональные возможности классов ACE_Service_Repository реализует паттерн Manager [Som98] с целью управления жизненным циклом объектов служб, конфигурируемых с помо- щью АСЕ-каркас Service Configurator, а также доступом к этим объектам. Дан- ный класс обеспечивает следующие функциональные возможности: • Следит за всеми реализациями служб, которые составляют приложение, и поддерживает информацию о состоянии каждой службы, например, является ли данная служба активной или находится в приостановленном состоянии.
164 Глава 5 • Обеспечивает механизм, посредством которого АСЕ-каркас Service Con- figurator включает службы, удаляет их, а также управляет ими. • Обеспечивает удобный механизм завершения работы всех служб, в об- ратной, по сравнению с инициализацией, последовательности. • Позволяет находить службу по ее имени. Интерфейс ACE_Service_Repository показан на рис. 5.4, а его основ- ные методы перечислены в следующей таблице: Метод Описание ACE_Service_Repository() open () Инициализирует репозиторий и выделяет память под его динамические ресурсы. ~ACE_Service_Repos itory() close () Прекращает работу репозитория и освобож- дает его динамически выделенные ресурсы. insert () Добавляет новую службу в заданный репозиторий. find() Находит один из элементов репозитория. remove() Удаляет имеющуюся службу из репозитория. suspend() Приостанавливает службу в репозитории. resume() Возобновляет приостановленную службу в репозитории. | instance () Статический метод, возвращающий указатель НО СИНГЛТОН ACE__Service__Repository. | ACE_Service_Repository связывает друг с другом следующие элементы: • Имя службы, представленное в виде строки символов. • Экземпляр ACE_Service_Type, который представляет собой класс, ис- пользуемый ACE-каркасом Service Configurator для подключения, ини- циализации, приостановки, возобновления, удаления и отключения служб от сервера статически или динамически. Класс ACE_Service_Type отвечает в каркасе за реализацию операций, необходимых для манипулирования сконфигурированными службами. АСЕ- каркас Service Configurator может использоваться для конфигурирования ста- тических и динамических служб, а. также для реализации возможностей, свя- занных с ACE_Module и ACE_Stream, которые изложены в разделах 9.2 и 9.3 соответственно. Класс ACE_Service_Type использует паттерн Bridge, который позволяет зависящим от типа службы данным и функциям, изменяться, не влияя на сам класс. Класс ACE_Se rvice_Type играет в паттерне Bridge роль Abstraction (аб- стракции), а класс ACE_Service_Type_Impl — роль Implementor (реализа- ции). Каждый из следующих далее классов играет роль конкретной реализации (Concreteimplementor), представляющей тип службы, которая может быть за- писана в репозиторий:
АСЕ-каркас Service Configurator 165 Рис. 54 Класс ACE_Service_Repository 1. ACE_Service_Obj ect_Type — Метод object() возвращает указатель на связанный с ним ACE_Service_Obj ect, рассмотренный в разделе 5.2. 2. ACE_Module_Type — Метод object() возвращает указатель на связанный с ним ACE_Module, рассмотренный в разделе 9.2. 3. ACE_Stream_Type — Метод object() возвращает указатель на связанный с ним ACE_Stream, рассмотренный в разделе 9.3. Для динамически связываемых объектов служб, ACE_Service_Type со- храняет также дескриптор той DLL, которая содержит исполняемый код данной службы. АСЕ-каркас Service Configurator использует этот дескриптор, чтобы
166 Глава 5 отключить (unlink) и выгрузить (unload) объект службы работающего сервера, если услуга, которую предлагает этот объект, больше не нужна. В блоке 30 пока- зано как программа может использовать ACE_Dynamic_Service и ACE_Ser- vice_Type с целью программируемого поиска служб в репозитории ACE_Ser- vice_Repository. ACE_S е г v i с e_Re pository_Iterator реализует паттерн Iterator [ GoF ], чтобы предоставить приложениям возможность последовательного доступа к элементам ACE_Service_Type в репозитории ACE_Service_Reposito- гу, не раскрывая их внутреннего представления. Интерфейс ACE_Servi- ce_Repository_Iterator показан на рис. 5.5, а егр основные методы пере- числены в следующей таблице: Метод Описание ACE_Service_Repository_Iterator() Инициализирует итератор. I next () Возвращает указатель на следующий В репозитории ACE_Service_Type. |done () Возвращает 1, если просмотрены все элементы. I advance() Перемещает на один элемент репозитория вперед. Никогда не удаляйте просматриваемые элементы из репозитория ACE_Service_Repository, так как ACE_Service_Repository_Itera- tor не является робастным итератором (robust iterator) [Kof93]. Пример Данный пример показывает, как использовать классы ACE_Servi- ce_Repositoty и ACE_Service_Repository_Iterator для реализации класса Service_Reporter. Данный класс предоставляет «мета-сервис», кото- рый клиенты могут использовать для получения информации обо всех служ- бах, которые АСЕ-каркас Service Configurator статически или динамически объ- единил в некоторое приложение. Клиент взаимодействует с Service_Re- porter следующим образом: • Клиент устанавливает TCP-соединение с объектом Service_Repor- ter. • Service_Reporter возвращает список всех служб сервера, предостав- ляемых данному клиенту. • Service_Reporter закрывает TCP/IP-соединение. В блоке 31 описывается ACE_Service_Manager, стандартный класс, по- ставляемый с АСЕ, который обеспечивает расширенный набор возможностей Service_Reporter. Класс Service_Reporter описывается ниже. Сначала мы создаем файл Service_Reporter. h, который содержит следующее определение класса:
АСЕ-каркас Service Configurator 167 Рис. 5.5 Класс ACE—Sе гvi сe_Repos i tогy_Itе гatо г class Service_Reporter : public ACE_Service_Object { public: Service_Reporter (ACE_Reactor *r = ACE_Reactor::instance ()) : ACE_Service_Object (r) {} // Ноок-методы, наследуемые от <ACE_Service_Object>. virtual int init (int argc, ACE_TCHAR *argv[]); virtual int fini (); virtual int info (ACE_TCHAR **, size_t) const; virtual int suspend (); virtual int resume (); protected: // Ноок-методы реактора (Reactor). virtual int handle_input (ACE_HANDLE); virtual ACE_HANDLE get_handle () const { return acceptor_.get_handle (); } private: ACE-SOCK-Acceptor acceptor—/ // Экземпляр акцептора, enum { DEFAULT-PORT = 9411 }; }; Так как Service__Reporter наследует от ACE_Service_Object, то его можно конфигурировать с помощью ACE-каркаса Service Configurator. АСЕ- каркас Service Configurator создает реализацию этого класса во время выполне- ния, так что его конструктор должен быть public. Реализации hopk-методов Service-Reporter размещаются в файле Service—Reporter. срр. АСЕ-каркас Service Configurator вызывает следую- щий hook-метод Service-Reporter:: init () при включении Servi-, св—Reporter в приложение:
168 Глава5 Блок 30: Шаблон ACEJSynamicJtervice Шаблон класса ACEJOynamicjService обеспечивает типобезопасный спо- соб программного доступа касе jservi ce_Repos i tor у. Прикладной процесс может использовать этот шаблон для поиска служб, зарегистрированных в его локальном репозитории ACE_Service_Repository. Как показано ниже, шаб- лонный параметр типа гарантирует, что статический метод instance о воз- вращает указатель на соответствующий тип службы: template <class TYPE> class ACE__Dynamic__Service { public: // Используем <name> для поиска в <ACE_Service_Repository>. static TYPE. *instance (const ACE__TCHAR *name) { const ACE__Service_Type *svc_rec; if (ACE_Service_Repository::instance ()->find (name, &svc_rec) — -1) return 0; const ACE_Service_Type_Impl *type - svc_rec->type (); if (type ~~ 0} return 0; ACE_Service_Object *obj ~ ACE__static__cast (ACE_Service_Object *, type->object ()); return ACE_dynamic__cast (TYPE *, obj) ; };. Если экземпляр службы server_Logging Daemon связывался (link) динамиче- ски и инициализировался ACE-каркасом Service Configurator, приложение мо- жет использовать шаблон ACE_Dynamic_Service для программного доступа к данной службе, как показано ниже: typedef Reactor_Logging_Server_Adapter<Logging_Acceptor> Server_Logging__Daemon ; Server__Logging_Daemon *logging_server = ACE_Dynamic_Service<Server_Logging_Daeman>::instance (ACE__TEXT (”Server_Logging_Daemon") ) ; ACEJTCHAR *service_infо ~0; logging_server->infо ' (&service_info); ACE^DEBUG ( (LMJDEBUG, "%s\n", service__info) ) ; ACEstrdelete (service_info); Учтите, что в данном примере предполагается, что inf о () выделяет память под строку с помощью метода асе :: strnew (), рассмотренного в блоке 29. lint Service_Reporter::init (int argc, ACE_TCHAR *argv[]) { 2 ACE_INET_Addr local_addr (ServiceJteporter::DEFAULT_PORT); 3 ACE_Get_Opt get_opt (argc, argv, ACE_TEXT ("p:") , 0) ; 4 get_opt.long_option (ACE_TEXT ("port"), 5 ’p’ , ACE_Get_Opt::ARG_REQUIRED) ; 6 for (int c; (c = get__opt ()) != -1;) 7 if (c == ’p’) local_addr.set_port_number 8 (ACE—OS::atoi (get_opt.opt_arg ()));
АСЕ-каркас Service Configurator 169 Блок 31: Класс ACE Service Manager ACE_Service_Manager обеспечивает клиентам доступ к административным командам, связанным с управлением теми службами, которые в данный мо- мент предлагает некоторый сервер сети. Эти команды придают «внешний» вид («externalize») внутренним атрибутам служб, объединенных в сервере. В про- цессе конфигурирования сервера ACE_Service_Manager обычно регистри- руется по хорошо известному (well-known) коммуникационному порту, напри- мер, порт 9414. Клиенты имеют возможность подключаться к ACE_Servi- ce__Manager по этому порту и выдавать одну из следующих команд. • help — возвращает клиенту список всех служб, включенных АСЕ-каркасом Service Configurator в некоторое приложение. • reconfigure — запускает процесс реконфигурации с целью повторно про- читать локальный файл конфигурации служб. Если клиент посылает нечто отличное от этих двух команд, его ввод передается ACE_Service_Conf ig: :process_di rec five (), ЧТО дает ВОЗМОЖНОСТЬ конфигу- рировать серверы удаленно с помощью инструкций командной строки, на- пример: % echo "suspend My__Service" I telnet hostname 9411 Поэтому важно использовать ACE_Service_Manager только в том случае, если приложение работает в надежном окружении, так как злоумышленник-нару- шитель может использовать его, чтобы закрыть доступ к имеющимся службам или подключить фиктивные (rogue) службы в стиле троянского коня (Trojan Hor- se). По этой причине асе _Se г v 1 сe_Man a ger является статичес кой службой, ко- торую АСЕ по умолчанию отключает. Приложение может предписать АСЕ загрузить ее статические службы, включая ACE_Service_Manager С методом ACE_Servi.ce_Config: :ореп (), ЛЮбЫМ ИЗ двух способов: 1. Во время компиляции, передавая 0 в аргументе ignore_static_svcs. 2. Во время выполнения, включая опцию ’ -у' в аргументы командной строки argc/argv, которая заменяет значение ignore_static_svcs. 9 acceptor_.open (local_addr); 10 return reactor ()—>register_handler 11 (this, 12 ACE Event Handler::ACCEPT MASK); 13 } Строка 2 Инициализируем local_addr номером ТСР-порта Ser- vice_Reporter по умолчанию. Строки 3-8 Осуществляем анализ опций конфигурации служб с помощью класса ACE_Get_Opt, рассмотренного в блоке 8. Анализ начинаем не с argv [ 1 ], а с argv [ 0 ], что является режимом по умолчанию. Если в init () передается опция -р, или ее длинная версия —port, номер порта local_addr устанавливается в заданное значение. Так как ACE_Get_Opt всегда возвращает соответствующую короткую опцию для любых длинных опций, которые она встречает, то в цикле итератора достаточно протестировать только ’ р ’.
170 Глава 5 Строки 9-12 Инициализируем ACE_SOCK—Acceptor на прослушивание порта с номером local_addr и регистрируем экземпляр Service__Repor- ter у реактора на обработку событий ACCEPT. Когда от клиента поступит за- прос на соединение, реагирующий объект перенаправит его следующему hook-методу Service—Reporter: : handle__input (): lint Service_Reporter::handle_input (ACE_HANDLE) { 2 ACE_SOCK_Stream peer_stream; 3 acceptor_.accept- (peer_stream); 4 5 ACE—Service—Repository—Iterator iterator 6 (*ACE_Service—Repository::instance () f 0) ; 7 8 for (const ACE—Service-Type *st; 9 iterator.next (st) != 0; 10 iterator.advance ()) { 11 iovec iov [3]; 12 iov[0].ioV—base = ACE—const—cast (char *, st->name ()); 13 iov[0].ioV—len = 14 ACE—OS-String::strlen (st->name ()) * sizeof (ACE_TCHAR); 15 const ACE—TCHAR * state = st->active () ? 16 ACE-TEXT (" (active) ”) : ACE-TEXT {" (paused) ”); 17 iov[1].ioV—base - ACE_const—cast (char *, state); 18 iov[1].ioV-len = 19 ACE—OS—String::strlen (state) * sizeof (ACE_TCHAR); 20 ACE—TCHAR *report =0; // Просим info()выделить.буфер. 21 int len = st->type ()->info (&report, 0); 22 iov[2].ioV—base = ACE—static_cast (char ★, report); 23 iov[2].ioV-len = ACE—static—cast (size_tr len); 24 iov[2].ioV-len *= sizeof (ACE—TCHAR); 25 peer—stream.sendV-П (iov, 3); 26 ACE::strdelete (report); 27 } 28 29 peer—stream.close (); 30 return Or- Sl } Строки 2-3 Принимаем новое клиентское соединение. Service-Repor- ter является последовательной службой, обрабатывающей в каждый момент времени запрос одного клиента. Строки 5-6 Инициализируем ACE_Service_Repository_Iterator, который мы будем использовать для уведомлений обо всех активных и приос- тановленных службах, предлагаемых данным сервером. То, что мы передаем 0 в качестве второго аргумента данного конструктора, означает, что нужно воз- вращать информацию и о приостановленных службах, которая по умолчанию игнорируется. Строки 8-27 Для каждой службы вызываем ее метод inf о О , чтобы полу- чить краткую описательную информацию об этой службе и отправить эту ин-
АСЕ-каркас Service Configurator 171 формацию обратно клиенту по сокету с установленным соединением. Метод записи-со-слиянием se.ndv_n () осуществляет эффективную передачу всех буферов сданными в массив структур iovec посредством одного вызова сис- темной функции, в соответствии с изложенным в-блоке 6 главы 3 [C++NPvl]. Поскольку в TCP-потоке не существует границ записей,' клиент, по-видимому, не сможет определить конец каждой строки текста. Поэтому, проявляя заботу о клиенте, следует при кодировании методов inf о () включать символ новой строки в конце сообщения. Заметьте, что этот код может работать и с «узкими», и с «широкими» символами (см. блок 28). Текст, который получает клиент, ис- пользует набор и ширину символов Service_Reporter. Создание механиз- ма для корректной обработки этой ситуации мы оставляем в качестве упражне- ния для читателя. Строка 29 Закрываем соединение с клиентом и освобождаем дескриптор сокета. Hook-метод Service_Reporter: : info () передает обратно строку, ко- торая сообщает номер прослушиваемого TCP-порта и какую функцию данная служба выполняет: int Service_Reporter::info (ACE_TCHAR **bufferp, size_t length) const { ACE_INET_Addr local_addr; acceptor_.get_local_addr (local_addr) ; ACEJTCHAR buffBUFSIZ]; ACE_OS::sprintf (buf, ACE_TEXT ("%hu"), local_addr.get_port_number () ); ACE_OS_String::strcat (buf, ACE_TEXT ("/tcp # lists services in daemon\n")); if (*bufferp == 0) *bufferp = ACE::strnew (buf); else ACE_OS_String::strncpy (*bufferp, buf, length); return ACE_OS_String::strlen (*bufferp); } Как и при использовании метода Reactor_Logging_Server_Adap- ter: : inf о (), вызывающая сторона должна удалить динамически выделен- ный буфер с помощью АСЕ: : strdelete (). Hook-метрды Service_Reporter suspend () и resume () передают управление соответствующим методам реактора-синглтона, следующим образом: int Service_Reporter::suspend () { return reactor ()->suspend_handler (this); } int Service_Reporter::resume () { return reactor ()->resume_handler (this); } Метод Service_Reporter::fini() приведен ниже: int Service_Reporter::fini () { reactor ()->remove_handler
172 Глава 5 (this, ACE_Event_Handler.: :ACCEPT_MASK I ACE_Event_Handler::D0NTJ2ALL) ; return acceptor_.close (); ) Данный метод закрывает конечную точку соединения ACE_SOCK_Accep- tor и удаляет Service_Reporter из реактора-синглтона. После вызова hook-метода f ini () за удаление объекта службы отвечает АСЕ-каркас Service Configurator. Следовательно, нам не нужно удалять этот объект путем delete this в handle_close (), поэтому мы передаем флаг DONT_CALL, чтобы от- менить выполнение реактором этого обратного вызова. В заключение, мы должны представить ACE-каркасу Service Configurator некоторую «учетную» («book-keeping») информацию относительно этой новой службы. Хотя код этой службы будет связываться с программой данного при- мера статически, мы хотим, чтобы каркас создавал экземпляр объекта Servi- ce_Reporter для выполнения данной службы при ее активизации. Поэтому мы включаем соответствующий служебный макрос АСЕ в файл реализации Service_Reporter. Этот макрос создает объект Service_Reporter и ре- гистрирует его в ACE_Service_Repository, в соответствии с изложенным в блоке 32. 1 ACE_FACTORY_DEFINE (ACE_Local_Service, Service_Reporter) 2 3 ACE_STATIC_SVC_DEFINE ( 4 Reporter_Descriptor, 5 ACE_TEXT ("Service_Reporter"), 6 ACE_SVC_OBJ_T, 7 &ACE_SVC_NAME (Service_Reporter), 8 ACE_Service_Type::DELETE_THIS 9 | ACE_Service_Type::DELETE_OBJ, 10 0 // Этот объект первоначально не является активным. И) 12 13 ACE_STATIC_SVC_REQUIRE (Reporter_Descriptor) Строка 1 Макрос ACE_FACTORY_DEFINE генерирует следующие функ- ции: void _gobble_Service_Reporter (void *arg) { ACE_Service_Object *svcobj = ACE_static_cast (ACE_Service_Object *, arg); delete svcobj; ) extern "C" ACE_Service_Object * _make_Service_Reporter (void (**gobbler) (void *)) { if (gobbler != 0) *gobbler = _gobble_Service_Reporter; return new Service_Reporter;
АСЕ-каркас Service Configurator 173 Блок 32: АСЕ-макросы Service Factory Приложения могут использовать следующие макросы, определенные в ace /os. h, чтобы упростить создание и использование функций-фабрик и регистрацию статических служб. За исключением ace_static_svc_regi ster, эти макросы должны применяться на глобальном уровне (file scope), а не в пространстве имен (namespace) класса или метода. Макросы функций-фабрик и gobbler-функций. Статические и динамические службы должны предоставлять функцию-фабрику (factory function) для созда- ния объекта службы и gobbler-функцию для его удаления. АСЕ предлагает сле- дующие три макроса, помогающие создавать и использовать эти функции: • ace_factory_define(lib, class) —- Используется в файле реализации ДЛЯ определения функции-фабрики и gobbler-функции службы, ыв представля- ет собой ACE-префикс экспорта макроса (см. блок 37), который использу- ется с библиотекой, включающей функцию-фабрику. Он должен быть ace_local_service,если данную функцию не нужно экспортировать из DLL. class определяет тип объекта службы, который должна создавать данная фабрика. • ace_factory_declare(lib, class) — Объявляет функцию-фабрику, опреде- ляемую макросом ace_factory_define. Используйте этот макрос для соз- дания ссылки на функцию-фабрику из блока компиляции, отличного от того, который содержит макрос ACE_FACTORY_DEFINE. • ace_svc_name(class) —- Генерирует имя функции-фабрики, определяемой макросом ace_factory_define. Сгенерированное имя можно использо- вать, чтобы получить во время компиляции адрес функции, также как для мак- . роса ace_static_svc_define ниже. Информационный макрос статических служб. АСЕ Предоставляет следую- щий макрос для генерации регистрационной информации статической служ- бы. Он определяет имя службы, ее тип и указатель на функцию-фабрику, кото- рую вызывает каркас при создании экземпляра службы: • ACE_STATIC_SVC_DEFINE (REG, NAME, TYPE, F'JNC_ADDR, FLAGS, ACTI- VE) — Используется в файле реализации для определения информации о статической службе, reg формирует имя информационного объекта, кото- рое должно соответствовать параметру, передаваемому в ace_sta- tic_svc__require и в ace_static_svc_register. Другие параметры уста- навливают атрибуты ACE_S ta t i c_ S vc_De s c r ip t о r. Макросы регистрации статических служб. Регистрационная информация статической службы должна передаваться АСЕ-каркасу Service Configurator при запуске программы. Для выполнения этой регистрации совместно исполь- зуются следующие два макроса: ♦ ace_static_svc_require (reg) — Используется в файле реализации служ- бы, чтобы определить статический объект, конструктор которого будет вклю- чать регистрационную информацию статической службы в список извест- ных каркасу статических служб. • ace_static_svc register (REG) — используется в начале основной (main) программы, чтобы гарантировать, что объект, определяемый в ace_sta- tic_svc_require зарегистрирует статическую службу не позже той точки; в которую вставлен данный макрос. Макрос ACE_FACTORY_DEFINE упрощает использование АСЕ-каркаса Service Configurator следующим образом:
174 Глава 5 • Генерирует функции-фабрики, независимые от компилятора. Данный макрос генерирует функцию-фабрику _make_Service_Reporter() с внешним «С»-связыванием, позволяющим каркасу найти эту функцию в таблице имен DLL, ничего не зная о схеме корректировки имен (name- mangling scheme) данного компилятора C++. • Обеспечивает согласованное управление динамической памятью. Что- бы гарантировать правильное функционирование на разных платфор- мах, важно чтобы память выделяемая в некоторой DLL, освобождалась в той же DLL, в соответствии с тем, о чем говорилось в блоке 29. Поэтому gobbler-функция, передаваемая в _make_Service_Reporter(), по- зволяет ACE-каркасу Service Configurator гарантировать, что память вы- деляется и освобождается в одной и той же куче. Строки 3-11 Макрос ACE_STATIC_SVC_DEFINE используется для инициа- лизации экземпляра ACE_Static_Svc_Descriptor. Данный объект хранит информацию, необходимую для описания статически конфигурируемой служ- бы, предоставляемой информационной службе (reporter service). Servi- ce_Reporter является именем класса данного объекта службы, a «Servi- ceJReporter» является именем, используемым для идентификации службы в ACE_Service_Repository. ACE_SVC_OBJ_T является типом контейнера объекта службы. Мы используем макрос ACE_SVC_NAME в сочетании с операто- ром взятия адреса C++ «address-of», чтобы получить адрес функции-фабрики _make_Service_Reporter (), создающей экземпляр Service_Reporter. DELETE_THIS и DELETE_OBJ являются константами перечисления, определяе- мыми в классе ACE_Service_Types, который выполняет обработку после вы- зова hook-метода f ini () данной службы, следующим образом: • DELETE_THIS указывает АСЕ, что нужно удалить объект ACE_Servi- ce_Object_Type, представляющий данную службу. • DELETE_OB J приводит к вызову gobbler-функции в результате чего уда- ляется объект Service_Reporter. Строка 13 Макрос ACE_STATlC_SVC_REQUIRE определяет объект, кото- рый регистрирует экземпляр объекта ACE_Static_Svc_Descriptfor объек- та Service_Reporter в репозитории ACE_Service_Repository. На мно- гих платформах этот макрос осуществляет также проверку того, что экземпляр данного объекта создан. На некоторых платформах, однако, нужно также ис- пользовать макрос ACE_STATIC_SVC_REGISTER в функции main () про- граммы, в которую включается данная служба. В примере из раздела 5.4 показано как Service_Reporter может быть включен статически в серверное приложение.
АСЕ-каркас Service Configurator 175 5.4 Класс ACE_Service_Config Обоснование Прежде чем служба начнет функционировать, она должна быть включена в адресное пространство приложения. Одним из способов включения служб в сетевое приложение является статическое включение функциональности, реализуемой различными классами и функциями, в отдельные процессы ОС, и затем их запуск или инициализация «вручную» во время выполнения. Мы ис- пользуем этот подход в примерах сервера регистрации в главах 3 и 4, а также на протяжении [C++NPvl], где'программа сервера регистрации работает в про- цессе, который обрабатывает регистрационные записи от клиентских приложе- ний. Хотя применение ACE-каркаса Reactor в первых главах улучшило модуль- ность и переносимость сервера сетевой регистрации, статическое связывание класса Reactor_Logging_Server с программой main () имеет следующие недостатки: • Решения о конфигурациях служб принимаются в цикле разработки преждевременно, что нежелательно, если разработчикам заранее неиз- вестен лучший способ совмещения или распределения служб. Кроме того, представление о «лучшей» конфигурации может изменяться при изменении контекста функционирования. Например, приложение мо-. жет сохранять регистрационные записи в локальном файле, пока работа- ет на отключенном от сети портативном компьютере. Тем не менее, после подключения портативного компьютера к LAN, приложение может на- правлять регистрационные записи центральному серверу регистрации. Вынуждая сетевые приложения преждевременно фиксировать конкрет- ную конфигурацию служб, мы препятствуем их гибкой настройке и мо- жем ухудшить их производительность и функциональность. Кроме того, это может привести к необходимости дорогостоящего перепроектирова- ния и повторной реализации на более поздних этапах жизненного цикла проектирования. • Модификация одной службы может негативно сказаться на других службах, если реализация службы тесно привязана к исходной конфигу- рации. Например, чтобы улучшить повторное использование, сервер ре- гистрации может сначала находиться в той же программе, что и другие службы, такие как служба именования. Если, однако, другие службы из- меняются, например, если изменяется алгоритм поиска службы именова- ния, то весь код такого сервера потребует модификации, повторной ста- тической компиляции и компоновки. Кроме того, завершение работы процесса с целью изменения отдельных фрагментов кода его служб, по- требует также завершения работы совмещенной службы регистрации. Это нарушение в работе службы может оказаться неприемлемым для систем с высокой интенсивностью обращений, таких как телекоммуни- кационные коммутаторы или центры обработки вызовов от лечащихся пациентов [SS94].
176 Глава 5 • Производительность системы может плохо масштабироваться, так как выделение отдельного процесса для каждой службы истощает ресурсы ОС, такие как дескрипторы ввода/вывода, виртуальная память и записи таблицы процессов. Такой дизайн особенно расточителен, если службы часто простаивают. Кроме того, процессы могут быть неэффективными для многих краткосрочных коммуникационных задач, таких как запросы текущего времени у службы времени или выполнение запроса об адресе хоста по протоколу Domain Name Service (DNS). Чтобы компенсировать недостатки чисто статических конфигураций, АСЕ-каркас Service Configurator определяет класс ACE_Service_Conf ig. Функциональные возможности класса ACE_Service_Conf ig реализует паттерн Facade [GoF] с целью интегра- ции других классов ACE-каркаса Service Configuratdr и координации действий, необходимых для управления службами приложения. Данный класс обеспечи- вает следующие функциональные возможности: • Он является интерпретатором языка сценариев, который дает возмож- ность приложениям или администраторам подавать ACE-каркасу Service Configurator команды, называемые директивами, для поиска и инициа- лизации реализаций службы во время выполнения, а также приостанав- ливать, возобновлять, повторно инициализировать и/или завершать ра- боту компонента, после того как он уже инициализирован. Задать дирек- тивы для ACE_Service_Conf ig можно любым из двух способов: 1. Используя файлы конфигураций (по умолчанию svc. conf), кото- рые содержат одну или несколько директив. 2. Программно, передавая отдельные директивы в виде строк. • Поддерживает управление службами, размещенными в приложении (так называемые статические службы), а также теми, которые должны связы- ваться динамически (так называемые динамические службы) из отдель- ных динамических библиотек (DLL). • Позволяет реконфигурировать службу во время выполнения с помощью следующих механизмов: 1. На POSIX-платформах ACE_Service_Conf ig может быть объеди- нен с ACE-каркасом Reactor, чтобы повторно обрабатывать конфигу- рационные файлы при получении сигнала SIGHUP или любого дру- гого задаваемого пользователем сигнала, например, SIGINT. 2. Передавая через ACE_Service_Manager команду «reconfigu- re», в соответствии с изложенным в блоке 31. 3. Приложение может попросить свой ACE_Service_Conf ig повтор- но обработать его конфигурационные файлы в любое время. Напри- мер, событие уведомления об изменении каталога Windows может быть использовано, чтобы сообщить программе об изменении ее файла конфигурации. Данное событие изменения может затем запус- тить повторную обработку конфигурации.
АСЕ-каркас Service Configurator 177 ACE_S«rvic«_Config + ACE_Service_Config (ignore_static_svcs : int = 1, repository_size : size_t « MAXJERVICES, signum : int - SIGHUP) +_авеп . (argc ; int/ argv ; ACEJCHAR *11, loggerkev : const ACEJCHAR * - ACE_DEFAULT_LOGGER_KEY. ianore stafic svcs : int - 1. ianore_default_svcs : int = 1. ianore_debuo_flaa : int - 0) : int -I- close () : int + process_directives () : int t.process^directive (directive : ACEJCHARfl) : int + reconfigure (> : int +_ suspend (name : const ACEJCHAR Ц) : int + resume (name : const ACEJCHAR Г1) : int Рис. 5.6 Класс ACE_Service_Config 4. Приложение может также создать индивидуальные директивы для своего AGE_Service_Conf ig, чтобы в любое время осуществлять обработку с помощью метода process_directive (). Интерфейс ACE_Service_Conf ig приведен на рис. 5.6. Интерфейс этого класса достаточно объемен, поскольку он экспортирует все возможности АСЕ- каркаса Service Configurator. Поэтому мы разбиваем описание его методов на три группы, перечисленные ниже. 1. Методы управления жизненным циклом Service Configurator. Следую- щие методы инициализируют и завершают работу ACE_Service_Con- fig: Метод Описание ACE_Service_Config() open () Данные методы создают и инициализируют ACE_Service_Config. close () Этот метод завершает работу, удаляет (finalize) все сконфигурированные службы и ресурсы, выделенные при инициализации ACE_Service__Conf ig. Любой процесс имеет только один экземпляр состояния ACE_Servi- ce_Conf ig. Этот класс является вариантом паттерна Monostate [СВ97], кото- рый гарантирует уникальность состояния своих экземпляров, объявляя все члены-данные как статические. Кроме того, методы ACE_Service_Conf ig также объявляются как статические. Конструктор ACE_Service_Conf ig, од- нако, является единственным способом установить максимальный размер ACE_Service_Repository. Это также единственный программируемый способ изменить номер сигнала, который мржет быть зарегистрирован у реаги- рующего объекта в качестве сигнала, запускающего повторный процесс конфи-
178 Глава 5 курирования. Поэтому создание экземпляра ACE_Service_Conf ig заключа- ется в установке параметров базового объекта, имеющего одно состояние, а не в создании отдельного объекта конфигурации. Таким образом, деструктор ACE_Service_Conf ig не выполняет никаких действий (no-op)’. Метод open () является обычным способом инициализации ACE_Servi- ce_Conf ig. Он осуществляет анализ аргументов, передаваемых в параметрах argc и argv, пропуская первый параметр (argv [ 0 ]) — имя программы. Оп- ции, распознаваемые ACE_Service_Conf ig, перечислены в следующей таб- лице: I Опция Описание ' -Ь ’ Преобразует прикладной процесс в процесс-демон (см. блок 5). ’-d’ Отображает диагностическую информацию в процессе обработки директив. .-fi Задает файл, содержащий директивы, отличный от файла по умолчанию svc. conf. Этот аргумент может повторяться для задания нескольких файлов конфигураций. 1 —n1 Не обрабатывает статические директивы, что исключает необходимость статической инициализации ACE_Service_Repository. ’_s» Задает сигнал, используемый для запуска повторной обработки ACE_Service_conf ig своего конфигурационного файла. По умолчанию используется sighup. '-S' Непосредственно задает директиву для ACE_Service_Conf ig. Этот аргумент может повторяться с целью обработки нескольких директив. Обрабатывает статические директивы, что требует статической инициализации АСЕ_Service_Repository. 2. Методы конфигурирования служб. После того как проанализированы все argc/argvаргументы,методACE_Service_’Config: :ореп() вызыва- ет один или оба следующих метода для конфигурирования данного прило- жения: Метод Описание |process_ directives () Обрабатывает последовательность директив, которые хранят- ся в указанном файле (или файлах) сценария. Данный метод позволяет сохранять множество директив на долговременной основе и обрабатывать последовательно в пакетном режиме. Каждая директива конфигурации служб в каждом конфигура- ционном файле выполняется в том порядке, в каком они заданы. process_ directive() Обрабатывает одиночную директиву, передаваемую в виде строки. Этот метод позволяет динамически создавать и инте- рактивно обрабатывать директивы, аналогично тому, как это делается в GUI или по сетевому соединению.
АСЕ-каркас Service Configurator 179 В следующей таблице обобщены директивы конфигурации служб, кото- рые могут обрабатываться этими двумя методами ACE_Service_Conf ig: Директива Описание dynamic Динамически связывает службу и инициализирует ее вызывая hook-метод init о. static Вызывает hook-метод init о, чтобы инициализировать службу, которая была связана статически. remove Полностью удаляет службу, то есть вызывает ее hook-метод f ini () и отключает ее от данного прикладного процесса, если она больше не используется. | suspend Вызывает hook-метод службы suspend (), чтобы приостановить работу службы, не удаляя ее. re.sume Вызывает hook-метод службы resume (), чтобы возобновить работу службы, которая была до этого приостановлена. stream Инициализирует упорядоченный список иерархически связанных модулей. Ниже мы описываем синтаксис и семантику лексем каждой из этих дирек- тив. • Динамическое связывание и инициализация службы: dynamic svc-na- те svc-type DLL-пате factory_func() [ «argc/aryy options»] Директива dynamic сообщает АСЕ-каркасу Service Configurator, что нужно динамически связать и инициализировать объект службы. Пара- метр svc-name является именем, назначаемым данной службе. Параметр svc-type обозначает тип данной службы, который может быть Servi- ce_Object *, Module * или Stream *. DLL-пате—имя динамически подключаемой библиотеки, которая содержит имя функции-фабрики factory_func(). Данное имя является точкой входа для extern «С» функ- ции, которую вызывает интерпретатор ACE_Service_Conf ig для соз- дания экземпляра службы. Если svc-type— Service_Ob j ect *, то facto- ry_func() должна возвращать указатель на объект, производный от ACE_Service_Object. Имя factory_func() должно начинаться с симво- ла подчеркивания, так как редакторы связей добавляют этот символ к идентификаторам с глобальной областью видимости. DLL-name может быть или полным именем пути, или именем файла без расширения. Если это полное имя пути, то используется метод ACE_DLL: : open (), описанный в блоке 33, для динамического подключе- ния указанного файла к прикладному процессу. Однако если это имя фай- ла, то ACE_DLL: : open () использует АСЕ: : Idf ind () (см. блок 33), что- бы найти указанную DLL и динамически включить ее в адресное простран- ство данного процесса с помощью ACE_DLL:: open (). Директива dynamic может использоваться переносимым образом в разных опера- ционных системах, так как АСЕ инкапсулирует детали платформ.
180 Глава 5 Argc/argv options — это необязательный список параметров, который мо- жет добавляться для инициализации объекта службы с помощью его hook-метода init (). АСЕ-каркас Service Configurator использует класс ACE_ARGV, описанный в блоке 35, для деления строки на аргументы и подстановки значений переменных окружения, которые включены в строку. • Инициализация статически' связываемой (linked) службы: static svc-name [«argc/argv options»] Хотя ACE_Service_Config обычно используется для динамической конфигурации служб, его можно использовать и для того, чтобы скон- фигурировать службу статически с помощью директивы static. Svc- name и необязательные argc/argv options аналогичны директиве dynamic. Тем не менее, их синтаксис проще, так как объект службы должен быть уже включен статически в загрузочный модуль программы. Таким обра- зом, нет необходимости искать и подключать DLL, и вызывать функ- цию-фабрику для создания объекта службы. Статическая конфигурация жертвует гибкостью в пользу повышения безопасности, что может быть полезным для некоторых типов серверов, которые должны включать только доверенные, статически связываемые службы. Напомним, что как указывалось в блоке 31, подключение статических служб по умолчанию отключено. Поэтому, чтобы статическими службами можно было поль- зоваться, их подключение должно быть разрешено явным образом, ина- че директива static не будет иметь никакого эффекта. Блок 33: Класс ACE DLL Приложения, которые подключают и отключают DLL напрямую, могут столк- нуться со следующими проблемами: • Неунифицированный программный интерфейс, который даже менее пере- носим, чем Socket API, рассмотренный в главе 3 (C++NPv1). • Небезопасные типы, подверженные ошибкам и неправильному использова- нию, так как собственные DLL API ОС возвращают слабо типизированные де- скрипторы, которые передаются DLL-функциям, например, тем, которые ис- пользуются для поиска идентификаторов и отключения DLL. • Потенциальная утечка ресурсов, так как можно забыть освободить деск- рипторы DLL. Чтобы решить эти проблемы, в АСЕ создан класс интерфейсного фасада ace_dll с целью инкапсулировать непосредственные действия, связанные с подключением/отключением (linking/unlinking) DLL. Этот класс исключает не- обходимость использования приложениями подверженных ошибкам слабо типизированных дескрипторов, а также обеспечивает корректное освобож- дение ресурсов в своем деструкторе. Кроме того, он использует метод АСЕ :: Idf ind () для поиска DLL с использованием следующих алгоритмов: • Расширение имени файла DLL — ace:: idfind () определяет имя DLL, до- бавляя соответствующий префикс и суффикс. Например, префикс i ib и суф- фикс . so в Solaris, а в Windows суффикс . dll. • Поиск пути к DLL — асе:: idfind () будет также искать указанную DLL, ис- пользуя переменные окружения платформы, задающие пути поиска DLL. На-
АСЕ-каркас Service Configurator 181 пример, он ищет DLL, используя ld_libraryj?ath но многих UNIX-системах и path в Windows. Основные методы класса ace_dll перечислены в следующей таблице. Класс АСЕ Описание ACE_DLL() орел () Открывает и динамически связывает указанную DLL -АСЕ J3LL () close () Закрывает и по выбору отключает DLL symbol () Возвращает указатель на функцию или объект в DLL | [ error () Возвращает строку комментария к ошибке | Интерфейс ace_dll приведен ниже. АСЕ DLL - handle : ACE_SELIB_HANDLE + open (name : const ACE_TCHAR *, mode : int = ACE_DEFAULT_SHLIB_MODE, close_on_dest ruct : int =• 1) : int t- close () : int + symbol (name : const ACE_TCEAR *) : void * + error (void) : const ACE TCHAR * • Полное удаление службы: remove svc-name Действие директивы remove приводит к тому, что интерпретатор ACE_Service_Config запрашивает у ACE_Service_Repository службу с указанным именем. Если такая служба есть, интерпретатор вы- зывает ее hook-метод f ini (), который выполняет действия, необходи- мые для освобождения ресурсов после завершения работы службы. Если указатель на функцию-деструктор службы связан с объектом службы, то вызывается он, чтобы объект службы удалил сам себя (макрос ACE_FAC- TORY_DEFINE определяет эту функцию автоматически). Наконец, если данная служба была подключена динамически из DLL, она отключается с помощью метода ACE_DLL: : close (). Так как DLL может подклю- чаться к процессу несколько раз, ACE_DLL: : close () гарантирует, что данная DLL отключается только тогда, когда она больше не используется. • Приостановка службы без ее удаления: suspend svc-name Действие директивы suspend приводит к тому, что интерпретатор ACE_Service_Config запрашивает в ACE_Service_Repository указанную svc-name службу. Если такая служба есть, вызывается ее hook- метод suspend (). Служба может подменить этот метод, чтобы реализо- вать соответствующие действия, необходимые для приостановки ее ра- боты. • Возобновление работы приостановленной службы: resume svc-name Действие директивы resume приводит к тому, что интерпретатор
182 Глава 5 ACE_Service_Config запрашивает в ACE_Service_Repos.itory указанную svc-name службу. Если такая служба есть, вызывается ее hook- метод resume (). Служба может подменить этот метод, чтобы реализо- вать соответствующие действия, необходимые при возобновлении ее ра- боты, которые обычно противоположны действиям метода suspend (). • Инициализация упорядоченного списка иерархически связанных мо- дулей: stream svc-name' {' module-list'}' Действие директивы stream приводит к тому, что интерпретатор ACE_Service_Conf ig инициализирует упорядоченный список иерар- хически связанных модулей. Каждый модуль состоит из двух служб, ко- торые взаимодействуют и обмениваются данными путем передачи объ- ектов ACE_Message_Block. Реализация директивы stream использу- ет АСЕ-каркас Streams, описанный в главе 9. Полный синтаксис в форме Бэкуса/Наура (БНФ) файлов svc.conf, ин- терпретируемых ACE_Service_Conf ig приведен на рис. 5.7. В блоке 34 опи- сано, как задавать файлы svc. conf с применением синтаксиса XML, который используется факультативно. 3. Вспомогательные методы. В ACE_Service_Conf ig определены следую- щие вспомогательные методы: Метод Описание reconfigure () Вызывает повторную обработку текущего файла(ов) конфигурации. suspend() Приостанавливает службу, идентифицируемую по имени. j resume() Возобновляет приостановленную службу, идентифицируемую по имени. <svc-conf-entries> ::= <svc-conf-entries> <svc-conf-entry> | NULL <svc-conf-entry> ::= <dynamic> | <static> p <suspend> ’ <resume>.| <remove> | <stream> <dynamic> := dynamic <svc-location> <parameters-opt> <static> : « static <svc-name> <parameters-opt> <suspend> := suspend <svc-name> <resume> : = resume <svc-name> <remove> : = remove <svc-name> <stream> : = stream <streamdef> ’{’ <module-list> ’}’ <streamdef> ::= <svc-name> I dynamic 1 static <module-list> ::= <module-list> <module> | NULL <module> ::= <dynamic> I <static> I <suspend> I <resume> | <remove> <svc-location> ::= <svc-name> <svc-type> <svc-factory> <status> <svc-type> ::= Service_Object | Module ’*• | Stream | NULL <svc-factory> ::= PATHNAME FUNCTION ')' <svc-name> ::= STRING <status> ::= active | inactive | NULL <parameters-opt> : := STRING | NULL Рис. 5.7 БНФ языка сценариев ACE_Service_Conf ig
АСЕ-каркас Service Configurator 183 Блок 34: Использование XML при конфигурировании служб Класс ACE_Service Config может быть настроен на интерпретацию языка сценариев на базе XML. Определение типа документа (Document Type Defini- tion, DTD) для этого языка приведено ниже: <!ELEMENT ACE_Svc_Conf (dynamic|static I suspend I resume I remove|stream|streamdef)* > <!ELEMENT streamdef ((dynamic static),module)> CIATTLIST streamdef id IDREF #REQUIRED> <1 ELEMENT module (dynamic static suspend resume remove)+> <!ELEMENT stream (module)> OATTLIST Stream id IDREF #REQUIRED> <I ELEMENT dynamic (initializer) > CIATTLIST dynamic id ID #REQUIRED status (active|inactive) "active" type (module | service_object I stream) #REQUIRED> CIELEMENT initializer EMPTY> CIATTLIST initializer init CDATA #REQUIRED path CDATA tfIMPLIED params CDATA #IMPLIED> с I ELEMENT static EMPTY> CIATTLIST static id ID #REQUIRED params CDATA #IMPLIED> . Cl ELEMENT suspend EMPTY> CIATTLIST suspend id IDREF #REQUIRED> с I ELEMENT resume EMPTY> CIATTLIST resume id IDREF #REQUIRED> с I ELEMENT remove EMPTY> CIATTLIST remove id IDREF #REQUIRED> Синтаксис языка конфигураций на базе XML отличается от представленного на рис. 5.7, но их семантика совпадает. Хотя в нем больше элементов для со- ставления сценариев, используемый в АСЕ формат конфигурационных файлов на базе XML является более гибким. Например, пользователи могут включать собственные обработчики событий XML, чтобы расширять поведение АСЕ- каркаса Service Configurator, не модифицируя базовую реализацию АСЕ. XML-формат файлов конфигурации является относительно новым изобретени- ем (он был введен в АСЕ 5.3), Поэтому он пока еще не используется АСЕ-карка- сом Service Configurator по умолчанию. Пользователи могут выбрать Service Configurator на базе XML скомпилировав АСЕ с включенным макросом ace_has_xml_svc_conf. В АСЕ есть perl-скрипт svcconf-convert .pl для пре- образования файлов из исходного формата в новый формат XML. Этот скрипт находится в каталоге $асе_коот/Ып/. Метод reconfigure () может использоваться для принудительной по- вторной обработки интерпретатором ACE_Service_Conf ig файлов конфи- гурации служб. Такая возможность полезна, если приложение осуществляет мониторинг файлов конфигурации служб с целью обнаружения изменений.
184 Глава 5 При обнаружении изменений, они могут быть обработаны с помощью метода reconfigure (). Данный метод рекомендуется использовать в Windows для внесения изменений во время выполнения, так как там не существует аналога обычной в UNIX практики передачи процессу сигналов, типа SIGHUP. Методы suspend () и resume() предоставляют возможность приоста- навливать и возобновлять службу, если известно ее имя. Это способ сокращен- ного именования методов, определенных в синглтоне ACE_Service_Repo- sitory. Пример Этот пример показывает, как применять АСЕ-каркас Service Configurator для создания сервера, начальная конфигурация которого функционирует сле- дующим образом: • Статически конфигурирует экземпляр Service_Reporter. • Динамически подключает в адресное пространство сервера и конфигури- рует шаблон Reactor_Logging_Server_Adapter из примера разде- ла 5.2. Затем мы покажем, как динамически переконфигурировать этот сервер так, чтобы он поддерживал различные реализации взаимно-согласованной службы регистрации. Исходная конфигурация сервера. Мы начинаем с разработки следующей главной программы main() в Configurable_Logging_Server .срр. Эта программа включает службы Service_Reporter и Reactor_Logging_Ser- ver_Adapter в прикладной процесс, азатем выполняет цикл обработки собы- тий реактора. 1#include "ace/OS.h" 2 #include "ace/Service__Config.h" 3#include "ace/Reactor.h" 4 5int ACE_TMAIN (int argc, ACE_TCHAR *argv[]) { 6 ACE_STATIC_SVC_REGISTER (Reporter); 7 8 ACE_Service_Config::open 9 (argc, argv, ACE_DEFAULT_LOGGER_KEY, 0) ; 10 11 ACE_Reactor::instance ()->run_reactor_event_loop (); 12 return 0; 13 } Строки 1-3 В программе main () нет специфических для данных служб за- головочных файлов (или кода). Поэтому она является полностью унифициро- ванной и может повторно использоваться во многих программах, которые кон- фигурируются с применением АСЕ-каркасов Service Configurator и Reactor.
АСЕ-каркас Service Configurator 185 Строка 5 Заменяем имя точки входа main () на макрос ACE_TMAIN. Дан- ный макрос использует в Windows альтернативную точку входа wmain () при работе с Unicode и обычную main () во всех .остальных случаях. Строка 6 Регистрируем статическую службу Reporter в ACE-каркасе Service Configurator. Хотя каркасу теперь известно о существовании этой службы, она не активизируется до тех пор, пока не поступит соответствующая директива конфигурации службы. Строки 8-9 Вызываем ACE_Service_Config: :ореп() для конфигури- рования приложения. Все решения о том, какую службу загружать и все пара- метры служб находятся в файле конфигурации, который является внешним по отношению к исполняемой прикладной программе. Затем мы запускаем цикл обработки событий реагирующего элемента с целью обработки событий вво- да/вывода, связанного с клиентами. Так как мы знаем, что наша программа будет активизировать статическую службу Service_Reporter, мы добавили четвертый аргумент (и по необхо- димости третий) в ACE_Service_Conf ig: : open (), чтобы разрешить в яв- ном виде подключение статических служб. Если бы мы, наоборот, решили бы оставить этот выбор на усмотрение пользователя или администратора, то дан- ный аргумент не нужно было бы включать и пользователь сам бы выбирал раз- решить или запретить использование статических служб, добавляя опцию -у в командную строку. Когда вызывается ACE_Service_Conf ig: : open (), он использует метод ACE_Service_Conf ig: :process_directives () для интерпретации, приведенного ниже, файла svc. conf. 1 static Service_Reporter "-p $SERVICE_REPORTER_PORT" 2 3 dynamic Server_Logging_Daemon Service_Object * 4 SLD:_make_Server_Logging_Daemon() 5 "$ SERVER_LOGGING_DAEMON_PORT" Строка 1 Код Service_Reporter, регистрационная информация и функ- ция-фабрика — все это было статически связано с исполняемой программой. Поэтому данная директива приводит просто к тому, что каркас Service Configura- tor активизирует эту службу путем вызова Service_Reporter:: init (). Ар- гументы argc/argv, передаваемые init (), представляют собой строку «-р» и расширение переменной окружения SERVICE_REPORTER_PORT. Каркас Ser- vice Configurator расширяет эту переменную окружения автоматически, исполь- зуя класс ACE_ARGV, рассмотренный в блоке 35. ACE_ARGV распознает в SERVI - CE_REPORTER_PORT переменную окружения по ее первому символу $ и под- ставляет ассоциированное с ней значение. Макрос ACE_STATIC_SVC_REQUIRE, используемый в Service_Reporter. срр обеспечивает регистрацию Servi- ce_Reporter в ACE_Service_Repository до того как будет вызван метод ACE_Service_Config::open().
186 Глава 5 Блок 35: Класс ACE.ARGV Класс ace_argv представляет собой полезный вспомогательный класс, кото- рый может выполнять следующие действия: 1. Преобразовывать строку в вектор или в строку в стиле argc/argv. 2. „Последовательно србирать набор строк в вектор argc/argv. 3. Преобразовывать вектор в стиле argc/argv в строку. В процессе такого преобразования данный класс может подставлять значе- ния переменных окружения для каждого встреченного имени переменной ок- ружения, заключенного в символы $. ace_argv обеспечивает простой и эф- фективный механизм создания любых аргументов командной строки, Поду- майте о его использовании везде, где требуется обработка командной строки, особенно, тогда, когда желательно использовать подстановку пере- менных окружения. В АСЕ часто используется ace_argv, особенно в каркасе Service Configurator. Строки 3-5 Данный код конфигурирует серверный демон регистрации с помощью следующих шагов: 1. Динамически подключает DLL SLD в адресное пространство процесса. 2.. Использует класс ACE_DLL, рассмотренный в блоке 33, для нахождения функции-фабрики _make_Server_Logging_Daemon () в таблице имен DLL SLD. 3. Вызывает функцию-фабрику для размещения объекта Server_Log- ging_Daemon. 4. Каркас Service Configurator вызывает hook-метод объекта службы Server_Logging_Daemon: : init (), передавая ему в виде аргументов argc/argv расширение переменной окружения SERVER_LOGGING_DA- EMON_PORT, обозначающей номер порта, на котором серверный демон ре- гистрации ждет клиентских запросов на соединение. 5. Если init () завершается успешно, указатель Serve.r_Logging_Daemon сохраняется в ACE_Service_Repository под именем «Server_Log- ging_Daemon>>. В блоке 36 показана XML-версия файла svc. conf „приведенного выше. DLL SLD создается на основе следующего файла SLD. срр: ♦include "Reactor_Logging_Server_Adapter.h" ♦include "Logging_Acceptor.h" ♦include "SLD_export.h" typedef Reactor_Logging_Server_Adapter<Logging_Acceptor> Server_Logging_Daemon; ACE_FACTORY_DEFINE (SLD, Server_Logging_Daemon) Файл SLD.cpp содержит определение типа Server_Logging_Daemon; которое создает реализацию шаблона Reactor_Logging_Server_Adapter
АСЕ-каркас Service Configurator 187 Блок 36: XML-пример файла svc.conf XML-представление файла svc. conf, приведенного выше: 1<ACE_Svc_Conf> 2 <static id='Service_Reporter' 3 params='-p $SERVICE_REPORTER_PORT’ /> 4 5 <dynamic id='Server_Logging_Daemon' 6 type=1service_object'> 7 initializer path='SLD' 8 init='_make_Server Logging_Daemon' 9 params='$SERVER_LOGGING_DAEMON_PORT'/> 10 </dynamic> 11 </ACE_Svc_Ccnf> XML-вариант файла svc. conf больше по объему, чем в исходном формате, так как в нем требуется явное задание имен полей. Однако XML-формат предос- тавляет файлам svc. conf больше возможностей, так как можно добавлять но- вые разделы и поля, не нарушая существующего синтаксиса. В данном слу- чае отсутствует также угроза, что будет утрачена обратная совместимость, из-за того, что к исходному формату добавлены поля или изменен порядок по- лей. с классом Logging_Acceptor в качестве параметра. Макрос ACE_FACTO- RY_DEFINE создает функцию-фабрику _make_Server_Logging_Dae- mon () в той DLL, которая содержит код дайной службы. Если коду, находящему- ся вне данной DLL, нужно сослаться на эту функцию-фабрику, то он может ис- пользовать макрос ACE_FACTORY_DECLARE для объявления функции-фабрики _make_Server_Logging_Daemon () с правильной декларацией импорта. Вспомогательные макросы АСЕ импорта/экспорта рассмотрены в блоке 37. Эти макросы помогают обеспечить правильный экспорт из DLL имен с гло- бальной областью видимости на всех поддерживаемых платформах, а также позволяют пользователям DLL правильно их импортировать. Применение макросов экспорта в служебных макросах АСЕ позволяет ACE-каркасу Service Configurator находить имя точки входа функции-фабрики при активизации службы. UML-диаграмма последовательности на рис. 5.8 показывает шаги, из кото- рых состоит процесс конфигурации серверного демона регистрации на базе файла svc.conf, представленного выше. При запуске программы, объект, создаваемый макросом ACE_STATIC_SVC_REQUIRE, регистрирует информа- цию Service_Reporter, создаваемую с использованием макроса ACE_STA- TIC_SVC_DEFINE в ACE_Service_Config. Когда вызывается метод ACE_Service_Conf ig: : open (), он использует заданную функцию-фабри- ку для создания экземпляра объекта Service_Reporter, но не активизирует его. Метод open () вызывает затем process_directives (), который ин- терпретирует директивы файла svc.conf. Первая директива активизирует
188 Глава 5 Блок 37: ACE-макросы импорта/экспорта DLL В Windows существуют особые правила импорта и экспорта имен DLL в явном виде, Разработчики, воспитанные на UNIX, могут их не знать, хотя они важны для управления использованием имен DLL в Windows. АСЕ упрощает выполнение этих правил, предлагая скрипт (script), который создает необходимые объяв- ления импорта/экспорта и устанавливает правила их корректного использо- вания. Чтобы упростить переносимость, на всех платформах, на которых рабо- тает АСЕ, можно использовать следующую процедуру: 1. Выбрать краткое мнемоническое имя для каждой создаваемой DLL. 2. Выполнить Perl-скрипт $ACE_ROOT/bin/generate export_file.pl, зада- вая в командной строке мнемоническое имя DLL. Скрипт создаст независи- мый от платформы заголовочный файл и запишет его в стандартный вывод. Следует перенаправить вывод в файл с именем cmnemonio export. h. 3. Включите (#include) созданный файл в каждый исходный файл DLL, кото- рый объявляет класс или имя с глобальной областью видимости. 4. При использовании в объявлении класса, вставьте ключевое слово cmnemo- nic>_Export между class и именем класса. 5. При компиляции исходного кода для данной DLL, определите макрос <mne- monic>_BUILD_DLL. Следование этой процедуре в Windows приводит к следующим результатам: • Имена, оформленные по перечисленным выше правилам, будут объявлены с использованием _declspec(dllexport) при создании в DLL. • При ссылках из компонентов внешних по отношению к данной DLL, имена бу- дут объявляться как_declspec (dllimport). Если вы выберите отдельное мнемоническое имя для каждой DLL и будете по- следовательно их применять, то будет проще создавать и использовать DLL на платформах разных ОС. статическую службу Service_Reporter. Вторая директива инициирует сле- дующие действия: 1. DLL S LD подключается динамически. 2. Для создания экземпляра Reactor_Logging_Server_Adapter вызы- вается функция-фабрика _make_Server_Logging_Daemon. 3. Для активизации службы вызывается метод init () объекта новой служ- бы. После того как все действия, связанные с конфигурацией, завершены, про- грамма main() вызывает ACE_Reactor::run_reactor_event_loop(). С этого момента службы включены в работу, точно также как объекты, которые конфигурировались статически в предыдущих примерах. Реконфигурация сервера. С помощью ACE-каркаса Service Configurator можно осуществлять реконфигурацию сервера во время выполнения в ответ на внешние события, например, сигналы или команды. После наступления собы- тия, каркас повторно читает свой файл (или файлы) svc. conf и выполняет указанные директивы, такие как подключение к серверу или удаление из него объектов служб, и приостановка или возобновление работы существующих объектов служб. Теперь мы покажем, как использовать эти возможности, что- бы динамически реконфигурировать наш серверный демон регистрации.
АСЕ-каркас Service Configurator 189 Рис. 5.8 UML-диаграмма последовательности конфигурирования сервера регистрации Начальная конфигурация сервера регистрации имеет следующие ограни- чения: • Используется реализация Logging_Acceptor из раздела 3.3, в которой не задаются тайм-ауты для обработчиков регистрации, простаивающих в течение длительного времени. • Нет способа завершить работу метода run_reactor_event_loop (), вызванного для синглтона ACE_Reactor. Мы можем добавить эти возможности, не изменяя ни существующего кода, ни службы Service_Reporter данного процесса, а, изменив файл svc.conf и отдав серверу команду осуществить собственную реконфигура- цию с помощью сигнала, например, SIGHUP или SIGINT. 1 remove Server_Logging_Daemon 2 3 dynamic Server_Logging_Daemon Service_Object * 4 SLDex:_make_Server_Logging_Daemon_Ex() 5 "$SERVER_LOGGING_DAEMON_PORT" 6 7 dynamic Server_Shutdown Service_Object * 8 SLDex:_make_Server_Shutdown() Файл svc. conf создан в предположении, что серверный процесс выпол- няется и Server_Logging_Daemon уже сконфигурирован. АСЕ-каркас Servi- ce Configurator обеспечивает механизмы конфигурирования и допускает нали- чие политик, определяющих, когда и что реконфигурировать, которые могут управляться администратором или другим приложением.
190 Глава 5 Строка 1 Удаляем существующий серверный демон регистрации из репо- зитория служб АСЕ и выгружаем его из адресного пространства приложения. Строки 3-5 Динамически конфигурируем другую реализацию шаблона Reactor_Logging_Server_Adapter в адресном пространстве серверного демона регистрации. Функция-фабрика _make_Server_Logging_Dae- mon_Ex () создается, в частности, с помощью приведенного ниже макроса ACE_FACTORY_DEFINE, находящегося в файле SLDex. срр, который исполь- зуется для создания DLL SLDex. typedef Reactor_Logging_Server_Adapter<Logging_Acceptor_Ex> Serve r_Loggi ng_Daemon_Ex; ACE_FACTORY_DEFINE (SLDEX, Server_Logging_Daemon_Ex) Этот макрос создает реализацию шаблона Reactor_Logging_Ser- ver_Adapter с параметром Logging_Acceptor_Ex. Строки 7-8 Динамически конфигурируем объект службы Server_Shut- down, который использует функцию controller () и класс Quit_Handler для ожидания того момента, когда администратор завершит работу сервера ко- мандой, поступающей на его стандартный ввод. Класс Server_Shutdown, при- веденный ниже, наследует от ACE_Service_Ob j ect, так что мы можем управ- лять его жизненным циклом с помощью ACE-каркаса Service Configurator. class Server_Shutdown : public ACE_Service_Object { public: Метод Server_Shutdown::init() создает поток для выполнения функции controller (): virtual int init (int, ACE_TCHAR * [] ) { reactor_ = ACE_Reactor::instance (); return ACE_Thread_Manager::instance ()->spawn (controller, reactor_, THR_DETACHED); 1 Мы передаем флаг THR_DETACHED, чтобы создать такой управляющий по- ток, идентификатор которого и другие ресурсы автоматически возвращаются ОС после его завершения. Метод Server_Shutdo,wn: : fini () сообщает реагирующему объекту о необходимости завершить работу: virtual int fini () {' Quit_Handler *quit_handler = 0; ACE_NEW_RETURN (quit_handler, Quit_Handler (reactor_), -1); return reactor_->notify (quit2_handler); } // ... Другие методы опущены ...
АСЕ-каркас Service Configurator 191 private: ACE_Reactor *reactor_; ); Мы используем ACE__FACTORY_DEFINE для создания функции-фабрики _make_Server_Shutdown (), нужной для ACE-каркаса Service Configurator. ACE_FACTORY_DEFINE (SLDEX, Server_Shutdown) UML-диаграмма последовательности на рис. 5.9 показывает последова- тельность шагов процесса реконфигурации серверного демона регистрации, на основе файла svc. conf приведенного выше. Механизм динамической реконфигурации ACE-каркаса Service Configura- tor позволяет разработчикам изменять функциональность сервера или точнее настраивать его характеристики, не требуя большой работы, связанной с по- вторной разработкой и установкой. Например, устранение неисправности, свя- занное с ошибочной реализацией службы регистрации, заключается просто в ди- намическом реконфигурировании функциональности эквивалентной службы, которая содержит дополнительный инструментарий, содействующий иденти- фикации ошибочного поведения. Этот процесс реконфигурации может быть выполнен без модификации, повторной компиляции и компоновки или по- вторного запуска выполняющегося в данный момент серверного демона реги- страции. В частности, эта реконфигурация не затрагивает Service_Repor- ter, который был сконфигурирован статически. Reactor Logging Server Adapter ACE_Service^Confia k X process directives fini Reactor Logging Server Adapter --------1------- "gobbler1 "factory1 init I r * I i i T I + A I I I I Service Reporter i "factory" init Рис. 5.9 UML-диаграмма последовательности реконфигурации сервера регистрации
192 Глава 5 5.5 Резюме Традиционные методы разработки программного обеспечения, при кото- рых сетевые приложения связываются в единое целое и конфигурируются ста- тически, могут иметь ограничения. Например, время и усилия, необходимые на повторное создание полноценной программы для каждой новой или модифици- рованной службы существенно повышают затраты на разработку и сопровожде- ние. Кроме того, осуществление таких изменений «по ходу дела» неэффективно и подвержено ошибкам, что может привести к тому, что затраты резко возрастут, а требования заказчика, по-прежнему, останутся неудовлетворенными. В данной главе приведено описание ACE-каркаса Service Configurator, кото- рый реализует паттерн Component Configurator [POSA2], чтобы обеспечить пе- реносимый способ статического или динамического связывания служб, с по- следующим динамическим созданием их реализацйй, приостановлением, во- зобновлением и завершением их работы во время выполнения. Этот каркас улучшает расширяемость сетевого ПО, позволяя приложениям откладывать выбор конкретной реализации службы до более позднего этапа в жизненном цикле ПО — до момента установки или даже до времени выполнения. Эта гиб- кость обеспечивает следующие важные преимущества: • Приложения можно собирать и переконфигурировать во время выпол- нения за счет комбинирования и подгонки (mix-and-match) независимо разработанных служб. • Разработчики могут сконцентрироваться на функциональности службы и других ключевых аспектах проектирования, не фиксируя раньше вре- мени конкретную конфигурацию служб. • Приложения собирают из нескольких независимо разработанных служб, которые не требуют наличия априорной информация друг о друге, но вполне могут работать совместно. В данной главе мы постарались объяснить происхождение и применение каждого класса ACE-каркаса Service Configurator и вспомогательных макросов. Мы использовали эти новые возможности для разделения структуры серверов регистрации из предыдущей главы на независимые, компонуемые и конфигу- рируемые службы. Результатом стала сетевая служба регистрации, которую можно разворачивать и конфигурировать различными способами. Расширяе- мость, обеспечиваемая ACE-каркасом Service Configurator, позволяет операто- рам и администраторам выбирать те возможности и альтернативные стратегии реализации, которые имеют наибольший смысл в конкретных ситуациях, а так- же создавать локализованные решения, которые проще инициализировать и развивать.
Глава 6 АСЕ-каркас Task Краткое содержание В данной главе рассматриваются структура и применение АСЕ-каркаса Task. Этот каркас повышает модульность и расширяемость параллельных объ- ектно-ориентированных сетевых приложений. АСЕ-каркас Тask создает общую основу паттернов параллелизма, таких как Active Object и Half-Sync/Half-Async [POSA2]. После обсуждения мотивов создания и применения классов данного каркаса, мы используем АСЕ-каркас Task для улучшения параллелизма и мас- штабируемости нашей сетевой службы регистрации. 6.1 Обзор АСЕ-каркас Task обеспечивает мощные, расширяемые возможности объ- ектно-ориентированного параллелизма, которые дают возможность создавать потоки в контексте объектов, а также передавать сообщения между объектами, выполняющимися в отдельных потоках и ставить эти сообщения "в очередь. Данный каркас можно использовать для реализации основных паттернов па- раллелизма [POSA2], например: • Паттерн Active Object разделяет поток, вызывающий метод, и поток, вы- полняющий данный метод. Этот паттерн повышает параллелизм и упро- щает синхронный доступ к объектам, выполняющимся в контексте од- ного или нескольких потоков. • Паттерн Half-Sync/Half-Async разделяет асинхронную и синхронную об- работку в параллельных системах, чтобы упростить программирование, не снижая заметно производительность. Этот паттерн вводит три уров- ня: один для асинхронной (или взаимно-согласованной) обработки, один для работы с синхронными сервисами и уровень очередей, который является посредником во взаимодействии между асинхронным и син- хронным уровнями. 7 Программирование сетевых приложений на C++. Том 2
194 Глава 6 В данной главе показано, как использовать эти паттерны и АСЕ-каркас Task, который является их реализацией, для разработки параллельных рбъект- но-ориентированных приложений на более высоком уровне абстракции, чем внутренние API операционных систем и интерфейсные фасады C+-I-. АСЕ-кар- кас Task состоит из следующих классов, которые сетевые приложения могут ис- пользовать для создания потоков и управления ими, а также для передачи сооб- щений между одним или несколькими потоками в одном процессе: Класс АСЕ Описание | АСЕ_Меs s age_Block Реализует паттерн Composite (GoF) с целью эффективной обработки сообщений как. фиксированного, так и нефиксированного размера. | ACE_Message_Queue Поддерживает очередь сообщений внутри процесса, которая дает приложениям возможность передавать сообщения между потоками процесса.и помещать их в буфер. ACE__Thread_Manager Позволяет приложениям переносимым образом создавать один или несколько потоков и управлять их свойствами, временем жизни и синхронизацией. ACE_Task Позволяет приложениям создавать пассивные или активные объекты, которые разделяют различные этапы обработки; используют сообщения, чтобы обмениваться запросами, ответами, данными и управляющей информацией; а также могут ставить в очередь и обрабатывать сообщения, последовательно или параллельно. | Наиболее важные связи между классами ACE-каркаса Task приведены на рис. 6.1. Данный каркас обеспечивает следующие преимущества: • Делает стиль программирования более последовательным, позволяя разработчикам использовать язык C++ и объектно-ориентированные методы во всех параллельных сетевых приложениях. Например, класс ACE_Task обеспечивает объектно-ориентированную программную аб- стракцию, которая связывает потоки уровня ОС и объекты C++. • Управляет группой потоков как единым целым. Многопоточным сете- вым приложениям часто требуется, чтобы несколько потоков начинали и завершали работу вместе. Поэтому класс ACE_Task реализует возмож- ность группы потоков (threadgroup), которая позволяет другим потокам ждать завершения работы всей группы, прежде чем продолжить свою ра- боту. • Разделяет потоки производители (producer) и потребители (consumer), которые работают параллельно и взаимодействуют, обмениваясь сооб- щениями через синхронную очередь сообщений. • Интегрирует параллельную обработку, например, синхронный уровень паттерна Half-Sync/Half-Async с ACE-каркасом Reactor, который рас-
АСЕ-каркас Task 195 сматривался в главе 3 или ACE-каркасом Proactor, который будет рас- смотрен в главе 8. • Облегчает динамическую конфигурацию задач (task) путем интеграции с ACE-каркасом Service Configurator, рассмотренным в главе 5. Разработ- чикам нет необходимости раньше времени, на этапе проектирования или даже во время выполнения, фиксировать решения, связанные с паралле- лизмом. Задачи могут быть спроектированы так, чтобы работать в кон- фигурациях, которые могут изменяться по мере того, как изменяются требования, диктуемые потреблением ресурсов сайта и изменением ус- ловий работы. Классы ACE_Message_Block и ACE_Thread_Manager были рассмотре- ны в главах 4 и 9 [C++NPvl] соответственно. Поэтому материал данной главы' в значительной степени сосредоточен на описании возможностей, предостав- ляемых классами ACE_Message_Queue и ACE_Task. Тем не менее, мы проде- монстрируем использование всех классов ACE-каркаса Task для повышения па- раллелизма наших клиентских и серверных демонов регистрации. Если вы не- знакомы с паттернами Active Object и Half-Sync/Half-Async [POSA2], мы советуем вам познакомиться с ними, прежде чем разбираться в деталях приме- ров данной главы. Рис. 6.1 Классы ACE-каркаса Task 6.2 Класс ACE__Message_Queue Обоснование Как упоминалось в разделе 2.1.4, сетевые приложения, службы которых яв- ляются многоуровневыми/модульными (layered/modular), часто формируются из совокупности взаимодействующих задач внутри процесса. Чтобы упростить интерфейсы и структуру, минимизировать затраты на сопровождение и повы- сить повторное использование, эти задачи должны иметь следующие свойства:
196 Глава 6 • Слабая взаимозависимость задач — объекты разных задач должны иметь минимальную зависимость друг от друга по данным и методам. • Сильная внутренняя связность задач — методы и данные задачи долж- ны объединяться родственной функциональностью. Чтобы этого добиться, задачи часто взаимодействуют, обмениваясь сооб- щениями посредством обобщенных методов, таких как push () или put (), вместо того, чтобы напрямую вызывать специализированные, статически типи- руемые методы. Сообщения могут представлять собой запросы на обслужива- ние, результаты работы или другие типы обрабатываемых данных. Они могут также представлять собой управляющие запросы, которые управляют задачами с целью изменения их поведения, например, самозавершения и самореконфигу- рации. Когда задачи производители и потребители совмещаются в одном процес- се, они часто обмениваются сообщениями через внутрипроцессную очередь со- общений. В таком дизайне, задача-производитель помещает сообщения в син- хронную очередь сообщений (synchronized message queue), обслуживаемую зада- чей-потребителем, которая обрабатывает и удаляет сообщения. Хотя некоторые операционные системы сами предоставляют очереди для обмена сообщениями внутри процесса, такая возможность существует не на всех платформах. Кроме того, там, где очереди есть, они чаще всего или сильно зависят от платформы, как очереди сообщений VxWorks, и/или неэффектив- ны, громоздки и подвержены ошибкам при использовании, как очереди сооб- щений System V IPC [Ste99]. Примеры того, как создавать классы интерфейсных фасадов для урегулирования этих проблем, приведены в [C++NPvl]. Интер- фейсные фасады могли бы инкапсулировать весь спектр доступных механиз- мов очередей внутрипроцессных сообщений в общий интерфейс, эмулируя,' где требуется отсутствующие возможности. АСЕ, тем не менее, придерживается другого подхода по следующим причинам: • Чтобы избежать «случайной» сложности. Внутренние механизмы оче- редей сообщений, там, где они есть, трудно программировать по всем правилам, так как они используют низкоуровневые С API. Они могут также накладывать ограничения на системное администрирование, что плохо отражается на процессе эксплуатации продукта и может увели- чить стоимость его сопровождения. Например, очереди сообщений System V IPC могут продолжать существовать и после завершения рабо- ты программы, если очистка осуществлена некорректно. Эти остатки очередей могут мешать приложению при повторном запуске или привес- ти к утечке ресурсов и часто требуют вмешательства администратора для корректировки системы «вручную». Аналогичным образом, очереди со- общений System V IPC предлагают такую организацию межпроцессных, очередей, которая приводит к повышению издержек, по сравнению с оче- редями внутри процесса. • Чтобы обойти потенциальные сложности. Каждый внутренний меха- низм организаций очередей имеет свой собственный формат сообще- ний. Правильно инкапсулированные интерфейсными фасадами очереди
АСЕ-каркас Task 197 сообщений требуют, поэтому, соответствующей инкапсуляции сообще- ний. Согласование нужного набора возможностей двух интерфейсов с базовыми очередями и типами сообщений существенно усложняет ди- зайн и процесс разработки, интерфейсов. Эта проблема может только обостряться по мере переноса АСЕ на новые платформы. • В АСЕ уже имеется класс сообщений, обладающий большими возмож- ностями. ACE_Message_Block является удобным и эффективным классом сообщений, обладающим большими возможностями. Он был рассмотрен в главе 4 [C++NPvl]. Данный класс предлагает больше воз- можностей, чем многие зависимые от платформ форматы сообщений, и, кроме того, переносимо работает на всех платформах, поддерживаемых АСЕ. Учитывая эти факторы, АСЕ определяет класс ACE_Message_Queue, ко- торый представляет собой переносимый и эффективный механизм организа- ции внутрипроцессных очередей, усиливающий и без того большие возможно- сти ACE_Message_Block. Функциональные возможности класса ACE_Message_Queue является упрощенным, переносимым механизмом организации внутрипроцессных очередей сообщений, который обеспечивает следующие возможности: • Позволяет ставить сообщения в начало очереди, в конец или в порядке приоритета сообщений. Сообщения могут исключаться как из начала очереди, так и из ее конца. • Использует ACE_Message_Block, чтобы обеспечить эффективный ме- ханизм буферизации сообщений, который сводит к минимуму выделе- ние динамической памяти и копирование данных. • Его экземпляры могут создаваться как в многопоточных, так и в однопо- точных конфигурациях, позволяя программистам выбирать между же- сткой синхронизацией и снижением издержек в тех случаях, когда орга- низация параллельного доступа к очереди не требуется. • В многопоточных конфигурациях он поддерживает настройку управле- ния потоком (flow control), чтобы Предотвратить ситуацию, когда пото- ки-производители с высокой скоростью создания сообщений перепол- няют ресурсы памяти и исчерпывают ресурсы обработки более медлен- ных потоков-потребителей сообщений. • Позволяет задавать тайм-ауты как для операций постановки в очередь, так и для операций исключения из очереди, чтобы избежать бесконечно- го блокирования. • Может интегрироваться в механизм обработки событий АСЕ-каркаса Reactor. • Реализует стратегические распределители (allocators), которые могут вы- делять память, используемую сообщениями, из разных источников, та-
1198 Глава 6 ких как общая память, память кучи, статическая память или локальная память потока. На рис. 6.2 показан интерфейс ACE_Message_Queue. Так как у этого клас- са большой диапазон возможностей, мы разделяем его описание на четыре ка- тегории, перечисленные ниже. 1. Методы инициализации и управления потоком. Следующие методы ACE_Message_Queue могут использоваться для инициализации и управ- ления потоком: | Метод Описание IACE_Message_Queue() j open () Инициализирует очередь, факультативно задавая ее нижнюю и верхнюю границы и стратегию уведомлений (блок 38). high_water_mark() low_water_mark() Устанавливают/получают (set/get) отметки уровней «малой» и «полной воды», которые определяют где управление потоком начинается и где заканчивается. notification_strategy() Устанавливает/получает (set/get) стратегию увёдомлений. ACE_Message_Queue содержит пару отметок «уровней воды», создавае- мых потоком управления, с целью предохранения высокоскоростного отпра- вителя сообщений от переполнения буферов и вычислительных ресурсов более медленного получателя. Для отображения общего использования ресурсов со- общениями очереди, отметки уровней измеряются в байтах. Каждый ACE_Message_Queue содержит счетчик загруженных байтов в каждом поме- щенном в очередь ACE_Message_Block, чтобы следить за количеством бай- тов в очереди. Новое сообщение может быть поставлено в очередь, если общее количество байтов в этой очереди до размещения нового сообщения меньше или равно отметке уровня «полной воды». Иначе, управление потоком ACE_Message_Queue работает следующим образом: • Если очередь «синхронная», вызывающий поток будет блокирован до тех пор, пока количество байтов в очереди опустится ниже уровня «малой воды» (low watermark) или истечет время тайм-аута. • Если очередь «несинхронная», данный вызов вернет -1с errno, установ- ленным В EWOULDBLOCK. Чтобы указать является очередь синхронной или нет, можно использовать классы характеристик (traits classes) ACE_MT_SYNCH или ACE_NULL_SYNCH со- ответственно. Как только объекты ACEj_Message_Block удаляются из очере- ди сообщений, счетчик байтов в очереди соответствующим образом декремен- тируется. Отметка уровня «малой воды» указывает на то количество байтов в очереди, при котором предыдущая очередь ACE_Message_Queue, с управ- лением потоком, не считалась полной.
АСЕ-каркас Task 199 г------------- --------------------------------1 SYNCH-STRATEGY J ACE_Massag« Queue —.....-.....rJ # head_ : ACE_Message_Block * # tail_ : ACE—Message—Block * # high-Water—mark— : size—t # low_water—mark— : size t + ACE-Message—Queue (high-Water-mark : size-t » DEFAULT—HWM, low—water—mark : size—t' - DEFAULT—LWMr notify : AdE-Notificatibii-Strategy * = 0) + open (high_water__mark : size_t ж DEFAULT_HWM, low_water_mark size_t DEFAULT_LWM, notify : ACE_Notification_Strategy * * 0) : int + flush () : int + notification_strategy (s : ACE_Notification_Strategy *) : void + iS— empty () : + is_full () : t int int + engueue_ tail (item : ACE_J4essage_Block *, timeout : ACE_Time_Value ♦ « 0) int + enqueue_head (item : ACE_Message_Block ★, timeout : ACE_Time_Value * ж 0) : int + enqueueJprio (item : ACE_Message_Block *, .timeout : ACE_Time_Value * « 0) : int + dequeuejhead (item : ACE_Message_Block *&, timeout : ACE_Time_Value * « 0) : int + dequeue_tail (item : ACE_Message_Block timeout : ACE_Time_Value * = 0) : int + high_water_mark (new_hwm : size_t) : void + high__water_mark (void) : size_t t low__water_mark (new_lwm : size_t) : void + low_wateremark (void) size_t + close () : int + deactivate () : int + activate () : int + pulse () : int + state () : int Рис. 6.2 Класс ACE_Mes sage_Queue По умолчанию оба уровня, и «полной воды» и «малой воды», установлены на 16К. Значения уровней по умолчанию приводят к тому, что ACE_Messa- ge_Queue начинает управлять потоком, когда в очереди больше 16К, и прекра- щает управление потоком, когда количество байтов в очереди становится мень- ше 16К. Значения по умолчанию настроены на приложения, у которых средний размер ACE_Message_Block значительно меньше 1К. В зависимости от проектных ограничений и ограничений, связанных с на- грузкой, приложениям могут требоваться различные установки этих уровней, или одного из уровней. Значения уровней по умолчанию могут быть изменены в конструкторе ACE_Message_Queue или методом open (), а также с помо- щью методов-мутаторов (mutator methods) high_water_mark () и low_wa- ter_mark (). Один из подходов заключается в том, чтобы задавать уровни с помощью ACE-каркаса Service Configurator, рассмотренного в главе 5, со зна- чениями по умолчанию, определяемыми по результатам измерений характери- стик приложения на контрольных задачах в процессе разработки и тестирова-
200 Глава 6 ния. Примеры разделов 6.3 и 7.4 иллюстрируют, как устанавливать и использо- вать уровень «полной воды» очереди сообщений, чтобы осуществлять управление потоком в многопоточных сетевых приложениях. 2. Методы постановки в очередь, исключения из очереди и буферизации сообщений. Следующие методы выполняют основную работу в ACE_Mes- sage_Queue: Метод Описание is_empty() is_full() Метод is_emp.ty () возвращает «истину», если очередь не содержит блоков сообщений. Метод is fui 1 () возвращает «истину», если количество байтов в очереди превышает отметку уровня «полной воды» (high watermark). enqueue_tail () Ставит сообщение в конец очереди. | enqueue_head () Ставит сообщение в начало очереди. enqueue_prio () Ставит сообщение в очередь в соответствии с его приоритетом. dequeue_head() Исключает и возвращает (return) сообщение из начала очереди. dequeue_tail () Исключает и возвращает (return) сообщение из конца очереди. Дизайн класса ACE_Message_Queue основан на возможностях буфериза- ции и организации очередей сообщений в System V STREAMS [Rag93]. Сообще- ния, передаваемые в очередь сообщений, являются экземплярами ACE_Mes- sage_Block и могут быть разделены на простые (simple) и составные (compo- site) сообщения. Простые сообщения содержат один ACE_Message_Block. Составные сообщения содержат несколько объектов ACE_Message_Block, которые связываются в единое целое в соответствии с паттерном Composite [GoF], который предоставляет структуру для создания рекурсивных объедине- ний. Составное сообщение часто содержит следующие типы сообщений: • Управляющее сообщение (control message), которое содержит служеб- ную (bookkeeping) информацию, такую как адреса назначения и длина полей, за которой следуют блоки данных (см. следующий пункт). • Одно или несколько информационных сообщений (data messages), бло- ков данных, которые составляют фактическое содержание составного со- общения. Сообщения в очереди связаны с двух сторон, двумя указателями, которые могут быть получены с' помощью методов доступа (accessor method) next () и prev (). Такой дизайн оптимизирует постановку в очередь в ее начале и ис- ключение из очереди в ее конце. Организация очереди на основе приоритетов с помощью метода ACE_Message_Queue: : enqueue_prio () использует метод доступа ACE_Message_Block: :msg_priority () для постановки
АСЕ-каркас Task 201 блока данных в очередь «выше» всех блоков данных, уже находящихся в очере- ди, приоритет которых ниже. Блоки данных с одинаковым приоритетом ста- вятся в очередь в порядке FIFO. Каждый блок данных составного сообщения объединяется в линейную по- следовательность через указатель продолжения (continuation pointer), который может быть получен с помощью метода доступа cont (). Рис.6.3 показывает, как три сообщения могут быть объединены в одно целое для формирования ACE_Message_Queue. 4 Synch strategy. I ACE_Me s s age_Queue tail head ACE_Message_Block next () prev() cont() r-----------J— --------• ACE_Data_Block | ACE_Me s s age_Block ACE_Message_Block next ()-----------------] prev() cont() (------------1——, --------1 ACE_Data_Block J next --------------- prev() cont() r------------I— i ACE_Data_Block j ACE_Me s s a ge_Block next () prev() cont () •} ACE_Data_Block } Рис. 6.3 Структура ACE_Message_Queue Первый и последний блоки данных в изображенной очереди являются про- стыми сообщениями, тогда как средний представляет собой составное сообще- ние с одним блоком данных, связанным своим указателем продолжения. В блоке 39 описан ACE_Message_Queue_Ex, который является вариан- том ACE_Message_Queue, рассчитанным на обмен строго типизированными сообщениями. 3. Параметризованные стратегии синхронизации. Если внимательно изу- чить рис. 6.2, то можно обнаружить, что шаблон ACE_Message_Queue па- раметризован классом характеристик (traits) SYNCH_STRATEGY. Такой ди- зайн базируется на паттерне Strategized Locking [POSA2], который парамет- ризует механизмы синхронизации, используемые классом для защиты критических секций от параллельного доступа. Внутри класс ACE_Messa- ge_Queue использует следующие характеристики (traits) из своего шаб- лонного параметра класса характеристик SYNCH_STRATEGY: template <class SYNCH_STRATEGY> class ACE_Message_Queue { H ... protected: // Характеристики (traits) C++, которые координируют
202 Глава 6 II параллельный доступ. ACE_TYPENAME SYNCH_STRATEGY::MUTEX 1оск_; ACE_TYPENAME SYNCH_STRATE.GY:: CONDITION notempty_; ACE_TYPENAME SYNCH_STRATEGY::CONDITION notfull_; В блоке 40 описаны характеристики (traits) C++ и идиомы (idioms) классов характеристик (traits class). Эти идиомы позволяют разработчикам приложе- ний настраивать стратегии синхронизации ACE_Message_Queue так, чтобы они соответствовали их частным потребностям. Фрагмент класса ACE_Messa- ge_Queue>приведенный выше, показывает, как используются следующие ха- рактеристики (traits) в его шаблонном параметре класса характеристик: • 1оск_ сериализует доступ к очереди; • notempty_ и notfull_ являются условными переменными, которые позволяют вызывающим потокам ждать получения сообщения из очере- ди или постановки сообщения в очередь, соответственно. Класс характеристик SYNCH_STRATEGY, используемый для параметриза- ции ACE_Message_Queue, позволяет настраиваться на синхронную или не- синхронную очередь во время компиляции. Блок 38: Объединение ACE Message Queue с ACE Reactor Некоторые платформы предлагают способ интеграции событий, связанных с внутренними очередями сообщений, и демультиплексированием синхрон- ных событий. Например, AIX-версия select о может демультиплексировать события, создаваемые .очередями сообщений System V. Хотя такое использо- вание select () не является переносимым, возможность интеграции очереди сообщений с реагирующим объектом полезна в тех приложениях, в которых один поток вынужден обрабатывать и события ввода/вывода, и события эле- ментов, размещенных в очереди сообщений. Класс ACE_Message_Queue, по- этому, предлагает переносимый способ объединения организации очере- дей событий с ACE-каркасом Reactor, представленным в главе 3. Конструктор класса ACE_Message_Queue, а также его методы open () и notl- f ication_strategy () можно использовать для задания стратегии уведомле- ний для ACE_Message_Queue. Такой дизайн является одним из примеров пат- терна Strategy (GoF), который допускает подстановку различных алгоритмов без изменения клиента (которым, в данном случае, является класс ACEjMessa- ge_Queue). Стратегия уведомлений должна быть производной от ACE_Notif i- cation_Strategy, что позволяет гибко вводить любую нужную приложению стратегию. Одной из таких стратегий-подклассов является ACE_Reactor_No- tificatior._strategy, конструктор которой связывает ее с ACE_Reactor, cACE_Event_Handler ис маской событий (event mask). После того как объект данной стратегии связан с ACE_Message_Queue, каждое помещаемое в оче- редь сообщение вызывает следующую последовательность действий: 1. ACE_Message_Queue вызывает метод данной стратегии notify () . 2. Метод ACE_Reactor_Notification_Stratogy: :notify() уведомляет ассо- циированный реагирующий объект, используя его механизм уведомлений. 3. Реагирующий объект перенаправляет это уведомление заданному обра- ботчику событий, используя назначенную маску.
АСЕ-каркас Task 203 Блок 39: Класс ACE Message Queue Ex Класс ACE_Message_Queue ставит в очередь и исключает из нее объекты ACE_Message_Block, что предоставляет возможность динамического расши- рения представления сообщений. Для программ, требующих обмена строго типированными сообщениями, АСЕ предлагает класс ACE_Message_Que- ue_Ex, который ставит в очередь и исключает из нее сообщения, являющиеся реализациями шаблонного параметра message_type, а не блоками ACE_Mes- sage_Block. • ACE_Message Queue_Ex предлагает те же возможности, что и АСЕ^меэ- sage_Queue. fro основным преимуществом является то, что типы данных, опре- деляемые приложением, можно ставить в очередь или исключать из очереди, без необходимости осуществлять при каждой операции приведение типа или копировать объекты В порции данных ACE_Message_Block. Так как ACE_Mes- sage_Queue_Ex не является производной от ACE_Message_Queue, она не может использоваться с классом ACE_Task, рассмотренным в разделе 6.3. Наборы интерфейсных фасадрв синхронизации АСЕ могут объединяться для формирования классов характеристик (traits classes), которые определяют настраиваемые стратегии синхронизации. АСЕ обеспечивает следующие два класса характеристик, которые предопределяют наиболее распространенные характеристики (traits) синхронизации: • ACE_NULL_SYNCH — Характеристики в этом классе реализуются наос- нове механизмов «nulln-блокировок, как показано ниже. class ACE_NULL_SYNCH { public: typedef ACE_Null_Mutex MUTEX; typedef ACE_Null_Mutex NULL_MUTEX; typedef ACE_Null_Mutex PROCESSJMUTEX; typedef ACE_Null_Mutex RECURSIVE_MUTEX; typedef ACE_Null_Mutex RW_MUTEX; typedef ACE_Null_Condition CONDITION; typedef ACE_Null_Sentaphore SEMAPHORE; typedef ACE_Null_Semaphore NULL_SEMAPHORE Класс ACE_NULL_SYNCH является примером паттерна Null Object [Woo97], который упрощает приложения за счет использования заполнителей «по-ор», ко- торые заменяют условные операторы в реализациях классов. ACE_NULL_SYNCH часто используется в однопоточных приложениях или в приложениях, в кото- рых отсутствует необходимость межпоточной синхронизации или за счет тща- тельного проектирования, или за счет ее реализации посредством какого-то другого механизма. Примеры клиентских демонов регистрации в разделе 7.4 иллюстрируют применение класса характеристик ACET_NULL_SYNCH. • ACE_MT_SYNCH — Характеристики в этом предопределенном классе реализуются на основе реальных механизмов блокировок:
204 Глава 6 class ACE_MT_SYNCH { public: typedef ACE_Thread_Mutex MUTEX; typedef ACE_Null_Mutex NULL_MUTEX; typedef ACE_Process_Mutex PROCESS_MUTEX; typedef ACE_Recursive_Thread_Mutex RECURSIVE_MUTEX; typedef ACE_RW_Thread_Mutex RW_MUTEX; typedef ACE_Condition_Thread_Mutex CONDITION; typedef ACE_Thfead_Semaphore SEMAPHORE; typedef ACE_Null_Semaphore NULL_SEMAPHORE; Класс характеристик ACE_MT_SYNCH определяет стратегию синхрониза- ции, включающую наличие переносимых, эффективных синхронизаторов, подходящих для многопоточных приложений. Примеры клиентских демонов регистрации в разделах 6.2 и 7.4 иллюстрируют применение класса характери- стик ACE_MT_SYNCH. Параметризация шаблона ACE_Message_Queue классом характеристик (traits class) обеспечивает следующие преимущества: • Позволяет использовать ACE_Message_Queue и в одяопоточных, и в многопоточных конфигурациях, не требуя изменения реализации класса. Блок 40: Характеристики C++ и идиомы классов характеристик Характеристика (trait) — это тип, который содержит информацию, используе- мую другим классом или алгоритмом для определения политик во время ком- пиляции, Класс характеристик (traits class) (Jos99) является удобным способом объединения набора характеристик, которые должны применяться в конкрет- ной ситуации, чтобы, соответствующим образом, изменить поведение другого класса. Характеристики и классы характеристик являются C++ идиомами соз- дания классов на базе политик (C++ policy-based class design idioms) (Ale01), ко- торые широко используются в стандартной библиотеке C++ (Aus99). Например, класс char traits определяет характеристики (traits) типов сим- волов, их тип данных и функции для сравнения, поиска и присваивания симво- лов этого типа. Стандартная библиотека C++ обеспечивает специализацию char_traits<> для char и wchar_t. Эти характеристики символов затем изме- няют поведение общих классов, таких как basic_iostream<> и ba- sic s t г i ng < >. Классы ios t ream и s tг ing определяются путем специализации шаблонов классов как char_traits<char>. Аналогично, классы wiostream и wstring определяются путем специализации шаблонов char_tra- i t s<wchar__t>. Данные идиомы C++ похожи по духу на паттерн Strategy (GoF), который допус- кает подстановку характеристик (characteristics) поведения класса, не требуя изменений в самом классе. Паттерн Strategy включает заданный интерфейс, который обычно связывается динамически во время выполнения с помощью виртуальных методов. В отличие от этого, характеристики (traits) и идиомы клас- сов характеристик (traits class) включают подстановку набора членов класса и/или методов, которые могут связываться статически во время компиляции с помощью параметризованных типов C++.
АСЕ-каркас Task 205 • Позволяет полностью изменять аспекты синхронизации реализаций ACE_Message_Queue с помощью паттерна Strategized Locking. Например, характеристики (traits) MUTEX и CONDITION ACE_Message_Qu- eue преобразуются, если используется класс характеристик ACE_NULL_SYNCH, в ACE_Null_Mutex и ACE_Null_Condition. В этом случае, результирую- щий класс очереди сообщений не имеет издержек синхронизации. Наоборот, если ACE_Message_Queue параметризуется классом характеристик ACE_MT_SYNCH, его характеристики (traits) MUTEX и CONDITION преобразуются в ACE_Thre- ad_Mutex и ACE_Condition_Thread_Mutex. В этом случае, результирую- щий класс очереди сообщений функционирует в соответствии с паттерном проектирования Monitor Object [POSA2], который предоставляет следующие возможности: • Осуществляет синхронизацию параллельно выполняемых методов, что- бы гарантировать, что только один метод в каждый момент времени вы- полняется внутри объекта. • Позволяет методам объекта планировать последовательность их совме- стных действий. В блоке 49 объясняется, как АСЕ использует макросы, чтобы реализовать ACE_NULL_SYNCH и ACE_MT_SYNCH для тех компиляторов C++, в которых отсутствует поддержка классов характеристик (traits classes) в шаблонах. Когда ACE_Message_Queue параметризуется ACE_NULL_S YNCH, вызовы его методов постановки в очередь и исключения из очереди никогда не блоки- руют вызывающий поток при достижении границ очереди. Вместо этого они возвращают -1 с errno, установленным в EWOULDBLOCK. Напротив; когда ACE_Message_Queue реализуется с ACE_MT_SYNCH, ее методы постановки в очередь и исключения из очереди поддерживают блокируемые операции, не- блокируемые операции и операции с контролем времени. Например, когда синхронная очередь пуста, вызовы ее методов исключения из очереди по умол- чанию будут блокироваться до тех пор, пока в очередь не будет помещено сооб- Щение и очередь перестанет быть пустой. Аналогичным образом, когда син- хронная очередь заполнена, вызовы ее методов постановки в очередь по умол- чанию блокируются до тех пор, пока сообщения достаточного объема будут исключены из очереди, чтобы уменьшить количество байтов в очереди до уровня, который ниже ее уровня «малой воды» и очередь перестает быть пол- ной. Такое, по умолчанию блокируемое, функционирование может быть изме- нено путем передачи следующих типов значений ACE_Time_Value данным методам: Значение Поведение NULL-указатель на AC E_T ime_V а1ue Указывает, что метод постановки в очередь или исключения из очереди должен ждать без ограничения времени, то есть он будет блокироваться до того момента, когда завершится работа метода, или когда закроется очередь, или когда сигнал прервет этот вызов.
206 Глава 6 Значение Поведение He-NULL указатель на ACE_Time_Value, меТОДЫ которого sec () И usee () возвращают 0 Указывает, что методы постановки в очередь или исключения из очереди должны выполнять неблокируемую операцию, то есть если метод не завершается успешно, то сразу возвращает -1 и устанавливает errno в ewouldblock. He-NULL указатель на ACE_Time_Value, Методы которого sec () И usee () возвращают значение > 0 Указывает, что методы постановки в очередь или исключения из очереди должны ждать заданное время (в абсолютном выражении), возвращая -1 с errno, установленным в ewouldblock, если метод не завершит свою работу к указанному времени. Этот вызов будет также завершаться раньше, если очередь закрывается или вызов прерывается сигналом. | В блоке 6 описаны различные интерпретации значений тайм-аутов, ис- пользуемых разными классами АСЕ. 4. Методы завершения работы и создания сообщений. Следующие методы могут быть использованы для завершения работы, дезактивации и/или создания сообщений в ACE_Message_Queue: Метод Описание | deactivate () Изменяет состояние очереди на deactivated и во- зобновляет работу всех потоков, ждущих выполнения операций с очередью (поставить, исключить)., Этот метод не исключает из очереди сообщений, уже на- ходящихся в ней. | pulse () Изменяет состояние очереди на PULSED и возобновляет работу всех потоков, ждущих выполнения операций с очередью (поставить, исключить). Этот метод не исклю- чает из очереди сообщений, уже находящихся в ней. z | state () Возвращает состояние очереди. I activate () Изменяет состояние очереди на act ivated. I ~ACE_Message_Queue() close () Прекращает работу данной очереди и немедленно исключает из нее все сообщения. | flush () Исключает все сообщения из очереди, но не изменяет ее состояние. | ACE_Message_Queue всегда находится в одном из трех внутренних со- стояний: • ACTIVATED, в котором все операции выполняются в обычном режиме (очередь всегда начинает работать в состоянии ACTIVATED). • DEACTIVATED, в котором все операции с очередью сразу возвращают-1 и устанавливают errno в ESHUTDOWN до того момента, когда очередь опять будет активизирована.
АСЕ-каркас Task 207 • PULSED, переход в которое приводит к ожиданию того, что все операции с очередью будут сразу завершаться так, как если бы очередь была дезак-; тивирована; и все же, все операции инициированные уже в состоянии PULSED выполняются так, как если бы очередь была в состоянии ACTI- VATED. Состояния DEACTIVATED и PULSED полезны в тех ситуациях, когда необ- ходимо уведомить все потоки, и производители, и потребители, о наступлении некоторого значимого события. Переход в любое из этих двух состояний при- водит к возобновлению работы всех потоков, ждущих выполнению операций с очередью. Отличаются эти состояния тем как выполняются операции с очере- дью после осуществления перехода. В состоянии DEACTIVATED, все операции с очередью завершаются ошибкой до тех пор, пока ее состояние не изменится на ACTIVATED. Состояние PULSED, однако, функционально эквивалентно со- стоянию ACTIVATED, то есть все операции с очередью выполнятся в обычном режиме. Состояние PULSED имеет, в основном, информационное значение — возобновивший работу производитель/потребитель может принять решение о целесообразности дальнейшего ожидания операций с очередью в зависимо- сти от того, какое значение возвращает метод state (). Пример из раздела 7.4 иллюстрирует использование метода pulse () для запуска процедуры повтор- ного установления соединения. Ни одно сообщение не удаляется из очереди, при переходе в любое из со- стояний. Сообщения могут быть исключены из очереди деструктором ACE_Mes- sage_Queue, методами close () или flush (). Эти методы исключают все блоки данных, остающиеся в очереди сообщений. Метод flush (), в отличие от двух других методов, не дезактивирует очередь. В блоке 41 описано несколь- ко протоколов, которые поэтапно завершают работу объектов ACE_Messa- ge_Queue. Блок 41: Протоколы «поэтапного» завершения работы ACE Message Queue Чтобы исключить непредвиденную потерю сообщений в очереди, когда нужно закрыть ACE_Message_Queue, потоки, производители и потребители, могут реа- лизовать следующий протокол: 1. Поток-производитель ставит в очередь специальное сообщение, напри- мер, блок данных, размер полезных данных которого равен 0 и/или тип ко- торого mb_stop, указывает на то, что он завершает очередь. 2. Поток-потребитель закрывает очередь, при получении этого сообщения о завершении работы, после того, как он обработает все другие сообще- ния, которые поступили в очередь раньше. Один из вариантов этого протокола может использовать метод ACE_Messa- ge_Queue:: enqueue_prio () с целью повышения приоритета сообщения о за- вершении (shutdown message) таким образом, чтобы оно имело преимущест- во по сравнению с сообщениями с низким приоритетом, которые могут уже на- ходиться в очереди.
208 Глава 6 Пример Данный пример показывает как можно использовать ACE_Message_Qu- eue для реализации клиентского демона регистрации. Как показано на рис. 1.10, клиентские демоны регистрации работают на каждом хосте, участвующим в ра- боте сетевой службы регистрации и выполняет следующие задачи: • Использует механизм локального IPC, например, общую память, каналы (pipes) или сокеты с обратной связью, чтобы получать регистрационные записи от клиентских приложений, работающих на том же хосте, что и клиентский демон регистрации. • Использует механизм удаленного IPC, например, TCP/IP, чтобы пере- дать регистрационные записи серверному демону регистрации, работаю- щему на заданном хосте. В нашем примере используется два потока, чтобы реализовать модель парал- лелизма с ограниченным буфером (bounded buffer) [ВА90], на базе синхронной ACE_Message_Queue с использованием класса характеристик ACE_MT_SYNCH. В нашем клиентском демоне регистрации обработчик событий и АСЕ-кар- кас Reactor используют основной поток для чтения регистрационных записей из сокетов, соединенных с клиентскими приложениями через сетевое устройст- во, реализующее петлю обратной связи. Обработчики событий ставят каждую регистрационную запись в синхронную ACE_Message_Queue. Отдельный поток-ретранслятор (forwarder thread), осуществляющий продвижение дан- ных, работает параллельно, непрерывно выполняя следующую последователь- ность действий: 1. Исключает сообщения из очереди. 2. Помещает сообщения большими порциями в буфер. 3. Передает эти порции серверному демону регистрации по ТСР-соединению. Используя синхронную очередь сообщений, основной (main) поток может продолжать чтение регистрационных записей от клиентских приложений до тех пор, пока в очереди сообщений есть место. Поэтому степень параллелизма сервера может быть повышена, даже в том случае, если поток-ретранслятор, осуществляющий продвижение данных, будет время от времени блокировать- ся при отсылке регистрационных записей серверу регистрации по соединению с регулированием потока (flow controlled connection). Клиентский демон регистрации выполняет несколько функций, включая прием соединений от клиентских приложений, получение регистрационных записей, установление соединений с сервером регистрации и передачу регист- рационных записей. Поэтому мы не можем его реализовать путем повторного использования класса Reactor_Logging_Server из примера раздела 3.5. Вместо этого, мы определяем новую группу классов, показанных на рис. 6.4. Роли каждого из классов перечислены ниже:
АСЕ-каркас Task 209 Класс Описание CLD_Handler Целевой объект обратных вызовов от ACE_Reactor. Полу- чает регистрационные записи от клиентов, преобразует блоки асе Message_Block и ставит их в синхронную оче- редь сообщений, которые обрабатываются отдельным по- током и передаются серверу регистрации. CLD__Acceptor Фабрика, пассивно принимающая соединения от клиентов и регистрирующая их у ACE_Reactor для обработки объектом CLD_Handler. CLD__Connector Фабрика, активно устанавливающая (и когда нужно пере- устанавливающая) соединения с сервером регистрации. Client_Logging__ Daemon Класс фасада, который объединяет в едйное целое три предыдущих класса. АСЕ_Мв8 e&ge_Queue <ACE_MT_SYNCH> ACEJSOCK-Stream --------ж------- Рис. 64 Классы клиентского демона регистрации Как показано на рис. 6.4, классы реализации клиентского демона регистра- ции спроектированы в соответствии с паттерном Acceptor-Connector. CLD_Ac- ceptor играет роль получателя (акцептора), CLD_Connector играет роль со- единителя (коннектора), a CLD_Handle г выполняет роль обработчика службы (service handler). Взаимосвязи между основным (main) потоком, потоком- ретранслятором, осуществляющим продвижение данных, классами, представ- ленными на рис. 6.4, и синхронной очередью ACE_Message_Queue, которая объединяет их, показаны на рис. 6.5.
210 Глава 6 Мы начинаем реализацию с включения необходимых заголовочных фай- лов АСЕ. ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include "ace/OS.h" "ace/Event__Handler. h" "ace/INET_Addr.h” "ace/Ge t_Opt.h” "ace/Log__Record.h" ”ace/Message_Block.h” "ace/Message__Queue. h" "ace/Reactor.h" "ace/Service_Object.h” "ace/Signal.h” "ace/Synch.h" "ace/SOCK_Acceptor.h" "ace/SOCK_Connector.h" "ace/SOCK_Stream.h" "ace/Thread__Manager. h" "Logging_Acceptor.h" ”CLD_export.h” Рис. 6.5 Взаимосвязь объектов клиентского демона регистрации Каждый класс, представленный на рис. 6.4, определен в файле Cli- ent__Logging_Daemon. срр и рассматривается ниже. CLDJHandler. Данный класс обеспечивает следующие возможности: • Получает регистрационные записи от клиентов. • Преобразует регистрационные записи в блоки ACE__Message_Block.
АСЕ-каркас Task 211 • Ставит эти блоки данных в синхронную очередь сообщений. • Выполняет отдельный поток, который исключает блоки данных из оче- реди и передает их большими порциями серверу регистрации. Класс CLD_Handler приведен ниже: #if !defined (FLUSH_TIMEOUT) ♦define FLUSH_TIMEOUT 120 /* 120 секунд == 2 минуты. */ ♦endif /* FLUSHJTIMEOUT */ class CLD_Handler : public ACE-Event_Handler { public: enum { QUEUE_MAX = sizeof (ACE_Log_Record) * ACE_IOV-MAX }; // Hook-метод инициализации. virtual int open (CLD_Connector *); // Ноок-метод завершения работы. virtual int close () ; // Устанавливает соединение с сервером регистрации. virtual АСЕ—SOCK—Stream &peer () { return peer_; } // Ноок-методы реактора. virtual int handle—input (ACE—HANDLE handle); virtual int handle_close (ACE_HANDLE = ACE_INVALID—HANDLE, ACE—Reactor_Mask = 0) ; protected: // Передает per.записи серверному демону регистрации. virtual АСЕ—THR—FUNC—RETURN forward (); // Отправляет per.записи из буфера операцией // записи-со-слиянием. virtual int send (ACE-Message_Block *chunk[], size-t count); // Точка входа потока управления (ретранслятора). static АСЕ—THR—FUNC—RETURN run_svc (void *arg); // Синхронная очередь сообщений <ACE_Message—Queue>. ACE-Message-Queue<ACE-MT—SYNCH>- msg_queue_; // Управляет потоком-ретранслятором. ACE—Thread—Manager thr_mgr_; // Указатель на <CLD-Connector>. CLD—Connector *connector-; // Соединение с сервером регистрации. ACE—SOCK—Stream peer-/ };
212 Глава 6 CLD_Handler не нужны ни конструктор, ни деструктор, так как его hook- методы open () и close () выполняют действия, связанные с инициализаци- ей и удалением при их вызове классом фабрики CLD_Acceptor. CLD_Hand- ler выполняет две функции — ввода и вывода — они поясняются ниже. • Функция ввода. Так как CLD_Handler наследует от ACE_Event_Hand- ler, он может использовать АСЕ-каркас Reactor для организации про- цесса ожидания поступления регистрационных записей от клиентских приложений, установивших соединение с клиентским демоном регист- рации через TCP-сокеты обратной связи. Когда клиентскому демону ре- гистрации поступают регистрационные записи, синглтон ACE_Reactor передает управление следующему hook-методу CLD_Handler: : hand- le_input(): lint CLD_Handler::handle_input (ACE_HANDLE handle) { 2 ACE_Message_Block *mblk = 0; 3 Logging_Handler logging_handler (handle); 4 5 if (logging_handler.recv_log_record (mblk) != -1) 6 if (msg_queue_.enqueue_tail (mblk->cont ()) != -1) ( 7 mblk->cont (0); 8 mblk->release (); 9 return 0; // Успешное завершение. 10 } else mblk->release (); 11 return -1; // Сообщение об ошибке. 12 } Строки3-5 Используем Logging_Handler из главы 4 [C++NPvl] для счи- тывания регистрационной записи, расположенной за параметром дескриптора сокета, и сохранения записи в ACE_Message_Block. Строки 6-8 Ставим сообщение в синхронную очередь, которая обслужива- ется потоком-ретранслятором, продвигающим данные. Серверный демон ре- гистрации обычно получает регистрационную запись, уже после маршалинга, за исключением строки с именем хоста, поэтому мы ставим в очередь только дан- ные регистрационной записи (ссылку на которые содержит mblk->cont ()), но не имя хоста (на которое ссылается mblk). Когда серверный демон регистрации получает регистрационную запись, он добавляет в ее начало имя клиентского хоста регистрации, сразу после того, как клиентский демон регистрации запи- шет имя клиента. Если вызов enqueue_tail () завершается успешно, поле продолжения устанавливается в NULL, чтобы метод mblk->release () вос- станавливал только тот блок данных, который хранит имя хоста. Строки 10-11 Если клиентское приложение разрывает TCP-соединение или возникает ошибка, hook-метод handle_input () возвращает -1 (в блоке 13 рассматриваются стратегии действий с партнерскими приложениями, которые просто прекращают обмениваться данными). Это значение приводит к тому, что реактор вызывает следующий hook-метод handle_close (), который за- крывает соединение:
АСЕ-каркас Task 213 int CLD_Handler::handle_close (ACE_HANDLE handle, ACE_Reactor_Mask) { return ACE_OS::closesocket (handle); ) Заметьте, что нам не нужно удалять этот объект в методе handle_clo- se (), так как памятью управляет класс Client_Logging_Daemon. • Функция вывода. Объект CLD_Handler инициализируется тогда, ко- гда метод connect () из CLD_Connector вызывает следующий hook- метод open (): 1 int CLD_Handler::open (CLD_Connector *connector) { 2 connector_ = connector; 3 int bufsiz = ACE_DEFAULT_MAX_SOCKET_BUFSIZ; 4 peer ().set_option (SOL_SOCKET, SO_SNDBUF, 5 &bufsiz, sizeof bufsiz); 6 msg_queue_.high_water_mark (CLD_Handler::QUEUE_MAX); 7 return thr_mgr_.spawn (&CLD_Handler::run_svc, 8 this, THR_SCOPE_SYSTEM); 9 ) Строки 2-5 Сохраняем указатель на CLD_Connector и увеличиваем пере- сылочный буфер сокета peer () до его максимального объема, чтобы макси- мально увеличить пропускную способность в сетях с большой задержкой и/или с высокой скоростью передачи. Строкаб Устанавливаем отметку уровня «полной воды» msg_queue_ рав- ной sizeof (ACE_Log_Record)* ACE_IOV_MAX. Так как регистрационные записи помещаются в буфер группами вплоть до ACE_IOV_MAX, прежде чем отправляются серверному демону регистрации, отметка уровня «полной воды» очереди устанавливается так, чтобы она могла принять, по меньшей мере, ACE_IOV_MAX регистрационных записей. Хотя поставленные в очередь реги- страционные записи будут еще проходить CDR-маршалинг, использование в качестве максимального размера регистрационной записи после демарша- линга (ACE_Log_Record) является хорошим приближением. Строка 7-8 Используем ACE_Thread_Manager из главы 9 [C++NPvI] для создания потока с областью видимости на уровне системы, который выполняет статический метод CLD_Handler: : run_svc () параллельно по отношению к основному потоку.1 Статический метод run_svc () осуществляет приведе- ние своего аргумента-указателя на void к указателю на CLD_Handler и затем поручает его обработку методу forward (): ACE_THR_FUNC_RETURN CLD_Handler::run_svc (void *arg) ( CLD_Handler ‘handler = ACE_static_cast (CLD_Handler *, arg); return handler->forward (); ) Так как метод CLD_Handler: : close () ждет завершения этого потока, нам не нужно пере- давать методу spawn () флаг THR_DETACHED.
214 Глава 6 Теперь мы покажем метод CLD_Handler: : forward (), который выпол- няется в своем собственном потоке и пересылает регистрационные записи сер- верному демону регистрации. Как показано ниже, этот метод оптимизирует пропускную способность сети за счет буферизации регистрационных записей, пока ни будет получено максимальное количество или пока ни истечет макси- мальное время. 1 АСЕ_THR_FUNC_RETURN CLDJiandler::forward () { .2 АСЕ_Message_Block *chunk[ACE_IOV_MAX]; 3 size_t message—index = 0; 4 ACE_Time_Value time_of_last—send (ACE_OS::gettimeofday ()); 5 ACE_/Time_Value timeout; 6 ACE_Sig_Action no_sigpipe ((ACE_SignalHandler) SIG_IGN); 7 ACE_Sig_Action original—action; 8 ho_sigpipe . register_action (SIGPIPE,, &original_action) ; 9 10 for (;;) { 11 if (message_index == 0) { 12 timeout = ACE_OS::gettimeofday (); 13 timeout += FLUSH TIMEOUT; 14 } 15 ACE_Message_Block *mblk = 0; 16 if (msg_queue_.dequeue_head (mblk, Stimeout) == -1) { 17 if (errno 1= EWOULDBLOCK) break; 18 else if (message_index == 0) continue; 19' } else { 20 if (mblk->size () == 0 21 && mblk->msg_type () == ACE_Message_Block::MB_STOP) 22 { mblk->release (); break; } 23 chunk[message_index] = mblk; 24 ++message index; 25 } 26 if (message_index >= ACE_IOV_MAX 27 || (ACE_OS::gettimeofday () - time_of_last_send 28 >= FLUSHJHMEOUT) ) { 29 if (send (chunk, message_index) «= -1) break; 30 time of last_send = ACE_OS::gettimeofday () ; 31 } 32 } 33 34 if (message_index > 0) send (chunk, message_index); 35 msg—queue_.close (); 36 nO-Sigpipe.restore_action (SIGPIPE, original_action); 37 return 0; 38 } Строки 2-5 Мы будем помещать в буфер столько блоков, сколько может быть послано одной операцией записи-со-слиянием. Поэтому мы объявляем массив указателей на ACE_Mes sage_Block, чтобы хранить указатели на бло- ки, которые будут исключаться из очереди. Мы также определяем индекс, что-
АСЕ-каркас Task 215 бы следить за количеством записей и объектов ACE_Time_Value в буфере и чтобы фиксировать время отправки последней регистрационной записи и время тайм-аута до следующей передачи содержимого буфера. Объекты ACE_Time_Value используются, чтобы ограничить интервал времени, в тече- ние которого регистрационные записи могут находиться в буфере, до того как будут переданы серверу регистрации. Строки 6-8 В UNIX-системах будет генерироваться сигнал SIGPIPE, если вызов send () завершится неудачей из-за того, что партнерское приложение закрыло соединение. По умолчанию сигнал SIGPIPE приводит к аварийному завершению процесса. Мы используем класс ACE_Sig_Act ion, чтобы с помо- щью его метода принимать и игнорировать сигнал SIGPIPE, что позволяет об- рабатывать отказы send () в обычной выполняемой ветви, а не по сценарию сигнала. Строка 10 Этот цикл будет выполняться до тех пор, пока очередь сообще- ний будет оставаться в дезактивированном состоянии. Строки 11-14 Повторно устанавливаем значение тайм-аута для операции передачи содержимого буфера на первой итераций, после истечения тайм-аута и после того, как блоки сообщения отправлены по назначению. Строки 15-16 Ждем истечения следующего тайм-аута, чтобы исключить из очереди msg_queue_ указатель на следующий ACE_Message_Block. Строки 17-18 Если операция исключения из очереди завершается неуда- чей, не по причине истечения тайм-аута, выходим из цикла. Если время тайм-аута истекло, но в буфере нет регистрационных записей, просто продол- жаем выполнение цикла, ожидая исключения блоков сообщения из очереди. Строки 19-22 После успешного исключения блока сообщения из очереди, мы сначала проверяем, не равен ли его размер 0 и не является ли его тип МВ_ЗТОР.,По соглашению, приложение использует данный тип блока сообще- ния, чтобы запросить завершение потока. После получения сообщения о завер- шении, мы исключаем этот блок и выходим из цикла. Строки 23-24 Мы сохраняем блок регистрационной записи в следующей доступной позиции массива порций и инкрементируем счетчик сохраненных блоков сообщения. Строки 26-30 Всякий раз, когда заполняется буфер или истекает FLUSH_TIMEOUT, вызываем метод CLD_Handler: : send () (см. ниже), что- бы переслать регистрационные записи из буфера одной операцией записи-со- слиянием. Метод s end () исключает все сохраненные блоки сообщений и сбра- сывает message_index, чтобы отразить тот факт, что блоков данных не оста- лось. Если записи были благополучно отправлены, мы записываем время, когда были отправлены эти записи. Строка 34 Если dequeue_head () завершается неудачей или было полу- чено сообщение о завершении, все оставшиеся в буфере регистрационные за- писи отправляются по назначению путем вызова CLD_Handler: : send (). Строка 35 Закрываем очередь сообщений, чтобы освободить ее ресурсы. Строка 36 Восстанавливаем сигнал SIGPIPE в его первоначальное состоя- ние, до вызова метода forward ().
216 Глава 6 Строка 37 Завершаем работу метода CLD_Handler: : forward (), что приводит к завершению работы потока. Метод CLD_Handler: : send () передает регистрационные записи из бу- фера серверу регистрации. Он также отвечает за возобновление соединения с сервером, если соединение закрывается. lint CLD_Handler::send (ACE_Message_Block *chunk[], 2 size_t &count) { 3 iovec iov[ACE_IOV_MAX]; 4 size_t iov_size; 5 int result = 0; 6 7 for (iov_size = 0; iov_size < count; ++iov_size) { 8 iov[iov_size].iov_base = chunk[iov_size]->rd_ptr (); 9 iov[iov_size].iov_len = chunk[iov_size]->length (); 10 } 11 12 while (peer ().sendv_n (iov, iov_size) == -1) 13 if (connector_->reconnect () == -1) ( 14 result = -1; 15 break; 16 } 17 18 while (iov_size > 0) { 19 chunk[--iov_size]->release (); chunk[iov_size] = 0; 20 ) 21 count = iov_size; 22 return result; 23 } Строки 3-9 Чтобы подготовить операцию записи-со-слиянием, мы собира- ем указатели на данные и размеры этих данных из поступающих блоков сооб- щения в массив iovec. Строки 12-16 Метод ACE_SOCK_Stream: : sendv_n () отправляет реги- страционные записи из буфера одной операцией записи-со-слиянием. Если происходит сбой sendv_n () из-за разрыва соединения, мы пробуем возобно- вить соединение,с помощью метода CLD_Connector: :reconnect (). Если reconnect () восстанавливает соединение, то повторно вызывается sendv_n (). CLD_Connector: : reconnect () осуществляет несколько попыток восста- новить соединение, в общей сложности до MAX_RETRIES раз. Данные посылаются на каждой итерации цикла whi 1е. Так как на приклад- ном уровне нет управления транзакциями, то нет и сквозного квитирования, то есть подтверждения того, что регистрационные записи были получены и запи- саны. Метод sendv_n () ^гожет возвращать количество байтов отправленных уровню TCP для последующей передачи, но не дает гарантии, что переданные байты были получены хостом-получателем или что они были прочитаны сер- верным демоном регистрации. Поскольку не существует надежного способа сообщить, сколько регистрационных записей было получено и записано, а ре-
АСЕ-каркас Task 217 гистрация какой-то записи несколько раз, во всяком случае, не повредит, то, в случае восстановления соединения, все записи должны быть переданы еще раз. Строки 18-21 Последний цикл метода освобождает все регистрационные данные и устанавливает все указатели ACE_Message_Block в 0. Чтобы отра- зить тот факт, что порций данных регистрационных записей действительно больше не осталось, мы сбрасываем счетчик в 0. Метод CLD_Handler: : close () является public-методом. Он вызывается методом CLD_Acceptor: :handle_close () или методом Client_Log- ging_Daemon: : f ini (), чтобы завершить обработчик. Он вставляет сообще- ние с размером 0 и типом MB_STOP в соответствующую очередь сообщений следующим образом: int CLD_Handler::close () { ACE_Message_Block *shutdown_message = 0; ACE_NEW_RETURN (shutdown_mes s age, ACE_Message_Block (0, ACE_Message_Block::MB_STOP) , -1); msg_gueue_.enqueue_tail (shutdown_message); return thr_mgr_.wait (); ) Когда поток-ретранслятор получает shutdown_message, он сбрасывает остающиеся регистрационные записи серверу регистрации, закрывает очередь сообщений, и завершает работу потока. Мы используем метод ACE_Thre- ad_Manager: : wait (), чтобы блокировать выход до завершения работы по- тока-ретранслятора. Данный метод получает также статус завершения пото- ка-ретранслятора, чтобы предотвратить утечку памяти. CLD_Acceptor. Данный класс предоставляет следующие возможности: • Представляет собой фабрику, которая пассивно принимает соединения от клиентов. • Затем регистрирует соединения у ACE_Reactor. Регистрационные за- писи, посылаемые клиентами по установленным соединениям, обраба- тываются экземпляром CLD_Handler, приведенным выше. Далее следует определение класса CLD_Acceptor: class CLD_Acceptor : public ACE_Event_Handler { public: /I Hook-метод инициализации. virtual int open (CLD_Handler *, const ACE_INET_Addr &, ACE_Reactor * = ACE_Reactor::instance ()); // Ноок-методы реактора. virtual int handle_input (ACE_HANDLE handle) ; virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE, ACE_Reactor_Mask = 0) ; virtual ACE_HANDLE get_handle () const;
218 Глава 6 protected: // Фабрика, которая пассивно соединяет <ACE_SOCK_Stream>w. ACE_SOCK_Acceptor acceptor^; // Указатель на обработчик регистрационных записей. CLD_Handler *handler_; }; Так как CLD—Acceptor наследует от ACE_Event_Handler, он может сам регистрироваться у ACE-каркаса Reactor, чтобы принимать соединения, как по- казано ниже: int CLD_Acceptor::open (CLD_Handler *handler, const ACE_INET_Addr &local_addr, ACE_Reactor *r) { reactor (r) ; /./ Сохраняем указатель на реактор. handler_ = handler; if (acceptor—.open (local—addr) == -1 || reactor ()->register—handler (this, ACE—Event—Handler::ACCEPT—MASK) == -1) return -1; return 0; } Данный метод дает указание объекту acceptor- начать прослушивание запросов на соединение и затем регистрирует этот объект у реактора-синглтона (singleton reactor) на прием новых соединений. Реактор дважды вызывает сле- дующий метод CLD—Acceptor: : get-handle (), чтобы получить дескрип- тор сокета acceptor—: ACE-HANDLE CLD-Acceptor::get_handle () const { return acceptor—.get—handle (); } Когда поступает запрос на соединение от клиентского демона регистрации, реактор-синглтон вызывает hook-метод CLD-Acceptor: :handle_input (): 1 int CLD—Acceptor: .-handle-input (ACE-HANDLE) { 2 ACE-SOCK-Stream peer_stream; 3 if (acceptor—.accept (peer_stream) == -1) return -1; 4 else if (reactor ()->register_handler 5 (рееГ-Stream.get-handle (), 6 handler—, 7 ACE—Event—Handler::READ—MASK) == -1) 8 return -1; 9 else return 0; 10 } Строки 2-3 Принимаем соединение в pee r_s t ream, который просто при- нимает это соединение и инициализирует дескриптор нового сокета, зарегист- рированный у реактора. Следовательно, после возврата handle-input (), он
АСЕ-каркас Task 219 не обязан сохраняться. Деструктор ACE_SOCK_S t ream не закрывает этот деск- риптор автоматически по причийам, изложенным в главе 3 [C++NPvl]. Строки 4-7 Используем вариант regis ter_handler () с тремя парамет- рами, чтобы зарегистрировать указатель на CLD_Handler у реактора на обра- ботку событий READ. Метод register_handler () позволяет клиентскому демону регистрации повторно использовать один и тот же объект C++ для всех своих обработчиков регистрации. Поступающие регистрационные записи ре- актор будет направлять методу CLD_Handler: : handle_input (). Следующий метод handle_close (). автоматически вызывается реакто- ром, если в процессе установления соединения или регистрации дескриптора и обработчика событий возникает ошибка: int CLD_Acceptor::handle_close (АСЕ_HANDLE, ACE_Reactor_Mask) { acceptor^.close (); handler—->close (); return 0; } Этот метод закрывает и фабрику, принимающую соединения, и CLD—Hand- ler. Метод CLD—Handler: : close () завершает работу очереди сообщений и потока-ретранслятора. CLD_Connector. Данный класс предоставляет следующие возможности: • Он представляет собой фабрику, которая активно устанавливает соеди- нение клиентского демона регистрации с серверным демоном регистра- ции. • Он активизирует экземпляр CLD_Handler, чтобы одновременно переда- вать регистрационные записи серверу регистрации. Далее приведено определение класса CLD_Connector: class CLD—Connector { public: // Устанавливает соединение с сервером регистрации // по адресу <remote—addr>. int connect (CLD—Handler *handler, const ACE-INET—Addr &remote—addr); // Возобновляет соединение с сервером регистрации. int reconnect (); private: // Указатель на <CLD_Handler>, с которым устанавливаем /7 соединение. CLD—Handler *handler—; // Адрес, на котором сервер регистрации прослушивает // запросы на соединение. АСЕ—INET—Addr remote_addr—;
220 Глава 6 Метод connect() приведен ниже: lint CLD_Connector::connect 2 (CLD_Handler *handler, 3 const ACE_INET_Addr &remote_addr) { 4 ACE_SOCK—Connector connector; 5 6 if (connector.connect (handler->peer (), remote_addr) == -1) 7 return -1; 8 else if (handler->open (this) == -1) 9 { handler->handle_close (); return -1; } 10 handler— = handler; 11 remote_addr_ = remo'te_addr ; 12 return 0; 13 } Строки 4-6 Используем интерфейсные фасады АСЕ Socket из главы 4 [C-H-NPvl] для установления TCP-соединения с серверным демоном регистра- ции. Строки 8-9 Активизируем CLD_Handler, вызывая его hook-метод open (). В случае успеха, этот метод создает поток, который выполняет ме^од CLD—Handler: : forward (), передающий регистрационные записи серверу регистрации. Если, однако, вызов open () завершается неудачей, то мы вызы- ваем метод обработчика handle_close (), чтобы закрыть сокет. Строки 10-11 Запоминаем обработчик и удаленный адрес с целью упроще- ния реализации метода CLD_Connector: : reconnect (), который использу- ется для возобновления соединения с сервером регистрации в тех случаях, если тот закрывает клиентские соединения, или из-за аварийного отказа, или в соот- ветствии с паттерном Evictor. Как показано ниже, метод reconnect () исполь- зует экспоненциальный алгоритм задержки (backoff), чтобы исключить пере- грузку сервера запросами на соединение: int CLD_Connector::reconnect () { // Максимальное количество попыток восстановления соединения, const size_t MAX_RETRIES = 5; АСЕ—SOCK—Connector connector; ACE—Time—Value timeout (1); // Начинаем с тайм-аута в 1 секунду, size—t i; for (i = 0; i < MAX—RETRIES; ++i) { if (i > 0) ACE_OS::sleep (timeout); if (connector.connect (handler—->peer (), remote—addr_, &timeout) == -1) timeout *= 2; // Экспоненциальная задержка. else { int bufsiz = ACE—DEFAULT—MAX—SOCKET—BUFSIZ; handler_->peer ().set-Option (SOL-SOCKET, SO-SNDBUF, &bufsiz, sizeof bufsiz); break;
АСЕ-каркас Task 221 ) return i == MAX_RETRIES ? -1 : 0; ) Как и раньше, мы увеличиваем пересылочный буфер сокета peer () до максимального размера, чтобы максимально увеличить пропускную способ- ность сетей с большой задержкой и/или высокоскоростных сетей. CIient_Logging_Daemon. Данный класс является фасадом, объединяющим три, описанных выше класса, при реализации клиентского демона регистра- ции. Его определение приведено ниже: class Client_Logging_Daemon : public ACE_Service_Object { public: // Hook-методы конфигуратора служб Service Configurator. virtual int init (int argc, ACE_TCHAR *argv[]); virtual int fini (); virtual int info (ACE_TCHAR **bufferp, size_t length = 0) const; virtual int suspend (); virtual int resume (); protected: // Получает, обрабатывает и отправляет дальше per.записи. CLD_Handler handler_; // Фабрика, которая пассивно присоединяет <CLD_Handler>. CLD_Acceptor acceptor_; // Фабрика, которая активно присоединяет <CLD_Handler>. CLD_Connector connector_; ); Client_Logging_Daemon наследует от ACE_Service_Object. Поэто- му он может конфигурироваться динамически цутем обработки файла svc. conf ACE-каркасом Service Configurator, рассмотренным в главе 5. Если экземпляр Client_Logging_Daemon связывается динамически, АСЕ-каркас Service'Configurator вызывает Client_Logging_Daemon: : init (): lint Client_Logging_Daemon::init (int argc, ACE_TCHAR *argv[]) { 2 u_short cld_port =• ACE_DEFAULT_SERVICE_PORT; 3 u_short sld_port = ACE_DEFAULT_LOGGING_SERVER_PORT; 4 ACE_TCHAR sld_host[MAXHOSTNAMELEN]; 5 ACE_OS_String::strcpy (sld_host, ACE_LOCALHOST); 6 7 ACE_Get_Opt get_opt (argc, argv, ACE_TEXT ("p:r:s:"), 0) ; 8 get_opt.long_option (ACE_TEXT ("client_port"), 'p', 9 ACE_Get_Opt::ARG_REQUIRED); 10 get_opt.long_option (ACE_TEXT ("server_port"), 'r', 11 ACE_Get_Opt::ARG_REQUIRED); 12 get_opt.long_option (ACE_TEXT ("server_name"), 's',
222 Глава 6 13 АСЕ_Get_Opt::ARG_REQUIRED); 14 15 for (int c; (c = get_opt ()) != -1;) 16 switch (c) { 17 case 'p’: // Номер принимающего порта клиентского // демона регистрации. 18 сId—port = АСЕ_static—cast >19 (u_short, ACE_OS : : atoi (get_opt. opt_arg ())); 20 break; 21 case ’r': // Номер принимающего порта серверного // демона регистрации. 22 sld_port = ACE—Static_cast 23 (u_short, ACE_OS::atoi (get_opt.opt_arg ())); 24 break; 25 case ’s’: // Имя хоста серверного демона регистрации. 26 АСЕ—OS—String::strsncpy 27 (sld_host, get—opt.opt—arg (), MAXHOSTNAMELEN); 28 break; 29 } 30 31 ACE—INET—Addr cld_addr (cld_port); 32 ACE—INET—Addr sld_addr (sld—port, sld_host); 33 34 if (acceptor_. open (^handler-, cld-addr) == -1) 35 return -1; 36 else if (connector-.connect (^handler , sld-addr) == -1) 37 { acceptor-.handle-dose (); return -T; } 38 return 0; 39 } Строки 2-5 Назначаем порт прослушивания (cld_port) клиентского де- мона регистрации по умолчанию, порт (sld-port) серверного демона регист- рации по умолчанию и имя хоста (sld_host). Эти сетевые адреса могут быть изменены, путем передачи соответствующих аргументов данному методу. В ча- стности, имя хоста серверного демона регистрации часто приходиться задавать с помощью опции -s. Строки 7-29 Используем итератор ACE_Get_Opt, рассмотренный в блоке 8, для анализа опций, передаваемых файлом svc. conf. Последний параметр 0, передаваемый АСЕ_Get_Opt, обеспечивает анализ опций, начиная с argv [ 0 ], а не, как по умолчанию, с argv[l]. Если в параметре argv передаются в init () какие-то из опций «-р», «-г» или «-s», или их «длинные» эквива- ленты, то изменяются соответствующие номер порта и имя хоста. Строки 31-32 С известными номерами портов и именем хоста серверного демона регистрации, формируем адреса, необходимые для установления со- единений. Строки 34-37 Инициализируем acceptor-и connector-. Когда удаляется клиентский демон регистрации, АСЕ-каркас Service Configu- rator вызывает следующий hook-метод Client-Logging_Daemon: : fini ():
АСЕ-каркас Task 223 int Client_Logging_Daemon::fini (). { acceptor_.handle_close (); handler_.close (); return 0; } Метод fini() закрывает фабрику сокетов ACE_SOCK_Acceptor и CLD_Handler, что приводит к завершению работы очереди сообщений и потока-ретранслятора. АСЕ-каркас Service Configurator, после завершения ра- боты f ini (), удалит экземпляр Client_Logging_Daemon. Теперь, когда мы создали все классы клиентского демона регистрации, мы помещаем следующий макрос ACE_FACTORY_DEFINE из блока 32 в файл реа- лизации:1 ACE_FACTORY_DEFINE (CLD, Client_Logging_Daemon) Этот макрос определяет функцию-фабрику _make_Client_Logging_Da- emon () . которая используется в следующем файле svc. conf: dynamic Client_Logging_Daemon Service_Object * CLD:_make_Client_Logging_Daemon() "-p $CLIENT_LOGGING_DAEMON_PORT" Данный файл предписывает АСЕ-каркасу Service Configurator выполнить следующие шаги при конфигурировании клиентского демона регистрации: 1. Динамически подключить DLL CLD к адресному Пространству процесса. 2. Использовать класс ACE_DLL, рассмотренный в блоке 33, для поиска фаб- рики-функции _make_Client_Logging_Daemon () в таблице имен DLLCLD. 3. Вызывается функция-фабрика для получения указателя на динамически создаваемый объект Client_Logging_Daemon. 4. Затем АСЕ-каркас Service Configurator вызывает Метод этого нового объек- та Client_Logging_Daemon:: init (), передавая в качестве его аргу- мента argc/argv строку «-р», за которой следует расширение перемен- ной окружения CLIENT_LOGGING_J)AEMON_PORT, обозначающей номер порта, на котором клиентский демон регистрации прослушивает запросы на соединение от клиентских приложений. Таким же образом могут быть переданы опции «-г» и «-s». 5. Если метод init () .успешно завершает свою работу, указатель на Cli- ent_Logging_Daemon сохраняется в ACE_Service_Repository под именем «Client_Logging_Daemon>>. Вместо того чтобы писать новую программу main (), мы повторно ис- пользуем функцию main()_ Configurable_Logging_Server. Файл svc. conf, представленный выше, просто сконфигурирует службу клиентско- 1 Мы оставляем hook-методы suspend (), resume () и info () в качестве упражнения для читателя.
224 Глава 6 го демона регистрации при запуске программы. Пример из раздела 7.4 показы- вает, как можно использовать АСЕ-каркас Acceptor-Connector, чтобы еще боль- ше упростить и усовершенствовать приведенную выше многопоточную реали- зацию клиентского демона регистрации. 6.3 Класс ACE_Task Обоснование Класс ACE_Message_Queue, рассмотренный в разделе 6.2, может быть ис- пользован в следующих целях: • для разделения передачи данных и их обработки; • для организации связи между потоками, которые параллельно выполня- ют службы в соответствии с моделью производитель/потребитель. Тем не менее, чтобы эффективно использовать модель параллелизма про- изводитель/потребитель в объектно-ориентированной программе, каждый по- ток должен иметь доступ к очереди сообщений и любой другой'информации, связанной со службой. Поэтому, чтобы сохранить модульность и связность, и уменьшить взаимозависимость, лучше инкапсулировать ACE_Messa- ge_Queue вместе со всеми ее данными и методами в один класс, обслуживаю- щие потоки которого смогут обращаться непосредственно к нему. Возможности создания потоков, предлагаемые популярными платформа- ми ОС, основываются на том, что каждый поток создается вызовом функции в стиле языка Си. В классе интерфейсного фасада ACE_Thread_Manager, рас- смотренного в главе 9 [C++NPvl], были реализованы возможности переноси- мой многопоточности. Тем не менее, программисты должны, по-прежнему, пе- редавать функции в стиле языка С своим методам spawn () и spawn_n (). Предоставление создаваемому потоку доступа к объекту C++ требует создания моста с объектной средой C++. Метод CLD_Handler: : open () является ил- люстрацией такого подхода. Так как реализовывать такой подход для каждого класса приходится снова и снова «вручную», то это хороший кандидат на по- вторное использование. Поэтому в ACE-каркасе Task определен класс ACE_Task для инкапсуляции возможностей обмена сообщениями и обеспече- ния переносимого способа выполнения потока(ов) в контексте объекта. Функциональные возможности класса ACE_Task представляет собой основу объектно-ориентированного карка- са параллелизма в АСЕ. Он обеспечивает следующие функциональные возмож- ности: • Использует экземпляр ACE_Message_Queue из раздела 6.2, чтобы отде- лить данные и запросы от их обработки. • Использует класс ACE_Thread_Manager для такой активизации зада- чи, при которой она выполняется как активный объект [POSA2], кото- рый обрабатывает сообщения в очереди одним или несколькими потока-
АСЕ-каркас Task 225 ми управления. Так как каждый поток выполняет заданный метод класса, то он может непосредственно обращаться к членам-данным задачи. • Наследует от ACE_Service_Object, следовательно, его экземпляры можно конфигурировать динамически с помощью ACE-каркаса Service Configurator (глава 5). • Наследует от ACE_Event_Handler, следовательно, его экземпляры мо- гут также служить обработчиками событий ACE-каркаса Reactor (глава 3). • Предоставляет виртуальные hook-методы, которые прикладные-классы могут подменять с целью реализации обслуживания и обработки сооб- щений в соответствии со спецификой решаемой задачи. Наше внимание в этом разделе сосредоточено на возможностях ACE_Tas к, связанных с организацией очередей и обработкой сообщений. Возможности, связанные с обработкой событий конфигурирования и динамического под- ключения/отключения, он приобретает в результате наследования от АСЕ- классов, рассмотренных в предыдущих главах. Интерфейс ACE_Task приведен на рис. б.б. Так как этот интерфейс имеет большие возможности, мы сгруппировали описание методов данного класса в три категории, рассмотренные ниже. В блоке 60 изложены некоторые допол- нительные методы ACE_Task, которые имеют отношение к ACE-каркасу Stre- ams. 1. Методы инициализации задач (task). Методы инициализации задач пере- числены в следующей таблице: Метод Описание ACE_Task() Конструктор, который может назначать указатели HQ ACE_Message_Queue И ACE_Thread_Manager, используемые данной задачей. open() Hook-меюд, который выполняет определяемые приложением действия, связанные синициализацией. thr_mgr() Получает и устанавливает указатель на ACE_Thread_Manager задачи. msg_queue() Получает и устанавливает указатель на ACE_Message_Queue задачи. activate () Преобразует задачу в активный объект, который выполняется в одном или нескольких потоках. Приложения могут изменять поведение ACE_Tas к во время запуска путем перегрузки его hook-метода open (). Этот метод выделяет ресурсы, используе- мые задачей, такие как обработчики соединений, дескрипторы ввода/вывода и блокировки синхронизации. Так как open () обычно вызывается после мето- да init (), который наследуется от ACE_Service_Ob j ect, то все опции, пе- редаваемые через АСЕ-каркас Service Configurator должны быть уже обработа- ны. Поэтому, open () может действовать с учетом уже установленных пред- 8 Программирование сетевых приложений на C++. Том 2
226 Глава 6 почтений. Метод open () часто используется также, чтобы преобразовать задачу в активный объект путем вызова ACE_Task: : activate (). Методы thr_mgr () и msg_queue () дают возможность получать доступ к механизмам управления потоками и организации очередей, используемым задачей, и изменять эти механизмы. Альтернативные механизмы управления потоками и организации очередей могут быть также переданы конструктору ACE_Task при создании экземпляра класса. Метод activate () использует указатель ACE_Thread_Manager, воз- вращаемый методом-аксессором thr_mgr () для создания одного или не- скольких потоков, которые выполняются внутри задачи. Этот метод преобра- зует задачу в активный объект, поток(и) которого управляют его поведением и реагируют на события, вместо того, чтобы полностью управляться вызовами пассивных методов, которые заимствуют поток у вызывающей стороны. В бло- ке 42 рассмотрено, как избежать утечек памяти при завершении работы пото- ка(ов) активной задачи. 2. Методы синхронизации, обработки и обмена данными между задачами. В следующей таблице описаны методы, используемые для связи задач и для пассивной и активной обработки сообщений внутри задачи. . ____________________________ ! ЧУЫГЙ 4TRATFH’ *: ?EGY ACE__Sarvica_Objact ACE_Thraad_Managar ACE__Maaaaga_Quaua J д * Ф Q ». SYNCH ST RAI АСЕ Тык + thr_count_ : size t + ACE_Task (mgr : ACE_Thread_Manager * - 0, q : ACE_Message_Queue * » 0) + open (args : void * - 0) : int + close (flags : u_long * 0) : int + activate (flags : long - THR_NEW_LWP | THR_JOINABLE, threads : int » 1, . ..) : int + thr_count () : size_t + wait {) : int + svc () : int -l- put (mb : ACE_Message_Block *, time : ACE_Time_Value * = 0) : int + putq (mb : ACE_Message_Block *t time : ACE_Time_Value * - 0) : int + getq (mb : ACE_Message_Block *&, time : ACE_Time_Value * =0) : int + ungetq (mb ; ACE_Message_Block *, time : ACE_Time_Value * = 0) : int + thr_mgr () : ACE_Thread_Manager * + thr_mgr (mgr : ACE_Thread_Manager *) + msg_queue () : ACE_Message_Queue * + msg_queue (new_q : ACE_Message_Queue ♦) Рис. 6.6 Класс асе Task
АСЕ-каркас Task 227 Метод Описание SVC ( ) Hook-метод, который может выполнять обработку услуги, реализуемой задачей. Он выполняется всеми потоками, созданными методом activate (). put о Hook-метод, который может использоваться для передачи сообщения задаче, которое может быть обработано немедленно или поставлено в очередь для последующей обработки hook-методом svc (). putq() getq() gngetqO Ставят, удаляют и замещают сообщения в очереди сообщений задачи. Методы putq (), getq () и ungetq () упрощают доступ К методам enqueue tail' (), dequeue_head ()• И enqueue_head () очереди сообщений задачи соответственно. Блок 42: Устранение утечек памяти при завершении работы потоков Вызовы ACE_Task::activate () или методов ACE_Thread_Manager spawn() и spawn_n () могут включать любые из следующих флагов: • thr_detached, который помечает созданный поток(и) как обособленный (detached) так, что когда этот поток завершает работу, ace_thread_mana- ger обеспечивает, чтобы память, которая использовалась для хранения со- стояния потока и статуса завершения была восстановлена. • thr_joinable, который помечает созданный поток(и) как объединяемый (joinable) так, что ACE_Thread_Manage г обеспечивает восстановление иден- тификатора и статуса завершения после завершения потока и после того, как другой поток получит его статус завершения. Термины «обособленный» (detached) и «объединяемый» (joinable) ведут свое происхождение от POSIX Pthreads (IEE96). По умолчанию, ACE_Thread_Manager (и, следовательно, класс АСЕ Task, кото- рый его использует) создает потоки с флагом thr_joinable. Чтобы избежать утечки ресурсов, которые ОС фиксирует для объединяемых потоков, прило- жение должно вызывать один из следующих методов: 1. ACE_Task:: wai t (), который ждет пока все потоки покинут объект ACE_Task. 2. ACE_Thread Manager : :wait_task (), который ЖДЭТ пока все ПОТОКИ ПОКИ- нут заданный объект ACEjrask. 3. ACE_Thread_Manager:: j oin (), который ждет завершения указанного потока. Если не вызывать ни один из этих методов, АСЕ и ОС не будут восстанавливать стек и статус завершения объединяемого потока и в программе будет иметь место утечка памяти. Если явно ждать завершения потоков в программе неудобно,’ можно просто передать thr_detached при создании потоков или активизации задач. Многие сетевые прикладные задачи и долговременные (long-running) потоки демонов могут быть упрощены за счет использования обособленных потоков. Однако приложение не может ждать завершения обособленного потока с помощью ACE_Task:: wai t () или получать его статус завершения через join (). Тем не: менее, приложение может использовать XCE_Thread Manager: :wait (), что- бы ждать завершения как объединяемых, так и обособленных потоков, управ- ляемых ACE_Thread_Managerj • г
228 Глава 6 Класс, производный от ACE_Task, может выполнять определяемую при- ложением обработку передаваемых ему сообщений путем подмены его hook- методов put () и svc () с целью реализации следующих двух моделей обра- ботки: 1. Пассивная обработка. Метод put () используется для передачи сообще- ний в ACE_Task. Между задачами передаются указатели на блоки ACE_Mes- sage_Block, чтобы избежать издержек копирования данных. Обработка задачи может быть выполнена полностью в контексте метода put (), где вызывающий поток заимствуется на время его обработки. Hook-метод за- дачи svc 0 не нужно использовать, если данная задача обрабатывает за- просы только пассивно в put (). 2. Активная обработка. Определяемая приложением обработка задачи может также быть выполнена активно. В этом случае, один или несколько пото- ков выполняют hook-метод задачи svc () с целью параллельной обработ- ки сообщения относительно других действий, реализуемых в приложении. Если метод задачи put () не выполняет всю обработку сообщения, то он может использовать putq (), чтобы поставить сообщение в очередь и сра- зу завершить работу. Hook-метод задачи svc () может использовать ACE_Task: : getq () для исключения сообщений, поставленных в очередь, и для их параллельной обра- ботки. Метод getq () блокируется до тех пор, пока в очереди есть сообщения, или пока истечет абсолютное время тайм-аута. То, что getq () по своей сути является блокируемым методом, позволяет потоку(ам) задачи блокироваться и возобновлять работу только тогда, когда очередь сообщений требует обслу- живания. 1. 2. 3. ACE_Task::activate() ACE_Thread_Manager:: (svc_run, this); _beginthreadex (0, 0, svc_run, this, 0, &thread_id); spawn Стек потока времени выполнения 4. template <SYNCH_STRATEGY> ACE_THR_FUNC_RETURN ACE_Task<SYNCH_STRATEGY>::svc_run (ACE_Task<SYNCH_STRATEGY> *t) { int status = t->svc()); return reinterpret_cast {^CE_THR_FUNC_RETURN, status); } Рис. 6.7 Активизация ACE Task В отличие от put (), метод svc () никогда не вызывается клиентом задачи непосредственно. Вместо этого, он вызывается одним или несколькими пото- ками, когда задача становится активным объектом, после вызова ее метода activate (). Метод activate () использует ACE_Thread_Manager ассо- циированный с ACE_Task для создания одного или нескольких потоков, как показано ниже:
АСЕ-каркас Task 229 template <class SYNCH_STRATEGY> int ACE_Task<SYNCH_STRATEGY>::activate (long flags, int n_threads, /* Остальные параметры опущены */) { И ... thr_mgr ()->spawn_n (n_threads, &ACE_Task<SYNCH_STRATEGY>::svc_run, ACE_static_cast (void *, this), flags, /* Остальные параметры опущены */); // ... ) Флаги THR_SCOPE_SYSTEM, THR_SCOPE_PROCESS, THR_NEW_LWP, THR_DE- TACHED и THR_JOINABLE могут передаваться в параметре флагов в activa- te (). В блоке 42 показывается, как можно использовать THR_DETACHED и THR_JOINABLE, чтобы избежать утечек памяти при завершении работы по- токов. ACE_Task: :svc_run() является статическим методом, используемым activate () в качестве функции-адаптера. Он выполняется во вновь создан- ном потоке(ах) управления, что обеспечивает контекст выполнения для hook- метода svc (). Рис. 6.7 иллюстрирует шаги, связанные с активизацией ACE_Task с помощью функции Windows _beginthreadex () для создания этого потока. Фактически, класс ACE_Task ограждает приложение от специ- фических деталей ОС. Когда класс, производный от ACE_Task, выполняется как активный объ- ект, его метод svc () выполняет цикл обработки событий, который использует метод getq () для ожидания поступления сообщений в очередь задачи. Эта очередь может служить буфером для последовательности управляющих и ин- формационных сообщений с целью последующей обработки методом задачи svc (). По мере поступления сообщений и постановки их в очередь методом задачи put (), ее метод svc () выполняется в отдельном потоке(ах), парал- лельно извлекая сообщения из очереди и выполняя заданную приложением об- работку, как показано на рис. 6.8. Ц: Subtask ®T£sk X 1 “*7 ; ACEjMe s s age Queu e 1:put(msg) 6:put (msg) :Task( State : ACE_Message_Queue 1:putq (msg) :Task State; 3:svc () 4:getq (msg) 5:do_work (msg) Рис. 6.8 Передача сообщений между объектами ACE_Task
230 Глава 6 В блоке 43 сопоставляются возможности ACE_Task и Java-интерфейса Runnable и класса Thread. 3. Удаление задач. Методы, используемые для частичного или полного уда- ления задач, перечислены в следующей таблице: Метод Описание ~ACE_Task() Удаляет ресурсы, выделенные конструктором ACE_Task, включая очередь сообщений, если она не была передана конструктору в качестве параметра. close () Hook-метод, который выполняет заданные приложением действия, связанные с завершением работы. Этот метод обычно не должен вызываться непосредственно приложениями, особенно, если задача является активным объектом. flush () Закрывает очередь сообщений, ассоциированную с задачей, что приводит к освобождению всех блоков данных очереди и к снятию связанных с очередью блокировок с потоков. thr_count() Возвращает количество активных в данный момент в ACEjrask потоков. wait () Метод барьерной синхронизации, который ждет завершения всех работающих в данной задаче объединяемых (joinable) потоков, прежде чем завершить свою работу. Время жизни объекта ACE_Tas к не связано со временем жизни каких-либо потоков, активизированных в этом объекте. Так как удаление объекта ACE_Task не приводит к завершению работы ни одного из активных потоков, эти потоки, следовательно, должны завершаться до удаления объекта задачи. АСЕ обеспечивает различные способы запроса завершения потоков задачи, включая механизм совместного завершения (cooperative cancellation). Очередь сообщений задачи может также использоваться для передачи сообщения о за- вершении (shutdown message) потокам задачи, в соответствии с изложенным в блоке 41. Приложения могут настраивать процесс-удаления ACE_Task, путем под- мены его hook-метода close (). Этот метод может освобождать определяемые приложением ресурсы, выделенные задачей, такие как управляющие блоки со- единений, дескрипторы ввода/вывода и блокировки синхронизации. В то вре- мя как hook-метод open () должен вызываться, по большей части, один раз для каждого экземпляра для инициализации объекта перед его активизацией, оба hook-метода, svc () и close (), вызываются один раз для каждого потока. Hook-метод svc (), следовательно, выполняет всю необходимую потоку ини- циализацию. АСЕ-каркас Task вызывает hook-метод close () в каждом потоке после возврата управления hook-методом svc (), таким образом, исключая прямые вызовы hook-метода задачи close (), особенно, если задача является активным объектом. Такая асимметрия hook-методов open () и close () яв-
АСЕ-каркас Task 231 Блок 43: ACE Task по сравнению с Java Runnable и Thread Если вам приходилось пользоваться средствами Java, интерфейсом Runnab- le'и классом Thread (LeaOO), дизайн ACEjias к должен вам показаться знако- мым. Ниже приводится их сравнение. • Метод ACE_Task::activate () ПОХОЖ на метод Java Thread, start (), ТОК как оба создают внутренние (internal) потоки. Метод Java Thread. start () создает только один поток, тогда как activate () может создавать несколь- ко потоков в одной и той же ACE_Task, упрощая создание пула потоков, что проиллюстрировано в примере данного раздела. • Метод ACE_Task:: svc () похож на метод Java Runnable. run (), так как оба являются hook-меюдами. которые выполняются во вновь создаваемом пото- ке(ах) управления. Hook-метод Java run () выполняется только в одном пото- ке на объект, тогда как метод ACE_Task: :svc() может выполняться в не- скольких потоках на объект-задачу. • ACE_Task содержит очередь сообщений, которая позволяет приложениям обмениваться сообщениями и помещать их в буфер. В отличие от этого, тако- го рода возможность организации очереди должна добавляться Java-раз- работчиками в явном виде. ляется необходимой, так как единственной надежным средством освобождения ресурсов задачи являются ее потоки. Если задача создает несколько потоков, метод close () не должен освобо- ждать ресурсы (или удалять сам объект задачи), если другие потоки продолжа- ют выполняться. Метод thr_count () возвращает количество активных пото- ков задачи. ACE_Task декрементирует счетчик потоков до вызова close (), поэтому если thr_count () возвращает значение больше 0, то данный объект, по-прежнему, активен. Метод wait () может быть использован для блокиро- вания до завершения всех потоков данной задачи, то есть до того момента, ко- гда thr_count () станет равным 0. В блоке 44 перечислены шаги, которые сле- дует выполнять при удалении ACE_Task. Пример В данном примере показано, как объединить.ACE_Tasк и ACE_Messa- ge_Queue с ACE_Reactor из главы 3 и ACE_Service_Conf ig из главы 5, чтобы реализовать параллельный сервер регистрации. Дизайн этого сервера ба- зируется на паттерне Half-Sync/Half-Async [POSA2] и на стратегии упреждаю- щего создания пула потоков, описанной в главе 5 [C++NPvl]. На рис. 6.9 пока- зан пул рабочих потоков, создаваемых в режиме упреждения при запуске серве- ра регистрации. Регистрационные записи могут обрабатываться параллельно, пока количество клиентских запросов, поступающих одновременно, не превы- шает количества рабочих потоков. Как только превысит, основной (main) по- ток помещает дополнительные запросы в буфер синхронной ACE_Messa- ge_Queue до тех Пор, пока не освободится один из рабочих потоков или пока не переполнится очередь. ACE_Me$sage_Queue играет несколько ролей в нашем сервере регистра- ции с пулом потоков в соответствии с моделью параллелизма half-sync/half- async:
252 Глава 6 Блок 44: Удаление ACEJfask Специальные меры предосторожности должны предприниматься при удале- нии асе Task, которая выполняется как активный объект. Прежде чем удалять активный объект, убедитесь, что поток(и), выполняющий его hook-метод svc () завершил свою работу. В блоке 41 описано несколько способов завершения работы hook-методов svc (), блокированных очередью сообщений задачи. Если управление жизненным циклом задачи осуществляется извне, при дина- мическом создании или при создании экземпляра в стеке, один из способов обеспечить правильную последовательность действий, связанных с удалени- ем, выглядит следующим образом: My_Task *task = new Task; // Динамически создаем новую задачу. task->open (); // Инициализируем задачу. task->activate (); // Выполняем задачу как активный объект. II ... выполняем нужные действия ... // Дезактивируем очередь сообщений, чтобы метод svc() // деблокировался и поток завершил свою работу. task->msg_gueue ()->deactivate О ; task->wait (); // Ждем завершения потока. delete task; // Освобождаем память задачи. Этот способ основан на том, что задача должна закрывать все свои потоки при удалении ее очереди сообщений. Такой подход, тем не менее, приводит к функциональной зависимости между классом Task и его пользователями. Пользователи попадают в зависимость от того какие действия будут выполнять- ся при удалении очереди, поэтому любые изменения этих действий могут вы- звать нежелательный волновой эффект во всех системах, использующих класс Task. Если задача создается динамически, то тогда, возможно, было бы лучше сде- лать так, чтобы задачу удалял ее hook-метод close (), когда последний поток покидает задачу, вместо того, чтобы непосредственно вызывать delete с ука- зателем на задачу. Возможно, тем не менее, вам, по-прежнему, придется ждать с помощью метода wait () завершения всех потоков задачи особенно, если вы готовитесь завершить процесс. На некоторых платформах ОС, когда основной поток завершает свою работу в main о, немедленно завершается весь процесс, независимо от наличия других активных потоков. • Разделяет основной поток-реактор и пул потоков. Такой дизайн позво- ляет иметь одновременно несколько активных рабочих потоков. Кроме того, он переносит ответственность за организацию очередей данных ре- гистрационных записей из пространства ядра в пользовательское про- странство, в котором больше виртуальной памяти для организации оче- реди регистрационных записей, чем у ядра. • Содействует управлению потоком между клиентами и сервером. Когда количество байтов в очереди сообщений достигает отметки уровня «пол- ной воды», ее протокол управления потоком (flow control protocol) бло- кирует основной поток (main thread). Как только заполняются нижеле- жащие буферы TCP сокетов, управление потоком передается обратно клиентам данного сервера. Это предотвращает установление клиентами
АСЕ-каркас Task 233 новых соединений или отправку регистрационных записей до того мо- мента, когда рабочие потоки получат возможность «догнать» (catch up) основной поток и разблокировать его. z- -ч Сервер регистрации Рис. 6.9 Архитектура сервера регистрации с пулом потоков Упреждающее создание потоков и организация очередей способствуют снижению затрат на создание потоков, а также сдерживают потребление ресур- сов ОС, что может существенно улучшить масштабируемость сервера. В следующей таблице перечислены классы, которые мы будем использо- вать в примере сервера регистрации с пулом потоков: Класс Описание TP_Logging_Tas к Выполняется как активный объект с пулом потоков, которые обрабатывают и сохраняют регистрационные записи, включенные в его синхронную очередь сообщений. TP_Logging_Handler Целевой объект (target) вызовов (upcalls) от ACE_Reactor, который получает регистрационные записи от клиентов и ставит их в очередь сообщений TP_Logging_Task. TP_L oggi ng_Ac с ep t о г Фабрика, которая принимает соединения и создает Объекты TP_Logging_Handler ДЛЯ обработки клиентских запросов. TP_Logging_Server Фасадный класс, который объединяет в единое целое остальные три класса.
234 Глава 6 Взаимосвязи между этими классами показаны на рис. 6.10. Классы TP_Log- ging_Acceptor и TP_Logging_Handler выполняют функции реагирующих элементов в паттерне Half-Sync/Half-Async, а метод TP_Logging_Tas k: : s vc (), который работает в параллель с рабочими потоками, выполняет функцию син- хронизации. Описание каждого класса, представленного на рис. 6.10 приведено ниже. Сначала мы включаем в файл TP_Logging_Server.h необходимые заголо- вЬчные файлы АСЕ: ♦include "ace/OS.h" ♦include "ace/Auto_Ptr.h" ♦include "ace/Singleton.h" ♦include "ace/Synch.h" ♦include "ace/Task.h" ♦include "Logging_Acceptor.h” ♦ include "Logg;ing_Event_Handler .h" ♦include "Reactor_Logging_Server.h" ♦include "TPLS_export.h" TP_Logging_Task. Данный класс предоставляет следующие возможности: • Является производным от ACE_Task, экземпляр которой создает, чтобы обеспечить синхронную ACE_Message_Queue. • Создает пул рабочих потоков, которые выполняют один и тот же метод svc (), с целью обработки и сохранения регистрационных записей, включенных в его синхронную очередь сообщений. Рис. 6.10 Классы сервера регистрации с пулом потоков
АСЕ-каркас Task 235 TP_Logging_Task приводится ниже: class TP_Logging_Task : public ACE_Task<ACE_MT_SYNCH> { // Экземпляры создаются с характеристикой синхронизации МТ. public: enum { MAX_THREADS = 4 ); II ... Методы, определение которых приводится ниже... }; Hook-методTP_Logging_Task: :ореп() вызывает ACE_Tas к: :acti- vate (), чтобы преобразовать эту задачу в активный объект, следующим обра- зом: virtual int open (void * = 0) { return activate (THR_NEW_LWP, MAX_THREADS); } Если activate () завершается без ошибок, метод TP_Logging_ Task: :svc () будет выполняться в MAX_THREADS отдельных потоков. Мы покажем метод TP_Logging_Task: : svc (), после того как рассмотрим клас- сы TP_Logging_Acceptor и TP_Logging_Handler. Метод TP_Logging_Task: : put () ставит в очередь блок данных с реги- страционной записью. virtual int put (ACE_Message_Block *mblk, ACE_Time_Value *timeout *= 0) { return putq (mblk, timeout); }; Нам нужен только один экземпляр TP_Logging_Task, поэтому мы пре- образуем его в синглтон с помощью шаблона преобразования в синглтон (sin- gleton adapter template), рассмотренного в блоке 45. Тем не менее, так как TP_Log- ging_Task будет находиться в DLL, мы должны использовать АСЕ Un- managed_Singleton, aHeACE_Singleton.TaKOft дизайн требует, чтЬбы мы явно закрывали синглтон, когда в TP_Logging_Server: : fini () заверша- ется работа задачи регистрации. typedef ACE_Unmanaged_Singleton<TP_Logging_Task, ACE_Null_Mutex> Т P_LOGGING_TASK; Так как обращения к TP_LOGGING_TASK: : instance () осуществляются только из основного потока, мы используем ACE_Null_Mutex как параметр типа синхронизации для ACE_Unmanaged_Singleton. Если доступ к этому синглтону другими потоками осуществлялся бы параллельно, нам пришлось бы задавать ему в качестве параметра ACE_Recursive_Thread_Mutex, что- бы сериализовать доступ. TP_Logging_Acceptor. Данный класс является фабрикой, которая предос- тавляет следующие возможности:
236 Глава 6 Блок 45: Шаблонные адаптеры ACE Singleton Хотя TP_Logging_Task можно запрограммировать в явном виде так, чтобы он был синглтоном, такой подход требует большой работы и чреват ошибками. Поэтому AGE определяет следующий шаблонный адаптер, который приложе- ния могут использовать для управления жизненным циклом синглтонов: template <class TYPE, class LOCK> class ACE_Singleton : public ACE_Cleanup { public: static TYPE *instance (void) { ACE_Singleton<TYPE, LOCK> *&s = singleton_; if (s == 0) { LOCK *lock = 0; ACE_GUARD_RETURN (LOCK, guard, ACE_Object_Manager::get_singleton_lock (lock), 0); if (s == 0) { ACE_NEW_RETURN (s, (ACE_Singleton<TYPE, LOCK>), 0); ACE_Object_Manager::at_exit (s); } ) return &s->instance_; } protected: ACE_Singleton (void); // Конструктор по умолчанию. TYPE in.stance_; // Внутренний экземпляр. // Единственный экземпляр адаптера <ACE_S-ingleton>. static АСЕ Singleton<TYPE, LOCK> *singleton_; Статический метод ACE_singleton::instance() использует паттерн Doub- le-Checked Locking Optimization (POSA2), для создания типозависимого экзем- пляра ace_s ing leton и доступа к нему. Затем он регистрирует этот экземпляр у ACE_Object_Manager, чтобы освободить ресурсы по завершении програм- мы. Как отмечено в блоке 23 (C++NPv1), ACE_pbject_Manager принимает на себя ответственность за удаление экземпляра ACEjsingleton, а также адап- тированного к type экземпляра. Программа может завершиться аварийно в процессе удаления синглтона, если объектный код, реализующий type отключается до того, как асе_оь- j ect_Manager удалит синглтоны, что часто бывают с синглтонами, которые на- ходятся в динамически связанных службах. Поэтому мы рекомендуем исполь- 3OBQTbACE_Unmanaged_Singleton при определении синглтонов в библиотеках DLL, которые будут подключаться и отключаться динамически. Данный класс предлагает ту же самую оптимизацию блокировок с двойным контролем для создания синглтона. И все же требуется явный вызов ACE_umnanaged_Single- ton:: close (), чтобы удалить синглтон. Метод fini () динамически создавае- мой службы — подходящее место для вызова указанного метода close (), что продемонстрировано в методе TP_Logging_Server: : f ini (). • Принимает соединения от клиентских демонов регистрации. • Создает обработчик TP_Logging_Handler, который принимает реги- страционные записи от клиентских соединений.
АСЕ-каркас Task 237 Класс TP_Logging_Acceptor приведен ниже: class TP_Logging_Acceptor : public Logging_Acceptor { public: TP_Logging_Acceptor (ACE_Reactor *r = ACE_Reactor::instance ()) : Logging_Acceptor (r) {} virtual int handle_input (ACE_HANDLE) { TP_Logging_Handler *peer_handler = 0; ACE_NEW_RETURN (peer_handler, TP_Logging_Handler (reactor' ()), -1); if (acceptor_.accept (peer_handler->peer ()) == -1) { delete peer_handler; return -1; } else if (peer_handler->open () == -1) peer_handler->handle_close (ACE_INVALID_HANDLE, 0); return 0; } }; Так как TP_Logging_Acceptor наследует от Logging_Acceptor, он может подменить handle_input (), чтобы создавать-экземпляры TP_Log- ging_Handler. TP_Loggmg_Handler. Данный класс обеспечивает следующие возможно- сти: • Принимает от клиентских соединений регистрационные записи. • Ставит регистрационные сообщения в синхронную очередь сообщений синглтона TP_LOGGING_TASK. Класс TP_Loggihg_Handler приведен ниже: class TP_Logging_Handler : public Logging_Event_Handler { friend class TP_Logging_Acceptor; Так как этот класс является производным, от Logging_Event_Handler (раздел 3.3), он может принимать регистрационные записи, если реактор их ему направит. Деструктор определяется как защищенный (protected), чтобы обеспечить динамическое размещение. Тем не менее, так как метод TP_Logging_Accep- tor: : handle_input () удаляет объекты этого типа, TP_Logging_Accep- tor должен быть дружественным (friend) этому классу. Подходящим решени- ем в данном случае является объявление дружественного класса, так как такие классы демонстрируют высокую степень зависимости и важно наложить огра- ничение на динамическое размещение TP_Logging_Handler. Три члена дан- ных используются для реализации протокола параллельного закрытия объек- тов TP_Logging_Handler, в соответствии с изложенным в блоке 46.
238 Глава 6 protected: virtual -TP_Logging_Handler- () {} // Пустой деструктор. // Количество указателей на экземпляр данного класса, которые // находятся в очереди сообщений синглтона <TP_LOGGING_TASK>. int queued_count_; // Указывает должен ли быть вызван // <Logging_Event_Handler::handle_close()>, чтобы удалить // объект this. int deferred_close_; // Сериализует доступ к <queued_count_> и к <deferred_close_>. ACE_Thread_Mutex lock_; Блок 46: Параллельное закрытие объектов TP Logging Hancller Проблемой, характерной для серверов с пулом потоков, является закрытие объектов, к которым могут одновременно обращаться несколько потоков. В нашем сервере регистрации с пулом потоков указатели TP_Logging_Hand- ier используются потоками tp_logging_task. Эти служебные потоки отделе- ны от потока, обрабатывающего цикл событий реактора, который управляет обратными вызовами TP_Logging_Handler. Поэтому мы должны обеспечить, чтобы объект TP_Logging_Handler не удалялся пока остаются указатели на него, которые использует tp_logging_task. Если клиент регистрации закры- вает соединение, TP_Logging_Handier: :handle_input () возвращает-1. За- тем реактор вызывает метод обработчика handie_ciose (), который обычно освобождает ресурсы и удаляет обработчик. К сожалению, это может при- вести к хаосу, если один или несколько указателей на этот обработчик, по- прежнему, находятся в очереди или используются потоками пула tp_log- gingjtask. Поэтому мы используем протокол подсчета ссылок, чтобы гаран- тировать, что обработчик не будет удален пока продолжают использоваться указатели на него. UML-диаграмма деятельности, приведенная ниже, иллюст- рирует действия, осуществляемые этим протоколом: Этот протокол подсчитывает, сколько раз обработчик включен в очередь со- общений синглтона tp_logging_task. Если значение счетчика больше 0, ко- гда закрывается сокет клиента регистрации, то удалить этот обработчик TP_Logging_Handler::handle_closeО пока еще не может. Позже, когда tp^loggingjtask обрабатывает каждую регистрационную запись, счетчик ссылок на этот обработчик декрементируется. Когда значение счетчика ста- новится равным 0, обработчик может завершить обработку запроса на закры- тие, который был до этого отложен.
АСЕ-каркас Task 259 В public-разделе класса определен конструктор и два метода, диспетчериза- цию которых осуществляет реактор при наступлении заданных событий. public: TP_Logging_Handler (ACE_Reactor *reactor) : Logging_Event__Handler (reactor) f queued—Count— (0), deferred_close_ (0) {) // Вызывается для событий ввода: соединение или данные, virtual int handle__input (ACE_HANDLE) ; // Вызывается при удалении объекта this, например, // когда он удаляется из реактора. virtual int handle_close (ACE_HANDLE, ACE_Reactor_Mask); ); ACE_FACTORY_DECLARE (TPLS, TP_Logging_Handler) TP_Logging_Handler: : handle_input () выполняет роль реактора в пат- терне Half-Sync/Half-Async. Он отличается от метода Logging_Event_Hand- ler: : handle_input (), так как он не обрабатывает регистрационную запись сразу после ее получения. Вместо этого, он ставит каждую регистрационную за- пись в очередь сообщений синглтона TP_LOGGING_TASK, где они затем обра- батываются параллельно. Тем не менее, в процессе обработки регистрационной записи, TP_LOGGING_TASK нужно обращаться к регистрационному файлу данного обработчика (унаследованного от Logging_Event_Handler). Что- бы упростить эту процедуру, handle_i при t () объединяет регистрационную запись с блоком данных, содержащим указатель на обработчик и включает ре- зультирующее составное сообщение в конец очереди сообщений синглтона TP_LOGGING_TASK, как показано ниже: lint TP_Logging_Handler::handle_input (ACE_HANDLE) { 2 ACE__Message_Block *mblk = 0; 3 if (logging_handler_.recv_log_record (mblk) •= -1) { 4 ACE_Message_Block *log_blk = 0; 5 ACE_NEW_RETURN 6 (log_blk, ACE_Message_Block 7 (ACE_reinterpret_cast (char *, this)), -1) ; 8 log_blk->cont (mblk); 9 ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, lock_, -1) ; 10 if (TP_LOGGING_TASK;:instance ()->put (log_blk) == -1) 11 { log_blk->release (); return -1; } 12 ++queued_count_; 13 return 0; 14 ) else return -1; 15 }
240 Глава 6 Строки 2-3 Читаем регистрационную запись из сокета соединения и поме- щаем в динамически создаваемый блок ACE_Message_Block. Строки 4-8 Создаем ACE_Message_Block, с именем log_blk, который содержит указатель на обработчик this. Конструктор ACE_Message_Block просто «заимствует» указатель this и устанавливает флаг ACE_Messa- ge_Block: : DONT_DELETE, чтобы гарантировать, что при удалении блока данных в TP_Logging_Task: : svc (), сам обработчик не будет удален. Блок mblk присоединяется к цепочке продолжения log_blk с целью формирова- ния составного сообщения. Строки 9-10 Используем ACE_Guard для запроса 1оск_, которая сериали- зует доступ к queued_count_. Чтобы избежать состояния гонок, которое мо- жет возникнуть, если обслуживающий поток обрабатывает запись до того, как queued_count_ может быть инкрементирован, блокировка запрашивается до вызова put (), чтобы вставить блок составного сообщения в очередь сообще- ний синглтона TP_LOGGING_TASK. Строки 11-13 Освобождаем ресурсы log_blk и возвращаем-1, если вызов put () завершился неудачей. Если же вызов проходит нормально, инкременти- руем счетчик вхождений обработчика в очередь TP_LOGGING_TASK. В любом случае оператор return приводит к тому, что защита снимает блокировку lbck_. Строка 14 Если клиент закрывает соединение или происходит серьезное нарушение работы, handle_input () возвращает-1. Это значение заставляет реактор вызвать TP_Logging_Handler: :handle_close (), который реа- лизует основную часть протокола параллельного закрытия объектов TP_Logging_Handler, как отмечалось в блоке 46. Метод handle_close () приведен ниже: lint TP_Logging_Handler::handle_close (ACE_HANDLE handle, 2 ACE_Reactor_Mask) { 3 int close_now = 0; 4 if (handle != ACE_INVALID_HANDLE) { 5 ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, lock_, -1) ; 6 if (queued_count_ == 0) close_now = 1; 7 else deferred_close_ = 1; 8 } else { 9 ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, lock_, -1) ; 10 queued_count_-- ; 11 if (queued_count == 0) close_now = deferred_close_; 12 } 13 14 if (close_now) return Logging_Event_Handler::handle_close (); 15 return 0; 16 } Строка 3 Переменная cl ose_now показывает должен ли в строке 14 вызы- ваться метод Logging_Event_Handler: : handle_close (). Ее значение зависит от действий, связанных со счетчиком ссылок, предпринимаемых в дру- гих частях этого метода.
АСЕ-каркас Task 241 Рис. 6.11 Цепочка блоков данных регистрационной записи Строки 4-7 Этот код выполняется при вызове handle_close () после того, как handle_input () возвращает -1. Строки 6-7 выполняются внутри критической секции, защищаемой ACE_Guard, которую автоматически зани- мает и освобождает lock^ в области действия оператора if. Если que- ue_count_ имеет значение равное 0, то ссылок на этот объект в TP_LOG- GING_TASK не осталось, поэтому мы устанавливаем значение локальной пере- менной close_now равным 1. Иначе, мы устанавливаем член данных defег- red_close_, чтобы отметить, что как только счетчик ссылок достигнет значе- ния 0, этот обработчик должен быть удален, так дак его клиент уже закрыл свою конечную точку сокета. Когда регистрационные записи обработаны, метод TP_Logging_Task: : svc () вызывает handle_close () снова и будет вы- полнять строки, приведенные ниже. Строки 8-12 Дескриптор handle принимает значение ACE_INVA- LID_HANDLE, когда handle_close () вызывается в TP_Logging_Task: :svc() после того как обработчик был удален из очереди сообщений задачи. Поэтому мы декрементируем queue_count_. Если счетчик равен 0, мы запоминаем должны ли мы закончить обработку запроса на завершение, который был до этого отложен. Как и в строках 5-7, мы используем ACE_Guard, чтобы автома- тически занимать и освобождать 1оск_ в области действия оператора else. Строка 14 Вызываем Logging_Event_Handler: : handle_close (), если значение локальной переменной close_now «истина», чтобы закрыть со- кет и регистрационный файл, а затем удалить его самого. Теперь, когда мы рассмотрели класс TP_Logging_Handler, мы можем перейти к методу TP_Logging_Task: : svc (), который параллельно выпол- няется в каждом рабочем потоке и выполняет роль синхронизации в паттерне Half-Sync/Half-Asynch. Этот метод выполняет свой собственный цикл собы- тий, который блокируется синхронной очередью сообщений. После того как сообщение поставлено в очередь методом TP_Logging_Handler: :hand- le_input (), оно будет исключено из очереди одним из рабочих потоков и за- писано в соответствующий регистрационный файл данного клиента: lint TP_Logging_Task::svc () { 2 for (ACE_Message_Block *log_blk; getq (log_blk) != -1; ) ( 3 TP_Logging_Handler *tp_handler = ACE_reinterpret_cast 4 (TP_Logging_Handler *, log_blk->rd_ptr ());
242 Глава 6 5 Logging_Handler logging_handler (tp_handler->log_file ()); 6 logging_handler.write_log_record (log_blk->cont ()); 7 log_blk->release (); 8 tp_handler->handle_close (ACE_INVALID_HANDLE, 0); 9 } 10 return 0; 11 } Строки 2-4 Вызываем метод ge tq (), который блокируется пока есть бло- ки данных. Как показано на рис.6.11, каждый блок данных является составным сообщением, которое содержит три блока данных соединенных в цепочку с по- мощью указателей продолжения в следующем порядке: 1. Указатель на TP_Logging_Handler, который содержит регистрацион- ный файл, где будут храниться регистрационные записи. 2. Имя хоста клиента, установившего соединение. 3. Содержимое регистрационной записи после ее маршалинга. Строки 5-6 Инициализируем logging_handl.er значением log_f ile, а затем вызываем Logging_Handler: : write_log_record (), который за- писывает регистрационную запись в файл. Метод write_log_record () от- вечает за освобождение цепочки блоков данных, как указано в главе 4 [C++NPV1], Строки 7-8 Вызываем log_blk->release (), чтобы освободить выде- ленные ресурсы. Однако поскольку указатель TP_Logging_Handler заимст- вуется раньше, чем динамически размещается, мы должны явным образом вы- звать TP_Logging_Handler: : handle_close () из tp_handler. Этот ме- тод уменьшает значение счетчика ссылок TP_Logging_Handler и корректно удаляет объект с помощью протокола, рассмотренного в блоке 46. TP_Logging_Server. Данный фасадный класс наследует от ACE_Servi- ce_Object, включающего Reactor_Logging_Server, и использует сингл- тон TP_LOGGING_TASK: class TP_Logging_Server : public ACE_Service_Object { protected: // Содержит реактор, акцептор и обработчики. typedef Reactor_Logging_Server<TP_Logging_Acceptor> LOGGING_DISPATCHER; LOGGING_DISPATCHER *logging_dispatcher_; public: TP_Logging_Server (): logging_dispatcher_ (0) {} // Другие методы, определение которых приведено ниже... }; Hook-метод TP_Logging_Server: : init () улучшает реализацию вза- имно-согласованного (reactive) сервера регистрации из главы 3 следующим об- разом:
АСЕ-каркас Task 243 virtual int init (int argc, ACE_TCHAR *argv[]) { int i; char **array = 0; ACE_NEW_RETURN (array, char*[argc], -1); ACE_Auto_Array_Ptr<char *> char_argv (array); for (i = 0; i < argc; ++i) char_argv[i] =ACE::strnew (ACE_TEXT_ALWAYS_CHAR (argv[i])); ACE_NEW_NORETURN (logging_dispatcher_, TP_Logging_Server::LOGGING_DISPATCHER (i, char_argv.get (), ACE_Reactor::instance ())); for (i =0; i < argc; ++i) ACE::strdelete (char_argv[i]); if (logging_dispatcher_ == 0) return -1; else return TP_LOGGING_TASK::instance ()->open () ; Метод init () создает экземпляр TP_Logging_Server: :LOGGING_DIS- PATCHER и сохраняет указатель на него в члене данных logging_dispat- cher_. Он вызывает также TP_Logging_Task: : open () с целью упреждаю- щего создания пула рабочих потоков, которые параллельно обрабатывают ре- гистрационные записи. Далее приводится метод TP_Logging_Server: : f ini (): 1 virtual int fini () ( 2 TP_LOGGING_TASK::instance ()->flush () ; 3 TP_LOGGIMG_TASK::instance ()->wait () ; 4 TP_LOGGING_TASK::close () ; 5 delete logging_dispatcher_; 6 return 0; 7 } Строка 2 Вызываем метод flush () синглтона'TP_LOGG I NG_T AS К, тем самым закрывая очередь сообщений, связанную с данной задачей, что приво- дит к удалению всех сообщений в очереди и служит сигналом для процессов пула о том, что следует завершить работу. Строки 3-4 Используем возможность барьерной синхронизации ACE_Thre- ad_Manager, чтобы ждать завершения работы пула потоков, созданного TP_Logging_Task: : open (), и затем явным образом закрываем синглтон, так как эта DLL должна быть отключена. Строки5-6 Удаляем logging_dispatcher_, созданный в init (), и за- вершаем работу. Для краткости, мы опускаем hook-методы suspend (), resu- me () и inf о (), которые похожи на аналогичные методы примеров, которые приводились раньше. В заключение, мы помещаем ACE_FACTORY_DEFINE в TP_Log- ging_Server.срр. ACE_FACTORY_DEFINE (TPLS, TP_Logging_Server)
244 Глава 6 Это макрос автоматически определяет функцию-фабрику _make_TP_Log- ging_Server (), которая используется в следующем файле svc. conf: dynamic TP_Logging_Server Service_Object * TPLS<_make_TP_Logging_Server() "$TP_LOGGING_SERVER_PORT" Данный файл управляет АСЕ-каркасом Service Configurator в процессе кон- фигурации сервера регистрации с пулом потоков с помощью следующих шагов: 1. Динамически подключаем DLL TPLS к адресному пространству процесса. 2. Используем класс ACE_DLL, чтобы найти функцию-фабрику _ша- ke_TP_Logging_Server () в таблице имен DLL TPLS. 3. Эта функция вызывается, чтобы получить указатель на динамически созда- ваемый TP_Logging_Server. 4. Каркас Service Configurator вызывает с помощью этого указателя hook-ме- тод TP_Logging_Server: : init (), передавая ему в качестве его единст- венного аргумента значение переменной окружения TP_LOGGING_SER- VER_PORT. Эта строка обозначает номер порта, на котором сервер регист- рации слушает клиентские запросы на соединение. 5. Если in it () завершается успешно, указатель TP_Logging_Server сохра- няется в ACE_Service_Repository под именем <<TP_Logging_Ser- ver». Напомним еще раз, АСЕ-каркас Service Configurator дает нам возможность по- вторно использовать программу main() из Configurable_Logging_Ser- ver. срр. 6.4 Резюме АСЕ-каркас Task позволяет разработчикам создавать и конфигурировать параллельные сетевые приложения, используя расширяемые объектно-ориен- тированные конструкции, обладающие большими возможностями. Данный каркас предоставляет класс ACE_Task, который объединяет многопоточную обработку с объектно-ориентированным программированием и организацией очередей. Механизм очередей в ACE_Task использует класс ACE_Messa- ge_Queue для эффективной передачи сообщений между задачами. Так как ACE_Task является производным от ACE_Service_Object (раздел 5.2), можно достаточно просто создавать службы, которые могут динамически пере- ходить в состояние активных объектов, а диспетчеризацию для них может осу- ществлять АСЕ-каркас Reactor. В данной главе было показано, как объединить АСЕ-каркас Reactor с АСЕ- каркасом Task для реализации вариантов паттерна Half-Sync/Half-Async [POSA2]. Классы ACE-каркаса Task также можно объединять с классами ACE_Euture, ACE_Method_Request и ACE_Activation_List для реализации паттерна Active Object [POSA2]. Сокращенный вариант реализации ACE_Mes- sage_Queue представлен в главе 10 [С-H-NPvl].
Глава 7 АСЕ-каркас Acceptor-Connector Краткое содержание В главе рассматривается структура и применение ACE-каркаса Acceptor- Connector. Этот каркас реализует паттерн Acceptor-Connector [POSA2], разде- ляющий (1) установление соединений и инициализацию взаимодействующих одноранговых служб сетевого приложения и (2) обработку, которую они затем выполняют. Каркас Acceptor-Connector позволяет приложениям изменять ос- новные свойства топологии соединений независимо от реализуемых сервисов. Мы показываем, каким образом этот каркас можно объединять с АСЕ-каркаса- ми Reactor и Task и использовать с целью улучшения повторного использова- ния, расширяемости, безопасности и масштабируемости нашей сетевой служ- бы регистрации. 7.1 Обзор Многие сетевые приложения, такие как e-mail, удаленное резервное копи- рование файлов и службы Web, используют службы, ориентированные на со- единения. В эти службы входят классы, которые играют следующие роли: • Роль в установлении соединения. Определяет, каким образом приложе- ние устанавливает соединения. • Коммуникационная роль. Определяет приложение как клиента, как сер- вера или и как клиента, и как сервера — в одноранговой конфигурации. Сетевые приложения, взаимодействующие по ориентированным на соеди- нение протоколам (например, TCP/IP), характеризуются следующими асиммет- ричными ролями в установлении соединений между клиентами и серверами: • Серверы чаще пассивно ждут запросов на соединение, прослушивая за- данный ТСР-порт. • Клиенты чаще активно инициируют соединения, устанавливая связь с портом, который прослушивает сервер.
246 Глава 7 Даже в одноранговых вариантах, где приложения могут выполнять функ- ции и клиента и сервера, соединения должны быть активно инициированы од- ним одноранговым приложением и пассивно приняты другим. С целью улуч- шения повторного использования и расширяемости, поддержки различных требований и окружений, сетевые приложения должны проектироваться так, чтобы можно было легко изменять роли в установлении соединений и в комму- никациях. АСЕ-каркас Acceptor-Connector реализует паттерн проектирования Accep- tor-Connector [POSA2], который улучшает повторное использование и расши- ряемость ПО, за счет разделения (1) установления соединений и инициализа- ции взаимодействующих одноранговых служб сетевого приложения и (2) об- работки, которую они затем осуществляют. В данной главе рассматриваются следующие классы ACE-каркаса Acceptor-Connector, которые сетевые приложе- ния могут использовать для установления соединений и инициализации одно- ранговых служб: Клосс АСЕ Описание IACE_Svc_Handler Представляет локальную конечную точку взаимодействующей службы, включает конечную точку IPC, используемую для обмена данными с одноранговым партнером (peer), по установленному соединению. ACE_Acceptor Данная фабрика пассивно ждет запросов на соединение, а затем инициализирует ACE_Svc_Handier в ответ на запрос активного соединения от партнера. ACE_Connector Данная фабрика активно устанавливает соединение с акцептором партнера, а затем инициализирует ACE_svc_Handier для взаимодействия с ним по установленному соединению. Наиболее важные взаимосвязи между классами-ACE-каркаса Acceptor-Con- nector показаны на рис. 7.1. Эти классы, в соответствии с паттерном Accep- tor-Connector [POSA2], играют следующие роли: • Классы уровня инфраструктуры событий реализуют общие, независи- мые от приложений, стратегии диспетчеризации событий. В качестве та- кого уровня инфраструктуры событий обычно используется АСЕ-каркас Reactor, рассмотренный в главе 3. • Классы уровня управления соединениями выполняют, независимо от приложений, установление соединений между службами и их инициали- зацию. К этим классам относятся ACE_Svc_Handler, ACE_Acceptor иACE_Connector. • Классы прикладного уровня специализируют (customize) общие страте- гии, реализуемые двумя другими уровнями, путем создания производ- ных классов и/или реализаций шаблонов с целью создания объектов, ко- торые устанавливают соединения, обмениваются данными и выполняют обработку, связанную с реализацией услуг.
АСЕ-каркас Acceptor-Connector 247 Все эти классы, являясь производными от ACE_Event_Handler, обеспе- чивают интеграцию с ACE-каркасом Reactor (глава 3). В главе 8 рассматривается вариант каркаса Acceptor-Connector, основанный на ACE-каркасе Proactor, для приложений, в которых асинхронный ввод/вывод является более подходящим или более выгодным. ACE_Evan t_Handler Д I SVC_HANDLER, ----- PEER_ACCEPTOR АСЕ—Connector ---SYNCH—STRATEGYj АСВ^ГмкТ’ SVC_HANDLER, ! PEER-ACCEPTOR I ACE—Acceptor I PEER_ACCEPTOR, J SYNCH STRATEGY ACE Svc Handler ••bind’ Прикладная .служба Рис. 7.1 Классы АСЕ-каркаса Acceptor-Connector АСЕ-каркас Acceptor-Connector обеспечивает следующие преимущества: • Повторное использование (reusability) и возможность расширения (ex- tensibility). Сетевые приложения, ориентированные на установление со- единений, часто содержат значительный объем низкоуровневого кода, связанного с установлением соединений и инициализацией служб. Этот код, в значительной степени, не зависит от той обработки, которую вы- полняют обработчики служб (service handlers), с данными передаваемы- ми по установленному соединению. АСЕ-каркас Acceptor-Connector,ре- факторизует (refactor) этот низкоуровневый код в повторно используе- мые, независимые от приложения акцепторно-коннекторные классы, которые инкапсулируют знание о том, как устанавливать соединение и как, после установления соединения, инициализировать ассоциирован- ный с этим соединением обработчик службы. Как следствие, обработчи- ки служб могут сосредоточиться исключительно на реализуемой ими прикладной обработке. Такое разделение ответственности упрощает включение прикладных служб новых типов, реализаций служб, протоко- лов аутентификации и коммуникационных протоколов, не затрагивая существующий код установления соединений и инициализации служб. • Переносимость (portability). Все шаблоны классов этого каркаса параме- ризуются типом механизма IPC, требующегося для установления соедине- ний и передачи данных. Гибкость, обеспечиваемая этой возможностью расширения, основанной на шаблонах, полезна при разработке приложе-
248 Глава 7 ний, которые должны работать переносимо и эффективно на нескольких платформах ОС. IPC-параметры классов в шаблонах Acceptor-Connector могут быть любыми классами, связанными с решением задач сетевого взаимодействия, интерфейс 'которых согласован с соответствующими шаблонами. Например, в зависимости от некоторых особенностей базо- вых платформ ОС, например в зависимости от варианта UNIX, BSD или System V, сервер может создавать реализации классов каркаса, которые используют интерфейсные фасады АСЕ Socket или.АСЕ ТЫ. Для прило- жений, которые используют не TCP/IP, можно создавать реализации шаблонов с разными интерфейсными ACE-фасадами IPC или любыми другими классами, разработанными для конкретных случаев и имеющи- ми соответствующий интерфейс. • Отказоустойчивость (robustness). Строго разделяя обработчик и акцеп- тор службы, каркас гарантирует, что конечная точка соединения транс- портного уровня, работающая в пассивном режиме, не будет случайно использована для чтения или записи данных. Это дополнительное сред- ство типовой безопасности исключает неочевидные и губительные ошибки, которые могут иметь место при программировании со слабо ти- пизированными сетевыми программными интерфейсами, такими как С API Socket или ТЫ. • Эффективность (efficiency). С помощью паттерна Acceptor-Connector можно активно устанавливать эффективные асинхронные соединения с большим количеством одноранговых партнеров по глобальным сетям, имеющим большую задержку. Асинхронность в этих ситуациях важна, так как большие сетевые приложения должны иметь возможность под- ключать сотни и тысячи взаимодействующих одноранговых приложе- ний. Далее в данной главе приводится обоснование классов ACE-каркаса Accep- tor-Connector и описание их функциональных возможностей. Показано, каким образом осуществляется инициализация обработчиков служб и их удаление, а также приведено обоснование основных проектных вариантов каркаса. Пока- зано также, каким образом данный каркас может быть помещен «поверх» ACE-каркасов Reactor и Task с целью обработки запросов на соединение и об- мен данными между клиентскими и серверными демонами регистрации. Если вы незнакомы с паттерном Acceptor-Connector из POSA2, мы рекомендуем вам сначала познакомиться с ним, а уже потом погружаться в детальный анализ примеров данной главы. Мы также рекомендуем вам ознакомиться с АСЕ-кар- касами Reactor и Task в главах 3 и 6 соответственно. 7.2 Класс ACE_Svc_Handler Обоснование В главе 2 служба была определена как набор услуг (set of functionality), предлагаемых клиенту сервером. Обработчик службы (service handler) — это
АСЕ-каркас Acceptor-Connector 249 часть сетевого приложения или реализующая услугу, или предоставляющая к ней доступ (или и то, и другое, в случае однорангового варианта). Сетевые приложения, ориентированные на установление соединения, требуют как ми- нимум двух взаимодействующих обработчиков служб — по одному на каждый конец соединения. Кстати, приложения, использующие групповую или широ- ковещательную связь, могут иметь несколько обработчиков служб. Хотя тако- го рода коммуникационные протоколы без установления соединения не впол- не соответствуют модели Acceptor-Connector, класс ACE_Svc_Handler часто является неплохим.вариантом для реализации обработчика службы и его обя- зательно нужно принимать во внимание. При проектировании обработчиков служб, разработчики приложения должны также принимать во внимание коммуникационные аспекты проекти- рования, рассмотренные в главе 1 [C++NPvl]. В общем случае, прикладные функции, реализуемые обработчиком службы, могут быть изолированы от следующих аспектов проектирования: • как устанавливается соединение с обработчиком службы (Активно или пассивно) и как он инициализируется; • какие протоколы используются для установления соединения, аутенти- фикации и обмена данными между двумя обработчиками служ(>; • какой API сетевого программирования используется для доступа к IPC- механизмам ОС. В общем случае, протоколы соединения/аутентификации и стратегий ини- циализацйи служб изменяются не так часто, как функциональность обработчи- ков служб, реализуемых приложением. Чтобы разделить ответственность и дать возможность разработчикам приложений сосредоточиться на функцио- нальности обработчиков предоставляемых ими служб АСЕ-каркас Acceptor- Connector определяет класс ACE_Svc_Handler. Функциональные возможности класса ACE_Svc_Handler служит основой механизмов синхронной и взаим- но-согласованной (reactive) передачи данных и прикладной обработки. Данный класс обеспечивает следующие функциональные возможности: • Обеспечивает основу для инициализации и реализации службы в син- хронных или взаимно-согласованных сетевых приложениях, являясь це- левым объектом для фабрик соединений ACE_Connector и АСЕ_Ас- ceptor. • Обеспечивает конечную точку IPC, используемую обработчиком услуги для взаимодействия с его одноранговым партнером (или партнерами). Тип этой конечной точки IPC может быть параметризован многими классами интерфейсных ACE-фасадов IPC разделяя, таким образом, ком- муникационные механизмы нижнего уровня и стратегии служебной об- работки прикладного уровня. • Поскольку ACE_Svc_Handler является производным от ACE_Task (a ACE_Tas к от ACE_Event_Handler), он наследует возможности, свя-
250 Глава 7 занные с параллельной обработкой, синхронизацией, динамическим конфигурированием и обработкой событий, рассмотренные в главах 3-6. • Приводит в систему наиболее общую практику применения событий- но-управляемых сетевых служб, такую как регистрация у реактора в на- чале работы службы и закрытие конечной точки IPC, при откреплении службы от реактора. Интерфейс ACE_Svc_Handler показан,на рис. 7.2. Как видно из рисунка, этот шаблон класса может параметризоваться следующими значениями: • Класс характеристик (traits) PEER_STREAM, который может передавать данные между одноранговыми обработчиками служб по установленно- му соединению. Он определяет также сопутствующую характеристику PEER_STREAM: : PEER_ADDR, которая представляет' класс адреса для идентификации службой одноранговых приложений (peers). Параметр PEER_STREAM часто реализуется одним из интерфейсных АСЕ-фасадов IPC, например, ACE_SOCK_St ream, рассмотренным в главе 3 [C++NPvl ]. • Класс характеристик (traits) SYNCH_STRATEGY, который использует пат- терн Strategized Locking [POSA2] для параметризации характеристик син- хронизации ACE_Message_Queue в родительском классе ACE_Task. Этот параметр чаще реализуется или классом характеристик ACE_NULL_SYNCH, или классом характеристик ACE_MT_SYNCH. В блоке 40 рассмотрены характеристики (traits) C++ и идиомы классов ха- рактеристик (traits class idioms). Поскольку ACE_Svc_Handler является производным от ACE_Event_Hand- ler, его экземпляр может быть зарегистрирован ACE-каркасом Reactor для об-. работки различных типов событий. Например, он может быть зарегистрирован для обработки событий чтения и записи. Его hook-методы handle_input () и handle_output () будут затем автоматически диспетчеризироваться реак- тором, когда дескриптор сокета передачи данных будет готов, соответственно, принимать или отправлять данные. PEER_STRK*M —J SYNCH_STRATEGY ! ACBJFiak "j I PEER STREAM, ACZ_Svc_B«nd-l.r L??l!CS_STfaTEGY + ACE__Svc_Handler (thrjmgr : ACE_Thread_Manager * = 0, mq : ACE_Message_Queue<SYNCH_STRATEGY> * = 0, r : ACE_Reactor * = ACE_Reactor::instance ()) + peer () : PEER_STREAM& + destroy () + shutdown () Рис. 7.2 Класс ACE Svc Handler
АСЕ-каркас Acceptor-Connector 251 Класс ACE_Svc_Handler имеет обширный интерфейс, который экспор- тирует и свои собственные возможности, и возможности своих родительских классов. Поэтому мы разделили описание его методов на три группы. 1. Методы создания и активизации служб. С помощью ACE-каркаса Accep- tor-Connector можно изменять различные аспекты создания и инициализа- ции обработчиков служб во время компиляции и во время выполнения. По умолчанию, производный класс ACE_Svc_Handler создается динамиче- ски фабрикой-акцептором или фабрикой-коннектором, которые исполь- зуют для создания и активизации, следующие методы: Метод Описание ACE_Svc_Handler() Конструктор, вызываемый акцептором (acceptor) или коннектором (connector) при создании обработчика службы. open () Hook-метод, автоматически вызываемый акцептором или коннектором для инициализации обработчика службы. В блоке 47 объясняется, почему АСЕ-каркас Acceptor-Connector разделяет создание обработчика услуги и его активизацию. Блок 47: Разделение создания и активизации обработчика службы Создание и активизация служб в АСЕ-каркасе Acceptor-Connector разделя- ются для того, чтобы: • Сделать создание обработчика службы гибким. АСЕ обеспечивает боль- шую гибкость в способе, которым приложение создает (или повторно ис- пользует) обработчики служб. Многие приложения создают новые обработ- чики динамически, по мере необходимости, однако, другие приложения мо- гут повторно использовать (recycle) обработчики или использовать один обработчик для всех соединений (см. блок 53). • Упростить обработку ошибок. АСЕ не использует исключения C++ по причи- нам, изложенным в приложении А.6 (C++NPv1). Поэтому конструктор, ис- пользуемый для создания обработчика службы, не должен выполнять опе- раций, которые могут привести к ошибке. Любые такие операции должны помещаться в hook-метод open о, который должен возвращать -1, если в процессе активизации возникает ошибка. • Обеспечить поддержку поточной обработке. Если поток создается конст- руктором, то не существует гарантии, что объект будет полностью проини- циализирован до того, как поток начнет свою работу. Чтобы избежать этого потенциального состояния гонок, АСЕ-каркас Acceptor-Connector разделя- ет создание обработчика службы и его активизацию. Указатели на объекты ACE_Thread_Manager, ACE_Message_Queue и ACE_Reactor могут быть переданы конструктору ACE_Svc_Handler, что- бы заменить те, которые он использует по умолчанию. Hook-метод open ()
252 Глава 7 может осуществлять действия, которые инициализируют обработчик услуги, например: • Создавать поток (или пул потоков), который будет выполнять приклад- ную обработку с помощью hook-метода svc (). • Регистрировать у реактора один или несколько источников событий, та- ких как события ввода или тайм-ауты. • Открывать регистрационные файлы и инициализировать статистику ис- пользования. • Инициализировать блокировки или другие ресурсы. Если эти действия, связанные с инициализацией, завершаются успешно, open () возвращает 0. Однако если возникает ошибка и служба не может или не должна продолжать работу, open () должен сообщить об этом событии вы- зывающей стороне, возвращая -1. Так как обработчик службы не контролирует процесс своего создания, то ошибка в open () должна быть передана обратно вызывающей стороне так, чтобы могли быть выполнены действия, связанные с удалением. По умолчанию, обработчик службы удаляется автоматически, если open () возвращает -1, что демонстрируют различные методы activa- te_svc_handler () фабрик акцепторов и коннекторов. ACE_Svc_Handler определяет реализацию open () по умолчанию, кото- рая выполняет обычный набор операций, приведенный ниже: template <class PEER_STREAM, class SYNCH_STRATEGY> int ACE_Svc_Handler<PEER STREAM, SYNCH_STRATEGY>::open (void *factory) { if (reactor () && reactor ()->register_handler (this, ACE_Event_Handler::READ_MASK) == -1) return -1; else return 0; ) Параметр open () типа void * является указателем на фабрику, акцептор или коннектор, создавшие данный обработчик службы. По умолчанию, обра- ботчик службы регистрируется у реактора и обрабатывает входящие события в событийно-управляемом стиле (reactively). В примере данного раздела пока- зан обработчик службы, который активизирует сам себя в своем методе open (), чтобы перейти в состояние активного объекта и обрабатывать входя- щие события параллельно. Так как обработчик службы, после успешной акти- визации, сам отвечает за управление своим жизненным циклом, то он редко взаимодействует с акцептором, который его создал и активизировал. Тем не ме- нее, как показано в примере раздела 7.4, обработчик службы часто использует коннектор для восстановления соединений при появлении ошибок. 2. Методы прикладной обработки. Как отмечено выше, обработчик службы может выполнять обработку несколькими способами. Например, он может обрабатывать события в событийно-управляемом стиле (reactively) с помо- щью реактора или паралледьно, используя один или несколько процессов или потоков. Следующие методы, унаследованные от родительских клас-
АСЕ-каркасAcceptor-Connector 253 сов ACE_S vC-Handler, могут быть подменены его производными класса- ми и использованы для выполнения обработки обработчиком службы: Метод Описание SVC ( ) ACE_Svc_Handier наследует hook-метод svc () от класса ACE_Task (раздел6.3). После вызова метода обработчика службы activate () большая часть его последующей обработки может быть выполнена параллельно в его hook-методе svc {). handle_*() ACE_svc_Handier наследует методы handie_* () класса ACE_Event_Handler (раздел 3.3). Обработчик службы, поэтому, может регистрироваться у реактора для получения обратных вызовов, например handie_input (), при наступлении различных событий, представляющих интерес (глава 3). peer () Возвращает ссылку на базовый peer_stream. peer_stream обработчика службы готов к использованию тогда, когда вызывается его hook-метод open (). Любые методы, связанные с прикладной обработкой, могут использовать этот аксессор для получения ссылки на используемый IPC-механизм. | Хотя аргумент SYNCH_STRATEGY шаблона ACE_Svc_Handler парамет- ризует ACE_Message_Queue, унаследованную от ACE_Task, он не оказывает влияния на конечную точку IPC PEER_STREAM. Он не подходит АСЕ-каркасу Acceptor-Connector для односторонней сериализации использования конечной точки IPC, поскольку к нему часто нельзя обращаться параллельно. Например, обработчик службы может работать как активный объект с одним Потоком или полностью управляться обратными вызовами реактора в однопоточной кон- фигурации. Тем не менее, hook-метод обработчика службы open () может создать не- сколько потоков, которые обращаются одновременно к его конечной точке IPC. В таких случаях, прикладной код обработчика службы должен осуществ- лять всю необходимую синхронизацию. В главе 10 [C++NPvl ] рассмотрены ме- ханизмы синхронизации АСЕ, которые могут использовать приложения. На- пример, если несколько потоков осуществляют запись в один и тот же сокет, то неплохо сериализовать доступ к нему с помощью ACE_Thread_Mutex, чтобы избежать чередования данных от разных вызовов s end () в одном и том же по- токе байтов TCP. 3. Методы завершения работы службы. Обработчик службы можно исполь- зовать разными способами. Например, его диспетчеризацию может осуще- ствлять реактор, он может выполняться в своем собственном потоке или . процессе или составлять часть пула потоков. В связи с этим класс ACE_Svc_Handler предоставляет следующие методы завершения работы обработчика службы:
254 Глава 7 Метод Описание I destroy() Может быть вызван для непосредственного завершения работы обработчика службы. handle_close() Вызывает destroy () посредством обратного вызова из реактора. close() Вызывает handie_ciose () при выходе из потока службы. Обработчики служб часто закрываются в соответствии с определяемым приложением протоколом, например, когда обработчик одноранговой службы закрывает соединение или когда возникает серьезный сбой связи. Тем не менее, независимо от конкретных обстоятельств, завершение работы обработчика службы обычно отменяет действия, выполненные hook-методом обработчика службы open () и удаляет обработчик службы, когда это необходимо. Методы завершения, перечисленные в таблице, приведенной выше, могут быть разде- лены на следующие три категории: • Непосредственное завершение (direct shutdown). Приложение, чтобы завершить работу обработчика службы, может непосредственно вызвать метод destroy(), который последовательно выполняет следующие действия: 1. Удаляет обработчик из списка реактора. 2. Отключает все таймеры, связанные с этим обработчиком. 3. Закрывает объект peer-потока (peer stream object), чтобы избежать утечки дескрипторов. 4. Удаляет объект обработчика, если он создавался динамически, чтобы избежать утечек памяти. В главе 3 [C++NPvl ] объяснялось, почему удаление объекта, который ведет свое происхождение от ACE_SOCK, не приводит к закрытию инкапсулирован- ного в нем: сокета. Однако ACE_Svc_Handler находится на более высоком уровне абстрагирования, и, поскольку он является частью каркаса, он система- тизирует широко используемые паттерны. Так как закрытие сокета представля- ет собой обычную деталь завершения работы обработчика службы, АСЕ-каркас Acceptor-Connector выполняет эту задачу автоматически. ACE_Svc_Handler использует идиому C++ Storage Class Tracker, рассмот- ренную в блоке 48, с целью проверки того, каким образом он был создан: стати- чески или динамически? Его метод destroy (), поэтому, может сообщить, что обработчик службы был создан динамически, и, если это так, удалить его. Если обработчик службы не был создан динамически, destroy () не удаляет его. Если обработчик службы зарегистрирован у реактора, лучше не вызывать destroy () из потока, который не выполняет цикл обработки событий реакто- ра. Такой вызов может вывести объект обработчика службы из-под контроля реактора, который осуществляет диспетчеризацию событий для этого объекта, что может привести к непредсказуемым (и нежелательным) последствиям (на- подобие тех, что рассмотрены в блоке 46). Поэтому, вместо того, чтобы вызы-
АСЕ-каркас Acceptor-Connector 255 вать destroy () непосредственно, используйте метод ACE_Reactor: : noti- fy () для передачи управления потоку, осуществляющему Диспетчеризацию событий реактора, в котором безопаснее использовать destroy О'. И все же, есть решение еще лучше — изменить дизайн таким образом, чтобы использо- вать метод взаимно-согласованного (reactive) завершения, который рассмотрен далее. • Взаимно-согласованное завершение (reactive shutdown). Когда ACE_Svc_Handler регистрируется у ACE-каркаса Reactor, он часто об- наруживает, что удаленное приложение уже закрыло соединение и ло- кально инициировало завершение работы. Реактор вызывает метод hand- le_close () обработчика службы, при поступлении команды удалить обработчик из его внутренней таблицы. Обычно это происходит тогда, когда метод handle_input () обработчика службы возвращает -1, по- сле того как удаленное приложение закрывает соединение. Взаимно-со- гласованные (reactive) обработчики должны объединять действия, свя- занные с завершением работы, в методе handle_close (), в соответст- вии с изложенным в блоке 9. Как показано на рис. 7.3, по умолчанию метод handle_close() вызывает метод destroy (). Метод hand- le_close () может быть подменен в производных классах, если дейст- вия, реализуемые им по умолчанию, не устраивают. • Завершение потока. Как отмечалось в разделе 6.3, hook-метод close () обработчика службы вызывается в каждом из потоков задачи после за- вершения работы метода svc(). Тогда как взаимно-согласованная (reactive) служба использует механизм завершения работы реактора, что- бы инициировать действия по завершению работы, активный поток, ко- Блок 48: Определение класса памяти обработчика службы Объекты ACE_Svc_Handler часто создаются динамически фабриками ACE_Acceptor и ACE_Connector АСЕ-каркаса Acceptor-Connector. Тем не ме- нее, существуют ситуации, когда обработчики служб создаются иначе, на- пример, статически или в стеке. Чтобы корректно освобождать память обра- ботчика, не связывая это непосредственно с классами и фабриками, которые могут его создавать, класс ACE_Svc_Handler использует идиому C++ Storage Class Tracker (vR96). Эта идиома выполняет следующие шаги для автоматиче- ского определения способа создания (статически или динамически) обра- ботчика службы и действует соответственно. 1. ACE_svc_Handler перегружает оператор new, который выделяет память динамически и устанавливает флаг в локальной памяти потока, а тот фикси- рует этот флаг. 2. Конструктор ACE__Svc_Handler осуществляет проверку этих данных в ло- кальной памяти потока, чтобы определить как был создан данный объект — динамически или нет. 3. Когда, в конечном итоге, вызывается метод destroy о, он проверяет флаг «создан динамически». Если данный объект был создан динамически, des t- г оу () удаляет его, если нет, предоставляет возможность удалить этот объект деструктору ACE_Svc_Handler при выходе объекта из области его действия,
Глава 7 256 ACE_Reactor ACE-SQCK^Stream handle input() {feturn -1} handle close() remove handler() cancel timer() recv() destroy () < close () *□ I X Рис. 7.3 Взаимно-согласованное (reactive) завершение работы ACE_Svc_Handle г торый обрабатывает одноранговое соединение, может завершаться про- сто при закрытии соединения одноранговым партнером. Так как одиноч- ный поток, выполняющий службу, — наиболее распространенный случай, реализация hook-метода ACE_Svc_Handler: : close () по умолчанию вызывает метод handle_close (), рассмотренный выше. Этот метод может быть подменен в производных классах с целью реали- зации кода очистки, специфической для приложения, если поведение по умолчанию нежелательно. Пример Данный пример показывает, как использовать класс ACE_Svc_Handler в реализации сервера регистрации, основанной на модели параллелизма по- ток-на-соединение, рассмотренной в главе 5 [C++NPvl]. Код примера находит- ся в файлах TPC_Logging_Server. срр и TPC_Logging_Server. h. Заго- ловочный файл включает объявления классов данного примера и начинается с включения необходимых заголовочных файлов. ♦include ♦include ♦include ♦include ♦include ♦include "ace/Acceptor.h" "ace/INET_Addr.h" "ace/Reactor.h" "ace/Svc_Handler.h" "ace/FILE_IO.h" "Logging_Handler.h" Приведенный ниже TPC_Logging_Handler наследует от ACE_Svc_Hand- ler. class TPC_Logging_Handler : public ACE_Svc_Handler<ACE_SOCK_Stream, ACE_NULL_SYNCH> {
АСЕ-каркас Acceptor-Connector 257 Мы параметризуем шаблон ACE_Svc_Handler классом передачи данных A(!E_SOCK_Stream и классом характеристик ACE_NULL_SYNCH, который оз- начает «пустую» (no-op) стратегию синхронизации. В блоке 49 объясняется, ка- кие меры принимает АСЕ в тех компиляторах C++, которые не поддерживают классы характеристик в шаблонах. TPC_Logging_Handler определяет следующие два члена данных, кото- рые инициализируются в его конструкторе. protected: ACE_FILE_IO log_file_; // Файл per.записей. // Соединение с обработчиком одноранговой службы. Logging_Handler logging_handler_; public: TPC_Logging_Handler (): logging_handler_ (log_file_) {} Как обычно, мы повторно используем Logging_Handler из главы 4 [C++NPvl] для чтения регистрационной записи, расположенной за параметром дескриптора сокета, и сохранения ее в блоке ACE_Message_Block. Каждый экземпляр TPC_Logging_Handler создается динамически объек- том TPC_Logging_Acceptor, когда поступает запрос на соединение от кон- нектора однорангового партнера. TPC_Logging_Handler, чтобы инициализи- ровать обработчик, подменяет hook-метод ACE_Svc_Handler: : open (): 1 virtual int open (void *) { 2 static const ACE_TCHAR LOGFILE_SUFFIX [] = ACE_TEXT (".log"); 3 ACE_TCHAR filename[MAXHOSTNAMELEN + sizeof (LOGFILE_SUFFIX)]; 4 ACE_INET_Addr logging_peer_addr; 5 6 peer ().get_remote_addr (logging_peer_addr); 7 logging_peer_addr.get_host_name (filename, MAXHOSTNAMELEN); 8 ACE_OS_String::strcat (filename, LOGFILE_SUFFIX); 9 10 ACE_FILE_Connector connector; 11 connector.connect (log_file_, 12 ACE_FILE_Addr (filename), 13 0, // Без тайм-аута. 14 ACE_Addr::sap_any, // Игнорируется. 15 0, // He используем повторно этот адрес. 16 0_RDWR | 0_CREAT | O_APPEND, 17 ACE DEFAULT FILE PERMS); 18 19 logging_handler_.peer ().set_handle (peer ().get_handle ()); 20 21 return activate (THR_NEW_LWP | THR_DETACHED); 22 } 9 Программирование сетевых приложений на C++. Том 2
258 Глава 7 Блок 49: Приемы, используемые при отсутствии поддержки,, классов характеристик Если вы внимательно изучите исходный код ACE-каркаса Acceptor-Connector, вы заметите, что шаблонный аргумент IPC класса ACE_Accept.or, ACE_Connec- tor и ACE_svc_Handler представляет собой макрос, а не параметр типа. Аналогично, параметр стратегий синхронизации ACE_Svc_Handler является макросом, а не параметром типа. АСЕ использует эти макросы, чтобы обойти отсутствие поддержки классов характеристик и шаблонов в некоторых ком- пиляторах C++. Чтобы работать переносимо на такого рода платформах, типы классов АСЕ, такие как ACE_INET_Addr или ACE_Thread_Mutex, должны передаваться явно как параметры шаблона, а не предоставлять доступ к себе как характеристики (traits) классов характеристик (traits classes), таких KOK ACE_SOCK_Addr : : PSER_ADDR ИЛИ ACE_MT_SYNCH : : MUTEX. Чтобы помочь разработчикам приложений, АСЕ определяет набор макросов, которые осуществляют условную подстановку (conditionally expand) соответст- вующих типов. Например, следующая таблица описывает макросы ace_sock*: Класс АСЕ Описание ACE_SOCK_ACCEPTOR : ! Подставляет (expand) ACE_SOCK_Acceptor или ACE_SOCK_AcceptorИ ACE_INET_Addr ACE__S ОС K_CONNECTOR Подставляет (expand) ACE_SOCK_Connectcr или ACE_SOCK_ConnectorИ ACE_INET_Addr | ACE__SOCK__STREAM Подставляет (expand) ACS_S0CK_stream или ACE_SOCK_Stream и ACE_INET_Addr Эти макросы предоставляют адресные классы, которые корректно работают со всеми компиляторами C++, поддерживаемыми АСЕ. Например, они преоб- разуются (expand) в один класс, если характеристики (traits) шаблонов под- держиваются и в два класса, если нет. АСЕ внутренне использует макрос ace_sock_stre.am как параметр IPC-клас- сов макроса шаблона ACE_Svc_Handler, а не класс ACE_SOCK_Stream, чтобы избежать проблем при переносе кода на более старые компиляторы C++. У большинства современных компиляторов C++ этой проблемы уже не суще- ствует, поэтому вам не нужно использовать эти макросы в коде приложений, за тем исключением, если для вас важна переносимость на устаревшие компи- ляторы. Для простоты, код в данной книге приводится в предположении, что ваш компилятор C++ полностью поддерживает характеристики шаблонов и классы характеристик и, поэтому, в нем ACE-макросы не используются. Ис- ходные коды примеров (C++NPv2), включенные в АСЕ, тем не менее, использу- ют данный макрос, чтобы обеспечить переносимость на все платформы АСЕ. Строки 2-17 Инициализируем регистрационный файл, используя ту же логику, что и в методе Logging_Event_Handler: : open (). Строка 19 Заимствуем дескриптор сокета у обработчика службы и назнача- ем его logging_handler_, который затем используется для получения и об- работки клиентских регистрационных записей.
АСЕ-каркас Acceptor-Connector 259 Строка21 Преобразуем TPC_Logging_Handler в активный объект. Вновь созданный обособленный (detached) поток выполняет следующий hook-метод TPC_Logging_Handler::svc(): virtual int svc () { for (;;) { switch (logging_handler_.log_record ()) case -1: return -1; // Ошибка. case 0i return 0; // Клиент закрыл соединение, default: continue; // Случай по умолчанию. } /* * НЕДОСТИЖИМО (NOTREACHED)*/ return 0; } Данный метод сосредоточен исключительно на чтении и обработке клиент- ских регистрационных записей. Мы выходим из цикла for и завершаем работу этого метода, когда метод log_record () обнаруживает, что связанный с ним одноранговый обработчик службы закрыл соединение, или когда возникает ошибка. Завершение работы метода приводит к завершению работы потока, что, в свою очередь, заставляет ACE_Task: : svc_run () вызвать унаследован- ный метод данного объекта ACE_Svc_Handler: : close (). По умолчанию, Блок 50: Методы завершения блокированных потоков служб Потоки служб часто выполняют блокируемые операции ввода/вывода. как по- казано на примере модели параллелизма поток-на-соединение в TPC_Log- ging_Handier: :svc о. Тем не менее, если данный поток службы должен быть остановлен до момента его нормального завершения, то простота этой моде- ли может создавать проблемы. Далее излагаются некоторые методы форси- рования завершения потоков служб и отмечаются их потенциальные недостат- ки: • Завершить серверный процесс, оставляя аварийное завершение одноран- гового соединения на ОС, также как и других используемых ресурсов, таких как файлы (регистрационный файл, в примере данной главы). Такой способ может привести к потере данных и утечке ресурсов. Например, в System V IPC-объекты окажутся, при таком подходе, уязвимыми. • Разрешить асинхронное завершение потоков и завершить поток службы. Та- кое решение не является переносимым и также может приводить к потере ресурсов при неправильном программировании. • Закрыть сокет, в надежде на то, что блокированный вызов ввода/вывода при- ведет к аварийному завершению потока службы. Такое решение может ока- заться эффективным, но будет работать не на всех платформах, • Вместо того чтобы блокировать ввод/вывод, использовать ввод/вывод с кон- тролем времени и проверять флаг завершения или использовать механизм совместного завершения ACE_Thread_Manager, чтобы корректно завер- шиться между двумя попытками ввода/вывода. Такой подход не менее эф- фективен, но может привести к задержке завершения вплоть до заданного значения тайм-аута. 9>
260 Глава 7 этот метод закрывает поток приложения (peer stream) и удаляет обработчик службы, если он был создан динамически. Поскольку данный поток был создан с использованием флага THR_DETACHED, то нет необходимости ждать его за- вершения. Возможно, вы заметили, что у TPC_Logging_Handler: : svc () нет спо- соба остановить работу потока, если сервер, каким-то образом, получает запрос на завершение работы до того, как одноранговый партнер закрыл сокет. Вклю- чение этой возможности мы оставляем в качестве упражнения для читателей. Некоторые общие подходы к реализации такой возможности приведены в бло- ке 50. 7.3 Класс ACE_Acceptor Обоснование Во многих серверных приложениях, ориентированных на установление со- единений, код установления соединения сильно связан с кодом инициализации службы так, что его трудно использовать повторно. Например, если внима- тельно изучить классы Logging_Acceptor, Logging_Acceptor_Ex, Log- ging_Acceptor_WFMO, CLD_Acceptor и TP_Logging_Acceptor, можно увидеть, что метод handle_input () переписывался для каждого обработчи- ка регистрации, даже если структура и поведение кода были почти идентичны- ми. АСЕ-каркас Acceptor-Connector определяет класс ACE_Acceptor, чтобы разработчикам приложений не нужно было снова и снова переписывать этот код. ACE__Service__Ob j ect PEER ACCEPTOR J SVC HANDLER, ACE__Accep tor _P?_E_R_AC C_E_PTO_R # flags__ : int # reuse_addr__ : int + open (addr : const PEER_ACCEPTOR::PEER_ADDR&, r : ACE_Reactor * = A$E_Reactor::instance (), flags : int « 0, use_select : int =1, reuse_addr : int - 1) : int + close () : int + acceptor ().: PEER_ACCEPTOR& # make_svc_handler (sh : SVC_HANDLER ★&) : int # connect_svc_handler (sh : SVC__HANDLER *) : int # activate_svc_handler (sh : SVC_HANDLER ★) : int Рис. 7.4 Класс ACE_Acceptor
АСЕ-каркас Acceptor-Connector 261 Функциональные возможности класса ACE_Acceptor является фабрикой, которая реализует роль Acceptor в паттерне Acceptor-Connector [POSA2]. Данный класс обеспечивает следую- щие функциональные возможности: • Разделяет (1) установление пассивного соединения и логику инициали- зации службы и (2) обработку, выполняемую обработчиком службы по- сле установления соединения и инициализации. • Обеспечивает конечную точку IPC пассивного режима, используемую для прослушивания и приема соединений от однорангового партнера. Тип этой конечной точки IPC может быть параметризован многими классами интерфейсных фасадов АСЕ IPC, разделяя, таким образом, (1) низкоуровневые механизмы соединений и (2) политику инициализации служб прикладного уровня. • Автоматизирует шаги, необходимые для пассивного соединения конеч- ной точки IPC и создания/активизации связанного с ней обработчика службы. • Поскольку ACE_Acceptor является производным от ACE_Servi- ce_Object, он наследует возможности, связанные с конфигурацией и обработкой событий, рассмотренные в главах 3 и 5. Интерфейс ACE_Acceptor показан на рис. 7.4. Как видно из рисунка, этот шаблон класса параметризуется следующими классами: • Классом SVC_HANDLER, который обеспечивает интерфейс для служб, определяемых клиентами и серверами. Реализацией этого параметра яв- ляется класс, производный от класса ACE_Svc_Handler (раздел 7.2). • Классом PEER_ACCEPTOR, который может пассивно принимать клиент- ские соединения. Этот параметр часто задается в виде одного из интер- фейсных фасадов АСЕ IPC, таких как ACE_SOCK_Acceptor, рассмот- ренный в главе 3 [C++NPvl]. Так как ACE_Acceptor является производным от ACE_Event_Handler, его экземпляр может быть зарегистрирован у ACE-каркаса Reactor для обработ- ки событий, связанных с принятием соединений. Диспетчеризация для его ме- тода handle_input () будет осуществляться реактором автоматически при поступлении от клиента нового запроса на соединение. Класс ACE_Acceptor имеет гибкий интерфейс, который может в значи- тельной степени изменяться разработчиками приложений. Поэтому мы груп- пируем описание его методов на две категории, рассматриваемые ниже. 1. Инициализация и удаление акцептора, методы доступа к акцептору. Для инициализации и удаления ACE_Acceptor используются следующие ме- тоды:
262 Глава 7 Метод Описание ACE_Acceptor () open() Связывает конечную точку IPC пассивного режима, принадлежащую акцептору, с конкретным адресом, таким как номер TCP-порта и IP-адрес хоста; затем прослушивает запросы на установление соединений. ~ACE_Acceptor() close () Закрывает конечную точку IPC акцептора и освобождает его ресурсы. acceptor () Возвращает ссылку на базовый peer_acceptor. Часть метода ACE_Acceptor: : open () приведена ниже: 1 template Cclass SVC_HANDLER, class PEER_ACCEPTOR> 2int ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::open 3 (const ACE_TYPENAME PEER_ACCEPTOR?: PEER_ADDR &addr, 4 ACE_Reactor * = ACE_Reactor::instance (), 5 int flags = 0, 6 /* ... Другие параметры опущены ... */) 7{/*...*/} Строка 3 Чтобы указать правильный тип IPC-класса адресации, шаблон- ный параметр PEER_ACCEPTOR должен определить характеристику (trait) PEER_ADDR. В блоке 5 главы 3 [C++NPvl] проиллюстрировано каким образом класс ACE_SOCK_Acceptor удовлетворяет этому критерию. Строка 4 По умолчанию, метод open () использует синглтон ACE_Reac- tor, чтобы зарегистрировать акцептор для обработки событий принятия со- единений. Этот реактор можно заменять по-экземплярно (per-instance), что по- лезно, когда процесс использует несколько реакторов, например, по одному на каждый поток. Строка 5 Параметр флагов указывает должна ли конечная точка IPC обра- ботчика службы, инициализированная акцептором, начинать работу в небло- кируемом режиме (ACE_NONBLOCK). 2. Методы установления соединений и инициализации обработчиков служб. Следующие методы ACE_Acceptor могут быть использованы для пассивного установления соединений и инициализации, связанных с эти- ми соединениями обработчиков служб: Метод Описание 1 handle_input () Этот шаблонный метод вызывается реактором при поступлении запроса на соединение от однорангового коннектора. Он может использовать три метода, приведенных ниже, для автоматизации шагов, необходимых для пассивного соединения конечной точки IPC, а также создания и активизации связанного с этим соединением обработчика службы.
АСЕ-каркас Acceptor-Connector 263 Метод Описание make_svc_handler() Этот метод-фабрика создает обработчик службы для обработки запросов на данные от его однорангового обработчика службы из соединяющей их конечной точки IPC. accept__svc_ handler() Этот hook-метод использует конечную точку IPC пассивного режима, принадлежащую акцептору, для создания соединения с конечной точкой IPC и инкапсуляции этой конечной точки с дескриптором ввода/вывода, который связан с обработчиком службы. activate_svc_ handler() Этот hook-метод вызывает hook-метод open () обработчика службы, который позволяет обработчику службы завершить свою инициализацию самостоятельно. На рис. 7.5 показаны шаги, которые; по умолчанию осуществляет АСЕ_Ас- ceptor в своем шаблонном методе handle_input (): 1. Вызывает метод-фабрику make_svc_handler () для динамического соз- дания обработчика службы. 2. Вызывает hook-метод accept_svc_handler (), чтобы принять соедине- ние и сохранить его в обработчике службы. 3. Вызывает hook-метод activate_svc_handler<), чтобы дать возмож- ность обработчику службы самому завершить свою инициализацию. Последовательность действий ACE_Acceptor при установлении соединения
264 Глава 7 В блоке 47 объясняется, почему шаблонный метод АСЕ_Ассер- tor: : handle_input () разделяет создание обработчика службы и его акти- визацию. ACE_Acceptor: : handle_input () использует паттерн Template Me- thod [GoF], чтобы дать возможность разработчикам приложений изменять пр- ведение на любом из трех шагов, перечисленных выше. Поэтому поведение по умолчанию make_svc_handler (), accept_svc_handler() и activa- te_svc_handler () может подменяться производными классами. Такой ди- зайн позволяет настраивать и изменять поведение в заданных пределах для поддержки нескольких вариантов использования. Три основных цели измене- ния ACE_Acceptor: : handle_input () описаны боле? подробно и проил- люстрированы ниже. 1. Создание обработчика службы. Шаблонный тчетод ACE_Acceptor: : hand- le_input () вызывает метод-фабрику make_svc_handler () для созда- ния нового обработчика службы. Реализация make_svc_handler () по умолчанию приведена ниже: 1 template Cclass SVC_HANDLER, class PEER_ACCEPTOR> int 2ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::make_svc_handler 3 (SVC_HANDLER *&sh) { 4 ACE_NEW_RETURN (sh, SVC_HANDLER, -1) ; 5 sh->reactor (reactor ()); 6 return 0; 7 } Строка 4 Динамически создаем экземпляр SVC_HANDLER. Конструктор SVC_HANDLER должен инициализировать все указатели на члены-данные зна- чением NULL, чтобы избежать возможных проблем во время выполнения. Эти проблемы могут возникнуть, если будут обнаружены ошибки в шаблонном мето- де ACE_Acceptor: : handle_input () и нужно будет закрыть SVC_HANDLER. Строка 5 Задаем в качестве реактора вновь созданного обработчика служ- бы тот же реактор, который связан с акцептором. Производные классы могут подменять make_svc_handler () с целью создания обработчиков служб любым нужным им способом, например: • С учетом некоторого критерия, например, количества имеющихся про- цессоров (CPU), хранящихся параметров конфигурации, вычисленного среднего значения загрузки за некоторый период времени или текущей рабочей нагрузки хоста. • Всегда возвращая синглтон обработчика службы. • Динамически подключая обработчик из DLL с помощью классов ACE_Service_Conf ig или ACE_DLL, рассмотренных в главе 5. 2. Установление соединения. Шаблонный метод вызывает hook-метод ас- cept_svc_handler(), чтобы пассивно принять новое соединение от партнера-коннектора. Реализация этого метода по умолчанию возлагается на метод PEER_ACCEPTOR::accept():
АСЕ-каркас Acceptor-Connector 265 template Cclass SVC_HANDLER, class PEER_ACCEPTOR> int ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::accept_svc_handler (SVC_HANDLER *sh) { if (acceptor ().accept (sh->peer ()) == -1) { sh->close (0); return -1; } return 0; ) Чтобы эта реализация accept_svc_handler () прошла компиляцию, шаблонный параметр PEER_ACCEPTOR должен иметь public-метод accept (). ACE_SOCK_Acceptor (глава 3 [C++NPvl]) удовлетворяет этому требованию, также как и большинство интерфейсных фасадов АСЕ IPC. Производные классы могут подменять accept_svc_handler (), чтобы включить дополнительную обработку, которую нуйкно выполнять до или по- сле того, как соединение будет принято, но до того, как оно будет использовано. Например, этот метод может выполнять аутентификацию нового соединения до того, как активизировать службу. Процесс аутентификации может прове- рять имя хоста партнера и/или номер порта, выполнять последовательность входа в систему или открывать на новом сокете SSL-сеанс. В примере данного раздела показано как реализовать аутентификацию путем подмены hook-мето- да accept_s vc_handler () с целью выполнения SSL-аутентификации до ак- тивизации службы. Тем не менее, будьте осторожны при выполнении обменов с партнером в этой ситуации. Если activate_svc_handler () вызывается посредством обратного вызова реактора, то весь цикл диспетчеризаций собы- тий данного приложения может быть блокирован на неприемлемо долгий про- межуток времени. 3. Активизация обработчика службы. Шаблонный метод АСЕ_Ассер- tor: :handle_input () вызывает hook-метод activate_svc_hand- ler (), чтобы активизировать новую службу после ее создания и после того, как новое соединение принято от имени этой службы. Поведение это- го метода по умолчанию показано ниже: Itemplate Cclass SVC_HANDLER, class PEER_ACCEPTOR> int 2 ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::activate_svc_handler 3 (SVC_HANDLER *sh) { 4 int result = 0 ; 5 if (ACE_BIT_ENABLED (flags_, ACE_NONBLOCK) ) { 6 if (sh->peer ().enable (ACE_NONBLOCK) == -1) 7 result = -1; 8 } else if (sh->peer ().disable (ACE_NONBLOCK) == -1) 9 result = -1; 10 11 if (result == 0 && sh->open (this) == -1) 12 result = -1; 13 if (result -= -1} sh->close (0) ; 14 return result; 15 1
266 Глава 7 Строка 5-9 Блокируемый/неблокируемый статус обработчика службы ус- танавливается в соответствии с флагами, которые акцептор хранит в своем кон- структоре. Если f lag_ акцептора указывает на неблокируемый режим, мы раз- решаем неблокируемый ввод/вывод потока (peer stream) обработчика службы. В противном случае, поток устанавливается в блокируемый режим. Строки 11-14 Hook-метод open () обработчика службы вызывается, что- бы активизировать обработчик. Если при активизации службы возникает ошибка, вызывается hook-метод close () обработчика службы, чтобы освобо- дить все ресурсы, связанные с этим обработчиком службы. Производные классы могут подменять activate_svc_handler (), что- бы активизировать службу каким-то другим способом, например, ассоциируя ее с пулом потоков или создавая новый процесс или поток для обработки дан- ных, посылаемых клиентами. Так как метод ACE_Acceptor: :handle_in- put () является виртуальным, можно (правда, редко) изменять последователь- ность шагов, выполняемых, чтобы принять соединение и инициализировать обработчик службы. Независимо от того, выполняются действия АСЕ_Ассер- tor по умолчанию или нет, функциональность самого обработчика службы полностью отделена от действий, выполняемых при пассивном принятии со- единения и его инициализации. Такой дизайн поддерживает модульность и разделение ответственности, что очень важно для уменьшения затрат на раз- работку службы и ее последующее сопровождение. Пример Данный пример — еще один вариант нашего серверного демона регистрации. Он использует ACE_Acceptor, реализованный с параметром ACE_SOCK_Ac- ceptor, для прослушивания TCP-сокета пассивного режима, дескриптор которо- го определяется точкой входа службы «асе_1оддег» в служебной базе дан- ных системы; обычно это /etc/services. Данная версия сервера использует модель параллелизма поток-на-соединение для того, чтобы работать одновре- менно с множеством клиентов. Как показано на рис. 7.6, основной (main) поток использует реактор, чтобы ждать поступления новых запросов на соединение от клиентов. Когда поступа- ет запрос на соединение, акцептор использует протокол аутентификации OpenSSL [ ОреО 1 ], о котором говорится в блоке 51, чтобы убедиться, что данный клиентский демон регистрации имеет право подключиться к данному серверу. Если данный клиент является зарегистрированным, акцептор динамически соз- дает TPC_Logging_Handler (см. пример раздела 7.2) для работы с данным соединением. Метод TPC_Logging_Handler: :ореп () создает поток для об- работки регистрационных записей, посылаемых клиентом по установленному соединению. Так как большая часть этого кода является повторно используемым кодом ACE-каркаса Acceptor-Connector и библиотеки OpenSSL, то этот пример, в ос- новном, дополняет уже существующий код, создает экземпляры классов и ис- пользует существующие возможности. Мы создаем класс, производный от ACE_Acceptor, и подменяем его метод ореп() и hook-метод ас- cept_svc_handler () с целью определения протокола аутентификации на
АСЕ-каркас Acceptor-Connector 267 Рис. 7.6 Архитектура сервера регистрации типа поток-на-соедийение стороне сервера. Класс TPC_Logging_Acceptor и его защищенные (protec- ted) члены-данные объявляются следующим образом: ♦include ”ace/SOCK_Acceptor.h” ♦include <openssl/ssl.h> class TPC_Logging_Acceptor : public ACE_Acceptor<TPC_Logging_Handler, ACE_SOCK_Acceptor> { protected: // Структура данных ’’контекста” SSL. SSL_CTX *ssl_ctx_; // Структура данных SSL, соответствующая // аутентифицированным соединениям SSL. SSL *ssl ; Члены-данные ssl_ctx_ и ssl_ передаются в вызовы OpenSSL API, вы- полняемые следующими открытыми методами в TPC_Logging_Acceptor. public: typedef' ACE_Acceptor<TPC_Logging_Handler, ACE_SOCK_Acceptor> PARENT; typedef ACE_SOCK_Acceptor::PEER_ADDR PEER_ADDR; TPC_Logging_Acceptor (ACE_Reactor *) : PARENT (r) , ssl_ctx_ (0), ssl_ (0) {} // Деструктор освобождает ресурсы SSL. virtual ~TPC_Logging_Acceptor (void) { SSL_free (ssl-);
268 Глава 7 SSL_CTX_free (ssl_ctx_) ; } // Инициализируем экземпляр акцептора. virtual int open (const ACE_SOCK_Acceptor::PEER_ADDR &local__addr, ACE_Reactor *reactor = ACE_Reactor::instance () , int flags = 0, int use_select = 1, int reuse_addr = 1); // Ноок-метод close <ACE_Reactor>. virtual int handle_close (ACE_HANDLE = ACE_INVALID—HANDLE, ACE-Reactor_Mask = ACE_Event_Handler::ALL_EVENTS—MASK); // Ноок-метод установления соединения и аутентификации, virtual int accept—svc_handler (TPC Logging Handler *sh); }; Блок 51: Обзор протоколов криптографии и аутентификации Чтобы защититься от возможных атак или от обнаружения со стороны, многие сетевые приложения должны проверять идентичность своих одноранговых партнеров и шифровать секретные данные, передаваемые по сети. С целью предоставления таких возможностей были разработаны различные пакеты криптографии, такие как OpenSSL (Оре01), и протоколов систем защиты, таких как Transport Layer Security (TLS) (DA99). Эти пакеты и протоколы реализуют биб- лиотечные вызовы, которые обеспечивают аутентификацию, целостность дан- ных и конфиденциальность при взаимодействии двух приложений. Например, протокол TLS может шифровать/дешифровать данные посылаемые/получае- мые по сети TCP/IP. TLS базируется на более раннем протоколе Secure Sockets Layer (SSL), разработанном Netscape. Инструментальная библиотека OpenSSL, используемая в примерах данной главы, базируется на библиотеке SSLeay, написанной Эриком Янгом (Eric Yo- ung) и Тимом Хадсоном (Tim Hudson). У нее открытые исходные коды, она интен- сивно развивается и работает на многих платформах, включая платформы, под- держиваемых АСЕ, такие как Linux, FreeBSD, OpenBSD, NetBSD, Solaris, AIX, IRIX, HP-UX, OpenUNIX, DG/UX, ReliantUNIX, UnixWare, Cray T90 и T3E, SCO Unix, Microsoft Windows и MacOS. OpenSSL пользуется большим успехом по свидетельству со- общества ее пользователей, как коммерческих, так и некоммерческих. TPC_Logging_Acceptor: : open () инициализируется с помощью реа- лизации своих базовых классов и установления идентичности сервера, следую- щим образом: 1#include "асе/OS .,h" 2 linclude "Reactor_Logging_Server_Adapter.h" 3 linclude "TPC_Logging_Server. h" 4 tinclude "TPCLS_export. h" 5 6 #if idefined (TPC_CERTIFICATE_FILENAME) 7# define TPC_CERTIFICATE_FILENAME "tpc-cert.pern"
АСЕ-каркас Acceptor-Connector 269 8#endif /* !TPC_CERTIFICATE_FILENAME */ 9 #if ’defined (TPC_KEY_FILENAME) 10# define TPC_KEY_FILENAME "tpc-key.pern” 11#endif /* !TPC KEY FILENAME */ 12 13 int TPC_Logging_Acceptor::open 14 (const ACE_SOCK_Acceptor::PEER_ADDR &local_addr, 15 ACE_Reactor *reactor, 16 int flags, int use_select, int reuse_addr) { 17 if (PARENT::open (local_addr, reactor, flags, 18 use_select, reuse_addr) != 0} 19 return -1; 20 OpenSSL_add_ssl_algorithms (); 21 ssl_ctx_ = SSL_CTX_new (SSLv3_server_method ()); 22 if (ssl_ctx_ =='0) return -1; 23 24 if (SSL_CTX_use_certificate_file (ssl_ctx_, 25 T PC_CERTIFICATE_FILENAME, 26 SSL_FILETYPE_PEM) <= 0 27 || SSL_CTX_use_PrivateKey_file (ssl_ctx_, 2 8 TPC_KEY_FILENAME, 29- SSL_FILETYPE_PEM) <= 0 30 || ’SSL_CTX_check_private_key (ssl_ctx_)) 31 return -1; 32 ssl_ = SSL_new (ssl_ctx_) ; 33 return ssl == 0 ? -1 : 0; 34 } Строки 6-11 Поскольку у нашего сервера регистрации нет пользователь- ского интерфейса, предполагается, что его серверный сертификат и соответст- вующий ключ находятся в файлах, задаваемых по умолчанию. Тем не менее, приложение имеет возможность заменить имена файлов по умолчанию, опре- делив соответствующий макрос препроцессора. Строки 17-18 Инициализируем ACE_Acceptor, используя реализацию open () по умолчанию. Строка 20 Инициализируем библиотеку OpenSSL. Для краткости, мы раз- мещаем вызов OpenSSL_add_ssl_algorithms() в TPC_Logging_Ac- ceptor: : open (). Хотя эту функцию можно без риска вызывать несколько раз в каждом процессе, в идеале этот вызов должен выполняться только один раз для каждого процесса. И все же инициализацию этой функции следует син- хронизировать, если ее могут вызывать несколько потоков, так как функции OpenSSL не поддерживают многопоточную обработку. Строки 21-22 Настраиваем соединение на SSL версии 3 и создаем структуру SSL, которая соответствует аутентифицируемым соединениям. Строки 24-31 Задаем сертификат и соответствующий закрытый ключ, ис- пользуемый для идентификации сервера при установлении соединений и затем проверяем, что закрытый ключ соответствует заданному сертификату. Этот код подразумевает, что сертификат и ключ закодированы в формате Privacy Enhanced Mail (РЕМ) в указанных файлах.
270 Глава 7 Строки 32-33 Инициализируем новую структуру данных SSL, которая ис- пользуется в hook-методе TPC_Logging_Acceptor: :accept_svc_hand- ler () при установлении SSL соединений посредством OpenSSL API, следую- щим образом: 1int TPC_Logging_Acceptor::accept_svc_handler 2 (TPC_Logging_Handler *sh) { 3 if (PARENT::accept_svc_handler (sh) == -1) return -1 ; 4 SSL_clear (ssl_);// Восстанавливаем для нового SSL соединения. 5 SSL_set_fd 6 (ssl_, ACE_reinterpret_cast (int, sh->get_handle ())); 7 8 SSL_set_verify 9 (ssl_, 10 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, 11 0) ; 12 if (SSL_accept (ssl_) == -1 13 || SSL_shutdown (ssl_) == -1) retWtn -1; 14 return 0; 15 } Строка 3 Принимаем TCP-соединение, используя реализацию по умолча- нию accept_svc_handler(). Строки 4-6 Восстанавливаем структуры данных SSL для использования с новым SSL соединением. Строки 8-11 Настраиваем структуры данных SSL так, чтобы при приеме SSL соединения обязательно выполнялась аутентификация клиента. Строка 12 Выполняем реальное соединение и согласование в соответствии с SSL. Если аутентификация клиента завершается отрицательно, вызов SSL_ac- cept () завершается ошибкой. Строка 13 Завершаем работу SSL соединения, если аутентификация про- шла успешно. Поскольку, фактически, мы не шифруем данные регистрацион- ных записей, мы просто обмениваемся с этого момента данными в виде TCP по- тока. Если требуется шифровать данные, можно использовать интерфейсные фасады АСЕ для OpenSSL, рассмотренные в блоке 52. Подменяя hook-методы open () и accept_svc_handler (), мы добавляем аутентификацию к нашему серверному демону регистрации, больше в его реали- зации ничего не трогая. Такая расширяемость иллюстрирует большие возможно- сти паттерна Template Method, используемого в дизайне класса ACE_Acceptor. Когда работа службы из нашего примера завершается с помощью АСЕ-кар- каса Service Configurator, Reactor_Logging_Server_Adapter::fini() завершается вызовом следующего метода handle_close (): int TPC_Logging_Acceptor::handle_close (ACE_HANDLE h, ACE_Reactor_Mask mask) { PARENT::handle_close (h, mask); delete this; return 0;
АСЕ-каркас Acceptor-Connector 271 Блок 52: Интерфейсные фасады АСЕ для OpenSSL Хотя OpenSSL API предоставляет полезный набор функций, ей присущи те же про- блемы, связанные с API ОС, написанных на Си (см. главу 2 в (C++NPv1)). Чтобы ре- шить эти проблемы, АСЕ предлагает классы, которые инкапсулируют OpenSSL, ис- пользуя API похожие на интерфейсные фасады АСЕ Socket. Например, классы ACE_SOCK_Acceptor, ACE_SOCK_Connector И ACE_SOCK_Stream, рассмотренные в главе 3 (C++NPv1), имеют свои SSL-совместимые аналоги: ace_ssl_sock_Ac- ceptor, ACE_SSL_SOCK_ConnectorИ ACE_SSL_SOCK_Stream. Интерфейсные фасады АСЕ SSL позволяют сетевым приложениям обеспечить целостность и конфиденциальность обмена данными по сети. Они придержива- ются той же структуры и тех же API, что и их Socket API аналоги, что упрощает их полную замену с помощью параметризованных типов C++ и шаблонного класса ACE_Svc_Handler. Например, чтобы применить интерфейсные фасады АСЕ для OpenSSL к нашему серверу сетевой регистрации мы можем просто удалить весь КОД OpenSSL API и реализовать ACE_Acceptor, ACE_Connector и ACE_Svc_Handler С ACE_SSL_SOCK_Acceptor, ACE_SSL_SOCK_Connector И ACE_SSL_SOCK_Stream, соответственно. Этот метод вызывает ACE_Acceptor: : handle_close (), чтобы за- крыть сокет-акцептор, осуществляющий прослушивание, и отменить его реги- страцию у каркаса Reactor. Чтобы не допустить утечек памяти, этот метод затем удаляет объект this, который был создан динамически во время инициализа- ции службы. В заключение мы создаем определение типа TPC_Logging_Server: typedef Reactor_Logging_Server_Adapter<TPC_Logging_Acceptor> TPC_Logging_Serveг; ACE_FACTORY_DEFINE (TPCLS, TPC_Logging_Server) Мы также используем макрос ACE_FACTORY_DEFINE, рассмотренный в блоке 32, чтобы автоматически создать функцию-фабрику _make_TPC_Log- ging_Server (), которая используется в следующем файле svc. conf: dynamic TPC_Logging_Server Service_Object * TPCLS:_make_TPC_Logging_Server() "$TPC_LOGGING_SERVER_PORT" Этот файл управляет АСЕ-каркасом Service Configurator при конфигуриро- вании сервера регистрации типа поток-на-соединение, реализуя следующие шаги: 1. Динамически подключает DLL TPCLS к адресному пространству процесса. 2. Использует класс ACE_DLL, чтобы найти функцию-фабрику _make_ TPC_Logging_Server () в таблице имен DLL TPCLS. 3. Эта функция вызывается для того, чтобы динамически создать TPC_Log- ging_Server и вернуть указатель на него. 4. Затем АСЕ-каркас Service Configurator вызывает, используя этот указатель, TPC_Logging_Server: :init(), передавая ему в качестве аргумента
212 Глава 7 argc/argv значение переменной окружения TPC_LOGGING_SER- VER_PORT, которая обозначает номер порта, на котором сервер регистра- ции слушает клиентские запросы на соединение. Номер порта, в конечном счете, передается дальше, конструктору Reactor_Logging_Server. 5. Если init () завершается успешно, указатель TPC_Logging_Server со- храняется в ACE_Service_Repository под именем «TPC_Log- ging_Server». Различные классы * Logging_Acceptor *, написанные «вручную» для на- ших предыдущих примеров сервера регистрации больше не нужны. Их функ- ции полностью перекрываются классом TPC_Logging_Acceptor, который является производным от ACE_Acceptor. Первым шаблонным аргументом базового класса ACE_Acceptor является TPC_Logging_Handler, производ- ный от ACE_Svc_Handler. В службе TPC_Logging_Server, АСЕ-каркас Acceptor-Connector выпол- няет большую часть действий, связанных с аутентификацией клиентского со- единения, а также инициализирует обработчик службы и оперирует с ним. Так как ACE_Acceptor рефакторизует функции принятия соединений, аутенти- фикации клиентов и активизации обработчиков служб в четкую последова- тельность действий в своем шаблонном методе handle_input [), исходный код серверного демона регистрации значительно сжимается. В частности, ACE_Acceptor предоставляет весь код, который раньше пришлось бы писать «вручную», например: • прослушивание и прием соединений; • создание и активизация новых обработчиков служб. Наш сервер регистрации типа поток-на-соединение повторно использует также классы из предыдущих версий, которые обеспечивали следующие воз- можности: • Инициализацию сетевого адреса прослушивания с помощью шаблона Reactor_Logging_Server_Adapter, определенного в разделе 5.2. • Динамическую конфигурацию сервера регистрации и выполнение цикла обработки событий АСЕ Reactor в функции main (). Следовательно, единственный код, который мы должны написать — это класс TPC_Logging_Handler, который создает поток для каждого соединения, чтобы принимать и обрабатывать регистрационные записи, и метод TPC_Log- ging_Acceptor: : accept_svc_handler (), который является реализацией протокола аутентификации и идентификации клиента и серверных демонов регистрации на стороне сервера. Подчеркнем еще раз, благодаря гибкости, обеспечиваемой ACE-каркасом Service Configurator, мы просто повторно ис- пользуем основную (main) программу Conf igurable_Logging_Server из главы 5.
АСЕ-каркас Acceptor-Connector 273 7.4 Класс ACE Connector Обоснование Материал раздела 7.3 был сосредоточен на вопросе о том, как отделять функциональность обработчиков служб от последовательности действий, не- обходимых для пассивного приема соединений и инициализации указанных обработчиков. Не менее полезно разделять функциональность обработчиков служб и последовательность действий, необходимых для активного установле- ния соединенйй и инициализации обработчиков. Более того, сетевые приложе- ния, которые взаимодействуют с большим количеством одноранговых партне- ров, могут нуждаться в том, чтобы активно устанавливать множество соедине- ний параллельно, расширяя их по мере необходимости. Чтобы предоставить эти возможности в виде гибкой, расширяемой и повторно используемой абст- ракции, АСЕ-каркас Acceptor-Connector определяет класс ACE_Connector. Функциональные возможности класса ACE_Connector является классом-фабрикой, который реализует роль Connector в паттерне Acceptor-Connector [POSA2]. Этот класс обеспечивает сле- дующие возможности: • Отделяет логику активного установления соединений и инициализации службы от обработки, выполняемой обработчиком службы после уста- новления соединения и инициализации. • Предоставляет фабрику IPC, которая активно устанавливает соединения с одноранговым акцептором, синхронно или взаимно-согласованно (reactively). Тип этой конечной точки IPC может быть параметризован многими классами интерфейсных фасадов АСЕ IPC, отделяя, таким об- разом, низкоуровневые механизмы установления соединений от поли- тик инициализации служб прикладного уровня. • Автоматизирует действия, необходимые для активного установления со- единения конечной точки IPC, а также для создания и активизации свя- занного с ней обработчика службы. • Поскольку ACE_Connector является йроизводным от ACE_Servi- се_ОЬ j ect, он наследует все возможности, связанные с обработкой со- бытий и динамической конфигурацией, рассмотренные в главах 3 и 5 со- ответственно. Интерфейс ACE_Connector показан на рис. 7.7. Этот шаблонный класс может быть параметризован следующими классами: • Классом SVC_HANDLER, который обеспечивает интерфейс для служб, определяемых клиентами, серверами или объектами, исполняющими роли и клиентов, и серверов в одноранговых службах. Этот параметр должен быть классом производным от ACE_Svc_Handler, как отмеча- лось в разделе 7.2.
274 Глава 7 • Классом PEER_CONNECTOR, который может активно устанавливать кли- ентские соединения. Этот параметр часто задается как один из интер- фейсных фасадов АСЕ IPC, таких как ACE_SOCK_Connector, рассмот- ренный в главе 3 [C++NPvl]. У класса ACE_Connector гибкий интерфейс, который может в значитель- ной мере изменяться разработчиками. Поэтому мы группируем описание его методов в две категории. АСЕ Serviae_Object PEER_CONNECTOR _ t 4 ; SVC_HANDLER, ! АСЕ Connector j PEER CONNECTOR } # flags_ : int + open (г : ACE_Reactor * « ACE_Reactor: '.instance () , flags : int = 0) : int + close () : int + connector () : PEER_CONNECTOR& + connect (sh : SVCJiANDLER *&, addr : const PEER_CONNECTOR::PEER_ADDR&, options : ACE_Synch_Options ® defaults, local_addr : const PEER_CONNECTOR::PEER_ADDR& « any, reuse_addr : int ® 0, flags : int = O_RDWR, perms : int ® 0) : int + cancel (sh : SVC_HANDLER ★) : int # make_svc_handler (sh : SVC_HANDLER ★&) : int # connect__svc_handler (sh : SVC_HANDLER *) : int 4 activate svc handler (sh : SVC HANDLER *) : int Рис. 7.7 Класс ACE_Connector 1. Методы инициализации и удаления коннектора, методы доступа к кон- нектору. Для инициализации, удаления и доступа к ACE_Connector ис- пользуются следующие методы: Метод Описание ACE_Connector() open () Методы инициализации коннектора (connector). ~ACE_Connector() 1 close () Методы, которые освобождают ресурсы, используемые коннектором. 1 connector() Возвращает ссылку на базовый peer_connector. Конструктору ACE_Connector и методу open () можно передавать флаг, указывающий в каком режиме должна начинать свою работу конечная точка IPC обработчика службы, инициализируемая коннектором: блокируемом (по
АСЕ-каркас Acceptor-Connector 275 умолчанию) или неблокируемом (ACE_NONBLOCK). Этим методам можно так- же передать связанный с данным коннектором реактор. По умолчанию они ис- пользуют синглтон ACE_Reactor, точно также как ACE_Svc_Handler и ACE_Acceptor. Объект ACE_Connector закрывается или при удалении, или при явном вызове его метода close (). ACE_Connector не выделяет ресурсов для син- хронных соединений, так что, если он используется только синхронно, то осво- бождать нечего. Тем не менее, для асинхронных соединений эти методы осво- бождают ресурсы, выделяемые коннектором для слежения за незакрытыми со- единениями, которые не завершили свою работу ко времени закрытия коннектора. Каждый обработчик службы, оставшийся без соединения, также закрывается вызовом hook-метода close (). 2. Методы установления соединений и инициализации обработчиков служб. Для активного установления соединений и инициализации, связан- ных с ними обработчиков служб могут быть использованы следующие ме- тоды ACE_Connector: Метод Описание connect () Этот шаблонный метод вызывается приложением, когда ему нужно установить соединение обработчика службы с одноранговым акцептором. Он может использовать следующие три метода для автоматизации действий, необходимых для активного соединения конечной точки IPC, а также для создания и активизации ассоциированного с ней обработчика службы. make_svc_handler() Метод-фабрика, создающий обработчик службы, который будет использовать конечную точку IPC с установленным соединением. | connect_svc_ handler() Этот hook-метод использует конечную точку IPC обработчика службы для установления активного соединения, синхронного или асинхронного. activate_svc_ handler() Этот hook-метод вызывает hook-метод обработчика службы open (), который позволяет обработчику службы самому завершить инициализацию после того, как соединение установлено. handle_output() Этот шаблонный метод вызывается реактором после завершения запроса на инициирование асинхронного соединения. Он вызывает метод activate_svc_handler (), чтобы ПОЗВОЛИТЬ обработчику службы самому себя инициализировать. cancel () Завершает работу обработчика службы, соединение с которым было установлено асинхронно. Вызывающая сторона — не коннектор — отвечает за закрытие обработчика службы.
276 Глава 7 Сетевые приложения используют шаблонный метод connect (), чтобы активно инициировать попытку установить соединение, независимо от, того осуществляется завершение синхронно или асинхронно. Этот метод использу- ет следующие действия для установления соединения и инициализации нового обработчика службы: 1. Получает указатель на обработчик службы: использует обработчик, пере- данный вызывающей стороной, или вызывает, метод-фабрику ша- ke_svc_handler(). 2. Затем вызывает метод connect_svc_handler (), чтобы инициировать соединение. Если установление пассивного соединения обычно происхо- дит быстро, установление активного соединения может занять много вре- мени, особенно в глобальных сетях. Поэтому есть смысл указать, чтобы ACE_Connector использовал АСЕ-каркас Reactor для асинхронного за- вершения соединений, независимо от того успешно они устанавливаются или нет. АСЕ Reactor ACE Connector register_handler() <-------------- handle_output() connect() * make_svc_handler() SVC HANDLER < ______create______ connect svc handler() peer() ]connect() j activite_svc_handler() p |< ।_____open ()_____ Рис. 7.8 Последовательность действий ACE_Connector при установлении асинхронного со- единения 3. Для синхронных соединений, connect () вызывает activate_svc_hand- ler (), чтобы дать возможность обработчику службы самому завершить инициализацию. Для асинхронных соединений, реактор вызывает АСЕ_Соп- nector: :handle_output (), чтобы завершить инициализацию обра- ботчика службы после установления соединения. Рис. 7.8 иллюстрирует последовательность действий для асинхронного случая. .
АСЕ-каркас Acceptor-Connector 277 Так как ACE_Connector: : connect () использует паттерн Template Me- thod [GoF], разработчики приложений могут изменять функции на любом из трех этапов, перечисленных выше, или на всех трех этапах. Как видно из рис. 7.7, методу ACE_Connector: : connect () передаются следующие аргументы: • Ссылка на указатель на SVC_HANDLER. Если этот указатель имеет значе- ние NULL, то, чтобы получить указатель, вызывается метод-фабрика make_svc_handler (). Версия make_svc_handler () по умолчанию создает его динамически. • Адресный аргумент, сигнатура которого, с использованием характери- стик (traits) C++, соответствует одноранговому коннектору и типам по- токов (stream). Адрес задает конечную точку, к которой подключается од- норанговый партнер. Это может быть, например, ACE_INET_Addr, кото- рый содержит номер порта и IP-адрес для обработчиков служб, использующих ACE_SOCK_Stream. Для других механизмов IPC это мо- жет быть имя, используемое для локализации службы с помощью служ- бы именования, или имя хоста сервера отображения портов (port map- ping server) — это решать разработчику приложения. • Ссылка на объект ACE_Synch_Options, который объединяет значения опций, используемых для определения поведения ACE_Connector. При каждом вызове connect () пытается установить соединение с указан- ным партнером. Если метод connect () сразу получает сообщение об успеш- ном завершении или ошибке, то он игнорирует параметр ACE_Synch_Op- t ions. Тем не менее, если сразу он такого сообщения не получает, то использу- ет параметр ACE_Synch_Options, чтобы изменять обработку завершения в соответствии с двумя независимыми факторами: 1. Следует ли использовать АСЕ-каркас Reactor для обнаружения завершения соединения? 2. Как долго следует ждать завершения соединения? Если используется реактор, метод connect () может регистрировать у ре- актора и PEER_CONNECTOR (для обнаружения завершения соединения) и тай- мер (для контроля заданного вызывающей стороной тайм-аута) и возвращать -1 вызывающей стороне с errno, установленным в EWOULDBLOCK. Оконча- тельным результатом успешного установления соединения или неудачи будет поведение метода ACE_Connector: : activate_svc_handler (), который, в случае успеха, активизирует обработчик или, в случае неудачи, вызывает ме- тод обработчика службы close (). Приложение выполняет цикл обработки событий реактора, и соответствующие вызовы будут сделаны в процессе этой обычной обработки. Если реактор не используется, метод connect () возвращает управление в случае установления соединения, появления ошибки или истечения срока тайм-аута. В зависимости от успеха или неудачи, по-прежнему, будут вызы- ваться или метод activate_svc_handler (), или метод обработчика служ- бы close (), соответственно. Таблица, приведенная ниже, суммирует поведе-
278 Глава 7 ние ACE_Connector, в зависимости от значений ACE_Synch_Opt ions, в том случае, если запрос на соединение не завершается немедленно. Реактор Тайм-аут Поведение Да 0,0 Возвращает-1 с errno ewouldblock; обработчик службы закрывается циклом обработки событий реактора. Да Время Возвращает-1 с errno ewouldblock; ждет завершения в течение заданного времени с помощью реактора. Да NULL Возвращает -1 с errno ewouldblock; ждет завершения (без ограничения времени) с помощью реактора. Нет 0,0 Непосредственно закрывает обработчик службы; возвращает-1 с errno ewouldblock Нет Время Блокируется в connect_svc_handler () на заданный интервал времени для завершения; если так и не завершается, возвращает-1 с errno etime. Нет NULL Блокируется в connect_svc_handier () без ограничения времени для завершения. Независимо от того, как установлено соединение, любые из методов по умолчанию (или все) make_svc_handler(), connect_svc_handler () и activate_svc_handler () могут быть подменены производными класса- ми. Такой расширяемый паттерном Template Method дизайн, дает возможность изменять и настраивать поведение в заданном диапазоне для поддержки не- скольких вариантов применения. Мы описываем три основных варианта изме- нений в ACE_Connector::connect(). 1. Получение обработчика службы. Так как акцептор часто управляется вы- зовами (upcalls) реактора, новый обработчик службы обычно создает его метод-фабрика make_svc_handler (). В отличие от этого, коннектор может выбрать установление соединения и инициализацию обработчика службы в любом из следующих вариантов: • Создается вызывающей стороной. Вызывающая сторона передает ука- затель на существующий обработчик службы методу коннектора con- nect (). • Создается коннектором. Вызывающая сторона передает указатель на об- работчик службы со значением NULL методу коннектора connect (), тем самым сообщая методу-фабрике make_svc_handler (), что указа- тель на обработчик службы нужно создать (или даже отложить это дейст- вие совсем, до того момента, когда метод connect_svc_handler () разрешит кэширование соединений). Реализация A.CE_Connector: :make_svc_handler () по умолчанию работает с этими двумя случаями следующим образом:
АСЕ-каркас Acceptor-Connector 279 template Cclass SVC_HANDLER, class PEER_CONNECTOR> int ACE_Connector<SVC_HANDLER, PEER_CONNECTOR>::make_svc_handler (SVC_HANDLER *&sh) { if (sh == 0) ACE_NEW_RETURN (sh, SVC_HANDLER, -1); sh->reactor (reactor ()); return 0; } Данный метод выглядит похожим на ACE_Acceptor: : make_svc_hand- ler () .Однако ACE_Connector: :make_svc_handler () создает новый об- работчик службы только в том случае, если значение указателя, переданного ему по ссылке, равно NULL, что дает возможность клиентскому приложению самому выбрать вариант создания обработчика службы: вызывающей сторо- ной или коннектором. 2. Установление соединения. Шаблонный метод ACE_Connector: : con- nect () вызывает свой hook-метод connect_svc_handler () для ини- циирования нового соединения с одноранговый акцептором. Реализация этого метода по умолчанию, с целью инициирования соединения, просто передает управление методу PEER_CONNECTOR: : connect (): template cclass SVC_HANDLER, class PEER_CONNECTOR> int ACE_Connector<SVC_HANDLER, PEER_CONNECTOR>::connect_svc_handler (SVC_HANDLER *sh, const ACE_TYPENAME PEER_CONNECTOR::PEER_ADDR &remote_addr, ACE_Time_Value *timeout, const ACE_TYPENAME PEER_CONNECTOR::PEER_ADDR &local_addr, int reuse_addr, int flags, int perms) { return connector_.connect (svc_handler->peer (), remote_addr, timeout, local_addr, reuse addr, flags, perms); } Реализации connect_svc_handler (), обладающие большими возмож- ностями, можно создать в классах, производных от ACE_Connector. Вот неко- торые примеры: •Кэширование соединения. Метод connect_svc_handler () может быть подменен так, чтобы можно было осуществлять поиск кэш-памяти уже существующих обработчиков службе установленными соединения- ми. Если подходящего обработчика с кэш-памятью нет, может быть соз- дан и присоединен новый SVC_HANDLER. Так как решение о создании нового SVC_HANDLER принимается в connect_svc_handler () (ко- торый вызывается после make_svc_handler ()), производный класс может определить CBoftMeroflmake_svc_handler () как пустой (no-op). • Аутентификация. Метод connect_svc_handler () может быть под- менен так, чтобы всегда устанавливалось синхронное соединение, а затем
280 Глава 7 на новом соединении организовать работу протокола аутентификации. Например, можно посылать закодированный пароль входа в систему и согласовывать права доступа с сервером, обеспечивающим защиту. Если аутентификация дает отрицательный результат, подмененный ме- тод connect_s vc_handler () может закрыть соединение и вернуть -1, чтобы сообщить об ошибке методу ACE_Connector:: connect () с тем, чтобы он не активизировал обработчик службы. В примере, приве- денном ниже, показано как реализовать аутентификацию SSL путем под- мены connect_svc_handler(). 3. Активизация обработчика службы. ACE_Connector может активизиро- вать обработчики служб следующими двумя способами, в зависимости от того, как соединение было инициировано: • Если соединение установлено синхронно, обработчик службы активизи- руется методом ACE_Connector: : connect () после успешного завер- шения connect_s vc_handler (). В этот момент connect () вызыва- ет activate_svc_handler () так, чтобы обработчик службы мог сам завершить инициализацию. • Если соединение установлено асинхронно, с помощью каркаса Reactor, реактор вызывает ACE_Connector: :handle_output (), чтобы про- информировать коннектор, что с конечной точкой IPC установлено со- единение. В этот момент handle_output () вызывает hook-метод ас- tivate_svc_handler () так, чтобы обработчик службы мог сам завер- шить инициализацию. Поведение ACE_Connector: : activate_svc_handler () по умолча- нию идентично поведению ACE_Acceptor: :activate_svc_handler (). Эта общность ACE_Acceptor и ACE_Connector подчеркивает большие воз- можности ACE-каркаса Acceptor-Connector, который полностью разделяет (1) пассивное и активное установление соединений и инициализацию, и (2) ис- пользование обработчика службы. Пример В этом примере АСЕ-каркас Acceptor-Connector применяется для реализа- ции еще одного клиентского демона регистрации, который расширяет демона, приведенного в примере раздела 6.2. Вместо того чтобы использовать специали- зированную реализацию паттерна Acceptor-Connector, изображенную на рис. 6.4, мы используем классы ACE_Acceptor, ACE_Connector и ACE_Svc_Hand- ler, рассмотренные в данной главе. Окончательная реализация получается бо- лее сжатой и переносимой, так как большая часть кода, который мы должны были писать «вручную» в предыдущем клиентском демоне регистрации, уже имеется в виде повторно используемых классов ACE-каркаса Acceptor-Connec- tor. Наша новая реализация имеет также больше возможностей, так как предос- тавляет прозрачно встроенный протокол аутентификации, позволяющий про- верять полномочия клиентского демона регистрации на установление соедине- ния с серверным демоном регистрации.
АСЕ-каркас Acceptor-Connector 281 Как и предыдущий клиентский демон регистрации, новая версия, исполь- зует два потока, которые решают, с помощью различных классов каркасов АСЕ, следующие задачи: • Обработка ввода — Основной (main) поток использует синглтон ACE_Re- actor, ACE_Acceptor и пассивный объект ACE_Svc_Handler для чтения регистрационных записей из сокетов, соединенных с клиентски- ми приложениями через сетевой интерфейс обратной связи. Каждая ре- гистрационная запись ставится в очередь у еще одного ACE_Svc_Hand- ler, который работает как активный объект. * Обработка вывода—Активный объект ACE_Svc_Handler выполняет- ся в своем собственном потоке. Он исключает сообщения из своей очере- ди, помещает порции сообщений в буфер и передает эти порции сервер- ному демону регистрации по TCP-соединению. Класс, производный от ACE_Connector, используется для установления (и, когда нужно, вос- становления) и аутентификации соединений с сервером регистрации. Классы, образующие новый клиентский демон регистрации на базе АСЕ- каркаса Acceptor-Connector, приведены на рис. 7.9. Роли каждого из классов пе- речислены ниже: Класс Описание AC_Input_Handler Целевой объект обратных вызовов от синглтона ACE_Reactor, который получает регистрационные записи от клиентов, сохраняет каждую в блоке ACE_Message_Block и передает их в AC_Output_Handler для обработки. AC_Output_Handler Активный объект, который выполняется в своем собственном потоке. Его метод put () ставит в очередь блоки данных, передаваемые ему от AC_input_Handier. Его метод svc () исключает сообщения из своей синхронной очереди и передает их серверу регистрации. AC_CLD_Acceptor Фабрика, которая пассивно принимает соединения от клиентов и регистрирует их у синглтона ACE_Reactor ДЛЯ обработки У AC_Input_Handler. AC_CLD_Connector Фабрика, которая активно устанавливает (восстанавливает) соединения с сервером регистрации и обеспечивает их аутентификацию. AC_Client_Logging_ Daemon Фасадный класс, который объединяет остальные классы в единое целое. Взаимодействие между реализациями этих классов показано на рис. 7.10. Пока служба функционирует, существует два потока. Первый — это исходный поток программы, он выполняет цикл обработки событий реактора. Этот по- ток выполняет следующую обработку:
282 Глава 7 Рис. 7.9 Классы клиентского демона регистрации типа Acceptor-Connector • Принимает новые соединения от клиентов регистрации с помощью AC_CLD_Acceptor. • Принимает регистрационные записи и идентифицирует разрыв соедине- ний с клиентами регистрации с помощью AC_Input_Handler; регист- рационные записи ставятся в очередь сообщений AC_Output_Hand- ler. • Обнаруживает разрыв соединений с сервером регистрации в AC_Out- put_Handler и повторно устанавливает соединения с сервером с помо- щью AC_CLD_Connector. Поток-ретранслятор (forwarder thread) начинает работу после установле- ния первоначального соединения с сервером регистрации и продолжает работу до завершения работы службы. Этот поток реализует служебный поток актив- ного объекта AC_Output_Handler. Мы начинаем реализацию с включения необходимых заголовочных фай- лов АСЕ. ♦include "ace/OS.h'* ♦include "ace/Acceptor.h" ♦include "ace/Connector.h” ♦include "ace/Get_Opt.h" ♦include "ace/Handle_Set.h" ♦include "ace/INET Addr.h"
АСЕ-каркас Acceptor-Connector 283 Основной поток Поток-ретранслятор Клиентские приложения ТСР-соединения обратной связи Клиентский демон регистрации Сервер Сеть AC_CLD Acceptor Рис. 7.10 Взаимодействия в клиентском демоне регистрации типа Acceptor-Connector getq() •putq () АСЕ >1 message v queue > TCP- соединение АСЕ reactor , ACCLD Connector #include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include ♦include "ace/Log_Record.h" "ace/Message_Block.h" ”ace/Reactor.h” "ace/Service_Object.h" "ace/Signal.h” "ace/Svc_Handler.h” "ace/Synch.h” "ace/SOCK_Acceptor.h” "ace/SOCK_Connector.h” "ace/SOCK_Stream.h" "ace/Thread_Manager.h" "Logging_Handler.h" "AC_CLD_export.h" <openssl/ssl.h> Классы, представленные на рис. 7.9, определены в файле AC_Cli- ent_Logging_Daemon. срр и рассмотрены ниже. AC_Input_Handler. Данный класс обеспечивает следующие возможности: • Отвечает за получение регистрационных записей от клиентов. • Сохраняет каждую регистрационную запись в ACE_Message_Block. • Передает блоки данных в AC_Output_Handler для обработки. Класс AC_Input_Handler приведен ниже: class AC_Input_Handler : public ACE_Svc_Handler<ACE_SOCK_Streamz ACE_NULL_SYNCH> {
284 Глава 7 public: AC_Input_Handler (AC__Output_Handler *handler = 0) : output_handler_ (handler) {} virtual int open (void *) ; // Ноок-метод инициализации. virtual int close (u_int = 0); // Ноок-метод завершения работы, protected: // Ноок-методы реактора. virtual int handle_input (ACE_HANDLE handle); virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE, ACE_Reactor_Mask = 0) ; // Указатель на обработчик вывода. AC_0utput_Handler *output__handler__; // Следит за дескрипторами присоединенных, клиентов. ACE_Handle_Set connected_clients_; }; Поскольку AC_Input_Handler не фиксирует состояние каждого клиент- ского соединения, в AC_CLD_Acceptor определяется только один экземпляр для обработки ввода от всех клиентских соединений. Поэтому мы определяем ACE_Handle_Set, который является интерфейсным фасадом для fd_set, рассмотренной в главе 7 [C++NPvl]. Данный ACE_Handle_Set следит за все- ми дескрипторами сокетов клиентов, установивших соединение, так что мы можем удалить его из синглтона-реактора, после закрытия AC_Input_Hand- ler. В блоке 53 рассматриваются «за и против» различных стратегий использо- вания, как нескольких обработчиков служб, так и одного. Так как AC_Input_Handler является производным от ACE_Svc_Hand- ler, он может использовать синглтон ACE_Reactor для ожидания поступле- ния регистрационных записей от любых клиентских приложений, соединен- ных с клиентским демоном регистрации через TCP сокеты с петлей обратной связи. Поэтому он не использует свою очередь сообщений, он реализует ACE_Svc_Handler со стратегией ACE_NULL_SYNCH, чтобы минимизировать использование своего синхронизатора. Когда регистрационная запись поступа- ет клиентскому демону регистрации, синглтон ACE_Reactor передает управ- ление следующему hook-методу AC_Input_Handler: : handle_input (): int AC__Input__Handler: :handle_input (ACE__HANDLE handle) { ACE__Message_Block *mblk = 0; Logging__Handler logging_handler (handle) ; if (logging__handler . recv__log__record (mblk) ’= -1) if (output__handler__->put (mblk->cont ()) ’= -1) { mblk->cont (0); mblk->release (); return 0; // Успешное завершение. } else mblk->release (); return -1; // Ошибка.
АСЕ-каркас Acceptor-Connector 285 Блок 53: Один обработчик службы по сравнению с несколькими В реализации серверного демона регистрации в примере из раздела 7.3 для каждого клиента, установившего соединение, динамически создается новый обработчик службы. В отличие от этого, в реализации клиентского демона ре- гистрации данного примера используется один обработчик службы для всех клиентов, установивших соединение. У этих двух подходов следующие осно- вания и компромиссы: • Если каждый обработчик службы поддерживает отдельную информацию о состоянии каждого клиента (в дополнение к дескриптору соединения), то- гда создание обработчика службы для каждого клиента является, как пра- вило, наиболее естественным решением. • Если обработчик службы не обязан следить отдельно за состоянием каждо- го клиента, то сервер, который создает один обработчик службы для всех клиентов, потенциально, может использовать меньше памяти и работать бы- стрее, чем в том случае, когда он динамически создает обработчик для каж- дого клиента. Данный положительный эффект возрастает по мере увеличе- ния числа одновременно подключенных клиентов. • Обычно управлять памятью гораздо проще, если для каждого клиента дина- мически создается отдельный обработчик службы, так как классы АСЕ-кар- каса Acceptor-Connector являются реализацией наиболее общих функций для такого рода случаев — обработчик службы просто вызывает destroy () из своего hook-меюда handle_close (). И наоборот, управление памятью становится сложнее, если один обработчик службы совместно использует- ся всеми клиентами, • Если инициализация обработчика службы может быть осуществлена из не- скольких потоков, как в случае использования ACE_WFMO_Reactor, осущест- вляющего диспетчеризацию нескольких потоков, то приходится принимать во внимание возможные состояния гонок и использовать соответствующую синхронизацию, чтобы избежать нерационального управления соедине- ниями. Для полноты картины, мы демонстрируем в этой главе оба подхода. И все же, решение, связанное с несколькими обработчиками служб, как правило, го- раздо легче программировать. Поэтому мы советуем на практике применять именно это решение, за исключением тех случаев, когда нужно экономить па- мять. Этот метод использует Logging_Handler из главы 4 [C++NPvl] для чте- ния регистрационной записи, расположенной после параметра дескриптора со- кета, сохранения этой записи в ACE_Message_Block и передачи этого сооб- щения в AC_Output_Handler, который ставит ее в очередь и обслуживает в отдельном потоке. Мы помещаем в очередь только данные регистрационной записи (ссылкой на которую является mblk->cont ()), но не имя хоста (на ко- торое ссылается mblk). Если клиентское приложение отключает сокет или возникает ошибка, handle_input () возвращает -1. Это значение приводит к тому, что сингл- тон-реактор вызывает следующий hook-метод handle_close (), который за- крывает этот сокет.
286 Глава 7 int AC_Input_Handler::handle_close (ACE_HANDLE handle, ACE_Reactor_Mask) { connected_clients_.clr_bit (handle); return ACE_OS::closesocket (handle); ) Заметьте, что нам не нужно удалять этот объект в handle_close (), так как память AC_Input_Handler управляется из AC_CLD_Acceptor. Метод handle_close () удаляет также указанный дескриптор из набора дескрипто- ров connected_clients_, к которому он был добавлен тогда, когда было от- крыто данное соединение. Когда клиентскому демону регистрации поступает запрос на соединение от клиентского приложения, AC_CLD_Acceptor: :handle_input () передает управление следующему hook-методу AC_Input_Handler: : open (): lint AC_Input_Handler::open (void *) { 2 ACE_HANDLE handle = peer ().get_handle (); 3 if (reactor ()->register_handler 4 (handle, this, ACE_Event_Handler::READ_MASK) == -1) 5 return -1; 6 connected_clients_.set_bit (handle); 7 return 0; 8 } Строки 2-5 Клиентский демон регистрации повторно использует один объ- ект AC_Input_Handler для всех своих обработчиков регистрации. Поэтому ко- гда вызывается AC_CLD_Acceptor: :accept_svc_handler (),чтобы принять новое соединение, он повторно использует дескриптор ACE_SOCK_Stream объ- екта AC_Input_Handler для каждого соединения. Мы используем метод ACE_Reactor: : register_handler () с тремя параметрами для регистра- ции указателя на этот единственный объект в синглтоне-реакторе для обработ- ки событий READ. При поступлении регистрационных записей, синглтон-реак- тор будет передавать управление AC_Input_Handler: : handle_input (). Напомним, что, в соответствии с изложенным в блоке 53, обработчики служб, синглтоны, и многопоточная диспетчеризация событий могут приво- дить к состоянию гонок. Мы можем утратить контроль за некоторыми соедине- ниями, если класс AC_Input_Handler используется с многопоточным цик- лом обработки событий реактора, так как член ACE_SOCK_Stream объекта AC_Input_Handler может изменяться несколькими потоками. Каждый уча- ствующий в диспетчеризации событий поток делает следующее: 1. Вызывает метод AC_CLD_AcceptOr: : accept_svc_handler (), кото- рый принимает новый сокет и сохраняет его дескриптор в АС_1п- put_Handler. 2. Вызывает метод AC_CLD_Acceptor: : activate_svc_handler (), ко- торый, в свою очередь, вызывает приведенный выше AC_Input_Hand- ler::open().
АСЕ-каркас Acceptor-Connector 287 Если поток В принимает новое соединение до того, как поток А завершает два перечисленных выше шага, то, возможно, что вызов AC_Input_Hand- ler: : open () в потоке А зарегистрирует, на самом деле, сокет, принятый по-; током В. Если такое случится, то первый принятый сокет, возможно, останется открытым, но никогда не будет зарегистрирован у реактора и, следовательно, окажется в подвешенном состоянии. Чтобы предотвратить такое состояние гонок, требуется синхронизировать весь код, в котором осуществляется доступ к совместно используемому объек- ту ACE_SOCK_Stream в AC_Input_Handler. К сожалению, область, на кото- рую должно распространяться действие синхронизации включает два приве- денных выше этапа и, поэтому, синхронизация должна выполняться вызываю- щей стороной, методом ACE_Acceptor: : handle_input (). Следовательно, использование множества потоков при диспетчеризации событий реактором может привести к необходимости создания новой реализации handle_input, чтобы правильно синхронизировать параллельный прием соединений. Поэто- му, в примере данного раздела, используется один поток, осуществляющий диспетчеризацию событий реактора. Строка 6 Записываем дескриптор клиента, установившего соединение, в connected_clients_ в АСЕ_ Handle_Set. Мы следим за дескрипторами клиентов, установивших соединение, поэтому, мы можем удалять их из реак- тора при вызове метода AC_Input_Handler: : close (). Этот метод, с целью завершения работы клиентского демона регистрации, вызывается или методом AC_CLD_Acceptor::handle_close(), или методом AC_Client_Log- ging_Daemon::fini(): lint AC_Input_Handler::close (u_int) { 2 ACE_Message_Block *shutdown_message = 0; 3 ACE_NEW_RETURN 4 (shutdown_message, 5 ACE_Message_Block (0, ACE_Message_Block::MB_STOP), -1); 6 output_handler_->put (shutdown_message); 7 8 reactor ()->remove_handler 9 (connected_clients_, ACE_Event_Handler::READ_MASK); 10 return output_handler_->wait (); 11 } Строки 2-6 Вставляем блок данных нулевого размера типа MB_STOP в оче- редь сообщений. Когда поток-ретранслятор, который выполняет AC_Out- put_Handler: :svc(), получит это shutdown_message, он передаст ос- тающиеся у него регистрационные записи серверу регистрации, закроет оче- редь сообщений и завершит работу самого потока. Строки 8-9 Одной операцией удаляем все дескрипторы из набора дескрип- торов connected_clients_. Каждый удаленный дескриптор приведет к об- ратному вызову реактором AC_Input_Handler: :handle_close (), кото- рый закрывает дескриптор сокета.
288 Глава 7 Строка 10 Используем метод wait () обработчика output_handler_ для того, чтобы блокироваться до завершения его hook-метода s vc (), прежде чем завершить работу AC_Input_Handler: : close (). Этот метод получает статус завершения потока-ретранслятора, чтобы предотвратить утечки памяти. AC_Output_Handler. Этот класс обеспечивает следующие функциональ- ные возможности: • Вставляет блоки данных, передаваемые ему из AC_Input_Handler, в свою синхронную очередь сообщений. • Выполняется как активный объект в своем собственном потоке, исклю- чает блоки данных из своей синхронной очереди сообщений, порциями (chunks) записывает их в буфер и передает эти порции серверу регистра- ции. • Регистрируется у синглтона ACE_Reactor для обработки отключений от сервера регистрации и восстановления соединений. Класс AC_Output_Handler приведен ниже: class AC_Output_Handler : public ACE_Svc_Handler<ACE_SOCK_Stream, ACE_MT_SYNCH> { public: enum { QUEUE_MAX = sizeof (ACE_Log_Record) * ACE_IOV_MAX }; virtual int open (void *); // Ноок-метод инициализации. // Точка входа в <AC_Output_Handler>. virtual int put (ACE_Message_Block *, ACE_Time_Value * = 0) ; protected: // Указатель на фабрику соединений для <AC_Output_Handler>. AC_CLD_Connector *connector_; 11 Обрабатывает отключения от сервера регистрации, virtual int handle_input (ACE_HANDLE handle) ; // Ноок-метод передает регистрационные записи серверному // демону. virtual int svc (); // Отправляем per.записи из буфера операцией // записи-со-слиянием. virtual int send (ACE_Message_Block *chunk[], size_t &count); }; ♦if (defined (FLUSHJTIMEOUT) ♦define FLUSH_TIMEOUT 120 /* 120 секунд == 2 минуты. */ ♦endif /* FLUSH_TIMEOUT */
АСЕ-каркас Acceptor-Connector 289 Так как AC_Output_Handler является производным от ACE_Svc_Hand- ler и реализует свои характеристики (traits) синхронизации с ACE_MT_SYNCH, он наследует ACE_SOCK_Stream, ACE_Thread_Manager и синхронную ACE_Message_Queue, а также возможность самоактивизироваться, то есть переходить в состояние активного объекта. AC_Input_Handler: : handle_input () играет роль реактора в том ва- рианте паттерна Half-Sync/Half-Async, который мы используем для упорядоче- ния архитектуры параллелизма данного клиентского демона регистрации. Он передает регистрационные записи AC_Output_Handler с помощью следую- щего метода put (): int AC_Output_Handler::put (ACE_Message_Block *mb, ACE_Time_Value *timeout) { int result; while ((result = putq (mb, timeobt)) == -1) if (msg_queue ()->state () != ACE_Message_Queue_Base::PULSED) break; return result; 1 Этот метод просто ставит блок данных в синхронную очередь сообщений AC_Output_Handle г. Если вызов putq () блокируется и очередь сообщений переходит в состояние PULSED, то он просто повторно выполняет вызов putq (), чтобы попытаться еще раз. Следующие два метода поясняют, каким образом этот класс использует состояние очереди PULSED. Фабрика AC_CLD_Connector инициализирует’ AC_Output_Handler, вызывая его hook-метод open (), приведенный ниже: lint AC_Output_Handler::open (void ‘connector) ( 2 connector_ = 3 ACE_static_cast (AC_CLD_Connector *, connector); 4 int bufsiz = ACE_DEFAULT_MAX_SOCKET_BUFSIZ; 5 peer ().set_option (SOL_SOCKET, SO_SNDBUF, 6 &bufsiz, sizeof bufsiz); 7 if (reactor ()->register_handler 8 (this, ACE_Event_Handler::READ_MASK) == -1) 9 return -1; 10 if (msg_queue ()->activate () 11 == ACE_Message_Queue_Base::ACTIVATED) { 12 msg_queue () ->high_water_mark (QUEUE_MAX) ; 13 return activate (THR_SCOPEj_SYSTEM) ; 14 } else return 0; 15 ) Строки 2-3 Сохраняем указатель на фабрику AC_CLD_Connector, кото- рая вызывала этот метод. Если потребуется восстановить соединение с серве- ром, то будет использоваться та же самая фабрика. 10 Программирование сетевых приложений на C++. Том 2
290 Глава 7 Строки 4-6 Увеличиваем пересылочный буфер сокета с установленным со- единением до максимального размера, чтобы максимально увеличить произво- дительность в высокоскоростных сетях и/или в сетях с большой задержкой. Строки 7-8 Регистрируем объект this у синглтона-реактора так, чтобы, если произойдет отключение сервера регистрации, об этом сразу сообщалось его методу handle_input (). Строки 10-13 Этот метод вызывается каждый раз, когда устанавливается новое соединение с сервером регистрации. При первом соединении очередь со- общений будет в активном состоянии, поэтому мы устанавливаем отметку уровня «полной воды» очереди сообщений в sizeof (ACE_Log_Re- cord) *ACE_IOV_MAX, точно также как мы это делали в строке 6 CLD_Hand- ler:: open (). Затем мы создаем поток с областью действия на уровне систе- мы, который параллельно выполняет hook-метод AC_Input_Hand- ler: : svc (). Поскольку AC_Input_Handler: : close () ждет завершения этого потока, мы не передаем флаг THR_DETACHED в activate (). Однако если очередь сообщений не была в состоянии ACTIVATED, мы зна- ем, что соединение с сервером регистрации было возобновлено. В этом случае, отметка уровня «полной воды» очереди сообщений уже установлена и поток службы уже выполняется. Теперь мы приведем hook-метод AC_Output_Handler: :svc(), кото- рый выполняется в своем собственном потоке и передает регистрационные за- писи серверному демону регистрации. Как показано ниже, этот метод оптими- зирует сетевую пропускную способность путем буферизации регистрацион- ных записей, пока не будет набрано максимальное количество или пока не истечет срок тайм-аута. 1 int AC_Output_Handler::svc () { 2 ACE_Message_Block * chunk [ACE_IOV_MAX] ; 3 size_t message_index = 0; 4 ACE_Time_Value time_of_last_send (ACE_OS::gettimeofday ()); '5 ACE_Time_Value timeout; 6 ACE_Sig_Action no_sigpipe ((ACE_SignalHandler) SIG_IGN); 7 ACE_Sig_Action original_action; 8 no_sigpipe.register_action (SIGPIPE, &original_action); 9 10 for {;;) ( 11 if (message_index ==0) { 12 timeout = ACE_OS::gettimeofday (); 13 timeout += FLUSH TIMEOUT; 14 } 15 ACE_Message_Block *mblk = 0 ; 16 if (getq (mblk, &timeout) == -1) { 17 if (errno == ESHUTDOWN) { 18 if (connector_->reconnect () == -1) break; 19 continue; 20 } else if (errno != EWOULDBLOCK) break; 21 else if (message_index == 0) continue; 22 } else {
АСЕ-каркас Acceptor-Connector 291 23 if (mblk->size () == 0 24 && mblk->msg_type () == ACE_Message_Block: :MB_STOP) 25 { mblk->release (); break; } 26 chunk[message_index] = mblk; 21 ++message__index; 28 } 29 if (message—index >= ACE_IOV_MAX 30 || (ACE—OS::gettimeofday () - time_of_last_send 31 >= FLUSHJTIMEOUT)) { 32 if (sehd (chunk, message_index) == -1) break; 33 time_of_last_send = ACE JDS::gettimeofday (); 34 } 35 ) 36 37 if (message—index > 0) send (chunk, message—index); 38 no_sigpipe.restore_action (SIGPIPE, original—action); 39 return 0; 40 } Мы опускаем реализацию AC_Output-Handler: :send(), так как она идентична методу CLD_Handler: : send (), который посылает регистрацион- ные записи серверу регистрации и восстанавливает соединения с сервером, если они были закрыты в процессе передачи. Фактически, AC_Output_Hand- ler: : svc () похож на CLD_Handler: : forward (). Основное отличие за- ключается в том, что строки с 17 по 20, приведенные выше, проверяют очередь на состояние PULSED, в которое она переходит в ответ на отключение сервера регистрации. Перевод в состояние PULSED выполняется методом ACjDut- put_Handler: : handle_input (), приведенным ниже, диспетчеризация ко- торого осуществляется синглтоном-реактором при закрытии сервером соеди- нения с клиентским демоном регистрации. После восстановления соединения с помощью ACJConnector: : reconnect (), класс ACEJDpnnec tor вызыва- ет AC_Output_Handler: : open (), который снова переводит очередь сооб- щений в состояние ACTIVATED. lint ACJ3utput—Handler: :handle_input (ACE_HANDLE h) { 2 peer () .close () ; 3 reactor ()->remove_handler 4 (h, ACE_Event_Handler::READ_MASK S I ACE—Event—Handler::DONT—CALL); 6 msg_queue ()->pulse (); 7 return 0; 8 ) Строка 2 Закрываем соединение, чтобы освободить дескриптор сокета. Строки 3-5 Удаляем дескриптор сокета из реактора, поскольку он больше не действует. Так как теперь мы выполнили всю необходимую очистку, а сам объект не удаляется, в remove-handler, () передается флаг DONT__CALL. Ко- гда соединение с серверным демоном регистрации восстанавливается, будет снова вызван open () и новый сокет будет зарегистрирован для этого объекта. 10*
292 Глава 7 Строка 6 Чтобы избежать попыток восстановить соединение с серверным демоном регистрации в то время как поток-ретранслятор, возможно, питается сделать то же самое, перепоручим эту работу потоку-ретранслятору с помо- щью метода pulse (). Если поток-ретранслятор ждет в синхронной очереди сообщений (и поэтому не знает, что соединение закрыто), его работа будет во- зобновлена и он немедленно начнет восстанавливать соединение. Метод AC_Input_Handler: :handle_input () может продолжать ставить блоки данных в очередь, гарантируя, что управление потоком будет возвращено (back-propagated) клиентским приложениям в правильной последовательно- сти, если соединение не получится восстановить в течение длительного перио- да времени. Метод ACE_Svc_Handler:: close () будет вызываться автоматически АСЕ-каркасом Acceptor-Connector при завершении потока, выполняющего hook-метод svc (). Этот метод освобождает все динамически выделенные ре- сурсы, такие как синхронная очередь сообщений и ее содержимое, и удаляет AC_Output_Handler из сингтона-реактора. AC_CLD_Acceptor. Данный класс обеспечивает следующие возможности:1 • Он является фабрикой, которая создает один экземпляр АС_1п- put_Handler. • Пассивно принимает соединения от клиентов. • Активизирует единственный экземпляр AC_Input_Handler, который регистрирует все соединения с синглтоном ACE_Reactor. Определение класса AC_CLD_Acceptor приведено ниже: class AC_CLD_Acceptor : public ACE_Acceptor<AC_Input_Handler, ACE_SOCK_Acceptor> { public: AC_CLD_Acceptor (AC_Output_Handler *handler = 0) : output_handler_ (handler), input_handler_ (handler) () protected: typedef ACE_Acceptor<AC_Input_Handler, ACE_SOCK_Acceptor> PARENT; // Метод-фабрика <ACE_Acceptor>. virtual int make_svc_handler (AC_Input_Handler *&sh); // Ноок-метод закрытия <ACE_Reactor>. virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE, ACE_Reactor_Mask = 0); // Указатель на обработчик вывода. AC_Output_Handler *output_handler_; Хотя AC_CLD_Acceptor не выполняет аутентификации нового клиента регистрации, ау- тентификация может быть добавлена и в AC_CLD_Acceptor, и в код клиента регистрации. Эти добавления оставлены в качестве упражнения для читателя.
АСЕ-каркас Acceptor-Connector 293 // Единственный обработчик ввода. AC_Input_Handler input_handler_; }; AC_CLD_Acceptor является классом производным от ACE_Acceptor, поэтому он наследует все возможности, рассмотренные в разделе 7.3. Так как нам нужен только один экземпляр AC_Input_Handler, мы следующим обра- зом подменяем метод ACE_Acceptor: :make_svc_handler (): int AC_CLD_Acceptor::make_svc_handler (AC_Input_Handler *&sh) { sh = &input_handler_; return 0; } Этот метод устанавливает обработчик службы на адрес члена данных in- put_handler_, что гарантирует наличие только одного экземпляра АС_1п- put_Handler, независимо от количества клиентов, установивших соедине- ние. Следующий метод AC_CLD_Acceptor: :handle_close () вызывается реактором автоматически, если, в процессе принятия соединения или регистра- ции дескриптора и обработчика событий у реактора, возникает ошибка: lint AC_CLD_Acceptor::handle_close (ACE_HANDLE, 2 ACE_Reactor_Mask) { 3 PARENT::handle_close (); 4 input_handler_.close (); 5 return 0; 6 } Строка 3 Вызываем родительский метод handle_close (), чтобы за- крыть акцептор. Строка4 Вызываем метод close () обработчика input_handler_, что- бы освободить ресурсы AC_Input_Handler и закрыть очередь сообщений AC_Output_Handler и поток svc (). AC_CLD_Connector. Данный класс обеспечивает следующие возможности: Активно устанавливает (и, когда нужно, восстанавливает) и аутентифици- рует соединения с сервером регистрации. Активизирует один экземпляр AC_Output_Handler, который передает регистрационные записи серверу регистрации параллельно по отношению к приему регистрационных записей AC_Input_Handler. Определение класса AC_CLD_Connector приведено ниже: class AC_CLD_Connector : public ACE_Connector<AC_Output_Handler, ACE_SOCK_Connector> { public: typedef ACE_Connector<AC_Output_Handler, ACE_SOCK_Connector> PARENT; AC_CLD_Connector (AC_Output_Handler *handler = 0) : handler_ (handler), ssl_ctx_ (0), ssl_ (0) {)
294 Глава 7 virtual ~AC_CLD_Connector (void) { // Освобождает ресурсы SSL. SSL_free (ssl_); SSL-CTX-free (ssl_ctx_); } // Инициализируем коннектор. virtual int open (ACE—Reactor *r = ACE—Reactor::instance (), int flags = 0); int reconnect (); // Восстанавливает соединение с сервером. protected: // Ноок-метод установления и аутентификации соединений. virtual int connect-SvC-handler (АС-Output-Handler *svC-handler, const ACE—SOCK-Connector::PEER-ADDR &remote—addr, ACE-Time—Value *timeout, const ACE-SOCK-Connector::PEER-ADDR &local_addr, int reuse-addr, int flags, int perms); // Указатель на <AC-Output_Handler>, к которому //мы подключаемся. AC—Output-Handler *handler_; // Адрес, по которому сервер регистрации слушает запросы //на соединения. ACE-INET—Addr remote—addr_; // Структура данных SSL ’’context”. SSL-CTX *ssl-ctx__; // Структура данных SSL, соответствующая аутентифицированным // соединениям SSL. SSL *ssl_; }; Реализация метода CLD_Connector: :open() выполняет стандартную инициализацию ACE-Connector, дополнительно к использованию OpenSSL для установления идентичности клиента. #if !defined (CLD—CERTIFICATE—FILENAME) # define CLD—CERTIFICATE—FILENAME ”cld-cert.pern" #endif /* ’CLD—CERTIFICATE—FILENAME */ #if ’defined (CLD—KEY—FILENAME) # define CLD—KEY—FILENAME ”cld-key.pern" #endif /* ’CLD—KEY—FILENAME */ int AC-CLD-Connector:-.open (ACE_Reactor *r, int flags) { if (PARENT::open (r, flags) != 0) return -1; OpenSSL—add-Ssl—algorithms () ; ssl—ctX— = SSL-CTX-Hew (SSLv3-Client-inethod ());
АСЕ-каркас Acceptor-Connector 295 if (ssl_ctx_ == 0) return -1; if (SSL_CTX_use_certificate_file (ssl_ctx_, CLD_CERTIFICATE_FILENAME, SSL_FILETYPE_PEM) <= 0 I | SSL_CTX_use_P.rivateKey_file (ssl_ctx_, CLD_KEY_FILENAME, SSL_FILETYPE_PEM) <= 0 I| !SSL_CTX_check_private_key (ssl_ctx_)) return -1; ssl_ = SSL_new (ssl_ctx_); if (ssl_ == 0) return -1; return 0; } Приведенный код инициализирует и проверяет правильность структур данных OpenSSL, используя, по существу, ту же логику, что и реализация TPC__Logging__Acceptor: : open (). В отличие от CLD__Connect or, AC_CLD_Connector не нужна реализация метода connect (). Вместо этого, он повторно использует шаблонный метод ACE_Connector: : connect (). Проверяется также и идентичность сервера. Этот протокол на стороне серверного демона регистрации — в TPC_Log- ging__Acceptor: : accept_svc_handler (), а на стороне клиентского де- мона регистрации приводится ниже: 1 int AC_CLD_Connector::connect_svc_handler 2 (AC_Output_Handler *svc_handler, 3 const ACE_SOCK_Connector::PEER—ADDR &remote_addr, 4 ACE_Time_Value *timeout, 5 const ACE_SOCK_Connector::PEER_ADDR &local_addr, 6 int reuse_addr, int flags, int perms) { 7 if (PARENT::connect_svc_handler 8 (svc_handler, remote_addr, timeout, 9 local_addr, reuse_addr, flags, perms) == -1) return -1; 10 SSL_clear (ssl_); 11 SSL_set_fd (ssl_, ACE_reinterpret_cast 12 (int, svc_handler->get_handle ())); 13 14 SSL_set_yerify (ssl_, SSL_VERIFY_PEER, 0); 15 16 if (SSL_connect (ssl_) == -1 17 I| SSL_shutdown (ssl_) == -1) return -1; 18 remote_addr_ = remote_addr; 19 return 0; 20 } Строки 7-9 Устанавливаем TCP-соединение, используя по умолчанию connect__svc__handler ().
296 Глава 7 Строки 10-12 Восстанавливаем структуры данных SSL для использования с новым SSL-соединением. Строка 14 Настраиваем структуры данных SSL так, чтобы аутентификация сервера выполнялась в обязательном порядке при установлении SSL-соедине- ния. Строка 16 Реально выполняем соединение/согласование SSL. Если аутен- тификация сервера не проходит, вызов SSL_connect () завершается ошиб- кой. Строка 17 Закрываем SSL-соединение, если аутентификация была успеш- ной. Так как мы, на самом деле, данные не шифруем, то с этого момента мы мо- жем обмениваться данными по TCP-потоку. Если требуется также и шифро- вать данные, то в блоке 52 приведена информация о том, каким образом можно использовать интерфейсные фасады АСЕ для OpenSSL. Строка 18 Сохраняем адрес серверного демона регистрации, с которым ус- тановлено соединение, на тот случай, если нужно будет восстанавливать соеди- нение с помощью метода AC_CLD_Connector:: reconnect () , рассматри- ваемого ниже. Путем подмены hook-методов open () и connect_svc_handler (), мы можем добавить аутентификацию к нашему клиентскому демону регистрации, больше ничего в его реализации не затрагивая. Эта расширяемость иллюстри- рует большие возможности паттерна Template Method, используемого в дизай- не класса ACE_Connector. Следующий метод AC_CLD_Connector: : reconnect () использует тот же алгоритм экспоненциальной задержки, что и CLD_Connector: : recon- nect (), чтобы предотвратить перегрузку сервера регистрации запросами на соединение: int AC_CLD_Connector::reconnect () { // Максимальное число попыток восстановления соединения. const size_t MAX_RETRIES = 5; ACE_Time_Value timeout (1); size_t i; for (i = 0; i < MAX_RETRIES; ++i) { ACE_Synch_Options options (ACE_Synch_Options::USE_TIMEOUT, timeout); if (i > 0) ACE_OS::sleep (timeout); if (connect (handler_, remote_addr_, options) == 0) break; timeout *= 2; // Экспоненциальная задержка. ) return i == MAX_RETRIES ? -1 : 0; ) AC_CLient_Logging_Daemon. Данный класс представляет собой фасад, ко- торый объединяет остальные классы, рассмотренные выше, для реализации но- вого клиентского демона регистрации. Его определение приведено ниже:
АСЕ-каркас Acceptor-Connector 297 class AC_Client—Logging_Daemon : public ACE_Service_Object { protected: // Фабрика пассивного соединения c <AC_Input_Handler>. AC—CLD—Acceptor acceptor^; // Фабрика активного соединения с <AC_Output_Handler>. AC_CLD_Connector connector^; // <AC_Output_Handler> подключенный <AC_CLD_Connector>. AC_Output_Hand1e r ou tput_hand1e r_; public: AC_Client_Logging_Daemon () : acceptor^ (&output_handler^)f connector- (&output—handler-) {} // Ноок-методы Service Configurator. virtual int init (int argc, ACE-TCHAR *argv[]); virtual int fini (); virtual int info (ACE-TCHAR **bufferp, size_t length = 0) const; virtual int suspend (); virtual int resume (); AC_Client-Logging_Daemon является производным от ACE-Servi- ce_Ob j ect. Поэтому он может конфигурироваться динамически с помощью файла svc. conf, который обрабатывается АСЕ-каркасом Service Configurator, рассмотренным в главе 5. Когда экземпляр AC-Client_Logging_Daemon связывается динамически, АСЕ-каркас Service Configurator вызывает приведен- ный ниже hook-метод AC_Client-Logging-Daemon: : init () : 1 int AC—Client—Logging-Daemon::init 2 3 4 5 6 7 8 9 10 11 12 13 14 (int argc, ACE_TCHAR *argv[]) { U-Short cld_port = ACE-DEFAULT-SERVICE-PORT; U-Short sld—port = ACE—DEFAULT—LOGGING—SERVER—PORT; ACE-TCHAR sld_host[MAXHOSTNAMELEN]; ACE—OS—String::strcpy (sld—host, ACE_LOCALHOST); ACE—Get—Opt get-Opt (argc, argv, ACE_TEXT ("p:r:s:" get—opt.long—option (ACE-TEXT ("client-port"), ’pr, ACE—Get—Opt::ARG-REQUIRED); get—opt.long_option (ACE—TEXT ("server_port"), 'r’, ACE—Get—Opt::ARG-REQUIRED); gfet—opt.long—option (ACE—TEXT ("server-name”), ’s’, ACE—Get—Opt::ARG-REQUIRED) ; 0); 15 for (int c; (c = get-Opt ()) != -1;) 16 switch (c) { 17 case ’p’: // Номер порта акцептора клиентского 11 демона регистрации. 18 cld-port = АСЕ-StatiC-Cast
298 Глава 7 19 (u_short, ACE_OS::atoi (get_opt.opt_arg ())); 20 break; 21 case 'r': // Номер порта акцептора серверного // демона регистрации. 22 sld—port = АСЕ—static—cast 23 (u_short, ACE-OS::atoi (get_opt.opt_arg ())); 24 break; 25 case ’s': // Имя хоста серверного демона регистрации. 26 АСЕ—OS—String::strsncpy 27 (sld—host, get—opt.opt—arg (), MAXHOSTNAMELEN); 28 break; 29 } 30 31 ACE—INET—Addr cld_addr (cld_port); 32 ACE—INET—Addr sld_addr (sld_port, sld-host); 33 34 if (acceptor—.open (cld—addr) == -1) return -1; 35 AC—Output—Handler *oh = &output—handler—; 36 if (connector—.connect (oh, sld—addr) == -1) 37 { acceptor-.close (); return -1; } 38 return 0; 39 } Строки 3-6 Назначаем порт прослушивания клиентского демона регистра- ции по умолчанию (cld_port), порт серверного демона регистрации по умол- чанию (sld-port) и имя хоста (sld-host). Они могут быть изменены с по- мощью аргументов, передаваемых в этот метод. В частности, имя хоста сервер- ного демона регистрации часто придется устанавливать с помощью опции -s. Строки 7-29 Итератор ACE_Get_Opt, рассмотренный в блоке 8, анализи- рует опции, передаваемые файлом svc. conf. Последний параметр ACE-Get-Opt равный 0 обеспечивает анализ опций, начиная с argv [ 0 ], а не с argv [ 1 ], по умолчанию. Если любые из опций «-р», «-г» или «-s», или их «длинных» эквивалентов, передаются в параметре argv init(), соответст- вующие номер порта или имя хоста соответственно изменяются. Строки 31-32 С известными номерами портов и именем хоста серверного демона регистрации, формируем адрес, необходимый для установления соеди- нений. Строки 34-37 Инициализируем acceptor-и connector-. Когда клиентский демон регистрации удаляется, АСЕ-каркас Service Configu- rator вызывает следующий hook-метод AC_Client_Logging_Daemon: :fi- ni(): int AC—Client—Logging—Daemon::fini () { return acceptor—.close (); } Метод fini () вызывает метод close (), унаследованный от AC_CLD_Ac- ceptor. Этот метод, в свою очередь, вызывает AC-CLD_Acceptor: :hand- le-dose (), чтобы завершить работу очереди сообщений и потока-ретранс- лятора. АСЕ-каркас Service Configurator удаляет экземпляр AC_Client_Log-
АСЕ-каркас Acceptor-Connector 299 Рис. 7.11 Последовательность завершения работы AC_Client_Logging_Daemon ging_Daemon после завершения f ini (). Эта последовательность завершения работы изображена на рис. 7.11. Теперь, когда мы реализовали все классы клиентского демона регистрации, МЫ можем ВКЛЮЧИТЬ макрос ACE_FACTORY_DEFINE.‘ ACE_FACTORY_DEFINE (AC_CLD, AC_Client_Logging_Daemon) Этот макрос автоматически задает функцию-фабрику _make_AC_Cli- ent_Logging_Daemon (), которая используется в следующем файле svc. conf: dynamic AC_Client_Logging_Daemon Service_Object * AC_CLD:_make_AC_Client_Logging_Daemon() "-p $CLIENT_LOGGING_DAEMON_PORT" Данный файл управляет АСЕ-каркасом Service Configurator при конфигу- рировании клиентского демона регистрации с помощью следующей последо- вательности действий: 1. Динамически подключает DLL AC_CLD к адресному пространству процесса. 2. Использует класс ACE_DLL, рассмотренный в блоке 33, чтобы найти функ- цию-фабрику _make_AC_Client_Logging_Daemon () в таблице имен DLL AC_CLD. Мы оставляем hook-методы suspend (), resume () и inf о () в качестве упражнения для читателя.
зоо Глава 7 3. Эта функция вызывается для получения указателя на динамически созда- ваемый AC_Client_Logging_Daemon. 4. Используя этот указатель, каркас затем вызывает AC_Client_Log- ging_Daemon : : init (), передавая в качестве его аргумента argc/argv строку -р, за которой следует расширение переменной окружения'CLI- ENT_LOGGING_DAEMON_PORT, обозначающей номер порта, на котором клиентский демон регистрации прослушивает запросы на соединение от клиентских приложений. 5. Если init () завершается успешно, указатель AC_Clien t_Logging_Da- emon сохраняется в ACE_Service_Repository как «AC_Client_Log- ging_Daemon». Теперь мы готовы перейти к функции main () из файла SR_Configu- rable_Logging_Server. срр. Она похожа на программу Configurab- le_Logging_Server. срр, используемую для других служб, но требует не- сколько иной тактики. В блоке 11 обсуждались отдельные проблемы, которые могут возникнуть, если обработчики событий не контролирует свой собствен- ный жизненный цикл. Служба AC_Client_Logging_Daemon вданной главе создается динамически ACE-каркасом Service Configurator при его активиза- ции. Однако его акцептор и обработчики служб участвует в работе службы в ка- честве объектов и, поэтому, не контролируют свой собственный жизненный цикл. Хотя в нашем проекте тщательно контролируются все действия, связан- ные с жизненным циклом обработчиков, в Windows проблемы все же могут появиться из-за заложенной в ACE_WFMO_Reactor логики отложенного осво- бождения ресурсов. Поэтому мы явным образом задаем синглтон ACE_Reac- tor как ACE_Select_Reactor. 1#include "асе/OS.h" 2#include "ace/Reactor.h" 3 #include "ace/Select_Reactor.h" 4 #include "ace/Service_Config.h" 5 6int ACE_TMAIN (int argc, ACE_TCHAR *argv[]) { 7 ACE_Select_Reactor *select_reactor; 8 ACE_NEW_RETURN (select_reactor, ACE_Select_Reactor, 1); 9 ACE_Reactor *reactor; 10 ACE_NEW_RETURN (reactor, ACE_Reactor (select_reactor, 1), 1); 11 ACE_Reactor::close_singleton () ; 12 ACE Reactor::instance (reactor, 1); 13 14 ACE_Service_Config::open (argc, .argv); 15 16 ACE_Reactor::instance ()->run_reactor_event_loop () ; 17 return 0; 18 )
АСЕ-каркас Acceptor-Connector 301 Строки 7-8 Примеры реализации реактора в главе 4, использовали автома- тическое создание нужных реакторных типов. В данном примере, тем не менее, реактор должен существовать пока ACE_Object_Manager завершает работу служб во время отключения. Чтобы это обеспечить, мы создаем реактор дина- мически. Третьим аргументом макроса ACE_NEW_RETURN является возвра- щаемое ma i п () значение, на случай появления ошибки в процессе размеще- ния. Строки 9-10 ACE_Reactor также создается динамически. ACE_Se- lect_Reactor передается в качестве используемой реализации. Второй аргу- мент конструктора ACE_Reactor сообщает новой реализации ACE_Reactor, что объект реализации нужно удалить при закрытии реактора. Строки 11-12 Закрываем существующий синглтон ACE_Reactor и заме- няем его новой реализацией, основанной на ACE_Select_Reactor. Второй аргумент ACE_Reactor: : instance () явно возвращает управление памя- тью ACE_Reactor механизму управления синглтоном ACE_Reactor. Такое решение гарантирует, что действия синглтона-реактора, связанные с заверше- нием работы, генерируемые ACE_Object_Manager, закроют реактор и осво- бодят динамически выделенную память после завершения работы служб. Строки 14-16 Как обычно, конфигурируем службы и запускаем цикл обра- ботки событий реактора. Примеры этой главы продемонстрировали несколько методов и стратегий ACE-каркаса Acceptor-Connector, обладающих большими возможностями: • Использование одного ACE_Svc_Handler для обработки нескольких сетевых соединений. • Использование нединамических обработчиков служб совместно с реак- тором. • Использование состояния PULSED очереди сообщений обработчика служб для взаимодействия с потоком службы обработчика. • Восстановление TCP-соединений сокета с использованием стратегии экс- поненциальной задержки. • Замена синглтона ACE_Reactor таким реактором, который АСЕ удаля- ет автоматически. Возможности клиентских и серверных демонов регистрации, приведенных в этой главе, могут, конечно, быть реализованы с помощью различных меха- низмов и протоколов. Благодаря тщательно продуманному дизайну каркасов АСЕ, тем не менее, ни один из различных способов перепроектирования этих демонов не потребует переписывать сетевой код, связанный с управлением бу- ферами, механизмами очередей, стратегиями параллелизма или методами де- мультиплексирования. Единственными изменениями являются изменения в реализации самих сетевых служб.
302 Глава 7 7.5 Резюме АСЕ-каркас Acceptor-Connector разделяет (1) стратегии установления со- единений и инициализации служб и (2) стратегии прикладной обработки. Та- кое разделение ответственности позволяет каждой совокупности стратегий раз- виваться независимо и содействует модульному дизайну сетевых приложений. АСЕ-каркас Acceptor-Connector факторизует стратегии установления соедине- ний и инициализации в шаблоны классов ACE_Acceptor и ACE_Connector, и стратегию прикладной обработки в шаблон класса ACE_Syc_Handler. ACE-каркасы Reactor, Service Configurator и Task, рассмотренные в преды- дущих главах, используют наследование классов и виртуальные методы в каче- стве основных механизмов расширения. АСЕ-каркас Acceptor-Connector тоже использует эти механизмы, в основном, как средство настройки стратегий уста- новления соединений, коммуникаций, параллелизма и функционирования служб. В отличие от каркасов предыдущих глав, однако, классы АСЕ-каркаса Acceptor-Connector совместно используют внутренние взаимосвязи сетевых прикладных служб, так что использование параметризованных типов играет здесь более существенную роль. Для того чтобы сделать возможной взаимо- связь между стратегиями и их комбинирование, ACE_Acceptor и АСЕ_Соп- nector включают класс производный от ACE_Svc_Handler в аргументы своих шаблонов в качестве целевых объектов фабрики соединений. В данной главе были определены и проиллюстрированы роли в коммуни- кациях и в установлении соединений, которые играют службы сетевых прило- жений, а также пассивный и активный режимы соединений, которые использу- ют службы, ориентированные на соединения. Хотя интерфейсные фасады АСЕ Socket, рассмотренные в главе 3 [C++NPvl] помогают преодолеть проблемы, связанные с С API операционных систем, в этой главе было показано, как АСЕ-каркас Acceptor-Connector содействует модульному разделению ролей, что приводит к структурам, которые просто расширять и сопровождать. Эти примеры показали, насколько просто определять обработчики служб приложе- ния путем определения класса(ов), производного отACE_Svc_Handler и до- бавления функциональности, зависящей от типа службы, в hook-методы и ме- тоды обратного вызова, унаследованные от ACE_Svc_Handler, ACE_Task и ACE_Event_Handler. Хотя АСЕ-каркас Acceptor-Connector инкапсулирует наиболее общие варианты создания служб, в данной главе показано как данный каркас может использовать паттерн Template Method [GoF], чтобы дать воз- можность разработчикам приложений настраивать каждый шаг создания службы, соответствующей требованиям, окружению и ресурсам конкретных сетевых приложений.
Глава 8 АСЕ-каркас Proactor Краткое содержание В данной главе приводится общая характеристика механизмов асинхрон- ного ввода/вывода, которые существуют на современных платформах попу- лярных ОС, а затем описывается структура и применение ACE-каркаса Proactor. Этот каркас является реализацией паттерна Proactor [POSA2], который позво- ляет событийно-управляемым приложениям эффективно осуществлять де- мультиплексирование и диспетчеризацию запросов на обслуживание, иниции- руемых завершением операций асинхронного ввода/вывода. В главе показано как усовершенствовать, с помощью активно-превентивной (proactive) модели, наш клиентский демон регистрации так, чтобы можно было (1) инициировать операции ввода/вывода, (2) демультиплексировать события окончания опера- ций ввода/вывода и (3) осуществлять, в ответ на события завершения, диспет- черизацию определяемых приложениями обработчиков завершения, обраба- тывающих результаты операций асинхронного ввода/вывода. 8.1 Обзор В главе 3 рассматривался АСЕ-каркас Reactor, который чаще всего исполь- зуется во взаимно-согласованной (reactive) модели ввода/вывода. Приложение, использующее эту модель, регистрирует объекты обработчиков событий, кото- рые уведомляются реактором при появлении возможности выполнить одну или несколько запланированных операций ввода/вывода, таких как прием дан- ных из сокета. Операции ввода/вывода часто выполняются одним потоком, управляемым циклом диспетчеризации событий реактора. Хотя взаимно-со- гласованный ввод/вывод является общепринятой программной моделью, каж- дый поток, в такой модели, может одновременно выполнять только одну опе- рацию ввода/вывода. Последовательный характер операций ввода/вывода мо- жет стать узким местом, так как приложения, передающие большие объемы данных по нескольким конечным точкам, не могут использовать предоставляв-
304 Глава 8 мую ОС возможность параллельного выполнения, а также несколько процессо- ров или несколько сетевых интерфейсов. Один из способов смягчить проблемы взаимно-согласованного ввода/вы- вода — использовать синхронный ввод/вывод в сочетании с многопоточной мо- делью, типа модели пула потоков (глава 6) или модели поток-на-соединение (глава 7). Многопоточность может помочь в параллельном выполнении опера- ций ввода/вывода приложения и повышении производительности. Однако включение в проект многопоточности требует использования соответствую- щих механизмов синхронизации, чтобы исключить опасности, связанные с па- раллелизмом, такие как состояния гонок (race conditions) и тупики (deadlocks) [Тап92]. А это, в свою очередь, требует экспертных знаний в области паралле- лизма и методов синхронизации. Кроме того, синхронизация усложняет разра- ботку и результирующий код, увеличивая риск неочевидных дефектов. Более того, многопоточность может привести к повышению затрат времени и памя- ти, которое связано с необходимостью выделения ресурсов для стеков времени выполнения, переключения контекстов [SS95b] и перемещения данных между кэшами процессоров [SKT96]. Активно-превентивная (proactive) модель ввода/вывода часто является бо- лее подходящей возможностью смягчения недостатков взаимно-согласованно- го ввода/вывода, без внесения сложностей и издержек, связанных с синхрон- ным вводом/выводом и многопоточностью. Эта модель позволяет приложе- нию выполнять операции ввода/вывода с помощью следующих двух этапов: 1. Приложение инициирует параллельно одну или несколько операций вво- да/вывода для нескольких дескрипторов ввода/вывода, не ожидая заверше- ния этих операций. 2. По завершении каждой операции ОС уведомляет определяемый приложе- нием обработчик завершения (completion handler), который затем обрабаты- вает результаты завершенных операций ввода/вывода. Два указанных этапа активно-превентивной модели ввода/вывода являют- ся, по существу, инверсией соответствующих этапов взаимно-согласованной модели ввода/вывода, в которой приложение выполняет следующие действия: 1. Использует демультиплексор событий для определения возможности вы- полнения операции ввода/вывода, которая, вероятно, будет выполнена не- медленно. 2. Синхронно выполняет указанную операцию. В дополнение к улучшению масштабируемости приложения за счет асин- хронности, активно-превентивная модель ввода/вывода может иметь и другие преимущества, в зависимости от реализации асинхронного ввода/вывода на данной платформе. Например, если несколько операций асинхронного вво- да/вывода могут быть инициированы одновременно и каждая операция содер- жит расширенную информацию, например, позицию в файле ввода/вывода, ОС может оптимизировать внутреннюю стратегию буферизации, чтобы избе- жать непроизводительного копирования данных. Она может также оптимизи- ровать производительность файлового ввода/вывода путем переупорядочения
АСЕ-каркас Proactor 305 операций с целью минимизации перемещений головок дисков и/или увеличе- ния числа попаданий в кэш. АСЕ-каркас Proactor упрощает разработку программ, использующих ак- тивно-превентивную модель ввода/вывода. В этом контексте, АСЕ-каркас Proactor отвечает за следующие действия: • Инициирование операций асинхронного ввода/вывода. • Хранение аргументов каждой операции и их передачу обработчику за- вершения. • Ожидание событий завершения, указывающих на то, что соответствую- щие операции завершены. • Демультиплексирование событий завершения по соответствующим об- работчикам завершений. • Диспетчеризацию hook-методов обработчиков с целью обработки собы- тий способом, определяемым приложением. Дополнительно к возможностям, связанным с вводом/выводом, АСЕ-кар- кас Proactor предлагает те же механизмы очередей таймеров, что и АСЕ-каркас Reactor в разделе 3.4. В данной главе рассматриваются следующие классы ACE-каркаса Proactor. Класс АСЕ Описание ACE__Hand 1е г Определяет интерфейс для получения результатов операций асинхронного ввода/вывода и обработки событий таймера. АСЕ—Asуnch—Read—Stream I ACE—Asynch—Write_Stream I ACE—Asynch—Result Инициирует операции асинхронного чтения (записи) из потока (в поток) ввода/вывода и связывает каждую такую операцию с объектом ACE_Handier, который принимает результаты этих операций. ACE—Asynch_Acceptor ACE—Asynch—Connector Реализация паттерна Acceptor-Connector, который асинхронно устанавливает новые соединения TCP/IP. ACE—Service_Handler Определяет целевой объект фабрик соединений ACE_Asynch_Acceptor и ACE_Asynch_connector и предоставляет 1 hook-методы для инициализации службы с установленным соединением TCP/IP. ACE—Proactor Управляет таймерами и демультиплексированием событий завершения асинхронного ввода/вывода. Этот класс аналогичен классу ACE_Reactor АСЕ-каркаса Reactor.
306 Глава 8 Наиболее важные взаимосвязи классов ACE-каркаса Proactor приведены на рис. 8.1. Эти классы, в соответствии с паттерном Proactor [POSA2], играют сле- дующие роли: • Классы уровня инфраструктуры асинхронного ввода/вывода реализу- ют стратегии, независимые от приложения, которые инициируют опера- ции асинхронного ввода/вывода, демультиплексируют события завер- шения обработчикам завершения, а затем осуществляют диспетчеризацию соответствующих hook-методов обработчиков завершения. Классы уровня инфраструктуры ACE-каркаса Proactor включают ACE_Asynch_Acc$ptor, ACE_Asynch_Connector, ACE_Asynch_Result, ACE_Asynqh_Re- ad_Stream, ACE_Asynch_Write_Stream и различные реализации ACE_Proactor. Уровень инфраструктуры использует также класс ACE_Time_Value и ACE-классы очередей таймеров из разделов 3.2 и 3.4. • Классы прикладного уровня включают обработчики завершения, кото- рые выполняют определяемую приложением обработку с помощью сво- их hook-методов. В ACE-каркасе Proactor эти классы являются производ- ными от ACE_Handler и/или ACE_Service_Handler. Большие возможности ACE-каркаса Proactor являются результатом разде- ления ответственности между его инфраструктурными и прикладными класса- ми. За счет отделения механизмов демультиплексирования и диспетчеризации от определяемых приложением стратегий обработки событий, АСЕ-каркас Pro- actor обеспечивает следующие преимущества: Рис. 8.1 Классы ACE-каркаса Proactor
АСЕ-каркас Proactor 307 • Улучшает переносимость. Приложения могут использовать преимуще- ства активно-превентивной модели ввода/вывода на платформах, реали- зующих различные механизмы асинхронного ввода/вывода. Асинхрон- ный ввод/вывод используется в Windows (требуется Windows NT вер- сии 4.0 и выше), там он называется вводом/выводом с перекрытием (overlapped I/O), и, в виде варианта Asynchronous I/O (АЮ) стандарта POSIX.4 Realtime Extension [POS95], на платформах, на которых этот стандарт реализован, включая HP-UX, IRIX, Linux, LynxOS и Solaris. • Автоматизирует обнаружение, демультиплексирование и диспетчери- зацию событий завершения. АСЕ-каркас Proactor изолирует внутренние API ОС, связанные с инициированием операций ввода/вывода и демуль- типлексированием событий завершения, а также поддержку таймера, в каркасных классах инфраструктурного уровня. Приложения могут ис- пользовать эти объектно-ориентированные механизмы для иницииро- вания асинхронных операций, нужных только для реализации опреде- ляемых приложениями обработчиков завершения. • Поддерживает прозрачную расширяемость. Как отмечалось в разделе 8.5, АСЕ-каркас Proactor использует паттерн Bridge [GoF] для того, чтобы экспортировать интерфейс с единообразным четко определенным пове- дением. Такой дизайн позволяет внутренним элементам каркаса изме- няться и приспосабливаться к изменяющимся реализациям асинхронно- го ввода/вывода, которые обеспечивают ОС, не требуя изменений на прикладном уровне. • Повышает повторное использование и минимизирует подверженные ошибкам детали программирования. Разделяя механизмы асинхронно- го ввода/вывода и определяемые приложением стратегии и поведение, АСЕ-каркас Proactor может быть повторно использован во многих, са- мых разных прикладных областях. • Обеспечивает потокобезопасность. Приложения могут использовать параллелизм ввода/вывода, обеспечиваемый платформами ОС, не ус- ложняя прикладной уровень стратегиями синхронизации. Если приклад- ной код инициирования операций и обработки событий завершения вы- полняется в одном потоке, то нужно всего лишь следовать простым пра- вилам доступа к данным, таким как «не используйте буфер, выделенный ОС под операцию ввода/вывода, пока эта операция не завершится». АСЕ-каркас Proactor является прозрачным (whitebox) каркасом, так как об- работчики событий сетевых приложений должны быть производными от ACE_Handler, также как и в ACE-каркасе Reactor. В следующих разделах приводится обоснование и описание возможностей каждого из классов ACE-каркаса Proactor. В них также показано как можно ис- пользовать этот каркас, чтобы реализовать асинхронный ввод/вывод в нашем клиентском демоне регистрации. Если вы не знакомы с паттерном Proactor из POSA2, мы советуем вам сначала познакомиться с этим паттерном, прежде чем погружаться в детали примеров этой главы.
308 Глава 8 8.2 Классы-фабрики асинхронного ввода/вывода Обоснование Активно-превентивную модель ввода/вывода, в общем случае, труднее программировать, чем взаимно-согласованную и синхронную модели, по сле- дующим причинам: • Операции ввЬда/вывода и события завершения — это дополнительная работа, которую нужно выполнять отдельно. • Несколько операций ввода/вывода могут быть инициированы одновре- менно, что требует хранения большого количества учетной информации. • При одновременном завершении нескольких операций ввода/вывода порядок завершения не гарантируется. • В многопоточной службе обработчик завершения может выполняться не тем потоком, который инициировал операцию ввода/вывода. Поэтому активно-превентивная модель ввода/вывода требует наличия фабрики, инициирующей операции асинхронного ввода/вывода. Так как мно- жество операций ввода/вывода может выполняться одновременно и завер- шаться в произвольном порядке, то активно-превентивная модель требует так- же явно связывать каждую асинхронную операцию, ее параметры (например, дескриптор ввода/вывода, буфер данных и его размер) и обработчик заверше- ния, который будет обрабатывать результаты этой операции. В теории, проектирование классов генерации асинхронных операций вво- да/вывода и их привязки к обработчикам завершения должно быть относитель- но простым. На практике, тем не менее, такая разработка осложняется тем фак- том, что асинхронный ввод/вывод на платформах известных ОС реализуется по-разному. Вот два самых простых примера: • Windows. Системные функции Windows ReadFile () и WriteFile () могут выполнять как синхронный ввод/вывод, так и инициировать асин- хронную (overlapped) операцию ввода/вывода. • POSIX. Функции POSIX aio_read() и aio_write() инициируют асинхронные операции чтения и записи соответственно. Эти функции отличаются от функций read () и write () (а также Sockets recv () и send ()), которые используются в классах интерфейсных фасадов АСЕ IPC (см. главу 3 [С+4-NPvl]). Средства асинхронного ввода/вывода на каждой платформе включают так- же собственный механизм привязки операции ввода/вывода к ее параметрам, таким как указатель на буфер и размер блока передачи данных. Например, в POSIX АЮ есть АЮ блок управления (aiocb), тогда как Windows предлагает структуру OVERLAPPED и аргумент ключа завершения (completion key argu- ment), передаваемый в порт завершения (completion port) устройства ввода/вы- вода. В блоке 54 рассматриваются и другие проблемы механизмов асинхронно- го ввода/вывода ОС.
АСЕ-каркас Proactor 309 Блок 54: Переносимость асинхронного ввода/вывода В отличие от механизмов синхронного и взаимно-согласованного ввода/выво- да, которые имеются в большинстве современных операционных систем, асинхронный ввод/вывод не так распространен. Из платформ ОС, поддержи- ваемых АСЕ, механизмы асинхронного ввода/вывода обеспечивают следую- щие платформы: • Платформы Windows поддерживают и асинхронный (overlapped) I/O, и пор- ты завершения (completion ports) I/O (Ric97). Асинхронный (overlapped) I/O является эффективным и масштабируемым механизмом ввода/вывода в Win- dows (Sol98). Windows осуществляет демультиплексирование событий за- вершения с помощью портов завершения I/O и дескрипторов событий (event handles). Порт завершения I/O представляет собой очередь, управ- ляемую ядром Windows, предназначенную для буферизации событий за- вершения ввода/вывода. • Платформы POSIX реализуют спецификацию POSIX.4 AIO (POS95). Эта спе- цификация сначала предназначалась для файлового ввода/вывода на диске (Оа195), но может также, с большим или меньшим успехом, использоваться для сетевого ввода/вывода. Поток приложения может ожидать наступления событий завершения с помощью aio_susoer.d () или уведомляться с помо- щью сигналов реального времени (real-time signals), что трудно интегриро- вать в событийно-управляемое приложение. Вообще, чтобы правильно и эф- 7 фективно программировать активно-превентивную (proactive) модель в PO- SIX.4 AIO, требуется особое внимание. Несмотря на обычную для UNIX взаимозаменяемость системных функций ввода/вывода для разных меха- низмов IPC, интеграция средств POSIX AIO с другими механизмами IPC, таки- ми как Socket API, на некоторых платформах оставляет желать лучшего. На- пример, функции Socket API, такие как connect () и accept () не интегриру- ются в модель POSIX АЮ, а некоторые реализации АЮ не всегда могут работать с несколькими отложенными операциями на одном дескрипторе. Характеристики производительности асинхронного ввода/вывода также мо- гут изменяться в большом диапазоне. Например, некоторые операционные системы реализуют функции POSIX АЮ путем создания потока для каждой опе- рации асинхронного ввода/вывода. Хотя такая реализация является совмес- тимой (compliant), она не дает выигрыша производительности по сравнению с тем случаем, когда приложение само создает поток. Фактически, такая реа- лизация может, скорее, привести к снижению производительности приложе- ний с интенсивным вводом/выводом, чем к ее улучшению! Будем надеяться, что реализации асинхронного ввода/вывода в ОС будут в ближайшем будущем усовершенствованы так, чтобы можно было более свободно использовать этот механизм, обладающий большими возможностями. Все обсуждавшиеся выше механизмы асинхронного ввода/вывода исполь- зуют дескриптор ввода/вывода (I/O handle) для ссылок на канал IPC или файл, участвующих в операциях ввода/вывода. АСЕ-каркас Proactor определяет сово- купность классов, которые инициируют асинхронный ввод/вывод для разных механизмов IPC. Эти классы улучшают переносимость, минимизируют слож- ность и исключают возможность появления проблем, связанных с дескрипто- рами ввода/вывода, которые были решены в [C++NPvl]. В данной книге рас- сматриваются два наиболее используемых в сетевых приложениях класса: ACE_Asynch_Read_Stream и ACE_Asynch_Write_Stream.
310 Глава 8 Функциональные возможности классов ACE_Asynch_Read_Stream и ACE_Asynch_Write_Stream являются классами-фабриками, которые позволяют приложениям переносимым обра- зом инициировать асинхронные операции read () и write () соответственно. Данные классы обеспечивают следующие функциональные возможности: • Могут инициировать операции асинхронного ввода/вывода для пото- ко-ориентированных механизмов IPC, таких как ТСР-сокет. • Осуществляют привязку дескриптора ввода/вывода, объекта ACE_Hand- ler и ACE_Proactor с целью корректной и эффективной обработки события завершения ввода/вывода. • Создают объект, который отвечает за передачу параметров операции внутри ACE-каркаса Proactor ее обработчику завершения. • Являются производными от ACE_Asynch_Operation, что обеспечива- ет интерфейс для инициализации этого объекта и для запроса на завер- шение незаконченных операций ввода/вывода. ACE_Asynch_Read_Stream и ACE_Asynch_Write_Stream определя- ют вложенные классы Result, представляющие связь между операцией и ее параметрами. АСЕ-каркас Proactor обобщает'поведение, ориентированное на результаты, в виде класса ACE_Asynch_Result. Вложенные классы Result являются производными от этого класса. Вся эта совокупность классов вместе представляет собой обработчик завершения со следующими возможностями: • Он может получать исходные параметры для операции ввода/вывода, та- кие как счетчик передаваемых байтов и адрес памяти. • Он может определять, как завершилась соответствующая операция: ус- пешно или с ошибкой. • Ему может быть передан асинхронный маркер завершения (asynchronous completion token, ACT) [POSA2], который предоставляет возможность рас- ширения объема и типа информации, передаваемой между инициато- ром операции и обработчиком завершения. Интерфейсы для классов ACE_Asynch_Result, ACE_Asynch_Re- ad_Stream, ACE_Asynch_Write_Stream, а также вложенных в них классов Result, приведены на рис. 8.2. В следующей таблице приведен список основ- ных методов ACE_Asynch_Read_Stream: Метод Описание | I open () Инициализирует объект с целью подготовки инициирования асинхронных операций read (). cancel () Пытается завершить незаконченные операции read (), инициированные с помощью данного объекта. read () 1 Инициирует асинхронную операцию чтения из ассоциированного потока IPC. |
АСЕ-каркас Proactor 311 Основные методы класса ACE_Asynch_Write_Stream приведены в сле- дующей таблице: Рис. 8.2 Взаимосвязи ACE-классов потоков (stream) асинхронных чтения/записи Методы open () связывают объект-фабрику асинхронного ввода/вывода со следующими объектами:
312 Глава 8 • Дескриптор, который используется для инициирования операций вво- да/вывода. • Объект ACE_Proactor, который обнаруживает и демультиплексирует события завершения для указанных операций ввода/вывода. • Объект ACE_Handler, который обрабатывает события завершения вво- да/вывода. Параметр act представляет собой асинхронный маркер завершения, кото- рый ассоциируется с каждой операцией ввода/вывода, исходящей от объек- та-фабрики ввода/вывода. После завершения этой операции, параметр act мо- жет быть получен с помощью метода ACE_Asynch_Result act (). Однако такая возможность существует только в Windows, поэтому в этой книге мы ее не используем. Методы read () и write () используют блок ACE_Message_Block при приеме и передаче, соответственно, обеспечивая таким образом следующие преимущества: • Упрощенное управление буфером. Поскольку начало и завершение асинхронного ввода/вывода обрабатывается в отдельных классах, ин- формация, касающаяся адресов буферов, доступной и используемой па- мяти должна быть ассоциирована с каждой операцией ввода/вывода. По- вторное использование ACE_Message_Block решает эти задачи пере- носимо и эффективно. • Автоматическое обновление счетчика передачи. Метод write() пере- дает байты, начиная с указателя-чтения (read pointer) блока данных, то есть читает байты за пределами сообщения. Напротив, метод г ead () чи- тает блоки данных, начиная с указателя-записи (write pointer), то есть за- писывает новые байты в сообщение. После успешного завершения опе- рации ввода/вывода, указатель-чтения и указатель-записи блока данных обновляются, чтобы отразить количество успешно переданных байтов. Таким образом, приложениям не нужно регулировать указатели буфера с данными или его размер, поскольку АСЕ-каркас Proactor делает это ав- томатически. В блоке 55 показано как АСЕ-каркас Proactor управляет ука- зателями ACE_Message_Block. • Естественная интеграция с другими ACE-каркасами. ACE_Messa- ge_Block предоставляет удобный механизм получения и передачи дан- ных для дальнейшей обработки в ACE-каркасах Task (глава 6), Accep- tor-Connector (глава 7) и Streams (глава 9). В отличие от интерфейсных фасадов АСЕ IPC, таких как рассмотренный в [C++NPvl] ACE_SOCK_Stream, ACE_Asynch_Read_Stream и АСЕ_ Asynch_Write_Stream не инкапсулируют никаких базовых механизмов IPC. Вместо этого, они определяют интерфейс для начала операций асинхрон- ного ввода/вывода. Такое решение дает следующие преимущества: • Дает возможность использовать повторно классы интерфейсных фаса- дов АСЕ IPC, таких как ACE_SOCK_Stream и ACE_SPIPE_Stream,
АСЕ-каркас Proactor 313 в ACE-каркасе Proactor и избегать повторного создания параллельного набора классов IPC, используемых только в каркасе Proactor. • , Задает структуру, предоставляющую только дескрипторы инициирован- ных операций ввода/вывода, чтобы избежать неправильного использо- вания дескрипторов ввода/вывода. • Упрощает использование одних и тех же классов IPC для синхронного и асинхронного ввода/вывода, передавая дескриптор ввода/вывода фаб- рике асинхронной операции. Таким образом, сетевые приложения, написанные с использованием АСЕ, могут использовать любые комбинации синхронного, взаимно-согласованно- го (reactive) и активно-превентивного (proactive) ввода/вывода. Решение, ка- сающиеся того, какой механизм ввода/вывода использовать, может быть при- нято во время компиляции или, если нужно, во время выполнения. Фактически решение может быть отложено до создания объекта IPC! Например, приложе- ние может принять решение какой механизм использовать после установления соединения сокетов [HPS97]. АСЕ-каркас Proactor включает также классы-фабрики для инициирования операций дейтаграммного ввода/вывода (ACE_Asynch_Read_Dgram и АСЕ_ Asynch_Write_Dgram) и операций файлового ввода/вывода (ACE_Asynch_Re- ad_File, ACE_Asynch_Write_File и ACE_Asynch_Transmit_File). Структура и возможности этих классов похожи на уже рассмотренные в данной главе. За подробностями обращайтесь, пожалуйста, к онлайновой документа- ции по АСЕ: http: //асе .есе. uci. edu/Doxygen/ или http: //www. ri- verace . com/docs/. Пример В данной главе, используя АСЕ-каркас Proactor, мы изменим клиентский демон регистрации из главы 7. Хотя классы, используемые в активно-превен- тивном (proactive) клиентском демоне службы регистрации, похожи на классы версии Acceptor-Connector, активно-превентивная версия использует один по- ток приложения для инициирования и обработки завершений всех его опера- ций ввода/вывода. Классы, составляющие клиентский демон регистрации на основе АСЕ-кар- каса Proactot, приведены на рис. 8.3. Функции каждого класса перечислены ниже: Класс Описание AIO_Output_Handler Ретранслирует сообщения, инициирует асинхронные операции чтения для передачи сообщений серверу регистрации. AIO_CLD_Connector Фабрика, которая активно устанавливает (восстанавливает) и аутентифицирует соединения с сервером регистрации и активизирует объект AIO_jDutput_Handler.
314 Глава8 Класс Описание AIO__Input_Handler Обрабатывает данные регистрационных записей от клиентов с помощью асинхронной операции read () и передает заполненные регистрационные записи Aio_Output_Handier для обработки на выходе. AIO_CLD_Acceptor Фабрика, которая принимает соединения от клиентов регистрации и создает для каждого новый AIO__Input__Handler. AIO_Client_Logging_ Daemon Фасадный класс, который объединяет остальные классы в единое целое. Взаимосвязи между экземплярами этих классов приведены на рис. 8.4. В данном примере мы начнем с описания класса AIO_Output_Handler, а дру- гие классы представим в следующих разделах данной главы. Исходный код этого примера находится в файле AIO_Client_Log- ging_Daemon. срр. Класс AIO_Output_Handler передает регистрационные записи серверному демону регистрации. Вот часть определения этого класса: class AIO_Output_Handler : public ACE_Task<ACE_NULL_SYNCH>, public ACE_Service_Handler ( AIO_Output_Handler наследует от ACE_Task, чтобы использовать его ACE_Message_Queue. Очередь не синхронизируется, так как всю работу в дан- ной службе выполняет один поток приложения. Так как AIO_Output_Hand- ler является производным от ACE_Service_Handler, то он может обраба- тывать события завершения (раздел 8.3) и служить целевым объектом фабри- ки асинхронных соединений AIO_CLD_Connector (рассматриваемой в раз- деле 8.4). public: AIO_Output_Handler () : can_write_ (0.) {} virtual ~AIO_Output_Handler (); // Точка входа в <AIO_Output_Handler>. virtual int put (ACE_Message_Block *, ACE_Time_Value * = 0); // Ноок-метод, вызываемый при установлении соединения // с сервером. virtual void open (ACE_HANDLE new_handle, ACE_Message_Block &message_block); protected: ACE_Asynch_Read_Stream reader_; // Обнаруживает разрыв // соединения. ACE_Asynch_Write_Stream wri'ter_; // Посылает записи серверу.
АСЕ-каркас Proactor 315 int can_write_; // Можно начать передачу per.записи? 11 Инициирует передачу per.записи. void start write (ACE_Message_Block *mblk = 0) ; typedef ACE_Unmanaged_Singleton<AIO_Output_Handler, ACE_Null_Mutex> OUTPUT_HANDLER; AIO_Output_Handler включает ACE_Asynch_Read_Stream, для оп- ределения момента закрытия серверного соединения. Он также включает ACE_Asynch_Write_Stream, чтобы инициировать асинхронные операции write (), которые передают регистрационные записи серверному демону ре- гистрации. Рис. 8.3 Классы активно-превентивного клиентского демона регистрации
316 Глава 8 Рис. 84 Взаимосвязи в активно-превентивном клиентском демоне регистрации Так как существует одно TCP соединение с серверным демоном регистра- ции, то мы используем один AlO_Output_Handler для передачи регистраци- онных записей серверному демону регистрации. Поэтому мы определяем OUTPUT_HANDLER как ACE_Unmanaged_S ingle ton. Он должен быть неза- висимым funmanaged), чтобы гарантировать, что мы контролируем его жиз- ненный цикл, так как сама служба может быть остановлена и отключена до за- вершения работы программы. Классом характеристик (traits) синхронизации является ACE_Null_Mutex, поскольку все обращения к синглтону выполня- ются одним потоком. После установления соединения с серверным демоном регистрации, АСЕ-каркас Proactor передает управление следующему hook-методу open (): 1 void AIO_Output_Handler::open 2 (ACE_HANDLE new_handle, ACE_Message_Block &) { 3 ACE_SOCK_Stream temp_peer (new_handle); 4 int bufsiz = ACE_DEFAULT_MAX_SOCKET_BUFSIZ; 5 temp_peer.set_option (SOL_SOCKET, SO_SNDBUF, 6 &bufsiz, sizeof bufsiz); 7 8 reader_.open (*this, new_handle, 0, proactor () ); 9 writer_.open (*this, new_handle, 0, proactor ()); 10 11 ACE_Message_Block *mb; 12 ACE_NEW (mb, ACE_Message_Block (1) ) ; 13 reader_.read (*mb, 1); 14 ACE_Sig_Action no_sigpipe ((ACE_SignalHandler) SIG_IGN);
АСЕ-каркас Proactor 317 15 no_sigpipe.register_action (SIGPIPE, 0); 16 can_write_ = 1; 17 start write (0); 18 ) Строки 3-6 Увеличиваем пересылочный буфер нового сокета до его макси- мального размера, чтобы максимально увеличить скорость передачи данных по .сетям с большой задержкой и/или с высокой скоростью передачи. Мы ис- пользуем временный объект ACE_SOCK_Stream, чтобы устанавливать буфер с учетом типовой безопасности. Строки8-9 Инициализируем объекты reader_ и writer_. Каждый зада- ет объект this в качестве обработчика завершения, дескриптор нового сокета для выполнения операций и тот же ACEJProact or, который используется при открытии соединения. Маркер завершения (ACT) не используется, так что тре- тий аргумент равен 0. После инициализации, reader_nwriter_ использу- ются для инициирования операций асинхронного ввода/вывода. Строки 11-13 Клиентский демон регистрации в примере раздела 7.4. регист- рировал серверный сокет у реактора для обработки событий ввода. В актив- но-превентивной модели мы инициируем асинхронную операцию read () для чтения одного байта. Завершение этой операции должно означать: (1) сервер послал данные (нарушение используемого протокола), (2) сервер закрыл ко- нечную точку сокета. Строки 14-15 Как и в других примерах, которые используют событие READ с целью обнаружения того, что сокет закрыт, игнорируем сигнал SIGPIPE так, Чтобы асинхронные операции write () не привели к аварийному завершению программы в том случае, если соединение будет закрыто. Строки 16-17 Чтобы избежать чередования регистрационных записей, свя- занного с частичными выборками, мы передаем только одну регистрационную запись в каждый данный момент времени. Флаг can_write_ показывает; можно начать запись новых регистрационных данных или нет. Так как данное соединение только что открыто, то запись начать можно, поэтому, мы устанав- ливаем флаг, и затем вызываем следующий метод start_write (), чтобы на- чать асинхронную операцию write (): 1 void AIO_Output_Handler::start write 2 (ACE_Message_Block *mblk) { 3 if (mblk == 0) { 4 ACE_Time_Value nonblock (0); 5 getq (mblk, &nonblock) ; 6 > 7 if (mblk != 0) { 8 can_write_ = 0; 9 if (writer_.write (*mblk, mblk->length () ) == -1). 10 ungetq (mblk); 11 ) 12 )
318 Глава 8 Строки 1-6 Метод start_write () может быть вызван с одним из двух аргументов: • Указатель NULL, который означает, что первое сообщение в очереди должно быть отправлено и исключено из очереди. • Не-NULL ACE_Message_Block означает необходимость немедленно начать запись. Строки 7-11 Если регистрационных записей для передачи нет, мы просто возвращаем управление, ничего не делая. Если же записи есть, мы сбрасываем can_write_ в О, чтобы не допустить передачу следующих блоков данных, пока не закончена передача текущего блока.. Объект writer_ инициирует асинхронную операцию wr ite () для передачи блока данных. Если в процессе запуска write () возникает ошибка, блок данных возвращается в очередь в предположении, что сокет закрыт. Стратегия восстановления соединений во- зобновит асинхронную операцию write (), как только соединение будет вос- становлено. При поступлении регистрационных записей от клиентов, они передаются в AIO_Output_Handler с помощью следующего метода put (), реализация которого взята из ACE_Task: int AIO_Output_Handler::put (ACE_Message_Block *mb, ACE_Time_Value *timeout) { if (can_write_) { start_write (mb); return 0; ) return putq (mb, timeout); ) Если операций write (), которые находятся в процессе выполнения, нет, вызываем start_write (), чтобы немедленно начать передачу заданного блока ACE_Message_Block. Если операция write () не может быть начата в данный момент, мы на время ставим сообщение в очередь. 8.3 Класс ACE_Handler Обоснование Основным различием между активно-превентивной и взаимно-согласо- ванной моделями ввода/вывода является то, что в активно-превентивном вводе начало и завершение являются отдельными этапами, которые выполняются от- дельно. Более того, эти два этапа могут выполняться в разных потоках управле- ния. Использование отдельных классов для обработки начала и завершения операций ввода/вывода позволяет устранить лишние связи между двумя этими этапами. В разделе 8.2 были рассмотрены классы ACE_Asynch_Read_Stream и ACE_Asynch_Write_St ream, используемые для инициирования операций асинхронного ввода/вывода. Материал данного раздела касается обработки за- вершения операций ввода/вывода. События завершения означают, что ранее инициированная операция вво- да/вывода завершена. Чтобы правильно и эффективно обработать результат
АСЕ-каркас Proactor 319 операции ввода/вывода, обработчику завершения, помимо самого результата, должны быть известны те аргументы, с которыми выполнялась данная опера- ция ввода/вывода. Эта информация, в совокупности, включает следующие пункты: • Какого типа операция была инициирована? • Была ли операция успешно завершена? • Код ошибки, в случае неудачного завершения. • Дескриптор ввода/вывода, идентифицирующий конечную точку соеди- нения. • Адрес памяти передаваемых данных. • Количество запрошенных и реально переданных байтов. Обработка завершения операций асинхронного ввода/вывода требует больше информации, чем доступно для методов обратного вызова в АСЕ-карка- се Reactor. Следовательно, класс ACE_Event_Handler, представленный в раз- деле 3.3, не подходит для ACE-каркаса Proactor. Так как обработка завершений зависит также от механизма асинхронного ввода/вывода, реализуемого базо- вой платформой ОС, для нее характерны те же вопросы переносимости, кото- рые рассматривались в разделе 8.2. Решать эти вопросы в каждом приложении излишне утомительно и дорого, вот почему АСЕ-каркас Proactor определяет класс ACE_Handler. Функциональные возможности класса ACE_Handler является базовым классом для всех обработчиков асин- хронного завершения в ACE-каркасе Proactor. Этот класс обеспечивает следую- щие функциональные возможности: • Предоставляет hook-методы для обработки завершения всех операций асинхронного ввода/вывода, определенных в АСЕ, включая установле- ние соединений и операции ввода/вывода из потока и в поток IPC. • Предоставляет hook-методы для обработки событий таймера. Интерфейс ACE_Handler приведен на рис. 8.5, а его основные методы пе- речислены в следующей таблице: Метод Описание handle() Получает дескриптор, используемый данным объектом. handle__read_stream() Ноок-метод, вызываемый по завершении операции read (), инициированной AC E_As уn ch_Re a d_S t ге am. handle_write_stream() Ноок-метод, вызываемый по завершении операции write(), инициированной ACE_Asynch_Write_Streaip. handle_time_out() Ноок-метод, вызываемый по истечении срока срабатывания таймера, установленного ACE_Proactor.
320 Глава 8 АСЕ_Нлпс11ег # proactor_ : ACE_Proactor * + handle () : ACE_HANDLE + handle_read_stream (result : const ACE_Asynch_Read_Stream::Result &) + handle_write_stream (result : const ACE_Asynch_Write_Stream::Result &) + handle_time_out (tv : const ACE_Time_Value &, act : const void *) + handle_accept (result : const ACE_Asynch_Accept::Result &) + handle_connect (result : const ACE_Asynch_Connect::Result &) Рис. 8.5 Класс ACE Handler Метод handle_time_out () вызывается тогда, когда истекает срок тайме- ра, установленный посредством ACE_Proactor. Его аргумент tv определяет абсолютное время суток, на которое установлен таймер. Фактическое время мо- жет отличаться. Это зависит от уровня активности и задержки, вносимой диспет- черизируемыми обработчиками. Заметьте, что эта ситуация несколько отличает- ся от той, когда ACE_Time_Value, которое представляет собой фактическое время суток, передается в ACE_Event_Handler: : handle_timeout () при передаче управления ACE-каркасом Proactor hook-методу обработки событий таймера. Hook-методы handle_read_stream() и handle_write_stream () вызываются со ссылкой на объект Result, связанный с завершенной асин- хронной операцией. Наиболее полезные методы объекта Result, которые дос- тупны из handle_read_stream () и handle_write_stream (), приведены в соответствующем контексте на рис. 8.2 и перечислены в следующей таблице: | Метод Описание I success () Указывает, была ли асинхронная операция успешно . завершена. handle () Получает дескриптор ввода/вывода, используемый данной асинхронной операцией ввода/вывода. message__block () Получает ссылку на ACE_Message_Block, используемый в данной операции. bytes_transferred() Указывает, сколько байтов было действительно передано данной асинхронной операцией. bytes_to_re^d () Указывает, сколько байтов было затребовано у асинхронной операции read (). bytes_to_write() Указывает, сколько байтов было затребовано у асинхронной операции write ().
АСЕ-каркас Proactor 321 Пример Все предыдущие демоны регистрации, и клиентские и серверные, исполь- зовали класс Logging_Handler, разработанный в главе 4 [C++NPvl] для по- лучения регистрационных записей от клиентов. Метод Logging_Hand- ler: :recv_log_record() использует для получения регистрационной запи- си операции синхронного ввода. Операции синхронного ввода относительно просто программировать, поскольку запись активизации (activation record) сте- ка времени выполнения принимающего потока может быть использована для хранения учетной информации и фрагментов данных. В отличие от этого, обработку асинхронного ввода сложнее программиро- вать, так как учетной информацией и фрагментами данных приходится управ- лять в явном виде, а не через стек времени выполнения. В данном примере, но- вый класс AIO_Input_Handler принимает регистрационные записи от кли- ентов путем инициирования асинхронных операций read() и сборки фрагментов данных в регистрационные записи, которые затем направляются серверному демону регистрации. Данный класс использует активно-превен- тивную модель ввода/вывода и асинхронные операции ввода с целью достиже- ния максимального параллелизма всех клиентов регистрации, используя один поток управления. Как видно из рис. 8.3, класс AIO_Input_Handler является производным от ACE_Handler (в разделе 8.4 рассматривается производный от ACE_Hand- ler ACE_Service_Handler) и определяется следующим образом: class AIO_Input_Handler : public ACE_Service_Handler { public: AIO_Input_Handler (AIO_CLD_Acceptor *acc = 0) : acceptor_ (ace), mblk_ (0) {} virtual ~AIO_Input_Handler (); // Вызывается <ACE_Asynch_Acceptor> при установлении // соединения с клиентом. virtual void open (ACE_HANDLE new_handle, ACE_Message_Block &message_block); protected: enum { LOG_HEADER_SIZE = 8 ); // Длина CDR-заголовка. AIO_CLD_Acceptor *acceptor_; 11 Наш акцептор. ACE_Message_Block *mblk_; // Блок для получения // per.записи. ACE_Asynch_Read_Stream reader_;// Фабрика асинхронной read(). // Обрабатывает ввод от клиентов. virtual void handle_read_stream (const ACE_Asynch_Read_Stream::Result &result); ); И Программирование сетевых приложений на C++. Том 2
322 Глава 8 Клиент регистрации отправляет каждую регистрационную запись в CDR- формате, который начинается с заголовка фиксированного размера (в разде- ле 4.4.2 [C++NPvl] представлены все детали CDR-маршалинга и формата реги- страционных записей). Заголовок содержит ACE_CDR: : Boolean для указания порядка байтов и ACE_CDR:ULong, размер полезных данных, следующих за заголовком. Из правил CDR-кодирования и выравнивания известно, что эти данные занимают 8 байтов, поэтому мы задаем константу перечисления LOG_HEADER_SIZE равной 8. Когда клиент регистрации устанавливает соединение с клиентским демо- ном регистрации, АСЕ-каркас Proactor передает управление следующему hook-методу open (): void AIO_Input_Handler::open (ACE_HANDLE new_handle, ACE_Message_Block &) { reader_.open (*this, new_handle, 0, proactor ()); ACE_NEW_NORETURN (mblk_, ACE_Message_Block (ACE_DEFAULT_CDR_BUFSIZE)); ACE_CDR::mb_align (mblk_); reader_.read (*mblk_, LOG_HEADER_SIZE); Данный метоД выделяет память под ACE_Message_Block для получения заголовка регистрационной записи от клиента. Во многих случаях выделяемого объема памяти достаточно не только для заголовка, но и для всей записи. Более того, при необходимости, размер блока может быть изменен. Указатель-записи (write pointer) данного блока устанавливается с учетом CDR-демаршалинга, и затем передается асинхронной операции read (), инициированной для прие- ма заголовка. После завершения операции read (), АСЕ-каркас Proactor вызывает сле- дующий метод обработчика завершения: 1 void AIO_Input_Handler::handle_read_stteam 2 (const ACE_Asynch_Read_Stream::Result &result) { 3 if (!result.success () II result.bytes_transferred () ==0) 4 delete this; 5 else if (result.bytes_transferred() < result.bytes_to_read()) 6 reader_.read (*mblk_, result.bytes_to_read () - 7 result.bytes_transferred ()); 8 else if (mblk_->length () == LOG_HEADER_SIZE) { 9 ACE_InputCDR cdr (mblk_); 10 11 ACE_CDR::Boolean byte_order; 12 cdr » ACE^InputCDR::to_boolean (byte_order); 13 cdr.reset_byte_order (byte_order); 14 15 ACE_CDR::ULong length; 16 cdr » length; 17
АСЕ-каркас Proactor 323 18 mblk_->size (length + LOG_HEADER_SIZE); 19 reader_.read (*mblk_, length); 20 } 21 else { 22 if (OUTPUT_HANDLER::instance ()->put (mblk_) == -1) 23 mblk ->release (); 24 25 ACE_NEW_NORETURN 26 (mblk_, ACE_Message_Block (ACE_DEFAULT_CDR_BUFSIZE)); 27 ACE_CDR::mb_align (mblk_); 28 reader_.read (*mblk_, LOG_HEADER_SIZE); 29 } 30 ) Строки 3-4 Удаляем объект this, если в read () возникает ошибка или одноранговый клиент регистрации закрыл соединение. Деструктор А1О_1п- put_Handler освобождает ресурсы, чтобы предотвратить их утечку. Строки 5-7 Если было получено меньше байтов, чем затребовано, иниции- руем еще одну асинхронную операцию read () для получения недостающих байтов. Нам не нужно устанавливать никаких указателей при запросе дополни- тельных данных этого блока. Как отмечается в блоке 55, АСЕ-каркас Proactor ав- томатически управляет указателями на ACE_Message_Block. Строки 8-19 Если все затребованные данные были получены и их размер совпадает с размером заголовка регистрационной записи, то мы получили только заголовок, а не сами данные. Такая проверка достаточно надежна, так как данные регистрационной записи всегда больше заголовка. Затем мы ис- пользуем класс ACE_InputCDR для демаршалинга заголовка и получения ко- личества полезных байтов данной записи. Размер mblk_ изменяется так, чтобы он соответствовал оставшимся данным, а для получения данных инициируется асинхронная операция read (). Этот метод будет вызываться снова и снова до завершения передачи и, если нужно, он будет продолжать инициировать асин- хронные операции чтения до тех пор, пока не будет получена вся запись или пока не возникнет ошибка. Строки 22-23 После получения всей регистрационной записи, она переда- ется дальше с помощью метода AlO_Output_Handler: : put (). После пере- дачи регистрационной записи, AIO_Output_Handler освобождает блок дан- ных. Если запись не может быть передана дальше, по каким-то причинам, то па- мять, выделенная под данные сразу освобождается, а регистрационная запись удаляется. Строки 25-28 Создаем новый объект ACE_Message_Block для следую- щей регистрационной записи и инициируем асинхронную операцию read () для получения ее заголовка. Метод AIO_Output_Handler: : start_write () инициирует асин- хронную операцию write () для передачи регистрационной записи серверно- му демону регистрации. Когда write () завершается, АСЕ-каркас Proactor вы- зывает следующий метод: и*
324 Глава 8 Блок 55: Управление указателями на ACE Message Block При инициировании асинхронных операций read() или write о в запросе должен задаваться ACE_Message_Block, и при приеме и при передаче дан- ных. Механизм обработки завершения ACE-каркаса Proactor обновляет ука- затели на ACE_wessage_Biock, чтобы отразить количество прочитанных или за- писанных данных, следующим образом: Операция Использование указателей ‘ : Read Исходным указателем на буфер считывания является wr_ptr () i сообщения. По завершении операции wr_ptr смещается ! вперед на количество прочитанных байтов. i Write Исходным указателем на буфер записи является rd_ptr () ; сообщения. По завершении операции rd_ptr смещается вперед на количество записанных байтов. Может показаться противоестественным, использовать указатель записи для чтения и указатель чтения для записи. Это проще понять, если учесть, что при чтении данные записываются в блок данных. Аналогично, при записи данных, они читаются из блока данных. По завершении, обновленный объем данных ACE_Message_Block становится больше при чтении (поскольку указатель-за- писи смещается вперед) и меньше при записи (поскольку смещается вперед указатель-чтения). 1 void AIO_Output_Handler::handle_write_stream 2 (const ACE_Asynch_Write_Stream::Result &result) { 3 ACE_Message_Block &mblk = result.message_block (); 4 if (!result.success ()) { 5 mblk.rd_ptr (mblk.base ()); 6 ungetq (&mblk); 7 ) 8 else ( 9 can_write_ = handle () == result.handle (); 10 if (mblk.length () == 0) { 11 mblk.release (); 12 if (can_write_) start_write (); 13 } 14 else if (can_write_) start_write (&mblk); 15 else ( mblk.rd_ptr (mblk.base ()); ungetq (&mblk); ) 16 } 17 } Строки 4-7 Если при выполнении операции write () возникает ошибка, восстанавливаем rd_ptr () блока данных на его начало. Мы полагаем, что, если в методе write () возникает ошибка, то сокет закрывается и позже соеди- нение устанавливается снова. В этом случае блок данных будет передан повтор- но с начала. Строка 9 Отменяем запрет, восстанавливая can_write_, если только со- кет не был закрыт. Если сокет закрывается пока асинхронные операции
АСЕ-каркас Proactor 325 write () ждут очереди или находятся в процессе выполнения, то нет гарантии, что ОС будет обрабатывать завершения надлежащим образом. Поэтому, мы контролируем дескриптор, который устанавливается в ACE_INVALID_HAND- LE при обработке закрытия сокета. Строки 10-13 Если был записан весь блок, освобождаем его. Если нужна еще одна write (), инициируем ее. Строка 14 Операция write () завершилась успешно, но была передана только часть сообщения. Если сокет остается открытым, инициируем асин- хронную операцию write () для передачи оставшейся части сообщения. Строка 15 Блок данных был частично записан, но сокет был закрыт, поэто- му возвращаем rd_ptr () сообщения в начало блока и ставим его обратно в очередь сообщений, чтобы позже передать еще раз. Если при выполнении операции write () возникает ошибка, мы не осво- бождаем сокет, так как эту проблему можно обнаружить по незаконченной опе- рации read (). Когда серверный демон регистрации закрывает сокет, асинхрон- ная операция read (), инициированная AIO_Output_Handler: : open (), за- вершается. По завершении, АСЕ-каркас Proactor вызывает следующий метод: 1 void AIO_Output_Handler::handle_read_stream 2 (const ACE_Asynch_Read_Stream::Result &result) { 3 result.message_block () .release () ; Рис. 8.6 Последовательность событий при передаче регистрационной записи
326 Глава 8 4 writer_.cancel (); 5 ACE_OS::closesocket result.handle ()); 6 handle (ACE_INVALID_HANDLE); 7 can_write_ = 0; 8 CLD_CONNECTOR:: instance () ->reconnect () ; 9 } Строки 3-5 Освобождаем ACE_Message_Block, который был выделен под операцию read (), завершаем все незаконченные операции write () и за- крываем сокет. Строки 6-8 Так как соединение теперь закрыто, устанавливаем дескриптор в ACE_INVALID_HANDLE и восстанавливаем флаг can_write_, чтобы пре- дотвратить возможность инициирования передачи любых регистрационных записей, до восстановления соединения с сервером. В разделе 8.4 рассматрива- ется, каким образом в ACE-каркасе Proactor устанавливаются соединения, пас- сивно и активно. На рис.8.6 показана последовательность событий при получении регистра- ционной записи от клиента и передаче ее серверному демону регистрации. 8.4 Классы Proactive Acceptor-Connector Обоснование Установление соединения TCP/IP представляет собой двухступенчатый процесс: 1 . Приложение или связывает прослушиваемый сокет с портом и осуществ- ляет прослушивание, или, «зная» о наличии осуществляющего прослуши- вание приложения, активно инициирует запрос на установление с ним со- единения. 2 . Операция установления соединения завершается после того, как ОС, яв- ляющаяся посредником обмена данными по протоколу TCP, открывает но- вое соединение. Этот двухступенчатый процесс часто осуществляется с использованием взаимно-согласованной или синхронной модели ввода/вывода, как отмечалось в главе 3 [C++NPvl] и в главе 7 данного тома. Однако начало/завершение (initiate/complete) работы протокола установления TCP-соединения больше со- ответствует активно-превентивной модели. Сетевые приложения, использую- щие асинхронный ввод/вывод, могут, следовательно, с выгодой использовать возможность асинхронного установления соединений. ОС отличаются и тем, как в них осуществляется поддержка асинхронного установления соединений. Windows, например, поддерживает асинхронное ус- тановление соединений, а в POSIX.4 АЮ такая поддержка отсутствует. Сущест- вует, тем не менее, возможность эмулировать асинхронное установление со- единений там, где оно отсутствует, за счет использования других механизмов ОС, таких как многопоточность (в блоке 57 обсуждается эмуляция АСЕ-каркаса
АСЕ-каркас Proactor 327 Proactor в POSIX). Так как повторное проектирование и кодирование с целью инкапсуляции и эмуляции асинхронного установления соединений для каждо- го проекта или платформы является трудоемким процессом, подверженным ошибкам, АСЕ-каркас Proactor определяет классы ACE_Asynch_Acceptor, ACE_Asynch_Connector и ACE_Service_Handler. Функциональные возможности классов ACE_Asynch_Acceptor представляет собой еще одну реализацию роли акцептора в паттерне Acceptor-Connector [POSA2]. Этот класс предоставляет следующие функциональные возможности: • Инициирует асинхронное установление пассивного соединения. • Действует как фабрика, создавая новый обработчик службы для каждого принятого соединения. • Может завершать инициированную асинхронную операцию accept (). • Предоставляет hook-метод для получения адреса приложения-партнера при установлении нового соединения. • Предоставляет hook-метод проверки идентичности приложения-партне- ра до инициализации нового обработчика службы. ACE_Asynch_Connector играет роль коннектора в реализации паттерна Acceptor-Connector в ACE-каркасе Proactor. Этот класс обеспечивает следую- щие функциональные возможности: • Инициирует асинхронное установление активного соединения. • Действует как фабрика, создавая новый обработчик службы для каждого установленного соединения. • Может завершать инициированную асинхронную операцию con- nect (). • Предоставляет hook-метод для получения адреса приложения-партнера при установлении нового соединения. • Предоставляет hook-метод проверки идентичности приложения-партне- ра до инициализации нового обработчика службы. В отличие от ACE-каркаса Acceptor-Connector, рассмотренного в главе 7, данные два класса только устанавливают соединения TCP/IP. Как отмечалось в разделе 8.2, АСЕ-каркас Proactor нацелен на инкапсуляцию операций, а не де- скрипторов ввода/вывода. Данные классы инкапсулируют операции установ- ления соединений TCP/IP. Механизмы IPC без установления соединения (на- пример, UDP и файловый ввод/вывод) не требуют установления соединения, следовательно, могут использоваться непосредственно с классами-фабриками ввода/вывода ACE-каркаса Proactor. Также как ACE_Acceptor и ACE_Connector, ACE_Asynch_Acceptor и ACE_Asynch_Connector являются фабриками шаблонных классов, кото- рые могут создать обработчик службы для обслуживания нового соединения. Шаблонным параметром и у ACE_Asynch_Acceptor, и у ACE_Asynch_Con-
328 Глава 8 nector является класс службы, создаваемый фабрикой, который называется ACE_Service_Handler. Этот класс действует как целевой объект, обрабатываю- щий завершения соединений от ACE_Asynch_Acceptor и ACE_Asynch_Con- nector. ACE_Service_Handler обеспечивает следующие возможности: • Создает основу для инициализации и выполнения сетевой прикладной службы, действуя в качестве целевого объекта фабрик соединений ACE_Asynch_Connector и ACE_Asynch_Acceptor. • Получает адрес партнера, установившего соединение (это важно для Windows), так как этот адрес недоступен после завершения асинхронного соединения. • Наследует способность работать с асинхронными событиями заверше- ния, так как является производным от ACE_Handler. В блоке 56 приводится обоснование решения не использовать повторно ACE_Svc_Handler для ACE-каркаса Proactor. Интерфейсы всех трех классов механизма Proactive Acceptor-Connector приведены на рис. 8.7. Основные методы класса ACE_Asynch_Acceptor пере- числены в следующей таблице: Метод Описание open () Инициализирует и инициирует одну или несколько асинхронных операций accept о. cancel () Завершает все асинхронные операции accept (), инициированные акцептором. validate_ connection () Ноок-метод для проверки достоверности адреса партнера до начала обслуживания нового соединения. make_handler () Ноок-метод получения объекта обработчика службы для нового соединения. Метод open () инициализирует для прослушивания TCP-сокет и иниции- рует одну или несколько асинхронных операций accept (). Если аргумент reissue_accept равен 1 (по умолчанию), если потребуется, автоматически будет инициирована новая операция accept (). ACE_Asynch_Acceptor реализует метод ACE_Handler: :handle_ac- cept () (рис. 8.5) для обработки каждого завершения операции accept () сле- дующим образом: • Собирает ACE_INET_Addr каждой конечной точки нового соединения. • Если параметр validate_new_connection, переданный open (), ра- вен 1, вызывает метод validate_connection (), передавая адрес партнера, установившего соединение. Если validate_connection () возвращает -1, то соединение нарушено.
АСЕ-каркас Proactor 329 Блок 56: ACE_Service_Handler по сравнению с ACE Svc Handler Класс ACE_Service_Handler играет роль аналогичную классу ACE_Svc_Hand- ler АСЕ-каркаса Acceptor-Connector, рассмотренную в разделе 7.2. Хотя в АСЕ-каркасе Proactor можно было повторно использовать ACE_Svc_Handler. в качестве целевого объекта для ACE_Asynch_Acceptor и ACE_Asynch_Con- nector, было принято решение создать отдельный класс по следующим при- чинам: • Сетевые приложения, использующие активно-превентивное (proactive) ус- тановление соединений, часто используют также активно-превентивный (proactive) ввод/вывод. Целевой объект завершения асинхронного установ- ления соединения должен, следовательно, создаваться на основе класса, который может органично взаимодействовать со всеми составляющими АСЕ-каркаса Proactor. • ACE_Svc_Handler инкапсулирует IPC-объект. АСЕ-каркас Proactor исполь- зует дескрипторы ввода/вывода, так что дополнительный IPC-объект может все запутать. • ACE_Svc_Handler спроектирован специально для использования с АСЕ- каркасом Reactor, так как он является производным от ACE_Event_Handler.j АСЕ поддерживает разделение каркасов, чтобы избежать ненужных связей и упростить разделение инструментальной библиотеки АСЕ на функцио- нальные подмножества. В тех вариантах применения, в которых асе Service_Handler имеет преиму- щество за счет использования АСЕ-каркасаТазк, обычным решением является добавить ACE_Tas к в качестве базового класса для класса службы, производ- ного ОТ ACE_Service_Handler. ACE_Svc_Handler тоже МОЖНО ИСПОЛЬЗОВОТЬ, так как он является производным от ACE_Task, но чаще используется сам ACE_Task. Фактически, такой вариант применения продемонстрирован В классе AIO_Output_Handler. • Вызывает hook-метод make_handler (), чтобы получить обработчик службы для нового соединения. Реализация по умолчанию использует operator new для динамического размещения нового обработчика. • Устанавливает указатель наACE_Proactor нового обработчика. • Если параметр pass_address, передаваемый в open (), равен 1, вызы- вает метод ACE_Service_Handler: : addresses () с локальным и удаленным адресами. • Устанавливает дескриптор ввода/вывода нового соединения и вызывает метод open () нового обработчика службы. Класс ACE_Asynch_Connector предоставляет методы, которые похожи на методы ACE_Asynch_Acceptor. Они перечислены в следующей таблице: Метод Описание open () Инициализирует информацию для фабрики активных соединений. connect () Инициирует асинхронную операцию connect ().
330 Глава 8 1 Метод Описание |cancel () Завершает все асинхронные операции connect (). validate_connection() Hook-метод для локализации соединения и проверки достоверности адреса партнера до начала обслуживания нового соединения. make_handler() Hook-метод получения объекта обработчика службы для нового соединения. HANDLER } ACE_Aeynch_Acceptor - listen_handle : ACE_HANDLE - reissue__accept_ : int + open (address : const ACE_INET_Addr&, bytes_to_read : size_t « 0, pass_address : int = 0, backlog : int - ACE_DEFAULT_BACKLOG, reuse_addr : int = 1, proactor : ACE_Proactor * ~ 0, validate_new_connection : int - 0, reissue_accept : int « 1, number_of_accepts : int ~ -1) : int + cancel () : int + validate_connection (result : const ACE_Asynch_Accept::Result&, remote : const ACE_INET_Addr&, local : const ACE_INET_Addr&) : int # make_handler () : HANDLER * i ACE^Handler + open (handle : ACE_HANDLE, block : ACE_Message&) + addresses (remote : const ACE_INET_Addr& local : const ACE_INET_Addr&) ACE_Asynch_Conn«ctor 1 HANDLER • # open (pass_address : int - 0, proactor : ACE_Proactor ★*=(), validate_new_connection : int » 1) : int connect (peer : const ACE_INET_Addr&, local : const ACE_INET_Addr& = ACE_INET_Addr ( (u_short) 0) reuse_addr : int « 1, act : void ★ = 0) : int cancel () : int validate_connection (result : const ACE_Asynch_Connect::Result&, remote : const ACE_INET_Addr&, local : const ACE_INET_Addr&) : int make_handler () : HANDLER * Рис. 8.7 Классы активно-превентивных (proactive) акцептора, коннектора и обработчика службы Метод open () принимает меньше аргументов, чем ACE_Asynch_Accep- tor: : open (). В частности, поскольку адресная информация может быть раз-
АСЕ-каркас Proactor 331 ной для каждой операции connect (), она задается в параметрах, передавае- мых методу connect (). ACE_Asynch_Connector реализует ACE_Handler::handle_con- nect () (рис. 8.5) для обработки каждого завершения соединения. Этапы обра- ботки такие же, как у ACE_Asynch_Acceptor, представленного выше. Каждый класс сетевой прикладной службы в ACE-каркасе Proactor является производным от ACE_Service_Handler. Их основные методы приведены в следующей таблице: Метод Описание open () Ноок-метод вызываемый для инициализации службы после установления нового соединения. addresses() Ноок-метод для фиксации локального и удаленного адресов соединения службы. | Как упоминалось выше, и ACE_Asynch_Acceptor и ACE_Asynch_Con- nector вызывают hook-метод ACE_Service_Handler: : open () для каж- дого нового установленного соединения. Аргумент дескриптора представляет собой дескриптор сокета установленного соединения. Аргумент ACE_Messa- ge_Block может содержать данные от удаленного партнера, если параметр bytes_to_read, передаваемый ACE_Asynch_Acceptor: : open (), больше 0. Так как это, специфичное для Windows, средство чаще используется с не-1Р протоколами (например, Х.25), здесь мы его не рассматриваем. АСЕ-каркас Proactor сам управляет ACE_Message_Block, так что службе этим заниматься не нужно. Блок 57: Эмуляция асинхронных соединений в POSIX В Windows встроена возможность асинхронного соединения сокетов. В отли- чие от этого, средство POSIX.4 AIO создавалось, в основном, для использова- ния с дисковым вводом/выводом, поэтому не включает возможности асин- хронного установления соединений TCP/IP. Чтобы обеспечить унифицирован- ную возможность для всех платформ, поддерживающих асинхронный ввод/ вывод АСЕ, там, где это требуется, эмулирует асинхронное установление со- единений. Чтобы эмулировать асинхронное установление соединений, запросы активных И пассивных соединений начинаются ACE_Asynch_Acceptor И ACE_Asynch_Con- nector в неблокируемом режиме. Если соединение не завершается сразу (чтобы бывает, обычно, в случае пассивных соединений), дескриптор сокета регистрируется у экземпляра асе Seiect_Reactor, управляемого самим каркасом. Поток, создаваемый АСЕ-каркасом Proactor (невидимый для при- ложения), выполняет цикл обработки событий приватного (private) реактора. После завершения запроса на соединение, управление возвращается карка- су с помощью метода обратного вызова реактора и уведомления о событии за- вершения. Исходный прикладной поток получает обратно уведомление о собы- тии завершения В классе ACE_Asynch_Acceptor ИЛИ классе ACE_Asynch_Con- nector, в зависимости от ситуации.
332 Глава 8 Если обработчику службы требуются локальный или удаленный адреса но- вого соединения, он должен реализовать hook-метод addresses () для их со- хранения после установления соединения. АСЕ-каркас Proactor вызывает этот метод, если аргумент pass_address, передаваемый фабрике асинхронных со- единений, равен 1. Этот метод имеет значение больше для Windows, так как ад- реса соединения, при использовании асинхронного установления соединений, не могут быть получены никак иначе. Пример Также как в клиентских демонах регистрации в главах 6 и 7, классы в актив- но-превентивной реализации подразделяются на отдельные роли ввода и вы- вода, которые поясняются ниже. Функция ввода. Роль ввода в клиентском демоне регистрации выполняется классами AIO_CLD_Acceptor и AIO_Input_Handler. AIO_Input_Hand- ler уже рассматривался ранее, поэтому мы сосредоточимся на AIO_CLD_Ac- ceptor, который является производным от ACE_Asynch_Acceptor, как сле- дует из рис. 8.3. Определение класса AIO_CLD_Acceptor приведено ниже: class AIO_CLD_Acceptor : public ACE_Asynch_Acceptor<AIO_Input_Handler> { public: // Завершает прием соединений и закрывает всех клиентов. void close (void); // Удаляет обработчик из набора клиентов. void remove (AIO_Input_Handler *ih) { clients_.remove (ih); } protected: // Метод-фабрика обработчика службы. virtual AIO_Input_Handler *make_handler (void); // Множество клиентов с установленными соединениями. ACE_Unbounded_Set<AIO_Input_Handler *> clients_; ); Так как АСЕ-каркас Proactor следит только за активными операциями вво- да/вывода, он не поддерживает набор зарегистрированных обработчиков так, как это делает АСЕ-каркас Reactor. Приложения должны, таким образом, когда нужно, находить и удалять обработчики. В примере данной главы, объекты AIO_Input_Handler создаются динамически, и они должны быть доступны- ми и после завершения работы службы. Чтобы удовлетворить это требование, член AIO_CLD_Acceptor: :clients_ является ACE_Unbounded_Set, ко- торый хранит указатели на все активные объекты AIO_Input_Handler. Ко- гда клиент регистрации устанавливает соединение с этим сервером, метод ACE_Asynch_Acceptor: :handle_accept () вызывает следующий метод- фабрику:
АСЕ-каркас Proactor 333 AIO_Input_Handler * AIO_CLD_Acceptor::make_handler (void) { AIO_Input_Handler *ih; ACE_NEW_RETURN (ih, AIO_Input_Handler (this), 0) ; if (clients_.insert (ih) == -1) { delete ih; return 0; } return ih; ) AIO_CLD_Acceptor повторно реализует метод-фабрику make_hand- ler (), который следит в cl ients_ за каждым указателем на созданный обра- ботчик службы. Если, по каким-то причинам, указатель на новый обработчик не может быть включен в набор, он удаляется; возвращаемое значение равное 0 приводит к тому, что АСЕ-каркас Proactor закрывает вновь принятое соедине- ние. Ноок-метод make_handler () передает свой объектный указатель на каж- дый динамически размещенный AIO_Input_Handler. Когда А10_1п- put_Handler обнаруживает ошибку при выполнении read () (чаще всего из-за того, что клиент закрыл соединение), его метод handle_read_stre- am () просто удаляет его самого. Деструктор AIO_Input_Handler освобож- дает все имеющиеся ресурсы и вызывает метод AIO_CLD_Acceptor: : re- move (), чтобы удалить самого себя из набора clients_: AIO_Input_Handler::~AIO_Input_Handler () { reader_.cancel () ; ACE_OS::closesocket (handle ()); if (mblk_ != 0) mblk_->release (); mblk_ = 0; acceptor ->remove (this); } Когда служба завершается методом AIO_Client_Logging_Dae- mon: : svc (), все остающиеся соединения AIO_Input_Handler и объекты освобождаются путем вызова метода close (), приведенного ниже: void AIO_CLD_Acceptor::close (void) { ACE_Unbounded_Set_Iterator<AIO_Input_Handler *> iter (clients_.begin ()); AIO_Input_Handler **ih; while (iter.next (ih)) delete *ih; } Этот метод просто перебирает все активные объекты AIO_Input_Hand- ler и каждый удаляет. Функция вывода. Функция вывода активно-превентивного клиентского де- мона регистрации осуществляется классами AIO_CLD_Connector и AIO_Out- put_Handler. Клиентский демон регистрации использует AIO_CLD_Con- nector для выполнения следующих действий:
334 Глава 8 • Установления (и, если нужно, восстановления) TCP-соединения с сервер- ным демоном регистрации. • Реализации SSL-аутентификации соединения коннектора с серверным демоном регистрации. Затем он использует AIO_Output_Handler, чтобы асинхронно ретранс- лировать регистрационные записи от установивших соединение клиентов сер- верному демону регистрации. Часть определения класса AIO_CLD_Connector приведена ниже: class AIO_CLD_Connector : public ACE_Asynch_Connector<AIO_Output_Handler> { public: enum { INITIAL_RETRY_DELAY = 3, MAX_RETRY_DELAY =60 }; // Конструктор. AIO_CLD_Connector () : retry_delay_ (INITIAL—RETRY_DELAY) , ssl_ctx_ (0) , ssl_ (0) { open (); } // Ноок-метод для обнаружения сбоев и проверки партнера // до создания обработчика. virtual int validate_connection (const ACE_Asynch_Connect::Result &result, const ACE_INET_Addr &remote, const ACE_INET_Addr &local); protected: // Шаблонный метод создания нового обработчика. virtual AIO_Output_Handler *make_handler (void) { return OUTPUT_HANDLER::instance (); } // Адрес прослушивания сервером запросов на соединение. АСЕ—INET—Addr remote_addr_; // Время (сек) ожидания до следующей попытки установить // соединение. int retry_delay_; 11 Структура данных "контекста" SSL. SSL—СТХ *ssl_ctx_; // Структура данных SSL, соответствующая // аутентифицированным SSL-соединениям. SSL *ssl_; }; typedef ACE—Unmanaged_Singleton<AIO_CLD_Connector, ACE_Null_Mutex> CLD_CONNECTOR;
АСЕ-каркас Proactor 335 Обращение к классу AIO_CLD_Connector осуществляется как к незави- симому синглтону (unmanaged singleton, см. блок 45) посредством CLD_CON- NECTOR typedef. Когда создается экземпляр AIO_CLD_Connector, его кон- структор вызывает метод ACE_Asynch_Connector: : open (). Метод vali- date_connection() будет вызываться, по умолчанию, по завершении каждой попытки вызова connect (). 8.5 Класс ACE_Proactor Обоснование Операции асинхронного ввода/вывода осуществляются в два этапа: начало (initiation) и завершение (completion). Поскольку процесс включает несколько этапов и классов, должна существовать возможность демультиплексирования событий завершения и эффективной привязки каждого события завершения к самой завершенной операции и к обработчику завершения, который обраба- тывает результат. Различие возможностей асинхронного ввода/вывода, реали- зуемых ОС, играет здесь более важную роль, чем во взаимно-согласованной (reactive) модели ввода/вывода по следующим причинам: • Разные платформы предоставляют разные способы получения уведом- лений о завершении. Например, Windows использует порты завершения ввода/вывода или события завершения ввода/вывода, a POSIX.4 АЮ ис- пользует сигналы реального времени или системную функцию a i o_s u s - pend () для ожидания завершения. • Платформы используют разные структуры данных для поддержания ин- формации о состоянии операций асинхронного ввода/вывода. Напри- мер, Windows использует структуру OVERLAPPED, a POSTX.4 АЮ ис- пользует struct aiocb. Таким образом, цепочка сведений, касающихся зависимых от платформы механизмов и структур данных, действует на всем протяжении, от иницииро- вания операций, через диспетчеризацию и до обработки завершений. Чтобы разрешить эти проблемы и обеспечить переносимое и гибкое средство демуль- типлексирования и диспетчеризации событий завершения, АСЕ-каркас Proac- tor определяет класс ACE_Prpactor. Функциональные возможности класса ACE_Proactor реализует паттерн Facade [GoF] для определения интер- фейса, который приложения могут использовать, чтобы гибко и переносимо получать доступ к различным возможностям ACE-каркаса Proactor. Этот класс обеспечивает следующие возможности: • Централизует обработку событий в активно-превентивном (proactive) приложении. • Осуществляет диспетчеризацию объектов ACE_Handler, ассоцииро- ванных с соответствующими событиями таймера.
336 Глава 8 • Осуществляет демультиплексирование событий завершения обработчи- кам завершения и диспетчеризацию соответствующих hook-методов об- работчиков завершения, которые затем выполняют, в ответ на события завершения, определяемую приложением обработку. • Может отделять поток(и), выполняющий(ие) обнаружение событий за- вершения, демультиплексирование и диспетчеризацию от потока(ов), инициирующего асинхронные операции. • Является посредником между классами, инициирующими операций ввода/вывода, и деталями реализации асинхронного ввода/вывода на разных платформах. Интерфейс ACE_Proactor приведен на рис. 8.8. Этот класс имеет разви- той интерфейс, который экспортирует все возможности ACE-каркаса Proactor. Поэтому мы разделяем описание его методов на четыре группы, которые рас- сматриваются ниже. 1. Методы управления жизненным циклом. Следующие методы инициали- зируют, удаляют ACE_Proactor и предоставляют к нему доступ: Метод Описание ACE_Proactor () open () Данные методы инициализируют экземпляр проактора (proactor). ~ACE_Proactor() close () Данные методы освобождают ресурсы, выделенные при инициализации проактора. instance () Статический метод, возвращающий указатель на синглтон ACE_Proactor, который создается и управляется паттерном Singleton (GoF) совместно с идиомой Double-Checked Locking Optimization (POSA2). ACE_Proactor может быть использован двумя способами: • Как синглтон [GoF] с помощью метода instance (), приведенного выше в таблице. • Путем создания одного или несколько экземпляров. Эта возможность может быть использована с целью поддержки нескольких проакторов в одном процессе. Каждый проактор часто ассоциируется с потоком, ра- ботающим со своим приоритетом [Sch98], 2. Методы управления циклом обработки событий. АСЕ-каркас Proactor поддерживает инверсию управлению. Аналогично ACE-каркасу Reactor, ACE_Proactor реализует следующие методы цикла обработки событий, которые управляют диспетчеризацией обработчиков завершения в прило- жениях:
АСЕ-каркас Proactor 337 Метод Описание handle_events() Ждет наступления событий завершения и затем осуществляет диспетчеризацию соответствующих обработчиков завершения. Параметр, задающий тайм-аут, может ограничивать время ожидания события, j proactor_run_ event_loop() Циклически вызывает метод handle_events () пока: (1) возникнет ошибка, (2) proactor_event_loop_done () вернет «истину», (3) истечет тайм-аут (факультативно). pro'actor—end- event—loop () Сообщает проактору, что нужно завершить цикл обработки событий. proactor_event— loop_done() Возвращает 1 после завершения цикла обработки событий проактора, например, после вызова proactor_end_event_loop(). ACE-Proactor^Impl ЛСЕ_ Timer_Queue b______________________________& ACE_Proactor # proactor,, : ACE_Proactor * + ACE_Proactor (impl : ACE_Proactor_lmpl * = 0, delete_impl : int » 0, tq : ACE_Timer_Queue * » 0) + instance (} ; ACEJ?roactor * + close () : int + handle_events () : int + handle—events (wait_time ; £CE_Time_Value&) : int + proactor_run^_event_loop (tv : ACE_Time_Value&, event_hook : int (*) (ACE_Proactor ★)) : int + proactor_end_event_loop () : int + proactor_event_loop—done () : int + schedule-timer (handler : ACE_Handler&, act : const void *, time : const ACE_Time_Value&) : long + cancel_timer (handler : ACE_Handler&, dont_calljhandle_close : int « 1) : int + create_asynch_read_stream () : ACE-Asynch_Read_Stream-Impl ★ + create_asynch—write_stream О : ACE—Asynch—Write_Stream_Impl Рис. 8.8 Класс ACE Proactor Цикл обработки событий АСЕ Proactor выполняется отдельно от цикла об- работки событий АСЕ-каркаса Reactor. Чтобы в одном и том же приложении можно было использовать оба цикла обработки событий, и АСЕ-каркаса Reac- tor и АСЕ-каркаса Proactor, и чтобы приложение оставалось переносимым на все платформы, поддерживающие асинхронный ввод/вывод, указанные два цикла обработки событий должны выполняться в отдельных потоках. Реализация ACE_Proactor в Windows может, однако, регистрировать его дескриптор порта завершения ввода/вывода у экземпляра ACE_WFMO_Reactor, с целью объедине- ния двух указанных механизмов циклов обработки событий в одно целое, позво- ляя использовать оба цикла в одном потоке. Блок 58 поясняет, как это сделать. 12 Программирование сетевых приложений на C++. Том 2
338 Глава 8 3. Методы управления таймером. По умолчанию, ACE_Proactor использу- ет механизм очереди таймеров ACE_Timer_Heap, рассмотренный в разде- ле 3.4 с целью планирования и диспетчеризации обработчиков событий в соответствии с истечением их тайм-аутов. Методы управления таймера- ми, предлагаемые ACE_Proactor, включают: Метод Описание schedule_timer() Регистрирует ACE_Handier, для которого будет осуществляться диспетчеризация, по истечении заданного пользователем времени. cancel_timer{) Завершает работу одного или нескольких ранее зарегистрированных таймеров. | Когда таймер срабатывает, ACE_Handler: :handle_time_out () пере- дает управление зарегистрированному обработчику. 4. Методы-посредники операций ввода/вывода. Класс ACE_Proactor включает информацию о деталях реализации асинхронного ввода/вывода на конкретной платформе, полезную для инициирования операций и обра- ботки событий завершения. То, что ACE_Proactor является единствен- ным посредником, располагающим информацией о деталях реализации, которые зависят от платформы, предохраняет от появления непредусмот- ренных связей между классами ACE-каркаса Proactor. В частности, АСЕ- каркас Proactor использует паттерн Bridge [GoF]. ACE_Asynch_Read_Stream и ACE_Asynch_Write_Stream использу- ют паттерн Bridge для доступа к различным реализациям фабрик операций вво- да/вывода, которые являются специфичными для данной платформы ОС. По- скольку ACE_Proactor является посредником этой зависящей от платформы информации, он определяет следующие методы, используемые классами ACE_Asynch_Read_Stream и ACE_Asynch_Write_Stream: Метод Описание create—asynch—read -Stream() Создает экземпляр платформо-зависимого класса, ПРОИЗВОДНОГО ОТ ACE_Asynch_Read_Stream_Impl, подходящего для инициирования асинхронных операций read (). create_asynch_ write—stream() Создает экземпляр платформо-зависимого класса, ПРОИЗВОДНОГО ОТ ACE_Asynch_Write_Stream_Impl, подходящего для инициирования асинхронных операций write О. Как видно из рис. 8.8, класс ACE_Proactor ссылается на объект типа ACE_Proactor_Impl, аналогично ACE-каркасу Reactor, приведенному на рис. 4.1. Все действия, которые зависят от специфики механизмов платформы, передаются для выполнения классу реализации Proactor. Ниже мы кратко опи- сываем платформо-зависимые варианты реализации ACE-каркаса Proactor.
АСЕ-каркас Proactor 339 Класс ACE_WIN32_Proactor ACE_WIN32_Proactor является Windows-реализацией класса ACE_Pro- actor. Этот класс может работать на Windows NT 4.0 и более новых версиях Блок 58: Интеграция событий Proactor и Reactor в Windows Циклы обработки событий АСЕ Reactor и АСЕ Proactor требуют разных меха- низмов обнаружения и демультиплексирования событий. Как следствие, они часто выполнятся в отдельных потоках. В Windows, однако, АСЕ предоставляет возможность объединения этих двух механизмов обработки событий, таким образом, что оба могут управляться одним потоком. Преимуществом исполь- зования одного потока, выполняющего цикл обработки событий, является то, что он позволяет упростить определяемые приложением обработчики собы- тий и обработчики завершений, так как они, по-видимому, больше не будут ну- ждаться в синхронизации с целью предотвращения состояний гонок. Windows-реализация асе Proactor использует порт завершения ввода/выво- да для обнаружения событий завершения. Когда одна или несколько асин- хронных операций завершаются, Windows посылает сигнал дескриптору со- ответствующего порта завершения. Следовательно, этот дескриптор может быть зарегистрирован у экземпляра ACE_WFMO_Reactor (глава 4). Используя эту схему, асе WFMO_Reactor осуществляет диспетчеризацию события «есть- сигнал» («signaled») порта завершения ввода/вывода объекту ACE_Proactor, который по очереди осуществляет диспетчеризацию событий завершения и возвращает управление циклу обработки событий реактора. Чтобы использовать приведенную выше схему, приложение должно создать экземпляр ACE_Proactor с конкретным набором опций, которые по умолча- нию не задаются. Следующий фрагмент кода показывает, как это сделать; он должен выполняться сразу после начала работы программы; 1 ACE_Proactor::close_singleton (); 2 ACE_WIN32_Proactor *impl = new ACE_WIN32_Proactor (0, 1); 3 ACE_Proactor::instance (new ACE_Proactor (impl, 1), 1); 4 ACE_Reactor::instance ()->register_handler 5 (impl, impl->get_handle ()); /7 ... Остальной код регистрации и инициализации опущен. 6 ACE_Reactor::instance ()->run_reactor_event_loop (); 7 ACE_Reactor::instance ()->remove_handler 8 (impl->get_handle (), ACE_Event_Handler::DONT_CALL); Строка 1 Закрываем существующий синглтон-проактор (proactor singleton). Строка 2 Создаем Windows-реализацию проактора (proactor). Второй аргу- мент показывает, что данный проактор будет использоваться совместно с ре- актором (reactor). Строка 3 Создаем новую реализацию ACE_Proactor с поддержкой реактора и делаем ее синглтоном. Второй аргумент, передаваемый ACE_Proactor, го- ворит о том, что данную реализацию нужно удалить при закрытии ACE_Proac- tor. Второй аргумент, передаваемый instance (), говоритотом, что, послеза- крытия, объект ACE_Proactor нужно удалить. Строки 4-6 Регистрируем дескриптор порта завершения ввода/вывода у ре- актора и запускаем его цикл обработки событий. Строки 7-8 После завершения цикла обработки событий, удаляем из реакто- ра порт завершения ввода/вывода проактора. 12*
340 Глава 8 Windows, таких как Windows 2000 и Windows ХР. Он не работает, однако, в Windows 95, 98, Me или СЕ, так как эти платформы не поддерживают асин- хронный ввод/вывод. Обзор реализации. ACE_WIN32_Proactor использует порт завершения ввода/вывода для обнаружения событий завершения. При инициализации фабрики асинхронных операций, например, ACE_Asynch_Read_Stream или ACE_Asynch_Write_Stream, устанавливается связь дескриптора ввода/вы- вода с портом завершения ввода/вывода Proactor. В данной реализации, цикл обработки событий выполняет функция Windows GetQueuedCompletion- Status(). Все классы Result, определенные для использования с ACE_WIN32_Pro- actor, являются производными от структуры Windows OVERLAPPED. К каж- дому классу добавляется информация, зависящая от выполняемой операции. Когда GetQueuedCompletionStatus () возвращает указатель на структуру OVERLAPPED) завершенной операции, ACE_WIN32_Proactor приводит его к указателю на объект Re s u 11. Tакое решение позволяет эффективно осущест- влять диспетчеризацию событий завершения соответствующему обработчику завершения, производному от ACE_Handler, при завершении операций вво- да/вывода. В разделе Реализация (Implementation) паттерна Proactor [POSA2] по- казано, как реализовать проактор, используя асинхронные механизмы Win- dows и паттерн Asynchronous Completion Token. Параллелизм. Несколько потоков могут одновременно выполнять цикл обработки событий ACE_WIN32_Proactor. Поскольку регистрацией и дис- петчеризацией всех событий управляет механизм порта завершения ввода/вы- вода, а не сам ACE_WIN32_Proactor, нет необходимости синхронизировать доступ к структурам данных, связанный с регистрацией, как в случае реализа- ции ACE_WFMO_Reactor. Управление очередью таймеров осуществляет отдельный поток, которым управляет ACE_WIN32_Proactor. Когда установленный срок таймера истека- ет, механизм тайм-аутов использует функцию PostQueuedCompletion- Status () для того, чтобы уведомить порт завершения ввода/вывода проакто- ра. Такое решение, естественным образом, объединяет механизм таймеров с ме- ханизмом диспетчеризации событий завершения. Кроме того, такое решение гарантирует, что для диспетчеризации таймера возобновляется работа только одного потока, так как все потоки, связанные с обнаружением завершений, ждут исключительно событий завершения и не реагируют на истечение срока, установленного для таймера. Класс ACE_POSIX_Proactor Реализации АСЕ Proactor в POSIX-системах предоставляют несколько ме- ханизмов инициирования операций ввода/вывода и обнаружения их заверше- ния. Более того, Solaris Operating Environment (SOE) компании Sun предлагает свою собственную версию асинхронного ввода/вывода. В Solaris 2.6 и выше производительность собственных функций Sun асинхронного ввода/вывода существенно выше, чем производительность реализации Solaris POSIX.4 АЮ.
. АСЕ-каркас Proactor 541 Чтобы использовать выгоды, связанные с повышением производительности, АСЕ инкапсулирует также и этот механизм в отдельном наборе классов. Обзор реализации. POSIX-реализации асинхронного ввода/вывода ис- пользуют управляющий блок (struct aiocb) для идентификации каждого запроса асинхронного ввода/вывода и его управляющей информации. Каждый aiocb может быть ассоциирован, в каждый момент времени, только с одним запросом ввода/вывода. Вариант асинхронного ввода/вывода Sun использует дополнительную структуру aio_result_t. Хотя инкапсулированные механизмы асинхронного ввода/вывода POSIX поддерживают операции read () и write (), они не поддерживают никаких операций, связанных с соединениями TCP/IP. Для поддержки функций ACE_Asynch_Acceptor и ACE_Asynch_Connector, выполняющих опера- ции, связанные с соединениями, используется отдельный поток. Такого рода эмуляция асинхронных соединений, рассмотрена в блоке 57. Три варианта реализации ACE_POSIX_Proactor описаны в следующей таблице: Вариант АСЕ Proactor Описание ACE_POS I Х-AIOCB- Proactor Данная реализация поддерживает параллельный список структур aiocb и объектов Result. Каждая ожидающая выполнения операция представлена одним элементом в каждом списке. Функция aio_suspend () приостанавливает выполнение цикла обработки событий до завершения одной или нескольких операций ввода/вывода. АСЕ-POSIX—SIG— Proactor Данная реализация является производной от ACE_P0Six_Ai0CB_Proactor, но использует сигналы реального времени POSIX для обнаружения завершения асинхронного ввода/вывода. Цикл обработки событий использует функции sigtimedwait() И sigwaitinfo () ДЛЯ выполнения итераций цикла и получения информации о завершенных операциях. Каждая операция асинхронного ввода/вывода, начатая с помощью этого проактора, имеет уникальное значение, связанное с aiocb. Этими значениями обмениваются посредством сигналов, уведомляя о завершении. Такое решение упрощает локализацию aiocb и соответствующих объектов Result, а также обеспечивает правильную диспетчеризацию обработчиков событий. ACE-SUN-Proactor Данная реализация также основана на АСЕ POSIX_AIOCB_Proactor, НО ИСПОЛЬЗуеТ собственное средство Sun асинхронного ввода/вывода, вместо средства POSIX.4 АЮ. Данная реализация работает в значительной степени так же, как и ACE_P0Six_Ai0CB_Proactor, но использует специфичную функцию aiowait (), реализуемую Sun, для обнаружения завершения ввода/вывода.
342 Глава 8 Параллелизм. Реализацией проактора по умолчанию на платформах, со- вместимых с POSIX.4 AIO, является ACE_POSIX_SIG_Proactor. Ее механизм демультиплексирования событий завершения использует функцию sigti- medwait (). Каждый экземпляр ACE_POSIX_SIG_Proactor может опреде- лить набор сигналов для sigtimedwait (). Чтобы использовать несколько потоков с разными приоритетами и разными экземплярами АСЕ_РО- SI X_S IG_Proactor, каждый экземпляр должен использовать отличающийся сигнал, или набор сигналов. Ограничения и характеристики некоторых платформ непосредственно влияют на выбор используемой реализации ACE_POSIX_Proactor. В Linux, например, потоки являются, по существу, клонированными процессами. По- скольку сигналы не могут передаваться за границы процессов, а операции асин- хронного ввода/вывода и срабатывания таймеров Proactor, и то и другое, реали- зуется с помощью потоков, ACE_POSIX_SIG_Proactor работает в Linux не очень хорошо. Поэтому реализацией проактора по умолчанию в Linux является ACE_POSIX_AIOCB_Proactor. Используемый в ACE_POSIX_AIOCB_Pro- actor механизм демультиплексирования aio_suspend () является потоко- безопасным, поэтому несколько потоков могут одновременно выполнять его цикл обработки событий. Пример Механизм восстановления соединений AIO_CLD_Connector упоминался при рассмотрении метода AIO_Output_Handler: :handle_read_stre- am (). Этот механизм восстановления инициируется с помощью приведенного ниже метода AIO_CLD_Connector::reconnect(): int reconnect (void) { return connect (remote_addr_); ) Данный метод просто инициирует новый запрос на установление асинхрон- ного соединения с серверным демоном регистрации. Чтобы избежать непрерыв- ных попыток инициировать соединение, например, в том случае, когда сервера ный демон регистрации прекратил прослушивание, используется стратегия экспоненциальной задержки (exponential backoff). Используется следующий hook-метод validate_connection (), чтобы включить определяемые при- ложением функции в обработчик заверщения соединения ACE_Asynch_Con- nector. Данный метод определяет, откуда исходит запрос асинхронного со- единения и осуществляет планирование таймера с целью восстановления со- единения в случае его разрыва. Если соединение успешно установлено, этот метод выполняет аутентификацию SSL с серверным демоном регистрации. 1 int AIO_CLD_Connector::validate_connection 2 (const ACE_Asynch_Connect::Result &result, 3 const ACE_INET_Addr &remote, const ACE_INET_Addr &) ( 4 remote_addr_ = remote; 5 if (!result.success ()) { 6 ACE_Time_Value delay (retry_delay_);
АСЕ-каркас Proactor 343 1 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 retry_delay_ *= 2; if (retry_delay_ > MAX_RETRY_DELAY) retry_delay_ = MAX_RETRY_DELAY; proactor () ->schedule__timer (*this, 0, delay); return -1; ) retry_delay_ = INITIAL—RETRY—DELAY; if (ssl_ctx_ == 0) { OpenSSL_add_ssl_algorithms (); ssl_ctx_ = SSL_CTX_new (SSLv3—client—method ()); if (ssl_ctx_ == 0) return -1; if (SSL_CTX_use_certificate_file (ssl_ctx_, CLD—CERTIFICATE—FILEMAME, SSL_FILETYPE_PEM) <= 0 I| SSL_CTX_use_PrivateKey_file (ssl_ctx_, CLD_________________________________KEY_FILENAME, SSL—FILETYPE—PEM) <= 0 II ’SSL—CTX—check_private_key (ssl—ctX—)) { SSL_CTX-free (ssl_ctx_); ssl_ctX— = 0; return -1; ) ssl— = SSL—new (ssl—ctx_); if (ssl- == 0) { SSL-CTX_free (ssl_ctx_); ssl_ctx_ = 0; return -1; } ) SSL—clear (ssl_); SSL-Set-fd (ssl_, ACE—reinterpret—cast(int, result.connect—handle())); SSL—set—verify (ssl_, SSL—VERIFY_PEER, 0); if (SSL—connect (ssl_) == -1 I| SSL-Shutdown (ssl-) == -1) return -1; return 0; Строка 4 Сохраняем удаленный адрес, с которым пытаемся установить со- единение так, чтобы его можно было использовать снова, при следующих по- пытках установления соединения, если это потребуется. Строки 5-12 Если операция установления соединения завершается неуда- чей, устанавливаем таймер, чтобы повторить попытку позже, затем удваиваем задержку по таймеру, и так вплоть до заданного, в секундах, значения тах_retry_delay.
344 Глава 8 Строка 13 Если соединение успешно установлено, мы восстанавливаем первоначальное значение retry_delay_ так, чтобы его можно было исполь- зовать в следующей серии попыток восстановления соединения. Строки 15-46 Оставшаяся часть метода validate_connection () похо- жа на TPC_Logging_Acceptor: : open (), поэтому мы ее здесь не комменти- руем. Если аутентификация SSL завершается неудачно, validate_connec- tion () возвращает -1, вынуждая ACE_Asynch_Connector закрыть новое соединение еще до того, как для него будет создан обработчик службы. Учтите, что вызовы функций SSL в строках 43 и 44 являются синхронными, и, поэтому, цикл обработки событий проактора не осуществляет обработку событий завер- шения, пока выполняются эти вызовы. Также как в случае с каркасом Reactor, разработчики должны быть в курсе такого типа задержек в методах обратного вызова. Когда истекает время, установленное на таймере методом handle_con- nection(), АСЕ-каркас Proactor вызывает следующий hook-метод hand- le_time_out(): void AIO_CLD_Connector::handle_time_out (const ACE_Time_Value &, const void *) { connect (remote_addr_); } Данный метод просто инициирует еще одну попытку асинхронного соеди- нения connect (), которая приведет к еще одному вызову validate_con- nect ion (), независимо от того, чем завершится попытка, успехом или неуда- чей. Служба AIO клиентского демона регистрации представлена следующим классом AIO_Client_Logging_Daemon: class AIO_Client_Logging_Daemon: public ACE_Task<ACE_NULL_SYNCH> { protected: ACE_INET_Addr cld_addr_; // Адрес прослушивания. ACE_INET_Addr sld_addr_; // Адрес сервера регистрации. // Фабрика пассивного соединения с <AIO_Input_Handler>. AIO_CLD_Acceptor acceptor_; public: // Ноок-методы Service Configurator. virtual int init (int argc, ACE_TCHAR *argv[j); virtual int fini (); virtual int svc (void); }; Данный класс похож на класс AC_Client_Logging_Daemon. Основное отличие заключается в том, что AIO_Client_Logging_Daemon создает но- вый поток для выполнения цикла обработки событий проактора, от которого зависит данная служба, тогда как AC_Client_Logging__Daemon полагается,
АСЕ-каркас Proactor 345 при выполнении цикла обработки событий, на поток основной программы. Чтобы этот поток было легче активизировать, AIO_Client_Logging_Dae- mon является производным от ACE_Task, а не от ACE_Service_Conf ig. Мы создаем новый поток для цикла обработки событий проактора, так как мы, по-прежнему, зависим от цикла обработки событий реактора в том, что ка- сается действий, связанных с конфигурированием служб (см. главу 5). Если бы наша служба создавалась только для Windows, мы могли бы объединить циклы обработки событий проактора и реактора, в соответствии с изложенным в бло- ке 58. Однако данная реализация клиентского демона регистрации должна быть переносимой на все платформы, поддерживаемые АСЕ, совместимые с АЮ. После того как наш метод AIO_Client_Logging_Daemon: : init () обра- ботает список аргументов и сформирует адреса, он вызывает ACE_Task: : ac- tivate (), чтобы создать поток, выполняющий следующий метод svc (): lint AIO_Client_Logging_Daemon::svc (void) { 2 if (acceptor_. open (cld_addr_) == -1) return -1; 3 if (CLD_CONNECTOR::instance ()->connect (sld_addr_) == 0) 4 ACE_Proactor::instance ()->proactor_run_event_loop (); 5 acceptor_.close (); 6 CLD—CONNECTOR::close () ; 7 OUTPUT_HANDLER::close () ; 8 return 0; 9 } Строки 2-3 Инициализируем объект acceptor_, чтобы начать прослу- шивание запросов на соединение от клиентов регистрации и инициировать первую попытку соединения с серверным демоном регистрации. Строка 4 Вызываем ACE_Proactor::proactor_run_event_loop() для обработки завершения асинхронных операций. Цикл обработки событий проактора завершается тогда, когда завершается работа службы с помощью следующего метода f ini (): int AIO_Client_Logging_Daemon::fini () { ACE_Proactor::instance ()->proactor_end_event_loop (); wait (); return 0; ) Данный метод вызывает ACE_Proactor: :proactor_end_event_lo- op (), который, в методе svc (), завершает цикл обработки событий. Затем он вызывает ACE_Task: : wait (), чтобы дождаться завершения метода svc () и завершить его поток. Строки 5-7 Закрываем все открытые соединения, объекты-синглтоны и за- вершаем поток svc (). В заключение, мы включаем макрос ACE_FACTORY_DEFINE с целью созда- ния функции-фабрики службы: ACE_FACTORY_DEFINE (AIO_CLD, AIO_Client_Logging_Daemon)
346 Глава 8 Новый активно-превентивный клиентский демон регистрации нашей службы использует АСЕ-каркас Service Configurator для самонастройки на лю- бую основную (main) программу, например, на Configurable_Log- ging_Server, путем включения следующего элемента в файл svc. conf: dynamic AIO_Client_Logging_Daemon Service_Object * AIO_CLD:_make_AIO_Client_Logging Daemon() "-p $CLIENT_LOGGING_DAEMON_PORT" 8.6 Резюме В данной главе была рассмотрена концепция активно-превентивного вво- да/вывода и объяснено чем активно-превентивная (proactive) модель отличает- ся от взаимно-согласованной (reactive) модели. В ней также показано как мож- но использовать активно-превентивную модель, чтобы преодолевать ограни- чения производительности взаимно-согласованной модели ввода/вывода, исключая недостатки, связанные с применением многопоточного синхронного ввода/вывода. Тем не менее, у активно-превентивной модель имеются следую- щие проблемы: • Проблемы проектирования. Многоступенчатый характер этой модели повышает вероятность слишком большой зависимости между механиз- мами ввода/вывода, инициирующими асинхронные операции, и меха- низмами обработки завершения этих операций. • Проблемы переносимости. Стандарты и реализации асинхронного вво- да/вывода, предлагаемые современными компьютерными платформа- ми, сильно отличаются друг от друга. Паттерн Proactor [POSA2], определяя множество ролей и связей, помогает упростить приложения, использующие активно-превентивный ввод/вывод. АСЕ-каркас Proactor реализует паттерн Proactor на множестве платформ, под- держивающих асинхронный ввод/вывод. АСЕ-каркас Proactor предлагает на- бор классов, которые упрощают сетевым приложениям использование воз- можностей асинхронного ввода/вывода на всех, предлагающих эти возможно- сти, платформах. В данной главе были рассмотрены все классы данного каркаса, приведено их обоснование и описание их возможностей. Продемонст- рирована реализация клиентского демона регистрации, который использует активно-превентивную модель ввода/вывода во всех своих сетевых операциях. Данная версия клиентского демона регистрации работает переносимым обра- зом на всех платформах АСЕ, которые реализуют механизмы асинхронного ввода/вывода.
Глава 9 АСЕ-каркас Streams Краткое содержание Данная глава описывает структуру и применение АСЕ-каркаса Streams. Этот каркас реализует паттерн Pipes & Filters [POSA1], который является архи- тектурным паттерном, определяющим структуру систем обработки потоков данных. Мы показываем, как использовать АСЕ-каркас Streams для разработки вспомогательной программы, которая форматирует и выводит на печать фай- лы регистрационных записей, собираемых серверами регистрации. 9.1 Обзор Архитектурный паттерн Pipes & Filters является стандартным способом ор- ганизации многоуровневых/модульных (layered/modular) приложений [SG96]. Данный паттерн определяет архитектуру обработки потоков (stream) данных. В этой архитектуре каждый этап обработки инкапсулируется в виде компонен- та-фильтра. Данные передаются от фильтра к фильтру с помощью коммуника- ционного механизма, который может изменяться: от каналов IPC, соединяю- щих локальные или удаленные процессы, до обычных указателей, которые ссылаются на объекты одного процесса. Каждый фильтр может добавлять, из- менять или удалять данные, прежде чем передать их следующему фильтру. Фильтры чаще всего не сохраняют состояния; это значит, что данные, переда- ваемые фильтру, преобразуются и передаются следующему фильтру, без со- хранения. Вот обычные примеры паттерна Pipes & Filters: • Механизм каналов IPC UNIX [Ste92], используемый оболочками UNIX для создания однонаправленных конвейеров. • Streams System V [Rit84], которые обеспечивают основу для интеграции двунаправленных протоколов в ядро UNIX.
348 Глава 9 АСЕ-каркас Streams базируется на паттерне Pipes & Filters. Этот каркас уп- рощает разработку многоуровневых/модульных приложений, которые могут взаимодействовать с помощью двунаправленных модулей обработки. В дан- ной главе описываются следующие классы АСЕ-каркас Streams: Класс АСЕ Описание ACE_Task Единый набор определяемых приложением функций, которые используют сообщения для работы с запросами, ответами, данными и управляющей информацией, и которые могут ставить сообщения в очередь и обрабатывать их последовательно или параллельно. ACE_Module Отдельный двунаправленный модуль обработки приложения, который включает два объекта асе_тэз к — один для «чтения» и один для «записи». ACE_Stream Содержит упорядоченный список взаимосвязанных объектов ACE_Moduie, которые можно использовать для конфигурации и выполнения определяемых приложением многоуровневых служб. Наиболее важные взаимосвязи между классами АСЕ-каркаса Streams при- ведены на рис. 9.1. Эти классы, в соответствии с паттерном Pipes & Filters [POSA1], играют следующие роли: • Классы-фильтры являются обрабатывающими блоками потока данных, которые улучшают, уточняют или преобразуют входные данные. Клас- сы-фильтры в ACE-каркасе Streams реализуются с помощью классов про- изводных от ACE_Task. • Классы-конвейеры символизируют взаимосвязи между фильтрами. Классы-конвейеры в ACE-каркасе Streams реализуются на основе объек- тов ACE_Module, которые включают взаимодействующие объекты ACE_Task, объединяемые с целью создания полноценного двунаправ- ленного ACE_Stream. Определяемые приложением методы соседних объектов-задач взаимодействуют, обмениваясь данными и блоками управляющей информации. АСЕ-каркас Streams обеспечивает следующие преимущества: • Повышение гибкости и повторного использования. Любой ACE_Task может быть подключен к любому ACE_Module, а любой ACE_Module может быть подключен к любому ACE_Stream. Такая гибкость позволя- ет нескольким приложениям использовать существующие модули и объекты-задачи на систематической основе, и все это можно динамиче- ски конфигурировать. • Прозрачное, последовательное развитие. Появляется возможность управлять реализацией функциональности приложений путем добавле- ния, удаления и изменения модулей и задач. Такого рода возможность эволюционного развития функциональности и структуры приложений
АСЕ-каркас Streams 349 является особенно полезной при разработке в «авральных» условиях, на- пример, при экстремальном программировании (Extreme Programming, ХР) [ВесОО]. • Макроуровневая настройка производительности. Приложения можно настраивать, путем изменения сценариев развертывания и свойств среды времени выполнения, избирательно исключая избыточные функции служб или изменяя конфигурации объектов ACE_Task и ACE_Module с целью оптимизации функциональности службы в некотором контексте. • Модульная структура. Структура, реализуемая ACE-каркасом Streams, воплощает положительный опыт проектирования, требующий строгой упорядоченности и минимальной зависимости. Модульные структуры помогают уменьшить сложность каждого из модулей и улучшить реали- зацию сетевых приложений и служб в целом. Кроме того, тщательно продуманные модульные структуры гораздо легче тестировать и доку- ментировать, что облегчает также сопровождение приложений и их по- вторное использование. _______I SYNCH ! ACE Task j<— ________I SYNCH ; ACE_Modula ----- ________; SYNCH ] ACE Stream | Рис. 9.1 Классы АСЕ-каркаса Streams • Простота освоения. АСЕ-каркас Streams содержит интуитивно понят- ную структуру классов, основанную на паттернах и структуре System V STREAMS, как отмечалось в блоке 59. Тем не менее, даже те разработчики, которые незнакомы с System V STREAMS, могут быстро ознакомиться со структурой паттерна Pipes & Filters, а затем использовать указанный пат- терн как основу для изучения остальных классов АСЕ-каркаса Streams. Блок 59: Взаимосвязь АСЕ Streams и System V STREAMS Имена и структура классов АСЕ-каркаса Streams аналогичны соответствую- щим элементам System V STREAMS (Rit84, Rag93). Методы, используемые для поддержки расширяемости и параллелизма в этих двух каркасах, тем не ме- нее, значительно отличаются. Например, определяемые приложением функ- ции в System V STREAMS добавляются с помощью таблиц указателей на С-функ- ции, тогда как в ACE-каркасе Streams они добавляется путем порождения классов от ACE_Task, что обеспечивает более высокую типовую безопасность и лучшую расширяемость. АСЕ-каркас Streams использует также АСЕ-каркас Task с целью улучшения механизмов параллелизма, основанных на сопро- граммах. Такие механизмы используются в System V STREAMS. Эти усовершен- ствования АСЕ, позволяют более эффективно использовать несколько процес- соров (CPU) на многопроцессорных платформах с общей памятью (SS95b) за счет снижения вероятности тупиков (deadlock) и упрощения управления пото- ком между активными объектами ACE_Task В ACE_Stream.
350 Глава 9 Класс ACE_Task рассматривался в главе 6. Поэтому в данной главе мы, в основном, сосредоточимся на описании возможностей классов ACE_Module и ACE_Stream. Тем не менее, мы приведем пример использования всех клас- сов АСЕ-каркас Streams при разработке вспомогательной программы, которая форматирует и выводит на печать регистрационные записи, хранящиеся на сервере регистрации. Если вы незнакомы с паттерном Pipes & Filters [POSA1], мы советуем, прежде чем разбирать примеры этой главы, ознакомиться с ним. 9.2 Класс ACE_Module Обоснование Многие сетевые приложения могут быть представлены в виде иерархиче- ски упорядоченного набора модулей обработки, где соседние модули обмени- ваются между собой сообщениями. Например, стеки протоколов уровня ядра [Rit84, Rag93] и пользовательского уровня [SS95b, HJE95], менеджеры центров обработки вызовов [SS94] и другие семейства сетевых приложений могут с вы- годой использовать дизайн с обменом сообщениями на базе многоуровне- вой/модульной архитектуры служб. Как упоминалось в разделе 2.1.4, каждый модуль представляет собой независимую составляющую службы сетевого при- ложения (например, ввод или вывод, анализ и фильтрация событий, или при- кладная обработка). Класс ACE_Tas к предоставляет повторно используемый компонент, кото- рый можно использовать для разделения обработки на этапы и передачи дан- ных по этим этапам. Однако поскольку объекты ACE_Task являются незави- симыми, требуется дополнительная структура для упорядочения объектов ACE_Task в двунаправленные пары «читатель-писатель», которые могут быть собраны и могут управляться как единое целое. Разрабатывать каждый раз эту структуру заново в каждом проекте — и трудно, и необязательно, поскольку эта структура имеет фундаментальный характер и не зависит от приложений. Чтобы освободить разработчиков от этой трудной и необязательной работы, в ACE-каркасе Streams определен класс ACE_Module. Функциональные возможности класса ACE_Module представляет собой выделенный слой функциональности, реализуемой приложением. Данный класс предлагает следующие функцио- нальные возможности: • Каждый ACE_Module представляет собой определяемый приложением двунаправленный модуль обработки, в который входят две задачи, чте- ния и записи, производные от ACE_Tas k. ACE_Module естественно впи- сывается в многоуровневые структуры, что упрощает разработку, обуче- ние и развитие. • АСЕ-каркас Service Configurator поддерживает динамическое создание объектов ACE_Module, которые могут объединяться в ACE_Stream во
АСЕ-каркас Streams 351 время выполнения. Таким образом, многоуровневые структуры на базе ACE_Module обладают хорошей расширяемостью. • Объекты ACE_Tas.k, читатель и писатель, которые входят в ACE_Modu- 1е, взаимодействуют с соседними объектами ACE_Tas к, передавая сооб- щения с помощью открытых (public) hook-методов, что ослабляет зави- симость и упрощает реконфигурацию. • Объекты, которые входят в ACE_Module, могут независимо изменяться и заменяться, что уменьшает затраты на сопровождение и модерниза- цию. Интерфейс ACE_Module приведен на рис. 9.2, а его основные методы пере- числены в следующей таблице: | Метод Описание | ACE_Module О 1 open () Инициализирует модуль и выделяет ему ресурсы. ~ACE_Module() close () Удаляет модуль и освобождает его ресурсы. reader () writer () Устанавливает/получает (set/get) задачу-читатель и задачу-писатель. name () Устанавливает/получает имя модуля. i SYNCH • ACK_Module - next_ : ACE_Module<SYNCH> * - flags_ : int - name : ACE TCHAR [] + ACE_Module (name : const ACE_TCHAR *, writer : ACE_Task<SYNCH> * - 0, reader : ACE_Task<SYNCH> * - 0, args : void * “ 0, flags : int - M_DELETE) + open (name : const ACE_TCHAR *, writer : ACE_Task<SYNCH> * - 0, reader : ACE_Task<SYNCH> * = 0, args : void * « 0, flags : int - M_DELETE) : int + close (flags : int - M_DELETE_NONE) : int + reader (t : ACE_Task<SYNCH> *) + reader () : ACE_Task<SYNCH> * + writer (t : ACE_Task<SYNCH> *, flags : int - M_DELETE_WRITER) + writer () : ACE_Task<SYNCH> * + name (const ACE_TCHAR *) + name () : const ACE TCHAR * _______I synch• АСЯ_Т>«к~у~' Рис. 9-2 Класс АСЕ Module
352 Глава 9 Класс ACE_Task, рассмотренный в разделе 6.3, обеспечивает объектно- ориентированную абстракцию обработки, которая может специализироваться под любую прикладную область, например, стеки сетевых протоколов [SW95] или управление центрами обработки вызовов клиентов [SS94]. Механизм ACE_Task, передачи сообщений и постановки их в очередь, обеспечивает про- стой способ разделения прикладной обработки на отдельные этапы и эффек- тивное, двунаправленное распределение работы и данных между ними. Во мно- гих областях этапы обработки данных при чтении и при записи являются сим- метричными. Например, обработка протоколов часто вюцрчает симметричные задачи верификации и применения преобразований, связанных с защитой, на- пример, шифрования. ACE_Module предоставляет унифицированный и гиб- кий механизм композиции, который объединяет экземпляры создаваемых приложением объектов ACE_Task в двунаправленный блок с двумя входами: • Вход-читатель, для обработки сообщений, поступающих на вход чтения (считывания) модуля ACE_Module. • Вход-писатель, для обработки сообщений, поступающих на вход записи (вывода) модуля ACE_Module. Две задачи, составляющие двунаправленный модуль называются «.близне- цами» (siblings). В случае с шифрованием, например, задача-читатель занима- лась бы верификацией и дешифрованием принятых данных, а ее близнец, зада- ча-писатель, до записи данных, осуществляла бы их шифрование. ACE_Modu- 1е формировался бы из этих задач и соответствующим образом встраивался бы в конвейер. Модуль- считывания записей Модуль- форматирбвания записей Модуль- сепаратор записей Модуль- вывода записей Рис. 9-3 Структура ACE_Stream для программы display_logf ile
АСЕ-каркас Streams 353 В тех случаях, когда модуль активно обрабатывает данные только в одном направлении, неактивная задача-близнец может быть задана NULL-указателем, что приводит к тому, что ACE_Module устанавливает ACE_Thru_Task, кото- рая просто ретранслирует все сообщения задаче следующего модуля, не моди- фицируя их. Такой дизайн сохраняет многоуровневую структуру даже тогда, когда уровни переупорядочиваются, добавляются или удаляются. Пример Данный пример посвящен разработке вспомогательной программы, кото- рая называется display_logf ile и которая считывает регистрационные за- писи, сохраняемые серверами регистрации, форматирует данные записей и вы- водит их на печать в удобном для восприятия человека виде. Как отмечалось в [С-H-NPvl], большинство полей регистрационной записи хранится в виде CDR-кода в двоичном формате, компактном, но трудном для восприятия чело- веком виде. Для создания программы display_logf ile, мы реализуем сле- дующие, производные от ACE_Module, классы: • Logrec Reader (модуль считывания регистрационных записей) — являет- ся модулем, который преобразует регистрационные записи в регистра- ционный файл (logfile) в стандартном формате, который затем передает- ся другим модулям и обрабатывается ими в ACE_Streatn. • Logrec Formatter (модуль форматирования регистрационных запи- сей) — является модулем, который определяет как будут форматирова- ны поля регистрационных записей, например, путем преобразования этих полей из двоичного формата в ASCII. • Logrec Separator (модуль-сепаратор регистрационных записей) — явля- ется модулем, который включает блоки данных в составные блоки реги- страционных записей. • Logrec Writer (модуль вывода регистрационных записей) — является мо- дулем, который выводит форматированные блоки регистрационных за- писей на стандартный вывод, из которого они могут быть перенаправле- ны в файл, на принтер или на консоль. Рис. 9.3 иллюстрирует структуру модулей программы display_logf ile, объединенных в ACE_Stream. Эта программа использует модель параллелиз- ма производитель/потребитель (producer/consumer), в которой Logrec_Rea- der и Logrec_Writer работают как активные объекты, которые производят и потребляют сообщения, соответственно. Рис. 9.3 показывает также структуру составных блоков ACE_Message_Block, создаваемых модулем Logrec Reader и обрабатываемых другими модулями-фильтрами ACE_Stream. В Примере, приведенном ниже, мы наследуем от ACE_Task и ACE_Module для создания модулей Logrec Reader, Logrec Formatter, Logrec Separator и Log- rec Writer. Для упрощения многих, приведенных ниже, примеров используют- ся следующие шаблон Logrec_Module и макрос LOGREC_MODULE:
354 Глава 9 template <class TASK> class Logrec_Module : public ACE_Module<ACE_MT_SYNCH> { public: Logrec_Module (const ACE_TCHAR *name) : ACE_Module<ACE_MT_SYNCH> (name, &task_,// Инициализируем задачу-писатель. О, // Игнорируем задачу-читатель. О, ACE_Module<ACE_MT_SYNCH>::M_DELETE_READER) {} private: TASK task_; }; ♦ define LOGiREC_MODULE (NAME) \ typedef Logrec_Module<NAME> NAME##_Module Так как поток данных является однонаправленным (от Logrec Reader к Log- rec Writer), то в конструкторе ACE_Module мы инициализируем только зада- чу-писатель. В случае передачи null-указателя в параметр задачи-читателя, кон- структор ACE_Module создает экземпляр ACE_Thru_Task, который будет ретранслировать данные, не модифицируя их. Флаг M_DELETE_READER ука- зывает деструктору ACE_Module, что нужно удалить только задачу-читатель, но не задачу-писатель. Logrec_Reader_Module. Данный модуль содержит объект-задачу Log- re c_Readeг, который выполняет следующие действия: 1. Открывает указанный регистрационный файл. 2. Преобразует экземпляр Logrec_Reader в активный объект. 3. Преобразует регистрационные записи в регистрационном файле в цепочку блоков данных, каждый из которых включает поле регистрационной запи- си после демарщалинга, которая затем обрабатывается конвейером моду- лей ACE_Stream. Класс Logrec_Reader приведен ниже: class Logrec_Reader : public ACE_Task<ACE_MT_SYNCH> ( private: ACE_TString filename_; // Имя per.файла. ACE_FILE_IO logfile_; // Файл c per.записями. public: enum {MB_CLIENT = ACE_Message_Block::MB_USER, MB_TYPE, MB_PID, MBJTIME, MB_TEXT); Logrec_Reader (const ACE_TString &file): filename_ (file) (} // ... Остальные методы приведены ниже ... }; Мы определяем пять констант перечисления (enumerators) для идентифи- кации полей регистрационной записи. Вместо того чтобы сохранять индикатор
АСЕ-каркас Streams 355 поля в самих данных, мы используем член данных типа ACE_Message_Block для индикации типа полей. ACE_Message_Block определяет два диапазона значений типа — обычное (normal) и приоритетное (priority) — с несколькими значениями в каждом из диапазонов. Он определяет также третий диапазон для типов сообщений, задаваемых пользователем. Мы инициализируем МВ_СЫ- ENT значением ACE_Message_Block: :MB_USER, которое представляет со- бой первое из значений типа, определяемых пользователем, для которого га- рантируется отсутствие конфликтов с другими значениями, определяемыми самой АСЕ. Ноок-метод Logrec_Reader: :ореп() открывает указанный регистра- ционный файл и преобразует задачу в активный объект: virtual int open (void *) { ACE_FILE_Addr name (filename_.c_str ()); ACE_FILE_Connector con; if (con.connect (logfile_, name) == -1) return -1; return activate (); ) Logrec_Reader: :svc () выполняется в потоке активного объекта. Он считывает регистрационные записи из файла, осуществляет их демаршалинг, сохраняет каждую запись в составном блоке данных и передает каждый состав- ной блок данных далее по конвейеру для дальнейшей обработки каждым из мо- дулей. Регистрационный файл записывается как последовательность регистра- ционных записей, каждая из которых состоит из: • Строки, которая содержит имя клиента, пославшего эту регистрацион- ную запись. • CDR-код ACE_Log_Record (код маршалинга записи был приведен в [C-H-NPvl]). Каждая регистрационная запись следует за предыдущей без маркера разде- ления. Как показано ниже, Logrec_Reader: : svc () читает содержимое фай- ла большими порциями и осуществляет их демаршалинг как потока данных. В цикле for читается содержимое файла, пока не будет достигнут EOF. Внут- ренний цикл for осуществляет демаршалинг регистрационных записей из ука- занных порций данных. 1 virtual int svc () { 2 const size_t FILE_READ_SIZE = 8 * 1024; 3 ACE_Message_Block mblk (FILE_READ_SIZE); 4 5 for (;; mblk.crunch ()) ( 6 ssize_t bytes_read = logfile_.recv (mblk.wr_ptr (), 7 mblk.space () ); 8 if (bytes_read <= 0) break; 9 mblk.wr_ptr (ACE_static_cast (size_t, bytes_read)); 10 for (;;) { 11 size_t name_len = ACE_OS_String::strnlen
336 Глава 9 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 (mblk.rd_ptr ()f mblk.length ()); if (name__len == mblk. length ()) break; char *name_p = mblk.rd_ptr () ; ACE_Message_Block *rec = 0, *head = 0, *temp = 0; ACE_NEW—RETURN (head, ACE_Message_JBlock (name_len, MB_CLIENT) , 0) ; head->copy (name_p, name__len) ; mblk.rd_ptr (name_len + 1); // Пропускаем также nul. size_t need = mblk.length () + ACE_CDR::MAX_ALIGNMENT; ACE_NEW_RETURN (rec, ACE_Message_Block (need), 0) ; ACE_CDR::mb_align (rec); rec->copy (mblk.rd_ptr (), mblk.length ()); ACE_InputCDR cdr (rec); rec->release (); ACE—CDR: :Boolean byte_order; if (! cdr. read_boolean (byte—order) ) { head->release (); mblk.rd—ptr (name_p) ; break; } cdr.reset_byte_order (byte_order); ACE—CDR::ULong length; if (!cdr.read—ulong (length)) { head->release (); mblk.rd—ptr (name_p); break; } if (length > cdr.length ()) { head->release (); mblk.rd—ptr (name_p); break; } ACE—NEW—RETURN (temp, ACE—Message—Block (length, MB—TEXT), 0); ACE-NEW_RETURN (temp, ACE—Message—Block (2 * sizeof CACE—CDR: :Long) , MB-TIME, temp), 0); ACE-NEW-RETURN (temp, ACE-Message—Block (sizeof (ACE_CDR::Long) , MB-PID, temp), 0); ACE_NEW-RETURN (temp, ACE—Message—Block (sizeof (ACE—CDR::Long), МВ-TYPE, temp), 0); head->cont (temp); // Извлекаем тип... ACE-CDR:: Long *lp = ACE-reinterpret-Cast (ACE-CDR::Long *, temp->wr_ptr ()); cdr » *lp; temp->wr—ptr (sizeof (ACE—CDR::Long)); temp = temp->cont (); // Извлекаем PID...
АСЕ-каркас Streams 357 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 } Ip = ACE_reinterpret_cast (ACE_CDR::Long *, temp->wr_ptr ()); cdr » *lp; temp->wr_ptr Isizeof (ACE_CDR: .-Long) ) ; temp = temp->cont () ; // Извлекаем отметку времени... Ip = ACE_reinterpret_cast (ACE-CDR::Long *, temp->wr_ptr ()); cdr » *lp; ++lp; cdr » *lp; temp->wr_ptr (2 * sizeof (ACE_CDR::Long)); temp = temp->cont (); // Извлекаем размер текста, а затем сам текст. ACE_CDR::ULong text_len; cdr » text_len; cdr.read_char_array (temp->wr_ptr ()f text_len); temp->wr_ptr (text_len); if (put_next (head) == -1) break; mblk.rd_ptr (mblk.length () - cdr.length ()); ACE_Message__Block *stop = 0; ACE_NEW_RETURN (stop, ACE_Message_Block (0, ACE_Message_Block::MB_STOP) , 0) ; put_next (stop); return 0; Строки 5-9 Начинаем цикл, который считывает содержимое файла в ACE__Message__Block, используя метод space (), чтобы определить, сколько свободного пространства имеется в данном блоке. Указатель-записи (write pointer) блока обновляется, чтобы отразить изменение в данных. Заключительная инст- рукция цикла for использует метод ACE_Message_Block: : crunch (), что- бы сдвинуть данные в mblk в начало буфера данных блока так, чтобы было ме- сто для дополнительных данных. Строка 10-20 Начинаем цикл демаршалинга регистрационной записи. Ис- пользуем метод ACE_OS_String: : strnlen (), чтобы определить длину строки с именем хоста, но просматриваем только символы, остающиеся в шЫ к. Если в mblk не все имя, мы выходим из цикла, чтобы считать дополнительные данные из файла. Если имя находится там, мы запоминаем указатель на начало имени (name__p) на тот случай, если нам потребуется получить дополнитель- ные данные и повторно начать демаршалинг. Выделяется память под заголовок ACE_Message_Block, чтобы запомнить имя; этот блок будет первым в це- почке блоков данных, которая передается вверх по конвейеру. Строки 22-25 Указатель-чтения (read pointer) mblk находится теперь у на- чала CDR-кода ACE_Log_Record. Напомним, что в соответствии с изложен- ным в главе 4 [C++NPvl], буфер, из которого ACE-классы CDR будут осущест-
358 Глава 9 влять демаршалинг данных, должен быть правильно выровнен. Текущее вы- равнивание указателя-чтения mblk неизвестно, и вряд ли соответствует тому, что должно быть. Поэтому мы создаем новый ACE_Message_Block доста- точно большой, чтобы вместить остающиеся в mblk данные, плюс необходи- мое количество байтов для правильного CDR-выравнивания. После вызова ACE_CDR: :mb_align() с целью выравнивания указателя-чтения нового бло- ка, мы копируем остальные данные файла. Строки 27-32 Создаем объект АСЕ_1 при tCDR для демаршалинга содержи- мого регистрационной записи из гес. Поскольку конструктор ACE_InputCDR инкрементирует счетчик ссылок на гес, мы вызываем rec->release (), что- бы освободить нашу ссылку на гес и предотвратить утечку памяти. Первым элементом, для которого осуществляется демаршалинг, является индикатор порядка байтов. Он используется для восстановления порядка байтов cdr так, чтобы можно» было правильно осуществить демаршалинг остальных данных. Строки 34-39 Заголовок, записанный при маршалинге ACE_Log_Record содержит длину поля, означающую количество байтов в регистрационной за- писи после маршалинга. Осуществляется демаршалинг этого значения и оно сравнивается с количеством байтов, остающихся в объекте cdr. Если присутст- вуют не все требующиеся байты, освобождаем блок с именем клиента (head), восстанавливаем указатель-чтения mblk, чтобы снова начать с имени хоста на следующем проходе демаршалинга, и выходим из цикла демаршалинга записи, чтобы прочитать дополнительные данные из файла. Строки 41-55 Выделяем блоки данных для всех остающихся полей регист- рационной записи. Каждому блоку назначается соответствующий тип для пра- вильной идентификации в модулях при продвижении по конвейеру. Блоки создаются в обратном порядке, чтобы упростить добавление указателя cont. Поэтому, после выделения последнего блока, он становится первым блоком це- почки, сразу после заголовка, и присоединяется к нему с помощью указателя продолжения. Строки 57-61 Осуществляем демаршалинг поля типа в первый блок дан- ных путем приведения указателя-чтения блока данных к ACE_CDR: :Long * и используем оператор ввода CDR для демаршалинга поля типа. Смещаем ука- затель-записи блока в соответствии с только что прочитанными данными и пе- реходим к следующему блоку цепочки. Строки 63-73 Тем же образом осуществляем демаршалинг ID процесса и отметки времени. Строки 75-78 При маршалинге данных регистрационной записи осуществ- ляется подсчет количества байтов последовательности. Выполняем демарша- линг длины последовательности и затем демаршалинг самих байтов в послед- ний блок данных цепочки. Строка 80 Используем put_next () для передачи блока данных следующе- му модулю конвейера для дальнейшей обработки. Как работает put_next () по- ясняется в блоке 60. Строка 81 Сдвигаем указатель-чтения блока с содержимым файла вверх, за все данные, которые только что были извлечены. Объект cdr уже отрегулиро- вал свои внутренние указатели по результатам операций CDR так, что метод
АСЕ-каркас Streams 359 Блок 60: Методы ACE.Task, связанные с АСЕ-каркасом Streams ACE_Task, рассмотренный в разделе 6,3, включает, кроме всего прочего, сле- дующие методы, которые можно использовать вместе с АСЕ-каркасом Streams: Метод Описание module() Возвращает указатель на модуль задачи, если он существует, иначе 0. next () sibling() Возвращает указатель на следующую задачу конвейера, если такая существует, иначе 0. _ _ Возвращает указатель на задачу-близнец модуля. put_next () Передает блок данных соседней задаче конвейера. | can_put () Возвращает 1, если блок данных можно поставить в очередь с помощью put_next () без блокирования из-за внутреннего потока управления конвейером, или 0. reply() Передает блок данных соседней задаче-близнецу конвейера, что позволяет задаче изменить направление передачи сообщения в конвейере на противоположное. ACE_Task, который является частью ACE_Module может использовать метод put_next () для ретрансляции блока данных соседнему модулю. Данный ме- тод отслеживает нужную задачу по указателю next () модуля, а затем вызыва- ет ее hook-метод pu t(), передавая ему блок данных. Метод put () заимствует поток той задачи, которая вызвала put_next (), Если задача выполняется как активный объект, ее метод put () может ставить сообщение в очередь сооб- щений задачи и разрешать ее hook-методу svc () обрабатывать сообщения параллельно по отношению к любой другой обработке, выполняемой конвей- ером. В блоке 62 перечислены модели параллелизма, поддерживаемые АСЕ-каркасом Streams. length () показывает, сколько данных еще осталось. Так как начальная длина была равна mblk.length () (которая не регулировалась на протяжении всего демаршалинга), мы можем определить, сколько исходных блоков данных было израсходовано. Строки 85-89 Обработан весь файл, поэтому отправляем ACEJMessa- ge_Block нулевого размера и типа MB_STOP вниз по конвейеру. В соответст- вии с соглашением, этот блок данных является для других модулей конвейера командой завершения обработки. Поскольку имя регистрационного файла передается конструктору Log- rec_Reader_Module, мы не можем использовать макрос LOGREC_MODULE. Вместо этого, мы определяем класс в явном виде, как показано ниже: class Logrec_Reader_Module : public ACE_Module<ACE_MT_SYNCH> { public: Logrec_Reader_Module (const ACE_TString &filename) : ACE_Module<ACE_MT_SYNCH>
JSo Глава 9 (АСЕ_ТЕХТ ("Logrec Reader"), &task_, // Инициализируем задачу-писатель. О, // Игнорируем задачу-читатель. О, АСЕ Module<ACE_MT_SYNCH>::MJ)ELETE_READER), task__ (filename) {} private: // Преобразуем регистрационный файл в цепочки блоков данных. LogreC—Reader task_; }; LogrecJFormatter_Module. Данный модуль содержит задачу Logrec_For- matter, которая определяет, каким образом будут форматироваться поля ре- гистрационной записи: class LogreC-Formatter : public ACE-Task<ACE-MT-SYNCH> { private: typedef void (*FORMATTER[5])(ACE—Message—Block *); static FORMATTER format—; // Массив статических методов // форматирования. public: Характеристика (trait) синхронизации для модулей конвейера и задач уста- навливается тем ACE_Stream, в который они входят. Так как модули и задачи, определенные выше, используют ACE_MT_SYNCH, Logrec_Formatter также Порождается от ACE_Task<ACE_MT_SYNCH>. Он выполняется, тем не менее, как пассивный объект, так как Logrec_Formatter никогда не вызывает ac- tivate О > а его метод pu t () с целью форматирования данных заимствует по- ток у вызывающей его стороны: virtual int put (ACE-Message-Block *mblk, ACE-Time_Value *) { if (mblk->msg-type () == Logrec_Reader::MB-CLIENT) for (ACE-Message-Block *temp = mblk; temp != 0 ; temp = temp->cont ()) ( int mb—type = temp->msg-type () - ACE_Message_Block::MB_USER; (*format-[mb-type])(temp); return put-next (mblk); ) Метод put () определяет тип данных ACE-Message_Block, передавае- мых модулю. Если типом данных является LogreC-Reader: :MB_CLIENT, тогда это первый блок цепочки с полями регистрационной записи. В этом слу- чае, метод проходит по полям записи, вызывая соответствующие статические методы для преобразования полей в удобный для восприятия человеком фор- мат. После того как все поля отформатированы, блок данных передается сле- дующему модулю конвейера. Если тип передаваемого блока не Logrec_Re-
АСЕ-каркас Streams 361 ader: : MB_CLIENT, то считается, что это блок MB_STOP, и он просто ретранс- лируется следующему модулю конвейера. Следующие статические методы форматируют соответствующие их типу поля. Начнем с f ormat_client (): static void format_client (ACE_Message_Block *) ( return; } В блоке MB_CLIENT есть текстовая строка известной длины. Поскольку для последующего форматирования она не нужна, forma t_client () просто воз- вращает управление. Заметьте, что указанный текст представлен в ASCII-коде. Любая обработка, необходимая для преобразования в формат «широких» сим- волов или для проверки имени путем извлечения IP-адреса, может быть вклю- чена здесь, не нарушая работы в остальной части конвейера. Мы используем следующий метод format_long () для преобразования типа регистрационной записи и ID процесса в ASCII-код: static void format_long (ACE_Message_Block *mblk) ( ACE_CDR::Long type = * (ACE_CDR::Long *)mblk->rd_ptr (); mblk->size (11);// Макс, размер 32-х разрядного слова в ASCII. mblk->reset О; mblk->wr_ptr ((size_t) sprintf (mblk->wr_ptr (), "%d", type)); ) Вызов метода s i z e () проверяет, что в блоке данных достаточно места для хранения текстового представления соответствующего значения, и, если нуж- но, перераспределяет память. Вызов метода reset () восстанавливает указа- тель-чтения и указатель-записи блока данных на начало его буфера в памяти, подготавливая вызов стандартной С-функции sprintf (), которая фактиче- ски и выполняет форматирование. Функция sprintf () возвращает количест- во символов, использованных при форматировании данного значения, исклю- чая завершающий строку символ нуля. Поскольку блоки данных содержат свой номер, мы не включаем символ нуля в новую длину при обновлении указате- ля-чтения блока данных. Метод f ormat_time () несколько сложнее, так как он преобразует секун- ды и микросекунды временного значения в строку ASCII-символов, следую- щим образом: static void format_time (ACE_Message_Block *mblk) { ACE_CDR::Long secs = * (ACE_CDR::Long *)mblk->rd_ptr (); mblk->rd_ptr (sizeof (ACE_CDR::Long)); ACE_CDR::Long usees = * (ACE_CDR::Long *) mblk->rd_ptr (); char timestamp[26]; // Макс.размер строки ctime_r(). time_t time_secs (secs); ACE_OS::ctime_r (&time_secs, timestamp, sizeof timestamp); mblk->size (26); // Макс.размер строки ctime_r(). mblk->reset (); timestamp[19] = ’\0'; // NUL-символ после времени. timestamp[24] = '\0'; // NUL-символ после даты.
362 Глава 9 size_t fmt_len (sprintf (mblk->wr_ptr (), "%s.%03d %s", timestamp + 4, usees / 1000, timestamp +20) ); mblk->wr_ptr (fmt_len); ) Последний метод format_string() идентичен методу format_cli- ent (), так как он тоже принимает строку, длина которой известна. Повторяем, если строка нуждается в какой-либо обработке, то эта обработка может быть включена здесь. static void format_string (ACE_Message_Block *) { return; } }; Мы инициализируем массив указателей на методы форматирования, сле- дующим образом: Logrec_Formatter::FORMATTER Logrec_Formatter::format_ = { format_client, format_long, format_long, format_time, format_string }; Мы используем указатели на статические методы вместо указателей на не- статические методы, так как здесь не требуется обращаться к состоянию Log- rec_Formatter. Затем мы выполняем макрос LOGREC_MODULE со значением класса Log- rec_Formatter для создания Logrec_Formatter_Module: LOGREC_MODULE (Logrec_Formatter); Logrec_Separator_Module. Данный модуль включает объект Logrec_Se- parator, который вставляет блоки данных между существующими блоками данных составного блока регистрационной записи. Каждый новый блок содер- жит строку-разделитель. class Logrec_Separator : public ACE_Task<ACE_MT_SYNCH> { private: ACE_Lock_Adapter<ACE_Thread_Mutex> lock_strategy_; public: Данный класс является пассивным объектом, поэтому Logrec_Separa- tor: :put () заимствует поток вызывающей стороны, чтобы вставить разде- лители: 1 virtual int put (ACE_Message_Block *mblk, 2 ACE_Time_Value *) { 3 if (mblk->msg_type () == Logrec_Reader::MB_CLIENT) { 4 ACE_Message_Block ‘separator = 0; 5 ACE_NEW_RETURN 6 (separator,
АСЕ-каркас Streams 363 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 } ACE_Message_Block (ACE_OS_String::strlen (”|”) + 1, ACE_Message_Block::MB_DATA, 0,0,0, &lock_strategy_), -1); separator->copy (" I ’’) ; ACE__Message_Block *dup = 0; for (ACE_Message_Block *temp = mblk; temp 1=0;) { dup = separator->duplicate () ; dup->cont (temp->cont () ) ; temp->cont (dup); temp = dup->cont () ; } ACE_Message_Block *nl = 0 ; ACE__NEW_RETURN (nl, ACE_Message_Block (2), 0) ; nl->copy ("\n"); dup->cont (nl); separator->release (); return put_next (mblk); Строка 3 Если тип блока MB_STOP, он ретранслируется следующему моду- ля конвейера. Так как Logrec_Separator является пассивным объектом, по- сле его создания никаких дополнительных действий не требуется. Строки 4-10 Создаем блок данных для хранения строки-разделителя, кото- рая задается как « I » (более гибкие реализации могли бы сделать строку-разде- литель параметром конструктора класса). Позже в этом же методе, мы будем использовать метод ACE_Message_Block: : duplicate (), который возвра- щает квази-копию («shallow» сору) данных, которая просто инкрементирует счетчик ссылок на единицу, но не создает реальной копии самих данных. В бло- ке 61 поясняется, как и почему мы конфигурируем разделитель ACE_Messa- ge_Block со стратегией блокировки ACE_Lock_Adapter<ACE_Thre- ad__Mutex>. Строки 12-18 Выполняем цикл по списку блоков данных, чтобы вставить дубликат разделителя, который включает строку-разделитель между каждым из исходных блоков данных. Строки 19-22 После того как разделители вставлены, добавляется послед- ний блок, включающий символ новой строки (newline) для правильного фор- матирования при окончательной записи в стандартный вывод. Строки 23-26 Блок, из которого копировались блоки-разделители, освобо- ждается, и скорректированное составное сообщение передается следующему модулю конвейера. Теперь мы выполняем макрос LOGREC_MODULE с классом Logrec_Sepa- rator для создания Logrec_Separator_Module: LOGREC_MODULE (Logrec_Separator);
364 Глава 9 Блок 61: Сериализация счетчиков ссылок ACE Message Block Если квази-копии (shallow copies) блока данных создаются и/или освобождаются в разных потоках, существует потенциальная возможность возникновения состоя- ния гонок при обращении к счетчику ссылок и общим данным. Следовательно, доступ к этим данным должен быть сериализован. Так как имеется несколько блоков данных, то применяется стратегия внешней блокировки. Следовательно, блок данных может быть ассоциирован с экземпляром ACE_Lock_Adapter. Logrec_Separator: :put () обращается к блокам данных из разных потоков, поэтому ACE_Lock_Adapter параметризуется посредством ACE_Thread_Mu- tex, Данная стратегия блокировки сериализует вызовы к методам блока дан- ных duplicate () и release (), чтобы исключить состояния гонок при парал- лельном создании и освобождении блока данных разными потоками. Хотя Logrec_Separator : : put () вызывает separator->release () перед тем, КОК передавать блок данных следующему модулю, мы предпринимаем эту меру предосторожности, так как, возможно, модуль, позднее включенный в данный конвейер будет обрабатывать блоки, используя несколько потоков. Logrec_Wnter_Module. Данный модуль включает задачу Logrec_Wri- ter, которая выполняет следующие действия: • Преобразует экземпляр Logrec_Writer в активный объект. • Принимает форматированные данные регистрационных записей, переда- ваемых соседним модулем и выводит их на свое стандартное устройство вывода. Класс Logrec_Writer приведен ниже: class Logrec_Writer : public ACE_Task<ACE_MT_SYNCH> { public: // Hook-метод инициализации. virtual int open (void *) { return activate (); ) Ноок-метод open () преобразует Logrec_Writer в активный объект. Ос- тальные два метода этого класса используют тот факт, что экземпляры его про- изводного класса ACE_Task создаются с классом характеристик (traits) ACE_MT_SYNCH. Например, Logrec_Writer: :put () ставит сообщения в свою синхронную очередь сообщений: virtual int put (ACE_Message_Block *mblk, ACE_Time_Value *to) ( return putq (mblk, to); } Аналогично, Logrec_Writer: : svc () выполняется в потоке своего ак- тивного объекта, исключая из свой очереди сообщения и записывая их в стан- дартный вывод: virtual int svc О ( int stop = 0; for (ACE_Message_Block *mb; Istop && getq (mb) != -1; ) ( if (mb->msg_type () == ACE_Message_Block::MB_STOP) stop = 1;
АСЕ-каркас Streams 365 else ACE::write_n (ACE_STDOUT, mb); put_next (mb); } return 0; ) }; При получении блока MB_STOP, метод выходит из цикла обработки и воз- вращает управление, завершая работу потока. Метод АСЕ:: write_n () выво- дит все остальные блоки данных, связанные своими указателями cont (), ис- пользуя эффективную операцию записи-со-слиянием. Все блоки данных пере- даются следующему модулю конвейера. По умолчанию, АСЕ-каркас Streams освобождает все блоки данных, которые исключаются из конвейера обработки данных, как отмечалось при рассмотрении ACE_Stream_Tail. В заключение, мы создаем реализацию макроса LOGREC_MODULE с классом Logrec_Writer для создания Logrec_Writer_Module: LOGREC_MODULE (Logrec_Writer); Пример в разделе 9.3 показывает, каким образом модули, представленные выше, могут быть объединены в ACE_Stream для создания законченной про- граммы display_logfile. 9.3 Класс ACE Stream Обоснование Класс ACE_Module, рассмотренный в разделе 9.2, может быть использован для декомпозиции сетевого приложения на ряд взаимосвязанных модулей, вы- полняющих разные функции. Каждый модуль реализует отдельный слой функциональности приложения, например, чтение, форматирование, вставку разделителей и вывод регистрационных записей. ACE_Module реализует мето- ды для передачи данных между задачами-близнецами внутри модуля, а также между модулями. Тем не менее, он не предоставляет средств для объединения модулей в некоторый упорядоченную структуру или для их переупорядочения. С целью предоставить разработчикам возможность объединять ряд иерархиче- ски упорядоченных модулей в один объект и управлять этим объектом, АСЕ- каркас Streams определяет класс ACE_Stream. Функциональные возможности класса ACE_Streatn реализует паттерн Pipes & Filters [POSA1], чтобы дать разра- ботчикам возможность создавать и исполнять иерархически взаимосвязанные службы путем настройки повторно используемых, независимых от приложе- ния, каркасных классов. Данный класс предоставляет следующие функцио- нальные возможности:
ЗЪ6 Глава 9 • Предоставляет методы для динамического включения, замены и удале- ния объектов ACE_Module с целью формирования различных конфигу- раций конвейера. • Предоставляет методы для передачи/получения (send/receive) сообще- ний в/из ACE_Stream. • Обеспечивает механизм соединения в единое целое двух конвейеров ACE_Stream. • Обеспечивает способ завершения работы всех модулей конвейера и воз- можность ожидания завершения работы всех модулей. Интерфейс для ACE_Stream приведен на рис.9.4. Поскольку данный класс предоставляет (экспортирует) многие из возможностей АСЕ-каркаса Streams, мы группируем описание его методов на три категории, перечисляемые ниже. 1. Методы инициализации и удаления конвейеров. Следующие методы инициализируют и удаляют ACE_Stream: Метод Описание ACE_Stream() open() Инициализирует конвейер. ~ACE_Stream() close () Удаляет конвейер, освобождая его ресурсы и удаляя каждый модуль. wait () Осуществляет синхронизацию с завершающим закрытием конвейера. По умолчанию, два создаваемых АСЕ объекта ACE_Module помещаются в ACE_Stream при его инициализации, один в начале (head) конвейера, другой в конце (tail). Начальный (головной) модуль содержит объекты ACE_Stre- am_Head на входе-читателе и На входе-писателе. Аналогично, конечный (хво- стовой) модуль конвейера на входе-читателе и на входе-писателе содержит объекты ACE_Stream_Tail. Эти два класса интерпретируют предопределен- ные управляющие сообщения и блоки сообщений с данными, которые могут передаваться по ACE_Stream во время выполнения. Класс ACE_Messa- ge_Block определяет типы этих блоков данных как перечислительные типы (enumerators), все значения которых меньше, чем MB_USER. ACE_Stream_Head может ставить в очередь сообщения, передаваемые между приложением и экземпляром ACE_S t г earn. Когда сообщение попадает на вход-писатель модуля ACE_S tream_Head, оно передается следующему модулю конвейера. Когда сообщение попадает на вход-читатель модуля ACE_Stre- am_Head, оно ставится в очередь сообщений задачи (кроме сообщений типа MB_FLUSH, при получении которых очередь завершает свою работу). Чтобы предотвратить утечку ресурсов в том случае, когда сообщения дос- тигают конца конвейера необработанными, ACE_Stream_Tail: :put () ос- вобождает сообщения, которые он принимает, действуя на входе-писателе. Такое поведение позволяет модулю Logrec_Writer передавать свои блоки данных
АСЕ-каркас Streams 367 ------------------------Г SYNCH • ACE_Str«am Г'" + ACE_Stream (arg : void * = 0, head : ACE_Module<SYNCH> * = 0, tail : ACE_Module<SYNCH> * = 0) + open (arg : void * = 0, head : ACE_Module<SYNCH> * = 0, tail : ACE_Module<SYNCH> * = 0) : int + close (flags : int - M_DELETE) : int + wait () : int + push (mod : ACE_Module<SYNCH> *) : int + pop (flags : int « M_DELETE) : int + get (mb : ACE_Message_Block *&, timeout : ACE_Time_Value ★ = 0) : int put (mb : ACE_Message_Block ★&, timeout : ACE Time Value * = 0) : int J synch ; ACE_ModuleГ "head, tail" Рис. 9.4 Класс ACE_Stream следующей задаче. Чаще всего следующей задачей является ACE_Stream_Tail. Тем не менее, если к программе добавить еще один модуль в этой точке конвейера, указанный модуль будет продолжать функционировать, как положено. 2. Методы Конфигурации конвейера. Следующие методы вставляют (push) и извлекают (pop) объекты ACE_Module в/из ACE_Stream: Метод Описание push () Вставляет новый модуль в начало конвейера. pop о Удаляет модуль в начале конвейера и завершает его работу. top () Возвращает указатель на модуль в начале конвейера. insert () Вставляет новый модуль за указанным модулем. replace () Заменяет указанный модуль новым модулем. remove() Удаляет указанный модуль из конвейера и завершает его работу. Методы push () и pop () позволяют приложениям конфигурировать кон- вейер во время выполнения, вставляя и удаляя в его начале (вершине) экземп- ляры ACE_Module. Когда модуль вставляется (pushed) в начало конвейера, hook-методы open () задач на входе-читателе и входе-писателе модуля вызы- ваются автоматически. Аналогично, когда модуль у начала конвейера удаляется (popped), вызываются hook-методы close () задач на входе-читателе и вхо- де-писателе модуля. Поскольку весь конвейер — это последовательность взаимосвязанных, не- зависимых модулей, часто оказывается полезным вставлять, удалять и заме- нять модули в произвольной точке конвейера. Поэтому, чтобы сделать такого
368 Глава 9 рода операции возможными, объекты ACE_Module получают в конвейере имена, а методы insert (), replace () и remove () позволяют манипулиро- вать модулями в конвейере. Объекты ACE_Module могут быть объединены, по существу, в любые конфигурации, которые удовлетворяют требованиям при- ложения и улучшают повторное использование модулей. 3. Методы передачи сообщений. Следующие методы используются для пере- дачи и получения сообщений в ACE_Stream: Метод Описание get о Принимает следующее сообщение, которое запоминается в начале конвейера. put () Передает сообщение вниз по конвейеру, начиная с модуля, расположенного в его начале (вершине). Методы get () и put () позволяют приложениям передавать и принимать сообщения в объекте ACE_Stream. Аналогично ситуации при вставке блоков данных в ACE_Message_Queue, поведение этих методов относительно блоки- рования может быть изменено за счет передачи следующих типов значений ACE_Time_Value: Значение Поведение ыиш-указатель на ACE_Time_Value Указывает, что get () или put () должны ждать без ограничения времени, то есть будут блокированы до завершения работы метода. He-NULL указатель на ACE_Time_Value, методы sec () и usee () которого возвращают 0 Указывает, что get () или put () должны выполнять неблокируемые операции, то есть если метод не завершается сразу же успешно, то они должны возвращать-1 и устанавливать errno В EWOULDBLOCK. He-NULL указатель на ACE_Time_Value, методы sec() И usee () которого возвращают >0 Указывает, что get () или put () должны ждать до указанного времени (абсолютного) суток, возвращая -1 с errno установленным в ewouldblock в том случае, если метод, к указанному времени, не завершил свою работу. Когда блоки данных поступают в ACE_Stream, они могут передаваться по последовательности взаимосвязанных объектов ACE_Task, вызывая методы put_next () (см. блок 60). Блоки данных могут быть из разных источников, таких как поток приложения, реактор или проактор, или ACE-диспетчер очере- ди таймеров. В блоке 62 отмечены модели параллелизма, поддерживаемые АСЕ-каркасом Streams.
АСЕ-каркас Streams 369 Пример Мы завершаем, показывая как сконфигурировать программу dis- play_logfile с объектом ACE_Stream, который включает модули, пред- ставленные в разделе 9.2. В программе, приведенной ниже, создается ACE_Stream, а затем в него вставляются (push) все модули конвейера. int ACE_TMAIN (int argc, ACE_TCHAR *argv[]) { if (argc != 2) ACE_ERROR_RETURN ((LM_ERROR, "usage: %s logfile\n", argv[0]), 1); ACE_TString logfile (argv[l]); ACE_Stream<ACE_MT_SYNCH> stream; if (stream.push (new Logrec_Writer_Module (ACE_TEXT ("Writer"))) != -1 && stream.push (new Logrec_Separator_Module (ACE_TEXT ("Separator"))) != -1 && stream.push (new Logrec_Formatter_Module (ACE_TEXT ("Formatter"))) != -1 Блок 62: Модели параллелизма АСЕ-каркаса Streams В разделах 5 и 6 (C++NPv1) описываются два стандартных типа архитектур па- раллелизма, ориентированных на задачи (tdsk-based)H на сообщения (messa- ge-based) (SS93), которые поддерживаются АСЕ-каркасом Streams. Например, у метода put о есть возможность ставить сообщения в очередь и откладывать их обработку методом задачи svc (), котррый выполняется параллельно в от- дельном потоке. Такой подход можно проиллюстрировать архитектурой ори- ентированной на задачи, приведенной ниже: У метода put () есть и альтернативная возможность заимствовать поток управ- ления у вызывающей стороны, чтобы обрабатывать сообщения немедленно. Такой подход можно проиллюстрировать архитектурой, ориентированной на сообщения, приведенной на. рисунке выше. Архитектура параллелизма, ис- поЛьзуемая конвейером, оказывает существенное влияние на производитель- ность и простоту программирования, как отмечается в (SS95b). 13 Программирование сетевых приложений на C++. Том 2
370 Глава 9 && stream.push (new Logrec_Reader_Module (logfile)) !*= -1) return ACE_Thread_Manager::instance ()->wait () == 0 ? 0 : 1 ; return 1; ) По мере подключения каждого модуля к конвейеру, АСЕ-каркас Streams вызывает hook-метод open () его задачи на входе-писателе, что приводит к инициализации этой задачи. Hook-методы open () модулей LogrecJWri- ter_Module и Logrec_Reader_Module преобразуют свои задачи-писатели в активные объекты. Основной (main) поток фактически не принимает участия в обработке. После успешного подключения всех модулей к конвейеру, он про- сто ждет завершения работы остальных потоков в активных объектах после за- вершения ими обработки регистрационного файла. Функция main (), приведенная выше, создает несколько модулей динами- чески при запуске программы и в процессе обработки регистрационного фай- ла. Мы опускаем большую часть логики обработки ошибок из функции main () с целью экономии места. Хотя вряд ли эта программа во время запуска исчерпает всю память кучи, корректно спроектированное коммерческое при- ложение должно контролировать возможные ошибки и соответствующим об- разом их обрабатывать. Реализация программы display_logf ile с использованием АСЕ-карка- са Streams делает возможным прозрачное, последовательное развитие функ- циональности приложения. Например, можно достаточно просто реализовать и сконфигурировать модуль Logrec Sorter, который изменит порядок, в котором отображаются поля регистрационной записи. Аналогично, АСЕ-каркас Streams уменьшает сложность каждого слоя модулей в программе display_logf ile, тем самым упрощая ее реализацию, тестирование и сопровождение. 9.4 Резюме АСЕ-каркас Streams является реализацией паттерна Pipes & Filters, который использует объектно-ориентированные методы проектирования, АСЕ-каркас Task и возможности языка C++. АСЕ-каркас Streams упрощает включение но- вых функций, или изменение существующих, в ACE_S t ream, без модификации независимых от приложения классов каркаса. Например, включение нового слоя служебных функций в ACE_Stream связано со следующими действиями: 1. Наследование интерфейса ACE_Task и подмена методов open (), close (), put () и svc () в производном от ACE_Task классе с целью реа- лизации функциональности приложения. 2. Создание нового модуля АСЕ JModule, в который входят один или два эк- земпляра определяемых приложением задач ACE_Task, один экземпляр на входе-читателе (reader-side) и один — на входе-писателе (writer-side).
АСЕ-каркас Streams 3. Включение модуля ACE_Module в объект ACE_Stream. В ACE_Stream могут быть включены несколько модулей ACE_Module для создания упо- рядоченной последовательностей иерархически взаимосвязанных функ- ций обработки. АСЕ-каркас Streams позволяет разработчикам создавать многоуровневые, модульные сетевые приложения, которые легко расширять, настраивать, со- провождать и конфигурировать. Более того, совместные возможности карка- сов АСЕ Task, Service Configurator и Streams предоставляют большой набор структур и конфигураций, которые могут быть расширены и настроены на проектные условия, оперативные среды и платформы ОС во всевозможных комбинациях, которым нет числа. 13«

Словарь терминов Callback-объект (callback) Объект, зарегистрированный у диспетчера, осуществляю- щего обратный вызов метода этого объекта в момент насту- пления определенного события. Ноок-метод (hook method) Зарегистрированный в каркасе виртуальный метод, кото- рый служит адресатом обратных вызовов (callbacks), ини- циируемых каркасом. Использование hook-методов являет- ся способом интеграции кода приложения и каркаса. Internet Всемирная «сеть сетей», основанная на протоколе IP (Internet Protocol). Рассматривается многими как самое значительное изобретение человечества после огня и MTV. Unicode Стандарт представления символов, который включает ал- фавиты большинства письменных языков, а также знаки пунк- туации, математические и другие символы. Активно-превентивный ввод/вывод (proactive I/O) Метод ввода/вывода, при котором управление сразу воз- вращается вызывающему потоку, часто до завершения опе- рации. Вызывающий поток может выполнять другую работу, пока ОС параллельно выполняет операцию ввода/вывода. Позднее осуществляется уведомление о завершении опе- рации ввода/вывода. (ср. Взаимно-согласованный ввод/вывод). Активное ожидание (busy wait) Используемый потоком способ ожидания снятия блокиров- ки, который заключается в зацикливании потока и в выполне- нии проверки снятия блокировки на каждой итерации цик- ла, в отличие от ожидания снятия блокировки путем перехо- да в пассивный режим (sleeping), что дает возможность продолжать выполнение другим потокам. Активный объект (active object) Объект, который реализует паттерн Active Object. Такие объекты обычно выполняют запросы на обслуживание в по- токе, отдельном от вызывающего потока (ср. Пассивный объект). Анализ предметной области (domain analysis) Индуктивный, управляемый обратнЫми’связями процесс, связанный с системным исследованием проблемной об- ласти, с целью определения основных проблем и аспектов проектирования, чтобы, опираясь на них, создать эффектив- ные методы решения. Архитектурный паттерн (architectural pattern) Паттерн, выражающий одну из фундаментальных схем структурной организации программных систем. Он предос- тавляет набор предопределенных подсистем, специфици- рует их функции и включает набор правил и руководящих принципов для организации взаимосвязей между указанны- ми подсистемами. Асинхронный ввод/вывод (asynchronous I/O) Механизм передачи данных, при котором инициировавшая операцию ввода-вывода сторона не приостанавливает свою работу и не ждет завершения операции ввода/выво- да.
374 Программирование сетевых приложений на C++. Том 2 Асинхронный маркер завершения (Asynchronous Completion Token, ACT) Аспекты (aspects) Определяемое разработчиком значение, ассоциируемое с асинхронной операцией. Используется для обмена ин- формацией, относящейся к указанной операции, с обра- ботчиком завершения (completion handler) операции. Качественные характеристики программы, такие как управ- ление памятью, синхронизация, отказоустойчивость, кото- рые выходят за границы модулей. Аффинность кэша (cache affinity) Оптимизация планирования потоков, при которой предпоч- тение, при выделении потокам процессорного времени, от- дается самому недавно выполнявшемуся потоку, чтобы уве- личить вероятность того, что информация о его состоянии еще присутствует в кэшах данных и команд процессора. Барьерная синхронизация (barrier synchronization) Механизм синхронизации потоков, который позволяет за- данной группе потоков синхронизировать свою работу в тот момент, когда каждый из этих потоков достигает некоторого состояния, например, завершения общей операции или за- дачи. «Барьер» представляет собой особую точку в выпол- няемой ветви. Каждый поток, достигающий барьерной точки (барьера)' ждет, когда другие потоки также достигнут этой точки. После того как все потоки заданной совокупности достигают барьера, барьер «падает» и все потоки одновре- менно продолжают свое выполнение. Блокировка (lock) Механизм, используемый для реализации разновидности критической секции. Блокировка, которую можно периоди- чески занимать и освобождать, например, статический мьютекс (static mutex), может быть включена в состав клас- са. Если множество потоков пытается одновременно занять блокировку, сможет это сделать только один, а все осталь- ные будут блокированы до момента снятия блокировки. Другие механизмы блокировки, такие как семафоры или блокировки типа «читатели/писатель» (readers/wrlter), опре- деляют другую логику синхронизации. Ввод/вывод с перекрытием (overlapped I/O) Взаимно-согласованный ввод/вывод (reactive I/O) См. асинхронный ввод/вывод. Модель ввода/вывода, при которой приложение уведомля- ется, когда нужная ему операция(и) ввода/вывода на кон- кретном источнике может быть успешно осуществлена. За- тем приложение выполняет нужную операцию(и), одну опе- рацию на одно уведомление, (ср. Активно-превентивный ввод/вывод.) Время ожидания (latency) Голодание (starvation) Задержка выполнения операций. Риск сбоя при выделении процессорного времени, который происходит тогда, когда один или несколько потоков посто- янно вытесняются потоками с более высоким приоритетом и не выполняются вообще. Двойная диспетчеризация (double-dispatching) Этот способ позволяет выполнять методы, в зависимости от типа запроса и типов двух приемников. Объект, который вы- зывает такой метод, передает самого себя приемнику в ка- честве параметра. Приемник может действовать по-разно- му в зависимости от типа вызывающего объекта.
Словарь терминов 375 (цшаршаммя (demarshaljng) Преобразование данных, подвергнутых маршалингу, из стандартного (host-independent) формата, независимого от хоста, во внутренний (host-specific) формат хоста. Демон (daemon) Серверный процесс, постоянно работающий в фоновом режиме показывающий различные услуги по запросу кли- ентов. Демультиплексирование (demultiplexing) Механизм, доставки входных данных из порта ввода к полу- чателям, которым они предназначены. Между входным пор- том и получателями существует связь 1 :N. Демультиплекси- рование обычно применяется к входящим событиям и пото- кам данных. Обратная операция называется мультиплексированием. Демультиплексор синхронных событий (synchronous event demultiplexer) Дескриптор (handle) Механизм (обычно системная функция) который демультип- лексирует события от разного рода источников, делая их обработку частью обычного процесса выполнения прило- жения. Дескриптор —это идентификатор ресурсов, управляемых ядром операционной системы. Эти ресурсы обычно вклю- чают, помимо прочего, сетевые соединения, открытые фай- лы, таймеры и объекты синхронизации. Динамически подклю- чаемая библиотека (Dynamically Linked Library, DLL) Библиотека, совместно используемая несколькими процес- сами, подключаемая к адресному пространству процесса и отключаемая от него динамически, с целью повышения гибкости и расширяемости приложения во время выполне- ния. Домен (domain) Задача (task) Домен в Internet — это элемент системы доменной адреса- ции, например, ucl.edu иди riverace.com. Автономная или полуавтономная часть приложения, кото- рая выполняет некоторую конкретную часть работы прило- жения, например, осуществляет прием и демаршалинг по- ступающих сетевых данных или обрабатывает запросы. За- дача выполняется с помощью одного или нескольких потоков. Запись-Ъо-слиянием (gather-write) Идиома (Idiom) Операция вывода, которая одной операцией передает со- держимое множества независимых буферов данных. Идиома — это низкоуровневый паттерн, характерный для языка программирования. Идиома описывает способ реа- лизации отдельных аспектов компонентов или взаимосвязей между ними, используя возможности данного языка. Идиома Scoped Locking (Scoped Locking idiom) Идиома C++, которая гарантирует, что блокировка будет переведена в состояние «занято» при вхождении потока управления в область действия (scope) и автоматически ос- вобождена при выходе из этой области действия, независи- мо от пути выхода. Инверсия приоритетов (priority Inversion) Сбой системы планирования, который происходит тогда, когда поток или запрос с более низким приоритетом блоки; рует выполнение потока или запроса с более высоким при: оритетом.
376 Программирование сетевых приложений на C++. Там 2 Интеллектуальный' указатель (smart pointer) Интеллектуальный указатель является объектом C++, кото- рый выглядит и действует, как обычный указатель, но может выполнять некоторые дополнительные действия, которые обычные указатели не поддерживают, например, кэширо- вание, долговременное хранение или доступ к локальной памяти потока. Интерфейсный фасад (wrapper facade) Один или несколько классов, которые инкапсулируют функ- ции и данные внутри типобезопасного объектно-ориентиро- ванного интерфейса. Каркас (framework) Качество обслуживания (Quality of Service, QoS) См. Объектно-ориентированный каркас. Совокупность политик и механизмов, созданных для улучше- ния связи и управления ее характеристиками, такими как полоса пропускания, время ожидания и флуктуации (jitter). Компонент (component) Инкапсулированная часть программных средств, реали- зующая определенный сервис или совокупность сервисов. Компонент имеет один или нёсколько интерфейсов, которые обеспечивают доступ к его сервисам. В конструкции систе- мы компоненты являются строительными блоками. На уров- не языка программирования компоненты могут быть пред- ставлены в качестве модулей, классов, объектов или сово- купности взаимосвязанных функций. Компонент, в котором отсутствует реализация элементов его интерфейса, называ- ется абстрактным. Конкретизация (reify) Акт создания конкретного экземпляра некоторой абстракт- ной сущности. Например, конкретная реализация «реакто- ра» (reactor) конкретизирует паттерн Reactor, а объект кон- кретизирует класс. Контейнер (container) Обобщенное название структур данных, которые содержат набор элементов. Примерами контейнеров являются спи- ски, множества и ассоциативные массивы. Кроме того, ком- понентные модели, такие как EJB и ActiveX Controls, опреде- ляют контейнеры, которые обеспечивают оперативную сре- ду, которая скрывает от компонентов детали их базовой инфраструктуры, такой как операционная система. Критическая секция (critical section) Программный код, который не должен выполняться в некото- ром объекте параллельно. С помощью критической секции можно синхронизировать подсистему. Критическая сек- ция -— это последовательность команд, которая подчиняет- ся следующему неизменному правилу: пока один поток или процесс выполняется в критической секции, ни один другой поток или процесс не могут выполняться в этой критической секции. Маршалинг (marshaling) Преобразование набора данных из внутреннего (host-specific) формата хоста в стандартный (host-independent) формат. Механизм синхронизации (synchronization mechanism) Механизм блокировки, координирующий порядок выполне- ния потоков.
Словарь терминов 377 Мьютекс (mutex) Мьютекс — механизм блокировки типа «взаимное исключе- ние» (mutual exclusion), который гарантирует, что в данный" момент времени только один поток является одновременно активным внутри критической секции, чтобы избежать со- стояния гонок. Наследование (inheritance) Свойство объектно-ориентированных языков, которое по- зволяет создавать новые классы на базе существующих. Наследование определяет Повторное использование реа- лизаций, взаимосвязь подтипов, или и то, и другое. В зависи- мости от языка программирования возможно единичное или множественное наследование. Нерекурсивный мьютекс (nonrecurslve mutex) Мьютекс, который должен быть освобожден прежде, чем он будет вновь занят любым потоком, включая тот поток, кото- рый изначально им владеет. Ср. Рекурсивный мьютекс. Обобщенное представление данных (Common Data Representation, CDR) Стандартный формат, определенный в CORBA, для выполне- ния маршалинга и демаршалинга данных*. Он использует стандартное представление «приемник упорядочивает», ко- торое заключается в том, что дополнительная обработка проводится только тогда, когда порядок байтов, принятый отправителем отличается от порядка байтов, принятого по- лучателем. Обобщенное программирование (generic programming) Методика программирования, объединяющая паттерны проектирования и параметризованные типы C++, что дает разработчикам возможность создавать выразительный, гиб- кий, эффективный программный код, специально приспо- собленный для повторного использования. Обработчик завершения (completion handler) Объект, интерфейс которого включает один или несколько hook-методов, созданных для обработки событий заверше- ния. Обработчик события (event handler) Объект, интерфейс которого состоит из одного или несколь- ких hook-методов, которые могут обрабатывать определяе- мые приложением события. Общие службы промежуточного слоя (common middleware' services) Этот уровень иерархии промежуточного слоя определяет службы, независящие от области применения, например, уведомление о событиях, регистрация, организация мульти- медийных потоков данных, долговременность, безопас- ность, привязка к глобальному времени, планирование в ре- альном времени и управление распределенными ресурса- ми, отказоустойчивость, управление параллельным выполнением операций и обратимые транзакции, — кото- рые выделяют, планируют и координируют различные ре- сурсы всей распределенной системы. Объект-функция (function object) Объектно-ориентирован- ный каркас (object-oriented framework) Объект, который можно вызывать так, как если бы он был функцией. Известен также как функтор (Functor). Объединенная группа классов, совместно обеспечиваю- щая архитектуру повторно используемого программного обеспечения для семейства родственных приложений. В объектно-ориентированном окружении каркас состоит из абстрактных и конкретных классов. Создание экземпляра такого каркаса заключается в композиции существующих классов и в создании на их основе производных классов.
378 Программирование сетевых приложений на С++.Тсм 2 Ограниченный буфер (bounded buffer) Буфер ограниченного размера, который позволяет двум । (и более) потокам совместно использовать его в параллель- ном режиме, при котором, по крайней мере, один поток по- мещает элементы в буфер и, по крайней мере, один поток удаляет элементы из буфера. Одноранговое взаимодействие (peer-to-peer) В распределенной системе одноранговые приложения яв- ляются процессами, взаимодействующими друг с другом. В отличие от Компонентов в архитектуре клиент-сервер, од- норанговые приложения могут действовать как клиенты, как' серверы, или, как и те, и другие, а также могут менять свои роли в динамике. Параллелизм (parallelism) Способность объекта, компонента или системы выполнять операции, которые являются «физически одновременными» (ср. Параллельное выполнение). Параллельное выполнение (concurrency) Параметризованный тип (parameterized type) Способность объекта, компонента или системы выполнять «логически одновременные» (logically simultaneous) опера- ции (ср. Параллелизм). Особенность языка Программирования, которая позволяет классам быть параметризованными другими различными типами (ср. Шаблон) Пассивный объект (passive object) Паттерн (pattern) Объект, который заимствует поток у вызывающего процесса для выполнения своих методов (ср. Активный объект). Паттерн представляет собой описание некоторой повто- ряющейся проблемы проектирования, возникающей в кон- кретных проектных ситуациях, и предлагает хорошо апро- бированное положительное решение этой проблемы. Спе- цификация этого решения включает описание структуры компонентов, их функций, взаимосвязей и способов взаимо- действия. Паттерн Acceptor-Connector (Acceptor-Connector pattern) Паттерн Active Object (Active Object pattern) Паттерн проектирования, разделяющий подключение и ини- циализацию взаимодействующих одноранговых сервисов сетевой системы и обработку, которую они осуществляют после подключения и инициализации. Паттерн проектирования, разделяющий выполнение и вызов метода, с целью повышения параллелизма и упрощения синхронного доступа к объектам, которые принадлежат разным потокам управления. Паттерн Bridge (Bridge pattern) Паттерн проектирования, который отделяет абстракцию от ее реализации, так что обе могут изменяться независимо друг от друга. Паттерн Component Configurator (Component Configurator pattern) Паттерн Double-Checked Locking Optimization (Double-Checked Locking Optimization pattern) Паттерн проектирования, который позволяет подключать и отключать реализации его компонентов во время выполне- ния, не требуя модификации, повторной компиляции или статической перекомпоновки приложения. Паттерн проектирования, который снижает издержки, свя- занные с состязаниями и синхронизацией каждый раз, ко- гда критические секции кода должны запрашивать блоки- ровки безопасным с точки зрения многопоточности спосо- бом непосредственно во время выполнения программы.
Словарь терминов 379 Паттерн Half-Sync/Half-Async (Half-Sync/Half-Async pattern) Паттерн Leader/Followers (Leader/Followers pattern) Паттерн Manager (Manager pattern) Паттерн Monitor Object (Monitor Object pattern) Паттерн Pipes and niters (Pipes and Filters pattern) Паттерн Proactor (Proactor pattern) Паттерн Reactor (Reactor pattern) Паттерн Strategized Locking (Strategized Locking pattern) Паттерн Strategy (Strategy pattern) Паттерн Thread-Safe Interface (Thread-Safe Interface pattern) Паттерн Thread-Specific Storage (Thread-Specific Storage pattern. TSS pattern) Архитектурный паттерн, который разделяет синхронную и асинхронную обработку в параллельных системах, чтобы упростить программирование без значительного снижения производительности. Этот паттерн вводит два взаимосвязан- ных иерархических уровня, один для синхронной и один для асинхронной сервисной обработки. Уровень организации очередей служит связующим звеном для коммуникаций ме- жду сервисами синхронного и асинхронного уровней. Архитектурный паттерн, обеспечивающий эффективную мо- дель параллелизма, в которой множество потоков nd оче- реди совместно используют множество источников событий для того, чтобы обнаруживать, распределять, координиро- вать и обрабатывать запросы на обслуживание, которые появляются в этих источниках. Этот паттерн проектирования управляет временем жизни объектов класса или доступом к ним. Паттерн проектирования, синхронизирующий выполнение параллельных методов так, чтобы в каждый момент времени внутри объекта выполнялся только один метод. Он также по- зволяет методам объекта совместно планировать последо- вательности их действий. Архитектурный паттерн проектирования, который обеспечи- вает структуру для систем, обрабатывающий поток данных. Архитектурный паттерн, который позволяет приложениям с управлением по событиям эффективно демультиплексиро- вать и диспетчеризировать запросы на обслуживание, ини- циируемые завершением асинхронных операций, чтобы достичь выигрыша в производительности за счет одновре- менного выполнения операций, избегая некоторых связан- ных с этим недостатков. Архитектурный папбрн, который позволяет приложениям с управлением по событиям демультиплексировать и дис- петчеризировать запросы на обслуживание, передавае- мые приложению от одного или нескольких клиентов. Паперн проектирования, который осуществляет парамет- ризацию механизмов синхронизации, защищающих крити- ческие секции компонентов от параллельного доступа. Паперн проектирования, который определяет семейство алгоритмов, каждый из которых инкапсулирует и делает их взаимозаменяемыми. Strategy позволяет изменять алгорит- мы образом, независимым от клиентов, которые используют этот паперн. Паперн проектирования, минимизирующий издержки бло- кировки и гарантирующий, что вызов методов внутри компо- нента не приведет к самоблокировке (self-deadlock) из-за попытки повторно занять блокировку, которая уже занята этим компонентом. Паперн проектирования, который позволяет множеству по- токов использовать одну «логически глобальную» (logically global) точку д оступа для извлечения локального объекта одного из потоков, не приводя к дополнительным издержкам на блокировку при каждом доступе к объекту.
380 Программирование сетевых приложений на C++. Том 2 Паттерн проектирования (design pattern) Паттерн проектирования предлагает схему усовершенство- вания компонентов программной системы или взаимосвязей между ними. Он представляет собой описание наиболее часто применяемой структуры взаимодействующих компо- нентов, которая решает одну из общих проблем проектиро- вания в некотором конкретном контексте. Передача сообщений (message passing) Платформа (platform) Механизм IPC, используемый для обмена сообщениями ме- жду потоками или процессами. Сочетание аппаратных и программных средств, используе- мых при создании системы. Программные платформы вклю- чают операционные системы, библиотеки и каркасы (frameworks). Платформа реализует виртуальную машину, на которой выполняются приложения. Подстановка (inlining) Способ оптимизации во время компиляции, который заменя- ет вызов функции или метода телом программы функции или метода. Подстановка тела функции/цетода большого объе- ма может привести к «раздуванию» кода, с сопутствующими негативными последствиями, связанными с потреблением памяти и подкачкой. Порождающее программирование (generative programming) Методика программирования, нацеленная на разработку и реализацию программных компонентов, которые могут объединяться с целью создания (порождения) специализи- рованных и высоко оптимизированных систем, удовлетво- ряющих специфическим требованиям. Поток (thread) Независимая последовательность команд, которая выпол- няется в совместно используемом с другими потоками ад- ресном пространстве. Каждый поток имеет свой собствен- ный стек времени выполнения и регистры, что позволяет ему выполнять синхронный ввод/вывод без блокирования других параллельнд выполняющихся потоков. По сравнению с про- цессами, потоки поддерживают минимум информации о состоянии, требуют относительно меньше издержек при создании, синхронизации и планировании, и обычно взаи- модействуют с другими потоками с помощью объектов в об- ласти памяти процесса, которому они принадлежат, а не через общую память. Поток-на-запрос (thread-per-request) Модель параллелизма, в которой для каждого запроса соз- дается новый поток. Эта модель подходит серверам, кото- рым приходится обрабатывать долговременные запросы от множества клиентов, например, запросы к базе данных. Эта модель подходит меньше при краткосрочных запросах, из-за издержек, связанных с созданием нового потока для каждого запроса. Эта модель может также потреблять боль- шое количество ресурсов операционной системы, если множество клиентов посылают запросы одновременно. Поток-на-соединение (thread-per-connectlon) Модель параллелизма, которая ассоциирует с каждым се- тевым соединением отдельный поток. Эта модель рассмат- ривает каждого клиента, соединяющегося с сервером, в виде отдельного потока действующего пока существует соединение. Такая модель подходит серверам, которым приходится поддерживать продолжительные сеансы (session) с множеством клиентов. Но не подходит клиентам, таким как HTTP 1.0 web-браузеры, которые выполняют каж- дый запрос в отдельном соединении, им больше подходит модель поток-на-запрос (thread per request).
Словарь терминов 381 Потокобезопасный (thread-safe) Защищенный от любых нежелательных побочных эффектов (состояний гонок, конфликтов из-за данных и т.д.), порож- даемых параллельным выполнением одного и того же участ- ка программы несколькими потоками. Предметная область (domain) Объединяет концепции, знания и другие элементы, относя- щиеся к конкретной проблемной области. Часто использу- ется в сочетании «область применения»*(аррНса11оп domain), чтобы выделить ту проблемную область, к которой относится приложение. Промежуточный слой (middleware) Совокупность уровней и компонентов, которые обеспечива- ют повторно используемые общие службы и механизмы се- тевого программирования. Промежуточный слой распола- гается над операционной системой и ее стеком протоко- лов, но ниже структурных и функциональных составляющих любого отдельно взятого приложения. Промежуточный слой инфраструктуры хоста (host infrastructure middleware) Данный уровень иерархии промежуточного слоя инкапсу- лирует имеющиеся на хостах механизмы параллельной об- работки и IPC, с целью интеграции возможностей ОО и се- тевого программирования, что исключает многие трудоем- кие, подверженные ошибкам и непереносимые аспекты, связанные с разработкой сетевых приложений на базе API ОС, таких как Socket API или Pthreads. Пул потоков (thread pool) Модель параллелизма, в которой создается несколько по- токов, Обрабатывающих запросы одновременно. Эта мо- дель является разновидностью модели поток-на-запрос, со снижением издержек на создание потоков путем упреж- дающего порождения пула потоков. Эта модель подходит серверам, для которых желательно ограничить количество потребляемых ими системных ресурсов. Запросы клиентов могут обрабатываться одновременно пока количество од- новременных запросов не превысит количество потоков в пуле. С этого момента дополнительные запросы должны дожидаться в очереди пока не освободится один из пото- ков. Распределение (distribution) Действия, связанные с размещением объекта в другом про- цессе или на другом хосте, по отношению к клиентам, кото- рые к этому объекту обращаются. Распределение часто применяется, чтобы повысить отказоустойчивость или с це- лью улучшения доступа к удаленным ресурсам (ср. Совме- щение). Распределительный уровень промежуточного слоя (distribution middleware) Этот уровень иерархии промежуточного слоя автоматизиру- ет решение задач сетевого программирования общего ха- рактера, таких как: управление соединениями и памятью, маршалинг и демаршалинг, демультиплексирование дан- ных и запросов, синхронизация и многопоточность —так, чтобы разработчики могли программировать распределен- ные приложения в значительной степени так же, как авто- номные приложения, то есть вызывая процедуры целевых объектов, независимо от их расположения, языка, ОС и ап- паратуры. Рекурсивный мьютекс (recursive mutex) Блокировка, которая может быть повторно занята потоком, уже владеющим этим мьютексом, без риска самоблокиров- ки (self-deadlock) потока, (ср. Нерекурсивный мьютекс)
382 Программирование сетевых приложений на C++. Том 2 Рефакторинг (refactoring) Последовательно-поступательные действия по абстрагиро- ванию универсальных функций существующего программ- ного обеспечения с целью улучшения структуры и повторно- го использования компонентов и каркасов. Робастный итератор (robust Iterator) Итератор, который гарантирует, что операции вставки и удаления не будут конфликтовать с Операцией перебора элементов. Семафор (semaphore) Механизм блокировки, использующий счетчик. Пока значе- ние счетчика больше нуля, любой поток может запросить семафор и не быть заблокированным. Но после того как счетчик обнуляется, потоки блокируются на этом семафоре до того момента, когда значение счетчика семафора ста- нет больше нуля в результате инкремента счетчика при ос- вобождении семафора другим потоком. Сериализация (serialization) Механизм, который с целью предотвращения состояния го- нок, гарантирует, что внутри критической секции одновре- менно выполняется только один поток. Синхронная очередь сообщений (synchronized message queue) Синхронный ввод/вывод (synchronous I/O) Ограниченный (bounded) буфер, на котором потоки-произ- водители (producer) могут блокироваться, если очередь за- полнена (full), а потоки-потребители (consumer) — если оче- редь пуста (empty). Механизм передачи и получения данных, при котором ини- циированная операция ввода/вывода приостанавливает работу вызывающего потока на время завершения этой операции. Слабо типизированный (weakly typed) Элемент данных, объявленный тип данных которого не пол- ностью отражает его предполргаемое использование или целевое назначение. Служба (service) В контексте программирования в сетевой среде служба мо- жет быть (1) четко определенной функцией, реализуемой сервером, например, служба echo, предоставляемая су- персервером inetd, (2) совокупностью возможностей, пред- лагаемых сервером-демоном, например, сам супер-сер- вер inetd, или (3) совокупность серверных процессов, взаи- модействующих при решении общей задачи, например, совокупность демонов rwho в подсети локальной сети (LAN), которые периодически рассылают широковещательные пакеты и получают информацию о состоянии с уведомлени- ем о действиях пользователей на других хостах. Событие (event) Сообщение, которое передает информацию о появлении существенного события, а также все данные, связанные с этим событием. Совместно используемая библиотека (shared library) Совмещение (collocation) См. Динамически подключаемая библиотека. Размещение объектов в одном процессе, или на одном хос- те; часто применяют с целью упрощения или повышения производительности (ср. Распределение).
Словарь терминов 383 Сокет (socket) Многозначный термин, относящийся к программированию в сетевой среде. Сокет представляет собой конечную точку соединения, которая задает конкретный сетевой адрес и номер порта. Socket API является библиотекой функций, которую поддерживает большинство операционных систем и которую используют сетевые приложения для установле- ния соединений и обмена информацией через конечные точки сокетов. Сокет передачи данных (data-mode socket) может быть использован для обмена данными между соеди- ненными одноранговыми процессами (peers). Сокет пас- сивного режима (passive-mode socket) является фабрикой, которая возвращает дескриптор сокета передачи данных. Сообщение (message) Сообщения используются для обмена информацией между объектами, потоками или процессами. В объектно-ориенти- рованной системе термин «сообщение» используется, что- бы описать выбор и активизацию некоторой операции или метода объекта. Этот тип сообщения является синхронным, то есть отправитель ждет пока получатель завершит активи- зированную операцию. Потоки и процессы чаще обменива- ются информацией асинхронно, то есть отправитель про- должает свою работу, не дожидаясь ответа получателя^ Состояние гонок (race condition) Состояние гонок представляет собой риск сбоя при парал- лельном выполнении, который может произойти, если не- сколько потоков выполняются одновременно в неправильно сериализованной'критической секции. Состязания на уровне процесса (process-scope contention) Состязания на уровне системы (system-scope contention) Типобезопасный (type safe) Политика параллелизма, в соответствии с которой область состязаний (scope of contention) или синхронизации пото- ков ограничена процессом, выполняющимся на некотором хосте (ср. Состязания на уровне системы). Политика параллелизма, при которой область состязаний и синхронизации потоков находится на уровне процессов хоста, (ср. Состязания на уровне процесса) Свойство, принудительно, обеспечиваемое системой типов языка программирования, чтобы гарантировать, что только корректные операции могут быть вызваны при обращении к экземплярам типов. Троянский конь (trojan horse) Потенциально опасный программный код, скрытый внутри безобидного на вид программного модуля, например, в библиотеке времени выполнения. Тупик (deadlock) Тупик (взаимоблокировка) — это ситуация, связанная с од- новременным выполнением операций, которая может воз- никнуть тогда, когда несколько потоков пытаются занять не- сколько блокировок и попадают в тупик,• в состояние беско- нечного ожидания. Управление потоком (flow control) Механизм сетевых протоколов, который предохраняет от- правителя, имеющего высокую скорость передачи, от пере- полнения буферной памяти и перегрузки вычислительных ресурсов менее мощного получателя.
384 Условная переменная (condition variable) Условная переменная является механизмом синхрониза- ции, используемым совместно работающими потоками, что- бы временно приостанавливать свою работу до того момен- та, когда условное выражение, включающее совместно,ис- пользуемые этими потоками данные, достигнет заданного значения. Условная переменная всегда используется вме- сте с мьютексом, которым сначала поток должен завладеть, а потом вычислять условное выражение. Если условное вы- ражение принимает.значение «ложь» (false), тб поток авто- матически приостанавливает свою работу на этой услов- ной переменной и освобождает мьютекс, чтобы другие по- токи могли изменять совместно используемые данные. Когда один из совместно работающих потоков изменяет эти данные, то он может «уведомить» об этом условную пере- менную, которая атомарно возобновляет работу приоста- новленного на этой условной переменной потока и снова запрашивает мьютекс. Установление активного соединения (active connection establishment) Установление пассивно- го соединения (passive connection establishment) Фабрика (factory) Роль, которую играет в установлении соединения одноран- говое приложение, инициирующее соединение с удален- ным партнером (ср. Установление пассивного соедине- ния'). Роль в установлении соединения, которую играет одноран- говое приложение, принимая запрос на установление со- единения от удаленного однорангового приложения (ср. Установление активного соединения). Метод или функция, которые создают и подключают ресур- сы, необходимые для создания и инициализации объекта или экземпляра компонента. Флуктуации (Jitter) Характеристики (traits) Стандартное отклонение времени ожидания для серии операций. Тип, который передает информацию, используемую другим классом или алгоритмом для определения политик или де- талей реализации на этапе компиляции. Цикл обработки событий (event loop) Чтение-с-раэнесением (scatter-read) Программная конструкция, которая непрерывно отслежи- вает и обрабатывает события. Операция ввода, сохраняющая данные в нескольких пре- доставляемых вызывающей стороной буферах, а не в одном непрерывном буфере. Шаблон (template) Свойство языка программирования C++, которое делает возможной параметризацию классов и функций различны- ми типами, константами или указателями на функции. Шаб- лон часто называют родовым (generic) или параметризо- ванным (parameterized) типом. Язык паттернов (pattern language) Семейство взаимосвязанных папернов проектирования, ко- торое, на систематической основе, определяет процесс решения проблем проектирования программного обеспе- чения.
Англо-русский указатель терминов Acceptor-Connector Pattern Active Connection Establishment Active Object Active Object Pattern Architectural Pattern Aspects Asynchronous Completion Token (ACT) Asynchronous I/O Barrier Synchronization Bounded Buffer Bridge Pattern Busy Wait Cache Affinity Collocation Common Data Representation (CDR) Common Middleware Services Completion Handler Component Component Configurator Pattern Concurrency Condition Variable Container Critical Section Daemon Deadlock Demarshaling Demultiplexing Design Pattern Distribution Distribution Middleware Паттерн Acceptor-Connector Установление активного соединения Активный объект Паттерн Active Object Архитектурный паттерн Аспекты Асинхронный маркер завершения Асинхронный ввод/вывод Барьерная синхронизация Ограниченный буфер Паттерн Bridge Активное ожидание Аффинность кэша Совмещение Обобщенное представление данных Общие службы промежуточного слоя Обработчик завершения Компонент Паттерн Component Configurator Параллельное выполнение Условная переменная Контейнер Критическая секция Демон Тупик Демаршалинг Демультиплексирование Паттерн проектирования Распределение Распределительный уровень промежуточного слоя Domain Domain Domain Analysis Double-Checked Locking Optimization Pattern Предметная область Домен Анализ предметной области Паттерн Double-Checked Locking Optimization
386 Программирование сетевых приложений на C++. Там 2 Double-Dispatching Dynamically Linked Library (DLL) Event Event Handler Event Loop Factory Flow Control Framework Function Object Gather-Write Generative Programming Generic Programming Half-Sync/Half-Async Pattern Handle Host Infrastructure Middleware Idiom Inheritance Inlining Jitter Latency Leader/Followers Pattern Lock Manager Pattern Marshaling Message Message Passing Middleware Monitor Object Pattern Mutex Nonrecursive Mutex Object-Oriented Framework Overlapped I/O Parallelism Parameterized Type Passive Connection Establishment Passive Object Pattern Двойная диспетчеризация Динамически подключаемая библиотека Событие Обработчик события Цикл обработки событий Фабрика Управление потоком Каркас Объект-функция Запись-со-слиянием Порождающее программирование Обобщенное программирование Паттерн Half-Sync/Half-Async Дескриптор Промежуточный слой инфраструктуры хоста Идиома Наследование Подстановка Флуктуации Время ожидания Паттерн Leader/Followers Блокировка Паттерн Manager Маршалинг Сообщение Передача сообщений Промежуточный слой Паттерн Monitor Object Мьютекс Нерекурсивный мьютекс Объектно-ориентированный каркас Ввод/вывод с перекрытием Параллелизм Параметризованный тип Установление пассивного соединения Пассивный объект Паттерн
Англо-русский указатель терминов 387 Pattern Language Язык паттернов Peer-to-Peer Одноранговое взаимодействие Pipes and Filters Pattern Паттерн Pipes and Filters Platform Платформа Priority Inversion Инверсия приоритетов Proactive I/O Активно-превентивный ввод/вывод Proactor Pattern Паттерн Proactor Process-Scope Contention Состязания на уровне процесса Quality of Service (QoS) Качество обслуживания Race Condition Состояние гонок Reactive I/O Взаимно-согласованный ввод/вывод Reactor Pattern Паттерн Reactor Recursive Mutex Рекурсивный мьютекс Refactoring Рефакторинг Reify Конкретизация Robust Iterator Робастный итератор Scatter-Read Чтение-с-разнесением Scoped Locking Idiom Идиома Scoped Locking Semaphore Семафор Serialization Сериализация Service Служба Shared Library Динамически подключаемая библиотека Smart Pointer Интеллектуальный указатель Socket Сокет Starvation Голодание Strategized Locking Pattern Паттерн Strategized Locking Strategy Pattern Паттерн Strategy Synchronization Mechanism Механизм синхронизации Synchronized Message Queue Синхронная очередь сообщений Synchronous Event Demultiplexer Демультиплексор синхронных событий Synchronous I/O Синхронный ввод/вывод System-Scope Contention Состязания на уровне системы Task Задача Template Шаблон Thread Поток Thread-per-Connection Поток-на-соединение Thread-per-Request Поток-на-запрос Thread Pool Пул потоков
388 Программирование сетевых приложений на C++. Том 2 Thread Safe Thread-Safe Interface Pattern Thread-Specific Storage (TSS) Pattern Traits Trojan Horse Type Safe Weakly Typed Wrapper Facade Потокобезопасный Паттерн Thread-Safe Interface Паттерн Thread-Specific Storage Характеристики Троянский конь Типобезопасный Слабо типизированный Интерфейсный фасад
Литература (AleO1) Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design PatternsApplied. Addison-Wesley, Boston, 2001. (Русский перевод: Александ- реску А. Современное проектирование на C++. Серия C++ In-Depth. — М.: Вильямс, 2002.) (AII02) Paul Allen. Model Driven Architecture. Component Development Strategies, 12(1), January 2002. (Aus99) Matthew H. Austem. Generic Programming and the STL: Using and Extending the C++ Standard. Addison-Wesley, Reading, MA, 1999. (BA90) M. Ben-Ari. Principles of Concurrent and Distributed Programming. Prentice Hall International Series in Computer Science, 1990. (Bay02) John Bay. Recent Advances In the Design of Distributed Embedded Systems. In Proceedings of Proceedings ofSPIE, Volume 47: Battlespace Digitization and Network Centric Warfare, April 2002. (BecOO) Kent Beck. Extreme Programming Explained: Embrace Change. Addison-Wesley, Boston, 2000. (Ber95) Steve Berczuk. A Pattern for Separating Assembly and Processing. In James O. Copllen and Douglas C. Schmidt, editors, Pattern Languages of Program Design. Addison-Wesley, Reading, MA, 1995. (BHLM94) J. T. Buck, S. Ha, E. A. Lee, and D. G. Messerschmitt. Ptolemy: A Framework for Simulating and Prototyping Heterogeneous Systems. International Journal of Computer Simulation, Special Issue on Simulation Software Development Component Development Strategies, 4, April 1994. (BjaOO) Bjarne Stroustrup. The C++ Programming Language, Special Edition. Addison-Wesley, Boston, 2000. (Русский перевод: Страуструп Б. Язык про- граммирования C++. Специальное изд. — СПб.: М.: Невский диалект — Би- ном, 2001.) (BL88) Ronald Е. Barkley and Т. Paul Lee. A Heap-based Callout Implementation to Meet Real-time Needs. In Proceedings of the USENIX Summer Conference, pages 213-222. USENIX Association, June 1988. (Bla91) U. Black. OSI: A Model for Computer Communications Standards. Prentice-Hall, Englewood Cliffs, New Jersey, 1991. (BM98) Gaurav Banga and Jeffrey C. Mogul. Scalable Kernel Performance for Internet Servers under Realistic Loads. In Proceedings of the USENIX 1998 Annual Technical Conference, New Orleans, LA, June 1998. USENIX. (Boo94) Grady Booch. Object Oriented Analysis and Design with Applications (2nd Edition). Benjamin/Cummlngs, Redwood City, California, 1994. (Русский пере- вод: Буч Г. Объектно-ориентированный анализ и проектирование с приме- рами приложений на C++. 2-е изд. — СПб.: М.: Невский диалект — Бином, 1999.) (Вох98) Don Box. Essential COM. Addison-Wesley, Reading, MA, 1998. (BvR94) Kenneth Birman and Robbert van Renesse. Reliable Distributed Computing with the Isis Toolkit. IEEE Computer Society Press, Los Alamitos, 1994. (CB97) John Crawford and Steve Ball. Monostate Classes: The Power of One. C++ Report, 9(5), May 1997. (CHW98) James Copllen, Daniel Hoffman, and David Weiss. Commonality and Variability In Software Engineering. IEEE Software, 15(6), November/December 1998.
390 Программирование сетевых приложений на C++. Том 2 (CN02) (C++NPV1) (Cul99) (DA99) (DlmO1) (Egr98) (FJS99a) (FJS99b) (FYOO) (Gal95) (GLDW87) (GoF) (GR93) (GSC02) (GSNW02) (HJE95) CHJS) Paul Clements and Linda Northrop. Software Product Lines: Practices and Patterns. Addison-Wesley, Boston, 2002. Douglas C. Schmidt and Stephen D. Huston. C++ Network Programming, Volume 1: Mastering Complexity with ACE and Patterns. Addison-Wesley, Boston, 2002. (Русский перевод: Шмидт Д., Хьюстон С. Программирование сетевых приложений на C++. Т. 1. Профессиональный подход к проблеме сложности: АСЕ и паттерны. — М.: Бином, 2003.) Timothy R. Culp. Industrial Strength Pluggable Factories. C++ Report, 11(9), October 1999. Tim Dierks and Christopher Allen. The TLS Protocol Version 1.0. Network Information Center RFC2246, January 1999. Dimitri van Heesch. Doxygen. http: I /www. doxygen. org, 2001. Carlton Egremont, III. Mr. Bunny's Guide to ActiveX. Addison-Wesley, Reading, MA, 1998. Mohamed Fayad, Ralph Johnson, and Douglas C. Schmidt, editors. Building Application Frameworks: Object-Oriented Foundations of Framework Design. Wiley & Sons, New York, 1999. Mohamed Fayad, Ralph Johnson, and Douglas C. Schmidt, editors. Implementing Application Frameworks: Object-Oriented Frameworks at Work. Wiley & Sons, New York, 1999. Brian Foote and Joe Yoder. Big Ball of Mud. In Brian Foote, Nell Harrison, and Hans Rohnert, editors, Pattern Languages of Program Design 4. Addison-Wesley, Boston, 2000. Bill Gallmelster. POSIX.4 Programming for the Real World. O'Reilly, Sebastopol, California, 1995. R. Glngell, M. Lee, X. Dang, and M. Weeks. Shared Libraries In SunOS. In Proceedings of the Summer 1987 USENIX Technical Conference, Phoenix, Arizona, 1987. Erich Gamma, Richard Helm, Ralph Johnson, and John Vllssides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, Massachusetts, 1995. (Русский перевод: Гамма Э., Хелм Р., Джонсон Р„ Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. — СПб.: Питер, 2001.) Jim Gray and Andreas Reuter. Transaction Processing: Concepts and Techniques. Morgan Kaufman, Boston, 1993. Chris Gill, Douglas C. Schmidt, and Ron Cytron. Multi-Paradigm Scheduling for Distributed Real-Time Embedded Computing. IEEE Proceedings Special Issue on Modeling and Design of Embedded Software, October 2002. Anlruddha Gokhale, Douglas C. Schmidt, Balachandra Natarajan, and Nanbor Wang. Applying Model-Integrated Computing to Component Middleware and Enterprise Applications. The Communications of the ACM Special Issue on Enterprise Components. Service and Business Rules, 45(10), October 2002. Herman Huenl, Ralph Johnson, and Robert Engel. A Framework for Network Protocol Software. In Proceedings ofOOPSLA ’95, Austin, TX, October 1995. ACM. Stephen D. Huston, James C.E. Johnson, and UmarSyyld. The ACE Programmer's Guide. Addison-Wesley, Boston (forthcoming).
Литература 391 (HLS97) Timothy Н. Harrison, David L. Levine, and Douglas C. Schmidt. The Design and Performance of a Real-time C0RBA Event Service. In Proceedings ofOOPSLA '97. pages 184-199, Atlanta, GA, October 1997. ACM. (HMS98) James Hu, Sumedh Mungee, and Douglas C. Schmidt. Principles for Developing and Measuring High-performance Web Servers over ATM. In Proceedings of INFOCOM '98, March/April 1998. (Hol97) Luke Holmann. Journey of the Software Professional: The Sociology of Computer Programming. Prentice Hall, Englewood Cliffs, [MJ, 1997. (HP91) Norman C. Hutchinson and Larry L. Peterson. The x-kernel: An Architecture for Implementing Network Protocols. IEEE Transactions on Software Engineering, 17(0:64-76, January 1991. (HPS97) James Hu, Man Pyarall, and Douglas C. Schmidt. Measuring the Impact of Event Dispatching and Concurrency Models on Web Server Performance Over High-speed Networks. In Proceedings of the 2° Global Internet Conference. IEEE, November 1997. (HS99) James Hu and Douglas C. Schmidt. JAWS: A Framework for High Performance Web Servers. In Mohamed Fayad and Ralph Johnson, editors, Domain-Specific Application Frameworks: Frameworks Experience by Industry. Wiley & Sons, New York, 1999. (HV99) Michl Henning and Steve Vlnoskl. Advanced CORBA Programming With C++. Addison-Wesley, Reading, Massachusetts, 1999. (IEE96) IEEE. Threads Extension for Portable Operating Systems (Draft 10), February 1996. (JF88) Ralph Johnson and Brian Foote. Designing Reusable Classes. Journal of Object-Oriented Programming. l(5):22-35, June/July 1988. (JKN+01) Philippe Joubert, Robert King, Richard Neves, Mark Russlnovlch, and John Tracey. High-Performance Memory-Based Web Servers: Kernel and User-Space Performance. In Proceedings of the USENIX Technical Conference. Boston, MA, June 2001. (Joh97) Ralph Johnson. Frameworks = Patterns + Components. Communications of the ACM, 40(10), October 1997. (Jos99) Nicolai Josuttls. The C++ Standard Library: A Tutorial arid Reference. Addison-Wesley, Reading, Massachusetts, 1999. (KMC+00) Eddie Kohler, Robert Morris, BenJIe Chen, John Jannottl, and M. Frans Kaashoek. The Click Modular Router. ACM Transactions on Computer Systems, 18(3):263-297, August 2000. (Koe92) Andrew Koenig. When Not to Use Virtual Functions. C++ Journal, 2(2), 1992. (Kof93) Thomas Kofler. Robust Iterators for ET++. Structured Programming, 14(2):62-85, 1993. (KSS96) Steve Kleiman, Devang Shah, and Bart Smaalders. Programming with Threads. Prentice Hall, Upper Saddle River, NJ, 1996. (Kuh97) Thomas Kuhne. The Function Object Pattern. C++ Report, 9(9), October 1997. (LBM+01) Akos Ledeczl, Arpad Bakay, Miklos Marotl, Peter Volgysel, Greg Nordstrom, Jonathan Sprinkle, and Gabor Karsai. Composing Domain-Specific Design Environments. IEEE Computer, November 2001. (LeaOO) Doug Lea. Concurrent Programming In Java: Design Principles and Patterns. Second Edition. Addison-Wesley, Boston, 2000. (Lew95) BII Lewis. Threads Primer: A Guide to Multithreaded Programming. Prentice-Hall,' Englewood Cliffs, NJ, 1995.
392 Программирование сетевых приложений на C++. Том 2 (Llp96) (MBKQ96) Stan Lippman. Inside the C++ Object Model. Addison-Wesley, 1996. Marshall Kirk McKusIck, Keith Bostic, Michael J. Karels, and John S. Quarterman. The Design and Implementation of the 4.4BSD Operating System. Addison Wesley, Reading, Massachusetts, 1996. (Мс168) M. Doug McIlroy. Mass Produced Software Components. In Proceedings of the NATO Software Engineering Conference, October 1968. (Меу96) Scott Meyers. More Effective C++. Addison-Wesley, Reading, Massachusetts, 1996. (Русский перевод: Мейерс С. Наиболее эффективное использование C++. —М: ДМК, 2000.) (Меу97) Bertrand Meyer. Object-Oriented Software Construction, Second Edition. Prentice Hall, Englewood Cliffs, NJ, 1997. (МН01) Richard Monson-Haefel. Enterprise JavaBeans, 3rd Edition. O'Reilly and Associates, Inc., Sebastopol, CA, 2001. (ОЬ]98) Object Management Group. CORBAServices: Common Object Services Specification, Updated Edition. Object Management Group, December 1998. (ObjOla) Object Management Group. CORBA 3.0 New Components Chapters, OMG TC Document ptc/2001-11-03 edition, November 2001. (ObJOlb) Object Management Group. Model Driven Architecture (MDA), OMG Document ormsc/2001-07-01 edition, July 2001. (ObJ02) Object Management Group. The Common Object Request Broker: Architecture and Specification, 3.0 edition, June 2002. (OOS01) Ossama Othman. Carlos O'Ryan, and Douglas C. Schmidt. An Efficient Adaptive Load Balancing Service for CORBA. IEEE Distributed Systems Online, 2(3); March 2001. (OpeOD (OSI92a) OpenSSLProject. Openssl. www.openssl.org/,2001. OSI Special Interest Group. Ddta Link Provider Interface Specification, December 1992. (OSI92b) OSI Special Interest Group. Transport Provider Interface Specification, December 1992. (POS95) Information Technology—Portable Operating System Interface (POSIX) — Part 1: System Application: Program Interface (API) (G Language), 1995. (POSOO) Irfan Pyarall, Carlos O'Ryan, and Douglas C. Schmidt. A Pattern Language for Efficient, Predictable, Scalable, and Flexible Dispatching Mechanisms for Distributed Object Computing Middleware. In Proceedings of the International Symposium on Object-Oriented Real-time Distributed Computing (ISORC), Newport Beach, CA, March 2000. IEEE/IFIP. (POSAD Frank Buschmann, Reglne Meunier, Hans Rohnert, P$ter Sommerlad,.and Michael Stal. Pattern-Oriented Software Architecture—A System of Patterns. Wiley & Sons, New York, 1996. (POSA2) Douglas C. Schmidt, Michael Stal, Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, Volume 2. Wiley & Sons, New York, 2000. (Pre95) Wolfgang Pree. Design Patterns for Object-oriented Software Development. Addison-Wesley, Reading, MA, 1995. (Rag93) Steve Rago. UNIX System V Network Programming. Addison-Wesley, Reading, Massachusetts, 1993.
Литература 393 (Rlc97) Jeffrey Richter. Advanced Windows, Third Edition. Microsoft Press, Redmond, WA, 1997. (Rlt84) Dennis Ritchie. A Stream Input-Output System. AT&T Bel! Labs Technical Journal, 63(8):311-324, October 1984. (Rob99) Robert Sedgwick. Algorithms in C++, Parts 1-4: Fundamentals, Data Structure, Sorting, Searching, 3rd Edition. Addison-Wesley, Reading, MA, 1999. (Sch98) Douglas C. Schmidt. Evaluating Architectures for Multl-threaded CORBA Object Request Brokers. Communications of the ACM Special issue on CORBA, 41(10), October 1998. (SchOO) Douglas C. Schmidt. Why Software Reuse Has Failed and How to Make It Work for You. C++ Report, 12(1), January 2000. (SG96) Mary Shaw and Dave Garlan. Software Architecture: Perspectives on an Emerging Discipline. Prentice Hall, Englewood Cliffs, NJ, 1996. (SK97) Janos Sztipanovlts and Gabor Karsal. Model-Integrated Computing. IEEE Computer, 30(4): 110-112, Арг1Г1997. ustrlght Jonathan M. Sprinkle, Gabor Karsal, Akos Ledeczl, and Greg G. Nordstrom. The (SKLN01) New Metamodeling Generation. In IEEE Engineering of Computer Based Systems, Washington, DC, April 2001. IEEE. (SKT96) James D. Salehl, James F. Kurose, and Don Towsley. The Effectiveness of Affinity-Based Scheduling In Multiprocessor Networking. In IEEE INFOCOM, San Francisco, USA, March 1996. IEEE Computer Society Pless. (SLM98) Douglas C. Schmidt, David L. Levine, and Sumedh Mungee. The Design and Performance of Real-Time Object Request Brokers. Computer Communications, 21(4): 294-324, April 1998. (Sol98) David A. Solomon. Inside Windows NT, 2nd Ed. Microsoft Press, Redmond, Washington, 2nd edition, 1998. (Som98) Peter Sommerland. The Manager Design Pattern. In Robert Martin, Frank Buschmann, and Dirk Rlehle, editors, Pattern Languages of Program Design 3. Addison-Wesley, Reading, MA, 1998. (SOP+OO) Douglas C. Schmidt, Carlos O'Ryan, Irfan Pyarall, Michael Kircher, and Frank Buschmann. Leader/Followers: A Design Pattern for Efficient Multi-threaded Event Demultiplexing and Dispatching. In Proceedings of the 6th Pattern Languages of Programming Conference, Monticello, IL, August 2000. (SROO) David A. Solomon and Mark E. Russlnovlch. Inside Windows 2000,3rd Edition. Microsoft Press, Redmond, WA, 2000. (SRL98) H. Schulzrlnne, A. Rao, and R. Lanphler. Real Time Streaming Protocol (RTSP). Network Information Center RFC 2326, April 1998. (SS93) Douglas C. Schmidt and Tatsuya Suda. Transport System Architecture Services for High-Performance Communications Systems. IEEE Journal on Selected Areas In Communication, 11(4):489-506, May 1993. (SS94) Douglas C. Schmidt and Tatsuya Suda. An Object-Oriented Framework for Dynamically Configuring Extensible Distributed Comrtiunlcatlon Systems. IEE/BCS Distributed Systems Engineering Journal (Special Issue on Configurable. Distributed Systems), 2:280-293, December 1994. (SS95a) Douglas C. Schmidt and Paul Stephenson. Experiences Using Design Patterns to Evolve System Software Across Diverse OS Platforms. In Proceedings of the 9th European Conference on Object-Oriented Programming, Aarhus, Denmark, August 1995. ACM.
394 Программирование сетевых приложений на C++. Там 2 (SS95b) Douglas C. Schmidt and Tatsuya Suda. Measuring the Performance of Parallel Message-based Process Architectures. In Proceedings of the Conference on Computer Communications (INFOCOM), pages 624-633, Boston, April 1995. IEEE. (SS02) Richard E. Schantz and Douglas C. Schmidt. Middleware for Distributed Systems: Evolving the Common Structure for Network-centric Applications. In John Marclniak and George Teleckl, editors. Encyclopedia of Software Engineering. Wiley & Sons, New York, 2002. (Ste92) W. Richard Stevens. Advanced Programming in the UNIX Environment. Addison-Wesley, Reading, Massachusetts, 1992. (Ste94) W. Richard Stevens. TCP/IP Illustrated, Volume 1. Addison-Wesley, Reading, Massachusetts, 1994. (Ste98) W. Richard Stevens. UNIX Network Programming. Volume 1: Networking APIs: Sockets andXTI, Second Edition. Prentice-Hall, Englewood Cliffs, NJ, 1998. (Ste99) W. Richard Stevens. UNIX Network Programming, Volume 2: Interprocess Communications, Second Edition. Prentice-Hall, Englewood Cliffs, NJ, 1999. (SW95) W. Richard Stevens and Gary R. Wright. TCP/IP Illustrated, Volume 2: The Implementation. Addison-Wesley, Reading, MA, 1995. (SX01) Randall Stewart and Qlaoblng Xie. Stream Control Transmission Protocol (SCTP) A Reference Guide. Addison-Wesley, Boston, 2001. (Szy98) Clemens Szyperskl. Component Software—Beyond Object-Oriented Programming. Addison-Wesley, Reading, Massachusetts, 1998. (Tan92) Andrew S. Tanenbaum. Modem Operating Systems. Prentice Hall, Englewood Cliffs, NJ, 1992. (Русский перевод: Таненбаум Э. Современные операцион- ные системы. 2-е изд. — СПб.: Питер, 2002.) (TL01) Thuan Thai and Hoang Lam. .NETFramework Essentials. O'Reilly, Sebastopol, CA, 2001. (VL97) George Varghese and Tony Lauck. Hashed and Hierarchical Timing Wheels: Data Structures for the Efficient Implementation of a Timer Facility. IEEE Transactions on Networking, December 1997. (VII98a) John Vllssldes. Pattern Hatching: Design Patterns Applied. Addison-Wesley, Reading, MA, 1998. (Vll98b) John Vllssldes. Pluggable Factory, Part 1. C++ Report, 10(10), November-December 1998. (VII99) (vR96) John Vllssldes. Pluggable Factory, Part 2. C++ Report, 11(2), February 1999. Michael van Rooyen. Alternative C++: A New Look at Reference Counting and Virtual Destruction in C++. C++ Report, 8(4), April 1996. (wKSOO) Martin Fowler with Kendall Scott. UML Distilled—A Brief Guide to the Standard Object Modeling Language (2nd Edition). Addison-Wesley, Boston, 2000. (WLS+85) D. Walsh, B. Lyon, G. Sager, J. M. Chang, D. Goldberg, S. Kleiman, T. Lyon, R. Sandberg, and P. WeisS. Overview of the SUN Network File System. In Proceedings of the Winter USENIX Conference. Dallas, TX, January 1985. (Woo97) Bobby Woolf. The Null Object Pattern. In Robert Martin, Frank Buschmann, and Dirk Rlehle, editors. Pattern Languages of Program Design. Addison-Wesley, Reading, Massachusetts, 1997.
Программирование сетевых приложений на Том 2 Дуглас Шмидт, Стивен Хьюстон / ли \ Вам необходимо разрабатывать гибкое ПО. которое можно быстро настраивать под требования заказчика? Вы хотите использовать мошный потенциал и эффективность каркасов в своих программах? ADAPTIVE Communication Environment (АСЕ) это инструментальное ПО с открытыми исходными кодами, специально предназначенное для создания высокопроизводительных сетевых приложений и промежуточного ПО следующего поколения. Большие возможности и гибкость АСЕ связаны с объектно-ориентированными каркасами, используемыми в качестве средства систематического повторного использования сетевого прикладного ПО. Каркасы АСЕ решают обшие задачи сетевого программирования; их можно настраивать, используя возможности языка C++, с целью создания законченных распределенных приложений. Книга Программирование сетевых приложений на C++, Том 2 посвящена каркасам АСЕ. Она охватывает широкий круг концепций, паттернов и принципов применения, формирующих структуру этих каркасов. По сути, это практическое руководство по проектированию объектно-ориентированных каркасов, которое показывает разработчикам, как применять каркасы в параллельных сетевых приложениях. Программирование сетевых приложений на C++, Том I это введение в АСЕ и в интер- фейсные фасады базовые элементы сетевой обработки. Второй том объясняет, как на основе интерфейсных фасадов создавать каркасы, а на их основе коммуникационные службы более высокого уровня. Написанная двумя экспертами в АСЕ, эта книга содержит: • Обзор каркасов АСЕ • Аспекты проектирования сетевых служб • Описание основных возможностей наиболее важных каркасов АСЕ • Многочисленные примеры кода C++, объясняющие, как использовать каркасы АСЕ Программирование сетевых приложений на C++, Том 2 является руководством по применению каркасов, которые позволяют ускорить написание сетевых приложений за счет уменьшения усилий и затрат, связанных с разработкой. Эта книга отличный помощник разработчикам сетевых приложений, пишущим на C++. Д-р Дуглас Шмидт автор-разработчик АСЕ, профессор University of California, Irvine, где занимается исследованием паттернов и критериев оптимизации распределенного промежуточного слоя встроенных систем и систем реального времени. Он бывший главный редактор C++Report, постоянный обозреватель C/C++ Users Journal и соредактор книги Pattern Languages of Program Design (Addison-Wesley, 1995). Он один из авторов книги Программирование сетевых приложений на C++, Том / (Перевод с англ. Издательство Бином, 2003). Стивен Д. Хьюстон президент и генеральный директор компании Riverace Corporation, которая оказывает техническую поддержку и консультирует компании, стремящиеся быть в курсе разработки программных проектов с применением АСЕ. Стив имеет десятилетний опыт работы с АСЕ и более двадцати лет разрабатывает программное обеспечение в основном сетевые протоколы и приложе- ния на C++ в самых разных аппаратных и программных средах. http://www.awprofessional.com http: //асе. есе .uci.edu/ http://www.riverace.com Дизайн обложки Майкл Гудман, Getty Images. Inc.