GitHub を眺めていたら protoreflect や gRPCurl で有名な jhump 氏が fullstorydev org 配下に面白いリポジトリを公開していることに気づいた。
grpchan は gRPC Channel の抽象を定義・提供し、HTTP/2 ではなくインメモリや HTTP/1.1 などのトランスポートを使うことを可能にしている。 grpchan のドキュメント に具体的な使い方も含めて詳細が書かれている。
Hello, world
実際に grpchan を試してみる。grpchan はライブラリと Protocol Buffers プラグイン (protoc-gen-grpchan) に分かれており、gRPC プラグイン (protoc-gen-go-grpc) で自動生成されたコードではなく protoc-gen-grpchan によって自動生成されたコードを利用することで Channel を差し替えることができる。
$ protoc --grpchan_out helloworld --proto_path helloworld --go_out=plugins=grpc:helloworld helloworld/helloworld.proto
生成されたコードは量もなく理解しやすい。
protoc-gen-go-grpc で生成されたコードでは *grpc.ClientConn
の Invoke
メソッドを内部的に叩いているが、protoc-gen-grpchan では grpchan.Channel
というインターフェースに差し替わっているところが大きな違いで、これを NewGreeterChannelClient
から DI できるようになっている。
// Code generated by protoc-gen-grpchan. DO NOT EDIT. // source: helloworld.proto package helloworld import "github.com/fullstorydev/grpchan" import "golang.org/x/net/context" import "google.golang.org/grpc" func RegisterHandlerGreeter(reg grpchan.ServiceRegistry, srv GreeterServer) { reg.RegisterService(&_Greeter_serviceDesc, srv) } type greeterChannelClient struct { ch grpchan.Channel } func NewGreeterChannelClient(ch grpchan.Channel) GreeterClient { return &greeterChannelClient{ch: ch} } func (c *greeterChannelClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new(HelloReply) err := c.ch.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) if err != nil { return nil, err } return out, nil }
クライアントは先程の NewGreeterChannelClient
を使って初期化する。ここでは grpchan.Channel
の HTTP/1.1 実装の httpgrpc.Channel
を利用する。
package main import ( "context" "fmt" "net/http" "net/url" "github.com/fullstorydev/grpchan/httpgrpc" "github.com/ktr0731/grpchan-playground/helloworld" ) func main() { u, err := url.Parse("http://127.0.0.1:50051") if err != nil { panic(err) } client := helloworld.NewGreeterChannelClient( &httpgrpc.Channel{ Transport: http.DefaultTransport, BaseURL: u, }, ) res, err := client.SayHello(context.Background(), &helloworld.HelloRequest{ Name: "ktr", }) if err != nil { panic(err) } fmt.Println(res.Message) }
サーバは RegisterHandlerGreeter
を使ってサービスを登録し、通常の HTTP サーバのように http.ListenAndService
を使って起動する。
package main import ( "context" "fmt" "net/http" "github.com/fullstorydev/grpchan" "github.com/fullstorydev/grpchan/httpgrpc" "github.com/ktr0731/grpchan-playground/helloworld" "google.golang.org/grpc" ) func main() { reg := grpchan.HandlerMap{} helloworld.RegisterHandlerGreeter(®, &server{}) srv := grpc.NewServer() reg.ForEach(srv.RegisterService) httpgrpc.HandleServices(http.HandleFunc, "/", reg, nil, nil) http.ListenAndServe(":50051", nil) } type server struct{} func (s *server) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { return &helloworld.HelloReply{Message: fmt.Sprintf("hello, %s", req.Name)}, nil }
サーバを起動し、クライアントを実行するとレスポンスがちゃんと返ってくる。
$ go run client/main.go hello, ktr
curl で叩いてみてもちゃんと動作した。拙作の pb を使って入出力のエンコード・デコードを行った。
$ in="$(echo '{"name": "ktr"}' | pb -F helloworld/helloworld.proto encode helloworld.HelloRequest)" $ curl -s -d "$in" -X POST -H 'Content-Type: application/x-protobuf' http://127.0.0.1:50051/helloworld.Greeter/SayHello | pb -F helloworld/helloworld.proto decode helloworld.HelloReply { "message": "hello, ktr" }
vs gRPC
HTTP/2 を利用した gRPC と比較すると、トランスポートに依存する点を中心として以下のような違いがある。ちゃんと考えてないのでもっとあるはず。
grpc.DialOption
が使えないgrpc.StatsHandler
が使えない (未実装)- gRPC リフレクションが使えない (未実装)
- metadata や
grpc-status
、grpc-status-details-bin
、grpc-timeout
のような header/trailer に依存する部分が独自実装
また、HTTP/1.1 を実装として利用する場合、以下のような制約がある。これは gRPC-Web の制約と同様。
- Bidirectional Streaming が限定的なサポート (レスポンスが返ったあとに再度リクエストを投げることができない)
vs improbable-eng/grpc-web
improbable-eng/grpc-web (以下 gRPC-Web) と比較すると以下のような違いがある。
- gRPC-Web は gRPC サーバを HTTP サーバでラップし、HTTP → gRPC への変換を行うので gRPC サーバへ独自のメカニズムを導入しないのに対し、grpchan はトランスポートを入れ替えている
- grpchan は特定のトランスポートのみを使用するので、gRPC-Web のように HTTP/1.1 と HTTP/2 を同時にサポートすることができない
- gRPC-Web は gRPC Status をレスポンスボディに格納するのに対し、grpchan は
X-GRPC-Status
ヘッダに格納する- 同様に Status Details や Trailer もレスポンスボディではなく独自のヘッダに格納する
より詳しい違いはドキュメントに記述されているので読んでみると良さそう。