Google Protobuf
Why Protobuf
protobuf它是Google提供的一個技術, 一個類庫, 也可以說是一套規范, 學java的人都知道java有自己的序列化機制, 對不同的java程序來說,他們可以使用同一種序列化機制進行數據的傳遞, 但是java的序列化機制並不適用於其他的語言比如python
如果想讓他們共享數據,我們就得定義中數據格式, 比如xml, 通過xml定義出一個對象, 這樣java,python都可以解析xml, 但是在網絡上傳輸xml的話是不是有點浪費資源呢? xml中有大量的冗余的標簽沒有實際的意義還不能去除, 嚴重影響性能. 導致傳輸的效率急劇降低
protobuf的出現就是為了迎戰這個效率低的問題
什么是protobuf?
protobuf 全稱是: protocol buffers 是一種語言中立的用於序列化結構化數據 ,相對於XML這種格式的數據來說,protobuf極其小, 機器靈活,我們只要定義好數據的格式, 就可以使用代碼生成器生成代碼,我們只需要使用它生成出來的代碼就能實現輕松編寫,讀取結構化數據, 並且目前Protobuf支持 C++ , C# , Dart , Go , Java , Python多種語言
安裝環境
想使用protobuf的話我們要先去下載兩個工具, 第一個就是protobuf的編譯器也就是protoc , 我們一會將使用它把我們定義的 .proto 文件編譯成java代碼, 然后我們直接使用它生成的java代碼就ok
下載鏈接: https://github.com/protocolbuffers/protobuf/releases>
根據同樣的系統選擇不同的編譯器就ok,我用的windows, 所以選擇 :protoc-3.11.0-win64.zip
如果我們想用java玩protobuf , 同樣得在上面的鏈接中將protobuf-java-3.11.0.zip 下載到本地
上手使用
總體思路:
首先我們只要根據需求制定出 .proto 文件中對消息的描述就ok. 因為代碼自動生成:
- 客戶端代碼生曾策略: stub(裝)
- 服務端代碼生曾策略: skeleton(骨架)
序列化encode和反序列decode化也叫做編碼和解碼:
使用流程:
- 定義結果說明文件: 描述接口對象(結構體), 對象成員,接口方法等一系列信息(這是個文本文件獨立於任何變成語言)
- 通過RPC框架提供的編譯器將接口說明文件編譯成具體的語言實現
- 在客戶端和服務端分別引入RPC編譯器生成的文件,即可進行RPC遠程過程調用
參照項目官方地址 https://github.com/protocolbuffers/protobuf/tree/master/java
我們需要添加maven依賴導入運行時環境, 確保我們下面導入的運行時依賴和protoc的版本一致
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.11.0</version>
</dependency>
一: 編寫 .proto 文件
這個 .proto 文件, 是一個描述性質的文件,我們使用這個文件去描述通信的雙方每次發送的數據的格式是怎么樣的,就像XML能描述出一個對象出來
下面是 .proto 文件的示例,語法和java 神似
// proto有兩個版本, 2和3, 這里使用的 proto2
syntax = "proto2";
// 以pakcet包名開始, 為了防止命名的沖突
package tutorial;
// 如果我們沒有顯示的指定 java_package 的話, 他就是用上面的packet當成生成的java類的包名
// 顯示的指定了 java_package , 最終生成的java代碼包的名字就用 java_package 為准
// 即便是 顯示的提供了 java_package, 也得提供上面的packet(防止在其他語言中出現命名的沖突)
option java_package, = "com.example.tutorial";
// 最終經過protoc處理后 會生成一個 叫AddressBookProtos的外部類, 這個類中包含了我們指定的下面的所有類
// 如果我們沒有顯示的指定的話,最終就會將文件名轉換為駝峰命名法得到的名,當成類名字
option java_outer_classname = "AddressBookProtos";
// message其實是不同類型的 field的聚合,比如 string, int32,bool,float,double
// 第一個消息
message Person {
// 這種等於1, 等於2, 並不是賦值, 而是進行一種唯一的標記,在同一個范圍中 ,標記不重復
// 如下面的123, 跳過枚舉后的4
// required 表示這個字段是必須要有的,如果不提供的話會拋出異常
required string name = 1;
required int32 id = 2;
// 表示這個字段的值是可選的
optional string email = 3;
// 枚舉類型的消息
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// 消息中的小 PhoneNumber
message PhoneNumber {
required string number = 1;
// Default表示這個字段的值, 默認就是枚舉中的HOME值, 如果用戶提供新的值, 會覆蓋默認值
optional PhoneType type = 2 [default = HOME];
}
// repeated 表示是可重復的,可以任意次, 可以理解成java中的list
repeated PhoneNumber phones = 4;
}
// 第二個消息
// 可以使用下面這種消息的嵌套
message AddressBook {
repeated Person people = 1;
}
二. 編譯 .proto 文件
編譯的命令如下:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
例:
protoc --java_out=src/main/java src/protobuf/Person.proto
在上面的命令中我們需要指定兩個文件目錄,一個是 源代碼的路徑, 還有就是 目標路徑
看一下生成的這個類的繼承體系
也不怕嘮叨, 再說一次, 根據我們提供的 .proto文件, 最外面的是StudentProtobuf是我們在java_outer_classname指定的名字, 內部類Student,是我們指定的 消息類型
- 消息Student僅僅存在get方法,並且它是一個不可變的, 一經構建出來, 就不可再改變
- 消息的構建需要借助於構建器, 看上圖, 構建器是消息內置對象
- 消息Student 中存在一個構建器, 這個構建器的作用就是通過構建者模式完成對Student的構建, 構造器提供了對消息Student的一系列set方法
所以就有了下面的構建對象的一幕
StudentProtobuf.Student student = StudentProtobuf.Student.newBuilder()
.setEmail("123123@qq.com")
.setId(1)
.setName("張三").build();
不要嘗試去修改這個生成的類, 因為每次重新生成, 都會進行一次覆蓋
然后protoc就會幫我們生成指定的文件AddressBookProtos.java
名子就是我們在 .proto文件中的 java_outer_classname指定的名字
補充 Message Method
這是Message中內置的一些方法
-
isInitialized()
: 檢查是否所有的 required類型的描述字段都被賦值了 -
toString()
: 用人類可讀的方式顯示這些字段 -
mergeFrom(Message other)
: (builder only)將其他的消息合並到次消息中 -
clear()
: (builder only)清除所有字段的空狀態 -
byte[] toByteArray();
: 將消息序列化成二進制數組 -
static Person parseFrom(byte[] data);
: 從給定的二進制數組中反序列化成 對象 -
void writeTo(OutputStream output);
: 序列化消息並將其寫入OutputStream -
static Person parseFrom(InputStream input);
: 讀取和解析來自InputStream的消息。
三. 測試使用
代碼如下, 雖然是在一個java文件中完成的,但是也是具有實際意義的, 就像前面說的 proto可以實現比XML更好,更快,更靈敏的對象的描述, 同樣也是跨越語言的
// 構建對象
StudentProtobuf.Student student = StudentProtobuf.Student.newBuilder()
.setEmail("123123@qq.com")
.setId(1)
.setName("張三").build();
// 轉換成字節數組
byte[] bytes = student.toByteArray();
// todo 從網絡中傳輸發往其他客戶端
// 其他客戶端,將對象反序列化出來
StudentProtobuf.Student stu = StudentProtobuf.Student.parseFrom(bytes);
System.out.println(stu.getName());
System.out.println(stu.getEmail());
System.out.println(stu.getId());
RMI: remote method invocation 遠程方法調用
只針對java, 服務端和客戶端之間之間進行通信, 一般他的流程是這樣的, 在client端生將消息序列化轉換成字節碼, 然后經過網絡的傳輸作用,流向服務端, 服務負端再將這些數據返回序列化會消息信息進行下一步處理操作
Netty對Protobuf的支援
Netty+Protobuf是可以實現 RPC(remote procedure call,遠程過程調用,達到跨語言的調用應用調用, 並且他們之間結合和傳統web service對比他的優勢是在編解碼的效率上很高,在網絡上的傳輸數據很快
思路: netty和protobuf之間的整合是必然的事情, protobuf 可以很好的完成對象的序列化, 而netty可以將這些已經完成序列化的數據發送出去
Netty服務端和客戶端的編碼其實挺機械化的,** 我們的關注點是netty提供了哪些針對protobuf編解碼的處理器**, 以及怎么給netty添加上這些處理器, 實例代碼如下:
毫無疑問,在netty啟動過程中動態的添加多個處理器肯定是通過實現 ChannelInitializer
類來實現
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// todo 順序
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast(new ProtobufDecoder(StudentProtobuf.Student.getDefaultInstance()));
pipeline.addLast(new ProtobufVarint32FrameDecoder());
pipeline.addLast(new ProtobufEncoder());
// 自定義的處理器
pipeline.addLast(new MyClientHandler());
}
}
如上代碼中的 ProtobufVarint32LengthFieldPrepender
和 ProtobufVarint32FrameDecoder
這兩個解碼器都能處理半包信息, ProtobufDecoder
中的泛型就是消息的實體的類型,表示按照這個類型完成數據的反序列化
踩坑
如上幾個處理器的添加順序中, ProtobufEncoder 這個處理器, 一定的放在處理半包數據的那兩個處理器的后面, 不處理半包數據, 就得不到完整的數據, 對不完整的數據進行解碼, 就會出現如下的異常