基本チュートリアル

C++でのgRPCの基本的なチュートリアル入門。

基本チュートリアル

C++でのgRPCの基本的なチュートリアル入門。

このチュートリアルは、gRPCを扱うための基本的なC++プログラマー向け入門を提供します。

この例を段階的に実行することで、以下のことを学びます。

  • .protoファイルでサービスを定義する。
  • プロトコルバッファコンパイラを使用して、サーバーとクライアントのコードを生成する。
  • C++ gRPC APIを使用して、サービス用のシンプルなクライアントとサーバーを作成する。

gRPCの「gRPCとは」を読み、プロトコルバッファに慣れていることを前提としています。このチュートリアルの例ではプロトコルバッファ言語のproto3バージョンを使用していることに注意してください。詳細については、proto3言語ガイドおよびC++生成コードガイドを参照してください。

gRPCを使用する理由

私たちの例は、クライアントがルート上の機能に関する情報を取得したり、ルートの要約を作成したり、サーバーや他のクライアントと交通情報などのルート情報を交換したりできる、シンプルなルートマッピングアプリケーションです。

gRPCを使用すると、1つの.protoファイルでサービスを一度定義し、gRPCがサポートする任意の言語でクライアントとサーバーを生成できます。これにより、大規模データセンター内のサーバーから個人のタブレットまで、さまざまな環境で実行できます。異なる言語や環境間の通信の複雑さはすべてgRPCによって処理されます。また、効率的なシリアライゼーション、シンプルなIDL、簡単なインターフェース更新など、プロトコルバッファを使用する利点もすべて得られます。

サンプルコードとセットアップ

サンプルコードは、examples/cpp/route_guideの下にあるgrpcリポジトリの一部です。サンプルコードを取得してgRPCをビルドしてください。

  1. 「クイックスタート」の指示に従って、gRPCをソースからビルドしてローカルにインストールしてください。

  2. リポジトリフォルダから、ルートガイドの例ディレクトリに移動します。

    cd examples/cpp/route_guide
    
  3. cmakeを実行します。

    mkdir -p cmake/build
    cd cmake/build
    cmake -DCMAKE_PREFIX_PATH=$MY_INSTALL_DIR ../..
    

サービスを定義する

最初の手順(「gRPCとは」で説明したように)は、プロトコルバッファを使用してgRPCの*サービス*とメソッドの*リクエスト*および*レスポンス*の型を定義することです。完全な.protoファイルは、examples/protos/route_guide.protoにあります。

サービスを定義するには、.protoファイルに名前付きのserviceを指定します。

service RouteGuide {
   ...
}

次に、サービス定義内にrpcメソッドを定義し、リクエストとレスポンスの型を指定します。gRPCでは、4種類のサービスメソッドを定義できます。これらはすべてRouteGuideサービスで使用されています。

  • クライアントがスタブを使用してサーバーにリクエストを送信し、通常の関数呼び出しのようにレスポンスが返ってくるのを待つ*シンプルなRPC*。

    // Obtains the feature at a given position.
    rpc GetFeature(Point) returns (Feature) {}
    
  • クライアントがサーバーにリクエストを送信し、メッセージのシーケンスを読み取るためのストリームを取得する*サーバーサイドストリーミングRPC*。クライアントは、メッセージがなくなるまで返されたストリームから読み取ります。例でわかるように、*レスポンス*の型の前にstreamキーワードを配置することで、サーバーサイドストリーミングメソッドを指定します。

    // Obtains the Features available within the given Rectangle.  Results are
    // streamed rather than returned at once (e.g. in a response message with a
    // repeated field), as the rectangle may cover a large area and contain a
    // huge number of features.
    rpc ListFeatures(Rectangle) returns (stream Feature) {}
    
  • クライアントがメッセージのシーケンスを書き込み、提供されたストリームを使用してサーバーに送信する*クライアントサイドストリーミングRPC*。クライアントがメッセージの書き込みを終了したら、サーバーがすべてを読み取ってレスポンスを返すのを待ちます。*リクエスト*の型の前にstreamキーワードを配置することで、クライアントサイドストリーミングメソッドを指定します。

    // Accepts a stream of Points on a route being traversed, returning a
    // RouteSummary when traversal is completed.
    rpc RecordRoute(stream Point) returns (RouteSummary) {}
    
  • 両側が読み書き可能なストリームを使用してメッセージのシーケンスを送信する*双方向ストリーミングRPC*。2つのストリームは独立して動作するため、クライアントとサーバーは任意の順序で読み書きできます。たとえば、サーバーはクライアントメッセージをすべて受信してからレスポンスを書き込むのを待つことも、メッセージを読み取ってからメッセージを書き込むことも、またはその他の読み書きの組み合わせを行うこともできます。各ストリーム内のメッセージの順序は保持されます。リクエストとレスポンスの両方の前にstreamキーワードを配置することで、このタイプのメソッドを指定します。

    // Accepts a stream of RouteNotes sent while a route is being traversed,
    // while receiving other RouteNotes (e.g. from other users).
    rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
    

