首先看下下面這個proto文件,我們后面的proto基本用法都是基於這個proto進行講解
package pkgName; option java_package = "test1.test2"; option java_outer_classname = "TestClass"; message mmData { optional int32 num = 1; optional int32 def_num = 2 [default=10]; required string str = 3; repeated string rep_str = 4; }
1、包名package
proto文件使用關鍵字package指定當前包名,它類似於java中的包名或者C++中的命名空間,主要是用來防止不同消息類型的命名沖突。使用protobuf編譯器將proto文件編譯成C++代碼之后,當前proto文件中的所有聲明都將位於命名空間pkgName::下。
2、option
在消息定義之前,可以通過option來進行配置,常用的兩個option:
- option java_package=“xxx/xxx” 該選項指定了java文件生成的路徑
- option java_outer_classname=“xxx” 該選項制定了生成的java類名
3、消息類型
3.1 message
Protobuf中定義一個消息類型是通過關鍵字message字段指定的,這個關鍵字類似於C++/Java中的class關鍵字,使用protobuf編譯器將proto編譯成C++代碼之后,每個message都會生成一個名字與之對應的C++類。
如上面的message 將生成一個名字為mmData的類,該類公開的繼承google::protobuf::Message。
3.2 字段規則
message中的字段規則有三種。
- required : 字段屬性為必填字段。若不設置,則會導致編解碼異常,導致消息被丟棄。
- optional : 字段屬性為可選字段。發送方可以選擇性根據需要進行設置;對於optional屬性的字段,可以通過default關鍵字為字段設置默認值,即當發送方沒有對該字段進行設置的時候,將使用默認值。如果沒有對字段設置默認值,就會根據特定的類型給字段賦予特定的默認值。對於bool類型,默認值為false;對於string類型,默認值為空字符串;對於數值類型,默認值為0;對於枚舉類型,默認值是枚舉類型中的第一個值。
- repeated : 字段屬性為可重復字段,該字段可以包含[0,n]個元素,字段中的元素順序被保留。
注意: (1)在proto3版本中,字段規則上移除了required,並把optional字段改名為singular。所有沒有指定字段規則的字段默認為optional,
對於為什么刪除了require規則,參考:為什么 proto3 移除了 required 和 optional? (2)在proto2版本中,默認配置下,一個optional沒有被設置或者被顯示的設置為默認值,在序列化二進制格式的時候,這個字段將會被去掉,
導致反序列化之后,無法區分當初沒有設置還是設置了默認值,即使使用hasXXX()方法,對於設置的默認值的字段,也是返回false。
解決方法:區分 Protobuf 中缺失值和默認值
3.3 標識號
在消息體的定義中,每個字段都必須要有一個唯一的標識號。這些標識號是用來在消息的二進制格式中識別各個字段的,一旦使用就不能再改變,否則會導致原有消息編解碼出現異常。
標識號是[0,2^29 - 1]范圍內的一個整數,其中**[19000,19999)之間的標識號在protobuf協議的實現中被預留了**,所以特寫注意不要使用這個范圍內的標識號,若使用進行編譯的時候也會告警。
Field numbers 19000 through 19999 are reserved for the protocol buffer library implementation.
注意:[1,15]內的標識號在編碼的時候占用一個字節,[16,2047]之內的標識符占用兩個字節,所以盡量為頻繁使用的字段分配[1,15]內的標識號,
另外預留出來一部分給未來可能頻繁使用的字段。
3.4 數據類型
3.4.1 基本數據類型
消息體中的每個字段都必須指定字段類型,可選的字段類型和語=與其對應的C++/Java中的類型如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KTrq0uIB-1588870734147)(/Users/wingwei/workspace/wx_workspace/文檔/image/proto數據類型.png)]
圖片資源來源:https://blog.csdn.net/m15927408113/article/details/79976528
3.4.2 枚舉類型
字段類型除了上述基本的字段類型之外,也可以是枚舉類型。protobuf中的枚舉類型使用方法與C++中的枚舉類型類似,在將proto文件編譯成C++代碼后,其對應的類型也是C++中的枚舉類型。
package pkgName; // 定義枚舉類型 enum DayName { Sun = 0; Mon = 1; Tues = 2; Wed = 3; Thur = 4; Fri = 5; Sat = 6; } message workDay { // 消息類型使用枚舉類型 optional DayName day = 1; }
枚舉常量的值必須在32位整數范圍內,因為enum值是使用可編碼方式存儲的,對負數存儲不夠高效,因此不推薦在enum中使用負數。枚舉類型可以定義在message內,也可以定義在message外,若定義在message內,其他message要使用則需要通過messageType.enumType來進行引用。默認情況下,枚舉類型中的字段值不可重復,但是通過對enum添加**option allow_alias = true;**來達到對同一個枚舉值起一個別名的目的,若不添加allow_alise並且有重復的枚舉值編譯的時候會報錯。
package pkgName; enum DayName { // 若不添加該option,會報錯: // "pkgName.Test" uses the same enum value as "pkgName.Sat". If this is intended, set 'option allow_alias = true;' to the enum definition. option allow_alias = true; Sun = 0; Mon = 1; Tues = 2; Wed = 3; Thur = 4; Fri = 5; Sat = 6; Test = 6; // Test與Sat字段值重名 }
3.4.3 map數據類型
除了上述類型之外,message還支持map<Type,Type>類型。
package pkgName; message Tdata { map<int32,string> data = 1; }
注意: (1) protobuf中的map實質上是unordered_map (2) proto中map類型不能用optional/required/repeated任何類型修飾。
3.4.4 message類型
protobuf允許將其他消息類型用作字段類型。如下面userData中存在一個workDay類型的數據。
package pkgName; message workDay { optional int day = 1; } message userData { optional workDay userDays = 1; }
3.4.5 嵌套消息類型
與C++中的類可以嵌套類似,消息也可以嵌套。即允許你在一個messageType中定義另一個messageType,然后使用它,
package pkgName; message OutterData { // 嵌套消息定義,在生成的C++代碼中,該message被組織為類:outterData_Tdata message Tdata { optional int32 a = 1; } // 引用嵌套消息 optional Tdata data = 1; }
message嵌套在生成C++代碼之后,實際上內部類生成的類名為OutterData_Tdata
4、import導入其他proto文件
4.1 import
我們可以通過import導入其他proto文件,並使用該proto文件中的定義的消息類型。與c++中的include或者Java中的import類似。
如:
// a.proto文件 package Ap; message A{ optional int32 a = 1; }
// b.proto文件 import "a.proto"; // 導入a.proto文件,這樣就可以直接使用a.proto中定義的消息類型了。 package Bp; message B{ optional Ap.A a = 1; }
4.2 import public
默認情況下,proto只允許引用直接import的文件中定義的數據類型。如b.proto中導入了a.proto,c.proto中導入了b.proto;默認情況下,c.proto中只能引用b.proto中定義的數據類型,而引用不到a.proto中的數據類型。若c.proto要使用a.proto中定義的數據類型,則b.proto引用a.proto的時候要使用import public。
// a.proto文件 package Ap; message A{ optional int32 a = 1; }
// b.proto文件,使用import public 允許a.proto對b.proto的引用者(c.proto)可見 import public "a.proto"; package Bp; message B{ optional Ap.A a = 1; }
// c.proto import "b.proto"; package Cp; message C{ optional Ap.A a = 1; }
這種用法在遷移proto文件到新的位置的時候十分有用,如Message類要從old.proto遷移到new.proto文件中,這個時候如果要在不修改對old.proto的文件的情況下,直接將Message移動到new.proto中,然后在old.proto中import public new.proto即可。
5、更新Message消息類型原則
為了達到前后消息類型兼容的目的,擴展Message消息類型的時候需要注意一下幾點:
1、不要更改任何已有的字段的數值標識。
2、所添加的字段屬性必須是optional 或者repeated類型,如果擴展required類型,會導致舊的消息解析異常
3、非required字段可以移除。要保證它們的標示在新的消息類型中不再使用
4、一個非required的字段可以轉換為一個擴展,反之亦然——只要它的類型和標識號保持不變。
5、int32, uint32, int64, uint64,和bool是全部兼容的,這意味着可以將這些類型中的一個轉換為另外一個,而不會破壞向前、 向后的兼容性。如果解析出來的數字與對應的類型不相符,那么結果就像在C++中對它進行了強制類型轉換一樣(例如,如果把一個64位數字當作int32來 讀取,那么它就會被截斷為32位的數字)。
6、sint32和sint64是互相兼容的,但是它們與其他整數類型不兼容。
7、string和bytes是兼容的——只要bytes是有效的UTF-8編碼。
8、嵌套消息與bytes是兼容的——只要bytes包含該消息的一個編碼過的版本。
9、fixed32與sfixed32是兼容的,fixed64與sfixed64是兼容的。
6、protobuf擴展
6.1 extensions&extend
protobuf允許Message中預留出一個標識號段用作給第三方擴展使用。當其他人需要在message中擴展新的字段的時候,就不需要直接修改原文件,直接在自己的proto文件中定義該Message的擴展字段即可。(注意:一定要保證不同文件中擴展的標識號不能重復,否則會導致數據不一致的現象)
// base.proto package base; message BaseProto{ optional int32 id = 1; extensions 1000 to 2000; } // 擴展BaseProto extend base.BaseProto{ optional string name = 1000; }
注意訪問擴展字段的方式與訪問普通字段的方式有所不同,如在C++中對擴展字段設置為:
base::BaseProto test;
test.SetExtension(name, "wing");
同時還提供了一些其他的訪問操作,如:HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()等。
6.2 嵌套擴展
嵌套擴展即為可以在另外的類中添加擴展。
// base.proto package base; message BaseProto{ optional int32 id = 1; extensions 1000 to 2000; } message OtherProto { extend BaseProto { optional string name = 1000; } }
嵌套擴展在c++中的訪問方式類似,即在訪問擴展字段的時候在字段前加上作用域空間。
base::BaseProto test;
test.SetExtension(OtherProto::name, "wing");