环境初始化
安装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文件
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);
}
编码
每一个字段都需要有一个数字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
轮询,循环使用
自定义负载均衡策略
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