Protobuf简介

Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个.proto文件。他们用于 RPC 系统和持续数据存储系统。

Protocol Buffers是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

主要参考资料:源码站点官方文档Google Protocol Buffer 的使用和原理ProtoBuf开发者指南

Protobuf特性

优点:

  • Protobuf 有如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

  • 它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

  • Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。

缺点:

  • Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。

  • XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。

  • 由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容

proto文件

编写proto文件,定义程序中需要处理的结构化数据,结构化数据被称为Message。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。proto文件类似于C的数据定义。

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

使用protobuf内置的编译器编译该proto,指定其生成C++语言的“订单包装类”(一般来说,一个message结构会生成一个包装类)。然后使用类似以下代码来序列化/解析该订单包装类:

        // 发送方
        Oreder order;
        order.set_time(XXX);
        order.set_userid(123);
        order.set_price(100);
        order.set_desc("order")
        
        string sOrder;
        order.SeraizeToString(&sOrder);     //将序列化后的字符串发送出去
        
        // 接收方
        Order order;
        if( order.ParseFromString(sOrder) )     //解析该字符串
        {   
            cout << order.userid() << order.desc();
        }

更复杂的版本:

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

消息格式很简单,每个消息类型有一个或多个特定的数学字段,每个字段有一个名字和一个值类型。

每个消息字段都有一个唯一的数字标签,这些标签用来表示该字段在二进制消息中的位置。并且一旦指定标签号,在使用过程中是不可以更改的,标记这些标签号在1-15的范围内每个字段需要使用1个字节来编码这一个字节(包括字段所在的位置和字段的类型),标签号在16-2047范围内需要使用2个字节来编码。最好将1-15的标签号为频繁使用到的字段保留。另外,不能使用19000-19999的标签号。

指定字段规则

  • required:必须有此字段(双方都要有)
  • optional:此字段是可选的(双方可选)
  • repeated:本字段的值可以拥有任意个,重复的值的次数会保存下来(双方可选,数组)

repeated字段如果是基本的数字类型的话,会无法编码。新的代码应该使用特殊的关键字[packed=true]来使其得到有效的编码。

编译Protobuf

运行编译器,需添加源文件目录,目标文件目录,与proto文件路径

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

在指定的目标目录下,会生成以下文件:***.pb.h***.pb.cc

API接口

***.ph.h的文件中:

  • set_开头的函数,通过其进行设置

  • has_方法,对每个字段(required或optional),如果其被设置则返回true

  • clear_方法设置该字段为最初的空状态

  • mutable_方法让使用者直接拥有字符串的指针,可通过其进行额外设置,如果email还没设置,也可调用mutable_email(),初始化为空字符串

repeated字段有一些特殊的方法:

  • _size有多少个repeat被设置
  • 用index得到指定的repeated字段
  • 对指定index更新数据
  • add_在message中添加字段

枚举

消息中的枚举可表示为如下形式:

        message Person{
            enum PhoneType{
                MOBILE=0;
                HOME=1;
                WORK=2;
            }
        }

则枚举表达为Person::PhoneType,其值为Person::MOBILEPerson::HOMEPerson::WORK

标准消息方法

每个消息类也包含了一些方法来检查或者操作整个消息,包括:

  • bool IsInitialized() const 检测是否所有字段都已经被设置

  • string DebugString() const 返回消息的可读形式,对于debugging比较有用

  • void CopyFrom( const Person& from) 覆盖已给定的消息值

  • void Clear() 清楚所有状态返回空

序列化和反序列化

灭个protobuf类都有方法来读写消息,通过使用protobuf的二进制形式,包括:

  • bool SerilizeToString(string* ouput) cosnt序列化消息,储存在给定的字符串中。注意这些bytes为二进制,而不是文本,我们只是使用string类作为容器

  • bool ParseFromString(const string& data) 对给定的字符串反序列化

  • bool SerializeToOstream(ostream* output) 将给定的消息写进给定的C++输出流

  • bool ParseFromIstream(istream* input) 从C++输入流中反序列化消息

  • 另外,如果想要增加已生成类的功能,最好的方法就是在新的应用类中加入已存在的protobuf,不应该对已生成类增加新行为

兼容

  • 向后兼容:新模块识别老模块,从老模块扩展到新模块时,可将所所添加的属性”状态“设置为optional或者缺省值

  • 向前兼容:老模块识别新模块,在新模块中,所增加的新属性被会忽略

消息与字段名

使用骆驼风格的大小写命名,即单词首写字母大写,做消息名。使用GNU的全部小写,使用下划线分割的方式定义字段名:

        message SongServerRequest{
            required string song_name=1;
        }

使用这中命名方法所得到的名字,在C++下如下:

        const string& song_name() {...}
        void set_song_name(const string& x) {...}

枚举

使用骆驼风格做枚举名,而用全部大写做值的名字

        enum Foo{
            FIRST_VALUE=1;
            SECOND_VALUE=2;
        }

Python

Protobuf在Python中生成的类并不会直接生成存取数据的代码,而是生成消息描述,枚举和字段:

        class Person(message.Message):
            __metaclass__=reflection.GeneratedProtocolMessageType
            
            class PhoneNumber(message.Message):
                __metaclass__=reflection.GeneratedProtocolMessageType
                DSECRIPTION=_PERSON_PHONENUMBER

其中__metaclass__=reflection.GeneratedProtocolMessageType通过Python的元类机制工作。在载入时,GeneratedProtocalMessageType元类使用特定的描述符创建Python方法,随后就可使用完整的功能:

        import addressbook_pb2
        person=addressbook_pb2.Person()
        person.id=1234
        person.name="John Doe"