目录

protobuf v3

protobuf 是什么

Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、跨平台的序列化库。

2001年初,Protobuf首先在Google内部创建,很多项目也采用Protobuf进行消息的通讯,还有基于Protobuf的微服务框架GRPC。

可以看做xml、json等序列化的又一种形式,只不过序列化后它是二进制的。

Protobuf支持很多语言,比如C++、C#、Dart、Go、Java、Python、Rust等,同时也是跨平台的。

序列化(serialization、marshalling)的过程是指将数据结构或者对象的状态转换成可以存储(比如文件、内存)或者传输的格式(比如网络)。反向操作就是反序列化(deserialization、unmarshalling)的过程。

protobuf 为什么要有

二十世纪九十年代后期,XML开始流行,它是一种人类易读的基于文本的编码方式,易于阅读和理解。

JSON是一种更轻量级的基于文本的编码方式,经常用在client/server端的通讯中。

除此之外还有很多序列化格式。

protobuf序列化和反序列化速度更快; 文件更小存储需要更少的空间,传输时间短。

protobuf 基础

官方推荐新代码采用proto3,这个教程主要介绍proto3的开发。

使用protobuf需要一个.proto文件,在这里定义要序列化的格式。可以理解为我们工作中和服务端定义的接口文档或者java bean。

举例: user.proto

syntax = "proto3";

//生成java类所在的包名
package com.example.protobuftest.bean;

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

第一行指定protobuf的版本,可以指定为proto2或3。如果没有指定,默认以proto2格式定义。

在这里我们定义了一个User类型,包括name和age字段。

package

package是可选的。对于生成的java语言对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package; 如果不写package,默认是文件名作为包名。 写了包名后,就可以用包名加以区分。比如test.model.UserInfo。 显示设置包名后生成的对应语言文件就按照这个来生成包名了。

option java_package = "test.protobuf.sample";
option go_package = "test.protobuf.sample";

字段规则

所指定的消息字段修饰符必须是如下之一: singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。这是proto3语法的默认字段规则。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

protobuf2中的required、optional规则已经不能使用了。

字段类型

.proto Type Notes Java Type Go Type
double double float64
float float float32
int32 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 int int32
int64 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 long int64
uint32 使用可变长度编码 int uint32
uint64 使用可变长度编码. long uint64
sint32 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 int int32
sint64 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 long int64
fixed32 总是四个字节。如果值通常大于228,则比uint32更有效。 int uint32
fixed64 总是八个字节。如果值通常大于256,则比uint64更有效 long uint64
sfixed32 总是四个字节 int int32
sfixed64 总是八个字节 long int64
bool boolean bool
string String string
bytes 可以包含不超过232的任意字节序列。 String []byte

标识号

正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。

保留标识符(Reserved)

如果你通过删除或者注释所有域,以后的用户在更新这个类型的时候可能重用这些标识号。如果你使用旧版本加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注:不要在同一行reserved声明中同时声明域名字和tag number。

类型嵌套及导入

在一个.proto文件中可以定义多个消息类型,可以引用,也可以嵌套

message SearchResponse {
  repeated Result result = 1;
}
message Result {
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

import "myproject/other_protos.proto";

嵌套使用:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它

枚举

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  optional Corpus corpus = 4 [default = UNIVERSAL];
}

protbuf3中第一个字段必须是0,枚举类的第一个值总是默认值。

Oneof

Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它oneof字段。

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

oneof中字段不能使用 required, optional, repeated 关键字.

Map

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

  • Map的字段不可以是repeated。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口。

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}