gRPC — фреймворк от Google для удалённого вызова процедур


В деле удалённого вызова процедур дела уже давно обстоят в точности как в известном комиксе «14 стандартов» — чего только тут не напридумано: древние DCOM и Corba, странные SOAP и .NET Remoting, современные REST и AMQP (да, я знаю, что кое-что из этого формально не RPC, для того чтобы обсудить терминологию даже вот специальный топик недавно создали, тем ни менее всё это используется как RPC, а если что-то выглядит, как утка и плавает, как утка — ну, вы в курсе).

И конечно же, в полном соответствии со сценарием комикса, на рынок пришел Google и заявил что вот теперь наконец он создал ещё один, последний и самый правильный стандарт RPC. Google можно понять — продолжать в 21-ом веке гонять петабайты данных по старому и неэффективному HTTP+REST, теряя на каждом байте деньги — просто глупо. В то же время взять чужой стандарт и сказать «мы не смогли придумать ничего лучше» — совершенно не в их стиле.

Поэтому, встречайте, gRPC, что расшифровывается как «gRPC Remote Procedure Calls» — новый фреймворк для удалённого вызова процедур от Google. В этой статье мы поговорим о том, почему же он, в отличии от предыдущих «14 стандартов» всё-таки захватит мир (ну или хотя бы его часть), попробуем собрать билд gRPC под Windows + Visual Studio (и даже не говорите мне, что инструкция не нужна — в официальной документации упущено штук 5 важных шагов, без которых ничего не собирается), а также попробуем написать простенький сервис и клиент, обменивающиеся запросами и ответами.

Зачем нужен ещё один стандарт?

Прежде всего давайте оглянемся вокруг. Что мы видим? Мы видим REST + HTTP/1.1. Нет, есть всякое, но именно эта туча закрывает добрых три четверти небосвода клиент-серверных коммуникаций. Присмотревшись ещё чуть пристальнее, мы видим, что REST в 95% случаев вырождается в CRUD.

В итоге мы имеем:

  • Неэффективность протокола HTTP/1.1 — несжатые заголовки, отсутствие полноценной двусторонней связи, неэффективный подход к использованию ресурсов ОС, лишний трафик, лишние задержки.
  • Необходимость натягивать нашу модель данных и событий на REST+CRUD, что часто получается, как воздушный шарик на глобус и вынуждает Яндекс писать вот такие, бесспорно, очень хорошие статьи, которые, однако, были бы не нужны, если бы людям не приходилось думать «Чем же вызвать заклинание для призыва элементаля — PUT’ом или POST’ом? И какой же HTTP-код вернуть, чтобы он означал „Перейдите на 3 клетки вперёд и тяните новую карту“ ?»

Именно с этого места и начинается gRPC. Итак, из коробки мы имеем:

  • Protobuf в качестве инструмента описания типов данных и сериализации. Очень классная и хорошо зарекомендовавшая себя на практике штука. Собственно говоря, те, кому была нужна производительность — и раньше брали Protobuf, а дальше уже отдельно заморачивались транспортом. Теперь всё в комплекте.
  • HTTP/2 в качестве транспорта. И это невероятно мощный ход! Вся прелесть полного сжатия данных, контроля трафика, инициации событий с сервера, переиспользования одного cокета для нескольких параллельных запросов — красотища.
  • Статические пути — никаких больше «сервис/коллекция/ресурс/запрос? параметр=значение». Теперь только «сервис», а что внутри — описывайте в терминах вашей модели и её событий.
  • Никакого привязывания методов к HTTP-методам, никакого привязывания возвращаемых значений к HTTP-статусам. Пишите, что хотите.
  • SSL/TLS, OAuth 2.0, аутентификация через сервисы Google, плюс можно прикрутить свою (например, двухфакторную)
  • Поддержка 9-ти языков: C, C++, Java, Go, Node.js, Python, Ruby, Objective-C, PHP, C# плюс, конечно, никто не запрещает взять и реализовать свою версию хоть для брейнфака.
  • Поддержка gRPC в публичных API от Google. Уже работает для некоторых сервисов. Нет, REST-версии, конечно, тоже останутся. Но посудите сами, если у вас будет выбор — использовать, скажем, из мобильного приложения REST-версию, отдающие данные за 1 сек или с теми же затратами на разработку взять gRPC-версию, работающую 0.5 сек — что вы выберете? А что выберет ваш конкурент?

Сборка gRPC

Нам понадобитья:

  • Git
  • Visual Studio 2013 + Nuget
  • CMake

