1.介绍 Protobuf
是Protocol Buffers
的简称,它是谷歌公司开发的一种数据描述语言,并于2008年开源。Protobuf
刚开源时的定位类似于XML、JSON
等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。
在序列化结构化数据的机制中,ProtoBuf
是灵活、高效、自动化的,相对常见的XML、JSON
,描述同样的信息,ProtoBuf
序列化后数据量更小、序列化/反序列化速度更快、更简单。
2. XML、JSON、Protobuf对比
json
: 一般的web
项目中,最流行的主要还是json
。因为浏览器对于json
数据支持非常好,有很多内建的函数支持。
xml
: 在webservice
中应用最为广泛,但是相比于json
,它的数据更加冗余,因为需要成对的闭合标签。json
使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
protobuf
:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf
是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
XML
JSON
Protocol Buffers
数据保存方式
文本
文本
二进制
可读性
较好
较好
不可读
解析速度
慢
一般
快
语言支持
所有语言
所有语言
所有语言
使用范围
文件存储、数据交互
文件存储、数据交互
文件存储、数据交互
3. 定义语法 3.1 示例 syntax = "proto3" ;package msg;option go_package = "server/msg" ;message FirstMsg { int32 id = 1 ; string name=2 ; string age=3 ; }
3.2 示例说明
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],因为是预留信息,如果使用,编译时会报警。
3.3 简单类型与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高效。
4. 保留标识符(reserved
)
什么是保留标示符?reserved 标记的标识号、字段名,都不能在当前消息中使用。
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 ; }
5. 枚举类型 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语法,枚举类的第一个值总是默认值.
6.嵌套消息 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 ; }
7.map类型消息 7.1 protobuf
源码 syntax = "proto3" ;option go_package = "server/demo" ;message DemoMapMsg { int32 userId = 1 ; map<string ,string > like =2 ; }
7.2 生成Go
代码 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"` }
8.切片(数组)类型消息 8.1 protobuf
源码 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 ; }
8.2 生成Go
代码 ...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"` } ...
9. 引入其他proto
文件 9.1 被引入文件class.proto
文件位置:proto/class.proto
syntax="proto3" ;package dto;option go_package = "grpc/server/dto" ;message ClassMsg { int32 classId = 1 ; string className = 2 ; }
9.2 使用引入文件user.proto
文件位置:proto/user.proto
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 ; }
9.3 Idea提示:Cannot resolve import..
10.定义服务(Service
)
上面学习的都是怎么定义一个消息类型,如果想要将消息类型用在RPC(远程方法调用)系统中,需要使用关键字(service
)定义一个RPC服务接口,使用rpc
定义具体方法,而消息类型则充当方法的参数和返回值。
10.1 编写service
文件位置:proto/hello_service.proto
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) ; }
10.2 生成Go
代码
使用命令:**protoc --go_out=. --go-grpc_out=. proto/hello_service.proto
**生成代码。
上述命令会生成很多代码,我们的工作主要是要实现SayHello
方法,下面是生成的部分代码。
1. 客户端部分代码 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 }
2. 服务端部分代码 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" ) }
10.3 实现服务端(SayHello
)方法 func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error ) { return &HelloResult{Result: fmt.Sprintf("%s say %s" ,p.GetName(),p.GetContext())},nil }
10.4 运行服务 1. 编写服务端 package mainimport ( "52lu/go-rpc/grpc/server" "fmt" "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) }
2. 编写客户端 package mainimport ( "52lu/go-rpc/grpc/server" "context" "fmt" "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 word!" , }) if err != nil { fmt.Println("请求失败:" , err) return } fmt.Printf("%+v\n" , result) }
3.启动运行 ➜ go run server.go 服务启动成功! ➜ go run client.go result:"张三 say hello word!"
11. oneof(只能选择一个) 11.1 编写proto
修改上面的服务中的请求参数:HelloParam
,
message HelloParam { string name = 1 ; string context = 2 ; oneof option { int32 age= 3 ; string gender= 4 ; } }
11.2 使用 1.客户端传参 生成Go
代码后,入参只能设置其中一个值,如下
...省略 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) ...
2. 服务端接收参数 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 }
12. Any 12.1 编写proto
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 ; }
12.2 使用 1. 客户端传参 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 }
2. 服务端接收参数 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 }