最近携わっているプロジェクトでgRPCを使った通信を行っているのですが、マイクロサービスを作る上で非常に使い勝手がいいので、使い方含めて紹介しようと思います。
gRPCとは
HTTP/2を標準でサポートしたRPCフレームワークで、2015年にGoogleが発表しました。
デフォルトで対応しているProtocolBufferをgRPC用に書いた上で、サポートしている言語に書き出しを行うと、異なる言語のサーバー間でもある程度型堅牢に通信を行うことができます。
GoogleではプロトコルをProtoclBufferで書いているそうです。
このようにサーバーをC++, クライアントをRubyやAndroidJavaといった形で通信できます。
携わっているプロジェクトでは、ScalaからGoのプロジェクトに対してgRPCのリクエストを送り、レスポンスを取得したかったので、Scalaをクライアント、Goをサーバーとして実装しました。
ProtocolBuffer
こちらは構造化されたデータ・フォーマットであり、構造化の仕方はGolangに似ています。
gRPCではmessageという構造体と、serviceというRPC部分の実装を記述します。
Proto3(version3)からはJSONフォーマットへのデコードもサポートするようになったので、もしgRPCではなく通常のREST Apiで送信したい場合はそのようにフォーマットするといいかと思います。
実装手順
実装手順は簡単にいうと下記の通りになります。
- Protocol bufferから各言語への書き出し実行ファイルをインストールする
- Protocol Bufferの定義ファイルを作成する
- 各言語ごとに書き出しを行う
- サーバーサイド、クライアントサイドで実際の処理を書く
- サーバーサイドを実行する
- クライアントサイドからRPCの関数を実行する
今回はgrpc-gachaという、以前個人的に作成したプロジェクトで作成してみようと思います。
https://github.com/kotamat/grpc-gacha
cardという構造体を配列にして引数に渡すと、ランダムで一つのcardを返すという非常に簡単な処理です。
書き出し実行ファイルのインストール
今回はScala用とGolang用が必要なので、それぞれインストールします。
共通のランタイムprotoc
のインストール
protobufのGithubからprotoc-<version>-<OS name>.zip
をダウンロードし、解凍します。
その中にbinディレクトリがあるので、実行可能なディレクトリにbin内のファイルをコピーします。
Golang用の書き出しプラグインのインストール
下記のコマンドを実行し、各種プラグインをインストールします。実行すると$GOPATH/bin
に実行ファイルがインストールされるので、PATHを通しておいてください。
1
2
| go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
go get -u google.golang.org/grpc
|
Scala用の書き出しプラグインのインストール
公式ではScalaをサポートしていないのでJava gRPCを使います。
ただ、ScalaPBというProtocolBuffer対応のOSSが、gRPCにも対応しているので、こちらのjarファイルのみsbtプロジェクト無いに入れてあげればいいです。
1
2
3
| addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.3")
libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.5.47"
|
1
2
3
4
5
6
7
| PB.targets in Compile := Seq(
scalapb.gen() -> (sourceManaged in Compile).value
)
// If you need scalapb/scalapb.proto or anything from
// google/protobuf/*.proto
libraryDependencies += "com.trueaccord.scalapb" %% "scalapb-runtime" % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf"
|
ただ、こちら、NettyServerを使っているのですが、バージョン的にPlay2.5とは共存できないので、もしPlayFrameworkで使いたい場合は2.4に下げるか別プロジェクトにしてあげる必要があります。
ProtocolBufferの定義ファイルを作成する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| syntax = "proto3";
package gacha;
service Gacha{
rpc Lottery (Request) returns (Response) {}
}
message Card {
string name = 1;
}
message Request {
repeated Card cards = 1;
}
message Response {
Card card = 1;
int32 ret_code = 2;
}
|
- RequestにCardを複数指定
- Lottery関数を実行
- Responseを返却、そこにはcardという変数と、ret_codeという変数が存在する
という感じです。
各言語ごとに書き出しを行う
Scalaの場合
client/scalaのディレクトリでsbt compile
と実行するだけです。
生成されたscalaファイルはtarget/scala-2.11/src_managed/mainの中に生成されます。(scala 2.11を使っている場合)
IntelliJ IDEAで使用する場合はこのディレクトリを読み取り可能になるように指定してあげてください。
Goの場合
proto
ディレクトリで下記コマンドを実行します
1
| $ protoc --go_out=plugins=grpc:../lib/gacha *.proto
|
go_outで指定したディレクトリにpb.goファイルを書き出します。
実際の処理を書く
基本的な考え方は通常のRPCと一緒かと思います。
サーバーサイドの場合
Golangで書く場合は、structにRPCの関数を生やし、生成されたパッケージの中にあるRegisterXXXServerという関数の第二引数にポインターつきで指定します。
実際の通信は、grpc.NewServerで生成された構造体を使いまわします。
今回はpb.Requestの中に
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| const (
port = ":50055"
)
type server struct{}
func (s *server) Lottery(ctx context.Context, in *pb.Request) (*pb.Response, error) {
if len(in.Cards) < 1 {
return &pb.Response{Card: nil, RetCode: 0}, errors.New("empty cards")
}
rand.Seed(time.Now().UnixNano())
chosenKey := rand.Intn(len(in.Cards))
return &pb.Response{Card: in.Cards[chosenKey], RetCode: 1}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGachaServer(s, &server{})
s.Serve(lis)
}
|
Scalaなどのオブジェクト指向の言語は、生成されたパッケージの中にあるクラスをnewして同様に使いまわします。
クライアントサイドの場合
クライアントからは、サーバーのエンドポイントを指定したオブジェクトを作成し、そのオブジェクトを元にStubを作成し、stubからRPCの関数を呼び出すという形になります。
Scalaだと下記のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| package com.github.kotamat.grpc_gacha
import gacha._
import io.grpc.ManagedChannelBuilder
class Client {
def main(args: Array[String]): Unit = {
val host = "localhost"
val port = "50055"
// チャンネルの作成(BuilderはNetty等も使用可能)
val channel = ManagedChannelBuilder.forAddress(host,port).usePlaintext(true).build()
// 同期的に取得するコネクションの作成
val connection =gacha.GachaGrpc.blockingStub(channel)
// 引数の定義
val requestSpec = gacha.Request(Array(
gacha.Card("card1"),
gacha.Card("card2")
))
// RPCの結果を取得
val response = connection.lottery(requestSpec)
printf("gain card: %s",response.card.map(_.name).getOrElse(""))
// Output: card1
}
}
|
Rubyだとこんな感じ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| def main
# stubの生成(チャンネルの生成も同時に行う)
stub = Gacha::Gacha::Stub.new('localhost:50055', :this_channel_is_insecure)
# 引数の定義
cards = [
Gacha::Card.new(name: "card1"),
Gacha::Card.new(name: "card2"),
]
begin
res = stub.lottery(Gacha::Request.new(cards: cards))
rescue => e
p e
end
if res.ret_code == 1
p "gained: #{res.card.name}"
# Output: card1
else
p "error"
end
end
main
|
まとめ
いろんな言語間での高速で堅牢な通信をgRPCを使えば簡単に実現してくれます。
開発中の結合テストで消耗するのはもうやめよう!
Author
kotamat
LastMod
2019-11-04
(2c6b62d)