Всем привет. На Хабре есть довольно большое количество примеров реализации 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-файлу
Каждый раз руками вводить эти команды в консоли раздражительно и долго. В официальном примере автоматизация делается на этапе сборки проекта с помощью 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/
Добавить комментарий