概览
通过上一讲,我们对gRPC的拦截器有了一定的认识,也能定制出很多通用的中间件。
但在大部分的业务系统中,我们面向的还是HTTP协议。那么,今天我们就从gRPC-Gateway的mux选项出发,一起来看看一些很实用的特性。
ServeMux
1 2 3 4 5 6 7
| import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
gwMux := runtime.NewServeMux()
func NewServeMux(opts ...ServeMuxOption) *ServeMux { }
|
我们将目标聚焦于这个ServeMux
:
- 目前官方区分v1和v2版本,版本不一致会导致很多编译上的问题
- 入参包括多个
option
选项函数,用于定制想要的mux内容
具体的内容可以参考ServeMux
的数据结构,我这里挑选几个重点的能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| type ServeMux struct { handlers map[string][]handler forwardResponseOptions []func(context.Context, http.ResponseWriter, proto.Message) error marshalers marshalerRegistry incomingHeaderMatcher HeaderMatcherFunc outgoingHeaderMatcher HeaderMatcherFunc metadataAnnotators []func(context.Context, *http.Request) metadata.MD errorHandler ErrorHandlerFunc streamErrorHandler StreamErrorHandlerFunc routingErrorHandler RoutingErrorHandlerFunc }
|
综合一下,核心能力其实包括2块:
- Header数据的处理
- 返回消息的处理(包括正常情况和错误情况)
HTTP与gRPC协议头匹配
1 2 3 4 5 6 7 8 9
| func WithIncomingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption { }
func WithOutgoingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption { }
type HeaderMatcherFunc func(string) (string, bool)
|
这个函数只做一个简单的映射,我们可以通过下面的例子开快速了解:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
func CustomMatcher(key string) (string, bool) { switch key { case "some-special-key": return key, true case "deprecated-key": return "", false default: return runtime.DefaultHeaderMatcher(key) } }
|
将HTTP头转成gRPC头
上面的matcher
只是做一个key的映射,如果Header
里包括更复杂的部分(例如Cookie),需要引入下面函数:
1 2 3 4 5
| func WithMetadata(annotator func(context.Context, *http.Request) metadata.MD) ServeMuxOption { return func(serveMux *ServeMux) { serveMux.metadataAnnotators = append(serveMux.metadataAnnotators, annotator) } }
|
我们注意两个点:
- 只做协议转换,不做逻辑处理(逻辑处理交给gRPC层的中间件统一处理)
metadata.MD
的底层数据结构为map[string][]string
,与HTTP Header很类似
下面给出一个HTTP的Cookie处理示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const ( HTTP_COOKIE_TOKEN = "http_cookie" MD_TOKEN = "md_cookie" ) func ExampleCookieMetadataAnnotator(ctx context.Context, r *http.Request) (md metadata.MD) { c, err := r.Cookie(ODIN_JWT_TOKEN) if err != nil { return }
return metadata.Pairs(MD_TOKEN, c.Value) }
|
返回数据处理
正确返回
1 2
| func WithForwardResponseOption(forwardResponseOption func(context.Context, http.ResponseWriter, proto.Message) error) ServeMuxOption { }
|
正确返回时,核心的数据结构为 protoMessage
。我们不妨做一个封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| type HTTPResponse struct { Errno int `json:"errno"` Msg string `json:"msg"` Data interface{} `json:"data"` }
func GatewayResponseModifier(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { newResp := &HTTPResponse{ Data: resp, } pbData, _ := json.Marshal(newResp) w.Write(pbData) return nil }
|
错误返回
1 2
| func WithErrorHandler(fn ErrorHandlerFunc) ServeMuxOption { }
|
错误处理在整个RPC框架中扮演了非常重要的角色,我们不妨通过如下例子来了解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func GatewayErrModifier(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) { s, ok := status.FromError(err) if !ok { runtime.DefaultHTTPErrorHandler(ctx, mux, m, w, r, err) return }
switch s.Code() { case codes.Unauthenticated: default: runtime.DefaultHTTPErrorHandler(ctx, mux, m, w, r, err) }
return }
|
分析一下重点:
error
尽可能用gRPC
标准的错误Status
表示
gRPC
的标准错误,对错误码code有一套定义(参考google.golang.org/grpc/codes
),类似于HTTP
的状态码
- 错误码code要尽量少,过多没有意义
- 标准错误码尽可能复用,如资源找不到、权限不足等
- 业务错误码可以独立,一般一个系统定义1个即可
小结
本文重点介绍了gRPC-Gateway
中2类ServeMux
,也演示了对应的示例,大家能理解其基本用法即可。
后续,随着整体项目的落地,我会增加一些日常项目中常见的定制需求,帮助大家更好地认识RPC框架的能力。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding