首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go语言学习 - RPC篇:gRPC拦截器剖析

Go语言学习 - RPC篇:gRPC拦截器剖析

作者头像
junedayday
发布于 2022-12-02 11:15:05
发布于 2022-12-02 11:15:05
1.1K00
代码可运行
举报
文章被收录于专栏:Go编程点滴Go编程点滴
运行总次数:0
代码可运行

概览

我们在前几讲提到过,优秀的RPC框架都提供了middleware的能力,可以减少很多重复代码的编写。在gRPC-Gateway的方案里,包括了两块中间件的能力:

  1. gRPC中的ServerOption,是所有gRPC+HTTP都会被处理
  2. gRPC-Gateway中的ServeMuxOption,只有HTTP协议会被处理

今天,我们先关注共同部分的ServerOption,它提供的能力最为全面,让我们一起了解下。

官方实现

在官方文件google.golang.org/grpc/server.go路径下,给出了很多公开的ServerOption方法。从本质上来说,这些方法都是为了修改服务端的一个核心数据结构体:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type serverOptions struct {
 creds                 credentials.TransportCredentials
 codec                 baseCodec
 cp                    Compressor
 dc                    Decompressor
 unaryInt              UnaryServerInterceptor
 streamInt             StreamServerInterceptor
 chainUnaryInts        []UnaryServerInterceptor
 chainStreamInts       []StreamServerInterceptor
 binaryLogger          binarylog.Logger
 inTapHandle           tap.ServerInHandle
 statsHandlers         []stats.Handler
 maxConcurrentStreams  uint32
 maxReceiveMessageSize int
 maxSendMessageSize    int
 unknownStreamDesc     *StreamDesc
 keepaliveParams       keepalive.ServerParameters
 keepalivePolicy       keepalive.EnforcementPolicy
 initialWindowSize     int32
 initialConnWindowSize int32
 writeBufferSize       int
 readBufferSize        int
 connectionTimeout     time.Duration
 maxHeaderListSize     *uint32
 headerTableSize       *uint32
 numServerWorkers      uint32
}

不难从命名中推断到,上述结构体包含了认证、编解码、压缩、日志等各种配置,其中在初始化时有一些默认值。我们将目光聚焦于核心middleware能力的实现 - 拦截器(Interceptor)。

gRPC协议提供了两种RPC调用的方式:

  • Unary普通的单次调用
  • Stream流式调用

我们框架的RPC调用都来自gRPC-Gateway对HTTP协议的转发,是属于Unary这块,所以我们聚焦于UnaryServerInterceptor即可。而chainUnaryInts的数据结构为[]UnaryServerInterceptor,即支撑了链式middleware的调用,是自定义入口的关键。

使用示例代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s := grpc.NewServer(
  grpc.ChainUnaryInterceptor(
   // 各个拦截器
  ),
)

分析UnaryServerInterceptor

我们先一起看看这个函数的签名:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func ExampleInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
 // 1 - 前处理
  
  // 2 - 调用具体实现
 resp, err = handler(ctx, req)
  // 3 - 后处理
  
 return 
}

运行逻辑

可以看到,整个代码分三步进行,其中handler这部分的实现是开发者编写的业务逻辑。

而当存在链式的拦截器时,这部分的实现类似于先入后出的逻辑:

  1. 前处理1 -> 前处理2 -> ... -> 前处理n
  2. 具体代码实现
  3. 后处理n -> 后处理n-1 -> ... -> 后处理1

参数说明

  1. ctx - 上下文
  2. req - 入参
  3. info - Unray调用的信息,主要是方法名
  4. handler - 正常处理的函数
  5. resp - 出参
  6. err - 错误

我们要了解这6个参数,才能真正地理解gRPC,进而合理地使用拦截器。下面,我挑选3个重点进行描述:

  1. 我们无法直接使用ctx提取值,而是要用metadata.FromIncomingContext(ctx)提取出gRPC的metadata、再塞入到ctx中。什么是metadata呢?你可以把它简单地类比到HTTP的Header。
  2. req与resp的类型与protobuf中定义的方法对应。不难猜到,对数据的序列化、反序列化等操作,是在拦截器之前工作的。
  3. resp与err这两个返回参数尽可能规范:当err != nil时,调用方只需关注err;当err == nil时,resp才有意义。

