Некоторое время назад мной был опубликован пост о создании собственной системы плагинов на C++11 [1]. Где было рассказано о плагинах в рамках одного процесса. Есть желание дать возможность плагинам не зависеть от процесса и компьютера, на котором они исполняются. Сделать межпроцессное и удаленное взаимодействие. Для этого надо решить пару задач: создать наборы Proxy/Stub и предоставить транспорт для обмена данными между клиентом и сервером.
Во многих библиотеках, предназначенных для организации удаленного взаимодействия предлагается некоторый скрипт или утилита для генерации Proxy/Stub из некоторого описания интерфейса. Так, например для libevent [2] при использовании ее части, связанной с RPC, есть скрипт event_rpcgen.py, который из С-подобного описания структур генерирует код для Proxy/Stub, а транспортом уже служит другая часть libevent. Так же gSOAP [3] предоставляет утилиту генерации кода из C++-подобного описания структур данных или из WSDL-описания и имеет свой встроенный транспорт, который можно использовать. gSOAP хороший и интересный продукт и в нем применение утилиты автогенерации кода оправдано, т. к. из C++-подобного описания можно сгенерировать WSDL-описание, которое уже может быть использовано при работе с Web-сервисами из других языков программирования.
Можно найти еще несколько примеров библиотек для построения клиент-серверного взаимодействия. Многие из них будут предлагать использовать те или иные механизмы генерации Proxy/Stub и свой встроенный транспорт.
Как можно взять за основу любой известный транспорт и отказаться от утилит генерации кода Proxy/Stub, возложив эту задачу на компилятор и воспользоваться преимуществами C++11 для создания объектного интерфейса удаленного взаимодействия с минимальными трудозатратами его использования? О принципах и реализации в виде законченного проекта, который может быть использован как библиотека при разработке своего клиент-серверного приложения изложены ниже.
Пост получился не из разряда самых коротких. Его можно читать с любого интересующего Вас раздела. В зависимости от того, что является первостепенным интересом: технические детали реализации или возможность использования и примеры.
Разделы:
- Введение
- Реализация
- Примеры
- Использование
- Ограничения
- Исходники, сборка, тестирование
- Заключение
- Материалы
Введение
Как уже было отмечено выше, появление поста было вызвано желанием «расселить» плагины одного процесса по разным и по возможности по разным компьютерам. Но пост не о доделках системы плагинов, а скорее о ее побочном полноценном и независимом от нее продукте, который в последствии и ляжет в ее основу реализации удаленного взаимодействия плагинов. А пока это всего лишь немного упрощенная реализация, позволяющая строить клиент-серверные приложения. В качестве интерфейса для такого взаимодействия выбран интерфейс в стиле C++ (структура с чисто виртуальными методами). Как из такого интерфейса получить набор Proxy/Stub'ов я уже ранее писал [4]. Это был пост с реализацией на C++03 и был своего рода идеально сферическим конем из абсолютно черной материи, т.е. не имел завершенной практической реализации, которую можно было бы взять и, что называется, «прямо из коробки» попробовать использовать. В этом же посте будет дана полная реализация с преимуществами, которые были получены благодаря стандарту C++11, а так же все можно будет протестировать «из коробки» на реальном тестовом сервере.
Реализация
В основу реализации легли материалы уже опубликованные мною ранее: пост о реализации Proxy/Stub'ов [4] и пост о создании своего http-сервера на базе libevent [5].
Что использовать в качестве транспорта не важно, и при желании его можно легко заменить, реализовав пару простых интерфейсов. Мною был выбран http встроенный функционал libevent, так как он мне показался простым и не затратным в использовании и уже неплохо себя зарекомендовавшим.
Для формирования пакетов данных, которыми обмениваются клиент и сервер была выбрана простая библиотека rapidxml [6], которая меня привлекла тем, что она поставляется в виде набора только включаемых файлов, что для меня дает возможность более простого ее внедрения и распространения исходного кода на ее основе, а так же она показывает хорошие результаты производительности, что при работе с xml так же играет не последнюю роль. Так же как и транспорт предлагаемая реализация удаленного взаимодействия может легко переключиться на иной формат/протокол сериализации и десериализации данных при желании пользователя. Для этого нужно реализовать два класса с заданным интерфейсом.
Вся реализация построена с использованием шаблонов и немного макросов. Мда… Говорят, что макросы — это зло, а шаблоны — это сложно и непонятно. Получается сложное и непонятное зло. Так ли это? Нет. Макросы в небольшом их количестве полезны, т. к. иногда не все возможно выразить только средствами языка, а для достижения результата нужно прибегнуть к препроцессору. В свою же очередь шаблоны дают обобщение и сокращение кода. Получается сдвиг от сложно-непонятного зла в сторону полезного сокращения и обобщения кода.
Будучи не раз обруганным многоэтажным матом компилятора его длинных сообщений об ошибках в шаблонном коде да еще и под макросами, мне удалось достичь минимализма, примеры которого я приведу перед тем как погрузиться в детали реализации, чтобы было видно к какому результату будет стремление в описании реализации.
struct IFace
{
virtual ~IFace() {}
virtual void Mtd1() = 0;
virtual void Mtd2(int i) = 0;
virtual void Mtd3(int i) const = 0;
virtual int const* Mtd4() const = 0;
virtual int const& Mtd5(int i, long *l1, long const *l2,
double &d, long const &l3) const = 0;
virtual char const* Mtd6() const = 0;
virtual wchar_t const* Mtd7() = 0;
virtual void Mtd8(char const *s1, wchar_t const *s2) = 0;
};
Тестовый интерфейс, который содержит методы как с параметрами, так и без. Параметры передаются по значению, ссылке или указателю. Так же методы могут ничего не возвращать или какое-то значение, переданное по ссылке, указателю или по значению (простите за тавтологию). В общем, материал как раз для того, чтобы в полном объеме протестировать то минимальное описание Proxy/Stub, которое приведено ниже. И которое не требует никакого указания информации о параметрах и возвращаемых значениях, а только интерфейс и имена методов.
PS_BEGIN_MAP(IFace)
PS_ADD_METHOD(Mtd1)
PS_ADD_METHOD(Mtd2)
PS_ADD_METHOD(Mtd3)
PS_ADD_METHOD(Mtd4)
PS_ADD_METHOD(Mtd5)
PS_ADD_METHOD(Mtd6)
PS_ADD_METHOD(Mtd7)
PS_ADD_METHOD(Mtd8)
PS_END_MAP()
PS_REGISTER_PSTYPES(IFace)
То к чему у меня было большое желание прийти — это отказаться от указания информации о параметрах и возвращаемых значениях при описании Proxy/Stub, задавая только имена методов. За это пришлось немного заплатить небольшим ограничением: отказом от использования перегрузки методов. И это ограничение можно обойти, добавив небольшой макрос, который уже не будет столь лаконичен. В рамках этого поста этого макроса не будет. При соответствующей мотивации это можно сделать очень быстро.
Тяга к такому минимализму обусловлена тем, что приходилось иметь дело с разными библиотеками и framework'ами, которые заставляли прилагать много усилий для описания каждого метода и если не было автогенерации, то при внесении изменений в метод, приходилось после пинка компилятора вспоминать, что забыл поправить Proxy/Stub, отправляться в соответствующий файл и править. А когда и была автогенерация, то она иногда сильно изобиловала тонкостями описания ее входных данных, на основании которых она работала. Полностью от этого все же не удалось избавиться, так как средствами C++ можно всю информацию о методах класса легко получить, а вот стандартных средств перечислить методы нет. Поэтому единственное, что придется делать при изменении интерфейса — это добавлять и удалять методы в описании Proxy/Stub при их добавлении и удалении в интерфейсе, при этом поддерживать строгий порядок следования Proxy/Stub описания методов последовательности методов самого интерфейса нет необходимости. Этого не было в ранее опубликованном посте [4], а теперь благодаря средствам C++11 такую независимость удалось получить.
#include <iostream>
#include <string>
#include "face.h" // Класс-реализация интерфейса IFace.
#include "iface_ps.h" // Описание Proxy/Stub интерфейса.
#include "xml/pack.h" // Реализация (де)сериализации
#include "http/server_creator.h" // Функция создания сервера.
#include "class_ids.h" // Идентификаторы классов-реализаций.
int main()
{
try
{
// Создание сервера.
auto Srv = Remote::Http::CreateServer
<
Remote::Pkg::Xml::InputPack, // Тип десериализатора входящих пакетов
Remote::ClassInfo // Описание реализации интерфейса.
// Описаний Remote::ClassInfo может быть несколько.
// На каждую реализацию здесь передается Remote::ClassInfo.
<
IFace, // Реализуемый интерфейс
Face, // Реализация интерфейса
FaceClsId // Идентификатор реализации
>
>("127.0.0.1" /*IP*/, 5555/*Port*/, 2/*ThreadCount*/); // Сетевой интерфейс, на котором работает сервер.
std::cin.get(); // "Завешиваем" основной поток. Сервер работает пока существует его объект.
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
В примере показано, что при создании сервера нужно указать тип, который будет заниматься сборкой и разбором пакетов данных и указать список классов, реализации которых сервер будет поставлять его клиентам. Так как для одного и того же интерфейса может быть несколько разных реализаций, то они маркируются идентификатором. При создании объекта клиент передает серверу идентификатор реализации. Классы-реализации могут наследовать множество всего, а для того, чтобы при запросе клиента сервер мог создать нужный объект-заглушку, указывается и интерфейс, который реализует класс. По этому интерфейсу производится поиск нужного типа заглушки. Сервер может поставлять множество реализаций для множества интерфейсов и все они должны быть перечислены при создании сервера. Функцию CreateServer можно переписать для своего вида транспорта, а все необходимое для этого уже есть. Нужен только транспорт при желании его заменить. Вся работа по созданию объектов и объектов-заглушек уже реализована и не нуждается в замене.
#include <iostream>
#include "iface_ps.h" // Описание Proxy/Stub интерфейса.
#include "class_ids.h" // Идентификаторы классов-реализаций.
#include "xml/pack.h" // Реализация (де)сериализации.
#include "http/http_remoting.h" // Реализация транспорта клиента.
#include "class_factory.h" // Фабрика классов.
int main()
{
try
{
// Создание транспорта. В данном случае на основе libevent.
// Реализовав интерфейс Remote::Http::Remoting можно предоставить собственный транспорт.
auto Remoting = std::make_shared<Remote::Http::Remoting>("127.0.0.1", 5555);
auto Factory = Remote::ClassFactory::Create // Создание фабрики классов.
<
Remote::Pkg::Xml::OutputPack, // Тип для сериализации исходящих пакетов.
IFace // Список поддерживаемых интерфейсов. В данном примере один интерфейс.
>(Remoting);
// Создание объекта с интерфейсом IFace и идентификатором реализации на стороне сервера FaceClsId.
auto Obj = Factory->Create<IFace>(FaceClsId);
// Вызов методов объекта на сервере.
Obj->Mtd2(10);
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
При создании фабрики классов ей нужно передать объект, реализующий транспорт и список интерфейсов, которые она поддерживает. По этому списку интерфейсов фабрика для созданного на сервере объекта у себя определяет прокси-объект, которым пользователь будет пользоваться для вызова методов интерфейса.
Можно заметить из примера, что фабрика не является классом-шаблоном, а только ее методы создания самой фабрики и объектов являются шаблонными. Зачем? Если бы фабрика была шаблоном, то по всем единицам трансляции нужно было бы «таскать» за собой всю информацию о ее типах, с которыми она работает. А так можно воспользоваться forward declaration и передав указатель на фабрику в иной файл проекта, в котором уже не надо знать и подключать все файлы с информацией о типе, производящем сериализацию и десереализацию пакетов и файлов с интерфейсами, использование которых в той части проекта не нужно.
Примеры приведены, даны пояснения для чего сделано то или иное решение, можно переходить к техническим аспектам реализации, а реализация еще раз повторюсь поделена на две крупные части:
- Создание Proxy/Stub'ов
- Инфраструктура и транспорт
Proxy/Stubs
При возникновении необходимости в разделении объекта и его использующего кода между клиентом и сервером, в этот момент появляется необходимость в прокси-объектах (Proxy) на стороне клиента и объектах-заглушках (Stub) на стороне сервера. Первые при вызове метода на стороне клиента создают запрос и отправляют его на сервер, а вторые на стороне сервера разбирают этот запрос и вызывают соответствующий метод реального объекта.
Как с помощью C++ можно получить всю информацию о методе на основе указателя на метод в момент компиляции. А так же как с помощью полученной информации и преимуществ C++11 создавать наборы Proxy/Stub так же в момент компиляции будет рассказано в этом разделе. Это можно сделать на основе шаблонов и немного прибегнув к средствам препроцессора.
Предположим есть такой интерфейс:
struct ISessionManager
{
virtual ~ISessionManager() {}
virtual std::uint32_t OpenSession(char const *userName, char const *password) = 0;
virtual void CloseSession(std::uint32_t sessionId) = 0;
virtual bool IsValidSession(std::uint32_t sessionId) const = 0;
};
предназначенный для работы с сессиями пользователей. OpenSession — открывает сессию для заданного пользователя и в качестве результата возвращает идентификатор открытой сессии. CloseSession закрывает сессию по переданному идентификатору. IsValidSession — проверяет идентификатор сессии на валидность.
При создании Proxy/Stub для каждого из методов надо получить его тип. Тип указателя на метод. В C++03 стандартными средствами этого сделать нельзя. Приходилось прибегать к некоторым компиляторозависимым решениям [4]. Так для gcc можно было воспользоваться его расширением typeof, а для MS Visual Studio сделать (более ранних версий, чем 2010) некоторый хак на шаблонах, который мог компилироваться только ее компилятором, т. к. подход выходит за рамки стандарта. Это все и многое другое можно подсмотреть, например, в boost. С появлением C++11 это стало возможно в рамках стандарта. Получать типы переменных и выражений можно с помощью decltype.
typedef decltype(&ISessionManager::OpenSession) OpenSessionMtdType;
typedef decltype(&ISessionManager::CloseSession) CloseSessionMtdType;
typedef decltype(&ISessionManager::IsValidSession) IsValidSessionMtdType;
В этом и кроется ограничение, которое не дает при рассматриваемой простоте использования макросов создания Proxy/Stub использовать перегрузку методов, так как при передаче в decltype указателя на метод нет средств указать компилятору какой из перегруженных методов нужно использовать.
Получив тип метода, можно с помощью еще одного средства C++11, шаблонов с переменным числом параметров сделать реализации для каждого из методов и на их основе построить или прокси-объект или объект-заглушку, получая при этом всю информацию о методе: о возвращаемом значении, передаваемых параметрах и его cv-квалификаторе (в данном случае интересен только const квалификатор для небольшого упрощения). А так как нужно в эту реализацию еще и имя метода подставить, то придется немного прибегнуть к препроцессору. На основании этого можно получить такой макрос для реализации метода:
namespace Methods
{
template <std::uint32_t, typename>
struct Method;
}
#define DECLARE_PROXY_METHOD(iface_, mtd_, id_)
namespace Methods
{
typedef decltype(&iface_::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct Method<id_, R (C::*)(P ...)>
: public virtual iface_
{
virtual R mtd_ (P ... p)
{
throw std::runtime_error("Not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<id_, R (C::*)(P ...) const>
: public virtual iface_
{
virtual R mtd_ (P ... p) const
{
throw std::runtime_error("Not implemented.");
}
};
typedef Method<id_, mtd_##Type> mtd_##ProxyType;
}
Можно заметить, что макрос определяет две специализации: для константного и неконстантного методов, и только одна будет из них инстанцирована для конкретного метода. С помощью этого макроса и всего что уже получено ранее можно собрать уже готовый прокси-класс:
DECLARE_PROXY_METHOD(ISessionManager, OpenSession, 1)
DECLARE_PROXY_METHOD(ISessionManager, CloseSession, 2)
DECLARE_PROXY_METHOD(ISessionManager, IsValidSession, 3)
template <typename ... T>
class Proxy
: public T ...
{
};
typedef Proxy
<
Methods::OpenSessionProxyType,
Methods::CloseSessionProxyType,
Methods::IsValidSessionProxyType
>
SessionManagerProxy;
Сам макрос принимает три параметра: интерфейс, метод и идентификатор метода. Если с интерфейсом и методом все просто, то идентификатор откуда-то надо взять. В [4] предлагалось сделать некоторый счетчик в момент компиляции и его использовать как идентификатор метода. Это накладывает некоторые ограничения: последовательность методов важна. Если собран сервер с одной последовательностью методов, а клиент с другой, то идентификторы методов не будут совпадать. И при обмене пакетами клиент и сервер не смогут «договориться» о том какой метод использовать для присланного идентификатора. Отсюда необходимость пересборки обеих частей при изменении порядка методов или при их удалении и добавлении в описание Proxy/Stub. С помощью C++11 такое ограничение можно устранить, вычислив CRC32 имени метода в момент компиляции, а constexpr позволяет это легко сделать в момент компиляции. Счетчик так же пригодится для других целей. А пока пара слов о генерации CRC32 в момент компиляции. Вариант создания CRC32 в момент компиляции уже приводился ранее в [1]. Здесь еще раз приведу его.
namespace Remote
{
namespace Private
{
template <typename T>
struct Crc32TableWrap
{
static constexpr std::uint32_t const Table[256] =
{
0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L,
0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L,
0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L,
// И т.д. заполнение таблицы
};
};
template <typename T>
constexpr std::uint32_t const Crc32TableWrap<T>::Table[256];
typedef Crc32TableWrap<void> Crc32Table;
template<std::uint32_t const I>
inline constexpr std::uint32_t Crc32Impl(char const *str)
{
return (Crc32Impl <I - 1>(str) >> 8) ^
Crc32Table::Table[(Crc32Impl<I - 1>(str) ^ str[I - 1]) & 0x000000FF];
}
template<>
inline constexpr std::uint32_t Crc32Impl<0>(char const *)
{
return 0xFFFFFFFF;
}
}
template <std::uint32_t N>
inline constexpr std::uint32_t Crc32(char const (&str)[N])
{
return (Private::Crc32Impl<sizeof(str)>(str) ^ 0xFFFFFFFF);
}
}
Теперь макрос определения прокси-объекта и его использование становятся чуть более дружелюбными, так как необходимости раздачи идентификаторов методам возложена на компилятор.
struct ISessionManager
{
virtual ~ISessionManager() {}
virtual std::uint32_t OpenSession(char const *userName, char const *password) = 0;
virtual void CloseSession(std::uint32_t sessionId) = 0;
virtual bool IsValidSession(std::uint32_t sessionId) const = 0;
};
typedef decltype(&ISessionManager::OpenSession) OpenSessionMtdType;
typedef decltype(&ISessionManager::CloseSession) CloseSessionMtdType;
typedef decltype(&ISessionManager::IsValidSession) IsValidSessionMtdType;
namespace Methods
{
template <std::uint32_t, typename>
struct Method;
}
#define DECLARE_PROXY_METHOD(iface_, mtd_)
namespace Methods
{
enum {mtd_##Id = Crc32(#mtd_)};
typedef decltype(&iface_::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...)>
: public virtual iface_
{
virtual R mtd_ (P ... p)
{
throw std::runtime_error("Not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...) const>
: public virtual iface_
{
virtual R mtd_ (P ... p) const
{
throw std::runtime_error("Not implemented.");
}
};
typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType;
}
DECLARE_PROXY_METHOD(ISessionManager, OpenSession)
DECLARE_PROXY_METHOD(ISessionManager, CloseSession)
DECLARE_PROXY_METHOD(ISessionManager, IsValidSession)
template <typename ... T>
class Proxy
: public T ...
{
};
typedef Proxy
<
Methods::OpenSessionProxyType,
Methods::CloseSessionProxyType,
Methods::IsValidSessionProxyType
>
SessionManagerProxy;
Несмотря на то, что в C++11 возможно наследование от шаблонного параметра, который является пакетом типов (шаблоны с переменным числом параметров), запись
typedef Proxy
<
Methods::OpenSessionProxyType,
Methods::CloseSessionProxyType,
Methods::IsValidSessionProxyType
>
SessionManagerProxy;
как-то еще очень слабо тянет на минимализм. Чтобы избавиться от конкретных имен типов в определении прокси-класса нужно создать некоторый реестр типов, пронумеровав каждый в нем содержащийся тип с помощью некоторого счетчика. На основе этого реестра и счетчика позднее построить иерархию наследования классов-реализаций методов интерфейса. Создать реестр можно с помощью небольшой связки шаблонов и макросов.
template <std::uint32_t>
struct TypeRegistry;
#define REGIDTER_TYPE(id_, type_)
template <>
struct TypeRegistry<id_>
{
typedef type_ Type;
};
Весьма простая реализация опять основанная на частных специализациях. Добавив это в ранее рассмотренный макрос определения метода он приблизится еще на шаг к конечной цели.
#define DECLARE_TYPE_REGISTRY(reg_name_)
namespace reg_name_
{
template <std::uint32_t>
struct TypeRegistry;
}
#define REGIDTER_TYPE(id_, type_)
template <>
struct TypeRegistry<id_>
{
typedef type_ Type;
};
DECLARE_TYPE_REGISTRY(Methods)
namespace Methods
{
template <std::uint32_t, typename>
struct Method;
}
#define DECLARE_PROXY_METHOD(iface_, mtd_, id_)
namespace Methods
{
enum {mtd_##Id = Crc32(#mtd_)};
typedef decltype(&iface_::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...)>
: public virtual iface_
{
virtual R mtd_ (P ... p)
{
throw std::runtime_error("Not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...) const>
: public virtual iface_
{
virtual R mtd_ (P ... p) const
{
throw std::runtime_error("Not implemented.");
}
};
typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType;
REGIDTER_TYPE(id_, mtd_##ProxyType)
}
DECLARE_PROXY_METHOD(ISessionManager, OpenSession, 1)
DECLARE_PROXY_METHOD(ISessionManager, CloseSession, 2)
DECLARE_PROXY_METHOD(ISessionManager, IsValidSession, 3)
template <typename ... T>
class Proxy
: public T ...
{
};
typedef Proxy
<
typename Methods::TypeRegistry<1>::Type,
typename Methods::TypeRegistry<2>::Type,
typename Methods::TypeRegistry<3>::Type
>
SessionManagerProxy;
Посмотрев на конечный код построения прокси-класса
typedef Proxy
<
typename Methods::TypeRegistry<1>::Type,
typename Methods::TypeRegistry<2>::Type,
typename Methods::TypeRegistry<3>::Type
>
SessionManagerProxy;
можно заметить, что в нем пропали какие-то специфичные имена при определении прокси-класса, но опять появился какой-то идентификатор. На данный момент это не идентификатор, это счетчик под которым находится определенная запись в реестре типов. Построив счетчик в момент компиляции можно отказаться от явного задания каких-то «цифр» и построить завершающий вариант упрощенных макросов для определения прокси-класса из информации о его методах.
Как сделать счетчик в момент компиляции уже было написано в [4]. Бегло повторю. Для построения счетчика нужно:
- Сгенерировать некоторую иерархию типов.
- Объявить функцию принимающую void * и возвращающую массив char размеров в один элемент.
- На каждом шаге для каждой новой константы счетчика с помощью sizeof получать размер массива объявленной (но не определенной) функции и объявлять новую функцию с одним из типов иерархии, которая при спуске по иерархии возвращает массив все большей и большей длины. Реализация функций не требуется, т. к. они никогда не вызываются, а используются в момент компиляции под sizeof для вычисления размера возвращаемого значения.
Описания алгоритма звучит более непонятным, чем он реализуется… Реализация проста:
namespace Private
{
template <unsigned N>
struct Hierarchy
: public Hierarchy<N - 1>
{
};
template <>
struct Hierarchy<0>
{
};
}
#define INIT_STATIC_COUNTER(counter_name_, max_count_)
namespace counter_name_
{
typedef ::Private::Hierarchy<max_count_> CounterHierarchyType;
char (&GetCounterValue(void const *))[1];
}
#define GET_NEXT_STATIC_COUNTER(counter_name_, value_name_)
namespace counter_name_
{
enum { value_name_ = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0))) };
char (&GetCounterValue(::Private::Hierarchy<value_name_> const *))[value_name_ + 1];
}
Для того, чтобы опробовать работу такого счетчика можно написать тест:
INIT_STATIC_COUNTER(MyCounter, 100)
GET_NEXT_STATIC_COUNTER(MyCounter, Item1)
GET_NEXT_STATIC_COUNTER(MyCounter, Item2)
GET_NEXT_STATIC_COUNTER(MyCounter, Item3)
int main()
{
std::cout << MyCounter::Item1 << std::endl;
std::cout << MyCounter::Item2 << std::endl;
std::cout << MyCounter::Item3 << std::endl;
return 0;
}
В результате на экране будут распечатаны значения от одного до трех. Это можно отправить компилятору с ключом -E для того чтобы развернуть все макросы.
#include <iostream>
namespace Private
{
template <unsigned N>
struct Hierarchy
: public Hierarchy<N - 1>
{
};
template <>
struct Hierarchy<0>
{
};
}
namespace MyCounter
{
typedef ::Private::Hierarchy<100> CounterHierarchyType; char (&GetCounterValue(void const *))[1];
}
namespace MyCounter
{
enum
{
Item1 = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<Item1> const *))[Item1 + 1];
}
namespace MyCounter
{
enum
{
Item2 = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<Item2> const *))[Item2 + 1];
}
namespace MyCounter
{
enum
{
Item3 = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<Item3> const *))[Item3 + 1];
}
int main()
{
std::cout << MyCounter::Item1 << std::endl;
std::cout << MyCounter::Item2 << std::endl;
std::cout << MyCounter::Item3 << std::endl;
return 0;
}
Как и говорилось все просто. В начале немного макросы пугают, но тут они как раз на пользу идут. Без них трудно будет реализовать счетчик.
Все составляющие для генерации конечного прокси-класса есть. Прокси-класс создается из его составляющих кубиков, каждый из которых содержит реализацию для одного из методов интерфейса. Можно написать конечный набор макросов для любого интерфейса и проверить на уже ранее разбираемом ISessionManager.
#define BEGIN_PROXY_MAP(iface_)
namespace iface_##PS
{
namespace Impl
{
typedef iface_ IFaceType;
INIT_STATIC_COUNTER(MtdCounter, 100)
namespace Methods
{
DECLARE_TYPE_REGISTRY(ProxiesReg)
template <std::uint32_t, typename>
struct Method;
}
#define ADD_PROXY_METHOD(mtd_)
GET_NEXT_STATIC_COUNTER(MtdCounter, mtd_##Counter)
namespace Methods
{
enum {mtd_##Id = Crc32(#mtd_)};
typedef decltype(&IFaceType::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...)>
: public virtual IFaceType
{
virtual R mtd_ (P ... p)
{
throw std::runtime_error(#mtd_ " not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...) const>
: public virtual IFaceType
{
virtual R mtd_ (P ... p) const
{
throw std::runtime_error(#mtd_ " not implemented.");
}
};
typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType;
REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType)
}
#define END_PROXY_MAP()
GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter)
template <unsigned I>
class ProxyItem
: public Methods::ProxiesReg::TypeRegistry<I>::Type
, public ProxyItem<I - 1>
{
};
template <>
class ProxyItem<0>
{
};
}
typedef Impl::ProxyItem<Impl::MtdCounter::LastCounter - 1> Proxy;
}
namespace Private
{
template <unsigned N>
struct Hierarchy
: public Hierarchy<N - 1>
{
};
template <>
struct Hierarchy<0>
{
};
}
#define INIT_STATIC_COUNTER(counter_name_, max_count_)
namespace counter_name_
{
typedef ::Private::Hierarchy<max_count_> CounterHierarchyType;
char (&GetCounterValue(void const *))[1];
}
#define GET_NEXT_STATIC_COUNTER(counter_name_, value_name_)
namespace counter_name_
{
enum { value_name_ = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0))) };
char (&GetCounterValue(::Private::Hierarchy<value_name_> const *))[value_name_ + 1];
}
#define DECLARE_TYPE_REGISTRY(reg_name_)
namespace reg_name_
{
template <std::uint32_t>
struct TypeRegistry;
}
#define REGIDTER_TYPE(reg_name_, id_, type_)
namespace reg_name_
{
template <>
struct TypeRegistry<id_>
{
typedef type_ Type;
};
}
struct ISessionManager
{
virtual ~ISessionManager() {}
virtual std::uint32_t OpenSession(char const *userName, char const *password) = 0;
virtual void CloseSession(std::uint32_t sessionId) = 0;
virtual bool IsValidSession(std::uint32_t sessionId) const = 0;
};
BEGIN_PROXY_MAP(ISessionManager)
ADD_PROXY_METHOD(OpenSession)
ADD_PROXY_METHOD(CloseSession)
ADD_PROXY_METHOD(IsValidSession)
END_PROXY_MAP()
int main()
{
try
{
ISessionManagerPS::Proxy Proxy;
Proxy.OpenSession("user", "111");
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Как можно видеть из приведенного примера получился простой интерфейс для описания прокси-класса, внешне мало отличающийся от того, что было приведено в примерах в начале поста. Этот код уже можно скомпилировать и попробовать вызвать методы прокси-объекта для интерфейса ISessionManager и в ответ получить исключение с сообщением «OpenSession not implemented.». На данный момент реализация каждого метода просто кидает исключение о том что метод пока ничего не делает. Немногим позднее эти реализации будут заполнены более осмысленным кодом. А пока можно попробовать пример, приведенный выше, отправить компилятору с ключом -E и посмотреть во что развернулись все макросы. Кода после раскрытия всех макросов получилось не так и мало для восприятия человеком как вспомогательного материала при чтении поста. Да и по большому счету он и не рассчитан на человека, препроцессор сделал свою грязную работу, заменив частично утилиту автогенерации, теперь очередь за компилятором продолжать работу над полученными типами, продолжая заменять утилиту автогенерации кода: инстанцировать, рассчитывать полученный счетчик и т. д. Можно по диагонали просмотреть код и легко понять его структуру, что получилось после разворачивания всех макросов.
namespace Private
{
template <unsigned N>
struct Hierarchy
: public Hierarchy<N - 1>
{
};
template <>
struct Hierarchy<0>
{
};
}
namespace ISessionManagerPS
{
namespace Impl
{
typedef ISessionManager IFaceType;
namespace MtdCounter
{
typedef ::Private::Hierarchy<100> CounterHierarchyType;
char (&GetCounterValue(void const *))[1];
}
namespace Methods
{
namespace ProxiesReg
{
template <std::uint32_t>
struct TypeRegistry;
}
template <std::uint32_t, typename>
struct Method;
}
namespace MtdCounter
{
enum
{
OpenSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<OpenSessionCounter> const *))[OpenSessionCounter + 1];
}
namespace Methods
{
enum
{
OpenSessionId = Crc32("OpenSession")
};
typedef decltype(&IFaceType::OpenSession) OpenSessionType;
template <typename R, typename C, typename ... P>
struct Method<OpenSessionId, R (C::*)(P ...)>
: public virtual IFaceType
{
virtual R OpenSession (P ... p)
{
throw std::runtime_error("OpenSession" " not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<OpenSessionId, R (C::*)(P ...) const>
: public virtual IFaceType
{
virtual R OpenSession (P ... p) const
{
throw std::runtime_error("OpenSession" " not implemented.");
}
};
typedef Method<OpenSessionId, OpenSessionType> OpenSessionProxyType;
namespace ProxiesReg
{
template <>
struct TypeRegistry<MtdCounter::OpenSessionCounter>
{
typedef OpenSessionProxyType Type;
};
}
}
namespace MtdCounter
{
enum
{
CloseSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<CloseSessionCounter> const *))[CloseSessionCounter + 1];
}
namespace Methods
{
enum
{
CloseSessionId = Crc32("CloseSession")
};
typedef decltype(&IFaceType::CloseSession) CloseSessionType;
template <typename R, typename C, typename ... P>
struct Method<CloseSessionId, R (C::*)(P ...)>
: public virtual IFaceType
{
virtual R CloseSession (P ... p)
{
throw std::runtime_error("CloseSession" " not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<CloseSessionId, R (C::*)(P ...) const>
: public virtual IFaceType
{
virtual R CloseSession (P ... p) const
{
throw std::runtime_error("CloseSession" " not implemented.");
}
};
typedef Method<CloseSessionId, CloseSessionType> CloseSessionProxyType;
namespace ProxiesReg
{
template <>
struct TypeRegistry<MtdCounter::CloseSessionCounter>
{
typedef CloseSessionProxyType Type;
};
}
}
namespace MtdCounter
{
enum
{
IsValidSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<IsValidSessionCounter> const *))[IsValidSessionCounter + 1];
}
namespace Methods
{
enum
{
IsValidSessionId = Crc32("IsValidSession")
};
typedef decltype(&IFaceType::IsValidSession) IsValidSessionType;
template <typename R, typename C, typename ... P>
struct Method<IsValidSessionId, R (C::*)(P ...)>
: public virtual IFaceType
{
virtual R IsValidSession (P ... p)
{
throw std::runtime_error("IsValidSession" " not implemented.");
}
};
template <typename R, typename C, typename ... P>
struct Method<IsValidSessionId, R (C::*)(P ...) const>
: public virtual IFaceType
{
virtual R IsValidSession (P ... p) const
{
throw std::runtime_error("IsValidSession" " not implemented.");
}
};
typedef Method<IsValidSessionId, IsValidSessionType> IsValidSessionProxyType;
namespace ProxiesReg
{
template <>
struct TypeRegistry<MtdCounter::IsValidSessionCounter>
{
typedef IsValidSessionProxyType Type;
};
}
}
namespace MtdCounter
{
enum
{
LastCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
};
char (&GetCounterValue(::Private::Hierarchy<LastCounter> const *))[LastCounter + 1];
}
template <unsigned I>
class ProxyItem
: public Methods::ProxiesReg::TypeRegistry<I>::Type
, public ProxyItem<I - 1>
{
};
template <>
class ProxyItem<0>
{
};
}
typedef Impl::ProxyItem<Impl::MtdCounter::LastCounter - 1> Proxy;
}
int main()
{
try
{
ISessionManagerPS::Proxy Proxy;
Proxy.OpenSession("user", "111");
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Реализовав все методы интерфейса в прокси-классе, эти реализации нужно заполнить кодом, который бы занимался упаковкой всех переданных параметров, отправкой полученного пакета данных на сервер и полученный ответ разбирал бы, находя в нем результат выполнения метода (если таковой имеется), а так же если в ходе выполнения метода было выброшено исключение, то получить информацию о нем из пакета с ответом, так как исключения так же передаются от сервера клиенту.
Такой код можно разместить в каждой из реализаций методов. Однако это было бы не самым красивым и оптимальным решением. Желательно этот код расположить в одном и том же месте. Для решения этой задачи хорошо подойдет такой шаблон проектирования, как CRTP [7].
Все «кубики» (классы-реализации методов) можно наследовать от класса, который будет иметь метод для выполнения манипуляций с пакетом данных. А так как вся необходимая информация находится в самом прокси-классе (в самом низу иерархии наследования), то туда и должны перенаправляться все вызовы. Зачем? Логичнее всего было бы в этом прокси-классе разместить информацию об идентификаторе экземпляра объекта на стороне сервера, а так же указатель на интерфейс, через который осуществляется взаимодействие (транспорт), а не вставлять однотипный код с обработкой этой информации по всем классам-реализациям методов интерфейса.
В данном случае CRTP очень хорошо подходит и имеет преимущество над обычными интерфейсами с наборами чисто виртуальных методов. Сделать шаблонной виртуальную функцию нельзя, а сделать метод класса в CRTP шаблонным ничего не мешает. А так как в данном случае количество параметров для каждого вызова такого метода для работы с пакетами данных разное в силу того что разные методы интерфейса могут иметь разное количество параметров, то использование CRTP оказывается очень полезным.
Базовым классом для всех классов-реализаций методов будет:
template <typename T>
class ProxyMethodBase
{
public:
template <typename R, typename ... P>
R Execute(std::uint32_t mtdId, P ... params)
{
return dynamic_cast<T &>(*this).Execute<R, P ...>(mtdId, params ...);
}
protected:
virtual ~ProxyMethodBase()
{
}
};
Так как все реализации методов наследуют ProxyMethodBase виртуально, то в реализации ProxyMethodBase придется использовать dynamic_cast, а не более привычный static_cast для CRTP. Виртуальное наследование нужно для того, чтобы для всех методов определить одну базу и чтобы компилятор не испытывал затруднения при определении ветки наследования при приведении типа. Виртуальный деструктор в ProxyMethodBase нужен как минимум для того же, чтобы компилятор мог работать с dynamic_cast, в противном случае он скажет, что тип, к которому производится попытка приведения с помощью dynamic_cast не является полиморфным, а должен.
С учетом добавленного базового класса для всех реализаций методов интерфейса немного меняются и макросы описания прокси-класса.
#define BEGIN_PROXY_MAP(iface_)
namespace iface_##PS
{
namespace Impl
{
typedef iface_ IFaceType;
INIT_STATIC_COUNTER(MtdCounter, 100)
class ProxyImpl;
namespace Methods
{
DECLARE_TYPE_REGISTRY(ProxiesReg)
template <std::uint32_t, typename>
struct Method;
}
#define ADD_PROXY_METHOD(mtd_)
GET_NEXT_STATIC_COUNTER(MtdCounter, mtd_##Counter)
namespace Methods
{
enum {mtd_##Id = Crc32(#mtd_)};
typedef decltype(&IFaceType::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...)>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
virtual R mtd_ (P ... p)
{
return Execute<R, P ...>(mtd_##Id, p ...);
}
};
template <typename R, typename C, typename ... P>
struct Method<mtd_##Id, R (C::*)(P ...) const>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
typedef Method<mtd_##Id, R (C::*)(P ...) const> ThisType;
virtual R mtd_ (P ... p) const
{
return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...);
}
};
typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType;
REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType)
}
#define END_PROXY_MAP()
GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter)
template <unsigned I>
class ProxyItem
: public Methods::ProxiesReg::TypeRegistry<I>::Type
, public ProxyItem<I - 1>
{
};
template <>
class ProxyItem<0>
{
};
class ProxyImpl
: public ProxyItem<MtdCounter::LastCounter - 1>
{
public:
private:
friend class ProxyMethodBase<ProxyImpl>;
template <typename R, typename ... P>
R Execute(std::uint32_t mtdId, P ... params)
{
throw std::runtime_error("Not implemented.");
}
};
}
typedef Impl::ProxyImpl Proxy;
}
Пользовательский код же никак не меняется. В то же время в реализации прокси-класса появилась одна локализованная точка, в которой можно производить все манипуляции с параметрами и работу с сервером. Если посмотреть на метод прокси-класса, куда в результате приходит вызов метода интерфейса
template <typename R, typename ... P>
R Execute(std::uint32_t mtdId, P ... params)
{
// ...
}
то здесь уже можно воспользоваться классической схемой перебора всех параметров вызванного метода для их сериализации, которая практически во всех источниках, посвященных C++11 приводится на примере реализации типа безопасной функции printf.
template <typename S>
void Serialize(S &)
{
}
template <typename S, typename T, typename ... P>
void Serialize(S &stream, T prm, P ... params)
{
stream << prm;
Serialize(stream, params ...);
}
И добавив вызов Serialize в метод Execute можно получить сериализацию параметров. Остается только реализовать свой класс для сериализации. Это рутинная и неинтересная задача. Ее решение на основе xml и rapidxml [6] приведено в исходных кодах прилагаемых к этому посту.
Все, реализация прокси-класса полностью готова! Она немного отличается деталями от той, что приведана в исходном коде к этому посту. Отличие только в более сложной обработке параметров и взаимодействии с сервером — это все рутина, а идеологически она построена точно так же со всеми описанными подходами.
Выше говорилось о Proxy/Stub, а пока реализована работа только с прокси. Реализация классов-заглушек (Stub) почти аналогичны реализации прокси-классов. Отличие заключается в том, что для прокси-класса нужно при вызове метода интерфейса всю информацию упаковать и отправить, а полученный результат отдать вызывающей стороне, а для класса-заглушки нужно выполнить все строго наоборот: распаковать полученный пакет, на его основе собрать параметры в некоторый список аргументов метода интерфейса и вызвать его, а полученный результат отправить вызывающей стороне. Для этого нужно в существующие макросы добавить немного изменений, связанных с вызовом метода с параметрами, извлеченными из пришедшего пакета. Все остальное остается таким же. Так же нужен и реестр, и счетчик, и сбор конечного класса из его «кубиков». Реестра теперь становится два: один для прокси-классов, второй для класса-заглушек. Можно и в один все поместить, а потом разбирать какой элемент чем является. Это приведет к более сложному коду. Поэтому добавлен второй реестр для объектов-заглушек.
#define PS_BEGIN_MAP(iface_)
namespace iface_##PS
{
namespace Impl
{
typedef iface_ IFaceType;
INIT_STATIC_COUNTER(MtdCounter, 100)
class ProxyImpl;
class StubImpl;
namespace Methods
{
DECLARE_TYPE_REGISTRY(ProxiesReg)
template <std::uint32_t, typename>
struct ProxyMethod;
DECLARE_TYPE_REGISTRY(StubsReg)
template <std::uint32_t, typename>
struct StubMethod;
}
#define PS_ADD_METHOD(mtd_)
GET_NEXT_STATIC_COUNTER(MtdCounter, mtd_##Counter)
namespace Methods
{
enum {mtd_##Id = Crc32(#mtd_)};
typedef decltype(&IFaceType::mtd_) mtd_##Type;
template <typename R, typename C, typename ... P>
struct ProxyMethod<mtd_##Id, R (C::*)(P ...)>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
virtual R mtd_ (P ... p)
{
return Execute<R, P ...>(mtd_##Id, p ...);
}
};
template <typename R, typename C, typename ... P>
struct ProxyMethod<mtd_##Id, R (C::*)(P ...) const>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
typedef ProxyMethod<mtd_##Id, R (C::*)(P ...) const> ThisType;
virtual R mtd_ (P ... p) const
{
return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...);
}
};
typedef ProxyMethod<mtd_##Id, mtd_##Type> mtd_##ProxyType;
REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType)
template <typename R, typename C, typename ... P>
struct StubMethod<mtd_##Id, R (C::*)(P ...)>
: public virtual StubMethodBase<StubImpl>
{
template <typename TPkg>
R Call(TPkg &pack)
{
return Call(mtd_##Id, pack, &IFaceType::mtd_);
}
};
template <typename R, typename C, typename ... P>
struct StubMethod<mtd_##Id, R (C::*)(P ...) const>
: public virtual StubMethodBase<StubImpl>
{
template <typename TPkg>
R Call(TPkg &pack)
{
return Call(mtd_##Id, pack, &IFaceType::mtd_);
}
};
typedef StubMethod<mtd_##Id, mtd_##Type> mtd_##StubType;
REGIDTER_TYPE(StubsReg, MtdCounter::mtd_##Counter, mtd_##StubType)
}
#define PS_END_MAP()
GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter)
template <unsigned I>
class ProxyItem
: public Methods::ProxiesReg::TypeRegistry<I>::Type
, public ProxyItem<I - 1>
{
};
template <>
class ProxyItem<0>
{
};
class ProxyImpl
: public ProxyItem<MtdCounter::LastCounter - 1>
{
public:
private:
friend class ProxyMethodBase<ProxyImpl>;
template <typename R, typename ... P>
R Execute(std::uint32_t mtdId, P ... params)
{
Serialize(std::cout, params ...);
throw std::runtime_error("Not implemented.");
}
};
template <unsigned I>
class StubItem
: public Methods::StubsReg::TypeRegistry<I>::Type
, public StubItem<I - 1>
{
};
template <>
class StubItem<0>
{
};
class StubImpl
: public StubItem<MtdCounter::LastCounter - 1>
{
public:
private:
friend class StubMethodBase<StubImpl>;
template <typename C, typename R, typename ... P>
R Call(std::uint32_t mtdId, R (C::*mtd)(P ...))
{
throw std::runtime_error("Not implenented.");
}
};
}
typedef Impl::ProxyImpl Proxy;
typedef Impl::StubImpl Stub;
}
ruct ISessionManager
{
virtual ~ISessionManager() {}
virtual std::uint32_t OpenSession(char const *userName, char const *password) = 0;
virtual void CloseSession(std::uint32_t sessionId) = 0;
virtual bool IsValidSession(std::uint32_t sessionId) const = 0;
};
PS_BEGIN_MAP(ISessionManager)
PS_ADD_METHOD(OpenSession)
PS_ADD_METHOD(CloseSession)
PS_ADD_METHOD(IsValidSession)
PS_END_MAP()
int main()
{
try
{
ISessionManagerPS::Proxy Proxy;
ISessionManagerPS::Stub Stub;
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Немного поменялись имена макросов и некоторых сущностей под ними и добавлен код по формированию классов-заглушек. Теперь это полноценное описание Proxy/Stub, которое можно использовать при реализации собственного RPC на основе интерфейсов. В рамках же данного поста будет приведена некоторая инфраструктура и работа с транспортным уровнем, что даст предложенным идеям завершенность и возможность их использования как готового продукта. А пока еще раз заострю внимание на некоторых отличиях реализации классов-заглушек от прокси-классов. Если заглянуть в макрос PS_ADD_METHOD, то можно увидеть, что в него добавлены частные специализации для реализации классов, которые реализуют заглушки для каждого из методов. Отличие же таких реализаций заглушек от прокси в том, что тип, реализующий один из методов прокси наследуется от интерфейса, а тип, реализующий метод заглушки такого наследования не имеет, так как оно ему не нужно; при реализации заглушки нужно получить адрес метода интерфейса, который будет в дальнейшем вызван. Основные различия, взятые из макроса PS_ADD_METHOD приведены ниже.
template <typename R, typename C, typename ... P>
struct ProxyMethod<mtd_##Id, R (C::*)(P ...)>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
virtual R mtd_ (P ... p)
{
return Execute<R, P ...>(mtd_##Id, p ...);
}
};
template <typename R, typename C, typename ... P>
struct ProxyMethod<mtd_##Id, R (C::*)(P ...) const>
: public virtual IFaceType
, public virtual ProxyMethodBase<ProxyImpl>
{
typedef ProxyMethod<mtd_##Id, R (C::*)(P ...) const> ThisType;
virtual R mtd_ (P ... p) const
{
return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...);
}
};
template <typename R, typename C, typename ... P>
struct StubMethod<mtd_##Id, R (C::*)(P ...)>
: public virtual StubMethodBase<StubImpl>
{
template <typename TPkg>
R Call(TPkg &pack)
{
return Call(mtd_##Id, pack, &IFaceType::mtd_);
}
};
template <typename R, typename C, typename ... P>
struct StubMethod<mtd_##Id, R (C::*)(P ...) const>
: public virtual StubMethodBase<StubImpl>
{
template <typename TPkg>
R Call(TPkg &pack)
{
return Call(mtd_##Id, pack, &IFaceType::mtd_);
}
};
Осталось рассмотреть еще одну деталь относящуюся к классам-заглушкам — вызов метода класса-реализации интерфейса из класса-заглушки. При этом остается решить задачу обратную задаче формирования пакета из параметров: пакет параметров как-то «выпрямить» до списка параметров вызываемого метода интерфейса, который реализован в соответствующем классе-реализации. В прилагаемых исходных кодах к посту в файле ps.h приведен полный код того как из некоторого пакета данных извлекаются параметры и после построения списка параметров делается вызов. Здесь, чтобы не загромождать нюансами, приведу пример аналогичного действия. Предположим есть некоторый класс с методом, который принимает два целочисленных параметра и выводит их на экран.
class MyClass
{
public:
void Func(int prm1, int prm2)
{
std::cout << "Prm1: " << prm1 << std::endl;
std::cout << "Prm2: " << prm2 << std::endl;
}
};
И есть некоторый массив данных, в данном случае это вектор из пары целочисленных значений. Нужно написать код, который бы из вектора данных формировал список параметров и вызывал метод. Это можно сделать примерно таким способом.
class MyClass
{
public:
void Func(int prm1, int prm2)
{
std::cout << __FUNCTION__ << std::endl;
std::cout << "Prm1: " << prm1 << std::endl;
std::cout << "Prm2: " << prm2 << std::endl;
}
};
template <typename R, typename C, typename ... P>
constexpr unsigned GetParamCount(R (C::*)(P ...))
{
return sizeof ... (P);
}
template <unsigned I>
struct Mtd
{
template <typename TCtr, typename C, typename M, typename ... P>
static void Call(TCtr const &ctr, C &obj, M mtd, P ... p)
{
auto Prm = ctr[I - 1];
Mtd<I - 1>::Call(ctr, obj, mtd, Prm, p ... );
};
};
template <>
struct Mtd<0>
{
template <typename TCtr, typename C, typename M, typename ... P>
static void Call(TCtr const &ctr, C &obj, M mtd, P ... p)
{
(obj.*mtd)(p ...);
};
};
int main()
{
std::vector<int> Data( {10, 20} );
MyClass Obj;
Mtd<GetParamCount(&MyClass::Func)>::Call(Data, Obj, &MyClass::Func);
return 0;
}
Аналогичная реализация лежит в предлагаемой реализации удаленного взаимодействия при реализации классов-заглушек для вызова метода объекта на сервере из полученного пакета данных от клиента.
В заключении части о Proxy/Stub'ах предлагаю взглянуть на реальные макросы (можно по диагонали, т.к. они немного громоздки), которые используются при реализации Proxy/Stub для удаленного взаимодействия клиента с сервером.
#define PS_BEGIN_MAP(iface_)
namespace iface_##_PS
{
DECLARE_RUNTIME_EXCEPTION(Proxy)
DECLARE_RUNTIME_EXCEPTION(Stub)
namespace PSImpl
{
template <typename>
class Proxy;
class Stub;
typedef iface_ IFaceType;
enum { IFaceId = ::Remote::Crc32(#iface_) };
namespace Methods
{
INIT_STATIC_COUNTER(PSCounter, PS_MAX_METHOD_COUNT)
namespace MethodsProxy
{
typedef Proxy<void> ProxyClass;
DECLARE_TYPE_REGISTRY(ProxyTypeItemsReg)
template <typename TProxy, std::uint32_t Id, typename T>
class ProxyMethodImpl;
}
namespace MethodsStub
{
DECLARE_TYPE_REGISTRY(StubTypeItemsReg)
template <std::uint32_t Id, typename T>
class StubMethodImpl;
}
}
#define PS_ADD_METHOD(mtd_)
namespace Methods
{
GET_NEXT_STATIC_COUNTER(PSCounter, mtd_##Counter)
typedef decltype(&IFaceType::mtd_) mtd_##Type;
enum { mtd_##Id = Remote::Crc32(#mtd_) };
namespace MethodsProxy
{
template <typename TProxy, typename C, typename R, typename ... P>
class ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... )>
: public virtual IFaceType
, public virtual ::Remote::Private::PoxyMethodIFace<TProxy>
{
private:
virtual R mtd_ (P ... p)
{
return this->template __ProxyExecute<R, P ...>(mtd_##Id, p ...);
}
};
template <typename TProxy, typename C, typename R, typename ... P>
class ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... ) const>
: public virtual IFaceType
, public virtual ::Remote::Private::PoxyMethodIFace<TProxy>
{
private:
typedef ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... ) const> ThisType;
virtual R mtd_ (P ... p) const
{
return const_cast<ThisType *>(this)->template __ProxyExecute<R, P ...>(mtd_##Id, p ...);
}
};
typedef ProxyMethodImpl<ProxyClass, mtd_##Id, mtd_##Type> mtd_##ProxyImpl;
REGISTRY_ADD_TYPE(ProxyTypeItemsReg, PSCounter::mtd_##Counter, mtd_##ProxyImpl)
}
namespace MethodsStub
{
template <typename C, typename R, typename ... P>
class StubMethodImpl<mtd_##Id, R (C::*)(P ... )>
: public virtual ::Remote::Private::StubMethodIFace<Stub>
{
public:
enum { MethodId = mtd_##Id };
template <typename TPkg>
R __Call(TPkg &pkg)
{
return this->__StubCall(pkg, &IFaceType::mtd_);
}
};
template <typename C, typename R, typename ... P>
class StubMethodImpl<mtd_##Id, R (C::*)(P ... ) const>
: public virtual ::Remote::Private::StubMethodIFace<Stub>
{
public:
enum { MethodId = mtd_##Id };
template <typename TPkg>
R __Call(TPkg &pkg)
{
return this->__StubCall(pkg, &IFaceType::mtd_);
}
};
typedef StubMethodImpl<mtd_##Id, mtd_##Type> mtd_##StubImpl;
REGISTRY_ADD_TYPE(StubTypeItemsReg, PSCounter::mtd_##Counter, mtd_##StubImpl)
}
}
#define PS_END_MAP()
namespace Methods
{
GET_NEXT_STATIC_COUNTER(PSCounter, LastPSCounter)
namespace MethodsProxy
{
template <typename T, typename TPack>
struct ChangePackPrm;
template <typename TPack, std::uint32_t Id, typename TMtd, typename TNewPack>
struct ChangePackPrm<ProxyMethodImpl<TPack, Id, TMtd>, TNewPack>
{
typedef ProxyMethodImpl<Proxy<TNewPack>, Id, TMtd> Type;
};
template <typename TPack, std::size_t const N>
class ProxyMethodsImpl
: public ProxyMethodsImpl<TPack, N - 1>
, public ChangePackPrm<typename ProxyTypeItemsReg<N>::Type, TPack>::Type
{
};
template <typename TPack>
class ProxyMethodsImpl<TPack, 0>
: public virtual IFaceType
{
};
}
}
template <typename TPack>
class Proxy
: public Methods::MethodsProxy::ProxyMethodsImpl<TPack, Methods::PSCounter::LastPSCounter - 1>
{
public:
Proxy(std::uint32_t instId, ::Remote::IRemotingPtr remoting)
: InstId(instId)
, Remoting(remoting)
{
if (!Remoting)
throw ProxyException("IRemoting pointer must not be null.");
}
private:
template <typename>
friend class ::Remote::Private::PoxyMethodIFace;
std::uint32_t InstId;
::Remote::IRemotingPtr Remoting;
template <typename R, typename ... P>
R __ProxyExecuteImpl(std::uint32_t mtdId, P ... p)
{
TPack Pack(IFaceId, mtdId, InstId, 0/*thread*/, p ...);
auto Buf(std::move(Remoting->Execute(Pack.GetData(), Pack.GetSize())));
auto const &BufRef = *Buf;
auto NewPack(std::move(TPack::CreateFromData(&BufRef[0], BufRef.size())));
if (NewPack->GetIFaceId() != IFaceId)
throw ProxyException("Bad interface Id.");
if (NewPack->GetInstId() != InstId)
throw ProxyException("Bad instance Id.");
if (NewPack->GetMethodId() != mtdId)
throw ProxyException("Bad method Id.");
auto Exception = NewPack->template GetException<::Remote::RemotingException>();
if (Exception != std::exception_ptr())
std::rethrow_exception(Exception);
::Remote::Private::UpdateOutputParams<decltype(*NewPack), P ...>(*NewPack, p ...);
LastPack = std::move(::Remote::Private::IDisposablePtr(new ::Remote::Private::Disposable<decltype(NewPack)>(NewPack)));
return ::Remote::Private::ResultExtractor<R>::Extract(*NewPack);
}
private:
::Remote::Private::IDisposablePtr LastPack;
};
namespace Methods
{
namespace MethodsStub
{
template <std::size_t const N>
class StubMethodsImpl
: public StubMethodsImpl<N - 1>
, public StubTypeItemsReg<N>::Type
{
public:
template <typename TPkg>
void __StubMethodCall(TPkg &pkg, std::uint32_t mtdId)
{
typedef typename StubTypeItemsReg<N>::Type StubMtdType;
if (mtdId == StubMtdType::MethodId)
{
typedef decltype(StubMtdType::__Call(pkg)) R;
::Remote::Private::CallAndPackRes<R>::Call(this, &StubMtdType::__Call, pkg);
return;
}
StubMethodsImpl<N - 1>::__StubMethodCall(pkg, mtdId);
}
};
template <>
class StubMethodsImpl<0>
{
public:
template <typename TPkg>
void __StubMethodCall(TPkg &, std::uint32_t)
{
throw StubException("Method not found.");
}
};
}
}
class Stub
: public Methods::MethodsStub::StubMethodsImpl<Methods::PSCounter::LastPSCounter - 1>
{
public:
typedef std::shared_ptr<IFaceType> IFaceTypePtr;
Stub(std::uint32_t instId, IFaceTypePtr obj)
: InstId(instId)
, Obj(obj)
{
if (!Obj)
throw StubException("IFace pointer must not be null.");
}
virtual ~Stub()
{
}
template <typename TPack>
::Remote::DataBufPtr Call(TPack &pack)
{
try
{
if (pack.GetIFaceId() != IFaceId)
throw StubException("Bad interface Id.");
if (pack.GetInstId() != InstId)
throw StubException("Bad instance Id.");
this->__StubMethodCall(pack, pack.GetMethodId());
pack.PackParams();
}
catch (std::exception const &e)
{
pack.GetPack().SetException(e);
}
::Remote::DataBufPtr Ret(new ::Remote::DataBuf);
auto &NewPack = pack.GetPack();
auto Size = NewPack.GetSize();
if (Size)
{
void const *Data = NewPack.GetData();
std::copy(
reinterpret_cast<char const *>(Data),
reinterpret_cast<char const *>(Data) + Size,
std::back_inserter(*Ret)
);
}
return std::move(Ret);
}
private:
template <typename>
friend class ::Remote::Private::StubMethodIFace;
std::uint32_t InstId;
IFaceTypePtr Obj;
template <typename TPkg, typename R, typename C, typename ... P>
R __StubCallImpl(TPkg &pkg, R (C::*mtd)(P ...))
{
return ::Remote::Private::CallStubMethod::Call(Obj.get(), mtd, pkg);
}
template <typename TPkg, typename R, typename C, typename ... P>
R __StubCallImpl(TPkg &pkg, R (C::*mtd)(P ...) const)
{
return ::Remote::Private::CallStubMethod::Call(Obj.get(), mtd, pkg);
}
};
}
template <typename T>
using Proxy = PSImpl::Proxy<T>;
using Stub = PSImpl::Stub;
}
Итоги создания Proxy/Stub
Подведу итоги последовательности шагов создания Proxy/Stub:
- Получить тип метода с помощью decltype.
- Для каждого метода вычислить его идентификатор с помощью CRC32 от имени метода.
- Создать по реализации для каждого метода интерфейса, основываясь на специализациях некоторого шаблона, реализующего метод интерфейса.
- Вести счетчик методов во время компиляции.
- Добавлять полученные реализации прокси-классов и классов-заглушек в реестры прокси и заглушек.
- В конце построения реализаций методов получить последнее значение счетчика методов.
- Пройти по всему реестру (по отдельности для прокси реализаций и реализаций заглушек) с помощью полученных значений счетчика методов и построить иерархию классов на основе типов, расположенных в реестре. Каждая такая иерархия будет являться законченной реализацией прокси-класса или класса-заглушки.
Инфраструктура и транспорт
Под инфраструктурой будет подразумеваться некоторый набор сущностей, который на стороне сервера будет отвечать за создание реальных объектов-реализаций и их заглушек, а так же управлять их временем жизни, а на стороне клиента набор сущностей для создания прокси-объектов, возвращаемых клиенту при запросе того или иного объекта реализации с заданным интерфейсом и расположенным на сервере.
Под транспортом подразумевается нечто, что позволяет обмениваться пакетами данных между клиентом и сервером. В данной реализации это сделано в виде надстройки над libevent (ее http-функционалом). Для этого взята почти без изменений реализация http-сервера из [5] и дополнена клиентской частью отправки http-запросов.
Основными интерфейсами для клиента и сервера, которые входят в инфраструктуру, являются интерфейс фабрики классов
namespace Remote
{
struct IClassFactory
{
virtual ~IClassFactory() {}
virtual std::uint32_t CreateObject(std::uint32_t classId) = 0;
};
}
и интерфейс, предназначенный для удаления созданных объектов на сервере, посредством которого клиент оповещает сервер, что он более не нуждается в объекте и сервер его должен удалить
namespace Remote
{
struct IObjectDeleter
{
virtual ~IObjectDeleter() {}
virtual void DeleteObject(std::uint32_t instId) = 0;
};
}
Реализации этих интерфейсов передаются клиенту с предопределенными идентификаторами их объектов. Клиент, после создания объекта, отвечающего за транспортный уровень, обращается к серверу за получением этих двух интерфейсов по предопределенным идентификаторам и с использованием все тех же Proxy/Stub'ов, рассмотренных выше.
namespace Remote
{
PS_BEGIN_MAP(IClassFactory)
PS_ADD_METHOD(CreateObject)
PS_END_MAP()
PS_BEGIN_MAP(IObjectDeleter)
PS_ADD_METHOD(DeleteObject)
PS_END_MAP()
}
PS_REGISTER_PSTYPES(Remote::IClassFactory)
PS_REGISTER_PSTYPES(Remote::IObjectDeleter)
Получив же экземпляры этих объектов, клиент создает через прокси-объект фабрики классов нужные ему объекты. На стороне сервера создается реальный объект, которому присваивается некоторый уникальный идентификатор экземпляра, а так же создается и связанный с ним объект-заглушка. Идентификатор возвращается клиенту. На стороне клиента создается прокси-объект, которому передается полученный идентификатор. Клиент использует умные указатели (в данном случае — это std::shared_ptr и в качестве deleter'а ему передан так же умный указатель на IObjectDeleter, под которым находится его прокси-объект) и когда прокси-объект уничтожается, на сервер отправляется его идентификатор экземпляра через интерфейс IObjectDeleter. Сервер на своей стороне, на основании этого идентификатора, разрушает реальный объект и заглушку с ним связанную. Так происходит управление временем жизни объектов между клиентом и сервером. В остальном же все вызовы прокси-объекта обрабатываются сервером на основании его идентификатора экземпляра, полученного при его создании.
Сервер
На стороне сервера создается http-сервер на основе [5], в качестве его обработчика запросов, передается объект, разбирающий входящие пакеты и отдающий их менеджеру объектов, в работу которого входит создание и удаление объектов, а так же определение объекта-исполнителя пришедшего запроса.
template <typename TPkg, typename ... TImpls>
class RequestHandler final
: private NonCopyable
{
public:
RequestHandler()
: Mgr(std::make_shared<ObjectManagerType>())
, Creator(STUB_CLASS(IClassFactory)(SysClassId::ClassFactory, Mgr))
, Deleter(STUB_CLASS(IObjectDeleter)(SysClassId::ObjectDeleter, Mgr))
{
}
DataBufPtr Proccess(void const *data, std::size_t bytes)
{
if (!data || !bytes)
throw RequestHandlerException("Request data must not be empty.");
TPkg Pack(data, bytes);
auto InstId = Pack.GetInstId();
switch (InstId)
{
case SysClassId::ClassFactory :
return std::move(Creator.Call(Pack));
case SysClassId::ObjectDeleter :
return std::move(Deleter.Call(Pack));
default :
break;
}
return std::move(Mgr->Call(InstId, Pack));
}
private:
typedef Private::ObjectManager<TPkg, TImpls ...> ObjectManagerType;
std::shared_ptr<ObjectManagerType> Mgr;
STUB_CLASS(IClassFactory) Creator;
STUB_CLASS(IObjectDeleter) Deleter;
};
Где STUB_CLASS — это всего лишь небольшой макрос формирующий имя класса-заглушки из имени интерфейса
#define STUB_CLASS(iface_) iface_ ## _PS::Stub
Менеджер объектов — это реализация IClassFactory и IObjectDeleter, которая является шаблоном, а параметрами его служат тип реализующий сериализацию/десериализацию и список типов, описывающих информацию о том какие объекты с какими интерфейсами и идентификаторами реализаций сервер может создавать и обрабатывать.
При обращении к обработчику запросов, он их передает менеджеру объектов, который имеет карту, состоящую из идентификатора экземпляра и заглушки связанной с реальным объектом. По этой карте вызов отправляется нужному объекту через его объект-заглушку.
Если посмотреть на примеры из начала поста, то на них видно, как создается сервер и как ему передается вся информация о поддерживаемых типах.
Клиент
На стороне клиента создается http-клиент, реализующий интерфейс IRemoting. Через реализацию этого интерфейса клиент отправляет запросы серверу. Умный указатель на этот интерфейс передается фабрике классов при ее создании. Так же фабрика классов при создании принимает тип, реализующий сериализацию/десериализацию и список поддерживаемых интерфейсов, объекты которых предполагается создавать на стороне сервера. В то же время сама фабрика классов не является шаблоном, но и не имеет открытого конструктора, а только шаблонный статический метод ее создания. Это позволяет хорошо инкапсулировать всю информацию, передаваемую при создании фабрики и отказаться от необходимости ее наличия в месте использования фабрики.
class ClassFactory final
: private NonCopyable
{
public:
template <typename TPkg, typename ... TIFaces>
static std::shared_ptr<ClassFactory> Create(IRemotingPtr remoting)
{
// Детали реализации
}
template <typename TIFace>
std::shared_ptr<TIFace> Create(std::uint32_t classId)
{
// Детали реализации
}
private:
std::shared_ptr<Private::IProxyObjectManager> Creator;
ClassFactory()
{
}
};
Нестатический метод фабрики Create принимает как шаблонный параметр интерфейс, на основании которого производится поиск прокси-класса, и обычный не шаблонный параметр идентификатор реализации, под которым на сервере зарегистрирована реализация интерфейса. Фабрика классов на основании этой информации и информации, переданной при ее создании, делает запрос на сервер на создание объекта, а полученный идентификатор экземпляра отдает найденному прокси-объекту. Пользователю возвращается std::shared_ptr, под которым лежит прокси-объект с указанным интерфейсом и в качестве deleter'а прокси-объект с интерфейсом IObjectDeleter, посылающий запрос на сервер при разрушении прокси-объекта на стороне клиента для уничтожения его на стороне сервера.
Связь интерфейсов и Proxy/Stub
Посмотрев на серверную и клиентскую части может возникнуть вопрос: как клиент и сервер связывают информацию об интерфейсах с информацией о Proxy/Stub'ах?
При описании Proxy/Stub, возможно, Вы заметили макрос PS_REGISTER_PSTYPES, о котором пока ничего не говорилось. Этот макрос регистрирует созданные Proxy/Stub'ы в некотором глобальном реестре типов в момент компиляции (похожем на реестры, используемые при реализации методов интерфейсов, с тем отличием, что в качестве идентификатора используется не число, а интерфейс). А когда фабрика классов на стороне клиента или менеджер объектов на стороне сервера ищет нужную ему реализацию прокси-класса или класса-заглушки, то делается обращение к этому реестру по типу интерфейса. Код поиска прокси или заглушки так же создается в момент компиляции.
namespace Remote
{
namespace Private
{
template <typename>
struct ProxyStubRegistry;
}
}
#define PROXY_CLASS(iface_, tpack_) iface_ ## _PS::Proxy<tpack_>
#define STUB_CLASS(iface_) iface_ ## _PS::Stub
#define PS_REGISTER_PSTYPES(iface_)
namespace Remote
{
namespace Private
{
template <>
struct ProxyStubRegistry<iface_>
{
enum { Id = Crc32(#iface_) };
template <typename I>
using Proxy = PROXY_CLASS(iface_, I);
typedef STUB_CLASS(iface_) Stub;
};
}
}
Казалось бы зачем еще один макрос для регистрации созданных прокси и заглушек, и почему бы эту регистрацию не сделать в макросе PS_END_MAP? Можно и там, но тогда придется пожертвовать возможностью раскладывать интерфейсы по пространствам имен, а так же раскладывать по одноименным пространствам имен и их Proxy/Stub. Из кода макросов причину можно понять: нельзя, находясь в некотором пространстве имен, сделать частную специализацию шаблона в другом пространстве имен, никак не связанном с текущим. Тут пришлось сделать выбор между отказом от пространств имен для пользовательского кода или добавить еще один макрос. Выбор был сделан в пользу добавления нового макроса. Позволить отказаться от пространств имен я себе не могу…
Итоги работы инфраструктуры
Подводя итоги инфраструктуры можно выделить следующие шаги по организации клиент-серверного взаимодействия:
- Нужна реализация транспорта.
- Реализация на стороне сервера и клиента сущностей для создания и управления объектами.
- Серверная сторона должна иметь информацию о поддерживаемых ею объектах.
- Клиентская сторона должна иметь информацию о поддерживаемых интерфейсах.
- Нужен реестр типов, в котором хранить всю информацию о привязке Proxy/Stub к определенному интерфейсу, на основании которой клиент создает прокси-объекты, а сервер объекты-заглушки.
Примеры
Один из примеров был приведен в самом начале поста. Еще один в качестве примера будет разработан. Можно придумать реализацию с уже знакомым интерфейсом ISessionManager и добавив новый IDateSoutce. Методы интерфейса ISessionManager будут производить эмуляцию работы с сессиями пользователя: выдавать идентификатор сессии на основе переданного имени пользователя и пароля, проверять полученный идентификатор на валидность и закрывать сессию. Методы интерфейса IDateSource будут получать текущую дату. Один метод для получения даты в виде строки «yyyy.mm.dd hh:mm:ss», а второй возвращает дату через выходные параметры по частям. Так же все методы интерфейсов могут генерировать исключения, которые будут передаваться на сторону клиента и там выбрасываться в виде исключений типа Remote::RemotingException.
// ifaces.h
namespace Test
{
struct ISessionManager
{
virtual ~ISessionManager() {}
virtual std::uint32_t OpenSession(char const *userName, char const *password) = 0;
virtual void CloseSession(std::uint32_t sessionId) = 0;
virtual bool IsValidSession(std::uint32_t sessionId) const = 0;
};
struct IDateSource
{
virtual ~IDateSource() {}
virtual void GetDate(std::uint16_t *year, std::uint16_t *month, std::uint16_t *day,
std::uint16_t *hour, std::uint16_t *min, std::uint16_t *sec) const = 0;
virtual char const* GetDateAsString() const = 0;
};
}
// ifaces_ps.h
namespace Test
{
PS_BEGIN_MAP(ISessionManager)
PS_ADD_METHOD(OpenSession)
PS_ADD_METHOD(CloseSession)
PS_ADD_METHOD(IsValidSession)
PS_END_MAP()
PS_BEGIN_MAP(IDateSource)
PS_ADD_METHOD(GetDate)
PS_ADD_METHOD(GetDateAsString)
PS_END_MAP()
}
PS_REGISTER_PSTYPES(Test::ISessionManager)
PS_REGISTER_PSTYPES(Test::IDateSource)
Где-то нужно еще определить константы, по которым клиент и сервер будут понимать с какой реализацией интерфейса они должны работать. Это можно сделать прямо в коде в месте вызова в виде «магических цифр», а можно вынести такие константы отдельно, стремясь сделать код более независимым от «магических чисел».
// class_ids.h
namespace Test
{
namespace ClassId
{
enum
{
SessionManager = Remote::Crc32("Test.SessionManager"),
DateSource = Remote::Crc32("Test.DateSource")
};
}
}
Сами реализации интерфейсов ISessionManager и IDateSource приводить не буду, рутина; можно посмотреть в прилагаемых исходных кодах примеров.
#include <iostream>
#include "session_manager.h"
#include "date_source.h"
#include "ifaces_ps.h"
#include "class_ids.h"
#include "xml/pack.h"
#include "http/server_creator.h"
int main()
{
try
{
auto Srv = Remote::Http::CreateServer
<
Remote::Pkg::Xml::InputPack,
Remote::ClassInfo<Test::ISessionManager, Test::SessionManager, Test::ClassId::SessionManager>,
Remote::ClassInfo<Test::IDateSource, Test::DateSource, Test::ClassId::DateSource>
>("127.0.0.1", 5555, 2);
std::cin.get();
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
#include <iostream>
#include "ifaces_ps.h"
#include "class_ids.h"
#include "xml/pack.h"
#include "http/http_remoting.h"
#include "class_factory.h"
int main()
{
try
{
auto Remoting = std::make_shared<Remote::Http::Remoting>("127.0.0.1", 5555);
auto Factory = Remote::ClassFactory::Create
<
Remote::Pkg::Xml::OutputPack,
Test::ISessionManager,
Test::IDateSource
>(Remoting);
auto Mgr = Factory->Create<Test::ISessionManager>(Test::ClassId::SessionManager);
auto SessionId = Mgr->OpenSession(UserName, Password);
// TODO:
Mgr->CloseSession(SessionId);
auto Date = Factory->Create<Test::IDateSource>(Test::ClassId::DateSource);
std::uint16_t Year = 0, Month = 0, Day = 0, Hour = 0, Min = 0, Sec = 0;
Date->GetDate(&Year, &Month, &Day, &Hour, &Min, &Sec);
auto const *DateAsString = Date->GetDateAsString();;
// TODO:
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Собираем и пробуем…
Использование
В качестве промежуточного итога приведу небольшое описание шагов по использованию.
Весь код предлагаемой библиотеки поставляется в виде включаемых файлов (за исключением примеров). Для включения в проект не надо собирать всевозможные библиотеки и после с ними производить линковку. Нужно только добавить путь к включаемым файлам библиотеки, в которых предоставлено все для создания Proxy/Stub'ов и свой http-сервер на основе libevent. Для сборки проекта с учетом предлагаемой библиотеки необходимо наличие libevent. Если же пользователь реализует свой транспорт, то такое условие снимается.
При создании клиент-серверного приложения на основе предлагаемой библиотеки нужно выполнить несколько шагов.
Общие действия:
- Определить интерфейсы
- Описать Proxy/Stub'ы
- При желании создать файл с константами реализаций
На стороне сервера:
- Реализовать определенные интерфейсы
- Создать сервер с привязкой к транспорту
- При создании сервера указать всю информацию о (де)сериализаторе и передать список с информацией о поддерживаемых сервером объектах
На стороне клиента:
- Создать транспорт
- Создать фабрику классов, передав ей созданный транспорт и список поддерживаемых интерфейсов
- Создавать (запрашивать с сервера) с помощью фабрики объекты за определенными ранее идентификаторами классов реализаций
Все исключения, выброшенные сервером в их методах, на клиентскую сторону приходят в виде Remote::RemotingException, который порожден от std::exception. Это исключение можно понимать как «что-то произошло в методе объекта на стороне сервера» и его причина описана в его сообщении. Все же остальные исключения говорят о системных ошибках и ошибках соединения.
Так же напомню, что сервер доступен пока существует его объект. Объект сервера не приостанавливает его создающий поток и поэтому после создания серверного объекта, поток, его создавший, возможно нужно приостановить иными средствами. Для своей работы серверный объект создает свои потоки обработки запросов.
При написании классов-реализаций интерфейсов стоит помнить, что вызов метода может происходить из разных потоков если сервер создан не с единственным потоком. Количество потоков указывается при создании сервера параметром threadCount. Поэтому вся синхронизация внутри классов-реализаций лежит на стороне их разработчика.
Клиент же предполагает использование в однопоточном режиме. Что в то же время не мешает создавать несколько клиентов, расположенных в разных потоках. Условием является обращение к методам созданного объекта через его интерфейс в рамках потока, в котором был создан его прокси объект.
Ограничения
Межпроцессное взаимодействие и взаимодействие между разными рабочими станциями в сети всегда более сковано нежели взаимодействие частей системы в рамках одного процесса. Переход через границу процесса если говорить о программировании или переход границы государства всегда связаны с какой-то скованностью нежели находясь внутри той или иной системы. Так если я решу открыть свой бизнес и найду стартовый капитал для покупки двадцати килограмм iPhone'ов одной из последних моделей в США, то конечно в рамках транспорта (авиаперевозчика) у меня не должно возникнуть проблем с ввозом их в Россию и даже не придется оплачивать дополнительный багаж. Все в разрешенных рамках. А вот таможенная служба столь дерзкие порывы сильно притупит безоговорочным желанием с законодательным подкреплением собрать таможенные сборы, так как не убедить ее сотрудников, что 20 кг iPhone я везу для личного использования и стоят они все вместе не более разрешенной беспошлинной суммы :) Так и в рамках описываемой библиотеки есть ограничения.
Если же встроенный транспорт готов передать любые данные с какими-то несущественными ограничениями по сравнению с (де)сериализатором, то (де)сериализатор как раз и является той самой таможней, которая беспошлинно разрешает использовать любое количество параметров в методах интерфейсов, ограниченное только возможностями компилятора и стандартом C++11. А на типы параметров введен таможенный сбор. Можно передавать в качестве параметров и возвращаемых значений только интегральные типы и типы с плавающей точкой. Параметры могут быть переданы как по значению, так и по ссылке и указателю и при необходимости с добавлением квалификатора const. Это «бесплатно», т.е. встроено в предлагаемую реализацию, а если есть желание передавать, например, stl контейнеры или иные типы, то придется внести «пошлину» в виде реализации собственного (де)сериализатора или модификации существующего.
Так же значения, полученные по указателю или ссылке не должны использоваться за пределами метода в/из который они переданы/получены; их значение должно быть скопировано. Мониторинг изменений значений по ссылке или указателю между процессами можно было реализовать, но он был бы неоправданно дорог в вычислительных ресурсах. В то же время это не исключает того, что переданные параметры по ссылке или указателю не могут быть изменены в методе, в который они переданы. Их можно изменять так же как и в рамках процесса, измененные параметры в месте их получения будут отправлены вызывающей стороне. Здесь полная аналогия с работой в рамках одного процесса.
Причины отказа от перегрузки методов интерфейсов были приведены при описании реализации Proxy/Stub.
Исходники, сборка, тестирование
Библиотека для реализации клиент-серверного взаимодействия, построенная поверх HTTP-протокола, скрытого под libevent доступна для скачивания в виде zip-архива с сервера, на котором можно ее и протестировать. Поставляется она в виде включаемых файлов (кроме примеров).
При тестировании я пользовался компилятором gcc/g++ 4.8.1. С большой долей вероятности все будет собираться и на более ранних версиях, например 4.7.2 и может быть на 4.7; не проверял. На еще более ранних версиях вероятность сборки близка к нулю. Так же не проверял, но gcc 4.6.x еще слабо поддерживают или вовсе не поддерживают некоторые вещи, используемые в реализации. Пробовал собирать на clang 3.5. Возникли небольшие проблемы при расчете CRC32 в момент компиляции от длинных строк, а длинной строкой этот компилятор посчитал «Remote.IClassFactory» и манипуляции с -ftemplate-depth и -fconstexpt-depth флагами не дали результата. Конечно, эту проблему можно решить извернувшись с длинами идентификаторов или, например, расчет их по частям строки и xor'ом полученных результатов, но пока от такого решения отказался в силу отсутствия нужды в нем и поддержке clang. При решении же проблемы с длиной идентификатора, больше проблем с clang 3.5 не обнаружено. Пробовать собирать с помощью MS Visual Studio 2013 не стал. Ранее пробовал собрать с помощью «студии 2013» свой проект [1], и был удивлен, что constexpr она не поддерживает. Убрав весь с ним связанный код и подставив заранее вычисленные идентификаторы она в одном из ключевых мест выдала сообщение о внутренней ошибке компилятора, после чего все эксперименты с ней я решил отложить как минимум до выхода новой версии.
Как libevent, так и предлагаемая библиотека являются кроссплатформенными. При тестировании сборка была осуществлена на Ubuntu 12.10 32bit и Ubuntu 12.04 64bit. Под Windows с помощью MinGW пока не пробовал собирать. Возможно все соберется с первого раза или возникнут небольшие тонкости. Надеюсь попробовать в скором будущем.
Вместе с библиотекой поставляются примеры. Их можно испытать на локальной машине или поменяв в клиентах строку с адресом «127.0.0.1» на адрес сервера «t-boss.ru», на котором расположен тестовый экземпляр серверной части примеров, протестировать примеры клиентов так сказать «в реальных условиях».
Заключение
Удалось ли достичь мне заявленного в названии минимализма в использовании полученного продукта судить Вам. У каждого этот идеал может быть разным. Мне же удалось покрыть свое желание минимализировать указываемую информацию при описании Proxy/Stub'ов для интерфейсов до того уровня, когда не надо ничего указывать кроме имени метода, а так же мне удалось отказаться от необходимости наличия информации о транспорте, сериализации и тех же Proxy/Stub'ах в месте использования в клиентском коде объектов с заданным интерфейсом и реализацией логики на сервере.
Большую помощь в этом мне оказал стандарт C++11. По сравнению с C++03 он дает больше возможностей: decltype, auto, шаблоны с переменным количеством параметров, лямбда функции, умные указатели, многопоточность и многое другое. Что позволяет при реализации кроссплатформенного кода отказаться от множества собственных оберток над системными функциями. Отказаться от разработки собственных умных указателей и в большей степени иметь возможность использования RAII [8], так как ранее одного std::auto_ptr было недостаточно.
decltype дал возможность получать типы переменных (в данном случае указателя на метод интерфейса). auto дает возможность абстрагироваться от указания какого-то типа и работать с полученным значением, основываясь на предположениях о том, что он должен содержать те или иные методы и при этом не указывать явно этот тип и не протягивать информацию о нем в место использования.
С появлением шаблонов с переменным числом параметров можно отказаться от ранее используемых и более громоздких списков типов в стиле Александреску. Избавиться от гор «копипастного кода», который часто появлялся при разработке библиотек общего использования с наличием шаблонов.
Библиотека работы с типами дает возможность сократить код, избавившись от написания собственных вещей типа std::remove_pointer, std::is_const и так далее, а некоторые вещи, ныне реализованные в этой библиотеке ранее можно было сделать только компиляторозависимыми.
Достигнув же с помощью этих возможностей некоторого для себя результата в минимализме и комфортном, по моему мнению, взаимодействии клиента с сервером, реализация же может показаться не столь минимальной или простой, а может и не показаться…
Всегда при разработке библиотек общего использования старался придерживаться мысли, что код, который использует библиотеку как можно меньше должен на ней зацикливаться и ей служить. Библиотека должна служить коду ее использующему. А вот реализация самой библиотеки может быть любой сложности, насколько хватит квалификации ее авторов.
Убеждение может показаться спорным. Но приведу любимую мной аналогию. Во многих проектах можно найти использование boost. Да, это неоднозначно воспринимаемый продукт IT-миром разработчиков C++. Кто-то ее чурается, а кто-то обеими руками за ее использование. В целом же можно сказать, что использование boost в большинстве своем воспринимается положительно, так как она избавляет от множества нюансов, скрывая их в своей реализации, временами прибегаю как к зависимостям от компилятора, зависимостям операционной системы и расширениям, вносимым с помощью препроцессора. Можно посмотреть на одну из старейших функций boost::bind и если посмотреть ее реализацию, то она окажется далеко не так проста как ее использование. И это куда не предел сложности реализации в boost ее простых вещей. В целом же на другой стороне малого, сильного и удобного плеча рычага использования boost видно гораздо более длинное плечо его реализации, местами очень хорошо окрашенное boost.preprocessor. И мало кто задается деталями реализации при оценке критериев внесения в проект возможности использования этой библиотеки.
Так же при реализации удаленного взаимодействия еще можно много критериев оценивать. Возможно это станет материалом одного из моих последующих постов. Материал же этого поста появился при разработке межпроцессного взаимодействия плагинов [1], о котором, надеюсь, скоро удастся написать, после его завершения и тестирования. Из него получился более упрощенный и самостоятельный продукт, который с некоторыми модификациями будет положен в основу IPC для системы плагинов [1].
Описанная библиотека прошла полностью стадию 1 согласно [9], многие вещи прошли третью стадию и немного задета вторая (кратко о стадиях: программа должна выполнять заложенную в нее логику, должна быть оптимальной по скорости, должна быть оптимальна в дизайне изнутри).
При желании использовать предложенную библиотеку или отдельно какие-то из ее принципов и возникновении всевозможных вопросов, пишите об этом в комментариях или в личку. Как правило, на все тематические вопросы я стараюсь по возможности максимально развернуто отвечать.
Всем спасибо за внимание!
Материалы
- Система плагинов как упражнение на C++ 11
- libevent
- gSOAP
- Proxy/Stubs своими руками
- Свой http-сервер менее чем в 40 строк кода на libevent и C++11
- RapidXml
- CRTP
- RAII
- Разработка через страдание
Автор: NYM