上一章学习了 gRPC 截止时间,多路复用和元数据等特性,今天学习名字解析器j及其实现原理。

名字解析器(Name Resolver)

名字解析器用作将给定的服务名称解析为对应的后端 IP 地址和端口号,gRPC 中默认使用的是 passthrough 解析器,即没有指定 scheme 的时候会默认使用它作为解析器。此外,gRPC还支持通过接口的方式,自定义名字解析器,详见后面的 demo

名字解析器的使用
  • 服务端
package mainimport ("context""fmt""log""net"pb "github.com/unendlichkeiten/private_projects/pb""google.golang.org/grpc"
)const addr = "localhost:50051"type ecServer struct {pb.UnimplementedEchoServeraddr string
}func (s *ecServer) UnaryEcho(ctx context.Context,req *pb.EchoRequest) (*pb.EchoResponse, error) {return &pb.EchoResponse{Message: fmt.Sprintf("%s (from %s)", req.Message,s.addr)}, nil
}func main() {lis, err := net.Listen("tcp", addr)if err != nil {log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()pb.RegisterEchoServer(s, &ecServer{addr: addr})log.Printf("serving on %s\n", addr)if err := s.Serve(lis); err != nil {log.Fatalf("failed to serve: %v", err)}
}
  • 客户端

客户端建立连接是使用自己定义的 scheme,需要自己实现 scheme 对应的 resolverresolverBuilder

package mainimport ("context""fmt""log""time""google.golang.org/grpc""google.golang.org/grpc/resolver"pb "github.com/unendlichkeiten/private_projects/pb"
)const (myScheme      = "custom"myServiceName = "resolver.custom.hamming.com"backendAddr = "localhost:50051"
)func callUnaryEcho(c pb.EchoClient, message string) {ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.UnaryEcho(ctx, &pb.EchoRequest{Message: message})if err != nil {log.Fatalf("could not greet: %v", err)}fmt.Println(r.Message)
}func makeRPCs(cc *grpc.ClientConn, n int) {hwc := pb.NewEchoClient(cc)for i := 0; i < n; i++ {callUnaryEcho(hwc, "this is examples/name_resolving")}
}func main() {passthroughConn, err := grpc.Dial(// passthrough 是 gRPC 内置的一个 scheme// Dial to "passthrough:///localhost:50051"fmt.Sprintf("passthrough:///%s", backendAddr),grpc.WithInsecure(),grpc.WithBlock(),)if err != nil {log.Fatalf("did not connect: %v", err)}defer passthroughConn.Close()fmt.Printf("calling SayHello to \"passthrough:///%s\"\n", backendAddr)makeRPCs(passthroughConn, 10)fmt.Println()ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)defer cancel()exampleConn, err := grpc.DialContext(ctx,// 使用自定义的名字解析器// Dial to "custom:///resolver.custom.hamming.com"fmt.Sprintf("%s:///%s", myScheme, myServiceName),grpc.WithInsecure(),grpc.WithBlock(),)if err != nil {log.Fatalf("did not connect: %v", err)}defer exampleConn.Close()fmt.Printf("calling SayHello to \"%s:///%s\"\n", myScheme, myServiceName)makeRPCs(exampleConn, 10)
}// resolver 的实现
// Following is an example name resolver. It includes a
// ResolverBuilder(https://godoc.org/google.golang.org/grpc/resolver#Builder)
// and a Resolver(https://godoc.org/google.golang.org/grpc/resolver#Resolver).
//
// A ResolverBuilder is registered for a scheme (in this example, "example" is
// the scheme). When a ClientConn is created for this scheme, the
// ResolverBuilder will be picked to build a Resolver. Note that a new Resolver
// is built for each ClientConn. The Resolver will watch the updates for the
// target, and send updates to the ClientConn.// customResolverBuilder is a
// ResolverBuilder(https://godoc.org/google.golang.org/grpc/resolver#Builder).
type customResolverBuilder struct{}// Build 构建解析器
func (*customResolverBuilder) Build(target resolver.Target,cc resolver.ClientConn,opts resolver.BuildOptions) (resolver.Resolver, error) {r := &customResolver{target: target,cc:     cc,addrsStore: map[string][]string{myServiceName: {backendAddr},},}r.ResolveNow(resolver.ResolveNowOptions{})return r, nil
}// Scheme 返回 customResolverBuilder 对应的 scheme
func (*customResolverBuilder) Scheme() string { return myScheme }// customResolver is a
// Resolver(https://godoc.org/google.golang.org/grpc/resolver#Resolver).
type customResolver struct {target     resolver.Targetcc         resolver.ClientConnaddrsStore map[string][]string
}func (r *customResolver) ResolveNow(o resolver.ResolveNowOptions) {// 直接从map中取出对于的addrListaddrStrs := r.addrsStore[r.target.Endpoint]addrs := make([]resolver.Address, len(addrStrs))for i, s := range addrStrs {addrs[i] = resolver.Address{Addr: s}}r.cc.UpdateState(resolver.State{Addresses: addrs})
}func (*customResolver) Close() {}func init() {// Register the example ResolverBuilder. This is usually done in a package's// init() function.resolver.Register(&customResolverBuilder{})
}
运行结果
  • 服务端
$ go run main.go
2022/07/29 19:49:56 serving on localhost:50051
  • 客户端
$ go run main.go
calling SayHello to "passthrough:///localhost:50051"
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)calling SayHello to "custom:///resolver.custom.hamming.com"
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)

