基本チュートリアル

gRPC for Android Java の基本的なチュートリアル入門。

基本チュートリアル

gRPC for Android Java の基本的なチュートリアル入門。

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

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

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

gRPC の 概要 を読み、 protocol buffers に精通していることを前提とします。このガイドでは、サーバー側のことについては一切触れません。詳細については、 Java のページ を参照してください。

gRPCを使用する理由

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

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

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

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

git clone -b v1.73.0 https://github.com/grpc/grpc-java.git

次に、現在のディレクトリを grpc-java/examples/android に変更します。

cd grpc-java/examples/android

クライアントインターフェイスコードを生成するための関連ツールもインストールされている必要があります。まだインストールしていない場合は、 grpc-java README のセットアップ手順に従ってください。

サービスを定義する

最初のステップは( gRPC の概要 で説明したように)、 protocol buffers を使用して gRPC サービス とメソッドの リクエスト および レスポンス の型を定義することです。完全な .proto ファイルは、 routeguide/app/src/main/proto/route_guide.proto で確認できます。

この例では Java コードを生成するため、 .proto ファイルに java_package ファイルオプションを指定しました。

option java_package = "io.grpc.examples";

これは、生成される Java クラスに使用したいパッケージを指定します。 .proto ファイルに明示的な java_package オプションが指定されていない場合、デフォルトで proto パッケージ(「package」キーワードを使用して指定)が使用されます。ただし、 proto パッケージは一般的に Java パッケージとしては適切ではありません。これは、 proto パッケージは逆ドメイン名で始まることが期待されていないためです。この .proto から別の言語でコードを生成する場合、 java_package オプションは影響しません。

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

service RouteGuide {
   ...
}

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

  • クライアントがスタブを使用してサーバーにリクエストを送信し、通常の関数呼び出しのようにレスポンスが返ってくるのを待つ*シンプルな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 Java プラグインを備えた protocol buffer コンパイラ protoc を使用して行います。 gRPC サービスを生成するには、 proto3 コンパイラ( proto2 および proto3 の構文の両方をサポート)を使用する必要があります。

この例のビルドシステムも Java-gRPC ビルドの一部です。独自の .proto ファイルからコードを生成する方法については、 grpc-java README および build.gradle を参照してください。 Android では、モバイルユースケースに最適化された protobuf lite を使用します。

次のクラスがサービス定義から生成されます。

  • Feature.javaPoint.javaRectangle.java など。これらには、リクエストおよびレスポンスメッセージタイプをポピュレート、シリアライズ、および取得するためのすべての protocol buffer コードが含まれています。
  • RouteGuideGrpc.java。これには(その他の便利なコードとともに)次のものが含まれています。
    • RouteGuide サーバーが実装するための基底クラス、 RouteGuideGrpc.RouteGuideImplBase。これには、 RouteGuide サービスで定義されたすべてのメソッドが含まれています。
    • クライアントが RouteGuide サーバーと通信するために使用できる スタブ クラス。

クライアントを作成する

このセクションでは、 RouteGuide サービス用の Java クライアントの作成について説明します。完全なサンプルクライアントコードは、 routeguide/app/src/main/java/io/grpc/routeguideexample/RouteGuideActivity.java で確認できます。

スタブを作成する

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

  • ブロッキング/同期 スタブ:これは、 RPC 呼び出しがサーバーからの応答を待機し、応答を返すか例外を発生させることを意味します。
  • 非ブロッキング/非同期 スタブ:これは、サーバーへの非ブロッキング呼び出しを行い、応答は非同期で返されます。非同期スタブのみを使用して、特定の種類のストリーミング呼び出しを行うことができます。

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

mChannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext(true).build();

これで、チャネルを使用して、 .proto から生成された RouteGuideGrpc クラスの newStub および newBlockingStub メソッドを使用してスタブを作成できます。

blockingStub = RouteGuideGrpc.newBlockingStub(mChannel);
asyncStub = RouteGuideGrpc.newStub(mChannel);

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

それでは、サービスメソッドの呼び出し方法を見てみましょう。

シンプルなRPC

ブロッキングスタブの単純な RPC GetFeature を呼び出すことは、ローカルメソッドを呼び出すのと同じくらい簡単です。

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();
Feature feature = blockingStub.getFeature(request);

リクエストプロトコルバッファオブジェクト(この場合は Point)を作成してポピュレートし、ブロッキングスタブの getFeature() メソッドに渡して、 Feature を取得します。

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

次に、地理的な Feature のストリームを返すサーバーサイドストリーミング呼び出し ListFeatures を見てみましょう。

Rectangle request =
    Rectangle.newBuilder()
        .setLo(Point.newBuilder().setLatitude(lowLat).setLongitude(lowLon).build())
        .setHi(Point.newBuilder().setLatitude(hiLat).setLongitude(hiLon).build()).build();
Iterator<Feature> features = blockingStub.listFeatures(request);

ご覧のとおり、単純な RPC と非常に似ていますが、単一の Feature を返す代わりに、クライアントが返されたすべての Feature を読み取るために使用できる Iterator を返します。

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

次は、少し複雑なものに進みます。クライアントサイドストリーミングメソッド RecordRoute です。このメソッドでは、 Point のストリームをサーバーに送信し、単一の RouteSummary を取得します。このメソッドには非同期スタブを使用する必要があります。 サーバーの作成 をすでに読んでいる場合は、これが非常に馴染みのあるものに思えるかもしれません。非同期ストリーミング RPC は、両側で同様の方法で実装されます。

