gRPC で遊んでみた話
仕事で gRPC を本格的に使いそうかつ、昔から興味があったものの実際に手を動かしたことがなかったので、使ってみた。
基本的には 公式チュートリアル に合わせてやった。
gRPC とは
RPC を実現するために Google が開発した通信プロトコルの一つ。サーバー間での高速な通信を実現できるから、マイクロサービスとかで注目されている。
詳しくは gRPCって何? によくまとめられていたから、忘れた時には確認したい。
上記のところでまとめられていない内容として、通信の定義がある。
Simple RPC
クライアントから一つのリクエストがサーバー側へ送信され、一つのレスポンスを待つ状態。いわゆる普通の通信。
Server-Side Streaming RPC
クライアントから一つのリクエストがサーバー側へ送信され、サーバーとしてはメッセージをシーケンスとして返すことができる。クライアントは、レスポンスをすべてのメッセージがなくなるまで読み取る。
Client-Side Streaming RPC
サーバーの逆クライアントバージョン。クライアントはメッセージの送信を終えると、サーバー側が全てを読み取りレスポンスを返すまで待ち状態になる。
Bidirectional Streaming RPC
bidirectional なので、双方向にストリーミングを可能としたもの。上記2つの組み合わせ的な感じ。read stream と write stream は独立で操作されるため、好きな順番で操作可能となっている。例えば、サーバーはレスポンスを書く前にすべてのメッセージを受け取ってもいいし、メッセージを受け取りながらレスポンスを変えしてもいいというもの。
実際に実装してみた
環境
用意する環境としては、ローカルに作るのもなんか嫌だったから以下の docker-compose 内で走らせることにした。
# Dockerfile FROM golang:1.13-stretch SHELL ["/bin/bash", "-c"] RUN apt update && apt-get install -y vim unzip WORKDIR /protoc RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.0-rc2/protoc-3.12.0-rc-2-linux-x86_64.zip RUN unzip protoc-3.12.0-rc-2-linux-x86_64.zip RUN ln -s /protoc/bin/protoc /bin/protoc WORKDIR /go-grpc ENV GO111MODULE on RUN go get -u github.com/golang/protobuf/protoc-gen-go
# docker-compose version: "3.7" services: go-grpc: build: context: ./ dockerfile: Dockerfile container_name: "go-grpc" volumes: - ./:/go-grpc tty: true privileged: true
これ以降、基本的な操作は protocol buffer を書く -> コンパイルする -> 実際に動かしてみるとなる。コマンドは以下の通り。
## compile protoc --proto_path ./proto/hoge --go_out=plugins=grpc:./pb/fuga file_name.proto
動かしたもの
とりあえず simple rpc さえちゃんと理解できればあとは遊んでみるだけだから、simple rpc だけ備忘として書いておく。
// unary.proto syntax = "proto3"; package unary; // サービス名を定義し、リクエストタイプとレスポンスタイプを定義してあげる。 // この例では、リクエストが point でレスポンスが feature となる。 service RouteGuide { rpc GetFeature(Point) returns (Feature) {} } // リクエストの定義 message Point { int32 latitude = 1; int32 longitude = 2; } // レスポンスの定義 message Feature { string result = 1; }
これをコンパイルすると、unary.pb.go
ができるけど、ここの内容は割愛。
次に、サーバー側とクライアント側の準備をしてあげる。
// ./server/main.go package main import ( "context" "fmt" "log" "net" pb "go-grpc/pb/unary" "github.com/pkg/errors" "google.golang.org/grpc" ) const port = ":50051" // ここで使用するサーバーの定義みたいな構造体を作る。 type routeGuideServer struct { pb.UnimplementedRouteGuideServer } // 実際に、リクエストを取得して、レスポンスを返してあげるメソッド。 func (s *routeGuideServer) GetFeature(ctx context.Context, in *pb.Point) (*pb.Feature, error) { // リクエストを取得 a := in.GetLatitude() b := in.GetLongitude() log.Printf("get %v and %v from client\n", a, b) // 今回は特に何かの処理をするわけではなく、ちゃんと受け取ったよと結果を作ってあげただけ。 reply := fmt.Sprintf("return recieved two value: %v %v", a, b) return &pb.Feature{ Result: reply, }, nil } // サーバーを構築する func setServer() error { lis, err := net.Listen("tcp", port) if err != nil { return errors.Wrap(err, "failed to build server") } s := grpc.NewServer() var server routeGuideServer pb.RegisterRouteGuideServer(s, &server) if err := s.Serve(lis); err != nil { return errors.Wrap(err, "failed to build server") } return nil } func main() { fmt.Println("server start...") if err := setServer(); err != nil { log.Fatalf("%v", err) } }
// ./client/main.go package main import ( "context" "log" "time" "github.com/pkg/errors" pb "go-grpc/pb/unary" "google.golang.org/grpc" ) // 送信したいリクエストを作るところ func request(client pb.RouteGuideClient, a int32, b int32) error { ctx, cancel := context.WithTimeout( context.Background(), time.Second, ) defer cancel() testRequest := pb.Point{ Latitude: a, Longitude: b, } reply, err := client.GetFeature(ctx, &testRequest) if err != nil { return errors.Wrap(err, "failed to get response") } log.Printf("get response\n %s", reply.GetResult()) return nil } // クライアントのコネクションを構築してリクエストを最終的に送信しているところ。 func setClient(a int32, b int32) error { address := "localhost:50051" conn, err := grpc.Dial( address, grpc.WithInsecure(), grpc.WithBlock(), ) if err != nil { return errors.Wrap(err, "failed to connect server") } defer conn.Close() client := pb.NewRouteGuideClient(conn) return request(client, a, b) } func main() { a := int32(10) b := int32(20) if err := setClient(a, b); err != nil { log.Fatalf("%v", err) } }
とりあえず、ここまでは理解できたから、続いて他の通信方法についても遊んでみたいと思う。
ちなみに、実際の実行結果は以下の感じ
root@f5cd980e824f:/go-grpc# go run server/unary/main.go server start... 2020/05/16 03:59:16 get 10 and 20 from client root@f5cd980e824f:/go-grpc# go run client/unary/main.go 2020/05/16 03:59:16 get response return recieved two value: 10 20