一切正常,说明我们的自定义 Resolver 是可以运行的,那么接下来从源码层面来分析一下 gRPC 中 Resolver 具体是如何工作的。

resolver 包括 ResolverBuilderResolver 两个部分,需要实现 BuilderResolver 两个接口,即上面自定义的 customResolverBuildercustomResolver

// resolver.go
// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {// Build creates a new resolver for the given target.//// gRPC dial calls Build synchronously, and fails if the returned error is// not nil.Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)// Scheme returns the scheme supported by this resolver.// Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.Scheme() string
}// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {// ResolveNow will be called by gRPC to try to resolve the target name// again. It's just a hint, resolver can ignore this if it's not necessary.//// It could be called multiple times concurrently.ResolveNow(ResolveNowOptions)// Close closes the resolver.Close()
}

Resolver 是整个功能最核心的代码,用于将服务名解析成对应实例。Builder 则采用 Builder 模式在包初始化时创建并注册构造自定义 Resolver 实例。当客户端通过 Dial 方法对指定服务进行拨号时,grpc resolver 查找注册的 Builder 实例调用其 Build() 方法构建自定义 Resolver

源码分析(基于 grpc-go v1.36.0
import (_ "google.golang.org/grpc/balancer/roundrobin" // To register roundrobin._ "google.golang.org/grpc/internal/resolver/dns" // To register dns resolver._ "google.golang.org/grpc/internal/resolver/passthrough" // To register passthrough resolver._ "google.golang.org/grpc/internal/resolver/unix" // To register unix resolver.
)// clientconn.go +103
// 客户端调用 grpc.Dial() 方法建立连接, 进入 DialContext() 方法
// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {return DialContext(context.Background(), target, opts...)
}

阅读DialContext() 方法中 resolver 解析和构建部分逻辑

// clientconn.go +249
// 解析 target 确定要使用的解析器
// grpc 内部支持 passthrough, dns 和 unix 类型cc.parsedTarget = grpcutil.ParseTarget(cc.target, cc.dopts.copts.Dialer != nil)channelz.Infof(logger, cc.channelzID, "parsed scheme: %q", cc.parsedTarget.Scheme)
// 根据上面解析的 scheme 到列表中找到对应的 reslverBuilderresolverBuilder := cc.getResolver(cc.parsedTarget.Scheme)if resolverBuilder == nil {// 如果指定的 scheme 找不到对应的 resolverBuilder,则使用默认的 defaultScheme// 默认使用 passthrough,直接从根据 target 获取对应的 endpoint 地址channelz.Infof(logger, cc.channelzID, "scheme %q not registered, fallback to default scheme", cc.parsedTarget.Scheme)cc.parsedTarget = resolver.Target{Scheme:   resolver.GetDefaultScheme(), Endpoint: target,}// *********** 阶段一 获取 builder ***********resolverBuilder = cc.getResolver(cc.parsedTarget.Scheme)if resolverBuilder == nil {return nil, fmt.Errorf("could not get resolver for default scheme: %q", cc.parsedTarget.Scheme)}}// ...... 这里省略不关心的代码 ...... // Build the resolver. 创建一个解析器// *********** 阶段二 获取 Resolver ***********rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)if err != nil {return nil, fmt.Errorf("failed to build resolver: %v", err)}

