protobuf概述
message
定義message結構
保留Filed和保留Filed number
枚舉類型
引用其它message類
message擴展
數據類型對應關系
編碼規則
可變長整數編碼
有符號整數編碼
定長編碼
代碼生成
下載安裝protobuf
生成代碼
方法1:使用cmd
方法2:使用java調用cmd
方法3:使用pom生成java類
使用
引入protobuf
使用builder
idea使用protobuf
添加protobuf支持
01protobuf基礎
參考:
- https://www.jianshu.com/p/419efe983cb2
- https://blog.csdn.net/Erica_1230/article/details/78746757
- https://blog.csdn.net/q2365921/article/details/52672346
- https://blog.csdn.net/antgan/article/details/52103966
- https://www.cnblogs.com/liugh/p/7505533.html
- https://blog.csdn.net/cb2474600377/article/details/49511873
protobuf概述
protobuf是google團隊開發的用於高效存儲和讀取結構化數據的工具。什么是結構化數據呢,正如字面上表達的,就是帶有一定結構的數據。比如電話簿上有很多記錄數據,每條記錄包含姓名、ID、郵件、電話等,這種結構重復出現。
xml、json也可以用來存儲此類結構化數據,但是使用protobuf表示的數據能更加高效,並且將數據壓縮得更小,大約是json格式的1/10,xml格式的1/20。
它是一種輕便高效的數據格式,平台無關、語言無關、可擴展,可用於通訊協議和數據存儲等領域。
- 優點
- 平台無關,語言無關,可擴展;
- 提供了友好的動態庫,使用簡單;
- 解析速度快,比對應的XML快約20-100倍;
- 序列化數據非常簡潔、緊湊,與XML相比,其序列化之后的數據量約為1/3到1/10。
- 缺點
- 不適合用於對基於文本的標記文檔(如HTML)建模,因為文本不適合描述數據結構
- 通用性較差:Json, XML已經成為多種行業標准的編寫工具,而Protobuf只是Google公司內部使用的工具
- 自解釋性差:以二進制數據流方式存儲(不可讀) ,需要通過.proto文件才能了解到數據結構
message
定義message結構
protobuf使用message,類似class文件,
例如:
message Person {
// ID(必需)
required int32 id = 1;[default = 0]
// 姓名(必需)
required string name = 2;
// email(可選)
optional string email = 3;[default = ""]
// 朋友(集合)
repeated string friends = 4 [packed=true];
}
其中Person是message
這種結構的名稱,name、id、email是其中的Field,每個Field保存着一種數據類型,后面的1、2、3是Filed對應的數字id。id在1~15之間編碼只需要占一個字節,包括Filed數據類型和Filed對應數字id,在16~2047之間編碼需要占兩個字節,所以最常用的數據對應id要盡量小一些。
optional
后面可以加default默認值,如果不加,數據類型的默認為0[default = 0],字符串類型的默認為空串[default = ""]。
repeated
后面加[packed=true]會使用新的更高效的編碼方式。
注意:使用required規則的時候要謹慎,因為以后結構若發生更改,這個Filed若被刪除的話將可能導致兼容性的問題。
syntax = "proto3";
package net.cc.luffy.entity.proto;//指定java的包名,生成java之后的包路徑
//option java_package = "net.cc.luffy.entity.proto"; \\指定java的報名
option java_outer_classname = "UpDownProto";//指定java的編譯前類名,生成java之后,java文件交
// 起降記錄
message UpDown {
// 起降記錄ID
fixed64 id = 1;
// 設備ID
string deviceId = 2;
// 用戶ID
fixed64 usrId = 3;
// 廠商ID
string mid = 4;
// 起飛時間
fixed64 upTime = 5;
// 降落時間
fixed64 downTime = 6;
// 飛行狀態
int32 flyStatus = 7;
// 是否刪除
bool isDelete = 8;
// 日志跟蹤ID
string traceId = 9;
// 創建時間
fixed64 createdate = 10;
// 平均速度
double avgSpeed = 11;
// 平均高度
double avgHeight = 12;
// 最大速度
double maxSpeed = 13;
// 最大高度
double maxHeight = 14;
// 最小速度
double minSpeed = 15;
// 最小高度
double minHeight = 16;
// 開關機記錄ID
fixed64 onOffId = 17;
}
保留Filed和保留Filed number
每個Filed對應唯一的數字id,但是如果該結構在之后的版本中某個Filed刪除了,為了保持向前兼容性,需要將一些id或名稱設置為保留的,即不能被用來定義新的Field。
實質:已經定義的數字id和字段為了兼容性,之后不能重復使用,若是對應的字段刪除,就記錄下來,以后不再使用
message Person {
reserved 2, 15, 9 to 11;//排除的id
reserved "samples", "email";//排除的字段
}
枚舉類型
比如電話號碼,只有移動電話、家庭電話、工作電話三種,因此枚舉作為選項,如果沒設置的話枚舉類型的默認值為第一項。
在上面的例子中在個人message中加入電話號碼這個Filed。如果枚舉類型中有不同的名字對應相同的數字id,需要加入option allow_alias = true
這一項,否則會報錯。枚舉類型中也有reserverd Filed和number,定義和message中一樣。
message Person {
//普通參數
required string name = 1;
required int32 id = 2;
optional string email = 3;
//枚舉--手機號類型(移動電話、家庭電話、工作電話)
enum PhoneType {
//allow_alias = true;
MOBILE = 0;
HOME = 1;
WORK = 2;
}
//定義手機號信息
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];//默認是家庭電話
}
//手機號字段
repeated PhoneNumber phones = 4;
}
引用其它message類
- 在同一個文件中,可以直接引用定義過的message類型。
- 在一個message類型中嵌套定義其它的message類型
- 在同一個項目中,可以用
import
來導入其它message類型。
import "myproject/other_protos.proto";
message擴展
message Person {
// ...
extensions 100 to 199;//擴展允許的id范圍
}
在另一個文件中,import 這個proto之后,可以對Person這個message進行擴展。
extend Person {
optional int32 bar = 126;
}
數據類型對應關系
在使用規則創建proto類型的數據結構文件之后,會將其轉化成對應編程語言中的頭文件或者類定義。
proto中的數據類型和c++,Python中的數據類型對應規則如下:
.proto | C++ | Python | java | 介紹 |
---|---|---|---|---|
double | double | float | double | |
float | float | float | float | |
int32 | int32 | int | int | 使用可變長編碼方式。編碼負數時不夠高效——如果你的字段可能含有負數,那么請使用sint32。 |
int64 | int64 | int/long | long | 使用可變長編碼方式。編碼負數時不夠高效——如果你的字段可能含有負數,那么請使用sint64。 |
uint32 | uint32 | int/long | int | Uses variable-length encoding. |
uint64 | uint64 | int/long | long | Uses variable-length encoding. |
sint32 | int32 | int | int | 使用可變長編碼方式。有符號的整型值。編碼時比通常的int32高效。 |
sint64 | int64 | int/long | long | 使用可變長編碼方式。有符號的整型值。編碼時比通常的int64高效。 |
fixed32 | uint32 | int/long | int | 總是4個字節。如果數值總是比總是比228大的話,這個類型會比uint32高效。 |
fixed64 | uint64 | int/long | long | 總是8個字節。如果數值總是比總是比256大的話,這個類型會比uint64高效。. |
sfixed32 | int32 | int | int | 總是4個字節。 |
sfixed64 | int64 | int/long | long | 總是8個字節。 |
bool | bool | bool | boolean | |
string | string | str/unicode | String | 一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 |
bytes | string | str | ByteString | 可能包含任意順序的字節數據。 |
編碼規則
protobuf有一套高效的數據編碼規則。
可變長整數編碼
每個字節有8bits,其中第一個bit是most significant bit(msb),0表示結束,1表示還要讀接下來的字節。
對message中每個Filed來說,需要編碼它的數據類型、對應id以及具體數據。
數據類型有以下6種,可以用3個bits表示。每個整數編碼用最后3個bits表示數據類型。所以,對應id在1~15之間的Filed,可以用1個字節編碼數據類型、對應id。
Type | Meaning | Used For | |
---|---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum | |
1 | 64-bit | fixed64, sfixed64, double | |
2 | Length-delimited | string, bytes, embedded messages, packed repeated | fields |
3 | Start group | groups (deprecated) | |
4 | End group | groups (deprecated) | |
5 | 32-bit | fixed32, sfixed32, float |
比如對於下面這個例子來說,如果給a賦值150,那么最終得到的編碼是什么呢?
message Test {
optional int32 a = 1;
}
首先數據類型編碼是000,因此和id聯合起來的編碼是00001000. 然后值150的編碼是1 0010110,采用小端序交換位置,即0010110 0000001,前面補1后面補0,即10010110 00000001,即96 01,加上最前面的數據類型編碼字節,總的編碼為08 96 01。
有符號整數編碼
如果用int32來保存一個負數,結果總是有10個字節長度,被看做是一個非常大的無符號整數。使用有符號類型會更高效。它使用一種ZigZag
的方式進行編碼。即-1編碼成1,1編碼成2,-2編碼成3這種形式。
也就是說,對於sint32來說,n編碼成 (n << 1) ^ (n >> 31),注意到第二個移位是算法移位。
定長編碼
定長編碼是比較簡單的情況。
代碼生成
下載安裝protobuf
- 在https://github.com/protocolbuffers/protobuf/releases 下載合適的版本
例如,我下載了protoc-3.9.0-win32.zip - 將解壓出來的protoc.exe放在一全英文路徑下,並把其路徑名放在windows環境變量下的
path
下,同時添加proto_path
,值為protoc.exe的路徑
生成代碼
方法1:使用cmd
在所使用的proto文件路徑下打開cmd窗口執行以下命令
```java
protoc -I=源地址 --java_out=目標地址 源地址/xxx.proto
注:此處生成時會以proto里面注明的java_package為路徑完整生成,所以目標地址不必包含java_package及之后的路徑,比如: `option java_package = "com.test.protocol"; `那么就會生成`com/test/protocol/XXX.java`
- -I選項,主要用於指定待編譯的.proto消息定義文件所在的目錄,即可能出現的包含文件的路徑,該選項可以被同時指定多個。此處指定的路徑不能為空,如果是當前目錄,直接使用.,如果是子目錄,直接使用子目錄相對徑,如:foo/bar/baz,如果要編譯的文件import指定的文件路徑為baz/test.proto,那么應這么寫-I=foo/bar,而不要一直寫到baz。
示例命令如下:
```java
protoc -I=. --java_out=../../../../ beans/*.proto apis/*.proto *.proto
方法2:使用java調用cmd
/**
* protoc.exe
* @author ganhaibin
*
*/
public class GenerateClass {
public static void main(String[] args) {
String protoFile = "person-entity.proto";//
String strCmd = "d:/dev/protobuf-master/src/protoc.exe -I=./proto --java_out=./src/main/java ./proto/"+ protoFile;
try {
Runtime.getRuntime().exec(strCmd);
} catch (IOException e) {
e.printStackTrace();
}//通過執行cmd命令調用protoc.exe程序
}
}
方法3:使用pom生成java類
<!--版本-->
<properties>
<grpc.version>1.6.1</grpc.version>
<protobuf.version>3.3.0</protobuf.version>
</properties>
<!--依賴-->
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>
<!--build配置-->
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 在
src/main/proto
文件夾中添加protobuf的文件; - 右鍵
proto
,將文件夾設置為源碼目錄 - 點擊
maven projects
中Plugins
--protobuf
--protobuf:compile
- 即可在
target/generated-sources/protobuf
中看到根據.proto文件生成的Java類 ;
使用
引入protobuf
- maven項目,引入pom依賴
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
</dependency>
- 普通項目需要引入protobuf-java-2.5.0.jar文件
使用builder
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
// 按照定義的數據結構,創建一個Person
PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();
personBuilder.setId(1);
personBuilder.setName("叉叉哥");
personBuilder.setEmail("xxg@163.com");
personBuilder.addFriends("Friend A");
personBuilder.addFriends("Friend B");
PersonMsg.Person xxg = personBuilder.build();
// 將數據寫到輸出流,如網絡輸出流,這里就用ByteArrayOutputStream來代替
ByteArrayOutputStream output = new ByteArrayOutputStream();
xxg.writeTo(output);
// -------------- 分割線:上面是發送方,將數據序列化后發送 ---------------
byte[] byteArray = output.toByteArray();
// -------------- 分割線:下面是接收方,將數據接收后反序列化 ---------------
// 接收到流並讀取,如網絡輸入流,這里用ByteArrayInputStream來代替
ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
// 反序列化
PersonMsg.Person xxg2 = PersonMsg.Person.parseFrom(input);
System.out.println("ID:" + xxg2.getId());
System.out.println("name:" + xxg2.getName());
System.out.println("email:" + xxg2.getEmail());
System.out.println("friend:");
List<String> friends = xxg2.getFriendsList();
for(String friend : friends) {
System.out.println(friend);
}
}
}
也可以:
序列化:byte[] bytes=personBuilder.build().toByteArray();
反序列化:PersonMsg.Person person=PersonMsg.Person.parseFrom(bytes);
可以直接 person.getId();等操作
注意:protobuf不是json,若是使用json解析會拋出異常
idea使用protobuf
添加protobuf支持
安裝插件Protobuf Support