Этот девлог рассказывает о путешествии grpcxx — попытке создать лучший gRPC Server API с использованием современного C++ (C++20).

В предыдущих журналах разработки я описал, как использовал классы-шаблоны для определения типов C++ для методов RPC и служб gRPC (Журнал разработчиков № 3) и как я использовал эти классы-шаблоны в реализации сервера gRPC для отправки запросов RPC (Журнал разработчиков № 4). ).

Давайте посмотрим, как собрать все это вместе и реализовать базовую службу gRPC Hello World

Если вы хотите сначала посмотреть код, он находится в папке examples/helloworld.

Привет Мир 👋

Служба gRPC начинается с определения Protobuf. Вдохновленный официальным примером helloworld.proto, но также учитывая производственный вариант использования с управлением версиями пакетов, я определил следующую службу;

Листинг 1 — greeter.proto

syntax = "proto3";

package helloworld.v1;

service Greeter {
  rpc Hello(GreeterHelloRequest) returns (GreeterHelloResponse) {}
}

message GreeterHelloRequest {
  string name = 1;
}

message GreeterHelloResponse {
  string message = 1;
}

Теперь я могу использовать вышеуказанный файл .proto и компилятор protobuf (protoc) для создания классов C++ для сообщений GreeterHelloRequest и GreeterHelloResponse сообщений. Поскольку я использую CMake, я использовал настраиваемое правило сборки для генерации необходимого кода в процессе сборки.

Если вам интересно, Руководство по основам протокольных буферов для C++ более подробно объясняет определения сообщений и генерацию кода.

Обычно компилятор protobuf также генерирует код для службы gRPC (ссылка: Учебник по основам gRPC C++). Но я не реализовал генератор (пока), так что давайте напишем сервисный код.

namespace helloworld {
namespace v1 {
namespace Greeter {
using rpcHello =
  grpcxx::rpc<"Hello", helloworld::v1::GreeterHelloRequest, helloworld::v1::GreeterHelloResponse>; // [1]

using Service = grpcxx::service<"helloworld.v1.Greeter", rpcHello>; // [2]

struct ServiceImpl { // [3]
  template <typename T> typename T::result_type call(const typename T::request_type &) {
    return {grpcxx::status::code_t::unimplemented, std::nullopt};
  }
};
}; // namespace Greeter
} // namespace v1
} // namespace helloworld

[1] — введите псевдоним для идентификации метода RPC Hello.

[2] — Введите псевдоним для идентификации службы Greeter (с одним методом RPC, идентифицированным псевдонимом типа rpcHello).

[3] — Удобный класс для реализации сервиса, возвращающий статус UNIMPLEMENTED gRPC для всех вызовов.

Это всего несколько строк кода, которые, на мой взгляд, намного легче читать и понимать по сравнению с примерно 330 строками кода, сгенерированным официальным grpc_cpp_plugin.

Код для запуска сервера gRPC будет таким же простым, как;

using namespace helloworld::v1::Greeter;

int main() {
  ServiceImpl greeter;
  Service     service(greeter); // instantiate a service using the convenient implementation

  grpcxx::server server;
  server.add(service);
  server.run("127.0.0.1", 7000);

  return 0;
}
❯ grpcurl -proto examples/helloworld/proto/helloworld/v1/greeter.proto -plaintext localhost:7000 helloworld.v1.Greeter/Hello
ERROR:
  Code: Unimplemented
  Message: 

Но на данный момент он возвращает только ответ UNIMPLEMENTED, так как в нем отсутствует код приложения, который реализовывал бы методы RPC.

Код приложения, реализующий методы RPC, может просто специфицировать шаблонную функцию call<>() в ServiceImpl{}.

using namespace helloworld::v1::Greeter;

// Implement rpc application logic using template specialisation for `ServiceImpl`
template <> rpcHello::result_type ServiceImpl::call<rpcHello>(const GreeterHelloRequest &req) {
  GreeterHelloResponse res;
  res.set_message("Hello `" + req.name() + "` 👋");
  return {grpcxx::status::code_t::ok, res};
}

Or,

Поскольку не все приложения сделаны из одной ткани, существует также свобода использования реализации, определяемой приложением.

using namespace helloworld::v1::Greeter;

// Application defined implementation
struct GreeterImpl {
  template <typename T> typename T::result_type call(const typename T::request_type &) {
    return {grpcxx::status::code_t::unimplemented, std::nullopt};
  }

  template <>
  rpcHello::result_type call<rpcHello>(const helloworld::v1::GreeterHelloRequest &req) {
    helloworld::v1::GreeterHelloResponse res;
    res.set_message("Hello `" + req.name() + "` 👋");
    return {grpcxx::status::code_t::ok, res};
  }
};

int main() {
  GreeterImpl greeter;
  Service     service(greeter);

  grpcxx::server server;
  server.add(service);
  server.run("127.0.0.1", 7000);

  return 0;
}
❯ grpcurl -proto examples/helloworld/proto/helloworld/v1/greeter.proto -plaintext -d '{"name": "World"}' localhost:7000 helloworld.v1.Greeter/Hello
{
  "message": "Hello `World` 👋"
}

Освобождение кода приложения от оков официального gRPC C++ API было моей главной мотивацией для этого проекта, и я очень доволен тем, как это получилось.

В следующий раз, это слишком медленно? 🐌