至此我们就拿到了指定 schemeresolver,继续阅读里面的代码发现,上面通过调用以下方法得到对应的 resolver

阶段一 获取 Builder 分析

// clientconn.go +1586
// 根据解析得到的 scheme 获取对应的 resolverBuilder
func (cc *ClientConn) getResolver(scheme string) resolver.Builder {for _, rb := range cc.dopts.resolvers {if scheme == rb.Scheme() {return rb}}return resolver.Get(scheme)
}// resolver.go +51
// Get returns the resolver builder registered with the given scheme.
// If no builder is register with the scheme, nil will be returned.
func Get(scheme string) Builder {if b, ok := m[scheme]; ok {return b}return nil
}

源码中可以看到,builder 的获取实际上是从 m 中拿到的,m 中的 builder 又是从哪里来的?返回最开始的代码片段,我们看到有 resolver 有引用4个包

import (_ "google.golang.org/grpc/balancer/roundrobin" // To register roundrobin._ "google.golang.org/grpc/internal/resolver/dns" // To register dns resolver._ "google.golang.org/grpc/internal/resolver/passthrough" // To register passthrough resolver._ "google.golang.org/grpc/internal/resolver/unix" // To register unix resolver.
)

这四个包都有一个 init() 函数,里面调用了 resolver.Register() 方法,将对应的 builder 注册到 m 中的 map 中,自定义的解析器同样通过初始化函数将 builder 注册到 m 中。

// passthrough.go +55
func init() {resolver.Register(&passthroughBuilder{})
}

阶段二 获取 Resolver 分析

// clientconn.go +313
// 根据 resovlerBuilder 创建解析器
rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)
if err != nil {return nil, fmt.Errorf("failed to build resolver: %v", err)
}// resolver_conn_wrapper +74
// newCCResolverWrapper 调用定义的 Build 方法创建 Resolver
ccr.resolver, err = rb.Build(cc.parsedTarget, ccr, rbo)
if err != nil {return nil, err
}

继续阅读 Build() 方法里面的代码,我们看到,里面会调用 resolveNow(), 进一步调用 UpdateState() 来更新客户端连接状态,至于如何更新这里不做阐述,后面有时间探讨。

func (r *customResolver) ResolveNow(o resolver.ResolveNowOptions) {// 直接从map中取出对于的addrListaddrStrs := r.addrsStore[r.target.Endpoint]addrs := make([]resolver.Address, len(addrStrs))for i, s := range addrStrs {addrs[i] = resolver.Address{Addr: s}}r.cc.UpdateState(resolver.State{Addresses: addrs})
}
总结
  • 客户端启动时,注册自定义的 resolver,通过 init() 将字对应的 resolverBuilder 注册到全局变量 map 中,还有 gRPC 内置的 resolverBuilder

  • 客户端调用 Dail() 方法构造连接对象 grpc.ClientConn

  • grpcutil.ParseTarget 获取对应的 scheme

  • 根据 scheme 拿到对应的 resolverBuilder(全局 map 中遍历得到)

  • 根据 resolverBuilder 拿到对应的 resolverbuild() 方法中调用 resolveNow() 完成名字到IP的解析 )

参考资料

  • gRPC Up & Running by Kasun Indrasiri and Danesh Kuruppu

  • https://github.com/grpc-up-and-running/samples

  • https://www.lixueduan.com/categories/gRPC

关注公众号一起学习——无涯的计算机笔记

