转载_ google-Protocol-Buffers

来源:互联网 发布:机械能守恒实验数据 编辑:程序博客网 时间:2024/05/02 07:06
使用Protocol-Buffers很久了, 现在所在的公司中虽然没有直接使用它, 但是也有自己的和Protocol-Buffers异曲同工的序列/反序列化工具.  最近决定翻译一下Protocol-Buffers的官网上的文章, 增加自己对它细节的理解, 也可以方便需要的人.

     这篇文章提供了一份使用Protocol-Buffers的注意细节, 其实就是类似于其他语言中的最佳实践, 由一些条目组成. 其实老外的文章并不一定都是那么的好, 哪怕是google的工程师, 他们写出来的这些文章页并不一定就是字字珠玑, 其实也有很多啰嗦和不清楚的地方. 所以, 我在这里先总结一个自己阅读这些文章中读到的推荐的使用规则.
    1. required 相当于数据库中的那些not null的列. required 的域不利于协议的扩展. 所以推荐使用optional, 当然required也有适用的地方.
    2. Protocol-Buffers只适合于小型消息.
    3. Protocol-Buffers的数据传输格式是不含结尾符的, 在多个消息之间的间隔要自己去实现.
    4. 1~15 的tag值占用的空间最少.
    5. 一定要看一下Protocol-Buffers 编码原理(英), 知其所以然, 才能用好, 后面我会翻译(好了, 呵呵).

下面是译文:

技巧

    本文描述了在使用Protocol-Buffers时常用的设计模式, 惯用法. 你也可以去Protocol-Buffers讨论组提出关于Protocol-Buffers的设计和使用的问题, 并获得解答.

多消息转化为流
    如果你想将多个消息写入单个文件或者流中, 那么你必须自己来记录跟踪一个消息和下一个消息边界. Protocol-Buffers的数据传输格式是不含结尾符的, 所以Protocol-Buffers解析器无法确定一个消息的结尾. 解决该问题的最简单方案就是在写入一个消息之前, 先写入该消息的长度大小, 当你读取消息的时候, 先读取消息的大小, 然后读入该大小字节数的数据读入一个独立的缓冲区中, 然后解析数据. 如果你想避免到独立缓冲区中的拷贝, 请看CodedInputStream类的使用方法 — 你可以用它来限制只读取指定大小字节的数据.

大数据集
    Protocol-Buffers不是设计来处理大消息的. 根据经验, 如果单条消息大于1M, 那么就应该采取其他的策略. 
    相反Protocol-Buffers 很适合处理一个大数据集内有多个单独的消息的情况. 通常大数据集是许多小数据的集合, 每个小数据块都是一块结构化数据. Protocol-Buffers并不能一次就处理整个数据集, 然而使用Protocol-Buffers 来编码每个小数据块可以很大的简化你的问题, 现在只需要处理一组字符串, 而不是一组结构体了.
    Protocol-Buffers 没有内置任何对大数据集的支持, 因为不同的应用情况需要不同的解决方案. 有时一个简单的记录列表就能解决问题, 而在其他情况下, 可能使用数据库是一种更好的选择. 对每个解决方案都开发一个独立的程序库的代价是很大的.

联合类型 (用处不大)
    有时你可能想发送一个消息, 其类型可以是几个不同的类型之一. 然而Protocol-Buffers 解析器无法仅凭消息内容来决定消息的类型. 这种情况下, 你如何确保接收方应用能知道怎么解析消息呢? 一个解决方案是创建一个封装的消息, 其中含N个optional的字段, 每一个字段对应一种消息类型.
    例如, 如果你有消息类型Foo, Bar, Baz, 那么你可以按类型将它们结合起来, 就如:

    message OneMessage {
        optional Foo foo = 1;
        optional Bar bar = 2;
        optional Baz baz = 3;
    }

    也可以添加一个枚举字段来标识拿一个消息被填充了, 这样的话就可以使用switch来处理:

    message OneMessage {
        enum Type {FOO = 1; BAR = 2; BAZ = 3;}
        required Type type = 1;
        optional Foo foo = 2;
        optional Bar bar = 3;
        optional Baz baz = 4;
    }

    如果你有大量可能的类型, 那么你的容器类型中将它们一一列举出来可能比较困难, 在这种情况下, 你应该考虑使用扩展:

     message OneMessage {
        extensions 100 to max;
    }
    extend OneMessage {
        optional Foo foo_ext = 100;
        optional Bar bar_ext = 101;
        optional Baz baz_ext = 102;
    }

    注意你可以使用 ListFields 反射函数来获取消息中所有字段的列表, 其中包含扩展. 这个功能在实现分派消息句柄时用的上它.