Забираем код

  1. Забираем репозипорий gRPC с Гитхаба
  2. Выполняем команду
    git submodule update --init 

    — это нужно для того, чтобы скачать зависимости (protobuf, openssl и т.д.).

Собираем Protobuf

  • Переходим в папку grpc\third_party\protobuf\cmake и создаём там папку build, переходим в неё.
  • Выполняем команду
    cmake -G «Visual Studio 12 2013» -DBUILD_TESTING=OFF…
  • Открываем созданный на предыдущем шаге файл protobuf.sln в Visual Studio и собираем (F7).
    На этом этапе мы получаем ценные артефакты — утилиту protoc.exe, которая понадобиться нам для генерации кода сериализации\десериализации данных и lib-файлы, которая будут нужны при линковке gRPC.
  • Копируем папку grpc\third_party\protobuf\cmake\build\Debug в папку grpc\third_party\protobuf\cmake.
    Ещё раз — папку Debug нужно скопировать на 1 уровень выше. Это какая-то неконсистентность в документациях gRPC и Protobuf. В Protobuf говорится, что всё билдить надо в папке build, а вот исходники проектов gRPC ничего об этой папке не знают и ищут библиотеки Protobuf прямо в grpc\third_party\protobuf\cmake\Debug

Собираем gRPC

  1. Открываем файл grpc\vsprojects\grpc_protoc_plugins.sln и собираем его.
    Если вы верно прошли сборку Protobuf на предыдущем этапе — всё должно пройти гладко. Теперь у вас есть плагины к protoc.exe, которые позволяют ему не только генерировать код сериализации\десериализации, но и добавлять в него функционал gRPC (собственно говоря, удалённый вызов процедур). Плагины и protoc.exe нужно положить в одну папку, например, в grpc\vsprojects\Debug.
  2. Открываем файл grpc\vsprojects\grpc.sln и собираем его.
    По ходу сборку должен запуститься Nuget и скачать необходимые зависимости (openssl, zlib). Если у вас нет Nuget или он почему-то не скачал зависимости — будут проблемы.
    По окончанию билда у нас появятся все необходимые библиотеки, которые мы сможем использовать в нашем проекте для коммуникаций через gRPC.

Наш проект

Давайте напишем такой себе API для Хабрахабра с применением gRPC
Методы у нас будут такие:

  • GetKarma будет получать строку с именем пользователя, а возвращать дробное число со значением его кармы
  • PostArticle будет получать запрос на создание новой статьи со всеми её метаданными, а возвращать результат публикации — структуру со ссылкой на статью, временем публикации ну и текстом ошибки, если публикация не удалась

Это всё нам нужно описать в терминах gRPC. Это будет выглядеть как-то так (описание типов можно посмотреть в документации на protobuf):

syntax = "proto3";  package HabrahabrApi;  message KarmaRequest {   string username = 1; }  message KarmaResponse {   string username = 1;   float karma = 2; }  message PostArticleRequest {   string title = 1;   string body = 2;   repeated string tag = 3;    repeated string hub = 4;  }  message PostArticleResponse {   bool posted = 1;   string url = 2;   string time = 3;   string error_code = 4; }	  service HabrApi {   rpc GetKarma(KarmaRequest) returns (KarmaResponse) {}   rpc PostArticle(PostArticleRequest) returns (PostArticleResponse) {} } 

Переходим в папку grpc\vsprojects\Debug и запускаем там 2 команды (кстати, обратите внимание, в официальной документации в этом месте ошибка, неверные аргументы):

protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe habr.proto protoc --cpp_out=. habr.proto  

На выходе мы получим 4 файла:

  • habr.pb.h
  • habr.pb.cc
  • habr.grpc.pb.h
  • habr.grpc.pb.cc

Это, как не сложно догадаться, заготовки наших будущих клиента и сервиса, которые смогут обмениваться сообщениями по описанному выше протоколу.

Давайте уже создадим проект!

  1. Создаём в Visual Studio новый solution, назовём его HabrAPI.
  2. Добавляем в него два консольных приложения — HabrServer и HabrClient.
  3. Добавляем в них сгенерированные на предыдущем шаге h и сс-файлы. В сервер надо включать все 4, в клиент — только habr.pb.h и habr.pb.cc.
  4. Добавляем в настройках проектов в Additional Include Directories путь к папкам grpc\third_party\protobuf\src и grpc\include
  5. Добавляем в настройках проектов в Additional Library Directories путь к grpc\third_party\protobuf\cmake\Debug
  6. Добавляем в настройках проектов в Additional Dependencies библиотеку libprotobuf.lib
  7. Выставляем тип линковки таким же, с каким был собран Protobuf (свойство Runtime Library на вкладке Code Generation). В этом месте может оказаться, что вы не собрали Protobuf в нужной вам конфигурации, и придётся вернуться и пересобрать его. Я выбирал и там и там /MTd.
  8. Добавляем через Nuget зависимости на zlib и openssl.

