gRPC客户端

317 阅读7分钟

image.png

环境初始化

安装protoc

1、去Github上下载对应环境的protoc安装包(windows下载win包,macos下载osx)
2、配置环境变量(以macos举例)
将下载的包的bin、include目录移动到/usr/local/protobuf目录中
执行如下命令添加环境变量

#修改.bash_profile文件,将如下内容添加到文件末尾
export PROTOBUF=/usr/local/protobuf 
export PATH=$PROTOBUF/bin:$PATH

刷新环境变量

source .bash_profile # 回到用户路径下执行

3、验证

protoc --version 

编写proto文件

官方提供的proto内置类型(emppty)

syntax="proto3";//指明协议版本
package main;
option go_package="./;main";//分号前面部分,表示文件放在哪里,后面部分是报名,在go 1.14之后,必须要这个

service ServerService{
    rpc GetServerTime(Request) returns (Response);
}

message Request{
    string clientName=1;
}
message Response{
    string serverName=1;
    string time=2;
}

实现Go的服务端和客户端

//注:在go中需要使用protobuf还需要安装protoc-gen-go,这个组件是通过proto文件生成对应的go源文件()。
go get github.com/golang/protobuf/protoc-gen-go
/生成go的文件
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
./api.proto

Server

package main

import (
	context "context"
	"log"
	"net"
	"time"

	"google.golang.org/grpc"
)

type server struct {
	UnimplementedServerServiceServer
}

func (s *server) GetServerTime(ctx context.Context, r *Request) (*Response, error) {
	res := &Response{
		ServerName: "server",
		Time:       time.Now().Format(time.RFC3339),
	}
	return res, nil
}

func main() {
	lis, err := net.Listen("tcp", ":9000")
	if err != nil {
		log.Fatal("msg", err)
	}
	s := grpc.NewServer()
	RegisterServerServiceServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Client

package main

import (
	"context"
	"log"

	grpc "google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:9000", grpc.WithInsecure())
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()
	client := NewServerServiceClient(conn)
	res, err := client.GetServerTime(context.Background(), &Request{ClientName: "client"})
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println("time", res)
}

四种模式

结论:服务端流结束是返回nil,客户端流结束是通过streamRes.CloseSend()关闭

请求响应模式

客户端发送一个消息,服务端响应一个消息

service ServerService{
    rpc GetServerTime(Request) returns (Response);
}

服务端流

客户端发送一个消息,服务端响应多个消息

service ServerService{
    rpc GetServerTime(Request) returns (stream Response);
}

客户端流

客户端发起多个消息,服务端响应一个消息

service ServerService{
    rpc GetServerTime(stream Request) returns (Response);
}

双向流

客户端发送多个消息,服务端响应多个消息

service ServerService{
    rpc GetServerTime(stream Request) returns (stream Response);
}

编码

proto文档

image.png

每一个字段都需要有一个数字tag(也就是数字编号),因为在进行序列化后,二进制数据中使用的是变量的编号,而非变量名本身(这是压缩后尺寸小的一个原因)
Tag的取值范围最小是1,最大是 2^29 - 1 或 536,870,911,但 19000~19999 是 protobuf 预留的,用户不能使用

  • 1 ~ 15:单字节编码
  • 16 ~ 2047:双字节编码

因此对于使用频率高的字段建议使用1~15的编号

多路复用

多个gRPC服务可以共用一个grpc连接

