Probotbuf簡介
在網絡通信和通用數據交換等應用場景中經常使用的技術是 JSON 或 XML,這兩種技術常被用于數據的結構化呈現和序列化。我們可以從兩個方面來看JSON 和 XML與protobuf的異同:一個是數據結構化,一個是數據序列化。這里的數據結構化主要面向開發或業務層面,數據序列化面向通信或存儲層面,當然數據序列化也需要“結構”和“格式”,所以這兩者之間的區別主要在于面向領域和場景不同,一般要求和側重點也會有所不同。數據結構化側重人類可讀性甚至有時會強調語義表達能力,而數據序列化側重效率和壓縮。
JSON、XML 同樣也可以直接被用來數據序列化,實際上很多時候它們也是這么被使用的,例如直接采用 JSON、XML 進行網絡通信傳輸,此時 JSON、XML 就成了一種序列化格式,它發揮了數據序列化的能力。但是經常這么被使用,不代表這么做就是合理。實際將 JSON、XML 直接作用數據序列化通常并不是最優選擇,因為它們在速度、效率、空間上并不是最優。換句話說它們更適合數據結構化而非數據序列化。
扯完 XML 和 JSON,我們來看看 ProtoBuf,同樣的 ProtoBuf 也具有數據結構化的能力,其實也就是上面介紹的 message 定義。我們能夠在 .proto 文件中,通過 message、import、內嵌 message 等語法來實現數據結構化,但是很容易能夠看出,ProtoBuf 在數據結構化方面和 XML、JSON 相差較大,人類可讀性較差,不適合上面提到的 XML、JSON 的一些應用場景。
但是如果從數據序列化的角度你會發現 ProtoBuf 有著明顯的優勢,效率、速度、空間幾乎全面占優,看完后面的 ProtoBuf 編碼的文章,你更會了解 ProtoBuf 是如何極盡所能的壓榨每一寸空間和性能,而其中的編碼原理正是 ProtoBuf 的關鍵所在,message 的表達能力并不是 ProtoBuf 最關鍵的重點。所以可以看出ProtoBuf重點側重于數據序列化而非數據結構化。
最終對這些個人思考做一些小小的總結:
XML、JSON、ProtoBuf 都具有數據結構化和數據序列化的能力
XML、JSON 更注重數據結構化,關注人類可讀性和語義表達能力。ProtoBuf 更注重數據序列化,關注效率、空間、速度,人類可讀性差,語義表達能力不足(為保證極致的效率,會舍棄一部分元信息)
ProtoBuf 的應用場景更為明確,XML、JSON 的應用場景更為豐富。
我們先來看看官方文檔給出的定義和描述:
protocol buffers 是一種語言無關、平臺無關、可擴展的序列化結構數據的方法,它可用于(數據)通信協議、數據存儲等。
Protocol Buffers 是一種靈活,高效,自動化機制的結構數據序列化方法-可類比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更為簡單。
你可以定義數據的結構,然后使用特殊生成的源代碼輕松的在各種數據流中使用各種語言進行編寫和讀取結構數據。你甚至可以更新數據結構,而不破壞由舊數據結構編譯的已部署程序。
簡單來講, ProtoBuf 是結構數據序列化[1] 方法,可簡單類比于 XML[2],其具有以下特點:
語言無關、平臺無關。即 ProtoBuf 支持 Java、C++、Python 等多種語言,支持多個平臺
高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更為簡單
擴展性、兼容性好。你可以更新數據結構,而不影響和破壞原有的舊程序
protobuf3的變化
默認值
protobuf3 刪除了 protobuf2 中用來設置默認值的 default 關鍵字,取而代之的是protobuf3為各類型定義的默認值,也就是約定的默認值,如下表所示:
類型
默認值
bool
false
整形
0
string
空字符串 ""
enum
第一個枚舉元素的值,因為Protobuf3強制要求第一個枚舉元素的值必須是0,所以枚舉的默認值就是0
message
不是null,而是DEFAULT_INSTANCE
可以看出來,protobuf3定義的默認值跟Java中類的屬性的默認值規則并不一樣:Java中,如果類的屬性類型是類,則該屬性默認值是null,而protobuf3中,string、message的默認值都不是null。
枚舉類型
不支持一個proto文件中,多個枚舉中定義相同的枚舉常量名
如下的兩個枚舉,定義在同一個proto文件中:
enum Enum1 {
IDLE = 0;
RUNNING = 1;
}
enum Enum2 {
IDLE = 5;
RUNNING = 6;
}
編譯時,會報出錯誤:IDLE is already defined in "xxx",出現這一錯誤的原因就是:Protobuf3中不允許同一proto中,多個枚舉中使用相同的枚舉值。
枚舉第一個常量的值必須是0
message類型
Java中,message類型的默認值是DEFAULT_INSTANCE,其值相當于空的message,即XXX.newBuilder().build(),這樣對message類型的判空操作就應該是這樣:
// protobuf message
message User {
int32 id = 1;
string name = 2;
string email = 3;
Address address = 4;
}
message Address {
string street = 1;
string building = 2;
}
// Java
if (user.getAddress() != null && user.getAddress() != UserProto.Address.getDefaultInstance()) {
...
} else {
...
}
Protobuf數據類型
基礎類型
proto type
描述
java type
double
雙精度
double
float
單精度
float
int32
32位整數,可變長度,編碼負數效率低,編碼負數推薦使用sint32
int
int64
64位整數,可變長度,編碼負數效率低,編碼負數推薦使用sint64
long
uint32
32位無符號整數,存儲正數時與int32一致,用的較少,一般用int32,長度可變
int
uint64
64位無符號整數,存儲正數與int64一致,用的較少,一般用int64,長度可變
long
sint32
有符號32位,長度可變,編碼負數效率高
int
sint64
有符號64位,長度可變,編碼負數效率高,存儲正數時不推薦使用
long
fixed32
固定4個字節的長度,比uint32更有效率,存儲正數時不推薦使用
int
fixed64
固定8個字節的長度,比uint64更有效率
long
sfixed32
有符號整數,固定4個字節
int
sfixed64
有符號整數,固定8個字節
long
bool
布爾值
boolean
string
字符串
String
bytes
字節數組
ByteString
枚舉類型
枚舉類型中必須包含至少一個元素,并且元素的編號必須從0開始。因為如果沒有設置值的話,可以使用0作為默認值。
定義消息體
syntax = "proto3";
option java_package = "com.ray.protobufdemo";
option java_outer_classname = "StudentProto3";
message Student {
string username = 1;
string password = 2;
string email = 3;
sint32 age = 4;
int64 timeSpane = 5;
double value = 6;
Address address = 7;
enum Gender {
MALE = 0;
FEMALE = 1;
}
Gender gender = 8;
}
message Address {
string province = 1;
string city = 2;
// 相當于java中的List
repeated string area = 3;
}
測試demo
StudentProto3.Student studentProto = StudentProto3.Student.newBuilder()
.setUsername("admin")
.setPassword("123456")
.setEmail("3306@qq.com")
.setValue(Double.MAX_VALUE)
// .setAge(Integer.MAX_VALUE)
.setAge(-2)
.setTimeSpane(System.currentTimeMillis())
.setAddress(address)
.setGender(StudentProto3.Student.Gender.MALE)
.build();
嵌套消息類型
Student.proto
syntax = "proto3";
option java_package = "com.ray.protobufdemo";
option java_outer_classname = "StudentProto3";
message Student {
string username = 1;
string password = 2;
string email = 3;
sint32 age = 4;
int64 timeSpane = 5;
double value = 6;
Address address = 7;
}
message Address {
string province = 1;
string city = 2;
// 相當于java中的List
repeated string area = 3;
}
StudentPtoto3Demo.java
// 使用protobuf3序列化
StudentProto3.Address address = StudentProto3.Address.newBuilder()
.setProvince("北京")
.setCity("北京")
.addArea("chaoyang")
.addArea("miyun")
.build();
StudentProto3.Student studentProto = StudentProto3.Student.newBuilder()
.setUsername("admin")
.setPassword("123456")
.setEmail("3306@qq.com")
.setValue(Double.MAX_VALUE)
// .setAge(Integer.MAX_VALUE)
.setAge(-2)
.setTimeSpane(System.currentTimeMillis())
.setAddress(address)
.build();
System.out.println(studentProto);
System.out.println(studentProto.toByteArray().length);
repeated類型
repeated相當于java中的List類型,在其內部定義的類型可以是任意的。
reserved類型
當定義文件中的一些字段需要移除,最好不要直接刪除,而是使用reserved標記要刪除的字段,如果有人使用了被標記刪除的字段,編譯器會報錯。有兩種標記刪除方式:
根據字段順序標記
message Demo {
reserved 2, 5, 9 to 11 // 字段順序為2、5,以及9到11的標記為刪除
}
根據字段名稱標記
message Demo {
reserved "name", "age" // 字段名稱為name和age的被標記為刪除
}
Map類型
在ProtoBuf中可以定義Map類型,語法如下:
map map_field = N;
key_type可以是其他的Message類型,string類型,PB類型定義表中(scalar value type)除了浮點類型和byte字節類型以外的其他類型。
需要注意以下幾點:
枚舉類型不能夠作為key_type,value_type可以是除了Map以外的其他任何類型
map不能定義為repeated類型
map不保證順序
定義proto文件
syntax = "proto3";
option java_package = "com.ray.protobufdemo";
option java_outer_classname = "MapProto3";
message MapPerson {
map projects = 1;
}
message Project {
string name = 1;
int32 age = 2;
}
測試demo
public class MapProtoDemo {
public static void main(String[] args) {
MapProto3.Project p1 = MapProto3.Project.newBuilder()
.setName("neo")
.setAge(22)
.build();
MapProto3.Project p2 = MapProto3.Project.newBuilder()
.setName("mary")
.setAge(33)
.build();
MapProto3.MapPerson student = MapProto3.MapPerson.newBuilder()
.putProjects("student", p1)
.putProjects("teacher", p2)
.build();
System.out.println(student);
}
}
oneof類型
oneof關鍵字內部可以定義多個field,在使用的時候只能設置一個值。
定義消息體
syntax = "proto3";
option java_package = "com.ray.protobufdemo";
option java_outer_classname = "OneOfProto3";
message Test1 {
string name = 1;
int32 age = 2;
oneof test_oneof {
Request req = 3;
Response rep = 4;
}
}
message Request {
string req = 1;
}
message Response {
string rep = 1;
}
測試demo
public class OneOfProtoDemo {
public static void main(String[] args) throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException {
OneOfProto3.Request request = OneOfProto3.Request.newBuilder()
.setReq("request")
.build();
OneOfProto3.Response response = OneOfProto3.Response.newBuilder()
.setRsp("response")
.build();
OneOfProto3.Test1 neo = OneOfProto3.Test1.newBuilder()
.setName("neo")
.setAge(22)
// rep和rsp只能設置其中的一個
// .setReq(request)
.setRsp(response)
.build();
System.out.println(neo);
OneOfProto3.Test1 test1 = OneOfProto3.Test1.parseFrom(neo.toByteArray());
}
}
package包定義
Package包定義,可以防止Message重名問題,類似于java中的包。在java中使用Protobuf的包,有以下兩種方式:
使用package關鍵字定義protobuf的模板消息包名,這種方式可以在多個語言中使用
syntax = "proto3";
package bar.foo;
option java_outer_classname = "OneOfProto3";
message PackageProto {
string name = 1;
int32 age = 2;
}
我們在使用的時候需要如下做:
package com.ray.protobufdemo.entity;
// 導入package處聲明的包
import bar.foo.OneOfProto3;
public class PackageProtoDemo {
public static void main(String[] args) {
OneOfProto3.PackageProto.Builder builder = OneOfProto3.PackageProto.newBuilder();
}
}
使用option java_package語句聲明java的包名,該用法是java獨有的,如果不定義則使用package中的包路徑
option java_package= "com.ray.protobufdemo";
import語法
在Protobuf中,不同的消息可以分別寫在不同的proto文件中,在使用的時候可以使用關鍵字import引用其他消息模板。
protobuf的使用
定義消息的格式
// 聲明使用proto3協議,如果不指定則默認使用proto2協議
syntax = "proto3";
option java_package = "com.ray.protobufdemo";
option java_outer_classname = "AddressBook";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
// 在proto3中,第一個枚舉值的序號必須為0
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
// 在AddressBook message中引用另一個message Person
repeated Person person = 1;
}
protobuf消息格式說明:Person消息定義指定了三個字段(名稱/值對),每一個字段對應于要包含在這種類型的消息中的數據。每個字段都有一個名稱和一個類型,以及一個序號。
指定字段類型
在上例中,所有字段都是標量類型:兩個整數(page_number和result_per_page)和一個字符串(query)。但是,您也可以為字段指定復合類型,包括枚舉和其他消息類型。
分配字段編號
如您所見,消息定義中的每個字段都有一個唯一的編號。這些字段編號用于以二進制格式標識您的字段,一旦您的消息類型被使用,就不應該被更改。請注意,1到15范圍內的字段編號需要一個字節來編碼,包括字段編號和字段類型(您可以在協議緩沖區編碼中找到更多信息)。16到2047范圍內的字段編號需要兩個字節。因此,您應該為經常出現的消息元素保留數字1到15。記住為將來可能添加的頻繁出現的元素留出一些空間。
那protobuf是怎么做到向前及向后兼容的呢?靠的就是這個字段的編號,在反序列化的時候,protobuf會從輸入流中讀取出字段編號,然后再設置message中對應的值。如果讀出來的字段編號是message中沒有的,就直接忽略,如果message中有字段編號是輸入流中沒有的,則該字段不會被設置。所以即使通信的兩端存在一方比另一方多出編號,也不會影響反序列化。但是如果兩端同一編號的字段規則或者字段類型不一樣,那就肯定會影響反序列化了。所以一般調整proto文件的時候,盡量選擇加字段或者刪字段,而不是修改字段編號或者字段類型。
您可以指定的最小字段編號為1,最大字段編號為229 - 1,即536,870,911。但是不能使用數字19000到19999 ( FieldDescriptor::kFirstReservedNumber 到FieldDescriptor::kLastReservedNumber),因為它們是為協議緩沖區實現而保留的-如果您在 .proto文件中使用這些保留的數字之一,協議緩沖區編譯器就會報錯。同樣,您也不能使用任何保留字段。
指定字段規則
消息字段可以是以下字段之一:
singular: 可以有零個或其中一個字段(但不超過一個)。
repeated: 該字段可以重復任意次數(包括零次)。重復值的順序將保留在Protocol Buffer中,將重復字段視為動態大小的數組。protobuf處理這個字段的時候,另外加了一個count計數變量,用于標明這個字段有多少個,這樣發送方發送的時候,同時發送了count計數變量和這個字段的起始地址,接收方在接受到數據之后,按照count來解析對應的數據即可。
在java中使用protobuf3
安裝idea插件
添加pom依賴
com.google.protobuf
protobuf-java
3.4.0
kr.motd.maven
os-maven-plugin
1.6.2
org.xolstice.maven.plugins
protobuf-maven-plugin
0.5.0
${project.basedir}/src/main/protobuf
com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}
compile
定義message
// user.proto
// 定義protobuf
syntax = "proto3";
option java_package = "com.ray.bigdata.protobuf";
// 指定生成的java類名
option java_outer_classname = "DemoModel";
message User {
int32 id = 1;
string name = 2;
string sex = 3;
}
測試demo
package com.ray.bigdata.canal;
import com.google.protobuf.InvalidProtocolBufferException;
import com.ray.bigdata.protobuf.DemoModel;
/**
* 使用protobuf進行數據的序列化和反序列化
*/
public class ProtobufDemo {
public static void main(String[] args) throws InvalidProtocolBufferException {
// 實例化protobuf對象
DemoModel.User.Builder builder = DemoModel.User.newBuilder();
// 給user對象進行賦值
builder.setId(1);
builder.setName("張三");
builder.setSex("男");
// 獲取user對象的屬性值
DemoModel.User userBuilder = builder.build();
System.out.println(userBuilder.getId());
System.out.println(userBuilder.getName());
System.out.println(userBuilder.getSex());
/**
* 數據的序列化和反序列化
* 序列化:可以將對象轉換成字節碼數據存儲到kafka中
* 反序列化:可以將kafka中的數據消費出來,轉換為java對象使用
*/
// 將一個對象序列化成二進制的字節碼數據存儲到kafka中
byte[] bytes = builder.build().toByteArray();
for (byte b: bytes) {
System.out.println(b);
}
// 將kafka中消費的數據反序列化
DemoModel.User user = DemoModel.User.parseFrom(bytes);
System.out.println(user);
System.out.println(user.getName());
System.out.println(user.getSex());
}
}