[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 代码(位于后缀为 .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 里面的
OpCodes
和Pool
正是构成上面的模式需要的几个参数。 -
从模板生成模式
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 链里面添加中间件实现。