文章目錄
簡介
- Protocol buffers 是一種語言無關,平臺無關,可擴展的序列化數據的格式,可用於通信協議,數據存儲等。Protocol buffers 很適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。
- protocol buffers 誕生之初是爲了解決服務器端新舊協議(高低版本)兼容性問題,名字也很體貼,“協議緩衝區”。只不過後期慢慢發展成用於傳輸數據
- 版本:目前 protocol buffers 最新版本是 proto3,與老的版本 proto2 的 API 不完全兼容。 proto中所有結構化的數據都被稱爲 message
使用入門
安裝
-
mac安裝,下載安裝包https://github.com/google/protobuf/releases
1. 下載protoc-3.10.0-osx-x86_64.zip 2. 解壓 3. 配置環境變量 export PROTOBUF=/Volumes/P/develope/protoc-3.10.0-osx-x86_64/ export PATH=$PATH:$PROTOBUF/bin source ~/.zshrc
準備
-
生成前的目錄結構
├── build.gradle └── src ├── main │ ├── java │ ├── proto │ │ └── UserProtobuf.proto │ └── resources │ └── logback.xml └── test ├── java └── resources
-
Protobuf 編譯器通過描述文件(.proto文件)生成對應於語言的代碼,代碼中定義了消息類型、獲取、設置、編解碼序列化等操作。這也是爲什麼 Protobuf 支持跨語言傳輸,因爲消息所有端共用一個通用的描述文件。
UserProtobuf.proto
內容如下://proto3語法註解:默認是proto2,這必須是文件的第一個非空的非註釋行。 syntax = "proto3"; //生成的包名 option java_package = "cn.jannal.protobuf"; //生成的java類名 option java_outer_classname = "User"; message UserProto{ //ID int32 id = 1; //姓名 string name = 2; //年齡 int32 age = 3; //狀態 int32 state = 4; }
生成並運行java代碼
-
第一種方式:直接通過命令行生成java,使用
protoc
工具可以把編寫好的proto
文件生成不同的語言代碼。對Java來說,編譯器爲每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創建消息類接口的)。對javaNano來說,JavaNano是專門爲資源受限系統(如Android)設計的特殊代碼生成器和運行時庫, 代碼量和運行時開銷都非常資源友好-I 後面是 proto 文件所在目錄 --java_out 後面是 java 文件存放地址 最後一行是 proto 文件名稱 protoc -I=src/main/proto --java_out=src/main/java UserProtobuf.proto
-
第二種方式:通過gradle插件生成,插件地址https://github.com/google/protobuf-gradle-plugin。可以在build之前根據proto文件自動生成java文件
buildscript { repositories { mavenLocal() } dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' } } subprojects { apply plugin: 'java' apply plugin: 'com.google.protobuf' sourceCompatibility = 1.8 targetCompatibility = 1.8 idea { module { downloadJavadoc = true downloadSources = true } } dependencies { compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.10.0' } //==============protobuf配置================ sourceSets { main { proto { //默認是 'src/main/proto' srcDir 'src/main/proto' } } test { proto { // 默認 'src/test/proto' srcDir 'src/test/proto' } } } protobuf { protoc { //從倉庫下載 artifact = 'com.google.protobuf:protoc:3.10.0' //生成代碼的目錄 generatedFilesBaseDir = "$projectDir/src" } } //在build之前執行proto代碼生成 build.dependsOn(":$project.name:generateProto") //==============protobuf配置================ }
-
生成後的目錄結構
. ├── build.gradle └── src ├── main │ ├── java │ │ └── cn │ │ └── jannal │ │ └── protobuf │ │ └── User.java │ ├── proto │ │ └── UserProtobuf.proto │ └── resources │ └── logback.xml └── test ├── java │ └── cn │ └── jannal │ └── protobuf │ └── UserTest.java └── resources
-
測試程序
public class UserTest { private static final Logger logger = LoggerFactory.getLogger(UserTest.class); public static void main(String[] args) throws IOException { User.UserProto.Builder user = User.UserProto.newBuilder(); user.setAge(12); user.setId(1000); user.setName("jannal"); User.UserProto userInfo = user.build(); // 將數據寫到輸出流 ByteArrayOutputStream output = new ByteArrayOutputStream(); userInfo.writeTo(output); // 將數據序列化後發送 byte[] byteArray = output.toByteArray(); // 接收到流並讀取 ByteArrayInputStream input = new ByteArrayInputStream(byteArray); // 反序列化 userInfo = User.UserProto.parseFrom(input); /** * id: 1000 * name: "jannal" * age: 12 */ logger.info("{}", userInfo.toString()); } }
語法
-
一個proto文件可以定義一個或者多個message
// 指定使用proto3,如果不指定的話,編譯器會使用proto2去編譯 syntax = "proto3"; //[proto2|proto3] message SearchRequests { // 定義SearchRequests的成員變量,需要指定:[變量類型]、[變量名]、[變量Tag] string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { repeated string result = 1; }
-
嵌套定義
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; } message SomeOtherMessage { //定義在message內部的message可以這樣使用 SearchResponse.Result result = 1; }
-
註釋
使用//表示註釋
-
消息由至少一個字段組合而成,類似於C語言中的結構。每個字段都有一定的格式
限定修飾符① | 數據類型② | 字段名稱③ | = | 字段編碼值④ | [字段默認值⑤]
-
限定修飾符① 包含
required\optional\repeated
Required
: 表示是一個必須字段,必須相對於發送方,在發送消息之前必須設置該字段的值,對於接收方,必須能夠識別該字段的意思。發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄。Optional
:表示是一個可選字段,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該字段的值。對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理。因爲optional字段的特性,很多接口在升級版本中都把後來添加的字段都統一的設置爲optional字段,這樣老的版本無需升級程序也可以正常的與新的軟件進行通信,只不過新的字段無法識別而已,因爲並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。Repeated
: 表示該字段可以包含0~N個元素。其特性和optional一樣,但是每一次可以包含多個值。可以看作是在傳遞一個數組的值。
變量
變量類型
-
變量類型
Proto 描述 Java double double float float int32 使用變長編碼,對負數編碼效率低,如果你的變量可能是負數,可以使用sint32 int int64 使用變長編碼,對負數編碼效率低,如果你的變量可能是負數,可以使用sint64 long uint32 使用變長編碼 int uint64 使用變長編碼 long sint32 使用變長編碼,帶符號的int類型,對負數編碼比int32高效 int sint64 使用變長編碼,帶符號的int類型,對負數編碼比int64高效 long fixed32 4字節編碼, 如果變量經常大於2^28 的話,會比uint32高效 int fixed64 8字節編碼, 如果變量經常大於2^56 的話,會比uint64高效 long sfixed32 4字節編碼 int sfixed64 8字節編碼 long bool bool string 必須包含utf-8編碼或者7-bit ASCII text String bytes 任意的字節序列 String -
Any可以讓你在 proto 文件中使用未定義的類型,具體裏面保存什麼數據,是在上層業務代碼使用的時候決定的,使用 Any 必須導入
import google/protobuf/any.proto
import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; }
-
如果你的消息中有很多可選字段,而同一個時刻最多僅有其中的一個字段被設置的話,你可以使用
oneof
來強化這個特性並且節約存儲空間(設置一個oneof字段會自動清理其他的oneof字段)。- 反射API對oneof 字段有效.
- oneof不支持
repeated
.
name 和 age 都是 LoginReply 的成員,但不能給他們同時設置值 message LoginReply { oneof test_oneof { string name = 3; string age = 4; } required string status = 1; required string token = 2; }
-
Map類型:protobuf 支持定義 map 類型的成員
1. key_type:必須是string或者int 2. value_type:任意類型 map<key_type, value_type> map_field = N; // 舉例:map<string, Project> projects = 3;
-
使用 map 要注意:
-
Map 類型不能使 repeated
-
Map 是無序的
-
以文本格式展示時,Map 以 key 來排序
-
如果有相同的鍵會導致解析失敗
-
變量默認值
- 當解析 message 時,如果被編碼的 message 裏沒有包含某些變量,那麼根據類型不同,他們會有不同的默認值,收到數據後反序列化後,對於標準值類型的數據,比如bool,如果它的值是 false,那麼我們無法判斷這個值是對方設置的,還是對方就沒設置這個值。
string
:默認是空的字符串byte
:默認是空的bytesbool
:默認爲falsenumeric
:默認爲0enums
:定義在第一位的枚舉值,也就是0messages
:根據生成的不同語言有不同的表現,參考generated code guide
Tag
- 每一個變量在message內都需要自定義一個唯一的數字Tag,protobuf會根據Tag從數據中查找變量對應的位置,Tag一旦指定,以後更新協議的時候也不能修改,否則無法對舊版本兼容
- Tag的取值範圍最小是
1
,最大是229229-1
,但19000~19999
是 protobuf 預留的,用戶不能使用 - 不同 Tag 會對 protobuf 編碼帶來一些影響
- 1 ~ 15:單字節編碼,使用頻率高的變量最好設置爲1 ~ 15,這樣可以減少編碼後的數據大小,由於Tag一旦指定不能修改,所以爲了以後擴展,也記得爲未來保留一些 1 ~ 15 的 Tag
- 16 ~ 2047:雙字節編碼
變量規則
- 當構建 message 的時候,build 數據的時候,會檢測設置的數據跟規則是否匹配。在 proto3 中,可以給變量指定以下兩個規則
singular
:0或者1個,但不能多於1個repeated
:任意數量(包括0)
- 在proto2中,規則爲:
required
:必須有一個optional
:0或者1個repeated
:任意數量(包括0)
廢棄變量
-
可以用
reserved
關鍵字,當一個變量不再使用的時候,我們可以把它的變量名或 Tag 用reserved
標註,這樣,當這個 Tag 或者變量名字被重新使用的時候,編譯器會報錯message Foo { // 注意,同一個 reserved 語句不能同時包含變量名和 Tag reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
枚舉
-
定義枚舉並使用
message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4; }
-
枚舉定義在一個消息內部或消息外部都是可以的,如果枚舉是定義在 message 內部,而其他 message 又想使用,那麼可以通過
MessageType.EnumType
的方式引用。定義枚舉的時候,要保證第一個枚舉值必須是0,枚舉值不能重複,除非使用 option allow_alias = true 選項來開啓別名enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; }
-
枚舉數常量必須在32位整數的範圍內。由於enum值在傳輸中使用不同的編碼,負值效率低下,因此不推薦使用
引用其他的proto文件
-
import
A ->import B B -> import C A不能使用C中的內容,A可以使用B的內容,B可以使用C的內容
-
import public
A ->import B B -> import C A可以使用C中的內容,A可以使用B的內容,B可以使用C的內容
packages
-
爲了防止不同消息之間的命名衝突,你可以對特定的.proto文件指定 package 名字。在定義消息的成員的時候,可以指定包的名字:
package foo.bar; message Open { ... } message Foo { ... // 帶上包名 foo.bar.Open open = 1; ... }
JSON映射
-
Proto3 支持JSON的編碼規範,可實現protobuf與json互相轉換。
- 如果JSON編碼的數據丟失或者其本身就是
null
,這個數據會在解析成protocol buffer的時候被表示成默認值。 - 如果一個字段在協議緩衝區中具有默認值,默認情況下它將在 JSON 編碼數據中省略以節省空間
- 如果JSON編碼的數據丟失或者其本身就是
-
映射表
message object {“fooBar”: v, “g”: null, …} 生成JSON對象。消息字段名映射到lowerCamelCase,成爲JSON對象鍵。如果指定了JSON_name字段選項,則指定的值將被用作密鑰。解析器接受lowerCamelCase名稱(或JSON_name選項指定的名稱)和原始的原域名稱。null是所有字段類型的接受值,並被視爲相應字段類型的默認值。 enum string “FOO_BAR” 使用proto中指定的枚舉值的名稱。 map<K,V> object {“k”: v, …} 所有鍵都轉換爲字符串。 repeated V array [v, …] null被接受爲空列表[]。 bool true, false true, false string string “Hello World!” bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+” JSON值使用標準base64編碼和paddings編碼作爲字符串編碼的數據。 int32, fixed32, uint32 number 1, -10, 0 JSON值將是十進制數。接受數字或字符串。 int64, fixed64, uint64 string “1”, “-10” JSON值將是十進制數。接受數字或字符串。 float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值將是一個數字或特殊字符串值"NaN"、“Infinity"和”-Infinity"之一。接受數字或字符串。指數記數法也被接受。 Any object {"@type": “url”, “f”: v, … } 如果Any包含具有特殊JSON映射的值,它將被轉換如下: {"@ type": XXX,“value”: yyy }。否則,該值將被轉換成JSON對象,並且“@ type”字段將被插入以指示實際的數據類型。 Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 3339,其中生成的輸出總是Z歸一化的,並使用0、3、6或9個小數位數。除“Z”之外的偏移也是可以接受的。 Duration string “1.000340012s”, “1s” 根據所需精度,生成的輸出總是包含0、3、6或9個小數位數,後跟後綴"s"。接受任何小數位數(也沒有),只要它們符合毫微秒精度,並且後綴"s"是必需的。 Struct object { … } 任何JSON對象都可以。 Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包裝器在JSON中使用與包裝基元類型相同的表示,只是在數據轉換和傳輸期間允許並保留null。 FieldMask string “f.fooBar,h” 請參見字段mask.proto ListValue array [foo, bar, …] Value value NullValue null JSON null -
proto3 的 JSON 實現中提供了以下 4 中 options:
- 使用默認值發送字段:在默認情況下,默認值的字段在Proto3 JSON 輸出中被忽略。一個實現可以提供一個選項來覆蓋這個行爲,並使用它們的默認值輸出字段。
- 忽略未知字段:默認情況下,Proto3 JSON 解析器應拒絕未知字段,但可能提供一個選項來忽略解析中的未知字段。
- 使用 proto 字段名稱而不是 lowerCamelCase 名稱:默認情況下,proto3 JSON 的 printer 將字段名稱轉換爲 lowerCamelCase 並將其用作 JSON 名稱。實現可能會提供一個選項,將原始字段名稱用作 JSON 名稱。 Proto3 JSON 解析器需要接受轉換後的 lowerCamelCase 名稱和原始字段名稱。
- 發送枚舉形式的枚舉值而不是字符串:在 JSON 輸出中默認使用枚舉值的名稱。可以提供一個選項來使用枚舉值的數值。
-
protocol buffers 替換 JSON,可能是考慮到:
- protocol buffers 相同數據,傳輸的數據量比 JSON 小,gzip 或者 7zip 壓縮以後,網絡傳輸消耗較少
- protocol buffers 不是自我描述的,在缺少
.proto
文件以後,有一定的加密性,數據傳輸過程中都是二進制流,並不是明文。 - protocol buffers 提供了一套工具,自動化生成代碼也非常方便
- protocol buffers 具有向後兼容性,改變了數據結構以後,對老的版本沒有影響
- protocol buffers 原生完美兼容 RPC 調用
-
如果很少用到整型數字,浮點型數字,全部都是字符串數據,那麼 JSON 和 protocol buffers 性能不會差太多
-
案例
1. 加入依賴 compile group: 'com.google.protobuf', name: 'protobuf-java-util', version: '3.10.0' 2. json測試 public class UserTest { private static final Logger logger = LoggerFactory.getLogger(UserTest.class); public static void main(String[] args) throws IOException { User.UserProto.Builder user = User.UserProto.newBuilder(); user.setAge(12); user.setId(1000); user.setName("jannal"); User.UserProto userInfo = user.build(); // 將數據寫到輸出流 ByteArrayOutputStream output = new ByteArrayOutputStream(); userInfo.writeTo(output); // 將數據序列化後發送 byte[] byteArray = output.toByteArray(); // 接收到流並讀取 ByteArrayInputStream input = new ByteArrayInputStream(byteArray); // 反序列化 userInfo = User.UserProto.parseFrom(input); /** * id: 1000 * name: "jannal" * age: 12 */ logger.info("{}", userInfo.toString()); //如果不使用protobuf提供的JSON API,而使用fastJson等,直接序列化Msg對象,會報錯。 // 如果希望使用第三方的JSON API,可以重新定義一個實體類,抽取需要的字段 //獲取Printer對象用於生成JSON字符串 JsonFormat.Printer printer = JsonFormat.printer(); //獲取parser對象用於解析JSON字符串 JsonFormat.Parser parser = JsonFormat.parser(); try { //生成JSON字符串 String jsonStr = printer.print(userInfo); System.out.println(jsonStr); //解析JSON字符串 //解析方法接收一個JSON字符串,並把其寫入指定的builder User.UserProto.Builder builder = User.UserProto.newBuilder(); parser.merge(jsonStr, builder); User.UserProto userProto = builder.build(); System.out.println(userProto); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } }
升級proto的原則
-
升級更改 proto 需要遵循以下原則
-
不要修改任何已存在的變量的 Tag
-
如果新增了變量,新生成的代碼依然能解析舊的數據,但新增的變量將會變成默認值。相應的,新代碼序列化的數據也能被舊的代碼解析,但舊代碼會自動忽略新增的變量。
-
廢棄不用的變量用
reserved
標註 -
int32、 uint32、 int64、 uint64 和 bool 是相互兼容的
,這意味你可以更改這些變量的類型而不會影響兼容性 -
sint32 和 sint64
是兼容的,但跟其他類型不兼容 -
string 和 bytes
可以兼容,前提是他們都是UTF-8編碼的數據 -
fixed32 和 sfixed32
是兼容的 -
fixed64 和 sfixed64
是兼容的
-
定義RPC Service
-
如果要使用 RPC(遠程過程調用)系統的消息類型,可以在
.proto
文件中定義 RPC 服務接口,protocol buffer 編譯器將使用所選語言生成服務接口代碼和 stubs。所以,例如,如果你定義一個 RPC 服務,入參是 SearchRequest 返回值是 SearchResponse,你可以在你的.proto
文件中定義它,如下所示:service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }
-
與 protocol buffer 一起使用的最直接的 RPC 系統是gRPC:在谷歌開發的語言和平臺中立的開源 RPC 系統。gRPC 在 protocol buffer 中工作得非常好,並且允許你通過使用特殊的 protocol buffer 編譯插件,直接從
.proto
文件中生成 RPC 相關的代碼。
命名規範
-
message 採用駝峯命名法。message 首字母大寫開頭。字段名採用下劃線分隔法命名。
message SongServerRequest { required string song_name = 1; }
-
枚舉類型採用駝峯命名法。枚舉類型首字母大寫開頭。每個枚舉值全部大寫,並且採用下劃線分隔法命名。
enum Foo { FIRST_VALUE = 0; SECOND_VALUE = 1; } 每個枚舉值用分號結束,不是逗號。
-
服務名和方法名都採用駝峯命名法。並且首字母都大寫開頭。
service FooService { rpc GetSomething(FooRequest) returns (FooResponse); }
遇到的問題
-
默認一個proto文件生成一個java文件,這樣如果proto比較長,java文件會比較大。在proto中添加
option java_multiple_files = true
的配置可以生成多個java文件syntax = "proto3"; option java_package = "cn.jannal.protobuf"; //生成的java名 option java_outer_classname = "PersonProto"; //生成多個java文件 option java_multiple_files = true; message Person{ string username = 1; string password = 2; } message Student{ string username = 1; string password = 2; }