zoukankan      html  css  js  c++  java
  • Google数据交换格式:ProtoBuf

    Protocol Buffer

    ProtocolBuffer是Google公司的一个开源项目,用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

    一个例子

    比如有个电子商务的系统(假设用C++实现),其中的模块A需要发送大量的订单信息给模块B,通讯的方式使用socket。

    假设订单包括如下属性:
    --------------------------------
      时间:time(用整数表示)
      客户id:userid(用整数表示)
      交易金额:price(用浮点数表示)
      交易的描述:desc(用字符串表示)
    --------------------------------

    如果使用protobuf实现,首先要写一个proto文件(不妨叫Order.proto),在该文件中添加一个名为"Order"的message结构,用来描述通讯协议中的结构化数据。该文件的内容大致如下:

    message Order
    {
      required int32 time = 1;
      required int32 userid = 2;
      required float price = 3;
      optional string desc = 4;
    }

    然后,使用protobuf内置的编译器编译该proto。由于本例子的模块是C++,你可以通过protobuf编译器让它生成 C++语言的“订单包装类”(一般来说,一个message结构会生成一个包装类)。

    然后你使用类似下面的代码来序列化/解析该订单包装类:

    发送方:

    Order order;
    order.set_time(XXXX);
    order.set_userid(123);
    order.set_price(100.0f);
    order.set_desc("a test order");
    string sOrder;
    order.SerailzeToString(&sOrder);

    然后调用某种socket的通讯库把序列化之后的字符串sOrder发送出去;

    接收方:

    string sOrder;
    // 先通过网络通讯库接收到数据,存放到某字符串sOrder
    // ......
    Order order;
    if(order.ParseFromString(sOrder)){ // 解析该字符串
      cout << "userid:" << order.userid() << endl
              << "desc:" << order.desc() << endl;
    } else {
      cerr << "parse error!" << endl;
    }

    有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了。万一将来需求发生变更,要求给订单再增加一个“状态”的属性,那只需要在Order.proto文件中增加一行代码。对于发送方,只要增加一行设置状态的代码;对于接收方只要增加一行读取状态的代码。另外,如果通讯双方使用不同的编程语言来实现,使用这种机制可以有效确保两边的模块对于协议的处理是一致的。

    从某种意义上讲,可以把proto文件看成是描述通讯协议的规格说明书(或者叫接口规范)。这种伎俩其实老早就有了,搞过微软的COM编程或者接触过CORBA的同学,应该都能从中看到IDL的影子。它们的思想是相通滴。

    ProtoBuf支持向后兼容(backward compatible)和向前兼容(forward compatible):

    1. 向后兼容,比如说,当接收方升级了之后,它能够正确识别发送方发出的老版本的协议。由于老版本没有“状态”这个属性,在扩充协议时,可以考 虑把“状态”属性设置成非必填 的(optional),或者给“状态”属性设置一个缺省值;
    2. 向前兼容,比如说,当发送方升级了之后,接收方能够正常识别发送方发出的新版本的协议。这时候,新增加的“状态”属性会被忽略;

    向后兼容和向前兼容有啥用捏?俺举个例子:当你维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的向后兼容或向前兼容。

    proto文件

    如上面的例子,使用protobuf,首先需要在一个 .proto 文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。

    例如:

    syntax = "proto3";

    message Person { required string name=1; required int32 id=2; optional string email=3; enum PhoneType { MOBILE=0; HOME=1; WORK=2; } message PhoneNumber { required string number=1; optional PhoneType type=2 [default=HOME]; } repeated PhoneNumber phone=4; }

    一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是 query() 和 set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:

    Person person;
    person.set_name("John Doe");
    person.set_id(1234);
    person.set_email("jdoe@example.com");
    fstream.output("myfile",ios::out | ios::binary);
    person.SerializeToOstream(&output);

    然后,你可以读取报文中的数据:

    fstream input("myfile",ios::in | ios:binary);
    Person person;
    person.ParseFromIstream(&input);
    cout << "Name: " << person.name() << endl;
    cout << "E-mail: " << person.email() << endl;

    你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。

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

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

    protobuf3 消息定义

    message由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式:

    字段规则 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]

    字段规则:

    • singular:格式良好的消息可以包含该字段中的零个或一个(但不超过一个);
    • repeated:此字段可以在格式良好的消息中重复任意次数(包括零),将保留重复值的顺序;

    字段名称:

    字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。
    protobuf建议字段的命名采用以下划线分割的驼峰式,例如 first_name 而不是firstName。

    字段编码值:

    编码值的取值范围为 1~2^32(4294967296)。

    消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。

    [1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。

    切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

    字段默认值:

    当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:

    • 对于string,默认是一个空string
    • 对于bytes,默认是一个空的bytes
    • 对于bool,默认是false
    • 对于数值类型,默认是0
    • 对于枚举,默认是第一个定义的枚举值,必须为0;
    • 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的;

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

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

    另外:

    • message消息支持嵌套定义,消息可以包含另一个消息作为其字段,也可以在消息内定义一个新的消息;
    • proto定义文件支持import导入其它proto定义文件;
    • 每个proto文件指定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。

     标量数值类型

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

     其中:

    varint(type=0),动态类型

    1. 每个字节第一位表示有无后续字节,有为1,无为0,(双字节,低字节在前,高字节在后);
    2. 剩余7位倒序合并。
    举例: 300 的二进制为 10 0101100
    
    第一位:1(有后续) + 0101100
    
    第二位:0(无后续) + 0000010
    
    最终结果: 101011000000010

    枚举类型

    在下面的例子中,在消息格式中添加了一个叫做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值作为默认值。
    • 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。

     

  • 相关阅读:
    Flexbox布局(转)
    css兼容性
    本地存储 localStorage/sessionStorage/cookie
    正则去掉字符串空格
    ajax请求成功或失败的参数
    怎样实现页面跳转和刷新
    new Date()时间对象
    列车时刻表查询 jqm/ajax/xml
    jquery mobile 学习总结
    响应式学习总结
  • 原文地址:https://www.cnblogs.com/chenny7/p/5157335.html
Copyright © 2011-2022 走看看