自描述的消息 (没啥用, 还绕口, 不推荐看)
    Protocol-Buffers 不包含自我描述的信息. 因此如果只提供序列化后的二进制数据, 而不提供对应的.proto文件的话, 很难从中提取什么有用的信息.
    然而, 一个,proto文件消息定义格式本身也可以使用Protocol-Buffers格式来描述, 源码包中的src/google/protobuf/descriptor.proto 文件定义了Protocol-Buffers消息格式. 通过使用 -descriptor_set_out 选项, protoc编译器可以输出一个FileDescriptorSet — 这个集合表示一系列的.proto文件. 利用它, 可以定义一个自描述的协议消息.

    message SelfDescribingMessage {
        required FileDescriptorSet proto_files = 1;
        required string type_name = 2;
        required bytes message_data = 3;
    }

    通过使用DynamicMessage 这样的类, 就可以编写出操作自描述消息的工具.
    总之, Protocol-Buffers库之所以没有包含这个特性, 是因为我们在Google里面还从来没有使用它的机会.

英文地址: Protocol-Buffers Techniques



  上一篇翻译了Protocol-Buffers技巧, 继续努力, 呵呵!
    强烈建议大家好好研读一下这一篇, 不看我翻译的也行, 直接看英文的也挺好的, 但是一定要看一下, 因为这篇文章中我个人认为可以获得的收获是:


    1. Protocol-Buffers官网号称的高效率的编码速度, 很小的二进制编码量, 等等优点. 但是对于我们来说, 这个听上去总是有一种道听途说的感觉, 我觉得不管你说的再好, 严肃的程序员往往还是会追求定量的描述, 而不是定性的大帽子. 这篇文章中就可以尽解你心头的疑虑.
    2. mode in google, google出品的东西在我们看来总是附着了一层光环, 没办法, 谁叫人家是名门之秀呢, 在平常的工作中, 我们也可能会自己去实现类似Protocol-Buffers的工具, 所以, 学习Protocol-Buffers, 看看人家是怎么实现这些的功能的, 以及在碰到同样的棘手的问题时, 人家是怎么处理的.
    3. 有的地方翻译的不好, 有点绕口, 请及时查看英文版本.

下面是译文:

编码

    本文描述了Protocol-Buffers消息二进制编码的格式, 你不许要了解它们就可以很好的使用Protocol-Buffers, 但是如果花些时间来了解它, 将非常有助于你了解Protocol-Buffers的不同格式对最终encoded-message大小的影响.

一个简单的消息
     让我们看一个非常简单的message的定义:

      message Test1 {
          requeired int32 a = 1;
      }

     在一个应用中, 你创建了一个名为Test1的message, 然后给字段a 赋值为150, 然后将这个message序列化到输出流中. 如果你查看输出结果, 将看到这个message被编码为3个byte:

      08 96 01

     当然, 它看起来是个很小的数字 — 但是这个数字含义是什么? 请继续阅读..

 

 Base 128 Varints
     为了理解Protocol-Buffers是如何对上面演示的简单message进行编码的, 你首先得理解Varints的概念. Varints是一种使用一个或者多个byte来序列化整数的方法, 特点是越小的数值使用越少的byte来编码.
     在一个Varint中, 除了最后一个byte, 其他的每个byte的最高有效位(most significant bit 简称msb)都被置位, 这个msb被用来表示接下来的一个byte也是当前整数编码的一部分. byte的低7位bit用来存储该整型数二进制补码的7个bit分组, 整数的低7bit被存储在第一个byte中.
     举个例子, 这里有一个整数1 — 只需要一个byte就行, 所以msb不用置位.

     0000 0001

     对于整数300来说 — 稍复杂一些

    1010 1100 0000 0010

     如何理解这个编码就是300? 首先你需要清除所有byte的msb, 因为msb只是用来表征是否已经达到一个整数编码的结尾 (就像你看到的一样, 当下一个byte也是该整数的编码时, 这个byte的msb才需要被置位).

     1010 1100 0000 0010
    -> 010 1100  000 0010

     然后对得到的所有bit以7bit为一组进行次序反转, 因为前面提到, Varint将较低的7bit组存在字节流的较前位置; 然后连接它们你将得到结果:

     010 1100  000 0010
反转-> 000 0010 010 1100
连接-> 0001 0010 1100
转十进-> 256 + 32 + 8 + 4 = 300

 

