- 記事作成日:2022/12/07
情報
名前 | URL |
---|---|
Github | https://github.com/grpc-ecosystem/grpc-gateway |
公式サイト | https://grpc-ecosystem.github.io/grpc-gateway/ |
デモサイト | |
開発母体 | CNCF |
version | 2.14.0 |
言語 | Go |
価格 | 無料 |
ライセンス | BSD 3-Clause |
何ができるもの?
gPRC のサービスに対して、RESTful API の口を用意するためのリバースプロキシ。
以下の図に書かれているように、protoファイルに付与した google.api.http のアノテーションからリバースプロキシの設定と、gRPC サービスを生成する。
利用シーン
- gRPC をメインで使っているが、RESTful API でも提供したいとき
- gRPC で独自の Marshal の仕組みを入れてる時の緩衝レイヤーとして
登場背景
by DeepL
gRPCは素晴らしい - 多くのプログラミング言語でAPIクライアントとサーバスタブを生成し、高速で使いやすく、帯域幅効率に優れ、そのデザインはGoogleによって戦闘的に証明されています。しかし、従来のRESTful APIも提供したいと思うかもしれない。その理由は、後方互換性の維持、gRPCで十分にサポートされていない言語やクライアントのサポート、単にRESTfulアーキテクチャに関わる美観やツールの維持など、多岐にわたります。
このプロジェクトは、あなたの gRPC サービスに HTTP+JSON インタフェースを提供することを目的としています。このライブラリを用いてリバースプロキシーを生成するために必要なのは、HTTPセマンティクスをアタッチするためのサービスにおけるわずかな設定だけです。
所感
- 既存の proto に annotation をつけるだけでコードが生成でき、それを http サーバに組み込むだけなので非常に簡単
- Go にしか対応していないので、Go 以外の言語で gRPC を使っている場合にはそれ+Go でサーバを書かないといけないのが面倒
- 調べると
encording/json
が 足を引っ張るケースがある ので、他のシリアライズライブラリに差し替えるのが良いという話があった - マニュアルを読むとエッジケースは対応しておらず、「80%ぐらいのユースケースに対応します」 と書いてあったのでガッツリ使う場合は事前に調査が必要
- ただしこれは自動のマッピングの話なので、grpc-gateway を使ったリバースプロキシに手を入れればなんとかなるハズ
- FAQ | gRPC-Gateway
gRPC-Gatewayは、包括的だが複雑なアノテーションを書くことを強いることなく、80%のユースケースをカバーすることを意図しています。そのため、ゲートウェイ自体が、デザイン上、必ずしもすべてのユースケースをカバーするわけではありません。言い換えれば、ゲートウェイはgRPCとHTTP/1通信間の典型的な退屈な定型マッピングを自動化しますが、任意に複雑なカスタムマッピングを代行するわけではありません。
一方、runtime.ServeMuxをラップするミドルウェアとして、好きなものを追加することは可能です。runtime.ServeMuxは標準的なhttp.Handlerなので、Goの既存のサードパーティライブラリ(例:gateway main.go program.ServeMux)を利用して、簡単にruntime.ServeMuxのカスタムラッパーを記述することができます。
使い方
$ go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway $ go get google.golang.org/protobuf/cmd/protoc-gen-go $ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
proto の生成
cat << EOF > proto/helloworld/hello_world.proto syntax = "proto3"; package helloworld; option go_package = "github.com/myuser/myrepo"; // The greeting service definition service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } EOF
cat << EOF > buf.yaml version: v1 name: buf.build/myuser/myrepo EOF ❯ cat << EOF > buf.gen.yaml heredoc> version: v1 plugins: - name: go out: . opt: paths=source_relative - name: go-grpc out: . opt: paths=source_relative heredoc> EOF ❯ buf generate Failure: plugin go-grpc: could not find protoc plugin for name go-grpc
エラーが出たので go install
しておく。
❯ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 go: downloading google.golang.org/grpc v1.2.1 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 3.
生成
❯ buf generate ❯ tree . ├── buf.gen.yaml ├── go.mod ├── go.sum └── proto ├── buf.yaml └── helloworld ├── hello_world.pb.go ├── hello_world.proto └── hello_world_grpc.pb.go
main.go
の生成
package main import ( "context" "log" "net" "google.golang.org/grpc" helloworldpb "github.com/myuser/myrepo/proto/helloworld" ) type server struct{ helloworldpb.UnimplementedGreeterServer } func NewServer() *server { return &server{} } func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8081") if err != nil { log.Fatalln("Failed to listen:", err) } // Create a gRPC server object s := grpc.NewServer() // Attach the Greeter service to the server helloworldpb.RegisterGreeterServer(s, &server{}) // Serve gRPC Server log.Println("Serving gRPC on 0.0.0.0:8081") log.Fatal(s.Serve(lis)) }
起動
❯ go mod tidy ❯ go run main.go 2022/12/07 10:38:06 Serving gRPC on 0.0.0.0:8081 # 接続テスト ❯ grpcurl -protoset <(buf build -o -) -d '{"name": "Jane"}' -plaintext 0.0.0.0:8081 helloworld.Greeter/SayHello { "message": "Jane world" }
grpc-gateway で RESTful API を提供する
/v1/example/echo
でアクセスできるように annotation をつけます。
syntax = "proto3"; package helloworld; option go_package = "github.com/myuser/myrepo"; + import "google/api/annotations.proto"; + // Here is the overall greeting service definition where we define all our endpoints service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/example/echo" + body: "*" + }; } } // The request message containing the user's name message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
version: v1 plugins: - name: go out: . opt: paths=source_relative - name: go-grpc out: . opt: paths=source_relative + - name: grpc-gateway + out: . + opt: paths=source_relative
version: v1 name: buf.build/myuser/myrepo +deps: + - buf.build/googleapis/googleapis
buf mod update
する必要があった。
❯ buf generate proto/helloworld/hello_world.proto:6:8:google/api/annotations.proto: does not exist ❯ buf mod update # buf.lock が作成された ❯ ls README.md buf.lock go.mod main.go buf.gen.yaml buf.yaml go.sum proto ❯ buf generate ❯ tree proto └── helloworld ├── hello_world.pb.go ├── hello_world.pb.gw.go ├── hello_world.proto └── hello_world_grpc.pb.go
package main import ( "context" "log" "net" "net/http" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" helloworldpb "github.com/myuser/myrepo/proto/helloworld" ) type server struct{ helloworldpb.UnimplementedGreeterServer } func NewServer() *server { return &server{} } func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8081") if err != nil { log.Fatalln("Failed to listen:", err) } // Create a gRPC server object s := grpc.NewServer() // Attach the Greeter service to the server helloworldpb.RegisterGreeterServer(s, &server{}) // Serve gRPC server log.Println("Serving gRPC on 0.0.0.0:8081") go func() { log.Fatalln(s.Serve(lis)) }() // Create a client connection to the gRPC server we just started // This is where the gRPC-Gateway proxies the requests conn, err := grpc.DialContext( context.Background(), "0.0.0.0:8081", grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalln("Failed to dial server:", err) } gwmux := runtime.NewServeMux() // Register Greeter err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn) if err != nil { log.Fatalln("Failed to register gateway:", err) } gwServer := &http.Server{ Addr: ":8090", Handler: gwmux, } log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090") log.Fatalln(gwServer.ListenAndServe()) }
起動
❯ go mod tidy ❯ go run main.go 2022/12/07 10:38:06 Serving gRPC on 0.0.0.0:8081 # 接続テスト # by grpcurl ❯ grpcurl -protoset <(buf build -o -) -d '{"name": "Jane"}' -plaintext 0.0.0.0:8081 helloworld.Greeter/SayHello { "message": "Jane world" } # by curl ❯ curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " Jane"}' {"message":" Jane world"}
Interceptor を足したらどうなるのか?
+ func testInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + log.Println("testInterceptor") + return handler(ctx, req) + } + } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8081") if err != nil { log.Fatalln("Failed to listen:", err) } // add interceptor + opt := grpc.ServerOption( + grpc.UnaryInterceptor(testInterceptor()), + ) // Create a gRPC server object - s := grpc.NewServer() + s := grpc.NewServer(opt)
この状態で gRPC / HTTP の両方を飛ばしたが、ちゃんと Interceptor のログが出力されていることを確認できた。
RegisterGreeterHandlerClient は、Greeter サービス用の http ハンドラを "mux" に登録します。ハンドラは与えられた "GreeterClient" の実装を介して grpc エンドポイントにリクエストを転送します。
注意: gRPC フレームワークは、gRPC ハンドラ内でインターセプターを実行します。渡された "GreeterClient" が通常の gRPC フロー (gRPC クライアントの作成など) を通らない場合、渡された "GreeterClient" が正しいインターセプターを呼び出すことになります。
と書かれていたので、HTTP -> gRPC の場合は内部でちゃんと Interceptor を通るようになってる。
おまけ
gRPC-Web との違い
利用方法
gRPC-Gatewayでは、プロトファイルのアノテーションからリバースプロキシーを生成します。フロントエンドでは、REST APIを直接呼び出します。OpenAPI v2の仕様を生成し、protoc-gen-openapiv2を使ってフロントエンドクライアントを生成することができる。
gRPC-webでは、クライアントコードはprotoファイルから直接生成され、フロントエンドで使用することができます。
性能
gRPC-Gateway は、gRPC サーバに送信する前に、JSON を protobuf バイナリフォーマットにパースします。その後、protobuf バイナリフォーマットから JSON への応答を再びパースする必要があります。パースのオーバーヘッドは性能にマイナスの影響を及ぼします。
gRPC-webでは、メッセージはすでにprotobufバイナリ形式で送信されるので、プロキシ側で追加のパースコストは発生しません。
メンテナンス
gRPC-Gatewayでは、protobufが変更された場合、ゲートウェイのリバースプロキシコードを再生成する必要があります。HTTP/JSON インタフェースを使用している場合、おそらくフロントエンドも変更する必要があり、これは 2 箇所の変更を意味します。
gRPC-web では、プロトファイルからファイルを再生成すると、自動的にフロントエンドのクライアントが更新されます。
他のツール
名前 | できること |
---|---|
grpc-dynamic-gateway | Node で書かれた grpc-gateway の動的に設定される代替品です。 |
rest2grpc | Nodeで書かれたgrpc-gatewayの静的な代替設定。 |
The Envoy proxy gRPC-JSON transcoder | 受信した JSON リクエストを gRPC に変換して返す Envoy プロキシフィルタです。 |
Google Cloud Platform HTTP/JSON gRPC transcoding | grpc-gatewayのような動作をするGCP製品です。 |