protocl buffers

Protocol Buffers

protocol buffers are a language-neutral, platform-neutral extensible mechnism for serializing structed data.

协议缓冲文件是google定义的类似于xml的简洁高效的结构化数据存储格式,可以用于结构数据的序列化(串行化).方便文件的存储与网络传输 .XML是众所周知的空间密集型,编码/解码可能会对应用程序造成巨大的性能损失,protobuff 更小,快速,简单,只需要定义相应的数据结构,然后可以生成相应的源文件.通过建立.proto 文件描述符,协议缓冲符编译器会创建一个类,该类使用有效的二进制格式实现数据有效的自动编码和解码.生成的类为组成协议缓冲区字段提供getter() setter() ,并将协议缓冲区的读写细节作为一个单元来处理.

协议缓冲区格式支持随着时间的推移而扩展的想法,即代码仍然可以读取使用旧格式编码的数据

  • 是一种序列化对象框架(或者说是编解码框架),其他功能相似的有Java自带的序列化、Facebook的Thrift和JBoss Marshalling等;

  • 通过proto文件定义结构化数据,其他功能相似的比如XML、JSON等;

  • 自带代码生成器,支持多种语言;

Base syntax

1.定义一个消息类型

syntax = "proto3";        //首句明确使用的是proto3的语法

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

在此例中,所有的数据都是标量,也可以采用其他的复杂类型作为数据域,包含枚举型(enumerations), 其他message类型.

明确字段(field)的编号:

在message的定义中,每一项都被赋予特定的唯一编号,并且在protobuff文件一旦使用后该编号即不可改变.注意域的编号一般要保留一定空间用于未来可能的扩充,并且不可以使用已经标记为reserved 的编号.

  • 1-15:采用一个字节编码,用于标识使用频率较高的message元素

  • 16-2047:采用两个字节编码

  • 最小的编号可以为1,最大可以为2**29-1

  • 编号19000到19999被作为协议缓冲区的实现,所以不可以作为字段编号

消息字段类型以是以下两种之一:

  • singular:一个格式正确(well-formed)的message可以至多包含一个该字段

  • repeated: 该字段可以被重复任意次

保留字段:(researed fields)

考虑到升级时的问题,如果升级时完全移除一个字段,之后的用户可能会重用该字段的编号,这与字段编号的唯一性不符(如果用户调用旧版本的proto文件时会造成十分严重的错误)

而避免这种错误的方式可以是将需要删除的字段号或者名称定义为reserved ,之后的用户如果尝试再使用此字段时,编译器会报错

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

注意不可以将保留的 names 和 field number 放在同一个reserved 下

2.标量值的类型

You can find out more about how these types are encoded when you serialize your message in Protocol Buffer Encoding.

默认值

在解析消息时,如果编码的消息不包含特定的singular元素,则解析对象中的对应的字段将设置为该字段的默认值:

  • string:默认为""

  • bytes: 空

  • bool: false

  • numeric type: 0

  • enums: 默认值是要保证第一个定义的值一定要是0

  • message: 依赖于具体语言

对于标量消息字段,一旦解析了消息,就无法判断字段是否明确设置为默认值(例如布尔值是否设置为false)或者根本没有设置:需要牢记定义的消息类型。例如,如果不希望该行为在默认情况下也发生,请将其设置为false时打开某些行为的布尔值。如果标量消息字段被设置为其默认值,则该值不会在连线上序列化

3.Enumerations 枚举类型

为了在定义消息类型时,将某个字段的域明确在某些预定义的值.可以使用枚举类型

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;   //  注意每个枚举类型必须保证第一个定义的数据为0
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

//可以将多个枚举常量赋给同一个值,但是需要明确 allow_alias = true;
enum EnumAllowingAlias{
    optional allow_alias = true;
    unknown = 0;
    started = 1;
    running = 1;
}

保留值:

注意,枚举类型中的值也不可以造成未来的隐患,所以需要保留字,将需要删除的值进行reserved标识

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

4.其他消息类型的引入

对于同一个protobuff文件中的message可以直接引入并在其他message的定义中使用,对于其他文中的定义可以使用import语句进行引入

通常情况下.只可以从直接import的proto文件中使用定义.但是,有时需要将.proto文件移至新位置,这时不需要直接移动文件并且在这次变换中更新所有的调用点;可以在原来的位置上放置一个虚拟的.proto 文件,然后使用import public 调用,将所有的导入转移到新位置.

// new.proto
// All definitions are moved here



// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";



// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

proto编译器使用-I/- proto_path 标志在命令行中的指定目录中搜索导入的文件,如果没有给出标志,将在调用编译器的目录中查找,原则上讲,应该将 --proto_path 设置为项目的根目录,并且所有的导入均使用完全限定名称.

5. Nested Types 嵌套类型

可以再一个message中定义嵌套的message 并且可以进行多层嵌套

message Search{
    message Result {
        string url = 1;
        string titile = 2;
        repeated string sni = 3;
    }
    repeated Result results = 1;
}

如果需要调用内部的嵌套子类,可以使用parent.child 进行调用

message other {
    Search.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;
    }
  }
}

Other syntax

1. Any 类型嵌套

any message 类型允许使用消息嵌套而不需要借助其.proto 定义,any 包含一个二进制序列化的messge 并作为bytes 类型,与一个url一起作为message type一个全局的统一定位符

