一、簡(jiǎn)介
最近在手?jǐn)] IM 系統(tǒng),關(guān)于數(shù)據(jù)傳輸格式的選擇,猶豫了下,對(duì)比了 JSON 和 XML,最后選擇了 Protobuf 作為數(shù)據(jù)傳輸格式。
畢竟 Google 出品,必屬精品??,[官網(wǎng)地址]。
好了,舔狗環(huán)節(jié)結(jié)束,關(guān)于技術(shù)選擇,都是需要根據(jù)實(shí)際的應(yīng)用場(chǎng)景的,否則都是耍流氓,下文會(huì)進(jìn)行簡(jiǎn)單的對(duì)比,先來(lái)看看官網(wǎng)的介紹:
他是一種與語(yǔ)言無(wú)關(guān)、與平臺(tái)無(wú)關(guān),是一種可擴(kuò)展的用于序列化和結(jié)構(gòu)化數(shù)據(jù)的方法,常用于用于通信協(xié)議,數(shù)據(jù)存儲(chǔ)等。
他是一種靈活,高效,自動(dòng)化的機(jī)制,用于序列化結(jié)構(gòu)化數(shù)據(jù),對(duì)比于 XML,他更?。?10倍),更快(20100倍),更簡(jiǎn)單。
當(dāng)然,最簡(jiǎn)單粗暴的理解方式,就是結(jié)合 JSON 和 XML 來(lái)理解,你可以暫時(shí)將他們仨理解成同一種類型的事物,但是呢,Protobuf 對(duì)比于他們兩個(gè),擁有著體量更小,解析速度更快的優(yōu)勢(shì),所以,在 IM 這種通信應(yīng)用中,非常適合將 Protobuf 作為數(shù)據(jù)傳輸格式。
二、關(guān)于 proto3
Protobuf 有兩個(gè)大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接觸的話,同樣建議直接入手 proto3 版本。所以下文的描述都是基于 proto3 的。
proto3 相對(duì) proto2 而言,簡(jiǎn)言之就是支持更多的語(yǔ)言(Ruby、C#等)、刪除了一些復(fù)雜的語(yǔ)法和特性、引入了更多的約定等。
為什么要關(guān)注語(yǔ)言,因?yàn)樗幌?JSON 一樣開箱即用,它依賴工具包來(lái)進(jìn)行編譯成 java 文件或 go 文件等。
正如硬幣的兩面性一樣,凡事皆有雙面性,Protobuf 數(shù)據(jù)的體量更小,所以自然失去了人類的直接可讀性, JSON 數(shù)據(jù)結(jié)構(gòu)是可以很直觀地閱讀的,但是 Protobuf 我們需要借助工具來(lái)進(jìn)行更友好地使用,所以,我們需要自定義一個(gè) schema 來(lái)定義數(shù)據(jù)結(jié)構(gòu)的描述,即下面的 message。
- Message
舉個(gè)很簡(jiǎn)單的栗子,摘自官網(wǎng):
syntax = "proto3"; // proto3 必須加此注解
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;
}
上面便是定義好的一個(gè) message,里面包含:
- String 類型的 query,編號(hào)是 1 (注:字段必須有編號(hào)且編號(hào)不允許重復(fù))
- int 類型的 page_number,編號(hào)是 2
- 枚舉類型的 corpus (注:枚舉內(nèi)部的編號(hào)也不允許重復(fù),并且第一個(gè)編號(hào)必須為0)
三、對(duì)比 JSON 和 XML

四、應(yīng)用
此處以 Windows 為例,其他的都差不多。
- windows 安裝
- protoc 下載:[官方下載地址],然后將 bin 路徑添加到 path 環(huán)境變量下去
- 查看是否安裝成功:控制臺(tái)輸入
protoc --version,控制臺(tái)輸出版本信息代表成功,如:libprotoc 3.7.1
- ideal 安裝插件
- ideal 插件庫(kù)搜索安裝 Protobuf Support 即可
- 此插件可以不用安裝,但是這有助于一些源碼閱讀的便利性和一些編碼提示
IDE 最大的作用不就是快速編碼嘛

