目錄
前言
一、Protobuf 基本語法
1.1、Protoc?版本?
1.2、文件格式配置
1.3、消息字段規則
1.3.1、字段數據類型
1.3.2、字段修飾規則
1.3.3、消息類型定義
1.3.4、enum 類型
1.3.5、Any 類型
1.3.6、oneof 類型
1.3.7、map 類型
1.3.8、默認值
1.3.9、更新消息規則
1.3.10、保留字段 reserved
1.3.11、選項 optional(了解,proto3 移除)
前言
前面在講?gRPC 的時候有講到?Protobuf 的語法,但實際上遠沒有這么簡單,有很多坑和注意事項,所以這篇文章就是來補坑的~
一、Protobuf 基本語法
1.1、Protoc?版本?
1.2、文件格式配置
a)創建文件:文件都是 proto 為后綴,例如 UserService.proto
b)基本內容:
// 設定使用的 proto 版本
syntax = "proto3";/**java_multiple_files = true 表示 Protobuf 編譯器會為每個定義的消息類型生成一個單獨的 Java 文件,而不是都放在一個文件中java_multiple_files = false 表示 Protobuf 編譯器會把所有的消息類型生成的 Java 代碼都放在一個文件中*/
option java_multiple_files = false;/**指定 protobuf 生成的類,放在哪個包中*/
option java_package = "org.cyk";/**指定 protobuf 生成的外部類的名字外部類是用來管理內部類的內部類才是開發中使用的*/
option java_outer_classname = "HelloProto";
c)導包:例如有 A.proto 和 B.proto 文件,現在需要在 B.proto 文件中引入 A.proto 文件的內容,就需要使用? import
import xxx/A.proto
1.3、消息字段規則
1.3.1、字段數據類型
消息中定義的數據類型(我們主要關心 .proto 對應的 Java/Kotlin 類型).
以下列表來自官網:Language Guide (proto 3) | Protocol Buffers Documentation
.proto Type | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type | |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
int64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
uint32 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int | |
uint64 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | |
sint32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
sint64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
fixed32 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int | |
fixed64 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | |
sfixed32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
sfixed64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | |
bytes | string | ByteString | str (Python 2) bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string |
1.3.2、字段修飾規則
消息字段可以使用如下規則修飾:
- singular(proto3 中默認使用此規則):表示該字段只能 null 或者是一個具體值.
- repeated:表示為 Java 中的 List 類型.
例如如下:
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";message Userinfo {string name = 1; // 這里的數字是字段的唯一標識,因為將來時面向字節流傳輸,需要讓每個字段能夠對應的上int32 age = 2;repeated string phone = 3;
}
1.3.3、消息類型定義
在單個 .proto 文件中可以定義多個消息,并且支持嵌套類型,不同消息體重的編碼可以重復.
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";//1.非嵌套
message Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;
}//2.嵌套
message Userinfo2 {string name = 1;int32 age = 2;repeated string phone = 3;message Stat {int32 rank = 1;int32 fans = 2;}
}//3.消息類型可作為字段使用
message Human {string aaa = 1;Userinfo1 userinfo = 2;
}
1.3.4、enum 類型
enum ArticlePubType {NORMAL = 0; //發布普通文章PRIVATE = 1; //發布私有文章TIMER = 2; //定時發布文章
}
規則如下:
- 第一個枚舉值必須是 0.
- 枚舉類型可以定義在消息外,也可以在定義在消息體內(嵌套).
- 枚舉的常量值在 32 位整數范圍內.? 但因為負值無效,所以不建議使用(和編碼規則有關).
- 同級(同一個文件下,或者是引入的其他 proto 文件)枚舉類型,枚舉值不能重名.
1.3.5、Any 類型
可以簡單的理解位 泛型.? ?因此 Any 中可以存儲任意消息類型.? 并且 Any 類型也可以使用 repeated 修飾.
Note:Any 類型是 google 已經定義好的類型,在 include 目錄下就可以找到所有 google 已經定義好的 .proto 文件.
import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;
}
將來通過 protoc 編譯生成的 Java 文件中,給 Any 類型生成的對象提供了如下方法:
- hasXXX:用來檢測當前字段是否被設置(這個方法存在的意義在于,字段即使不設置也是有默認值的,因此 has 就可以檢測到底是否真的有設置值).
- setXXX:要求傳一個 Any 類型的對象.
- 對于 Any 類型:
- Any.pack(T message) 可以講任意消息類型轉化成 Any 類型.
- message.getAny().unpack(Class<T> clazz) 方法可以將 Any 類型轉回之前設置的任意消息類型.
- message.getAny().is(Class<T> clazz) 用來判斷存放的消息類型.
1.3.6、oneof 類型
如果消息中有很多可選字段,并且將來只有一個字段會被設置,那么就可以使用 oneof 來約束這個行為.
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;oneof other_content {string qq = 5;string wechat = 6;}
}
注意事項:
- 可選字段中的 字段編號 不能和 非可選字段 的編號沖突.
- oneof 中不能使用 repeated 字段.
- 如果將來 oneof 中有多個字段被設置了值,那么只會保留最后一個設置的成員,之前設置的 oneof 成員會自動清除.
oneof 將來生成 Java 代碼中會提供以下方法:
- clear():清空 oneof 中的字段.
- getXXXCase():獲取當前設置了哪些字段.
- hasXXX():檢查當前字段是否被設置.
1.3.7、map 類型
類似于 Java 中的 HashMap,使用方式如下:
map<key_type>, value_type> map_field = N;
注意:
- key_type 是除了 float 和 bytes 類型以外的任意標量類型.
- value_type 可以是任意類型.
- map 字段不可以用 repeated 修飾.
例如:
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;oneof other_content {string qq = 5;string wechat = 6;}map<string, string> arguments = 7;
}
1.3.8、默認值
如果將來給服務端發送的消息對象中有一些字段沒有設置值,那么將來這些消息字段在反序列化時就會被設置默認值.? ?不同類型默認值不同:
類型 | 默認值 |
---|---|
字符串 | 空字符串 |
字節 | 空字節 |
布爾值 | false |
數值類型 | 0 |
枚舉 | 默認是第一個定義的枚舉值,也就是 0(也必須是 0). |
消息字段 | 具體根據語言而定 |
repeated 修飾的字段 | 空列表 |
消息字段、oneof字段、any字段 | 有 has 方法來檢測當前字段是否被設置 |
1.3.9、更新消息規則
當現有消息類型已經不再滿足我們的需求,需要擴展一個字段的時候,要注意遵守以下規則:
a)禁止修改任何已有的字段編號.
例如你更新的這個 proto 文件中一個已有的字段編號,并且這個新版的 proto 文件被用于生成序列化代碼,但是服務端這邊反序列化代碼還是使用的舊代碼進行反序列化,這就可能導致數據丟失或者解析錯誤.
b)如果要刪除老字段,要保證不再使用刪除字段的編號.? 正確的做法是通過 reserved 保留字段編號,確保該編號不能重復使用.
因為如果我們只是簡單的刪除了某一個字段而不采取其他措施,那么就可能導致和 (a) 一樣的情況.
c)int32、uint32、int64、bool 之間是完全兼容的.? 也就是說這些類型中任意一個改成另一個,都不會破壞兼容性.? 不過還是要注意,如果從精度較高的字段轉化為精度較低的字段可能會被截斷.
例如 64位 當作 32位 讀取,雖然不存在兼容性問題,但會截斷 32 位.
d)新增一個字段到 oneof 類型是不安全的.
這個本質上 和 (a) 問題一致.? ?因為 oneof 如果被客戶端設置的字段是新增的字段,而服務端這邊還是使用舊的反序列化代碼解析,就可能會出現問題.
1.3.10、保留字段 reserved
如果通過刪除字段來更新消息,未來用戶在添加新字段時,可能會使用以前被刪除的字段編號.? 將來使用 proto 舊版本的程序就會引發很多問題.
那么為了確保不會發生這種情況的方法就是:使用 reserverd 將指定字段的 編號 或 名稱 設置位保留項.? 當我們再使用這些 編號 或 名稱 時,protocol 編譯器將會警告這些編號不可用.
1.3.11、選項 optional(了解,proto3 移除)
在 proto2 中 optional 是一個字段修飾符,標識字段在消息中是可選的(消息中可能包含該字段,也可能不包含). 但是從 proto3 開始,optional 就被移除了,并且所有字段都是可選的(因為他們都有默認值).?