基本チュートリアル

Dart gRPC の基本チュートリアル入門。

基本チュートリアル

Dart gRPC の基本チュートリアル入門。

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

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

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

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

gRPCを使用する理由

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

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

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

チュートリアルのサンプルコードは、grpc/grpc-dart/example/route_guide にあります。サンプルをダウンロードするには、次のコマンドを実行して grpc-dart リポジトリをクローンします。

git clone --depth 1 https://github.com/grpc/grpc-dart

次に、現在のディレクトリを grpc-dart/example/route_guide に変更します。

cd grpc-dart/example/route_guide

クライアントとサーバーのインターフェイスコードを生成するために必要なツールは、すでにインストールされているはずです。まだの場合は、クイックスタート でセットアップ手順を確認してください。

サービスを定義する

(gRPC の概要 で説明したように) 最初の手順は、プロトコルバッファ を使用して gRPC サービス およびメソッドの リクエストレスポンス の型を定義することです。完全な .proto ファイルは、example/route_guide/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 クライアントとサーバーのインターフェイスを生成する必要があります。これは、特別な Dart プラグインを使用したプロトコルバッファコンパイラ protoc を使用して行います。これは、クイックスタート で行ったことと同様です。

route_guide の例のディレクトリから以下を実行します。

protoc -I protos/ protos/route_guide.proto --dart_out=grpc:lib/src/generated

このコマンドを実行すると、route_guide の例のディレクトリの下の lib/src/generated ディレクトリに次のファイルが生成されます。

  • route_guide.pb.dart
  • route_guide.pbenum.dart
  • route_guide.pbgrpc.dart
  • route_guide.pbjson.dart

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

  • リクエストおよびレスポンスメッセージの型をポピュレート、シリアライズ、および取得するためのすべてのプロトコルバッファコード。
  • RouteGuide サービスで定義されたメソッドを呼び出すためのクライアント用のインターフェイス型(または スタブ)。
  • 同様に、RouteGuide サービスで定義されたメソッドを持つ、サーバーが実装するためのインターフェイス型。

サーバーを作成する

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

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

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

例の RouteGuide サーバーは、grpc-dart/example/route_guide/lib/src/server.dart で確認できます。どのように機能するか、詳しく見てみましょう。

RouteGuideを実装する

ご覧のとおり、サーバーには生成された抽象クラス RouteGuideServiceBase を拡張する RouteGuideService クラスがあります。

class RouteGuideService extends RouteGuideServiceBase {
  Future<Feature> getFeature(grpc.ServiceCall call, Point request) async {
    ...
  }

  Stream<Feature> listFeatures(
      grpc.ServiceCall call, Rectangle request) async* {
    ...
  }

  Future<RouteSummary> recordRoute(
      grpc.ServiceCall call, Stream<Point> request) async {
    ...
  }

  Stream<RouteNote> routeChat(
      grpc.ServiceCall call, Stream<RouteNote> request) async* {
    ...
  }

  ...
}
シンプルなRPC

RouteGuideService は、すべてのサービスメソッドを実装しています。最も単純な型である GetFeature から見ていきましょう。これは、クライアントから Point を受け取り、データベースから対応する機能情報を Feature として返します。

/// GetFeature handler. Returns a feature for the given location.
/// The [context] object provides access to client metadata, cancellation, etc.
@override
Future<Feature> getFeature(grpc.ServiceCall call, Point request) async {
  return featuresDb.firstWhere((f) => f.location == request,
      orElse: () => Feature()..location = request);
}

メソッドには、RPC のコンテキストオブジェクトと、クライアントの Point プロトコルバッファリクエストが渡されます。レスポンス情報を含む Feature プロトコルバッファオブジェクトを返します。メソッド内で、Feature を適切な情報でポピュレートし、それを gRPC フレームワークに return します。これにより、クライアントに返送されます。

サーバーサイドストリーミングRPC

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

/// ListFeatures handler. Returns a stream of features within the given
/// rectangle.
@override
Stream<Feature> listFeatures(
    grpc.ServiceCall call, Rectangle request) async* {
  final normalizedRectangle = _normalize(request);
  // For each feature, check if it is in the given bounding box
  for (var feature in featuresDb) {
    if (feature.name.isEmpty) continue;
    final location = feature.location;
    if (_contains(normalizedRectangle, location)) {
      yield feature;
    }
  }
}

ご覧のとおり、メソッドで単純なリクエストとレスポンスオブジェクトを取得して返す代わりに、今回はリクエストオブジェクト(クライアントが検索したい FeatureRectangle)を受け取り、Feature オブジェクトの Stream を返します。

