Google protocol buffer简介

  1. 云栖社区>
  2. 博客>
  3. 正文

Google protocol buffer简介

code_xzh 2017-04-29 11:21:08 浏览1834
展开阅读全文

Google Protocol Buffer

Google Protocol Buffer又简称Protobuf,它是一种很高效的结构化数据存储格式,一般用于结构化数据的串行化,即我们常说的数据序列化。这种序列化的协议非常轻便高效,而且是跨平台的,目前已支持多种主流语言。通过这种方式序列化得到的二进制流数据比传统的XML, JSON等方式的结果都占用更小的空间,并且其解析效率也更高,非常适合用于通讯协议或数据存储。

为什么使用Protocol Buffers

通常序列化和解析结构化数据的几种方式?

  • 使用Java默认的序列化机制。这种方式缺点很明显:性能差、跨语言性差。
  • 将数据编码成自己定义的字符串格式。简单高效,但是仅适合比较简单的数据格式。
  • 使用XML序列化。比较普遍的做法,优点很明显,人类可读,扩展性强,自描述。但是相对来说XML结构比较冗余,解析起来比较复杂性能不高。

缺点

其最大的缺点应该就是它缺乏自描述性,所以它不适合用来描述数据结构。我们只能通过.proto来解读其文件结构。

Protocol Buffer使用

1,定义Protocol Buffer消息

message SearchRequest 
{
  required string query = 1;
  optional int32 page_number = 2;// Which page number do we want?
  optional int32 result_per_page = 3;// Number of results to return per page.
}

该消息定义了三个字段,两个int32类型和一个string类型的字段,每个字段由字段限制,字段类型,字段名和Tag四部分组成.对于C++,每一个.proto文件经过编译之后都会对应的生成一个.h和一个.cc文件。

2, 指定字段的类型

下面的表格列出了消息里域允许的字段类型:
这里写图片描述

字段限制共有3类:
required:必须赋值的字段
optional:可有可无的字段
repeated:可重复字段(变长字段),类似于数值
由于一些历史原因,repeated字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:

Tag

消息中的每一个字段都有一个独一无二的数值类型的Tag.1到15使用一个字节编码,16到2047使用2个字节编码,所以应该将Tags 1到15留给频繁使用的字段。可以指定的最小的Tag为1, 最大为2^{29}-1或536,870,911.但是不能使用19000到19999之间的值,这些值是预留给protocol buffer的。

3,使用

当定义好了.proto文件,并且下载安装好对应版本的compile后,执行以下命令可以生成对应的.h和.cc文件。 其中$SRC_DIR表示希望生成的文件所在的目录,以及对应.proto所在文件目录位置。

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

后将生成的头文件引入要工程项目中,直接调用里面对应的方法就好了。

语法

可选字段与缺省值

在消息解析时,如果发现消息中没有包含可选字段,此时会将消息解析对象中相对应的字段设置为默认值,可以通过下面的语法为optional字段设置默认值。

optional int32 result_per_page = 3 [default = 10];

如果没有指定默认值,则会使用系统默认值,对于string默认值为空字符串,对于bool默认值为false,对于数值类型默认值为0,对于enum默认值为定义中的第一个元素。

枚举

由于枚举值采用varint编码,所以为了提高效率,不建议枚举值取负数.这些枚举值可以在其他消息定义中重复使用。

