Apache Thrift系列詳解(三)
前言
Thrift
支援二進位制,壓縮格式,以及json
格式資料的序列化和反序列化。開發人員可以更加靈活的選擇協議的具體形式。協議是可自由擴充套件的,新版本的協議,完全相容老的版本!
正文
資料交換格式簡介
當前流行的資料交換格式可以分為如下幾類:
(一) 自解析型
序列化的資料包含完整的結構, 包含了field
名稱和value
值。比如xml/json/java serizable
,大百度的mcpack/compack
,都屬於此類。即調整不同屬性的順序對序列化/反序列化不造成影響。
(二) 半解析型
序列化的資料,丟棄了部分資訊, 比如field
名稱, 但引入了index
(常常是id
+type
的方式)來對應具體屬性
google protobuf/thrift
也屬於此類。
(三) 無解析型
傳說中大百度的infpack
實現,就是藉助該種方式來實現,丟棄了很多有效資訊,效能/壓縮比最好,不過向後相容需要開發做一定的工作, 詳情不知。
交換格式 | 型別 | 優點 | 缺點 |
---|---|---|---|
Xml | 文字 | 易讀 | 臃腫,不支援二進位制資料型別 |
JSON | 文字 | 易讀 | 丟棄了型別資訊,比如"score":100,對score型別是int/double解析有二義性, 不支援二進位制資料型別 |
Java serizable | 二進位制 | 使用簡單 | 臃腫,只限制在JAVA領域 |
Thrift | 二進位制 | 高效 | 不易讀,向後相容有一定的約定限制 |
Google Protobuf | 二進位制 | 高效 | 不易讀,向後相容有一定的約定限制 |
Thrift的資料型別
- 基本型別:
bool: 布林值
byte: 8位有符號整數
i16: 16位有符號整數
i32: 32位有符號整數
i64: 64位有符號整數
double: 64位浮點數
string: UTF-8編碼的字串
binary: 二進位制串 - 結構體型別:
struct: 定義的結構體物件 - 容器型別:
list: 有序元素列表
set: 無序無重複元素集合
map: 有序的key/value集合 - 異常型別:
exception: 異常型別 - 服務型別:
service: 具體對應服務的類
Thrift的序列化協議
Thrift
可以讓使用者選擇客戶端與服務端之間傳輸通訊協議的類別,在傳輸協議上總體劃分為文字(text
)和二進位制(binary
)傳輸協議。為節約頻寬,提高傳輸效率,一般情況下使用二進位制型別的傳輸協議為多數,有時還會使用基於文字型別的協議,這需要根據專案/產品中的實際需求。常用協議有以下幾種:
- TBinaryProtocol:二進位制編碼格式進行資料傳輸
- TCompactProtocol:高效率的、密集的二進位制編碼格式進行資料傳輸
- TJSONProtocol: 使用
JSON
文字的資料編碼協議進行資料傳輸 - TSimpleJSONProtocol:只提供
JSON
只寫的協議,適用於通過指令碼語言解析
Thrift的序列化測試
(a). 首先編寫一個簡單的thrift
檔案pair.thrift
:
struct Pair {
1: required string key
2: required string value
}
這裡標識了
required
的欄位,要求在使用時必須正確賦值,否則執行時會丟擲TProtocolException
異常。預設和指定為optional
時,則執行時不做欄位非空校驗。
(b). 編譯並生成java
原始碼:
thrift -gen java pair.thrift
©. 編寫序列化和反序列化的測試程式碼:
- 序列化測試,將
Pair
物件寫入檔案中
private static void writeData() throws IOException, TException {
Pair pair = new Pair();
pair.setKey("key1").setValue("value1");
FileOutputStream fos = new FileOutputStream(new File("pair.txt"));
pair.write(new TBinaryProtocol(new TIOStreamTransport(fos)));
fos.close();
}
- 反序列化測試,從檔案中解析生成
Pair
物件
private static void readData() throws TException, IOException {
Pair pair = new Pair();
FileInputStream fis = new FileInputStream(new File("pair.txt"));
pair.read(new TBinaryProtocol(new TIOStreamTransport(fis)));
System.out.println("key => " + pair.getKey());
System.out.println("value => " + pair.getValue());
fis.close();
}
(d) 觀察執行結果,正常輸出表明序列化和反序列化過程正常完成。
Thrift協議原始碼
(一) writeData()分析
首先檢視thrift
的序列化機制,即資料寫入實現,這裡採用二進位制協議TBinaryProtocol
,切入點為pair.write(TProtocol)
:
檢視scheme()
方法,決定採用元組計劃(TupleScheme
)還是標準計劃(StandardScheme
)來實現序列化,預設採用的是標準計劃StandardScheme
。
標準計劃(StandardScheme
)下的write()
方法:
這裡完成了幾步操作:
(a). 根據Thrift IDL
檔案中定義了required
的欄位驗證欄位是否正確賦值。
public void validate() throws org.apache.thrift.TException {
// check for required fields
if (key == null) {
throw new org.apache.thrift.protocol.TProtocolException("Required field 'key' was not present! Struct: " + toString());
}
if (value == null) {
throw new org.apache.thrift.protocol.TProtocolException("Required field 'value' was not present! Struct: " + toString());
}
}
(b). 通過writeStructBegin()
記錄寫入結構的開始標記。
public void writeStructBegin(TStruct struct) {}
©. 逐一寫入Pair
物件的各個欄位,包括欄位欄位開始標記、欄位的值和欄位結束標記。
if (struct.key != null) {
oprot.writeFieldBegin(KEY_FIELD_DESC);
oprot.writeString(struct.key);
oprot.writeFieldEnd();
}
// 省略...
(1). 首先是欄位開始標記,包括type
和field-id
。type
是欄位的資料型別的標識號,field-id
是Thrift IDL
定義的欄位次序,比如說key
為1,value
為2。
public void writeFieldBegin(TField field) throws TException {
writeByte(field.type);
writeI16(field.id);
}
Thrift
提供了TType
,對不同的資料型別(type
)提供了唯一標識的typeID
。
public final class TType {
public static final byte STOP = 0; // 資料讀寫完成
public static final byte VOID = 1; // 空值
public static final byte BOOL = 2; // 布林值
public static final byte BYTE = 3; // 位元組
public static final byte DOUBLE = 4; // 雙精度浮點型
public static final byte I16 = 6; // 短整型
public static final byte I32 = 8; // 整型
public static final byte I64 = 10; // 長整型
public static final byte STRING = 11; // 字串型別
public static final byte STRUCT = 12; // 引用型別
public static final byte MAP = 13; // Map
public static final byte SET = 14; // 集合
public static final byte LIST = 15; // 列表
public static final byte ENUM = 16; // 列舉
}
(2). 然後是寫入欄位的值,根據欄位的資料型別又歸納為以下實現:writeByte()
、writeBool()
、writeI32()
、writeI64()
、writeDouble()
、writeString()
和writeBinary()
方法。
TBinaryProtocol
通過一個長度為8
的byte
位元組陣列快取寫入或讀取的臨時位元組資料。
private final byte[] inoutTemp = new byte[8];
**常識1:**16進位制的介紹。以0x開始的資料表示16進位制,0xff換成十進位制為255。在16進制中,A、B、C、D、E、F這五個字母來分別表示10、11、12、13、14、15。
16
進位制變十進位制:f表示15。第n位的權值為16的n次方,由右到左從0位起:0xff = 1516^1 + 1516^0 = 255
16
進位制變二進位制再變十進位制:0xff = 1111 1111 = 2^8 - 1 = 255
**常識2:**位運算子的使用。>>表示代表右移符號,如:int i=15; i>>2的結果是3,移出的部分將被拋棄。而<<表示左移符號,與>>剛好相反。
轉為二進位制的形式可能更好理解,0000 1111(15)右移2位的結果是0000 0011(3),0001 1010(18)右移3位的結果是0000 0011(3)。
- writeByte():寫入單個位元組資料。
public void writeByte(byte b) throws TException {
inoutTemp[0] = b;
trans_.write(inoutTemp, 0, 1);
}
- writeBool():寫入布林值資料。
public void writeBool(boolean b) throws TException {
writeByte(b ? (byte)1 : (byte)0);
}
- writeI16():寫入短整型
short
型別資料。
public void writeI16(short i16) throws TException {
inoutTemp[0] = (byte)(0xff & (i16 >> 8));
inoutTemp[1] = (byte)(0xff & (i16));
trans_.write(inoutTemp, 0, 2);
}
- writeI32():寫入整型
int
型別資料。
public void writeI32(int i32) throws TException {
inoutTemp[0] = (byte)(0xff & (i32 >> 24));
inoutTemp[1] = (byte)(0xff & (i32 >> 16));
inoutTemp[2] = (byte)(0xff & (i32 >> 8));
inoutTemp[3] = (byte)(0xff & (i32));
trans_.write(inoutTemp, 0, 4);
}
- writeI64():寫入長整型
long
型別資料。
public void writeI64(long i64) throws TException {
inoutTemp[0] = (byte)(0xff & (i64 >> 56));
inoutTemp[1] = (byte)(0xff & (i64 >> 48));
inoutTemp[2] = (byte)(0xff & (i64 >> 40));
inoutTemp[3] = (byte)(0xff & (i64 >> 32));
inoutTemp[4] = (byte)(0xff & (i64 >> 24));
inoutTemp[5] = (byte)(0xff & (i64 >> 16));
inoutTemp[6] = (byte)(0xff & (i64 >> 8));
inoutTemp[7] = (byte)(0xff & (i64));
trans_.write(inoutTemp, 0, 8);
}
- writeDouble():寫入雙浮點型
double
型別資料。
public void writeDouble(double dub) throws TException {
writeI64(Double.doubleToLongBits(dub));
}
- writeString():寫入字串型別,這裡先寫入字串長度,再寫入字串內容。
public void writeString(String str) throws TException {
try {
byte[] dat = str.getBytes("UTF-8");
writeI32(dat.length);
trans_.write(dat, 0, dat.length);
} catch (UnsupportedEncodingException uex) {
throw new TException("JVM DOES NOT SUPPORT UTF-8");
}
}
- writeBinary:寫入二進位制陣列型別資料,這裡資料輸入是
NIO
中的ByteBuffer
型別。
public void writeBinary(ByteBuffer bin) throws TException {
int length = bin.limit() - bin.position();
writeI32(length);
trans_.write(bin.array(), bin.position() + bin.arrayOffset(), length);
}
(3). 每個欄位寫入完成後,都需要記錄欄位結束標記。
public void writeFieldEnd() {}
(d). 當所有的欄位都寫入以後,需要記錄欄位停止標記。
public void writeFieldStop() throws TException {
writeByte(TType.STOP);
}
(e). 當所有資料寫入完成後,通過writeStructEnd()
記錄寫入結構的完成標記。
public void writeStructEnd() {}
(二) readData()分析
檢視thrift
的反序列化機制,即資料讀取實現,同樣採用二進位制協議TBinaryProtocol
,切入點為pair.read(TProtocol)
:
資料讀取和資料寫入一樣,也是採用的標準計劃StandardScheme
。標準計劃(StandardScheme
)下的read()
方法:
這裡完成的幾步操作:
(a). 通過readStructBegin
讀取結構的開始標記。
iprot.readStructBegin();
(b). 迴圈讀取結構中的所有欄位資料到Pair
物件中,直到讀取到org.apache.thrift.protocol.TType.STOP
為止。iprot.readFieldBegin()
指明開始讀取下一個欄位的前需要讀取欄位開始標記。
while (true) {
schemeField = iprot.readFieldBegin();
if (schemeField.type == org.apache.thrift.protocol.TType.STOP) {
break;
}
// 欄位的讀取,省略...
}
©. 根據Thrift IDL
定義的field-id
讀取對應的欄位,並賦值到Pair
物件中,並設定Pair
物件相應的欄位為已讀狀態(前提:欄位在IDL
中被定義為required
)。
switch (schemeField.id) {
case 1: // KEY
if (schemeField.type == org.apache.thrift.protocol.TType.STRING) {
struct.key = iprot.readString();
struct.setKeyIsSet(true);
} else {
org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
}
break;
case 2: // VALUE
if (schemeField.type == org.apache.thrift.protocol.TType.STRING) {
struct.value = iprot.readString();
struct.setValueIsSet(true);
} else {
org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
}
break;
default:
org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);
}
關於讀取欄位的值,根據欄位的資料型別也分為以下實現:readByte()
、readBool()
、readI32()
、readI64()
、readDouble()
、readString()
和readBinary()
方法。
- readByte():讀取單個位元組資料。
public byte readByte() throws TException {
if (trans_.getBytesRemainingInBuffer() >= 1) {
byte b = trans_.getBuffer()[trans_.getBufferPosition()];
trans_.consumeBuffer(1);
return b;
}
readAll(inoutTemp, 0, 1);
return inoutTemp[0];
}
- readBool():讀取布林值資料。
public boolean readBool() throws TException {
return (readByte() == 1);
}
- readI16():讀取短整型
short
型別資料。
public short readI16() throws TException {
byte[] buf = inoutTemp;
int off = 0;
if (trans_.getBytesRemainingInBuffer() >= 2) {
buf = trans_.getBuffer();
off = trans_.getBufferPosition();
trans_.consumeBuffer(2);
} else {
readAll(inoutTemp, 0, 2);
}
return (short) (((buf[off] & 0xff) << 8) |
((buf[off+1] & 0xff)));
}
- readI32():讀取整型
int
型別資料。
public int readI32() throws TException {
byte[] buf = inoutTemp;
int off = 0;
if (trans_.getBytesRemainingInBuffer() >= 4) {
buf = trans_.getBuffer();
off = trans_.getBufferPosition();
trans_.consumeBuffer(4);
} else {
readAll(inoutTemp, 0, 4);
}
return ((buf[off] & 0xff) << 24) |
((buf[off+1] & 0xff) << 16) |
((buf[off+2] & 0xff) << 8) |
((buf[