GRPC(5):名字解析器相关推荐

  1. Wireshark Protobuf 和 gRPC 内置解析器使用介绍

    Wireshark Protobuf 和 gRPC 内置解析器使用介绍 目录 Wireshark Protobuf 和 gRPC 内置解析器使用介绍 1. 主要功能 2. 示例中使用的.proto文件 ...

  2. CSS 选择器:BeautifulSoup4解析器

    和 lxml 一样,Beautiful Soup 也是一个HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据. lxml 只会局部遍历,而Beautiful Soup 是基 ...

  3. Android XML pull 解析器

    Android 并未提供对 Java StAX API 的支持.但是,Android 确实附带了一个 pull 解析器,其工作方式类似于 StAX.它允许您的应用程序代码从解析器中获取事件,这与 SA ...

  4. Python之父发文,将重构现有核心解析器

    原题 | PEG Parsers 作者 | Guido van Rossum 译者 | 豌豆花下猫 转载自 Python猫(ID: python_cat) 导语:Guido van Rossum 是 ...

  5. Go语言写的解析器(支持json,linq,sql,net,http等)

    Monkey程序语言 Monkey v2.0版本已发布. monkey v2.0 增加了如下内容: 新增 short arrow(->)支持(类似C#的lambda表达式) 增加 列表推导和哈希 ...

  6. 详解Spring MVC 4之ViewResolver视图解析器

    所有的We MVC框架都有一套它自己的解析视图的机制,Spring MVC也不例外,它使用ViewResolver进行视图解析,让用户在浏览器中渲染模型.ViewResolver是一种开箱即用的技术, ...

  7. PHP的词法解析器:re2c

    http://www.phppan.com/2011/09/php-lexical-re2c/ PHP的词法解析器:re2c 胖胖 PHP 2011/09/26 1 条留言 147 views re2 ...

  8. BeautfuiSoup4解析器

    BeautifulSoup是一个HTML/XML的解析器,主要的功能是如何解析和提取HTML/XML的数据. 官方文档:http://beautifulsoup.readthedocs.io/zh_C ...

  9. 用 C 语言开发一门编程语言 — 语法解析器

    目录 文章目录 目录 前文列表 编程语言的本质 词法分析 语法分析 使用 MPC 解析器组合库 安装 快速入门 实现波兰表达式的语法解析 波兰表达式 正则表达式 代码实现 前文列表 <用 C 语 ...

最新文章

  1. 隐私保护新突破:高斯差分隐私框架与深度学习结合
  2. 微软开发x86模拟器,让Windows for ARM能运行x86应用
  3. Remoting系列(二)----建立第一个入门程序
  4. QT配置OpenCV(二):成功
  5. linux python json,在Python中使用JSON
  6. Linux之虚拟机配置双网卡
  7. Veeam ONE v10.0.2.1094 安装教程+许可证
  8. “依赖混淆”供应链攻击现身 微软苹果特斯拉优步等超35家企业内网失陷
  9. 10个JavaScript常见BUG及修复方法 1
  10. 【CAD开发】3dxml文件格式读取(Python、C++、C#)
  11. java简历模板免费下载word格式_个人简历模板下载即用word版.doc
  12. 得到知乎注册进行体验,谈谈感受
  13. java 排秩,lamd(java lambda表达式)
  14. CCS编译 报警#190-D enumerated type mixed with another type
  15. vue回到顶部(常用)
  16. 基本面分析 ≠ 基本面量化投资?
  17. 用计算机修改图片或照片,如何利用电脑自带的画图工具修改图片的基本属性
  18. 计算机科学家手抄报图片,关于简洁又漂亮的科学手抄报图片
  19. 计算机审计体会论文,审计论文格式_计算机审计实验报告_审计论文范文3000字
  20. 020-JVM-类加载器的四个层级-ClassLoader

热门文章

  1. C++中的Thunk技术 / 非静态类成员函数作为回调函数 的实现方法
  2. “Hello World!”团队第六周的第五次会议
  3. 仿网易新闻评论的楼层效果
  4. Robotframework做web测试
  5. 易语言组件花源码花大法防误报免杀360QVM云引擎(洪雨原创)
  6. (C#)Windows Shell 编程系列1 - 基础,浏览一个文件夹
  7. html input type=file
  8. 挑战程序设计竞赛:Millionaire
  9. 【ArcGIS】使用ArcGIS进行坡度分析
  10. 数据与智能融合,新赛道的投资机会如何判断?