gRPC是互联网后台常用的RPC框架,而protobuf是一个常用的通信协议,而gRPC中,protobuf常用作其服务间的协议通信,因此很有必要一块掌握这两个技术点。

Protobuf

protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。protobuf 是以二进制方式存储,占用空间小,但也带来了可读性差的缺点(二进制协议,因为不可读而难以调试,不好定位问题)。

在序列化协议中,JSON,protobuf以及msgpack都是业界常用的协议,我经历的项目都有用到。我经历的团队里,QQ音乐,全民K歌用的是内部开发的JCE协议,只是protobuf换皮的自研协议而已。而梦幻西游微服务使用protobuf作为内部服务通信协议。正因为protobuf的轻量级以及效率极其优秀,因此在众多后台项目中广泛使用。在对外的接口,我们用http协议支持对方的服务调用,而对内的服务间rpc调用,我们倾向于使用protobuf这种轻量级、效率优先的协议。

安装

macos安装probubuf步骤如下:

  • brew install protobuf // protobuf项目库
  • go get -u github.com/golang/protobuf/protoc-gen-go // 安装protobuf转go的工具

检查是否安装完成

  1. junshideMacBook-Pro:~ junshili$ protoc --version
  2. libprotoc 3.13.0

协议定义

protobuf协议定义,这里我们新建一个user.proto协议,里面新增了Student结构体。

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本


option go_package ="pb/proto_demo"; //包名


message Student {
  string name = 1;
  bool male = 2;
  repeated int32 scores = 3;
  map<string, int32> subject = 4;
}

语法相关:

  1. protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = “proto3” 标明版本。
  2. package,即包名声明符是可选的,用来防止不同的消息类型有命名冲突。如果需要指定不一样的包名,可以使用go_package选项
  3. repeated 表示字段可重复,即用来表示 Go 语言中的数组类型。
  4. 每个字符 =后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。
  5. 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端
  6. 文件名使用小写下划线的命名风格,例如 lower_snake_case.proto
  7. 命名规范:
  • 消息名使用首字母大写驼峰风格(CamelCase),例如message StudentRequest;
  • 字段名使用小写下划线的风格,例如 string status_code = 1;
  • 枚举类型,枚举名使用首字母大写驼峰风格,例如 enum FooBar,枚举值使用全大写下划线隔开的风格(CAPITALS_WITH_UNDERSCORES ),例如 FOO_DEFAULT=1

protobuf转go

进入到proto/目录下,进行协议转go代码

protoc --go_out=. *.proto

生成go的协议文件proto/pb/proto_demo/user.pb.go

文件组织结构

web
├── go.mod
├── go.sum
├── main.go
├── proto
│   ├── pb
│   │   └── proto_demo
│   │       └── user.pb.go
│   └── user.proto

proto转化的go代码,无需手动修改


// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//     protoc-gen-go v1.26.0
//     protoc        v3.13.0
// source: user.proto

package proto_demo

import (
    protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    reflect "reflect"
    sync "sync"
)

const (
    // Verify that this generated code is sufficiently up-to-date.
    _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
    // Verify that runtime/protoimpl is sufficiently up-to-date.
    _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type Student struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name    string           `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Male    bool             `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
    Scores  []int32          `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
    Subject map[string]int32 `protobuf:"bytes,4,rep,name=subject,proto3" json:"subject,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}

func (x *Student) Reset() {
    *x = Student{}
    if protoimpl.UnsafeEnabled {
        mi := &file_user_proto_msgTypes[0]
        ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
        ms.StoreMessageInfo(mi)
    }
}

func (x *Student) String() string {
    return protoimpl.X.MessageStringOf(x)
}

func (*Student) ProtoMessage() {}

func (x *Student) ProtoReflect() protoreflect.Message {
    mi := &file_user_proto_msgTypes[0]
    if protoimpl.UnsafeEnabled && x != nil {
        ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
        if ms.LoadMessageInfo() == nil {
            ms.StoreMessageInfo(mi)
        }
        return ms
    }
    return mi.MessageOf(x)
}

// Deprecated: Use Student.ProtoReflect.Descriptor instead.
func (*Student) Descriptor() ([]byte, []int) {
    return file_user_proto_rawDescGZIP(), []int{0}
}

func (x *Student) GetName() string {
    if x != nil {
        return x.Name
    }
    return ""
}

func (x *Student) GetMale() bool {
    if x != nil {
        return x.Male
    }
    return false
}

func (x *Student) GetScores() []int32 {
    if x != nil {
        return x.Scores
    }
    return nil
}

func (x *Student) GetSubject() map[string]int32 {
    if x != nil {
        return x.Subject
    }
    return nil
}




...

后续代码无需关注

Protobuf序列化和反序列化的实践

下面给出一个go使用protobuf对对象进行序列化以及反序列化的一个实战例子,用的prttobuf协议是我们上面刚定义好的Student.proto。

main.go的调用,验证序列化前后数据是否一致

package main

import (
    "fmt"
    "github.com/golang/protobuf/proto"
    "web_demo/proto/pb/proto_demo"
)

func main() {
    test := &proto_demo.Student {
        Name: "James",
        Male: true,
        Scores: []int32{98, 85, 88},
        Subject: map[string]int32{"age":18, "level":1},
    }

    // 序列化
    data, err := proto.Marshal(test)
    if err != nil {
        fmt.Println("proto encode error: ", err)
        return
    }

    // 反序列化
    newTest := &proto_demo.Student{}
    err = proto.Unmarshal(data, newTest)
    if err != nil {
        fmt.Println("proto decode error: ", err)
    }

    if test.GetScores()[1] != newTest.GetScores()[1] {
        fmt.Printf("data mismatch score %d != %d", test.GetScores()[1], newTest.GetScores()[1])
        return
    }
    if test.GetName() != newTest.GetName() {
        fmt.Printf("data mismatch name %s != %s", test.GetName(), newTest.GetName())
        return
    }
    fmt.Println("data match!")
}

gRPC

gRPC的特点

  • gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统,基于HTTP2协议标准设计开发
  • gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers
  • 支持多种开发语言

1.png

一次RPC的完整流程:

  1. 客户端(gRPC Sub)调用 A 方法,发起 RPC 调用
  2. 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)
  3. 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回
  4. 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)
  5. 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果

