六、protobuf的使用和原理

yongfutian · · 1977 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

protobuf详解

1、安装

  • 源码包下载

      git clone  https://github.com/google/protobuf
    
  • 安装依赖

      sudo apt-get install autoconf automake libtool curl make g++ unzip
    
  • 生成需要的配置脚本

      cd protobuf //进入源码库中
      ./autogen.sh //运行脚本生成需要的配置脚本
    
  • 编译安装

      ./configure
      make
      make check
      sudo make install
      sudo ldconfig  //用于刷新共享库的缓存
    
  • 安装proto-gen-go

      go get -u github.com/golang/protobuf/proto
      go get -u github.com/golang/protobuf/protoc-gen-go
      
      vi  ~/.bashrc
      export PATH="$PATH:$GOPATH/bin"
      source ~/.bashrc
    

2、定义×××.proto文件

package example;

enum FOO { X = 17; };

message Test {
    required string label = 1;
    optional int32 type = 2 [default=77];
    repeated int64 reps = 3;
    optional group OptionalGroup = 4 {
        required string RequiredField = 5;
    }
}

生成.pb.go文件

protoc --go_out=. *.proto //如果这个命令执行过程中出现 protoc-gen-go: program not found or is not executable 这个错误,表示该protoc-gen-go没有被加入到Path环境变量中,应该把该文件的所在目录加入Path变量中。该文件存放在环境变量GOPATH目录下的bin子目录里

golang中可用的protobuf的接口

在使用之前,我们先了解一下每个 Protobuf 消息在 Golang 中有哪一些可用的接口:
1.  每一个 Protobuf 消息对应一个 Golang 结构体
2. 消息中域名字为 camel_case 在对应的 Golang 结构体中为 CamelCase
3. 消息对应的 Golang 结构体中不存在 setter 方法,只需要直接对结构体赋值即可,赋值时可能使用到一些辅助函数,例如: msg.Foo = proto.String("hello")
4. 消息对应的 Golang 结构体中存在 getter 方法,用于返回域的值,如果域未设置值,则返回一个默认值****
5. 消息中非 repeated 的域都被实现为一个指针,指针为 nil 时表示域未设置消息中 repeated 的域被实现为 slice
6. 访问枚举值时,使用“枚举类型名_枚举名”的格式(更多内容可以直接阅读生成的源码)
7. 使用 proto.Marshal 函数进行编码,使用 proto.Unmarshal 函数进行解码

3、定义一个消息类型

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
} 
  • 3.1 指定字段类型
    在上面的例子中,所有字段都是标量类型:

      两个整型(page_number和result_per_page)
      一个string类型(query)
    

当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型

  • 3.2 分配标识号
    正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。

      注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
    

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。

    不可以使用其中的[19000-19999],即从:
    FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber 的标识号

Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

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

在proto3中,repeated的标量域默认情况虾使用packed。
你可以了解更多的pakced属性在Protocol Buffer 编码

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

      message SearchRequest {
        string query = 1;
        int32 page_number = 2;
        int32 result_per_page = 3;
      }
       
      message SearchResponse {
       ...
      }
    
  • 3.5 添加注释
    向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式,如:

      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.
      }
    
  • 3.6 保留标识符(Reserved)
    如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。

      message Foo {
        reserved 2, 15, 9 to 11;
        reserved "foo", "bar";
      }
    
  • 3.7 从.proto文件生成了什么?标量数值类型
    当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息

      1 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类
      
      2 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问
      
      3 对go来说,编译器会位每个消息类型生成了一个.pd.go文件
    
      4 对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。
      
      5 javaNano来说,编译器输出类似域java但是没有Builder类
      
      6 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
      
      7 对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
    

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

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT) ByteString string

可以在文章Protocol Buffer 编码中,找到更多“序列化消息时各种类型如何编码”的信息。

1 在java中,无符号32位和64位整型被表示成他们的整型对应形似,最高位被储存在标志位中。
2  对于所有的情况,设定值会执行类型检查以确保此值是有效。
3  64位或者无符号32位整型在解码时被表示成为ilong,但是在设置时可以使用int型值设定,在所有的情况下,值必须符合其设置其类型的要求。
4  python中string被表示成在解码时表示成unicode。但是一个ASCIIstring可以被表示成str类型。
5 Integer在64位的机器上使用,string在32位机器上使用
  • 3.8 默认值
    当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
    对于strings,默认是一个空string
    对于bytes,默认是一个空的bytes
    对于bools,默认是false
    对于数值类型,默认是0
    对于枚举,默认是第一个定义的枚举值,必须为0;

    对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide

    对于可重复域的默认值是空(通常情况下是对应语言中空列表)。

    注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。

    查看generated code guide选择你的语言的默认值的工作细节。
  • 3.9 枚举
    当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。
    在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:

      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的语法格式。
当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。
关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言generated code guide

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

      message SearchResponse {
        repeated Result results = 1;
      }
       
      message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
      }
    
  • 3.11 嵌套类型
    你可以在其他消息类型中定义、使用消息类型,在下面的例子中,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;
    }

4、导入定义

  • 4.1 使用proto2消息类型嵌套类型

  • 4.2 更新一个消息类型
    如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

      1.不要更改任何已有的字段的数值标识。
      2.如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)
      3.非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
      4.int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
      5.sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
      6.string和bytes是兼容的——只要bytes是有效的UTF-8编码。
      7.嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
      8.fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
      9.枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的
    
  • 4.3 Any
    Any类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入import 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。
不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。

    // 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替换拓展

  • **4.4 Oneof **
    如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.
    Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()或者WhichOneof() 方法检查哪个oneof字段被设置, 看你使用什么语言了.
    使用Oneof
    为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的test_oneof:

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

然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用repeated 关键字.
在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言API指南中找到oneof API介绍.
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不支持repeated.
反射API对oneof 字段有效.

  • 4.5 向后兼容性问题Map

  • 4.6 向后兼容性问题Package

  • 4.7 包及名称的解析定义服务(Service)

5、JSON 映射

  • 5.1选项
  • 5.2自定义选项生成访问类

原始链接:https://www.cnblogs.com/tohxyblog/p/8974763.html


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:yongfutian

查看原文:六、protobuf的使用和原理

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1977 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传