閱讀855 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Google protocol buffer簡介

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的缺點

最後更新:2017-05-01 08:01:17

  上一篇:go spark源碼分析Master與Worker啟動流程篇
  下一篇:go 華為中央研究院解析大數據商業模式