消息结构
     一个Protocol-Buffers message是由一系列的key-value组成. message对应的二进制版本使用字段编号(field number)来作为key — 每个字段的名字和类型是在该message类型的定义(编写.proto文件)和生成(根据.proto生成message)时决定的.
     当一个message被编码时, key-value一起序列化到byte流中. 当消息被解码时, 解码器可以忽略那些它不能识别的字段. 正因为有了这样的机制, 你可以在不破坏老的程序的情况下向message中加入新的字段. 其实在一个wire-format的message中的每一个key也是一个pair值 — 包括你在.proto文件中定义的field编号, 以及一个wire-type, 用来提供刚刚好可以找出后续byte长度的信息.
     wire-type的定义如下表

TypeMeaningUsed For0Varintint32,int64,uint32,uint64,sint32,uint64,bool,enum164-bitfixed64,sfixed64,double2Length-delimitedstring,bytes,embedded message,packed repeated fields3Start groupgroups(deprecated)4End groupgroups(deprecated)532-bitfixed32,sfixed32,float

     序列化好的消息中的每个key都是一个Varint, 它的值的计算公式是

     field_number << 3 | wire_type

    换句话说, key的低三位bit用来存储wire-type信息.
    现在让我们来看一个简单的例子, 你现在知道了在一个流中第一个byte肯定是一个key, 类型为Varint, 值为08,

     000 1000

     你可以使用低3 bit查到它的wire-type, 为0, 然后右移3位得到field编号值为1. 所以你可以得到结论, 将要从流中读出的是一个tag为1的Varint. 现在你就可以使用前面学到的Varint的解码知识来获得数据. 假设流中的下两个byte是96 01, 那么你就可以析出数值150

     96 01 = 1001 0110 0000 0001
               -> 000 0001  001 0110 (抹掉msb并反转)
               -> 1001 0110
               -> 128 + 16 + 4 + 2 = 150

 

带符号的整型
     在前面的小节中我们看到的Protocol-Buffers的wire-type都是0, 并且都使用Varint编码方法. 但是有符号的int类型比如sint32和sint64和标准的int类型比如int32和int64编码时有一个很重要的区别. 如果你使用int32或者是int64表示负数, 则最后总是编码成10个byte, 这是挺高效的做法, 处理起来就像很大的无符号int一样. 如果你使用有符号类型如sint32或sint64, 则会使用ZigZag算法来编码, 这是一种更高效编码.
     ZigZag编码将有符号整数映射到无符号整数空间, 所以那些绝对值很小的数(比如-1), 就有很小的Varint编码. ZigZag编码就用这种方式来处理正负数, 所以-1的编码是1, 1的编码是2, -2的编码是3等等, 你可以从下面的表中看到.

      原数值   编码00-1112-2321474836474294967294-21474836484294967295

     换句话说, 每个数字都是用如下的编码公式:

     对于sint32:  (n << 1) ^ (n >> 31)
     对于sint64:  (n << 1) ^ (n >> 63)

     注意 >> 是算术位移:), 所以换句话说, 对于正数来说, 这个位移的结果是全0 bits, 对于负数来说, 这个位移的结果是全1 bits. 当sint32和sint64被解析时, 它就会被还原回有符号数.

 

Non-Varint 数值
      Non-Varint类型很简单, double和fixed64的wire-type是1, 它表明后续的64个bit是一个数值. 类似的float和fixed32的wire-type是5, 它表明后续的32个bit是一个数值. 这些类型都是以little-endian存储的.

 

字符串
     字符串的wire-type是2(长度是确定的), 后面紧跟一个Varint, 用来表示后续byte的个数.例如

     message Test2 {
          requeired string b = 2;
     }

     给它赋值"testing"后, 编码如下

      12 07 74 65 73 74 69 6e 67

    其中红色部分是"testing"的UTF8编码, 黑色的0×12表示tag=2, wire-type=2, 蓝色的0×07表示后面有7个byte数据.

嵌入式消息
     这里有一个消息, 在它内部有一个嵌入式的消息:

     message Test3 {
           required Test1 c = 3;
     }

     它的编码:

     1a 03 08 96 01

     就像你看到的那样, 红色的低三个byte就是我们前面演示的Test1的编码, 前面的0×03表征了后续数据的byte个数, 0x1a表示了tag=3, wire-type=2, 在编码嵌入式message时和编码String时是一样的.

 