私たちの.protoファイルには、サービスメソッドで使用されるすべてリクエストとレスポンスの型に対応するプロトコルバッファメッセージ型定義も含まれています。たとえば、ここにPointメッセージ型があります。

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

クライアントとサーバーのコードを生成する

次に、.protoサービス定義からgRPCクライアントとサーバーのインターフェースを生成する必要があります。これは、特別なgRPC C++プラグインとともに、プロトコルバッファコンパイラprotocを使用して行います。

簡単にするために、適切なプラグイン、入力、および出力でprotocを実行するCMakeLists.txtを提供しています(これを自分で実行したい場合は、まずprotocをインストールし、gRPCコードのインストール手順に従っていることを確認してください)。

make route_guide.grpc.pb.o

これは実際には次を実行します。

protoc -I ../../protos --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../../protos/route_guide.proto
protoc -I ../../protos --cpp_out=. ../../protos/route_guide.proto

このコマンドを実行すると、現在のディレクトリに次のファイルが生成されます。

  • route_guide.pb.h、生成されたメッセージクラスを宣言するヘッダー。
  • route_guide.pb.cc、メッセージクラスの実装が含まれています。
  • route_guide.grpc.pb.h、生成されたサービスクラスを宣言するヘッダー。
  • route_guide.grpc.pb.cc、サービスクラスの実装が含まれています。

これらには以下が含まれます。

  • リクエストおよびレスポンスメッセージ型をポピュレート、シリアライズ、および取得するためのすべてのプロトコルバッファコード。

  • RouteGuideという名前のクラス。これは以下を含みます。

    • RouteGuideサービスで定義されたメソッドをクライアントが呼び出すためのリモートインターフェース型(または*スタブ*)。
    • RouteGuideサービスで定義されたメソッドと同様の、サーバーが実装するための2つの抽象インターフェース。

サーバーを作成する

まず、RouteGuideサーバーを作成する方法を見てみましょう。gRPCクライアントの作成のみに興味がある場合は、このセクションをスキップして、「クライアントを作成する」に直接進んでください(それでも興味深いかもしれません!)。

RouteGuideサービスがそのジョブを実行できるようにするには、2つの部分があります。

  • サービス定義から生成されたサービスインターフェースを実装すること。これがサービスの実質的な「作業」です。
  • クライアントからのリクエストをリッスンし、サービスレスポンスを返すgRPCサーバーを実行すること。

例のRouteGuideサーバーは、examples/cpp/route_guide/route_guide_server.ccにあります。どのように機能するかを詳しく見てみましょう。

RouteGuideを実装する

ご覧のように、私たちのサーバーには、生成されたRouteGuide::Serviceインターフェースを実装するRouteGuideImplクラスがあります。

class RouteGuideImpl final : public RouteGuide::Service {
...
}

この場合、RouteGuideの*同期*バージョンを実装しています。これはデフォルトのgRPCサーバーの動作を提供します。非同期インターフェースRouteGuide::AsyncServiceを実装することも可能で、これによりサーバーのスレッド動作をさらにカスタマイズできますが、このチュートリアルでは扱いません。

RouteGuideImplは、すべてのサービスメソッドを実装しています。まず最もシンプルなタイプであるGetFeatureを見てみましょう。これはクライアントからPointを取得し、データベースから対応する機能情報をFeatureで返します。

Status GetFeature(ServerContext* context, const Point* point,
                  Feature* feature) override {
  feature->set_name(GetFeatureName(*point, feature_list_));
  feature->mutable_location()->CopyFrom(*point);
  return Status::OK;
}

メソッドには、RPCのコンテキストオブジェクト、クライアントのPointプロトコルバッファリクエスト、およびレスポンス情報を記入するためのFeatureプロトコルバッファが渡されます。メソッド内で、適切な情報でFeatureをポピュレートし、次にOKステータスでreturnして、RPCの処理が完了したこと、およびFeatureがクライアントに返されることをgRPCに伝えます。

すべてのサービスメソッドは、同時に複数のスレッドから呼び出される可能性がある(そして呼び出されます!)ことに注意してください。メソッド実装がスレッドセーフであることを確認する必要があります。私たちの例では、feature_list_は構築後に変更されないため、設計上安全です。しかし、feature_list_がサービスの有効期間中に変更される場合、このメンバーへのアクセスを同期する必要があります。

次に、もう少し複雑なものを見てみましょう。ストリーミングRPCです。ListFeaturesはサーバーサイドストリーミングRPCなので、クライアントに複数のFeatureを返す必要があります。

Status ListFeatures(ServerContext* context, const Rectangle* rectangle,
                    ServerWriter<Feature>* writer) override {
  auto lo = rectangle->lo();
  auto hi = rectangle->hi();
  long left = std::min(lo.longitude(), hi.longitude());
  long right = std::max(lo.longitude(), hi.longitude());
  long top = std::max(lo.latitude(), hi.latitude());
  long bottom = std::min(lo.latitude(), hi.latitude());
  for (const Feature& f : feature_list_) {
    if (f.location().longitude() >= left &&
        f.location().longitude() <= right &&
        f.location().latitude() >= bottom &&
        f.location().latitude() <= top) {
      writer->Write(f);
    }
  }
  return Status::OK;
}

ご覧のとおり、メソッドのパラメーターに単純なリクエストとレスポンスオブジェクトを受け取る代わりに、今回はリクエストオブジェクト(クライアントがFeatureを検索したい*Rectangle*)と特別なServerWriterオブジェクトを受け取ります。メソッド内で、返すのに必要なだけのFeatureオブジェクトをポピュレートし、ServerWriterWrite()メソッドを使用して書き込みます。最後に、シンプルなRPCと同様に、return Status::OKを呼び出して、レスポンスの書き込みが完了したことをgRPCに伝えます。

クライアントサイドストリーミングメソッドRecordRouteを見ると、ほぼ同様ですが、今回はリクエストオブジェクトと単一のレスポンスの代わりにServerReaderを受け取ります。ServerReaderRead()メソッドを使用して、メッセージがなくなるまでクライアントのリクエストをリクエストオブジェクト(この場合はPoint)に繰り返し読み込みます。サーバーは、各呼び出し後にRead()の戻り値をチェックする必要があります。trueの場合はストリームは正常であり、読み取りを続行できます。falseの場合はメッセージストリームが終了したことを意味します。

while (stream->Read(&point)) {
  ...//process client input
}

最後に、双方向ストリーミングRPCRouteChat()を見てみましょう。

Status RouteChat(ServerContext* context,
                  ServerReaderWriter<RouteNote, RouteNote>* stream) override {
  RouteNote note;
  while (stream->Read(&note)) {
    std::unique_lock<std::mutex> lock(mu_);
    for (const RouteNote& n : received_notes_) {
      if (n.location().latitude() == note.location().latitude() &&
          n.location().longitude() == note.location().longitude()) {
        stream->Write(n);
      }
    }
    received_notes_.push_back(note);
  }

  return Status::OK;
}

