gRPC拦截器(一)

185 阅读4分钟

什么是拦截器

gRPC拦截器(interceptor)是个函数,它可以在gRPC执行服务之前和之后执行一些逻辑,例如认证、授权、日志记录、监控和统计等

函数作为实参传递

在 Go 中,一个普通的函数长这样:

func Service(request Request) (Response, error)

作用就是传入请求,完成处理后返回响应,有错误就返回 error

作为一门函数式编程语言,同样函数也支持把函数当实参传入另一个函数中,比如

type Handler func(request Request) (Response, error)

把最开始的函数签名定义成 Handler ,那么我们就可以这样来使用:

func WrapHandler(request Request, handler Handler) (Response, error) {
  ...... // pre
  resp, err := handler(request)
  ...... // post
  return resp, err
}

如何,相当于我们把实际需要实现的业务逻辑提取到外部定义,这样就可以在实际处理业务之前或者之后定义一些额外的逻辑。

拦截器原理

现在来抽象一下,把 Service 方法弄个称呼:业务方法 ,函数签名如下

func Service(ctx Context, req any) (any, error)

然后给这个签名定义一个类型

type Handler func(ctx Context, req any) (any, error)

记住,Handler 代表的就是一个最终的业务方法实现,现在把这个业务方法作为实参传递到另一个函数中去

func Intercept(ctx Context, req any, handler Handler) (any, error) {
  ......                             // 前处理
  resp, err := Handler(ctx, request) // 调用业务方法
  ......                             // 后处理
  return resp, err
}

完事,这就是拦截器的原理:在业务方法之前或者之后增加额外的逻辑,这就是某些支持注解的语言(Java、TS)的原理,或者把当作装饰设计模式看待。

拦截器链原理

再来抽象一下,把 Intercept 也弄个称呼:拦截器 ,函数签名如下

type Interceptor func(ctx Context, req any, handler Handler) (any, error)

记住,Interceptor 代表的就是一个拦截器,在调用业务方式时增加前处理和后处理逻辑。

那么问题来了,如果我定义了两个拦截器:

func Interceptor1(ctx Context, req any, handler Handler) (any, error) {
  return nil, nil
}

func Interceptor2(ctx Context, req any, handler Handler) (any, error) {
  return nil, nil
}

业务方法好像?似乎?也许?只能放到 拦截器 1 或者 拦截器 2 其中一个执行,然后返回响应。但是我想业务方法先流过拦截器 1,然后流过拦截器 2,这样就可以不断新增拦截器逻辑,而不必全堆在一个拦截器中实现全部额外逻辑。

第一版

那有没有办法实现这种逻辑呢?听我说,有的兄弟,有的。

func WrapInterceptors(ctx Context, req any, handler Handler) (any, err) {
  interceptor1 := func(ctx Context, req any, handler Handler) (any, err) {
    ...... // 1 -> pre
    interceptor2 := func(ctx Context, req any, handler Handler) (any, err) {
      ...... // 2 -> pre
      resp, err := handler(ctx, req)  // 业务方法
      ...... // 2 -> post
      return resp, err
    }
    ...... // 1 -> post
    return interceptor2(ctx, req, handler)
  }
  return interceptor1(ctx, req, handler)
}

resp, err := WrapInterceptors(ctx, req, handler)

怎么样兄弟,满不满足要求?什么?你说这样的实现太丑陋了?要高级点?好吧,听我说,有的兄弟,有的。

第二版

先改动一下拦截器的函数签名,让拦截器返回业务处理函数,记住更改签名后,拦截器是包装业务函数的函数,返回也是业务函数

type Interceptor func(handler Handler) Handler

拦截器的形式改成闭包形式:

func Interceptor1(handler Handler) Handler {
  return func(ctx Context, req any) (any, error) {
    ...... // pre-processing
    resp, err := handler(ctx, req)
    ...... // post-processing
    return response, err
  }
}

多个拦截器装饰在一起就完事了

func WrapInterceptors(ctx Context, req any, handler Handler) (any, error) {
  wrap := Interceptor1(Interceptor2(handler))
  return wrap(ctx, req)
}

这也是Go里面HTTP的拦截器写法,如何兄弟,收不收货?什么?你说这样写还是很丑陋。好吧,要求真多,不过你听我说,有的兄弟,还是有的。

第三版

一些前缀知识

// 切片的最后一个元素
last := slice[len(slice)-1]

// 移除切片的最后一个元素
slice := slice[:len(slice)-1]

然后只能祭出 闭包 + 递归 大法了

func WrapInterceptors(interceptors ...Interceptor) Interceptor {
  if len(interceptors) == 1 {
    return interceptors[0]
  }
  return func(handler Handler) Handler {
    last := len(interceptors) - 1
    wrap = interceptors[last](handler) // 从最后一个拦截器开始包装业务方法
    return WrapInterceptors(interceptors[:last]...)(wrap)
  }
}

怎样兄弟?什么?你说倒序太难看懂,行吧,有的,顺序也有的

func WrapInterceptors(interceptors ...Interceptor) Interceptor {
  if len(interceptors) == 1 {
    return interceptors[0]
  }
  return func(handler Handler) Handler {
    wrap := WrapInterceptors(interceptors[1:]...)(handler)
    return interceptors[0](wrap)
  }
}
func main() {
  interceptor := WrapInterceptors(Interceptor1, Interceptor2)
  resp, err := interceptor(handler)(ctx, req)
  ......
}

兄弟兄弟,怎么样,是时候收收货了吧?什么?你还想不改动拦截器的签名?特喵的,屁事还真多,行,没问题。

最终版

func WrapHandler(ctx Context, req any, handler Handler, interceptors ...Interceptor) Handler {
  if len(interceptors) == 0 {
    return handler
  }
  return func(ctx Context, req any) (any, error) {
    wrap := WrapHandler(ctx, req, handler, interceptors[1:]...)
    return interceptors[0](ctx, req, wrap)
  }
}

多个拦截器直接传入即可

func main() {
  fn := WrapInterceptors(ctx, req, handler, Interceptor1, Interceptor2)
  resp, err := fn(ctx, req)
  ......
}

兄弟,夜里小心走路脚滑。

其实gRPC的拦截器就是以上的方式实现的,就是定义拦截器的时候多了几个形参而已,原理就是那么点事,下次聊聊gRPC的拦截器。