message SearchRequest 
{
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  enum Corpus 
  {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  optional Corpus corpus = 4 [default = UNIVERSAL];
}

嵌套类型

在protocol中可以定义如下的嵌套类型:

message SearchResponse 
{
  message Result 
  {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

在另外一个消息中需要使用Result定义,则可以通过Parent.Type来使用。

message SomeOtherMessage 
{
  optional SearchResponse.Result result = 1;
}

protocol支持更深层次的嵌套和分组嵌套,但是为了结构清晰起见,不建议使用过深层次的嵌套。

更新数据类型

在更新一个数据类型时更多的是需要考虑与旧版本的兼容性问题:

  1. 不要改变任何已存在字段的Tag值,如果改变Tag值可能会导致数值类型不匹配,具体原因参加protocol编码
  2. 建议使用optional和repeated字段限制,尽可能的减少required的使用
  3. 不需要的字段可以删除,删除字段的Tag不应该在新的消息定义中使用.
  4. 不需要的字段可以转换为扩展,反之亦然只要类型和数值依然保留
  5. int32, uint32, int64, uint64,
    和bool是相互兼容的,这意味着可以将其中一种类型任意改编为另外一种类型而不会产生任何问题
  6. sint32 和 sint64是相互兼容的
  7. string 和 bytes是相互兼容的
  8. fixed32 兼容 sfixed32, fixed64 兼容 sfixed64.
  9. optional 兼容repeated

扩展

extend特性来让你声明一些Tags值来供第三方扩展使用。如:

message Foo 
{
  // ...
  extensions 100 to 199;
}

假如你在你的proto文件中定义了上述消息,之后别人在他的.proto文件中import你的.proto文件,就可以使用你指定的Tag范围的值。

extend Foo 
{
  optional int32 bar = 126;
}

常见API使用

假如有如下的消息定义:

message PBStudent 
{    
    optional uint32 StudentID   = 1;
    optional string Name        = 2;
    optional uint32 Score       = 3;
}    

message PBMathScore
{    
    optional uint32 ClassID     = 1;  
    repeated PBStudent ScoreInf   = 2;
}

protocol buffer编译器会为每个消息生成一个类,每个类包含基本函数,消息实现,嵌套类型,访问器等部分。

1, 基本函数

public:
 PBStudent();
 virtual ~PBStudent();

 PBStudent(const PBStudent& from);

 inline PBStudent& operator=(const PBStudent& from) {
   CopyFrom(from);
   return *this;
 }

 inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
   return _unknown_fields_;
 }

 inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
   return &_unknown_fields_;
 }

 static const ::google::protobuf::Descriptor* descriptor();
 static const PBStudent& default_instance();

 void Swap(PBStudent* other);

2,消息实现

PBStudent* New() const;
void CopyFrom(const ::google::protobuf::Message& from);
void MergeFrom(const ::google::protobuf::Message& from);
void CopyFrom(const PBStudent& from);
void MergeFrom(const PBStudent& from);
void Clear();
bool IsInitialized() const;                                                                          

int ByteSize() const;
bool MergePartialFromCodedStream(
    ::google::protobuf::io::CodedInputStream* input);
void SerializeWithCachedSizes(
    ::google::protobuf::io::CodedOutputStream* output) const;
::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
int GetCachedSize() const { return _cached_size_; }
private:
void SharedCtor();
void SharedDtor();
void SetCachedSize(int size) const;

3,嵌套类型

访问器

// optional uint32 StudentID = 1;
inline bool has_studentid() const;
inline void clear_studentid();
static const int kStudentIDFieldNumber = 1;
inline ::google::protobuf::uint32 studentid() const;
inline void set_studentid(::google::protobuf::uint32 value);

// optional string Name = 2;
inline bool has_name() const;                                
inline void clear_name();
static const int kNameFieldNumber = 2;
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline void set_name(const char* value, size_t size);
inline ::std::string* mutable_name();
inline ::std::string* release_name();
inline void set_allocated_name(::std::string* name);

// optional uint32 Score = 3;                            
inline bool has_score() const;
inline void clear_score();
static const int kScoreFieldNumber = 3;
inline ::google::protobuf::uint32 score() const;
inline void set_score(::google::protobuf::uint32 value);

protocol buffer编译器会对每一个字段生成一些get和set方法,这些方法的名称采用标识符所有小写加上相应的前缀或后缀组成.生成一个值为Tags的k标识符FieldNum常量。
除了生成上述类型的方法外, 编译器还会生成一些用于消息类型处理的私有方法. 每一个.proto文件在编译的时候都会自动包含message.h文件,这个文件声明了很多序列化和反序列化,调试, 复制合并等相关的方法。
在我们平时的使用中,通常一个message对应一个类,在对应的类中定义一个set和create方法来生成和解析PB信息。如:

// test.h
class CStudent
{
public:
    unsigned    mStudentID;
    unsigned    mScore;
    string      mName;

    CStudent()
    {
        Init();
    }

    inline void Init()
    {
        mStudentID = 0;
        mScore = 0;
        mName = "";
    }
}

class CMathScore
{
private:
    unsigned    mClassID;
    CStudent    mScoreInf[100];
public:
    CMathSCore()
    {
        Init();
    }
    ~CMathScore() {};

    void Init();
    void SetFromPB(const PBMathScore* pPB);
    void CreatePB(PBMathScore* pPB);

    // Get & Set mClassID
    ...
    // Get & set mScoreInf
    ...
    // some other function
    ...
}
对应的cpp文件中实现对PB的操作

// test.cpp
void CMathScore::Init()
{
    mClassID = 0;
    memset(mScoreInf, 0, sizeof(mScoreInf));
}

void CMathScore::SetFromPB(const PBMathScore* pPB)
{
    if ( NULL == pPB ) return;

    mClassID = pPB->classid();
    for(unsigned i = 0; i < (unsigned)pPB->scoreinf_size() && i < 100; ++i)
    {
        PBStudent* pStu = pPB->mutable_scoreinf(i);
        mScoreInf[i].mStudentID = pStu->studentid();
        mScoreInf[i].mScore        = pStu->score();
        mScoreInf[i].mName        = pStu->name();
    }
}

void CMathScore::CreatePB(PBMathScore* pPB)
{
    if ( NULL == pPB ) return;

    pPB->set_classid(mClassID);
    for(unsigned i = 0; i < 100; ++i)
    {
        PBStudent* pStu = pPB->add_scoreinf();
        pStu->set_studentid(mScoreInf[i].mStudentID)
        pStu->set_score(mScoreInf[i].mScore);
        pStu->set_name(mScoreInf[i].mName);        
    }
}

PB文件的读写

// use.cpp
#include<test.h>

#defind        MAX_BUFFER        1024 * 1024
int write()
{
    CMathScore    mMath;
    PBMathScore mPBMath;
    // use set functions to init member variable

    fstream fstm("./math.dat", ios::out | ios::binary);
    if ( fstm.is_open() == false )
    {
        return -1;
    }    
    char* tpBuffer = (char*)malloc(MAX_BUFFER);
    if ( NULL == tpBuffer )
    {
        return -2;
    }

    mMath.CreatePB(&mPBMath);
    if ( mPBMath.SerializeToArray(tpBuffer, mPBMath.ByteSize()) == false )
    {
        return -3;
    }
    fstm.write(tpBuffer, mPBMath.ByteSize());
    free(tpBuffer);
    fstm.close();

    return 0;
}

int read()
{
    CMathScore    mMath;
    PBMathScore mPBMath;

    fstream fstm.open("./math.dat", ios::out | ios::binary);
    if ( fstm.is_open() == false )
    {
        return -1;
    }    
    char* tpBuffer = (char*)malloc(MAX_BUFFER);
    if ( NULL == tpBuffer )
    {
        return -2;
    }
    char*    tpIdx = tpBuffer;
    int     tLen;
    while ( !fstm.eof() && tLen < MAX_BUFFER )
    {
        fstm.read(tpIdx, 1);
        tpIdx += 1;
        tLen++;
    }
    if ( mPBMath.ParseFromArray(tpBuffer, tLen - 1) == false )
    {
        return -3;
    }
    fstm.close();
    free(tpBuffer);
    tpIdx = NULL;

    mMath.SetFromPB(&mPBMath);
    // do some thing

    return 0;
}

原理分析

通过上面的简单使用我们可以了解到,实际上protobuf就是帮我们生成对应的消息类,且每个类中包含了对指定字段的getter/setter方法,以及序列化和反序列化整个消息类的parse和serialize方法,对于使用者来说只需要简单调用这些方法就可以实现消息的序列化和反序列化操作了。

TLV

实际上protobuf使用一种类似((T)([L]V))的形式来组织数据的,即Tag-Length-Value(其中Length是可选的)。每一个字段都是使用TLV的方式进行序列化的,一个消息就可以看成是多个字段的TLV序列拼接成的一个二进制字节流。其实这种方式很像Key-Value的方式,所以Tag一般也可以看做是Key。由上可知,这种方式组织的数据并不需要额外的分隔符来划分数据,所以其可以减低序列化结果的大小。可以使用下面的图简单的说明下TLV数据模型:
这里写图片描述
Value的值很自然知道就是字段的值,那么Tag值是什么呢?在.proto文件中,定义的每一个字段都需要声明其数据类型,其还表明该字段是可变长度还是固定长度,这部分一般称为wire_type。此外, 每个字段都有一个filed值,这个值代表该字段是message里的第几个值,一般称为field_num。
在Protobuf中,数据类型是进行了划分的,其中wire_type主要是以下几种类型:

  • Varint是一种比较特殊的编码方式,后面会再介绍。
  • FixedXXX是固定长度的数字类型。
  • Length-delimited是可变长数据类型,常见的就是string, bytes之类的。
enum WireType {
    WIRETYPE_VARINT           = 0,
    WIRETYPE_FIXED64          = 1,
    WIRETYPE_LENGTH_DELIMITED = 2,
    WIRETYPE_START_GROUP      = 3,
    WIRETYPE_END_GROUP        = 4,
    WIRETYPE_FIXED32          = 5,
};

这里写图片描述

了解了wire_type的含义后,就可以知道Tag是怎么解析的:结合移位操作和或操作就可以判断出其是哪种数据类型了。

Varint

Varint是一种紧凑的表示数字的方式。它可以用一个或多个字节来表示一个数字,其中值越小的数字需要的字节数越少。Varint中每一个字节的最高位bit都是有特殊含义的,如果其值为1,则表示下一个字节也是该数字的一部分,如果其值为0,则表明该数字到这一个字节就结束了。
通常情况下一个int32类型的数字,一般需要4个字节来表示。使用Varint方式编码的话,对于比较小的数字,比如说-128~127之间的数字则只需要一个字节,而如果是300(下图有解释),则需要两个字节来表示。然而其也有不好的地方,比如说对于一个大数字,其最多可能需要5个字节来表示,但从概率统计的角度来说,绝大多数情况下采用Varint编码可以减少字节数来表示数字。
在计算机里,一个负数会被表示为一个很大的整数,如果采用Varint来编码的话则一定会需要5个字节了。所以Google protocol buffer 定义了sint32, sint64这些数据类型,其采用zigzag编码。如下图:
这里写图片描述

这样无论是正数还是负数,只要其绝对值比较小的时候需要的字节数就少,可以充分发挥Varint编码的优势。

序列化与反序列化

现在来了解一下序列化的过程。先看一段代码:

//序列化接口,传入一个输出流参数
void ReqBody::SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream* output) const {
  //这个message中有一个可选参数叫msg_set_req,其field_num = 1;
  //optional message msg_set_req = 1;
  //先判断该字段是否设置,如果设置则调用相应函数
  if (has_msg_set_req()) {
        ::google::protobuf::internal::WireFormatLite::WriteMessage(1, this->msg_set_req(),output);
    }
}

//判断该值是否已经设置 
inline bool ReqBody::has_msg_set_req() const {
    return (_has_bits_[0] & 0x00000001u) != 0;
}

序列化就是判断某些字段是否已经设置了值,如果设置了值就调用相应的函数写出该字段。如果找一个包括多个字段的看的话,其SerializeWithCachedSizes方法中应该会包含多个类似上面的if()操作。然后还有很多类似判断该字段是否已经设置的内联函数。通过查看protobuf源代码的你会发现:头文件中的定义,会发现针对不同类型的数据类型,都有对应的writeXXX方法。例如:

// Write fields, including tags.
static void WriteInt32   (field_number,  int32 value, output);
static void WriteInt64   (field_number,  int64 value, output); 
static void WriteUInt32  (field_number, uint32 value, output);
static void WriteUInt64  (field_number, uint64 value, output);
static void WriteSInt32  (field_number,  int32 value, output);
static void WriteSInt64  (field_number,  int64 value, output);
static void WriteFixed32 (field_number, uint32 value, output);
static void WriteFixed64 (field_number, uint64 value, output);
static void WriteSFixed32(field_number,  int32 value, output);
static void WriteSFixed64(field_number,  int64 value, output);
static void WriteFloat   (field_number,  float value, output);
static void WriteDouble  (field_number, double value, output);
static void WriteBool    (field_number,   bool value, output);
static void WriteEnum    (field_number,    int value, output);

