Около трех лет назад у меня была идея создания небольшого каркаса для разработки небольших сервисов, которые могли бы как-то взаимодействовать между собой, предоставлять API во вне, работать с базами данных и кое-что по мелочи еще. Во время решения некоторых рабочих задач, окончательно сформировалась идея своего проекта, близкого к решению рабочих задач. Примерно год назад все это сформировалось в проект MIF (MetaInfo Framework). Предполагалось, что с его помощью можно будет решать такие задачи, как:
- Разработка легковесных HTTP сервисов
- Коммуникация микросервисов через передаваемые между процессами интерфейсы
- Сериализация и десериализация на базе рефлексии структур данных в разные форматы
- Работа с базами данных
- Некоторые вспомогательные компоненты для создания каркасов сервисов
Все это ориентировано на разработку backend сервисов для веба, но может использоваться и в других системах.
Введение
Выход нового стандарта C++, подготовка следующего. Проходят года, а рефлексии в C++ нет. Обсуждалась возможность появления рефлексии в C++, и казалось, что в очередной стандарт она будет включена. Нет… Может быть рефлексия и не нужна? Может быть… Но есть задачи, где она могла бы быть полезна: межпроцессное взаимодействие (IPC, RPC, REST API, микросервисы), работа с базами данных (ORM), различные (де)сериализаторы и т.д. — в общем есть пространство для применения.
Компилятор имеет всю информацию о типах, и почему бы ему не поделиться ей с разработчиками? Попробую предположить, что всего лишь нет документа, по которому разработчики компиляторов должны отпускать честным разработчикам прочего программного обеспечения эту информацию. Такой документ — это стандарт C++, в котором никак не появится регламент «отоваривания».
Во многих языках рефлексия есть. C++ считается языком для разработки программ, где важна производительность и, казалось бы, одна из его сфер применения — это веб-разработка. Разработка backend сервисов. В этой отрасли есть и REST API, и ORM и всевозможная сериализация во что угодно (часто угодно в json). Для решения этих задач приходится использовать готовые библиотеки или писать свои, в которых данные C++ связываются вручную с другими сущностями, например, отображение структур в C++ в формат json. Иногда с помощью макросов добавляется метаинформация о типе, которая используется в дальнейшем при построении REST API, ORM и т.д. Есть даже решения с плагинами для компилятора, например, odb.
Хотелось бы иметь что-то более естественное, без кодогенерации внешними утилитами, нагромождения макросов и шаблонов или ручного «мапинга». Этого пока нет и для решения приведенных задач нужно выбирать какой-то из приведенных подходов.
Предлагаемое решение основано на добавлении метаинформации к C++ типам с последующим ее использованием при решении разных задач. Можно сказать, что «тип нужно обуть в метаинформацию о нем» после чего он сможет смело шагать через границы процессов и датацентров и иметь возможность представляться по разному (binary, json, xml, text, etc) с минимальным вмешательством разработчика.
Часто подобные задачи решаются с применением кодогенерации, например, thrift, gSOAP, protobuf и т.д. Мне хотелось получить свое решение, которое исключит внешнюю кодогенерацию, а все необходимое будет добавлено к типу в момент компиляции, и при этом есть желание максимально сохранить естественный синтаксис языка C++, не создавая новый язык в существующем языке.
MIF в примерах
Хотелось бы показать некоторые возможности проекта MIF. А по обратной связи на пост, возможно, подготовлю и пост о реализации со всеми ее занудствами, тонкостями и объяснениями почему было выбрано то или иное решение.
Перечисленные в начале поста задачи предполагают, что будет какая-то возможность отображать C++ типы данных для сериализации, объектно-ориентированного межпроцессного взаимодействия, реализации REST API и т.д. С этого предлагаю и начать…
Рефлексия
Рефлексия — основа всего проекта, позволяющая решать мои прикладные задачи.
Пример, как можно получить название C++ структуры данных, количество ее полей и обратиться к одному из полей по его номеру.
struct Data
{
int field1 = 0;
std::string field2;
};
Решение поставленной задачи могло выглядеть так:
int main()
{
Data data;
data.field1 = 100500;
data.field2 = "Text";
using Meta = Mif::Reflection::Reflect<Data>;
std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
std::cout << "Field count: " << Meta::Fields::Count << std::endl;
std::cout << "Field1 value: " << data.field1 << std::endl;
std::cout << "Field2 value: " << data.field2 << std::endl;
std::cout << "Modify fields." << std::endl;
data.*Meta::Fields::Field<0>::Access() = 500100;
data.*Meta::Fields::Field<1>::Access() = "New Text.";
std::cout << "Field1 value: " << data.field1 << std::endl;
std::cout << "Field2 value: " << data.field2 << std::endl;
return 0;
}
Все примерно так бы и было, немного по другому записано, но стандарт не дает возможности так просто отображать C++ типы данных. А чтобы приведенный код работал, нужно добавить к структуре Data метаинформацию
MIF_REFLECT_BEGIN(Data)
MIF_REFLECT_FIELD(field1)
MIF_REFLECT_FIELD(field2)
MIF_REFLECT_END()
MIF_REGISTER_REFLECTED_TYPE(Data)
Метаинформацию о типе можно было бы добавить и в сам тип, расширив его. От такого решения хотелось отказаться чтобы иметь возможность отображать типы, вмешиваться в код которых нет возможности (типы сторонних библиотек). Несколько макросов позволяют добавить всю нужную для дальнейшей работы информацию. Так же есть возможность наследования, но об этом позже …
// STD
#include <iostream>
#include <string>
// MIF
#include <mif/reflection/reflect_type.h>
#include <mif/reflection/reflection.h>
struct Data
{
int field1 = 0;
std::string field2;
};
MIF_REFLECT_BEGIN(Data)
MIF_REFLECT_FIELD(field1)
MIF_REFLECT_FIELD(field2)
MIF_REFLECT_END()
MIF_REGISTER_REFLECTED_TYPE(Data)
int main()
{
Data data;
data.field1 = 100500;
data.field2 = "Text";
using Meta = Mif::Reflection::Reflect<Data>;
std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
std::cout << "Field count: " << Meta::Fields::Count << std::endl;
std::cout << "Field1 value: " << data.field1 << std::endl;
std::cout << "Field2 value: " << data.field2 << std::endl;
std::cout << "Modify fields." << std::endl;
data.*Meta::Fields::Field<0>::Access() = 500100;
data.*Meta::Fields::Field<1>::Access() = "New Text.";
std::cout << "Field1 value: " << data.field1 << std::endl;
std::cout << "Field2 value: " << data.field2 << std::endl;
return 0;
}
Немного усложненный пример: попробовать написать обобщенный код вывода на консоль всех полей структуры и ее базовых структур.
// STD
#include <iostream>
#include <map>
#include <string>
#include <type_traits>
// MIF
#include <mif/reflection/reflect_type.h>
#include <mif/reflection/reflection.h>
#include <mif/serialization/traits.h>
struct Base1
{
int field1 = 0;
bool field2 = false;
};
struct Base2
{
std::string field3;
};
struct Nested
{
int field = 0;
};
struct Data : Base1, Base2
{
int field4 = 0;
std::string field5;
std::map<std::string, Nested> field6;
};
MIF_REFLECT_BEGIN(Base1)
MIF_REFLECT_FIELD(field1)
MIF_REFLECT_FIELD(field2)
MIF_REFLECT_END()
MIF_REFLECT_BEGIN(Base2)
MIF_REFLECT_FIELD(field3)
MIF_REFLECT_END()
MIF_REFLECT_BEGIN(Nested)
MIF_REFLECT_FIELD(field)
MIF_REFLECT_END()
MIF_REFLECT_BEGIN(Data, Base1, Base2)
MIF_REFLECT_FIELD(field4)
MIF_REFLECT_FIELD(field5)
MIF_REFLECT_FIELD(field6)
MIF_REFLECT_END()
MIF_REGISTER_REFLECTED_TYPE(Base1)
MIF_REGISTER_REFLECTED_TYPE(Base2)
MIF_REGISTER_REFLECTED_TYPE(Nested)
MIF_REGISTER_REFLECTED_TYPE(Data)
class Printer final
{
public:
template <typename T>
static typename std::enable_if<Mif::Reflection::IsReflectable<T>(), void>::type
Print(T const &data)
{
using Meta = Mif::Reflection::Reflect<T>;
using Base = typename Meta::Base;
PrintBase<0, std::tuple_size<Base>::value, Base>(data);
std::cout << "Struct name: " << Meta::Name::GetString() << std::endl;
Print<0, Meta::Fields::Count>(data);
}
template <typename T>
static typename std::enable_if
<
!Mif::Reflection::IsReflectable<T>() && !Mif::Serialization::Traits::IsIterable<T>(),
void
>::type
Print(T const &data)
{
std::cout << data << std::boolalpha << std::endl;
}
template <typename T>
static typename std::enable_if
<
!Mif::Reflection::IsReflectable<T>() && Mif::Serialization::Traits::IsIterable<T>(),
void
>::type
Print(T const &data)
{
for (auto const &i : data)
Print(i);
}
private:
template <std::size_t I, std::size_t N, typename T>
static typename std::enable_if<I != N, void>::type
Print(T const &data)
{
using Meta = Mif::Reflection::Reflect<T>;
using Field = typename Meta::Fields::template Field<I>;
std::cout << Field::Name::GetString() << " = ";
Print(data.*Field::Access());
Print<I + 1, N>(data);
}
template <std::size_t I, std::size_t N, typename T>
static typename std::enable_if<I == N, void>::type
Print(T const &)
{
}
template <typename K, typename V>
static void Print(std::pair<K, V> const &p)
{
Print(p.first);
Print(p.second);
}
template <std::size_t I, std::size_t N, typename B, typename T>
static typename std::enable_if<I != N, void>::type
PrintBase(T const &data)
{
using Type = typename std::tuple_element<I, B>::type;
Print(static_cast<Type const &>(data));
PrintBase<I + 1, N, B>(data);
}
template <std::size_t I, std::size_t N, typename B, typename T>
static typename std::enable_if<I == N, void>::type
PrintBase(T const &)
{
}
};
int main()
{
Data data;
data.field1 = 1;
data.field2 = true;
data.field3 = "Text";
data.field4 = 100;
data.field5 = "String";
data.field6["key1"].field = 100;
data.field6["key2"].field = 200;
Printer::Print(data);
return 0;
}
Результат
Struct name: Base1
field1 = 1
field2 = true
Struct name: Base2
field3 = Text
Struct name: Data
field4 = 100
field5 = String
field6 = key1
Struct name: Nested
field = 100
key2
Struct name: Nested
field = 200
Пример является прототипом для полноценного сериализатора, т.к. на основе добавленной метаинформации сериализует структуру в поток (в примере в стандартный поток вывода). Для определения является ли тип контейнером используется функция из пространства имен Serialization. В этом пространстве имен собраны готовые сериализаторы в json и boost.archives (xml, text, binary). Построены они по принципу близкому к приведенному в примере. Если нет необходимости расширить фреймворк своим сериализатором, то писать подобный код нет необходимости.
Вместо использования класса Printer можно воспользоваться готовым сериализатором, например, в json, и количество кода сильно сократится.
#include <mif/reflection/reflect_type.h>
#include <mif/serialization/json.h>
// Data and meta
int main()
{
Data data;
// Fill data
auto const buffer = Mif::Serialization::Json::Serialize(data); // Сериализация в json
std::cout << buffer.data() << std::endl;
return 0;
}
{
"Base1" :
{
"field1" : 1,
"field2" : true
},
"Base2" :
{
"field3" : "Text"
},
"field4" : 100,
"field5" : "String",
"field6" :
[
{
"id" : "key1",
"val" :
{
"field" : 100
}
},
{
"id" : "key2",
"val" :
{
"field" : 200
}
}
]
}
Для разнообразия можно попробовать воспользоваться сериализацией boost.archives в формате xml.
// BOOST
#include <boost/archive/xml_oarchive.hpp>
// MIF
#include <mif/reflection/reflect_type.h>
#include <mif/serialization/boost.h>
// Data and meta
int main()
{
Data data;
// Fill data
boost::archive::xml_oarchive archive{std::cout};
archive << boost::serialization::make_nvp("data", data);
return 0;
}
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="14">
<data class_id="0" tracking_level="0" version="0">
<Base1 class_id="1" tracking_level="0" version="0">
<field1>1</field1>
<field2>1</field2>
</Base1>
<Base2 class_id="2" tracking_level="0" version="0">
<field3>Text</field3>
</Base2>
<field4>100</field4>
<field5>String</field5>
<field6 class_id="3" tracking_level="0" version="0">
<count>2</count>
<item_version>0</item_version>
<item class_id="4" tracking_level="0" version="0">
<first>key1</first>
<second class_id="5" tracking_level="0" version="0">
<field>100</field>
</second>
</item>
<item>
<first>key2</first>
<second>
<field>200</field>
</second>
</item>
</field6>
</data>
</boost_serialization>
Из примера видно, что кроме вызова конкретного сериализатора ничего не меняется. Используется одна и та же метаинфомация. Она же будет использоваться и в других местах при реализации межпроцессного взаимодействия.
Сериализации и десериализаторы нескольких форматов реализованы в каркасе. А при необходимости добавить поддержку нового формата можно сделать на примере класса Printer или взяв за основу (де)сериализатор формата json, используя предложенное API для обхода C++ структур данных. Есть некоторые ограничения на типы (куда же без них), но об этом позже.
Далее предлагаю перейти к более интересным вещам — реализации межпроцессного взаимодействия на базе передаваемых между процессами интерфейсов (C++ структур данных с чисто виртуальными методами).
Межпроцессное взаимодействие
С чего начинался проект MIF — это реализация межпроцессного взаимодействия. В ней была первоочередная потребность. На одной из первых реализаций этого механизма был разработан один из сервисов, который на момент написания этих строк поста отработал стабильно более полугода без падений и перезагрузок.
Имелась потребность сделать коммуникацию сервисов, расположенных на нескольких машинах, с помощью интерфейсов. Хотелось работать с сервисами как будто они все находятся в одном процессе.
Пример показывает насколько близко удалось приблизиться к желаемому результату.
Задача: разработать сервер и клиент для обмена информацией о сотрудниках вымышленной компании.
Решение подобных задач сводится к нескольким однотипным шагам
- Определить интерфейс(ы) взаимодействия компонент
- Определить пользовательские структуры данных (если в этом есть необходимость) для параметров методов или как возвращаемые значения
- Добавить метаинформацию
- Реализовать серверное приложение
- Реализовать клиентское приложение
Общая часть
Структуры данных
// data.h
namespace Service
{
namespace Data
{
using ID = std::string;
struct Human
{
std::string name;
std::string lastName;
std::uint32_t age = 0;
};
enum class Position
{
Unknown,
Developer,
Manager
};
struct Employee
: public Human
{
Position position = Position::Unknown;
};
using Employees = std::map<ID, Employee>;
} // namespace Data
} // namespace Service
Метаинформация
// meta/data.h
namespace Service
{
namespace Data
{
namespace Meta
{
using namespace ::Service::Data;
MIF_REFLECT_BEGIN(Human)
MIF_REFLECT_FIELD(name)
MIF_REFLECT_FIELD(lastName)
MIF_REFLECT_FIELD(age)
MIF_REFLECT_END()
MIF_REFLECT_BEGIN(Position)
MIF_REFLECT_FIELD(Unknown)
MIF_REFLECT_FIELD(Developer)
MIF_REFLECT_FIELD(Manager)
MIF_REFLECT_END()
MIF_REFLECT_BEGIN(Employee, Human)
MIF_REFLECT_FIELD(position)
MIF_REFLECT_END()
} // namespace Meta
} // namespace Data
} // namespace Service
MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Human)
MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Position)
MIF_REGISTER_REFLECTED_TYPE(::Service::Data::Meta::Employee)
Интерфейс
// imy_company.h
namespace Service
{
struct IMyCompany
: public Mif::Service::Inherit<Mif::Service::IService>
{
virtual Data::ID AddEmployee(Data::Employee const &employee) = 0;
virtual void RemoveAccount(Data::ID const &id) = 0;
virtual Data::Employees GetEmployees() const = 0;
};
} // namespace Service
Метаинформация
// ps/imy_company.h
namespace Service
{
namespace Meta
{
using namespace ::Service;
MIF_REMOTE_PS_BEGIN(IMyCompany)
MIF_REMOTE_METHOD(AddEmployee)
MIF_REMOTE_METHOD(RemoveAccount)
MIF_REMOTE_METHOD(GetEmployees)
MIF_REMOTE_PS_END()
} // namespace Meta
} // namespace Service
MIF_REMOTE_REGISTER_PS(Service::Meta::IMyCompany)
Определение структуры данных и добавление метаинформации к ней такое же, как и в примерах с рефлексией, за исключением того, что все разнесено по пространствам имен.
Определение интерфейса — это определение C++ структуры данных, содержащей только чисто виртуальные методы.
Для интерфейсов было другое пожелание — возможность запрашивать из одного интерфейса другие, содержащиеся в реализации, и, возможно, не связанные в единую иерархию. Поэтому определяемый интерфейс всегда должен наследовать Mif::Service::IService или любой другой, наследуемый от Mif::Service::IService. Есть множественное наследование. Наследование делается через промежуточную сущность Mif::Service::Inherit. Это шаблон с переменным числом параметров. Его параметрами служат наследуемые интерфейсы или реализации (inheritance). Это необходимо для реализации механизма запроса интерфейсов такого же, как dynamic_cast, но работающего и за границами процесса.
Добавление метаинформации к интерфейсу особо не отличается от добавления метаинформации для структур данных. Это другой, но аналогичный, набор макросов. Возможно позднее все же все будет сведено к единому набору макросов для определения структур данных и интерфейсов. Пока они разные. Так сложилось в ходе развития проекта.
Указывать при добавлении метаинформации к интерфейсу его базовые интерфейсы не надо. Вся иерархия будет найдена в момент компиляции. Здесь небольшая вспомогательная сущность Mif::Service::Inherit играет свою основную роль в поиске наследников и связанной с ними метаинформации.
При добавлении метаинформации к интерфейсам указывается только интерфейс и его методы без указания параметров, возвращаемых значений и cv-квалификаторов. Было желание сделать добавление метаинформации к интерфейсам в духе минимализма. Отсутствие перегрузки стало платой за минимализм. Считаю это малой ценой за возможность не перечислять все параметры и возвращаемые типы значений для каждого метода и не править их при небольших правках в интерфейсе.
Определив общие сущности, осталось реализовать серверное и клиентское приложения.
У каждого интерфейса может быть множество реализаций. Их как-то нужно отличать. При создании объекта нужно явно указывать желаемую реализацию. Для этого нужны идентификаторы реализаций интерфейсов и их связь с реализациями.
Для удобства и отказа от «магических значений в коде» идентификаторы реализаций лучше вынести в один или несколько заголовочных файлов. В проекте MIF в качестве идентификатора используются числа. Чтобы им как-то придать уникальность при этом не городить какие-то счетчики или все не помещать в один единый enum и иметь возможность логически разнести идентификаторы по разным файлам и пространствам имен, предлагается в качестве идентификаторов использовать crc32 от строки, с придумыванием уникальности которой проблем у разработчика должно быть меньше.
Для реализации интерфейса IMyCompany нужен идентификатор
// id/service.h
namespace Service
{
namespace Id
{
enum
{
MyCompany = Mif::Common::Crc32("MyCompany")
};
} // namespace Id
} // namespace Service
Серверное приложение
Реализация IMyCompany
// service.cpp
// MIF
#include <mif/common/log.h>
#include <mif/reflection/reflection.h>
#include <mif/service/creator.h>
// COMMON
#include "common/id/service.h"
#include "common/interface/imy_company.h"
#include "common/meta/data.h"
namespace Service
{
namespace Detail
{
namespace
{
class MyCompany
: public Mif::Service::Inherit<IMyCompany>
{
public:
// …
private:
// …
// IMyCompany
virtual Data::ID AddEmployee(Data::Employee const &employee) override final
{
// ...
}
virtual void RemoveAccount(Data::ID const &id) override final
{
// ... }
}
virtual Data::Employees GetEmployees() const override final
{
// ...
}
};
} // namespace
} // namespace Detail
} // namespace Service
MIF_SERVICE_CREATOR
(
::Service::Id::MyCompany,
::Service::Detail::MyCompany
)
Есть несколько моментов, на которые хотелось бы обратить внимание:
- Наследование в реализации так же через Mif::Service::Inherit. Это не обязательно, но можно считать хорошим тоном и будет полезно при реализации нескольких интерфейсов с наследованием части ранее уже реализованных интерфейсов.
- Вся реализация может и, предпочтительно, должна быть сделана в одном cpp-файле, без разделения на h и cpp файлы. Что позволяет усилить инкапсуляцию и в больших проектах уменьшить время компиляции за счет того, что все необходимые включаемые файлы реализации находятся в файле с реализацией. При их модификации перекомпиляции подвергается меньшее число зависимых cpp-файлов.
- Каждая реализация имеет точку входа — MIF_SERVICE_CREATOR, что является фабрикой реализации. Параметрами являются класс-реализация, идентификатор и при необходимости переменное количество параметров, передаваемых в конструктор реализации.
Для завершения серверного приложения осталось добавить точку входа — функцию main.
// MIF
#include <mif/application/tcp_service.h>
// COMMON
#include "common/id/service.h"
#include "common/ps/imy_company.h"
class Application
: public Mif::Application::TcpService
{
public:
using TcpService::TcpService;
private:
// Mif.Application.Application
virtual void Init(Mif::Service::FactoryPtr factory) override final
{
factory->AddClass<::Service::Id::MyCompany>();
}
};
int main(int argc, char const **argv)
{
return Mif::Application::Run<Application>(argc, argv);
}
При создании точки входа нужно реализовать свой класс приложения — наследник от базового класса приложений или от одного из предопределенных шаблонов приложений. В переопределенном методе Init нужно добавить к фабрике все существующие реализации интерфейсов, которые будет экспортировать сервис (factory->AddClass). В метод AddClass можно передавать параметры конструктора реализации.
Сервис использует предопределенный транспорт tcp, сериализацию на базе boost.archive в бинарном формате со сжатием gzip данных для обмена информацией об интерфейсах, методах, параметрах, возвращаемых результатах, исключениях и экземплярах объектов.
Можно использовать другой вид транспорта (например, http, который так же доступен в MIF или реализовать свой), сериализации и собрать свою уникальную цепочку обработки данных (определение границ пакета, сжатие, шифрование, многопоточную обработку и т.д.). Для этого нужно воспользоваться уже не шаблоном приложения, а базовым классом приложений (Mif::Application::Application), определить самостоятельно нужные части цепочки обработки данных или транспорт.
В первой версии проекта MIF не было предопределенных шаблонов приложений. Примеры выглядели не такими короткими, но показывали весь путь, который нужно проделать для полного контроля над потоком обработки данных. Вся цепочка показана в примерах первой версии проекта (MIF 1.0).
Клиентское приложение
На стороне клиента используется все, что было определено в общей части.
Клиент — это такой же каркас приложения (в примере используется предопределенный шаблон приложения), в котором запрашивается удаленная фабрика классов / сервисов, через которую создается нужный объект и вызываются его методы.
// MIF
#include <mif/application/tcp_service_client.h>
#include <mif/common/log.h>
// COMMON
#include "common/id/service.h"
#include "common/ps/imy_company.h"
class Application
: public Mif::Application::TcpServiceClient
{
public:
using TcpServiceClient::TcpServiceClient;
private:
void ShowEmployees(Service::Data::Employees const &employees) const
{
// ...
}
// Mif.Application.TcpServiceClient
virtual void Init(Mif::Service::IFactoryPtr factory) override final
{
auto service = factory->Create<Service::IMyCompany>(Service::Id::MyCompany);
{
Service::Data::Employee e;
e.name = "Ivan";
e.lastName = "Ivanov";
e.age = 25;
e.position = Service::Data::Position::Manager;
auto const eId = service->AddEmployee(e);
MIF_LOG(Info) << "Employee Id: " << eId;
}
{
Service::Data::Employee e;
e.name = "Petr";
e.lastName = "Petrov";
e.age = 30;
e.position = Service::Data::Position::Developer;
auto const eId = service->AddEmployee(e);
MIF_LOG(Info) << "Employee Id: " << eId;
}
auto const &employees = service->GetEmployees();
ShowEmployees(employees);
if (!employees.empty())
{
auto id = std::begin(employees)->first;
service->RemoveAccount(id);
MIF_LOG(Info) << "Removed account " << id;
auto const &employees = service->GetEmployees();
ShowEmployees(employees);
try
{
MIF_LOG(Info) << "Removed again account " << id;
service->RemoveAccount(id);
}
catch (std::exception const &e)
{
MIF_LOG(Warning) << "Error: " << e.what();
}
}
}
};
int main(int argc, char const **argv)
{
return Mif::Application::Run<Application>(argc, argv);
}
Результат
2017-08-09T14:01:23.404663 [INFO]: Starting network application on 0.0.0.0:55555
2017-08-09T14:01:23.404713 [INFO]: Starting server on 0.0.0.0:55555
2017-08-09T14:01:23.405442 [INFO]: Server is successfully started.
2017-08-09T14:01:23.405463 [INFO]: Network application is successfully started.
Press 'Enter' for quit.
2017-08-09T14:01:29.032171 [INFO]: MyCompany
2017-08-09T14:01:29.041704 [INFO]: AddEmployee. Name: Ivan LastName: Ivanov Age: 25 Position: Manager
2017-08-09T14:01:29.042948 [INFO]: AddEmployee. Name: Petr LastName: Petrov Age: 30 Position: Developer
2017-08-09T14:01:29.043616 [INFO]: GetEmployees.
2017-08-09T14:01:29.043640 [INFO]: Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
2017-08-09T14:01:29.043656 [INFO]: Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
2017-08-09T14:01:29.044481 [INFO]: Removed employee account for Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
2017-08-09T14:01:29.045121 [INFO]: GetEmployees.
2017-08-09T14:01:29.045147 [INFO]: Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
2017-08-09T14:01:29.045845 [WARNING]: RemoveAccount. Employee with id 0 not found.
2017-08-09T14:01:29.046652 [INFO]: ~MyCompany
2017-08-09T14:02:05.766072 [INFO]: Stopping network application ...
2017-08-09T14:02:05.766169 [INFO]: Stopping server ...
2017-08-09T14:02:05.767180 [INFO]: Server is successfully stopped.
2017-08-09T14:02:05.767238 [INFO]: Network application is successfully stopped.
2017-08-09T14:01:29.028821 [INFO]: Starting network application on 0.0.0.0:55555
2017-08-09T14:01:29.028885 [INFO]: Starting client on 0.0.0.0:55555
2017-08-09T14:01:29.042510 [INFO]: Employee Id: 0
2017-08-09T14:01:29.043296 [INFO]: Employee Id: 1
2017-08-09T14:01:29.044082 [INFO]: Employee. Id: 0 Name: Ivan LastName: Ivanov Age: 25 Position: Manager
2017-08-09T14:01:29.044111 [INFO]: Employee. Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
2017-08-09T14:01:29.044818 [INFO]: Removed account 0
2017-08-09T14:01:29.045517 [INFO]: Employee. Id: 1 Name: Petr LastName: Petrov Age: 30 Position: Developer
2017-08-09T14:01:29.045544 [INFO]: Removed again account 0
2017-08-09T14:01:29.046357 [WARNING]: Error: [Mif::Remote::Proxy::RemoteCall] Failed to call remote method "IMyCompany::RemoveAccount" for instance with id "411bdde0-f186-402e-a170-4f899311a33d". Error: RemoveAccount. Employee with id 0 not found.
2017-08-09T14:01:29.046949 [INFO]: Client is successfully started.
2017-08-09T14:01:29.047311 [INFO]: Network application is successfully started.
Press 'Enter' for quit.
2017-08-09T14:02:02.901773 [INFO]: Stopping network application ...
2017-08-09T14:02:02.901864 [INFO]: Stopping client ...
2017-08-09T14:02:02.901913 [INFO]: Client is successfully stopped.
2017-08-09T14:02:02.901959 [INFO]: Network application is successfully stopped.
Да, исключения так же преодолевают границы процессов…
[WARNING]: Error: [Mif::Remote::Proxy::RemoteCall] Failed to call remote method "IMyCompany::RemoveAccount" for instance with id "411bdde0-f186-402e-a170-4f899311a33d". Error: RemoveAccount. Employee with id 0 not found.
Из сообщения видно, что был повторно вызван метод удаления информации о сотруднике с идентификатором 0. На стороне сервера уже такой записи нет, о чем сервер сообщил исключением с текстом «Employee with id 0 not found»/
Пример продемонстрировал межпроцессное взаимодействие клиента с сервером, максимально скрывая все детали связанные с транспортом и форматом передаваемых данных.
Этот пример завершает показ базы, лежащей в основе проекта MIF. К дополнительным возможностям можно отнести
- Возможность запрашивать интерфейсы у реализации не объединенные в единую иерархию (не считая наследования от Mif::Service::IService).
- Передавать указатели и умные указатели на интерфейсы между сервисами. Что может пригодиться для разных реализаций сервисов с обратными вызовами. Например, сервис на базе publish/subscribe. В качестве примера приведена реализация паттерна visitor, части которого находятся в разных процессах и взаимодействуют по tcp.
HTTP
В примере межпроцессного взаимодействия можно легко заменить существующий TCP -транспорт, на HTTP -транспорт. Появится возможность обращаться к сервисам с помощью привычных средств, например, curl или из браузера.
Поддержка HTTP дает возможность строить сервисы, которые одновременно могут являться и web-сервером с каким-нибудь json REST API, и в то же время поддерживать ранее продемонстрированное взаимодействие через C++ интерфейсы.
Комфорт часто связан с ограничениями. Таким ограничением при использовании HTTP-транспорта является отсутствие возможности обратного вызова методов, передаваемых интерфейсов. Решение на базе HTTP-транспорта не ориентировано на построение приложений publish / subscribe. Это связано с тем, что работа по HTTP предполагает подход «запрос-ответ». Клиент посылает запросы серверу и ждет ответа.
Предложенный в MIF TCP-транспорт такого ограничения не имеет. Вызвать методы может как клиент, так и сервер. В этом случае размываются разграничения клиента и сервера. Появляется канал, по которому объекты могут общаться друг с другом, вызывая методы с любой его стороны. Это дает возможность двустороннего взаимодействия для построения архитектуры publish / subscribe, что было продемонстрировано в примере межпроцессным посетителем.
В MIF HTTP уделено особое внимание, т.к. изначально направленность была на разработку backend сервисов для веба. Нужно было создавать небольшие HTTP веб-сервисы, получать данные от поставщиков по HTTP и только после этого была добавлена возможность использования HTTP в качестве транспорта при маршалинге интерфейсов между процессами. Поэтому хотелось бы в начале продемонстрировать примеры создания простых web-сервисов, клиентов, а в конце привести пример web-сервера с поддержкой передачи интерфейсов между процессами.
Простой HTTP web-сервер
// MIF
#include <mif/application/http_server.h>
#include <mif/common/log.h>
#include <mif/net/http/constants.h>
class Application
: public Mif::Application::HttpServer
{
public:
using HttpServer::HttpServer;
private:
// Mif.Application.HttpServer
virtual void Init(Mif::Net::Http::ServerHandlers &handlers) override final
{
handlers["/"] = [] (Mif::Net::Http::IInputPack const &request,
Mif::Net::Http::IOutputPack &response)
{
auto data = request.GetData();
MIF_LOG(Info) << "Process request "" << request.GetPath()
<< request.GetQuery() << ""t Data: "
<< (data.empty() ? std::string{"null"} :
std::string{std::begin(data), std::end(data)});
response.SetCode(Mif::Net::Http::Code::Ok);
response.SetHeader(
Mif::Net::Http::Constants::Header::Connection::GetString(),
Mif::Net::Http::Constants::Value::Connection::Close::GetString());
response.SetData(std::move(data));
};
}
};
int main(int argc, char const **argv)
{
return Mif::Application::Run<Application>(argc, argv);
}
Для проверки работы можно воспользоваться командой
curl -iv -X POST "http://localhost:55555/" -d 'Test data'
Всего около трех десятков строк кода и многопоточный HTTP эхо-сервер на базе каркаса приложений MIF готов. В качестве backend используется libevent. Простое тестирование с помощью утилиты ab выдает средний результат до 160K запросов в секунду. Конечно все зависит от «железа», на котором проводится тестирование, операционной системы, сети и т.д. Но для сравнения что-то подобное было сделано на Python и Go. Сервер на Python отработал в 2 раза медленнее, а на Go результат был лучше в среднем на 10% от результата сервера из примера. Так что, если Вы C++ разработчик и Python и Go Вам чужды или есть иные, возможно официально закрепленные внутренними распоряжениями в рамках проекта, причины любить только C++, то можно воспользоваться предложенным решением и получить неплохие результаты по скорости работы и срокам разработки …
HTTP клиент
Клиентская часть представлена классом Mif::Net::Http::Connection. Это основа HTTP-транспорта маршалинга C++ интерфейсы в MIF. Но пока не про маршалинг… Класс может использоваться отдельно от всей инфраструктуры межпроцессного взаимодействия MIF-микросервисов, например, для скачивания данных поставщиков, например, медиаконтента, прогноза погоды, биржевых котировок и т.д.
Запустив приведенный выше эхо-сервис, можно к нему обратиться таким клиентом:
// STD
#include <cstdlib>
#include <future>
#include <iostream>
#include <string>
// MIF
#include <mif/net/http/connection.h>
#include <mif/net/http/constants.h>
int main()
{
try
{
std::string const host = "localhost";
std::string const port = "55555";
std::string const resource = "/";
std::promise<std::string> promise;
auto future = promise.get_future();
Mif::Net::Http::Connection connection{host, port,
[&promise] (Mif::Net::Http::IInputPack const &pack)
{
if (pack.GetCode() == Mif::Net::Http::Code::Ok)
{
auto const data = pack.GetData();
promise.set_value({std::begin(data), std::end(data)});
}
else
{
promise.set_exception(std::make_exception_ptr(
std::runtime_error{
"Failed to get response from server. Error: "
+ pack.GetReason()
}));
}
}
};
auto request = connection.CreateRequest();
request->SetHeader(Mif::Net::Http::Constants::Header::Connection::GetString(),
Mif::Net::Http::Constants::Value::Connection::Close::GetString());
std::string data = "Test data!";
request->SetData({std::begin(data), std::end(data)});
connection.MakeRequest(Mif::Net::Http::Method::Type::Post,
resource, std::move(request));
std::cout << "Response from server: " << future.get() << std::endl;
}
catch (std::exception const &e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Это базовый пример без учета особого внимания на обработку ошибок. Он демонстрирует возможность разработки простого клиента. Конечно лучше использовать что-то иное, например, на базе библиотеки curl, но иногда в простых случаях вполне может быть достаточно приведенного выше кода.
HTTP web-сервер с двойным интерфейсом
Пример подводит итог по поддержке HTTP в проекте MIF. В примере показан HTTP web-сервер, к которому можно обращаться из браузера или с помощью curl, а так же клиентом, работающим через C++ интерфейсы и не желающего знать ничего о транспорте и формате данных. Используется тот же каркас приложения Mif::Application::HttpServer, что и ранее. Ниже приведены необходимые для демонстрации фрагменты кода. Целиком пример доступен на github в примере http.
Общая часть
Интерфейс
namespace Service
{
struct IAdmin
: public Mif::Service::Inherit<Mif::Service::IService>
{
virtual void SetTitle(std::string const &title) = 0;
virtual void SetBody(std::string const &body) = 0;
virtual std::string GetPage() const = 0;
};
} // namespace Service
Добавление метаинформации к интерфейсу, идентификатор сервиса-реализации — все, аналогично приведенным ранее примерам межпроцессного взаимодействия.
Серверная часть
Каркас приложения
class Application
: public Mif::Application::HttpServer
{
//...
private:
// Mif.Application.HttpService
virtual void Init(Mif::Net::Http::ServerHandlers &handlers) override final
{
std::string const adminLocation = "/admin";
std::string const viewLocation = "/view";
auto service = Mif::Service::Create<Service::Id::Service>(viewLocation);
auto webService = Mif::Service::Cast<Mif::Net::Http::IWebService>(service);
auto factory = Mif::Service::Make<Mif::Service::Factory, Mif::Service::Factory>();
factory->AddInstance(Service::Id::Service, service);
std::chrono::microseconds const timeout{10000000};
auto clientFactory = Service::Ipc::MakeClientFactory(timeout, factory);
handlers.emplace(adminLocation, Mif::Net::Http::MakeServlet(clientFactory));
handlers.emplace(viewLocation, Mif::Net::Http::MakeWebService(webService));
}
};
Обработчик ресурса задается уже не напрямую, а через дополнительную обертку, которая позволяет:
- Добавлять к ресурсу разные обработчики
- Автоматически делать разбор параметров запроса и данных из тела запроса
- Автоматически сериализовать ответ в выбранный формат
- Объединять два подхода: работу по HTTP через, возможно, REST API и работу с клиентом с поддержкой маршалинга C++ интерфейсов
Все эти возможности реализованы в базовом классе Mif::Net::Http::WebService. Пользовательский класс должен наследовать его. Т.к. это уже сервис, который содержит чисто виртуальные методы, реализация которых скрыта в сервисном коде каркаса, то экземпляры классов-наследников должны создаваться так же как и все сервисы — фабричным методом. Чтобы сервис можно было использовать как обработчик HTTP web-сервера нужно создать обертку функцией Mif::Net::Http::MakeServlet.
Сервисы MIF и веб-сервисы — это разные сервисы. Под MIF сервисами стоит понимать только классы-реализации интерфейсов, которые не обязаны заниматься только обработкой HTTP-запросов или иных сетевых запросов. Веб-сервисы уже более привычное понятие. Но при необходимости в MIF это все легко объединяется в единое целое и приведенный пример этому подтверждение.
Сервис-обработчик
namespace Service
{
namespace Detail
{
namespace
{
class WebService
: public Mif::Service::Inherit
<
IAdmin,
Mif::Net::Http::WebService
>
{
public:
WebService(std::string const &pathPrefix)
{
AddHandler(pathPrefix + "/stat", this, &WebService::Stat);
AddHandler(pathPrefix + "/main-page", this, &WebService::MainPage);
}
private:
// …
// IAdmin
virtual void SetTitle(std::string const &title) override final
{
// ...
}
// …
// Web hadlers
Result<Mif::Net::Http::JsonSerializer> Stat()
{
// ...
std::map<std::string, std::int64_t> resp;
// Fill resp
return resp;
}
Result<Mif::Net::Http::PlainTextSerializer>
MainPage(Prm<std::string, Name("format")> const &format)
{
// ...
}
};
} // namespace
} // namespace Detail
} // namespace Service
MIF_SERVICE_CREATOR
(
::Service::Id::Service,
::Service::Detail::WebService,
std::string
)
Хотелось бы обратить внимание на несколько моментов:
- Выше уже упоминалась необходимость использования Mif::Service::Inherit при написании реализаций. Здесь как раз роль Mif::Service::Inherit полностью оправдана. С одной стороны класс наследует интерфейс IAdmin и реализует все его методы, а с другой наследует реализацию интерфейса IWebService в виде базового класса Mif::Net::Http::WebService, которая делает работу с HTTP проще.
- В конструкторе добавляются обработчики ресурсов. Полный путь строится из пути ресурса, с которым связан класс-наследник Mif::Net::Http::WebService и пути, который указан при вызове AddHandler. В примере путь для получения статистики по выполненным запросам будет выглядеть, как /view/stat
- Возвращаемый тип обработчика может быть любым, который можно вывести в поток. Кроме того в качестве возвращаемого типа можно использовать обертку-сериализатор. Она сериализует переданный встроенный или пользовательский тип в свой формат. В этом случае для сериализации пользовательских типов используется добавленная к ним метаинформация.
- Методы-обработчики могут принимать параметры. Для описания параметров используется сущность Prm. Класс Prm принимает тип параметра, его имя. При необходимости пользовательская реализация разбора параметров может быть передана в Prm. Поддерживается конвертация параметров запроса к интегральным типам, типам с плавающей точкой, множества параметров с разделителем в виде точки с запятой (некоторые stl контейнеры, например, list, vector, set). Так же дата, время и timestamp целиком, которые можно привести к типам boost::posix_time. Десериализация данных тела запроса производится классом Content. Класс использует метаинформацию и преданный десериализатор. При необходимости получить доступ к заголовкам запроса или ко всей карте параметров, в качестве параметра обработчика можно указать Headers и / или Params. Одновременно можно использовать все указанные классы. Подробная работа с параметрами показана в примере http_crud
- При определении фабричного метода для создания класса реализации в макрос MIF_SERVICE_CREATOR передается дополнительный параметр, который должен быть передан в конструктор реализации. Использование макроса MIF_SERVICE_CREATOR упоминалось выше.
Клиент
Клиент не имеет особых отличий от ранее приведенных примеров. Исключением является отсутствие предопределенной цепочки обработки запросов. Она формируется разработчиком самостоятельно. Для работы по HTTP в MIF нет предопределенных цепочек обработки запросов. Особенности реализации… О причинах, возможно, что-то в последующих постах будет написано. Полный код клиента — это пример http.
Результаты
Для тестирования собранный сервер нужно запустить и попробовать обратиться к нему, например, с использованием команды curl или из браузера
curl "http://localhost:55555/view/main-page?format=text"
curl "http://localhost:55555/view/main-page?format=html"
curl "http://localhost:55555/view/main-page?format=json"
После чего запустить клиентское приложение, которое обращается через интерфейс администратора и меняет данные и повторно попробовать выполнить приведенные curl команды. Можно заметить, что внесенные клиентом изменения были успешно применены и выдаваемый по HTTP результат изменился.
Работа с базами данных
Backedn без БД? Как же без них? Не обязательно, но и не редко…
На текущий момент работа с базами данных в MIF реализована без опоры на метаинформацию. Пока что почти классика работы с БД. Эта часть была нужна для разработки сервисов, которые что-то должны были хранить в БД. Но разработка этой части велась с прицелом на то, что она станет основой для ORM, где уже в полной мере будет использована вся метаинформация, добавляемая к типам. С ORM как-то пока не сложилось. Было несколько попыток реализовать. Получалось или сильно громоздко, или не гибко. Думаю, что компромисс между лаконичностью и гибкостью скоро будет достигнут и в ближайших версиях ORM все же появится, т.к. уж очень хочется иметь возможность какие-то проверки переложить на компилятор, а не ловить в момент исполнения программы ошибки, связанные с простейшими ошибками и опечатками в сырых строках SQL-запросов.
Пока ORM нет, немного классики …
Работа с БД содержит несколько шагов:
- Создание объекта-подключения к БД
- Выполнение запросов, через полученное подключение
- Обработка результатов
- Транзакции
Кто работал с какими-нибудь обертками такими, как JDBC (в Java) или что-то подобное для C++ ничего нового для себя тут не откроют.
В примере db_client показана работа с двумя СУБД: PostgreSQL и SQLite.
HTTP CRUD сервер
Пример http_crud является логическим завершением демонстрации работы с HTTP и БД — классика простых микросервисов для веба, которые не взаимодействуют непосредственно друг с другом.
Класс web-сервера содержит обработчики всех базовых операций CRUD. Работа с БД ведется в обработчиках. Такой код можно нередко встретить в разных успешно работающих сервисах. Но не смотря на его изначальную простору, в нем есть недостаток — зависимость от СУБД. А при желании отказаться от реляционной БД и перейти к NoSQL базе данных код всех обработчиков почти полностью будет переписан. Во многих реальных проектах стараюсь избегать подобной работы с БД. Выносить в отдельные фасады высокоуровневые функции логики. Фасады — это как правило C++ интерфейсы с конкретными реализациями. А замена БД сводится к очередной реализации интерфейса и запросу у фабрики реализации с новым идентификатором. Играя капитана очевидности, можно сказать, что такой подход себя не раз оправдал.
Заключение
Все, что рассмотрено выше: рефлексия, сериализация, межпроцессное взаимодействие, маршалинг интерфейсов, работа с БД, поддержка HTTP — все нашло свое отражение в итоговом примере построения небольшой системы на микросервисах. Пример демонстрирует небольшой сервис, состоящий из двух микросервисов, взаимодействующих между собой. Один сервис является фасадом к БД, а второй предоставляет Json API для внешних клиентов. Пример является модифицированной версией рассмотренного примера http_crud и является логическим завершением (объединением) всему, о чем шла речь в рамках поста.
Некоторые части MIF, не были рассмотрены, например, db_client, но работа с БД показана в других примера. Работа с сервисами (реализациями интерфейсов) так же частично затронута в примерах. Некоторые части MIF, такие как сериализация, работа с БД и поддержка HTTP уже неоднократно проверены. Межпроцессному взаимодействию пока было уделено меньше времени на проверку и отладку, несмотря на то, что некоторым частям в разработке было уделено много времени. Например, к таким можно отнести поддержку передачи интерфейсов как параметров в другой процесс и обратный вызов их методов. Что является по сути «эго-фичей». Хотелось попробовать реализовать подобный механизм, показанный в примере visitor, но в дальнейшем есть желание довести эту часть до полноценного проверенного и отлаженного решения.
В дальнейшем, возможно, при наличии интереса к изложенному материалу, пост будет иметь продолжение с тем, что не вошло в этот, например, что «под капотом» MIF, почему приняты те или иные решения, какие задачи пришлось решить, чем не подошел thrift или иные подобные решения, какие недочеты вижу и что хотелось бы поменять и усовершенствовать и т.д. Проект появился, как «home project» и, к моему же удивлению, является проектом, которому я уделяю внимание уже почти год, то не должно возникнуть проблем с тем, о чем можно еще написать.
Подводя итог, продублирую ссылку на проект MIF on C++.
Спасибо за внимание.
Автор: NYM