Protocol Buffer 技術(shù)詳解

Protocol Buffer技術(shù)詳解(語(yǔ)言規(guī)范)
該系列Blog的內(nèi)容主體主要源自于Protocol Buffer的官方文檔,而代碼示例則抽取于當(dāng)前正在開發(fā)的一個(gè)公司內(nèi)部項(xiàng)目的Demo。這樣做的目的主要在于不僅可以保持Google文檔的良好風(fēng)格和系統(tǒng)性,同時(shí)再結(jié)合一些比較實(shí)用和通用的用例,這樣就更加便于公司內(nèi)部的培訓(xùn),以及和廣大網(wǎng)友的技術(shù)交流。需要說(shuō)明的是,Blog的內(nèi)容并非line by line的翻譯,其中包含一些經(jīng)驗(yàn)性總結(jié),與此同時(shí),對(duì)于一些不是非常常用的功能并未予以說(shuō)明,有興趣的開發(fā)者可以直接查閱Google的官方文檔。

一、為什么使用Protocol Buffer

在回答這個(gè)問(wèn)題之前,我們還是先給出一個(gè)在實(shí)際開發(fā)中經(jīng)常會(huì)遇到的系統(tǒng)場(chǎng)景。比如:我們的客戶端程序是使用Java開發(fā)的,可能運(yùn)行自不同的平臺(tái),如:Linux、Windows或者是Android,而我們的服務(wù)器程序通常是基于Linux平臺(tái)并使用C++開發(fā)完成的。在這兩種程序之間進(jìn)行數(shù)據(jù)通訊時(shí)存在多種方式用于設(shè)計(jì)消息格式,如:

  1. 直接傳遞C/C++語(yǔ)言中一字節(jié)對(duì)齊的結(jié)構(gòu)體數(shù)據(jù),只要結(jié)構(gòu)體的聲明為定長(zhǎng)格式,那么該方式對(duì)于C/C++程序而言就非常方便了,僅需將接收到的數(shù)據(jù)按照結(jié)構(gòu)體類型強(qiáng)行轉(zhuǎn)換即可。事實(shí)上對(duì)于變長(zhǎng)結(jié)構(gòu)體也不會(huì)非常麻煩。在發(fā)送數(shù)據(jù)時(shí),也只需定義一個(gè)結(jié)構(gòu)體變量并設(shè)置各個(gè)成員變量的值之后,再以char*的方式將該二進(jìn)制數(shù)據(jù)發(fā)送到遠(yuǎn)端。反之,該方式對(duì)于Java開發(fā)者而言就會(huì)非常繁瑣,首先需要將接收到的數(shù)據(jù)存于ByteBuffer之中,再根據(jù)約定的字節(jié)序逐個(gè)讀取每個(gè)字段,并將讀取后的值再賦值給另外一個(gè)值對(duì)象中的域變量,以便于程序中其他代碼邏輯的編寫。對(duì)于該類型程序而言,聯(lián)調(diào)的基準(zhǔn)是必須客戶端和服務(wù)器雙方均完成了消息報(bào)文構(gòu)建程序的編寫后才能展開,而該設(shè)計(jì)方式將會(huì)直接導(dǎo)致Java程序開發(fā)的進(jìn)度過(guò)慢。即便是Debug階段,也會(huì)經(jīng)常遇到Java程序中出現(xiàn)各種域字段拼接的小錯(cuò)誤。

  2. 使用SOAP協(xié)議(WebService)作為消息報(bào)文的格式載體,由該方式生成的報(bào)文是基于文本格式的,同時(shí)還存在大量的XML描述信息,因此將會(huì)大大增加網(wǎng)絡(luò)IO的負(fù)擔(dān)。又由于XML解析的復(fù)雜性,這也會(huì)大幅降低報(bào)文解析的性能??傊褂迷撛O(shè)計(jì)方式將會(huì)使系統(tǒng)的整體運(yùn)行性能明顯下降。 對(duì)于以上兩種方式所產(chǎn)生的問(wèn)題,Protocol Buffer均可以很好的解決,不僅如此,Protocol Buffer還有一個(gè)非常重要的優(yōu)點(diǎn)就是可以保證同一消息報(bào)文新舊版本之間的兼容性。至于具體的方式我們將會(huì)在后續(xù)的博客中給出。

二、定義第一個(gè)Protocol Buffer消息

創(chuàng)建擴(kuò)展名為.proto的文件,如:MyMessage.proto,并將以下內(nèi)容存入該文件中。

message LogonReqMessage {

         required int64 acctID = 1;
         required string passwd = 2;
     }

這里將給出以上消息定義的關(guān)鍵性說(shuō)明。

  1. message是消息定義的關(guān)鍵字,等同于C++中的struct/class,或是Java中的class。
  2. LogonReqMessage為消息的名字,等同于結(jié)構(gòu)體名或類名。
  3. required前綴表示該字段為必要字段,既在序列化和反序列化之前該字段必須已經(jīng)被賦值。與此同時(shí),在Protocol Buffer中還存在另外兩個(gè)類似的關(guān)鍵字,optional和repeated,帶有這兩種限定符的消息字段則沒(méi)有required字段這樣的限制。相比于optional,repeated主要用于表示數(shù)組字段。具體的使用方式在后面的用例中均會(huì)一一列出。
  4. int64和string分別表示長(zhǎng)整型和字符串型的消息字段,在Protocol Buffer中存在一張類型對(duì)照表,既Protocol Buffer中的數(shù)據(jù)類型與其他編程語(yǔ)言(C++/Java)中所用類型的對(duì)照。該對(duì)照表中還將給出在不同的數(shù)據(jù)場(chǎng)景下,哪種類型更為高效。該對(duì)照表將在后面給出。
  5. acctID和passwd分別表示消息字段名,等同于Java中的域變量名,或是C++中的成員變量名。
  6. 標(biāo)簽數(shù)字12則表示不同的字段在序列化后的二進(jìn)制數(shù)據(jù)中的布局位置。在該例中,passwd字段編碼后的數(shù)據(jù)一定位于acctID之后。需要注意的是該值在同一message中不能重復(fù)。另外,對(duì)于Protocol Buffer而言,標(biāo)簽值為1到15的字段在編碼時(shí)可以得到優(yōu)化,既標(biāo)簽值和類型信息僅占有一個(gè)byte,標(biāo)簽范圍是16到2047的將占有兩個(gè)bytes,而Protocol Buffer可以支持的字段數(shù)量則為2的29次方減一。
    有鑒于此,我們?cè)谠O(shè)計(jì)消息結(jié)構(gòu)時(shí),可以盡可能考慮讓repeated類型的字段標(biāo)簽位于1到15之間,這樣便可以有效的節(jié)省編碼后的字節(jié)數(shù)量。

三、定義第二個(gè)(含有枚舉字段)Protocol Buffer消息

//在定義Protocol Buffer的消息時(shí),可以使用和C++/Java代碼同樣的方式添加注釋。

 enum UserStatus {  
      
       OFFLINE = 0; //表示處于離線狀態(tài)的用戶 
       ONLINE = 1;  //表示處于在線狀態(tài)的用戶
     }

 message UserInfo {

      required int64 acctID = 1;
      required string name = 2;   
      required UserStatus status = 3;  
  }

這里將給出以上消息定義的關(guān)鍵性說(shuō)明(僅包括上一小節(jié)中沒(méi)有描述的)。

  1. enum是枚舉類型定義的關(guān)鍵字,等同于C++/Java中的enum。
  2. UserStatus為枚舉的名字。
  3. 和C++/Java中的枚舉不同的是,枚舉值之間的分隔符是分號(hào),而不是逗號(hào)。
  4. OFFLINE/ONLINE為枚舉值。
  5. 0和1表示枚舉值所對(duì)應(yīng)的實(shí)際整型值,和C/C++一樣,可以為枚舉值指定任意整型值,而無(wú)需總是從0開始定義。
    如:
   enum OperationCode {

     LOGON_REQ_CODE = 101;   
 
     LOGOUT_REQ_CODE = 102;

     RETRIEVE_BUDDIES_REQ_CODE = 103;

     LOGON_RESP_CODE = 1001;

     LOGOUT_RESP_CODE = 1002;

     RETRIEVE_BUDDIES_RESP_CODE = 1003;

 }

