次世代gRPC?『connect-go』やってみた

2022/09/27に公開

2022.6.1に公開されたConnectについてやってみました。
https://buf.build/blog/connect-a-better-grpc

この記事でやること 🏃

connect-goのGetting startedを元に進めていき基本的な実装を確認しつつ、加えて以下の点についても見ていきます。

  • エラーハンドリング(ステータスコードの追加)
  • リフレクション設定
  • インターセプタの設定

なお、そもそもConnectとは何者?については以下の記事がまとまっており参考になりました🙏
https://future-architect.github.io/articles/20220623a/

Getting started! 🎉

https://github.com/rai-wtnb/connect-demo に全コードを置いております

  1. 下準備。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
    
  2. 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) {}
    }
    
  3. buf mod init し、 buf.yaml 作成、touchなどで buf.gen.yaml 作成

    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
    
  4. buf generate すると、buf.gen.yaml の plugins[].out で指定したディレクトリ配下に生成されたファイルが置かれます

    • greet.pb.go はgoogleのprotoc-gen-go が作成したもの
    • greet/v1/greetv1connect/greet.connect.goprotoc-gen-connect-go が作成したもの(HTTPハンドラ、クライアントインターフェース、コンストラクタを含んでいてたったの86行..!)
    gen
    └── greet
        └── v1
            ├── greet.pb.go
            └── greetv1connect
                └── greet.connect.go
    
  5. ハンドラ実装

    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() (エラー時のヘッダーやり取りに使用)、 EncodeBinaryHeaderDecodeBinaryHeader (バイナリヘッダ)などを使用してシンプルに行えそうですね
      • ※ドキュメントのサンプルコード参照: Headers & trailers | Connect

      • 単純にHTTPヘッダなので標準の net/http に準拠していてミドルウェアもそのまま使えます

  6. コネクション確認

    • サーバを立ち上げます
    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!"
    }
    
  7. クライアントも実装

    • clientはデフォルトでは Connect Protocol を使用
      • connect.WithGRPC() or connect.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)
    }
    
  8. クライアント実行

    • 以下でクライアント実行してみます
    go run ./cmd/client/main.go
    
    • しっかり返ってくることが確認できました🙌
    2022/09/26 20:10:28 Hello, Jane!
    
    • ちなみにリクエストヘッダは以下のようになっています(Content-Typeapplication/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も、後々やってみようと思います。

参考 🙏🏻

https://docs.buf.build/introduction
https://connect.build/docs/go/getting-started/
https://future-architect.github.io/articles/20220623a/

Discussion