使用前需要导入"google/protobuf/any.proto" 文件

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

默认的类型URL为: type.googleapis.com/packagename.messagename

2. oneof

如果有一个包含多字段的消息,并且最多只能同时设置一个字段,则可以使用此功能强制执行此操作可以节省内存

oneof字段与普通字段的一样除了所有被定义在oneof 中的字段共享一段内存资源,并且每次仅可以设置一个字段.设置oneof中的一个成员会自动清除其他成员.

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

现在可以添加oneof字段,可以添加到任何类型,但是不可以使用repeated 关键字

c++实例:

  • 设置oneof字段会自动清除其他字段,因此只有最后的设置有效

    SampleMessage message;
    message.set_name("Name);
    CHECK(message.has_name());
    message.mutable_sub_message();
    CHECK(!message.has_name())
  • 如果解析器在连线上遇到同一成员,则只有最后一个成员在解析消息时有用

  • 以下代码会导致memory crash因为sub_message已经被删除

    SampleMessage message;
    SubMessage *sub_message = message.mutable_sub_message();
    message.set_name("Clear");    //将会清除其他的所有字段
    sub_message -> set_name("sub");    //已经被删除,报错
  • 如果你用oneofs交换两个消息,每个消息都会以另一个case为结尾:在下面的例子中,msg1将有 一个sub_message,而msg2将有一个名字

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());

3. Maps

可以再protobuf中使用 map<> 来创建两个相关联的二元组(字典),包含了key_type ,value_type 分别指定键值和实际值的类型,键值可以是string 或者数字,但是不可以是其他的map,比如说enum

  • map 字段不可以 repeated

  • 线格式排序和map迭代排序的map值是不能确定的,所以不能认为map类型的每一项会有一个特定的顺序

  • 当为.proto生成文本格式时,map按照键值排序

  • 从线路解析或者合并时,如果有重复的键值,则使用的是所看到的最后一个键值;从文本格式解析映射时,如果有重复的键值,可能解析失败.

  • 如果对于同一个map只提供键值而不提供值,则根据使用的语言来自动填补如:java c++ python会填补默认值

map<string, Project> projects = 3;

组织构造

Packages

在.proto文件中可以指定一个包名,指明message定义所属的组织结构,然后可应通过 packagename.message_name 进行调用.

package foo.bar;
message Open { ... }


//**********************************************//


message Foo{
    foo.bar.Open open = 1;
}

在不同的编程语言的表现:

  • c++: 产生的类将会被包装在namespace 本例中的Open将会被保证在foo::bar

  • java: 作为java文件的包

  • python: 被忽略,因为python的模块的组织架构根据文件系统

定义服务

可以用proto文件定义一RPC系统,可以再 .proto文件中定义RPC服务的所有接口,然后可以生成所需语言的RPC脚本.

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

JOSN 转换

//TODO

OPTION

可以再.proto文件中声明option语句进行可用性设定,这些设定包含

file:即可以作用在最高层次的命名域,

message:在一个message的定义的作用域起作用,

field:在一个字段声明中起作用

  • java_package :指定生成java类的包,如果未指明,默认会在package 语句定义的路径,若不生成java文件,该语句则不起作用

  • java_mutable_files : 导致在包级别定义顶级消息,枚举和服务,而不是在以.proto文件命名的外部类中定义

  • java_outer_classname: 要生成的最外面的Java类(以及因此的文件名)的类名称。如果没有在.proto文件中指定明确的java_outer_classname,则通过将.proto文件名称转换为camel-case[驼峰模式](因此foo_bar.proto变为FooBar.java)来构造类名称。如果不生成Java代码,则此选项不起作用。

  • optimize_for: 可以对java c++起作用有三个选项

    • SPEED(default): 编译器将生成用于序列化,解析和执行消息类型的其他常见操作的代码。这段代码是高度优化的。

    • CODE_SIZE: 编译器将生成最少的类,并将依靠共享的基于反射的代码来实现序列化,解析和各种其他操作。生成的代码因此比SPEED要小得多,但操作会更慢。类仍将实现与SPEED模式中完全相同的公共API。此模式在包含大量.proto文件的应用程序中非常有用,并且不需要所有这些文件都快速地闪烁

    • LITE_RUNTIME: 编译器将生成仅取决于“lite”运行时库(libprotobuf-lite而不是libprotobuf)的类。 lite运行时比整个库小得多(大约小一个数量级),但省略了描述符和反射等特定功能。这对于在移动电话等受限平台上运行的应用程序特别有用。编译器仍然会像在SPEED模式下那样生成所有方法的快速实现。生成的类将仅实现每种语言的MessageLite接口,该接口仅提供完整的Message接口的一部分方法。

  • deprecated :如果设置为true,则表示该字段已弃用且不应被新代码使用。在大多数语言中,这没有 实际影响。 在Java中,这变成了@Deprecated注释。将来,其他语言特定的代码生成器可能会在字段的访问器上生成弃用注释,这会在编译试图使用该字段的代码时导致发出警告。如果该字段未被任何人使用,并且您想阻止新用户使用该字段,请考虑用保留语句替换该字段声明。

产生相应的代码

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.prototest

Last updated