Google Protobuf (一)

[Google Protobuf 编解码]

Google Protobuf 优点:

  • 在谷歌内部长期使用, 产品成熟度高.
  • 跨语言、支持多种语言, 包括 C++、Java 和 Python.
  • 编码后的消息更小, 更加有利于存储和传输.
  • 编解码的性能非常高.
  • 支持不同协议版本的前向兼容.
  • 支持定义可选和必选字段.

Protobuf 的入门

Protobuf 是一个灵活、高效、结构化的数据序列化框架, 相比与 xml 等传统的序列化工具, 它更小、更快、更简单.

Protobuf 支持数据结构化一次可以到处使用, 甚至跨语言使用, 通过代码生成工具可以自动生成不同语言版本的源代码, 甚至可以在使用不同版本的数据结构进程间进行数据传递, 实现数据结构前向兼容.

定义消息类型

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

该文件的第一行指定使用 proto3 语法, 如果不写的话表示 proto2.

分配字段编号

string query = 1; 1 就是字段编号, 字段号主要用来标识二进制格式字段的. 1 到 15 字段号占一个字节. 16 到 2047 字段号需要两个字节.

我们将对象转换为报文的时候, 是按照字段编号进行报文封装的; 我们接收到数据之后框架会帮我们按照字段号进行赋值.

不能使用数字19000到19999, 因为它们是为 Google Protobuf 保留的.

字段类型对应

.proto Type Notes C++ Type Java Type
double double double
float float float
int32 使用可变长度编码, 对负数编码效率低下
如果您的字段可能有负值, 则使用sint32代替. int32 int
int64 使用可变长度编码, 对负数编码效率低下
如果您的字段可能有负值, 则使用sint64代替. int64 long
uint32 使用可变长度编码 uint32 int
uint64 使用可变长度编码 uint64 long
sint32 使用可变长度编码
有符号的int值这些编码比常规int32更有效地编码负数 uint32 int
sint64 使用可变长度编码
有符号的int值这些编码比常规int64更有效地编码负数 int64 long
fixed32 四个字节, 如果值通常大于2的28次方, 则比uint32更有效 uint32 int
fixed64 四个字节, 如果值通常大于2的56次方, 则比uint64更有效 uint64 long
sfixed32 四个字节 int32 int
sfixed64 四个字节 int64 long
bool bool boolean
string 字符串必须始终包含UTF-8编码或7位ASCII文本 string String
bytes 字符串必须始终包含UTF-8编码或7位ASCII文本 string ByteString

默认值

  • 对于字符串, 默认值是空字符串.
  • 对于字节, 默认值为空字节.
  • 对于bool, 默认值为false.
  • 对于数字类型, 默认值为零.
  • 对于枚举, 默认值是第一个定义的枚举值, 必须为0.

还请注意, 如果消息字段设置为默认值, 则该值将不会序列化.

允许嵌套

Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

更多的例子:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

导入定义

可以在文件的顶部添加一个import语句:

import "myproject/other_protos.proto";

未知字段

未知字段就是解析器无法识别的字段. 例如, 当服务端使用新消息发送数据, 客户端使用旧消息解析数据, 那么这些新字段将成为旧消息中的未知字段.

在3.5和更高版本中, 未知字段在解析过程中被保留, 并包含在序列化中输出.

Map 类型

repeated 类型可以用来表示数组, Map 类型则可以用来表示字典.

map<key_type, value_type> map_field = N;

map<string, Project> projects = 3;

key_type 可以是任何 int 或者 string 类型(任何的标量类型, 具体可以见上面标量类型对应表格, 但是要除去 floatdoublebytes)

枚举值也不能作为 key.

key_type 可以是除去 map 以外的任何类型.

需要特别注意的是:

  • map 是不能用 repeated 修饰的.
  • map 迭代顺序的是不确定的, 所以你不能确定 map 是一个有序的.
  • .proto 生成文本格式时, map 按 key 排序. 数字的 key 按数字排序.
  • 从数组中解析或合并时, 如果有重复的 key, 则使用所看到的最后一个 key(覆盖原则).从文本格式解析映射时, 如果有重复的 key, 解析可能会失败.

Protocol Buffer 虽然不支持 map 类型的数组, 但是可以转换一下, 用以下思路实现 maps 数组:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

上述写法和 map 数组是完全等价的,所以用 repeated 巧妙的实现了 maps 数组的需求.

Protocol Buffer 命名规范

message 采用驼峰命名法. message 首字母大写开头. 字段名采用下划线分隔法命名.

message SongServerRequest {
  required string song_name = 1;
}

枚举类型采用驼峰命名法. 枚举类型首字母大写开头. 每个枚举值全部大写, 并且采用下划线分隔法命名.

enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

每个枚举值用分号结束, 不是逗号.

服务名和方法名都采用驼峰命名法. 并且首字母都大写开头.

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

常用方法

getDefaultInstance(): 返回单例实例, 它与 newBuilder().build() 实例相同
getDescriptor(): 返回类型的描述符. 包括具有哪些字段以及类型. 这可以与 Message 的反射方法一起使用, 例如getField().
parseFrom(...): 返回反序列化后的 Message. 注意不会抛出 UninitializedMessageExceptionInvalidProtocolBufferException 异常.
Message.Builder: 中的 mergeFrom() 放会将数据解析为此类型的消息, 并进行消息合并.
newBuilder(): 创建一个新的构建器.

Any

Any类型允许包装任意的message类型:

import "google/protobuf/any.proto";

message Response {
    google.protobuf.Any data = 1;
}

总结

message SubscribeReq {
  int32 subReqID = 1;
  string userName = 2;
  string productName = 3;
  string address = 4;
}

可以通过 pack()unpack()(方法名在不同的语言中可能不同)方法装箱/拆箱,以下是Java的例子:

People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc编译后生成的message类
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包装people

System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// proto

Oneof

如果你有一些字段同时最多只有一个能被设置, 可以使用 oneof 关键字来实现, 任何一个字段被设置, 其它字段会自动被清空(被设为默认值):

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

默认值

比如我们创建了上面的消息类型, 我们在代码中设置 builder.setSubReqID(0); 为 0, 零是数值类型的默认值; 所以我们会看到序列化后的数据中, 没有对此字段进行序列化.

byte[] arry = builder.build().toByteArray();

arry 长度为 0. 对于字段类型是 string 类型的也是一样的; 也就是说显示赋值默认值也不会对其进行序列化.

保留字段

message SubscribeReq {

  reserved 2;

  int32 subReqID = 1;
  string userName = 2;
  string productName = 3;
  string address = 4;
}

顾名思义, 就是此字段会被保留可能在以后会使用此字段. 使用关键字 reserved 表示我要保留字段数 2.

上面代码我们在生成 Java 文件的时候会出现 ubscribeReqPeoro.proto: Field "userName" uses reserved number 2 错误信息, 所以我们需要将 string userName = 2; 注释, 或者删除.

保留后我们无法对其设置或序列化和反序列化.

</article>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354