Junedayday Blog

六月天天的个人博客

0%

Go语言学习 - RPC篇:gRPC-Gateway定制mux选项

概览

通过上一讲,我们对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

  1. 目前官方区分v1和v2版本,版本不一致会导致很多编译上的问题
  2. 入参包括多个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 {
// 这个是server的核心实现:即注册的handlerf
handlers map[string][]handler
// 1、转发正常响应
forwardResponseOptions []func(context.Context, http.ResponseWriter, proto.Message) error
// 2、序列化工具
marshalers marshalerRegistry
// 3、进入时(http->grpc)的header匹配规则
incomingHeaderMatcher HeaderMatcherFunc
// 4、返回时(grpc->http)的header匹配规则
outgoingHeaderMatcher HeaderMatcherFunc
// 5、metadata的转换(从http转成grpc的md)
metadataAnnotators []func(context.Context, *http.Request) metadata.MD
// 6、错误处理
errorHandler ErrorHandlerFunc
// 7、流式错误处理
streamErrorHandler StreamErrorHandlerFunc
// 8、路由错误
routingErrorHandler RoutingErrorHandlerFunc
}

综合一下,核心能力其实包括2块:

  1. Header数据的处理
  2. 返回消息的处理(包括正常情况和错误情况)

Header的数据处理

HTTP与gRPC协议头匹配

1
2
3
4
5
6
7
8
9
// HTTP -> gRPC
func WithIncomingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption {
}

// gRPC -> HTTP
func WithOutgoingHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption {
}

type HeaderMatcherFunc func(string) (string, bool)

这个函数只做一个简单的映射,我们可以通过下面的例子开快速了解:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 入参:header key
// 出参:返回header key,以及是否返回
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)
}
}

我们注意两个点:

  1. 只做协议转换,不做逻辑处理(逻辑处理交给gRPC层的中间件统一处理)
  2. 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) {
// 从HTTP的cookie中读出对应的数据
c, err := r.Cookie(ODIN_JWT_TOKEN)
if err != nil {
return
}

// 将值放到md里,方便在后续提取
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 {
// 返回的数据,在外层同一封装了数据结构HTTPResponse,对一些历史项目兼容有很棒的效果
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) {
// 提取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
}

分析一下重点:

  1. error尽可能用gRPC标准的错误Status表示
  2. gRPC的标准错误,对错误码code有一套定义(参考google.golang.org/grpc/codes),类似于HTTP的状态码
  3. 错误码code要尽量少,过多没有意义
    1. 标准错误码尽可能复用,如资源找不到、权限不足等
    2. 业务错误码可以独立,一般一个系统定义1个即可

小结

本文重点介绍了gRPC-Gateway中2类ServeMux,也演示了对应的示例,大家能理解其基本用法即可。

后续,随着整体项目的落地,我会增加一些日常项目中常见的定制需求,帮助大家更好地认识RPC框架的能力。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码