今回は、メッセージの読み書きに使用できるServerReaderWriterを受け取ります。ここでの読み書きの構文は、クライアントストリーミングおよびサーバーストリーミングメソッドとまったく同じです。各側は常に相手のメッセージを書き込まれた順序で受け取りますが、クライアントとサーバーは任意の順序で読み書きできます。ストリームは完全に独立して動作します。

received_notes_はインスタンス変数であり、複数のスレッドからアクセスされる可能性があるため、ここではmutexロックを使用して排他的アクセスを保証していることに注意してください。

サーバーを起動する

すべてのメソッドを実装したら、クライアントが実際にサービスを使用できるように、gRPCサーバーを起動する必要があります。次のスニペットは、RouteGuideサービスでこれをどのように行うかを示しています。

void RunServer(const std::string& db_path) {
  std::string server_address("0.0.0.0:50051");
  RouteGuideImpl service(db_path);

  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();
}

ご覧のように、ServerBuilderを使用してサーバーをビルドして起動します。これを行うには、次の手順を実行します。

  1. サービス実装クラスRouteGuideImplのインスタンスを作成します。
  2. ファクトリクラスServerBuilderのインスタンスを作成します。
  3. ビルダーのAddListeningPort()メソッドを使用して、クライアントリクエストをリッスンするために使用したいアドレスとポートを指定します。
  4. ビルダーにサービス実装を登録します。
  5. ビルダーのBuildAndStart()を呼び出して、サービス用のRPCサーバーを作成して起動します。
  6. サーバーのWait()を呼び出して、プロセスが終了するかShutdown()が呼び出されるまでブロックします。

クライアントを作成する

このセクションでは、RouteGuideサービス用のC++クライアントの作成について説明します。完全なサンプルクライアントコードは、examples/cpp/route_guide/route_guide_client.ccにあります。

スタブを作成する

サービスメソッドを呼び出すには、まず*スタブ*を作成する必要があります。

まず、サーバーアドレスと接続したいポートを指定して、スタブ用のgRPC*チャネル*を作成する必要があります。この場合、SSLは使用しません。

grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());

これで、チャネルを使用して、.protoから生成されたRouteGuideクラスで提供されるNewStubメソッドを使用してスタブを作成できます。

public:
 RouteGuideClient(std::shared_ptr<ChannelInterface> channel,
                  const std::string& db)
     : stub_(RouteGuide::NewStub(channel)) {
   ...
 }

サービスメソッドを呼び出す

それでは、サービスメソッドの呼び出し方法を見てみましょう。このチュートリアルでは、各メソッドの*ブロッキング/同期*バージョンを呼び出していることに注意してください。これは、RPC呼び出しがサーバーの応答を待機し、レスポンスを返すか例外を発生させることを意味します。

シンプルなRPC

シンプルなRPCGetFeatureの呼び出しは、ローカルメソッドの呼び出しとほとんど同じくらい簡単です。

Point point;
Feature feature;
point = MakePoint(409146138, -746188906);
GetOneFeature(point, &feature);

...

bool GetOneFeature(const Point& point, Feature* feature) {
  ClientContext context;
  Status status = stub_->GetFeature(&context, point, feature);
  ...
}

ご覧のように、リクエストプロトコルバッファオブジェクト(この場合はPoint)を作成してポピュレートし、サーバーが記入するためのレスポンスプロトコルバッファオブジェクトを作成します。また、呼び出し用のClientContextオブジェクトも作成します。このオブジェクトでRPC構成値(デッドラインなど)をオプションで設定できますが、今回はデフォルト設定を使用します。このオブジェクトは呼び出し間で再利用できないことに注意してください。最後に、スタブのメソッドを呼び出し、コンテキスト、リクエスト、レスポンスを渡します。メソッドがOKを返した場合、レスポンスオブジェクトからサーバーからのレスポンス情報を読み取ることができます。

