proto_prototype - CSDN
精华内容
参与话题
  • Protobuf语法指南(proto3)

    千次阅读 2019-04-10 21:42:21
    在阅读本篇文章之前可参考我的另一篇博文:[译]Protobuf语法指南(proto2) 定义一个消息类型 先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询...

    本文是一篇译文,原文地址为:https://developers.google.com/protocol-buffers/docs/proto3

    在阅读本篇文章之前可参考我的另一篇博文:Protobuf语法指南(proto2)

    定义一个消息类型

    先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件:

    syntax = "proto3";
    
    message SearchRequest {
      string query = 1;
      int32 page_number = 2;
      int32 result_per_page = 3;
    }
    
    • 文件的第一行指定你正在使用 proto3 语法: protocol buffer 编译器默认使用的是 proto2 。 这必须是文件的非空、非注释的第一行。
    • 这个 SearchRequest 消息定义了三个字段(名称/值对),每一条 SearchRequest 消息类型的数据都包含这三个字段定义的数据。每个字段包含一个名称和类型。

    指定字段类型

    在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。

    分配标识号

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

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

    指定字段规则

    消息的字段可以是一下情况之一:

    • singular(默认):一个格式良好的消息可以包含该段可以出现 0 或 1 次(不能大于 1 次)。
    • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

    默认情况下,标量数值类型的repeated字段使用packed的编码方式。

    关于 packed 编码的信息,请查看 Protocol Buffer Encoding

    在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:

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

    添加注释

    向.proto文件添加注释,可以使用C/C++风格的 // 和 /* … */ 语法格式

    /* SearchRequest represents a search query, with pagination options to
     * indicate which results to include in the response. */
    
    message SearchRequest {
      string query = 1;
      int32 page_number = 2;  // Which page number do we want?
      int32 result_per_page = 3;  // Number of results to return per page.
    }
    

    保留字段

    如果通过将字段完全删除或将其注释来更新消息类型,则在将来,当用户更新其消息类型时,他们可以重用那些字段的编号。 如果以后加载相同.proto文件的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。 确保不会发生这种情况的一种方法是指定已删除字段的字段编号为“reserved”。 如果将来的任何用户尝试使用这些字段标识符,协议缓冲编译器将会发出抱怨。

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

    注意:不能在同一 “reserved” 语句中将字段名称和字段编号混合在一起指定。

    从.proto文件生成了什么?

    当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

    • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
    • 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
    • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
    • 对于 Go 语言,针对每一个定义的消息类型编译器会创建一个带类型的.pb.go 文件。
    • 对于 Ruby 语言,编译器会创建一个带 Ruby 模块的.rb 文件,其中包含了所有你定义的消息类型。
    • 对于 JavaNano,编译器会创建 Java 语言类似的输出文件,但是没有 Builder 构造类。
    • 对于 Ojective-C,编译器会创建一个 pbobjc.h 和一个 pbobjc.m 文件,为每一个消息类型都创建一个类来操作。
    • 对于 C#语言,编译器会为每一个.proto 文件创建一个.cs 文件,为每一个消息类型都创建一个类来操作。

    标量类型

    一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:

    .proto Type Notes C++ Type Java Type Python Type[2] Go Type
    double double double float *float64
    float float float float *float32
    int32 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 int32 int int *int32
    int64 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 int64 long int/long[3] *int64
    uint32 使用可变长度编码 uint32 int[1] int/long[3] *uint32
    uint64 使用可变长度编码 uint64 long[1] int/long[3] *uint64
    sint32 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 int32 int int *int32
    sint64 使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码 int64 long int/long[3] *int64
    fixed32 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 uint32 int[1] int/long[3] *uint32
    fixed64 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 uint64 long[1] int/long[3] *uint64
    sfixed32 总是四个字节 int32 int int *int32
    sfixed64 总是八个字节 int64 long int/long[3] *int64
    bool bool boolean bool *bool
    string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 string String str/unicode[4] *string
    bytes 可以包含任意字节序列 string ByteString str []byte

    在 Protocol Buffer 编码 中你可以找到有关序列化 message 时这些类型如何被编码的详细信息。
    [1] 在 Java 中,无符号的 32 位和 64 位整数使用它们对应的带符号表示,第一个 bit 位只是简单的存储在符号位中。
    [2] 在所有情况下,设置字段的值将执行类型检查以确保其有效。
    [3] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以为int。在所有情况下,该值必须适合设置时的类型。见 [2]。
    [4] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以是 str(这条可能会发生变化)。
    [5] 整型用于64位的机器,字符串用于32位的机器

    默认值

    当一个消息被解析的时候,如果在编码后的消息结构中某字段没有初始值,相应的字段在被解析的对象中会被设置默认值。这些默认值都是类型相关的。

    • 字符串默认值为空字符串。
    • 字节类型默认值是空字节。
    • 布尔类型默认值为 false。
    • 数值类型默认值为 0。
    • 枚举类型默认值是其定义中的第一个值,它必须为 0。
    • 消息类型的默认值没有设置。它的具体值与使用的编程语言有关。

    repeated字段的默认值为空(通常是相应编程语言的空列表)。

    注意:对于标量消息字段,当消息被解析时,我们没有办法知道某个字段是否被显示地设定为默认值(例如一个布尔类型的字段值是否被设置为 false),也许这个字段压根就没有被设定值。当我们定义一个消息类型时,我们需要牢记这点。例如,如果一个布尔类型的字段在其值被设置为false时,会导致某种行为的发生,而我们并不想让这种行为在默认情况下也会发生,那么我们就不要定义这个bool类型的字段。 还要注意的是,在序列化的时候,如果标量消息字段的值设为默认值,这个值是不会被序列化的。

    枚举

    当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝 试指定不同的值,解析器就会把它当作一个未知的字段来对待)。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:

    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;
    }
    

    你会发现,这个 Corpus 枚举类型的第一个常量被设置为 0,每个枚举类型的定义中,它的第一个元素都应该是一个等于 0 的常量。 这是因为:

    • 枚举类型中必须包含值为0的元素,这样我们才可以使用0作为数值型字段的默认值。
    • 这个为 0 的元素必须是第一个元素,是为了兼容 proto2 语法(proto2 中枚举类型的第一个元素总是默认值)。
      你可以为枚举常量定义别名。 需要设置option allow_alias为 true, 否则 protocol编译器会产生错误信息。
    enum EnumAllowingAlias {
      option allow_alias = true;
      UNKNOWN = 0;
      STARTED = 1;
      RUNNING = 1;
    }
    enum EnumNotAllowingAlias {
      UNKNOWN = 0;
      STARTED = 1;
      // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google 
      								and a warning message outside.
    }
    

    枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。你可以在一个消息定义的内部定义枚举,就像上面的例子那样。你也可以在消息的外部定义枚举类型,这样这些枚举值可以在同一.proto文件中定义的任何消息中重复使用。当然也可以在一个消息使用在另一个消息中定义的枚举类型——采用MessageType.EnumType的语法格式。

    使用其他的Message类型

    你可以将其他message类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在同一.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

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

    导入定义

    在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?

    你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

    import "myproject/other_protos.proto";
    

    默认情况下你只能使用直接导入的.proto文件中的定义。然而, 有时候你需要移动一个.proto文件到一个新的位置。现在,你可以在旧位置放置一个虚拟 .proto 文件,以使用命令 import public将所有导入转发到新位置,而不是直接移动 .proto 文件并在一次更改中更新所有调用点。导入包含 import public 语句的 proto 的任何人都可以导入公共依赖项。例如:

    // 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
    

    使用命令 -I/–proto_path 让 protocol 编译器在指定的一组目录中搜索要导入的文件。如果没有给出这个命令选项,它将查找调用编译器所在的目录。通常,你应将 --proto_path 设置为项目的根目录,并对所有导入使用完全限定名称。

    使用proto2消息类型

    我们有可能在proto3消息中导入并使用proto2消息类型,反之亦然。然而在proto3语法中不能直接使用proto2的枚举类型字段(如果是被导入的proto2消息使用的,这是可以的)

    嵌套类型

    你可以在其他 message 类型中定义和使用 message 类型,如下例所示 - 此处Result消息在SearchResponse 消息中定义:

    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;
    }
    

    你可以嵌套任意多层的消息:

    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;
        }
      }
    }
    

    更新 message 类型

    如果现有的 message 类型不再满足你的所有需求 - 例如,你希望 message 格式具有额外的字段 - 但你仍然希望使用基于旧的message格式产生的代码,请不要担心!在不破坏任何现有代码的情况下更新 message 类型非常简单。请记住以下规则:

    • 请勿更改任何现有字段的字段编号
    • 如果添加了新的字段,基于“旧的”消息格式的代码而序列化的任何消息仍可以被新生成的代码解析。你应该为这些元素设置合理的默认值,以便新代码可以正确地与旧代码生成的 message 进行交互。同样,你的新代码创建的 message 可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。但是未丢弃这个新字段,如果稍后序列化消息,则将新字段与其一起序列化。因此,如果将消息传递给新代码,则新字段仍然可用。(兼容性强
    • 可以删除字段,只要在新的 message 类型中不再使用该字段的编号。也许你希望的是重命名该字段,那么可以添加前缀 “OBSOLETE_”,或者将字段编号保留(reserved),以便将来你的 .proto 文件的用户不会不小心重用这个编号。
    • int32,uint32,int64,uint64 和 bool 都是兼容的 - 这意味着你可以将字段从这些类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从中解析出一个不符合相应类型的数字,你将获得与在 C++ 中将该数字转换为该类型时相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。
    • sint32 和 sint64 彼此兼容,但与其他整数类型不兼容。
    • 只要bytes是有效的 UTF-8,string 和 bytes 就是兼容的。
    • 如果bytes包含 message 的编码版本,则内嵌的 message 与 bytes 兼容。
    • fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
    • enum 与 int32,uint32,int64 和 uint64兼容(注意,如果它们不匹配,值将被截断)。但请注意,在反序列化消息时,客户端代码可能会以不同方式对待它们:例如,无法识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示它,这是与语言相关的。 Int字段总是保留它们的值。
    • 将单个值更改为新的oneof 的成员是安全且二进制兼容的。如果你确定没有代码一次设置多个,则将多个 字段移动到新的 oneof 中可能是安全的。但是将任何字段移动到现有的 oneof 是不安全的。

    未知字段

    未知字段是格式良好的protocol buffer序列化数据中解析器无法识别的字段。 例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

    最初,proto3消息在解析期间总是丢弃未知字段,但在3.5版本中,我们将未知字段保存以匹配proto2行为。 在版本3.5及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

    Any

    Any消息类型允许您可以像使用嵌入类型一样使用消息,而无需拥有其.proto定义。 一个Any型的消息包含任意字节的序列化消息,以及一个URL,它作为全局唯一标识符来解析消息的类型。为了使用 Any 类型的消息,你需要import google/protobuf/any.proto

    import "google/protobuf/any.proto";
    
    message ErrorStatus {
      string message = 1;
      repeated google.protobuf.Any details = 2;
    }
    

    给定 Any 消息类型的默认 URL 是type.googleapis.com/packagename.messagename

    不同的语言实现都会支持运行库,以通过类型安全的方式来封包或解包 Any 类型的消息。在 Java 语言中,Any 类型有专门的访问函数 pack()和unpack()。在 C++中对应的是 PackFrom()和 PackTo()方法。

    // Storing an arbitrary message type in Any.
    NetworkErrorDetails details = ...;
    ErrorStatus status;
    status.add_details()->PackFrom(details);
    
    // Reading an arbitrary message from Any.
    ErrorStatus status = ...;
    for (const Any& detail : status.details()) {
      if (detail.Is<NetworkErrorDetails>()) {
        NetworkErrorDetails network_error;
        detail.UnpackTo(&network_error);
        ... processing network_error ...
      }
    }
    

    当前,Any 类型的运行时库还在开发中。

    如果你已经熟悉 proto2 语法 ,Any 类型就是替代了 proto2 中的 extensions 。

    Oneof

    如果你的 message 包含许多可选字段,并且最多只能同时设置其中一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。

    Oneof 字段类似于可选字段,除了 oneof 共享内存中的所有字段,并且最多只能同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。你可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 字段中当前是哪个值(如果有)被设置,具体方法取决于你选择的语言。

    使用 Oneof

    要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:

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

    然后,将 oneof 字段添加到 oneof 定义中。你可以添加任何类型的字段,但不能使用 required,optional 或 repeated 关键字。如果需要向 oneof 添加重复字段,可以使用包含重复字段的 message。

    在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。

    Oneof 特性

    • 设置 oneof 字段将自动清除 oneof 的所有其他成员。因此,如果你设置了多个字段,则只有你设置的最后一个字段仍然具有值。
    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message();   // Will clear name field.
    CHECK(!message.has_name());
    
    • 如果解析器遇到同一个 oneof 的多个成员,则在解析的消息中仅使用看到的最后一个成员。
    • oneof 不支持扩展
    • oneof 不能使用 repeated
    • 反射 API 适用于 oneof 字段
    • 如果你使用的是 C++,请确保你的代码不会导致内存崩溃。以下示例代码将崩溃,因为已通过调用 set_name() 方法删除了 sub_message。
    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
    • 同样在 C++中,如果你使用 Swap() 交换了两条 oneofs 消息,则每条消息将以另一条消息的 oneof 实例结束:在下面的示例中,msg1 将具有 sub_message 而 msg2 将具有 name。
    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

    向后兼容性问题

    添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

    标签重用问题

    • 将字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。

    • 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。

    • 拆分或合并 oneof:这与移动常规的字段有类似的问题。

    Maps

    如果要在数据定义中创建关联映射,protocol buffers 提供了一种方便快捷的语法:

    map<key_type, value_type> map_field = N;
    

    其中 key_type 可以是任何整数或字符串类型(任何标量类型除浮点类型和 bytes)。请注意,枚举不是有效的 key_type。value_type 可以是除 map 之外的任何类型。

    因此,举个例子,如果要创建项目映射,其中每个 “Project” message 都与字符串键相关联,则可以像下面这样定义它:

    map<string, Project> projects = 3;
    

    Maps 特性

    • maps 不能是 repeated
    • map 值的网络序和 map 迭代序未定义,因此你不能依赖于特定顺序的 map 项
    • 生成 .proto 的文本格式时,maps 按键排序。数字键按数字排序
    • 当解析或合并时,如果有重复的 map 键,则使用最后看到的键。从文本格式解析 map 时,如果存在重复键,则解析可能会失败。
    • 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。 在C ++,Java和Python中,类型的默认值是序列化的,而在其他语言中,键值均不是序列化的。

    向后兼容性

    map 语法等效于以下内容,因此不支持 map 的 protocol buffers 实现仍可处理你的数据:

    message MapFieldEntry {
      optional key_type key = 1;
      optional value_type value = 2;
    }
    
    repeated MapFieldEntry map_field = N;
    

    任何支持 maps 的 protocol buffers 实现都必须生成和接受上述定义所能接受的数据。

    Packages

    你可以将可选的package说明符添加到 .proto 文件,以防止 protocol message 类型之间的名称冲突。

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

    然后,你可以在定义 message 类型的字段时使用package说明符:

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

    package 对生成的代码的影响取决于你所选择的语言:

    • 在 C++ 中,生成的类包含在 C++ 命名空间中。例如,Open 将位于命名空间 foo::bar 中。
    • 在 Java 中,除非在 .proto 文件中明确提供选项 java_package,否则该包将用作 Java 包
    • 在 Python 中,package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置进行组织的
    • 在Go 中,package 指令被忽略,生成的.pb.go文件位于以相应的go_proto_library规则命名的包中。
    • 在 Ruby 中,所生成的类被包裹在嵌套的 Ruby 命名空间中,包名被转换为 Ruby 大写样式(第一个字母大写,如果第一个不是字母字符,添加PB_前缀)。例如,Open 类会出现在命名空间 Foo::Bar 中。
    • 在 JavaNano 中,除非你在.proto 文件中显示声明了 option java_package,否则这个包名会被作为 Java 包名使用。

    Packages 和名称解析

    protocol buffer 语言中的类型名称解析与 C++ 类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的 “内部”。开头的 ‘.’(例如 .foo.bar.Baz)意味着从最外层的范围开始。

    protocol buffer 编译器通过解析导入的 .proto 文件来解析所有类型名称。每种语言的代码生成器都知道如何使用相应的语言类型,即使它具有不同的范围和规则。

    定义服务

    如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将以你选择的语言生成服务接口和stub(桩)。因此,例如,如果要定义一个 RPC 服务,其中包含一个根据 SearchRequest 返回 SearchResponse 的方法,可以在 .proto 文件中定义它,如下所示:

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

    与 ProtoBuf 直接搭配使用的 RPC 系统是 gRPC :一个 Google 开发的平台无关语言无关的开源 RPC 系统。gRPC 和 ProtoBuf 能够非常完美的配合,你可以使用专门的 ProtoBuf 编译插件直接从.proto 文件生成相关 RPC 代码。

    如果你不想使用 gRPC,你也可以用自己的 RPC 来实现和 ProtoBuf 协作。 更多的关于RPC 的信息请参考[译]Protobuf语法指南(proto2)

    JSON 映射

    Proto3 支持标准的 JSON 编码,使得在不同的系统直接共享数据变得简单。下表列出的是基础的类型对照。

    在 JSON 编码中,如果某个值被设置为 null 或丢失,在映射为 ProtoBuf 的时候会转换为相应的 默认值 。 在 ProtoBuf 中如果一个字段是默认值,在映射为 JSON 编码的时候,这个默认值会被忽略以节省空间。可以通过选项设置,使得 JSON 编码输出中字段带有默认值。

    proto3JSONJSON exampleNotes
    messageobject{"fooBar": v, "g": null, …} 生成JSON对象。 消息字段名称映射到lowerCamelCase并成为JSON对象键。 如果指定了json_name字段选项,则将指定的值用作键。解析器接受 lowerCamelCase名称(或json_name选项指定的名称)和原始proto字段名称。 null是所有字段类型的可接受值,并被视为相应字段类型的默认值。
    enumstring"FOO_BAR"使用proto中指定的枚举值的名称。 解析器接受枚举名称和整数值。
    map<K,V>object{"k": v, …}所有键都转换为字符串。
    repeated Varray[v, …]null被转换为空列表[]
    booltrue, falsetrue, false
    stringstring"Hello World!"
    bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON值将是使用带填充的标准base64编码编码为字符串的数据。 接受带有/不带填充的标准或URL安全base64编码。
    int32, fixed32, uint32number1, -10, 0JSON值将是十进制数。 接受数字或字符串。
    int64, fixed64, uint64string"1", "-10"JSON值将是十进制字符串。 接受数字或字符串。
    float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON值将是一个或多个特殊字符串值“NaN”,“Infinity”和“-Infinity”。 接受数字或字符串。 指数表示法也被接受。
    Anyobject{"@type": "url", "f": v, … }如果Any包含具有特殊JSON映射的值,则它将按如下方式转换: {“@ type”:xxx,“value”:yyy} 。 否则,该值将转换为JSON对象,并将插入 “@ type” 字段以指示实际数据类型。
    Timestampstring"1972-01-01T10:00:20.021Z"使用RFC 3339,其中生成的输出将始终被Z标准化并使用0,3,6或9个小数位。 也接受“Z”以外的偏移。
    Durationstring"1.000340012s", "1s"生成的输出始终包含0,3,6或9个小数位,具体取决于所需的精度,后跟后缀“s”。 接受的是任何小数位(也没有),只要它们符合纳秒精度并且需要后缀“s”。
    Structobject{ … }任意JSON对象
    Wrapper typesvarious types2, "2", "foo", true, "true", null, 0,Wrappers在JSON中使用与包装基元类型相同的表示形式,除了在数据转换和传输期间允许并保留 null .
    FieldMaskstring"f.fooBar,h"field_mask.proto.
    ListValuearray[foo, bar, …]
    Valuevalue任意JSON 值
    NullValuenullJSON null

    选项 Options

    .proto 文件中的各个声明可以使用一些选项进行诠释。选项不会更改声明的含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表在 google/protobuf/descriptor.proto中定义。

    一些选项是文件级选项,这意味着它们应该在更高层的范围内编写,而不是在任何消息,枚举或服务定义中。一些选项是 message 消息级选项,这意味着它们应该写在 message 消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法上,但是,目前在这几个项目上并没有任何有用的选项。

    以下是一些最常用的选项:

    • java_package(文件选项):要用于生成的 Java 类的包。如果 .proto 文件中没有给出显式的 java_package 选项,那么默认情况下将使用 proto 包(使用 .proto 文件中的 “package” 关键字指定)。但是,proto 包通常不能生成好的 Java 包,因为 proto 包不会以反向域名开头。如果不生成Java 代码,则此选项无效。
    option java_package = "com.example.foo";
    
    • java_outer_classname(文件选项):要生成的最外层 Java 类(以及文件名)的类名。如果 .proto 文件中没有指定显式的 java_outer_classname,则通过将 .proto 文件名转换为 camel-case 来构造类名(因此 foo_bar.proto 变为 FooBar.java)。如果不生成 Java 代码,则此选项无效。
    option java_outer_classname = "Ponycopter";
    
    • optimize_for(文件选项):可以设置为 SPEED,CODE_SIZE 或 LITE_RUNTIME。这会以下列方式影响 C++和 Java 的代码生成器(可能还有第三方生成器):

      • SPEED(默认值):protocol buffer 编译器将生成用于对 message 类型进行序列化、解析和执行其他常见操作的代码。此代码经过高度优化。

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

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

    option optimize_for = CODE_SIZE;
    
    • cc_generic_services,java_generic_services,py_generic_services(文件选项):protocol buffer 编译器应根据服务定义判断是否生成 C++,Java 和 Python 抽象服务代码。由于遗留原因,这些默认为 “true”。但是,从版本 2.3.0(2010年1月)开始,RPC 实现最好提供代码生成器插件,以生成每个系统的具体代码,而不是依赖于 “抽象” 服务。
    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
    • cc_enable_arenas(文件选项):为 C++ 生成的代码启用 arena allocation
    • objc_class_prefix(文件级选项):这个选项用来设置编译器从.proto 文件生成的类和枚举类型的名称前缀。这个选型没有默认值。您应该使用 3–5 个大写字母作为前缀。 请注意:所有的 2 个字母的前缀由苹果公司保留使用。
    • deprecated (字段选项):这个选项如果设置为 true ,表示该字段已被废弃,你不应该在后续的代码中使用它。 在大多数语言中这没有任何实际的影响。在 Java 中,它会变成废弃的注释。将来,其他特定的语言的代码生成器在为被标注为 deprecated 的字段生成操作函数的时候,编译器会在尝试使用该字段的代码时发出警告。如果不希望将来有人使用使用这个字段,请考虑用 reserved 关键字 声明该字段。
    int32 old_field = 6 [deprecated=true];
    

    自定义选项

    Protocol Buffers 甚至允许你定义和使用自己的选项。请注意,这是高级功能,大多数人不需要。更多信息参考[译]Protobuf语法指南(proto2)

    生成类

    要生成 Java, Python, C++, Go, Ruby, Objective-C, 或者r C#代码,你需要使用 .proto 文件中定义的 message 类型,你需要在 .proto 上运行 protocol buffer 编译器 protoc。

    Protocol 编译器的调用如下:

    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.proto
    

    IMPORT_PATH 指定在解析导入指令时查找 .proto 文件的目录。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项来指定多个导入目录;他们将按顺序搜索。-I = IMPORT_PATH 可以用作 --proto_path 的缩写形式。

    你可以提供一个或多个输出指令:

    • –cpp_out在 DST_DIR 中生成 C++ 代码。
    • –java_out在DST_DIR中生成 Java 代码。
    • –python_out 在 DST_DIR 中生成 Python 代码。
    • –go_out 在 DST_DIR 中 生成 Go 代码 。
    • –ruby_out 在 DST_DIR 中生成 Ruby 代码。
    • –javanano_out 在 DST_DIR 中 生成 JavaNano 代码 。
    • –objc_out 在 DST_DIR 中生成 Objective-C 代码。
    • –csharp_out 在DST_DIR 中生成 C#代码。

    为了方便起见,如果 DST_DIR 以 .zip 或 .jar 结尾,编译器会将输出写入到具有给定名称的单个 ZIP 格式的存档文件。.jar 输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖;编译器不够智能,无法将文件添加到现有存档中。

    你必须提供一个或多个 .proto 文件作为输入。可以一次指定多个 .proto 文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个 IMPORT_PATH 中,以便编译器可以确定其规范名称。

    展开全文
  • 深入看看 __proto__到底是什么

    千次阅读 2016-07-01 09:26:02
    __proto__属性在js中是相当重要的概念,面向对象编程和委托设计都是围绕它展开的。但同时它在js的内部实现中,却十分的复杂,这里我们好好讨论下这个独特的属性。  首先这个属性是什么?在官方的es5中,定义了一个...
                  __proto__属性在js中是相当重要的概念,面向对象编程和委托设计都是围绕它展开的。但同时它在js的内部实现中,却十分的复杂,这里我们好好讨论下这个独特的属性。
                首先这个属性是什么?在官方的es5中,定义了一个名叫[[prototype]]的属性,每个对象都拥有这样一个属性,这个属性是一个指针,它指向一个名叫原型对象的内存堆。而原型对象也是对象,因此又含有自己的[[prototype]]的属性,又指向下一个原型对象。那么终点是哪? 当然是我们的Object.prototype对象。
              
    也就是说,js中的对象模型大概是这个样子:
             
           注意,这里使用的是[[prototype]],而并非__proto__。那么这是一个东西吗?当然![[prototype]]是官方所定义的属性,而__proto__是浏览器自己对[[prototype]]所做的实现。也就是说,官方并未定义[[prototype]]叫什么,是浏览器自己根据标准制定出来的。但是!在ES6官方把[[prototype]]作为对象的内部属性,跟[[configurable]]一样,外部无法直接访问。现在已经定义了专门访问该内部属性的函数,有兴趣大家可以看看:点击打开链接
          
    不过上面的图,有点问题。当对象刚刚被创建时,它的内部并非如此。实际上它的内部状态分好几种情况:
    1. 使用字面量定义一个对象时: var foo={};
    2. 创建一个函数对象时:function Foo();
    3. 使用构造函数定义一个对象时: var  foo=new Foo();
           以上是最基本的三个状态,其他的状态都是由这三个状态变化而成的。
           1 先来看第一个情况: var foo={};
           此时会生成一个新的对象,而由于没有对他的[[prototype]]进行任何的操作,所以默认情况下所指向的是Object.prototype对象:
        var  foo={};
        foo.__proto__===Object.prototype;//true
        Object.prototype.toString.call(foo.__proto__);//该方法返回对象类型的字符串:object Object 
        foo.hasOwnProperty('__proto__');//false
    
          

           但是,让我们来看下代码的最后一行,执行的结果却是false。也就是__proto__并非在普通对象的内部,它其实是在Object.prototype上。而 foo.__proto__可以正常运行,是因为引擎调用了getter函数,大家可以试下执行:alert(foo.__proto__),你会看到输出结果为:function(){} ,意思为这个指针所访问的内容是个空函数。这个里面比较复杂,有兴趣的可以去看看:深入js这本书。但是一般情况下,我们可以简单的理解为访问__proto__时,就是在访问Object.prototype,如果你没把__proto__指向其他对象的话。
           2 接下来看看比较复杂的第二张情况:function Foo();
          
    我们知道,Foo函数也是一个对象,当然他也可以访问__proto__内部属性。但是他的__proto__的内部属性并非跟普通对象一样指向Object.prototype对象,相反他指向Function.prototype对象,而Function.prototyp对象的__proto__再指向Object.prototype。
         
        function foo(){};
        Object.prototype.toString.call(foo.__proto__);//object  Function
        foo.__proto__===Function.prototype;//ture
       Object.prototype.toString.call(foo.__proto__.__proto__);//object  Object   
        foo.__proto__.__proto__.===Object.prototype;//ture
    
                  
         我们知道,每个Js函数对象,都拥有一个原型对象,这个原型对象是函数自己的属性,并且这个原型对象跟普通对象没有区别,因此它也可以访问__proto__,自然它的__proto__指向的是Object.prototype了。
        function foo(){};
        alert(foo.hasOwnProperty('prototype'));//ture  prototype为foo的自身属性
        alert(foo.propertyIsEnumerable('prototype'));//false  prototype不可枚举
        foo.prototype.__proto__===Object.prototype;// 原型对象访问__proto__属性,指向Object.prototype
    
         也就是说。原型对象是在函数对象下的一个属性,而普通对象是没有原型对象。这就为接下来的用new 调用函数进行面向对象编程打下了基础。
         结合起来,当我们定义一个函数时,内部同时产生了 __proto__和prototype属性
         
          3 最后来看看最复杂的: var foo=new Foo{};
           对于new操作,一般来讲是四步操作:1 生成一个新对象  2 将新对象执行原型链接到函数的prototype对象上,3 把新对象绑定到this上, 4 如果没有return,返回函数执行结果的新对象。
          这里的关键点在于第二步操作。很多人将其解释为,new的新对象是函数自身的prototype对象,其实根本不是如此。通过上面的第二点的代码,我们就已经知道了函数原型对象一开始就便存在,只是它是不可枚举的空对象而已。而且,有个重点:函数原型对象没有在原型链中!也就是说引擎进行右值查询时,根本不会理会函数原型对象!
        

      function foo(){};
      foo.prototype.y=10;
      alert(foo.y);//    undefined
      foo.__proto__.x=10;
      alert(foo.x);//    10
        从上面的我们就可以看出,对函数对象进行原型链查询,是不会检索函数原型对象的。
         那么new所创建的是个什么对象呢? 我们知道在函数作用域里所定义的变脸,外部环境是访问不到的。这样的话,如何把内部所生成好的变量传递给外部变量呢?最主要的方式是使用return .而另一个情况:在c++这些语言中,则是创建一个匿名对象,由匿名对象保存函数所生成的数据,然后当它把数据传递给外表环境时,自身就会释放内存消失掉。
         js中正是使用的后一个方法:它创建一个匿名的对象,负责保存所有的数据,同时在没有return情况下,当对象把数据传递给外表环境后就释放内存:
       
      function Foo(){this.a=a;};
      var test=new Foo(10);

       上面代码的内部情况为:
       
        当Foo函数执行完成后,匿名对象消失,所有的数据都被保存到了test上。此时,test的__proto__就指向了之前的函数原型对象了,而不再是Object.prototype.
    展开全文
  • js中的__proto__和prototype

    千次阅读 2019-09-28 11:45:51
    想弄清原型链其实就是弄清楚__proto__和prototype的关系。 任何对象都有一个__proto__属性 任何方法都有一个prototype属性,prototype也是一个对象 ,所以其中也有一个___proto__ 我们先来看prototype属

    这两天想看看 jquery的源代码。可是一开始就懵逼了。什么是js的原型链呢?于是在网上寻找答案,发现还挺多的。于是就来这里总结一番。

    想弄清原型链其实就是弄清楚__proto__和prototype的关系。

    任何对象都有一个__proto__属性
    任何方法都有一个prototype属性和__proto__属性,prototype也是一个对象 ,所以其中也有一个___proto__

    我们先来看prototype属性

       var fn=function(){
    
        };
        console.log(fn.prototype);
    

    这里写图片描述

    可以看见prototype中有两个属性constructor和__proto__。constructor指向函数自己。同时可以看见prototype中也有__proto__。因为prototype也是一个对象。

    prototype是拿来干什么的呢。它的作用很像java中的静态属性/方法。其中的对象可以给所有实例使用,如

        var fn =function (){
    
        };
        fn.prototype.name='wen';
        fn.prototype.age='18';
        
        var obj1=new fn();
        var obj2=new fn();
    
    
        console.log(obj1.name);
        console.log(obj2.age);
    
    

    输出

    wen
    18
    

    那么__proto__是什么呢?像要弄清这点我们想要知道js中创建对象的一些细节。js创建对象我们有如下三种方法

    1 通过构造函数创建对象

    var obj=new Object();
    

    2通过Object.create创建对象

    var obj1={};
    var obj2= Object.create(obj1);
    
    

    3 通过花括号创建,如

    var obj={};
    

    其实上述三种方法都是通过构造函数创建的。而第三种其实调用的时Object()方法创建的。所以我们可以怎么认为

    js中的对象都是new +构造函数创建的。而这个构造函数就是我们定义的函数。

    而所有的对象中都有__proto__属性,这个属性就是一个指针,指向构造函数中的prototype属性。

    我们可以做一个简单的验证

    
        var obj1={};
        console.log(obj1.__proto__===Object.prototype);
    

    //输出

    true
    

    通过上面的分析,我们知道js中声明的方法其实就是一个类,它的构造函数就是自己。我们可以向其中的prototype塞入各种对象。这些对象会被所有实例继承。而所有的实例都有一个__proto__指针指向着构造函数的prototype属性。

    tips:

    遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject的原型。从 ECMAScript 6 开始,[[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。

    -------------------------------------分割线----------------------------------------------------
    下面是深入的分析。

    非函数对象的创建

    非函数对象只有__proto__,它指向构造函数的prototype属性。我们知道prototype的属性中也有__proto__。那么该__proto__指向谁呢。回忆下一我之前说的所有对象的__proto__指向创建它们的构造函数的prototype。那么谁创建的prototype的呢?。那当然是Object构造函数了。
    所以函数中的prototype属性中的__proto__指向的是Object构造函数的prototype属性。但是问题又来了,这个prototype中也有__proto__,这不死循环了。很高兴的告诉你这个prototype中__proto__等于null的。所以不会死循环的,除非你把它改了。

    如下图
    这里写图片描述

    图片是我之前存下来的,忘了出自哪里了。如果侵害了别人的别人的著作。希望告诉我,我会主动下架的。

    我们来验证一下

     
        var obj1={};
        
    
        console.log(obj1.__proto__.__proto__===null);
    
    

    //输出

    true
    

    好我们接着思考

    函数对象的创建

    函数对象有prototype和__proto__属性。prototype中的__proto__指向的是Object构造函数,这就不用多说了。我们来看__proto__。

    函数对象的__proto__指向构造函数的prototype。而函数的构造函数是Function构造函数。接着
    Function的prototype中的__proto__指向谁呢?前面已经说了指向Object构造函数。那么Function函数的__proto__指向谁呢。答案是指向自己,因为Function函数也是一个函数。来给大家上一张神图。

    这里写图片描述

    接着我们拿出点佐证出来。

        var fn =function (){
    
        };
        //fn.prototype.__proto__->Object.prototype
        //Object.prototype.__proto__===null?
        console.log(fn.prototype.__proto__.__proto__===null);
        //fn.__proto__->Function.prototype
        //Function.prototype.__proto__->Object.prototype
        //Object.prototype.__proto__===null
        console.log(fn.__proto__.__proto__.__proto__===null);
        
    

    从上图你可以看见其实Object构造函数的__proto__
    也是指向Function构造函数的。

    总结

    通过上面的分析,我们可以发现js代码的设计是非常巧妙的。

    总结一下,当你需要理清js中的__proto__和prototype的链接顺序,你只需记住。

    1. __proto__ 指向构造函数的prototype属性
    2. 函数对象有__proto__和prototype属性
    3. 非函数对象只有__proto__属性
    4. prototype中有__proto__属性。且是Object构造函数创建的
    5. 函数对象__proto__指向它的创建者及Function构造函数
    6. Function构造函数__proto__指向它自己
    7. Object对象的prototype中的__proto__是null

    如果您仔细看了这篇文章并且思考过,以上这几点刚好可以线程完美的闭环

    展开全文
  • Protobuf

    2020-08-26 13:51:53
    深入 ProtoBuf-简介 ...Protobuf: protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。 可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种...
    1.创建 .proto文件: student.proto
    2.生成.pb.go 文件: protoc --go_out=plugins=grpc:. student.proto    (.student.proto 和 . 之间有空格)
    3. 编写服务端和客户端的代码
    
    /* 1.syntax : 指定使用proto版本的语法,缺省是proto2。若使用syntax语法,则必须位于文件的非空非注释的第一个行。若不指定proto3,却使用了proto3的语法,则会报错。
        2.package : 指定包名。防止不同 .proto 项目间命名发生冲突。
        3.message : 定义消息类型。
        4.enum : 定义枚举类型。第一个枚举值设置为零。
        5.repeated : 表示被修饰的变量允许重复,可以理解成集合、数组、切片。
        6.map : 待补充
        7.Oneof : 待补充
        8.定义变量 : (字段修饰符) + 数据类型 + 字段名称 = 唯一的编号标识符;
        9.编号标识符 :在message中,每个字段都有唯一的编号标识符。用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变。[1,15]之内的标识符在编码时占用一个字节。[16,2047]之内的标识符占用2个字节。
    */
    

    https://developers.google.com/protocol-buffers/docs/gotutorial

    深入 ProtoBuf-简介

    https://www.jianshu.com/p/a24c88c0526a

    Protobuf: protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

    可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

    简单来讲, ProtoBuf 是结构数据序列化方法,可简单类比于 XML,其具有以下特点:

    1.语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台 高效。
    2.即比 XML 更小(3 ~10倍)、更快(20 ~ 100倍)、更为简单
    3.扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序


    序列化[1]:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。
    类比于 XML[2]:这里主要指在数据通信和数据存储应用场景中序列化方面的类比,但个人认为 XML作为一种扩展标记语言和 ProtoBuf 还是有着本质区别的。

    使用ProtoBuf

    第一步,创建 .proto 文件,定义数据结构,如下例1所示:

    // 例1: 在 xxx.proto 文件中定义 Example1 message
    message Example1 {
        optional string stringVal = 1;
        optional bytes bytesVal = 2;
        message EmbeddedMessage {
            int32 int32Val = 1;
            string stringVal = 2;
        }
        optional EmbeddedMessage embeddedExample1 = 3;
        repeated int32 repeatedInt32Val = 4;
        repeated string repeatedStringVal = 5;
    }
    

    定义了一个名为 Example1 的 消息,语法很简单,message 关键字后跟上消息名称:

    message xxx {
    
    }
    

    之后我们在其中定义了 message 具有的字段,形式为:

    message xxx {
      // 字段规则:required -> 字段只能也必须出现 1 次
      // 字段规则:optional -> 字段可出现 0 次或1次
      // 字段规则:repeated -> 字段可出现任意多次(包括 0)
      // 类型:int32、int64、sint32、sint64、string、32-bit ....
      // 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
      字段规则 类型 名称 = 字段编号;
    }
    

    在上例中,我们定义了:

    1.类型 string,名为 stringVal 的 optional 可选字段,字段编号为 1,此字段可出现 0 或 1 次
    2.类型bytes,名为 bytesVal 的 optional 可选字段,字段编号为 2,此字段可出现 0 或 1 次
    3.类型EmbeddedMessage(自定义的内嵌 message 类型),名为 embeddedExample1 的 optional可选字段,字段编号为 3,此字段可出现 0 或 1 次
    4.类型 int32,名为 repeatedInt32Val 的 repeated可重复字段,字段编号为 4,此字段可出现 任意多次(包括 0)
    5.类型 string,名为 repeatedStringVal 的
    repeated 可重复字段,字段编号为 5,此字段可出现 任意多次(包括 0)

    第二步,protoc 编译 .proto 文件生成读写接口
    我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。

    当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。那么如何实现呢?不用担心, ProtoBuf 将会为我们提供相应的接口代码。如何提供?答案就是通过 protoc 这个编译器。

    可通过如下命令生成相应的接口代码:

    // $SRC_DIR: .proto 所在的源目录
    // --cpp_out: 生成 c++ 代码
    // $DST_DIR: 生成代码的目标目录
    // xxx.proto: 要针对哪个 proto 文件生成接口代码
    
    protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
    

    最终生成的代码将提供类似如下的接口:
    在这里插入图片描述
    例子-序列化和解析接口.png
    在这里插入图片描述
    例子-protoc 生成接口.png

    第三步,调用接口实现序列化、反序列化以及读写
    针对第一步中例1定义的 message,我们可以调用第二步中生成的接口,实现测试代码如下:

    //
    // Created by yue on 18-7-21.
    //
    #include <iostream>
    #include <fstream>
    #include <string>
    #include "single_length_delimited_all.pb.h"
    
    int main() {
        Example1 example1;
        example1.set_stringval("hello,world");
        example1.set_bytesval("are you ok?");
    
        Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();
    
        embeddedExample2->set_int32val(1);
        embeddedExample2->set_stringval("embeddedInfo");
        example1.set_allocated_embeddedexample1(embeddedExample2);
    
        example1.add_repeatedint32val(2);
        example1.add_repeatedint32val(3);
        example1.add_repeatedstringval("repeated1");
        example1.add_repeatedstringval("repeated2");
    
        std::string filename = "single_length_delimited_all_example1_val_result";
        std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
        if (!example1.SerializeToOstream(&output)) {
            std::cerr << "Failed to write example1." << std::endl;
            exit(-1);
        }
    
        return 0;
    }
    

    关于 ProtoBuf 的一些思考
    官方文档以及网上很多文章提到 ProtoBuf 可类比 XML 或 JSON。

    那么 ProtoBuf 是否就等同于 XML 和 JSON 呢,它们是否具有完全相同的应用场景呢?

    个人认为如果要将 ProtoBuf、XML、JSON 三者放到一起去比较,应该区分两个维度。一个是数据结构化,一个是数据序列化。这里的数据结构化主要面向开发或业务层面,数据序列化面向通信或存储层面,当然数据序列化也需要“结构”和“格式”,所以这两者之间的区别主要在于面向领域和场景不同,一般要求和侧重点也会有所不同。数据结构化侧重人类可读性甚至有时会强调语义表达能力,而数据序列化侧重效率和压缩。

    从这两个维度,我们可以做出下面的一些思考。

    XML 作为一种扩展标记语言,JSON 作为源于 JS 的数据格式,都具有数据结构化的能力。

    例如 XML 可以衍生出 HTML (虽然 HTML 早于 XML,但从概念上讲,HTML 只是预定义标签的 XML),HTML 的作用是标记和表达万维网中资源的结构,以便浏览器更好的展示万维网资源,同时也要尽可能保证其人类可读以便开发人员进行编辑,这就是面向业务或开发层面的数据结构化。

    再如 XML 还可衍生出 RDF/RDFS,进一步表达语义网中资源的关系和语义,同样它强调数据结构化的能力和人类可读。

    JSON 也是同理,在很多场合更多的是体现了数据结构化的能力,例如作为交互接口的数据结构的表达。在 MongoDB 中采用 JSON 作为查询语句,也是在发挥其数据结构化的能力。

    当然,JSON、XML 同样也可以直接被用来数据序列化,实际上很多时候它们也是这么被使用的,例如直接采用 JSON、XML 进行网络通信传输,此时 JSON、XML 就成了一种序列化格式,它发挥了数据序列化的能力。但是经常这么被使用,不代表这么做就是合理。实际将 JSON、XML 直接作用数据序列化通常并不是最优选择,因为它们在速度、效率、空间上并不是最优。换句话说它们更适合数据结构化而非数据序列化。

    扯完 XML 和 JSON,我们来看看 ProtoBuf,同样的 ProtoBuf 也具有数据结构化的能力,其实也就是上面介绍的 message 定义。我们能够在 .proto 文件中,通过 message、import、内嵌 message 等语法来实现数据结构化,但是很容易能够看出,ProtoBuf 在数据结构化方面和 XML、JSON 相差较大,人类可读性较差,不适合上面提到的 XML、JSON 的一些应用场景。

    但是如果从数据序列化的角度你会发现 ProtoBuf 有着明显的优势,效率、速度、空间几乎全面占优,看完后面的 ProtoBuf 编码的文章,你更会了解 ProtoBuf 是如何极尽所能的压榨每一寸空间和性能,而其中的编码原理正是 ProtoBuf 的关键所在,message 的表达能力并不是 ProtoBuf 最关键的重点。所以可以看出 ProtoBuf 重点侧重于数据序列化 而非 数据结构化。

    最终对这些个人思考做一些小小的总结:

    1. XML、JSON、ProtoBuf 都具有数据结构化和数据序列化的能力
    2. XML、JSON 更注重数据结构化,关注人类可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注效率、空间、速度,人类可读性差,语义表达能力不足(为保证极致的效率,会舍弃一部分元信息)
    3. ProtoBuf 的应用场景更为明确,XML、JSON 的应用场景更为丰富。

    https://www.jianshu.com/p/a24c88c0526a

    深入 ProtoBuf -编码

    https://www.jianshu.com/p/73c9ed3a4877
    编码结构
    TLV 格式是我们比较熟悉的编码格式。

    所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 便是数据本身。

    ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图:
    ProtoBuf 编码结构图.png:
    在这里插入图片描述
    首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value,如下图所示:
    在这里插入图片描述

    特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。

    继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成:

    field_number: message 定义字段时指定的字段编号
    wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。
    

    整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。

    Tag 结构如下图所示:
    在这里插入图片描述
    3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示:
    在这里插入图片描述
    第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。

    注意其中的 Start group 和 End group 两种类型已被遗弃。

    另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64
    又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。

    重新来看完整的编码结构图:
    在这里插入图片描述
    现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式:

    Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,
    Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。
    

    其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。

    现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field…

    Varints 编码

    上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。

    总结的讲,Varints 编码的规则主要为以下三点:

    1.在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
    2.存储数字对应的二进制补码
    3.补码的低位排在前面
    

    为什么低位排在前面?这里主要是为编码实现(移位操作)做的一个小优化。可以尝试写个二进制移位进行编码解码的小例子来体会这一点。

    先来看一个最为简单的例子:

    int32 val =  1;  // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
    原码:0000 ... 0000 0001  // 1 的原码表示
    补码:0000 ... 0000 0001  // 1 的补码表示
    Varints 编码:0#000 00010x01// 1 的 Varints 编码,其中第一个字节的 msb = 0
    

    编码过程:
    数字 1 对应补码 0000 … 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 … 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到

    0 | 000 00010x01

    解码过程:
    我们再做一遍解码过程,加深理解。

    编码结果为 0#000 0001(0x01)。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。

    在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。

    注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。

    再看一个需要两个字节的数字 666 的编码:

    int32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
    原码:000 ... 101 0011010  // 666 的源码
    补码:000 ... 101 0011010  // 666 的补码
    Varints 编码:1#0011010  0#000 01019a 05// 666 的 Varints 编码
    
    

    编码过程:
    666 的补码为 000 … 101 0011010,从后依次向前取 7 位组并反转排序,则得到:

    0011010 | 0000101
    

    加上 msb,则

    1 0011010 | 0 00001010x9a 0x05

    解码过程:
    编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:

    0011010  000 0101
    

    将这两个 7-bit 组反转得到补码:

    000 0101 0011010
    

    然后还原其原码为 666。

    注意:这里编码数字 666,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666。

    仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。

    这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 228,而不再是 232 了。
    这意味着什么?意味着当数字大于 228 时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。
    但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 228 的数字比大于 228 的数字出现的更为频繁。

    到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。

    int32 val = -1
    原码:1000 ... 0001  // 注意这里是 8 个字节
    补码:1111 ... 1111  // 注意这里是 8 个字节
    再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
    上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
    最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
    Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01

    注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。

    为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。
    为什么之前讲正数的时候没有这种扩展?。请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容!

    所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决?

    ZigZag 编码

    在上一节中我们提到了 Varints 编码对负数编码效率低的问题。

    为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。

    ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:

    ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码

    如下图所示:
    在这里插入图片描述
    ZigZag 编码.png

    对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。

    例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。

    这里的“映射”是以移位实现的,并非存储映射表。

    Varint 类型

    介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。

    在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit。

    接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。

    注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。

    int32、int64、uint32、uint64、bool、enum

    当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。

    其中 bool 的本质为 0 和 1,enum 本质为整数常量。

    在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。

    int32、int64、uint32、uint64
    来看一个最简单的 int32 的小例子:

    syntax = "proto3";
    
    // message 定义
    message Example1 {
        int32 int32Val = 1;
    }
    

    在程序中设置字段值为 1,其编码结果为:

    //  设置字段值 为 1
    Example1 example1;
    example1.set_int32val(1);
    // 编码结果
    tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01
    

    在程序中设置字段值为 666,其编码结果为:

    //  设置字段值 为 666
    Example1 example1;
    example1.set_int32val(666);
    // 编码结果
    tag-(Varints)00001 000 + value-(Varints)1#0011010  0#000 0101 = 0x08 0x9a 0x05
    

    在程序中设置字段值为 -1,其编码结果为:

    //  设置字段值 为 1
    Example1 example1;
    example1.set_int32val(-1);
    // 编码结果
    tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01
    

    int64、uint32、uint64 与 int32 同理

    bool、enum

    bool 的例子:

    syntax = "proto3";
    
    // message 定义
    message Example1 {
        bool boolVal = 1;
    }
    

    在程序中设置字段值为 true,其编码结果为:

    //  设置字段值 为 true
    Example1 example1;
    example1.set_boolval(true);
    
    // 编码结果
    tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01
    

    在程序中设置字段值为 false,其编码结果为:

    //  设置字段值 为 false
    Example1 example1;
    example1.set_boolval(false);
    
    // 编码结果

    这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么?
    这里是 ProtoBuf为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为false。也就是说将 false
    编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是false)。如此,可进一步节省空间提高效率。

    enum 的例子:

    syntax = "proto3";
    
    // message 定义
    message Example1 {
        enum COLOR {
            YELLOW = 0;
            RED = 1;
            BLACK = 2;
            WHITE = 3;
            BLUE = 4;
        }
        // 枚举常量必须在 32 位整型值的范围
        // 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
        COLOR colorVal = 1;
    }
    

    在程序中设置字段值为 Example1_COLOR_BLUE,其编码结果为:

    //  设置字段值 为 Example1_COLOR_BLUE
    Example1 example1;
    example1.set_colorval(Example1_COLOR_BLUE);
    
    // 编码结果
    tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04
    

    sint32、sint64

    sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。
    来看 sint32 的例子:

    syntax = “proto3”;

    // message 定义

    message Example1 {
        sint32 sint32Val = 1;
    }
    

    在程序中设置字段值为 -1,其编码结果为:

    //  设置字段值 为 -1
    Example1 example1;
    example1.set_colorval(-1);
    
    // 编码结果,1 映射回 -1 
    tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01
    

    在程序中设置字段值为 -2,其编码结果为:

    //  设置字段值 为 -2
    Example1 example1;
    example1.set_colorval(-2);
    
    // 编码结果,3 映射回 -2
    编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03
    

    sint64 与 sint32 同理。

    int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正数场景下需要提供 uint 类型。

    64-bit 和 32-bit 类型

    64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。

    为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints就不太合适了,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 256 大的话,64-bit 这个类型比 uint64高效,如果数值比 228 大的话,32-bit 这个类型比 uint32 高效。

    fixed64、sfixed64、double

    来看例子:

    // message 定义
    syntax = "proto3";
    
    message Example1 {
        fixed64 fixed64Val = 1;
        sfixed64 sfixed64Val = 2;
        double doubleVal = 3;
    }
    

    在程序中分别设置字段值 1、-1、1.2,其编码结果为:

    //  设置字段值 为 -2
    example1.set_fixed64val(1)
    example1.set_sfixed64val(-1)
    example1.set_doubleval(1.2)
    
    // 编码结果,总是 8 个字节
    09 # 01 00 00 00 00 00 00 00
    11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)
    19 # 33 33 33 33 33 33 F3 3F
    

    fixed32、sfixed32、float

    与 64-bit 同理。

    Length-delimited 类型

    string、bytes、EmbeddedMessage、repeated

    终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value

    这种编码方式很好理解,来看例子:

    syntax = "proto3";
    
    // message 定义
    message Example1 {
        string stringVal = 1;
        bytes bytesVal = 2;
        message EmbeddedMessage {
            int32 int32Val = 1;
            string stringVal = 2;
        }
        EmbeddedMessage embeddedExample1 = 3;
        repeated int32 repeatedInt32Val = 4;
        repeated string repeatedStringVal = 5;
    }
    

    设置相应的值:

    Example1 example1;
    example1.set_stringval("hello,world");
    example1.set_bytesval("are you ok?");
    
    Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();
    
    embeddedExample2->set_int32val(1);
    embeddedExample2->set_stringval("embeddedInfo");
    example1.set_allocated_embeddedexample1(embeddedExample2);
    
    example1.add_repeatedint32val(2);
    example1.add_repeatedint32val(3);
    example1.add_repeatedstringval("repeated1");
    example1.add_repeatedstringval("repeated2");
    

    最终编码的结果为:

    0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64 
    12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F 
    1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F 
    22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)
    2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?)
    

    读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行…)

    补充 packed 编码

    在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。

    packed 目前只能用于 primitive 类型。

    packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value…,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value…

    上一节例子中 repeatedInt32Val 字段的编码结果为:

    22 | 02 02 03
    

    22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。

    深入 ProtoBuf - 序列化源码解析
    https://www.jianshu.com/p/62f0238beec8

    展开全文
  •   作为一名前端工程师,必须搞懂JS中的prototype、__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞懂它们。这里说明一点,__...
  • 再说说__proto__和prototype以及js的继承

    千次阅读 2018-08-22 20:37:48
    1.proto和prototype JS中的原型链已经是一个老生常谈的问题,毕竟也是JS 这门语言的特色之一了。 首先“万物皆对象“,虽然这句话一直有争议,但是有它的道理的,null类型这些的争论这里就不说了。 对象中有个...
  • prototype和__proto__

    千次阅读 2019-06-19 11:33:28
    prototype和__proto__关系图 概念 prototype是函数的一个属性(每个函数都有一个prototype属性),这个属性是一个指针,指向一个对象。它是显示修改对象的原型的属性。 __proto__是一个对象拥有的内置属性,是JS...
  • proto.zip

    2020-05-26 23:30:06
    lightcnn 训练的proto
  • Protobuf语言指南 l&amp;nbsp;&amp;nbsp;定义一个消息(message)类型 l&amp;nbsp;&amp;nbsp;标量值类型 l&amp;nbsp;&amp;nbsp;Optional 的字段及默认值 l&...a
  • Protobuf的介绍

    万次阅读 2018-07-20 11:26:05
    参看:... 参看:https://developers.google.com/protocol-buffers/docs/proto3 一、简介  Google Protocol Buffer(简称Protobuf)是Google公司内部的混合语言数据标准,用于RPC系统...
  • 包名的含义与平台语言无关,这个package仅仅被用在proto文件中用于区分同名的message类型。可以理解为message全名的前缀,和message名合起来唯一标识一个message类型。比如com.user.User与com.company.User就是两个...
  • protobuffer 编译时报错;...Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax 在.proto 文件开始加上  syntax = "proto2"; 或 syntax = "proto3"; 来指明 使用版本
  • 如何在一个proto中调用另一proto的message先看一看这个proto的文件目录 现在我们想要在HandoutTypeProto中使用HandoutProto中的一个message,首先这2个文件必须有如下包名 然后在HandoutTypeProto的proto中写上...
  • protobuf引入不同包下的proto文件

    万次阅读 2018-03-31 10:50:03
    比如现在有个RankInfoRes.proto文件,它的package是 com.road.ddt.proto.command现在有com.road.ddt.proto.game包的一个proto引用它1、引入部分package com.road.ddt.proto.game;option java_package = "...
  • protobuf之导入其他proto文件

    千次阅读 2019-08-16 18:38:35
    场景:假如有文件hundredbulls.proto,需要导入另一个文件common.proto,两者在同一个目录中. 导入方式 在hundredbulls.proto文件的开头,使用关键字import导入另一个文件,如下↓↓↓↓↓↓↓ 使用方式 假设...
  • proto文件生成go代码

    千次阅读 2018-04-03 22:11:36
    一句话总结:多个文件在一个包里需一起编译,有依赖其他proto文件需手动改导入路径 1、单个文件 protoc --go_out=plugins=grpc:. hello.proto 2、多个文件 protoc --go_out=plugins=grpc:. *.proto 3、多个...
  • Protobuf 的 proto3 与 proto2 的区别

    千次阅读 2017-09-04 14:51:48
    总的来说,proto3 比 proto2 支持更多语言但 更简洁。去掉了一些复杂的语法和特性,更强调约定而弱化语法。如果是首次使用 Protobuf ,建议使用 proto3 。 1、在第一行非空白非注释行,必须写: syntax = ...
  • 用protoc编译.proto文件遇到的问题

    千次阅读 2019-06-26 17:14:20
    报错:object_detection/protos/flexible_grid_anchor_generator.proto: File not found. object_detection/protos/grid_anchor_generator.proto: File not found. object_detection/protos/multiscale_anchor_...
  • js 对象转数组

    千次阅读 2016-08-05 14:13:13
    var arr = []; for(i in obj){ arr.push(i); } 或 只需要一句,arr.__proto__=[];但是要注意浏览器支持__proto__才行,最新的浏览器应该都支持
  • 彻底理解什么是原型链,prototype和__proto__的区别。

    万次阅读 多人点赞 2018-04-19 20:45:12
    1.Javascript中所有的对象都是Object的实例,并继承Object.prototype的属性和方法,也就是说,Object.prototype是所有对象的爸爸。(个人感觉搞清楚这一点很重要) ...而定义普通的对象的时候,就会生成一个__proto_...
1 2 3 4 5 ... 20
收藏数 135,238
精华内容 54,095
关键字:

proto