基本チュートリアル

C++におけるgRPCの基本的なチュートリアル入門。

基本チュートリアル

C++におけるgRPCの基本的なチュートリアル入門。

このチュートリアルは、gRPCを使った作業に関するC++プログラマー向けの入門を提供します。

この例を通して、次のことを学習します。

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

gRPCの概要とgRPC入門を読んでいること、およびプロトコルバッファに精通していることを前提としています。このチュートリアルの例では、プロトコルバッファ言語のproto3バージョンを使用していることに注意してください。proto3言語ガイドC++生成コードガイドで詳細を確認できます。

gRPCを使う理由

私たちの例は、クライアントがルート上のフィーチャに関する情報を入手し、ルートのサマリーを作成し、サーバーや他のクライアントと交通状況の更新などのルート情報を交換できるシンプルなルートマッピングアプリケーションです。

gRPCを使用すると、.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入門で説明されているように、プロトコルバッファを使用して、gRPC *サービス*とメソッドの*リクエスト*と*レスポンス*の型を定義することです。examples/protos/route_guide.protoで完全な.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を使用します。

簡略化のため、CMakeLists.txtを提供しています。これは、適切なプラグイン、入力、出力を使用してprotocを実行します(自分で実行する場合は、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 {
...
}

ここでは、デフォルトのgRPCサーバーの動作を提供するRouteGuideの*同期*バージョンを実装しています。非同期インターフェース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ステータスを返すことで、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オブジェクトを設定し、そのWrite()メソッドを使用してServerWriterに書き込みます。最後に、シンプルなRPCと同様に、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_はインスタンス変数であり、複数のスレッドからアクセスできるため、ここでミューテックスロックを使用して排他アクセスを保証します。

サーバーの起動

すべてのメソッドを実装したら、クライアントが実際にサービスを使用できるように、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. プロセスが終了するかShutdown()が呼び出されるまで、サーバーでWait()を呼び出してブロッキング待ちを行います。

クライアントの作成

このセクションでは、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