std::cout << "Found feature called " << feature->name()  << " at "
          << feature->location().latitude()/kCoordFactor_ << ", "
          << feature->location().longitude()/kCoordFactor_ << std::endl;
ストリーミングRPC

それでは、ストリーミングメソッドを見てみましょう。すでに「サーバーを作成する」を読んだことがある場合、これには見慣れたものがあるかもしれません。ストリーミングRPCは、両側で同様の方法で実装されます。ここでは、地理的なFeatureのストリームを返すサーバーサイドストリーミングメソッドListFeaturesを呼び出します。

std::unique_ptr<ClientReader<Feature> > reader(
    stub_->ListFeatures(&context, rect));
while (reader->Read(&feature)) {
  std::cout << "Found feature called "
            << feature.name() << " at "
            << feature.location().latitude()/kCoordFactor_ << ", "
            << feature.location().longitude()/kCoordFactor_ << std::endl;
}
Status status = reader->Finish();

メソッドにコンテキスト、リクエスト、レスポンスを渡す代わりに、コンテキストとリクエストを渡し、ClientReaderオブジェクトを受け取ります。クライアントはClientReaderを使用してサーバーのレスポンスを読み取ることができます。ClientReaderRead()メソッドを使用して、サーバーのレスポンスをレスポンスプロトコルバッファオブジェクト(この場合はFeature)に、メッセージがなくなるまで繰り返し読み込みます。クライアントは、各呼び出し後にRead()の戻り値をチェックする必要があります。trueの場合はストリームは正常であり、読み取りを続行できます。falseの場合はメッセージストリームが終了したことを意味します。最後に、ストリームのFinish()を呼び出して呼び出しを完了し、RPCステータスを取得します。

クライアントサイドストリーミングメソッドRecordRouteは似ていますが、ここではメソッドにコンテキストとレスポンスオブジェクトを渡し、ClientWriterを受け取ります。

std::unique_ptr<ClientWriter<Point> > writer(
    stub_->RecordRoute(&context, &stats));
for (int i = 0; i < kPoints; i++) {
  const Feature& f = feature_list_[feature_distribution(generator)];
  std::cout << "Visiting point "
            << f.location().latitude()/kCoordFactor_ << ", "
            << f.location().longitude()/kCoordFactor_ << std::endl;
  if (!writer->Write(f.location())) {
    // Broken stream.
    break;
  }
  std::this_thread::sleep_for(std::chrono::milliseconds(
      delay_distribution(generator)));
}
writer->WritesDone();
Status status = writer->Finish();
if (status.IsOk()) {
  std::cout << "Finished trip with " << stats.point_count() << " points\n"
            << "Passed " << stats.feature_count() << " features\n"
            << "Travelled " << stats.distance() << " meters\n"
            << "It took " << stats.elapsed_time() << " seconds"
            << std::endl;
} else {
  std::cout << "RecordRoute rpc failed." << std::endl;
}

クライアントのリクエストをストリームにWrite()を使用して書き込み終えたら、ストリームでWritesDone()を呼び出して、書き込みが完了したことをgRPCに通知し、次にFinish()を呼び出して呼び出しを完了し、RPCステータスを取得する必要があります。ステータスがOKの場合、最初にRecordRoute()に渡したレスポンスオブジェクトがサーバーのレスポンスでポピュレートされます。

最後に、双方向ストリーミングRPCRouteChat()を見てみましょう。この場合、コンテキストをメソッドに渡すだけで、メッセージの読み書きに使用できるClientReaderWriterを受け取ります。

std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote> > stream(
    stub_->RouteChat(&context));

ここでの読み書きの構文は、クライアントストリーミングおよびサーバーストリーミングメソッドとまったく同じです。各側は常に相手のメッセージを書き込まれた順序で受け取りますが、クライアントとサーバーは任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみましょう!

クライアントとサーバーをビルドする

make

サーバーを実行する

./route_guide_server --db_path=path/to/route_guide_db.json

別のターミナルから、クライアントを実行します。

./route_guide_client --db_path=path/to/route_guide_db.json