Optional和Repeated元素
     如果你的message定义中含有repeated字段(而且没有使用[packed=true]选项), 那么该消息的编码中就会有0个或者多个具有相同tag值的key-value组合. 这些repeated值在编码中不一定是连续存储的; 它们中间可能会夹有消息中其他字段的值. 但repeated各个元素之间的顺序是有保证的, 这种顺序会在解析的时候被正确还原, 之所以repeated字段和其他字段的顺序是不保存的, 因为这些顺序并没有任何意义.
     如果你使用了optional元素的话, 在编码中可能会有或者没有该tag的key-value组合.
     通常, 一个编码过的消息中不会出现多于一个具有optional或required属性的字段的实例. 然而解析器必须对这种情况做处理. 对于数值类型和String, 如果相同的tag字段出现多次的话, 解析器将会接受最后一次出现的值作为最终值. 对于嵌入式消息字段, 解析器会试图合并多个相同tag值的值, 就像Message::MergeFrom 方法一样 — 换句话说, 对于数值标量, 用新值来代替老的值; 对于嵌入式消息, 合并他们的值; 对于repeated字段, 进行连接操作. 这个规则的效应就是当把两段编码解析为一个消息时, 解析两段编码的组合或者是分别解析两段编码并合并结果的效果是一样的, 如下

     MyMessage message;
     message.ParseFromString(str1 + str2);

     是等价于:

      MyMessage mess1, mess2;
      mess1.ParseFromString(str1);
      mess2.ParseFromString(str2);
      mess1.MergeFrom(mess2);

     这个属性有时很有用, 比如通过使用它, 可以在不知道类型的情况下合并两个消息.

 

打包Repeated字段
     Protocol-Buffers版本2.1.0中引入了打包的repeated字段, 在定义repeated字段的时候, 可以在后面指定[packed=true]选项. 打包的repeated字段和repeated字段是一样的, 只是在编码时有所不同而已. 一个打包的repeated字段包含有0个元素时, 它将不会出现在消息的编码中. 另一方面, 所有的元素都会被打包进一个单独的key-value组合, wire-type为2(和String类似). 每个元素都使用相同的方法打包起来, 这些元素不再将tag也打包进去.
     举个例子, 想象一下你有一个消息类型是:

     message Test4 {
          repeated int32 d = 4;
     }

     现在我们构造一个Test4的实例, 并且赋值3, 270和86942三个数给repeated字段d, 然后, 编码为

      22             // tag=4 wire-type=2
      06             // 负载大小 6bytes
      03             // 第一个元素 Varint 3
      8E 02        // 第二个元素 Varint  270
      9E A7 05  // 第三个元素 Varint 86942

     注意, 不是所有类型都能被打包的, 只有repeated字段的元素类型是原始的数值时(类型是Varint, 32-bit或64-bit的wire-type)才能能声明为"packed".
     注意, 虽然常常没有理由编码多于一个的key-value组合给packed repeated 字段, 但是编码器也必须为发生这种情况该如果处理做好准备, 一般在这种情况下, 数值编码负载将被连接在一起, 每个组合必须包含完整的数值元素的编码.

 

字段的顺序
     你可以在.proto文件中使用任意顺序的字段编号, 当消息会根据字段编号的顺序来序列化, 这个方式在C++/Java/Python的序列化代码中都有实现. 因为这种方式的存在, 解析代码可以根据字段编号顺序来进行优化. 但是, Protocol-Buffers解析器也必须能够解析以任意字段编号序列化的消息. 毕竟要被序列化的对象不可能都是一些简单的对象 — 比如, 通过简单的连接来合并两个消息.
     如果一个消息含有未知字段, 当前实现中, C++/Java的做法是将它们以任意顺序写到有序字段的后面, Python的做法是忽视这些未知字段.




  上一篇翻译了Protocol-Buffers编码, 现在翻译Protocol-Buffers C++指南:), 个人在使用中主要是使用C++方面的, 所以这篇文章对于我来说收获很大, 其中, 我认为最重要的就是
    1. 文中明确指出了一些禁忌的实践, 比如通过继承来扩展对象的行为等, 这些特别值得注意.
    2. 在定义Protocol-Buffers中定义协议时, 要考虑扩展性, 注意参照文中扩展协议的相关文字.

下面是译文:

