Kotlin で gRPC のチュートリアルをやってみた

REST はまぁ分かる。SOAP はなんとなく分かる。gRPC はまったく分からない。 ってことで gRPC について少し調べてみた。

gRPC について

gRPC は 2015 年に Google によって公開された Remote Procedure Call (RPC) プロトコルである。RPC プロトコルなので location transparency を実現するものである。つまり、ユーザが手続きを呼び出すとき、その手続きがローカルにあるかリモートにあるかを意識する必要がないのが利点である。

REST とは異なり、gRPC はクライアントとサーバ間のストリーム通信等がサポートされている。gRPC はトランスポート層に HTTP/2 を採用しているものの、ユーザはこれを意識する必要がなく、呼び出す手続きや引数・戻り値だけケアすればよいのも利点。

クライアントとサーバのインターフェースとして protocol buffers という Interface Definition Language (IDL) を採用している。このインターフェースはプログラミング言語やプラットフォームに中立的な形をもつため、クライアントとサーバが異なるプログラミング言語で実装されていても容易に通信できる。

protocol buffers で記述したインターフェースの例を以下に示す。

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

SayHello が手続きの名前であり、リクエストとレスポンスの型がそれぞれ HelloRequestHelloResponse である。HelloRequest の型は構造体として定義され、フィールドとして greeting という名前の文字列をもつ。= 1 はフィールドに ID を付与するものであり、通信時のデータのシリアライズのときに使用される。HelloResponse も同様である。

要するに、protocol buffers を用いて、クライアントとサーバが通信するときのルールを定めている。REST のように URL や HTTP メソッドでリソースを指定するのではなく、gRPC では手続き名とリクエストとレスポンスの型を定義している。

具体的な通信を実装するときは、クライアントは、手続きに合うリクエストを作り、レスポンスを処理する必要がある。同様に、サーバは、リクエストを処理し、その結果をレスポンスとして返却する必要がある。このような処理は特定のプログラミング言語を用いてそれぞれ実装する必要がある。そこで protocol buffers で定義されたインターフェースに合うように、特定のプログラミング言語でクライアント・サーバを実装するためのソースコードを自動生成する機能がある。このような protocol buffers から各言語へのトランスパイラやランタイムは Github 上で 配布されている。

Kotlin の Quick Start やってみた

gRPC を用いた通信を実装してみたかったので、もっとも簡単なチュートリアルである Quick Start をやってみた。使用するプログラミング言語ごとにコースがあるのだが、最近気に入っている Kotlin を選択。このコースでは、サーバもクライアントも Kotlin で書かれる。

チュートリアルでは IDE の使用を想定していないが、ソースコードを変更しながらいろいろ実験してみたかったので、Intellij IDEA Community Edition 2020.2 を使用。Gradle 5.2.1。以下、チュートリアルのセクションごとの Intellij を用いるための設定メモ。

Get the example code

git clone したあとに Intellijexamples ディレクトリを開く。自動的に Gradle が走ってビルドしてくれる。けっこう時間かかる。

Run the example

サーバの起動は、Intellij 上で

server/src/main/kotlin/io/grpc/examples/helloworld/HelloWorldServer.kt 

のファイルを開いて main 関数を実行。このときパスの設定が上手く行ってないので、エディタ上ではエラーが表示されるが実行はできる。このエラーはあとで解消する。

クライアントの起動は、以下のファイル中の main 関数を実行。

client/src/main/kotlin/io/grpc/examples/helloworld/HelloWorldClient.kt 

クライアントが起動した直後、Received: Hello world というメッセージを出力して終了することを確認。

Update the gRPC service

以下の protocol buffers を定義したファイルをチュートリアルの通りに書き換えて保存。

protos/src/main/proto/io/grpc/examples/helloworld/hello_world.proto 

Update the app

更新した hello_world.proto から gRPC のためのサーバ・クライアント用の Kotlin ファイルを自動生成する。まず、stub/build.gradle.kts に以下のように1行追加することで自動生成されるファイルの場所を変更する。

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcVersion"]}"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:${rootProject.ext["grpcKotlinVersion"]}:jdk7@jar"
        }
    }
    generatedFilesBaseDir = "$projectDir/src" // added
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
        }
    }
}

Intellij の Gradle ウィンドウから stub 配下の build を実行すると、stub/main 以下にいくつかのソースコードが自動生成される。

このとき、stub/main ディレクトリ直下に grpcgrpckt ディレクトリが生成されるが、これらがプロジェクトのソースコードとして認識されていないので認識させる。具体的には、この2つのディレクトリそれぞれを Intellijエクスプローラー上で右クリックし、"Mark Directory as Source Root" をクリック。これでエディタ上で

server/src/main/kotlin/io/grpc/examples/helloworld/HelloWorldServer.kt 

などを開いたときに出ていたエラーが消える。

あとは Update the server や Update the client の通りにソースコードを修正。自動生成されたスタブ(クライアント側)やサービス(サーバ側)がプロジェクトによって正しく認識されているので、エディタ上での自動補完なども効くはず。

Run the updated app

Run the example で述べた手順と同様にサーバとクライアントを起動すればよい。 自分で手を加えた修正が実行結果に反映されたことを確認したら本チュートリアルは終わり。

おまけ:サーバを Kotlin、クライアントを Go で通信

同じ Quick Start のチュートリアルGo のコース をやると、Go 言語で書かれたクライアントが作成できる。ポート番号を合わせて、Kotlin のチュートリアルで作成したサーバと通信してみた。

$ go run greeter_client/main.go Alice
2020/10/22 01:35:42 Greeting: Hello Alice
2020/10/22 01:35:42 Greeting: Hello again Alice

確かに通信できている。これが protocol buffers を用いたプログラミング言語をまたぐ gRPC 通信の例。

参考リンク