通过测试gRPC的DNS解析,以及实现自定义名称解析服务来了解gRPC的名称解析。
什么是名称解析(Name Resolution)
信息世界中,名称解析本质上是用于服务发现的。在gRPC中也是如此,客户端必须确定服务端具体的IP+端口。最常见的名称解析服务就是DNS,我们在浏览器输入一个域名,这个域名会被DNS这个名称解析服务识别,并返回该域名对应的IP地址,从而浏览器可以被路由到正确的服务。
使用 gRPC 客户端发出请求时,默认使用 DNS 名称解析。但是,也可以使用其他各种名称解析机制:
Resolver解析器 | Example | Note |
---|---|---|
DNS | grpc.io:50051 | 默认情况下采用 DNS。 |
DNS | dns:///grpc.io:50051 | 额外的斜线用于提供权限 |
Unix Domain Socket | unix:///run/containerd/containerd.sock | |
xDS | xds:///wallet.grpcwallet.io | |
IPv4 | ipv4:198.51.100.123:50051 |
默认情况下gRPC使用DNS作为名称解析服务,是dns://grpc.io:50051
的简写。第二种dns:///
才是更明确的指定方式,表示dns:[//authority/]grpc.io:40051
, dns:///
表示authority为空,指没有具体的DNS解析服务器,交给系统决定。跟第一种写法其实一样,只是表示的更明确。authority其实就是user:password@host:port
。
一些语言的接口支持用户自定义他们的名称解析器,这意味你可以自己定义任意的名字,然后解析出你想要的IP地址,就像修改操作系统的hosts文件一样。
DNS和IP作为名称解析服务
我们来演示一个demo,使用DNS作为名称解析服务来实现gRPC的server和client通信。
这个演示将包含以下几个部分: .proto 文件定义:定义服务和消息。 服务端代码:启动一个 gRPC 服务,监听一个本地地址。 客户端代码:使用dns:///
和域名来连接服务端。 DNS 配置(本地模拟):我们会修改本地的 hosts 文件来模拟一个 DNS 条目,使得一个自定义的域名能指向我们的本地服务地址。
代码结构如下
grpc-dns-demo/
├── go.mod
├── proto/
│ ├── greet.pb.go
│ ├── greet.proto //proto文件
│ └── greet_grpc.pb.go
├── server/
│ └── server.go // 服务端
├── client/
│ └── client.go // 新的客户端
首先,创建一个grpc-dns-demo的目录,使用go mod init grpc-dns-demo
来初始化项目。
定义proto文件
grpc-dns-demo目录下创建一个proto目录,在proto目录中创建geert.proto文件。写入如下代码。
syntax = "proto3";
option go_package = "./proto";
package proto;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
该proto文件定义了一个Greeter的服务,用于接收客户端发过来的HelloRequest消息类型,返回HelloReply的消息类型。
然后在grpc-dns-demo目录运行如下命令
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/greet.proto
实现server服务
创建一个 server 目录,并在其中添加 server.go 文件。
该server监听50051端口,实现了一个SayHello方法,该方法接收HelloRequest消息,返回“Hello ‘client name’”。
代码如下:
package main
import (
"context"
"log"
"net"
pb "grpc-dns-demo/proto"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements proto.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("Server listening at %v", lis.Addr())
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
实现client
client在初始化的时候直接传入一个dns:///myserver.local:50051
的地址,然后调用SayHello方法给服务端发送消息。
package main
import (
"context"
"log"
"time"
pb "grpc-dns-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
address = "dns:///myserver.local:50051"
clientName = "Colin"
)
func main() {
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: clientName})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
配置本地DNS
为了让 dns:///myserver.local:50051 工作,你需要让你的操作系统能够将 myserver.local 解析到服务端运行的 IP 地址。
- Linux/macOS: 编辑 /etc/hosts 文件,添加一行:
127.0.0.1 myserver.local
- Windows: 编辑 C:\Windows\System32\drivers\etc\hosts 文件,添加同样的一行。
测试DNS解析
在终端中分别运行server和client。
客户端会接收到如下消息:
[root@debian client]# go run client.go
2025/05/17 16:35:13 Greeting: Hello Colin
如果我们将
address = "dns:///myserver.local:50051"
改为
address = "myserver.local:50051"
同样在测试一遍,结果是一样的。
[root@debian client]# go run client.go
2025/05/17 16:44:11 Greeting: Hello Colin
同样的改为IP
address = "127.0.0.1:50051"
结果也一样
因此,gRPC如果使用DNS作为名称解析服务,支持两种地址传入,分别是dns:///myserver.local:50051
和 myserver.local:50051
。也可以使用IP+端口作为地址传入。
自定义名称解析器
gRPC是可以自定义名称解析服务的,而不是用默认的DNS,自定义名称解析服务需要自己实现解析器(Resolver)、构建器(Builder)、自定义schema。这提升了工作量,但也增加了灵活性。
下面我们实现一个自定义名称解析器的demo,代码结构如下:
grpc-custom-resolver-demo/
├── go.mod
├── proto/
│ ├── greet.pb.go
│ ├── greet.proto
│ └── greet_grpc.pb.go
├── server/
│ └── server.go
├── client/
│ └── client.go
└── resolver/
└── resolver.go
proto复用前面的就行。
Server实现
Server部分的代码只需要小小改动一下,我们定义两个端口,50051和50052。通过这个两个端口提供两个gRPC服务端实例。
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
pb "grpc-dns-demo/proto"
"google.golang.org/grpc"
)
const (
port1 = ":50051"
port2 = ":50052"
)
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements proto.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go startServer(port1)
go startServer(port2)
<-stop
log.Println("Shutting down servers...")
}
func startServer(port string) {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("Server listening at %v", lis.Addr())
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Resolver
要实现自定义名称解析器,需要通过google.golang.org/grpc/resolver的Register方法注册进去。
func Register(b Builder) {
m[b.Scheme()] = b
}
Register方法需要传入一个Builder的实例,Buider接口有两个方法,实现这两个方法才能实现Builder实例。
type Builder interface {
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
Scheme() string
}
所以需要在我们自定义解析器里实现一个Builder接口,并且返回值是Resolver,所以在Build里面需要实现Resolver接口,完整代码如下
grpc-dns-demo/resolver/resolver.go
package custom_resolver
import (
"google.golang.org/grpc/resolver"
)
const (
myScheme = "example"
myServiceName = "my-custom-service:1234"
backendAddr = "127.0.0.1:50051"
backendAddr2 = "127.0.0.1:50052"
)
type myResolver struct {
target resolver.Target
cc resolver.ClientConn
addrsStore map[string][]string
}
type myResolverBuilder struct{}
func (*myResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {
r := &myResolver{
target: target,
cc: cc,
addrsStore: map[string][]string{
myServiceName: {backendAddr, backendAddr2},
},
}
r.start()
return r, nil
}
func (*myResolverBuilder) Scheme() string { return myScheme }
func (r *myResolver) start() {
addrStrs := 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 (r *myResolver) ResolveNow(resolver.ResolveNowOptions) {}
func (r *myResolver) Close() {}
func init() {
resolver.Register(&myResolverBuilder{})
}
client实现
客户端里我们通过 _ "grpc-dns-demo/resolver"
这一行代码来引入resolver,这会自动执行init方法,这会将example:///my-custom-service:1234
注册进gRPC全局注册表。
客户端里我们写入上述地址,作为address传入即可。
address = "example:///my-custom-service:1234"
完整代码如下
package main
import (
"context"
"log"
"time"
pb "grpc-dns-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
// 重要:导入自定义解析器包,这会执行其 init() 函数来注册解析器
_ "grpc-dns-demo/resolver"
)
const (
// 使用自定义 scheme "example" 和我们解析器中定义的服务名
// 解析器会将 my-custom-service:1234 解析为 ["127.0.0.1:50051", "127.0.0.1:50052"]
address = "example:///my-custom-service:1234"
clientName = "Colin"
)
func main() {
log.Println("Client: Dialing with custom resolver address:", address)
// Set up a connection to the server.
// gRPC 会自动选择注册到 "example" scheme 的解析器
// 默认情况下,gRPC 会在解析出的多个地址之间进行轮询 (round_robin) 负载均衡
conn, err := grpc.NewClient(
address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
// 如果你的解析器返回了 service config, 你可能不需要在这里显式设置
// grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
if err != nil {
log.Fatalf("Client: Did not connect: %v", err)
}
defer conn.Close()
log.Println("Client: Connected successfully!")
c := pb.NewGreeterClient(conn)
for range 5 { // 发送几次请求,观察负载均衡(如果服务端监听在不同端口)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: clientName})
if err != nil {
log.Fatalf("Client: Could not greet: %v", err)
}
log.Printf("Client: Greeting from server: %s", r.GetMessage())
time.Sleep(500 * time.Millisecond)
}
}
运行测试
通过运行客户端我们发现 example:///my-custom-service:1234
被成功解析并访问到了server。
[root@debian client]# go run client.go
2025/05/18 09:35:59 Client: Dialing with custom resolver address: example:///my-custom-service:1234
2025/05/18 09:35:59 Client: Connected successfully!
2025/05/18 09:35:59 Client: Greeting from server: Hello Colin
2025/05/18 09:35:59 Client: Greeting from server: Hello Colin
2025/05/18 09:36:00 Client: Greeting from server: Hello Colin
2025/05/18 09:36:00 Client: Greeting from server: Hello Colin
2025/05/18 09:36:01 Client: Greeting from server: Hello Colin