blog.syfm

徒然なるままに考えていることなどを書いていくブログ

gRPC のトランスポートを任意の実装に差し替えられる grpchan を試す

GitHub を眺めていたら protoreflect や gRPCurl で有名な jhump 氏が fullstorydev org 配下に面白いリポジトリを公開していることに気づいた。

github.com

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.ClientConnInvoke メソッドを内部的に叩いているが、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(&reg, &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-statusgrpc-status-details-bingrpc-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 もレスポンスボディではなく独自のヘッダに格納する

より詳しい違いはドキュメントに記述されているので読んでみると良さそう。

pkg.go.dev