高效數(shù)據(jù)傳輸?shù)拿孛芪淦鱌rotobuf的使用教程
當涉及到網(wǎng)絡通信和數(shù)據(jù)存儲時,數(shù)據(jù)序列化一直都是一個重要的話題;特別是現(xiàn)在很多公司都在推行微服務,數(shù)據(jù)序列化更是重中之重,通常會選擇使用 JSON 作為數(shù)據(jù)交換格式,且 JSON 已經(jīng)成為業(yè)界的主流。但是 Google 這么大的公司使用的卻是一種被稱為 Protobuf 的數(shù)據(jù)交換格式,它是有什么優(yōu)勢嗎?這篇文章介紹 Protobuf 的相關知識。

GitHub:https://github.com/protocolbuffers/protobuf
官方文檔:https://protobuf.dev/overview/
Protobuf 介紹
Protobuf(Protocol Buffers)是由 Google 開發(fā)的一種輕量級、高效的數(shù)據(jù)交換格式,它被用于結構化數(shù)據(jù)的序列化、反序列化和傳輸。相比于 XML 和 JSON 等文本格式,Protobuf 具有更小的數(shù)據(jù)體積、更快的解析速度和更強的可擴展性。
Protobuf 的核心思想是使用協(xié)議(Protocol)來定義數(shù)據(jù)的結構和編碼方式。使用 Protobuf,可以先定義數(shù)據(jù)的結構和各字段的類型、字段等信息,然后使用Protobuf提供的編譯器生成對應的代碼,用于序列化和反序列化數(shù)據(jù)。由于 Protobuf 是基于二進制編碼的,因此可以在數(shù)據(jù)傳輸和存儲中實現(xiàn)更高效的數(shù)據(jù)交換,同時也可以跨語言使用。

