IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點(diǎn)

本文由vivo技術(shù)團(tuán)隊(duì)Li Guanyun分享,為了提升閱讀體驗(yàn),即時(shí)通訊網(wǎng)進(jìn)行了較多修訂和重新排版。
1、引言
Protobuf 作為一種跨平臺(tái)、語(yǔ)言無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)通訊協(xié)議,已廣泛應(yīng)用于網(wǎng)絡(luò)數(shù)據(jù)交換的場(chǎng)景中(比如IM通信、分布式RPC調(diào)用等)。
隨著互聯(lián)網(wǎng)的發(fā)展,分布式系統(tǒng)的異構(gòu)性會(huì)愈發(fā)突出,跨語(yǔ)言的需求會(huì)愈加明顯,同時(shí)?gRPC?也大有取代Restful之勢(shì),而 Protobuf 作為gRPC 跨語(yǔ)言、高性能的法寶,我們技術(shù)人有必要深入理解 Protobuf 原理,為以后的技術(shù)更新和選型打下基礎(chǔ)。
借此機(jī)會(huì),我將個(gè)人的Protobuf學(xué)習(xí)過(guò)程以及實(shí)踐經(jīng)驗(yàn),總結(jié)成文,與大家一起探討學(xué)習(xí)。本篇主要從Protobuf的基礎(chǔ)概念開(kāi)始,包括技術(shù)背景、技術(shù)原理、使用方法和優(yōu)缺點(diǎn)。
PS:本篇本跟上篇《Protobuf從入門(mén)到精通,一篇就夠!》類(lèi)似,都適合作為Protobuf的入門(mén)文章,但本篇力求簡(jiǎn)潔,盡量不涉及Protobuf的具體技術(shù)細(xì)節(jié),目的是降低閱讀的門(mén)檻、提升閱讀效果,希望對(duì)你有用。

學(xué)習(xí)交流:
- 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》
- 開(kāi)源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文已同步發(fā)布于:http://www.52im.net/thread-4081-1-1.html)
2、系列文章
本文是系列文章中的第?2?篇,本系列總目錄如下:
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(一):Protobuf從入門(mén)到精通,一篇就夠!》
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點(diǎn)》(* 本文)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(三):由淺入深,從通信編解碼原理上理解Protobuf》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?請(qǐng)看全方位實(shí)測(cè)!》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(七):手把手教你如何在NodeJS中從零使用Protobuf》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)? 》(稍后發(fā)布..)
《IM通訊協(xié)議專(zhuān)題學(xué)習(xí)(九):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(實(shí)戰(zhàn)篇) 》(稍后發(fā)布..)
3、什么是Protobuf?