四、定義第三個(gè)(含有嵌套消息字段)Protocol Buffer消息

我們可以在同一個(gè).proto文件中定義多個(gè)message,這樣便可以很容易的實(shí)現(xiàn)嵌套消息的定義。
如:

 enum UserStatus {        

       OFFLINE = 0;     
       ONLINE = 1;    

 }

 message UserInfo {
      
  required int64 acctID = 1; 
  
  required string name = 2;    
    
  required UserStatus status = 3;  

 }

 message LogonRespMessage {   

      required LoginResult logonResult = 1;
      required UserInfo userInfo = 2;
  }

這里將給出以上消息定義的關(guān)鍵性說(shuō)明(僅包括上兩小節(jié)中沒(méi)有描述的)。

  1. LogonRespMessage消息的定義中包含另外一個(gè)消息類型作為其字段,如UserInfo userInfo。
  2. 上例中的UserInfo和LogonRespMessage被定義在同一個(gè).proto文件中,那么我們是否可以包含在其他.proto文件中定義的message呢?Protocol Buffer提供了另外一個(gè)關(guān)鍵字import,這樣我們便可以將很多通用的message定義在同一個(gè).proto文件中,而其他消息定義文件可以通過(guò)import的方式將該文件中定義的消息包含進(jìn)來(lái),如:
 import "myproject/CommonMessages.proto"

五、限定符(required/optional/repeated)的基本規(guī)則

  1. 在每個(gè)消息中必須至少留有一個(gè)required類型的字段。
  2. 每個(gè)消息中可以包含0個(gè)或多個(gè)optional類型的字段。
  3. repeated表示的字段可以包含0個(gè)或多個(gè)數(shù)據(jù)。需要說(shuō)明的是,這一點(diǎn)有別于C++/Java中的數(shù)組,因?yàn)楹髢烧咧械臄?shù)組必須包含至少一個(gè)元素。
  4. 如果打算在原有消息協(xié)議中添加新的字段,同時(shí)還要保證老版本的程序能夠正常讀取或?qū)懭耄敲磳?duì)于新添加的字段必須是optional或repeated。道理非常簡(jiǎn)單,老版本程序無(wú)法讀取或?qū)懭胄略龅膔equired限定符的字段。

六、Protocol Buffer消息升級(jí)原則。

在實(shí)際的開發(fā)中會(huì)存在這樣一種應(yīng)用場(chǎng)景,既消息格式因?yàn)槟承┬枨蟮淖兓坏貌贿M(jìn)行必要的升級(jí),但是有些使用原有消息格式的應(yīng)用程序暫時(shí)又不能被立刻升級(jí),這便要求我們?cè)谏?jí)消息格式時(shí)要遵守一定的規(guī)則,從而可以保證基于新老消息格式的新老程序同時(shí)運(yùn)行。
規(guī)則如下:

  1. 不要修改已經(jīng)存在字段的標(biāo)簽號(hào)。
  2. 任何新添加的字段必須是optional和repeated限定符,否則無(wú)法保證新老程序在互相傳遞消息時(shí)的消息兼容性。
  3. 在原有的消息中,不能移除已經(jīng)存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標(biāo)簽號(hào)必須被保留,不能被新的字段重用。
  4. int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味著如果想修改原有字段的類型時(shí),為了保證兼容性,只能將其修改為與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
  5. optional和repeated限定符也是相互兼容的。

七、Packages

我們可以在.proto文件中定義包名,如:

      package ourproject.lyphone;

