介绍
ProtoBuf
是 Protocol Buffers
的简称,它是Google开发的一种数据描述语言,于2008年开源。 ProtoBuf
刚开源时的定位类似于 XML、JSON
等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。
在序列化结构化数据的机制中, ProtoBuf
是灵活、高效、自动化的,相对常见的 XML、JSON
,描述同样的信息, ProtoBuf
序列化后数据量更小、序列化/反序列化速度更快、更简单。
XML、JSON、ProtoBuf对比
json
: 一般的 web
项目中,最流行的主要还是 json
。因为浏览器对于 json
数据支持非常好,有很多内建的函数支持。
xml
: 在 webservice
中应用最为广泛,但是相比于 json
,它的数据更加冗余,因为需要成对的闭合标签。 json
使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
protobuf
: 是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为 profobuf
是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
|
XML |
JSON |
ProtoBuf |
数据保存方式 |
文本 |
文本 |
二进制 |
可读性 |
较好 |
较好 |
不可读 |
解析速度 |
慢 |
一般 |
快 |
语言支持 |
所有语言 |
所有语言 |
所有语言 |
使用范围 |
文件存储、数据交互 |
文件存储、数据交互 |
文件存储、数据交互 |
定义语法
示例
1 2 3 4 5 6 7 8 9 10 11 12 13
| syntax = "proto3";
package msg;
option go_package = "server/msg";
message FirstMsg { int32 id = 1; string name = 2; string age = 3; }
|
示例说明
syntax
: 用来标记当前使用 proto
的哪个版本。
package
: 包名,用来防止消息类型命名冲突。
option go_package
: 选项信息,代表生成后的 go
代码包路径。
message
: 声明消息的关键字,类似 Go
语言中的 struct
。
- 定义字段语法: 类型 字段名 标识号
标识号说明
- 每个字段都有唯一的一个数字标识符,一旦开始使用就不能够再改变。
- [1, 15]之内的标识号在编码的时候会占用一个字节。[16, 2047]之内的标识号则占用2个字节。
- 最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999],因为是预留信息,如果使用,编译时会报警。
简单类型与Go对应
.proto |
Go |
Notes |
double |
float64 |
|
float |
float32 |
|
int32 |
int32 |
对于负值的效率很低,如果有负值,使用sint64 |
uint32 |
uint32 |
使用变长编码 |
uint64 |
uint64 |
使用变长编码 |
sint32 |
int32 |
负值时比int32高效的多 |
sint64 |
int64 |
使用变长编码,有符号的整型值。编码时比通常的int64高效。 |
fixed32 |
uint32 |
总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 |
uint64 |
是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
保留标识符( reserved
)
什么是保留标示符?reserved 标记的标识号、字段名,都不能在当前消息中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| syntax = "proto3"; package demo;
message DemoMsg { reserved 1, 2, 10 to 13; reserved "test", "name"; string name = 3; int32 id = 11; }
message Demo2Msg { int32 id = 1; string name = 2; }
|
枚举类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| syntax = "proto3"; package demo;
option go_package = "server/demo";
message DemoEnumMsg { enum Gender{ UnKnown = 0; Body = 1; Girl = 2; } Gender sex = 2; }
message DemoTwoMsg{ enum Animal { option allow_alias = true; Other = 0; Cat = 1; Dog = 2; WhiteCat = 1; } }
|
每个枚举类型必须将其第一个类型映射为0, 原因有两个:1.必须有个默认值为0; 2.为了兼容proto2语法,枚举类的第一个值总是默认值.
嵌套消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| syntax = "proto3"; option go_package = "server/nested";
message UserInfo { int32 userId = 1; string userName = 2; } message Common { message ClassInfo{ int32 classId = 1; string className = 2; } }
message NestedDemoMsg { UserInfo userInfo = 1; Common.ClassInfo classInfo = 2; }
|
map类型消息
protobuf
源码
1 2 3 4 5 6 7 8
| syntax = "proto3"; option go_package = "server/demo";
message DemoMapMsg { int32 userId = 1; map<string, string> like = 2; }
|
生成Go代码
1 2 3 4 5 6 7 8 9 10 11
| ...
type DemoMapMsg struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields UserId int32 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"` Like map[string]string `protobuf:"bytes,2,rep,name=like,proto3" json:"like,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } ...
|
切片(数组)类型消息
protobuf
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| syntax = "proto3"; option go_package = "server/demo";
message DemoSliceMsg { repeated int32 id = 1; repeated string name = 2; repeated float price = 3; repeated double money = 4; }
|
生成 Go
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ...
type DemoSliceMsg struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id []int32 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"` Name []string `protobuf:"bytes,2,rep,name=name,proto3" json:"name,omitempty"` Price []float32 `protobuf:"fixed32,3,rep,packed,name=price,proto3" json:"price,omitempty"` Money []float64 `protobuf:"fixed64,4,rep,packed,name=money,proto3" json:"money,omitempty"` } ...
|
引入其他 proto
文件
被引入文件 class.proto
文件位置: proto/class.proto
1 2 3 4 5 6 7 8 9 10
| syntax = "proto3";
package dto;
option go_package = "grpc/server/dto";
message ClassMsg { int32 classId = 1; string className = 2; }
|
使用引入文件 user.proto
文件位置: proto/user.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| syntax = "proto3";
import "proto/class.proto";
option go_package = "grpc/server/dto";
package dto;
message UserDetail{ int32 id = 1; string name = 2; string address = 3; repeated string likes = 4; ClassMsg classInfo = 5; }
|
定义服务( Service
)
上面学习的都是怎么定义一个消息类型,如果想要将消息类型用在RPC(远程方法调用)系统中,需要使用关键字( service
)定义一个RPC服务接口,使用 rpc
定义具体方法,而消息类型则充当方法的参数和返回值。
编写 service
文件位置: proto/hello_service.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| syntax = "proto3";
option go_package = "grpc/server";
message HelloParam{ string name = 1; string context = 2; }
message HelloResult { string result = 1; }
service HelloService{ rpc SayHello(HelloParam) returns (HelloResult); }
|
生成 Go
代码
使用命令: protoc --go_out=. --go-grpc_out=. proto/hello_service.proto
生成代码。
上述命令会生成很多代码,我们的工作主要是要实现SayHello方法,下面是生成的部分代码。
- 客户端部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...
type HelloServiceClient interface { SayHello(ctx context.Context, in *HelloParam, opts ...grpc.CallOption) (*HelloResult, error) }
func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloParam, opts ...grpc.CallOption) (*HelloResult, error) { out := new(HelloResult) err := c.cc.Invoke(ctx, "/HelloService/SayHello", in, out, opts...) if err != nil { return nil, err } return out, nil } ...
|
- 服务端部分代码
1 2 3 4 5 6 7 8 9 10 11
| ...
type HelloServiceServer interface { SayHello(context.Context, *HelloParam) (*HelloResult, error) mustEmbedUnimplementedHelloServiceServer() }
func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloParam) (*HelloResult, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") } ...
|
实现服务端( SayHello
)方法
1 2 3 4
| func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) { return &HelloResult{Result: fmt.Sprintf("%s say %s", p.GetName(), p.GetContext())}, nil }
|
运行服务
- 编写服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
package main
import ( "fmt" "go-advanced/grpc/2/grpc/server" "google.golang.org/grpc" "net" )
func main() { rpcServer := grpc.NewServer() server.RegisterHelloServiceServer(rpcServer, new(server.UnimplementedHelloServiceServer)) listen, err := net.Listen("tcp", ":1234") if err != nil { fmt.Println("服务启动失败", err) return } fmt.Println("服务启动成功") _ = rpcServer.Serve(listen) }
|
- 编写客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
package main
import ( "context" "fmt" "go-advanced/grpc/2/grpc/server" "google.golang.org/grpc" )
func main() { dial, err := grpc.Dial("127.0.0.1:1234", grpc.WithInsecure()) if err != nil { fmt.Println("Dial Error ", err) return } defer dial.Close() client := server.NewHelloServiceClient(dial) result, err := client.SayHello(context.TODO(), &server.HelloParam{ Name: "张三", Context: "hello world!", }) if err != nil { fmt.Println("请求失败:", err) return } fmt.Printf("%+v\n", result) }
|
- 启动运行
1 2 3 4 5 6 7
| ➜ 2 go run server.go 服务启动成功
➜ 2 go run client.go result:"张三 say hello world!"
|
oneof(只能选择一个)
编写 proto
修改上面的服务中的请求参数: HelloParam
1 2 3 4 5 6 7 8 9 10
| message HelloParam{ string name = 1; string context = 2; oneof option { int32 age = 3; string gender = 4; } }
|
使用
1.客户端传参
生成 Go
代码后,入参只能设置其中一个值,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...
client := server.NewHelloServiceClient(dial) reqParam := &server.HelloParam{ Name: "张三", Context: "hello word!", }
reqParam.Option = &server.HelloParam_Age{Age: 19}
result, err := client.SayHello(context.TODO(), reqParam) ...
|
- 服务端接收参数
1 2 3 4 5
| func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) { res := fmt.Sprintf("name:%s| gender:%s| age:%s | context:%s", p.GetName(), p.GetGender(),p.GetAge(), p.GetContext()) return &HelloResult{Result:res,}, nil }
|
Any
编写 proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| syntax = "proto3";
option go_package = "grpc/server";
import "google/protobuf/any.proto";
message Context { int32 id = 1; string title = 2; }
message HelloParam{ google.protobuf.Any data = 1; }
message HelloResult { string result = 1; }
|
使用
- 客户端传参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ...
content := &server.Context{ Id: 100, Title: "Hello Word", }
any, err := anypb.New(content) if err != nil { fmt.Println("any 类型参数序列化失败") return }
reqParam := &server.HelloParam{Data: any}
result, err := client.SayHello(context.TODO(), reqParam) if err != nil { fmt.Println("请求失败:", err) return } ...
|
- 服务端接收参数
1 2 3 4 5 6 7 8
| func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) { var context Context p.Data.UnmarshalTo(&context) res := fmt.Sprintf("%v|%v", context.GetId(), context.GetTitle()) return &HelloResult{Result: res}, nil }
|