ひとつのポートで gRPC と HTTP を同時に受けるには


ひとつのポートで gRPC と HTTP を同時に受けるには

次のように grpc っぽいリクエストを grpc.Server.ServeHTTP へ分岐させる http.Handler を書き、 TLS を有効にして Listen させれば良い。(参考)

func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
                        grpcServer.ServeHTTP(w, r)
                } else {
                        otherHandler.ServeHTTP(w, r)
                }
        })
}

Listenするコードは次のとおり。

func main() {
    opts := []grpc.ServerOption{
        grpc.Creds(credentials.NewClientTLSFromCert(cert.CertPool, "localhost"))}

    g := grpc.NewServer(opts...)
    pb.RegisterSKVSServer(g, &srv{data: map[string]string{}})

    h := http.NewServeMux()
    h.Handle("/", http.HandlerFunc(hello))

    s := &http.Server{
        Addr: ":8080",
        Handler: grpcHandlerFunc(g, h),
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{*cert.KeyPair},
            NextProtos: []string{"h2"},
        },
    }

    conn, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }

    log.Println("Listening...")
    s.Serve(tls.NewListener(conn, s.TLSConfig))
}
  • ここで pb は適当な proto から生成したパッケージである

注意点

  • この方法は、grpc.Server.Serve() を使う場合よりもパフォーマンスが悪い。いずれ改善されるだろうが、優先度は高くないようだ(参考)
    • パフォーマンスが必要な場合、 cmux で振り分けたあと grpc.Serve.Serve() を使うことで改善できそうだが、これはChromeでアクセスしたときにうまく動作しないらしい。 結局パフォーマンスが必要なら http 系とはポートを分けろ、というのが現在の結論のようだ(参考)
  • TLSを有効にしないと、 http.Server が自身の Handler を呼び出す前にリクエストを弾いてしまうようだ。こちらも将来的にTLSが不要になるよう対処される可能性はあるが、現在のところ優先度は低いようだ(参考)