該包名在生成對(duì)應(yīng)的C++文件時(shí),將被替換為名字空間名稱,既namespace ourproject { namespace lyphone。而在生成的Java代碼文件中將成為包名。

八、Options。

Protocol Buffer允許我們?cè)?proto文件中定義一些常用的選項(xiàng),這樣可以指示Protocol Buffer編譯器幫助我們生成更為匹配的目標(biāo)語(yǔ)言代碼。Protocol Buffer內(nèi)置的選項(xiàng)被分為以下三個(gè)級(jí)別:

  1. 文件級(jí)別,這樣的選項(xiàng)將影響當(dāng)前文件中定義的所有消息和枚舉。

  2. 消息級(jí)別,這樣的選項(xiàng)僅影響某個(gè)消息及其包含的所有字段。

  3. 字段級(jí)別,這樣的選項(xiàng)僅僅響應(yīng)與其相關(guān)的字段。 下面將給出一些常用的Protocol Buffer選項(xiàng)。

  4. option java_package = "com.companyname.projectname";
    java_package是文件級(jí)別的選項(xiàng),通過(guò)指定該選項(xiàng)可以讓生成Java代碼的包名為該選項(xiàng)值,如上例中的Java代碼包名為com.companyname.projectname。與此同時(shí),生成的Java文件也將會(huì)自動(dòng)存放到指定輸出目錄下的com/companyname/projectname子目錄中。如果沒(méi)有指定該選項(xiàng),Java的包名則為package關(guān)鍵字指定的名稱。該選項(xiàng)對(duì)于生成C++代碼毫無(wú)影響。

  5. option java_outer_classname = "LYPhoneMessage"; java_outer_classname是文件級(jí)別的選項(xiàng),主要功能是顯示的指定生成Java代碼的外部類名稱。如果沒(méi)有指定該選項(xiàng),Java代碼的外部類名稱為當(dāng)前文件的文件名部分,同時(shí)還要將文件名轉(zhuǎn)換為駝峰格式,如:my_project.proto,那么該文件的默認(rèn)外部類名稱將為MyProject。該選項(xiàng)對(duì)于生成C++代碼毫無(wú)影響。 注:主要是因?yàn)镴ava中要求同一個(gè).java文件中只能包含一個(gè)Java外部類或外部接口,而C++則不存在此限制。因此在.proto文件中定義的消息均為指定外部類的內(nèi)部類,這樣才能將這些消息生成到同一個(gè)Java文件中。在實(shí)際的使用中,為了避免總是輸入該外部類限定符,可以將該外部類靜態(tài)引入到當(dāng)前Java文件中,
    如:

    import static com.company.project.LYPhoneMessage;     
  1. option optimize_for = LITE_RUNTIME;
    optimize_for是文件級(jí)別的選項(xiàng),Protocol Buffer定義三種優(yōu)化級(jí)別SPEED/CODE_SIZE/LITE_RUNTIME。缺省情況下是SPEED。
    SPEED: 表示生成的代碼運(yùn)行效率高,但是由此生成的代碼編譯后會(huì)占用更多的空間。
    CODE_SIZE: 和SPEED恰恰相反,代碼運(yùn)行效率較低,但是由此生成的代碼編譯后會(huì)占用更少的空間,通常用于資源有限的平臺(tái),如Mobile。
    LITE_RUNTIME: 生成的代碼執(zhí)行效率高,同時(shí)生成代碼編譯后的所占用的空間也是非常少。這是以犧牲Protocol Buffer提供的反射功能為代價(jià)的。因此我們?cè)贑++中鏈接Protocol Buffer庫(kù)時(shí)僅需鏈接libprotobuf-lite,而非libprotobuf。在Java中僅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。
    注:對(duì)于LITE_MESSAGE選項(xiàng)而言,其生成的代碼均將繼承自MessageLite,而非Message。
  2. [pack = true]: 因?yàn)闅v史原因,對(duì)于數(shù)值型的repeated字段,如int32、int64等,在編碼時(shí)并沒(méi)有得到很好的優(yōu)化,然而在新近版本的Protocol Buffer中,可通過(guò)添加[pack=true]的字段選項(xiàng),以通知Protocol Buffer在為該類型的消息對(duì)象編碼時(shí)更加高效。
    如:
 repeated int32 samples = 4 [packed=true]。     