Protobuf(全稱(chēng)是Protocol Buffers)是一種跨平臺(tái)、語(yǔ)言無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)的方法,可用于網(wǎng)絡(luò)通信數(shù)據(jù)交換及存儲(chǔ)。
在序列化結(jié)構(gòu)化數(shù)據(jù)的機(jī)制中,Protobuf是靈活、高效、自動(dòng)化的,相對(duì)常見(jiàn)的XML、JSON,描述同樣的信息,Protobuf序列化后數(shù)據(jù)量更小、序列化/反序列化速度更快、更簡(jiǎn)單。
一旦定義了要處理的數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)之后,就可以利用Protobuf的代碼生成工具生成相關(guān)的代碼。只需使用 Protobuf 對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行一次描述,即可利用各種不同語(yǔ)言(proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#)或從各種不同流中對(duì)你的結(jié)構(gòu)化數(shù)據(jù)輕松讀寫(xiě)。
PS:類(lèi)似的介紹,在上篇《Protobuf從入門(mén)到精通,一篇就夠!》中也有涉及,有興趣可以一并閱讀之。
4、為什么是 Protobuf?
4.1 技術(shù)背景
大家可能會(huì)覺(jué)得 Google 發(fā)明 Protobuf 是為了解決序列化速度的,其實(shí)真實(shí)的原因并不是這樣的。
Protobuf最先開(kāi)始是 Google用來(lái)解決索引服務(wù)器?request/response?協(xié)議的。
在沒(méi)有Protobuf之前,Google 已經(jīng)存在了一種?request/response?格式,用于手動(dòng)處理?request/response?的編解碼。
這種sstk式也能支持多版本協(xié)議,不過(guò)代碼不夠優(yōu)雅:
if(protocolVersion=1) {
????doSomething();
} elseif(protocolVersion=2) {
????doOtherThing();
} ...
如果是非常明確的格式化協(xié)議,會(huì)使新協(xié)議變得非常復(fù)雜。因?yàn)殚_(kāi)發(fā)人員必須確保請(qǐng)求發(fā)起者與處理請(qǐng)求的實(shí)際服務(wù)器之間的所有服務(wù)器都能理解新協(xié)議,然后才能切換開(kāi)關(guān)以開(kāi)始使用新協(xié)議。
這也就是每個(gè)服務(wù)器開(kāi)發(fā)人員都遇到過(guò)的低版本兼容、新舊協(xié)議兼容相關(guān)的問(wèn)題。
為了解決這些問(wèn)題,于是Protobuf就誕生了。
4.2 Protobuf 誕生了
Protobuf 最初被寄予以下 2 個(gè)期望:
1)更容易引入新的字段,并且不需要檢查數(shù)據(jù)的中間服務(wù)器可以簡(jiǎn)單地解析并傳遞數(shù)據(jù)(而無(wú)需了解所有字段);
2)數(shù)據(jù)格式更加具有自我描述性,可以用各種語(yǔ)言來(lái)處理(比如C++, Java 等各種語(yǔ)言)。
但這個(gè)版本的 Protobuf 仍需要自己手寫(xiě)解析的代碼。
隨著Protobuf的發(fā)展、演進(jìn),它具有了更多的特性:
1)自動(dòng)生成的序列化和反序列化代碼(避免了手動(dòng)解析的需要。官方提供自動(dòng)生成代碼工具,各個(gè)語(yǔ)言平臺(tái)的基本都有);
2)除了用于數(shù)據(jù)交換之外,Protobuf也被用作某些持久化數(shù)據(jù)的便捷自描述格式。
Protocol Buffers 命名的由來(lái):
Why the name "Protocol Buffers"?
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.
4.3 Protobuf 在谷歌業(yè)務(wù)中的地位
Protobuf 現(xiàn)在是 Google 用于數(shù)據(jù)交換和存儲(chǔ)的通用語(yǔ)言。
谷歌代碼樹(shù)中定義了 48162 種不同的消息類(lèi)型,包括 12183 個(gè)?.proto?文件。它們既用于 RPC 系統(tǒng),也用于在各種存儲(chǔ)系統(tǒng)中持久存儲(chǔ)數(shù)據(jù)。
Protobuf 誕生之初是為了解決服務(wù)器端新舊協(xié)議(高低版本)兼容性問(wèn)題,名字也很體貼——“協(xié)議緩沖區(qū)”,只不過(guò)后期慢慢發(fā)展成用于傳輸數(shù)據(jù)。
5、Protobuf 協(xié)議的工作原理
如下圖所示:可以看到,對(duì)于序列化協(xié)議來(lái)說(shuō),使用方只需要關(guān)注業(yè)務(wù)對(duì)象本身,即 idl 定義,序列化和反序列化的代碼只需要通過(guò)工具生成即可。

