什么是protocal buffer?
protocal buffer 以下簡稱protobuf是google 的一種數據交換的格式,它獨立于語言,獨立于平臺。(作用類似json、xml等,但是更安全,更快)
簡要說明一下流程:
文章分兩個部分 我先講講protobuf的語法規則與介紹,再講講如何使用(已經使用在項目中,如不想了解介紹可以直接跳到使用部分,一般.proto文件服務端會提供好)
介紹
如果你用 android studio 在plugin中安裝了 protobuf android插件。那么android studio 將可以識別.proto文件(.proto文件就是一種描述文件)
效果如下圖所示:
syntax
:是指定編譯的格式、我們可以使用 “proto2” “proto3” 具體區別我也沒有深究 默認使用“proto2”
packge
指定生成的java文件所在包
option java_package
指定生成的java文件所在的完整包名
-
message
是消息定義的關鍵字,等同于C++中的struct/class,或是Java中的class。 -
Request
為消息的名字,等同于結構體名或類名。 -
required
前綴表示該字段為必要字段,既在序列化和反序列化之前該字段必須已經被賦值。與此同時,在Protocol Buffer中還存在另外兩個類似的關鍵字,optional
和repeated
,帶有這兩種限定符的消息字段則沒有required
字段這樣的限制。相比于optional
,repeated
主要用于表示數組字段。具體的使用方式在后面的用例中均會一一列出。 -
int64
和string
分別表示長整型和字符串型的消息字段,在Protocol Buffer中存在一張類型對照表,既Protocol Buffer中的數據類型與其他編程語言(C++/Java)中所用類型的對照。該對照表中還將給出在不同的數據場景下,哪種類型更為高效。該對照表將在后面給出。 -
token
和sign
分別表示消息字段名,等同于Java中的域變量名,或是C++中的成員變量名。 - 標簽數字1和2則表示不同的字段在序列化后的二進制數據中的布局位置。在該例中,
sign
字段編碼后的數據一定位于token
之后。需要注意的是該值在同一message
中不能重復。另外,對于Protocol Buffer而言,標簽值為1到15的字段在編碼時可以得到優化,既標簽值和類型信息僅占有一個byte,標簽范圍是16到2047的將占有兩個bytes,而Protocol Buffer可以支持的字段數量則為2的29次方減一。有鑒于此,我們在設計消息結構時,可以盡可能考慮讓repeated類型的字段標簽位于1到15之間,這樣便可以有效的節省編碼后的字節數量。
定義一個含有枚舉字段
Protocol Buffer消息。
//在定義Protocol Buffer的消息時,可以使用和C++/Java代碼同樣的方式添加注釋。
enum UserStatus {
OFFLINE = 0; //表示處于離線狀態的用戶
ONLINE = 1; //表示處于在線狀態的用戶
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;
required UserStatus status = 3;
}
這里將給出以上消息定義的關鍵性說明(僅包括上一小節中沒有描述的)。
-
enum
是枚舉類型定義的關鍵字,等同于C++/Java中的enum。
-
UserStatus
為枚舉的名字。 - 和C++/Java中的枚舉不同的是,枚舉值之間的分隔符是分號,而不是逗號。
-
OFFLINE/ONLINE
為枚舉值。 - 0和1表示枚舉值所對應的實際整型值,和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
,這樣便可以很容易的實現嵌套消息的定義。如:
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;**
}
這里將給出以上消息定義的關鍵性說明(僅包括上兩小節中沒有描述的)。
- LogonRespMessage消息的定義中包含另外一個消息類型作為其字段,如UserInfo userInfo。
- 上例中的UserInfo和LogonRespMessage被定義在同一個.proto文件中,那么我們是否可以包含在其他.proto文件中定義的message呢?Protocol Buffer提供了另外一個關鍵字import,這樣我們便可以將很多通用的message定義在同一個.proto文件中,而其他消息定義文件可以通過import的方式將該文件中定義的消息包含進來,如:
import "myproject/CommonMessages.proto"**
限定符(required/optional/repeated)的基本規則
- 在每個消息中必須至少留有一個required類型的字段。
- 每個消息中可以包含0個或多個optional類型的字段。
- repeated表示的字段可以包含0個或多個數據。需要說明的是,這一點有別于C++/Java中的數組,因為后兩者中的數組必須包含至少一個元素。
- 如果打算在原有消息協議中添加新的字段,同時還要保證老版本的程序能夠正常讀取或寫入,那么對于新添加的字段必須是optional或repeated。道理非常簡單,老版本程序無法讀取或寫入新增的required限定符的字段。
** Protocol Buffer消息升級原則。**
在實際的開發中會存在這樣一種應用場景,既消息格式因為某些需求的變化而不得不進行必要的升級,但是有些使用原有消息格式的應用程序暫時又不能被立刻升級,這便要求我們在升級消息格式時要遵守一定的規則,從而可以保證基于新老消息格式的新老程序同時運行。規則如下:
- 不要修改已經存在字段的標簽號。
- 任何新添加的字段必須是optional和repeated限定符,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
- 在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標簽號必須被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味著如果想修改原有字段的類型時,為了保證兼容性,只能將其修改為與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
Packages
我們可以在.proto文件中定義包名,如: package ourproject.lyphone; 該包名在生成對應的C++文件時,將被替換為名字空間名稱,既namespace ourproject { namespace lyphone。而在生成的Java代碼文件中將
Options。
Protocol Buffer允許我們在.proto文件中定義一些常用的選項,這樣可以指示Protocol Buffer編譯器幫助我們生成更為匹配的目標語言代碼。Protocol Buffer內置的選項被分為以下三個級別:
- 文件級別,這樣的選項將影響當前文件中定義的所有消息和枚舉。
- 消息級別,這樣的選項僅影響某個消息及其包含的所有字段。
- 字段級別,這樣的選項僅僅響應與其相關的字段。 下面將給出一些常用的Protocol Buffer選項。
- option java_package = "com.companyname.projectname"; java_package是文件級別的選項,通過指定該選項可以讓生成Java代碼的包名為該選項值,如上例中的Java代碼包名為com.companyname.projectname。與此同時,生成的Java文件也將會自動存放到指定輸出目錄下的com/companyname/projectname子目錄中。如果沒有指定該選項,Java的包名則為package關鍵字指定的名稱。該選項對于生成C++代碼毫無影響。
- option java_outer_classname = "LYPhoneMessage"; java_outer_classname是文件級別的選項,主要功能是顯示的指定生成Java代碼的外部類名稱。如果沒有指定該選項,Java代碼的外部類名稱為當前文件的文件名部分,同時還要將文件名轉換為駝峰格式,如:my_project.proto,那么該文件的默認外部類名稱將為MyProject。該選項對于生成C++代碼毫無影響。 注:主要是因為Java中要求同一個.java文件中只能包含一個Java外部類或外部接口,而C++則不存在此限制。因此在.proto文件中定義的消息均為指定外部類的內部類,這樣才能將這些消息生成到同一個Java文件中。在實際的使用中,為了避免總是輸入該外部類限定符,可以將該外部類靜態引入到當前Java文件中,如:import static com.company.project.LYPhoneMessage.*。
- option optimize_for = LITE_RUNTIME; optimize_for是文件級別的選項,Protocol Buffer定義三種優化級別SPEED/CODE_SIZE/LITE_RUNTIME。缺省情況下是SPEED。 SPEED: 表示生成的代碼運行效率高,但是由此生成的代碼編譯后會占用更多的空間。 CODE_SIZE: 和SPEED恰恰相反,代碼運行效率較低,但是由此生成的代碼編譯后會占用更少的空間,通常用于資源有限的平臺,如Mobile。 LITE_RUNTIME: 生成的代碼執行效率高,同時生成代碼編譯后的所占用的空間也是非常少。這是以犧牲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。
- [pack = true]: 因為歷史原因,對于數值型的repeated字段,如int32、int64等,在編碼時并沒有得到很好的優化,然而在新近版本的Protocol Buffer中,可通過添加[pack=true]的字段選項,以通知Protocol Buffer在為該類型的消息對象編碼時更加高效。如: repeated int32 samples = 4 [packed=true]。 注:該選項僅適用于2.3.0以上的Protocol Buffer。
- [default = default_value]: optional類型的字段,如果在序列化時沒有被設置,或者是老版本的消息中根本不存在該字段,那么在反序列化該類型的消息是,optional的字段將被賦予類型相關的缺省值,如bool被設置為false,int32被設置為0。Protocol Buffer也支持自定義的缺省值,如: optional int32 result_per_page = 3 [default = 10]。
類型對照表
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 |
使用
為了方便大家使用,我將下面用到的文件都放到了csdn上面供大家下載(mac和windows的)
http://download.csdn.net/detail/qq_22605283/9722829
-
生成java文件(普通版)
1.在git上或者下面谷歌的官網鏈接下載根據平臺下載protoc的可運行文件,然后把 .proto文件放到同一個目錄下(或者從上面鏈接下載)
2 cd 到該目錄下用terminal 運行以下代碼:
protoc --java_out ./ ./.proto
protoc
:表示利用剛才下載的可運行程序打包
--java_out :
是輸出指令 第一個從參數是生成的java目錄,第二個參數是指定編 譯的.proto文件 ./.proto表示當前目錄下所有.proto文件
3把java文件拷貝到項目上
4 在項目的build.gradle的dependencies節點下加入代碼:
compile 'com.google.protobuf:protobuf-java:3.0.0'
這行代碼是引用android 使用上面生成的java代碼需要的jar包
該方法生成的java文件方法會比較多,文件也比較大,會占用許多空間,所以當運用到實際項目的時候會采用輕量版
-
生成java文件(lite版)
1.在git上或者下面谷歌的官網鏈接下載根據平臺下載protoc的可運行文件,然后把 .proto文件放到同一個目錄下(或者從上面鏈接下載)
2.在git上面下載proto-gen-javalite可運行文件放在上面的目錄下
2 cd 到該目錄下用terminal 運行以下代碼:
protoc --javalite_out ./ ./*.proto
3把java文件拷貝到項目上
4 在項目的build.gradle的dependencies節點下加入代碼:
compile 'com.google.protobuf:protobuf-lite:3.0.1'
注意有區別哦
還有你會發現雖然方法數少了,size也少了,可是這個java文件依舊占了很大的size。不要急,打開后你會發現其實有很大一部分的代碼都是注釋,所以當文件打包的時候或者混淆的時候實際會變得很小。舉個實例:我的項目中生成的java文件將近700k,混淆完最后打包只占用了40k的大小。
-
使用java代碼
例如 我生成了一個Pb.java文件 ,如果原本的.proto帶有一個message Request,那么這個Pb.java文件中就會有一個Request的內部類。 看例子代碼:
String appIdUTF8 = "";
try {//注意格式轉碼
appIdUTF8 = URLEncoder.encode("你好啊", "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "err: " + e.getMessage());
}
//實例化一個PB對象
Pb.Request request = Pb.Request.newBuilder().setId(appIdUTF8).set***.builder
byte[] data = rq.toByteArray();//將PB對象轉換成二進制流
String stream =new String(data);//講PB對象轉換成String
...//解析 從網絡上或者數據 比如我們已經連接上一個HttpURLConnection conn
//可以被解析的參數有很多種,可以查看下面的圖
Pb.Request rq =Pb.Request.parseFrom(conn.getInputStream())
rq.getSid();//獲得Pb對象的sid屬性
好了到這里已經介紹完了,當我使用pb時,都是服務器給定的.proto文件,所以不會動態的去生成java文件,一次生成一直使用,如果你的項目有需求動態更新.proto文件的時候并且生成新的java代碼 請參考這個帖子:
http://www.tuicool.com/articles/ruIFvif
資料參考
https://developers.google.com/protocol-buffers/
http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.html