Рейтинг пользователей: / 0
ХудшийЛучший 

Гниденко А.С., Гниденко И.Г., Мердина О.Д.

ПРОГРАММНАЯ АРХИТЕКТУРА КЛИЕНТ-СЕРВЕР. ПРИНЦИПЫ ОРГАНИЗАЦИИ И ОСНОВНЫЕ ПРОБЛЕМЫ

Балтийский государственный технический университет

Санкт-Петербургский государственный инженерно-экономический университет 

 

This article is concerned about the client-server program architecture. It describes some common principles and concepts as well as several key problems that occur while implementing the architecture. 

Keywords: Client-server architecture, interface, object oriented programming, class, instantiation, reference counting, class factory, COM

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

Ключевые слова: Архитектура клиент-сервер,  интерфейс, объектно-ориентированное программирование, класс, инстанциирование, подсчет ссылок, фабрика классов, COM

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

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

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

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

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

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

Избыточность интерфейса, т.е. возможность клиента запросить одну и ту же услугу несколькими способами, плоха потому, что обусловливает увеличение нагрузки на сервер и ухудшает расширяемость. Однако на макроуровне, в случае комплектов средств разработки (SDK) или программных интерфейсов ОС, избыточность встречается достаточно часто и вполне допустима.

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

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

Скелет объявления интерфейса в языке C++ выглядит следующим образом:

class ISmth

{

public:

  virtual ~ISmth () {}

 

  virtual type1 Method1 (...) = 0;

  virtual type2 Method2 (...) = 0;

  virtual type3 Method3 (...) = 0;

  ...

};

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

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

В других языках ООП, таких как Java и C#, существует самостоятельное ключевое слово interface, семантика которого совпадает с ранее описанной для языка С++. Реализация методов интерфейса классом в разных языках объявляется либо напрямую через наследование, либо посредством ключевого слово implements:

public class CSmthImpl implements ISmth

{

  ...     

}

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

Указатели (ссылки) на все интерфейсы, которые реализует класс, экземпляром которого является их объект, совместимы между собой, допустимо их приведение друг к другу. Если класс С1 реализует интерфейсы I1 и I2, и имеется указатель (ссылка) на I1, под которым лежит С1, то ее приведение к типу I2 – вполне допустимая операция, а попытка привести указатель к некоторому интерфейсу I3 завершится с ошибкой.

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

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

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

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

Как уже было отмечено, знание клиента об объекте, реализующем интерфейс, ограничивается процедурой инстанциирования. Сама реализация может находиться как “рядом”, так и в другом бинарном модуле и, возможно, на другой машине – для клиента это безразлично. Для серверов, находящихся вне  клиентского модуля, существует разбиение на внутрипроцессные (inproc server) и внепроцессные (out of proc server).

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

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

Многим из обозначенных ранее требований к архитектуре клиент-серверного взаимодействия отвечает технология COM (Component Object Model – компонентная объектная модель) компании Microsoft. Для инстанциирования интерфейса клиенту достаточно указать глобальный идентификатор COM-класса (CLSID), прописанного в реестре. Запись в реестре создается посредством регистрации COM-объекта и включает полный путь к серверному бинарному модулю. Фабрика классов COM (интерфейс IClassFactory) унифицирует это процесс.  Каждый COM-объект поддерживает базовый интерфейс IUnknown, методы которого позволяют управлять счетчиком ссылок объекта, а также осуществлять запрос интерфейса. Технология COM включает собственный язык описания интерфейсов (MIDL), а также поддерживает работу как с внутрипроцессным, так и с внепроцессным сервером. Технология COM, несмотря на свою широкую распространенность, не лишена недостатков. Одной из классических проблем является явление циклических ссылок. Если какой-либо клиент владеет двумя COM-объектами, хранящими ссылки друг  на друга и захочет уничтожить их, он вызовет для каждого из них метод IUnknown :: Release. Однако ни один из объектов в действительности не будет уничтожен, так как счетчик ссылок каждого из них будет равен 1, благодаря ссылке другого объекта на него. Трудности, связанные с решением подобного рода коллизии, ложатся  на плечи клиента. Еще одним серьезным недостатком является порождение известного явления DLL Hell (“ад динамических библиотек”). Поскольку CLSID COM-объекта, прописанный в реестре, однозначно определяет серверную динамическую библиотеку, все клиенты, инстранциируя интерфейс данным CLSID, будут обращаться к ней. В ряде случаев такой эффект является нежелательным и влечет за собой конфликты разных программных продуктов, установленных на одной машине. Однако главным недостатком COM все-таки является неоправданная сложность. Это привело к появлению ряда оберток над COM, таких как ATL.

В качестве альтернативы COM выступает технология CORBA (Common Object Request Broker Architecture), продвигаемая консорциумом OMG. CORBA, как и COM, имеет собственный язык описания интерфейсов. Функциональность CORBA-объекта становится доступной для клиента благодаря созданию серванта, отвечающего за реализацию интерфейса и имеющего свой счетчик ссылок. Функции создания CORBA-объекта и доставки запроса клиента нужному серванту возложены на объектный адаптер POA. (Portable object adapter). Объектные адаптеры в приложении образуют древовидную структуру.

Важным достоинством CORBA является поддержка распределенных вычислений: клиент и сервер могут находиться на разных машинах и взаимодействовать через TCP/IP. Аналогичная технология от Microsoft носит название DCOM(Distributed COM).

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

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

Существуют три основных метода работы с неуправляемым кодом из среды .NET Framework. К ним относятся Platform Invoke - обращение к функциям, экспортируемым динамическими библиотеками, COM Interop - обращение к COM-компонентам из managed-кода, а также механизм unsafe (небезопасных) вызовов. Как правило, для организации взаимодействия .NET-приложение регистрирует сборку в реестре и генерирует библиотеку типов (.tlb). Неуправляемый COM-клиент импортирует библиотеку типов и получает доступ к фабрике классов управляемого сервера так, как будто работает с обычным COM-сервером.

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

Сложность разработки сервера, обслуживающего множество клиентов, заключается, прежде всего, в том, что он ориентирован на длительную бесперебойную работу. Диагностика и устранение ошибок, приводящих к краху системы с периодичностью раз в месяц, является сложной и затратной работой, в ряде случаев именно ее длительность приводит к переносу сроков выпуска продукта. Обеспечение стабильности при длительной работе компонента ужесточает требования к программистам, заставляет четко отслеживать безопасность кода, наличие необходимых проверок и корректной обработки ошибок. Большую роль в данном случае играет выбор языка программирования, используемых API и SDK, а также целевая платформа.

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

 

 

 
Секции-декабрь 2011
КОНФЕРЕНЦИЯ:
  • "Современные проблемы и пути их решения в науке, транспорте, производстве и образовании'2011"
  • Дата: Октябрь 2011 года
  • Проведение: www.sworld.com.ua
  • Рабочие языки: Украинский, Русский, Английский.
  • Председатель: Доктор технических наук, проф.Шибаев А.Г.
  • Тех.менеджмент: к.т.н. Куприенко С.В., Федорова А.Д.

ОПУБЛИКОВАНО В:
  • Сборник научных трудов SWorld по материалам международной научно-практической конференции.