Go - protobuf与gRPC入门使用
一、Protobuf介绍
Protobuf 是一种语言中立、平台无关、可扩展的序列化数据的格式,可用于通信协议,数据存储等。
1.1 Protobuf优点
ProtoBuf 序列化数据方面它是灵活的、高效的。相比于 XML 来说,ProtoBuf 更加小巧、更加快速、更加简单。
一旦定义了要处理的数据的数据结构之后,就可以利用 ProtoBuf 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 ProtoBuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
ProtoBuf 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
如果想看详细的介绍可以查看:Protobuf 官方文档
1.2 Protobuf的安装
-
下载protobuf的安装包
选择对应的系统下载安装包: GitHub下载地址
windows:下载完成后配置环境变量即可好环境变量即可
打开cmd,输入protoc --version
,如果输出版本,说明安装成功。
- 安装Go的protobuf 库
go
go get -u github.com/golang/protobuf/protoc-gen-go
注意,protoc-gen-go 将自动安装到 $GOPATH/bin
目录下,也需要将这个目录加入到环境变量。
- 安装Go的gRPC的
go
go get google.golang.org/grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
二、Protobuf简单语法
这里以一个简单的示例演示简单语法使用
2.1 编写一个pb文件
我们这里创建个person.proto 文件
,将以下的经典的示例写法写入
go
syntax = "proto3";
option go_package = "/person";
package pb;
message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}
这个.proto文件中定义了一个名为 Person 的消息类型,包含了 name、age 和 hobbies 三个字段。
-
name 和 age 都是普通的单值类型字段,hobbies 是一个字符串数组类型字段。
-
每个字段都有一个唯一的标签号,用于标识这个字段在二进制编码中的位置和类型。
2.2 使用protoc生成GO代码
在此文件的目录下,运行protoc --go_out=. *.proto
命令,即可生成GO代码。
运行后,我们可以看到该目录下多出了一个person文件夹,里面包含 Go 文件 person.pb.go
。这个文件内部定义了一个结构体 Person
go
type Person struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Hobbies []string `protobuf:"bytes,3,rep,name=hobbies,proto3" json:"hobbies,omitempty"`
}
除了结构体外,还有很多方法,这些方法提供了对 Protocol Buffers 消息进行编码、解码和操作的基础设施.
有以下几个主要的方法。
- func ( *Person) Reset(): 将 Person 消息重置为默认值。
- func (*Person) String() string: 返回一个字符串,包含 Person 消息的文本表示形式。
- func (*Person) ProtoMessage(): 使 Person 结构体实现 proto.Message 接口,这是在序列化和反序列化 Protobuf 消息时所需的。
- func (*Person) Descriptor() ([]byte, []int): 返回关于 Person 消息类型的描述符信息。
- func ( *Person) GetName() string: 返回 Person 消息中 Name 字段的值。
- func (*Person) GetAge() int32: 返回 Person 消息中 Age 字段的值。
列出一个常用的protoc的命令:
protoc --proto_path=client/pb/. --go_out=plugins=grpc:./client/code client/pb/person.proto
-
--proto_path: 当前命令扫描的pb文件目录
-
--go_out=:输出编译的go文件
-
client/pb/person.proto:表示当前编译的pb文件
2.3 序列化与反序列化
序列化和反序列化函数在github.com/golang/protobuf/proto
包中
go
func TestPersonSerialization(t *testing.T) {
// 创建一个 Person 消息实例并设置其字段
p:=&Person{Name: "yzy",Age: 23,Hobbies: []string{"music","sport"}}
// 将消息序列化为二进制格式
data, err := proto.Marshal(p)
if err != nil {
t.Fatal("marshaling error: ", err)
}
// 反序列化消息
p2 := &Person{}
err = proto.Unmarshal(data, p2)
if err != nil {
t.Fatal("unmarshaling error: ", err)
}
// 比较原始消息和反序列化后的消息
if p.String()!=p2.String() {
t.Fatalf("original message %v != unmarshaled message %v", p, p2)
}
}
如果想链接跟多字段的使用可以参考:Go Protobuf 的其它字段类型)
2.4 公共包处理方式
如:有一个公共pb文件common.proto
, 对于公共文件包可以新起一个Github项目单独生成PB代码,在实际项目中引用该公共的PB文件。
- 创建一个空项目first
- 在项目目录创建common.proto
go
syntax = "proto3";
package pb;
option go_package = "./common";
import "google/protobuf/descriptor.proto";
extend google.protobuf.ServiceOptions {
optional uint32 service_option_id = 10001;
}
extend google.protobuf.MethodOptions {
optional uint32 method_option_id = 10002;
}
extend google.protobuf.MethodOptions {
optional string http_option_path = 10003;
}
message Dto {
string name = 1;
}
-
使用命令生成公共文件pb的代码
protoc --go_out=. *.proto
go
first
└─common
| common.pb.go
│ common.proto
│ go.mod
-
上传公共项目至Github上
-
业务引用使用该公共PB
注意: 引用公共pb后需要讲其go_packge的改为github对应的地址 , 并且在使用protoc编译时不需要公共proto文件。生成的代码中会自动引入Github的公共项目。
2.5 protobuf拓展协议
2.5.1 Extend拓展
扩展是在其消息体之外定义的字段,通常位于基础 .proto 与 消息体.proto文件文件中进行分开。
为什么使用扩展?
让消息体的.proto文件将具有较少的导入/依赖项,缩短构建时间,打破循环依赖,促进松散耦合。
允许系统以最小的依赖性和协调将数据附加到容器消息。
您的用例需要对大量扩展进行非常低的协调,请考虑改用 Any消息类型
拓展示例
- 创建基础person.proto文件 与 student.proto文件
go
// person.proto
syntax = "proto2";
package person;
option go_package = "./person";
message Person {
optional string id_code = 1;
optional string name = 2;
extensions 100 to 190; // 定义的拓展范围
}
go
// student.proto
syntax = "proto2";
package student;
option go_package = "./student";
import "person.proto";
// 拓展person
extend person.Person {
optional int32 age = 100;
}
-
使用命令编译文件
protoc --go_out=. *.proto
-
设置/获取拓展字段值
go
func main() {
p1 := &person.Person{}
proto.SetExtension(p1, student.E_Age, int32(10))
extension := proto.GetExtension(p1, student.E_Age)
fmt.Println(extension.(int32))
}
2.5.2 字段选项
Protocol Buffers 还允许定义和使用自己的选项进行描述PB ,主要拓展的是google/protobuf/descriptor.proto的选项
- google.protobuf.MessageOptions 消息描述选项
- google.protobuf.OneofOptions 可选字段选项
- google.protobuf.EnumOptions 枚举选项
- google.protobuf.EnumValueOptions 枚举值选项
- google.protobuf.FileOptions 字段选项
- google.protobuf.serviceOptions 服务选项
- google.protobuf.Methodoptions 方法选项
该拓展可以用于自定义一些PB的服务,方法,字段,枚举等相关的自定义的描述。
如服务id、方法id、方法路由、字段的validate检验规则的定义等。
选项示例
- proto文件中定义拓展选项
go
syntax = "proto2";
package student;
option go_package = "./student";
import "google/protobuf/descriptor.proto";
import "person.proto";
extend person.Person {
optional int32 age = 100;
}
extend google.protobuf.ServiceOptions {
optional uint32 service_option_id = 10001;
}
extend google.protobuf.MethodOptions {
optional uint32 method_option_id = 10002;
}
extend google.protobuf.MethodOptions {
optional string http_option_path = 10003;
}
message PersonRequest {
optional string IdNum = 1;
}
message PersonResponse {
optional person.Person person = 1;
}
service StudentService {
option (service_option_id) = 12345;
rpc GetPersonInfo (PersonRequest) returns (PersonResponse) {
option (method_option_id) = 1;
option (http_option_path) = "get/person-info";
};
}
- 代码中读取解析拓展选项字段
go
func main() {
studentProto := student.File_student_proto.Services()
fmt.Println(proto.GetExtension(studentProto.ByName("StudentService").Options(), student.E_ServiceOptionId))
methods := studentProto.ByName("StudentService").Methods()
for i := 0; i < methods.Len(); i++ {
method := methods.Get(i)
methodId := proto.GetExtension(method.Options(), student.E_MethodOptionId)
path := proto.GetExtension(method.Options(), student.E_HttpOptionPath)
fmt.Println(fmt.Sprintf("%v %v %v", method.Name(), methodId, path))
}
}
三、gRPC的使用
gRPC是什么可以用官网的一句话来概括, A high-performance, open-source universal RPC framework 一个高性能、开源的通用 RPC 框架。
如果你想了解更多详情可以参考:grpc-go
3.1 服务定义
创建person.proto文件且编写如下内容:
go
syntax = "proto3";
package pb;
option go_package = "/person";
message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}
message PersonRequest {
string IdNum = 1;
}
message PersonResponse {
Person person = 1;
}
service PersonService {
rpc GetPersonInfo (PersonRequest) returns (PersonResponse);
}
在文件下执行 protoc --go_out=plugins=grpc:. *.proto
该命令会生成对应的grpc客户端请求的接口 、服务端需要实现的方法示例接口
3.2 服务端与客户端
3.2.1 服务端代码
- 当上面输入命令后会生
person.pb.go
文件,需要自行待实现的接口
go
type PersonServiceServer interface {
GetPersonInfo(context.Context, *PersonRequest) (*PersonResponse, error)
}
-
实现服务端接口
创建service/person.go文件,并实现上面的自动生成的接口中的方法
go
type PersonServiceImpl struct{}
func (p *PersonServiceImpl) GetPersonInfo(ctx context.Context, request *person.PersonRequest) (*person.PersonResponse, error) {
response := &person.PersonResponse{
Person: &person.Person{
Name: "test",
Age: 10,
Hobbies: []string{"打篮球", "看书"},
},
}
return response, nil
}
-
注册服务端代码
创建service/main.go文件,进行启动服务,并注册方法实例
go
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 8081))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建一个grpc Server服务对象
s := grpc.NewServer()
// 注册服务
person.RegisterPersonServiceServer(s, &PersonServiceImpl{})
// 启动RPC并监听
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
3.2.2 客户端代码
- 创建client/main.go文件进行调用服务端的接口
go
func main() {
conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 调用接口
info, err := person.NewPersonServiceClient(conn).GetPersonInfo(ctx, &person.PersonRequest{
IdNum: "15",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(info)
}
3.2.3 添加头信息
grpc可以通过MD,metadata进行定义header头部信息
-
客户端请求添加header
修改文件client/main.go
go
func main() {
conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 添加头部信息
ctx := metadata.NewOutgoingContext(context.Background(), map[string][]string{
"operator_id": {"110"},
"operator_name": {"admin"},
})
info, err := person.NewPersonServiceClient(conn).GetPersonInfo(ctx, &person.PersonRequest{IdNum: "15"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(info)
}
-
服务端获取请求header
修改文件service/person.go
go
type PersonServiceImpl struct{}
func (p *PersonServiceImpl) GetPersonInfo(ctx context.Context, request *person.PersonRequest) (*person.PersonResponse, error) {
// 从上下文中读取header
incomingContext, ok := metadata.FromIncomingContext(ctx)
fmt.Println(ok, incomingContext["operator_id"], incomingContext["operator_name"])
response := &person.PersonResponse{
Person: &person.Person{
Name: "test",
Age: 10,
Hobbies: []string{"打篮球", "看书"},
},
}
return response, nil
}