private String recordRoute(List<Point> points, int numPoints, RouteGuideStub asyncStub)
        throws InterruptedException, RuntimeException {
    final StringBuffer logs = new StringBuffer();
    appendLogs(logs, "*** RecordRoute");

    final CountDownLatch finishLatch = new CountDownLatch(1);
    StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {
        @Override
        public void onNext(RouteSummary summary) {
            appendLogs(logs, "Finished trip with {0} points. Passed {1} features. "
                    + "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
                    summary.getFeatureCount(), summary.getDistance(),
                    summary.getElapsedTime());
        }

        @Override
        public void onError(Throwable t) {
            failed = t;
            finishLatch.countDown();
        }

        @Override
        public void onCompleted() {
            appendLogs(logs, "Finished RecordRoute");
            finishLatch.countDown();
        }
    };

    StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
    try {
        // Send numPoints points randomly selected from the points list.
        Random rand = new Random();
        for (int i = 0; i < numPoints; ++i) {
            int index = rand.nextInt(points.size());
            Point point = points.get(index);
            appendLogs(logs, "Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
                    RouteGuideUtil.getLongitude(point));
            requestObserver.onNext(point);
            // Sleep for a bit before sending the next one.
            Thread.sleep(rand.nextInt(1000) + 500);
            if (finishLatch.getCount() == 0) {
                // RPC completed or errored before we finished sending.
                // Sending further requests won't error, but they will just be thrown away.
                break;
            }
        }
    } catch (RuntimeException e) {
        // Cancel RPC
        requestObserver.onError(e);
        throw e;
    }
    // Mark the end of requests
    requestObserver.onCompleted();

    // Receiving happens asynchronously
    if (!finishLatch.await(1, TimeUnit.MINUTES)) {
        throw new RuntimeException(
               "Could not finish rpc within 1 minute, the server is likely down");
    }

    if (failed != null) {
        throw new RuntimeException(failed);
    }
    return logs.toString();
}

ご覧のとおり、このメソッドを呼び出すには StreamObserver を作成する必要があります。これは、サーバーが RouteSummary レスポンスで呼び出すための特別なインターフェイスを実装しています。 StreamObserver では、

  • サーバーがメッセージストリームに RouteSummary を書き込んだときに返された情報を表示するために、 onNext() メソッドをオーバーライドします。
  • サーバー がその側で呼び出しを完了したときに呼び出される onCompleted() メソッドをオーバーライドして、サーバーが書き込みを終了したかどうかを確認できる SettableFuture を設定します。

次に、 StreamObserver を非同期スタブの recordRoute() メソッドに渡して、 Point を書き込むための独自の StreamObserver リクエストオブザーバーを取得し、サーバーに送信します。ポイントの書き込みが完了したら、リクエストオブザーバーの onCompleted() メソッドを使用して、クライアント側での書き込みが完了したことを gRPC に伝えます。完了したら、 SettableFuture をチェックして、サーバーがその側で完了したことを確認します。

双方向ストリーミング RPC

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

private String routeChat(RouteGuideStub asyncStub) throws InterruptedException,
        RuntimeException {
    final StringBuffer logs = new StringBuffer();
    appendLogs(logs, "*** RouteChat");
    final CountDownLatch finishLatch = new CountDownLatch(1);
    StreamObserver<RouteNote> requestObserver =
            asyncStub.routeChat(new StreamObserver<RouteNote>() {
                @Override
                public void onNext(RouteNote note) {
                    appendLogs(logs, "Got message \"{0}\" at {1}, {2}", note.getMessage(),
                            note.getLocation().getLatitude(),
                            note.getLocation().getLongitude());
                }

                @Override
                public void onError(Throwable t) {
                    failed = t;
                    finishLatch.countDown();
                }

                @Override
                public void onCompleted() {
                    appendLogs(logs,"Finished RouteChat");
                    finishLatch.countDown();
                }
            });

    try {
        RouteNote[] requests =
                {newNote("First message", 0, 0), newNote("Second message", 0, 1),
                        newNote("Third message", 1, 0), newNote("Fourth message", 1, 1)};

        for (RouteNote request : requests) {
            appendLogs(logs, "Sending message \"{0}\" at {1}, {2}", request.getMessage(),
                    request.getLocation().getLatitude(),
                    request.getLocation().getLongitude());
            requestObserver.onNext(request);
        }
    } catch (RuntimeException e) {
        // Cancel RPC
        requestObserver.onError(e);
        throw e;
    }
    // Mark the end of requests
    requestObserver.onCompleted();

    // Receiving happens asynchronously
    if (!finishLatch.await(1, TimeUnit.MINUTES)) {
        throw new RuntimeException(
                "Could not finish rpc within 1 minute, the server is likely down");
    }

    if (failed != null) {
        throw new RuntimeException(failed);
    }

    return logs.toString();
}

クライアントサイドストリーミング例と同様に、 StreamObserver レスポンスオブザーバーを取得し、返しますが、今回はメソッドのレスポンスオブザーバーを介して値を送信します。これは、サーバーがまだ それらの メッセージストリームにメッセージを書き込んでいる間です。ここで読み取りと書き込みの構文は、クライアントストリーミングメソッドとまったく同じです。各側は常に、書き込まれた順序で相手のメッセージを取得しますが、クライアントとサーバーは任意の順序で読み書きできます。ストリームは完全に独立して動作します。

試してみましょう!

クライアントとサーバーをビルドして実行するには、 example directory README の手順に従ってください。