Google高效能序列化框架Protobuf認識及與Netty的結合
Google Protocol Buffer
( 簡稱 Protobuf
) 是 Google公司研發的 一種靈活高效的可序列化的資料協議 。什麼是序列化呢?
序列化(Serialization)將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程
舉例來說,我們接觸的最多的序列化資料格式有JSON和XML。JSON相對於其他序列化來說,可讀性比較強且便於快速編寫,因此在前後端分離的今天,一般都採用JSON進行序列化傳輸。而XML的格式統一,符合標準,同樣具有良好的可讀性,在Java中的絕大多數配置檔案都採用XML。
但是,在上面的兩種序列化格式中,XML體積龐大,並且它與JSON的效能都不及今天介紹的主角——Protobuf
1.2 安裝
首先,在Github上下載 Protobuf
編譯器,下載地址為: ofollow,noindex">Github releases 。如果你和我一樣使用的Windows系統,那麼則下載 protoc-3.6.1-win32.zip
檔案。解壓完之後,將 Your path\protoc-3.6.1-win32\bin
新增到環境變數中。
在命令列上輸入 protoc
檢視是否安裝成功:

1.3 使用
首先,我們需要編寫一個 proto
檔案,用來定義程式中需要處理的結構化資料(即 Message
)。 proto
檔案類似於Java或者C語言的資料定義。
如下,建立 person.proto
檔案,定義一個 Person
的 Message
,包含三個屬性: id
、 name
、 email
:
syntax = "proto3";// 執行protobuf的協議版本 option java_package = "site.pushy.protobuf";// 指定包名 option java_outer_classname = "PersonEntity"; //生成的資料訪問類的類名 message Person { int32 id = 1; string name = 2; string email = 3; } 複製程式碼
然後,通過 protoc
來將該 proto
檔案定義的結構化資料編譯成為Java檔案,編譯命令格式為:
$ protoc -I=存放proto檔案的目錄 --java_out=生成的Java檔案輸入路徑 proto檔案的路徑 複製程式碼
例如,我將 proto
檔案放在了E盤的 demo
下,並將它生成的Java檔案放在 E:\demo\protobuf\src\main\java
下,則命令如下:
$ protoc -I=E:\demo --java_out=E:\demo\protobuf\src\main\java E:\demo\person.proto 複製程式碼
執行完之後,將會生成 PersonEntity
類:
package site.pushy.protobuf; public final class PersonEntity { private PersonEntity() {} // 程式碼省略 } 複製程式碼
生成的 PersonEntity
類,我們可以通過建造者模式建立 Person
物件:
public class CreatePerson { public static PersonEntity.Person create() { PersonEntity.Person person = PersonEntity.Person.newBuilder() .setId(1) .setName("Pushy") .setEmail("[email protected]") .build(); System.out.println(person); return person; } } 複製程式碼
列印的結果為:
id: 1 name: "Pushy" email: "[email protected]" 複製程式碼
怎麼樣?使用是不是非常簡單,下面我們來了解一下 Protobuf
的序列化。
2. 序列化
2.1 位元組陣列
Protobuf
最簡單序列化方式是將 Person
物件轉換為位元組陣列,例如:
// 序列化 PersonEntity.Person person = CreatePerson.create(); byte[] data = person.toByteArray(); // 反序列化 PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(data); System.out.println(parsePerson.name); 複製程式碼
這種方式可以適用於很多場景, Protobuf
會根據自己的編碼方式將Java物件序列化成位元組陣列。同時 Protobuf
也會從位元組陣列中重新編碼,得到新的Java POJO物件。
2.2 Stream
第二種序列化方式是將 Protobuf
物件寫入Stream:
// 序列化,粘包,將一個或者多個ProtoBuf寫入到Stream PersonEntity.Person person = CreatePerson.create(); ByteArrayOutputStream os = new ByteArrayOutputStream(); person.writeTo(os); // 反序列化,拆包,從stream中讀出一個或者多個Protobuf位元組物件 ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(is); System.out.println(parsePerson); 複製程式碼
這種方式比較適用於RPC呼叫和Socket傳輸,在序列化的位元組陣列之前,新增一個 varint32
的數字表示位元組陣列的長度;那麼在反序列化時,可以通過先讀取 varint
,然後再依次讀取此長度的位元組;這種方式有效的解決了socket傳輸時如何“拆包”“封包”的問題。在 Netty
中,適用了同樣的技巧。
2.3 檔案
第三種則是通過寫入檔案進行序列化:
// 序列化,將Protobuf物件儲存為檔案 PersonEntity.Person person = CreatePerson.create(); FileOutputStream fos = new FileOutputStream("pushy.dt"); person.writeTo(fos); fos.close(); // 反序列化,從檔案中讀取和解析Protobuf物件 FileInputStream fis = new FileInputStream("pushy.dt"); PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(fis); System.out.println(parsePerson); fis.close(); 複製程式碼
3. 結合Netty
在Netty中,對 Protobuf
做了支援,並內建了響應的編解碼器實現,如下:
名稱 | 描述 |
---|---|
ProtobufEncoder | 使用Protobuf對訊息進行編碼 |
ProtobufDecoder | 使用Protobuf對訊息進行解碼 |
ProtobufVarint32FrameDecoder | 根據訊息中的Protobuf的 Base 128 Varints 整型長度欄位值動態地分割所接受到的ByteBuf |
ProtobufVarint32LengthFieldPrepender | 向ByteBuf前追加一個Protobuf的 Base 128 Varints 整型的長度欄位值 |
3.1 服務端
引導部分在此不做贅述,更多可以看demo原始碼。我們主要來介紹一下 ChannelPipeline
中的設定。
服務端部分,需要新增關於 Protobuf
相應的編解碼器,另外,還新增 ServerHandler
來處理服務端的業務邏輯:
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufEncoder()); pipeline.addLast(new ProtobufDecoder(PersonEntity.Person.getDefaultInstance())); pipeline.addLast(new ServerHandler()); } } 複製程式碼
伺服器端的解碼器會自動將型別轉換為 PersonEntity.Person
:
public class ServerHandler extends SimpleChannelInboundHandler<PersonEntity.Person> { @Override protected void channelRead0(ChannelHandlerContext ctx, PersonEntity.Person person) throws Exception { System.out.println("chanelRead0 =>" + person.getName() ); } } 複製程式碼
3.2 客戶端
同樣,客戶端也要新增 Protobuf
相應的編解碼器:
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufDecoder(PersonEntity.Person.getDefaultInstance())); pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder()); pipeline.addLast(new ClientHandler()); } } 複製程式碼
並使用 ClientHandler
來向服務端傳送 Protobuf
的訊息,用於配置了客戶端的解碼器,因此在使用 writeAndFlush
寫入資料時可以直接傳入 PersonEntity.Person
型別資料:
public class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(getPerson()); } private PersonEntity.Person getPerson() { return PersonEntity.Person.newBuilder() .setName("Pushy") .setEmail("[email protected]") .build(); } } 複製程式碼
測試一下,可以看到服務端確實能通過 Protobuf
序列化收到客戶端傳送的訊息:

最後,程式碼已上傳到 Github ,想要了解更多關於Protobuf的知識,可以到官網瀏覽文件!