//server
...
func main() {
	lis, err := net.Listen("tcp", ":9000")
	if err != nil {
		log.Fatal("msg", err)
	}
	s := grpc.NewServer()
	RegisterServerServiceServer(s, &server{})
	RegisterV2ServerServiceServer(s, &v2Server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
//客户端
...
func main() {
	conn, err := grpc.Dial("127.0.0.1:9000", grpc.WithInsecure())
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()
	client := NewServerServiceClient(conn)
	v2Client := NewV2ServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	res, err := client.GetServerTime(ctx, &Request{ClientName: "client"})
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println(res)

	res, err = v2Client.GetServerTime(ctx, &Request{ClientName: "client"})
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println(res)
}

命名解析器

利用命名解析器可以实现自定义服务名称的解析规则,实现服务发现功能

实现命名解析器

1.实现resolver.Builder接口

type Builder interface {
	Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
	Scheme() string
}

2.resolver.Resolver接口

type Resolver interface {
	ResolveNow(ResolveNowOptions)
	Close()
}

3.注册resolver

func init() {
	resolver.Register(&NameResolverBuilrder{})
}

具体实现


const (
	Scheme      = "dns"
	ServiceName = "server.pingwazi.com"
)
// 实现resolver.Builder接口
type NameResolverBuilrder struct {
}

func (r *NameResolverBuilrder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	resolver := &NameResolver{
		target:     target,
		cc:         cc,
		addrsStore: map[string][]string{ServiceName: {"127.0.0.1:9000", "localhost:9001"}},
	}
	resolver.start()
	return resolver, nil
}
func (r *NameResolverBuilrder) Scheme() string {
	return Scheme
}
// 实现resolver.Resolver接口
type NameResolver struct {
	target     resolver.Target
	cc         resolver.ClientConn
	addrsStore map[string][]string
}

func (r *NameResolver) start() {
	addrStrs := r.addrsStore[strings.TrimPrefix(r.target.URL.Path, "/")]
	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 *NameResolver) ResolveNow(opt resolver.ResolveNowOptions) {
}

func (r *NameResolver) Close() {

}

func init() {
	resolver.Register(&NameResolverBuilrder{})
}

客户端使用

func main() {
	conn, err := grpc.Dial(
		"dns:///server.pingwazi.com",//支持解析任意自定义名称
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
	)
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()

	client := NewServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	res, err := client.GetServerTime(ctx, &Request{ClientName: "client"})
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println(res)
}

客户端负载均衡

因为grpc底层是使用http/2,所以只要使用支持http/2的外部负载均衡器也可以完成负载均衡。 不过grpc实现的客户端也支持负载均衡,默认支持pick_first和round_robin两种策略

pick_first

永远都服务列表的第一个,只有当第一个不可用时才选择第二个,以此类推

round_robin

轮询,循环使用

自定义负载均衡策略

image.png

1.实现base.PickerBuilder接口

type PickerBuilder interface {
	Build(info PickerBuildInfo) balancer.Picker
}

2.注册负载均衡策略

func init() {
	// 注册负载均衡器
	balancer.Register(base.NewBalancerBuilder(WEIGHT_LB_NAME, &PingwaziNamePickerBuilder{}, base.Config{HealthCheck: false}))
}
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"strings"
	sync "sync"

	grpc "google.golang.org/grpc"
	"google.golang.org/grpc/attributes"
	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/balancer/base"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/resolver"
)

const (
	// 协议
	RESOLVER_SCHEME = "wang"
	// 服务名
	RESOLVER_SERVICENAME = "server.pingwazi.com"
	// 负载均衡策略的名称
	WEIGHT_LB_NAME = "weight"
)

func init() {
	// 注册命名解析器
	resolver.Register(&PingwaziNameResolverBuilrder{})
	// 注册负载均衡器(HealthCheck表示是否健康检查,如果为true并且配置了健康检查,则不健康的服务将不会加载到)
	balancer.Register(base.NewBalancerBuilder(WEIGHT_LB_NAME, &PingwaziNamePickerBuilder{}, base.Config{HealthCheck: false}))
}

func main() {
	conn, err := grpc.Dial(
		"wang:///server.pingwazi.com",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy": "%s"}`, WEIGHT_LB_NAME)),
	)
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()

	client := NewServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	for i := 0; i < 10; i++ {
		res, err := client.GetServerTime(ctx, &Request{ClientName: "client"})
		if err != nil {
			log.Fatal("msg", err)
		}
		log.Println(res)
	}

}

type PingwaziNameResolverBuilrder struct {
}

// 构建命名解析器
func (r *PingwaziNameResolverBuilrder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	resolver := &PingwaziNameResolver{
		target:     target,
		cc:         cc,
		addrsStore: map[string][]string{RESOLVER_SERVICENAME: {"127.0.0.1:9001", "127.0.01:9000"}},
	}
	resolver.start()
	return resolver, nil
}
func (r *PingwaziNameResolverBuilrder) Scheme() string {
	return RESOLVER_SCHEME
}

type PingwaziNameResolver struct {
	target     resolver.Target
	cc         resolver.ClientConn
	addrsStore map[string][]string
}

// 更新连接信息
func (r *PingwaziNameResolver) start() {
	addrStrs := r.addrsStore[strings.TrimPrefix(r.target.URL.Path, "/")]
	addrs := make([]resolver.Address, len(addrStrs))
	for i, s := range addrStrs {
		addrs[i] = resolver.Address{Addr: s, Attributes: attributes.New("weight", i+1)}
		r.cc.UpdateState(resolver.State{Addresses: addrs})
	}
}

func (r *PingwaziNameResolver) ResolveNow(opt resolver.ResolveNowOptions) {
}

func (r *PingwaziNameResolver) Close() {

}

type PingwaziNamePickerBuilder struct {
}

// 构建picker对象
func (*PingwaziNamePickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
	if len(info.ReadySCs) == 0 {
		return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
	}
	var scs []balancer.SubConn
	for subConn, addr := range info.ReadySCs {
		weight := addr.Address.Attributes.Value("weight").(int)
		if weight <= 0 {
			weight = 1
		}
		for i := 0; i < weight; i++ {
			scs = append(scs, subConn)
		}
	}
	return &PingwaziNamePick{
		subConn: scs,
	}
}

type PingwaziNamePick struct {
	subConn []balancer.SubConn //连接列表
	mu      sync.Mutex
}

// rpc调用时根据策略获取连接
func (l *PingwaziNamePick) Pick(info balancer.PickInfo) (r balancer.PickResult, err error) {
	l.mu.Lock()
	defer l.mu.Unlock()
	index := rand.Intn(len(l.subConn))
	r.SubConn = l.subConn[index]
	return r, nil
}

重试

官方设计文档
grpc客户端的重试机制主要以配置为主

{
"methodConfig": [{
  "name": [{"service": "main.ServerService","method":"GetServerTime"}],
  "retryPolicy": {
          "MaxAttempts": 4,
          "InitialBackoff": ".5s",
          "MaxBackoff": "10s",
          "BackoffMultiplier": 5.0,
          "RetryableStatusCodes": [ "UNAVAILABLE" ]
  }
}]}

name 指定下面的配置信息作用的 RPC 服务或方法

  • service : 通过服务名匹配,语法为 package.service 。 package 就是 proto 文件中指定的 package , service 是 proto 文件中指定的 service 。
  • method : 匹配具体方法,值为 proto 文件中定义的 RPC 名称。

gRPC 的重试机制使用了退避算法,即失败一次,等待一定时间后再次尝试,如果还是失败,则等待更久的时间再次尝试,直到达到最大尝试次数或者最大尝试时间。重试策略配置如下:

  • MaxAttempts : 最大尝试次数
  • InitialBackoff : 默认退避时间
  • MaxBackoff : 最大退避时间
  • BackoffMultiplier : 退避时间增加倍率
  • RetryableStatusCodes : 服务端返回什么错误代码才重试

实例代码

func main() {
	grpcConfig := `{
		"methodConfig": [{
		  "name": [{"service": "main.ServerService","method":"GetServerTime"}],
		  "retryPolicy": {
			  "MaxAttempts": 4,
			  "InitialBackoff": ".5s",
			  "MaxBackoff": "10s",
			  "BackoffMultiplier": 5.0,
			  "RetryableStatusCodes": [ "UNAVAILABLE" ]
		  }
		}]}`
	conn, err := grpc.Dial(
		"dns:///127.0.0.1:9000",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(grpcConfig),
		grpc.WithBlock(),
	)
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()
	client := NewServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	for i := 0; i < 10; i++ {
		res, err := client.GetServerTime(ctx, &Request{ClientName: "client"})
		if err != nil {
			log.Fatal("msg", err)
		}
		log.Println(res)
	}
}

健康检查

设计文档
服务端健康检查实际上是服务端提供了一个接口,包含Check、Watch方法(前者是检查一元模式的服务状态、后者是检查服务端流的状态)。不过grpc提供了开箱即用的健康检查api
参考官方例子

健康检查+自定义负载均衡策略(在注册的时候可以设置是否健康检查)

压缩

grpc支持指定压缩方法对传输内容进行处理。grpc官方提供了gzip。

服务端

导包就可以了

_ "google.golang.org/grpc/encoding/gzip" // Install the gzip compressor

客户端

1.每次rpc调用时传参
只是对标记了压缩的rpc调用才进行压缩处理

func main() {
	conn, err := grpc.Dial(
		"dns:///127.0.0.1:9000",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
	)
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()
	client := NewServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	res, err := client.GetServerTime(ctx, &Request{ClientName: "client"}, grpc.UseCompressor(gzip.Name))
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println(res)
	time.Sleep(1 * time.Second)
}

2.直接在连接上设置
每次请求都将对内容进行压缩处理

func main() {
	conn, err := grpc.Dial(
		"dns:///127.0.0.1:9000",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
		grpc.WithDefaultCallOptions(
			grpc.UseCompressor(gzip.Name),
		),
	)
	if err != nil {
		log.Fatal("msg", err)
	}
	defer conn.Close()
	client := NewServerServiceClient(conn)
	ctx := metadata.AppendToOutgoingContext(context.Background(), "grpc-name", "pingwazi")
	res, err := client.GetServerTime(ctx, &Request{ClientName: "client"})
	if err != nil {
		log.Fatal("msg", err)
	}
	log.Println(res)
	time.Sleep(1 * time.Second)
}

调试

grpcurl

参考官方文档

安装

//1.安装grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
//2.检查安装情况
grpcurl -help

使用

1.请求 -plaintext表使用非TLS请求

//获取服务列表
grpcurl -plaintext grpc地址 -list
//获取方法列表
grpcurl -plaintext grpc地址 -list package.serviceName 
//调用方法(携带参数)
grpcurl -plaintext grpc地址 -d package.serviceName.methodName

grpcui

参考官方文档

安装

//1.安装grpcurl
go install github.com/fullstorydev/grpcui/cmd/grpcui@latest
//2.检查安装情况
grpcui -help