概要
前回の
gRPCにおける各RPC方式の実装方法【Server streaming RPC】 - Carpe Diem
に引き続き、今回はClient streaming RPCの実装方法を紹介します。
こちらは大きなリクエストを分割して送りたい時に有用です。
環境
- golang 1.9.2
- grpc 1.7.2
- protobuf 3.4.0
成果物
最終的に出来たコードはこちら
実装
画像をアップロードしてサーバ側で保存するサービスを作ります。
proto
ポイントはリクエストの方にstream
を付けることです。
syntax = "proto3"; package upload; service Uploader { rpc Upload(stream Chunk) returns (UploadResponse) {} } message Chunk { bytes data = 1; } message UploadResponse { string status = 1; }
サーバ
上のprotoをコンパイルすると
type UploaderServer interface { Upload(Uploader_UploadServer) error }
というコードが生成されるので、これを実装します。
package main import ( "io" "log" "net" "os" pb "github.com/jun06t/grpc-sample/client-streaming/proto" "google.golang.org/grpc" ) const ( port = ":8080" ) type server struct{} func (s *server) Upload(stream pb.Uploader_UploadServer) error { file, err := os.Create("supercar.jpg") if err != nil { return err } defer file.Close() for { resp, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } file.Write(resp.Data) } err = stream.SendAndClose(&pb.UploadResponse{"success"}) if err != nil { return err } return nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatal(err) } s := grpc.NewServer() pb.RegisterUploaderServer(s, new(server)) err = s.Serve(lis) if err != nil { log.Fatal(err) } }
ポイント
stream.Recv()
でデータを受け取るerr == io.EOF
でリクエストの終了を検知stream.SendAndClose()
でレスポンスを送ってRPCを終了させる
クライアント
package main import ( "io" "log" "os" pb "github.com/jun06t/grpc-sample/client-streaming/proto" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( address = "localhost:8080" ) func main() { conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatal(err) } defer conn.Close() client := pb.NewUploaderClient(conn) stream, err := client.Upload(context.Background()) if err != nil { log.Fatal(err) } err = upload(stream) if err != nil { log.Fatal(err) } } func upload(stream pb.Uploader_UploadClient) error { file, err := os.Open("supercar.jpg") if err != nil { return err } defer file.Close() buf := make([]byte, 1024) for { _, err := file.Read(buf) if err == io.EOF { break } if err != nil { return err } stream.Send(&pb.Chunk{Data: buf}) } resp, err := stream.CloseAndRecv() if err != nil { return err } log.Println(resp.Status) return nil }
ポイント
- fileを
buf
に分割して送信している stream.Send()
でデータを複数回送るstream.CloseAndRecv()
でリクエストの完了を通知し、レスポンスを受け取る
動作確認
サーバの起動
$ go run server/main.go
クライアントの実行
$ go run main.go 2017/11/13 00:46:16 success
サーバ側のディレクトリを見てみると、supercar.jpg
というファイルが保存されています。
$ md5 supercar.jpg MD5 (supercar.jpg) = fb7a18198978208362486a1ca31c0cd4
ハッシュ値もクライアントと一致しています。
まとめ
Protocol Buffersは大きなデータを送信するよう設計されていないので、画像のような大きなデータはこのように分割してStreamで扱うのが良いです。