trpc.group/trpc-go/trpc-go@v1.0.3/filter/README.zh_CN.md (about)

     1  [English](README.md) | 中文
     2  
     3  # tRPC-Go 开发拦截器插件
     4  
     5  
     6  ## 前言
     7  
     8  本文介绍如何开发 tRPC-Go 框架的拦截器(也称之为过滤器)。tRPC 框架利用拦截器的机制,将接口请求相关的特定逻辑组件化,插件化,从而同具体的业务逻辑解除耦合,达到复用的目的。例如监控拦截器,分布式追踪拦截器,日志拦截器,鉴权拦截器等。
     9  
    10  ## 原理
    11  
    12  理解拦截器的原理关键点在于理解拦截器的`触发时机` 以及 `顺序性`。
    13  
    14  触发时机:拦截器可以拦截到接口的请求和响应,并对请求,响应,上下文进行处理(用通俗的语言阐述也就是 可以在`请求接受前`做一些事情,`请求处理后`做一些事情),因此,拦截器从功能上说是分为两个部分的 前置(业务逻辑处理前) 和 后置(业务逻辑处理后)。
    15  
    16  顺序性:如下图所示,拦截器是有明确的顺序性,根据拦截器的注册顺序依次执行前置部分逻辑,并逆序执行拦截器的后置部分。
    17  
    18  ![The Order of Filters](/.resources/filter/filter.png)
    19  
    20  ## 示例
    21  
    22  下面以一个 rpc 耗时统计上报拦截器进行举例说明如何开发拦截器。
    23  
    24  第一步:如下为实现拦截器的函数原型
    25  
    26  ```golang
    27  // ServerFilter server 耗时统计,从收到请求到返回响应的处理时间
    28  func ServerFilter(ctx context.Context, req interface{}, next filter.ServerHandleFunc) (rsp interface{}, err error)
    29  ```
    30  
    31  ```golang
    32  // ClientFilter client 耗时统计,从发起请求到接收响应的调用时间
    33  func ClientFilter(ctx context.Context, req, rsp interface{}, next filter.ClientHandleFunc) (err error)
    34  ```
    35  
    36  第二步:实现
    37  
    38  ```golang
    39  func ServerFilter(ctx context.Context, req interface{}, next filter.ServerHandleFunc) (interface{}, error) {
    40      begin := time.Now()        // 业务逻辑处理前打点记录时间戳
    41  
    42      rsp, err := next(ctx, req) // 注意这里必须用户自己调用下一个拦截器,除非有特定目的需要直接返回
    43  
    44      cost := time.Since(begin)  // 业务逻辑处理后计算耗时
    45  
    46      // 上报耗时到具体监控平台
    47  
    48      return rsp, err // 必须返回 next 的 rsp 和 err,要格外注意不要被自己的逻辑的 rsp 和 err 覆盖
    49  }
    50  
    51  func ClientFilter(ctx context.Context, req, rsp interface{}, next filter.ClientHandleFunc) error {
    52      begin := time.Now() // 发起请求前打点记录时间戳
    53  
    54      err := next(ctx, req, rsp)
    55  
    56      cost := time.Since(begin) // 接受响应后计算耗时
    57  
    58      // 上报耗时到具体监控平台
    59  
    60      return err
    61  }
    62  ```
    63  
    64  第三步:将拦截器注册到框架中
    65  
    66  ```golang
    67  filter.Register("name", ServerFilter, ClientFilter) // 拦截器名字自己随便定义,供后续配置文件使用,必须放在 trpc.NewServer() 之前
    68  ```
    69  
    70  第四步:配置使用
    71  
    72  ```yaml
    73  server:
    74    filter: # 对所有 service 全部生效
    75      - name1 # 上面第三步注册到框架中的 server 拦截器名字
    76    service:
    77      - name: trpc.app.server.service
    78        filter: # 只对当前 service 生效
    79          - name2
    80  
    81  client:
    82    filter:
    83      - name
    84  ```
    85  
    86  ## 流式拦截器
    87  
    88  因为流式服务和普通 RPC 调用接口差异较大,例如普通 RPC 的客户端通过 `proxy.SayHello`发起一次 RPC 调用,但是流式客户端通过`proxy.ClientStreamSayHello`创建一个流。流创建后,再调用`SendMsg` `RecvMsg` `CloseSend`来进行流的交互,所以针对流式服务,提供了不一样的拦截器接口。
    89  
    90  虽然暴露的接口不同,但是底层的实现方式和普通 RPC 类似,原理参考普通 RPC 拦截器的原理
    91  
    92  ### 客户端
    93  
    94  在客户端配置流式拦截器,需要实现`client.StreamFilter`
    95  
    96  ```golang
    97  type StreamFilter func(context.Context, *client.ClientStreamDesc, client.Streamer) (client.ClientStream, error)
    98  ```
    99  
   100  以流式交互过程中的耗时统计上报拦截器进行举例说明如何开发流式拦截器
   101  
   102  **第一步**:实现`client.streamFilter`
   103  
   104  ```golang
   105  func StreamClientFilter(ctx context.Context, desc *client.ClientStreamDesc, streamer client.Streamer) (client.ClientStream, error) {
   106      begin := time.Now() // 创建流之前,打点记录时间戳
   107  
   108      s, err := streamer(ctx, desc) // 注意这里必须用户自己调用 streamer 执行下一个拦截器,除非有特定目的需要直接返回
   109  
   110      cost := time.Since(begin) // 流创建完成后,计算耗时
   111  
   112      // 上报耗时到具体监控平台
   113  
   114      return &wrappedStream{s}, err // wrappedStream 封装了 client.ClientStream,用于后续拦截 SendMsg、RecvMsg 等方法。注意这里必须返回 streamer 的 err
   115  }
   116  ```
   117  
   118  **第二步**:封装 `client.ClientStream`,重写对应方法方法
   119  
   120  因为流式服务的交互过程中客户端有`SendMsg`、`RecvMsg`、`CloseSend`这些方法,为了拦截这些交互过程,需要引入一个新的结构体。用户需要为这个结构体重写`client.ClientStream`接口,框架调用`client.ClientStream`接口时,会执行这个结构体的对应方法,这样就实现了拦截。
   121  
   122  因为用户可能不需要拦截`client.ClientStream`的所有方法,所以可以将`client.ClientStream`设置为结构体的匿名字段,这样,不需要拦截的方法,会直接走原始的路径。用户需要拦截哪些方法,就在这个结构体中重写那些方法。
   123  
   124  例如我只想拦截发送数据的过程,那么只需要重写`SendMsg`方法,至于`client.ClientStream`其他的方法都不需要重新实现。这里是为了演示,所以实现了`client.ClientStream`的所有方法。
   125  
   126  ```golang
   127  // wrappedStream 封装原始流,需要拦截哪些方法,就重写哪些方法
   128  type wrappedStream struct {
   129      client.ClientStream // 必须内嵌 client.ClientStream
   130  }
   131  
   132  // 重写 RecvMsg,用来拦截流的所有 RecvMsg 调用
   133  func (w *wrappedStream) RecvMsg(m interface{}) error {
   134      begin := time.Now() // 接收数据之前,打点记录时间戳
   135  
   136      err := w.ClientStream.RecvMsg(m) // 注意这里必须用户自己调用 RecvMsg 让底层流接收数据,除非有特定目的需要直接返回
   137  
   138      cost := time.Since(begin) // 接收到数据后,计算耗时
   139  
   140      // 上报耗时到具体监控平台
   141  
   142      return err // 注意这里必须返回前面产生的 err
   143  }
   144  
   145  // 重写 SendMsg,用来拦截流的所有 SendMsg 调用
   146  func (w *wrappedStream) SendMsg(m interface{}) error {
   147      begin := time.Now() // 发送数据之前,打点记录时间戳
   148  
   149      err := w.ClientStream.SendMsg(m) // 注意这里必须用户自己调用 SendMsg 让底层流接收数据,除非有特定目的需要直接返回
   150  
   151      cost := time.Since(begin) // 发送数据后,计算耗时
   152  
   153      // 上报耗时到具体监控平台
   154  
   155      return err // 注意这里必须返回前面产生的 err
   156  }
   157  
   158  // 重写 CloseSend,用来拦截流的所有 CloseSend 调用
   159  func (w *wrappedStream) CloseSend() error {
   160      begin := time.Now() // 关闭本端之前,打点记录时间戳
   161  
   162      err := w.ClientStream.CloseSend() // 注意这里必须用户自己调用 CloseSend 让底层流关闭本端,除非有特定目的需要直接返回
   163  
   164      cost := time.Since(begin) // 关闭本端后,计算耗时
   165  
   166      // 上报耗时到具体监控平台
   167  
   168      return err // 注意这里必须返回前面产生的 err
   169  }
   170  ```
   171  
   172  **第三步**:将拦截器配置到 client,可以通过配置文件配置或者在代码中配置
   173  
   174  方式 1: 在配置文件配置
   175  
   176  先将拦截器注册到框架中
   177  
   178  ```golang
   179  client.RegisterStreamFilter("name1", StreamClientFilter)    // 拦截器名字自己随便定义,供后续配置文件使用,必须放在 trpc.NewServer() 之前
   180  ```
   181  
   182  再在配置文件中配置
   183  
   184  ```yaml
   185  client:
   186    stream_filter: # 对所有 service 全部生效
   187      - name1 # 上面注册到框架中 client 流式拦截器的名字
   188    service:
   189      - name: trpc.app.server.service
   190        stream_filter: # 只对当前 service 生效
   191          - name2
   192  ```
   193  
   194  方式 2: 在代码中配置
   195  
   196  ```golang
   197  // 通过 client.WithStreamFilters 将拦截器添加进去
   198  proxy := pb.NewGreeterClientProxy(client.WithStreamFilters(StreamClientFilter))
   199  
   200  // 创建流
   201  cstream,err := proxy.ClientStreamSayHello(ctx)
   202  
   203  // 流的交互过程
   204  cstream.Send()
   205  cstream.Recv()
   206  ```
   207  
   208  ### 服务端
   209  
   210  在服务端配置流式拦截器,需要实现`server.StreamFilter`
   211  
   212  ```golang
   213  type StreamFilter func(Stream, *StreamServerInfo, StreamHandler) error
   214  ```
   215  
   216  以流式交互过程中的耗时统计上报拦截器进行举例说明如何开发流式拦截器
   217  
   218  **第一步**:实现`server.StreamFilter`
   219  
   220  ```golang
   221  func StreamServerFilter(ss server.Stream, si *server.StreamServerInfo, handler server.StreamHandler) error {
   222      begin := time.Now() // 进入流式处理之前,打点记录时间戳
   223  
   224      // wrappedStream 封装了 server.Stream,用于后续拦截 SendMsg、RecvMsg 等方法
   225      ws := &wrappedStream{ss}
   226  
   227      // 注意这里必须用户自己调用 handler 执行下一个拦截器,除非有特定目的需要直接返回。
   228      err := handler(ws)
   229  
   230      cost := time.Since(begin) // 处理函数退出后,计算耗时
   231  
   232      // 上报耗时到具体监控平台
   233  
   234      return err // 注意这里必须返回 handler 的 err
   235  }
   236  ```
   237  
   238  **第二步**:封装 `server.Stream`,重写对应方法
   239  
   240  因为流式服务的交互过程中服务端端有`SendMsg`、`RecvMsg`这些方法,为了拦截这些交互过程,需要引入一个新结构体。用户需要为这个结构体重写`server.Stream`接口,框架调用`server.Stream`接口时,会执行这个结构体的对应方法,这样就实现了拦截。
   241  
   242  因为用户可能不需要拦截`server.Stream`的所有方法,所以可以将`server.Stream`设置为结构体的匿名字段,这样,不需要拦截的方法,会直接走原始的路径。用户需要拦截哪些方法,就在这个结构体中重写那些方法。
   243  
   244  例如我只想拦截发送数据的过程,那么只需要重写`SendMsg`方法,至于`server.Stream`其他的方法都不需要实现。这里是为了演示,所以实现了`server.Stream`的所有方法。
   245  
   246  ```golang
   247  // wrappedStream 封装原始流,需要拦截哪些方法,就重写哪些方法
   248  type wrappedStream struct {
   249      server.Stream // 必须内嵌 server.Stream
   250  }
   251  
   252  // 重写 RecvMsg,用来拦截流的所有 RecvMsg 调用
   253  func (w *wrappedStream) RecvMsg(m interface{}) error {
   254      begin := time.Now() // 接收数据之前,打点记录时间戳
   255  
   256      err := w.Stream.RecvMsg(m) // 注意这里必须用户自己调用 RecvMsg 让底层流接收数据,除非有特定目的需要直接返回
   257  
   258      cost := time.Since(begin) // 接收到数据后,计算耗时
   259  
   260      // 上报耗时到具体监控平台
   261  
   262      return err // 注意这里必须返回前面产生的 err
   263  }
   264  
   265  // 重写 SendMsg,用来拦截流的所有 SendMsg 调用
   266  func (w *wrappedStream) SendMsg(m interface{}) error {
   267      begin := time.Now() // 发送数据之前,打点记录时间戳
   268  
   269      err := w.Stream.SendMsg(m) // 注意这里必须用户自己调用 SendMsg 让底层流接收数据,除非有特定目的需要直接返回
   270  
   271      cost := time.Since(begin) // 发送数据后,计算耗时
   272  
   273      // 上报耗时到具体监控平台
   274  
   275      return err // 注意这里必须返回前面产生的 err
   276  }
   277  ```
   278  
   279  **第三步**:将拦截器配置到 server,可以通过配置文件配置或者在代码中配置
   280  
   281  方式 1: 在配置文件配置
   282  
   283  先将拦截器注册到框架中
   284  
   285  ```golang
   286  server.RegisterStreamFilter("name1", StreamServerFilter)    // 拦截器名字自己随便定义,供后续配置文件使用,必须放在 trpc.NewServer() 之前
   287  ```
   288  
   289  再在配置文件中配置
   290  
   291  ```yaml
   292  server:
   293    stream_filter: # 对所有 service 全部生效
   294      - name1 # 上面注册到框架中的 server 流式拦截器名字
   295    service:
   296      - name: trpc.app.server.service
   297        stream_filter: # 只对当前 service 生效
   298          - name2
   299  ```
   300  
   301  方式 2: 在代码中配置
   302  
   303  ```golang
   304  // 通过 server.WithStreamFilters 将拦截器添加进去
   305  s := trpc.NewServer(server.WithStreamFilters(StreamServerFilter))
   306  
   307  pb.RegisterGreeterService(s, &greeterServiceImpl{})
   308  if err := s.Serve(); err != nil {
   309      log.Fatal(err)
   310  }
   311  ```
   312  
   313  ## FAQ
   314  
   315  ### Q:拦截器入口这里能否拿到二进制数据
   316  
   317  不可以,拦截器入口这里的 req rsp 都是已经经过序列化过的结构体了,可以直接使用数据,没有二进制。
   318  
   319  ### Q:多个拦截器执行顺序如何
   320  
   321  多个拦截器的执行顺序按配置文件中的数组顺序执行,如
   322  
   323  ```yaml
   324  server:
   325    filter:
   326      - filter1
   327      - filter2
   328    service:
   329      - name: trpc.app.server.service
   330        filter:
   331          - filter3
   332  ```
   333  
   334  则执行顺序如下:
   335  
   336  ```
   337  接收到请求 -> filter1 前置逻辑 -> filter2 前置逻辑 -> filter3 前置逻辑 -> 用户的业务处理逻辑 -> filter3 后置逻辑 -> filter2 后置逻辑 -> filter1 后置逻辑 -> 回包
   338  ```
   339  
   340  ### Q:一个拦截器必须同时设置 server 和 client 吗
   341  
   342  不需要,只需要 server 时,client 传入 nil,同理只需要 client 时,server 传入 nil,如
   343  
   344  ```golang
   345  filter.Register("name1", serverFilter, nil)  // 注意,此时的 name1 拦截器只能配置在 server 的 filter 列表里面,配置到 client 里面,rpc 请求会报错
   346  filter.Register("name2", nil, clientFilter)  // 注意,此时的 name2 拦截器只能配置在 client 的 filter 列表里面,配置到 server 里面会启动失败
   347  ```