Protocol Buffer Basics: C++

    这个教程提供了面向C++程序员的Protocol-Buffers简单介绍. 你可以通过一些简单的示例程序学习到如下知识:  
    1. 如何书写.proto文件, 及如何定义消息格式.
    2. 如何使用Protocol-Buffer的编译器.
    3. 如何使用Protoco-Buffer的C++ API来读写消息.
    本文并不是一份完整的Protocol-Buffers的C++使用指南. 在Protocol-Buffers的官方主页上, 你可以找到更多的参考资料:
     Protocol-Buffers 语言指南
     Protocol-Buffers C++ API 手册
     Protocol-Buffers C++ 代码生成指南
     Protocol-Buffers 编码原理手册

    为什么要使用Protocol-Buffers?
    打个比方, 我们要实现地址薄这样一个简单的应用, 它可以通过读写文件来获取和保存朋友的通信信息, 每一个朋友的信息都包含名字, ID, E-mail, 以及电话号码.
    你打算怎么样实现朋友信息数据结构的序列化和反序列化? 这里有一些不同的方法可以解决这个问题.
        1. 将内存中对应的数据结构直接以二进制的方式保存. 随着时间的流逝, 这种方法会显得很脆弱, 因为收到/读取代码必须按照严格的内存布局进行, 比如大小端的问题等等, 而且, 这种做法下, 文件中大量个原始结构的数据越来越多, 以及程序被越来越多的人使用, 在这样的背景下想要扩展朋友的信息结构是十分困难的.
        2. 你可以选择一种特别的方式来将数据都编码成字符串. 比如将四个int编码成"12:3-23:67". 这的确是一种简单而且很富有弹性的方式, 但是它要求你编写一次性的编码和解码代码, 在解析的时候也将耗费不少的运行时间. 对于简单的数据结构这是一个好的编码方式.
        3. 把数据序列化成为XML. 这是一个很有吸引力的方案, 因为XML是一种可读的格式, 而且很多语言和程序库都直接支持XML的读写操作. 当应用要和别的应用或项目共享数据时, 这是一个绝妙的主意. 但是, XML的空间占用臭名昭彰, 而且它的解码和编码也很可能成为应用中的性能热点. 而且, 即使是对于一些简单的数据结构来说, 他们的XML DOM也是很复杂的.
        4. Protocol-Buffers 是针对这类问题的一个弹性的, 高效率的, 自动生成解决方案. 为了使用Protocol-Buffers, 你可以书写.proto文件来描述你的数据结构. 通过使用Protocol-Buffers 编译器处理.proto文件, 就可以创建该数据结构的类, 以及通过高效的二进制来整解编码代码的生成. 生成的类给每个数据域提供了getter和setter.更重要的是, Protocol-Buffers结构提供了一种扩展原有数据格式的方案, 而且代码依然可以和读取老的结构的数据.

    在哪里可以找到Protocol-Buffers的实例代码?
    实例代码在Protocol-Buffers源码包中的 ./examples 目录下.

    如何定义自己的协议格式?
    为了创建上面所说的地址薄应用, 首先我们需要编写一个.proto文件. .proto文件的格式很简单: 为每一个需要序列化的数据结构添加一个消息, 然后为消息中的每个域指定一个名字和类型. 下面定义了一个多个消息的文件 addressbook.proto

    package tutorial;
    message Person {
        required string name = 1;
        required int32 id = 2;
        required 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;
    }

    可以看出来, Protocol-Buffers的语法和C++, Java的语法很类似. 让我们依次来查看一下文件中每一部分的作用.
    package是打包的意思, 为了防止不同项目之间命名的冲突. 对应的C++名字空间. 事实上, 它也正是被Protocol-Buffers编译器装换成C++的名字空间.
    下面就是Person消息的定义了. 一个消息就是有类型的字段的集合. Protocol-Buffers提供了很多标志, 简单的数据类型可供使用作为字段的类型, 包括bool, int32, float, double, string. 你也可以使用消息嵌套, 及使用其他的消息类型作为字段的类型, 在上面的例子中, 消息Person中的phone字段就是这样一个例子.
    在每一个字段后, 都有类似的=1, =2等等标志, 用来指出该字段的唯一标志, 这个主要用于二进制编码中. 标号1~15被编码时, 比更大的标识号使用的字节数要少1个, 所以这是一个优化点, 给经常使用或重复的字段使用1~15的标志, 其他经常使用的optional字段使用 >= 16的标志. 在重复字段中, 每一项都要求重复编码标志号, 所以重复的字段特别适合小的标志号.
    注意, 每个字段都必须使用一下之一的特征符来标记:
        1. required 该字段必须有值, 否者这个消息会被认为是"未初始化的". 如果libprotobuf是以debug版本, 序列化一个未初始化消息会导致断言. 在release版本, 这个检查会被跳过, 消息会被写入. 然而, 解析一个未初始化消息依然会失败, 表现是解析函数返回false. 除此之外, 一个required字段和一个optional字段没有区别.
        2. optional 该字段可以没有值. 如果没有指定值, 它就会使用默认值. 对于简单类型来说, 可以指定自己的默认值, 就像Person.phone.type字段. 如果不给它指定默认值, 就会使用系统默认值0, string默认值是空字符串, bool默认值false. 对嵌套消息来说, 默认值总是消息的默认实例或原型, 即没有任何一个字段指定了值. 调用访问类来取一个未显式指定值的optional或required字段的值, 总是返回该字段的默认值.
        3. repeated 该字段可能重复N次, 包括0. 重复的值的顺序将被保存在Protocol-Buffers中. 可以将重复字段视为动态数组.
    注: required 修饰的字段是永久性的, 在把一个字段标示为required的时候, 要特别小心. 如果在某些情况下你不想写入或者发送一个required字段, 那么该字段更改成optional时会遇到麻烦, 旧版本的程序收到该消息后会认为不含有该字段的消息是不完整的, 未初始化的, 从而拒绝解析该消息. 在这种情况下, 你必须考虑编写特别针对应用的, 自定义的消息效验函数,在Google, 一些工程师得出required 修饰符弊大于利的结论, 他们更愿意使用optional和repeated. 当然, 这个观点并不具有普遍性.
    你可以在Protocol-Buffers 语言指南中找到如何编写.proto文件的完整说明, 包括所有可用的字段类型. 但是要记住, Protocol-Buffers 没有继承这一特性, 因为它不是用来干这个的.

    如何编译Protocol-Buffers的.proto文件?
    在编写完一个.proto文件之后,下一步就要生成可以读写AddressBook消息(当然也就包括了Person以及PhoneNumber消息)的类了. 此时你需要运行Protocol-Buffers 编译器来编译你的.proto文件:
    1. 如果还没有安装Protocol-Buffers 编译器, 必须先下载安装包, 并参照README文件中的说明来安装它.
    2. 安装编译器之后, 就可以运行编译器了. 指定源目录(即你的应用程序源代码所在的目录——默认使用当前目录), 目标目录(即生成的代码放置的目录, 通常与$SRC_DIR是一样的), 以及你的.proto文件所在的目录, 就像下面:

        protoc -I=$SRC_DIR –cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

    使用了-cpp_out选项参数生成的是C++类——Protocol-Buffers 为其他支持的语言提供了类似的选项参数. 键入上面的命令就可以在你指定的目标目录下生成如下文件:

    addressbook.pb.h  : 声明你生成的类的头文件
    addressbook.pb.cc : 你生成的类的实现文件

    Protocol-Buffers 消息的API
    让我们看一下Protocol-Buffers 编译器生成的代码, 看看它为你创建了什么样的类和函数. 先看tutorial.pb.h文件, 可以看到得到了一个类, 它对应于tutorial.proto文件中定义的每一个消息. 进一步看Person 类: 编译器为其每一个字段生成了对于的读写函数. 例如name, id, email以及phone字段分别有如下函数:

    // name
      inline bool has_name() const;
      inline void clear_name();
      inline const ::std::string& name() const;
      inline void set_name(const ::std::string& value);
      inline void set_name(const char* value);
      inline ::std::string* mutable_name(); 
  // id
      inline bool has_id() const;
      inline void clear_id();
      inline int32_t id() const;
      inline void set_id(int32_t value); 
  // email
      inline bool has_email() const;
      inline void clear_email();
      inline const ::std::string& email() const;
      inline void set_email(const ::std::string& value);
      inline void set_email(const char* value);
      inline ::std::string* mutable_email(); 
  // phone
      inline int phone_size() const;
      inline void clear_phone();
      inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
    inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
      inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
      inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
      inline ::tutorial::Person_PhoneNumber* add_phone();

    从代码中可以看出, getter函数具有与字段名相同的名字, 并且是小写的, 而setter函数都是以set_作为前缀开头. 此外还有has_前缀的函数, 对每一个单一的(required或optional的)字段来说, 如果字段被置(set)了值, 也就是不是空值, 该函数会返回true. 最后每一个字段还有一个clear_前缀的函数, 用来将字段重置(un-set)到空状态(empty state).
    注意, 数值类型的字段id就只有如上所述的基本读写函数, 而name和email字段则有一些额外函数, 因为它们是string类型——前缀为mutable_的函数返回string的直接指针即char*指针. 除此还有一个额外的setter函数. 可以在email还没有被置(set)值的时候就调用mutable_email(), 它会被自动初始化为一个空字符串而返回NULL. 在这个例子中, 如果有一个单一消息字段, 那么它就会有一个mutable_ 前缀的函数, 但是没有一个set_ 前缀的函数.
    重复的字段有一些特殊的函数——就像重复字段phone一样, 有这样一些函数:
        1. 得到重复字段的_size(换句话说,这个Person关联了多少个电话号码).
        2. 通过索引(index)来获取一个指定的电话号码.
        3. 通过指定的索引(index)来更新一个已经存在的电话号码.
        4. 向消息(message)中添加另一个电话号码, 然后你可以编辑它(重复的标量类型有一个add_前缀的函数, 允许你传新值进去).
    关于编译器如何生成特殊字段的更多信息, 请查看文章C++ generated code reference.

    枚举和嵌套类
    生成的代码中包含一个名为PhoneType的枚举, 对应于addressbook.proto文件中的那个PhoneType 同名枚举. 这个类型其实是Person::PhoneType, 值为Person::MOBILE, Person::HOME和Person::WORK(实现的细节有一些复杂, 但不理解它并不影响你使用该枚举).
    Protocol-Buffers 编译器生成了一个Person::PhoneNumber嵌套类. 通过观察生成的代码, 就可以发现"真正的"类实际上叫做Person_PhoneNumber, 只不过Person 内部的typedef让你将它视作一个嵌套类来对待. 唯一要注意的一点就是:如果想在另一个文件中对该类进行前向声明, 因为在C++中, 你不能对嵌套类型进行前向声明, 但是你可以通过对Person_PhoneNumber进行前向声明来实现这一点.

    标准消息函数
    每一个消息还包含了其他一系列的函数, 用来检查和管理该消息, 比如:
        bool IsInitialized() const; 检查是否全部的required字段都被置了值.
        string DebugString() const; 返回一个可读的消息表示形式, 对调试特别有用.
        void CopyFrom(const Person& from); 用一个给定的消息来覆写消息的值.
        void Clear(); 将所有项复位到空状态.
    这些函数以及后面章节将要提到的I/O函数实现了Message 的接口, 所有C++ Protocol-Buffers消息类都使用这些统一的接口. 更多信息请查看文章 complete API documentation for Message.

    解析和序列化
   
