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的工具
检查是否安装完成
junshideMacBook-Pro:~ junshili$ protoc --version
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;
}
语法相关:
- protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = “proto3” 标明版本。
- package,即包名声明符是可选的,用来防止不同的消息类型有命名冲突。如果需要指定不一样的包名,可以使用go_package选项
- repeated 表示字段可重复,即用来表示 Go 语言中的数组类型。
- 每个字符 =后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。
- 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端
- 文件名使用小写下划线的命名风格,例如 lower_snake_case.proto
- 命名规范:
- 消息名使用首字母大写驼峰风格(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
- 支持多种开发语言
一次RPC的完整流程:
- 客户端(gRPC Sub)调用 A 方法,发起 RPC 调用
- 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)
- 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回
- 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)
- 客户端接受到服务端响应,解码请求体。回调被调用的 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