Java
Android
gRPC

Android+JavaサーバでgRPCを使った通信をためす

はじめに

この記事は、今携わっているProjectでサーバとクライアント間の通信をgRPCを使って行おうということになり、AndroidへgRPCを実装するチュートリアルをやりながら色々と調べたことを書いたものになります。

そもそもgRPCとは?

gRPCって何? の概要でも述べられている通り、Googleが開発した通信プロトコルのことです。 Remote procedure call というクライアントからサーバのメソッドを直接呼ぶことでサーバとクライアントが通信を行え、またその逆もできるという技術の実現のために開発されました。

従来のREST APIではJsonで情報をやりとりすることが主でしたが、サーバとクライアントで別々の実装を行うため、実装コストが大きくなってしまっていました。

しかし、gRPCは.protoファイルにAPI仕様を定義するだけで様々なプログラミング言語の実装を生成することができるのでサーバ、クライアント双方で実装コストの減少に繋がります。

また、Protocol Buffers によってRESTより早い通信が可能になります。1 2

早速チュートリアルを試してみる

環境

  • Mac OS Sierra (10.12.6)
  • JDK 1.8.0_131 (1.7以上なら大丈夫っぽい)
  • Git
  • Android SDK
  • Android端末 (Nexus5 Android 6.0.1)

Hello World

まずは 公式のクイックスタートガイド を見ながらHello 〇〇(任意の入力文字列)を返すアプリを動かすとこまでやってみます。

サンプルのダウンロード

Githubリポジトリからサンプルコードをgit cloneでダウンロードします。

$ # 2017/12/22 最新のバージョン1.8.0をダウンロード
$ git clone -b v1.8.0 https://github.com/grpc/grpc-java
$ # サンプルコードのあるディレクトリへ移動
$ cd grpc-java/examples

gRPCアプリケーションの実行

grpc-java/examples ディレクトリ内で以下を実行してサーバをコンパイルします。

$ ./gradlew installDist

そしてサーバを実行させておきます。

$ ./build/install/examples/bin/hello-world-server

Android端末をデバッグモードで接続しておき、adbで以下を実行しておきます。

adb reverse tcp:8080 tcp:50051

サーバを実行させたターミナルとは別のターミナルを開いて、クライアントをコンパイル、実行します。

$ cd android/helloworld
$ ./gradlew installDebug

成功すれば端末の方にアプリがインストールされ、以下のような画面が表示されるはずです。

Screenshot_20171223-183811.png

Hostに localhost 、Portに 8080 (adbで50051とリバースしたので)を入力し、messageに適当に送信したい文字を入力(例えば world )を入力し、以下のようにレスポンスが帰ってきたらサーバとの通信成功です!

Screenshot_20171223-184944.png

コードについて

とりあえず動きはしましたが、では、実際コードの中身はどうなっているんでしょうか。
次は .proto ファイル、サーバのコード、クライアント(Android)のコードについて見てみます。

helloworld.proto と生成コード

// メソッドの定義
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// リクエストのデータ構造の定義
message HelloRequest {
  string name = 1;
}

// レスポンスのデータ構造の定義
message HelloReply {
  string message = 1;
}

helloworld.proto ファイルには上でも書いたようにAPI仕様を記述します。
具体的にはメソッドと使用するデータのデータ構造を記述しておきます。

詳しい記述方法については Protocol BuffersLanguage Guide (proto3) を参照してください。

この helloworld.proto ファイルを各言語ごとにコンパイルすることで言語に応じたgRPCのインターフェイスが生成されます。

サーバのコード

サーバ側については 今更だけど gRPC を試してみた の記事で書かれているのでここでは割愛します。

クライアント( Android )のコード

helloworld.protoから生成されたコード

helloworld.protoから生成されたコードは gradlew でビルドされ、app/build/generatedディレクトリ内の以下のロケーションに配置されます。

generated/source/proto/debug(/grpc/io.grpc.examples.helloworld & /javalite/io.grpc.examples.helloworld)

サンプルの実装コード

サンプルとして実装されているコードの方は以下のような感じです。(一部省略)

ちなみにOnClickListenerが実装されてないのでどうやってsendMessageメソッド呼んでるのかわからず探し回ってましたが、layoutのxmlから呼んでるようです。

HelloworldActivity.java
public class HelloworldActivity extends AppCompatActivity {

    ...

    public void sendMessage(View view) {
        new GrpcTask().execute();
    }

    private class GrpcTask extends AsyncTask<Void, Void, String> {
        private String mHost;
        private String mMessage;
        private int mPort;
        private ManagedChannel mChannel;

        @Override
        protected void onPreExecute() {
            mHost = mHostEdit.getText().toString();
            mMessage = mMessageEdit.getText().toString();
            String portStr = mPortEdit.getText().toString();
            mPort = TextUtils.isEmpty(portStr) ? 0 : Integer.valueOf(portStr);
            mResultText.setText("");
        }

        @Override
        protected String doInBackground(Void... nothing) {
            try {
                // Channelを作成
                mChannel = ManagedChannelBuilder.forAddress(mHost, mPort)
                    .usePlaintext(true)
                    .build();

                // stubを作成
                GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(mChannel);

                // requestを作成、値を指定する
                HelloRequest message = HelloRequest.newBuilder().setName(mMessage).build();

                // sayHelloメソッドを実行、replyを受け取る
                HelloReply reply = stub.sayHello(message);

                return reply.getMessage();
            } catch (Exception e) {
                ...
            }
        }

        @Override
        protected void onPostExecute(String result) {
            try {
                mChannel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            mResultText.setText(result);
        }
    }
}

通信は非同期処理として実装する必要があるのでAsyncTaskを継承したGrpcTaskクラスを作成しています。

非同期処理を行なっているdoInBackgroundメソッド内を見てみると、

  1. Channelの作成
  2. stubの作成
  3. requestを作成
  4. 実行

という流れになっており、クラスのメソッドを呼び出すのと同じようにsayHelloメソッドにrequestを引数として指定するだけで、通信してreplyが返ってくるという処理を実現できているのがわかると思います。

終わりに

今回初めてgRPCを触ってみた感じとしては、RESTよりかなりシンプルな実装ができるように感じました。
通信部分をメソッドの形で直感的に書けるのはとてもありがたいと思いました。

今後projectの方で実装しながらさらに理解を深めていければと思います。

参考情報