gRPC-сервер на C++

от автора

Всем привет. На Хабре есть довольно большое количество примеров реализации gRPC-серверов на GO, чуть в меньшей степени на python, еще меньше — на других языках. Поиск примеров данного проекта для C++ дал мне не так много информации, как хотелось бы. К счастью, очень крутое решение-экземпляр есть на официальном сайте (ссылка). Если вам не хочется читать код и комментарии на английском языке, добро пожаловать под кат.

Теория, а точнее, ее отсутствие

Давайте предположим, что вы знаете базовую теорию о gRPC и в некоторой степени разбираетесь в том, что такое proto-файлы, а также как с помощью утилиты protoc из них сгенерировать исходный код на C++.

Тестовый пример

Сейчас перейдем к примеру, на базе которого будем разбирать по шагам последовательность действий по созданию gRPC-сервера. Пусть у нас есть потребность создать приложение, в некотором смысле напоминающее гастрономическую социальную сеть.

Здесь пользователь может:

  • зарегистрироваться

  • подписаться на другого человека

  • оценить свое посещение в ресторан или кафе по 5-балльной шкале с указанием заказанных блюд и даты посещения

  • зайти на страницу того, на кого подписался, просмотреть его посещения

Архитектура

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

«Возможность подписаться/оценить посещение/оставить комментарий» — всю эту информацию нужно где-то хранить. Добавим компонент «storage».

«Зарегистрироваться» — наличие этой функциональной возможности подразумевает реализацию сразу трех важных составляющих: идентификация, аутентификация и авторизация. Но для простоты примера мы их опустим.

Пока мы никак не можем взаимодействовать с приложением, нужна API-шка. Вот как раз здесь и будет базироваться наша gRPC-составляющая (помимо всего прочего, для локального взаимодействия с сервером на C++ мы могли бы развернуть unix domain socket, для удаленного — взаимодействовать по принципам REST, так что одним gRPC API-шка не ограничивается).

Единственное, что осталось — точка входа в ПО, функция main, которую мы поместим в компонент «app».

Примерная архитектура проекта

Примерная архитектура проекта

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

К gRPC для плюсов

В первую очередь нас интересует gRPC-API. Углубимся в эту часть архитектуры. Тут следует подумать над proto-контрактами, на основе которых будем в дальнейшем генерировать C++ код. Отталкиваться будем все от той же описательной части.

Пользователь может зарегистрироваться, значит сделаем rpc Registrate(…) returns (…). Первое троеточие в скобках — то, что rpc-метод (вызываемый на клиенте) использует в качестве аргумента, второе троеточие — то, что метод возвращает, то есть ответ от сервера. Назовем их ClientRegistrationReq и ClientRegistrationResp соответственно. Дальше следует подумать, чем наполнить содержимое этих двух сообщений. Что обычно указывает человек при регистрации? Электронную почту, имя/фамилия, телефон (по желанию). Что в ответ на такое сообщение может прислать сервер? Статус регистрации (успех или нет) и необязательная описательная часть (например, причина, по которой не удалось осуществить регистрацию). Тогда имеем что-то вроде этого:

