[gRPC] 在 grpc-gateway 中手动编译 gRPC 路由规则并实现纯 HTTP 接口

平常我们都是像下面这样在 Protocol Buffer.proto 文件中定义 gRPC 函数的对应的 HTTP 路由规则:

rpc ListGroupUsers (ListGroupUsersRequest) returns (ListGroupUsersResponse) {
    option (google.api.http) = {
        get: "/v1/groups/{name=*}/users";
    };
}

然后结合 grpc-ecosystem 的 grpc-gateway 插件,可以自动生成 HTTP 转 RPC 的代码。

下面这张官方的示意图很好地解释了其工作方式:

HTTP 转 gRPC

生成的 HTTP 转 gRPC 代码(位于后缀为 .gw.go 的文件中):

// 这里是处理 HTTP 路由
mux.Handle("GET", pattern_API_ListGroupUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
    ctx, cancel := context.WithCancel(req.Context())
    defer cancel()

    inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)

    // 把 HTTP 的头部转换为 gRPC 的 metadata
    rctx, err := runtime.AnnotateContext(ctx, mux, req)
    if err != nil {
        runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
        return
    }

    // 调用 gRPC 服务实现
    resp, md, err := request_API_ListGroupUsers_0(rctx, inboundMarshaler, client, req, pathParams)
    ctx = runtime.NewServerMetadataContext(ctx, md)
    if err != nil {
        runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
        return
    }

    // 把 gRPC 的返回转换成 HTTP 的返回
    forward_API_ListGroupUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})

其中的路由 pattern_API_ListGroupUsers_0 是一个由 gRPC 中定义的 HTTP 路由规则编译而来的模式。

在 gRPC 中被定义为:

"/v1/groups/{name=*}/users"

编译成模式后:

pattern_API_ListGroupUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"v1", "groups", "name", "users"}, ""))

其中的1是版本号,当前始终不变;[]int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}是内部的模式判断操作码(字节码),用于判断 HTTP 的资源路径是否和这个模式匹配,如果匹配,参数的值也会求出来;[]string{"v1", "groups", "name", "users"}是前面的操作码操作的对象。

func(w http.ResponseWriter, req *http.Request, pathParams map[string]string是一个回调函数的原型。pathParams是一个map[string]string,即是上面的模式中匹配出来的参数,参数类型全部是字符串。

我目前的项目需要用到一些纯 HTTP 实现的接口,它们不方便用 gRPC 实现)。比如:图片上传。因为 gRPC 是基于 message 的,不方便处理 HTTP 的文件上传。 为了不单独再开一个 HTTP 服务器,所以直接复用这个 HTTP 转 gRPC 的 grpc-gateway 提供的 HTTP 路由器:runtime.ServeMux

现在还需要一个东西,那就是怎么把路由规则转换成模式。这需要借助 grpc-gateway 提供的httprule包。

为了讲解,我分成了下面几个步骤:

  • 编译路由规则为一个模板

    compileTemplate := func(rule string) httprule.Template {
        if compiler, err := httprule.Parse(rule); err != nil {
            panic(err)
        } else {
            return compiler.Compile()
        }
    }

    它接受一个形如/v1/groups/{name=*}/users的参数, 并把它转换成下面这样的一个模板

    type Template struct {
        // Version is the version number of the format.
        Version int
        // OpCodes is a sequence of operations.
        OpCodes []int
        // Pool is a constant pool
        Pool []string
        // Verb is a VERB part in the template.
        Verb string
        // Fields is a list of field paths bound in this template.
        Fields []string
        // Original template (example: /v1/a_bit_of_everything)
        Template string
    }

    这个 Template 里面的 OpCodesPool 正是构成上面的模式需要的几个参数。

  • 从模板生成模式

    compilePattern := func(rule string) runtime.Pattern {
        t := compileTemplate(rule)
        pattern, err := runtime.NewPattern(1, t.OpCodes, t.Pool, t.Verb)
        if err != nil {
            panic(err)
        }
        return pattern
    }
  • 添加自己的处理函数

    handle := func(method string, rule string, handler runtime.HandlerFunc) {
        pattern := compilePattern(rule)
        mux.Handle(method, pattern, handler)
    }

最终合并后的函数:

func handle(mux *runtime.ServeMux, method string, rule string, handler runtime.HandlerFunc) {
    var tmpl httprule.Template
    if compiler, err := httprule.Parse(rule); err != nil {
        panic(err)
    } else {
        tmpl = compiler.Compile()
    }

    pattern, err := runtime.NewPattern(1, tmpl.OpCodes, tmpl.Pool, tmpl.Verb)
    if err != nil {
        panic(err)
    }

    mux.Handle(method, pattern, handler)
}

测试用例:

package main

import (
    "log"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/httprule"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
)

// GetTopicImage ...
func GetTopicImage(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
    fmt.Fprintln(w, "GetTopicImage:", pathParams)
}

func main() {
    mux := runtime.NewServeMux()
    handle(mux, "GET", `/v1/topics/{name=*}/image`, GetTopicImage)
    if err := http.ListenAndServe(":6789", mux); err != nil {
        panic(err)
    }
}
$ curl localhost:6789/v1/topics/123/image
GetTopicImage: map[name:123]

成功运行。

后话:不得不说,Go语言的包做得真是不错。一开始真没有想到可以把编译时的东西直接拿到运行时来用。

注:本文未实现异常处理、授权管理等,可以自己在 handler 链里面添加中间件实现。

发表于:2019年11月21日 ,阅读量:97 ,标签:HTTP · gRPC · grpc-gateway · ProtocolBuffer

版权声明:若非特别注明,本站所有文章均为作者原创,转载请务必注明原文地址。