这里,我再额外补充两个容易陷入误区的点:

  1. gRPC-Gateway中也有拦截器的实现,但我们尽可能只做协议的转换:将HTTP Header转换到gRPC-Gateway。这样可以保证gRPC和HTTP的调用,数据处理逻辑用一个拦截器就可以完成,如用户认证。
  2. 尽可能只用err来表示错误,而不要在resp里封装errno等字段(我在下一篇也会给出对应兼容的方案)。这里的error用google.golang.org/grpc/internal/status生成,如status.Error(codes.Unauthenticated, "用户校验失败"),这样错误才能兼容框架,同时具备错误码与错误信息。

示例拦截器

分析完上述内容后,我们结合一些经典的拦截器,方便大家了解它的价值:

日志拦截器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func ServerLoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
 // 进入打印日志,确认入参
 log.Info()

 resp, err = handler(ctx, req)

 // 处理完打印日志,包括出参和error
 if err != nil {
  log.Error()
  return
 }
 log.Info()
 return
}

一个RPC调用最终会落成2个日志:

  1. 进入时的Info日志
  2. 返回时
    1. 正常,则打印Info日志
    2. 有错误,则打印Error日志

日志拦截器的对我们的日常开发意义非常大,核心思路是:通过日志的一入一出,快速定位问题。常见的如:

  1. 先看进入时的日志,看看打印的参数是否如预期,如果有错往往先从协议排查,如字段命名
  2. 再看返回的日志,如果打印的输出和预期的一致,那往往是调用方的协议问题,如字段未解析
  3. 如果进入时的日志正确,但返回的打印异常,那就是handler的实现有问题

recovery拦截器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func ServerRecoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
 defer func() {
  if e := recover(); e != nil {
   const size = 64 << 10
   stacktrace := make([]byte, size)
   stacktrace = stacktrace[:runtime.Stack(stacktrace, false)]
   // error及堆栈进行日志打印
      log.Error()
      
   err = status.Error(codes.Unavailable, "系统异常")
  }
 }()

 return handler(ctx, req)
}

随着项目的迭代,handler里的实现很有可能出现会导致panic的代码,我们必须对这种异常兜底,而不是随便导致程序崩溃。

示例代码就是捕获对应的panic,输出到日志,返回给调用方系统异常。recovery是保证HTTP服务稳定的重要实现,其中的日志对开发者事后排查问题也提供了参考,是一个必备的工具利器。

用户认证拦截器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const (
  USER_TOKEN    = "USER_TOKEN"
 CTX_USERNAME  = "CTX_USERNAME"
)

func ServerAuthUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
  // 1. 提取出metadata
 md, ok := metadata.FromIncomingContext(ctx)
 if !ok {
  return nil, status.Error(codes.Unauthenticated, "用户校验失败")
 }
  // 2. parseUserName 从对应的metadata的Key里提取信息,解析出用户名
 userName, err := parseUserName(md.Get(USER_TOKEN)[0])
 if err != nil {
  return nil, status.Error(codes.Unauthenticated, "用户校验失败")
 }
  // 3. 将用户名塞入到ctx中
 ctx = context.WithValue(ctx, CTX_USERNAME, userName)

  // 4. 继续逻辑处理
 return handler(ctx, req)
}

// 在handler里,调用这个函数可以提取到用户名
func GetUserName(ctx context.Context) string {
 return ctx.Value(CTX_USERNAME).(string)
}

相关的步骤已经在代码注释里写得很清楚了,这里再补充3个细节:

  1. metadata的USER_TOKEN这个Key,按调用方,来源分2种情况:
    1. 如果调用方是gRPC,那就要求调用方在metadata里填充这个Key
    2. 如果调用方是HTTP,需要人工将HTTP的Header映射到gRPC的metadata,这部分就是在gRPC-Gateway的中间件里实现
  2. 示例中的1与2会对未认证的请求直接拦截 - 不会调用到具体handler的代码,直接返回错误给调用方
  3. 如果服务的接口要区分认证与无需认证,建议从info.FullMethod入手,即调用的方法名,也就是增加一段if-else的判断逻辑

数据校验拦截器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// PGV里的结构,都实现了这个方法
type Validator interface {
 ValidateAll() error
}

func ServerValidationUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
  // 如果接口实现了PGV的方法,就认为必须要进行校验
 if r, ok := req.(Validator); ok {
  err = r.ValidateAll()
    // 校验失败,则打印错误并返回参数校验失败
  if err != nil {
   log.Error
   return nil, status.Error(codes.InvalidArgument, "参数校验失败")
  }
 }

 return handler(ctx, req)
}

在protobuf里有一个非常有用的插件 - PGV,可参考Github,它能帮助开发者快速实现对应的参数校验:

  • 简单的如整型要大于1,字符串要非空
  • 复杂的如邮箱、IP等格式检查

但是,它需要开发者手工判断一次。这时,我们就可以利用拦截器+接口,组装出一个参数校验的拦截器,而无需再每个handler中都去判定。

这个实现很简洁,也充分利用了接口的特性,是一个经典的拦截器实现。

小结

今天,我们对gRPC中的拦截器进行了分析,并给出了4个经典的拦截器代码实现。而gin等框架中的middleware实现思路也基本与其一致,差别主要在参数类型不一样。

gRPC拦截器能有效地收敛很多重复代码,保证框架的统一与高效;相反地,如果某个公共能力无法用拦截器实现,就非常值得我们反思了。

接下来,我们将视角转移到gRPC-Gateway方案,看看在针对HTTP方面又有哪些高效的middleware。

Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go编程点滴 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Interceptor拦截器 -- gRPC生态里的中间件
gRPC的拦截器(interceptor)类似各种Web框架里的请求中间件,请求中间件大家都知道是利用装饰器模式对最终处理请求的handler程序进行装饰,这样中间件就可以在处理请求前和完成处理后这两个时机上,拦截到发送给 handler 的请求以及 handler 返回给客户端的响应 。
KevinYan
2021/05/11
1.7K0
​来瞧一瞧 gRPC的拦截器
可是朋友们,有没有想过,要是每一个客户端与服务端通信的接口都进行一次认证,那么这是否会非常多余呢,且每一个接口的实现都要做一次认证,这真的太难受了
阿兵云原生
2023/02/16
7540
(转载非原创)gRPC 拦截器
第一篇内容我们已经基本了解到 gRPC 如何使用 、对应的三种流模式。现在已经可以让服务端和客户端互相发送消息。本篇仍然讲解功能性的使用说明:如何使用拦截器。使用过 Java 的同学知道 Spring 或者 Dubbo,这两个框架都提供了拦截器的支持,拦截器的作用无需多言,鉴权,Tracing,数据统计等等。
xlj
2021/07/08
8400
golang源码分析:middleware的执行顺序
go-grpc-middleware封装了认证(auth), 日志( logging), 消息(message), 验证(validation), 重试(retries) 和监控(retries)等拦截器。如何使用呢?在初始化grpcserver的时候,参数传入即可
golangLeetcode
2022/08/03
5950
grpc-go之异常处理(四)
我在之前的文章《go里面的异常处理》简单地说了下go的异常处理机制, 在web中, 一般可以通过框架层面提供的过滤器/拦截器统一地处理这种异常, 避免main函数被带崩.
Johns
2022/10/11
1.5K0
(转载非原创)gRPC 全局数据传输和超时处理
gRPC 在多个 GoRoutine 之间传递数据使用的是 Go SDK 提供的 Context 包。关于 Context 的使用可以看我之前的一篇文章:Context 使用。
xlj
2021/07/23
5330
Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(五):鉴权 gRPC-Interceptor 拦截器实战
拦截器(gRPC-Interceptor)类似于 Gin 中间件(Middleware),让你在真正调用 RPC 服务前,进行身份认证、参数校验、限流等通用操作。
为少
2021/05/27
1.7K0
Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(五):鉴权 gRPC-Interceptor 拦截器实战
gRPC拦截器那点事,希望帮到你
上一篇介绍了gRPC的接口认证,我们客户端需要实现gRPC提供的接口,然后在服务端业务接口实现中通过metadata获取认证信息,进行判断,那么当我们有几十个,几百个业务接口时,如果都在接口实现中去做,那将是一个噩梦,也不符合DRY(Don't Repeat Yourself)原则,今天一起来看看如何通过gRPC的拦截器做到统一接口认证工作
阿伟
2019/09/17
4.8K0
gRPC拦截器那点事,希望帮到你
Go语言学习 - RPC篇:gRPC-Gateway示例代码概览
gRPC-Gateway是gRPC生态的一环,用于对HTTP协议的扩展,是一套高性能、高扩展的开源RPC框架。
junedayday
2022/12/02
1.1K0
Go语言学习 - RPC篇:gRPC-Gateway示例代码概览
grpc-go 从使用到实现原理全解析!
本期将从rpc背景知识开始了解,如何安装进行开发前的环境准备,protobuf文件格式了解,客户端服务端案例分享等,逐渐深入了解如何使用grpc-go框架进行实践开发。
小许code
2023/10/08
1.8K0
grpc-go 从使用到实现原理全解析!
golang源码分析:分布式链路追踪
在上一节搭完分布式追踪的采集展示链路后,这一节开始分析分析分布式链路追踪的核心源码。我们知道分布式追踪的原理是通过traceId串联调用链路上的所有服务和日志,每个服务都有一个自己的spanId,每一次rpc调用都需要生成一个子spanId,通过父子spanID的对应关系,构建一个有向无环图实现分布式追踪的。因此在业务代码的接入过程中需要实现如下功能,父子span关系的构建,父子span关系的传递(包括context内部传递和rpc服务之间的传递有可能跨协议比如http和grpc协议之间传递),rpc日志的采样,上报等等。每一个厂商都有自己的实现,opentrace定义了统一的标准接口,我们按照标准实现即可。在业务代码中实现包括四步:
golangLeetcode
2022/12/17
8680
golang源码分析:分布式链路追踪
kratos源码分析系列(6)
直接获取当前节点:selector/node/direct/direct.go
golangLeetcode
2023/09/06
6230
kratos源码分析系列(6)
微服务难点剖析 | 服务拆的挺爽,问题是日志该怎么串联起来呢?
现在微服务架构盛行,很多以前的单体应用服务都被拆成了多个分布式的微服务,以解决应用系统发展壮大后的开发周期长、难以扩展、故障隔离等挑战。
KevinYan
2022/02/09
6700
微服务难点剖析 | 服务拆的挺爽,问题是日志该怎么串联起来呢?
grpc-go之超时与重试(三)
go里面一般会使用Context进行超时控制以及参数传递, 其中超时控制可以使用context.WithDeadline()或者context.WithTimeout()实现, 二者实现效果是一致的.
Johns
2022/09/28
3.2K1
grpc-go之参数验证(五)
参数验证是一个非常常用的场景, grpc-go中一般地我们会直接使用使用第三方插件go-proto-validators自动生成验证规则, 然后配合grpc-go的拦截器来实现参数验证的逻辑.
Johns
2022/10/11
1.9K0
把Go项目从单体扩展成微服务要做哪些工作?
我们的课程到这里已经基本结束,在整个课程中大家从项目初始化时敲下的第一行命令开始咱们一起经历了怎么从零搭建出一个好用的项目框架。这里我们花了大量时间来研究怎么把项目的配置化、可观测、可追踪做好。
KevinYan
2025/05/21
1430
把Go项目从单体扩展成微服务要做哪些工作?
golang源码分析:grpc context
gRPC 是基于 HTTP/2 协议的。进程间传输定义了一个 metadata 对象,该对象放在 Request-Headers 内,所以通过 metadata 我们可以将上一个进程中的全局对象透传到下一个被调用的进程。
golangLeetcode
2022/08/03
1.1K0
Go语言微服务框架 - 11.接口的参数校验功能-buf中引入PGV
大量开发接口的朋友会经常遇到接口参数校验的问题。举个例子,我们希望将某个字段是必填的,如name,我们需要做两步:
junedayday
2021/11/25
2.1K0
从源码透析gRPC调用原理
gRPC是如何work的,清楚的理解其调用逻辑,对于我们更好、更深入的使用gRPC很有必要。因此我们必须深度解析下gRPC的实现逻辑,在本文中,将分别从客户端和服务端来说明gRPC的实现原理。
netkiddy
2018/08/19
18.7K1
从源码透析gRPC调用原理
[系列] - go-gin-api 路由中间件 - Jaeger 链路追踪(六)
说实话,这篇文章确实让大家久等了,主要是里面有一些技术点都是刚刚研究的,没有存货。
新亮
2019/09/25
1.3K0
[系列] - go-gin-api 路由中间件 - Jaeger 链路追踪(六)
相关推荐
Interceptor拦截器 -- gRPC生态里的中间件
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档