Protocol Buffer基本语法
為什么使用Protocol Buffer?
?? ?? 在回答這個問題之前,我們還是先給出一個在實際開發(fā)中經(jīng)常會遇到的系統(tǒng)場景。比如:我們的客戶端程序是使用Java開發(fā)的,可能運(yùn)行自不同的平臺,如:Linux、Windows或者是Android,而我們的服務(wù)器程序通常是基于Linux平臺并使用C++開發(fā)完成的。在這兩種程序之間進(jìn)行數(shù)據(jù)通訊時存在多種方式用于設(shè)計消息格式,如:?? ?? 1. 直接傳遞C/C++語言中一字節(jié)對齊的結(jié)構(gòu)體數(shù)據(jù),只要結(jié)構(gòu)體的聲明為定長格式,那么該方式對于C/C++程序而言就非常方便了,僅需將接收到的數(shù)據(jù)按照結(jié)構(gòu)體類型強(qiáng)行轉(zhuǎn)換即可。事實上對于變長結(jié)構(gòu)體也不會非常麻煩。在發(fā)送數(shù)據(jù)時,也只需定義一個結(jié)構(gòu)體變量并設(shè)置各個成員變量的值之后,再以char*的方式將該二進(jìn)制數(shù)據(jù)發(fā)送到遠(yuǎn)端。反之,該方式對于Java開發(fā)者而言就會非常繁瑣,首先需要將接收到的數(shù)據(jù)存于ByteBuffer之中,再根據(jù)約定的字節(jié)序逐個讀取每個字段,并將讀取后的值再賦值給另外一個值對象中的域變量,以便于程序中其他代碼邏輯的編寫。對于該類型程序而言,聯(lián)調(diào)的基準(zhǔn)是必須客戶端和服務(wù)器雙方均完成了消息報文構(gòu)建程序的編寫后才能展開,而該設(shè)計方式將會直接導(dǎo)致Java程序開發(fā)的進(jìn)度過慢。即便是Debug階段,也會經(jīng)常遇到Java程序中出現(xiàn)各種域字段拼接的小錯誤。
?? ?? 2. 使用SOAP協(xié)議(WebService)作為消息報文的格式載體,由該方式生成的報文是基于文本格式的,同時還存在大量的XML描述信息,因此將會大大增加網(wǎng)絡(luò)IO的負(fù)擔(dān)。又由于XML解析的復(fù)雜性,這也會大幅降低報文解析的性能。總之,使用該設(shè)計方式將會使系統(tǒng)的整體運(yùn)行性能明顯下降。
?? ?? 對于以上兩種方式所產(chǎn)生的問題,Protocol Buffer均可以很好的解決,不僅如此,Protocol Buffer還有一個非常重要的優(yōu)點就是可以保證同一消息報文新舊版本之間的兼容性。至于具體的方式我們將會在后續(xù)的博客中給出。? 二、定義第一個Protocol Buffer消息。
?? ?? 創(chuàng)建擴(kuò)展名為.proto的文件,如:MyMessage.proto,并將以下內(nèi)容存入該文件中。
?? ?? message LogonReqMessage {
? ?? ???? required int64 acctID = 1;
?? ? ? ?? required string passwd = 2;
?? ?? }
?? ?? 這里將給出以上消息定義的關(guān)鍵性說明。
?? ?? 1. message是消息定義的關(guān)鍵字,等同于C++中的struct/class,或是Java中的class。
?? ?? 2. LogonReqMessage為消息的名字,等同于結(jié)構(gòu)體名或類名。
?? ?? 3. required前綴表示該字段為必要字段,既在序列化和反序列化之前該字段必須已經(jīng)被賦值。與此同時,在Protocol Buffer中還存在另外兩個類似的關(guān)鍵字,optional和repeated,帶有這兩種限定符的消息字段則沒有required字段這樣的限制。相比于optional,repeated主要用于表示數(shù)組字段。具體的使用方式在后面的用例中均會一一列出。
?? ?? 4. int64和string分別表示長整型和字符串型的消息字段,在Protocol Buffer中存在一張類型對照表,既Protocol Buffer中的數(shù)據(jù)類型與其他編程語言(C++/Java)中所用類型的對照。該對照表中還將給出在不同的數(shù)據(jù)場景下,哪種類型更為高效。該對照表將在后面給出。
?? ?? 5. acctID和passwd分別表示消息字段名,等同于Java中的域變量名,或是C++中的成員變量名。
?? ?? 6. 標(biāo)簽數(shù)字1和2則表示不同的字段在序列化后的二進(jìn)制數(shù)據(jù)中的布局位置。在該例中,passwd字段編碼后的數(shù)據(jù)一定位于acctID之后。需要注意的是該值在同一message中不能重復(fù)。另外,對于Protocol Buffer而言,標(biāo)簽值為1到15的字段在編碼時可以得到優(yōu)化,既標(biāo)簽值和類型信息僅占有一個byte,標(biāo)簽范圍是16到2047的將占有兩個bytes,而Protocol Buffer可以支持的字段數(shù)量則為2的29次方減一。有鑒于此,我們在設(shè)計消息結(jié)構(gòu)時,可以盡可能考慮讓repeated類型的字段標(biāo)簽位于1到15之間,這樣便可以有效的節(jié)省編碼后的字節(jié)數(shù)量。
定義第一個Protocol Buffer消息
?? ?? 創(chuàng)建擴(kuò)展名為.proto的文件,如:MyMessage.proto,并將以下內(nèi)容存入該文件中。
?? ?? message LogonReqMessage {
? ?? ???? required int64 acctID = 1;
?? ? ? ?? required string passwd = 2;
?? ?? }
?? ?? 這里將給出以上消息定義的關(guān)鍵性說明。
?? ?? 1. message是消息定義的關(guān)鍵字,等同于C++中的struct/class,或是Java中的class。
?? ?? 2. LogonReqMessage為消息的名字,等同于結(jié)構(gòu)體名或類名。
?? ?? 3. required前綴表示該字段為必要字段,既在序列化和反序列化之前該字段必須已經(jīng)被賦值。與此同時,在Protocol Buffer中還存在另外兩個類似的關(guān)鍵字,optional和repeated,帶有這兩種限定符的消息字段則沒有required字段這樣的限制。相比于optional,repeated主要用于表示數(shù)組字段。具體的使用方式在后面的用例中均會一一列出。
?? ?? 4. int64和string分別表示長整型和字符串型的消息字段,在Protocol Buffer中存在一張類型對照表,既Protocol Buffer中的數(shù)據(jù)類型與其他編程語言(C++/Java)中所用類型的對照。該對照表中還將給出在不同的數(shù)據(jù)場景下,哪種類型更為高效。該對照表將在后面給出。
?? ?? 5. acctID和passwd分別表示消息字段名,等同于Java中的域變量名,或是C++中的成員變量名。
?? ?? 6. 標(biāo)簽數(shù)字1和2則表示不同的字段在序列化后的二進(jìn)制數(shù)據(jù)中的布局位置。在該例中,passwd字段編碼后的數(shù)據(jù)一定位于acctID之后。需要注意的是該值在同一message中不能重復(fù)。另外,對于Protocol Buffer而言,標(biāo)簽值為1到15的字段在編碼時可以得到優(yōu)化,既標(biāo)簽值和類型信息僅占有一個byte,標(biāo)簽范圍是16到2047的將占有兩個bytes,而Protocol Buffer可以支持的字段數(shù)量則為2的29次方減一。有鑒于此,我們在設(shè)計消息結(jié)構(gòu)時,可以盡可能考慮讓repeated類型的字段標(biāo)簽位于1到15之間,這樣便可以有效的節(jié)省編碼后的字節(jié)數(shù)量。
定義第二個(含有枚舉字段)Protocol Buffer消息
? ? ? //在定義Protocol Buffer的消息時,可以使用和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)鍵性說明(僅包括上一小節(jié)中沒有描述的)。
?? ?? 1. enum是枚舉類型定義的關(guān)鍵字,等同于C++/Java中的enum。
?? ?? 2. UserStatus為枚舉的名字。
?? ?? 3. 和C++/Java中的枚舉不同的是,枚舉值之間的分隔符是分號,而不是逗號。
?? ?? 4. OFFLINE/ONLINE為枚舉值。
?? ?? 5. 0和1表示枚舉值所對應(yīng)的實際整型值,和C/C++一樣,可以為枚舉值指定任意整型值,而無需總是從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;
?? ?? }
定義第三個(含有嵌套消息字段)Protocol Buffer消息
?? ?? 我們可以在同一個.proto文件中定義多個message,這樣便可以很容易的實現(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)鍵性說明(僅包括上兩小節(jié)中沒有描述的)。
?? ?? 1. LogonRespMessage消息的定義中包含另外一個消息類型作為其字段,如UserInfo userInfo。
?? ?? 2. 上例中的UserInfo和LogonRespMessage被定義在同一個.proto文件中,那么我們是否可以包含在其他.proto文件中定義的message呢?Protocol Buffer提供了另外一個關(guān)鍵字import,這樣我們便可以將很多通用的message定義在同一個.proto文件中,而其他消息定義文件可以通過import的方式將該文件中定義的消息包含進(jìn)來,如:
??????import?"myproject/CommonMessages.proto"
限定符(required/optional/repeated)的基本規(guī)則
? ? ? 1. 在每個消息中必須至少留有一個required類型的字段。??? ?? 2. 每個消息中可以包含0個或多個optional類型的字段。
?? ?? 3. repeated表示的字段可以包含0個或多個數(shù)據(jù)。需要說明的是,這一點有別于C++/Java中的數(shù)組,因為后兩者中的數(shù)組必須包含至少一個元素。
?? ?? 4. 如果打算在原有消息協(xié)議中添加新的字段,同時還要保證老版本的程序能夠正常讀取或?qū)懭?#xff0c;那么對于新添加的字段必須是optional或repeated。道理非常簡單,老版本程序無法讀取或?qū)懭胄略龅膔equired限定符的字段。
類型對照表
| .proto Type | Notes | C++ Type | Java Type |
| double | ? | ?double | ?double |
| float | ? | ?float | ?float |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | ?int32 | ?int |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | ?int64 | ?long |
| uint32 | Uses variable-length encoding. | ?uint32 | ?int |
| uint64 | Uses variable-length encoding. | ?uint64 | ?long |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | ?int32 | ?int |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.? | ?int64 | ?long |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228.? | ?uint32 | ?int |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | ?uint64 | ?long |
| sfixed32 | Always four bytes. | ?int32 | ?int |
| sfixed64 | Always eight bytes. | ?int64 | ?long |
| bool | ? | ?bool | ?boolean |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | ?string | ?String |
| bytes | May contain any arbitrary sequence of bytes. | string | ByteString |
Protocol Buffer消息升級原則
?? ?? 在實際的開發(fā)中會存在這樣一種應(yīng)用場景,既消息格式因為某些需求的變化而不得不進(jìn)行必要的升級,但是有些使用原有消息格式的應(yīng)用程序暫時又不能被立刻升級,這便要求我們在升級消息格式時要遵守一定的規(guī)則,從而可以保證基于新老消息格式的新老程序同時運(yùn)行。規(guī)則如下:?? ?? 1. 不要修改已經(jīng)存在字段的標(biāo)簽號。
?? ?? 2. 任何新添加的字段必須是optional和repeated限定符,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
?? ?? 3. 在原有的消息中,不能移除已經(jīng)存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標(biāo)簽號必須被保留,不能被新的字段重用。
?? ?? 4. int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味著如果想修改原有字段的類型時,為了保證兼容性,只能將其修改為與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
?? ?? 5. optional和repeated限定符也是相互兼容的。
Packages
?? ?? 我們可以在.proto文件中定義包名,如:
?? ???package?ourproject.lyphone;
?? ?? 該包名在生成對應(yīng)的C++文件時,將被替換為名字空間名稱,既namespace ourproject { namespace lyphone。而在生成的Java代碼文件中將成為包名。
Options
?? ?? Protocol Buffer允許我們在.proto文件中定義一些常用的選項,這樣可以指示Protocol Buffer編譯器幫助我們生成更為匹配的目標(biāo)語言代碼。Protocol Buffer內(nèi)置的選項被分為以下三個級別:?? ?? 1. 文件級別,這樣的選項將影響當(dāng)前文件中定義的所有消息和枚舉。
?? ?? 2. 消息級別,這樣的選項僅影響某個消息及其包含的所有字段。
?? ?? 3. 字段級別,這樣的選項僅僅響應(yīng)與其相關(guān)的字段。
?? ?? 下面將給出一些常用的Protocol Buffer選項。
?? ?? 1. option java_package = "com.companyname.projectname";
?? ???java_package是文件級別的選項,通過指定該選項可以讓生成Java代碼的包名為該選項值,如上例中的Java代碼包名為com.companyname.projectname。與此同時,生成的Java文件也將會自動存放到指定輸出目錄下的com/companyname/projectname子目錄中。如果沒有指定該選項,Java的包名則為package關(guān)鍵字指定的名稱。該選項對于生成C++代碼毫無影響。
?? ?? 2. option java_outer_classname = "LYPhoneMessage";
?? ???java_outer_classname是文件級別的選項,主要功能是顯示的指定生成Java代碼的外部類名稱。如果沒有指定該選項,Java代碼的外部類名稱為當(dāng)前文件的文件名部分,同時還要將文件名轉(zhuǎn)換為駝峰格式,如:my_project.proto,那么該文件的默認(rèn)外部類名稱將為MyProject。該選項對于生成C++代碼毫無影響。
?? ?? 注:主要是因為Java中要求同一個.java文件中只能包含一個Java外部類或外部接口,而C++則不存在此限制。因此在.proto文件中定義的消息均為指定外部類的內(nèi)部類,這樣才能將這些消息生成到同一個Java文件中。在實際的使用中,為了避免總是輸入該外部類限定符,可以將該外部類靜態(tài)引入到當(dāng)前Java文件中,如:import static com.company.project.LYPhoneMessage.*。
?? ?? 3. option optimize_for = LITE_RUNTIME;
?? ???optimize_for是文件級別的選項,Protocol Buffer定義三種優(yōu)化級別SPEED/CODE_SIZE/LITE_RUNTIME。缺省情況下是SPEED。
?? ?? SPEED: 表示生成的代碼運(yùn)行效率高,但是由此生成的代碼編譯后會占用更多的空間。
?? ?? CODE_SIZE: 和SPEED恰恰相反,代碼運(yùn)行效率較低,但是由此生成的代碼編譯后會占用更少的空間,通常用于資源有限的平臺,如Mobile。
?? ?? LITE_RUNTIME: 生成的代碼執(zhí)行效率高,同時生成代碼編譯后的所占用的空間也是非常少。這是以犧牲Protocol Buffer提供的反射功能為代價的。因此我們在C++中鏈接Protocol Buffer庫時僅需鏈接libprotobuf-lite,而非libprotobuf。在Java中僅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。
?? ?? 注:對于LITE_MESSAGE選項而言,其生成的代碼均將繼承自MessageLite,而非Message。?? ?
?? ?? 4. [packed = true]: 因為歷史原因,對于數(shù)值型的repeated字段,如int32、int64等,在編碼時并沒有得到很好的優(yōu)化,然而在新近版本的Protocol Buffer中,可通過添加[packed=true]的字段選項,以通知Protocol Buffer在為該類型的消息對象編碼時更加高效。如:
?? ?? repeated int32 samples = 4 [packed=true]。
?? ?? 注:該選項僅適用于2.3.0以上的Protocol Buffer。
?? ?? 5. [default?= default_value]: optional類型的字段,如果在序列化時沒有被設(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 --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
?? ?? 這里將給出上述命令的參數(shù)解釋。
?? ?? 1. protoc為Protocol Buffer提供的命令行編譯工具。
?? ?? 2. --proto_path等同于-I選項,主要用于指定待編譯的.proto消息定義文件所在的目錄,該選項可以被同時指定多個。
?? ?? 3. --cpp_out選項表示生成C++代碼,--java_out表示生成Java代碼,--python_out則表示生成Python代碼,其后的目錄為生成后的代碼所存放的目錄。
?? ?? 4. path/to/file.proto表示待編譯的消息定義文件。
?? ?? 注:對于C++而言,通過Protocol Buffer編譯工具,可以將每個.proto文件生成出一對.h和.cc的C++代碼文件。生成后的文件可以直接加載到應(yīng)用程序所在的工程項目中。如:MyMessage.proto生成的文件為MyMessage.pb.h和MyMessage.pb.cc。
總結(jié)
以上是生活随笔為你收集整理的Protocol Buffer基本语法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有符号数据的符号位扩展
- 下一篇: Protocol Buffer C++应