美文网首页
Protobuf 学习笔记

Protobuf 学习笔记

作者: Jancd | 来源:发表于2018-06-22 03:19 被阅读73次

    文章内容源自Google官方文档翻译,详见原文Language Guide。部分内容可能重复,望多见谅。

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

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

    SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

    注意:

    • 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。

    消息定义

    1. 指定字段类型

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

    2. 分配标识号

    正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。

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

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

    3. 指定字段规则

    所指定的消息字段修饰符必须是如下之一:

    • required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;
    • optional:消息格式中该字段可以有0个或1个值(不超过1个)。
    • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。

    由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:

    repeated int32 samples = 4 [packed=true];
    

    required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。

    Google的一些工程师得出了一个结论:使用required弊多于利;他们更 愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。

    4. 添加更多消息类型

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

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

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

    6. .proto文件生成了什么

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

    • 对go来说,编译器会位每个消息类型生成了一个.pd.go文件。
    • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
    • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
    • 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。

    7. 标量数值类型

    一个标量消息字段可以自动生成的访问类中定义的类型.

    更多“序列化消息时各种类型如何编码”的信息请看这里

    8. Optional的字段和默认值

    消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解 析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。例如,要为 SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:

    optional int32 result_per_page = 3 [default = 10];
    

    如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。

    9. 枚举

    当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个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;
    }
    

    如果给枚举常量定义别名, 需要设置allow_alias option 为 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的语法格式。

    当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。


    使用

    message支持嵌套使用,作为另一message中的字段类型

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

    导入定义(import)

    可以使用import语句导入使用其它描述文件中声明的类型

    import "others.proto";
    

    protocol buffer编译器会在-I / --proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。


    Message嵌套

    栗子:

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

    内部声明的message类型名称只可在内部直接使用,在外部引用需要前置父级message名称,如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;
            }
        }
    }
    

    Map类型

    proto3 支持 map 类型声明:

    map<key_type, value_type> map_field = N;
    
    message Project {...}
    map<string, Project> projects = 1;
    
    • 键、值类型可以是内置的标量类型,也可以是自定义message类型
    • 字段不支持repeated属性
    • 不要依赖map类型的字段顺序

    包(Packages)

    在.proto文件中使用package声明包名,避免命名冲突。

    syntax = "proto3";
    package foo.bar;
    message Open {...}
    

    在其他的消息格式定义中可以使用包名+消息名的方式来使用类型,如:

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

    在不同的语言中,包名定义对编译后生成的代码的影响不同:

    • C++ 中:对应C++命名空间,例如Open会在命名空间foo::bar中
    • Java 中:package会作为Java包名,除非指定了option jave_package选项
    • Python 中:package被忽略
    • Go 中:默认使用package名作为包名,除非指定了option go_package选项
    • JavaNano 中:同Java
    • C# 中:package会转换为驼峰式命名空间,如Foo.Bar,除非指定了option csharp_namespace选项

    定义服务(Service)

    如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码。

    例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

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

    生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。

    比较蛋疼的是即便业务上不需要参数也必须指定一个请求消息,一般会定义一个空message。


    选项(Options)

    在定义.proto文件时可以标注一系列的options。Options并不改变整个文件声明的含义,但却可以影响特定环境下处理方式。完整的可用选项可以查google。

    一些选项是文件级别的,意味着它可以作用于顶层作用域,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,可以用在消息定义的内部。当然有些选项可以作用在字段、enum类型、enum值、服务类型及服务方法中。

    但是到目前为止,并没有一种有效的选项能作用于这些类型。

    以下是一些常用的选择:

    • java_package (file option):指定生成java类所在的包,如果在.proto文件中没有明确的声明java_package,会使用默认包名。不需要生成java代码时不起作用
    • java_outer_classname (file option):指定生成Java类的名称,如果在.proto文件中没有明确声明java_outer_classname,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),不需要生成java代码时不起任何作用
    • objc_class_prefix (file option): 指定Objective-C类前缀,会前置在所有类和枚举类型名之前。没有默认值,应该使用3-5个大写字母。注意所有2个字母的前缀是Apple保留的。

    基本规范

    • 描述文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

    • 结构定义包括:message、service、enum

    • rpc方法定义结尾的分号可有可无

    • Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式

    message SongServerRequest {
        required string song_name = 1;
    }
    
    • Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
    enum Foo {
        FIRST_VALUE = 1;
        SECOND_VALUE = 2;
    }
    
    • Service与rpc方法名统一采用驼峰式命名

    • message对应golang中的struct,编译生成go代码后,字段名会转换为驼峰式

    编译

    通过定义好的.proto文件生成Java, Python, C++, Go, Ruby, JavaNano, Objective-C, or C# 代码,需要安装编译器protoc。

    参考Github项目 google/protobuf 安装编译器,Go语言需要同时安装一个特殊的插件:golang/protobuf。

    运行命令:

    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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
    

    这里只做参考就好,具体语言的编译实例请参考详细文档,

    相关文章

      网友评论

          本文标题:Protobuf 学习笔记

          本文链接:https://www.haomeiwen.com/subject/yhuhyftx.html