相比于 XML 和 JSON,Protobuf 有以下幾個優(yōu)勢:
- 更小的數(shù)據(jù)量:Protobuf 的二進制編碼通常比 XML 和 JSON 小 3-10 倍,因此在網(wǎng)絡傳輸和存儲數(shù)據(jù)時可以節(jié)省帶寬和存儲空間。
- 更快的序列化和反序列化速度:由于 Protobuf 使用二進制格式,所以序列化和反序列化速度比 XML 和 JSON 快得多。
- 跨語言:Protobuf 支持多種編程語言,可以使用不同的編程語言來編寫客戶端和服務端。這種跨語言的特性使得 Protobuf 受到很多開發(fā)者的歡迎(JSON 也是如此)。
- 易于維護可擴展:Protobuf 使用 .proto 文件定義數(shù)據(jù)模型和數(shù)據(jù)格式,這種文件比 XML 和 JSON 更容易閱讀和維護,且可以在不破壞原有協(xié)議的基礎上,輕松添加或刪除字段,實現(xiàn)版本升級和兼容性。
編寫 Protobuf
使用 Protobuf 的語言定義文件(.proto)可以定義要傳輸?shù)男畔⒌臄?shù)據(jù)結構,可以包括各個字段的名稱、類型等信息。同時也可以相互嵌套組合,構造出更加復雜的消息結構。
比如想要構造一個地址簿 AddressBook 信息結構。一個 AddressBook 可以包含多個人員 Person 信息,每個 Person 信息可以包含 id、name、email 信息,同時一個 Person 也可以包含多個電話號碼信息 PhoneNumber,每個電話號碼信息需要指定號碼種類,如手機、家庭電話、工作電話等。
如果使用 Protobuf 編寫定義文件如下:
// 文件:addressbook.proto
syntax = "proto3";
// 指定 protobuf 包名,防止有相同類名的 message 定義
package com.wdbyte.protobuf;
// 是否生成多個文件
option java_multiple_files = true;
// 生成的文件存放在哪個包下
option java_package = "com.wdbyte.tool.protos";
// 生成的類名,如果沒有指定,會根據(jù)文件名自動轉(zhuǎn)駝峰來命名
option java_outer_classname = "AddressBookProtos";
message Person {
// =1,=2 作為序列化后的二進制編碼中的字段的唯一標簽,也因此,1-15 比 16 會少一個字節(jié),所以盡量使用 1-15 來指定常用字段。
optional int32 id = 1;
optional string name = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Protobuf 文件中的語法解釋。
頭部全局定義
syntax = "proto3";指定 Protobuf 版本為版本3(最新版本)package com.wdbyte.protobuf;指定 Protobuf 包名,防止有相同類名的message定義,這個包名是生成的類中所用到的一些信息的前綴,并非類所在包。option java_multiple_files = true;是否生成多個文件。若false,則只會生成一個類,其他類以內(nèi)部類形式提供。option java_package =生成的類所在包。option java_outer_classname生成的類名,若無,自動使用文件名進行駝峰轉(zhuǎn)換來為類命名。
消息結構具體定義
message Person 定一個了一個 Person 類。
Person 類中的字段被 optional 修飾,被 optional 修飾說明字段可以不賦值。
- 修飾符
optional表示可選字段,可以不賦值。 - 修飾符
repeated表示數(shù)據(jù)重復多個,如數(shù)組,如 List。 - 修飾符
required表示必要字段,必須給值,否則會報錯RuntimeException,但是在 Protobuf 版本 3 中被移除。即使在版本 2 中也應該慎用,因為一旦定義,很難更改。
字段類型定義
修飾符后面緊跟的是字段類型,如 int32 、string。常用的類型如下:
int32、int64、uint32、uint64:整數(shù)類型,包括有符號和無符號類型。float、double:浮點數(shù)類型。bool:布爾類型,只有兩個值,true 和 false。string:字符串類型。bytes:二進制數(shù)據(jù)類型。enum:枚舉類型,枚舉值可以是整數(shù)或字符串。message:消息類型,可以嵌套其他消息類型,類似于結構體。
字段后面的 =1,=2 是作為序列化后的二進制編碼中的字段的對應標簽,因為 Protobuf 消息在序列化后是不包含字段信息的,只有對應的字段序號,所以節(jié)省了空間。也因此,1-15 比 16 會少一個字節(jié),所以盡量使用 1-15 來指定常用字段。且一旦定義,不要隨意更改,否則可能會對不上序列化信息。
編譯 Protobuf
使用 Protobuf 提供的編譯器,可以將 .proto 文件編譯成各種語言的代碼文件(如 Java、C++、Python 等)。

下載編譯器:https://github.com/protocolbuffers/protobuf/releases/latest
安裝完成后可以使用 protoc 命令編譯 proto 文件,如編譯示例中的 addressbook.proto.
protoc --java_out=./java ./resources/addressbook.proto
# --java_out 指定輸出 java 格式文件,輸出到 ./java 目錄
# ./resources/addressbook.proto 為 proto 文件位置
生成后可以看到生產(chǎn)的類文件。
./
├── java
│ └── com
│ └── wdbyte
│ └── tool
│ ├── protos
│ │ ├── AddressBook.java
│ │ ├── AddressBookOrBuilder.java
│ │ ├── AddressBookProtos.java
│ │ ├── Person.java
│ │ ├── PersonOrBuilder.java
└── resources
├── addressbook.proto
使用 Protobuf
使用 Java 語言操作 Protobuf,首先需要引入 Protobuf 依賴。
Maven 依賴:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.22.3</version>
</dependency>構造消息對象
// 直接構建
PhoneNumber phoneNumber1 = PhoneNumber.newBuilder().setNumber("18388888888").setType(PhoneType.HOME).build();
Person person1 = Person.newBuilder().setId(1).setName("www.wdbyte.com").setEmail("xxx@wdbyte.com").addPhones(phoneNumber1).build();
AddressBook addressBook1 = AddressBook.newBuilder().addPeople(person1).build();
System.out.println(addressBook1);
System.out.println("------------------");
// 鏈式構建
AddressBook addressBook2 = AddressBook
.newBuilder()
.addPeople(Person.newBuilder()
.setId(2)
.setName("www.wdbyte.com")
.setEmail("yyy@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18388888888")
.setType(PhoneType.HOME)
)
)
.build();
System.out.println(addressBook2);
輸出:
people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
------------------
people {
id: 2
name: "www.wdbyte.com"
email: "yyy@126.com"
phones {
number: "18388888888"
type: HOME
}
}
序列化、反序列化
序列化:將內(nèi)存中的數(shù)據(jù)對象序列化為二進制數(shù)據(jù),可以用于網(wǎng)絡傳輸或存儲等場景。
反序列化:將二進制數(shù)據(jù)反序列化成內(nèi)存中的數(shù)據(jù)對象,可以用于數(shù)據(jù)處理和業(yè)務邏輯。
下面演示使用 Protobuf 進行字符數(shù)組和文件的序列化及反序列化過程。
package com.wdbyte.tool.protos;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
*
* @author www.wdbyte.com
*/
public class ProtobufTest2 {
public static void main(String[] args) throws IOException {
PhoneNumber phoneNumber1 = PhoneNumber.newBuilder().setNumber("18388888888").setType(PhoneType.HOME).build();
Person person1 = Person.newBuilder().setId(1).setName("www.wdbyte.com").setEmail("xxx@wdbyte.com").addPhones(phoneNumber1).build();
AddressBook addressBook1 = AddressBook.newBuilder().addPeople(person1).build();
// 序列化成字節(jié)數(shù)組
byte[] byteArray = addressBook1.toByteArray();
// 反序列化 - 字節(jié)數(shù)組轉(zhuǎn)對象
AddressBook addressBook2 = AddressBook.parseFrom(byteArray);
System.out.println("字節(jié)數(shù)組反序列化:");
System.out.println(addressBook2);
// 序列化到文件
addressBook1.writeTo(new FileOutputStream("AddressBook1.txt"));
// 讀取文件反序列化
AddressBook addressBook3 = AddressBook.parseFrom(new FileInputStream("AddressBook1.txt"));
System.out.println("文件讀取反序列化:");
System.out.println(addressBook3);
}
}
輸出:
字節(jié)數(shù)組反序列化:
people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
文件讀取反序列化:
people {
id: 1
name: "www.wdbyte.com"
email: "xxx@wdbyte.com"
phones {
number: "18388888888"
type: HOME
}
}
Protobuf 為什么高效
在分析 Protobuf 高效之前,我們先確認一下 Protobuf 是否真的高效,下面將 Protobuf 與 JSON 進行對比,分別對比序列化和反序列化速度以及序列化后的存儲占用大小。
測試工具:JMH,F(xiàn)astJSON,
測試對象:Protobuf 的 addressbook.proto,JSON 的普通 Java 類。
Maven 依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.33</version>
<scope>provided</scope>
</dependency>先編寫與addressbook.proto 結構相同的 Java 類 AddressBookJava.java.
public class AddressBookJava {
List<PersonJava> personJavaList;
public static class PersonJava {
private int id;
private String name;
private String email;
private PhoneNumberJava phones;
// get...set...
}
public static class PhoneNumberJava {
private String number;
private PhoneTypeJava phoneTypeJava;
// get....set....
}
public enum PhoneTypeJava {
MOBILE, HOME, WORK;
}
public List<PersonJava> getPersonJavaList() {
return personJavaList;
}
public void setPersonJavaList(List<PersonJava> personJavaList) {
this.personJavaList = personJavaList;
}
}
序列化大小對比
分別在地址簿中添加 1000 個人員信息,輸出序列化后的數(shù)組大小。
package com.wdbyte.tool.protos;
import java.io.IOException;
import java.util.ArrayList;
import com.alibaba.fastjson.JSON;
import com.wdbyte.tool.protos.AddressBook.Builder;
import com.wdbyte.tool.protos.AddressBookJava.PersonJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneNumberJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneTypeJava;
import com.wdbyte.tool.protos.Person.PhoneNumber;
import com.wdbyte.tool.protos.Person.PhoneType;
/**
* @author https://www.wdbyte.com
*/
public class ProtobufTest3 {
public static void main(String[] args) throws IOException {
AddressBookJava addressBookJava = createAddressBookJava(1000);
String jsonString = JSON.toJSONString(addressBookJava);
System.out.println("json string size:" + jsonString.length());
AddressBook addressBook = createAddressBook(1000);
byte[] addressBookByteArray = addressBook.toByteArray();
System.out.println("protobuf byte array size:" + addressBookByteArray.length);
}
public static AddressBook createAddressBook(int personCount) {
Builder builder = AddressBook.newBuilder();
for (int i = 0; i < personCount; i++) {
builder.addPeople(Person.newBuilder()
.setId(i)
.setName("www.wdbyte.com")
.setEmail("xxx@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18333333333")
.setType(PhoneType.HOME)
)
);
}
return builder.build();
}
public static AddressBookJava createAddressBookJava(int personCount) {
AddressBookJava addressBookJava = new AddressBookJava();
addressBookJava.setPersonJavaList(new ArrayList<>());
for (int i = 0; i < personCount; i++) {
PersonJava personJava = new PersonJava();
personJava.setId(i);
personJava.setName("www.wdbyte.com");
personJava.setEmail("xxx@126.com");
PhoneNumberJava numberJava = new PhoneNumberJava();
numberJava.setNumber("18333333333");
numberJava.setPhoneTypeJava(PhoneTypeJava.HOME);
personJava.setPhones(numberJava);
addressBookJava.getPersonJavaList().add(personJava);
}
return addressBookJava;
}
}
輸出:
json string size:108910
protobuf byte array size:50872
可見測試中 Protobuf 的序列化結果比 JSON 小了將近一倍左右。
序列化速度對比
使用 JMH 進行性能測試,分別測試 JSON 的序列化和反序列以及 Protobuf 的序列化和反序列化性能情況。每次測試前進行 3 次預熱,每次 3 秒。接著進行 5 次測試,每次 3 秒,收集測試情況。
package com.wdbyte.tool.protos;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import com.alibaba.fastjson.JSON;
import com.google.protobuf.InvalidProtocolBufferException;
import com.wdbyte.tool.protos.AddressBook.Builder;
import com.wdbyte.tool.protos.AddressBookJava.PersonJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneNumberJava;
import com.wdbyte.tool.protos.AddressBookJava.PhoneTypeJava;
import com.wdbyte.tool.protos.Person.PhoneNumber;
import com.wdbyte.tool.protos.Person.PhoneType;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
/**
* @author https://www.wdbyte.com
*/
@State(Scope.Thread)
@Fork(2)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 3)
@BenchmarkMode(Mode.Throughput) // Throughput:吞吐量,SampleTime:采樣時間
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ProtobufTest4 {
private AddressBookJava addressBookJava;
private AddressBook addressBook;
@Setup
public void init() {
addressBookJava = createAddressBookJava(1000);
addressBook = createAddressBook(1000);
}
@Benchmark
public AddressBookJava testJSON() {
// 轉(zhuǎn) JSON
String jsonString = JSON.toJSONString(addressBookJava);
// JSON 轉(zhuǎn)對象
return JSON.parseObject(jsonString, AddressBookJava.class);
}
@Benchmark
public AddressBook testProtobuf() throws InvalidProtocolBufferException {
// 轉(zhuǎn) JSON
byte[] addressBookByteArray = addressBook.toByteArray();
// JSON 轉(zhuǎn)對象
return AddressBook.parseFrom(addressBookByteArray);
}
public static AddressBook createAddressBook(int personCount) {
Builder builder = AddressBook.newBuilder();
for (int i = 0; i < personCount; i++) {
builder.addPeople(Person.newBuilder()
.setId(i)
.setName("www.wdbyte.com")
.setEmail("xxx@126.com")
.addPhones(PhoneNumber.newBuilder()
.setNumber("18333333333")
.setType(PhoneType.HOME)
)
);
}
return builder.build();
}
public static AddressBookJava createAddressBookJava(int personCount) {
AddressBookJava addressBookJava = new AddressBookJava();
addressBookJava.setPersonJavaList(new ArrayList<>());
for (int i = 0; i < personCount; i++) {
PersonJava personJava = new PersonJava();
personJava.setId(i);
personJava.setName("www.wdbyte.com");
personJava.setEmail("xxx@126.com");
PhoneNumberJava numberJava = new PhoneNumberJava();
numberJava.setNumber("18333333333");
numberJava.setPhoneTypeJava(PhoneTypeJava.HOME);
personJava.setPhones(numberJava);
addressBookJava.getPersonJavaList().add(personJava);
}
return addressBookJava;
}
}
JMH 吞吐量測試結果(Score 值越大吞吐量越高,性能越好):
Benchmark Mode Cnt Score Error Units
ProtobufTest3.testJSON thrpt 10 1.877 ± 0.287 ops/ms
ProtobufTest3.testProtobuf thrpt 10 2.813 ± 0.446 ops/ms
JMH 采樣時間測試結果(Score 越小,采樣時間越小,性能越好):
Benchmark Mode Cnt Score Error Units
ProtobufTest3.testJSON sample 53028 0.565 ± 0.005 ms/op
ProtobufTest3.testProtobuf sample 90413 0.332 ± 0.001 ms/op
從測試結果看,不管是吞吐量測試,還是采樣時間測試,Protobuf 都優(yōu)于 JSON。
為什么高效?
Protobuf 是如何實現(xiàn)這種高效緊湊的數(shù)據(jù)編碼和解碼的呢?
首先,Protobuf 使用二進制編碼,會提高性能;其次 Protobuf 在將數(shù)據(jù)轉(zhuǎn)換成二進制時,會對字段和類型重新編碼,減少空間占用。它采用 TLV 格式來存儲編碼后的數(shù)據(jù)。TLV 也是就是 Tag-Length-Value ,是一種常見的編碼方式,因為數(shù)據(jù)其實都是鍵值對形式,所以在 TAG 中會存儲對應的字段和類型信息,Length 存儲內(nèi)容的長度,Value 存儲具體的內(nèi)容。

