本文章著做權歸Pushy全部,如需轉載請聯繫做者,並註明出處:pushy.sitejava
Google Protocol Buffer
( 簡稱 Protobuf
) 是 Google公司研發的一種靈活高效的可序列化的數據協議。什麼是序列化呢?git
序列化(Serialization)將對象的狀態信息轉換爲能夠存儲或傳輸的形式的過程github
舉例來講,咱們接觸的最多的序列化數據格式有JSON和XML。JSON相對於其餘序列化來講,可讀性比較強且便於快速編寫,所以在先後端分離的今天,通常都採用JSON進行序列化傳輸。而XML的格式統一,符合標準,一樣具備良好的可讀性,在Java中的絕大多數配置文件都採用XML。shell
可是,在上面的兩種序列化格式中,XML體積龐大,而且它與JSON的性能都不及今天介紹的主角——Protobuf後端
首先,在Github上下載Protobuf
編譯器,下載地址爲:Github releases。若是你和我同樣使用的Windows系統,那麼則下載protoc-3.6.1-win32.zip
文件。解壓完以後,將Your path\protoc-3.6.1-win32\bin
添加到環境變量中。數組
在命令行上輸入protoc
查看是否安裝成功:bash
首先,咱們須要編寫一個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文件,編譯命令格式爲:socket
$ 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("1437876073@qq.com")
.build();
System.out.println(person);
return person;
}
}
複製代碼
打印的結果爲:
id: 1
name: "Pushy"
email: "1437876073@qq.com"
複製代碼
怎麼樣?使用是否是很是簡單,下面咱們來了解一下Protobuf
的序列化。
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對象。
第二種序列化方式是將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
中,適用了一樣的技巧。
第三種則是經過寫入文件進行序列化:
// 序列化,將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();
複製代碼
在Netty中,對Protobuf
作了支持,並內置了響應的編解碼器實現,以下:
名稱 | 描述 |
---|---|
ProtobufEncoder | 使用Protobuf對消息進行編碼 |
ProtobufDecoder | 使用Protobuf對消息進行解碼 |
ProtobufVarint32FrameDecoder | 根據消息中的Protobuf的Base 128 Varints 整型長度字段值動態地分割所接受到的ByteBuf |
ProtobufVarint32LengthFieldPrepender | 向ByteBuf前追加一個Protobuf的Base 128 Varints 整型的長度字段值 |
引導部分在此不作贅述,更多能夠看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() );
}
}
複製代碼
一樣,客戶端也要添加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("1437876073@qq.com")
.build();
}
}
複製代碼
測試一下,能夠看到服務端確實能經過Protobuf
序列化收到客戶端發送的消息: