Google Protocol Buffers淺析

  本文主要偏向於介紹怎麼使用Google的Protocol Buffer技術來壓縮與解析你的數據文件,更加詳細的信息請參閱Google開放的開發者網頁文檔,地址爲:http://code.google.com/apis/protocolbuffers/docs/overview.html 。html

     1、簡單的介紹ios

     固然,在繼續本文以前,讀者仍是須要對Google Protocol Buffers有一些基本的認識。Protocol buffers是一個用來序列化結構化數據的技術,支持多種語言諸如C++、Java以及Python語言,可使用該技術來持久化數據或者序列化成網絡傳輸的數據。相比較一些其餘的XML技術而言,該技術的一個明顯特色就是更加節省空間(以二進制流存儲)、速度更快以及更加靈活。api

     一般,編寫一個protocol buffers應用須要經歷以下三步:數組

     一、定義消息格式文件,最好以proto做爲後綴名網絡

     二、使用Google提供的protocol buffers編譯器來生成代碼文件,通常爲.h和.cc文件,主要是對消息格式以特定的語言方式描述數據結構

     三、使用protocol buffers庫提供的API來編寫應用程序  函數

 

     2、定義Proto文件 ui

     proto文件即消息協議原型定義文件,在該文件中咱們能夠經過使用描述性語言,來良好的定義咱們程序中須要用到數據格式。首先咱們能夠經過Google在線文檔上提供的一個電話簿的例子來了解下,不過稍微加了點改動。     this

複製代碼
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;
  
  required bytes  unsure = 5;      //Add byte array here    
}

message AddressBook {
  repeated Person person = 1;
}
複製代碼

     誠如你看到的同樣,消息格式定義很簡單,對於每一個字段而言都有一個修飾符(required/repeated/optional)、字段類型(bool/string/bytes/int32等)和字段標籤(Tag)組成。google

     三個修飾符從詞義上能夠很清楚的弄明白,

    1)對於required的字段而言,初值是必需要提供的,不然字段的即是未初始化的。在Debug模式的buffer庫下編譯的話,序列化話的時候可能會失敗,並且在反序列化的時候對於該字段的解析會老是失敗的。因此,對於修飾符爲required的字段,請在序列化的時候務必給予初始化。

    2)對於optional的字段而言,若是未進行初始化,那麼一個默認值將賦予該字段,固然也能夠指定默認值,如上述proto定義中的PhoneType字段類型。

    3)對於repeated的字段而言,該字段能夠重複多個,google提供的這個addressbook例子便有個很好的該修飾符的應用場景,即每一個人可能有多個電話號碼。在高級語言裏面,咱們能夠經過數組來實現,而在proto定義文件中可使用repeated來修飾,從而達到相同目的。固然,出現0次也是包含在內的。      

    其中字段標籤標示了字段在二進制流中存放的位置,這個是必須的,並且序列化與反序列化的時候相同的字段的Tag值必須對應,不然反序列化會出現意想不到的問題。

    

     3、編譯proto文件,生成特定語言數據的數據定義代碼  

     在定義好了proto文件,就能夠將該文件做爲protocol buffers編譯器的輸入文件,編譯產生特定語言的數據定義代碼文件了。本文主要是針對C++語言,因此使用編譯器後生成的是.h與.cc的代碼文件。對於C++、Java還有Python都有各自的編譯器,下載地址:http://code.google.com/p/protobuf/downloads/list  

     當你下載完了對應的編譯器二進制文件後,就可使用下列命令來完成編譯過程:

     protoc.exe -proto_path=SRC --cpp_out=DST SRC/addressbook.proto 

     其中--proto_path指出proto文件所在的目錄,--cpp_out則是生成的代碼文件要放的目錄,最後的一個參數指出proto文件的路徑。如上述命令中能夠看出,將SRC目錄下的addressbook.proto編譯後放在DST目錄下,應該會生成addressbook.pb.h和addressbook.pb.cc文件(/Files/royenhome/addressbook.rar)。

     經過查看頭文件,能夠發現針對每一個字段都會大體生成以下幾種函數,以number爲例:

複製代碼
  // required string number = 1;
  inline bool has_number() const;
  inline void clear_number();
  inline const ::std::string& number() const;
  inline void set_number(const ::std::string& value);
  inline void set_number(const char* value);
  inline ::std::string* mutable_number();
複製代碼

      能夠看出,對於每一個字段會生成一個has函數(has_number)、clear清除函數(clear_number)、set函數(set_number)、get函數(number和mutable_number)。這兒解釋下get函數中的兩個函數的區別,對於原型爲const std::string &number() const的get函數而言,返回的是常量字段,不能對其值進行修改。可是在有一些狀況下,對字段進行修改是必要的,因此提供了一個mutable版的get函數,經過獲取字段變量的指針,從而達到改變其值的目的。

      而對於字段修飾符爲repeated的字段生成的函數,則稍微有一些不一樣,如phone字段,則編譯器會爲其產生以下的代碼: 

複製代碼
  // repeated .Person.PhoneNumber phone = 4;
  inline int phone_size() const;
  inline void clear_phone();
  inline const ::google::protobuf::RepeatedPtrField< ::Person_PhoneNumber >& phone() const;
  inline ::google::protobuf::RepeatedPtrField< ::Person_PhoneNumber >* mutable_phone();
  inline const ::Person_PhoneNumber& phone(int index) const;
  inline ::Person_PhoneNumber* mutable_phone(int index);
  inline ::Person_PhoneNumber* add_phone();
複製代碼

      能夠看出,set函數變成了add函數,這個其實很好理解。上面也說過,repeated修飾的字段在高級語言中的實現多是個數組或動態數組,因此固然經過添加的方式來加入新的字段值。而起get函數也變化很大,這個也不用多說了。

     好了,本文主要是對了解protocol buffer做了些簡單的介紹,固然更詳細的仍是看官方文檔。下篇文章開始將介紹怎麼利用protocol buffers來完成序列化與反序列化數據。

本文開始將逐漸介紹怎麼使用protocol buffers來完成序列化與反序列化數據的應用,開發環境爲VS2008,語言爲C++,外部庫用的是googlebuffer庫。

      一、Google Protocol Buffer庫

      在咱們的應用程序裏面,須要使用到google buffer提供的庫,你們能夠到官網去下載,筆者也會提供一個精簡後的Win32 Release版的Lib庫下載(Debug版與X64版的都刪去了,否則lib包超過200M)。下載地址:GoogleBufferLib 

      解壓縮後能夠看出文件夾結構以下所示:

      ---GoogleProtocolBuffer

          ---include文件夾

          ---lib文件夾

              ---win32文件夾

                   ---release文件夾

                       ---proto文件夾(本身建的,放proto文件的)

                       ---royen文件夾(本身建的,生成的.h和.cc的目錄)

                       ---*.lib 庫文件

                       ---protoc.exe編譯器 

      其中,include文件夾下是一堆程序中須要引用的頭文件,而在lib文件夾下則是有protoc編譯器和連接庫。

 

      二、創建並正確設置項目  

      1)使用VS2008新建一個項目後,將上面下載的GoogleProtocolBuffer文件夾拷貝到你的項目工程裏,以下圖所示:

        

     

     2)使用編譯器將定義的proto文件編譯成.h與.cc文件,拷貝到項目工程目錄下,以下圖所示:    

  

     3)在項目中將.h與.cc文件引用進來,並在addressbook.pb.cc頭部加上一句include "stdafx.h",不然編譯時會報錯

     4)打開項目屬性,右鍵項目-》Configuration-》C/C++ -》General ,設置Additional Include Directories,以下所示:

            

  

     5)定位到Configuration -》Linker -》General,設置Additional Library Directories,以下所示:    

                

    

     6)定位到Configuration -》Input-》Additional Dependencies,以下圖所示:

      

 

     7)定位到Configuration -》C/C++ -》Code Generation,修改Runtime Library項爲Multi-threaded(/MT),以下圖所示:    

         

     

        經過上述的一些列項目屬性設置,程序能夠正確編譯經過了,可是什麼功能都沒有,下篇文章中將介紹怎麼序列化與反序列化咱們的數據。

本文主要會介紹怎麼使用Google Protocol的Lib來序列化咱們的數據,方法不少種,本文只介紹其中的三種,其餘的方法讀者能夠經過自行研究摸索。但總的來講,序列化數據總的來講分爲如下倆步:

     1)使用數據源填充數據結構,不管數據源來自文件仍是內存仍是標準輸入

     2)利用Lib提供的序列化接口將數據結構序列化,而後存儲在內存或者磁盤上

    

     1、填充數據結構 

     從數據源中獲取數據,這兒的數據源可能來自磁盤上的一個文件或者內存中存儲的一段數據或者來自標準輸入的數據。咱們須要作的就是,將AddressBook這個數據結構中的各個字段填充。本例中是經過AddressBook提供的add_person函數來得到一個Person的指針,從而對其進行填充,以下代碼所示: 

複製代碼
    //地址簿數據定義
    AddressBook    addressBook;            
    
    //第一個聯繫人的數據定義與初始化
    Person    *personMe  = addressBook.add_person();
    personMe->set_id(1);
    personMe->set_name("royen");    
    personMe->set_email("zwg19891129@163.com");
    personMe->set_unsure("19bf173a0e87ab");
    
    //第二個聯繫人的數據定義與初始化
    Person  *personHim = addressBook.add_person();
    personHim->set_id(2);
    personHim->set_name("XXX");
    personHim->set_email("XXX@XXX.com");
    personHim->set_unsure("19bf173a0e87ab");
    
    //personMe的手機號碼數據定義與初始化
    Person_PhoneNumber *phoneNumberMobile = personMe->add_phone();
    phoneNumberMobile->set_number("15996110120");
    phoneNumberMobile->set_type(Person_PhoneType_MOBILE);
        
    //personMe的座機號碼數據定義與初始化
    Person_PhoneNumber *phoneNumberHome   = personMe->add_phone();
    phoneNumberHome->set_number("0256110120");
    phoneNumberHome->set_type(Person_PhoneType_HOME);

    //personHim的一個號碼數據定義與初始化
    Person_PhoneNumber *phoneNumberHim      = personHim->add_phone();
    phoneNumberHim->set_number("15996111111");    
    phoneNumberHim->set_type(Person_PhoneType_HOME);
複製代碼

         很容易看出,上述代碼即在地址簿中添加了倆個聯繫人,而後又分別填充各個聯繫人的數據信息,經過上述代碼一個地址簿的數據便準備好了。

        

        2、序列化數據 

        其實經過看編譯器生成的AddressBook這個類所提供的方法名,既能夠大體知道有哪些序列化的方式,以下所示:

         

        從上圖能夠看出,可利用序列化的方法不少,本文中主要使用SerializeToString、SerializeToCodedStream以及SerializeToOstream來完成序列化。 

        下面就分別就這幾種方式來介紹下:

        1) SerializeToCodedStream方式

        首先能夠知道該函數的原型是bool SerializeToCodedStream(std::ostream *),因此使用該函數須要結合C++的fstream流,代碼以下:         

複製代碼
    //方法一: 使用SerializePartialToOstream來序列化,注意ios::binary以二進制流寫入文件
    fstream  fserial("addressbook.data",ios::out | ios::trunc | ios::binary);    
    if (!addressBook.SerializePartialToOstream(&fserial))
    {
       cerr<<"Failed to serial address book data!\n";
       return;
    }
    cout<<"Serial address book data successfully!\n";
    fserial.close();
    fserial.clear();
複製代碼

          能夠看出,採用這種方法至關的便捷,並且也很簡潔,但有個缺點就是輸出到文件的編碼格式很差控制,因此可使用下面介紹的這種方法。

         

       2)SerializeToString方式

       函數原型爲bool SerializeToString(std::string* output) ,因此能夠講填充在數據結構AddressBook中的數據取出存到一個string對象中,而後再以二進制流的方式將其寫入到磁盤文件中,代碼以下:               

複製代碼
    FILE    *g_AddressBook = fopen("addressbook.data","wb,ccs = UNICODE");
    if( NULL == g_AddressBook )
    {
        cerr<<"Create addressbook.data failed!\n";
        return ;
    }

    string    serialStream = "";
    if!addressBook.SerializePartialToString(&serialStream) )
    {
        cerr<<"Failed to serial addressbook data!\n";
        return;
    }

    fwrite( serialStream.c_str(),sizeof(char),addressBook.ByteSize(),g_AddressBook);
    cout<<"serial address successfully!\n";
    if( g_AddressBook )
    {
        fclose(g_AddressBook);
        g_AddressBook = NULL;
    } 
複製代碼

       上述代碼稍微繁瑣了點,可是也是一種序列化的方式,經過結合使用C庫中的文件操做函數,能夠更方便的定製輸出文件。

  

       3)SerializeToCodedStream方式

       該方式主要指用到的google buffer的庫中提供的一組數據流操做對象,在使用這些對象以前須要引入一些頭文件,以下所示:       

   #include <google/protobuf/io/zero_copy_stream_impl.h>
   #include <google/protobuf/io/zero_copy_stream.h>
   #include <google/protobuf/io/coded_stream.h> 
   using namespace::google::protobuf::io;

       該方式也結合C庫的open與write函數,序列化部分的代碼以下:               

