С чего начать писать микросервис на C++

—

от автора

В данной статье я буду опираться на использование libevent в рамках debian+gcc+cmake, но на других unix-подобных ОС сложностей возникнуть не должно(для windows потребуется сборка из исходников и доработка FindLibEvent.cmake файла)

Предисловие

Около 3х лет занимаюсь разработкой микросервисов, однако изначального понимания подходящего стека технологий у меня не было. Испробовал множество различных подходов (одними из которых были OpenDDS и apache-thrift), но в конце концов остановился на RestApi.

RestApi общается по средствам HTTP-запросов, которые в свою очередь представляют структуру данных из заголовков и тела запроса передаваемые через сокет. Первым на что я обратил внимание это boost/asio который предоставляет tcp-сокеты, но тут возникают сложности с объемами разработки:

  • Надо написать корректный прием данных по сокету

  • Самописный парсинг заголовков

  • Самописный парсинг GET-параметров

  • Маршрутизацию путей обращения

Второй на очереди была POCO (POcket COmponents) в которой есть более высокоуровневый HTTP сервер, но по прежнему в нем оставалась проблема с кучей самописного функционала. К тому же данное средство чуть более тяжеловесное и предоставляет функционал который может не потребоваться (немного перегружает наши микросервисы). POCO заточена под другие задачи нежели микросервисы.

Поэтому далее поговорим про libevent на котором я и остановился.

Почему libevent?

  • Легковесный

  • Быстрый

  • Стабильный

  • Кроссплатформенный

  • Из коробки предустановлен в большинстве unix-подобных ОС

  • Используется множеством разработчиков (легче найти сотрудников кто знаком с данной технологией)

  • Есть встроенный маршрутизатор (router)

Однако у libevent есть и очень существенный минус. Данная библиотека в своем публичном интерфейсе использует си-стайл код. Это означает что она заточена на использование «сырых» указателей в купе со встроенными средствами очистки памяти, что в современном C++ категорически недопустимо из-за возможных проблем с утечками памяти (рекомендуется использовать умные указатели).

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

Линковка

Библиотека libevent предоставляется в составе пакета libevent-dev и стоит из коробки на множестве unix-подобных ОС.

Чтобы узнать установлен ли у вас данный пакет, можно ввести команду dpkg -l | grep event или аналогичную для вашей системы.

Для начала нам нужно найти библиотеку в системе, для этого необходимо написать FindLibEvent.cmake (я создаю такие файлы в директории корень_проекта/cmake_modules)

# Находим путь до папки с заголовочными файлами и записываем в ${LIBEVENT_INCLUDE_DIR} find_path(LIBEVENT_INCLUDE_DIR event.h   PATHS     /usr/local     /opt   PATH_SUFFIXES     include )  # Находим бинарные файлы библиотеки и записываем в ${LIBEVENT_LIB} find_library(LIBEVENT_LIB   NAMES     event   PATHS     /usr/local     /opt   PATH_SUFFIXES     lib     lib64 )  include(FindPackageHandleStandardArgs) find_package_handle_standard_args(   LIBEVENT_LIB   LIBEVENT_INCLUDE_DIR )

Так же для удобства можно создать файл импорта библиотеки (я создаю такие файлы в директории корень_проекта/imported/libevent.cmake)

find_package(LibEvent REQUIRED) # Запускаем наш FindLibEvent.cmake add_library(libevent STATIC IMPORTED GLOBAL) # Тут мы создаем target библиотеки с которым и будем работать  # Указываем нашему target-у пути до папки с заголовочными файлами найденными в FindLibEvent.cmake set_target_properties(libevent PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${LIBEVENT_INCLUDE_DIR}) # Указываем нашему target-у пути до бинарников найденных в FindLibEvent.cmake set_target_properties(libevent PROPERTIES IMPORTED_LOCATION ${LIBEVENT_LIB})

Теперь libevent библиотека находится cmake-ом и можно приступить к линковке.

Если мы решили использовать импорт, то нам достаточно вызвать всего 1 команду

target_link_libraries(${PROJECT_NAME}   PUBLIC     libevent )

Если мы не хотим создавать лишние файлы импорта, линкуем то что нашел наш FindLibEvent.cmake

find_package(LibEvent REQUIRED)  target_link_libraries(${PROJECT_NAME}   PUBLIC     ${LIBEVENT_LIB} )  target_include_directories(${PROJECT_NAME}   PUBLIC     ${LIBEVENT_INCLUDE_DIR} )

Чтобы запустить HTTP сервер, нужно

// Подключаем заголовочный файл, содержащий: // * Базовый слушатель запросов // * Буферы для работы с передаваемыми данными // * Средства для работы с HTTP(парсер, маршрутизатор и пр.) #include <evhttp.h>  // Создаем слушатель через умный указатель auto listener = std::make_shared<event_base, decltype(&event_base_free)>(event_base_new(),           &event_base_free); // Создаем HTTP сервер через умный указатель auto server   = std::make_shared<evhttp,     decltype(&evhttp_free)>    (evhttp_new(listener.get()), &evhttp_free);  // Настраиваем маршрутизатор // Устанавливаем обработчик на пути для которых нету собственных обработчиков evhttp_set_gencb(server.get(),             [](evhttp_request*, void*) {}, nullptr); // Устанавливаем обработчик на определенный путь обращения evhttp_set_cb   (server.get(), "/my_path", [](evhttp_request*, void*) {}, nullptr);  // Запускаем прослушку сервера return event_base_dispatch(listener.get());

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

// Создаем буфер для тела ответа auto buffer = std::make_shared<evbuffer, decltype(&evbuffer_free)>(evbuffer_new(), &evbuffer_free); evbuffer_add(buffer, msg.c_str(), msg.length()); // Записываем тело в буфер evhttp_send_reply(request, HTTP_OK, "", buffer); // Отправляем ответ

Мы доделали полноценное общение у нашего сервера, теперь поговорим о получении полезной информации из клиентских запросов.

Первым делом нужно разобрать GET-параметры. Это те параметры, которые передаются в URI запроса(например http://www.hostname.ru?key=value)

struct evkeyvalq params; evhttp_parse_query(request->uri, &params); // Разбираем GET параметры  // Таким способом можно получить значение GET-параметра по его ключу std::string value = evhttp_find_header(&params, "key");  // Таким способом можно перебрать все GET-параметры for (auto it = params.tqh_first; it != nullptr; it = it->next.tqe_next)   std::cout << it->key << ":" << it->value << std::endl;  // Далее необходимо почистить за собой evhttp_clear_headers(&params);

Далее нужно получить тело запроса

auto input = request->input_buffer; // Получаем буфер для чтения тела запроса  // Так лучше память не выделять, но более безопасных способов я не нашел auto length = evbuffer_get_length(input); char* data = new char[length];  evbuffer_copyout(input, data, length); // Читаем тело запроса std::string body(data, length); // Упаковываем в более безопасную сущность delete[] data; // Чистим за собой  return body;

Внимание Callback-функции не поддерживают прерываний (захвата значений лямбда-функциями) поэтому внутри callback можно использовать только статические члены и методы!

ссылка на оригинал статьи https://habr.com/ru/post/547320/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *