Open7

gRPC の Basics Tutorial を Go でやる

湯豆腐湯豆腐

gRPC の service method には4種類ある

  • simple RPC
    • 通常の関数呼び出しのように、クライアントからの1リクエストに対して1レスポンスが返ってくる形式
  • server-side streaming RPC
    • クライアントからのリクエストに対して、サーバーからストリームを返す形式
    • ストリームから複数のメッセージを受け取れる
    • .proto ファイルの rpc 定義で、レスポンスの型の前に stream キーワードをつけるとこれになる
  • client-side streaming RPC
    • クライアントがストリームに複数のメッセージを書き込み、サーバーがそれを受け取る形式
    • クライアント側からの書き込みが完了したら、サーバーがすべてを読み取りレスポンスを返すのを待つ
    • .proto ファイルの rpc 定義で、リクエストの型の前に stream キーワードをつけるとこれになる
  • bidirectional streaming RPC
    • クライアント、サーバーが相互に、read-write ストリームを使ってメッセージを送り合う形式
    • それぞれのストリームは独立しているので、クライアントとサーバーは好きな順序で読み書きが可能
      • サーバーはクライアントからのメッセージを全部待ってからクライアントにメッセージを送ってもよいし、メッセージを受け取ったら即返してもよいし、クライアントからのメッセージが来なくてもメッセージを送っても良い
    • それぞれのストリーム内でのメッセージの順番は保存される
    • .proto ファイルの rpc 定義で、レスポンスとリクエスト両方の型の前に stream キーワードをつけるとこれになる
湯豆腐湯豆腐

.proto ファイルから Go のコードを生成するには protoc を使う

$ protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    routeguide/route_guide.proto

これを実行すると route_guide.pb.goroute_guide_grpc.pb.go というファイルが生成される。
route_guide.pb.go には message を実装する struct が記述されている。
route_guide_grpc.pb.go には rpc で定義したメソッドのインタフェースと実装が記述されている。

protoc の引数はプラグインに対する設定のよう。
Go にコンパイルするプラグイン proto-gen-go の設定が --go_out--go_opt
Go の gRCP プラグイン proto-gen-go-grpc の設定が --go-grpc_out--go-grpc_opt
https://christina04.hatenablog.com/entry/protoc-usage

湯豆腐湯豆腐

生成されたサーバーの interface を実装して、サーバーのコードを書く。

simple RPC の関数は、context.Context と protocol buffer で定義された型の引数を受け取り、同様に protocol buffer で定義された型の戻り値と error を返すだけ。
ほぼほぼ普通の関数。

server-side streaming RPC の関数は protocol buffer で定義された型の引数と、クライアントへの送信を行うストリームを受け取り、error だけを返す関数。
.proto ファイルの定義だとレスポンスがストリームになっていたので若干混乱する。
このストリームの Send() メソッドを呼ぶことで、クライアントにメッセージを送信できる。
すべてのメッセージの送信が正常に完了したら nil を返せばOK。
エラーを返すと適切な gRPC のステータスに変換してくれる。

client-side streaming RPC の関数はクライアントからの/へのメッセージを読み書きするストリームのみを受け取り、error だけを返す関数。
このストリームの Recv() メソッドを呼ぶことで、クライアントからのメッセージを受信できる。
SendAndClose() メソッドを呼ぶと、クライアントにレスポンスを送信できる。
Recv()io.EOF が返ってきたときはクライアントからの書き込みが完了しそれ以上送られてくることはないので、SendAndClose() でレスポンスを返す。

bidirectional streaming RPC の関数は client-side streaming と同様にクライアントからの/へのメッセージを読み書きするストリームのみを受け取り、error だけを返す関数。
client-side streaming とほぼ同様だが、クライアントへの書き込みが Send() メソッドとなり、複数回実行できる。

湯豆腐湯豆腐

gRPC サーバーを立てるには

  1. net.Listen() でリッスンする
  2. grpc.NewServer() でサーバーインスタンスを作成する
    a. TLS を有効化する際はこの引数にオプションとして credentials.NewServerTLSFromFile() で作成した証明書の cred を渡す
  3. RegisterXXXServer()(XXX はサービスの名前)で実装したサーバーを登録する
  4. サーバーインスタンスの Serve() を呼び出して開始する
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)
湯豆腐湯豆腐

クライアントはまず grpc.Dial() を使ってサーバーに接続し、gRPC channel を作る。

var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
  ...
}
defer conn.Close()

認証等が必要な場合は grpc.DialOption に設定する。

次に作成した gRPC channel を NewXXXClient()(XXX はサービスの名前)メソッドに渡し、クライアントを作る。

client := pb.NewRouteGuideClient(conn)

Go の gRPC 呼び出しはすべて同期的で、レスポンスが返ってくるまでブロックする。
また、context.Context を引数として渡せて、タイムアウトやキャンセルなどができる。

simple RPC の呼び出しは普通の関数呼び出しと同じ。

server-side streaming RPC の関数を呼び出すと、サーバーからのメッセージを読むためのストリームが返ってくる。
Recv() メソッドでメッセージを取得でき、io.EOF が返ってきたら終了。

client-side streaming RPC の関数を呼び出すと、サーバーへのメッセージ書き込みと読み取りのためのストリームが返ってくる。
Send() メソッドでメッセージを送信できる。
全部送信が終わったら CloseAndRecv() メソッドを呼び出して完了通知をし、サーバーからのレスポンスを受け取る。

bidirectional streaming RPC の関数も client-side とほぼ同じでサーバーへのメッセージ書き込みと読み取りのためのストリームが返ってくる。
クライアントからの送信が完了したら CloseSend() を呼び出す。
サーバー側のストリームとは独立しているので、書き込みと同時に読み込みも可能。
サンプルでは goroutine を使って同時に行えるようにしていた。