static void WriteString(field_number, const string& value, output);
static void WriteBytes (field_number, const string& value, output);

static void WriteGroup(field_number, const MessageLite& value, output);
static void WriteMessage(field_number, const MessageLite& value, output);

然后通过,字符类型和message类型的字段的具体writeXXX方法,就能更清楚的了解TLV这种序列化方式了。

//数值类型的字段,这里是int32
void WireFormatLite::WriteInt32(int field_number, int32 value, io::CodedOutputStream* output) {
  //tag
  WriteTag(field_number, WIRETYPE_VARINT, output);
  //这里应该是int32这些固定长度的数值类型,可以省去长度这个字段?
  //value
  WriteInt32NoTag(value, output);
}

//可变字长类型,这里是string
void WireFormatLite::WriteString(int field_number, const string& value, io::CodedOutputStream* output) {
  // String is for UTF-8 text only
  //tag
  WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
  //length,这里长度是采用varint编码方式,可以省不少字节
  GOOGLE_CHECK(value.size() <= kint32max);
  output->WriteVarint32(value.size());
  //value
  output->WriteString(value);
}

//嵌套的message字段
void WireFormatLite::WriteMessage(int field_number, const MessageLite& value, io::CodedOutputStream* output) {
  //tag
  WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
  //length, 这里计算message的长度,然后再写出。
  const int size = value.GetCachedSize();
  output->WriteVarint32(size);
  //value,这里的value是message类型,可以看做是在递归进行序列化
  value.SerializeWithCachedSizes(output);
}

