隨著微服務(wù)架構(gòu)的流行,RPC框架漸漸地成為服務(wù)框架的一個(gè)重要部分世杀。在很多RPC的設(shè)計(jì)中阀参,都采用了高性能的編解碼技術(shù),Protocol Buffers就屬于其中的佼佼者瞻坝。Protocol Buffers是Google開源的一個(gè)語(yǔ)言無(wú)關(guān)蛛壳、平臺(tái)無(wú)關(guān)的通信協(xié)議,其小巧所刀、高效和友好的兼容性設(shè)計(jì)衙荐,使其被廣泛使用。
概述
protobuf是什么浮创?
Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
- Google良心企業(yè)出廠的忧吟;
- 是一種序列化對(duì)象框架(或者說(shuō)是編解碼框架),其他功能相似的有Java自帶的序列化斩披、Facebook的Thrift和JBoss Marshalling等溜族;
- 通過(guò)proto文件定義結(jié)構(gòu)化數(shù)據(jù),其他功能相似的比如XML垦沉、JSON等煌抒;
- 自帶代碼生成器,支持多種語(yǔ)言厕倍;
為什么叫“Protocol Buffers”寡壮?
官方如是說(shuō):
The name originates from the early days of the format, before we had the protocol buffer compiler to generate classes for us. At the time, there was a class called ProtocolBuffer which actually acted as a buffer for an individual method. Users would add tag/value pairs to this buffer individually by calling methods like AddValue(tag, value). The raw bytes were stored in a buffer which could then be written out once the message had been constructed.
Since that time, the "buffers" part of the name has lost its meaning, but it is still the name we use. Today, people usually use the term "protocol message" to refer to a message in an abstract sense, "protocol buffer" to refer to a serialized copy of a message, and "protocol message object" to refer to an in-memory object representing the parsed message.
核心特點(diǎn)
- 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)
- 簡(jiǎn)潔
- 高性能
- 良好的兼容性
“變態(tài)的”性能表現(xiàn)
有位網(wǎng)友曾經(jīng)做過(guò)各種通用序列化協(xié)議技術(shù)的對(duì)比,我這里直接拿來(lái)給大家感受一下:
序列化響應(yīng)時(shí)間對(duì)比
序列化bytes對(duì)比
具體的數(shù)字
快速開始
以下示例源碼已上傳至github:https://github.com/ginobefun/learning_projects/tree/master/learning-protobuf
新建一個(gè)maven項(xiàng)目并添加依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ginobefunny.learning</groupId>
<artifactId>leanring-protobuf</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
</project>
新建protobuf的消息定義文件addressbook.proto
syntax = "proto3"; // 聲明為protobuf 3定義文件
package tutorial;
option java_package = "com.ginobefunny.learning.protobuf.message"; // 聲明生成消息類的java包路徑
option java_outer_classname = "AddressBookProtos"; // 聲明生成消息類的類名
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
使用protoc工具生成消息對(duì)應(yīng)的Java類
- 從已發(fā)布版本中下載protoc工具况既,比如protoc-3.2.0-win32屋群;
- 解壓后將bin目錄添加到path路徑;
- 執(zhí)行以下protoc命令生成Java類:
protoc -I=. --java_out=src/main/java addressbook.proto
編寫測(cè)試類寫入和讀取序列化文件
- AddPerson類通過(guò)用戶每次添加一個(gè)聯(lián)系人坏挠,并序列化保存到指定文件中芍躏。
public class AddPerson {
// 通過(guò)用戶輸入構(gòu)建一個(gè)Person對(duì)象
static AddressBookProtos.Person promptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
AddressBookProtos.Person.Builder person = AddressBookProtos.Person.newBuilder();
stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));
stdout.print("Enter name: ");
person.setName(stdin.readLine());
stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}
while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}
AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
AddressBookProtos.Person.PhoneNumber.newBuilder().setNumber(number);
stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}
person.addPhones(phoneNumber);
}
return person.build();
}
// 加載指定的序列化文件(如不存在則創(chuàng)建一個(gè)新的),再通過(guò)用戶輸入增加一個(gè)新的聯(lián)系人到地址簿降狠,最后序列化到文件中
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}
AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.AddressBook.newBuilder();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPeople(promptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
- ListPeople類讀取序列化文件并輸出所有聯(lián)系人信息对竣。
public class ListPeople {
// 打印地址簿中所有聯(lián)系人信息
static void print(AddressBookProtos.AddressBook addressBook) {
for (AddressBookProtos.Person person: addressBook.getPeopleList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (!person.getPhonesList().isEmpty()) {
System.out.println(" E-mail address: " + person.getEmail());
}
for (AddressBookProtos.Person.PhoneNumber phoneNumber : person.getPhonesList()) {
switch (phoneNumber.getType()) {
case MOBILE:
System.out.print(" Mobile phone #: ");
break;
case HOME:
System.out.print(" Home phone #: ");
break;
case WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}
// 加載指定的序列化文件,并輸出所有聯(lián)系人信息
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBookProtos.AddressBook addressBook =
AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));
print(addressBook);
}
}
驗(yàn)證效果
先添加一個(gè)聯(lián)系人Gino
再添加一個(gè)聯(lián)系人Slightly
最后顯示所有聯(lián)系人信息
實(shí)例小結(jié)
- 通過(guò)以上的例子我們能大概感受到開發(fā)protobuf序列化的大致步驟:定義proto文件榜配、生成對(duì)應(yīng)的Java類文件否纬、通過(guò)消息類的構(gòu)造器構(gòu)造對(duì)象并通過(guò)writeTo序列化、通過(guò)parseFrom反序列化對(duì)象蛋褥;
- 如果查看中間序列化的文件临燃,我們可以發(fā)現(xiàn)protobuf序列化的二進(jìn)制文件非常緊湊,因此文件更小烙心,傳輸性能更好膜廊。
深入學(xué)習(xí)
關(guān)于proto文件
protobuf版本
- protobuf現(xiàn)在主流的有2.X和3.X版本,兩者之間相差比較大淫茵,對(duì)于剛采用的建議使用3.X版本爪瓜;
- 如果采用3.X版本,需要再proto文件第一個(gè)非注釋行聲明(就像我們上面的例子那樣)匙瘪,因?yàn)閜rotobuf默認(rèn)認(rèn)為是2.X版本铆铆;
message結(jié)構(gòu)
- 在一個(gè)proto文件中可以包含多個(gè)message定義,message之間可以互相引用丹喻,message還可以嵌套message和枚舉類薄货;
- 一個(gè)message通常包含一至多個(gè)字段;
- 每個(gè)字段包含以下幾個(gè)部分:字段描述符(可選)碍论、字段類型谅猾、字段名稱和字段對(duì)應(yīng)的Tag;
字段描述符
字段描述符用于描述字段出現(xiàn)的頻率骑冗,有以下兩個(gè)可選值:
- singular:表示出現(xiàn)0次或1次赊瞬;如果沒有聲明描述符先煎,默認(rèn)為singular贼涩;
- repeated:表示出現(xiàn)0次或多次;
字段類型
- 基本數(shù)據(jù)類型:包括double薯蝎、float遥倦、bool、string、bytes袒哥、int32缩筛、int64、uint32堡称、uint64瞎抛、sint32、sint64却紧、fixed32桐臊、fixed64、sfixed32晓殊、sfixed64断凶;
- 引用其他message類型:這個(gè)就有點(diǎn)像我們Java里面的對(duì)象引用的方式;
- 枚舉類型:對(duì)于枚舉類型巫俺,protobuf有個(gè)約束:枚舉的第一項(xiàng)對(duì)應(yīng)的值必須為0认烁;下面是一個(gè)包含枚舉類型的消息定義:
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;
}
字段對(duì)應(yīng)的Tag
- 對(duì)應(yīng)同一個(gè)message里面的字段,每個(gè)字段的Tag是必須唯一數(shù)字介汹;
- Tag主要用于說(shuō)明字段在二進(jìn)制文件的對(duì)應(yīng)關(guān)系却嗡,一旦指定字段為對(duì)應(yīng)的Tag,不應(yīng)該在后續(xù)進(jìn)行變更嘹承;
- 對(duì)于Tag的分配稽穆,115只用一個(gè)byte進(jìn)行編碼(因此應(yīng)該留給那些常用的字段),162047用兩個(gè)byte進(jìn)行編碼赶撰,最大支持到536870911舌镶,但是中間有一段(19000~19999)是protobuf內(nèi)部使用的;
- 可以通過(guò)reserved關(guān)鍵字來(lái)預(yù)留Tag和字段名豪娜,還有一種場(chǎng)景是如果某個(gè)字段已經(jīng)被廢棄了不希望后續(xù)被采用餐胀,也可以用reserved關(guān)鍵字聲明;
字段的默認(rèn)值
protobuf 2.X版本是支持在字段中聲明默認(rèn)值的瘤载,但是在3.X版本中去掉了默認(rèn)值的定義否灾,主要是為了區(qū)別用戶是否設(shè)置了一個(gè)和默認(rèn)值一樣的值的情況。對(duì)于3.X版本鸣奔,protobuf采用以下規(guī)則處理默認(rèn)值:
- 對(duì)應(yīng)string類型墨技,默認(rèn)值為一個(gè)空字符串;
- 對(duì)于bytes類型挎狸,默認(rèn)值為一個(gè)空的byte數(shù)組扣汪;
- 對(duì)于bool類型,默認(rèn)值為false锨匆;
- 對(duì)于數(shù)值類型崭别,默認(rèn)值為0;
- 對(duì)于枚舉類型,默認(rèn)值為第一項(xiàng)茅主,也即值為0的那個(gè)枚舉值舞痰;
- 對(duì)于引用其他message類型:其默認(rèn)值和對(duì)應(yīng)的語(yǔ)言是相關(guān)的;
Map字段類型
- protobuf也支持定義Map類型的字段诀姚,但是對(duì)于Map的key的類型只能是整數(shù)型(包括各種int32和int64)和string類型响牛;
- Map類型不能定義為repeated;
- Map類型的數(shù)據(jù)是無(wú)序的赫段;
- 以下是一個(gè)Map類型的字段定義示例:
map<string, Project> projects = 3;
導(dǎo)入其他proto文件
- 可以通過(guò)import關(guān)鍵字導(dǎo)入其他proto文件娃善,從而重用message類型;下面是一個(gè)import的示例:
import "myproject/other_protos.proto";
如果proto中的message要擴(kuò)展怎么辦瑞佩?
proto具有很好的擴(kuò)展性聚磺,但是也要遵循以下原則:
- 不能修改原有字段的Tag;
- 如果新增一個(gè)字段炬丸,對(duì)于老的二進(jìn)制序列化文件處理時(shí)會(huì)給這個(gè)字段增加默認(rèn)值瘫寝;如果是升級(jí)了proto文件而沒有升級(jí)對(duì)應(yīng)的代碼,則新的字段會(huì)被忽略稠炬;
- 可以刪除字段焕阿,但是對(duì)應(yīng)的Tag不應(yīng)該再被使用,否則對(duì)于之前的二進(jìn)制序列化消息處理時(shí)對(duì)應(yīng)關(guān)系出現(xiàn)問(wèn)題首启;
- int32暮屡、uint32、int64毅桃、uint64和bool類型是相互兼容的褒纲,這意味著你可以在他們之間修改類型而不會(huì)有兼容性問(wèn)題;
Any消息類型
- protobuf內(nèi)置了一些通用的消息類型钥飞,Any就是其他的一種莺掠,通過(guò)查看它的proto文件可以看到它包含了一個(gè)URL標(biāo)識(shí)符和一個(gè)byte數(shù)組;
- 在使用Any消息類型之前读宙,需要通過(guò)import "google/protobuf/any.proto";導(dǎo)入proto文件定義彻秆;
Oneof關(guān)鍵字
- oneof關(guān)鍵字用于聲明一組字段中,必須要有一個(gè)字段被賦值结闸;通常比如我們?cè)诘顷懙臅r(shí)候,可以用手機(jī)號(hào)桦锄、郵箱和用戶名登陸扎附,這種時(shí)候就可以使用oneof來(lái)定義察纯;
- 當(dāng)我們對(duì)oneof其中一個(gè)字段賦值時(shí)即纲,其他字段的值將會(huì)被清空膊畴;所以只有最后一次賦值是有效的;
- 下面是一個(gè)oneof的示例:
message LoginMessage {
oneof user_identifier {
string user_name = 4;
string phone_num = 5;
string user_email = 6;
}
string password = 10;
}
定義服務(wù)
- 在proto文件中還允許定義RPC服務(wù)病游,以下是一個(gè)示例:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
小結(jié)
- 隨著微服務(wù)架構(gòu)的流行唇跨,RPC框架漸漸地成為服務(wù)框架的一個(gè)重要部分。在很多RPC的設(shè)計(jì)中衬衬,都采用了高性能的編解碼技術(shù)买猖,protobuf就屬于其中的佼佼者;
- protobuf相對(duì)于其他編解碼框架滋尉,有著非常驚人的性能表現(xiàn)玉控;
- 通過(guò)一個(gè)簡(jiǎn)單的實(shí)例,我們了解如果使用protobuf進(jìn)行序列化和數(shù)據(jù)交互狮惜;
- 最后高诺,我們列舉了一些重要的特性和配置說(shuō)明,這些在我們使用protobuf中都會(huì)給頻繁使用碾篡;
- 后續(xù)學(xué)習(xí):后面我會(huì)根據(jù)所學(xué)的Netty和protobuf知識(shí)懒叛,開發(fā)一個(gè)簡(jiǎn)單的RPC框架。
參考資料
- Language Guide (proto3)
- Protocol Buffer Basics: Java
- Java Generated Code
- Protobuf 的 proto3 與 proto2 的區(qū)別
- 幾種序列化協(xié)議(protobuf,xstream,jackjson,jdk,hessian)相關(guān)數(shù)據(jù)對(duì)比