我们在protobuf协议定义上扩展一个类型定义:Service,这在RPC通讯上有着重要作用。

gRpc实战例子

先定义一个hello服务协议,注意Hello结构体内声明了SayHello和SayHi两个接口,这两个接口需要在server侧实现,client侧会发起rpc直接调用。

syntax = "proto3"; // 指定proto版本

// 指定golang包名
option go_package = "pb/proto_demo";

// 定义Hello服务
service Hello {
    // 定义SayHello方法
    rpc SayHello(HelloRequest) returns (HelloResponse) {}
    rpc SayHi(HiRequest) returns (HiResponse) {}
}

// HelloRequest 请求结构
message HelloRequest {
    string name = 1;
}

// HelloResponse 响应结构
message HelloResponse {
    string message = 1;
}

// HiRequest 请求结构
message HiRequest {
    string name = 1;
    string school = 2;
    int32 age = 3;
    int32 grade = 4;
    int32 status = 5;
}

// HiResponse 响应结构
message HiResponse {
    string message = 1;
    int32 status = 2;
}

进入到proto文件夹,执行指令生成grpc版本的协议go代码

protoc -I . --go_out=plugins=grpc:. ./hello.proto

生成了文件proto/pb/hello.pb.go,项目组织如下:

web
├── client
│   └── main.go
├── go.mod
├── go.sum  
├── proto
│   ├── hello.proto
│   ├── pb
│   │   └── proto_demo
│   │       ├── hello.pb.go
│   │       └── user.pb.go
│   └── user.proto
└── server
    └── main.go

此时我们先编写服务器一侧的service:server/main.go,主要功能是定义好监听地址端口,定义service相关函数SayHello和SayHi。

package main

// server

import (
    "fmt"
    "net"
    "web_demo/proto/pb/proto_demo"
    "google.golang.org/grpc"
    "golang.org/x/net/context"
    "google.golang.org/grpc/grpclog"
)

const (
    // gRPC服务地址
    Address = "127.0.0.1:9988"
)

type helloService struct {}

var HelloService = helloService{}

func (h helloService) SayHello(ctx context.Context, in *proto_demo.HelloRequest) (*proto_demo.HelloResponse, error) {
    resp := new(proto_demo.HelloResponse)
    resp.Message = fmt.Sprintf("Hello %s.", in.Name)

    return resp, nil
}

func (h helloService) SayHi(ctx context.Context, in *proto_demo.HiRequest) (*proto_demo.HiResponse, error) {
    resp := new(proto_demo.HiResponse)
    resp.Message = fmt.Sprintf("Hi %s, grade=%d, school=%s, grade=%d, status=%d", in.Name, in.Grade, in.School, in.Grade, in.Status)

    return resp, nil
}

func main() {
    listen, err := net.Listen("tcp", Address)
    if err != nil {
        grpclog.Fatalf("Failed to listen: %v", err)
    }

    s := grpc.NewServer()

    proto_demo.RegisterHelloServer(s, HelloService)
    fmt.Println("Listen on " + Address)
    grpclog.Println("Listen on " + Address)
    s.Serve(listen)
}

启动server/main.go,开始监听端口9988

junshideMacBook-Pro:server junshili$ go run main.go 
Listen on 127.0.0.1:9988

开始编写client的发起rpc调用部分,生成文件client/main.go

package main

// client

import (
    "web_demo/proto/pb/proto_demo"
    "google.golang.org/grpc"
    "golang.org/x/net/context"
    "google.golang.org/grpc/grpclog"
    "fmt"
)

const (
    // gRPC服务地址
    Address = "127.0.0.1:9988"
)


func main() {
    conn, err := grpc.Dial(Address, grpc.WithInsecure())
    if err != nil {
        grpclog.Fatalln(err)
    }
    defer conn.Close()

    c := proto_demo.NewHelloClient(conn)

    req := &proto_demo.HelloRequest{Name:"grpc"}
    res, err := c.SayHello(context.Background(), req)
    if err != nil {
        grpclog.Fatalln(err)
    }

    fmt.Println(res.Message)

    req2 := &proto_demo.HiRequest{Name:"grpc", Grade:3, Age:10, Status:2, School:"zhuhai"}
    res2, err := c.SayHi(context.Background(), req2)
    if err != nil {
        grpclog.Fatalln(err)
    }

    fmt.Println(res2.Message)
}

启动client/main.go,连接”127.0.0.1:9988”,请求rpc hello服务

junshideMacBook-Pro:client junshili$ go run main.go 
Hello grpc.
Hi grpc, grade=3, school=zhuhai, grade=3, status=2