Теперь у нас всё собирается. Правда, ничего пока ещё не работает.

Клиент

Здесь всё просто. Во-первых, нам нужно создать класс, унаследованный от заглушки, сгенерированной в habr.pb.h. Во-вторых, реализовать в нём методы GetKarma и PostArticle. В-третьих, вызывать их и, к примеру, выводить результаты в консоль. Получится как-то так:

#include <iostream> #include <memory> #include <string>  #include <grpc/grpc.h> #include <grpc++/channel.h> #include <grpc++/client_context.h> #include <grpc++/create_channel.h> #include <grpc++/credentials.h> #include "habr.grpc.pb.h"  using grpc::Channel; using grpc::ChannelArguments; using grpc::ClientContext; using grpc::Status; using HabrahabrApi::KarmaRequest; using HabrahabrApi::KarmaResponse; using HabrahabrApi::PostArticleRequest; using HabrahabrApi::PostArticleResponse; using HabrahabrApi::HabrApi;  class HabrahabrClient {  public:   HabrahabrClient(std::shared_ptr<Channel> channel)       : stub_(HabrApi::NewStub(channel)) {}    float GetKarma(const std::string& username) {     KarmaRequest request;     request.set_username(username);     KarmaResponse reply;     ClientContext context;      Status status = stub_->GetKarma(&context, request, &reply);     if (status.ok()) {       return reply.karma();     } else {       return 0;     }   }    bool PostArticle(const std::string& username) {     PostArticleRequest request;     request.set_title("Article about gRPC");     request.set_body("bla-bla-bla");     request.set_tag("UFO");     request.set_hab("Infopulse");     PostArticleResponse reply;     ClientContext context;      Status status = stub_->PostArticle(&context, request, &reply);     return status.ok() && reply.posted();   }   private:   std::unique_ptr<HabrApi::Stub> stub_; };  int main(int argc, char** argv) {   HabrahabrClient client(       grpc::CreateChannel("localhost:50051", grpc::InsecureCredentials(),                           ChannelArguments()));   std::string user("tangro");   std::string reply = client.GetKarma(user);   std::cout << "Karma received: " << reply << std::endl;    return 0; }  

Сервер

С сервером похожая история — мы наследуемся от класса сервиса, сгенерированного в habr.grpc.pb.h и реализуем его методы. Дальше мы запускаем слушателя на определённом порту, ну и ждём клиентов. Как-то вот так:

#include <iostream> #include <memory> #include <string>  #include <grpc/grpc.h> #include <grpc++/server.h> #include <grpc++/server_builder.h> #include <grpc++/server_context.h> #include <grpc++/server_credentials.h> #include "habr.grpc.pb.h"  using grpc::Server; using grpc::ServerBuilder; using grpc::ServerContext; using grpc::Status; using HabrahabrApi::KarmaRequest; using HabrahabrApi::KarmaResponse; using HabrahabrApi::PostArticleRequest; using HabrahabrApi::PostArticleResponse; using HabrahabrApi::HabrApi;  class HabrahabrServiceImpl final : public HabrApi::Service {   Status GetKarma(ServerContext* context, const KarmaRequest* request,                   KarmaResponse* reply) override {     reply->set_karma(42);     return Status::OK;   }    Status PostArticle(ServerContext* context, const PostArticleRequest* request,                   PostArticleResponse* reply) override {     reply->set_posted(true);     reply->set_url("some_url");      return Status::OK;   } };  void RunServer() {   std::string server_address("0.0.0.0:50051");   HabrahabrServiceImpl service;    ServerBuilder builder;   builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());   builder.RegisterService(&service);   std::unique_ptr<Server> server(builder.BuildAndStart());   std::cout << "Server listening on " << server_address << std::endl;   server->Wait(); }  int main(int argc, char** argv) {   RunServer();    return 0; } 

Удачи вам в использовании gRPC.

ссылка на оригинал статьи http://habrahabr.ru/post/265805/

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

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