☕
次世代gRPC?『connect-go』やってみた
2022.6.1に公開されたConnectについてやってみました。
この記事でやること 🏃
connect-goのGetting startedを元に進めていき基本的な実装を確認しつつ、加えて以下の点についても見ていきます。
- エラーハンドリング(ステータスコードの追加)
- リフレクション設定
- インターセプタの設定
なお、そもそもConnectとは何者?については以下の記事がまとまっており参考になりました🙏
Getting started! 🎉
※ https://github.com/rai-wtnb/connect-demo に全コードを置いております
-
下準備。connect_demoディレクトリ以下で色々やっていきます。
mkdir connect_demo cd connect_demo go mod init demo go install github.com/bufbuild/buf/cmd/buf@latest go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest
-
protoファイルにサービス定義
mkdir -p greet/v1 touch greet/v1/greet.proto
syntax = "proto3"; package greet.v1; // ※ bufの managed mode を使用するためgo_packageを記述していません message GreetRequest { string name = 1; } message GreetResponse { string greeting = 1; } service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {} }
-
buf mod init し、
buf.yaml
作成、touchなどでbuf.gen.yaml
作成- managed mode の設定をしています
version: v1 managed: # managed mode を使用 enabled: true go_package_prefix: default: demo/gen/ plugins: - name: go # protoc-gen-go out: gen opt: paths=source_relative - name: connect-go # protoc-gen-connect-go out: gen opt: paths=source_relative
-
buf generate すると、
buf.gen.yaml
の plugins[].out で指定したディレクトリ配下に生成されたファイルが置かれます-
greet.pb.go
はgoogleのprotoc-gen-go
が作成したもの -
greet/v1/greetv1connect/greet.connect.go
はprotoc-gen-connect-go
が作成したもの(HTTPハンドラ、クライアントインターフェース、コンストラクタを含んでいてたったの86行..!)
gen └── greet └── v1 ├── greet.pb.go └── greetv1connect └── greet.connect.go
-
-
ハンドラ実装
mkdir -p cmd/server
- Greetの実装にはGenericsが使用されていますね👀
- ドキュメントの例では行っていませんが、こちらでは以下パッケージを使用してリフレクションを設定してみます
- エラーハンドリングも追加します
- connect.NewErrorでステータスコードをアタッチできるようです
- Errors | Connect
- インターセプタも追加
// cmd/server/main.go package main import ( "context" "fmt" "log" "net/http" "github.com/bufbuild/connect-go" grpcreflect "github.com/bufbuild/connect-grpcreflect-go" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" greetv1 "demo/gen/greet/v1" "demo/gen/greet/v1/greetv1connect" ) type GreetServer struct{} func (s *GreetServer) Greet(ctx context.Context, req *connect.Request[greetv1.GreetRequest]) (*connect.Response[greetv1.GreetResponse], error) { log.Println("Request headers: ", req.Header()) if req.Msg.Name == "" { // エラーにステータスコードを追加 return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name is required.")) } greetResp := &greetv1.GreetResponse{ Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name), } resp := connect.NewResponse(greetResp) // ヘッダをセットしてみたり resp.Header().Set("Greet-Version", "v1") return resp, nil } // リフレクション設定 func newServeMuxWithReflection() *http.ServeMux { mux := http.NewServeMux() reflector := grpcreflect.NewStaticReflector( "greet.v1.GreetService", // 作成したサービスを指定 ) mux.Handle(grpcreflect.NewHandlerV1(reflector)) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) return mux } // インターセプタ設定 func newInterCeptors() connect.Option { interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { // ここでヘッダをセットするなど色々処理を書ける req.Header().Set("hoge", "fuga") return next(ctx, req) }) } return connect.WithInterceptors(connect.UnaryInterceptorFunc(interceptor)) } func main() { greetServer := &GreetServer{} mux := newServeMuxWithReflection() interceptor := newInterCeptors() path, handler := greetv1connect.NewGreetServiceHandler(greetServer, interceptor) mux.Handle(path, handler) http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{})) // Use h2c so we can serve HTTP/2 without TLS. }
- ヘッダーのやりとりも、
req.Header().Get()
、res.Header().Get()
、err.Meta().Get()
(エラー時のヘッダーやり取りに使用)、EncodeBinaryHeader
・DecodeBinaryHeader
(バイナリヘッダ)などを使用してシンプルに行えそうですね-
※ドキュメントのサンプルコード参照: Headers & trailers | Connect
-
単純にHTTPヘッダなので標準の
net/http
に準拠していてミドルウェアもそのまま使えます
-
-
コネクション確認
- サーバを立ち上げます
go run ./cmd/server/main.go
- httpie で HTTP をサポートしていることを確認
http :8080/greet.v1.GreetService/Greet Content-Type:application/json name=rai
# response HTTP/1.1 200 OK Accept-Encoding: gzip Content-Encoding: gzip Content-Length: 50 Content-Type: application/json Date: Tue, 27 Sep 2022 05:56:16 GMT Greet-Version: v1 // 追加したヘッダ { "greeting": "Hello, rai!" }
- evans で gRPC もサポートしていることを確認
echo '{ "name": "rai" }' | evans -r -p 8080 cli call greet.v1.GreetService.Greet
# response { "greeting": "Hello, rai!" }
-
クライアントも実装
- clientはデフォルトでは Connect Protocol を使用
-
connect.WithGRPC()
orconnect.WithGRPCWeb()
をclient optionにセット(NewGreetServiceClient()の第三引数以降にセット)することでプロトコルを設定できます。ここではconnect.WithGRPC()
を設定してみます - greetv1connect.NewGreetServiceClient()の第三引数以降に、
connect.WithGRPCWeb()
とか、connect.WithSendGzip()
とか、connect.WithCompressMinBytes()
とか、色々オプション設定できます
-
package main import ( "context" "log" "net/http" greetv1 "demo/gen/greet/v1" "demo/gen/greet/v1/greetv1connect" "github.com/bufbuild/connect-go" ) func main() { ctx := context.Background() client := greetv1connect.NewGreetServiceClient(http.DefaultClient, "http://localhost:8080", connect.WithGRPC()) res, err := client.Greet(ctx, connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"})) if err != nil { log.Println(err) return } log.Println(res.Msg.Greeting) }
- clientはデフォルトでは Connect Protocol を使用
-
クライアント実行
- 以下でクライアント実行してみます
go run ./cmd/client/main.go
- しっかり返ってくることが確認できました🙌
2022/09/26 20:10:28 Hello, Jane!
- ちなみにリクエストヘッダは以下のようになっています(
Content-Type
にapplication/grpc+proto
が入っている👀)
2022/09/26 20:10:28 Request headers: map[Accept-Encoding:[identity] Content-Type:[application/grpc+proto] Grpc-Accept-Encoding:[gzip] Hoge:[fuga] Te:[trailers] User-Agent:[grpc-go-connect/0.4.0-dev (go1.18.1)]]
おわりに 🙌
エラーハンドリング、リフレクション設定、インターセプタ実装など、ひととおりのことが確認できました。標準パッケージに準拠している点、gRPC Webをサポートしてる点、シンプルに書けそうな点などに好感を持ち、後方互換性が担保されるv1.0がリリースされたら、ぜひ使っていきたいなあと思いました。streamingの実装やconnect-webのGetting Startedも、後々やってみようと思います。
参考 🙏🏻
Discussion