基本チュートリアル
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をビルドします。
クイックスタートの手順に従って、ソースからgRPCをビルドしてローカルにインストールします。
リポジトリフォルダーから、ルートガイドの例ディレクトリに変更します。
$ cd examples/cpp/route_guide
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
を取得します。ServerReader
のRead()
メソッドを使用して、メッセージがなくなるまでクライアントのリクエストをリクエストオブジェクト(この場合は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(¬e)) {
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
を使用してサーバーを構築して起動します。これを行うには、
- サービス実装クラス
RouteGuideImpl
のインスタンスを作成します。 - ファクトリ
ServerBuilder
クラスのインスタンスを作成します。 - ビルダーの
AddListeningPort()
メソッドを使用して、クライアント要求をリッスンするために使用するアドレスとポートを指定します。 - ビルダーにサービス実装を登録します。
- ビルダーで
BuildAndStart()
を呼び出して、サービスのRPCサーバーを作成して起動します。 - プロセスが終了するか
Shutdown()
が呼び出されるまで、サーバーでWait()
を呼び出してブロッキング待ちを行います。
クライアントの作成
このセクションでは、RouteGuide
サービスのC++クライアントを作成する方法について説明します。完全な例となるクライアントコードは、examples/cpp/route_guide/route_guide_client.ccで確認できます。
スタブの作成
サービスメソッドを呼び出すには、まずスタブを作成する必要があります。
最初に、スタブのgRPCチャネルを作成する必要があります。接続するサーバーのアドレスとポートを指定します。ここではSSLを使用しません。
grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
注記
チャネルに追加のオプションを設定するには、特別なチャネル引数(grpc::ChannelArguments
)を使用してgrpc::CreateCustomChannel()
APIを使用します。これで、生成された.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
を使用してサーバーのレスポンスを読み取ることができます。ClientReader
のRead()
メソッドを使用して、メッセージがなくなるまでサーバーのレスポンスをレスポンスプロトコルバッファーオブジェクト(この場合は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