相关文档
源码:https://github.com/grpc/grpc-go
官方文档:https://www.grpc.io/docs/what-is-grpc/introduction/
protobuf编译器源码:https://github.com/protocolbuffers/protobuf
proto3文档:https://protobuf.dev/programming-guides/proto3/
用于编译.proto文件,生成对应语言的模板文件
#下载地址
https://github.com/protocolbuffers/protobuf/releases/
windows的话选择对应版本,下载解压后配置对应环境变量
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go- grpc_opt=paths=source_relative .\echo\echo.proto
定义一个搜索相关的proto 消息,请求有 查询字符串,有分页页数,和每页的数量。例子如下
message SearchRequest {
string query = 1; // 查询字符串
optional int32 page_number = 2; // 第几页
optional int32 result_per_page = 3; // 每页的结果数
}
字段有很多数据类型,看个例子
syntax = "proto3";
option go_package = "protos/pbs";
enum Status{
Status1=0;
Status2=1;
Status3=2;
}
message Source{
//金条
int64 Gold =1;
//血条
int64 Blood=2;
}
message Role {
//id
int64 Id =1; //有符号整型
//姓名
string Name=2;//字符串类型
//属性
map Attr=3; //map 类型
//状态
Status typ =4; //枚举类型
//是不是vip
bool IsVip =5; //bool 类型
//资源
Source source=6; //复合类型
}
syntax = "proto3";
option go_package = "protos/pbs";
message Role {
int64 Id =19527;
}
编译会报下面的错
protoc --go_out=. ./*.proto
intro.proto:5:14: Field numbers 19000 through 19999 are reserved for the protocol buffer library implementation.
protoYype | notes | GO type |
---|---|---|
double | *float64 | |
float | *float32 | |
int32 | 可变长编码,负数编码效率低,要经过zigzag | *int32 |
int64 | 可变长编码,负数编码效率低,要经过zigzag | *int64 |
uint32 | 可变长编码 | *uint32 |
uint64 | 可变长编码 | *uint64 |
sint32 | 可变长编码,比int32效率高 | *int32 |
sint64 | 可变长编码,比int64效率高 | *int64 |
fixed32 | 总是4字节,如果经常比228大,那比uint32效率更高 | *uint32 |
fixed64 | 总是4字节,如果经常比256大,那比uint32效率更高 | *uint64 |
sfixed32 | 总是4字节 | *int32 |
sfixed64 | 总是8字节 | *int64 |
bool | *bool | |
string | utf8或7-bit ASCII文本编码 | *string |
bytes | 任意序列字节 | []byte |
解析消息的时候,编码的消息字段没有赋值,将会设置默认值
当我们想定义一个消息类型,只使用定义好的一系列值的一个,我们就可以使用枚举
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus { //定义枚举
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4; //使用枚举
}
注意:
枚举第一个字段必须是0,像上面UNIVERSAL = 0,而且不能省略,原因有两点:
如何给枚举定义别名? 当我们希望两个枚举值一样,但是变量名不一样的时候,我们可以添加allow_alias option,并设置值为true,类似于下面这个样式,要不然编译器会报错。
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1; //直接使用会报错,因为这两个值一样了
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
}
枚举值范围是32位范围内的整数,这是因为是是varint encoding 编码的。对于没有定义的枚举值,在go 和c++中会识别成数字
枚举更新的安全性,官方新增了个枚举保留字段,使用方法如下
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
如果删除枚举定义或者注释来更新枚举类型,将来用户可能不注意去重用该类型的值,如果以后proto buffer 版本更新了,再加载到旧版本,那么可能导致严重问题,包括数据损坏、隐私漏洞等。
官方提供了保留字段,可以保留枚举字段名和枚举字段值,如下,使用保留的字段会报错,超过max 也会报错。
例如下面这个例子
enum TestType {
Hello1=0;
Hello2=1;
Hello3=2;
Hello4=3;
FOO=4; # 使用保留字段
BAR=5;
foo=6;
Bar=7;
Foo1=8;
Hello6=39;
Hello6=40;
Hello5=99; # 超过最大值
reserved 2,3, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
当使用其他消息的时候,如果在本文件,直接使用就可以了。Result代表自定义消息类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
如何在消息里面再定义消息了?例子如下,在SearchResponse定义了个内部消息Result,然后直接引用就可以了。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果其他消息引用消息内部的消息呢?语法为_Parent_.Type
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
在项目开发中,我们有这种需要,将相同的结构放在一个公共文件夹,将请求响应的业务消息放一个文件夹,然后请求响应的proto 会引用通文件夹。我们来写一个例子,文件结构如下。
user_business.proto
syntax = "proto3";
option go_package = "protos/pbs";
import "share/user.proto";
//获取角色信息请求
message GetUserRequest {
}
//获取角色信息响应
message GetUserResponse {
User user=1;
}
user.proto
syntax = "proto3";
option go_package = "protos/pbs";
//用户定义
message User {
string Id=1;
string Name=2;
string Age=3;
}
protoc --go_out=. ./business/*.proto ./share/*.proto
官方说作用是集成proto 没有定义的类型,其实可以理解为go 语言接口类型,可以存任何类型的值,但是跨语言只能通过字节流代表任意类型,所以any 内部实现包含字节流,和标识字节流的唯一url。
用这个关键字,官方说要导入官方proto,类似下面,相信如果直接编译肯定会有坑,编译不过,因为没有any.proto这个文件
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
最后序列化出来的any 结构包含下面两个字段:
TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl,proto3" json:"type_url,omitempty"`
// Must be a valid serialized protocol buffer of the above specified type.
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
如果在平时在一个消息有许多字段,但是最多设置一个字段,我们可以使用oneof 来执行并节省内存。
Oneof 字段类似于常规字段,除了Oneof共享内存的所有字段之外,最多可以同时设置一个字段。设置Oneof 的任何成员都会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于选择的语言。
syntax = "proto3";
option go_package = "protos/pbs";
message SubMessage {
int32 Id=1;
string Age2=2;
}
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
oneof 可以添加任何字段,除了repeated字段
package main
import (
"fmt"
"grpcdemo/protobuf/any/protos/pbs"
)
func main() {
p:=&pbs.SampleMessage{
TestOneof: &pbs.SampleMessage_Name{Name: "hello"},
}
fmt.Println(p)
fmt.Println(p.GetTestOneof())
p.TestOneof=&pbs.SampleMessage_SubMessage{SubMessage: &pbs.SubMessage{Id: 1}}
fmt.Println(p)
fmt.Println(p.GetTestOneof())
}
兼容性问题
添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。
标签重用问题
在数据定义创建map,语法格式为
map map_field = N;
例如,创建一个项目,key 是string,value 是Project
map projects = 3;
PS:
总的来说,map 语法等价于下面的语法,所以protocol buffers 的实现在不支持map 的语言上也能处理数据
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
package提供命名空间,防冲突
package foo.bar;
message Open { ... }
在其他地方引用
message Foo {
...
foo.bar.Open open = 1;
...
}
int32 old_field = 6 [deprecated = true];
proto buffer 提供大多人都不会使用的高级功能-自定义选项。
由于选项是由 google/protobuf/descriptor.proto(如 FileOptions 或 FieldOptions)中定义的消息定义的,因此定义你自己的选项只需要扩展这些消息
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
获取选项
package main
import (
"fmt"
"grpcdemo/protobuf/any/protos/pbs"
)
func main() {
p:=&pbs.MyMessage{}
fmt.Println(p.ProtoReflect().Descriptor().Options())
//[my_option]:"Hello world!"
}
Protocol Buffers可以为每种类型提供选项
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50007;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
oneof qux {
option (my_oneof_option) = 42;
string quux = 3;
}
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
引用其他包的选项需要加上包名
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
每种选项类型(文件级别,消息级别,字段级别等)都有自己的数字空间,例如:可以使用相同的数字声明 FieldOptions 和 MessageOptions 的扩展名。
可变字节长度编码,用一个字节或者多个字节表示整数类型,更小的数占用更小的字节。
编码的理念是:越小的数字花费越少的字节
来看看下面的例子:
00000000 00000000 00000000 00000001 //int32
Base 128 Varints 原理
Base128 Varints 采用的是小端序, 即数字的低位存放在高地址。
比如数字 666, 其以标准的整型存储, 其二进制表示为
而采用 Varints 编码, 其二进制形式为
可以尝试来复原一下上面这个 Base128 Varints 编码的二进制串, 首先看最高有效位
接下来我们移除标识位, 由于 Base128 Varints 采用小端字节序, 因此数字的高位存放于低地址上
移除标志位并交换字节序, 便得到原本的数值 1010011010, 即数字 666
可变长整型编码对于不同大小的数字, 其所占用的存储空间是不同的, 编码思想与 CPU 的间接寻址原理相似, 都是用一比特来标识是否走到末尾, 但采用这种方式存储数字, 也有一个相对不好的点便是, 无法对一个序列的数值进行随机查找, 因为每个数字所占用的存储空间不是等长的, 因此若要获得序列中的第 N 个数字, 无法像等长存储那样在查找之前直接计算出 Offset, 只能从头开始顺序查找
Varints 编码的实质在于去掉数字开头的 0, 因此可缩短数字所占的存储字节数, 在上面的例子中, 我们只举例说明了正数的 Varints 编码, 但如果数字为负数, 则采用 Varints 编码会恒定占用 10 个字节, 原因在于负数的符号位为 1, 对于负数其从符号位开始的高位均为 1, 在 Protobuf 的具体实现中, 会将此视为一个很大的无符号数, 以 Go 语言的实现为例, 对于 int32 类型的 pb 字段, 对于如下定义的 proto
syntax = "proto3";
package pbTest;
message Request {
int32 a = 1;
}
Request 中包含类型为 int32 类型的字段, 当 a 为负数时, 其序列化之后将恒定占用 10 个字节, 我们可以使用如下的测试代码
func main() {
a := pbTest.Request{
A: -5,
}
bytes, err := proto.Marshal(&a)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(fmt.Sprintf("%08b", bytes))
}
对于 int32 类型的数字 -5, 其序列化之后的二进制为
究其原因在于 Protobuf 的内部将 int32 类型的负数转换为 uint64 来处理, 转换后的 uint64 数值的高位全为 1, 相当于是一个 8 字节的很大的无符号数, 因此采用 Base128 Varints 编码后将恒定占用 10 个字节的空间, 可见 Varints 编码对于表示负数毫无优势, 甚至比普通的固定 32 位存储还要多占 4 个字节。Varints 编码的实质在于设法移除数字开头的 0 比特, 而对于负数, 由于其数字高位都是 1, 因此 Varints 编码在此场景下失效
Zigzag 编码便是为了解决这个问题, Zigzag 编码的大致思想是首先对负数做一次变换, 将其映射为一个正数, 变换以后便可以使用 Varints 编码进行压缩, 这里关键的一点在于变换的算法, 首先算法必须是可逆的, 即可以根据变换后的值计算出原始值, 否则就无法解码, 同时要求变换算法要尽可能简单, 以避免影响 Protobuf 编码、解码的速度, 我们假设 n 是一个 32 位类型的数字, 则 Zigzag 编码的计算方式为
(n << 1) ^ (n >> 31)
首先对其进行一次逻辑左移, 移位后空出的比特位由 0 填充
然后对原数字进行 15 次算术右移, 得到 16 位全为原符号位(即 1)的数字
然后对逻辑移位和算术移位的结果按位异或, 便得到最终的 Zigzag 编码
可以看到, 对负数使用 Zigzag 编码以后, 其高位的 1 全部变成了 0, 这样以来我们便可以使用 Varints 编码进行进一步地压缩, 再来看正数的情形, 对于 16 位的正数 5。后面就可以用varints编码了
可以看到, 对负数使用 Zigzag 编码以后, 其高位的 1 全部变成了 0, 这样以来我们便可以使用 Varints 编码进行进一步地压缩, 再来看正数的情形, 对于 16 位的正数 5, 其在内存中的存储形式为
我们按照与负数相同的处理方法, 可以得到其 Zigzag 编码为
从上面的结果来看, 无论是正数还是负数, 经过 Zigzag 编码以后, 数字高位都是 0, 这样以来, 便可以进一步使用 Varints 编码进行数据压缩, 即 Zigzag 编码在 Protobuf 中并不单独使用, 而是配合 Varints 编码共同来进行数据压缩
protocol buffer消息是由一些key-value 组成的,其中key 代表字段后面的数字,变量名和变量类型仅仅决定编码的最终截止位置。
消息编码的时候,key 和value 都会被编码进字节流。当解码器解码时,需要跳过不能识别的字段,因为新添加字段不会对原来造成影响。每个key 由两部分组成,1个是定义在proto消息字段后面的数字,后面跟的是wire type (消息类型)。通过消息类型能够找到后面值的长度。
可用的wire type
每个key 在消息流里面都是这样的结构,(field_number << 3) | wire_type,最后三位存储wire_type,直白来说,wire_type类似语言中的数据类型,标识存储数据的长度
假设有下面这种消息类型Test1
message Test1 {
optional int32 a = 1;
}
当我们定义上面的消息,并赋值a=150,我们将得到下面序列化结构,总共三个字节
08 96 01
解码步骤
1、数据流第一个数字是 varint key,这里是08,二进制数据为000 1000
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 128 + 16 + 4 + 2 = 150
字符串的wire_type 是2,代表值是可变的,长度会被编码进字节流里面。
如下例子:
message Test2 {
optional string b = 2;
}
将b 赋值为"testing" ,得到下面的结果
12 07 [74 65 73 74 69 6e 67]
0x12
→ 0001 0010 (binary representation)
→ 00010 010 (regroup bits)
→ field_number = 2, wire_type = 2
message Test1 {
optional int32 a = 1;
}
message Test3 {
optional Test1 c = 3;
}
Test1’s a 字段依然是150:
1a 03 08 96 01
MyMessage message;
message.ParseFromString(str1 + str2);
和下面的结果是一样的
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
例如下面的类型
message Test4 {
repeated int32 d = 4 [packed=true];
}
Test4 的repeated 有三个值,3、270、86942 。编码结果将会如下面所示
22 // key (field number 4, wire type 2)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
字段数字顺序可以任何顺序出现在proto里面。顺序对消息序列化没有任何影响。
当消息被序列化时,是无法保证已知字段和未知字段被写入,序列化是一个实现细节,任何特定实现的细节在将来都会被改变,因此protocol buffer 必须能够解析字段在任何顺序。
【侵权删】