メソッド内では、返したい Feature オブジェクトをできるだけ多くポピュレートし、yield を使用して返されるストリームに追加します。メソッドが返されるとストリームは自動的に閉じられ、応答の書き込みが完了したことを gRPC に伝えます。

この呼び出しでエラーが発生した場合、エラーはストリームに例外として追加され、gRPC レイヤーはそれをワイヤーで送信される適切な RPC ステータスに変換します。

クライアントサイドストリーミングRPC

次に、もう少し複雑なクライアントサイドストリーミングメソッド RecordRoute を見てみましょう。クライアントから Point のストリームを受け取り、旅行に関する情報を含む単一の RouteSummary を返します。ご覧のとおり、今回はリクエストパラメータがストリームになり、サーバーはクライアントからのリクエストメッセージを読み取るためにこれを使用できます。サーバーは、単純な RPC ケースと同様に単一のレスポンスを返します。

/// RecordRoute handler. Gets a stream of points, and responds with statistics
/// about the "trip": number of points, number of known features visited,
/// total distance traveled, and total time spent.
@override
Future<RouteSummary> recordRoute(
    grpc.ServiceCall call, Stream<Point> request) async {
  int pointCount = 0;
  int featureCount = 0;
  double distance = 0.0;
  Point previous;
  final timer = Stopwatch();

  await for (var location in request) {
    if (!timer.isRunning) timer.start();
    pointCount++;
    final feature = featuresDb.firstWhereOrNull((f) => f.location == location);
    if (feature != null) {
      featureCount++;
    }
    // For each point after the first, add the incremental distance from the
    // previous point to the total distance value.
    if (previous != null) distance += _distance(previous, location);
    previous = location;
  }
  timer.stop();
  return RouteSummary()
    ..pointCount = pointCount
    ..featureCount = featureCount
    ..distance = distance.round()
    ..elapsedTime = timer.elapsed.inSeconds;
}

メソッド本体では、リクエストストリームで await for を使用して、メッセージがなくなるまでクライアントのリクエスト(この場合は Point オブジェクト)を繰り返し読み取ります。リクエストストリームが終了すると、サーバーは RouteSummary を返すことができます。

双方向ストリーミング RPC

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

/// RouteChat handler. Receives a stream of message/location pairs, and
/// responds with a stream of all previous messages at each of those
/// locations.
@override
Stream<RouteNote> routeChat(
    grpc.ServiceCall call, Stream<RouteNote> request) async* {
  await for (var note in request) {
    final notes = routeNotes.putIfAbsent(note.location, () => <RouteNote>[]);
    for (var note in notes) yield note;
    notes.add(note);
  }
}

今回は RouteNote のストリームを受け取ります。クライアントサイドストリーミングの例と同様に、メッセージを読み取るために使用できます。ただし、今回はクライアントが 自分たちの メッセージストリームにメッセージを書き込んでいる間に、メソッドの返されるストリーム経由で値を返します。

ここでの読み書きの構文は、クライアントストリーミングとサーバーストリーミングのメソッドと似ています。各サイドは相手のメッセージを書き込まれた順序で常に取得しますが、クライアントとサーバーはどちらも任意の順序で読み書きできます。ストリームは完全に独立して動作します。

サーバーを起動する

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

Future<void> main(List<String> args) async {
  final server = grpc.Server.create([RouteGuideService()]);
  await server.serve(port: 8080);
  print('Server listening...');
}

サーバーをビルドして起動するには、

  1. grpc.Server.create() を使用して gRPC サーバーのインスタンスを作成し、サービス実装のリストを渡します。
  2. サーバーの serve() を呼び出してリクエストのリスニングを開始します。オプションでリッスンするアドレスとポートを渡すことができます。サーバーは shutdown() が呼び出されるまで、非同期でリクエストを提供し続けます。

クライアントを作成する

このセクションでは、RouteGuide サービス用の Dart クライアントの作成について説明します。完全なクライアントコードは、grpc-dart/example/route_guide/lib/src/client.dart から入手できます。

スタブを作成する

サービスメソッドを呼び出すには、まずサーバーと通信するための gRPC チャネル を作成する必要があります。これは、サーバーのアドレスとポート番号を ClientChannel() に渡すことによって作成されます。

final channel = ClientChannel('127.0.0.1',
    port: 8080,
    options: const ChannelOptions(
        credentials: ChannelCredentials.insecure()));

必要に応じて、ChannelOptions を使用してチャネルの TLS オプション(例: 信頼された証明書)を設定できます。