最后, 每一个Protocol-Buffers类都有一些将消息转化成为Protocol-Buffers的二进制格式以及从二进制格式数据还原成原来消息对象的函数. 它们包括:
        bool SerializeToString(string* output) const; 将消息序列化并储存在指定的string中. 注意里面的内容是二进制的, 而不是文本, 这里只是将string作为一个方便的容器.
        bool ParseFromString(const string& data); 从给定的string解析消息, 同上, string里面的内容是二进制的, 而不是文本, 只是将string作为一个方便的容器.
        bool SerializeToOstream(ostream* output) const; 将消息写入到给定的C++ ostream中.
        bool ParseFromIstream(istream* input); 从给定的C++ istream解析消息.
这些只是用于告诉你Protocol-Buffers对象的解析和序列化的几个可选函数, 完整的相关API列表请参考Message API reference.

    Protocol-Buffers和面向对象的设计
    Protocol-Buffers消息类只是纯粹的数据存储器(就像C++中的结构体那样). 它们在对象模型中并不是一等公民. 如果你想向生成的类中添加更丰富的行为, 最好的方法就是在应用程序中对它进行封装. 在你无权控制.proto文件的设计的情况下, 封装Protocol-Buffers也是一个好主意(例如, 你从另一个项目中重用一个.proto文件). 你可以用封装类来设计接口, 以更好地适应你的应用程序的特定环境: 隐藏一些数据和方法, 暴露一些便于使用的函数等等. 但绝对不要通过继承生成的类来为其添加行为. 这样做会破坏其内部机制并且不是一个好的面向对象的实践.

    写消息
    现在来尝试使用你的Protocol-Buffers类. 你想让你的address book程序完成的第一件事情就是向address book文件写入详细的个人信息. 要实现这一点, 你需要创建Protocol-Buffers类的实例对象并将它们写入到一个输出流中.
    下面的这个程序从一个文件中读取AddressBook, 然后根据用户的输入向其中添加一个新的Person, 然后再将新的AddressBook写回文件中. 由Protocol-Buffers编译器生成的代码或者直接调用的代码都被突出显示了.

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, ‘\n’);         
  cout << "Enter name: ";
  getline(cin, *person->mutable_name());
  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }
  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }
    tutorial::Person::PhoneNumber* phone_number = person->add_phone();
    phone_number->set_number(number);
    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}