注:該選項(xiàng)僅適用于2.3.0以上的Protocol Buffer。

  1. [default = default_value]: optional類型的字段,如果在序列化時(shí)沒(méi)有被設(shè)置,或者是老版本的消息中根本不存在該字段,那么在反序列化該類型的消息是,optional的字段將被賦予類型相關(guān)的缺省值,如bool被設(shè)置為false,int32被設(shè)置為0。Protocol Buffer也支持自定義的缺省值,
    如:
  optional int32 result_per_page = 3 [default = 10]。

十、命令行編譯工具。

protoc 

--proto_path=IMPORT_PATH(pb源文件目錄) 

--cpp_out=DST_DIR 

--java_out=DST_DIR

--python_out=DST_DIR   
 
--objc_out=DST_DIR  

path/to/file.proto   (可省略不指定具體文件路徑)

這里將給出上述命令的參數(shù)解釋。

  1. protoc為Protocol Buffer提供的命令行編譯工具。
  2. --proto_path等同于-I選項(xiàng),主要用于指定待編譯的.proto消息定義文件所在的目錄,該選項(xiàng)可以被同時(shí)指定多個(gè)。
  3. --cpp_out選項(xiàng)表示生成C++代碼,
    --java_out表示生成Java代碼,
    --python_out則表示生成Python代碼,其后目錄為生成后的代碼所存放的目錄。
    --objc_out 表示生成object-c代碼,
  4. path/to/file.proto表示待編譯的消息定義文件。

注:對(duì)于C++而言,通過(guò)Protocol Buffer編譯工具,可以將每個(gè).proto文件生成出一對(duì).h和.cc的C++代碼文件。生成后的文件可以直接加載到應(yīng)用程序所在的工程項(xiàng)目中。如:MyMessage.proto生成的文件為MyMessage.pb.h和MyMessage.pb.cc。

十一、命令參數(shù)

$ protoc -help

 Usage: protoc [OPTION] PROTO_FILES Parse PROTO_FILES and generate output based on the options given:   

-IPATH, --proto_path=PATH

 --version          
       Show version info and exit.   

-h, --help           
  Show this text and exit.  

 --encode=MESSAGE_TYPE 
   Read a text-format message of the given type  from standard input and write it in binary  to standard output.  The message type must be defined in PROTO_FILES or their imports.   

--plugin=EXECUTABLE     
 Specifies a plugin executable to use.                               
Normally, protoc searches the PATH for plugins, but you may specify additional  executables not in the path using this flag.                               Additionally, EXECUTABLE may be of the form                               NAME=PATH, in which case the given plugin name is mapped to the given executable even if  the executable's own name differs.

--cpp_out=OUT_DIR    
    Generate C++ header and source.  

--csharp_out=OUT_DIR  
     Generate C# source file.   

--java_out=OUT_DIR 
    Generate Java source file.  

--javanano_out=OUT_DIR  
    Generate Java Nano source file.   

--js_out=OUT_DIR      
  Generate JavaScript source.  
 
--objc_out=OUT_DIR          
   Generate Objective C header and source.  

--python_out=OUT_DIR      
   Generate Python source file.   

--ruby_out=OUT_DIR        
   Generate Ruby source file.

例子
Java 文件生成

$ protoc --java_out=./java/ ./proto/helloworld.proto

protoc 的命令格式為 protoc [OPTION] PROTO_FILES (最后是待編譯的 proto文件)
--java_out 為輸出java代碼的目錄,這里指定的是 ./java/ 目錄。
隨后我們指定了proto文件的位置 ./proto/helloworld.proto 。
執(zhí)行上述命令,我們就 ./java/ 目錄下就產(chǎn)生了對(duì)應(yīng)的 java文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容