v2とは???
以下の記事にまとまっています。
Protocol BufferのGo言語用APIの新しい実装です。v1.20から始まるという奇怪なバージョニングになっています。しかも、後方互換性のない変更でありAPIv2とまで呼ばれているのに、モジュールの方はメジャー番号が1というカオスな状況です*1。
2020年3月に出たばかりであり、まだまだ情報が少ないです。特に上の記事にもあるように新しいprotoc-gen-goはgRPC用のスタブを生成しなくなっており、そもそもこの新しいモジュールを使ってHello worldするのに試行錯誤したため、ここにメモを残しておきます。
ちなみに色々調べた後gRPCの公式ドキュメントを見たら普通に書いていました。最初から一次情報に当たれ、という話でした……
環境
- macOS Catalina 10.15.6
- Go 1.15.2
- protoc v3.13.0
- google.golang.org/protobuf/cmd/protoc-gen-go v1.23.0
- google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.1
Hello World
プロジェクトの準備
適当にgo mod initしてプロジェクトを作ります。
$ mkdir go-protobuf-v2-sample && cd go-protobuf-v2-sample $ go mod init github.com/pddg/go-protobuf-v2-sample
ツール類をインストールします。protocのインストール方法は何でもよいです。GitHub Releasesからダウンロードして展開するのが楽でしょう。
$ PROTOC_VERSION=3.13.0 $ PROTOC_PKG=protoc-${PROTOC_VERSION}-osx-x86_64.zip $ wget -q -O tmp/ https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_PKG} $ unzip -q -o tmp/${PROTOC_PKG} $ ./bin/protoc --version libprotoc 3.13.0
protocのプラグインをインストールします。僕はGoのプロジェクトでGo製のCLIツールが必要な時はgexを使っています。
$ gex --add google.golang.org/protobuf/cmd/protoc-gen-go $ gex --add google.golang.org/grpc/cmd/protoc-gen-go-grpc
サービスを定義する
proto/hello.proto
に名前を与えると Hello ○○
と返してくれるだけのサービスを定義します。
syntax = "proto3"; option go_package = "github.com/pddg/go-protobuf-v2-sample/hello/pb"; message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } service HelloService { rpc Hello(HelloRequest) returns(HelloResponse); }
gRPC用のコードを自動生成する
v1まではgithub.com/golang/protobuf/protoc-gen-goを使い、 --go_out=plugins=grpc:パス
といった形式で指定することでコードを自動生成していました。
v2からは2つのプラグイン(google.golang.org/protobuf/cmd/protoc-gen-go とgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc)を使い、それぞれ役割の違うコードを2つ自動生成します。
今回は ./hello/pb
以下に自動生成したファイルを置くことにします。
$ mkdir -p hello/pb $ ./bin/protoc \ -I proto/ \ --plugin protoc-gen-go=bin/protoc-gen-go \ --plugin protoc-gen-go-grpc=bin/protoc-gen-go-grpc \ --go-grpc_opt paths=source_relative \ --go-grpc_out hello/pb/ \ --go_opt paths=source_relative \ --go_out hello/pb/ \ proto/hello.proto
これで hello/pb/hello.pb.go
と hello/pb/hello_grpc.pb.go
という2つのファイルが生成されます。
hello.pb.go
は、gRPCを使わずGoでProtocol Buffersを利用するために必要なコードが生成されるようです。HelloServiceの定義はここにはありません。
hello_grpc.pb.go
には、gRPCを利用するための定義が記述されています。サーバ側が満たすべきインターフェースと、クライアントの実装が含まれます。
サーバを実装する
github.com/pddg/go-protobuf-v2-sample/hello/pb
にある HelloServiceServer
というインターフェースを満たすようにサーバを実装します。 hello/hello.go
として記述します。
以下のサンプルを参考に実装しました。
package hello import ( "context" "github.com/pddg/go-protobuf-v2-sample/hello/pb" ) // HelloServiceの実際の実装。 type helloServer struct{ pb.UnimplementedHelloServiceServer } // Hello ○○するハンドラ func (hs helloServer) Hello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello " + request.Name}, nil } // 実装したサーバを初期化する関数 func NewHelloServiceServer() pb.HelloServiceServer { return &HelloServer{} }
cmd/server/main.go
を作成し、サーバを起動するコマンドを実装します。オプションとしてリッスンするportとホスト名を取れるようにしました。
簡単のため、http2ではなくtcpでリッスンします。
package main import ( "flag" "fmt" "log" "net" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "github.com/pddg/go-protobuf-v2-sample/hello" "github.com/pddg/go-protobuf-v2-sample/hello/pb" ) var ( port int host string ) func main() { flag.IntVar(&port, "port", 8080, "Port number") flag.StringVar(&host, "host", "0.0.0.0", "Listen host name") flag.Parse() listenAddr, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) if err != nil { log.Fatalf("Failed to listen '%s:%d'\n", host, port) } log.Printf("HelloServiceServer is listening on tcp://%s:%d\n", host, port) s := grpc.NewServer() helloServer := hello.NewHelloServiceServer() pb.RegisterHelloServiceServer(s, helloServer) // Enable reflection reflection.Register(s) if err := s.Serve(listenAddr); err != nil { log.Fatal(err) } }
せっかくv2で実装するのでreflectionを有効化しました。これにより、grpcurlなどのクライアントツールで proto ファイルを読み込む必要がなくなります。
このコマンドを bin/hello-server
としてビルドします。
$ go build -o bin/hello-server ./cmd/server
クライアントコマンドの実装
cmd/client/main.go
に記述します。オプションとしてサーバのアドレスを渡せるようにしました。
package main import ( "context" "flag" "fmt" "log" "os" "time" "google.golang.org/grpc" "github.com/pddg/go-protobuf-v2-sample/hello/pb" ) var server string func main() { flag.StringVar(&server, "server", "localhost:8080", "Server address") flag.Parse() if flag.NArg() == 0 { flag.Usage() os.Exit(1) } args := flag.Args() ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second) defer cancel() conn, err := grpc.DialContext(ctx, server, grpc.WithInsecure()) if err != nil { log.Fatal(err) } client := pb.NewHelloServiceClient(conn) response, err := client.Hello(ctx, &pb.HelloRequest{Name: args[0]}) if err != nil { log.Fatal(err) } fmt.Println(response.Message) }
このコマンドを bin/hello-client
としてビルドします。
$ go build -o bin/hello-client ./cmd/client
動かしてみる
適当にターミナルを立ち上げ、サーバを起動します。
$ ./bin/hello-server -port 8080 -host 0.0.0.0
別のターミナルからクライアントコマンドを使用して接続し、Hello worldしてみます。
$ ./bin/hello-client -server localhost:8080 world
Hello world
やったぜ。
grpcurlで叩いてみる
grpcurlはコマンドラインで使えるgRPC用の汎用ツールです。curlのような感覚で使えます。
$ gex --add github.com/fullstorydev/grpcurl/cmd/grpcurl
grpcurl サーバアドレス サービス名.呼び出す関数
という形式で使えます。 データを送信したい場合、JSON形式でPOSTできます。レスポンスもJSON形式で返ってきます。
今回のHelloServiceを叩く場合は以下の様になります。
$ gex grpcurl -plaintext -d '{"name": "world"}' localhost:8080 HelloService.Hello { "message": "Hello world" }
まとめ
これまでのprotoc-gen-goが単体でGo用のProtocol Buffersのコード・gRPCのスタブコードの両方を生成するスタイルよりも、役割が明確になったのはよかったのではないでしょうか*2。
v2の恩恵をまだreflectionくらいしか理解していないので、もう少し色々探索していきたいですね。
[追記]
実装全体を以下で公開しました。