// Main function:  Reads the entire address book from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  // Add an address.
  PromptForAddress(address_book.add_person());
  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

    注意GOOGLE_PROTOBUF_VERIFY_VERSION宏. 你最好像这样——尽管这不是严格要求的——在使用C++ Protocol-Buffers库之前执行该宏, 它会检查你是不是在无意中链接到了与你使用的头文件不兼容的Protocol-Buffers库. 如果检测到了不匹配情况, 程序会中止运行下去. 注意, 每一个.pb.cc文件在开始的时候都会自动调用该宏.
    另外还需要注意的是程序结尾处调用的ShutdownProtobufLibrary()函数. 该函数所做的所有工作就是删除由Protocol-Buffers库分配的全局对象. 在大多数程序中, 这都是没有必要的, 因为进程一退出, 操作系统就回收了它的内存. 然而, 如果你使用了内存检查工具来检查你的程序的话(内存检查工具要求每一个对象最后都要被释放)或者你写了一个可能会在一个进程中多次被加载卸载的库, 那么你可能就需要强制Protocol-Buffers来清理了.

    读消息
    当然, 如果你不能从一个address book中取出信息的话, 那么它也就没什么用了! 下面的例子展示了如何读取上面的程序创建的文件, 并将读到的所有信息打印出来.

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.person_size(); i++) {
    const tutorial::Person& person = address_book.person(i);
    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;
    if (person.has_email()) {
      cout << "  E-mail address: " << person.email() << endl;
    }
    for (int j = 0; j < person.phone_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phone(j);       switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}
// Main function:  Reads the entire address book from a file and prints all
//   the information inside.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook address_book;
  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  ListPeople(address_book);
  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

    扩展一个Protocol-Buffers
   
无论或早或晚, 在你发布了使用Protocol-Buffers的代码之后, 你必定会想"改进"Person 的定义. 如果你想让你的新Person 向后兼容, 并且旧的Person 能够向前兼容——你一定希望如此——那么你在新的Person 中就要遵守其他的一些规则了
     1. 对已存在的任何字段, 你都不能更改其标识tag号.
    2. 你绝对不能添加或删除任何required的字段.
    3. 你可以添加新的optional或repeated的字段, 但是你必须使用新的标识tag号(例如, 在这个Person 中从未使用过的标识号——甚至于已经被删除过的字段使用过的标识号也不行).
    4. 有一些例外情况, 但是它们很少使用.
    如果你遵守这些规则, 老的代码将能很好地解析新的消息, 并忽略掉任何新的字段. 对老代码来说, 已经被删除的optional字段将被赋予默认值. 已被删除的repeated字段将是空的. 新的代码也能够透明地读取旧的消息. 但是请牢记心中: 新的optional字段将不会出现在旧的消息中, 所以你要么需要显式地检查它们是否由has_前缀的函数置了值, 要么在你的.proto文件中, 在标识tag号的后面用[default = value]提供一个合理的默认值. 如果没有为一个optional项指定默认值, 那么就会使用与特定类型相关的默认值: 对string来说默认值是空字符串, 对boolean来说默认值是false, 对数值类型来说默认值是0. 还要注意: 如果你添加了一个新的repeated字段, 你的新代码将无法告诉你它是否被留空了(被新代码), 或者是否从未被置值(被旧代码), 这是因为它没有has_标志.

    优化的小技巧
    Protocol-Buffers 的C++库已经做了很大的优化. 但是你必须正确的使用它, 才能提高性能. 下面是一些小技巧用来提升Protocol-Buffers库的最后一丝速度能力.
   如果有可能的话, 最好重复利用消息对象. 即使被清除掉内容, 消息对象也会尽量保存所有被分配来重用的内存. 如果你正在处理很多类型相同的消息以及一系列相似的结构就可以重复使用同一个消息对象, 从而使内存分配的压力减小一些. 然而随着时间的增加, 对象占用的内存也有可能变得越来越大, 尤其是当你的消息尺寸不同时或者你偶尔创建了一个比平常大很多的消息的时. 你应该自己监测消息对象的大小——通过调用SpaceUsed函数——并在它太大的时候删除它.
    在多线程中分配大量小对象的内存的时候, 你的操作系统的内存分配器可能做得不够好. 在这种情况下, 你可以尝试一下Google’s tcmalloc.

     高级用法
    Protocol-Buffers不仅仅可以实现用来做简单的数据存取以及序列化. 可以阅读C++ API reference全文来看看它还能用来做什么.
    Protocol-Buffers消息类所提供的一个关键特性–反射. 有了它, 你不需要编写针对一个特殊的消息类型的代码, 就可以遍历一个消息的字段并操纵它们的值, 就像XML和JSON一样. "反射"的一个更高级用法可能就是可以找出两个相同类型的消息之间的区别, 或者开发某种"协议消息的正则表达式", 利用正则表达式可以对某种消息内容进行匹配. 只要你发挥你的想像力就有可能将Protocol-Buffers应用到一个更广泛的程度, 这很可能远远超过你一开始期望解决的问题.
    "反射"是由Message::Reflection interface提供的.




原创粉丝点击