複製代碼
    int fd  = _open("addressbook.data", _O_WRONLY |_O_CREAT| _O_BINARY, _S_IREAD|_S_IWRITE);    
    if-1 == fd )
    {
        cerr<<"Create addressbook.data failed!\n";
        return ;
    }
    char tmpArr[MAX_SIZE];
    memset(tmpArr,0,sizeof(tmpArr));
    ZeroCopyOutputStream *raw_output = new ArrayOutputStream(tmpArr,addressBook.ByteSize()+1);    
    CodedOutputStream* coded_output = new CodedOutputStream(raw_output);    
    if!addressBook.SerializeToCodedStream( coded_output ))
    {
        cerr<<"Fail to serial addressbook data!\n";
        return;
    }    
    _write(fd,tmpArr,addressBook.ByteSize()+1);
    cout<<"serial address successfully!\n";
    delete coded_output;
    delete raw_output;        
    close(fd);    
複製代碼

        本文暫時介紹這三種序列化話方式,還有像SerializeToArray以及SerializeToFileDescriptor等方式都應該比較相似,因此感興趣的朋友能夠本身動手試試。   

        下篇文章再稍微介紹下反序列化的方法,可是應該不會太多內容,畢竟都方法都很類似。

本文做爲結束篇,會稍微介紹下怎麼反序列化GoogleBuffer數據,並在最後提供本系列文章中所用到的代碼整理供下載。

     上一篇文章介紹了怎樣將數據序列化到了addressbook.data中,那麼對於接受方而言該怎麼解析出本來的數據呢。一樣,protoc編譯器生成的代碼文件中提供了反序列化的接口,基本上和序列化的函數對應的,以下圖所示:

       

      上文中採用了SerializeToOstream、SerializeToString、SerializeToCodedStream來序列化數據的,反序列化反其道行之便可。本文反序列化採用ParseFromArray方式,從某個角度算是對上文的一個補充吧!

       反序列化也是分爲兩個步驟:

       1)將數據載入內存或者輸入流 

       2)調用庫提供的反序列化接口函數進行反序列化

      

       1、將數據載入 

       將數據從文件中讀出時候,須要注意以二進制的模式打開,且編碼格式要指定正確,以下所示:               

複製代碼
    FILE    *g_AddressBook = fopen("addressbook.data","rb,ccs=UNICODE");
    if( NULL == g_AddressBook )
    {
        cerr<<"Open addressbook.data failed!\n"<<endl;
        return ;
    }

    int lfilesize = 0;
    fseek( g_AddressBook,0,SEEK_END);
    lfilesize = ftell( g_AddressBook );
    fseek( g_AddressBook ,0,SEEK_SET );
    
    char *buffer =new char[lfilesize+1];
    if( NULL == buffer )
    {
        cerr<<"malloc memory error!\n";
        return;
    }
    memset(buffer,'\0',sizeof(buffer));
    fread( buffer,sizeof(char),lfilesize,g_AddressBook);
    if( g_AddressBook )
    {
        fclose(g_AddressBook);
        g_AddressBook = NULL;
    }
複製代碼

              

       2、反序列化

       上述代碼將addressbook.data中的數據載入了buffer中,接着咱們就能夠將其做爲參數傳給ParseFromArray來反序列化,並格式化輸出到控制檯,以下:     

複製代碼
    AddressBook    addressBook;
    addressBook.par
    addressBook.Clear();

    if!addressBook.ParseFromArray(buffer,lfilesize) )
    {
        cerr<<"Deserial from addressbook.data failed!\n";
        return;
    }

    int personSize = addressBook.person_size();

    forint i=0 ;i<personSize; i++ )
    {
        Person p = addressBook.person( i );
        cout<<"Person "<<i+1<<":\nid\t"<<p.id()<<"\nname:\t"<<p.name()<<"\n";
        int phoneSize = p.phone_size();
        forint j=0;j<phoneSize;j++ )
        {
            Person_PhoneNumber phone = p.phone(j);
            cout<<"Phone "<<j+1<<":\nType:\t";
            switch( phone.type())
            {
            case Person_PhoneType_MOBILE:
                cout<<"Mobile\t\tPhone Number:\t"<<phone.number()<<endl;
                break;
            case Person_PhoneType_HOME:
                cout<<"Home\t\tPhone Number:\t"<<phone.number()<<endl;
                break;
            case Person_PhoneType_WORK:
                cout<<"Work\t\tPhone Number:\t"<<phone.number()<<endl;
                break;
            default:
                cout<<"Unkown\n";
                break;
            }
        }
        cout<<endl;
    }
複製代碼

         運行結果以下所示:

         

         好了,相信經過本系列文章,讀者應該對Google Protocol Buffer有必定的認識了吧。固然,想要更深刻的瞭解,仍是參考Google的官方在線文檔吧!

         示例代碼下載地址:SerialProtocolBuffer示例代碼

相關文章
相關標籤/搜索