還記得上面定義結構體時每個字段都對應一個數(shù)字嗎?如 =1,=2,=3.
message Person {
optional int32 id = 1;
optional string name = 2;
optional string email = 3;
}在序列化成二進制時候就是通過這個數(shù)字來標記對應的字段的,二進制中只存儲這個數(shù)字,反序列化時通過這個數(shù)字找對應的字段。這也是上面為什么說盡量使用 1-15 范圍內(nèi)的數(shù)字,因為一旦超過 15,就需要多一個 bit 位來存儲。
那么類型信息呢?比如 int32 怎么標記,因為類型個數(shù)有限,所以 Protobuf 規(guī)定了每個類型對應的二進制編碼,比如 int32 對應二進制 000,string 對應二進制 010,這樣就可以只用三個比特位存儲類型信息。
這里只是舉例描述大概思想,具體還有一些變化。
詳情可以參考官方文檔:https://protobuf.dev/programming-guides/encoding/
其次,Protobuf 還會采用一種變長編碼的方式來存儲數(shù)據(jù)。這種編碼方式能夠保證數(shù)據(jù)占用的空間最小化,從而減少了數(shù)據(jù)傳輸和存儲的開銷。具體來說,Protobuf 會將整數(shù)和浮點數(shù)等類型變換成一個或多個字節(jié)的形式,其中每個字節(jié)都包含了一部分數(shù)據(jù)信息和一部分標識符信息。這種編碼方式可以在數(shù)據(jù)值比較小的情況下,只使用一個字節(jié)來存儲數(shù)據(jù),以此來提高編碼效率。
最后,Protobuf 還可以通過采用壓縮算法來減少數(shù)據(jù)傳輸?shù)拇笮?/strong>。比如 GZIP 算法能夠?qū)⒃紨?shù)據(jù)壓縮成更小的二進制格式,從而在網(wǎng)絡傳輸中能夠節(jié)省帶寬和傳輸時間。Protobuf 還提供了一些可選的壓縮算法,如 zlib 和 snappy,這些算法在不同的場景下能夠適應不同的壓縮需求。
綜上所述,Protobuf 在實現(xiàn)高效編碼和解碼的過程中,采用了多種優(yōu)化方式,從而在實際應用中能夠有效地提升數(shù)據(jù)傳輸和處理的效率。
總結
ProtoBuf 是一種輕量、高效的數(shù)據(jù)交換格式,它具有以下優(yōu)點:
- 語言中立,可以支持多種編程語言;
- 數(shù)據(jù)結構清晰,易于維護和擴展;
- 二進制編碼,數(shù)據(jù)體積小,傳輸效率高;
- 自動生成代碼,開發(fā)效率高。
但是,ProtoBuf 也存在以下缺點:
- 學習成本較高,需要掌握其語法規(guī)則和使用方法;
- 需要先定義數(shù)據(jù)結構,然后才能對數(shù)據(jù)進行序列化和反序列化,增加了一定的開發(fā)成本;
- 由于二進制編碼,可讀性較差,這點不如 JSON 可以直接閱讀。
總體來說,Protobuf 適合用于數(shù)據(jù)傳輸和存儲等場景,能夠提高數(shù)據(jù)傳輸效率和減少數(shù)據(jù)體積。但對于需要人類可讀的數(shù)據(jù),或需要實時修改的數(shù)據(jù),或者對數(shù)據(jù)的傳輸效率和體積沒那么在意的場景,選擇更加通用的 JSON 未嘗不是一個好的選擇。
以上就是高效數(shù)據(jù)傳輸?shù)拿孛芪淦鱌rotobuf的使用教程的詳細內(nèi)容,更多關于Protobuf數(shù)據(jù)傳輸?shù)馁Y料請關注腳本之家其它相關文章!
相關文章
SpringBoot使用WebSocket實現(xiàn)向前端推送消息功能
WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡協(xié)議,它實現(xiàn)了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發(fā)送信息給客戶端,本文給大家介紹了SpringBoot使用WebSocket實現(xiàn)向前端推送消息功能,需要的朋友可以參考下2024-05-05
JAVA核心知識之ConcurrentHashMap源碼分析
這篇文章主要介紹了JAVA核心知識之ConcurrentHashMap源碼分析,想了解ConcurrentHashMap的同學一定要看啊2021-04-04
解決RestTemplate 請求接收自定義400+ 或500+錯誤
這篇文章主要介紹了解決RestTemplate 請求接收自定義400+ 或500+錯誤,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
Intellij IDEA集成JProfiler性能分析工具
作為Java程序員,性能分析是我們必須掌握的技能之一,在性能分析中,JProfiler是一款非常強大的工具,本文就來介紹一下Intellij IDEA集成JProfiler性能分析工具,就有一定的參考價值,感興趣的可以了解一下2023-12-12