6、Protobuf 協(xié)議的消息定義
Protobuf 的消息是在idl文件(.proto)中描述的。
下面是本次樣例中使用到的消息描述符?customer.proto:
syntax = "proto3";
?
package domain;
?
option java_package = "com.Protobuf.generated.domain";
option java_outer_classname = "CustomerProtos";
?
message Customers {
????repeated Customer customer = 1;
}
?
message Customer {
????int32 id= 1;
????string firstName = 2;
????string lastName = 3;
?
????enum EmailType {
????????PRIVATE = 0;
????????PROFESSIONAL = 1;
????}
?
????message EmailAddress {
????????string email = 1;
????????EmailType type= 2;
????}
?
????repeated EmailAddress email = 5;
}
上面的消息比較簡(jiǎn)單,Customers包含多個(gè)Customer(Customer包含一個(gè)id字段、一個(gè)firstName字段、一個(gè)lastName字段以及一個(gè)email的集合)。
除了上述定義外,文件頂部還有三行可幫助代碼生成器的申明:
1)syntax = "proto3":用于idl語(yǔ)法版本,目前有兩個(gè)版本proto2和proto3,兩個(gè)版本語(yǔ)法不兼容,如果不指定,默認(rèn)語(yǔ)法是proto2(由于proto3比proto2支持的語(yǔ)言更多,語(yǔ)法更簡(jiǎn)潔,本文使用的是proto3);
2)package domain:此配置用于嵌套生成的類(lèi)/對(duì)象;
3)option java_package:生成器還使用此配置來(lái)嵌套生成的源(此處的區(qū)別在于這僅適用于Java,在使用Java創(chuàng)建代碼和使用JavaScript創(chuàng)建代碼時(shí),使用了兩種配置來(lái)使生成器的行為有所不同。也就是說(shuō),Java類(lèi)是在包c(diǎn)om.Protobuf.generated.domain下創(chuàng)建的,而JavaScript對(duì)象是在包domain下創(chuàng)建的)。
Protobuf 提供了更多選項(xiàng)和數(shù)據(jù)類(lèi)型,本文不做詳細(xì)介紹,感興趣可以參考官方文檔。
7、Protobuf 的代碼生成
首先安裝 Protobuf 編譯器 protoc(點(diǎn)這里有詳細(xì)的安裝教程)。
安裝完成后,可以使用以下命令生成 Java 源代碼:
1protoc --java_out=./src/main/java./src/main/idl/customer.proto
上述命令的意圖是:從項(xiàng)目的根路徑執(zhí)行該命令,并添加了兩個(gè)參數(shù)?java_out(即定義?./src/main/java/?為Java代碼的輸出目錄;而?./src/main/idl/customer.proto?是.proto文件所在目錄)。
生成的代碼非常復(fù)雜,但幸運(yùn)的是它的用法卻非常簡(jiǎn)單:
CustomerProtos.Customer.EmailAddress email = CustomerProtos.Customer.EmailAddress.newBuilder()
????????.setType(CustomerProtos.Customer.EmailType.PROFESSIONAL)
????????.setEmail("crichardson@email.com").build();
?
CustomerProtos.Customer customer = CustomerProtos.Customer.newBuilder()
????????.setId(1)
????????.setFirstName("Lee")
????????.setLastName("Richardson")
????????.addEmail(email)
????????.build();
// 序列化
byte[] binaryInfo = customer.toByteArray();
System.out.println(bytes_String16(binaryInfo));
System.out.println(customer.toByteArray().length);
// 反序列化
CustomerProtos.Customer anotherCustomer = CustomerProtos.Customer.parseFrom(binaryInfo);
System.out.println(anotherCustomer.toString());
8、Protobuf 的性能數(shù)據(jù)
我們簡(jiǎn)單地以上述Customers為模型,分別構(gòu)造、選取小對(duì)象、普通對(duì)象、大對(duì)象進(jìn)行性能對(duì)比。
序列化耗時(shí)以及序列化后數(shù)據(jù)大小對(duì)比:

反序列化耗時(shí):