- 編寫 proto 文件
定義一個(gè) JetProtos.proto 文件
syntax = "proto3"; // PB協(xié)議版本
import "google/protobuf/any.proto"; // 引用外部的message,可以是本地的,也可以是此處比較特殊的 Any
package jet.protobuf; // 包名,其他 proto 在引用此 proto 的時(shí)候,就可以使用 test.protobuf.PersonTest 來(lái)使用,
// 注意:和下面的 java_package 是兩種易混淆概念,同時(shí)定義的時(shí)候,java_package 具有較高的優(yōu)先級(jí)
option java_package = "com.jet.protobuf"; // 生成類的包名,注意:會(huì)在指定路徑下按照該包名的定義來(lái)生成文件夾
option java_outer_classname="PersonTestProtos"; // 生成類的類名,注意:下劃線的命名會(huì)在編譯的時(shí)候被自動(dòng)改為駝峰命名
message PersonTest {
int32 id = 1; // int 類型
string name = 2; // string 類型
string email = 3;
Sex sex = 4; // 枚舉類型
repeated PhoneNumber phone = 5; // 引用下面定義的 PhoneNumber 類型的 message
map<string, string> tags = 6; // map 類型
repeated google.protobuf.Any details = 7; // 使用 google 的 any 類型
// 定義一個(gè)枚舉
enum Sex {
DEFAULT = 0;
MALE = 1;
Female = 2;
}
// 定義一個(gè) message
message PhoneNumber {
string number = 1;
PhoneType type = 2;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
}
}
- 編譯成 java 文件
進(jìn)入 proto 文件所在路徑,輸入下面 protoc 命令(后面有三部分參數(shù)),然后將編譯得出的 java 文件拷貝到項(xiàng)目中即可(此 java 文件可以理解成使用的數(shù)據(jù)對(duì)象):
protoc -I=./ --java_out=./ ./JetProtos.proto
或
protoc -proto_path=./ --java_out=./ ./JetProtos.proto
參數(shù)說(shuō)明:
- -I 等價(jià)于 -proto_path:指定 .proto 文件所在的路徑
- --java_out:編譯成 java 文件時(shí),標(biāo)明輸出目標(biāo)路徑
- ./JetProtos.proto:指定需要編譯的 .proto 文件
- 使用
- maven 引入指定包
<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.7.1</version>
</dependency>
- 使用
序列化和反序列化有多種方式,可以是 byte[],也可以是 inputStream 等,
package com.jet.mini.protobuf;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* @ClassName: ProtoTest
* @Description: ProtoBuf 測(cè)試
* @Author: Jet.Chen
* @Date: 2019/5/8 9:55
* @Version: 1.0
**/
public class ProtoTest {
public static void main(String[] args) {
try {
/** Step1:生成 personTest 對(duì)象 */
// personTest 構(gòu)造器
PersonTestProtos.PersonTest.Builder personBuilder = PersonTestProtos.PersonTest.newBuilder();
// personTest 賦值
personBuilder.setName("Jet Chen");
personBuilder.setEmail("ckk505214992@gmail.com");
personBuilder.setSex(PersonTestProtos.PersonTest.Sex.MALE);
// 內(nèi)部的 PhoneNumber 構(gòu)造器
PersonTestProtos.PersonTest.PhoneNumber.Builder phoneNumberBuilder = PersonTestProtos.PersonTest.PhoneNumber.newBuilder();
// PhoneNumber 賦值
phoneNumberBuilder.setType(PersonTestProtos.PersonTest.PhoneNumber.PhoneType.MOBILE);
phoneNumberBuilder.setNumber("17717037257");
// personTest 設(shè)置 PhoneNumber
personBuilder.addPhone(phoneNumberBuilder);
// 生成 personTest 對(duì)象
PersonTestProtos.PersonTest personTest = personBuilder.build();
/** Step2:序列化和反序列化 */
// 方式一 byte[]:
// 序列化
// byte[] bytes = personTest.toByteArray();
// 反序列化
// PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseFrom(bytes);
// System.out.println(String.format("反序列化得到的信息,姓名:%s,性別:%d,手機(jī)號(hào):%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
// 方式二 ByteString:
// 序列化
// ByteString byteString = personTest.toByteString();
// System.out.println(byteString.toString());
// 反序列化
// PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseFrom(byteString);
// System.out.println(String.format("反序列化得到的信息,姓名:%s,性別:%d,手機(jī)號(hào):%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
// 方式三 InputStream
// 粘包,將一個(gè)或者多個(gè)protobuf 對(duì)象字節(jié)寫入 stream
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
personTest.writeDelimitedTo(byteArrayOutputStream);
// 反序列化,從 steam 中讀取一個(gè)或者多個(gè) protobuf 字節(jié)對(duì)象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseDelimitedFrom(byteArrayInputStream);
System.out.println(String.format("反序列化得到的信息,姓名:%s,性別:%d,手機(jī)號(hào):%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、message 部分語(yǔ)法說(shuō)明
在 proto3 中,枚舉的第一個(gè)常量名的編號(hào)必須為 0
在 proto3 中,由于默認(rèn)值的規(guī)則進(jìn)行了調(diào)整,而枚舉的默認(rèn)值為第一個(gè),所以必須將第一個(gè)常量的編號(hào)置為 0,但是這與我們的業(yè)務(wù)有時(shí)候是有沖突的,所以,我們常將第一個(gè)常量設(shè)為:xx_UNSPECIFIED = 0,如:ENUM_TYPE_UNSPECIFIED = 0;,當(dāng)然這不是我們自己約定的,這是 Google API Guilder 中建議的。同一個(gè) proto 文件中,多個(gè)枚舉之間不允許定義相同的常量名
如下面的 message 在編譯的時(shí)候就會(huì)報(bào)錯(cuò)IDEA is already defined in "xxx":
enum IDE1 {
IDEA = 0;
ECLIPSE = 1;
}
enum IDE2 {
IDEA = 7;
ECLIPSE = 8;
}
-
關(guān)于數(shù)據(jù)類型匹配
見下圖,摘自官網(wǎng):
Protobuf 數(shù)據(jù)類型參考圖 關(guān)于默認(rèn)值
proto3 中,數(shù)據(jù)的默認(rèn)值不再支持自定義,而是由程序自行推倒:
- string:默認(rèn)值為空
- bytes:默認(rèn)值為空
- bools:默認(rèn)值為 false
- 數(shù)字類型:默認(rèn)值為 0
- 枚舉類型: 默認(rèn)為定義的第一個(gè)元素,并且編號(hào)必須為 0
- message 類型:默認(rèn)值為 DEFAULT_INSTANCE,其值相當(dāng)于空的 message
六、總結(jié)
- XML、JSON、Protobuf 都具有數(shù)據(jù)結(jié)構(gòu)化和數(shù)據(jù)序列化的能力
- XML、JSON 更注重 數(shù)據(jù)結(jié)構(gòu)化,關(guān)注人類可讀性和語(yǔ)義表達(dá)能力。Protobuf 更注重 數(shù)據(jù)序列化,關(guān)注效率、空間、速度,人類可讀性差,語(yǔ)義表達(dá)能力不足
- Protobuf 的應(yīng)用場(chǎng)景更為明確,XML、JSON 的應(yīng)用場(chǎng)景更為豐富
七、其它