gRPC チャネル のセットアップが完了したら、RPC を実行するためのクライアント スタブ が必要です。これは、例の .proto ファイルから生成されたパッケージによって提供される RouteGuideClient をインスタンス化することで取得します。

stub = RouteGuideClient(channel,
    options: CallOptions(timeout: Duration(seconds: 30)));

CallOptions を使用して、サービスが認証情報(例: GCE 認証情報または JWT 認証情報)を必要とする場合に設定できます。RouteGuide サービスでは認証情報は必要ありません。

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

次に、サービスメソッドの呼び出し方法を見てみましょう。gRPC-Dart では、RPC は常に非同期であることに注意してください。つまり、RPC はレスポンスまたはエラーを取得するためにリッスンする必要がある Future または Stream を返します。

シンプルなRPC

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

final point = Point()
  ..latitude = 409146138
  ..longitude = -746188906;
final feature = await stub.getFeature(point));

ご覧のとおり、以前に取得したスタブでメソッドを呼び出します。メソッドパラメータには、リクエストプロトコルバッファオブジェクト(この場合は Point)を渡します。オプションの CallOptions オブジェクトを渡すこともできます。これにより、タイムアウトなど、RPC の動作を変更できます。呼び出しがエラーを返さない場合、返された Future はサーバーからのレスポンス情報で完了します。エラーがある場合、Future はエラーで完了します。

サーバーサイドストリーミングRPC

ここで、サーバーサイドストリーミングメソッド ListFeatures を呼び出します。これは地理的な Feature のストリームを返します。もし サーバーの作成 をすでに読んでいるなら、この一部は非常に慣れているはずです。ストリーミング RPC は、両方のサイドで同様の方法で実装されます。

final rect = Rectangle()...; // initialize a Rectangle

try {
  await for (var feature in stub.listFeatures(rect)) {
    print(feature);
  }
catch (e) {
  print('ERROR: $e');
}

単純な RPC と同様に、メソッドにリクエストを渡します。ただし、Future を受け取る代わりに、Stream を受け取ります。クライアントはストリームを使用してサーバーのレスポンスを読み取ることができます。

返されたストリームで await for を使用して、メッセージがなくなるまでサーバーのレスポンス(この場合は Feature)をレスポンスプロトコルバッファオブジェクトに繰り返し読み取ります。

クライアントサイドストリーミングRPC

クライアントサイドストリーミングメソッド RecordRoute はサーバーサイドメソッドと似ていますが、メソッドに Stream を渡し、Future を受け取る点が異なります。

final random = Random();

// Generate a number of random points
Stream<Point> generateRoute(int count) async* {
  for (int i = 0; i < count; i++) {
    final point = featuresDb[random.nextInt(featuresDb.length)].location;
    yield point;
  }
}

final pointCount = random.nextInt(100) + 2; // Traverse at least two points

final summary = await stub.recordRoute(generateRoute(pointCount));
print('Route summary: $summary');

generateRoute() メソッドは async* なので、gRPC がリクエストストリームをリッスンしてポイントメッセージをサーバーに送信するときにポイントが生成されます。ストリームが完了すると(generateRoute() が返るとき)、gRPC は書き込みを完了し、レスポンスを期待していることを認識します。返された Future は、サーバーから受信した RouteSummary メッセージ、またはエラーで完了します。

双方向ストリーミング RPC

最後に、双方向ストリーミング RPC RouteChat() を見てみましょう。RecordRoute の場合と同様に、メソッドにリクエストメッセージを書き込むストリームを渡し、ListFeatures の場合と同様に、レスポンスメッセージを読み取るために使用できるストリームを受け取ります。ただし、今回はクライアントが 自分たちの メッセージストリームにメッセージを書き込んでいる間に、メソッドのストリーム経由で値を送信します。

Stream<RouteNote> outgoingNotes = ...;

final responses = stub.routeChat(outgoingNotes);
await for (var note in responses) {
  print('Got message ${note.message} at ${note.location.latitude}, ${note
      .location.longitude}');
}

ここでの読み書きの構文は、クライアントサイドおよびサーバーサイドのストリーミングメソッドと非常に似ています。各サイドは相手のメッセージを書き込まれた順序で常に取得しますが、クライアントとサーバーはどちらも任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみましょう!

例のディレクトリから作業します。

cd example/route_guide

パッケージの取得

dart pub get

サーバーを実行する

dart bin/server.dart

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

dart bin/client.dart

問題の報告

Dart gRPC で問題を見つけた場合は、問題トラッカーで 問題を報告 してください。