更多性能數(shù)據(jù)可以參考官方的測(cè)試Benchmark。
9、Protobuf 的優(yōu)點(diǎn)
9.1效率高
從序列化后的數(shù)據(jù)體積角度,與XML、JSON這類(lèi)文本協(xié)議相比,Protobuf通過(guò)?T-(L)-V(TAG-LENGTH-VALUE)方式編碼,不需要", {, }, :等分隔符來(lái)結(jié)構(gòu)化信息。同時(shí)在編碼層面使用varint壓縮。
所以描述同樣的信息,Protobuf序列化后的體積要小很多,在網(wǎng)絡(luò)中傳輸消耗的網(wǎng)絡(luò)流量更少,進(jìn)而對(duì)于網(wǎng)絡(luò)資源緊張、性能要求非常高的場(chǎng)景。比如在移動(dòng)網(wǎng)絡(luò)下的IM即時(shí)通訊應(yīng)用中,Protobuf協(xié)議就是非常不錯(cuò)的選擇(PS:這也是我為什么著手分享Protobuf系列文章的原因啦)。
我們來(lái)簡(jiǎn)單做個(gè)對(duì)比。
要描述如下JSON數(shù)據(jù):
1{"id":1,"firstName":"Chris","lastName":"Richardson","email":[{"type":"PROFESSIONAL","email":"crichardson@email.com"}]}
使用JSON序列化后的數(shù)據(jù)大小為118byte:
7b226964223a312c2266697273744e616d65223a224368726973222c226c6173744e616d65223a2252696368617264736f6e222c22656d61696c223a5b7b2274797065223a2250524f46455353494f4e414c222c22656d61696c223a226372696368617264736f6e40656d61696c2e636f6d227d5d7d
而使用Protobuf序列化后的數(shù)據(jù)大小為48byte:
0801120543687269731a0a52696368617264736f6e2a190a156372696368617264736f6e40656d61696c2e636f6d1001
從序列化/反序列化速度角度,與XML、JSON相比,Protobuf序列化/反序列化的速度更快,比XML要快20-100倍。
9.2支持跨平臺(tái)、多語(yǔ)言
Protobuf是平臺(tái)無(wú)關(guān)的,無(wú)論是Android、iOS、PC,還是C#與Java,都可以利用Protobuf進(jìn)行無(wú)障礙通訊。
proto3支持C++、Java、Python、Go、Ruby、Objective-C、C#(詳見(jiàn)《Protobuf從入門(mén)到精通,一篇就夠》)。
9.3擴(kuò)展性、兼容性好
Protobuf具有向后兼容的特性:更新數(shù)據(jù)結(jié)構(gòu)以后,老版本依舊可以兼容,這也是Protobuf誕生之初被寄予解決的問(wèn)題,因?yàn)榫幾g器對(duì)不識(shí)別的新增字段會(huì)跳過(guò)不處理。
9.4使用簡(jiǎn)單
Protobuf 提供了一套編譯工具,可以自動(dòng)生成序列化、反序列化的樣板代碼,這樣開(kāi)發(fā)者只要關(guān)注業(yè)務(wù)數(shù)據(jù)idl,簡(jiǎn)化了編碼解碼工作以及多語(yǔ)言交互的復(fù)雜度。
10、Protobuf 的缺點(diǎn)
Protobuf的優(yōu)點(diǎn)很突出,但缺點(diǎn)也很明顯。
Protobuf的缺點(diǎn)主要是:
1)不具備自描述能力:跟XML、JSON相比,這兩者是自描述的,而ProtoBuf則不是;
2)數(shù)據(jù)可讀性非常差:ProtoBuf是二進(jìn)制協(xié)議,如果沒(méi)有idl文件,就無(wú)法理解二進(jìn)制數(shù)據(jù)流,對(duì)調(diào)試非常不友好。
不過(guò):Charles已經(jīng)支持Protobuf協(xié)議,導(dǎo)入數(shù)據(jù)的描述文件即可,詳情可參考?Charles Protocol Buffers。
然而:由于沒(méi)有idl文件無(wú)法解析二進(jìn)制數(shù)據(jù)流,ProtoBuf在一定程度上可以保護(hù)數(shù)據(jù),提升核心數(shù)據(jù)被破解的門(mén)檻,降低核心數(shù)據(jù)被盜爬的風(fēng)險(xiǎn)(也算是缺點(diǎn)變優(yōu)點(diǎn)的典型范例)。
11、參考資料
[1]?Protobuf官方網(wǎng)站
[2]?Protobuf從入門(mén)到精通,一篇就夠!
[3]?如何選擇即時(shí)通訊應(yīng)用的數(shù)據(jù)傳輸格式
[4]?強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式
[5]?APP與后臺(tái)通信數(shù)據(jù)格式的演進(jìn):從文本協(xié)議到二進(jìn)制協(xié)議
[6]?面試必考,史上最通俗大小端字節(jié)序詳解
[7]?移動(dòng)端IM開(kāi)發(fā)需要面對(duì)的技術(shù)問(wèn)題(含通信協(xié)議選擇)
[8]?簡(jiǎn)述移動(dòng)端IM開(kāi)發(fā)的那些坑:架構(gòu)設(shè)計(jì)、通信協(xié)議和客戶(hù)端
[9]?理論聯(lián)系實(shí)際:一套典型的IM通信協(xié)議設(shè)計(jì)詳解
[10]?58到家實(shí)時(shí)消息系統(tǒng)的協(xié)議設(shè)計(jì)等技術(shù)實(shí)踐分享
(本文已同步發(fā)布于:http://www.52im.net/thread-4081-1-1.html)