Protocol Buffers,一款比Xml快100倍的序列化框架!
本文轉(zhuǎn)載自微信公眾號(hào)「程序新視界」,作者二師兄。轉(zhuǎn)載本文請(qǐng)聯(lián)系程序新視界公眾號(hào)。
我們通常習(xí)慣用Json、XML等形式的數(shù)據(jù)存儲(chǔ)格式,但相信還有很多人沒有聽說(shuō)過(guò)Protocol Buffer(簡(jiǎn)稱protobuf)。protobuf是Google開源的一個(gè)語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)的通信協(xié)議,其小巧、高效和友好的兼容性設(shè)計(jì),使其被廣泛使用。性能比Json、XML真的強(qiáng)太多了!
而且,隨著微服務(wù)架構(gòu)的流行,RPC框架也成為服務(wù)框架的重要組成部分。在很多RPC的設(shè)計(jì)中,都采用了高性能的編解碼技術(shù),而protobuf就屬于其中的佼佼者。
也就說(shuō),要想深入了解微服務(wù)架構(gòu)中的RPC環(huán)節(jié)底層實(shí)現(xiàn),設(shè)計(jì)出高效的傳輸、序列化、編碼解碼等功能,學(xué)習(xí)protobuf的使用和原理非常有必要。
protobuf簡(jiǎn)介
protobuf是一種序列化對(duì)象框架(或者說(shuō)是編解碼框架)。它有兩部分功能組成:結(jié)構(gòu)化數(shù)據(jù)(數(shù)據(jù)存儲(chǔ)結(jié)構(gòu))和序列化&反序列化。
其中數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)的作用與XML、JSON相似;序列化和反序列化的作用與Java自帶的序列化、Facebook的Thrift和JBoss Marshalling等相似。
總之:protobuf是通過(guò)定義結(jié)構(gòu)化數(shù)據(jù),并提供對(duì)數(shù)據(jù)的序列化和反序列化功能,從而實(shí)現(xiàn)數(shù)據(jù)存儲(chǔ)/RPC數(shù)據(jù)交換的功能。
它的特點(diǎn)是:
- 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)
- 簡(jiǎn)潔
- 高性能(序列化速度快 & 序列化后的數(shù)據(jù)體積小)
- 良好的兼容性
可以通過(guò)數(shù)據(jù)直觀的看一下不同框架在序列化響應(yīng)時(shí)間上的對(duì)比:
protobuf
可以看出,protobuf的性能要遠(yuǎn)高于其他框架。
protobuf的使用流程
上面介紹了protobuf的功能,但僅僅知道這些功能我們無(wú)法知道它是怎么使用的??戳司W(wǎng)上很多的文章,要么直接開始寫代碼要么直接開始分析報(bào)文格式,對(duì)于新手來(lái)說(shuō)往往會(huì)一頭霧水。
所以,我們先來(lái)梳理一下使用protobuf的步驟。
protobuf
在上圖中將protobuf的使用分了四個(gè)步驟:
- 步驟一,搭建環(huán)境:使用protobuf要定義通信的數(shù)據(jù)結(jié)構(gòu),并編譯生成不同的編程語(yǔ)言代碼,這就需要有這么一個(gè)編譯器的環(huán)境。
- 步驟二,構(gòu)建數(shù)據(jù):使用protobuf是要傳輸數(shù)據(jù)的,那么數(shù)據(jù)包含什么,有哪些項(xiàng)目,整個(gè)結(jié)構(gòu)層次是什么樣子的。這里基于protobuf的語(yǔ)法來(lái)進(jìn)行數(shù)據(jù)結(jié)構(gòu)的定義。
- 步驟三,項(xiàng)目集成:集成pom依賴(Java為例)、集成編譯的Java類(對(duì)照proto文件);
- 步驟四,具體使用:通過(guò)集成進(jìn)來(lái)的Java類,來(lái)構(gòu)建消息、賦值,然后基于protobuf進(jìn)行序列化,接收方進(jìn)行反序列化操作;
了解了上述步驟,下面就針對(duì)具體的步驟來(lái)進(jìn)行實(shí)戰(zhàn)演示。
這里演示基于Mac OS操作系統(tǒng)和Java編程語(yǔ)言來(lái)進(jìn)行操作。如果你使用的是其他操作系統(tǒng)和編程語(yǔ)言,基本思路一樣,在不同的步驟時(shí)可針對(duì)性的找一下具體操作。
安裝Protocol Buffers
安裝protobuf是為了進(jìn)行數(shù)據(jù)結(jié)構(gòu)的定義和對(duì)應(yīng)編程語(yǔ)言代碼的生成。通常有兩種方式:本地安裝和IDE插件。我們先來(lái)看本地安裝。
protobuf的代碼是托管在GitHub上的,對(duì)應(yīng)地址為:https://github.com/protocolbuffers/protobuf 。
點(diǎn)擊項(xiàng)目右邊的release鏈接可看到對(duì)應(yīng)版本:https://github.com/protocolbuffers/protobuf/releases 。
protobuf
這里包含了各種編程語(yǔ)言、環(huán)境的版本。本文選protobuf-java-3.17.3.zip版本。
在Mac操作系統(tǒng)下,需要先安裝一下依賴組件,才能夠?qū)rotobuf進(jìn)行編譯和安裝。
安裝依賴組件:
- // 安裝 Protocol Buffer依賴
- // 注:Protocol Buffer依賴于autoconf、automake、libtool、curl
- brew install autoconf automake libtool curl
解壓protobuf-java-3.17.3.zip,進(jìn)入根目錄,執(zhí)行以下命令:
- // 運(yùn)行autogen.sh腳本
- ./autogen.sh
- // 運(yùn)行configure.sh腳本
- ./configure
- // 編譯未編譯的依賴包
- make
- // 檢查依賴包是否完整
- make check
- // 開始安裝Protocol Buffer
- make install
安裝完成,檢驗(yàn)版本:
- $protoc --version
- libprotoc 3.14.0
輸出版本信息,說(shuō)明安裝成功。
這里的protoc命令就是Protocol Buffer的編譯器,可以將 .proto文件編譯成對(duì)應(yīng)平臺(tái)的頭文件和源代碼文件。
另外一種方式就是安裝IDE插件,這里以IDEA為例,搜索插件:
protobuf
關(guān)于protobuf的插件比較多,選擇適合自己就行。
然后gRPC官方推薦了一種更優(yōu)雅的使用姿勢(shì),可以通過(guò)maven輕松搞定(需安裝上圖中的“Protobuf Support”插件)。也就是引入grpc的一些組件,然后在maven的build中進(jìn)行配置,編譯proto文件成為Java代碼。此種方式暫時(shí)不展開,后續(xù)可直接看項(xiàng)目集成部分的源代碼。
構(gòu)建數(shù)據(jù)
在Java中,如果通過(guò)JSON來(lái)傳輸一個(gè)數(shù)據(jù),我們首先要定義一個(gè)對(duì)象,這里以Person為例:
- public class Person {
- private String name;
- private Integer id;
- // ... getter/setter
- }
那么,如果用protobuf來(lái)定義Person這個(gè)對(duì)象的數(shù)據(jù)結(jié)構(gòu)是什么樣呢?
先創(chuàng)建一個(gè)person.proto文件,然后定義如下的結(jié)構(gòu):
- syntax = "proto3"; // 聲明為protobuf 3定義文件
- package tutorial;
- option java_package = "com.choupangxia.protobuf.message"; // 聲明生成消息類的java包路徑
- option java_outer_classname = "Person"; // 聲明生成消息類的類名
- message PersonProto {
- string name = 1;
- int32 id = 2;
- }
上面每項(xiàng)語(yǔ)法的具體說(shuō)明可參看注釋部分。當(dāng)然Person的結(jié)構(gòu)可以更豐富,這里只是出于演示需要,做了最簡(jiǎn)單的示例,更多語(yǔ)法可參看官方文檔。
編譯protot文件
定義完成之后,我們可以通過(guò)兩種方式來(lái)生成目標(biāo)Java類。這里先采用本機(jī)安裝的編譯器來(lái)進(jìn)行操作。
執(zhí)行protoc命令之前,可先執(zhí)行-h命令來(lái)查看protoc的使用說(shuō)明:
- protoc -h
進(jìn)入person.proto文件所在目錄,執(zhí)行以下命令進(jìn)行編譯:
- protoc --java_out=../java ./person.proto
--java_out參數(shù)指定了Java類的輸出路徑,第二個(gè)參數(shù)執(zhí)行的要編譯的文件為當(dāng)前目錄下的person.proto文件。
執(zhí)行命令,會(huì)發(fā)現(xiàn)com.choupangxia.protobuf.message下生成了名為Person的類。注意proto中定義的message名稱不要與Java類名重復(fù),否則會(huì)出現(xiàn)命令執(zhí)行失敗的狀況。
對(duì)應(yīng)的Person類比較復(fù)雜,甚至有一些語(yǔ)法層面的錯(cuò)誤或改進(jìn),如果需要,進(jìn)行對(duì)應(yīng)的改進(jìn)優(yōu)化即可。
protobuf
上圖為生成的Person類的部分結(jié)構(gòu)。比如上面的java.lang.String getName()這個(gè)方法的返回值就可以進(jìn)行優(yōu)化,不用指定String的package。
項(xiàng)目集成
其實(shí)上面講生成的Person代碼放入項(xiàng)目,已經(jīng)算是項(xiàng)目集成的一部分了。如果未引入protobuf的依賴,上面的代碼還是會(huì)報(bào)錯(cuò)的。
Maven項(xiàng)目的pom文件中添加protobuf依賴:
- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-java</artifactId>
- <version>3.17.3</version>
- </dependency>
如果想通過(guò)IDEA直接編譯proto文件,需安裝“Protobuf Support”插件,還需引入grpc的依賴,完整依賴如下:
- <properties>
- <grpc.version>1.6.1</grpc.version>
- <protobuf.version>3.17.3</protobuf.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-java</artifactId>
- <version>${protobuf.version}</version>
- </dependency>
- <!-- 編譯使用部分 -->
- <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>
- </dependencies>
- <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>
在執(zhí)行執(zhí)行maven compile命令進(jìn)行編譯之前,將需要編譯的proto文件放在與src/main/java同級(jí)目錄下的/src/main/proto目錄。
protobuf
此時(shí)將生成的Java復(fù)制到對(duì)應(yīng)的包下即可。
業(yè)務(wù)應(yīng)用
一切準(zhǔn)備就緒,現(xiàn)在就來(lái)寫個(gè)例子使用對(duì)應(yīng)的代碼了。
- public class Test {
- public static void main(String[] args) throws InvalidProtocolBufferException {
- Person.PersonProto sourcePersonProto = Person.PersonProto.newBuilder().setId(123).setName("Tom").build();
- // 序列化
- byte[] binaryInfo = sourcePersonProto.toByteArray();
- System.out.println("序列化字節(jié)碼內(nèi)容:" + Arrays.toString(binaryInfo));
- System.out.println("序列化字節(jié)碼長(zhǎng)度:" + binaryInfo.length);
- System.out.println("-----------以下為接收方反序列化操作-------------");
- // 反序列化
- Person.PersonProto targetPersonProto = Person.PersonProto.parseFrom(binaryInfo);
- System.out.println("反序列化結(jié)果:" + targetPersonProto.toString());
- }
- }
上述代碼就是基于生成的Person類的基本使用。首先通過(guò),Person類中的內(nèi)部類和Builder方法進(jìn)行參數(shù)的封裝,然后調(diào)用其toByteArray方法,即可將報(bào)文信息進(jìn)行序列化。接收方呢,有同樣的一套代碼,先獲得Person.PersonProto對(duì)象,然后執(zhí)行parseFrom方法即可進(jìn)行反序列化操作。
為什么protobuf比較高效
單從序列化后的數(shù)據(jù)體積角度來(lái)分析。與XML、JSON這類文本協(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)景,ProtoBuf協(xié)議是不錯(cuò)的選擇。
做一個(gè)簡(jiǎn)單直觀的例子:
- {"id":1,"firstName":"Chris","lastName":"Richardson","email":[{"type":"PROFESSIONAL","email":"aicchrrdson@email.com"}]}
對(duì)于上面的JSON數(shù)據(jù),使用JSON序列化后的數(shù)據(jù)大小為118byte,而使用protobuf序列化后的數(shù)據(jù)大小為48byte。如果數(shù)據(jù)量更多,層次結(jié)構(gòu)更復(fù)雜,差距還是很明顯的。
從序列化/反序列化速度角度,與XML、JSON相比,protobuf序列化/反序列化的速度更快,比XML要快20-100倍。
但protobuf是基于二進(jìn)制的協(xié)議,編碼后的數(shù)據(jù)可讀性差,如果沒有idl文件,就無(wú)法理解二進(jìn)制數(shù)據(jù)流,對(duì)調(diào)試不友好。
小結(jié)
本文帶大家從0到1學(xué)習(xí)了protobuf的使用步驟。很多文章之所以看不懂,就是因?yàn)闆]有梳理清楚使用protobuf的整個(gè)核心邏輯。只要掌握了如何搭建環(huán)境、如何編寫數(shù)據(jù)結(jié)構(gòu)、如何編譯、如何集成到項(xiàng)目中并運(yùn)用。那么,protobuf的其他知識(shí)點(diǎn)逐步在實(shí)踐中補(bǔ)充即可。
隨著微服務(wù)的不斷發(fā)展,RPC框架為了追求高效的通信,使用像protobuf這類框架也必然是趨勢(shì)。也是想更好的學(xué)習(xí)微服務(wù)架構(gòu)的底層的必備知識(shí)。
本文源碼:https://github.com/secbr/protobuf-demo