service GrpcTransport {     // Registrate new user     rpc Registrate(ClientRegistrationReq) returns (ClientRegistrationResp) {} }  // Message for registrate new user message ClientRegistrationReq {     string   electronic_mail       = 1;     string   name                  = 2;     string   sername               = 3;     optional string phone_number   = 4; }  // Response on new user registration message ClientRegistrationResp {     bool            ok     = 1;     optional string reason = 2; }

Совсем немного о том, что в proto-файле происходит. Во-первых, мы объявили сервис GrpcTransport, в рамках которого существуют те или иные rpc-методы, вызываемые клиентом (синтаксис: rpc $MehtodName($Params) returns ($ReturnedVals) {}). Далее мы описываем каждое из сообщений, то есть $Params и $ReturnedVals соответственно. С точки зрения языка программирования их можно воспринимать как структуры с перечисленными полями определенного типа (со всеми типами proto можно ознакомиться здесь). Используемое в примере ключевое слово optional говорит о том, что параметр является необязательным.

Идем дальше. «Подписаться на другого человека». В социальных сетях люди ищут других по имени или же ник-нейму, а его мы не предусмотрели. Но с 100%-ной вероятностью электронная почта — уникальный идентификационный ключ. Конечно, это является приватной информацией, однако для тестового примера поиск другого пользователя по почте вполне подойдет. В ответ от сервера получаем статус, удалось ли подписаться и, если нет, то почему. Итак, наш rpc-метод Subscribe и соответствующие ему сообщения:

// Subscribe rpc Subscribe(SubscriptionReq) returns (SubscriptionResp) {}  // Message for subscribe to another user message SubscriptionReq {     string electronic_mail = 1; }  // Server response about subscription message SubscriptionResp {     bool            ok     = 1;     optional string reason = 2; }

Оценка заведения. Здесь дадим человеку возможность ввести название и адрес заведения, указав список оцененных блюд (map из названия блюда, то есть строки, в числовую целую оценку с максимумом 5). В ответ будем ожидать статус — добавилось посещение или нет с тем же опциональным указанием причины.

// Estimate dishes rpc EstimateEstablishment(EstimationReq) returns (EstimatonResp) {}  // Message for estimate dishes message EstimationReq {     string             name    = 1;     string             address = 2;     map<string, int32> dishes  = 3; }  // Server response about dishes estimation message EstimatonResp {     bool            ok     = 1;     optional string reason = 2; }

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

// Subscription estimations rpc GetSubscriptionEstimations(SubscriptionEstimationsReq) returns (SubscriptionEstimationsResp) {}  // Message for get subscription dishes estimations message SubscriptionEstimationsReq {     string electronic_mail = 1; }  // Server response about getting subscription estimations message SubscriptionEstimationsResp {     bool                   ok          = 1;     optional string        reason      = 2;     repeated EstimationReq estimations = 3; }

Тип последнего поля — массив (ключевое слово repeated) messag-ей, придуманных нами же.

Автоматизированная кодогенерация

Итак, proto-файл у нас есть, все наши «контракты» продуманы. Что дальше? Следующий шаг — использование специальной утилиты protoc, которая на базе proto-файлов сгенерирует нам файлы с кодом на C++. Подробное описание того, как установить protoc, можно найти здесь. Для кодогенерации нам потребуется использование двух команд:

protoc -I <path to folder with proto-files>\ --cpp_out=<path where need to place generated cpp messages files>\ <path to proto-file>  protoc -I --grpc_out=<path where need to place cpp services files>\ --plugin=protoc-gen-grpc=`which grpc_cpp_plugin`\ <path to proto-file>

Разберёмся в перечисленных параметрах.

<path to folder with proto files> — путь к папке, где лежат proto-контракты (в нашем случае файл всего один, но их может быть больше)

<path where need to place generated cpp messages files> — путь, куда мы хотим поместить сгенерированные header и cpp-файлы, описывающие messag-ы

<path where need to place cpp services files> — путь, по которому будут лежать сгенерированные header и cpp-файлы, описывающие сервисы и их rpc-методы

<path to proto file> — путь к нашему proto-файлу

Пример использования protoc

Пример использования protoc

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

Файловая структура проекта

Файловая структура проекта

Ключевое — CMakeLists.txt папки ./lib/api/src/ (относительно корня проекта). В CMake-листе необходимы следующие строки:

# Generate cpp-files due to proto execute_process(     COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generate.sh )

Команда execute_process нужна для запуска bash-скрипта generate.sh с автоматизированной генерацией кода. В самом скрипте мы указываем пути к исходникам proto и те инструкции, которые с ними нужно сделать:

#!/bin/bash  SCRIPT_DIR_PATH=$(cd "$(dirname "$0")" && pwd) echo $SCRIPT_DIR_PATH  SRC_DIR=$SCRIPT_DIR_PATH/ PROTO_DIR=/$SCRIPT_DIR_PATH/../resource  protoc -I $PROTO_DIR --cpp_out=$SRC_DIR $PROTO_DIR/main.proto protoc -I $PROTO_DIR --grpc_out=$SRC_DIR --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` $PROTO_DIR/main.proto

Использование сгенерированного кода

В результате генерации кода в новые файлы сохраняются в папку ./lib/api/src/ (относительно корня). Их мы будем использовать для реализации двух простейших публичных функций (запуск и остановка сервера):

namespace api_grpc {  //! Public function for start gRPC-server void runServer(const std::string& address, std::shared_ptr<storage::IStorageManager> pStoreManager);  //! Public function for stop gRPC-server void stopServer();  } // namespace api_grpc

Так, они будут объявлены в файле ./lib/api/include/api/GrpcAPI.h, а определены — в ./lib/api/src/GrpcAPI.cpp:

using api_grpc::ServerGRPC;  using grpc::Server; using grpc::ServerBuilder;  ServerGRPC*             pService = nullptr; std::unique_ptr<Server> pServer  = nullptr;  void api_grpc::runServer(const std::string&                        address,                          std::shared_ptr<storage::IStorageManager> pStoreManager) {     // создаем свой сервис     pService = new ServerGRPC(pStoreManager);      // создаем gRPC-шный server builder     ServerBuilder serverBuilder;      // добавляем порт и специфицируем вид подключения (не защищенное)     serverBuilder.AddListeningPort(address, grpc::InsecureServerCredentials());      // регистрируем наш собственный сервис и запускаем     serverBuilder.RegisterService(pService);     pServer = serverBuilder.BuildAndStart();     std::cout << "Server listening on " << address << std::endl;      // этот метод является блокирующим     pServer->Wait(); }  //! Public function for stop gRPC-server void api_grpc::stopServer() {     // этот метод завершит блокоирующий Wait()     pServer->Shutdown();      delete pService;     delete(pServer.release()); }

Имплементация завязана на написанном нами классе ServerGRPC, который является наследником GrpcTransport::Service (как раз тот сервис, что мы описали в proto файле и сгенерировали при помощи protoc):

namespace api_grpc {  //! gRPC-server implementation class ServerGRPC final : public GrpcTransport::Service { public:     //! Ctor by default     ServerGRPC() = delete;      //! Constructor     ServerGRPC(std::shared_ptr<storage::IStorageManager> pStoreManager);      //! Destructor     ~ServerGRPC();      //! Registrate new user     grpc::Status Registrate(         grpc::ServerContext* context,         const ClientRegistrationReq* request,         ClientRegistrationResp* response     ) override;      //! Subscribe to user     grpc::Status Subscribe(         grpc::ServerContext* context,         const SubscriptionReq* request,         SubscriptionResp* response     ) override;          //! Estimate dishes     grpc::Status EstimateEstablishment(         grpc::ServerContext* context,         const EstimationReq* request,         EstimatonResp* response     ) override;          //! Subscription estimations     grpc::Status GetSubscriptionEstimations(         grpc::ServerContext* context,         const SubscriptionEstimationsReq* request,         SubscriptionEstimationsResp* response     ) override;  private:     std::shared_ptr<storage::IStorageManager> pStorageManager_; };  }

Как видите, все те rpc-методы, что присутствуют в proto-файле, есть и здесь, причем каждый из них помечен override, потому что точно такие же виртуальные методы есть в классе-родителе GrpcTransport::Service.

То, каким смыслом вы наполните эти методы, зависит от вашей фантазии.

Заключение

Какой бизнес-логикой наполнил методы я, вы можете посмотреть на github-е, куда я прикрепил весь код данного проекта (статья уже получилась довольно жирной), а также написал юнит-тесты (использовал GTest), чтобы у вас была возможность подебажиться, дабы лучше понять всю суть. Для полноты тестов сервера нужен и gRPC-клиент, который тоже там есть. Надеюсь, данный материал будет полезен. Ссылка: https://github.com/Daniel-Ager-Okker/gRPC-example


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


Комментарии

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

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