inline void WireFormatLite::WriteTag(int field_number, WireType type, io::CodedOutputStream* output) {
  output->WriteTag(MakeTag(field_number, type));
}

inline uint32 WireFormatLite::MakeTag(int field_number, WireType type) {
  return GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(field_number, type);
}

//这有个宏很关键,正好印证上面提到的计算key值的方式
#define GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(FIELD_NUMBER, TYPE)                  \
  static_cast<uint32>(                                                   \
    ((FIELD_NUMBER) << ::google::protobuf::internal::WireFormatLite::kTagTypeBits) \
      | (TYPE))

// Number of bits in a tag which identify the wire type.
static const int kTagTypeBits = 3;

而反序列化就是调用方法读取相应字段的值,整个处理过程在一个while循环中,直到数据处理完毕才终止。

bool RspBody::MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream* input) {
#define DO_(EXPRESSION) if (!(EXPRESSION)) return false
    ::google::protobuf::uint32 tag;
    while ((tag = input->ReadTag()) != 0) {
        switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
            //optional message msg_set_req = 1;
            case 1: {
                if (::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
                    ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED) {
                    DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(input, mutable_msg_set_rsp()));
                } else {
                    goto handle_uninterpreted;
                }
                if (input->ExpectAtEnd()) return true;
                break;
            }
            default: {
            handle_uninterpreted:
                if (::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
                    ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
                    return true;
                }
                DO_(::google::protobuf::internal::WireFormatLite::SkipField(input, tag));
                break;
            }
        }
    }
    return true;
#undef DO_
}

虽然相比xml、json,protocol buffer格式是进步了很多,但是还是有一些问题待解决
Google protocol buffer的缺点

网友评论

登录后评论
0/500
评论