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

陪她去流浪 桃子 2019年11月21日 编辑 阅读次数:3964

更新:从 v2 版本开始或者自 2020-06-09 以后的版本自带 ServeMux.HandlePath 方法了,不需要再繁琐地使用我下文的方法了。 参考 add mux.HandlePath method (v2)

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

1
2
3
4
5
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 的文件中):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这里是处理 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"

编译成模式后:

1
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包。

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

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

    1
    2
    3
    4
    5
    6
    7
    
    compileTemplate := func(rule string) httprule.Template {
    	if compiler, err := httprule.Parse(rule); err != nil {
    		panic(err)
    	} else {
    		return compiler.Compile()
    	}
    }
    

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    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 正是构成上面的模式需要的几个参数。

  • 从模板生成模式

    1
    2
    3
    4
    5
    6
    7
    8
    
    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
    }
    
  • 添加自己的处理函数

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

最终合并后的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)
}

测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
	}
}
1
2
$ curl localhost:6789/v1/topics/123/image
GetTopicImage: map[name:123]

成功运行。

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

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

标签:HTTP · gRPC · grpc-gateway · ProtocolBuffer